├── .cargo └── config.toml ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── actionlint-matcher.json ├── dependabot.yml └── workflows │ ├── audit.yml │ ├── cd.yml │ ├── ci.yml │ └── pages.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── codecov.yml ├── deny.toml ├── docs └── configuration.md ├── examples └── lib │ ├── Cargo.toml │ ├── README.md │ └── src │ └── lib.rs ├── release.toml ├── src ├── cli.rs ├── cli │ └── args.rs ├── config.rs ├── config │ ├── badges.rs │ ├── de.rs │ ├── metadata.rs │ ├── package.rs │ └── tests.rs ├── diff.rs ├── lib.rs ├── macros.rs ├── main.rs ├── sync.rs ├── sync │ ├── contents.rs │ ├── contents │ │ ├── badge.rs │ │ ├── rustdoc.rs │ │ ├── rustdoc │ │ │ ├── code_block.rs │ │ │ ├── heading.rs │ │ │ └── intra_link.rs │ │ └── title.rs │ ├── marker.rs │ └── marker │ │ ├── find.rs │ │ └── replace.rs ├── traits.rs ├── vcs.rs ├── vcs │ └── git.rs └── with_source.rs └── xtask ├── Cargo.toml ├── README.md ├── release.toml └── src └── main.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | xtask = "run --package xtask --target-dir target/xtask/run --" 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.{cmd,[cC][mM][dD]} text eol=crlf 3 | *.{bat,[bB][aA][tT]} text eol=crlf 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Bug description 11 | 12 | 13 | 14 | - Would you like to work on a fix? [y/n] 15 | 16 | ## To Reproduce 17 | 18 | Steps to reproduce the behavior: 19 | 20 | 1. ... 21 | 2. ... 22 | 3. ... 23 | 4. ... 24 | 25 | 26 | 27 | ## Expected behavior 28 | 29 | 30 | 31 | ## Screenshots 32 | 33 | 34 | 35 | ## Environment 36 | 37 | 38 | 39 | - OS: [e.g. Ubuntu 20.04] 40 | - cargo-sync-rdme version: [e.g. 0.1.0] 41 | 42 | ## Additional context 43 | 44 | 45 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Motivations 11 | 12 | 15 | 16 | - Would you like to implement this feature? [y/n] 17 | 18 | ## Solution 19 | 20 | 21 | 22 | ## Alternatives 23 | 24 | 25 | 26 | ## Additional context 27 | 28 | 29 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | -------------------------------------------------------------------------------- /.github/actionlint-matcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "actionlint", 5 | "pattern": [ 6 | { 7 | "regexp": "^(?:\\x1b\\[\\d+m)?(.+?)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*: (?:\\x1b\\[\\d+m)*(.+?)(?:\\x1b\\[\\d+m)* \\[(.+?)\\]$", 8 | "file": 1, 9 | "line": 2, 10 | "column": 3, 11 | "message": 4, 12 | "code": 5 13 | } 14 | ] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "cargo" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | name: Security Audit 2 | 3 | on: 4 | schedule: 5 | # Runs at 00:00 UTC everyday 6 | - cron: "0 0 * * *" 7 | push: 8 | branches: 9 | - main 10 | pull_request: 11 | 12 | jobs: 13 | audit: 14 | uses: gifnksm/rust-template/.github/workflows/reusable-audit.yml@main 15 | 16 | audit-complete: 17 | needs: audit 18 | runs-on: ubuntu-latest 19 | if: ${{ always() }} 20 | steps: 21 | - run: | 22 | if ${{ needs.audit.result == 'success' }}; then 23 | echo "Audit succeeded" 24 | exit 0 25 | else 26 | echo "Audit failed" 27 | exit 1 28 | fi 29 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v?[0-9]+.[0-9]+.[0-9]+" 7 | branches: 8 | - main 9 | pull_request: 10 | 11 | jobs: 12 | cd: 13 | uses: gifnksm/rust-template/.github/workflows/reusable-cd.yml@main 14 | with: 15 | upload-dist-archive: true 16 | 17 | cd-complete: 18 | needs: cd 19 | runs-on: ubuntu-latest 20 | if: ${{ always() }} 21 | steps: 22 | - run: | 23 | if ${{ needs.cd.result == 'success' }}; then 24 | echo "CD succeeded" 25 | exit 0 26 | else 27 | echo "CD failed" 28 | exit 1 29 | fi 30 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | ci: 14 | uses: gifnksm/rust-template/.github/workflows/reusable-ci.yml@main 15 | secrets: 16 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 17 | 18 | ci-complete: 19 | needs: ci 20 | runs-on: ubuntu-latest 21 | if: ${{ always() }} 22 | steps: 23 | - run: | 24 | if ${{ needs.ci.result == 'success' }}; then 25 | echo "CI succeeded" 26 | exit 0 27 | else 28 | echo "CI failed" 29 | exit 1 30 | fi 31 | -------------------------------------------------------------------------------- /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Rustdoc to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | concurrency: 15 | group: "pages" 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | build-docs: 20 | name: "Publishing GitHub Pages" 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: dtolnay/rust-toolchain@stable 25 | - uses: Swatinem/rust-cache@v2 26 | - run: rustup toolchain add nightly --profile minimal 27 | - run: cargo xtask docsrs --workspace 28 | # https://github.com/actions/upload-pages-artifact#file-permissions 29 | - name: Fix permissions 30 | run: | 31 | chmod -c -R +rX "target/doc" | while read -r line; do 32 | echo "::warning title=Invalid file permissions automatically fixed::$line" 33 | done 34 | - uses: actions/upload-pages-artifact@v3 35 | with: 36 | path: target/doc 37 | 38 | deploy-docs: 39 | environment: 40 | name: github-pages 41 | url: ${{ steps.deployment.outputs.page_url }} 42 | runs-on: ubuntu-latest 43 | needs: [build-docs] 44 | steps: 45 | - id: deployment 46 | uses: actions/deploy-pages@v4 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | 9 | 10 | ## [Unreleased] - ReleaseDate 11 | 12 | ## [0.4.2] - 2025-02-28 13 | 14 | ### Fixed 15 | 16 | * Match code block `#` hiding behavior with rustdoc: hide lines beginning with any number of whitespace plus `# ` (or a plain `#`), and turn `##` at the beginning of lines into `#`. 17 | 18 | ## [0.4.1] - 2025-01-26 19 | 20 | ## [0.4.0] - 2024-11-30 21 | 22 | ## [0.3.9] - 2024-10-19 23 | 24 | ## [0.3.8] - 2024-10-14 25 | 26 | ## [0.3.7] - 2024-09-22 27 | 28 | ## [0.3.6] - 2024-06-09 29 | 30 | ## [0.3.5] - 2024-05-21 31 | 32 | ## [0.3.4] - 2024-03-28 33 | 34 | ## [0.3.3] - 2024-03-28 35 | 36 | ## [0.3.2] - 2023-08-25 37 | 38 | ## [0.3.1] - 2023-07-22 39 | 40 | ## [0.3.0] - 2023-01-29 41 | 42 | ## [0.2.1] - 2023-01-04 43 | 44 | ### Changed 45 | 46 | * Bump rustdoc-types from 0.17.0 to 0.18.0 47 | 48 | ## [0.2.0] - 2022-09-15 49 | 50 | ### Fixed 51 | 52 | * (breaking change) fix typo in command line arguments: `--allow-no-vsc` -> `--allow-no-vcs` 53 | 54 | ## [0.1.4] - 2022-09-15 55 | 56 | ### Added 57 | 58 | * `--check`: show diff if README is not updated 59 | 60 | ### Changed 61 | 62 | * Resolve links to exported public items defined in private modules 63 | 64 | ## [0.1.3] - 2022-09-14 65 | 66 | ### Fixed 67 | 68 | * Windows pre-built binaries were broken. 69 | 70 | ## [0.1.2] - 2022-09-14 71 | 72 | ### Added 73 | 74 | * Added [`cargo-binstall`] support for installing binaries. 75 | 76 | [`cargo-binstall`]: https://github.com/cargo-bins/cargo-binstall 77 | 78 | ## [0.1.1] - 2022-09-13 79 | 80 | ### Changed 81 | 82 | * Resolve more links in the documentation if possible (workaround for [rust-lang/rust#101687](https://github.com/rust-lang/rust/issues/101687)] 83 | 84 | ### Fixed 85 | 86 | * Remove un-resolved intra-doc links from the documentation 87 | 88 | ## [0.1.0] - 2022-09-11 89 | 90 | * First release 91 | 92 | 93 | [Unreleased]: https://github.com/gifnksm/cargo-sync-rdme/compare/v0.4.2...HEAD 94 | [0.4.2]: https://github.com/gifnksm/cargo-sync-rdme/compare/v0.4.1...v0.4.2 95 | [0.4.1]: https://github.com/gifnksm/cargo-sync-rdme/compare/v0.4.0...v0.4.1 96 | [0.4.0]: https://github.com/gifnksm/cargo-sync-rdme/compare/v0.3.9...v0.4.0 97 | [0.3.9]: https://github.com/gifnksm/cargo-sync-rdme/compare/v0.3.8...v0.3.9 98 | [0.3.8]: https://github.com/gifnksm/cargo-sync-rdme/compare/v0.3.7...v0.3.8 99 | [0.3.7]: https://github.com/gifnksm/cargo-sync-rdme/compare/v0.3.6...v0.3.7 100 | [0.3.6]: https://github.com/gifnksm/cargo-sync-rdme/compare/v0.3.5...v0.3.6 101 | [0.3.5]: https://github.com/gifnksm/cargo-sync-rdme/compare/v0.3.4...v0.3.5 102 | [0.3.4]: https://github.com/gifnksm/cargo-sync-rdme/compare/v0.3.3...v0.3.4 103 | [0.3.3]: https://github.com/gifnksm/cargo-sync-rdme/compare/v0.3.2...v0.3.3 104 | [0.3.2]: https://github.com/gifnksm/cargo-sync-rdme/compare/v0.3.1...v0.3.2 105 | [0.3.1]: https://github.com/gifnksm/cargo-sync-rdme/compare/v0.3.0...v0.3.1 106 | [0.3.0]: https://github.com/gifnksm/cargo-sync-rdme/compare/v0.2.1...v0.3.0 107 | [0.2.1]: https://github.com/gifnksm/cargo-sync-rdme/compare/v0.2.0...v0.2.1 108 | [0.2.0]: https://github.com/gifnksm/cargo-sync-rdme/compare/v0.1.4...v0.2.0 109 | [0.1.4]: https://github.com/gifnksm/cargo-sync-rdme/compare/v0.1.3...v0.1.4 110 | [0.1.3]: https://github.com/gifnksm/cargo-sync-rdme/compare/v0.1.2...v0.1.3 111 | [0.1.2]: https://github.com/gifnksm/cargo-sync-rdme/compare/v0.1.1...v0.1.2 112 | [0.1.1]: https://github.com/gifnksm/cargo-sync-rdme/compare/v0.1.0...v0.1.1 113 | [0.1.0]: https://github.com/gifnksm/cargo-sync-rdme/commits/v0.1.0 114 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | This project adheres to the Rust Code of Conduct, which can be found [here](https://www.rust-lang.org/conduct.html). 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution guidelines 2 | 3 | First off, thank you for considering contributing to cargo-sync-rdme. 4 | 5 | If your contribution is not straightforward, please first discuss the change you 6 | wish to make by creating a new issue before making the change. 7 | 8 | ## Reporting issues 9 | 10 | Before reporting an issue on the 11 | [issue tracker](https://github.com/gifnksm/cargo-sync-rdme/issues), 12 | please check that it has not already been reported by searching for some related 13 | keywords. 14 | 15 | ## Pull requests 16 | 17 | Try to do one pull request per change. 18 | 19 | ### Updating the changelog 20 | 21 | Update the changes you have made in 22 | [CHANGELOG](https://github.com/gifnksm/cargo-sync-rdme/blob/main/CHANGELOG.md) 23 | file under the **Unreleased** section. 24 | 25 | Add the changes of your pull request to one of the following subsections, 26 | depending on the types of changes defined by 27 | [Keep a changelog](https://keepachangelog.com/en/1.0.0/): 28 | 29 | - `Added` for new features. 30 | - `Changed` for changes in existing functionality. 31 | - `Deprecated` for soon-to-be removed features. 32 | - `Removed` for now removed features. 33 | - `Fixed` for any bug fixes. 34 | - `Security` in case of vulnerabilities. 35 | 36 | If the required subsection does not exist yet under **Unreleased**, create it! 37 | 38 | ## Developing 39 | 40 | ### Set up 41 | 42 | This is no different than other Rust projects. 43 | 44 | ```console 45 | git clone https://github.com/gifnksm/cargo-sync-rdme 46 | cd cargo-sync-rdme 47 | cargo test 48 | ``` 49 | 50 | ### Useful Commands 51 | 52 | - Build and run release version: 53 | 54 | ```console 55 | cargo build --release && cargo run --release 56 | ``` 57 | 58 | - Run Clippy: 59 | 60 | ```console 61 | cargo clippy --all-targets --all-features --workspace 62 | ``` 63 | 64 | - Run all tests: 65 | 66 | ```console 67 | cargo test --all-features --workspace 68 | ``` 69 | 70 | - Check to see if there are code formatting issues 71 | 72 | ```console 73 | cargo fmt --all -- --check 74 | ``` 75 | 76 | - Format the code in the project 77 | 78 | ```console 79 | cargo fmt --all 80 | ``` 81 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["xtask", "examples/lib"] 3 | 4 | [package] 5 | name = "cargo-sync-rdme" 6 | version = "0.4.2" 7 | edition = "2021" 8 | rust-version = "1.81.0" 9 | description = "Cargo subcommand to synchronize README with crate documentation" 10 | readme = "README.md" 11 | repository = "https://github.com/gifnksm/cargo-sync-rdme" 12 | license = "MIT OR Apache-2.0" 13 | keywords = [] 14 | categories = [] 15 | 16 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 17 | [features] 18 | default = ["vcs-git"] 19 | vcs-git = ["dep:git2"] 20 | vendored-libgit2 = ["git2?/vendored-libgit2"] 21 | 22 | [package.metadata.cargo-sync-rdme] 23 | extra-targets = "./docs/configuration.md" 24 | 25 | [package.metadata.cargo-sync-rdme.badge] 26 | style = "flat-square" 27 | 28 | [package.metadata.cargo-sync-rdme.badge.badges] 29 | maintenance = true 30 | license = { link = "#license" } 31 | crates-io = true 32 | docs-rs = true 33 | rust-version = true 34 | github-actions = { workflows = ["ci.yml"] } 35 | codecov = true 36 | 37 | [package.metadata.cargo-sync-rdme.badge.badges-maintenance] 38 | maintenance = true 39 | [package.metadata.cargo-sync-rdme.badge.badges-license] 40 | license = true 41 | [package.metadata.cargo-sync-rdme.badge.badges-crates-io] 42 | crates-io = true 43 | [package.metadata.cargo-sync-rdme.badge.badges-docs-rs] 44 | docs-rs = true 45 | [package.metadata.cargo-sync-rdme.badge.badges-rust-version] 46 | rust-version = true 47 | [package.metadata.cargo-sync-rdme.badge.badges-github-actions] 48 | github-actions = true 49 | [package.metadata.cargo-sync-rdme.badge.badges-codecov] 50 | codecov = true 51 | 52 | [package.metadata.binstall] 53 | pkg-url = "{ repo }/releases/download/v{ version }/{ name }-v{ version }-{ target }.{ archive-format }" 54 | bin-dir = "{ bin }{ binary-ext }" 55 | pkg-fmt = "tgz" 56 | 57 | [dependencies] 58 | cargo_metadata = "0.19.2" 59 | clap = { version = "4.5.35", features = ["derive"] } 60 | console = "0.15.11" 61 | git2 = { version = "0.20.1", default-features = false, optional = true } 62 | miette = { version = "7.5.0", features = ["fancy"] } 63 | once_cell = "1.21.3" 64 | pulldown-cmark = "0.13.0" 65 | pulldown-cmark-to-cmark = "21.0.0" 66 | rustdoc-types = "0.38.0" 67 | serde = { version = "1.0.219", features = ["derive"] } 68 | serde_json = "1.0.140" 69 | serde_yaml = "0.9.34" 70 | similar = { version = "2.7.0", features = ["inline", "unicode"] } 71 | tempfile = "3.19.1" 72 | thiserror = "2.0.12" 73 | toml = "0.5.11" 74 | tracing = "0.1.41" 75 | tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } 76 | url = "2.5.4" 77 | void = "1.0.2" 78 | 79 | [build-dependencies] 80 | 81 | [profile.dev] 82 | 83 | [profile.release] 84 | strip = true 85 | 86 | [badges] 87 | maintenance = { status = "actively-developed" } 88 | 89 | [dev-dependencies] 90 | indoc = "2.0.6" 91 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 gifnksm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # cargo-sync-rdme 3 | 4 | 5 | [![Maintenance: actively-developed](https://img.shields.io/badge/maintenance-actively--developed-brightgreen.svg?style=flat-square)](https://doc.rust-lang.org/cargo/reference/manifest.html#the-badges-section) 6 | [![License: MIT OR Apache-2.0](https://img.shields.io/crates/l/cargo-sync-rdme.svg?style=flat-square)](#license) 7 | [![crates.io](https://img.shields.io/crates/v/cargo-sync-rdme.svg?logo=rust&style=flat-square)](https://crates.io/crates/cargo-sync-rdme) 8 | [![docs.rs](https://img.shields.io/docsrs/cargo-sync-rdme.svg?logo=docs.rs&style=flat-square)](https://docs.rs/cargo-sync-rdme) 9 | [![Rust: ^1.81.0](https://img.shields.io/badge/rust-^1.81.0-93450a.svg?logo=rust&style=flat-square)](https://doc.rust-lang.org/cargo/reference/manifest.html#the-rust-version-field) 10 | [![GitHub Actions: CI](https://img.shields.io/github/actions/workflow/status/gifnksm/cargo-sync-rdme/ci.yml.svg?label=CI&logo=github&style=flat-square)](https://github.com/gifnksm/cargo-sync-rdme/actions/workflows/ci.yml) 11 | [![Codecov](https://img.shields.io/codecov/c/github/gifnksm/cargo-sync-rdme.svg?label=codecov&logo=codecov&style=flat-square)](https://codecov.io/gh/gifnksm/cargo-sync-rdme) 12 | 13 | 14 | Cargo subcommand to synchronize README with the cargo manifest and crate documentation. 15 | 16 | ## Installation 17 | 18 | There are multiple ways to install cargo-sync-rdme. 19 | Choose any one of the methods below that best suits your needs. 20 | 21 | ### Pre-built binaries 22 | 23 | Executable binaries are available for download on the [GitHub Release page]. 24 | 25 | [GitHub Release page]: https://github.com/gifnksm/cargo-sync-rdme/releases/ 26 | 27 | ### Build from source using Rust 28 | 29 | To build cargo-sync-rdme executable from the source, you must have the Rust toolchain installed. 30 | To install the rust toolchain, follow [this guide](https://www.rust-lang.org/tools/install). 31 | 32 | Once you have installed Rust, the following command can be used to build and install cargo-sync-rdme: 33 | 34 | ```console 35 | # Install released version 36 | $ cargo install cargo-sync-rdme 37 | 38 | # Install latest version 39 | $ cargo install --git https://github.com/gifnksm/cargo-sync-rdme.git cargo-sync-rdme 40 | ``` 41 | 42 | ## Usage 43 | 44 | cargo-sync-rdme is a subcommand to synchronize the contents of README.md with the cargo manifest and crate documentation. 45 | By embedding marker comments in README.md, you can insert the documentation generated by cargo-sync-rdme. 46 | There are three types of marker comments as follows. 47 | 48 | * `` : generate document title (H1 element) from package name. 49 | * `` : generate badges from package metadata. 50 | * `` : generate documentation for a crate from document comments. 51 | 52 | Write the README.md as follows: 53 | 54 | ```markdown 55 | 56 | 57 | 58 | ``` 59 | 60 | To update the contents of README.md, run the following: 61 | 62 | ```console 63 | cargo sync-rdme --toolchain nightly 64 | ``` 65 | 66 | cargo-sync-rdme uses the unstable features of rustdoc, so nightly toolchain is required to generate READMEs from comments in the crate documentation. 67 | If nightly toolchain is not installed, it can be installed with the following command 68 | 69 | ```console 70 | rustup toolchain install nightly 71 | ``` 72 | 73 | The contents of README.md will be updated as follows: 74 | 75 | ```markdown 76 | 77 | # (Package name) 78 | 79 | 80 | (Badges) 81 | 82 | 83 | (Crate documentation) 84 | 85 | ``` 86 | 87 | See [examples/lib](examples/lib) for actual examples. 88 | 89 | ## Configuration 90 | 91 | You can customize the behavior of cargo-sync-rdme by adding the following section to `Cargo.toml`. 92 | 93 | ```toml 94 | [package.metadata.cargo-sync-rdme.badges] 95 | maintenance = true 96 | license = true 97 | 98 | [package.metadata.cargo-sync-rdme.rustdoc] 99 | html-root-url = "https://gifnksm.github.io/cargo-sync-rdme/" 100 | ``` 101 | 102 | See [Configuration](./docs/configuration.md) for details. 103 | 104 | ## Minimum supported Rust version (MSRV) 105 | 106 | The minimum supported Rust version is **Rust 1.81.0**. 107 | At least the last 3 versions of stable Rust are supported at any given time. 108 | 109 | While a crate is a pre-release status (0.x.x) it may have its MSRV bumped in a patch release. 110 | Once a crate has reached 1.x, any MSRV bump will be accompanied by a new minor version. 111 | 112 | ## License 113 | 114 | This project is licensed under either of 115 | 116 | * Apache License, Version 2.0 117 | ([LICENSE-APACHE](LICENSE-APACHE) or ) 118 | * MIT license 119 | ([LICENSE-MIT](LICENSE-MIT) or ) 120 | 121 | at your option. 122 | 123 | ## Contribution 124 | 125 | Unless you explicitly state otherwise, any contribution intentionally submitted 126 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 127 | dual licensed as above, without any additional terms or conditions. 128 | 129 | See [CONTRIBUTING.md](CONTRIBUTING.md). 130 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | informational: true 6 | patch: 7 | default: 8 | informational: true 9 | ignore: 10 | - xtask 11 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # This template contains all of the possible sections and their default values 2 | 3 | # Note that all fields that take a lint level have these possible values: 4 | # * deny - An error will be produced and the check will fail 5 | # * warn - A warning will be produced, but the check will not fail 6 | # * allow - No warning or error will be produced, though in some cases a note 7 | # will be 8 | 9 | # The values provided in this template are the default values that will be used 10 | # when any section or field is not specified in your own configuration 11 | 12 | # Root options 13 | 14 | # The graph table configures how the dependency graph is constructed and thus 15 | # which crates the checks are performed against 16 | [graph] 17 | # If 1 or more target triples (and optionally, target_features) are specified, 18 | # only the specified targets will be checked when running `cargo deny check`. 19 | # This means, if a particular package is only ever used as a target specific 20 | # dependency, such as, for example, the `nix` crate only being used via the 21 | # `target_family = "unix"` configuration, that only having windows targets in 22 | # this list would mean the nix crate, as well as any of its exclusive 23 | # dependencies not shared by any other crates, would be ignored, as the target 24 | # list here is effectively saying which targets you are building for. 25 | targets = [ 26 | # The triple can be any string, but only the target triples built in to 27 | # rustc (as of 1.40) can be checked against actual config expressions 28 | #"x86_64-unknown-linux-musl", 29 | # You can also specify which target_features you promise are enabled for a 30 | # particular target. target_features are currently not validated against 31 | # the actual valid features supported by the target architecture. 32 | #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, 33 | ] 34 | # When creating the dependency graph used as the source of truth when checks are 35 | # executed, this field can be used to prune crates from the graph, removing them 36 | # from the view of cargo-deny. This is an extremely heavy hammer, as if a crate 37 | # is pruned from the graph, all of its dependencies will also be pruned unless 38 | # they are connected to another crate in the graph that hasn't been pruned, 39 | # so it should be used with care. The identifiers are [Package ID Specifications] 40 | # (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html) 41 | #exclude = [] 42 | # If true, metadata will be collected with `--all-features`. Note that this can't 43 | # be toggled off if true, if you want to conditionally enable `--all-features` it 44 | # is recommended to pass `--all-features` on the cmd line instead 45 | all-features = false 46 | # If true, metadata will be collected with `--no-default-features`. The same 47 | # caveat with `all-features` applies 48 | no-default-features = false 49 | # If set, these feature will be enabled when collecting metadata. If `--features` 50 | # is specified on the cmd line they will take precedence over this option. 51 | #features = [] 52 | 53 | # The output table provides options for how/if diagnostics are outputted 54 | [output] 55 | # When outputting inclusion graphs in diagnostics that include features, this 56 | # option can be used to specify the depth at which feature edges will be added. 57 | # This option is included since the graphs can be quite large and the addition 58 | # of features from the crate(s) to all of the graph roots can be far too verbose. 59 | # This option can be overridden via `--feature-depth` on the cmd line 60 | feature-depth = 1 61 | 62 | # This section is considered when running `cargo deny check advisories` 63 | # More documentation for the advisories section can be found here: 64 | # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html 65 | [advisories] 66 | # The path where the advisory databases are cloned/fetched into 67 | #db-path = "$CARGO_HOME/advisory-dbs" 68 | # The url(s) of the advisory databases to use 69 | #db-urls = ["https://github.com/rustsec/advisory-db"] 70 | # A list of advisory IDs to ignore. Note that ignored advisories will still 71 | # output a note when they are encountered. 72 | ignore = [ 73 | #"RUSTSEC-0000-0000", 74 | #{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" }, 75 | #"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish 76 | #{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" }, 77 | ] 78 | # If this is true, then cargo deny will use the git executable to fetch advisory database. 79 | # If this is false, then it uses a built-in git library. 80 | # Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support. 81 | # See Git Authentication for more information about setting up git authentication. 82 | #git-fetch-with-cli = true 83 | 84 | # This section is considered when running `cargo deny check licenses` 85 | # More documentation for the licenses section can be found here: 86 | # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html 87 | [licenses] 88 | # List of explicitly allowed licenses 89 | # See https://spdx.org/licenses/ for list of possible licenses 90 | # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. 91 | allow = [ 92 | "MIT", 93 | "Apache-2.0", 94 | "ISC", 95 | "Unicode-3.0", 96 | #"Apache-2.0 WITH LLVM-exception", 97 | ] 98 | # The confidence threshold for detecting a license from license text. 99 | # The higher the value, the more closely the license text must be to the 100 | # canonical license text of a valid SPDX license file. 101 | # [possible values: any between 0.0 and 1.0]. 102 | confidence-threshold = 0.8 103 | # Allow 1 or more licenses on a per-crate basis, so that particular licenses 104 | # aren't accepted for every possible crate as with the normal allow list 105 | exceptions = [ 106 | # Each entry is the crate and version constraint, and its specific allow 107 | # list 108 | #{ allow = ["Zlib"], crate = "adler32" }, 109 | ] 110 | 111 | # Some crates don't have (easily) machine readable licensing information, 112 | # adding a clarification entry for it allows you to manually specify the 113 | # licensing information 114 | #[[licenses.clarify]] 115 | # The package spec the clarification applies to 116 | #crate = "ring" 117 | # The SPDX expression for the license requirements of the crate 118 | #expression = "MIT AND ISC AND OpenSSL" 119 | # One or more files in the crate's source used as the "source of truth" for 120 | # the license expression. If the contents match, the clarification will be used 121 | # when running the license check, otherwise the clarification will be ignored 122 | # and the crate will be checked normally, which may produce warnings or errors 123 | # depending on the rest of your configuration 124 | #license-files = [ 125 | # Each entry is a crate relative path, and the (opaque) hash of its contents 126 | #{ path = "LICENSE", hash = 0xbd0eed23 } 127 | #] 128 | 129 | [licenses.private] 130 | # If true, ignores workspace crates that aren't published, or are only 131 | # published to private registries. 132 | # To see how to mark a crate as unpublished (to the official registry), 133 | # visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field. 134 | ignore = false 135 | # One or more private registries that you might publish crates to, if a crate 136 | # is only published to private registries, and ignore is true, the crate will 137 | # not have its license(s) checked 138 | registries = [ 139 | #"https://sekretz.com/registry 140 | ] 141 | 142 | # This section is considered when running `cargo deny check bans`. 143 | # More documentation about the 'bans' section can be found here: 144 | # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html 145 | [bans] 146 | # Lint level for when multiple versions of the same crate are detected 147 | multiple-versions = "warn" 148 | # Lint level for when a crate version requirement is `*` 149 | wildcards = "allow" 150 | # The graph highlighting used when creating dotgraphs for crates 151 | # with multiple versions 152 | # * lowest-version - The path to the lowest versioned duplicate is highlighted 153 | # * simplest-path - The path to the version with the fewest edges is highlighted 154 | # * all - Both lowest-version and simplest-path are used 155 | highlight = "all" 156 | # The default lint level for `default` features for crates that are members of 157 | # the workspace that is being checked. This can be overridden by allowing/denying 158 | # `default` on a crate-by-crate basis if desired. 159 | workspace-default-features = "allow" 160 | # The default lint level for `default` features for external crates that are not 161 | # members of the workspace. This can be overridden by allowing/denying `default` 162 | # on a crate-by-crate basis if desired. 163 | external-default-features = "allow" 164 | # List of crates that are allowed. Use with care! 165 | allow = [ 166 | #"ansi_term@0.11.0", 167 | #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" }, 168 | ] 169 | # List of crates to deny 170 | deny = [ 171 | #"ansi_term@0.11.0", 172 | #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" }, 173 | # Wrapper crates can optionally be specified to allow the crate when it 174 | # is a direct dependency of the otherwise banned crate 175 | #{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] }, 176 | ] 177 | 178 | # List of features to allow/deny 179 | # Each entry the name of a crate and a version range. If version is 180 | # not specified, all versions will be matched. 181 | #[[bans.features]] 182 | #crate = "reqwest" 183 | # Features to not allow 184 | #deny = ["json"] 185 | # Features to allow 186 | #allow = [ 187 | # "rustls", 188 | # "__rustls", 189 | # "__tls", 190 | # "hyper-rustls", 191 | # "rustls", 192 | # "rustls-pemfile", 193 | # "rustls-tls-webpki-roots", 194 | # "tokio-rustls", 195 | # "webpki-roots", 196 | #] 197 | # If true, the allowed features must exactly match the enabled feature set. If 198 | # this is set there is no point setting `deny` 199 | #exact = true 200 | 201 | # Certain crates/versions that will be skipped when doing duplicate detection. 202 | skip = [ 203 | #"ansi_term@0.11.0", 204 | #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" }, 205 | ] 206 | # Similarly to `skip` allows you to skip certain crates during duplicate 207 | # detection. Unlike skip, it also includes the entire tree of transitive 208 | # dependencies starting at the specified crate, up to a certain depth, which is 209 | # by default infinite. 210 | skip-tree = [ 211 | #"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies 212 | #{ crate = "ansi_term@0.11.0", depth = 20 }, 213 | ] 214 | 215 | # This section is considered when running `cargo deny check sources`. 216 | # More documentation about the 'sources' section can be found here: 217 | # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html 218 | [sources] 219 | # Lint level for what to happen when a crate from a crate registry that is not 220 | # in the allow list is encountered 221 | unknown-registry = "warn" 222 | # Lint level for what to happen when a crate from a git repository that is not 223 | # in the allow list is encountered 224 | unknown-git = "warn" 225 | # List of URLs for allowed crate registries. Defaults to the crates.io index 226 | # if not specified. If it is specified but empty, no registries are allowed. 227 | allow-registry = ["https://github.com/rust-lang/crates.io-index"] 228 | # List of URLs for allowed Git repositories 229 | allow-git = [] 230 | 231 | [sources.allow-org] 232 | # github.com organizations to allow git sources for 233 | github = [] 234 | # gitlab.com organizations to allow git sources for 235 | gitlab = [] 236 | # bitbucket.org organizations to allow git sources for 237 | bitbucket = [] 238 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | See [Configuration](https://gifnksm.github.io/cargo-sync-rdme/cargo_sync_rdme/config/index.html) for details. 4 | 5 | You can customize the behavior of cargo-sync-rdme by adding the following section to `Cargo.toml`. 6 | 7 | ```toml 8 | [package.metadata.cargo-sync-rdme] 9 | extra-targets = "./docs/configuration.md" 10 | 11 | [package.metadata.cargo-sync-rdme.badge] 12 | style = "for-the-badge" 13 | 14 | [package.metadata.cargo-sync-rdme.badge.badges] 15 | maintenance = true 16 | license = true 17 | 18 | [package.metadata.cargo-sync-rdme.rustdoc] 19 | html-root-url = "https://gifnksm.github.io/cargo-sync-rdme/" 20 | ``` 21 | 22 | ## Common configuration 23 | 24 | You can customize the behavior of cargo-sync-rdme by adding the following section to `Cargo.toml`. 25 | 26 | ```toml 27 | [package.metadata.cargo-sync-rdme] 28 | extra-targets = "./docs/configuration.md" 29 | ``` 30 | 31 | ### `extra-targets` (`package.metadata.cargo-sync-rdme.extra-targets`) 32 | 33 | The `extra-targets` option specifies the paths to the files that are also updated when `cargo sync-rdme` is executed. 34 | 35 | String or array of strings can be specified. 36 | 37 | ```toml 38 | [package.metadata.cargo-sync-rdme] 39 | extra-targets = "./docs/configuration.md" 40 | ``` 41 | 42 | ```toml 43 | [package.metadata.cargo-sync-rdme] 44 | extra-targets = ["./docs/configuration.md", "./docs/usage.md"] 45 | ``` 46 | 47 | ## Badge configuration 48 | 49 | You can customize the badges generated by cargo-sync-rdme by adding the following section to `Cargo.toml`: 50 | 51 | ```toml 52 | [package.metadata.cargo-sync-rdme.badge] 53 | style = "flat-square" 54 | 55 | [package.metadata.cargo-sync-rdme.badge.badges] 56 | maintenance = true 57 | license = true 58 | ``` 59 | 60 | Badges are output in the order in which the configuration items are written. 61 | 62 | Output README file is as follows: 63 | 64 | ```markdown 65 | 66 | [![Maintenance: actively-developed](https://img.shields.io/badge/maintenance-actively--developed-brightgreen.svg)](https://doc.rust-lang.org/cargo/reference/manifest.html#the-badges-section) 67 | [![License: MIT OR Apache-2.0](https://img.shields.io/crates/l/cargo-sync-rdme.svg)](#license) 68 | 69 | ``` 70 | 71 | ### `style` (`package.metadata.cargo-sync-rdme.badge.style`) 72 | 73 | The `style` option specifies the style of the badge. 74 | 75 | The following styles are supported: 76 | 77 | | Style | Example | 78 | | --------------- | -------------------------------------------------------------------------------------------------- | 79 | | `flat` | ![flat](https://img.shields.io/badge/style-flat-green.svg?style=flat) | 80 | | `flat-square` | ![flat-square](https://img.shields.io/badge/style-flat--square-green.svg?style=flat-square) | 81 | | `for-the-badge` | ![for-the-badge](https://img.shields.io/badge/style-for--the--badge-green.svg?style=for-the-badge) | 82 | | `plastic` | ![plastic](https://img.shields.io/badge/style-plastic-green.svg?style=plastic) | 83 | | `social` | ![social](https://img.shields.io/badge/style-social-green.svg?style=social) | 84 | 85 | ### Badge configuration items 86 | 87 | The following configuration items are available for badges: 88 | 89 | #### Maintenance status (`package.metadata.cargo-sync-rdme.badge.{badges,badges-*}.{maintenance,maintenance-*}`) 90 | 91 | 92 | [![Maintenance: actively-developed](https://img.shields.io/badge/maintenance-actively--developed-brightgreen.svg?style=flat-square)](https://doc.rust-lang.org/cargo/reference/manifest.html#the-badges-section) 93 | 94 | 95 | A badge indicating the maintenance status of the package. 96 | 97 | The badge is generated from the `package.metadata.maintenance.status` field in `Cargo.toml` 98 | (see [the cargo documentation](https://doc.rust-lang.org/cargo/reference/manifest.html#the-badges-section) for details). 99 | 100 | The link target of the badge is set to . 101 | 102 | Available values: 103 | 104 | * `maintenance = true` : Output a maintenance status badge 105 | * `maintenance = false` : Do not output a maintenance status badge 106 | 107 | #### License (`package.metadata.cargo-sync-rdme.badge.{badges,badges-*}.{license,license-*}`) 108 | 109 | 110 | ![License: MIT OR Apache-2.0](https://img.shields.io/crates/l/cargo-sync-rdme.svg?style=flat-square) 111 | 112 | 113 | A badge indicating the license of the package. 114 | 115 | The badge is generated from the `package.license` field or `package.license-file` field in `Cargo.toml` 116 | (see [the cargo documentation](https://doc.rust-lang.org/cargo/reference/manifest.html#the-license-and-license-file-fields) for details). 117 | 118 | The link target of the badge is determined by the badge configuration. 119 | 120 | Available values: 121 | 122 | * `license = { link = "" }` : Output a license badge. The link target of the badge is set to `` 123 | * `license = true` : Output a license badge 124 | * If `package.license-file` is specified, the link target of the badge is set to the license file 125 | * If `package.license` is specified, no link is set 126 | * `license = false` : Do not output a license badge 127 | 128 | #### crates.io (`package.metadata.cargo-sync-rdme.badge.{badges,badges-*}.{crates-io,crates-io-*}`) 129 | 130 | 131 | [![crates.io](https://img.shields.io/crates/v/cargo-sync-rdme.svg?logo=rust&style=flat-square)](https://crates.io/crates/cargo-sync-rdme) 132 | 133 | 134 | A badge indicating the version of the package on crates.io. 135 | 136 | The badge is generated from the `package.name` field in `Cargo.toml` 137 | (see [the cargo documentation](https://doc.rust-lang.org/cargo/reference/manifest.html#the-name-field) for details). 138 | 139 | The link target of the badge is set to `https://crates.io/crates/)`. 140 | 141 | Available values: 142 | 143 | * `crates-io = true` : Output a crates.io badge 144 | * `crates-io = false` : Do not output a crates.io badge 145 | 146 | #### Docs.rs (`package.metadata.cargo-sync-rdme.badge.{badges,badges-*}.{docs-rs,docs-rs-*}`) 147 | 148 | 149 | [![docs.rs](https://img.shields.io/docsrs/cargo-sync-rdme.svg?logo=docs.rs&style=flat-square)](https://docs.rs/cargo-sync-rdme) 150 | 151 | 152 | A badge indicating the documentation build status of the package on docs.rs. 153 | 154 | The badge is generated from the `package.name` field in `Cargo.toml` 155 | (see [the cargo documentation](https://doc.rust-lang.org/cargo/reference/manifest.html#the-name-field) for details). 156 | 157 | The link target of the badge is set to `https://docs.rs/)`. 158 | 159 | Available values: 160 | 161 | * `docs-rs = true` : Output a docs.rs badge 162 | * `docs-rs = false` : Do not output a docs.rs badge 163 | 164 | #### Rust Version (MSRV) (`package.metadata.cargo-sync-rdme.badge.{badges,badges-*}.{rust-version,rust-version-*}`) 165 | 166 | 167 | [![Rust: ^1.81.0](https://img.shields.io/badge/rust-^1.81.0-93450a.svg?logo=rust&style=flat-square)](https://doc.rust-lang.org/cargo/reference/manifest.html#the-rust-version-field) 168 | 169 | 170 | A badge indicating the minimum supported Rust version (MSRV) of the package. 171 | 172 | The badge is generated from the `package.metadata.rust-version` field in `Cargo.toml` 173 | (see [the cargo documentation](https://doc.rust-lang.org/cargo/reference/manifest.html#the-rust-version-field) for details). 174 | 175 | The link target of the badge is set to . 176 | 177 | Available values: 178 | 179 | * `rust-version = true` : Output a supported rust version badge 180 | * `rust-version = false` : Do not output a supported rust version badge 181 | 182 | #### GitHub Actions (`package.metadata.cargo-sync-rdme.badge.{badges,badges-*}.{github-actions,github-actions-*}`) 183 | 184 | 185 | [![GitHub Actions: CD](https://img.shields.io/github/actions/workflow/status/gifnksm/cargo-sync-rdme/cd.yml.svg?label=CD&logo=github&style=flat-square)](https://github.com/gifnksm/cargo-sync-rdme/actions/workflows/cd.yml) 186 | [![GitHub Actions: CI](https://img.shields.io/github/actions/workflow/status/gifnksm/cargo-sync-rdme/ci.yml.svg?label=CI&logo=github&style=flat-square)](https://github.com/gifnksm/cargo-sync-rdme/actions/workflows/ci.yml) 187 | [![GitHub Actions: Deploy Rustdoc to GitHub Pages](https://img.shields.io/github/actions/workflow/status/gifnksm/cargo-sync-rdme/pages.yml.svg?label=Deploy+Rustdoc+to+GitHub+Pages&logo=github&style=flat-square)](https://github.com/gifnksm/cargo-sync-rdme/actions/workflows/pages.yml) 188 | [![GitHub Actions: Security Audit](https://img.shields.io/github/actions/workflow/status/gifnksm/cargo-sync-rdme/audit.yml.svg?label=Security+Audit&logo=github&style=flat-square)](https://github.com/gifnksm/cargo-sync-rdme/actions/workflows/audit.yml) 189 | 190 | 191 | A badge indicating the status of the GitHub Actions workflow. 192 | 193 | The badge is generated from the `package.repository` field in `Cargo.toml` 194 | (see [the cargo documentation](https://doc.rust-lang.org/cargo/reference/manifest.html#the-repository-field) for details). 195 | 196 | The link target of the badge is set to `/actions/workflows/`. 197 | `` is the name of the file in the `.github/workflows` directory. 198 | 199 | Available values: 200 | 201 | * `github-actions = { workflows = [ { file = "", name = "" }, ..] }` : 202 | Output a GitHub Actions status badges. 203 | 204 | The link target of the badge is set to `/actions/workflows/`. 205 | 206 | `` is used as the badge name. 207 | If `` is not specified, the name of the workflow defined in the `` is used as the badge name. 208 | * `github-actions = { workflows = [ "" ] }` : 209 | Same as `github-actions = { workflows = [ { file = "" } ] }` 210 | * `github-actions = { workflows = "" }` : 211 | Same as `github-actions = { workflows = [ { file = "" } ] }` 212 | * `github-actions = { workflows = [] }` : 213 | Output a GitHub Actions status badge for all workflows in the `.github/workflows` directory. 214 | * `github-actions = true` : Same as `github-actions = { workflows = [] }` 215 | * `github-actions = false` : Do not output a GitHub Actions status badge 216 | 217 | #### Codecov (`package.metadata.cargo-sync-rdme.badge.{badges,badges-*}.{codecov,codecov-*}`) 218 | 219 | 220 | [![Codecov](https://img.shields.io/codecov/c/github/gifnksm/cargo-sync-rdme.svg?label=codecov&logo=codecov&style=flat-square)](https://codecov.io/gh/gifnksm/cargo-sync-rdme) 221 | 222 | 223 | A badge indicating the coverage of the package. 224 | 225 | The badge is generated from the `package.repository` field in `Cargo.toml` 226 | (see [the cargo documentation](https://doc.rust-lang.org/cargo/reference/manifest.html#the-repository-field) for details). 227 | 228 | The link target of the badge is set to `https://codecov.io/gh//`. 229 | 230 | Available values: 231 | 232 | * `codecov = true` : Output a Codecov badge 233 | * `codecov = false` : Do not output a Codecov badge 234 | 235 | 254 | 255 | ### Multiple badge items with the same kind 256 | 257 | If you want to use the same kind of badge multiple times, add the `-*` suffix to the configuration item name. 258 | 259 | ```toml 260 | [package.metadata.sync-rdme.badge.badges] 261 | github-actions-foo = { workflows = "foo.yml" } 262 | github-actions-bar = { workflows = "bar.yml" } 263 | ``` 264 | 265 | ### Badge groups 266 | 267 | You can define multiple badge groups and select the badge groups to be output using the `package.metadata.sync-rdme.badge.badges-` section. 268 | 269 | ```toml 270 | [package.metadata.sync-rdme.badge.badges-foo] 271 | # foo group definition 272 | license = true 273 | maintenance = true 274 | 275 | [package.metadata.sync-rdme.badge.badges-bar] 276 | # bar group definition 277 | crates-io = true 278 | docs-rs = true 279 | ``` 280 | 281 | You can embed the badge groups in the `README.md` using the following syntax: 282 | 283 | ```markdown 284 | Before sync: 285 | 286 | 287 | After sync: 288 | 289 | badges... 290 | 291 | ``` 292 | 293 | ## Rustdoc configuration 294 | 295 | You can customize the crate documentation generated by cargo-sync-rdme by adding the following section to `Cargo.toml`: 296 | 297 | ```toml 298 | [package.metadata.cargo-sync-rdme.rustdoc] 299 | html-root-url = "" 300 | ``` 301 | 302 | The following configuration items are available for rustdoc: 303 | 304 | * `html-root-url` : Set the root URL of the documentation for the packages in the workspace. 305 | The default value is `https://docs.rs//`. 306 | 307 | If you host the documentation of main/master branch on GitHub Pages, you can set the value to `https://.github.io//`. 308 | -------------------------------------------------------------------------------- /examples/lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-sync-rdme-example-lib" 3 | version = "0.1.0" 4 | edition = "2021" 5 | readme = "README.md" 6 | publish = false 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | [package.metadata.cargo-udeps.ignore] 10 | normal = ["async-trait", "serde", "num"] 11 | 12 | [package.metadata.cargo-sync-rdme.badge.badges] 13 | 14 | [package.metadata.cargo-sync-rdme.rustdoc] 15 | html-root-url = "https://gifnksm.github.io/cargo-sync-rdme/" 16 | 17 | [dependencies] 18 | async-trait = "0.1.88" 19 | num = { version = "0.4.3", features = ["num-bigint"] } 20 | serde = { version = "1.0.219", features = ["derive"] } 21 | -------------------------------------------------------------------------------- /examples/lib/README.md: -------------------------------------------------------------------------------- 1 | 2 | # cargo-sync-rdme-example-lib 3 | 4 | 5 | 6 | Example library of \[`cargo-sync-rdme`\] 7 | 8 | This is document comments embedded in the source code. 9 | It will be extracted and used to generate README.md. 10 | 11 | ## Intra-doc link support 12 | 13 | Intra-doc links are also supported. 14 | 15 | ### Supported Syntax 16 | 17 | [All rustdoc syntax for intra-doc links][intra-doc-link] is supported. 18 | 19 | #### Source code 20 | 21 | ````markdown 22 | * Normal link: [the struct](Struct) 23 | * Normal with backtick link: [the struct](`Struct`) 24 | * Reference link: [the enum][e1] 25 | * Reference link with backtick: [the enum][e2] 26 | * Reference shortcut link: [Union] 27 | * Reference shortcut link with backtick: [`Union`] 28 | 29 | * Link with paths: [`crate::Struct`], [`self::Struct`] 30 | * Link with namespace: [`Struct`](struct@Struct), [`macro_`](macro@macro_) 31 | * Link with disambiguators: [`function()`], [`macro_!`] 32 | 33 | [e1]: Enum 34 | [e2]: `Enum` 35 | ```` 36 | 37 | #### Rendered 38 | 39 | * Normal link: [the struct](https://gifnksm.github.io/cargo-sync-rdme/cargo_sync_rdme_example_lib/struct.Struct.html) 40 | 41 | * Normal with backtick link: [the struct](https://gifnksm.github.io/cargo-sync-rdme/cargo_sync_rdme_example_lib/struct.Struct.html) 42 | 43 | * Reference link: [the enum][e1] 44 | 45 | * Reference link with backtick: [the enum][e2] 46 | 47 | * Reference shortcut link: [Union](https://gifnksm.github.io/cargo-sync-rdme/cargo_sync_rdme_example_lib/union.Union.html) 48 | 49 | * Reference shortcut link with backtick: [`Union`](https://gifnksm.github.io/cargo-sync-rdme/cargo_sync_rdme_example_lib/union.Union.html) 50 | 51 | * Link with paths: [`crate::Struct`](https://gifnksm.github.io/cargo-sync-rdme/cargo_sync_rdme_example_lib/struct.Struct.html), [`self::Struct`](https://gifnksm.github.io/cargo-sync-rdme/cargo_sync_rdme_example_lib/struct.Struct.html) 52 | 53 | * Link with namespace: [`Struct`](https://gifnksm.github.io/cargo-sync-rdme/cargo_sync_rdme_example_lib/struct.Struct.html), [`macro_`](https://gifnksm.github.io/cargo-sync-rdme/cargo_sync_rdme_example_lib/macro.macro_.html) 54 | 55 | * Link with disambiguators: [`function()`](https://gifnksm.github.io/cargo-sync-rdme/cargo_sync_rdme_example_lib/fn.function.html), [`macro_!`](https://gifnksm.github.io/cargo-sync-rdme/cargo_sync_rdme_example_lib/macro.macro_.html) 56 | 57 | ### Link showcase 58 | 59 | |Item Kind|[`crate`](https://gifnksm.github.io/cargo-sync-rdme/cargo_sync_rdme_example_lib/index.html)|[`std`](https://doc.rust-lang.org/nightly/std/index.html)|External Crate| 60 | |---------|-------|-----|--------------| 61 | |Module|[`module`](https://gifnksm.github.io/cargo-sync-rdme/cargo_sync_rdme_example_lib/module/index.html)|[`std::collections`](https://doc.rust-lang.org/nightly/std/collections/index.html)|[`num::bigint`](https://docs.rs/num/0.4/num/bigint/index.html)| 62 | |Struct|[`Struct`](https://gifnksm.github.io/cargo-sync-rdme/cargo_sync_rdme_example_lib/struct.Struct.html)|[`std::collections::HashMap`](https://doc.rust-lang.org/nightly/std/collections/hash/map/struct.HashMap.html)|[`num::bigint::BigInt`](https://docs.rs/num-bigint/0.4/num_bigint/bigint/struct.BigInt.html)| 63 | |Struct Field [^1]|[`Struct::field`](https://gifnksm.github.io/cargo-sync-rdme/cargo_sync_rdme_example_lib/struct.Struct.html#structfield.field)|\[`std::ops::Range::start`\]|| 64 | |Union|[`Union`](https://gifnksm.github.io/cargo-sync-rdme/cargo_sync_rdme_example_lib/union.Union.html)||| 65 | |Enum|[`Enum`](https://gifnksm.github.io/cargo-sync-rdme/cargo_sync_rdme_example_lib/enum.Enum.html)|[`Option`](https://doc.rust-lang.org/nightly/core/option/enum.Option.html)|[`num::traits::FloatErrorKind`](https://docs.rs/num-traits/0.2/num_traits/enum.FloatErrorKind.html)| 66 | |Enum Variant [^2]|[`Enum::Variant`](https://gifnksm.github.io/cargo-sync-rdme/cargo_sync_rdme_example_lib/enum.Enum.html#variant.Variant)|\[`Option::Some`\]|\[`num::traits::FloatErrorKind::Empty`\]| 67 | |Function|[`function`](https://gifnksm.github.io/cargo-sync-rdme/cargo_sync_rdme_example_lib/fn.function.html)|[`std::iter::from_fn`](https://doc.rust-lang.org/nightly/core/iter/sources/from_fn/fn.from_fn.html)|[`num::abs`](https://docs.rs/num-traits/0.2/num_traits/sign/fn.abs.html)| 68 | |Typedef|[`Typedef`](https://gifnksm.github.io/cargo-sync-rdme/cargo_sync_rdme_example_lib/type.Typedef.html)|[`std::io::Result`](https://doc.rust-lang.org/nightly/std/io/error/type.Result.html)|[`num::BigRational`](https://docs.rs/num-rational/0.4/num_rational/type.BigRational.html)| 69 | |Constant|[`CONSTANT`](https://gifnksm.github.io/cargo-sync-rdme/cargo_sync_rdme_example_lib/constant.CONSTANT.html)|[`std::path::MAIN_SEPARATOR`](https://doc.rust-lang.org/nightly/std/path/constant.MAIN_SEPARATOR.html)|| 70 | |Trait|[`Trait`](https://gifnksm.github.io/cargo-sync-rdme/cargo_sync_rdme_example_lib/trait.Trait.html)|[`std::clone::Clone`](https://doc.rust-lang.org/nightly/core/clone/trait.Clone.html)|[`num::Num`](https://docs.rs/num-traits/0.2/num_traits/trait.Num.html)| 71 | |Method (trait) [^3]|[`Trait::method`](https://gifnksm.github.io/cargo-sync-rdme/cargo_sync_rdme_example_lib/Trait/fn.method.html)|\[`std::clone::Clone::clone`\]|\[`num::Num::from_str_radix`\]| 72 | |Method (impl) [^3]|\[`Struct::method`\]|\[`Vec::clone`\]|\[`num::bigint::BigInt::from_str_radix`\]| 73 | |Static|[`STATIC`](https://gifnksm.github.io/cargo-sync-rdme/cargo_sync_rdme_example_lib/static.STATIC.html)||| 74 | |Macro|[`macro_`](https://gifnksm.github.io/cargo-sync-rdme/cargo_sync_rdme_example_lib/macro.macro_.html)|[`println`](https://doc.rust-lang.org/nightly/std/macro.println.html)|| 75 | |Attribute Macro|||[`async_trait::async_trait`](https://docs.rs/async-trait/0.1.88/async_trait/attr.async_trait.html)| 76 | |Derive Macro|||[`serde::Serialize`](https://docs.rs/serde_derive/1.0.219/serde_derive/derive.Serialize.html)| 77 | |Associated Constant [^4]|[`Trait::CONST`](https://gifnksm.github.io/cargo-sync-rdme/cargo_sync_rdme_example_lib/trait.Trait.html#associatedconstant.CONST)|\[`i32::MAX`\]|| 78 | |Associated Type [^4]|[`Trait::Type`](https://gifnksm.github.io/cargo-sync-rdme/cargo_sync_rdme_example_lib/trait.Trait.html#associatedtype.Type)|\[`Iterator::Item`\]|| 79 | |Primitive||[`i32`](https://doc.rust-lang.org/nightly/std/primitive.i32.html)|| 80 | 81 | [^1]: Intra-doc links to struct fields are not supported in cargo-sync-rdme yet due to [rustdoc bug]. 82 | 83 | [^2]: Intra-doc links to enum variants are not supported in cargo-sync-rdme yet due to [rustdoc bug]. 84 | 85 | [^3]: Intra-doc links to methods are not supported in cargo-sync-rdme yet due to [rustdoc bug]. 86 | 87 | [^4]: Intra-doc links to associated constants or associated types are not supported in cargo-sync-rdme yet due to [rustdoc bug]. 88 | 89 | #### Code Block 90 | 91 | Fenced code block: 92 | 93 | ````rust 94 | println!("Hello, world!"); 95 | ```` 96 | 97 | Indented code blcok: 98 | 99 | ````rust 100 | println!("Hello, world!"); 101 | 102 | ```` 103 | 104 | [intra-doc-link]: https://doc.rust-lang.org/rustdoc/write-documentation/linking-to-items-by-name.html 105 | [e1]: https://gifnksm.github.io/cargo-sync-rdme/cargo_sync_rdme_example_lib/enum.Enum.html 106 | [e2]: https://gifnksm.github.io/cargo-sync-rdme/cargo_sync_rdme_example_lib/enum.Enum.html 107 | [rustdoc bug]: https://github.com/rust-lang/rust/issues/101687 108 | 109 | 110 | -------------------------------------------------------------------------------- /examples/lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Example library of [`cargo-sync-rdme`] 2 | //! 3 | //! This is document comments embedded in the source code. 4 | //! It will be extracted and used to generate README.md. 5 | //! 6 | //! # Intra-doc link support 7 | //! 8 | //! Intra-doc links are also supported. 9 | //! 10 | //! ## Supported Syntax 11 | //! 12 | //! [All rustdoc syntax for intra-doc links][intra-doc-link] is supported. 13 | //! 14 | //! [intra-doc-link]: https://doc.rust-lang.org/rustdoc/write-documentation/linking-to-items-by-name.html 15 | //! 16 | //! ### Source code 17 | //! 18 | //! ```markdown 19 | //! * Normal link: [the struct](Struct) 20 | //! * Normal with backtick link: [the struct](`Struct`) 21 | //! * Reference link: [the enum][e1] 22 | //! * Reference link with backtick: [the enum][e2] 23 | //! * Reference shortcut link: [Union] 24 | //! * Reference shortcut link with backtick: [`Union`] 25 | //! 26 | //! * Link with paths: [`crate::Struct`], [`self::Struct`] 27 | //! * Link with namespace: [`Struct`](struct@Struct), [`macro_`](macro@macro_) 28 | //! * Link with disambiguators: [`function()`], [`macro_!`] 29 | //! 30 | //! [e1]: Enum 31 | //! [e2]: `Enum` 32 | //! ``` 33 | //! 34 | //! ### Rendered 35 | //! 36 | //! * Normal link: [the struct](Struct) 37 | //! * Normal with backtick link: [the struct](`Struct`) 38 | //! * Reference link: [the enum][e1] 39 | //! * Reference link with backtick: [the enum][e2] 40 | //! * Reference shortcut link: [Union] 41 | //! * Reference shortcut link with backtick: [`Union`] 42 | //! 43 | //! * Link with paths: [`crate::Struct`], [`self::Struct`] 44 | //! * Link with namespace: [`Struct`](struct@Struct), [`macro_`](macro@macro_) 45 | //! * Link with disambiguators: [`function()`], [`macro_!`] 46 | //! 47 | //! [e1]: Enum 48 | //! [e2]: `Enum` 49 | //! 50 | //! ## Link showcase 51 | //! 52 | //! | Item Kind | [`crate`] | [`std`] | External Crate | 53 | //! | ------------------------ | ------------------ | ----------------------------- | -------------------------------------------- | 54 | //! | Module | [`module`] | [`std::collections`] | [`num::bigint`] | 55 | //! | Struct | [`Struct`] | [`std::collections::HashMap`] | [`num::bigint::BigInt`] | 56 | //! | Struct Field [^1] | [`Struct::field`] | [`std::ops::Range::start`] | | 57 | //! | Union | [`Union`] | | | 58 | //! | Enum | [`Enum`] | [`Option`] | [`num::traits::FloatErrorKind`] | 59 | //! | Enum Variant [^2] | [`Enum::Variant`] | [`Option::Some`] | [`num::traits::FloatErrorKind::Empty`] | 60 | //! | Function | [`function`] | [`std::iter::from_fn`] | [`num::abs`] | 61 | //! | Typedef | [`Typedef`] | [`std::io::Result`] | [`num::BigRational`] | 62 | //! | Constant | [`CONSTANT`] | [`std::path::MAIN_SEPARATOR`] | | 63 | //! | Trait | [`Trait`] | [`std::clone::Clone`] | [`num::Num`] | 64 | //! | Method (trait) [^3] | [`Trait::method`] | [`std::clone::Clone::clone`] | [`num::Num::from_str_radix`] | 65 | //! | Method (impl) [^3] | [`Struct::method`] | [`Vec::clone`] | [`num::bigint::BigInt::from_str_radix`] | 66 | //! | Static | [`STATIC`] | | | 67 | //! | Macro | [`macro_`] | [`println`] | | 68 | //! | Attribute Macro | | | [`async_trait::async_trait`] | 69 | //! | Derive Macro | | | [`serde::Serialize`](macro@serde::Serialize) | 70 | //! | Associated Constant [^4] | [`Trait::CONST`] | [`i32::MAX`] | | 71 | //! | Associated Type [^4] | [`Trait::Type`] | [`Iterator::Item`] | | 72 | //! | Primitive | | [`i32`] | | 73 | //! 74 | //! [^1]: Intra-doc links to struct fields are not supported in cargo-sync-rdme yet due to [rustdoc bug]. 75 | //! 76 | //! [^2]: Intra-doc links to enum variants are not supported in cargo-sync-rdme yet due to [rustdoc bug]. 77 | //! 78 | //! [^3]: Intra-doc links to methods are not supported in cargo-sync-rdme yet due to [rustdoc bug]. 79 | //! 80 | //! [^4]: Intra-doc links to associated constants or associated types are not supported in cargo-sync-rdme yet due to [rustdoc bug]. 81 | //! 82 | //! [rustdoc bug]: https://github.com/rust-lang/rust/issues/101687 83 | //! 84 | //! ### Code Block 85 | //! 86 | //! Fenced code block: 87 | //! 88 | //! ``` 89 | //! # fn main() { 90 | //! println!("Hello, world!"); 91 | //! # } 92 | //! ``` 93 | //! 94 | //! Indented code blcok: 95 | //! 96 | //! # fn main() { 97 | //! println!("Hello, world!"); 98 | //! # } 99 | 100 | #[cfg(doc)] 101 | use num::Num as _; 102 | 103 | /// This is a module. 104 | pub mod module {} 105 | 106 | /// This is a struct. 107 | pub struct Struct { 108 | /// This is a struct field. 109 | pub field: usize, 110 | } 111 | 112 | /// This is union. 113 | pub union Union { 114 | pub x: u32, 115 | pub y: i32, 116 | } 117 | 118 | /// This is an enum. 119 | pub enum Enum { 120 | /// This is an enum variant. 121 | Variant, 122 | } 123 | 124 | /// This is a function. 125 | pub fn function() {} 126 | 127 | /// This is a type definition. 128 | pub type Typedef = i32; 129 | 130 | /// This is a constant. 131 | pub const CONSTANT: &str = "This is a constant."; 132 | 133 | /// This is a trait. 134 | pub trait Trait { 135 | /// This is a trait method. 136 | fn method(&self); 137 | 138 | /// This is an associated constant. 139 | const CONST: &'static str; 140 | 141 | /// This is an associated type. 142 | type Type: Trait; 143 | } 144 | 145 | /// This is an impl. 146 | impl Trait for Struct { 147 | fn method(&self) {} 148 | 149 | const CONST: &'static str = "This is an associated constant."; 150 | 151 | type Type = Struct; 152 | } 153 | 154 | /// This is a static. 155 | pub static STATIC: &str = "This is a static."; 156 | 157 | /// This is a macro. 158 | #[macro_export] 159 | macro_rules! macro_ { 160 | () => {}; 161 | } 162 | -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | pre-release-replacements = [ 2 | {file = "CHANGELOG.md", search = "Unreleased", replace = "{{version}}"}, 3 | {file = "CHANGELOG.md", search = "/commits/HEAD", replace = "/commits/{{tag_name}}", min = 0, max = 1}, 4 | {file = "CHANGELOG.md", search = "\\.\\.\\.HEAD", replace = "...{{tag_name}}", min = 0, max = 1}, 5 | {file = "CHANGELOG.md", search = "ReleaseDate", replace = "{{date}}"}, 6 | {file = "CHANGELOG.md", search = "", replace = "\n\n## [Unreleased] - ReleaseDate", exactly = 1}, 7 | {file = "CHANGELOG.md", search = "", replace = "\n[Unreleased]: https://github.com/gifnksm/cargo-sync-rdme/compare/{{tag_name}}...HEAD", exactly = 1}, 8 | {file = "src/lib.rs", search = "^//! cargo-sync-rdme = \".*\"$", replace = "//! cargo-sync-rdme = \"{{version}}\"", exactly = 1}, 9 | {file = "src/lib.rs", search = "^#!\\[doc\\(html_root_url = \"https://docs.rs/cargo-sync-rdme/.*\"\\)\\]$", replace = "#![doc(html_root_url = \"https://docs.rs/cargo-sync-rdme/{{version}}\")]", exactly = 1}, 10 | ] 11 | pre-release-hook = ["cargo", "xtask", "pre-release"] 12 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | mod args; 2 | 3 | /// Command line interface definition for `cargo-sync-rdme` command. 4 | #[derive(Debug, Clone, Default, clap::Parser)] 5 | #[clap( 6 | name = "cargo sync-rdme", 7 | bin_name = "cargo sync-rdme", 8 | version, 9 | about = "Cargo subcommand to synchronize README with crate documentation." 10 | )] 11 | pub struct App { 12 | #[clap(flatten)] 13 | pub(crate) verbosity: args::Verbosity, 14 | #[clap(flatten)] 15 | pub(crate) workspace: args::WorkspaceArgs, 16 | #[clap(flatten)] 17 | pub(crate) package: args::PackageArgs, 18 | #[clap(flatten)] 19 | pub(crate) feature: args::FeatureArgs, 20 | #[clap(flatten)] 21 | pub(crate) toolchain: args::ToolchainArgs, 22 | #[clap(flatten)] 23 | pub(crate) fix: args::FixArgs, 24 | } 25 | -------------------------------------------------------------------------------- /src/cli/args.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, process::Command}; 2 | 3 | use cargo_metadata::{camino::Utf8Path, Metadata, Package}; 4 | use clap::ArgAction; 5 | use miette::{IntoDiagnostic, WrapErr}; 6 | use tracing::Level; 7 | 8 | use crate::{ 9 | diff, 10 | vcs::{self, Status}, 11 | Result, 12 | }; 13 | 14 | #[derive(Debug, Clone, Copy, Default, clap::Args)] 15 | pub(crate) struct Verbosity { 16 | /// More output per occurrence 17 | #[clap(long, short = 'v', action = ArgAction::Count, global = true)] 18 | verbose: u8, 19 | /// Less output per occurrence 20 | #[clap( 21 | long, 22 | short = 'q', 23 | action = ArgAction::Count, 24 | global = true, 25 | conflicts_with = "verbose" 26 | )] 27 | quiet: u8, 28 | } 29 | 30 | impl From for Option { 31 | fn from(verb: Verbosity) -> Self { 32 | let level = i8::try_from(verb.verbose).unwrap_or(i8::MAX) 33 | - i8::try_from(verb.quiet).unwrap_or(i8::MAX); 34 | match level { 35 | i8::MIN..=-3 => None, 36 | -2 => Some(Level::ERROR), 37 | -1 => Some(Level::WARN), 38 | 0 => Some(Level::INFO), 39 | 1 => Some(Level::DEBUG), 40 | 2..=i8::MAX => Some(Level::TRACE), 41 | } 42 | } 43 | } 44 | 45 | #[derive(Debug, Clone, Default, clap::Args)] 46 | pub(crate) struct WorkspaceArgs { 47 | /// Path to Cargo.toml 48 | #[clap(long, value_name = "PATH")] 49 | manifest_path: Option, 50 | } 51 | 52 | impl WorkspaceArgs { 53 | pub(crate) fn metadata(&self) -> Result { 54 | let mut cmd = cargo_metadata::MetadataCommand::new(); 55 | if let Some(path) = &self.manifest_path { 56 | cmd.manifest_path(path); 57 | } 58 | let workspace = cmd 59 | .exec() 60 | .into_diagnostic() 61 | .wrap_err("failed to get package metadata")?; 62 | Ok(workspace) 63 | } 64 | } 65 | 66 | #[derive(Debug, Clone, Default, clap::Args)] 67 | pub(crate) struct PackageArgs { 68 | /// Sync READMEs for all packages in the workspace 69 | #[clap(long)] 70 | workspace: bool, 71 | 72 | /// Package to sync README 73 | #[clap(long, short, value_name = "SPEC")] 74 | package: Option>, 75 | } 76 | 77 | impl PackageArgs { 78 | pub(crate) fn packages<'a>(&self, workspace: &'a Metadata) -> Result> { 79 | if self.workspace { 80 | return Ok(workspace.workspace_packages()); 81 | } 82 | 83 | if let Some(names) = &self.package { 84 | let packages = names 85 | .iter() 86 | .map(|name| { 87 | workspace 88 | .packages 89 | .iter() 90 | .find(|pkg| pkg.name == *name) 91 | .ok_or_else(|| miette!("package not found: {name}")) 92 | }) 93 | .collect(); 94 | return packages; 95 | } 96 | 97 | let package = workspace 98 | .root_package() 99 | .ok_or_else(|| miette!("no root package found"))?; 100 | Ok(vec![package]) 101 | } 102 | } 103 | 104 | #[derive(Debug, Clone, Default, clap::Args)] 105 | pub(crate) struct FeatureArgs { 106 | /// Space or comma separated list of features to activate 107 | #[clap(long, short = 'F', value_name = "FEATURES")] 108 | features: Vec, 109 | 110 | /// Activate all available features 111 | #[clap(long)] 112 | all_features: bool, 113 | 114 | /// Do not activate the `default` feature 115 | #[clap(long)] 116 | no_default_features: bool, 117 | } 118 | 119 | impl FeatureArgs { 120 | pub(crate) fn cargo_args(&self) -> impl Iterator + '_ { 121 | self.all_features 122 | .then_some("--all-features") 123 | .into_iter() 124 | .chain(self.features.iter().flat_map(|f| ["--feature", f])) 125 | .chain(self.no_default_features.then_some("--no-default-features")) 126 | } 127 | } 128 | 129 | #[derive(Debug, Clone, Default, clap::Args)] 130 | pub(crate) struct ToolchainArgs { 131 | /// Toolchain name to run `cargo rustdoc` with 132 | #[clap(long)] 133 | toolchain: Option, 134 | } 135 | 136 | impl ToolchainArgs { 137 | pub(crate) fn cargo_command(&self) -> Command { 138 | if let Some(toolchain) = &self.toolchain { 139 | // rustup run toolchain cargo ... 140 | // cargo +nightly ...` fails on windows, so use rustup instead 141 | // https://github.com/rust-lang/rustup/issues/3036 142 | let mut command = Command::new("rustup"); 143 | command.args(["run", toolchain, "cargo"]); 144 | command 145 | } else { 146 | Command::new("cargo") 147 | } 148 | } 149 | } 150 | 151 | #[derive(Debug, Clone, Default, clap::Args)] 152 | pub(crate) struct FixArgs { 153 | /// Check if READMEs are synced 154 | #[clap(long)] 155 | check: bool, 156 | /// Sync README even if a VCS was not detected 157 | #[clap(long)] 158 | allow_no_vcs: bool, 159 | /// Sync README even if the target file is dirty 160 | #[clap(long)] 161 | allow_dirty: bool, 162 | /// Sync README even if the target file has staged changes 163 | #[clap(long)] 164 | allow_staged: bool, 165 | } 166 | 167 | impl FixArgs { 168 | pub(crate) fn check_update_allowed( 169 | &self, 170 | readme_path: impl AsRef, 171 | old_text: &str, 172 | new_text: &str, 173 | ) -> Result<()> { 174 | let readme_path = readme_path.as_ref(); 175 | 176 | if self.check { 177 | bail!( 178 | "README is not synced: {readme_path}\n{}", 179 | diff::diff(old_text, new_text) 180 | ); 181 | } 182 | 183 | if self.allow_no_vcs { 184 | return Ok(()); 185 | } 186 | 187 | let vcs = vcs::discover(readme_path) 188 | .wrap_err_with(|| format!("failed to detect VCS for README: {readme_path}"))? 189 | .ok_or_else(|| miette!("no VSC detected for README: {readme_path}"))?; 190 | 191 | let workdir = vcs 192 | .workdir() 193 | .ok_or_else(|| miette!("VCS workdir not found for README: {readme_path}"))?; 194 | let path_in_repo = readme_path.strip_prefix(workdir).unwrap(); 195 | 196 | let status = vcs 197 | .status_file(path_in_repo) 198 | .wrap_err_with(|| format!("failed to get VCS status for README: {readme_path}"))?; 199 | 200 | match status { 201 | Status::Dirty => { 202 | if !self.allow_dirty { 203 | bail!("README has uncomitted changes: {readme_path}"); 204 | } 205 | } 206 | Status::Staged => { 207 | if !self.allow_dirty && !self.allow_staged { 208 | bail!("README has staged changes: {readme_path}"); 209 | } 210 | } 211 | Status::Clean => {} 212 | } 213 | 214 | Ok(()) 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use miette::{NamedSource, SourceSpan}; 4 | use once_cell::sync::Lazy; 5 | use serde::Deserialize; 6 | use toml::Spanned; 7 | 8 | use crate::with_source::WithSource; 9 | 10 | // To detect items that do not have explicit values, wrap cargo's standard 11 | // configuration items in Options. 12 | 13 | pub(crate) mod badges; 14 | mod de; 15 | pub(crate) mod metadata; 16 | pub(crate) mod package; 17 | #[cfg(test)] 18 | mod tests; 19 | 20 | #[derive(Debug, thiserror::Error, miette::Diagnostic)] 21 | pub(crate) enum GetConfigError { 22 | #[error(transparent)] 23 | #[diagnostic(transparent)] 24 | KeyNotSet(#[from] Box), 25 | } 26 | 27 | impl From for GetConfigError { 28 | fn from(inner: KeyNotSet) -> Self { 29 | Self::KeyNotSet(inner.into()) 30 | } 31 | } 32 | 33 | #[derive(Debug, thiserror::Error, miette::Diagnostic)] 34 | #[error("key `{key}` is not set in {name}")] 35 | pub(crate) struct KeyNotSet { 36 | name: String, 37 | key: String, 38 | #[label] 39 | span: SourceSpan, 40 | #[source_code] 41 | source_code: NamedSource>, 42 | } 43 | 44 | impl GetConfigError { 45 | pub(crate) fn with_key(mut self, key: impl Into) -> Self { 46 | let key = key.into(); 47 | match &mut self { 48 | Self::KeyNotSet(inner) => inner.key = key, 49 | } 50 | self 51 | } 52 | } 53 | 54 | #[derive(Debug, Clone, Default, Deserialize)] 55 | #[serde(rename_all = "kebab-case")] 56 | pub(crate) struct Manifest { 57 | #[serde(default)] 58 | pub(crate) package: Option>, 59 | #[serde(default)] 60 | pub(crate) badges: Option>, 61 | } 62 | 63 | impl WithSource { 64 | pub(crate) fn try_badges( 65 | &self, 66 | ) -> Result>, GetConfigError> { 67 | let badges = self.value().badges.as_ref().ok_or_else(|| KeyNotSet { 68 | name: self.name().to_owned(), 69 | key: "badges".to_owned(), 70 | span: (0..0).into(), 71 | source_code: self.to_named_source(), 72 | })?; 73 | Ok(self.map(|_| badges)) 74 | } 75 | } 76 | 77 | impl Manifest { 78 | pub(crate) fn config(&self) -> &metadata::CargoSyncRdme { 79 | static DEFAULT: Lazy = Lazy::new(Default::default); 80 | (|| { 81 | Some( 82 | &self 83 | .package 84 | .as_ref()? 85 | .get_ref() 86 | .metadata 87 | .as_ref()? 88 | .get_ref() 89 | .cargo_sync_rdme, 90 | ) 91 | })() 92 | .unwrap_or(&DEFAULT) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/config/badges.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use toml::Spanned; 3 | 4 | use super::{GetConfigError, KeyNotSet}; 5 | use crate::with_source::WithSource; 6 | 7 | #[derive(Debug, Clone, Default, Deserialize)] 8 | #[serde(rename_all = "kebab-case")] 9 | pub(crate) struct Badges { 10 | #[serde(default)] 11 | pub(crate) maintenance: Option>, 12 | } 13 | 14 | impl<'a> WithSource<&'a Spanned> { 15 | pub(crate) fn try_maintenance( 16 | &self, 17 | ) -> Result>, GetConfigError> { 18 | let maintenance = self 19 | .value() 20 | .get_ref() 21 | .maintenance 22 | .as_ref() 23 | .ok_or_else(|| KeyNotSet { 24 | name: self.name().to_owned(), 25 | key: "badges.maintenance".to_owned(), 26 | span: self.span(), 27 | source_code: self.to_named_source(), 28 | })?; 29 | Ok(self.map(|_| maintenance)) 30 | } 31 | } 32 | 33 | #[derive(Debug, Clone, Default, Deserialize)] 34 | #[serde(rename_all = "kebab-case")] 35 | pub(crate) struct Maintenance { 36 | #[serde(default)] 37 | pub(crate) status: Option>, 38 | } 39 | 40 | impl<'a> WithSource<&'a Spanned> { 41 | pub(crate) fn try_status( 42 | &self, 43 | ) -> Result>, GetConfigError> { 44 | let status = self 45 | .value() 46 | .get_ref() 47 | .status 48 | .as_ref() 49 | .ok_or_else(|| KeyNotSet { 50 | name: self.name().to_owned(), 51 | key: "badges.maintenance.status".to_owned(), 52 | span: self.span(), 53 | source_code: self.to_named_source(), 54 | })?; 55 | Ok(self.map(|_| status)) 56 | } 57 | } 58 | 59 | #[derive(Debug, Clone, Copy, Default, Deserialize)] 60 | #[serde(rename_all = "kebab-case")] 61 | pub(crate) enum MaintenanceStatus { 62 | ActivelyDeveloped, 63 | PassivelyMaintained, 64 | AsIs, 65 | Experimental, 66 | LookingForMaintainer, 67 | Deprecated, 68 | #[default] 69 | None, 70 | } 71 | 72 | impl MaintenanceStatus { 73 | pub(crate) fn as_str(&self) -> &'static str { 74 | match self { 75 | Self::ActivelyDeveloped => "actively-developed", 76 | Self::PassivelyMaintained => "passively-maintained", 77 | Self::AsIs => "as-is", 78 | Self::Experimental => "experimental", 79 | Self::LookingForMaintainer => "looking-for-maintainer", 80 | Self::Deprecated => "deprecated", 81 | Self::None => "done", 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/config/de.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, marker::PhantomData, str::FromStr}; 2 | 3 | use serde::{de::Visitor, Deserialize, Deserializer}; 4 | use void::{ResultVoidExt, Void}; 5 | 6 | pub(super) fn bool_or_map<'de, T, D>(deserializer: D) -> Result, D::Error> 7 | where 8 | T: Deserialize<'de> + Default, 9 | D: Deserializer<'de>, 10 | { 11 | struct BoolOrMap(PhantomData); 12 | 13 | impl<'de, T> Visitor<'de> for BoolOrMap 14 | where 15 | T: Deserialize<'de> + Default, 16 | { 17 | type Value = Option; 18 | 19 | fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 20 | formatter.write_str("a boolean or a map") 21 | } 22 | 23 | fn visit_bool(self, v: bool) -> Result 24 | where 25 | E: serde::de::Error, 26 | { 27 | Ok(v.then(T::default)) 28 | } 29 | 30 | fn visit_map(self, map: M) -> Result 31 | where 32 | M: serde::de::MapAccess<'de>, 33 | { 34 | let v = T::deserialize(serde::de::value::MapAccessDeserializer::new(map))?; 35 | Ok(Some(v)) 36 | } 37 | } 38 | 39 | let map = deserializer.deserialize_any(BoolOrMap(PhantomData))?; 40 | Ok(map) 41 | } 42 | 43 | pub(super) fn string_or_seq<'de, D>(deserializer: D) -> Result, D::Error> 44 | where 45 | D: Deserializer<'de>, 46 | { 47 | struct StringOrSeq; 48 | 49 | impl<'de> Visitor<'de> for StringOrSeq { 50 | type Value = Vec; 51 | 52 | fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 53 | formatter.write_str("a string or a seq") 54 | } 55 | 56 | fn visit_seq(self, mut seq: A) -> Result 57 | where 58 | A: serde::de::SeqAccess<'de>, 59 | { 60 | let mut values = vec![]; 61 | while let Some(value) = seq.next_element::()? { 62 | values.push(value); 63 | } 64 | Ok(values) 65 | } 66 | 67 | fn visit_str(self, v: &str) -> Result 68 | where 69 | E: serde::de::Error, 70 | { 71 | Ok(vec![v.to_owned()]) 72 | } 73 | } 74 | 75 | let seq = deserializer.deserialize_any(StringOrSeq)?; 76 | Ok(seq) 77 | } 78 | 79 | pub(super) fn string_or_map_or_seq<'de, T, D>(deserializer: D) -> Result, D::Error> 80 | where 81 | T: Deserialize<'de> + FromStr, 82 | D: Deserializer<'de>, 83 | { 84 | struct StringOrMapOrSeq(PhantomData); 85 | 86 | impl<'de, T> Visitor<'de> for StringOrMapOrSeq 87 | where 88 | T: Deserialize<'de> + FromStr, 89 | { 90 | type Value = Vec; 91 | 92 | fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 93 | formatter.write_str("a string or a map or a seq") 94 | } 95 | 96 | fn visit_seq(self, mut seq: A) -> Result 97 | where 98 | A: serde::de::SeqAccess<'de>, 99 | { 100 | struct StringOrMap(T); 101 | impl<'de, T> Deserialize<'de> for StringOrMap 102 | where 103 | T: Deserialize<'de> + FromStr, 104 | { 105 | fn deserialize(deserializer: D) -> Result 106 | where 107 | D: Deserializer<'de>, 108 | { 109 | string_or_map(deserializer).map(Self) 110 | } 111 | } 112 | 113 | let mut values = vec![]; 114 | while let Some(value) = seq.next_element::>()? { 115 | values.push(value.0); 116 | } 117 | Ok(values) 118 | } 119 | 120 | fn visit_str(self, v: &str) -> Result 121 | where 122 | E: serde::de::Error, 123 | { 124 | Ok(vec![v.parse().void_unwrap()]) 125 | } 126 | 127 | fn visit_map(self, map: M) -> Result 128 | where 129 | M: serde::de::MapAccess<'de>, 130 | { 131 | let v = T::deserialize(serde::de::value::MapAccessDeserializer::new(map))?; 132 | Ok(vec![v]) 133 | } 134 | } 135 | 136 | let map = deserializer.deserialize_any(StringOrMapOrSeq(PhantomData))?; 137 | Ok(map) 138 | } 139 | 140 | pub(super) fn string_or_map<'de, T, D>(deserializer: D) -> Result 141 | where 142 | T: Deserialize<'de> + FromStr, 143 | D: Deserializer<'de>, 144 | { 145 | struct StringOrMap(PhantomData); 146 | 147 | impl<'de, T> Visitor<'de> for StringOrMap 148 | where 149 | T: Deserialize<'de> + FromStr, 150 | { 151 | type Value = T; 152 | 153 | fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 154 | formatter.write_str("a string or a map") 155 | } 156 | 157 | fn visit_str(self, v: &str) -> Result 158 | where 159 | E: serde::de::Error, 160 | { 161 | Ok(v.parse().void_unwrap()) 162 | } 163 | 164 | fn visit_map(self, map: M) -> Result 165 | where 166 | M: serde::de::MapAccess<'de>, 167 | { 168 | let v = T::deserialize(serde::de::value::MapAccessDeserializer::new(map))?; 169 | Ok(v) 170 | } 171 | } 172 | 173 | let map = deserializer.deserialize_any(StringOrMap(PhantomData))?; 174 | Ok(map) 175 | } 176 | -------------------------------------------------------------------------------- /src/config/metadata.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, fmt, str::FromStr, sync::Arc}; 2 | 3 | use serde::{ 4 | de::{Error, Visitor}, 5 | Deserialize, 6 | }; 7 | use void::Void; 8 | 9 | use super::de; 10 | 11 | #[derive(Debug, Clone, Default, Deserialize)] 12 | #[serde(rename_all = "kebab-case")] 13 | pub(crate) struct Metadata { 14 | #[serde(default)] 15 | pub(crate) cargo_sync_rdme: CargoSyncRdme, 16 | } 17 | 18 | #[derive(Debug, Clone, Default, Deserialize)] 19 | #[serde(deny_unknown_fields, rename_all = "kebab-case")] 20 | pub(crate) struct CargoSyncRdme { 21 | #[serde(default, deserialize_with = "de::string_or_seq")] 22 | pub(crate) extra_targets: Vec, 23 | #[serde(default)] 24 | pub(crate) badge: Badge, 25 | #[serde(default)] 26 | pub(crate) rustdoc: Rustdoc, 27 | } 28 | 29 | #[derive(Debug, Clone, Default)] 30 | pub(crate) struct Badge { 31 | pub(crate) style: Option, 32 | pub(crate) badges: HashMap, Arc<[BadgeItem]>>, 33 | } 34 | 35 | #[derive(Debug, Clone, Default, Deserialize)] 36 | #[serde(deny_unknown_fields, rename_all = "kebab-case")] 37 | pub(crate) enum BadgeStyle { 38 | #[default] 39 | Plastic, 40 | Flat, 41 | FlatSquare, 42 | ForTheBadge, 43 | Social, 44 | } 45 | 46 | impl BadgeStyle { 47 | pub(crate) fn as_str(&self) -> &'static str { 48 | match self { 49 | Self::Plastic => "plastic", 50 | Self::Flat => "flat", 51 | Self::FlatSquare => "flat-square", 52 | Self::ForTheBadge => "for-the-badge", 53 | Self::Social => "social", 54 | } 55 | } 56 | } 57 | 58 | #[derive(Debug, Clone, PartialEq, Eq)] 59 | pub(crate) enum BadgeItem { 60 | Maintenance, 61 | License(License), 62 | CratesIo, 63 | DocsRs, 64 | RustVersion, 65 | GithubActions(GithubActions), 66 | Codecov, 67 | } 68 | 69 | #[derive(Debug, Clone)] 70 | enum BadgeKind { 71 | Maintenance, 72 | License, 73 | CratesIo, 74 | DocsRs, 75 | RustVersion, 76 | GithubActions, 77 | Codecov, 78 | } 79 | 80 | impl FromStr for BadgeKind { 81 | type Err = (); 82 | 83 | fn from_str(s: &str) -> Result { 84 | let kind = match s { 85 | "maintenance" => Self::Maintenance, 86 | "license" => Self::License, 87 | "crates-io" => Self::CratesIo, 88 | "docs-rs" => Self::DocsRs, 89 | "rust-version" => Self::RustVersion, 90 | "github-actions" => Self::GithubActions, 91 | "codecov" => Self::Codecov, 92 | _ => { 93 | if s.starts_with("maintenance-") { 94 | Self::Maintenance 95 | } else if s.starts_with("license-") { 96 | Self::License 97 | } else if s.starts_with("crates-io-") { 98 | Self::CratesIo 99 | } else if s.starts_with("docs-rs-") { 100 | Self::DocsRs 101 | } else if s.starts_with("rust-version-") { 102 | Self::RustVersion 103 | } else if s.starts_with("github-actions-") { 104 | Self::GithubActions 105 | } else if s.starts_with("codecov-") { 106 | Self::Codecov 107 | } else { 108 | return Err(()); 109 | } 110 | } 111 | }; 112 | Ok(kind) 113 | } 114 | } 115 | 116 | impl BadgeKind { 117 | fn expecting() -> &'static [&'static str] { 118 | &[ 119 | "maintenance", 120 | "license", 121 | "crates-io", 122 | "docs-rs", 123 | "rust-version", 124 | "github-actions", 125 | "codecov", 126 | "maintenance-*", 127 | "license-*", 128 | "crates-io-*", 129 | "docs-rs-*", 130 | "rust-version-*", 131 | "github-actions-*", 132 | "codecov-*", 133 | ] 134 | } 135 | } 136 | 137 | impl<'de> Deserialize<'de> for Badge { 138 | fn deserialize(deserializer: D) -> Result 139 | where 140 | D: serde::Deserializer<'de>, 141 | { 142 | fn deserialize_badge_list<'de, D>(deserializer: D) -> Result, D::Error> 143 | where 144 | D: serde::Deserializer<'de>, 145 | { 146 | struct BadgeList; 147 | 148 | impl<'de> Visitor<'de> for BadgeList { 149 | type Value = Arc<[BadgeItem]>; 150 | 151 | fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 152 | formatter.write_str("map") 153 | } 154 | 155 | fn visit_map(self, mut map: M) -> Result 156 | where 157 | M: serde::de::MapAccess<'de>, 158 | { 159 | let mut data = vec![]; 160 | while let Some(key) = map.next_key::<&str>()? { 161 | let kind = BadgeKind::from_str(key) 162 | .map_err(|_| M::Error::unknown_variant(key, BadgeKind::expecting()))?; 163 | #[derive(Deserialize)] 164 | #[serde(bound = "T: Default + Deserialize<'de>")] 165 | struct Wrap(#[serde(deserialize_with = "de::bool_or_map")] Option); 166 | 167 | match kind { 168 | BadgeKind::Maintenance => { 169 | if map.next_value::()? { 170 | data.push(BadgeItem::Maintenance); 171 | } 172 | } 173 | BadgeKind::License => { 174 | if let Wrap(Some(license)) = map.next_value::>()? { 175 | data.push(BadgeItem::License(license)); 176 | } 177 | } 178 | BadgeKind::CratesIo => { 179 | if map.next_value::()? { 180 | data.push(BadgeItem::CratesIo); 181 | } 182 | } 183 | BadgeKind::DocsRs => { 184 | if map.next_value::()? { 185 | data.push(BadgeItem::DocsRs); 186 | } 187 | } 188 | BadgeKind::RustVersion => { 189 | if map.next_value::()? { 190 | data.push(BadgeItem::RustVersion); 191 | } 192 | } 193 | BadgeKind::GithubActions => { 194 | if let Wrap(Some(github_actions)) = 195 | map.next_value::>()? 196 | { 197 | data.push(BadgeItem::GithubActions(github_actions)); 198 | } 199 | } 200 | BadgeKind::Codecov => { 201 | if map.next_value::()? { 202 | data.push(BadgeItem::Codecov); 203 | } 204 | } 205 | } 206 | } 207 | Ok(data.into()) 208 | } 209 | } 210 | 211 | deserializer.deserialize_any(BadgeList) 212 | } 213 | 214 | struct Badges; 215 | impl<'de> Visitor<'de> for Badges { 216 | type Value = Badge; 217 | 218 | fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 219 | formatter.write_str("map") 220 | } 221 | 222 | fn visit_map(self, mut map: M) -> Result 223 | where 224 | M: serde::de::MapAccess<'de>, 225 | { 226 | #[derive(Deserialize)] 227 | struct BadgeList( 228 | #[serde(deserialize_with = "deserialize_badge_list")] Arc<[BadgeItem]>, 229 | ); 230 | 231 | let mut data = Badge::default(); 232 | 233 | while let Some(key) = map.next_key::()? { 234 | let expected = &["badges", "badges-*", "style"]; 235 | match key.as_str() { 236 | "style" => { 237 | data.style = map.next_value()?; 238 | } 239 | _ => { 240 | let key = if key == "badges" { 241 | String::new() 242 | } else if let Some(rest) = key.strip_prefix("badges-") { 243 | rest.to_owned() 244 | } else { 245 | return Err(M::Error::unknown_field(&key, expected)); 246 | }; 247 | let value = map.next_value::()?; 248 | data.badges.entry(key.into()).or_insert(value.0); 249 | } 250 | } 251 | } 252 | 253 | Ok(data) 254 | } 255 | } 256 | 257 | deserializer.deserialize_any(Badges) 258 | } 259 | } 260 | 261 | #[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)] 262 | pub(crate) struct License { 263 | #[serde(default)] 264 | pub(crate) link: Option, 265 | } 266 | 267 | #[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)] 268 | #[serde(deny_unknown_fields, rename_all = "kebab-case")] 269 | pub(crate) struct GithubActions { 270 | #[serde(default, deserialize_with = "de::string_or_map_or_seq")] 271 | pub(crate) workflows: Vec, 272 | } 273 | 274 | #[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)] 275 | #[serde(deny_unknown_fields, rename_all = "kebab-case")] 276 | pub(crate) struct GithubActionsWorkflow { 277 | #[serde(default)] 278 | pub(crate) name: Option, 279 | pub(crate) file: String, 280 | } 281 | 282 | impl FromStr for GithubActionsWorkflow { 283 | type Err = Void; 284 | 285 | fn from_str(s: &str) -> Result { 286 | Ok(Self { 287 | name: None, 288 | file: s.to_string(), 289 | }) 290 | } 291 | } 292 | 293 | #[derive(Debug, Clone, Default, Deserialize)] 294 | #[serde(deny_unknown_fields, rename_all = "kebab-case")] 295 | pub(crate) struct Rustdoc { 296 | #[serde(default)] 297 | pub(crate) html_root_url: Option, 298 | } 299 | -------------------------------------------------------------------------------- /src/config/package.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use toml::Spanned; 3 | 4 | use super::metadata; 5 | 6 | #[derive(Debug, Clone, Default, Deserialize)] 7 | #[serde(rename_all = "kebab-case")] 8 | pub(crate) struct Package { 9 | #[serde(default)] 10 | pub(crate) metadata: Option>, 11 | } 12 | -------------------------------------------------------------------------------- /src/config/tests.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use indoc::indoc; 4 | 5 | use crate::config::metadata::{BadgeItem, GithubActions, GithubActionsWorkflow, License}; 6 | 7 | use super::*; 8 | 9 | fn get_badges(manifest: Manifest) -> Arc<[BadgeItem]> { 10 | let badges = &manifest 11 | .package 12 | .unwrap() 13 | .into_inner() 14 | .metadata 15 | .unwrap() 16 | .into_inner() 17 | .cargo_sync_rdme 18 | .badge 19 | .badges[""]; 20 | Arc::clone(badges) 21 | } 22 | 23 | #[test] 24 | fn test_badges_order() { 25 | let input = indoc! {r#" 26 | [package.metadata.cargo-sync-rdme.badge.badges] 27 | license = true 28 | maintenance = true 29 | github-actions = false 30 | crates-io = true 31 | codecov = true 32 | docs-rs = false 33 | rust-version = true 34 | "#}; 35 | let badges = get_badges(toml::from_str(input).unwrap()); 36 | assert!(matches!( 37 | *badges, 38 | [ 39 | BadgeItem::License(_), 40 | BadgeItem::Maintenance, 41 | BadgeItem::CratesIo, 42 | BadgeItem::Codecov, 43 | BadgeItem::RustVersion 44 | ] 45 | )); 46 | } 47 | 48 | #[test] 49 | fn test_duplicated_badges() { 50 | let input = indoc! {r#" 51 | [package.metadata.cargo-sync-rdme.badge.badges] 52 | license = true 53 | license-x = true 54 | maintenance = true 55 | license-z = true 56 | "#}; 57 | let badges = get_badges(toml::from_str(input).unwrap()); 58 | assert!(matches!( 59 | *badges, 60 | [ 61 | BadgeItem::License(_), 62 | BadgeItem::License(_), 63 | BadgeItem::Maintenance, 64 | BadgeItem::License(_), 65 | ] 66 | )); 67 | } 68 | 69 | #[test] 70 | fn test_license() { 71 | let input = indoc! {r#" 72 | [package.metadata.cargo-sync-rdme.badge.badges] 73 | license = true 74 | "#}; 75 | let badges = get_badges(toml::from_str(input).unwrap()); 76 | assert!(matches!( 77 | &*badges, 78 | [BadgeItem::License(License { link: None })] 79 | )); 80 | 81 | let input = indoc! {r#" 82 | [package.metadata.cargo-sync-rdme.badge.badges] 83 | license = false 84 | "#}; 85 | let badges = get_badges(toml::from_str(input).unwrap()); 86 | assert!(matches!(&*badges, [])); 87 | 88 | let input = indoc! {r#" 89 | [package.metadata.cargo-sync-rdme.badge.badges] 90 | license = {} 91 | "#}; 92 | let badges = get_badges(toml::from_str(input).unwrap()); 93 | assert!(matches!( 94 | &*badges, 95 | [BadgeItem::License(License { link: None })] 96 | )); 97 | 98 | let input = indoc! {r#" 99 | [package.metadata.cargo-sync-rdme.badge.badges] 100 | license = { link = "foo" } 101 | "#}; 102 | let badges = get_badges(toml::from_str(input).unwrap()); 103 | assert!(matches!( 104 | &*badges, 105 | [BadgeItem::License(License { link: Some(link) })] if link == "foo" 106 | )); 107 | } 108 | 109 | #[test] 110 | fn test_github_actions() { 111 | let input = indoc! {r#" 112 | [package.metadata.cargo-sync-rdme.badge.badges] 113 | github-actions = true 114 | "#}; 115 | let badges = get_badges(toml::from_str(input).unwrap()); 116 | assert!(matches!( 117 | &*badges, 118 | [BadgeItem::GithubActions(GithubActions { workflows })] if matches!(workflows.as_slice(), &[]) 119 | )); 120 | 121 | let input = indoc! {r#" 122 | [package.metadata.cargo-sync-rdme.badge.badges] 123 | github-actions = false 124 | "#}; 125 | let badges = get_badges(toml::from_str(input).unwrap()); 126 | assert!(matches!(*badges, [])); 127 | 128 | let input = indoc! {r#" 129 | [package.metadata.cargo-sync-rdme.badge.badges] 130 | github-actions = {} 131 | "#}; 132 | let badges = get_badges(toml::from_str(input).unwrap()); 133 | assert!(matches!( 134 | &*badges, 135 | [BadgeItem::GithubActions(GithubActions { workflows })] if matches!(workflows.as_slice(), &[]) 136 | )); 137 | 138 | let input = indoc! {r#" 139 | [package.metadata.cargo-sync-rdme.badge.badges] 140 | github-actions = { workflows = "foo.yml" } 141 | "#}; 142 | let badges = get_badges(toml::from_str(input).unwrap()); 143 | assert!(matches!( 144 | &*badges, 145 | [BadgeItem::GithubActions(GithubActions { workflows })] 146 | if matches!( 147 | workflows.as_slice(), 148 | [ 149 | GithubActionsWorkflow { name: None, file } 150 | ] if file == "foo.yml" 151 | ) 152 | )); 153 | 154 | let input = indoc! {r#" 155 | [package.metadata.cargo-sync-rdme.badge.badges] 156 | github-actions = { workflows = { file = "foo.yml" } } 157 | "#}; 158 | let badges = get_badges(toml::from_str(input).unwrap()); 159 | assert!(matches!( 160 | &*badges, 161 | [BadgeItem::GithubActions(GithubActions { workflows })] 162 | if matches!( 163 | workflows.as_slice(), 164 | [ 165 | GithubActionsWorkflow { name: None, file } 166 | ] if file == "foo.yml" 167 | ) 168 | )); 169 | 170 | let input = indoc! {r#" 171 | [package.metadata.cargo-sync-rdme.badge.badges] 172 | github-actions = { workflows = [ "foo.yml", {file = "bar.yml"} ] } 173 | "#}; 174 | let badges = get_badges(toml::from_str(input).unwrap()); 175 | assert!(matches!( 176 | &*badges, 177 | [BadgeItem::GithubActions(GithubActions { workflows })] 178 | if matches!( 179 | &workflows.as_slice(), &[ 180 | GithubActionsWorkflow { name: None, file: file1 }, 181 | GithubActionsWorkflow { name: None, file: file2 } 182 | ] if file1 == "foo.yml" && file2 == "bar.yml") 183 | )); 184 | } 185 | -------------------------------------------------------------------------------- /src/diff.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Write}; 2 | 3 | use console::{style, Style}; 4 | use similar::{ChangeTag, TextDiff}; 5 | 6 | #[derive(Debug)] 7 | struct Line(Option); 8 | 9 | impl fmt::Display for Line { 10 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 11 | match self.0 { 12 | None => write!(f, " "), 13 | Some(idx) => write!(f, "{:>4}", idx + 1), 14 | } 15 | } 16 | } 17 | 18 | pub(crate) fn diff(old: &str, new: &str) -> String { 19 | let diff = TextDiff::from_lines(old, new); 20 | let mut output = String::new(); 21 | 22 | for (idx, group) in diff.grouped_ops(3).iter().enumerate() { 23 | if idx > 0 { 24 | writeln!(&mut output, "{0:─^1$}┼{0:─^2$}", "─", 9, 120).unwrap(); 25 | } 26 | for op in group { 27 | for change in diff.iter_inline_changes(op) { 28 | let (sign, s) = match change.tag() { 29 | ChangeTag::Delete => ("-", Style::new().red()), 30 | ChangeTag::Insert => ("+", Style::new().green()), 31 | ChangeTag::Equal => (" ", Style::new().dim()), 32 | }; 33 | write!( 34 | &mut output, 35 | "{}{} │{}", 36 | style(Line(change.old_index())).dim(), 37 | style(Line(change.new_index())).dim(), 38 | s.apply_to(sign).bold(), 39 | ) 40 | .unwrap(); 41 | for (emphasized, value) in change.iter_strings_lossy() { 42 | if emphasized { 43 | write!(&mut output, "{}", s.apply_to(value).underlined().on_black()) 44 | .unwrap(); 45 | } else { 46 | write!(&mut output, "{}", s.apply_to(value)).unwrap(); 47 | } 48 | } 49 | if change.missing_newline() { 50 | writeln!(&mut output).unwrap(); 51 | } 52 | } 53 | } 54 | } 55 | 56 | output 57 | } 58 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Cargo subcommand to synchronize README with the cargo manifest and crate 2 | //! documentation. 3 | //! 4 | //! See [repository's README] for `cargo-sync-rdme` command usage. 5 | //! 6 | //! # Usage 7 | //! 8 | //! Add this to your `Cargo.toml`: 9 | //! 10 | //! ```toml 11 | //! [dependencies] 12 | //! cargo-sync-rdme = "0.4.2" 13 | //! ``` 14 | //! 15 | //! [repository's README]: https://github.com/gifnksm/cargo-sync-rdme/blob/main/README.md 16 | #![doc(html_root_url = "https://docs.rs/cargo-sync-rdme/0.4.2")] 17 | #![warn( 18 | elided_lifetimes_in_paths, 19 | explicit_outlives_requirements, 20 | keyword_idents, 21 | missing_copy_implementations, 22 | missing_debug_implementations, 23 | missing_docs, 24 | single_use_lifetimes, 25 | unreachable_pub, 26 | unused 27 | )] 28 | 29 | use std::{env, io}; 30 | 31 | use clap::Parser; 32 | use tracing::Level; 33 | use tracing_subscriber::EnvFilter; 34 | 35 | #[macro_use] 36 | mod macros; 37 | 38 | mod cli; 39 | mod config; 40 | mod diff; 41 | mod sync; 42 | mod traits; 43 | mod vcs; 44 | mod with_source; 45 | 46 | pub use self::cli::App; 47 | 48 | /// Error type for `cargo-sync-rdme` command. 49 | pub type Error = miette::Error; 50 | 51 | /// Result type for `cargo-sync-rdme` command. 52 | pub type Result = miette::Result; 53 | 54 | /// Entry point of `cargo-sync-rdme` command. 55 | pub fn main() -> Result<()> { 56 | // If this command is run by cargo, the first argument is the subcommand name 57 | // `sync-rdme`. We need to remove it to avoid parsing error. 58 | let args = env::args().enumerate().filter_map(|(idx, arg)| { 59 | if idx == 1 && arg == "sync-rdme" { 60 | None 61 | } else { 62 | Some(arg) 63 | } 64 | }); 65 | let app = App::parse_from(args); 66 | install_logger(app.verbosity.into())?; 67 | 68 | let workspace = app.workspace.metadata()?; 69 | for package in app.package.packages(&workspace)? { 70 | sync::sync_all(&app, &workspace, package)?; 71 | } 72 | 73 | Ok(()) 74 | } 75 | 76 | fn install_logger(verbosity: Option) -> Result<()> { 77 | if env::var_os("RUST_LOG").is_none() { 78 | match verbosity { 79 | Some(Level::ERROR) => env::set_var("RUST_LOG", "error"), 80 | Some(Level::WARN) => env::set_var("RUST_LOG", "warn"), 81 | Some(Level::INFO) => env::set_var("RUST_LOG", "info"), 82 | Some(Level::DEBUG) => env::set_var("RUST_LOG", "debug"), 83 | Some(Level::TRACE) => env::set_var("RUST_LOG", "trace"), 84 | None => env::set_var("RUST_LOG", "off"), 85 | } 86 | } 87 | 88 | tracing_subscriber::fmt() 89 | .with_env_filter(EnvFilter::from_default_env()) 90 | .with_writer(io::stderr) 91 | .with_target(false) 92 | .try_init() 93 | .map_err(|e| miette!(e))?; 94 | 95 | Ok(()) 96 | } 97 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_macros, unused_macro_rules)] 2 | 3 | // workaround for https://github.com/zkat/miette/issues/201 4 | macro_rules! bail { 5 | ($msg:literal $(,)?) => { 6 | { 7 | return miette::private::Err(miette!($msg)); 8 | } 9 | }; 10 | ($err:expr $(,)?) => { 11 | { 12 | return miette::private::Err(miette!($err)); 13 | } 14 | }; 15 | ($fmt:expr, $($arg:tt)*) => { 16 | { 17 | return miette::private::Err(miette!($fmt, $($arg)*)); 18 | } 19 | }; 20 | } 21 | 22 | // workaround for https://github.com/zkat/miette/issues/201 23 | macro_rules! ensure { 24 | ($cond:expr, $msg:literal $(,)?) => { 25 | if !$cond { 26 | return miette::private::Err($crate::miette!($msg)); 27 | } 28 | }; 29 | ($cond:expr, $err:expr $(,)?) => { 30 | if !$cond { 31 | return miette::private::Err($crate::miette!($err)); 32 | } 33 | }; 34 | ($cond:expr, $fmt:expr, $($arg:tt)*) => { 35 | if !$cond { 36 | return miette::private::Err($crate::miette!($fmt, $($arg)*)); 37 | } 38 | }; 39 | } 40 | 41 | // workaround for https://github.com/zkat/miette/issues/201 42 | macro_rules! miette { 43 | ($msg:literal $(,)?) => { 44 | miette::private::new_adhoc(format!($msg)) 45 | }; 46 | ($err:expr $(,)?) => ({ 47 | use miette::private::kind::*; 48 | let error = $err; 49 | (&error).miette_kind().new(error) 50 | }); 51 | ($fmt:expr, $($arg:tt)*) => { 52 | miette::private::new_adhoc(format!($fmt, $($arg)*)) 53 | }; 54 | } 55 | 56 | /// try for iterator implementation 57 | macro_rules! itry { 58 | ($e:expr) => { 59 | match $e { 60 | Ok(v) => v, 61 | Err(e) => { 62 | return Some(Err(e)); 63 | } 64 | } 65 | }; 66 | } 67 | 68 | macro_rules! opt_try { 69 | ($e:expr) => { 70 | match $e { 71 | Some(v) => v, 72 | None => { 73 | return Ok(None); 74 | } 75 | } 76 | }; 77 | } 78 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() -> cargo_sync_rdme::Result<()> { 2 | cargo_sync_rdme::main() 3 | } 4 | -------------------------------------------------------------------------------- /src/sync.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs, 3 | io::{self, Write}, 4 | sync::Arc, 5 | }; 6 | 7 | use cargo_metadata::{ 8 | camino::{Utf8Path, Utf8PathBuf}, 9 | Metadata, Package, 10 | }; 11 | use miette::{IntoDiagnostic, NamedSource, WrapErr}; 12 | use pulldown_cmark::{Options, Parser}; 13 | use tempfile::NamedTempFile; 14 | 15 | use crate::{cli::App, config::Manifest, traits::PackageExt, with_source::WithSource, Result}; 16 | 17 | mod contents; 18 | mod marker; 19 | 20 | #[derive(Debug, Clone)] 21 | struct MarkdownFile { 22 | path: Utf8PathBuf, 23 | text: Arc, 24 | } 25 | 26 | impl MarkdownFile { 27 | fn new(package: &Package, path: &Utf8Path) -> Result { 28 | let path = package.root_directory().join(path); 29 | let text = fs::read_to_string(&path) 30 | .into_diagnostic() 31 | .wrap_err_with(|| { 32 | format!( 33 | "failed to read README of {package}: {path}", 34 | package = package.name 35 | ) 36 | })? 37 | .into(); 38 | Ok(Self { path, text }) 39 | } 40 | 41 | fn to_named_source(&self) -> NamedSource> { 42 | NamedSource::new(self.path.clone(), Arc::clone(&self.text)) 43 | } 44 | } 45 | 46 | type ManifestFile = WithSource; 47 | 48 | pub(crate) fn sync_all(app: &App, workspace: &Metadata, package: &Package) -> Result<()> { 49 | let manifest = ManifestFile::from_toml("package manifest", &package.manifest_path)?; 50 | let _span = tracing::info_span!("sync", "{}", package.name).entered(); 51 | 52 | let paths = package 53 | .readme 54 | .as_deref() 55 | .into_iter() 56 | .chain( 57 | manifest 58 | .value() 59 | .config() 60 | .extra_targets 61 | .iter() 62 | .map(Utf8Path::new), 63 | ) 64 | .collect::>(); 65 | 66 | if paths.is_empty() { 67 | bail!("no target files found. Please specify `package.readme` or `package.metadata.cargo-sync-rdme.extra-targets`"); 68 | } 69 | 70 | for path in paths { 71 | tracing::info!("syncing {path}..."); 72 | 73 | let markdown = MarkdownFile::new(package, path)?; 74 | 75 | // Setup markdown parser 76 | let parser = Parser::new_ext(&markdown.text, Options::all()).into_offset_iter(); 77 | 78 | // Find replace markers from markdown file 79 | let all_markers = marker::find_all(&markdown, &manifest, parser)?; 80 | 81 | // Create contents for each marker 82 | let replaces = all_markers.iter().map(|x| x.0.clone()); 83 | let all_contents = contents::create_all(replaces, app, &manifest, workspace, package)?; 84 | 85 | // Replace markers with content 86 | let new_text = marker::replace_all(&markdown.text, &all_markers, &all_contents); 87 | 88 | // Compare new markdown file with old one 89 | let changed = new_text.as_str() != &*markdown.text; 90 | if !changed { 91 | tracing::info!("already up-to-date {path}"); 92 | continue; 93 | } 94 | 95 | // Update README if allowed 96 | app.fix 97 | .check_update_allowed(&markdown.path, &markdown.text, &new_text)?; 98 | write_readme(&markdown.path, &new_text) 99 | .into_diagnostic() 100 | .wrap_err_with(|| format!("failed to write markdown file: {path}"))?; 101 | 102 | tracing::info!("updated {path}"); 103 | } 104 | 105 | Ok(()) 106 | } 107 | 108 | pub(crate) fn write_readme(path: &Utf8Path, text: &str) -> io::Result<()> { 109 | let output_dir = path.parent().unwrap(); 110 | let mut tempfile = NamedTempFile::new_in(output_dir)?; 111 | tempfile.as_file_mut().write_all(text.as_bytes())?; 112 | tempfile.as_file_mut().sync_data()?; 113 | let file = tempfile.persist(path).map_err(|err| err.error)?; 114 | file.sync_all()?; 115 | drop(file); 116 | Ok(()) 117 | } 118 | -------------------------------------------------------------------------------- /src/sync/contents.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use cargo_metadata::{Metadata, Package}; 4 | 5 | use crate::App; 6 | 7 | use super::{marker::Replace, ManifestFile}; 8 | 9 | mod badge; 10 | mod rustdoc; 11 | mod title; 12 | 13 | pub(super) fn create_all( 14 | replaces: impl IntoIterator, 15 | app: &App, 16 | manifest: &ManifestFile, 17 | workspace: &Metadata, 18 | package: &Package, 19 | ) -> Result, CreateAllContentsError> { 20 | let mut contents = vec![]; 21 | let mut errors = vec![]; 22 | for replace in replaces { 23 | let res = replace.create_content(app, manifest, workspace, package); 24 | match res { 25 | Ok(c) => contents.push(c), 26 | Err(err) => errors.push(err), 27 | } 28 | } 29 | 30 | if !errors.is_empty() { 31 | return Err(CreateAllContentsError { errors }); 32 | } 33 | 34 | Ok(contents) 35 | } 36 | 37 | #[derive(Debug, thiserror::Error, miette::Diagnostic)] 38 | #[error("failed to create contents of README")] 39 | pub(super) struct CreateAllContentsError { 40 | #[related] 41 | errors: Vec, 42 | } 43 | 44 | #[derive(Debug, thiserror::Error, miette::Diagnostic)] 45 | pub(super) enum CreateContentsError { 46 | #[error(transparent)] 47 | #[diagnostic(transparent)] 48 | CreateBadge(#[from] badge::CreateAllBadgesError), 49 | #[error(transparent)] 50 | #[diagnostic(transparent)] 51 | CreateRustdoc(#[from] rustdoc::CreateRustdocError), 52 | } 53 | 54 | #[derive(Debug, Clone)] 55 | pub(super) struct Contents { 56 | text: String, 57 | } 58 | 59 | impl Replace { 60 | fn create_content( 61 | self, 62 | app: &App, 63 | manifest: &ManifestFile, 64 | workspace: &Metadata, 65 | package: &Package, 66 | ) -> Result { 67 | let text = match self { 68 | Replace::Title => title::create(package), 69 | Replace::Badge { name: _, badges } => { 70 | badge::create_all(badges, manifest, workspace, package)? 71 | } 72 | Replace::Rustdoc => rustdoc::create(app, manifest, workspace, package)?, 73 | }; 74 | 75 | assert!(text.is_empty() || text.ends_with('\n')); 76 | 77 | Ok(Contents { text }) 78 | } 79 | } 80 | 81 | impl Contents { 82 | pub(super) fn text(&self) -> &str { 83 | &self.text 84 | } 85 | } 86 | 87 | struct Escape<'s>(&'s str, &'s [char]); 88 | 89 | impl fmt::Display for Escape<'_> { 90 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 91 | let mut s = self.0; 92 | while let Some(idx) = s.find(self.1) { 93 | f.write_str(&s[..idx])?; 94 | write!(f, r"\{}", s.as_bytes()[idx] as char)?; 95 | s = &s[idx + 1..]; 96 | } 97 | f.write_str(s)?; 98 | Ok(()) 99 | } 100 | } 101 | 102 | #[cfg(test)] 103 | mod tests { 104 | use super::*; 105 | 106 | #[test] 107 | fn escape() { 108 | let need_escape = [ 109 | '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', 110 | ]; 111 | 112 | assert_eq!(Escape(r"foo", &need_escape).to_string(), r"foo"); 113 | assert_eq!(Escape(r"`foobar", &need_escape).to_string(), r"\`foobar"); 114 | assert_eq!(Escape(r"foo*bar", &need_escape).to_string(), r"foo\*bar"); 115 | assert_eq!(Escape(r"foobar_", &need_escape).to_string(), r"foobar\_"); 116 | assert_eq!( 117 | Escape(r"`foo*bar_", &need_escape).to_string(), 118 | r"\`foo\*bar\_" 119 | ); 120 | assert_eq!( 121 | Escape(r"\foo\bar\", &need_escape).to_string(), 122 | r"\\foo\\bar\\" 123 | ); 124 | assert_eq!(Escape(r"*", &need_escape).to_string(), r"\*"); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/sync/contents/badge.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, cmp::Ordering, fmt, fmt::Write, fs, io, sync::Arc}; 2 | 3 | use cargo_metadata::{ 4 | camino::{Utf8Path, Utf8PathBuf}, 5 | semver::{Comparator, Op, VersionReq}, 6 | Metadata, Package, 7 | }; 8 | use miette::{NamedSource, SourceSpan}; 9 | use serde::Deserialize; 10 | use url::Url; 11 | 12 | use super::Escape; 13 | use crate::{ 14 | config::{badges::MaintenanceStatus, metadata, GetConfigError}, 15 | sync::ManifestFile, 16 | }; 17 | 18 | type CreateResult = std::result::Result; 19 | 20 | pub(super) fn create_all( 21 | badges: Arc<[metadata::BadgeItem]>, 22 | manifest: &ManifestFile, 23 | workspace: &Metadata, 24 | package: &Package, 25 | ) -> Result { 26 | let mut output = String::new(); 27 | 28 | let mut errors = vec![]; 29 | 30 | for badge in &*badges { 31 | match BadgeLinkSet::from_config(badge, manifest, workspace, package) { 32 | Ok(BadgeLinkSet::None) => {} 33 | Ok(BadgeLinkSet::One(badge)) => writeln!(&mut output, "{badge}").unwrap(), 34 | Ok(BadgeLinkSet::ManyResult(bs)) => { 35 | for b in bs { 36 | match b { 37 | Ok(b) => writeln!(&mut output, "{b}").unwrap(), 38 | Err(e) => errors.push(e), 39 | } 40 | } 41 | } 42 | Err(err) => errors.push(err), 43 | } 44 | } 45 | 46 | if !errors.is_empty() { 47 | return Err(CreateAllBadgesError { errors }); 48 | } 49 | 50 | Ok(output) 51 | } 52 | 53 | #[derive(Debug)] 54 | enum BadgeLinkSet { 55 | None, 56 | One(BadgeLink), 57 | ManyResult(Vec>), 58 | } 59 | 60 | impl From for BadgeLinkSet { 61 | fn from(badge: BadgeLink) -> Self { 62 | Self::One(badge) 63 | } 64 | } 65 | 66 | impl From> for BadgeLinkSet { 67 | fn from(badge: Option) -> Self { 68 | match badge { 69 | Some(badge) => Self::One(badge), 70 | None => Self::None, 71 | } 72 | } 73 | } 74 | 75 | impl From>> for BadgeLinkSet { 76 | fn from(badges: Vec>) -> Self { 77 | Self::ManyResult(badges) 78 | } 79 | } 80 | 81 | impl BadgeLinkSet { 82 | fn from_config( 83 | config: &metadata::BadgeItem, 84 | manifest: &ManifestFile, 85 | workspace: &Metadata, 86 | package: &Package, 87 | ) -> CreateResult { 88 | Ok(match config { 89 | metadata::BadgeItem::Maintenance => BadgeLink::maintenance(manifest)?.into(), 90 | metadata::BadgeItem::License(license) => { 91 | BadgeLink::license(license, manifest, package)?.into() 92 | } 93 | metadata::BadgeItem::CratesIo => BadgeLink::crates_io(manifest, package).into(), 94 | metadata::BadgeItem::DocsRs => BadgeLink::docs_rs(manifest, package).into(), 95 | metadata::BadgeItem::RustVersion => BadgeLink::rust_version(manifest, package)?.into(), 96 | metadata::BadgeItem::GithubActions(github_actions) => { 97 | BadgeLink::github_actions(github_actions, manifest, workspace, package)?.into() 98 | } 99 | metadata::BadgeItem::Codecov => BadgeLink::codecov(manifest, package)?.into(), 100 | }) 101 | } 102 | } 103 | 104 | #[derive(Debug, thiserror::Error, miette::Diagnostic)] 105 | #[error("failed to create badges of README")] 106 | pub(in super::super) struct CreateAllBadgesError { 107 | #[related] 108 | errors: Vec, 109 | } 110 | 111 | #[derive(Debug, thiserror::Error, miette::Diagnostic)] 112 | enum CreateBadgeError { 113 | #[error(transparent)] 114 | #[diagnostic(transparent)] 115 | GetConfig(#[from] GetConfigError), 116 | #[error("key `{key}` is not set in `name`: {path}")] 117 | GetConfigFromMetadata { 118 | name: String, 119 | key: String, 120 | path: Utf8PathBuf, 121 | }, 122 | #[error("failed to open GitHub Action's workflows directory: {path}")] 123 | OpenWorkflowsDir { 124 | #[source] 125 | source: io::Error, 126 | path: Utf8PathBuf, 127 | }, 128 | #[error("failed to read GitHub Action's workflows directory: {path}")] 129 | ReadWorkflowsDir { 130 | source: io::Error, 131 | path: Utf8PathBuf, 132 | }, 133 | #[error("failed to read GitHub Action's workflow file: {path}")] 134 | ReadWorkflowFile { 135 | #[source] 136 | source: io::Error, 137 | path: Utf8PathBuf, 138 | }, 139 | #[error("failed to parse GitHub Action's workflow file: {path}")] 140 | ParseWorkflowFile { 141 | #[source] 142 | source: serde_yaml::Error, 143 | path: Utf8PathBuf, 144 | #[source_code] 145 | souce_code: NamedSource>, 146 | #[label] 147 | span: Option, 148 | }, 149 | #[error("`package.repository` must starts with `https://github.com/`")] 150 | InvalidGithubRepository, 151 | } 152 | 153 | #[derive(Debug, Clone)] 154 | struct ShieldsIo<'a> { 155 | path: Cow<'a, str>, 156 | label: Option>, 157 | logo: Option>, 158 | } 159 | 160 | impl<'a> ShieldsIo<'a> { 161 | fn with_path(path: impl Into>) -> Self { 162 | Self { 163 | path: path.into(), 164 | label: None, 165 | logo: None, 166 | } 167 | } 168 | 169 | fn new_static(label: &str, message: &str, color: &str) -> Self { 170 | let message = message 171 | .replace('-', "--") 172 | .replace('_', "__") 173 | .replace(' ', "_"); 174 | Self::with_path(format!("badge/{label}-{message}-{color}.svg")) 175 | } 176 | 177 | fn new_maintenance(status: &MaintenanceStatus) -> Option { 178 | use MaintenanceStatus as Ms; 179 | // image url borrowed from https://gist.github.com/taiki-e/ad73eaea17e2e0372efb76ef6b38f17b 180 | let color = match status { 181 | Ms::ActivelyDeveloped => "brightgreen", 182 | Ms::PassivelyMaintained => "yellowgreen", 183 | Ms::AsIs => "yellow", 184 | Ms::Experimental => "blue", 185 | Ms::LookingForMaintainer => "orange", 186 | Ms::Deprecated => "red", 187 | Ms::None => return None, 188 | }; 189 | Some(Self::new_static("maintenance", status.as_str(), color)) 190 | } 191 | 192 | fn new_license(package_name: &str) -> Self { 193 | Self::with_path(format!("crates/l/{package_name}.svg")) 194 | } 195 | 196 | fn new_version(package_name: &str) -> Self { 197 | Self::with_path(format!("crates/v/{package_name}.svg")) 198 | } 199 | 200 | fn new_docs_rs(package_name: &str) -> Self { 201 | Self::with_path(format!("docsrs/{package_name}.svg")) 202 | } 203 | 204 | fn new_rust_version(version: &VersionReq) -> Self { 205 | Self::new_static("rust", &format!("{version}"), "93450a") 206 | } 207 | 208 | fn new_github_actions(repo_path: &str, name: &str) -> Self { 209 | Self::with_path(format!( 210 | "github/actions/workflow/status/{repo_path}/{name}.svg" 211 | )) 212 | } 213 | 214 | fn new_codecov(repo_path: &str) -> Self { 215 | Self::with_path(format!("codecov/c/github/{repo_path}.svg")) 216 | } 217 | 218 | fn label(mut self, label: impl Into>) -> Self { 219 | self.label = Some(label.into()); 220 | self 221 | } 222 | 223 | fn logo(mut self, logo: impl Into>) -> Self { 224 | self.logo = Some(logo.into()); 225 | self 226 | } 227 | 228 | fn build(self, manifest: &ManifestFile) -> Url { 229 | let mut url = Url::parse("https://img.shields.io/").unwrap(); 230 | url.set_path(&self.path); 231 | { 232 | let mut query = url.query_pairs_mut(); 233 | if let Some(label) = self.label { 234 | query.append_pair("label", &label); 235 | } 236 | if let Some(logo) = self.logo { 237 | query.append_pair("logo", &logo); 238 | } 239 | if let Some(style) = &manifest.value().config().badge.style { 240 | query.append_pair("style", style.as_str()); 241 | } 242 | query.finish(); 243 | } 244 | url 245 | } 246 | } 247 | 248 | #[derive(Debug, Clone)] 249 | struct BadgeLink { 250 | alt: String, 251 | link: Option, 252 | image: String, 253 | } 254 | 255 | impl fmt::Display for BadgeLink { 256 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 257 | let need_escape = &['\\', '`', '_', '[', ']', '(', ')', '!']; 258 | 259 | if let Some(link) = &self.link { 260 | write!( 261 | f, 262 | "[![{}]({})]({})", 263 | Escape(&self.alt, need_escape), 264 | self.image, 265 | link 266 | ) 267 | } else { 268 | write!(f, "![{}]({})", Escape(&self.alt, need_escape), &self.image) 269 | } 270 | } 271 | } 272 | 273 | impl BadgeLink { 274 | fn maintenance(manifest: &ManifestFile) -> CreateResult> { 275 | let status_with_source = (|| manifest.try_badges()?.try_maintenance()?.try_status())() 276 | .map_err(|err| err.with_key("badges.maintenance.status"))?; 277 | let status = status_with_source.value().get_ref(); 278 | 279 | let image = match ShieldsIo::new_maintenance(status) { 280 | Some(shields_io) => shields_io.build(manifest).to_string(), 281 | None => return Ok(None), 282 | }; 283 | 284 | let alt = format!("Maintenance: {}", status.as_str()); 285 | let link = Some( 286 | "https://doc.rust-lang.org/cargo/reference/manifest.html#the-badges-section".to_owned(), 287 | ); 288 | 289 | let badge = Self { alt, link, image }; 290 | Ok(Some(badge)) 291 | } 292 | 293 | fn license( 294 | license: &metadata::License, 295 | manifest: &ManifestFile, 296 | package: &Package, 297 | ) -> CreateResult { 298 | let (license_str, license_path) = if let Some(name) = &package.license { 299 | (name.as_str(), package.license_file.as_deref()) 300 | } else if let Some(file) = &package.license_file { 301 | ("non-standard", Some(file.as_ref())) 302 | } else { 303 | return Err(CreateBadgeError::GetConfigFromMetadata { 304 | name: "package".into(), 305 | key: "license` or `license-file".into(), 306 | path: package.manifest_path.clone(), 307 | }); 308 | }; 309 | 310 | let alt = format!("License: {license_str}"); 311 | let link = license 312 | .link 313 | .clone() 314 | .or_else(|| license_path.map(|p| p.to_string())); 315 | let image = ShieldsIo::new_license(&package.name) 316 | .build(manifest) 317 | .to_string(); 318 | Ok(Self { alt, link, image }) 319 | } 320 | 321 | fn crates_io(manifest: &ManifestFile, package: &Package) -> Self { 322 | let alt = "crates.io".to_owned(); 323 | let link = Some(format!("https://crates.io/crates/{}", package.name)); 324 | let image = ShieldsIo::new_version(&package.name) 325 | .logo("rust") 326 | .build(manifest) 327 | .to_string(); 328 | Self { alt, link, image } 329 | } 330 | 331 | fn docs_rs(manifest: &ManifestFile, package: &Package) -> Self { 332 | let alt = "docs.rs".to_owned(); 333 | let link = Some(format!("https://docs.rs/{}", package.name)); 334 | let image = ShieldsIo::new_docs_rs(&package.name) 335 | .logo("docs.rs") 336 | .build(manifest) 337 | .to_string(); 338 | Self { alt, link, image } 339 | } 340 | 341 | fn rust_version(manifest: &ManifestFile, package: &Package) -> CreateResult { 342 | let rust_version = package.rust_version.as_ref().ok_or_else(|| { 343 | CreateBadgeError::GetConfigFromMetadata { 344 | name: "package".into(), 345 | key: "rust-version".into(), 346 | path: package.manifest_path.clone(), 347 | } 348 | })?; 349 | 350 | let rust_version = VersionReq { 351 | comparators: vec![Comparator { 352 | op: Op::Caret, 353 | major: rust_version.major, 354 | minor: Some(rust_version.minor), 355 | patch: Some(rust_version.patch), 356 | pre: rust_version.pre.clone(), 357 | }], 358 | }; 359 | 360 | let alt = format!("Rust: {rust_version}"); 361 | let link = Some( 362 | "https://doc.rust-lang.org/cargo/reference/manifest.html#the-rust-version-field" 363 | .to_owned(), 364 | ); 365 | let image = ShieldsIo::new_rust_version(&rust_version) 366 | .logo("rust") 367 | .build(manifest) 368 | .to_string(); 369 | Ok(Self { alt, link, image }) 370 | } 371 | 372 | fn github_actions( 373 | github_actions: &metadata::GithubActions, 374 | manifest: &ManifestFile, 375 | workspace: &Metadata, 376 | package: &Package, 377 | ) -> CreateResult>> { 378 | let Some(repository) = &package.repository else { 379 | return Err(CreateBadgeError::GetConfigFromMetadata { 380 | name: "package".into(), 381 | key: "repository".into(), 382 | path: package.manifest_path.clone(), 383 | }); 384 | }; 385 | let repo_path = repository 386 | .strip_prefix("https://github.com/") 387 | .ok_or(CreateBadgeError::InvalidGithubRepository)?; 388 | 389 | let results = if github_actions.workflows.is_empty() { 390 | Self::github_actions_from_directory(workspace)? 391 | } else { 392 | Self::github_actions_from_config(&github_actions.workflows, workspace)? 393 | }; 394 | 395 | let results = results 396 | .into_iter() 397 | .map(|res| { 398 | res.map(|(name, file)| { 399 | let alt = format!("GitHub Actions: {name}"); 400 | let link = format!( 401 | "{}/actions/workflows/{}", 402 | repository.trim_end_matches('/'), 403 | file 404 | ); 405 | let image = ShieldsIo::new_github_actions(repo_path, &file) 406 | .label(&name) 407 | .logo("github") 408 | .build(manifest) 409 | .to_string(); 410 | Self { 411 | alt, 412 | link: Some(link), 413 | image, 414 | } 415 | }) 416 | }) 417 | .collect(); 418 | 419 | Ok(results) 420 | } 421 | 422 | fn codecov(manifest: &ManifestFile, package: &Package) -> CreateResult { 423 | let Some(repository) = &package.repository else { 424 | return Err(CreateBadgeError::GetConfigFromMetadata { 425 | name: "package".into(), 426 | key: "repository".into(), 427 | path: package.manifest_path.clone(), 428 | }); 429 | }; 430 | let repo_path = repository 431 | .strip_prefix("https://github.com/") 432 | .ok_or(CreateBadgeError::InvalidGithubRepository)?; 433 | 434 | let alt = "Codecov".to_owned(); 435 | let link = format!("https://codecov.io/gh/{}", repo_path.trim_end_matches('/')); 436 | let image = ShieldsIo::new_codecov(repo_path) 437 | .label("codecov") 438 | .logo("codecov") 439 | .build(manifest) 440 | .to_string(); 441 | Ok(Self { 442 | alt, 443 | link: Some(link), 444 | image, 445 | }) 446 | } 447 | 448 | fn github_actions_from_directory( 449 | workspace: &Metadata, 450 | ) -> CreateResult>> { 451 | let mut badges = vec![]; 452 | 453 | let workflows_dir_path = workspace.workspace_root.join(".github/workflows"); 454 | let dirs = match workflows_dir_path.read_dir_utf8() { 455 | Ok(dirs) => dirs, 456 | Err(err) if err.kind() == io::ErrorKind::NotFound => { 457 | tracing::warn!("workflows directory does not exist: {workflows_dir_path}"); 458 | return Ok(vec![]); 459 | } 460 | Err(err) => { 461 | return Err(CreateBadgeError::OpenWorkflowsDir { 462 | source: err, 463 | path: workflows_dir_path.clone(), 464 | }) 465 | } 466 | }; 467 | 468 | for res in dirs { 469 | let entry = match res { 470 | Ok(entry) => entry, 471 | Err(err) => { 472 | badges.push(Err(CreateBadgeError::ReadWorkflowsDir { 473 | source: err, 474 | path: workflows_dir_path.clone(), 475 | })); 476 | continue; 477 | } 478 | }; 479 | 480 | let path = entry.path(); 481 | if !path.is_file() 482 | || (path.extension() != Some("yml") && path.extension() != Some("yaml")) 483 | { 484 | continue; 485 | } 486 | 487 | let name = match read_workflow_name(workspace, path) { 488 | Ok(name) => name, 489 | Err(err) => { 490 | badges.push(Err(err)); 491 | continue; 492 | } 493 | }; 494 | let file = path.file_name().unwrap().to_owned(); 495 | 496 | badges.push(Ok((name, file))); 497 | } 498 | 499 | badges.sort_by(|a, b| match (a, b) { 500 | (Ok((a_name, a_file)), Ok((b_name, b_file))) => { 501 | a_name.cmp(b_name).then_with(|| a_file.cmp(b_file)) 502 | } 503 | (Ok(_), Err(_)) => Ordering::Less, 504 | (Err(_), Ok(_)) => Ordering::Greater, 505 | (Err(_), Err(_)) => Ordering::Equal, 506 | }); 507 | 508 | Ok(badges) 509 | } 510 | 511 | fn github_actions_from_config( 512 | workflows: &[metadata::GithubActionsWorkflow], 513 | workspace: &Metadata, 514 | ) -> CreateResult>> { 515 | let workflows_dir_path = workspace.workspace_root.join(".github/workflows"); 516 | 517 | let mut badges = vec![]; 518 | for workflow in workflows { 519 | let full_path = workflows_dir_path.join(&workflow.file); 520 | let name = match &workflow.name { 521 | Some(name) => name.to_owned(), 522 | None => match read_workflow_name(workspace, &full_path) { 523 | Ok(name) => name, 524 | Err(err) => { 525 | badges.push(Err(err)); 526 | continue; 527 | } 528 | }, 529 | }; 530 | badges.push(Ok((name, workflow.file.clone()))); 531 | } 532 | 533 | Ok(badges) 534 | } 535 | } 536 | 537 | fn read_workflow_name(workspace: &Metadata, path: &Utf8Path) -> CreateResult { 538 | #[derive(Debug, Deserialize)] 539 | struct Workflow { 540 | #[serde(default)] 541 | name: Option, 542 | } 543 | 544 | let text = fs::read_to_string(path).map_err(|err| CreateBadgeError::ReadWorkflowFile { 545 | source: err, 546 | path: path.to_owned(), 547 | })?; 548 | 549 | let workflow: Workflow = serde_yaml::from_str(&text).map_err(|err| { 550 | let span = err.location().map(|l| SourceSpan::from((l.index(), 0))); 551 | CreateBadgeError::ParseWorkflowFile { 552 | source: err, 553 | path: path.to_owned(), 554 | souce_code: NamedSource::new(path, text.into()), 555 | span, 556 | } 557 | })?; 558 | 559 | // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions 560 | // > If you omit name, GitHub sets it to the workflow file path relative to the 561 | // > root of the repository. 562 | Ok(workflow.name.unwrap_or_else(|| { 563 | path.strip_prefix(&workspace.workspace_root) 564 | .unwrap() 565 | .to_string() 566 | })) 567 | } 568 | -------------------------------------------------------------------------------- /src/sync/contents/rustdoc.rs: -------------------------------------------------------------------------------- 1 | use std::process::ExitStatus; 2 | 3 | use cargo_metadata::{Metadata, Package}; 4 | use rustdoc_types::{Crate, Item}; 5 | 6 | use crate::{ 7 | sync::ManifestFile, 8 | with_source::{ReadFileError, WithSource}, 9 | App, 10 | }; 11 | 12 | mod code_block; 13 | mod heading; 14 | mod intra_link; 15 | 16 | type CreateResult = Result; 17 | 18 | #[derive(Debug, thiserror::Error, miette::Diagnostic)] 19 | pub(in super::super) enum CreateRustdocError { 20 | #[error("failed to create rustdoc process")] 21 | Spawn(#[source] std::io::Error), 22 | #[error("rustdoc exited with non-zero status code: {0}")] 23 | Exit(ExitStatus), 24 | #[error(transparent)] 25 | #[diagnostic(transparent)] 26 | ReadFileError(#[from] ReadFileError), 27 | #[error("crate {crate_name} does not have a crate-level documentation")] 28 | RootDocNotFound { crate_name: String }, 29 | } 30 | 31 | pub(super) fn create( 32 | app: &App, 33 | manifest: &ManifestFile, 34 | workspace: &Metadata, 35 | package: &Package, 36 | ) -> CreateResult { 37 | let config = manifest.value().config(); 38 | 39 | run_rustdoc(app, package)?; 40 | 41 | let output_file = workspace 42 | .target_directory 43 | .join("doc") 44 | .join(format!("{}.json", package.name.replace('-', "_"))); 45 | 46 | let doc_with_source: WithSource = WithSource::from_json("rustdoc output", output_file)?; 47 | let doc = doc_with_source.value(); 48 | let root = doc.index.get(&doc.root).unwrap(); 49 | let local_html_root_url = config.rustdoc.html_root_url.clone().unwrap_or_else(|| { 50 | format!( 51 | "https://docs.rs/{}/{}", 52 | package.name, 53 | doc.crate_version.as_deref().unwrap_or("latest") 54 | ) 55 | }); 56 | 57 | let root_doc = extract_doc(root)?; 58 | let mut parser = intra_link::Parser::new(doc, root, &local_html_root_url); 59 | let events = parser.events(&root_doc); 60 | let events = heading::convert(events); 61 | let events = code_block::convert(events); 62 | 63 | let mut buf = String::with_capacity(root_doc.len()); 64 | pulldown_cmark_to_cmark::cmark(events, &mut buf).unwrap(); 65 | if !buf.is_empty() && !buf.ends_with('\n') { 66 | buf.push('\n'); 67 | } 68 | Ok(buf) 69 | } 70 | 71 | fn run_rustdoc(app: &App, package: &Package) -> CreateResult<()> { 72 | let mut command = app.toolchain.cargo_command(); 73 | command 74 | .args(["rustdoc", "--package", &package.name]) 75 | .args(app.feature.cargo_args()) 76 | .args([ 77 | "-Zrustdoc-map", 78 | "--", 79 | "--document-private-items", 80 | "-Zunstable-options", 81 | "--output-format=json", 82 | ]); 83 | 84 | tracing::info!( 85 | "executing {}{}", 86 | command.get_program().to_string_lossy(), 87 | command.get_args().fold(String::new(), |mut s, a| { 88 | s.push(' '); 89 | s.push_str(a.to_string_lossy().as_ref()); 90 | s 91 | }) 92 | ); 93 | 94 | let status = command.status().map_err(CreateRustdocError::Spawn)?; 95 | if !status.success() { 96 | return Err(CreateRustdocError::Exit(status)); 97 | } 98 | Ok(()) 99 | } 100 | 101 | fn extract_doc(item: &Item) -> CreateResult { 102 | item.docs 103 | .clone() 104 | .ok_or_else(|| CreateRustdocError::RootDocNotFound { 105 | crate_name: item.name.clone().unwrap(), 106 | }) 107 | } 108 | -------------------------------------------------------------------------------- /src/sync/contents/rustdoc/code_block.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use pulldown_cmark::{CodeBlockKind, CowStr, Event, Tag, TagEnd}; 4 | 5 | pub(super) fn convert<'a, 'b>( 6 | events: impl IntoIterator> + 'b, 7 | ) -> impl Iterator> + 'b { 8 | let mut in_codeblock = None; 9 | events.into_iter().map(move |mut event| { 10 | if let Some(is_rust) = in_codeblock { 11 | match &mut event { 12 | Event::Text(text) => { 13 | if !text.ends_with('\n') { 14 | // workaround for https://github.com/Byron/pulldown-cmark-to-cmark/issues/48 15 | *text = format!("{text}\n").into(); 16 | } 17 | if is_rust { 18 | // Hide lines starting with any number of whitespace 19 | // followed by `# ` (comments), or just `#`. But `## ` 20 | // should be converted to `# `. 21 | *text = text 22 | .lines() 23 | .filter_map(|line| { 24 | // Adapted from 25 | // https://github.com/rust-lang/rust/blob/942db6782f4a28c55b0b75b38fd4394d0483390f/src/librustdoc/html/markdown.rs#L169-L182. 26 | let trimmed = line.trim(); 27 | if trimmed.starts_with("##") { 28 | // It would be nice to reuse 29 | // `pulldown_cmark::CowStr` here, but (at 30 | // least as of version 0.12.2) it doesn't 31 | // support collecting into a `String`. 32 | Some(Cow::Owned(line.replacen("##", "#", 1))) 33 | } else if trimmed.starts_with("# ") { 34 | // Hidden line. 35 | None 36 | } else if trimmed == "#" { 37 | // A plain # is a hidden line. 38 | None 39 | } else { 40 | Some(Cow::Borrowed(line)) 41 | } 42 | }) 43 | .flat_map(|line| [line, Cow::Borrowed("\n")]) 44 | .collect::() 45 | .into(); 46 | } 47 | } 48 | Event::End(TagEnd::CodeBlock) => {} 49 | _ => unreachable!(), 50 | } 51 | } 52 | 53 | match &mut event { 54 | Event::Start(Tag::CodeBlock(kind)) => { 55 | let is_rust; 56 | match kind { 57 | CodeBlockKind::Indented => { 58 | is_rust = true; 59 | *kind = CodeBlockKind::Fenced("rust".into()); 60 | } 61 | CodeBlockKind::Fenced(tag) => { 62 | is_rust = update_codeblock_tag(tag); 63 | } 64 | } 65 | 66 | assert!(in_codeblock.is_none()); 67 | in_codeblock = Some(is_rust); 68 | } 69 | Event::End(TagEnd::CodeBlock) => { 70 | assert!(in_codeblock.is_some()); 71 | in_codeblock = None; 72 | } 73 | _ => {} 74 | } 75 | event 76 | }) 77 | } 78 | fn is_attribute_tag(tag: &str) -> bool { 79 | // https://doc.rust-lang.org/rustdoc/write-documentation/documentation-tests.html#attributes 80 | // to support future rust edition, `edition\d{4}` treated as attribute tag 81 | matches!( 82 | tag, 83 | "" | "ignore" | "should_panic" | "no_run" | "compile_fail" 84 | ) || tag 85 | .strip_prefix("edition") 86 | .map(|x| x.len() == 4 && x.chars().all(|ch| ch.is_ascii_digit())) 87 | .unwrap_or_default() 88 | } 89 | 90 | fn update_codeblock_tag(tag: &mut CowStr<'_>) -> bool { 91 | let mut tag_count = 0; 92 | let is_rust = tag 93 | .split(',') 94 | .filter(|tag| !is_attribute_tag(tag)) 95 | .all(|tag| { 96 | tag_count += 1; 97 | tag == "rust" 98 | }); 99 | if is_rust && tag_count == 0 { 100 | if tag.is_empty() { 101 | *tag = "rust".into(); 102 | } else { 103 | *tag = format!("rust,{tag}").into(); 104 | } 105 | } 106 | is_rust 107 | } 108 | 109 | #[cfg(test)] 110 | mod tests { 111 | #[test] 112 | fn update_codeblock_tag() { 113 | fn check(tag: &str, expected_tag: &str, expected_is_rust: bool) { 114 | let mut tag = tag.into(); 115 | let is_rust = super::update_codeblock_tag(&mut tag); 116 | assert_eq!(tag.as_ref(), expected_tag); 117 | assert_eq!(is_rust, expected_is_rust) 118 | } 119 | check("", "rust", true); 120 | check("typescript", "typescript", false); 121 | check("rust", "rust", true); 122 | check("ignore", "rust,ignore", true); 123 | check("ignore,rust", "ignore,rust", true); 124 | check("ignore,typescript", "ignore,typescript", false); 125 | 126 | check( 127 | "ignore,should_panic,no_run,compile_fail,edition2015,edition2018,edition2021", 128 | "rust,ignore,should_panic,no_run,compile_fail,edition2015,edition2018,edition2021", 129 | true, 130 | ); 131 | check("edition9999", "rust,edition9999", true); 132 | check("edition99999", "edition99999", false); 133 | check("editionabcd", "editionabcd", false); 134 | } 135 | 136 | #[test] 137 | fn hide_codeblock_line() { 138 | let input = r#" 139 | Lorem ipsum 140 | 141 | ```rust 142 | # This line and the next should be hidden, but the following should not. 143 | # 144 | #[derive(Debug)] 145 | struct Foo; 146 | 147 | fn main() { 148 | # As should this and the next line. 149 | # 150 | #But not this. 151 | ## This should become a single #. 152 | ##And this. 153 | } 154 | ``` 155 | 156 | ```toml 157 | # This is not Rust so it should not be hidden. 158 | ``` 159 | "#; 160 | 161 | let expected = r#"Lorem ipsum 162 | 163 | ````rust 164 | #[derive(Debug)] 165 | struct Foo; 166 | 167 | fn main() { 168 | #But not this. 169 | # This should become a single #. 170 | #And this. 171 | } 172 | ```` 173 | 174 | ````toml 175 | # This is not Rust so it should not be hidden. 176 | ````"#; 177 | 178 | let events: Vec<_> = pulldown_cmark::Parser::new(input).collect(); 179 | let events: Vec<_> = super::convert(events).collect(); 180 | 181 | let mut output = String::new(); 182 | pulldown_cmark_to_cmark::cmark(events.into_iter(), &mut output).unwrap(); 183 | assert_eq!(output, expected, "output matches expected"); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/sync/contents/rustdoc/heading.rs: -------------------------------------------------------------------------------- 1 | use pulldown_cmark::{Event, Tag, TagEnd}; 2 | 3 | pub(super) fn convert<'a, 'b>( 4 | events: impl IntoIterator> + 'b, 5 | ) -> impl Iterator> + 'b { 6 | use pulldown_cmark::HeadingLevel::*; 7 | events.into_iter().map(|mut event| { 8 | match &mut event { 9 | Event::Start(Tag::Heading { level, .. }) | Event::End(TagEnd::Heading(level)) => { 10 | *level = match level { 11 | H1 => H2, 12 | H2 => H3, 13 | H3 => H4, 14 | H4 => H5, 15 | H5 => H6, 16 | H6 => H6, 17 | } 18 | } 19 | _ => {} 20 | } 21 | event 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /src/sync/contents/rustdoc/intra_link.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, 3 | cmp::Reverse, 4 | collections::{BinaryHeap, HashMap}, 5 | fmt::Write, 6 | rc::Rc, 7 | }; 8 | 9 | use pulldown_cmark::{BrokenLink, CowStr, Event, Options, Tag}; 10 | use rustdoc_types::{ 11 | Crate, Id, Item, ItemEnum, ItemKind, ItemSummary, MacroKind, StructKind, VariantKind, 12 | }; 13 | 14 | trait CowStrExt<'a> { 15 | fn as_str(&'a self) -> &'a str; 16 | } 17 | 18 | impl<'a> CowStrExt<'a> for CowStr<'a> { 19 | fn as_str(&'a self) -> &'a str { 20 | match self { 21 | CowStr::Boxed(s) => s, 22 | CowStr::Borrowed(s) => s, 23 | CowStr::Inlined(s) => s, 24 | } 25 | } 26 | } 27 | 28 | #[derive(Debug)] 29 | pub(super) struct Parser { 30 | broken_link_callback: B, 31 | iterator_map: M, 32 | } 33 | 34 | type BrokenLinkPair<'a> = (CowStr<'a>, CowStr<'a>); 35 | 36 | impl Parser<(), ()> { 37 | pub(super) fn new<'a>( 38 | doc: &'a Crate, 39 | item: &'a Item, 40 | local_html_root_url: &str, 41 | ) -> Parser< 42 | impl FnMut(BrokenLink<'_>) -> Option>, 43 | impl FnMut(Event<'a>) -> Option>, 44 | > { 45 | let url_map = Rc::new(resolve_links(doc, item, local_html_root_url)); 46 | 47 | let broken_link_callback = { 48 | let url_map = Rc::clone(&url_map); 49 | move |link: BrokenLink<'_>| { 50 | let url = url_map.get(link.reference.as_str())?.as_ref()?; 51 | Some((url.to_owned().into(), "".into())) 52 | } 53 | }; 54 | let iterator_map = move |event| convert_link(&url_map, event); 55 | 56 | Parser { 57 | broken_link_callback, 58 | iterator_map, 59 | } 60 | } 61 | } 62 | 63 | impl<'a, B, M> Parser 64 | where 65 | B: FnMut(BrokenLink<'_>) -> Option> + 'a, 66 | M: FnMut(Event<'a>) -> Option> + 'a, 67 | { 68 | pub(super) fn events<'b>(&'b mut self, doc: &'a str) -> impl Iterator> + 'b 69 | where 70 | 'a: 'b, 71 | { 72 | pulldown_cmark::Parser::new_with_broken_link_callback( 73 | doc, 74 | Options::all(), 75 | Some(&mut self.broken_link_callback), 76 | ) 77 | .filter_map(&mut self.iterator_map) 78 | } 79 | } 80 | 81 | fn resolve_links<'doc>( 82 | doc: &'doc Crate, 83 | item: &'doc Item, 84 | local_html_root_url: &str, 85 | ) -> HashMap<&'doc str, Option> { 86 | let extra_paths = extra_paths(&doc.index, &doc.paths); 87 | item.links 88 | .iter() 89 | .map(move |(name, id)| { 90 | let url = id_to_url(doc, &extra_paths, local_html_root_url, id).or_else(|| { 91 | tracing::warn!(?id, "failed to resolve link to `{name}`"); 92 | None 93 | }); 94 | (name.as_str(), url) 95 | }) 96 | .collect() 97 | } 98 | 99 | #[derive(Debug)] 100 | struct Node<'a> { 101 | depth: usize, 102 | kind: ItemKind, 103 | name: Option<&'a str>, 104 | parent: Option<&'a Id>, 105 | } 106 | 107 | fn extra_paths<'doc>( 108 | index: &'doc HashMap, 109 | paths: &'doc HashMap, 110 | ) -> HashMap<&'doc Id, Node<'doc>> { 111 | let mut map: HashMap<&Id, Node<'_>> = index 112 | .iter() 113 | .map(|(id, item)| { 114 | ( 115 | id, 116 | Node { 117 | depth: usize::MAX, 118 | kind: item_kind(item), 119 | name: item.name.as_deref(), 120 | parent: None, 121 | }, 122 | ) 123 | }) 124 | .collect(); 125 | 126 | #[derive(Debug)] 127 | struct HeapItem<'doc> { 128 | depth: Reverse, 129 | id: &'doc Id, 130 | parent: Option<&'doc Id>, 131 | item: &'doc Item, 132 | } 133 | 134 | impl PartialOrd for HeapItem<'_> { 135 | fn partial_cmp(&self, other: &Self) -> Option { 136 | Some(self.cmp(other)) 137 | } 138 | } 139 | impl Ord for HeapItem<'_> { 140 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 141 | self.depth.cmp(&other.depth) 142 | } 143 | } 144 | impl PartialEq for HeapItem<'_> { 145 | fn eq(&self, other: &Self) -> bool { 146 | self.depth == other.depth 147 | } 148 | } 149 | impl Eq for HeapItem<'_> {} 150 | 151 | let mut heap: BinaryHeap> = index 152 | .iter() 153 | .map(|(id, item)| { 154 | let depth = if paths.contains_key(id) { 155 | 0 156 | } else { 157 | usize::MAX 158 | }; 159 | HeapItem { 160 | depth: Reverse(depth), 161 | id, 162 | item, 163 | parent: None, 164 | } 165 | }) 166 | .collect(); 167 | 168 | while let Some(HeapItem { 169 | depth: Reverse(depth), 170 | id, 171 | parent, 172 | item, 173 | }) = heap.pop() 174 | { 175 | let node = map.get_mut(id).unwrap(); 176 | if depth >= node.depth { 177 | continue; 178 | } 179 | node.parent = parent; 180 | 181 | map.get_mut(id).unwrap().depth = depth; 182 | 183 | for child in item_children(item).into_iter().flatten() { 184 | let child = match index.get(child) { 185 | Some(child) => child, 186 | None => { 187 | tracing::trace!(?item, ?child, "child item missing"); 188 | continue; 189 | } 190 | }; 191 | let child_depth = depth + 1; 192 | heap.push(HeapItem { 193 | depth: Reverse(child_depth), 194 | id: &child.id, 195 | item: child, 196 | parent: Some(id), 197 | }); 198 | } 199 | } 200 | 201 | map 202 | } 203 | 204 | fn item_kind(item: &Item) -> ItemKind { 205 | match &item.inner { 206 | ItemEnum::Module(_) => ItemKind::Module, 207 | ItemEnum::ExternCrate { .. } => ItemKind::ExternCrate, 208 | ItemEnum::Use(_) => ItemKind::Use, 209 | ItemEnum::Union(_) => ItemKind::Union, 210 | ItemEnum::Struct(_) => ItemKind::Struct, 211 | ItemEnum::StructField(_) => ItemKind::StructField, 212 | ItemEnum::Enum(_) => ItemKind::Enum, 213 | ItemEnum::Variant(_) => ItemKind::Variant, 214 | ItemEnum::Function(_) => ItemKind::Function, 215 | ItemEnum::Trait(_) => ItemKind::Trait, 216 | ItemEnum::TraitAlias(_) => ItemKind::TraitAlias, 217 | ItemEnum::Impl(_) => ItemKind::Impl, 218 | ItemEnum::TypeAlias(_) => ItemKind::TypeAlias, 219 | ItemEnum::Constant { .. } => ItemKind::Constant, 220 | ItemEnum::Static(_) => ItemKind::Static, 221 | ItemEnum::ExternType => ItemKind::ExternType, 222 | ItemEnum::Macro(_) => ItemKind::Macro, 223 | ItemEnum::ProcMacro(pm) => match pm.kind { 224 | MacroKind::Bang => ItemKind::Macro, 225 | MacroKind::Attr => ItemKind::ProcAttribute, 226 | MacroKind::Derive => ItemKind::ProcDerive, 227 | }, 228 | ItemEnum::Primitive(_) => ItemKind::Primitive, 229 | ItemEnum::AssocConst { .. } => ItemKind::AssocConst, 230 | ItemEnum::AssocType { .. } => ItemKind::AssocType, 231 | } 232 | } 233 | 234 | fn item_children<'doc>(parent: &'doc Item) -> Option + 'doc>> { 235 | match &parent.inner { 236 | ItemEnum::Module(m) => Some(Box::new(m.items.iter())), 237 | ItemEnum::ExternCrate { .. } => None, 238 | ItemEnum::Use(_) => None, 239 | ItemEnum::Union(u) => Some(Box::new(u.fields.iter())), 240 | ItemEnum::Struct(s) => match &s.kind { 241 | StructKind::Unit => None, 242 | StructKind::Tuple(t) => Some(Box::new(t.iter().flatten())), 243 | StructKind::Plain { 244 | fields, 245 | has_stripped_fields: _, 246 | } => Some(Box::new(fields.iter())), 247 | }, 248 | ItemEnum::StructField(_) => None, 249 | ItemEnum::Enum(e) => Some(Box::new(e.variants.iter())), 250 | ItemEnum::Variant(v) => match &v.kind { 251 | VariantKind::Plain => None, 252 | VariantKind::Tuple(t) => Some(Box::new(t.iter().flatten())), 253 | VariantKind::Struct { 254 | fields, 255 | has_stripped_fields: _, 256 | } => Some(Box::new(fields.iter())), 257 | }, 258 | ItemEnum::Function(_) => None, 259 | ItemEnum::Trait(t) => Some(Box::new(t.items.iter())), 260 | ItemEnum::TraitAlias(_) => None, 261 | ItemEnum::Impl(i) => Some(Box::new(i.items.iter())), 262 | ItemEnum::TypeAlias(_) => None, 263 | ItemEnum::Constant { .. } => None, 264 | ItemEnum::Static(_) => None, 265 | ItemEnum::ExternType => None, 266 | ItemEnum::Macro(_) => None, 267 | ItemEnum::ProcMacro(_) => None, 268 | ItemEnum::Primitive(_) => None, 269 | ItemEnum::AssocConst { .. } => None, 270 | ItemEnum::AssocType { .. } => None, 271 | } 272 | } 273 | 274 | fn convert_link<'a>( 275 | url_map: &HashMap<&str, Option>, 276 | mut event: Event<'a>, 277 | ) -> Option> { 278 | if let Event::Start(Tag::Link { dest_url: url, .. }) = &mut event { 279 | if let Some(full_url) = url_map.get(url.as_ref()) { 280 | match full_url { 281 | Some(full_url) => *url = full_url.to_owned().into(), 282 | None => return None, 283 | } 284 | } 285 | } 286 | Some(event) 287 | } 288 | 289 | fn id_to_url( 290 | doc: &Crate, 291 | extra_paths: &HashMap<&Id, Node<'_>>, 292 | local_html_root_url: &str, 293 | id: &Id, 294 | ) -> Option { 295 | let item = item_summary(doc, extra_paths, id)?; 296 | let html_root_url = if item.crate_id == 0 { 297 | // local item 298 | local_html_root_url 299 | } else { 300 | // external item 301 | let external_crate = doc.external_crates.get(&item.crate_id)?; 302 | external_crate.html_root_url.as_ref()? 303 | }; 304 | 305 | let mut url = html_root_url.trim_end_matches('/').to_owned(); 306 | let mut join = |paths: &[String], args| { 307 | for path in paths { 308 | write!(&mut url, "/{path}").unwrap(); 309 | } 310 | write!(&mut url, "/{args}").unwrap(); 311 | }; 312 | match (&item.kind, item.path.as_slice()) { 313 | (ItemKind::Module, ps) => join(ps, format_args!("index.html")), 314 | // (ItemKind::ExternCrate, [..]) => todo!(), 315 | // (ItemKind::Import, [..]) => todo!(), 316 | (ItemKind::Struct, [ps @ .., name]) => join(ps, format_args!("struct.{name}.html")), 317 | (ItemKind::StructField, [ps @ .., struct_name, field]) => join( 318 | ps, 319 | format_args!("struct.{struct_name}.html#structfield.{field}"), 320 | ), 321 | (ItemKind::Union, [ps @ .., name]) => join(ps, format_args!("union.{name}.html")), 322 | (ItemKind::Enum, [ps @ .., name]) => join(ps, format_args!("enum.{name}.html")), 323 | (ItemKind::Variant, [ps @ .., enum_name, variant_name]) => join( 324 | ps, 325 | format_args!("enum.{enum_name}.html#variant.{variant_name}"), 326 | ), 327 | (ItemKind::Function, [ps @ .., name]) => join(ps, format_args!("fn.{name}.html")), 328 | (ItemKind::TypeAlias, [ps @ .., name]) => join(ps, format_args!("type.{name}.html")), 329 | // (ItemKind::OpaqueTy, [..]) => todo!(), 330 | (ItemKind::Constant, [ps @ .., name]) => join(ps, format_args!("constant.{name}.html")), 331 | (ItemKind::Trait, [ps @ .., name]) => join(ps, format_args!("trait.{name}.html")), 332 | // (ItemKind::TraitAlias, [..]) => todo!(), 333 | // (ItemKind::Impl, [..]) => todo!(), 334 | (ItemKind::Static, [ps @ .., name]) => join(ps, format_args!("static.{name}.html")), 335 | // (ItemKind::ForeignType, [..]) => todo!(), 336 | (ItemKind::Macro, [ps @ .., name]) => join(ps, format_args!("macro.{name}.html")), 337 | (ItemKind::ProcAttribute, [ps @ .., name]) => join(ps, format_args!("attr.{name}.html")), 338 | (ItemKind::ProcDerive, [ps @ .., name]) => join(ps, format_args!("derive.{name}.html")), 339 | (ItemKind::AssocConst, [ps @ .., trait_name, const_name]) => join( 340 | ps, 341 | format_args!("trait.{trait_name}.html#associatedconstant.{const_name}"), 342 | ), 343 | (ItemKind::AssocType, [ps @ .., trait_name, type_name]) => join( 344 | ps, 345 | format_args!("trait.{trait_name}.html#associatedtype.{type_name}"), 346 | ), 347 | (ItemKind::Primitive, [ps @ .., name]) => join(ps, format_args!("primitive.{name}.html")), 348 | // (ItemKind::Keyword, [..]) => todo!(), 349 | (item, path) => { 350 | tracing::warn!(?item, ?path, "unexpected intra-doc link item & path found"); 351 | return None; 352 | } 353 | } 354 | Some(url) 355 | } 356 | 357 | fn item_summary<'doc>( 358 | doc: &'doc Crate, 359 | extra_paths: &'doc HashMap<&'doc Id, Node<'doc>>, 360 | id: &'doc Id, 361 | ) -> Option> { 362 | if let Some(summary) = doc.paths.get(id) { 363 | return Some(Cow::Borrowed(summary)); 364 | } 365 | // workaround for https://github.com/rust-lang/rust/issues/101687 366 | // if the item is not found in the paths, try to find it in the extra_paths 367 | 368 | let node = extra_paths.get(id)?; 369 | let mut stack = vec![node]; 370 | let mut current = node; 371 | while let Some(parent) = current.parent { 372 | if let Some(summary) = doc.paths.get(parent) { 373 | let mut path = summary.path.clone(); 374 | while let Some(node) = stack.pop() { 375 | let name = node.name?; 376 | path.push(name.to_string()); 377 | } 378 | return Some(Cow::Owned(ItemSummary { 379 | crate_id: summary.crate_id, 380 | kind: node.kind, 381 | path, 382 | })); 383 | } 384 | current = extra_paths.get(&parent)?; 385 | stack.push(current); 386 | } 387 | None 388 | } 389 | -------------------------------------------------------------------------------- /src/sync/contents/title.rs: -------------------------------------------------------------------------------- 1 | use cargo_metadata::Package; 2 | 3 | pub(super) fn create(package: &Package) -> String { 4 | format!("# {}\n", package.name) 5 | } 6 | -------------------------------------------------------------------------------- /src/sync/marker.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, sync::Arc}; 2 | 3 | use miette::SourceSpan; 4 | 5 | pub(super) use self::{find::*, replace::*}; 6 | use crate::{config::metadata::BadgeItem, traits::StrSpanExt}; 7 | 8 | use super::ManifestFile; 9 | 10 | mod find; 11 | mod replace; 12 | 13 | #[derive(Debug, Clone, PartialEq, Eq)] 14 | pub(super) enum Replace { 15 | Title, 16 | Badge { 17 | name: Arc, 18 | badges: Arc<[BadgeItem]>, 19 | }, 20 | Rustdoc, 21 | } 22 | 23 | #[derive(Debug, thiserror::Error)] 24 | pub(super) enum ParseReplaceError { 25 | #[error("unknown replace specifier: {0:?}")] 26 | UnknownReplace(String), 27 | #[error("badge group not configured in package manifest: package.metadata.cargo-sync-rdme.badge.badges{hyphen}{0}", hyphen = if .0.is_empty() { "" } else { "-" })] 28 | NoSuchBadgeGroup(String), 29 | } 30 | 31 | impl Replace { 32 | fn from_str(s: &str, manifest: &ManifestFile) -> Result { 33 | let group = match s { 34 | "title" => return Ok(Self::Title), 35 | "rustdoc" => return Ok(Self::Rustdoc), 36 | "badge" => "", 37 | _ => { 38 | if let Some(group) = s.strip_prefix("badge:") { 39 | group 40 | } else { 41 | return Err(ParseReplaceError::UnknownReplace(s.to_owned())); 42 | } 43 | } 44 | }; 45 | let badges = &manifest.value().config().badge.badges; 46 | let (name, badges) = badges 47 | .get_key_value(group) 48 | .ok_or_else(|| ParseReplaceError::NoSuchBadgeGroup(group.to_owned()))?; 49 | Ok(Self::Badge { 50 | name: Arc::clone(name), 51 | badges: Arc::clone(badges), 52 | }) 53 | } 54 | } 55 | 56 | impl fmt::Display for Replace { 57 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 58 | match self { 59 | Self::Title => write!(f, "title"), 60 | Self::Badge { name, .. } => { 61 | if name.is_empty() { 62 | write!(f, "badge") 63 | } else { 64 | write!(f, "badge:{name}") 65 | } 66 | } 67 | Self::Rustdoc => write!(f, "rustdoc"), 68 | } 69 | } 70 | } 71 | 72 | #[derive(Debug, Clone, PartialEq, Eq)] 73 | pub(super) enum Marker { 74 | Replace(Replace), 75 | Start(Replace), 76 | End, 77 | } 78 | 79 | const MAGIC: &str = "cargo-sync-rdme"; 80 | 81 | impl fmt::Display for Marker { 82 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 83 | match self { 84 | Self::Replace(replace) => write!(f, ""), 85 | Self::Start(replace) => write!(f, ""), 86 | Self::End => write!(f, ""), 87 | } 88 | } 89 | } 90 | 91 | #[derive(Debug, thiserror::Error, miette::Diagnostic)] 92 | pub(super) enum ParseMarkerError { 93 | #[error("{err}")] 94 | ParseReplace { 95 | err: ParseReplaceError, 96 | #[label] 97 | span: SourceSpan, 98 | }, 99 | #[error("no replace specifier found")] 100 | NoReplace { 101 | #[label] 102 | span: SourceSpan, 103 | }, 104 | } 105 | 106 | impl From<(ParseReplaceError, SourceSpan)> for ParseMarkerError { 107 | fn from((err, span): (ParseReplaceError, SourceSpan)) -> Self { 108 | Self::ParseReplace { err, span } 109 | } 110 | } 111 | 112 | impl Marker { 113 | pub(super) fn matches( 114 | text: (&str, SourceSpan), 115 | manifest: &ManifestFile, 116 | ) -> Result, ParseMarkerError> { 117 | let body = opt_try!(Self::matches_marker(text)?); 118 | 119 | // [[ 120 | if let Some(replace) = body.strip_suffix_str("[[") { 121 | let replace = replace.trim(); 122 | let replace = Replace::from_str(replace.0, manifest).map_err(|err| (err, replace.1))?; 123 | return Ok(Some(Marker::Start(replace))); 124 | } 125 | 126 | if body.0 == "]]" { 127 | return Ok(Some(Marker::End)); 128 | } 129 | 130 | let replace = Replace::from_str(body.0, manifest).map_err(|err| (err, body.1))?; 131 | Ok(Some(Marker::Replace(replace))) 132 | } 133 | 134 | fn matches_marker( 135 | text: (&str, SourceSpan), 136 | ) -> Result, ParseMarkerError> { 137 | // 138 | let text = opt_try!(trim_comment(text)); 139 | 140 | if text.0 == MAGIC { 141 | return Err(ParseMarkerError::NoReplace { span: text.1 }); 142 | } 143 | let (head, body) = opt_try!(text.split_once_fn(char::is_whitespace)); 144 | Ok((head.0 == MAGIC).then_some(body)) 145 | } 146 | } 147 | 148 | fn trim_comment(text: (&str, SourceSpan)) -> Option<(&str, SourceSpan)> { 149 | let body = text 150 | .trim() 151 | .strip_prefix_str("")? 154 | .trim_end(); 155 | Some(body) 156 | } 157 | 158 | #[cfg(test)] 159 | mod tests { 160 | use super::*; 161 | 162 | #[test] 163 | fn matches() { 164 | fn ok(s: &str) -> Option { 165 | let config = indoc::indoc! {" 166 | [package.metadata.cargo-sync-rdme.badge.badges] 167 | [package.metadata.cargo-sync-rdme.badge.badges-foo] 168 | "}; 169 | let manifest = ManifestFile::dummy(toml::from_str(config).unwrap()); 170 | let span = SourceSpan::from(0..s.len()); 171 | Marker::matches((s, span), &manifest).unwrap() 172 | } 173 | fn err_kind(s: &str) -> String { 174 | let config = indoc::indoc! {" 175 | [package.metadata.cargo-sync-rdme.badge.badges] 176 | [package.metadata.cargo-sync-rdme.badge.badges-foo] 177 | "}; 178 | let manifest = ManifestFile::dummy(toml::from_str(config).unwrap()); 179 | let span = SourceSpan::from(0..s.len()); 180 | match Marker::matches((s, span), &manifest).unwrap_err() { 181 | ParseMarkerError::ParseReplace { 182 | err: ParseReplaceError::UnknownReplace(s), 183 | .. 184 | } => s, 185 | e => panic!("unexpected: {e}"), 186 | } 187 | } 188 | fn err_norep(s: &str) { 189 | let config = indoc::indoc! {" 190 | [package.metadata.cargo-sync-rdme.badge.badges] 191 | [package.metadata.cargo-sync-rdme.badge.badges-foo] 192 | "}; 193 | let manifest = ManifestFile::dummy(toml::from_str(config).unwrap()); 194 | let span = SourceSpan::from(0..s.len()); 195 | match Marker::matches((s, span), &manifest).unwrap_err() { 196 | ParseMarkerError::NoReplace { .. } => {} 197 | e => panic!("unexpected: {e}"), 198 | } 199 | } 200 | 201 | assert_eq!(ok(""), None); 202 | assert_eq!(ok(""), None); 203 | 204 | assert_eq!( 205 | ok(""), 206 | Some(Marker::Replace(Replace::Title)) 207 | ); 208 | assert!(matches!( 209 | ok(""), 210 | Some(Marker::Start(Replace::Badge { name, .. })) if name.is_empty() 211 | )); 212 | assert!(matches!( 213 | ok(""), 214 | Some(Marker::Start(Replace::Badge { name, ..})) if name.is_empty() 215 | )); 216 | assert_eq!(ok(""), Some(Marker::End)); 217 | 218 | err_norep(""); 219 | assert_eq!(err_kind(""), "title ["); 220 | assert_eq!(err_kind(""), "]"); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/sync/marker/find.rs: -------------------------------------------------------------------------------- 1 | use std::{ops::Range, sync::Arc}; 2 | 3 | use miette::{NamedSource, SourceSpan}; 4 | use pulldown_cmark::Event; 5 | 6 | use crate::sync::ManifestFile; 7 | 8 | use super::{super::MarkdownFile, Marker, ParseMarkerError, Replace}; 9 | 10 | pub(in super::super) fn find_all<'events>( 11 | readme: &MarkdownFile, 12 | manifest: &ManifestFile, 13 | events: impl IntoIterator, Range)> + 'events, 14 | ) -> Result)>, FindAllError> { 15 | let events = events.into_iter(); 16 | let it = Iter { manifest, events }; 17 | let mut markers = vec![]; 18 | let mut errors = vec![]; 19 | for res in it { 20 | match res { 21 | Ok(marker) => markers.push(marker), 22 | Err(err) => errors.push(err), 23 | } 24 | } 25 | 26 | if !errors.is_empty() { 27 | let source_code = readme.to_named_source(); 28 | return Err(FindAllError { 29 | source_code, 30 | errors, 31 | }); 32 | } 33 | 34 | Ok(markers) 35 | } 36 | 37 | #[derive(Debug, thiserror::Error, miette::Diagnostic)] 38 | #[error("failed to parse README")] 39 | pub(in super::super) struct FindAllError { 40 | #[source_code] 41 | source_code: NamedSource>, 42 | #[related] 43 | errors: Vec, 44 | } 45 | 46 | #[derive(Debug, thiserror::Error, miette::Diagnostic)] 47 | enum FindError { 48 | #[error(transparent)] 49 | #[diagnostic(transparent)] 50 | ParseMarker(#[from] ParseMarkerError), 51 | #[error("unexpected end marker")] 52 | UnexpectedEndMarker { 53 | #[label = "the end marker defined here"] 54 | span: SourceSpan, 55 | }, 56 | #[error("corresponding end marker not found")] 57 | EndMarkerNotFound { 58 | #[label = "the start label defined here"] 59 | start_span: SourceSpan, 60 | }, 61 | #[error("nested markers are not allowed")] 62 | NestedMarker { 63 | #[label = "the nested marker defined here"] 64 | nested_span: SourceSpan, 65 | #[label = "the previous marker starts here"] 66 | previous_span: SourceSpan, 67 | }, 68 | } 69 | 70 | #[derive(Debug)] 71 | struct Iter<'manifest, I> { 72 | manifest: &'manifest ManifestFile, 73 | events: I, 74 | } 75 | 76 | impl<'event, I> Iterator for Iter<'_, I> 77 | where 78 | I: Iterator, Range)>, 79 | { 80 | type Item = Result<(Replace, Range), FindError>; 81 | 82 | fn next(&mut self) -> Option { 83 | match itry!(self.next_marker())? { 84 | (Marker::Replace(replace), range) => Some(Ok((replace, range))), 85 | (Marker::Start(replace), start_range) => match itry!(self.next_marker()) { 86 | Some((Marker::End, end_range)) => { 87 | Some(Ok((replace, start_range.start..end_range.end))) 88 | } 89 | Some((_, nested_range)) => Some(Err(FindError::NestedMarker { 90 | nested_span: nested_range.into(), 91 | previous_span: start_range.into(), 92 | })), 93 | None => Some(Err(FindError::EndMarkerNotFound { 94 | start_span: start_range.into(), 95 | })), 96 | }, 97 | (Marker::End, range) => { 98 | Some(Err(FindError::UnexpectedEndMarker { span: range.into() })) 99 | } 100 | } 101 | } 102 | } 103 | 104 | impl<'event, I> Iter<'_, I> 105 | where 106 | I: Iterator, Range)>, 107 | { 108 | fn next_marker(&mut self) -> Result)>, FindError> { 109 | for (event, range) in self.events.by_ref() { 110 | if let Event::Html(html) = &event { 111 | if let Some(marker) = Marker::matches((html, range.clone().into()), self.manifest)? 112 | { 113 | return Ok(Some((marker, range))); 114 | } 115 | } 116 | } 117 | Ok(None) 118 | } 119 | } 120 | 121 | #[cfg(test)] 122 | mod tests { 123 | use pulldown_cmark::Parser; 124 | 125 | use super::*; 126 | 127 | fn line_ranges(lines: &[impl AsRef]) -> Vec> { 128 | lines 129 | .iter() 130 | .scan(0, |offset, line| { 131 | let line = line.as_ref(); 132 | let range = *offset..*offset + line.len() + 1; 133 | *offset = range.end; 134 | Some(range) 135 | }) 136 | .collect() 137 | } 138 | 139 | #[test] 140 | fn no_markers() { 141 | let input = "Hello, world!"; 142 | let mut markers = Iter { 143 | manifest: &ManifestFile::dummy(Default::default()), 144 | events: Parser::new(input).into_offset_iter(), 145 | }; 146 | assert!(markers.next().is_none()); 147 | } 148 | 149 | #[test] 150 | fn replace_marker() { 151 | let lines = [ 152 | "Good morning, world!".to_string(), 153 | Marker::Replace(Replace::Title).to_string(), 154 | "Good afternoon, world!".to_string(), 155 | Marker::Replace(Replace::Badge { 156 | name: "".into(), 157 | badges: vec![].into(), 158 | }) 159 | .to_string(), 160 | "Good evening, world!".to_string(), 161 | Marker::Replace(Replace::Rustdoc).to_string(), 162 | "Good night, world!".to_string(), 163 | ]; 164 | let ranges = line_ranges(&lines); 165 | let input = lines.join("\n"); 166 | 167 | let config = indoc::indoc! {" 168 | [package.metadata.cargo-sync-rdme.badge.badges] 169 | "}; 170 | 171 | let mut markers = Iter { 172 | manifest: &ManifestFile::dummy(toml::from_str(config).unwrap()), 173 | events: Parser::new(&input).into_offset_iter(), 174 | }; 175 | assert_eq!( 176 | markers.next().unwrap().unwrap(), 177 | (Replace::Title, ranges[1].clone()) 178 | ); 179 | assert_eq!( 180 | markers.next().unwrap().unwrap(), 181 | ( 182 | Replace::Badge { 183 | name: "".into(), 184 | badges: vec![].into() 185 | }, 186 | ranges[3].clone() 187 | ) 188 | ); 189 | assert_eq!( 190 | markers.next().unwrap().unwrap(), 191 | (Replace::Rustdoc, ranges[5].clone()) 192 | ); 193 | assert!(markers.next().is_none()); 194 | } 195 | 196 | #[test] 197 | fn replace_region() { 198 | let lines = [ 199 | "Good morning, world!".to_string(), 200 | Marker::Start(Replace::Title).to_string(), 201 | "Good afternoon, world!".to_string(), 202 | "# Heading!".to_string(), 203 | Marker::End.to_string(), 204 | "Good evening, world!".to_string(), 205 | ]; 206 | let ranges = line_ranges(&lines); 207 | let input = lines.join("\n"); 208 | 209 | let config = indoc::indoc! {" 210 | [package.metadata.cargo-sync-rdme.badge.badges] 211 | "}; 212 | 213 | let mut markers = Iter { 214 | manifest: &ManifestFile::dummy(toml::from_str(config).unwrap()), 215 | events: Parser::new(&input).into_offset_iter(), 216 | }; 217 | assert_eq!( 218 | markers.next().unwrap().unwrap(), 219 | (Replace::Title, ranges[1].start..ranges[4].end) 220 | ); 221 | assert!(markers.next().is_none()); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/sync/marker/replace.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, iter, ops::Range}; 2 | 3 | use super::{super::contents::Contents, Marker, Replace}; 4 | 5 | pub(in super::super) fn replace_all( 6 | text: &str, 7 | markers: &[(Replace, Range)], 8 | contents: &[Contents], 9 | ) -> String { 10 | let pairs = markers 11 | .iter() 12 | .zip(contents) 13 | .map(|((replace, range), contents)| ((replace.clone(), contents), range.clone())); 14 | 15 | interpolate_ranges(0..text.len(), pairs) 16 | .map(|(contents, range)| match contents { 17 | Some((replace, contents)) => { 18 | if contents.text().is_empty() { 19 | Cow::Owned(format!("{}\n", Marker::Replace(replace))) 20 | } else { 21 | Cow::Owned(format!( 22 | "{}\n{}{}\n", 23 | Marker::Start(replace), 24 | contents.text(), 25 | Marker::End 26 | )) 27 | } 28 | } 29 | None => Cow::Borrowed(&text[range]), 30 | }) 31 | .collect() 32 | } 33 | 34 | fn interpolate_ranges( 35 | range: Range, 36 | items: impl IntoIterator)>, 37 | ) -> impl Iterator, Range)> { 38 | let mut items = items.into_iter().peekable(); 39 | let mut offset = range.start; 40 | iter::from_fn(move || match items.peek() { 41 | Some(&(_, Range { start, .. })) if offset < start => { 42 | let range = offset..start; 43 | offset = start; 44 | Some((None, range)) 45 | } 46 | Some(_) => { 47 | let (item, Range { start, end }) = items.next().unwrap(); 48 | offset = end; 49 | Some((Some(item), start..end)) 50 | } 51 | None if offset < range.end => { 52 | let range = offset..range.end; 53 | offset = range.end; 54 | Some((None, range)) 55 | } 56 | None => None, 57 | }) 58 | } 59 | 60 | #[cfg(test)] 61 | mod tests { 62 | #[test] 63 | fn interpolate_ranges() { 64 | let items = [(1, 0..1), (2, 1..2), (3, 2..3)]; 65 | let ranges = super::interpolate_ranges(0..3, items); 66 | assert_eq!( 67 | ranges.collect::>(), 68 | vec![(Some(1), 0..1), (Some(2), 1..2), (Some(3), 2..3),] 69 | ); 70 | 71 | let items = [(1, 3..4), (2, 4..5), (3, 6..7), (4, 8..9)]; 72 | let ranges = super::interpolate_ranges(0..10, items); 73 | assert_eq!( 74 | ranges.collect::>(), 75 | vec![ 76 | (None, 0..3), 77 | (Some(1), 3..4), 78 | (Some(2), 4..5), 79 | (None, 5..6), 80 | (Some(3), 6..7), 81 | (None, 7..8), 82 | (Some(4), 8..9), 83 | (None, 9..10), 84 | ] 85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/traits.rs: -------------------------------------------------------------------------------- 1 | use cargo_metadata::{camino::Utf8Path, Package}; 2 | use miette::SourceSpan; 3 | 4 | /// Extension methods for [`cargo_metadata::Package`]. 5 | pub(crate) trait PackageExt { 6 | /// Returns the package root directory. 7 | fn root_directory(&self) -> &Utf8Path; 8 | } 9 | 10 | impl PackageExt for Package { 11 | fn root_directory(&self) -> &Utf8Path { 12 | // `manifest_path` is the path to the manifest file, so parent must exist. 13 | self.manifest_path.parent().unwrap() 14 | } 15 | } 16 | 17 | pub(crate) trait StrSpanExt: Sized { 18 | fn trim(&self) -> Self { 19 | self.trim_start().trim_end() 20 | } 21 | fn trim_start(&self) -> Self; 22 | fn trim_end(&self) -> Self; 23 | fn strip_prefix_str(&self, prefix: &str) -> Option; 24 | fn strip_suffix_str(&self, suffix: &str) -> Option; 25 | fn split_once_fn(&self, f: impl Fn(char) -> bool) -> Option<(Self, Self)>; 26 | } 27 | 28 | mod imp { 29 | use super::*; 30 | 31 | fn new(s: &str, offset: usize) -> (&str, SourceSpan) { 32 | (s, (offset, s.len()).into()) 33 | } 34 | 35 | fn same_end<'a>(original: (&str, SourceSpan), trimmed: &'a str) -> (&'a str, SourceSpan) { 36 | let new_offset = original.1.offset() + (original.0.len() - trimmed.len()); 37 | new(trimmed, new_offset) 38 | } 39 | 40 | fn same_start<'a>(original: (&str, SourceSpan), trimmed: &'a str) -> (&'a str, SourceSpan) { 41 | let new_offset = original.1.offset(); 42 | new(trimmed, new_offset) 43 | } 44 | 45 | impl StrSpanExt for (&str, SourceSpan) { 46 | fn trim_start(&self) -> Self { 47 | same_end(*self, self.0.trim_start()) 48 | } 49 | 50 | fn trim_end(&self) -> Self { 51 | same_start(*self, self.0.trim_end()) 52 | } 53 | 54 | fn strip_prefix_str(&self, prefix: &str) -> Option { 55 | Some(same_end(*self, self.0.strip_prefix(prefix)?)) 56 | } 57 | 58 | fn strip_suffix_str(&self, suffix: &str) -> Option { 59 | Some(same_start(*self, self.0.strip_suffix(suffix)?)) 60 | } 61 | 62 | fn split_once_fn(&self, f: impl Fn(char) -> bool) -> Option<(Self, Self)> { 63 | let (head, tail) = self.0.split_once(f)?; 64 | Some((same_start(*self, head), same_end(*self, tail))) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/vcs.rs: -------------------------------------------------------------------------------- 1 | use cargo_metadata::camino::Utf8Path; 2 | 3 | use crate::Result; 4 | 5 | #[cfg(feature = "vcs-git")] 6 | mod git; 7 | 8 | pub(crate) fn discover(path: impl AsRef) -> Result>> { 9 | let path = path.as_ref(); 10 | 11 | // suppress unused variable warning 12 | let _ = path; 13 | 14 | #[cfg(feature = "vcs-git")] 15 | if let Some(vcs) = git::Git::discover(path)? { 16 | return Ok(Some(Box::new(vcs))); 17 | } 18 | 19 | Ok(None) 20 | } 21 | 22 | pub(crate) trait Vcs { 23 | fn workdir(&self) -> Option<&Utf8Path>; 24 | fn status_file(&self, path: &Utf8Path) -> Result; 25 | } 26 | 27 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 28 | #[allow(dead_code)] 29 | pub(crate) enum Status { 30 | Dirty, 31 | Staged, 32 | Clean, 33 | } 34 | -------------------------------------------------------------------------------- /src/vcs/git.rs: -------------------------------------------------------------------------------- 1 | use cargo_metadata::camino::Utf8Path; 2 | 3 | use super::{Status, Vcs}; 4 | use crate::Result; 5 | 6 | pub(super) struct Git { 7 | repo: git2::Repository, 8 | } 9 | 10 | impl Git { 11 | pub(super) fn discover(path: impl AsRef) -> Result> { 12 | let path = path.as_ref(); 13 | 14 | let repo = match git2::Repository::discover(path) { 15 | Ok(repo) => repo, 16 | Err(err) if err.code() == git2::ErrorCode::NotFound => return Ok(None), 17 | Err(err) => bail!(err), 18 | }; 19 | Ok(Some(Self { repo })) 20 | } 21 | } 22 | 23 | impl Vcs for Git { 24 | fn workdir(&self) -> Option<&Utf8Path> { 25 | self.repo 26 | .workdir() 27 | .map(|path| <&Utf8Path>::try_from(path).unwrap()) 28 | } 29 | 30 | fn status_file(&self, path: &Utf8Path) -> Result { 31 | let status = match self.repo.status_file(path.as_ref()) { 32 | Ok(status) => status, 33 | Err(err) if err.code() == git2::ErrorCode::NotFound => { 34 | // treat untracked files as dirty 35 | return Ok(Status::Dirty); 36 | } 37 | Err(err) => bail!(err), 38 | }; 39 | 40 | if status.is_wt_new() 41 | || status.is_wt_modified() 42 | || status.is_wt_deleted() 43 | || status.is_wt_renamed() 44 | || status.is_wt_typechange() 45 | { 46 | return Ok(Status::Dirty); 47 | } 48 | 49 | if status.is_index_new() 50 | || status.is_index_modified() 51 | || status.is_index_deleted() 52 | || status.is_index_renamed() 53 | || status.is_index_typechange() 54 | { 55 | return Ok(Status::Staged); 56 | } 57 | 58 | Ok(Status::Clean) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/with_source.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, io, rc::Rc, sync::Arc}; 2 | 3 | use cargo_metadata::camino::Utf8PathBuf; 4 | use miette::{NamedSource, SourceOffset, SourceSpan}; 5 | use serde::Deserialize; 6 | 7 | use toml::Spanned; 8 | 9 | #[derive(Debug, thiserror::Error, miette::Diagnostic)] 10 | pub(crate) enum ReadFileError { 11 | #[error("failed to read {name}: {path}")] 12 | Io { 13 | name: String, 14 | path: Utf8PathBuf, 15 | #[source] 16 | source: io::Error, 17 | }, 18 | #[error("failed to parse {name}")] 19 | ParseToml { 20 | name: String, 21 | #[source] 22 | source: toml::de::Error, 23 | #[source_code] 24 | source_code: NamedSource>, 25 | #[label] 26 | label: Option, 27 | }, 28 | #[error("failed to parse {name}")] 29 | ParseJson { 30 | name: String, 31 | #[source] 32 | source: serde_json::Error, 33 | #[source_code] 34 | source_code: NamedSource>, 35 | #[label] 36 | label: SourceSpan, 37 | }, 38 | } 39 | 40 | #[derive(Debug, Clone)] 41 | struct SourceInfo { 42 | name: String, 43 | path: Utf8PathBuf, 44 | text: Arc, 45 | } 46 | 47 | impl SourceInfo { 48 | fn open(name: impl Into, path: impl Into) -> Result { 49 | let name = name.into(); 50 | let path = path.into(); 51 | let text = fs::read_to_string(&path) 52 | .map_err(|err| ReadFileError::Io { 53 | name: name.clone(), 54 | path: path.clone(), 55 | source: err, 56 | })? 57 | .into(); 58 | 59 | Ok(Self { name, path, text }) 60 | } 61 | 62 | pub(crate) fn to_named_source(&self) -> NamedSource> { 63 | NamedSource::new(&self.path, Arc::clone(&self.text)) 64 | } 65 | } 66 | 67 | #[derive(Debug, Clone)] 68 | pub(crate) struct WithSource { 69 | source_info: Rc, 70 | value: T, 71 | } 72 | 73 | impl WithSource { 74 | pub(crate) fn from_toml( 75 | name: impl Into, 76 | path: impl Into, 77 | ) -> Result 78 | where 79 | T: for<'de> Deserialize<'de>, 80 | { 81 | let source_info = SourceInfo::open(name, path)?; 82 | 83 | let value: T = toml::from_str(&source_info.text).map_err(|err| { 84 | let label = err.line_col().map(|(line, col)| { 85 | let offset = SourceOffset::from_location(&source_info.text, line + 1, col + 1); 86 | SourceSpan::new(offset, 1) 87 | }); 88 | let source_code = source_info.to_named_source(); 89 | ReadFileError::ParseToml { 90 | name: source_info.name.clone(), 91 | source: err, 92 | source_code, 93 | label, 94 | } 95 | })?; 96 | 97 | let source_info = Rc::new(source_info); 98 | Ok(Self { source_info, value }) 99 | } 100 | 101 | pub(crate) fn from_json( 102 | name: impl Into, 103 | path: impl Into, 104 | ) -> Result 105 | where 106 | T: for<'de> Deserialize<'de>, 107 | { 108 | let source_info = SourceInfo::open(name, path)?; 109 | 110 | let value: T = serde_json::from_str(&source_info.text).map_err(|err| { 111 | let offset = SourceOffset::from_location(&source_info.text, err.line(), err.column()); 112 | let label = SourceSpan::new(offset, 1); 113 | let source_code = source_info.to_named_source(); 114 | ReadFileError::ParseJson { 115 | name: source_info.name.clone(), 116 | source: err, 117 | source_code, 118 | label, 119 | } 120 | })?; 121 | 122 | let source_info = Rc::new(source_info); 123 | Ok(Self { source_info, value }) 124 | } 125 | 126 | pub(crate) fn name(&self) -> &str { 127 | &self.source_info.name 128 | } 129 | 130 | pub(crate) fn value(&self) -> &T { 131 | &self.value 132 | } 133 | 134 | pub(crate) fn to_named_source(&self) -> NamedSource> { 135 | self.source_info.to_named_source() 136 | } 137 | 138 | pub(crate) fn map(&self, f: impl FnOnce(&T) -> U) -> WithSource { 139 | WithSource { 140 | source_info: Rc::clone(&self.source_info), 141 | value: f(&self.value), 142 | } 143 | } 144 | 145 | #[cfg(test)] 146 | pub(crate) fn dummy(value: T) -> Self { 147 | Self { 148 | source_info: Rc::new(SourceInfo { 149 | name: "dummy".to_string(), 150 | path: Utf8PathBuf::from("dummy"), 151 | text: "dummy".into(), 152 | }), 153 | value, 154 | } 155 | } 156 | } 157 | 158 | impl WithSource<&'_ Spanned> { 159 | pub(crate) fn span(&self) -> SourceSpan { 160 | SourceSpan::from(self.value().start()..self.value().end()) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /xtask/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xtask" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | readme = "README.md" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | cli-xtask = { version = "0.10.1", features = ["bin-crate", "bin-crate-extra", "main", "subcommand-doc", "subcommand-docsrs"] } 12 | cargo-sync-rdme = { path = ".." } 13 | -------------------------------------------------------------------------------- /xtask/README.md: -------------------------------------------------------------------------------- 1 | 2 | # xtask 3 | 4 | 5 | This is a collection of tasks for the `cargo-sync-rdme` project. 6 | 7 | See `cargo xtask --help` for more information. 8 | -------------------------------------------------------------------------------- /xtask/release.toml: -------------------------------------------------------------------------------- 1 | release = false 2 | -------------------------------------------------------------------------------- /xtask/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use cli_xtask::{ 4 | camino::Utf8PathBuf, 5 | cargo_metadata::TargetKind, 6 | clap::CommandFactory, 7 | color_eyre::eyre::{ensure, eyre}, 8 | config::{ConfigBuilder, DistConfigBuilder}, 9 | workspace, Result, Xtask, 10 | }; 11 | 12 | fn main() -> Result<()> { 13 | let bin_path = build_sync_rdme()?; 14 | let path = env::var("PATH").unwrap_or_default(); 15 | let sep = if cfg!(windows) { ";" } else { ":" }; 16 | env::set_var( 17 | "PATH", 18 | format!("{}{}{}", bin_path.parent().unwrap(), sep, path), 19 | ); 20 | 21 | ::main_with_config(|| { 22 | let workspace = workspace::current(); 23 | let (dist, package) = DistConfigBuilder::from_root_package(workspace)?; 24 | let command = cargo_sync_rdme::App::command(); 25 | let target = package 26 | .binary_by_name(&command.get_name().replace(' ', "-"))? 27 | .command(command) 28 | .build()?; 29 | let dist = dist.package(package.target(target).build()?).build()?; 30 | let config = ConfigBuilder::new().dist(dist).build()?; 31 | Ok(config) 32 | }) 33 | } 34 | 35 | fn build_sync_rdme() -> Result { 36 | let metadata = workspace::current(); 37 | let package = metadata 38 | .root_package() 39 | .ok_or_else(|| eyre!("cargo-sync-rdme package not found"))?; 40 | let target = &package 41 | .targets 42 | .iter() 43 | .find(|t| t.kind.iter().any(|k| k == &TargetKind::Bin)) 44 | .ok_or_else(|| eyre!("binary target not found in cargo-sync-rdme package"))?; 45 | ensure!( 46 | target.name == "cargo-sync-rdme", 47 | "invalid binary target name" 48 | ); 49 | let bin_path = cli_xtask::cargo::build( 50 | metadata, 51 | Some(package), 52 | Some(target), 53 | None, 54 | None, 55 | false, 56 | None, 57 | )? 58 | .next() 59 | .ok_or_else(|| eyre!("no output file found"))??; 60 | Ok(bin_path) 61 | } 62 | --------------------------------------------------------------------------------