├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── rustfmt.toml └── src ├── commands ├── generate.rs └── mod.rs ├── error.rs ├── io.rs └── main.rs /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Desktop (please complete the following information):** 23 | - OS: 24 | - Documentation browser [e.g. dash, zeal]: 25 | - Version: 26 | 27 | **Additional context** 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # cargo-docset changelog 2 | 3 | ## 9/26/2022 - v0.3.1 4 | 5 | * Bugfix: update the crate version in Cargo.lock (thanks @antifuchs) 6 | * Bugfix: attempt not to index files that only consist of a redirection. 7 | * CI: switch to github actions 8 | * Tests: add some basic unit tests surrounding CLI arguments parsing. 9 | * Maintenance: update dependencies 10 | 11 | ## 7/30/2022 - v0.3.0 12 | 13 | * Bugfix: module names are no longer suffixed by `::index`. 14 | * Bugfix: fix several issues with virtual workspaces. 15 | * Feature: add `--target-dir` option and respect `CARGO_TARGET_DIR` environment variable and `build.target_dir` config. 16 | * Feature: add the `--docset-name` option in order to specify or override the docset name. 17 | * Feature: add the `--docset-index` option in order to specify or override the docset index package. 18 | * Feature: add the `--platform-family` option in order to specify or override the docset platform family string. 19 | * Feature: add the enabled by default `color` cargo feature which can be disabled to turn off colored terminal output. 20 | * Refactored: use the [cargo-metadata](https://crates.io/crates/cargo_metadata) crate to obtain the workspace metadata, 21 | replace hand-rolled mechanisms. 22 | * Maintenance: update dependencies to their latest versions. 23 | * Maintenance: update to Rust edition 2021. 24 | 25 | ## 8/23/2020 - v0.2.1 26 | 27 | * Bugfix: fix spelling of the `manifest-path` when passed down to cargo. 28 | * Bugfix: fix detection of the workspace base directory for filesystem operations. 29 | * Documentation: mention the dependency on SQLite and link to rusqlite's documentation in the README. 30 | * Feature: provide the ability to use the SQLite version bundled with rusqlite through the `bundled-sqlite` feature. 31 | * Maintenance: update `rusqlite` to v0.24. 32 | 33 | ## 6/22/2020 - v0.2.0 34 | 35 | * Enhancement: do not depend on cargo anymore. This greatly improves the compile time, and should fix the recurring 36 | issues regarding the bundled version of cargo being unable to parse the Cargo.lock file. Drop other dependencies that 37 | are not needed anymore as a consequence. 38 | * Feature: support the `--target` and `--manifest-path` options. 39 | * Enhancement: enable default features by default, use the `--no-default-features` flag to disable this behavior. 40 | * Maintenance: update the other dependencies to their latest versions. 41 | 42 | ## 6/19/2020 - v0.1.5 43 | 44 | * Maintenance: update cargo to 0.42 (thanks to [@zgotch](https://github.com/zgotsch)) and run cargo update. 45 | 46 | ## 1/6/2020 - v0.1.4 47 | 48 | * Bugfix: enable external JavaScript in Info.plist, should fix docsets not rendering properly in Dash. 49 | 50 | ## 10/28/2019 - v0.1.3 51 | 52 | * Bugfix: don't crash the application when invoked directly as `cargo-docset`, print the usage message instead. 53 | * Maintenance: run `cargo update`. 54 | 55 | ## 9/5/2019 - v0.1.2 56 | 57 | * Feature: add the following command line options mimicking `cargo doc`: --features, --no-default-features, 58 | --all-features, --frozen, --locked, --offline, --lib, --bin and --bins. 59 | * Feature: make cleaning the doc directory optional, through `--no-clean` option. 60 | * Enhancement: use cargo clean command instead of `remove\_dir\_all` to clean the rustdoc directory. 61 | * Enhancement: better error output. 62 | 63 | ## 8/29/2019 - v0.1.1 64 | 65 | * Feature: add --exclude option 66 | * Feature: add --quiet and --verbose options 67 | * Enhancement: update dependencies to latest versions 68 | 69 | ## 8/14/2019 - v0.1.0 70 | 71 | Initial release. 72 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in contributing. There are several ways you can help the project: 4 | 5 | * Report bugs. If you encounter any, please open an issue on the github issue tracker. 6 | * Feature requests. Please post those on the issue tracker too. 7 | * Pull requests are welcome. Don't hesitate to let me know on the issue tracker that you'd like to work on a particular 8 | issue. 9 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.7.6" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" 10 | dependencies = [ 11 | "getrandom", 12 | "once_cell", 13 | "version_check", 14 | ] 15 | 16 | [[package]] 17 | name = "atty" 18 | version = "0.2.14" 19 | source = "registry+https://github.com/rust-lang/crates.io-index" 20 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 21 | dependencies = [ 22 | "hermit-abi", 23 | "libc", 24 | "winapi", 25 | ] 26 | 27 | [[package]] 28 | name = "bitflags" 29 | version = "1.3.2" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 32 | 33 | [[package]] 34 | name = "camino" 35 | version = "1.1.1" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "88ad0e1e3e88dd237a156ab9f571021b8a158caa0ae44b1968a241efb5144c1e" 38 | dependencies = [ 39 | "serde", 40 | ] 41 | 42 | [[package]] 43 | name = "cargo-docset" 44 | version = "0.3.1" 45 | dependencies = [ 46 | "atty", 47 | "cargo_metadata", 48 | "clap", 49 | "clap-cargo", 50 | "derive_more", 51 | "rusqlite", 52 | "snafu", 53 | "termcolor", 54 | ] 55 | 56 | [[package]] 57 | name = "cargo-platform" 58 | version = "0.1.2" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27" 61 | dependencies = [ 62 | "serde", 63 | ] 64 | 65 | [[package]] 66 | name = "cargo_metadata" 67 | version = "0.15.0" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "3abb7553d5b9b8421c6de7cb02606ff15e0c6eea7d8eadd75ef013fd636bec36" 70 | dependencies = [ 71 | "camino", 72 | "cargo-platform", 73 | "semver", 74 | "serde", 75 | "serde_json", 76 | ] 77 | 78 | [[package]] 79 | name = "cc" 80 | version = "1.0.73" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" 83 | 84 | [[package]] 85 | name = "cfg-if" 86 | version = "1.0.0" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 89 | 90 | [[package]] 91 | name = "clap" 92 | version = "4.0.2" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "31c9484ccdc4cb8e7b117cbd0eb150c7c0f04464854e4679aeb50ef03b32d003" 95 | dependencies = [ 96 | "atty", 97 | "bitflags", 98 | "clap_derive", 99 | "clap_lex", 100 | "once_cell", 101 | "strsim", 102 | "termcolor", 103 | ] 104 | 105 | [[package]] 106 | name = "clap-cargo" 107 | version = "0.10.0" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "eca953650a7350560b61db95a0ab1d9c6f7b74d146a9e08fb258b834f3cf7e2c" 110 | dependencies = [ 111 | "cargo_metadata", 112 | "clap", 113 | "doc-comment", 114 | ] 115 | 116 | [[package]] 117 | name = "clap_derive" 118 | version = "4.0.1" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "ca689d7434ce44517a12a89456b2be4d1ea1cafcd8f581978c03d45f5a5c12a7" 121 | dependencies = [ 122 | "heck", 123 | "proc-macro-error", 124 | "proc-macro2", 125 | "quote", 126 | "syn", 127 | ] 128 | 129 | [[package]] 130 | name = "clap_lex" 131 | version = "0.3.0" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" 134 | dependencies = [ 135 | "os_str_bytes", 136 | ] 137 | 138 | [[package]] 139 | name = "convert_case" 140 | version = "0.4.0" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" 143 | 144 | [[package]] 145 | name = "derive_more" 146 | version = "0.99.17" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" 149 | dependencies = [ 150 | "convert_case", 151 | "proc-macro2", 152 | "quote", 153 | "rustc_version", 154 | "syn", 155 | ] 156 | 157 | [[package]] 158 | name = "doc-comment" 159 | version = "0.3.3" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 162 | 163 | [[package]] 164 | name = "fallible-iterator" 165 | version = "0.2.0" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" 168 | 169 | [[package]] 170 | name = "fallible-streaming-iterator" 171 | version = "0.1.9" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" 174 | 175 | [[package]] 176 | name = "getrandom" 177 | version = "0.2.7" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" 180 | dependencies = [ 181 | "cfg-if", 182 | "libc", 183 | "wasi", 184 | ] 185 | 186 | [[package]] 187 | name = "hashbrown" 188 | version = "0.12.3" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 191 | dependencies = [ 192 | "ahash", 193 | ] 194 | 195 | [[package]] 196 | name = "hashlink" 197 | version = "0.8.1" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa" 200 | dependencies = [ 201 | "hashbrown", 202 | ] 203 | 204 | [[package]] 205 | name = "heck" 206 | version = "0.4.0" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" 209 | 210 | [[package]] 211 | name = "hermit-abi" 212 | version = "0.1.19" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 215 | dependencies = [ 216 | "libc", 217 | ] 218 | 219 | [[package]] 220 | name = "itoa" 221 | version = "1.0.3" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" 224 | 225 | [[package]] 226 | name = "libc" 227 | version = "0.2.133" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966" 230 | 231 | [[package]] 232 | name = "libsqlite3-sys" 233 | version = "0.25.1" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "9f0455f2c1bc9a7caa792907026e469c1d91761fb0ea37cbb16427c77280cf35" 236 | dependencies = [ 237 | "cc", 238 | "pkg-config", 239 | "vcpkg", 240 | ] 241 | 242 | [[package]] 243 | name = "once_cell" 244 | version = "1.15.0" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" 247 | 248 | [[package]] 249 | name = "os_str_bytes" 250 | version = "6.3.0" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" 253 | 254 | [[package]] 255 | name = "pkg-config" 256 | version = "0.3.25" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" 259 | 260 | [[package]] 261 | name = "proc-macro-error" 262 | version = "1.0.4" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 265 | dependencies = [ 266 | "proc-macro-error-attr", 267 | "proc-macro2", 268 | "quote", 269 | "syn", 270 | "version_check", 271 | ] 272 | 273 | [[package]] 274 | name = "proc-macro-error-attr" 275 | version = "1.0.4" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 278 | dependencies = [ 279 | "proc-macro2", 280 | "quote", 281 | "version_check", 282 | ] 283 | 284 | [[package]] 285 | name = "proc-macro2" 286 | version = "1.0.46" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" 289 | dependencies = [ 290 | "unicode-ident", 291 | ] 292 | 293 | [[package]] 294 | name = "quote" 295 | version = "1.0.21" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" 298 | dependencies = [ 299 | "proc-macro2", 300 | ] 301 | 302 | [[package]] 303 | name = "rusqlite" 304 | version = "0.28.0" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a" 307 | dependencies = [ 308 | "bitflags", 309 | "fallible-iterator", 310 | "fallible-streaming-iterator", 311 | "hashlink", 312 | "libsqlite3-sys", 313 | "smallvec", 314 | ] 315 | 316 | [[package]] 317 | name = "rustc_version" 318 | version = "0.4.0" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 321 | dependencies = [ 322 | "semver", 323 | ] 324 | 325 | [[package]] 326 | name = "ryu" 327 | version = "1.0.11" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" 330 | 331 | [[package]] 332 | name = "semver" 333 | version = "1.0.14" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" 336 | dependencies = [ 337 | "serde", 338 | ] 339 | 340 | [[package]] 341 | name = "serde" 342 | version = "1.0.145" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" 345 | dependencies = [ 346 | "serde_derive", 347 | ] 348 | 349 | [[package]] 350 | name = "serde_derive" 351 | version = "1.0.145" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" 354 | dependencies = [ 355 | "proc-macro2", 356 | "quote", 357 | "syn", 358 | ] 359 | 360 | [[package]] 361 | name = "serde_json" 362 | version = "1.0.85" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" 365 | dependencies = [ 366 | "itoa", 367 | "ryu", 368 | "serde", 369 | ] 370 | 371 | [[package]] 372 | name = "smallvec" 373 | version = "1.9.0" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" 376 | 377 | [[package]] 378 | name = "snafu" 379 | version = "0.7.1" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "5177903bf45656592d9eb5c0e22f408fc023aae51dbe2088889b71633ba451f2" 382 | dependencies = [ 383 | "doc-comment", 384 | "snafu-derive", 385 | ] 386 | 387 | [[package]] 388 | name = "snafu-derive" 389 | version = "0.7.1" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "410b26ed97440d90ced3e2488c868d56a86e2064f5d7d6f417909b286afe25e5" 392 | dependencies = [ 393 | "heck", 394 | "proc-macro2", 395 | "quote", 396 | "syn", 397 | ] 398 | 399 | [[package]] 400 | name = "strsim" 401 | version = "0.10.0" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 404 | 405 | [[package]] 406 | name = "syn" 407 | version = "1.0.101" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2" 410 | dependencies = [ 411 | "proc-macro2", 412 | "quote", 413 | "unicode-ident", 414 | ] 415 | 416 | [[package]] 417 | name = "termcolor" 418 | version = "1.1.3" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 421 | dependencies = [ 422 | "winapi-util", 423 | ] 424 | 425 | [[package]] 426 | name = "unicode-ident" 427 | version = "1.0.4" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" 430 | 431 | [[package]] 432 | name = "vcpkg" 433 | version = "0.2.15" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 436 | 437 | [[package]] 438 | name = "version_check" 439 | version = "0.9.4" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 442 | 443 | [[package]] 444 | name = "wasi" 445 | version = "0.11.0+wasi-snapshot-preview1" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 448 | 449 | [[package]] 450 | name = "winapi" 451 | version = "0.3.9" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 454 | dependencies = [ 455 | "winapi-i686-pc-windows-gnu", 456 | "winapi-x86_64-pc-windows-gnu", 457 | ] 458 | 459 | [[package]] 460 | name = "winapi-i686-pc-windows-gnu" 461 | version = "0.4.0" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 464 | 465 | [[package]] 466 | name = "winapi-util" 467 | version = "0.1.5" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 470 | dependencies = [ 471 | "winapi", 472 | ] 473 | 474 | [[package]] 475 | name = "winapi-x86_64-pc-windows-gnu" 476 | version = "0.4.0" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 479 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-docset" 3 | authors = ["R.Chavignat "] 4 | description = "Generates a Zeal/Dash docset for your rust package." 5 | edition = "2021" 6 | version = "0.3.1" 7 | 8 | repository = "https://github.com/Robzz/cargo-docset" 9 | readme = "README.md" 10 | license = "Apache-2.0" 11 | keywords = ["zeal", "dash", "docset", "documentation"] 12 | categories = ["development-tools", "command-line-utilities"] 13 | 14 | [badges] 15 | maintenance = { status = "experimental" } 16 | 17 | [features] 18 | bundled-sqlite = ["rusqlite/bundled"] 19 | color = ["clap/color", "termcolor", "atty"] 20 | default = ["color"] 21 | 22 | [dependencies] 23 | atty = { version = "0.2", optional = true } 24 | cargo_metadata = "0.15" 25 | clap-cargo = { version = "0.10", features = ["cargo_metadata"] } 26 | clap = { version = "4.0", features = ["std", "suggestions", "derive"], default_features = false } 27 | derive_more = "0.99" 28 | rusqlite = "0.28" 29 | snafu = "0.7" 30 | termcolor = { version = "1.1", optional = true } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `cargo-docset` - Generate a Zeal/Dash docset for your Rust crate or workspace 2 | 3 | ![Build status](https://github.com/Robzz/cargo-docset/actions/workflows/rust.yml/badge.svg?branch=master) 4 | [![Crate](https://img.shields.io/crates/v/cargo-docset.svg)](https://crates.io/crates/cargo-docset) 5 | 6 | `cargo-docset` is a tool allowing you to generate a [Dash](https://kapeli.com/dash)/[Zeal](https://zealdocs.org/) 7 | compatible docset for your Rust packages and their dependencies. 8 | 9 | ## Installation 10 | 11 | `cargo-docset` depends on the SQLite3 library. You can either install the SQLite3 library on your system (see 12 | [rusqlite's documentation](https://github.com/rusqlite/rusqlite#notes-on-building-rusqlite-and-libsqlite3-sys) for 13 | help), or build the version that is bundled in the `libsqlite3-sys` crate by turning on the `bundled-sqlite` feature 14 | flag when building `cargo-docset`. 15 | 16 | You can install cargo docset with the usual cargo command: `cargo install cargo-docset`. 17 | 18 | ## How to use 19 | 20 | Just run `cargo docset` in your crate's directory to generate the docset. It will be placed in the `target/docset` 21 | directory. cargo-docset generally supports the same options as `cargo doc`, with a few additional ones. For more 22 | information, run `cargo docset --help` or look below in this README. 23 | 24 | To install your shiny new docset, copy it to your Zeal/Dash docset directory (available in the preferences, on Zeal at 25 | least) and restart Zeal/Dash. 26 | 27 | ### Examples 28 | 29 | Some more advanced examples: 30 | 31 | * Include documentation only for some of the documented package's dependencies: `cargo docset --no-deps --package 32 | dependency1 --package dependency2 ...` 33 | * Generate a docset for nightly Rust from the properly initialized (e.g. `git clone --recurse-submodules ...`) official 34 | Rust repository: `cargo +nightly docset --package std --package core --no-deps --docset-name "Rust nightly $(git rev-parse --short HEAD)" --docset-index std --platform-family rust-nightly` 35 | 36 | ### `cargo docset --help` 37 | 38 | ``` 39 | cargo-docset-docset 40 | Generate a docset. This is currently the only available command, and should remain the default one 41 | in the future if new ones are added 42 | 43 | USAGE: 44 | cargo-docset docset [OPTIONS] 45 | 46 | OPTIONS: 47 | --all-features 48 | Activate all available features 49 | 50 | --bin 51 | Document only the specified binary 52 | 53 | --bins 54 | Document all binaries 55 | 56 | --docset-index 57 | Specify or override the package whose index will be used as the docset index page 58 | 59 | --docset-name 60 | Specify or override the name of the docset, this is the display name used by your docset 61 | browser 62 | 63 | --document-private-items 64 | Generate documentation for private items 65 | 66 | --exclude 67 | Exclude packages from being processed 68 | 69 | -F, --features 70 | Space-separated list of features to activate 71 | 72 | -h, --help 73 | Print help information 74 | 75 | --lib 76 | Document only this package's library 77 | 78 | --manifest-path 79 | Path to Cargo.toml 80 | 81 | --no-clean 82 | Do not clean the doc directory before generating the rustdoc 83 | 84 | --no-default-features 85 | Do not activate the `default` feature 86 | 87 | --no-deps 88 | Do not document dependencies 89 | 90 | -p, --package 91 | Package to process (see `cargo help pkgid`) 92 | 93 | --platform-family 94 | Specify or override the docset platform family, this is used as the keyword you can 95 | specify in your docset browser search bar to search this specific docset) 96 | 97 | --target 98 | Build documentation for the specified target triple 99 | 100 | --target-dir 101 | Override the workspace target directory 102 | 103 | --workspace 104 | Process all packages in the workspace 105 | ``` 106 | 107 | ## How it works 108 | 109 | Currently, `cargo docset` runs `cargo` to generate the documentation, and then recursively walks the generated 110 | directory. The contents of every file is inferred from the file path, and cargo-docset then fills a SQLite database with 111 | the gathered information. The details of docset generation are available [here](https://kapeli.com/docsets#dashDocset). 112 | 113 | `cargo-docset` does not (yet, at least) try to parse the generated documentation in any way, and therefore is limited in 114 | the granularity of the index it can provide. In particular, the generated docset does not make use of the table of 115 | contents feature. 116 | 117 | Also, because `cargo-docset` walks through the whole `doc` directory, it must clear it before attempting to generate 118 | the docset, in case there is some previously generated documentation that we don't want to pickup in the docset there. 119 | You should probably not be storing anything of value in that directory anyway, but keep it in mind. 120 | 121 | ## Contributing 122 | 123 | See [here](./CONTRIBUTING.md). 124 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | trailing_comma = "Never" 2 | -------------------------------------------------------------------------------- /src/commands/generate.rs: -------------------------------------------------------------------------------- 1 | //! Implementation of the `docset` subcommand. 2 | 3 | use crate::{error::*, io::*, DocsetParams}; 4 | 5 | use cargo_metadata::Metadata; 6 | use derive_more::Constructor; 7 | use rusqlite::{params, Connection}; 8 | use snafu::{ensure, ResultExt}; 9 | 10 | use std::{ 11 | borrow::ToOwned, 12 | ffi::OsStr, 13 | fmt::Display, 14 | fs::{copy, create_dir_all, read_dir, remove_dir_all, File}, 15 | io::{Write, BufReader, BufRead}, 16 | path::{Path, PathBuf}, 17 | process::Command, 18 | result::Result as StdResult, 19 | }; 20 | 21 | #[derive(Debug, Clone, PartialEq, Eq)] 22 | pub enum EntryType { 23 | Constant, 24 | Enum, 25 | Function, 26 | Macro, 27 | Module, 28 | Package, // i.e. crate 29 | Struct, 30 | Trait, 31 | Type //Union // Is this even implemented in Rust ? 32 | } 33 | 34 | impl Display for EntryType { 35 | fn fmt(&self, f: &mut std::fmt::Formatter) -> StdResult<(), std::fmt::Error> { 36 | match self { 37 | EntryType::Constant => write!(f, "Constant"), 38 | EntryType::Enum => write!(f, "Enum"), 39 | EntryType::Function => write!(f, "Function"), 40 | EntryType::Macro => write!(f, "Macro"), 41 | EntryType::Module => write!(f, "Module"), 42 | EntryType::Package => write!(f, "Package"), 43 | EntryType::Struct => write!(f, "Struct"), 44 | EntryType::Trait => write!(f, "Trait"), 45 | EntryType::Type => write!(f, "Type") 46 | } 47 | } 48 | } 49 | 50 | #[derive(Debug, Clone, PartialEq, Eq, Constructor)] 51 | pub struct DocsetEntry { 52 | pub name: String, 53 | pub ty: EntryType, 54 | pub path: PathBuf 55 | } 56 | 57 | fn check_if_redirection(html_file: &mut File) -> bool { 58 | // 512 bytes should get to the end of the head section for most redirection pages in one read, 59 | // while reading less data than the 8kB default. 60 | let mut reader = BufReader::with_capacity(512, html_file); 61 | 62 | let mut file_contents = String::new(); 63 | loop { 64 | let prev_len = file_contents.len(); 65 | let n = reader.read_line(&mut file_contents).expect("Could not read from file"); 66 | if n == 0 { 67 | // EOF 68 | break; 69 | } 70 | if file_contents[prev_len..prev_len+n].contains("") { 71 | // End of the head section, stop here instead of parsing the whole file 72 | break; 73 | } 74 | } 75 | file_contents.contains("Redirection") 76 | } 77 | 78 | fn parse_docset_entry, P2: AsRef>( 79 | module_path: &Option<&str>, 80 | rustdoc_root_dir: P1, 81 | file_path: P2 82 | ) -> Option { 83 | if file_path.as_ref().extension() == Some(OsStr::new("html")) { 84 | let file_name = file_path.as_ref().file_name().unwrap().to_string_lossy(); 85 | let mut file = File::open(file_path.as_ref()) 86 | .expect("Could not open file"); 87 | if check_if_redirection(&mut file) { 88 | return None; 89 | } 90 | 91 | let parts = file_name.split('.').collect::>(); 92 | 93 | let file_db_path = file_path 94 | .as_ref() 95 | .strip_prefix(&rustdoc_root_dir) 96 | .unwrap() 97 | .to_owned(); 98 | match parts.len() { 99 | 2 => { 100 | match parts[0] { 101 | "index" => { 102 | if let Some(mod_path) = module_path { 103 | if mod_path.contains(':') { 104 | // Module entry 105 | Some(DocsetEntry::new( 106 | mod_path.to_string(), 107 | EntryType::Module, 108 | file_db_path 109 | )) 110 | } else { 111 | // Package entry 112 | Some(DocsetEntry::new( 113 | mod_path.to_string(), 114 | EntryType::Package, 115 | file_db_path 116 | )) 117 | } 118 | } else { 119 | None 120 | } 121 | } 122 | _ => None 123 | } 124 | } 125 | 3 => match parts[0] { 126 | "const" => Some(DocsetEntry::new( 127 | format!("{}::{}", module_path.unwrap(), parts[1]), 128 | EntryType::Constant, 129 | file_db_path 130 | )), 131 | "enum" => Some(DocsetEntry::new( 132 | format!("{}::{}", module_path.unwrap(), parts[1]), 133 | EntryType::Enum, 134 | file_db_path 135 | )), 136 | "fn" => Some(DocsetEntry::new( 137 | format!("{}::{}", module_path.unwrap(), parts[1]), 138 | EntryType::Function, 139 | file_db_path 140 | )), 141 | "macro" => Some(DocsetEntry::new( 142 | format!("{}::{}", module_path.unwrap(), parts[1]), 143 | EntryType::Macro, 144 | file_db_path 145 | )), 146 | "trait" => Some(DocsetEntry::new( 147 | format!("{}::{}", module_path.unwrap(), parts[1]), 148 | EntryType::Trait, 149 | file_db_path 150 | )), 151 | "struct" => Some(DocsetEntry::new( 152 | format!("{}::{}", module_path.unwrap(), parts[1]), 153 | EntryType::Struct, 154 | file_db_path 155 | )), 156 | "type" => Some(DocsetEntry::new( 157 | format!("{}::{}", module_path.unwrap(), parts[1]), 158 | EntryType::Type, 159 | file_db_path 160 | )), 161 | _ => None 162 | }, 163 | _ => None 164 | } 165 | } else { 166 | None 167 | } 168 | } 169 | 170 | const ROOT_SKIP_DIRS: &[&str] = &["src", "implementors"]; 171 | 172 | fn recursive_walk( 173 | root_dir: &Path, 174 | cur_dir: &Path, 175 | module_path: Option<&str> 176 | ) -> Result> { 177 | let dir = read_dir(cur_dir).context(IoReadSnafu)?; 178 | let mut entries = vec![]; 179 | let mut subdir_entries = vec![]; 180 | 181 | for dir_entry in dir { 182 | let dir_entry = dir_entry.unwrap(); 183 | if dir_entry.file_type().unwrap().is_dir() { 184 | let mut subdir_module_path = 185 | module_path.map(|p| format!("{}::", p)).unwrap_or_default(); 186 | let dir_name = dir_entry.file_name().to_string_lossy().to_string(); 187 | 188 | // Ignore some of the root directories which are of no interest to us 189 | if !(module_path.is_none() && ROOT_SKIP_DIRS.contains(&dir_name.as_str())) { 190 | subdir_module_path.push_str(&dir_name); 191 | subdir_entries.push(recursive_walk( 192 | root_dir, 193 | &dir_entry.path(), 194 | Some(&subdir_module_path) 195 | )); 196 | } 197 | } else if let Some(entry) = parse_docset_entry(&module_path, root_dir, &dir_entry.path()) { 198 | entries.push(entry); 199 | } 200 | } 201 | for v in subdir_entries { 202 | entries.extend(v?); 203 | } 204 | Ok(entries) 205 | } 206 | 207 | fn generate_sqlite_index>(docset_dir: P, entries: Vec) -> Result<()> { 208 | let mut conn_path = docset_dir.as_ref().to_owned(); 209 | conn_path.push("Contents"); 210 | conn_path.push("Resources"); 211 | conn_path.push("docSet.dsidx"); 212 | let mut conn = Connection::open(&conn_path).context(SqliteSnafu)?; 213 | conn.execute( 214 | "CREATE TABLE searchIndex(id INTEGER PRIMARY KEY, name TEXT, type TEXT, path TEXT); 215 | CREATE UNIQUE INDEX anchor ON searchIndex (name, type, path); 216 | )", 217 | params![] 218 | ) 219 | .context(SqliteSnafu)?; 220 | let transaction = conn.transaction().context(SqliteSnafu)?; 221 | { 222 | let mut stmt = transaction 223 | .prepare("INSERT INTO searchIndex (name, type, path) VALUES (?1, ?2, ?3)") 224 | .context(SqliteSnafu)?; 225 | for entry in entries { 226 | stmt.execute([ 227 | entry.name, 228 | entry.ty.to_string(), 229 | entry.path.to_str().unwrap().to_owned() 230 | ]) 231 | .context(SqliteSnafu)?; 232 | } 233 | } 234 | transaction.commit().context(SqliteSnafu)?; 235 | Ok(()) 236 | } 237 | 238 | fn copy_dir_recursive, Pd: AsRef>(src: Ps, dst: Pd) -> Result<()> { 239 | create_dir_all(&dst).context(IoWriteSnafu)?; 240 | for entry in read_dir(&src).context(IoReadSnafu)? { 241 | let entry = entry.context(IoWriteSnafu)?.path(); 242 | if entry.is_dir() { 243 | let mut dst_dir = dst.as_ref().to_owned(); 244 | dst_dir.push(entry.strip_prefix(&src).unwrap()); 245 | copy_dir_recursive(entry, dst_dir)?; 246 | } else if entry.is_file() { 247 | let mut dst_file = dst.as_ref().to_owned(); 248 | dst_file.push(entry.file_name().unwrap()); 249 | copy(entry, dst_file).context(IoWriteSnafu)?; 250 | } 251 | } 252 | Ok(()) 253 | } 254 | 255 | fn write_metadata>( 256 | docset_root_dir: P, 257 | docset_name: &str, 258 | index_package: Option, 259 | platform_family: Option 260 | ) -> Result<()> { 261 | let mut info_plist_path = docset_root_dir.as_ref().to_owned(); 262 | info_plist_path.push("Contents"); 263 | info_plist_path.push("Info.plist"); 264 | 265 | let mut info_file = File::create(info_plist_path).context(IoWriteSnafu)?; 266 | let index_entry = if let Some(index_package) = index_package { 267 | format!( 268 | "dashIndexFilePath 269 | {}/index.html", 270 | index_package 271 | ) 272 | } else { 273 | String::new() 274 | }; 275 | let identifier_entry = if let Some(platform_family) = &platform_family { 276 | format!( 277 | "CFBundleIdentifier 278 | {}", 279 | platform_family 280 | ) 281 | } else { 282 | String::new() 283 | }; 284 | 285 | let platform_family_entry = if let Some(platform_family) = &platform_family { 286 | format!( 287 | "DocSetPlatformFamily 288 | {}", 289 | platform_family 290 | ) 291 | } else { 292 | String::new() 293 | }; 294 | 295 | write!(info_file, 296 | "\ 297 | 298 | 299 | 300 | 301 | {} 302 | CFBundleName 303 | {} 304 | {} 305 | {} 306 | isDashDocset 307 | 308 | isJavaScriptEnabled 309 | 310 | 311 | ", 312 | identifier_entry, docset_name, index_entry, platform_family_entry).context(IoWriteSnafu)?; 313 | Ok(()) 314 | } 315 | 316 | fn get_workspace_name(metadata: &Metadata) -> String { 317 | metadata.workspace_root 318 | .file_name() 319 | // I doubt this could be None, but let's have a fallback just in case. 320 | .unwrap_or("generated-docset") 321 | .to_owned() 322 | } 323 | 324 | /// Determine the name we will use for the generated docset. 325 | /// If a name was provided on the command line, we use this one. 326 | /// If no name was provided: 327 | /// * If a single package was requested, use this one. 328 | /// * Otherwise, if there is a workspace root package and we have been asked to generate 329 | /// documentation for it, use this one. 330 | /// * Otherwise, generate a name from the workspace "name" and the list of workspace member packages being built. 331 | fn get_docset_name(cfg: &DocsetParams, metadata: &Metadata) -> String { 332 | if let Some(docset_name) = &cfg.docset_name { 333 | return docset_name.to_owned(); 334 | } 335 | 336 | let (included, _excluded) = cfg.workspace.partition_packages(metadata); 337 | 338 | if included.len() == 1 { 339 | return included[0].name.to_owned(); 340 | } 341 | 342 | // Rust 1.64 will stabilize the `let_chains` feature which should allow combining both conditionals 343 | if let Some(root_package) = metadata.root_package() { 344 | if included.contains(&root_package) { 345 | return root_package.name.to_owned(); 346 | } 347 | } 348 | 349 | let package_list_str = included 350 | .into_iter() 351 | .filter_map(|p| { 352 | if cfg.workspace.exclude.contains(&p.name) { None } else { Some(p.name.as_str()) } 353 | }) 354 | .collect::>() 355 | .join(", "); 356 | 357 | format!("{}: {}", get_workspace_name(metadata), package_list_str) 358 | } 359 | 360 | /// Return the name of the package that should be used for the docset index, if any. 361 | /// This uses the same rules as docset name selection, except no index is a valid option. 362 | fn get_docset_index(cfg: &DocsetParams, metadata: &Metadata) -> Option { 363 | if cfg.docset_index.is_some() { 364 | return cfg.docset_index.clone(); 365 | } 366 | 367 | match (cfg.workspace.all, cfg.workspace.package.len()) { 368 | (false, 1) => Some(cfg.workspace.package[0].to_owned()), 369 | _ => metadata.root_package().map(|p| p.name.to_owned()) 370 | } 371 | } 372 | 373 | /// Return the keyword that should be used for the docset platform family, if any. 374 | /// This uses the same rules as docset name selection, except no identifier is a valid option. 375 | fn get_docset_platform_family(cfg: &DocsetParams, metadata: &Metadata) -> Option { 376 | if let Some(platform_family) = &cfg.platform_family { 377 | return Some(platform_family.to_owned()); 378 | } 379 | 380 | match (cfg.workspace.all, cfg.workspace.package.len()) { 381 | (false, 1) => Some(cfg.workspace.package[0].to_owned()), 382 | _ => metadata.root_package().map(|p| p.name.to_owned()) 383 | } 384 | } 385 | 386 | pub fn generate_docset(cfg: DocsetParams) -> Result<()> { 387 | // Step 1: generate rustdoc 388 | // Figure out for which crate to build the doc and invoke cargo doc. 389 | // If no crate is specified, run cargo doc for the current crate/workspace. 390 | if cfg.workspace.all { 391 | ensure!( 392 | cfg.workspace.exclude.is_empty(), 393 | ArgsSnafu { 394 | msg: "--exclude must be used with --all" 395 | } 396 | ); 397 | } 398 | 399 | let cargo_metadata = cfg.manifest.metadata().exec().context(CargoMetadataSnafu)?; 400 | 401 | // Clean the documentation directory if the user didn't explicitly ask not to clean it. 402 | if !cfg.no_clean { 403 | println!("Running 'cargo clean --doc'..."); 404 | let mut cargo_clean_args = vec!["clean".to_owned()]; 405 | if let Some(ref manifest_path) = &cfg.manifest.manifest_path { 406 | cargo_clean_args.push("--manifest-path".to_owned()); 407 | cargo_clean_args.push(manifest_path.to_string_lossy().to_string()); 408 | } 409 | let cargo_clean_result = Command::new("cargo") 410 | .args(cargo_clean_args) 411 | .arg("--doc") 412 | .status() 413 | .context(SpawnSnafu)?; 414 | if !cargo_clean_result.success() { 415 | return CargoCleanSnafu { 416 | code: cargo_clean_result.code() 417 | } 418 | .fail(); 419 | } 420 | } 421 | // Good to go, generate the documentation. 422 | println!("Running 'cargo doc'..."); 423 | let args = cfg.clone().into_args(); 424 | let cargo_doc_result = Command::new("cargo") 425 | .arg("doc") 426 | .args(args) 427 | .status() 428 | .context(SpawnSnafu)?; 429 | if !cargo_doc_result.success() { 430 | return CargoDocSnafu { 431 | code: cargo_doc_result.code() 432 | } 433 | .fail(); 434 | } 435 | 436 | // Step 2: iterate over all the html files in the doc directory and parse the filenames 437 | let docset_name = get_docset_name(&cfg, &cargo_metadata); 438 | let mut docset_root_dir = cfg 439 | .target_dir 440 | .clone() 441 | .unwrap_or_else(|| cargo_metadata.target_directory.clone().into_std_path_buf()); 442 | let mut rustdoc_root_dir = docset_root_dir.clone(); 443 | rustdoc_root_dir.push("doc"); 444 | docset_root_dir.push("docset"); 445 | let platform_family = get_docset_platform_family(&cfg, &cargo_metadata); 446 | docset_root_dir.push( 447 | format!("{}.docset", 448 | platform_family.clone() 449 | .unwrap_or_else(|| get_workspace_name(&cargo_metadata)))); 450 | let entries = recursive_walk(&rustdoc_root_dir, &rustdoc_root_dir, None)?; 451 | 452 | // Step 3: generate the SQLite database 453 | // At this point, we need to start writing into the output docset directory, so create the 454 | // hirerarchy, and clean it first if it already exists. 455 | if docset_root_dir.exists() { 456 | remove_dir_all(&docset_root_dir).context(IoWriteSnafu)?; 457 | } 458 | let mut docset_hierarchy = docset_root_dir.clone(); 459 | docset_hierarchy.push("Contents"); 460 | docset_hierarchy.push("Resources"); 461 | create_dir_all(&docset_hierarchy).context(IoWriteSnafu)?; 462 | generate_sqlite_index(&docset_root_dir, entries)?; 463 | 464 | // Step 4: Copy the rustdoc to the docset directory 465 | docset_hierarchy.push("Documents"); 466 | copy_dir_recursive(&rustdoc_root_dir, &docset_hierarchy)?; 467 | 468 | // Step 5: add the required metadata 469 | if platform_family.is_none() { 470 | warn("no platform family was provided and none could be generated, consider adding the '--platform-family' option."); 471 | } 472 | 473 | write_metadata( 474 | &docset_root_dir, 475 | &docset_name, 476 | get_docset_index(&cfg, &cargo_metadata), 477 | platform_family 478 | )?; 479 | 480 | println!( 481 | "Docset successfully generated in {}", 482 | docset_root_dir.to_string_lossy() 483 | ); 484 | 485 | Ok(()) 486 | } 487 | -------------------------------------------------------------------------------- /src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod generate; 2 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use snafu::Snafu; 2 | 3 | use std::result::Result as StdResult; 4 | 5 | #[derive(Debug, Snafu)] 6 | #[snafu(visibility(pub(crate)))] 7 | pub enum Error { 8 | #[snafu(display("Cargo doc exited with code {:?}.", code))] 9 | CargoDoc { 10 | code: Option 11 | }, 12 | #[snafu(display("Cargo clean exited with code {:?}.", code))] 13 | CargoClean { 14 | code: Option 15 | }, 16 | #[snafu(display("Error running 'cargo metadata' command: {}", source))] 17 | CargoMetadata { 18 | source: cargo_metadata::Error 19 | }, 20 | #[snafu(display("Process spawn error: {}", source))] 21 | Spawn { 22 | source: std::io::Error 23 | }, 24 | #[snafu(display("Cannot determine the current directory: {}", source))] 25 | Cwd { 26 | source: std::io::Error 27 | }, 28 | #[snafu(display("I/O read error: {}", source))] 29 | IoRead { 30 | source: std::io::Error 31 | }, 32 | #[snafu(display("I/O write error: {}", source))] 33 | IoWrite { 34 | source: std::io::Error 35 | }, 36 | #[snafu(display("SQLite error {}", source))] 37 | Sqlite { 38 | source: rusqlite::Error 39 | }, 40 | #[snafu(display("CLI arguments error: {}", msg))] 41 | Args { 42 | msg: &'static str 43 | } 44 | } 45 | 46 | pub type Result = StdResult; 47 | -------------------------------------------------------------------------------- /src/io.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | pub fn warn(s: &str) { 4 | #[cfg(feature = "color")] 5 | warn_color(s); 6 | #[cfg(not(feature = "color"))] 7 | warn_no_color(s); 8 | } 9 | 10 | fn warn_no_color(s: &str) { 11 | let mut stderr = std::io::stderr(); 12 | writeln!(&mut stderr, "Warning: {}", s).unwrap(); 13 | } 14 | 15 | #[cfg(feature = "color")] 16 | fn warn_color(s: &str) { 17 | use termcolor::*; 18 | 19 | if atty::is(atty::Stream::Stderr) { 20 | let mut stderr = StandardStream::stderr(ColorChoice::Auto); 21 | stderr.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)).set_bold(true)).unwrap(); 22 | write!(&mut stderr, "Warning: ").unwrap(); 23 | stderr.set_color(ColorSpec::new().set_fg(None).set_bold(false)).unwrap(); 24 | writeln!(&mut stderr, "{}", s).unwrap(); 25 | } 26 | else { 27 | warn_no_color(s); 28 | } 29 | } 30 | 31 | pub fn error(s: &str) { 32 | #[cfg(feature = "color")] 33 | error_color(s); 34 | #[cfg(not(feature = "color"))] 35 | error_no_color(s); 36 | } 37 | 38 | fn error_no_color(s: &str) { 39 | let mut stderr = std::io::stderr(); 40 | writeln!(&mut stderr, "Error: {}", s).unwrap(); 41 | } 42 | 43 | #[cfg(feature = "color")] 44 | fn error_color(s: &str) { 45 | use termcolor::*; 46 | 47 | if atty::is(atty::Stream::Stderr) { 48 | let mut stderr = StandardStream::stderr(ColorChoice::Auto); 49 | stderr.set_color(ColorSpec::new().set_fg(Some(Color::Red)).set_bold(true)).unwrap(); 50 | write!(&mut stderr, "Error: ").unwrap(); 51 | stderr.set_color(ColorSpec::new().set_fg(None).set_bold(false)).unwrap(); 52 | writeln!(&mut stderr, "{}", s).unwrap(); 53 | } 54 | else { 55 | error_no_color(s); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::{Parser, Subcommand, Args}; 4 | 5 | mod commands; 6 | mod error; 7 | mod io; 8 | 9 | use crate::error::*; 10 | use commands::generate::generate_docset; 11 | 12 | #[derive(Debug, Parser)] 13 | struct Cli { 14 | #[clap(subcommand)] 15 | command: Commands 16 | } 17 | 18 | #[derive(Args, Default, Debug, Clone)] 19 | /// Generate a docset. This is currently the only available command, and should remain the 20 | /// default one in the future if new ones are added. 21 | pub struct DocsetParams { 22 | #[clap(flatten)] 23 | pub manifest: clap_cargo::Manifest, 24 | #[clap(flatten)] 25 | pub workspace: clap_cargo::Workspace, 26 | #[clap(flatten)] 27 | features: clap_cargo::Features, 28 | #[clap(long("no-deps"))] 29 | /// Do not document dependencies. 30 | pub no_dependencies: bool, 31 | #[clap(long("document-private-items"))] 32 | /// Generate documentation for private items. 33 | pub doc_private_items: bool, 34 | #[clap(long, value_parser)] 35 | /// Build documentation for the specified target triple. 36 | pub target: Option, 37 | #[clap(long, value_parser)] 38 | /// Override the workspace target directory. 39 | pub target_dir: Option, 40 | #[clap(long, action)] 41 | /// Do not clean the doc directory before generating the rustdoc. 42 | pub no_clean: bool, 43 | #[clap(long, action)] 44 | /// Document only this package's library. 45 | pub lib: bool, 46 | #[clap(long, value_parser)] 47 | /// Document only the specified binary. 48 | pub bin: Vec, 49 | #[clap(long, action)] 50 | /// Document all binaries. 51 | pub bins: bool, 52 | #[clap(long, value_parser)] 53 | /// Specify or override the name of the docset, this is the display name used by your docset 54 | /// browser. 55 | pub docset_name: Option, 56 | #[clap(long, value_parser, name("PACKAGE"))] 57 | /// Specify or override the package whose index will be used as the docset index page. 58 | pub docset_index: Option, 59 | #[clap(long, value_parser)] 60 | /// Specify or override the docset platform family, this is used as the keyword you can specify 61 | /// in your docset browser search bar to search this specific docset). 62 | pub platform_family: Option 63 | } 64 | 65 | impl DocsetParams { 66 | /// Generate args for the cargo doc invocation. 67 | fn into_args(self) -> Vec { 68 | let mut args = Vec::new(); 69 | if let Some(manifest_path) = self.manifest.manifest_path { 70 | args.push("--manifest-path".to_owned()); 71 | args.push(manifest_path.to_string_lossy().to_string()); 72 | } 73 | if self.workspace.workspace || self.workspace.all { 74 | args.push("--workspace".to_owned()); 75 | for exclude in self.workspace.exclude { 76 | args.extend_from_slice(&["--exclude".to_owned(), exclude]); 77 | } 78 | } else { 79 | for package in self.workspace.package { 80 | args.extend_from_slice(&["--package".to_owned(), package]); 81 | } 82 | } 83 | if self.no_dependencies { 84 | args.push("--no-deps".to_owned()) 85 | } 86 | if self.doc_private_items { 87 | args.push("--document-private-items".to_owned()) 88 | } 89 | if !self.features.features.is_empty() { 90 | args.push("--features".to_owned()); 91 | args.extend(self.features.features); 92 | } 93 | if self.features.no_default_features { 94 | args.push("--no-default-features".to_owned()) 95 | } 96 | if self.features.all_features { 97 | args.push("--all-features".to_owned()) 98 | } 99 | if let Some(target) = self.target { 100 | args.push("--target".to_owned()); 101 | args.push(target); 102 | } 103 | if let Some(target_dir) = self.target_dir { 104 | args.push("--target-dir".to_owned()); 105 | args.push(target_dir.to_string_lossy().to_string()); 106 | } 107 | if self.lib { 108 | args.push("--lib".to_owned()); 109 | } 110 | if self.bins { 111 | args.push("bins".to_owned()); 112 | } 113 | args 114 | } 115 | } 116 | 117 | #[derive(Debug, Subcommand)] 118 | enum Commands { 119 | Docset(DocsetParams) 120 | } 121 | 122 | fn run(cli: Cli) -> Result<()> { 123 | match cli.command { 124 | Commands::Docset(params) => { 125 | generate_docset(params) 126 | } 127 | } 128 | } 129 | 130 | fn main() { 131 | let cli = Cli::parse(); 132 | if let Err(e) = run(cli) { 133 | io::error(&e.to_string()) 134 | } 135 | } 136 | 137 | #[cfg(test)] 138 | mod tests { 139 | //use std::{path::PathBuf, str::FromStr}; 140 | 141 | use clap::Parser; 142 | 143 | use crate::{DocsetParams, Commands}; 144 | 145 | use super::Cli; 146 | 147 | #[test] 148 | fn clap_verify_cli() { 149 | use clap::CommandFactory; 150 | Cli::command().debug_assert(); 151 | } 152 | 153 | #[test] 154 | fn test_default_docset_params_into_args_is_empty() { 155 | let params = DocsetParams::default(); 156 | let args = params.into_args(); 157 | assert!(args.is_empty()); 158 | } 159 | 160 | const TEST_DOCSET_PARAMS_1_MANIFEST_PATH: &str = "../somewhere_else/"; 161 | const TEST_DOCSET_PARAMS_1_ENABLED_FEATURE: &str = "feature1"; 162 | const TEST_DOCSET_PARAMS_1_EXCLUDED: &str = "excluded_package"; 163 | const TEST_DOCSET_PARAMS_1_TARGET: &str = "x86_64-pc-windows-gnu"; 164 | const TEST_DOCSET_PARAMS_1_DOCSET_NAME: &str = "Unit test docset 1"; 165 | const TEST_DOCSET_PARAMS_1_DOCSET_INDEX: &str = "member1"; 166 | const TEST_DOCSET_PARAMS_1_PLATFORM_FAMILY: &str = "dp1"; 167 | const TEST_DOCSET_PARAMS_1_ARGS: &[&str] = &[ 168 | "cargo", 169 | "docset", 170 | "--manifest-path", 171 | TEST_DOCSET_PARAMS_1_MANIFEST_PATH, 172 | "--workspace", 173 | "--no-default-features", 174 | "--features", 175 | TEST_DOCSET_PARAMS_1_ENABLED_FEATURE, 176 | "--exclude", 177 | TEST_DOCSET_PARAMS_1_EXCLUDED, 178 | "--no-deps", 179 | "--document-private-items", 180 | "--target", 181 | TEST_DOCSET_PARAMS_1_TARGET, 182 | "--no-clean", 183 | "--docset-name", 184 | TEST_DOCSET_PARAMS_1_DOCSET_NAME, 185 | "--docset-index", 186 | TEST_DOCSET_PARAMS_1_DOCSET_INDEX, 187 | "--platform-family", 188 | TEST_DOCSET_PARAMS_1_PLATFORM_FAMILY 189 | ]; 190 | 191 | fn get_test_docset_params_1() -> Cli { 192 | Cli::parse_from(TEST_DOCSET_PARAMS_1_ARGS) 193 | } 194 | 195 | #[test] 196 | fn test_parse_docset_params_1() { 197 | let res = Cli::try_parse_from(TEST_DOCSET_PARAMS_1_ARGS); 198 | assert!(res.is_ok(), "Could not parse CLI arguments: {}", res.err().unwrap()); 199 | } 200 | 201 | #[test] 202 | fn test_validate_matches_docset_params_1() { 203 | let params = get_test_docset_params_1(); 204 | match params.command { 205 | Commands::Docset(params) => { 206 | assert!(params.manifest.manifest_path.is_some()); 207 | assert_eq!(params.manifest.manifest_path.unwrap().to_string_lossy(), TEST_DOCSET_PARAMS_1_MANIFEST_PATH); 208 | 209 | assert!(params.workspace.workspace); 210 | assert!(params.workspace.package.is_empty()); 211 | assert_eq!(params.workspace.exclude.len(), 1); 212 | assert_eq!(params.workspace.exclude[0], TEST_DOCSET_PARAMS_1_EXCLUDED); 213 | 214 | assert!(params.features.no_default_features); 215 | assert_eq!(params.features.features.len(), 1); 216 | assert_eq!(params.features.features[0], TEST_DOCSET_PARAMS_1_ENABLED_FEATURE); 217 | 218 | assert!(params.no_dependencies); 219 | 220 | assert!(params.doc_private_items); 221 | 222 | assert!(params.target.is_some()); 223 | assert_eq!(params.target.unwrap(), TEST_DOCSET_PARAMS_1_TARGET); 224 | 225 | assert!(params.target_dir.is_none()); 226 | 227 | assert!(params.no_clean); 228 | 229 | assert!(!params.lib); 230 | 231 | assert!(params.bin.is_empty()); 232 | 233 | assert!(!params.bins); 234 | 235 | assert!(params.docset_name.is_some()); 236 | assert_eq!(params.docset_name.unwrap(), TEST_DOCSET_PARAMS_1_DOCSET_NAME); 237 | 238 | assert!(params.docset_index.is_some()); 239 | assert_eq!(params.docset_index.unwrap(), TEST_DOCSET_PARAMS_1_DOCSET_INDEX); 240 | 241 | assert!(params.platform_family.is_some()); 242 | assert_eq!(params.platform_family.unwrap(), TEST_DOCSET_PARAMS_1_PLATFORM_FAMILY); 243 | } 244 | } 245 | } 246 | 247 | #[test] 248 | fn test_validate_docset_params_1_into_args() { 249 | let params = get_test_docset_params_1(); 250 | match params.command { 251 | Commands::Docset(params) => { 252 | let mut cargo_doc_args = params.into_args(); 253 | 254 | let expected_flags = &[ 255 | "--workspace", 256 | "--no-default-features", 257 | "--no-deps", 258 | "--document-private-items", 259 | ]; 260 | 261 | let expected_pairs = &[ 262 | ("--manifest-path", TEST_DOCSET_PARAMS_1_MANIFEST_PATH), 263 | ("--features", TEST_DOCSET_PARAMS_1_ENABLED_FEATURE), 264 | ("--exclude", TEST_DOCSET_PARAMS_1_EXCLUDED), 265 | ("--target", TEST_DOCSET_PARAMS_1_TARGET) 266 | ]; 267 | 268 | for flag in expected_flags { 269 | assert!(cargo_doc_args.contains(&flag.to_string()), "Expected flag {} in vector {:?}", flag, cargo_doc_args); 270 | let i = cargo_doc_args.iter().enumerate().find(|(_i, arg)| flag == arg).unwrap().0; 271 | cargo_doc_args.remove(i); 272 | } 273 | 274 | for pair in expected_pairs { 275 | let mut i = -1; 276 | 'inner: for (j, sub) in cargo_doc_args.chunks_exact(2).enumerate() { 277 | if sub[0] == pair.0 && sub[1] == pair.1 { 278 | i = j as i32; 279 | break 'inner; 280 | } 281 | } 282 | 283 | assert!(i >= 0, "Expected argument pair {:?} in arguments {:?}", pair, cargo_doc_args); 284 | } 285 | } 286 | } 287 | } 288 | } 289 | --------------------------------------------------------------------------------