├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── README.tpl ├── docs └── uml │ ├── class.puml │ └── sequence.puml ├── src ├── config │ ├── badges.rs │ ├── manifest.rs │ ├── mod.rs │ └── project.rs ├── helper.rs ├── lib.rs ├── main.rs └── readme │ ├── extract.rs │ ├── mod.rs │ ├── process.rs │ └── template.rs └── tests ├── .gitignore ├── alternate-input.rs ├── alternate-template.rs ├── append-license.rs ├── badges.rs ├── badges ├── .gitignore ├── Cargo.toml ├── README.tpl └── src │ └── lib.rs ├── default-behavior.rs ├── entrypoint-resolution.rs ├── entrypoint-resolution ├── cargo-bin │ ├── Cargo.toml │ └── src │ │ └── bin │ │ └── bin.rs ├── cargo-lib │ ├── Cargo.toml │ └── src │ │ ├── alt.rs │ │ └── bin │ │ └── bin.rs ├── lib │ ├── Cargo.toml │ └── src │ │ ├── bin │ │ └── bin.rs │ │ ├── lib.rs │ │ └── main.rs └── main │ ├── Cargo.toml │ └── src │ └── main.rs ├── multiline-doc.rs ├── multiple-bin-fail.rs ├── multiple-bin-fail └── Cargo.toml ├── no-entrypoint-fail.rs ├── no-entrypoint-fail └── Cargo.toml ├── no-template.rs ├── project-with-version.rs ├── project-with-version ├── .gitignore ├── Cargo.toml ├── README.tpl └── src │ └── lib.rs └── test-project ├── .gitignore ├── Cargo.toml ├── NOTITLE.tpl ├── OTHER.tpl ├── README.tpl └── src ├── lib.rs ├── multiline.rs ├── no_docs.rs ├── other.rs └── single_line.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Build 17 | run: cargo build --verbose 18 | - name: Run tests 19 | run: cargo test --verbose 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | target 3 | *.t.err 4 | -------------------------------------------------------------------------------- /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.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [3.3.1] - 2023-11-06 11 | 12 | ### Changed 13 | 14 | - Update dependencies including a major version change (toml v0.7 to v0.8). [#86] 15 | 16 | ### Fixed 17 | 18 | - Circle CI badge was incorrect [#28] 19 | 20 | [#86]: https://github.com/webern/cargo-readme/pull/86 21 | [#28]: https://github.com/webern/cargo-readme/pull/28 22 | 23 | ## [3.3.0] - 2013-09-05 24 | 25 | ## Changed 26 | 27 | - Update dependencies including major version changes: [#84] 28 | - clap v2 to v4 29 | - toml v0.5 tp v0.7 30 | - Update documentation to reflect that the maintainer of this repository has changed/ [#84] 31 | 32 | [#84]: https://github.com/webern/cargo-readme/pull/84 33 | 34 | [unreleased]: https://github.com/webern/cargo-readme/compare/v3.3.1...HEAD 35 | [3.3.1]: https://github.com/webern/cargo-readme/compare/v3.3.0...v1.3.1 36 | [3.3.0]: https://github.com/webern/cargo-readme/compare/v3.2.0...v3.3.0 37 | -------------------------------------------------------------------------------- /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 = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.2" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "anstream" 31 | version = "0.6.4" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" 34 | dependencies = [ 35 | "anstyle", 36 | "anstyle-parse", 37 | "anstyle-query", 38 | "anstyle-wincon", 39 | "colorchoice", 40 | "utf8parse", 41 | ] 42 | 43 | [[package]] 44 | name = "anstyle" 45 | version = "1.0.4" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" 48 | 49 | [[package]] 50 | name = "anstyle-parse" 51 | version = "0.2.2" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" 54 | dependencies = [ 55 | "utf8parse", 56 | ] 57 | 58 | [[package]] 59 | name = "anstyle-query" 60 | version = "1.0.0" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 63 | dependencies = [ 64 | "windows-sys", 65 | ] 66 | 67 | [[package]] 68 | name = "anstyle-wincon" 69 | version = "3.0.1" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" 72 | dependencies = [ 73 | "anstyle", 74 | "windows-sys", 75 | ] 76 | 77 | [[package]] 78 | name = "assert_cli" 79 | version = "0.6.3" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "a29ab7c0ed62970beb0534d637a8688842506d0ff9157de83286dacd065c8149" 82 | dependencies = [ 83 | "colored", 84 | "difference", 85 | "environment", 86 | "failure", 87 | "failure_derive", 88 | "serde_json", 89 | ] 90 | 91 | [[package]] 92 | name = "backtrace" 93 | version = "0.3.69" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" 96 | dependencies = [ 97 | "addr2line", 98 | "cc", 99 | "cfg-if", 100 | "libc", 101 | "miniz_oxide", 102 | "object", 103 | "rustc-demangle", 104 | ] 105 | 106 | [[package]] 107 | name = "bitflags" 108 | version = "2.4.1" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" 111 | 112 | [[package]] 113 | name = "cargo-readme" 114 | version = "3.3.1" 115 | dependencies = [ 116 | "assert_cli", 117 | "clap", 118 | "lazy_static", 119 | "percent-encoding", 120 | "regex", 121 | "serde", 122 | "toml", 123 | ] 124 | 125 | [[package]] 126 | name = "cc" 127 | version = "1.0.83" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 130 | dependencies = [ 131 | "libc", 132 | ] 133 | 134 | [[package]] 135 | name = "cfg-if" 136 | version = "1.0.0" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 139 | 140 | [[package]] 141 | name = "clap" 142 | version = "4.4.7" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "ac495e00dcec98c83465d5ad66c5c4fabd652fd6686e7c6269b117e729a6f17b" 145 | dependencies = [ 146 | "clap_builder", 147 | "clap_derive", 148 | ] 149 | 150 | [[package]] 151 | name = "clap_builder" 152 | version = "4.4.7" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "c77ed9a32a62e6ca27175d00d29d05ca32e396ea1eb5fb01d8256b669cec7663" 155 | dependencies = [ 156 | "anstream", 157 | "anstyle", 158 | "clap_lex", 159 | "strsim", 160 | ] 161 | 162 | [[package]] 163 | name = "clap_derive" 164 | version = "4.4.7" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" 167 | dependencies = [ 168 | "heck", 169 | "proc-macro2", 170 | "quote", 171 | "syn 2.0.39", 172 | ] 173 | 174 | [[package]] 175 | name = "clap_lex" 176 | version = "0.6.0" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" 179 | 180 | [[package]] 181 | name = "colorchoice" 182 | version = "1.0.0" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 185 | 186 | [[package]] 187 | name = "colored" 188 | version = "1.9.4" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "5a5f741c91823341bebf717d4c71bda820630ce065443b58bd1b7451af008355" 191 | dependencies = [ 192 | "is-terminal", 193 | "lazy_static", 194 | "winapi", 195 | ] 196 | 197 | [[package]] 198 | name = "difference" 199 | version = "2.0.0" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" 202 | 203 | [[package]] 204 | name = "environment" 205 | version = "0.1.1" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "1f4b14e20978669064c33b4c1e0fb4083412e40fe56cbea2eae80fd7591503ee" 208 | 209 | [[package]] 210 | name = "equivalent" 211 | version = "1.0.1" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 214 | 215 | [[package]] 216 | name = "errno" 217 | version = "0.3.5" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" 220 | dependencies = [ 221 | "libc", 222 | "windows-sys", 223 | ] 224 | 225 | [[package]] 226 | name = "failure" 227 | version = "0.1.8" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" 230 | dependencies = [ 231 | "backtrace", 232 | "failure_derive", 233 | ] 234 | 235 | [[package]] 236 | name = "failure_derive" 237 | version = "0.1.8" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" 240 | dependencies = [ 241 | "proc-macro2", 242 | "quote", 243 | "syn 1.0.109", 244 | "synstructure", 245 | ] 246 | 247 | [[package]] 248 | name = "gimli" 249 | version = "0.28.0" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" 252 | 253 | [[package]] 254 | name = "hashbrown" 255 | version = "0.14.2" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" 258 | 259 | [[package]] 260 | name = "heck" 261 | version = "0.4.1" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 264 | 265 | [[package]] 266 | name = "hermit-abi" 267 | version = "0.3.3" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" 270 | 271 | [[package]] 272 | name = "indexmap" 273 | version = "2.1.0" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" 276 | dependencies = [ 277 | "equivalent", 278 | "hashbrown", 279 | ] 280 | 281 | [[package]] 282 | name = "is-terminal" 283 | version = "0.4.9" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" 286 | dependencies = [ 287 | "hermit-abi", 288 | "rustix", 289 | "windows-sys", 290 | ] 291 | 292 | [[package]] 293 | name = "itoa" 294 | version = "1.0.9" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" 297 | 298 | [[package]] 299 | name = "lazy_static" 300 | version = "1.4.0" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 303 | 304 | [[package]] 305 | name = "libc" 306 | version = "0.2.150" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" 309 | 310 | [[package]] 311 | name = "linux-raw-sys" 312 | version = "0.4.10" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" 315 | 316 | [[package]] 317 | name = "memchr" 318 | version = "2.6.4" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" 321 | 322 | [[package]] 323 | name = "miniz_oxide" 324 | version = "0.7.1" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" 327 | dependencies = [ 328 | "adler", 329 | ] 330 | 331 | [[package]] 332 | name = "object" 333 | version = "0.32.1" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" 336 | dependencies = [ 337 | "memchr", 338 | ] 339 | 340 | [[package]] 341 | name = "percent-encoding" 342 | version = "2.3.0" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" 345 | 346 | [[package]] 347 | name = "proc-macro2" 348 | version = "1.0.69" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" 351 | dependencies = [ 352 | "unicode-ident", 353 | ] 354 | 355 | [[package]] 356 | name = "quote" 357 | version = "1.0.33" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 360 | dependencies = [ 361 | "proc-macro2", 362 | ] 363 | 364 | [[package]] 365 | name = "regex" 366 | version = "1.10.2" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" 369 | dependencies = [ 370 | "aho-corasick", 371 | "memchr", 372 | "regex-automata", 373 | "regex-syntax", 374 | ] 375 | 376 | [[package]] 377 | name = "regex-automata" 378 | version = "0.4.3" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" 381 | dependencies = [ 382 | "aho-corasick", 383 | "memchr", 384 | "regex-syntax", 385 | ] 386 | 387 | [[package]] 388 | name = "regex-syntax" 389 | version = "0.8.2" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" 392 | 393 | [[package]] 394 | name = "rustc-demangle" 395 | version = "0.1.23" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 398 | 399 | [[package]] 400 | name = "rustix" 401 | version = "0.38.21" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" 404 | dependencies = [ 405 | "bitflags", 406 | "errno", 407 | "libc", 408 | "linux-raw-sys", 409 | "windows-sys", 410 | ] 411 | 412 | [[package]] 413 | name = "ryu" 414 | version = "1.0.15" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" 417 | 418 | [[package]] 419 | name = "serde" 420 | version = "1.0.190" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" 423 | dependencies = [ 424 | "serde_derive", 425 | ] 426 | 427 | [[package]] 428 | name = "serde_derive" 429 | version = "1.0.190" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" 432 | dependencies = [ 433 | "proc-macro2", 434 | "quote", 435 | "syn 2.0.39", 436 | ] 437 | 438 | [[package]] 439 | name = "serde_json" 440 | version = "1.0.108" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" 443 | dependencies = [ 444 | "itoa", 445 | "ryu", 446 | "serde", 447 | ] 448 | 449 | [[package]] 450 | name = "serde_spanned" 451 | version = "0.6.4" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" 454 | dependencies = [ 455 | "serde", 456 | ] 457 | 458 | [[package]] 459 | name = "strsim" 460 | version = "0.10.0" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 463 | 464 | [[package]] 465 | name = "syn" 466 | version = "1.0.109" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 469 | dependencies = [ 470 | "proc-macro2", 471 | "quote", 472 | "unicode-ident", 473 | ] 474 | 475 | [[package]] 476 | name = "syn" 477 | version = "2.0.39" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" 480 | dependencies = [ 481 | "proc-macro2", 482 | "quote", 483 | "unicode-ident", 484 | ] 485 | 486 | [[package]] 487 | name = "synstructure" 488 | version = "0.12.6" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" 491 | dependencies = [ 492 | "proc-macro2", 493 | "quote", 494 | "syn 1.0.109", 495 | "unicode-xid", 496 | ] 497 | 498 | [[package]] 499 | name = "toml" 500 | version = "0.8.6" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "8ff9e3abce27ee2c9a37f9ad37238c1bdd4e789c84ba37df76aa4d528f5072cc" 503 | dependencies = [ 504 | "serde", 505 | "serde_spanned", 506 | "toml_datetime", 507 | "toml_edit", 508 | ] 509 | 510 | [[package]] 511 | name = "toml_datetime" 512 | version = "0.6.5" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" 515 | dependencies = [ 516 | "serde", 517 | ] 518 | 519 | [[package]] 520 | name = "toml_edit" 521 | version = "0.20.7" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" 524 | dependencies = [ 525 | "indexmap", 526 | "serde", 527 | "serde_spanned", 528 | "toml_datetime", 529 | "winnow", 530 | ] 531 | 532 | [[package]] 533 | name = "unicode-ident" 534 | version = "1.0.12" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 537 | 538 | [[package]] 539 | name = "unicode-xid" 540 | version = "0.2.4" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" 543 | 544 | [[package]] 545 | name = "utf8parse" 546 | version = "0.2.1" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 549 | 550 | [[package]] 551 | name = "winapi" 552 | version = "0.3.9" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 555 | dependencies = [ 556 | "winapi-i686-pc-windows-gnu", 557 | "winapi-x86_64-pc-windows-gnu", 558 | ] 559 | 560 | [[package]] 561 | name = "winapi-i686-pc-windows-gnu" 562 | version = "0.4.0" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 565 | 566 | [[package]] 567 | name = "winapi-x86_64-pc-windows-gnu" 568 | version = "0.4.0" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 571 | 572 | [[package]] 573 | name = "windows-sys" 574 | version = "0.48.0" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 577 | dependencies = [ 578 | "windows-targets", 579 | ] 580 | 581 | [[package]] 582 | name = "windows-targets" 583 | version = "0.48.5" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 586 | dependencies = [ 587 | "windows_aarch64_gnullvm", 588 | "windows_aarch64_msvc", 589 | "windows_i686_gnu", 590 | "windows_i686_msvc", 591 | "windows_x86_64_gnu", 592 | "windows_x86_64_gnullvm", 593 | "windows_x86_64_msvc", 594 | ] 595 | 596 | [[package]] 597 | name = "windows_aarch64_gnullvm" 598 | version = "0.48.5" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 601 | 602 | [[package]] 603 | name = "windows_aarch64_msvc" 604 | version = "0.48.5" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 607 | 608 | [[package]] 609 | name = "windows_i686_gnu" 610 | version = "0.48.5" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 613 | 614 | [[package]] 615 | name = "windows_i686_msvc" 616 | version = "0.48.5" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 619 | 620 | [[package]] 621 | name = "windows_x86_64_gnu" 622 | version = "0.48.5" 623 | source = "registry+https://github.com/rust-lang/crates.io-index" 624 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 625 | 626 | [[package]] 627 | name = "windows_x86_64_gnullvm" 628 | version = "0.48.5" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 631 | 632 | [[package]] 633 | name = "windows_x86_64_msvc" 634 | version = "0.48.5" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 637 | 638 | [[package]] 639 | name = "winnow" 640 | version = "0.5.19" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" 643 | dependencies = [ 644 | "memchr", 645 | ] 646 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-readme" 3 | version = "3.3.1" 4 | edition = "2021" 5 | authors = ["Livio Ribeiro "] 6 | description = "A cargo subcommand to generate README.md content from doc comments" 7 | repository = "https://github.com/webern/cargo-readme" 8 | readme = "README.md" 9 | keywords = ["readme", "documentation", "cargo", "subcommand"] 10 | categories = ["development-tools::cargo-plugins"] 11 | license = "MIT OR Apache-2.0" 12 | 13 | [dependencies] 14 | clap = { version = "4", features = [ "derive" ] } 15 | toml = "0.8" 16 | regex = "1" 17 | serde = { version = "1", features = ["derive"] } 18 | percent-encoding = "2" 19 | lazy_static = "1" 20 | 21 | [dev-dependencies] 22 | assert_cli = "0.6" 23 | 24 | [badges] 25 | github = { repository = "webern/cargo-readme" } 26 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | https://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 | https://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 | Copyright (c) 2015 The cargo-readme Developers 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Crates.io](https://img.shields.io/crates/v/cargo-readme.svg)](https://crates.io/crates/cargo-readme) 2 | [![Workflow Status](https://github.com/webern/cargo-readme/workflows/main/badge.svg)](https://github.com/webern/cargo-readme/actions?query=workflow%3A%22main%22) 3 | 4 | # cargo-readme 5 | 6 | Generate README.md from doc comments. 7 | 8 | Cargo subcommand that extract documentation from your crate's doc comments that you can use to 9 | populate your README.md. 10 | 11 | #### Attribution 12 | 13 | This library was authored by Livio Ribeiro ([@livioribeiro](https://github.com/livioribeiro)) 14 | and originally located at `https://github.com/livioribeiro/cargo-readme`, which now redirects 15 | here (as of August 2023). Thank you, Livio, for this lib! 16 | 17 | ## Installation 18 | 19 | ```sh 20 | cargo install cargo-readme 21 | ``` 22 | 23 | ## Motivation 24 | 25 | As you write documentation, you often have to show examples of how to use your software. But 26 | how do you make sure your examples are all working properly? That we didn't forget to update 27 | them after a breaking change and left our (possibly new) users with errors they will have to 28 | figure out by themselves? 29 | 30 | With `cargo-readme`, you just write the rustdoc, run the tests, and then run: 31 | 32 | ```sh 33 | cargo readme > README.md 34 | ``` 35 | 36 | And that's it! Your `README.md` is populated with the contents of the doc comments from your 37 | `lib.rs` (or `main.rs`). 38 | 39 | ## Usage 40 | 41 | Let's take the following rust doc: 42 | 43 | ```rust 44 | //! This is my awesome crate 45 | //! 46 | //! Here goes some other description of what it is and what is does 47 | //! 48 | //! # Examples 49 | //! ``` 50 | //! fn sum2(n1: i32, n2: i32) -> i32 { 51 | //! n1 + n2 52 | //! } 53 | //! # assert_eq!(4, sum2(2, 2)); 54 | //! ``` 55 | ``` 56 | 57 | Running `cargo readme` will output the following: 58 | 59 | ~~~markdown 60 | [![Build Status](__badge_image__)](__badge_url__) 61 | 62 | # my_crate 63 | 64 | This is my awesome crate 65 | 66 | Here goes some other description of what it is and what is does 67 | 68 | ## Examples 69 | ```rust 70 | fn sum2(n1: i32, n2: i32) -> i32 { 71 | n1 + n2 72 | } 73 | ``` 74 | 75 | License: MY_LICENSE 76 | ~~~ 77 | 78 | Let's see what's happened: 79 | 80 | - a badge was created from the one defined in the `[badges]` section of `Cargo.toml` 81 | - the crate name ("my-crate") was added 82 | - "# Examples" heading became "## Examples" 83 | - code block became "```rust" 84 | - hidden line `# assert_eq!(4, sum2(2, 2));` was removed 85 | 86 | `cargo-readme` also supports multiline doc comments `/*! */` (but you cannot mix styles): 87 | 88 | ~~~rust 89 | /*! 90 | This is my awesome crate 91 | 92 | Here goes some other description of what it is and what is does 93 | 94 | ``` 95 | fn sum2(n1: i32, n2: i32) -> i32 { 96 | n1 + n2 97 | } 98 | ``` 99 | */ 100 | ~~~ 101 | 102 | If you have additional information that does not fit in doc comments, you can use a template. 103 | Just create a file called `README.tpl` in the same directory as `Cargo.toml` with the following 104 | content: 105 | 106 | ```tpl 107 | {{badges}} 108 | 109 | # {{crate}} 110 | 111 | {{readme}} 112 | 113 | Current version: {{version}} 114 | 115 | Some additional info here 116 | 117 | License: {{license}} 118 | ``` 119 | 120 | The output will look like this 121 | 122 | ~~~markdown 123 | [![Build Status](__badge_image__)](__badge_url__) 124 | 125 | # my_crate 126 | 127 | Current version: 3.0.0 128 | 129 | This is my awesome crate 130 | 131 | Here goes some other description of what it is and what is does 132 | 133 | ## Examples 134 | ```rust 135 | fn sum2(n1: i32, n2: i32) -> i32 { 136 | n1 + n2 137 | } 138 | ``` 139 | 140 | Some additional info here 141 | 142 | License: MY_LICENSE 143 | ~~~ 144 | 145 | By default, `README.tpl` will be used as the template, but you can override it using the 146 | `--template` to choose a different template or `--no-template` to disable it. 147 | 148 | ## License 149 | 150 | Licensed under either of 151 | 152 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0) 153 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT) 154 | 155 | at your option. 156 | 157 | ### Contribution 158 | 159 | Unless you explicitly state otherwise, any contribution intentionally 160 | submitted for inclusion in the work by you, as defined in the Apache-2.0 161 | license, shall be dual licensed as above, without any additional terms or 162 | conditions. 163 | -------------------------------------------------------------------------------- /README.tpl: -------------------------------------------------------------------------------- 1 | [![Crates.io](https://img.shields.io/crates/v/cargo-readme.svg)](https://crates.io/crates/cargo-readme) 2 | {{badges}} 3 | 4 | # {{crate}} 5 | 6 | {{readme}} 7 | 8 | ## License 9 | 10 | Licensed under either of 11 | 12 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0) 13 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT) 14 | 15 | at your option. 16 | 17 | ### Contribution 18 | 19 | Unless you explicitly state otherwise, any contribution intentionally 20 | submitted for inclusion in the work by you, as defined in the Apache-2.0 21 | license, shall be dual licensed as above, without any additional terms or 22 | conditions. 23 | -------------------------------------------------------------------------------- /docs/uml/class.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | package std::iter { 4 | interface Iterator 5 | } 6 | 7 | package config { 8 | class ConfigHelper { 9 | + get_config(defaults_from_cli) 10 | - try_environment() 11 | - try_file(Option) 12 | } 13 | 14 | class ReadmeConfig { 15 | + project_root: Path 16 | + entrypoint: Path 17 | + template: Option 18 | + add_title: bool 19 | + add_badges: bool 20 | + add_license: bool 21 | + indent_headings: bool 22 | } 23 | 24 | class ProjectHelper { 25 | + find_root(cwd: &Path) 26 | + find_entrypoint(cwd: &Path, Manifest) 27 | + find_template(cwd: &Path) 28 | } 29 | 30 | class ManifestHelper { 31 | + crate_info() 32 | - process_badges() 33 | } 34 | 35 | class Manifest { 36 | + name: String 37 | + license: String 38 | + lib: Pair 39 | + bin: Pair[] 40 | + badges: String[] 41 | } 42 | 43 | ConfigHelper ..> ReadmeConfig: <> 44 | ManifestHelper .> Manifest: <> 45 | ProjectHelper ..> Manifest 46 | ConfigHelper ---> ManifestHelper 47 | ConfigHelper --> ProjectHelper 48 | } 49 | 50 | package docs { 51 | class Renderer { 52 | + render(docs, crate_info) 53 | - render_template() 54 | - render_no_template() 55 | } 56 | 57 | Renderer ..> ReadmeConfig 58 | Renderer ..> Manifest 59 | 60 | class Extractor { 61 | + extract(entrypoint) 62 | } 63 | 64 | class Processor { 65 | + process() 66 | } 67 | 68 | Iterator <|--- Processor 69 | Extractor --o Processor 70 | 71 | enum Section { 72 | CodeRust 73 | CodeOther 74 | None 75 | } 76 | 77 | Processor --> Section 78 | } 79 | 80 | class Main 81 | Main --> ConfigHelper 82 | Main -> Readme 83 | 84 | class Readme { 85 | + generate_readme(ReadmeConfig, Manifest) 86 | } 87 | 88 | Readme --> Renderer 89 | Readme --> Extractor 90 | ' Readme ..> ReadmeConfig 91 | 92 | @enduml -------------------------------------------------------------------------------- /docs/uml/sequence.puml: -------------------------------------------------------------------------------- 1 | @startuml flow 2 | 3 | actor User 4 | boundary Main 5 | participant ConfigHelper 6 | participant ManifestHelper 7 | participant EntrypointHelper 8 | participant Renderer 9 | participant Extractor 10 | participant Processor 11 | 12 | User -> Main 13 | activate Main 14 | 15 | Main -> ConfigHelper: find_configuration() 16 | activate ConfigHelper 17 | note right: Search for configuration 18 | 19 | ConfigHelper -> ConfigHelper: try environment 20 | ConfigHelper -> ConfigHelper: try file 21 | ConfigHelper -> ConfigHelper: try command line 22 | 23 | Main <-- ConfigHelper: configuration 24 | deactivate ConfigHelper 25 | 26 | Main -> ManifestHelper: manifest(manifest) 27 | activate ManifestHelper 28 | note right: Transform badges into markdown 29 | 30 | ManifestHelper -> ManifestHelper: process badges 31 | 32 | Main <-- ManifestHelper: crate info 33 | deactivate ManifestHelper 34 | 35 | opt no entrypoint given 36 | Main -> EntrypointHelper: find_entrypoint() 37 | activate EntrypointHelper 38 | Main <-- EntrypointHelper: entrypoint: File 39 | deactivate EntrypointHelper 40 | end 41 | 42 | Main -> Extractor: extract_docs(File) 43 | activate Extractor 44 | 45 | Extractor -> Processor: process_docs(String[]) 46 | note over Processor 47 | - change fence blocks to 48 | remove rustdoc specifics 49 | - optionally indent heading 50 | end note 51 | activate Processor 52 | 53 | Extractor <-- Processor: processed docs 54 | deactivate Processor 55 | 56 | Main <-- Extractor: processed docs 57 | deactivate Extractor 58 | 59 | Main -> Renderer: render(docs, manifest) 60 | ref over Renderer, Extractor, Processor: render template 61 | Main <-- Renderer: readme 62 | 63 | User <-- Main: result 64 | 65 | deactivate Main 66 | 67 | @enduml 68 | 69 | @startuml render 70 | 71 | participant Renderer 72 | participant Extractor 73 | participant Processor 74 | 75 | [-> Renderer: render(docs, manifest) 76 | activate Renderer 77 | 78 | alt without template 79 | Renderer -> Renderer: add crate info 80 | note left 81 | Add title 82 | Add badges 83 | Add license 84 | end note 85 | else using template 86 | Renderer -> Renderer: process\nsubstitutions 87 | Renderer -> Renderer: process\ninclusions 88 | activate Renderer 89 | 90 | loop included files 91 | note left of Renderer 92 | Include doc string 93 | from other files 94 | end note 95 | Renderer -> Extractor: extract_docs(file) 96 | activate Extractor 97 | Extractor -> Processor: process_docs(docs) 98 | activate Processor 99 | Extractor <-- Processor: processed docs 100 | deactivate Processor 101 | Renderer <-- Extractor: extracted docs 102 | deactivate Extractor 103 | Renderer -> Renderer: include docs 104 | end 105 | ||| 106 | deactivate Renderer 107 | ||| 108 | end 109 | 110 | [<-- Renderer: rendered readme 111 | deactivate Renderer 112 | 113 | @enduml -------------------------------------------------------------------------------- /src/config/badges.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | // https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata 4 | 5 | use percent_encoding as pe; 6 | 7 | const BADGE_BRANCH_DEFAULT: &str = "master"; 8 | const BADGE_SERVICE_DEFAULT: &str = "github"; 9 | const BADGE_WORKFLOW_DEFAULT: &str = "main"; 10 | 11 | type Attrs = BTreeMap; 12 | 13 | pub fn appveyor(attrs: Attrs) -> String { 14 | let repo = &attrs["repository"]; 15 | let branch = attrs 16 | .get("branch") 17 | .map(|i| i.as_ref()) 18 | .unwrap_or(BADGE_BRANCH_DEFAULT); 19 | let service = attrs 20 | .get("service") 21 | .map(|i| i.as_ref()) 22 | .unwrap_or(BADGE_SERVICE_DEFAULT); 23 | 24 | format!( 25 | "[![Build Status](https://ci.appveyor.com/api/projects/status/{service}/{repo}?branch={branch}&svg=true)]\ 26 | (https://ci.appveyor.com/project/{repo}/branch/{branch})", 27 | repo=repo, branch=branch, service=service 28 | ) 29 | } 30 | 31 | pub fn circle_ci(attrs: Attrs) -> String { 32 | let repo = &attrs["repository"]; 33 | let branch = attrs 34 | .get("branch") 35 | .map(|i| i.as_ref()) 36 | .unwrap_or(BADGE_BRANCH_DEFAULT); 37 | let service = badge_service_short_name( 38 | attrs 39 | .get("service") 40 | .map(|i| i.as_ref()) 41 | .unwrap_or(BADGE_SERVICE_DEFAULT), 42 | ); 43 | 44 | format!( 45 | "[![Build Status](https://circleci.com/{service}/{repo}/tree/{branch}.svg?style=shield)]\ 46 | (https://circleci.com/{service}/{repo}/tree/{branch})", 47 | repo = repo, 48 | service = service, 49 | branch = percent_encode(branch) 50 | ) 51 | } 52 | 53 | pub fn gitlab(attrs: Attrs) -> String { 54 | let repo = &attrs["repository"]; 55 | let branch = attrs 56 | .get("branch") 57 | .map(|i| i.as_ref()) 58 | .unwrap_or(BADGE_BRANCH_DEFAULT); 59 | 60 | format!( 61 | "[![Build Status](https://gitlab.com/{repo}/badges/{branch}/pipeline.svg)]\ 62 | (https://gitlab.com/{repo}/commits/master)", 63 | repo = repo, 64 | branch = percent_encode(branch) 65 | ) 66 | } 67 | 68 | pub fn travis_ci(attrs: Attrs) -> String { 69 | let repo = &attrs["repository"]; 70 | let branch = attrs 71 | .get("branch") 72 | .map(|i| i.as_ref()) 73 | .unwrap_or(BADGE_BRANCH_DEFAULT); 74 | 75 | format!( 76 | "[![Build Status](https://travis-ci.org/{repo}.svg?branch={branch})]\ 77 | (https://travis-ci.org/{repo})", 78 | repo = repo, 79 | branch = percent_encode(branch) 80 | ) 81 | } 82 | 83 | pub fn github(attrs: Attrs) -> String { 84 | let repo = &attrs["repository"]; 85 | let workflow = attrs 86 | .get("workflow") 87 | .map(|i| i.as_ref()) 88 | .unwrap_or(BADGE_WORKFLOW_DEFAULT); 89 | 90 | format!( 91 | "[![Workflow Status](https://github.com/{repo}/workflows/{workflow}/badge.svg)]\ 92 | (https://github.com/{repo}/actions?query=workflow%3A%22{workflow_plus}%22)", 93 | repo = repo, 94 | workflow = percent_encode(workflow), 95 | workflow_plus = percent_encode(&str::replace(workflow, " ", "+")) 96 | ) 97 | } 98 | 99 | pub fn codecov(attrs: Attrs) -> String { 100 | let repo = &attrs["repository"]; 101 | let branch = attrs 102 | .get("branch") 103 | .map(|i| i.as_ref()) 104 | .unwrap_or(BADGE_BRANCH_DEFAULT); 105 | let service = badge_service_short_name( 106 | attrs 107 | .get("service") 108 | .map(|i| i.as_ref()) 109 | .unwrap_or(BADGE_SERVICE_DEFAULT), 110 | ); 111 | 112 | format!( 113 | "[![Coverage Status](https://codecov.io/{service}/{repo}/branch/{branch}/graph/badge.svg)]\ 114 | (https://codecov.io/{service}/{repo})", 115 | repo = repo, 116 | branch = percent_encode(branch), 117 | service = service 118 | ) 119 | } 120 | 121 | pub fn coveralls(attrs: Attrs) -> String { 122 | let repo = &attrs["repository"]; 123 | let branch = attrs 124 | .get("branch") 125 | .map(|i| i.as_ref()) 126 | .unwrap_or(BADGE_BRANCH_DEFAULT); 127 | let service = attrs 128 | .get("service") 129 | .map(|i| i.as_ref()) 130 | .unwrap_or(BADGE_SERVICE_DEFAULT); 131 | 132 | format!( 133 | "[![Coverage Status](https://coveralls.io/repos/{service}/{repo}/badge.svg?branch=branch)]\ 134 | (https://coveralls.io/{service}/{repo}?branch={branch})", 135 | repo = repo, 136 | branch = percent_encode(branch), 137 | service = service 138 | ) 139 | } 140 | 141 | pub fn is_it_maintained_issue_resolution(attrs: Attrs) -> String { 142 | let repo = &attrs["repository"]; 143 | format!( 144 | "[![Average time to resolve an issue](https://isitmaintained.com/badge/resolution/{repo}.svg)]\ 145 | (https://isitmaintained.com/project/{repo} \"Average time to resolve an issue\")", 146 | repo=repo 147 | ) 148 | } 149 | 150 | pub fn is_it_maintained_open_issues(attrs: Attrs) -> String { 151 | let repo = &attrs["repository"]; 152 | format!( 153 | "[![Percentage of issues still open](https://isitmaintained.com/badge/open/{repo}.svg)]\ 154 | (https://isitmaintained.com/project/{repo} \"Percentage of issues still open\")", 155 | repo = repo 156 | ) 157 | } 158 | 159 | pub fn maintenance(attrs: Attrs) -> String { 160 | let status = &attrs["status"]; 161 | 162 | // https://github.com/rust-lang/crates.io/blob/5a08887d4b531e034d01386d3e5997514f3c8ee5/src/models/badge.rs#L82 163 | let status_with_color = match status.as_ref() { 164 | "actively-developed" => "activly--developed-brightgreen", 165 | "passively-maintained" => "passively--maintained-yellowgreen", 166 | "as-is" => "as--is-yellow", 167 | "none" => "maintenance-none-lightgrey", // color is a guess 168 | "experimental" => "experimental-blue", 169 | "looking-for-maintainer" => "looking--for--maintainer-darkblue", // color is a guess 170 | "deprecated" => "deprecated-red", 171 | _ => "unknow-black", 172 | }; 173 | 174 | //example https://img.shields.io/badge/maintenance-experimental-blue.svg 175 | format!( 176 | "![Maintenance](https://img.shields.io/badge/maintenance-{status}.svg)", 177 | status = status_with_color 178 | ) 179 | } 180 | 181 | fn percent_encode(input: &str) -> pe::PercentEncode { 182 | pe::utf8_percent_encode(input, pe::NON_ALPHANUMERIC) 183 | } 184 | 185 | fn badge_service_short_name(service: &str) -> &'static str { 186 | match service { 187 | "github" => "gh", 188 | "bitbucket" => "bb", 189 | "gitlab" => "gl", 190 | _ => "gh", 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/config/manifest.rs: -------------------------------------------------------------------------------- 1 | //! Read crate information from `Cargo.toml` 2 | 3 | use serde::Deserialize; 4 | use std::collections::BTreeMap; 5 | use std::fs::File; 6 | use std::io::Read; 7 | use std::path::{Path, PathBuf}; 8 | 9 | use toml; 10 | 11 | use super::badges; 12 | 13 | /// Try to get manifest info from Cargo.toml 14 | pub fn get_manifest(project_root: &Path) -> Result { 15 | let mut cargo_toml = File::open(project_root.join("Cargo.toml")) 16 | .map_err(|e| format!("Could not read Cargo.toml: {}", e))?; 17 | 18 | let buf = { 19 | let mut buf = String::new(); 20 | cargo_toml 21 | .read_to_string(&mut buf) 22 | .map_err(|e| format!("{}", e))?; 23 | buf 24 | }; 25 | 26 | let cargo_toml: CargoToml = toml::from_str(&buf).map_err(|e| format!("{}", e))?; 27 | 28 | let manifest = Manifest::new(cargo_toml); 29 | 30 | Ok(manifest) 31 | } 32 | 33 | #[derive(Debug)] 34 | pub struct Manifest { 35 | pub name: String, 36 | pub license: Option, 37 | pub lib: Option, 38 | pub bin: Vec, 39 | pub badges: Vec, 40 | pub version: String, 41 | } 42 | 43 | impl Manifest { 44 | fn new(cargo_toml: CargoToml) -> Manifest { 45 | Manifest { 46 | name: cargo_toml.package.name, 47 | license: cargo_toml.package.license, 48 | lib: cargo_toml.lib.map(|lib| ManifestLib::from_cargo_toml(lib)), 49 | bin: cargo_toml 50 | .bin 51 | .map(|bin_vec| { 52 | bin_vec 53 | .into_iter() 54 | .map(|bin| ManifestLib::from_cargo_toml(bin)) 55 | .collect() 56 | }) 57 | .unwrap_or_default(), 58 | badges: cargo_toml 59 | .badges 60 | .map(|b| process_badges(b)) 61 | .unwrap_or_default(), 62 | version: cargo_toml.package.version, 63 | } 64 | } 65 | } 66 | 67 | #[derive(Debug)] 68 | pub struct ManifestLib { 69 | pub path: PathBuf, 70 | pub doc: bool, 71 | } 72 | 73 | impl ManifestLib { 74 | fn from_cargo_toml(lib: CargoTomlLib) -> Self { 75 | ManifestLib { 76 | path: PathBuf::from(lib.path), 77 | doc: lib.doc.unwrap_or(true), 78 | } 79 | } 80 | } 81 | 82 | fn process_badges(badges: BTreeMap>) -> Vec { 83 | let mut b: Vec<(u16, _)> = badges 84 | .into_iter() 85 | .filter_map(|(name, attrs)| match name.as_ref() { 86 | "appveyor" => Some((0, badges::appveyor(attrs))), 87 | "circle-ci" => Some((1, badges::circle_ci(attrs))), 88 | "gitlab" => Some((2, badges::gitlab(attrs))), 89 | "travis-ci" => Some((3, badges::travis_ci(attrs))), 90 | "github" => Some((4, badges::github(attrs))), 91 | "codecov" => Some((5, badges::codecov(attrs))), 92 | "coveralls" => Some((6, badges::coveralls(attrs))), 93 | "is-it-maintained-issue-resolution" => { 94 | Some((7, badges::is_it_maintained_issue_resolution(attrs))) 95 | } 96 | "is-it-maintained-open-issues" => { 97 | Some((8, badges::is_it_maintained_open_issues(attrs))) 98 | } 99 | "maintenance" => Some((9, badges::maintenance(attrs))), 100 | _ => return None, 101 | }) 102 | .collect(); 103 | 104 | b.sort_unstable_by(|a, b| a.0.cmp(&b.0)); 105 | b.into_iter().map(|(_, badge)| badge).collect() 106 | } 107 | 108 | /// Cargo.toml crate information 109 | #[derive(Clone, Deserialize)] 110 | struct CargoToml { 111 | pub package: CargoTomlPackage, 112 | pub lib: Option, 113 | pub bin: Option>, 114 | pub badges: Option>>, 115 | } 116 | 117 | /// Cargo.toml crate package information 118 | #[derive(Clone, Deserialize)] 119 | struct CargoTomlPackage { 120 | pub name: String, 121 | pub license: Option, 122 | pub version: String, 123 | } 124 | 125 | /// Cargo.toml crate lib information 126 | #[derive(Clone, Deserialize)] 127 | struct CargoTomlLib { 128 | pub path: String, 129 | pub doc: Option, 130 | } 131 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | mod badges; 2 | mod manifest; 3 | pub mod project; 4 | 5 | pub use self::manifest::get_manifest; 6 | pub use self::manifest::Manifest; 7 | -------------------------------------------------------------------------------- /src/config/project.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::{Path, PathBuf}; 3 | 4 | use crate::config::manifest::{Manifest, ManifestLib}; 5 | 6 | /// Get the project root from given path or defaults to current directory 7 | /// 8 | /// The given path is appended to the current directory if is a relative path, otherwise it is used 9 | /// as is. If no path is given, the current directory is used. 10 | /// A `Cargo.toml` file must be present is the root directory. 11 | pub fn get_root(given_root: Option<&str>) -> Result { 12 | let current_dir = env::current_dir().map_err(|e| format!("{}", e))?; 13 | let root = match given_root { 14 | Some(root) => { 15 | let root = Path::new(root); 16 | if root.is_absolute() { 17 | root.to_path_buf() 18 | } else { 19 | current_dir.join(root) 20 | } 21 | } 22 | None => current_dir, 23 | }; 24 | 25 | if !root.join("Cargo.toml").is_file() { 26 | return Err(format!( 27 | "`{:?}` does not look like a Rust/Cargo project", 28 | root 29 | )); 30 | } 31 | 32 | Ok(root) 33 | } 34 | 35 | /// Find the default entrypoiny to read the doc comments from 36 | /// 37 | /// Try to read entrypoint in the following order: 38 | /// - src/lib.rs 39 | /// - src/main.rs 40 | /// - file defined in the `[lib]` section of Cargo.toml 41 | /// - file defined in the `[[bin]]` section of Cargo.toml, if there is only one 42 | /// - if there is more than one `[[bin]]`, an error is returned 43 | pub fn find_entrypoint(current_dir: &Path, manifest: &Manifest) -> Result { 44 | // try lib.rs 45 | let lib_rs = current_dir.join("src/lib.rs"); 46 | if lib_rs.exists() { 47 | return Ok(lib_rs); 48 | } 49 | 50 | // try main.rs 51 | let main_rs = current_dir.join("src/main.rs"); 52 | if main_rs.exists() { 53 | return Ok(main_rs); 54 | } 55 | 56 | // try lib defined in `Cargo.toml` 57 | if let Some(ManifestLib { 58 | path: ref lib, 59 | doc: true, 60 | }) = manifest.lib 61 | { 62 | return Ok(lib.to_path_buf()); 63 | } 64 | 65 | // try bin defined in `Cargo.toml` 66 | if manifest.bin.len() > 0 { 67 | let mut bin_list: Vec<_> = manifest 68 | .bin 69 | .iter() 70 | .filter(|b| b.doc == true) 71 | .map(|b| b.path.clone()) 72 | .collect(); 73 | 74 | if bin_list.len() > 1 { 75 | let paths = bin_list 76 | .iter() 77 | .map(|p| p.to_string_lossy()) 78 | .collect::>() 79 | .join(", "); 80 | return Err(format!("Multiple binaries found, choose one: [{}]", paths)); 81 | } 82 | 83 | if let Some(bin) = bin_list.pop() { 84 | return Ok(bin); 85 | } 86 | } 87 | 88 | // if no entrypoint is found, return an error 89 | Err("No entrypoint found".to_owned()) 90 | } 91 | -------------------------------------------------------------------------------- /src/helper.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::{ErrorKind, Write}; 3 | use std::path::{Path, PathBuf}; 4 | 5 | use cargo_readme::get_manifest; 6 | use cargo_readme::project; 7 | 8 | const DEFAULT_TEMPLATE: &'static str = "README.tpl"; 9 | 10 | /// Get the project root from given path or defaults to current directory 11 | /// 12 | /// The given path is appended to the current directory if is a relative path, otherwise it is used 13 | /// as is. If no path is given, the current directory is used. 14 | /// A `Cargo.toml` file must be present is the root directory. 15 | pub fn get_project_root(given_root: Option<&str>) -> Result { 16 | project::get_root(given_root) 17 | } 18 | 19 | /// Get the source file from which the doc comments will be extracted 20 | pub fn get_source(project_root: &Path, input: Option<&str>) -> Result { 21 | match input { 22 | Some(input) => { 23 | let input = project_root.join(input); 24 | File::open(&input) 25 | .map_err(|e| format!("Could not open file '{}': {}", input.to_string_lossy(), e)) 26 | } 27 | None => find_entrypoint(&project_root), 28 | } 29 | } 30 | 31 | /// Get the destination file where the result will be output to 32 | pub fn get_dest(project_root: &Path, output: Option<&str>) -> Result, String> { 33 | match output { 34 | Some(filename) => { 35 | let output = project_root.join(filename); 36 | File::create(&output).map(|f| Some(f)).map_err(|e| { 37 | format!( 38 | "Could not create output file '{}': {}", 39 | output.to_string_lossy(), 40 | e 41 | ) 42 | }) 43 | } 44 | None => Ok(None), 45 | } 46 | } 47 | 48 | /// Get the template file that will be used to render the output 49 | pub fn get_template_file( 50 | project_root: &Path, 51 | template: Option<&str>, 52 | ) -> Result, String> { 53 | match template { 54 | // template path was given, try to read it 55 | Some(template) => { 56 | let template = project_root.join(template); 57 | File::open(&template).map(|f| Some(f)).map_err(|e| { 58 | format!( 59 | "Could not open template file '{}': {}", 60 | template.to_string_lossy(), 61 | e 62 | ) 63 | }) 64 | } 65 | // try to read the defautl template file 66 | None => { 67 | let template = project_root.join(DEFAULT_TEMPLATE); 68 | match File::open(&template) { 69 | Ok(file) => Ok(Some(file)), 70 | // do not generate an error on file not found 71 | Err(ref e) if e.kind() != ErrorKind::NotFound => { 72 | return Err(format!( 73 | "Could not open template file '{}': {}", 74 | DEFAULT_TEMPLATE, e 75 | )) 76 | } 77 | // default template not found, return `None` 78 | _ => Ok(None), 79 | } 80 | } 81 | } 82 | } 83 | 84 | /// Write result to output, either stdout or destination file 85 | pub fn write_output(dest: &mut Option, readme: String) -> Result<(), String> { 86 | match dest.as_mut() { 87 | Some(dest) => { 88 | let mut bytes = readme.into_bytes(); 89 | // Append new line at end of file to match behavior of `cargo readme > README.md` 90 | bytes.push(b'\n'); 91 | 92 | dest.write_all(&mut bytes) 93 | .map(|_| ()) 94 | .map_err(|e| format!("Could not write to output file: {}", e))?; 95 | } 96 | None => println!("{}", readme), 97 | } 98 | 99 | Ok(()) 100 | } 101 | 102 | /// Find the default entrypoiny to read the doc comments from 103 | /// 104 | /// Try to read entrypoint in the following order: 105 | /// - src/lib.rs 106 | /// - src/main.rs 107 | /// - file defined in the `[lib]` section of Cargo.toml 108 | /// - file defined in the `[[bin]]` section of Cargo.toml, if there is only one 109 | /// - if there is more than one `[[bin]]`, an error is returned 110 | pub fn find_entrypoint(current_dir: &Path) -> Result { 111 | let manifest = get_manifest(current_dir)?; 112 | let entrypoint = project::find_entrypoint(current_dir, &manifest)?; 113 | 114 | File::open(current_dir.join(entrypoint)).map_err(|e| format!("{}", e)) 115 | } 116 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Generate README.md from doc comments. 2 | //! 3 | //! Cargo subcommand that extract documentation from your crate's doc comments that you can use to 4 | //! populate your README.md. 5 | //! 6 | //! ### Attribution 7 | //! 8 | //! This library was authored by Livio Ribeiro ([@livioribeiro](https://github.com/livioribeiro)) 9 | //! and originally located at `https://github.com/livioribeiro/cargo-readme`, which now redirects 10 | //! here (as of August 2023). Thank you, Livio, for this lib! 11 | //! 12 | //! # Installation 13 | //! 14 | //! ```sh 15 | //! cargo install cargo-readme 16 | //! ``` 17 | //! 18 | //! # Motivation 19 | //! 20 | //! As you write documentation, you often have to show examples of how to use your software. But 21 | //! how do you make sure your examples are all working properly? That we didn't forget to update 22 | //! them after a breaking change and left our (possibly new) users with errors they will have to 23 | //! figure out by themselves? 24 | //! 25 | //! With `cargo-readme`, you just write the rustdoc, run the tests, and then run: 26 | //! 27 | //! ```sh 28 | //! cargo readme > README.md 29 | //! ``` 30 | //! 31 | //! And that's it! Your `README.md` is populated with the contents of the doc comments from your 32 | //! `lib.rs` (or `main.rs`). 33 | //! 34 | //! # Usage 35 | //! 36 | //! Let's take the following rust doc: 37 | //! 38 | //! ```ignore 39 | //! //! This is my awesome crate 40 | //! //! 41 | //! //! Here goes some other description of what it is and what is does 42 | //! //! 43 | //! //! # Examples 44 | //! //! ``` 45 | //! //! fn sum2(n1: i32, n2: i32) -> i32 { 46 | //! //! n1 + n2 47 | //! //! } 48 | //! //! # assert_eq!(4, sum2(2, 2)); 49 | //! //! ``` 50 | //! ``` 51 | //! 52 | //! Running `cargo readme` will output the following: 53 | //! 54 | //! ~~~markdown 55 | //! [![Build Status](__badge_image__)](__badge_url__) 56 | //! 57 | //! # my_crate 58 | //! 59 | //! This is my awesome crate 60 | //! 61 | //! Here goes some other description of what it is and what is does 62 | //! 63 | //! ## Examples 64 | //! ```rust 65 | //! fn sum2(n1: i32, n2: i32) -> i32 { 66 | //! n1 + n2 67 | //! } 68 | //! ``` 69 | //! 70 | //! License: MY_LICENSE 71 | //! ~~~ 72 | //! 73 | //! Let's see what's happened: 74 | //! 75 | //! - a badge was created from the one defined in the `[badges]` section of `Cargo.toml` 76 | //! - the crate name ("my-crate") was added 77 | //! - "# Examples" heading became "## Examples" 78 | //! - code block became "```rust" 79 | //! - hidden line `# assert_eq!(4, sum2(2, 2));` was removed 80 | //! 81 | //! `cargo-readme` also supports multiline doc comments `/*! */` (but you cannot mix styles): 82 | //! 83 | //! ~~~ignore 84 | //! /*! 85 | //! This is my awesome crate 86 | //! 87 | //! Here goes some other description of what it is and what is does 88 | //! 89 | //! # Examples 90 | //! ``` 91 | //! fn sum2(n1: i32, n2: i32) -> i32 { 92 | //! n1 + n2 93 | //! } 94 | //! # assert_eq!(4, sum2(2, 2)); 95 | //! ``` 96 | //! */ 97 | //! ~~~ 98 | //! 99 | //! If you have additional information that does not fit in doc comments, you can use a template. 100 | //! Just create a file called `README.tpl` in the same directory as `Cargo.toml` with the following 101 | //! content: 102 | //! 103 | //! ```tpl 104 | //! {{badges}} 105 | //! 106 | //! # {{crate}} 107 | //! 108 | //! {{readme}} 109 | //! 110 | //! Current version: {{version}} 111 | //! 112 | //! Some additional info here 113 | //! 114 | //! License: {{license}} 115 | //! ``` 116 | //! 117 | //! The output will look like this 118 | //! 119 | //! ~~~markdown 120 | //! [![Build Status](__badge_image__)](__badge_url__) 121 | //! 122 | //! # my_crate 123 | //! 124 | //! Current version: 3.0.0 125 | //! 126 | //! This is my awesome crate 127 | //! 128 | //! Here goes some other description of what it is and what is does 129 | //! 130 | //! ## Examples 131 | //! ```rust 132 | //! fn sum2(n1: i32, n2: i32) -> i32 { 133 | //! n1 + n2 134 | //! } 135 | //! ``` 136 | //! 137 | //! Some additional info here 138 | //! 139 | //! License: MY_LICENSE 140 | //! ~~~ 141 | //! 142 | //! By default, `README.tpl` will be used as the template, but you can override it using the 143 | //! `--template` to choose a different template or `--no-template` to disable it. 144 | 145 | mod config; 146 | mod readme; 147 | 148 | pub use config::get_manifest; 149 | pub use config::project; 150 | pub use readme::generate_readme; 151 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! Generate README.md from doc comments. 2 | 3 | use clap::Parser; 4 | use std::io; 5 | use std::io::Write; 6 | 7 | mod helper; 8 | 9 | fn main() { 10 | let args = Args::parse(); 11 | let result = match &args.command { 12 | Command::Readme(readme_args) => execute(readme_args), 13 | }; 14 | match result { 15 | Err(e) => { 16 | io::stderr() 17 | .write_fmt(format_args!("Error: {}\n", e)) 18 | .expect("An error occurred while trying to show an error message"); 19 | std::process::exit(1); 20 | } 21 | _ => {} 22 | } 23 | } 24 | /// The command line interface for setting up a Bottlerocket TestSys cluster and running tests. 25 | #[derive(Debug, Parser)] 26 | #[clap(author, version, about)] 27 | struct Args { 28 | #[clap(subcommand)] 29 | command: Command, 30 | } 31 | 32 | #[derive(Debug, Parser)] 33 | enum Command { 34 | /// Generate README.md from doc comments 35 | Readme(ReadmeArgs), 36 | } 37 | 38 | /// Generate README.md from doc comments 39 | #[derive(Debug, Parser)] 40 | #[clap(author, version, about)] 41 | struct ReadmeArgs { 42 | /// Do not prepend badges line. 43 | /// By default, badges defined in Cargo.toml are prepended to the output. 44 | /// Ignored when using a template. 45 | #[clap(long)] 46 | no_badges: bool, 47 | 48 | /// Do not add an extra level to headings. 49 | /// By default, '#' headings become '##', so the first '#' can be the crate name. Use this 50 | /// option to prevent this behavior. 51 | #[clap(long)] 52 | no_indent_headings: bool, 53 | 54 | /// Do not append license line. 55 | /// By default, the license defined in `Cargo.toml` will be prepended to the output. 56 | /// Ignored when using a template. 57 | #[clap(long)] 58 | no_license: bool, 59 | 60 | /// Ignore template file when generating README. 61 | /// Only useful to ignore default template `README.tpl`. 62 | #[clap(long)] 63 | no_template: bool, 64 | 65 | /// Do not prepend title line. 66 | /// By default, the title ('# crate-name') is prepended to the output. 67 | #[clap(long)] 68 | no_title: bool, 69 | 70 | /// File to read from. 71 | /// If not provided, will try to use `src/lib.rs`, then `src/main.rs`. If neither file 72 | /// could be found, will look into `Cargo.toml` for a `[lib]`, then for a single `[[bin]]`. 73 | /// If multiple binaries are found, an error will be returned. 74 | #[clap(long, short = 'i')] 75 | input: Option, 76 | 77 | /// File to write to. If not provided, will output to stdout. 78 | #[clap(long, short = 'o')] 79 | output: Option, 80 | 81 | /// Directory to be set as project root (where `Cargo.toml` is) 82 | /// Defaults to the current directory. 83 | #[clap(long = "project-root", short = 'r')] 84 | root: Option, 85 | 86 | /// Template used to render the output. 87 | /// Default behavior is to use `README.tpl` if it exists. 88 | #[clap(long, short = 't')] 89 | template: Option, 90 | } 91 | 92 | // Takes the arguments matches from clap and outputs the result, either to stdout of a file 93 | fn execute(args: &ReadmeArgs) -> Result<(), String> { 94 | // get project root 95 | let project_root = helper::get_project_root(args.root.as_deref())?; 96 | 97 | // get source file 98 | let mut source = helper::get_source(&project_root, args.input.as_deref())?; 99 | 100 | // get destination file 101 | let mut dest = helper::get_dest(&project_root, args.output.as_deref())?; 102 | 103 | // get template file 104 | let mut template_file = if args.no_template { 105 | None 106 | } else { 107 | helper::get_template_file(&project_root, args.template.as_deref())? 108 | }; 109 | 110 | let add_title = !args.no_title; 111 | let add_badges = !args.no_badges; 112 | let add_license = !args.no_license; 113 | let indent_headings = !args.no_indent_headings; 114 | 115 | // generate output 116 | let readme = cargo_readme::generate_readme( 117 | &project_root, 118 | &mut source, 119 | template_file.as_mut(), 120 | add_title, 121 | add_badges, 122 | add_license, 123 | indent_headings, 124 | )?; 125 | 126 | helper::write_output(&mut dest, readme) 127 | } 128 | -------------------------------------------------------------------------------- /src/readme/extract.rs: -------------------------------------------------------------------------------- 1 | //! Extract raw doc comments from rust source code 2 | 3 | use std::io::{self, BufRead, BufReader, Read}; 4 | 5 | /// Read the given `Read`er and return a `Vec` of the rustdoc lines found 6 | pub fn extract_docs(reader: R) -> io::Result> { 7 | let mut reader = BufReader::new(reader); 8 | let mut line = String::new(); 9 | 10 | while reader.read_line(&mut line)? > 0 { 11 | if line.starts_with("//!") { 12 | return extract_docs_singleline_style(line, reader); 13 | } 14 | 15 | if line.starts_with("/*!") { 16 | return extract_docs_multiline_style(line, reader); 17 | } 18 | 19 | line.clear(); 20 | } 21 | 22 | Ok(Vec::new()) 23 | } 24 | 25 | fn extract_docs_singleline_style( 26 | first_line: String, 27 | reader: BufReader, 28 | ) -> io::Result> { 29 | let mut result = vec![normalize_line(first_line)]; 30 | 31 | for line in reader.lines() { 32 | let line = line?; 33 | 34 | if line.starts_with("//!") { 35 | result.push(normalize_line(line)); 36 | } else if line.trim().len() > 0 { 37 | // doc ends, code starts 38 | break; 39 | } 40 | } 41 | 42 | Ok(result) 43 | } 44 | 45 | fn extract_docs_multiline_style( 46 | first_line: String, 47 | reader: BufReader, 48 | ) -> io::Result> { 49 | let mut result = Vec::new(); 50 | if first_line.starts_with("/*!") && first_line.trim().len() > "/*!".len() { 51 | result.push(normalize_line(first_line)); 52 | } 53 | 54 | let mut nesting: isize = 0; 55 | 56 | for line in reader.lines() { 57 | let line = line?; 58 | nesting += line.matches("/*").count() as isize; 59 | 60 | if let Some(pos) = line.rfind("*/") { 61 | nesting -= line.matches("*/").count() as isize; 62 | if nesting < 0 { 63 | let mut line = line; 64 | let _ = line.split_off(pos); 65 | if !line.trim().is_empty() { 66 | result.push(line); 67 | } 68 | break; 69 | } 70 | } 71 | 72 | result.push(line.trim_end().to_owned()); 73 | } 74 | 75 | Ok(result) 76 | } 77 | 78 | /// Strip the "//!" or "/*!" from a line and a single whitespace 79 | fn normalize_line(mut line: String) -> String { 80 | if line.trim() == "//!" || line.trim() == "/*!" { 81 | line.clear(); 82 | line 83 | } else { 84 | // if the first character after the comment mark is " ", remove it 85 | let split_at = if line.find(" ") == Some(3) { 4 } else { 3 }; 86 | line.split_at(split_at).1.trim_end().to_owned() 87 | } 88 | } 89 | 90 | #[cfg(test)] 91 | mod tests { 92 | use super::*; 93 | use std::io::Cursor; 94 | 95 | const EXPECTED: &[&str] = &[ 96 | "first line", 97 | "", 98 | "```", 99 | "let rust_code = \"safe\";", 100 | "```", 101 | "", 102 | "```C", 103 | "int i = 0; // no rust code", 104 | "```", 105 | ]; 106 | 107 | const INPUT_SINGLELINE: &str = "\ 108 | //! first line \n\ 109 | //! \n\ 110 | //! ``` \n\ 111 | //! let rust_code = \"safe\"; \n\ 112 | //! ``` \n\ 113 | //! \n\ 114 | //! ```C \n\ 115 | //! int i = 0; // no rust code \n\ 116 | //! ``` \n\ 117 | use std::any::Any; \n\ 118 | fn main() {}"; 119 | 120 | #[test] 121 | fn extract_docs_singleline_style() { 122 | let reader = Cursor::new(INPUT_SINGLELINE.as_bytes()); 123 | let result = extract_docs(reader).unwrap(); 124 | assert_eq!(result, EXPECTED); 125 | } 126 | 127 | const INPUT_MULTILINE: &str = "\ 128 | /*! \n\ 129 | first line \n\ 130 | \n\ 131 | ``` \n\ 132 | let rust_code = \"safe\"; \n\ 133 | ``` \n\ 134 | \n\ 135 | ```C \n\ 136 | int i = 0; // no rust code \n\ 137 | ``` \n\ 138 | */ \n\ 139 | use std::any::Any; \n\ 140 | fn main() {}"; 141 | 142 | #[test] 143 | fn extract_docs_multiline_style() { 144 | let reader = Cursor::new(INPUT_MULTILINE.as_bytes()); 145 | let result = extract_docs(reader).unwrap(); 146 | assert_eq!(result, EXPECTED); 147 | } 148 | 149 | const INPUT_MIXED_SINGLELINE: &str = "\ 150 | //! singleline \n\ 151 | /*! \n\ 152 | multiline \n\ 153 | */"; 154 | 155 | #[test] 156 | fn extract_docs_mix_styles_singleline() { 157 | let input = Cursor::new(INPUT_MIXED_SINGLELINE.as_bytes()); 158 | let expected = "singleline"; 159 | let result = extract_docs(input).unwrap(); 160 | assert_eq!(result, &[expected]) 161 | } 162 | 163 | const INPUT_MIXED_MULTILINE: &str = "\ 164 | /*! \n\ 165 | multiline \n\ 166 | */ \n\ 167 | //! singleline"; 168 | 169 | #[test] 170 | fn extract_docs_mix_styles_multiline() { 171 | let input = Cursor::new(INPUT_MIXED_MULTILINE.as_bytes()); 172 | let expected = "multiline"; 173 | let result = extract_docs(input).unwrap(); 174 | assert_eq!(result, &[expected]); 175 | } 176 | 177 | const INPUT_MULTILINE_NESTED_1: &str = "\ 178 | /*! \n\ 179 | level 0 \n\ 180 | /* \n\ 181 | level 1 \n\ 182 | */ \n\ 183 | level 0 \n\ 184 | */ \n\ 185 | fn main() {}"; 186 | 187 | const EXPECTED_MULTILINE_NESTED_1: &[&str] = &["level 0", "/*", "level 1", "*/", "level 0"]; 188 | 189 | #[test] 190 | fn extract_docs_nested_level_1() { 191 | let input = Cursor::new(INPUT_MULTILINE_NESTED_1.as_bytes()); 192 | let result = extract_docs(input).unwrap(); 193 | assert_eq!(result, EXPECTED_MULTILINE_NESTED_1); 194 | } 195 | 196 | const INPUT_MULTILINE_NESTED_2: &str = "\ 197 | /*! \n\ 198 | level 0 \n\ 199 | /* \n\ 200 | level 1 \n\ 201 | /* \n\ 202 | level 2 \n\ 203 | */ \n\ 204 | level 1 \n\ 205 | */ \n\ 206 | level 0 \n\ 207 | */ \n\ 208 | fn main() {}"; 209 | 210 | const EXPECTED_MULTILINE_NESTED_2: &[&str] = &[ 211 | "level 0", "/*", "level 1", "/*", "level 2", "*/", "level 1", "*/", "level 0", 212 | ]; 213 | 214 | #[test] 215 | fn extract_docs_nested_level_2() { 216 | let input = Cursor::new(INPUT_MULTILINE_NESTED_2.as_bytes()); 217 | let result = extract_docs(input).unwrap(); 218 | assert_eq!(result, EXPECTED_MULTILINE_NESTED_2); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/readme/mod.rs: -------------------------------------------------------------------------------- 1 | use std::io::Read; 2 | use std::path::Path; 3 | 4 | mod extract; 5 | mod process; 6 | mod template; 7 | 8 | use crate::config; 9 | 10 | /// Generates readme data from `source` file 11 | /// 12 | /// Optionally, a template can be used to render the output 13 | pub fn generate_readme( 14 | project_root: &Path, 15 | source: &mut T, 16 | template: Option<&mut T>, 17 | add_title: bool, 18 | add_badges: bool, 19 | add_license: bool, 20 | indent_headings: bool, 21 | ) -> Result { 22 | let lines = extract::extract_docs(source).map_err(|e| format!("{}", e))?; 23 | 24 | let readme = process::process_docs(lines, indent_headings).join("\n"); 25 | 26 | // get template from file 27 | let template = if let Some(template) = template { 28 | Some(get_template_string(template)?) 29 | } else { 30 | None 31 | }; 32 | 33 | // get manifest from Cargo.toml 34 | let cargo = config::get_manifest(project_root)?; 35 | 36 | template::render(template, readme, &cargo, add_title, add_badges, add_license) 37 | } 38 | 39 | /// Load a template String from a file 40 | fn get_template_string(template: &mut T) -> Result { 41 | let mut template_string = String::new(); 42 | match template.read_to_string(&mut template_string) { 43 | Err(e) => return Err(format!("Error: {}", e)), 44 | _ => {} 45 | } 46 | 47 | Ok(template_string) 48 | } 49 | -------------------------------------------------------------------------------- /src/readme/process.rs: -------------------------------------------------------------------------------- 1 | //! Transform code blocks from rustdoc into markdown 2 | //! 3 | //! Rewrite code block start tags, changing rustdoc into equivalent in markdown: 4 | //! - "```", "```no_run", "```ignore" and "```should_panic" are converted to "```rust" 5 | //! - markdown heading are indentend to be one level lower, so the crate name is at the top level 6 | 7 | use lazy_static::lazy_static; 8 | use regex::Regex; 9 | use std::iter::{IntoIterator, Iterator}; 10 | 11 | lazy_static! { 12 | // Is this code block rust? 13 | static ref RE_CODE_RUST: Regex = Regex::new(r"^(?P`{3,4}|~{3,4})(?:rust|(?:(?:rust,)?(?:no_run|ignore|should_panic)))?$").unwrap(); 14 | // Is this code block just text? 15 | static ref RE_CODE_TEXT: Regex = Regex::new(r"^(?P`{3,4}|~{3,4})text$").unwrap(); 16 | // Is this code block a language other than rust? 17 | static ref RE_CODE_OTHER: Regex = Regex::new(r"^(?P`{3,4}|~{3,4})\w[\w,\+]*$").unwrap(); 18 | } 19 | 20 | /// Process and concatenate the doc lines into a single String 21 | /// 22 | /// The processing transforms doc tests into regular rust code blocks and optionally indent the 23 | /// markdown headings in order to leave the top heading to the crate name 24 | pub fn process_docs, L: Into>>( 25 | lines: L, 26 | indent_headings: bool, 27 | ) -> Vec { 28 | lines.into().into_iter().process_docs(indent_headings) 29 | } 30 | 31 | pub struct Processor { 32 | section: Section, 33 | indent_headings: bool, 34 | delimiter: Option, 35 | } 36 | 37 | impl Processor { 38 | pub fn new(indent_headings: bool) -> Self { 39 | Processor { 40 | section: Section::None, 41 | indent_headings: indent_headings, 42 | delimiter: None, 43 | } 44 | } 45 | 46 | pub fn process_line(&mut self, mut line: String) -> Option { 47 | // Skip lines that should be hidden in docs 48 | if self.section == Section::CodeRust && line.starts_with("# ") { 49 | return None; 50 | } 51 | 52 | // indent heading when outside code 53 | if self.indent_headings && self.section == Section::None && line.starts_with("#") { 54 | line.insert(0, '#'); 55 | } else if self.section == Section::None { 56 | let l = line.clone(); 57 | if let Some(cap) = RE_CODE_RUST.captures(&l) { 58 | self.section = Section::CodeRust; 59 | self.delimiter = cap.name("delimiter").map(|x| x.as_str().to_owned()); 60 | line = format!("{}rust", self.delimiter.as_ref().unwrap()); 61 | } else if let Some(cap) = RE_CODE_TEXT.captures(&l) { 62 | self.section = Section::CodeOther; 63 | self.delimiter = cap.name("delimiter").map(|x| x.as_str().to_owned()); 64 | line = self.delimiter.clone().unwrap(); 65 | } else if let Some(cap) = RE_CODE_OTHER.captures(&l) { 66 | self.section = Section::CodeOther; 67 | self.delimiter = cap.name("delimiter").map(|x| x.as_str().to_owned()); 68 | } 69 | } else if self.section != Section::None && Some(&line) == self.delimiter.as_ref() { 70 | self.section = Section::None; 71 | line = self.delimiter.take().unwrap_or("```".to_owned()); 72 | } 73 | 74 | Some(line) 75 | } 76 | } 77 | 78 | #[derive(PartialEq)] 79 | enum Section { 80 | CodeRust, 81 | CodeOther, 82 | None, 83 | } 84 | 85 | pub trait DocProcess> { 86 | fn process_docs(self, indent_headings: bool) -> Vec 87 | where 88 | Self: Sized + Iterator, 89 | { 90 | let mut p = Processor::new(indent_headings); 91 | self.into_iter() 92 | .filter_map(|line| p.process_line(line.into())) 93 | .collect() 94 | } 95 | } 96 | 97 | impl, I: Iterator> DocProcess for I {} 98 | 99 | #[cfg(test)] 100 | mod tests { 101 | use super::process_docs; 102 | 103 | const INPUT_HIDDEN_LINE: &[&str] = &[ 104 | "```", 105 | "#[visible]", 106 | "let visible = \"visible\";", 107 | "# let hidden = \"hidden\";", 108 | "```", 109 | ]; 110 | 111 | const EXPECTED_HIDDEN_LINE: &[&str] = 112 | &["```rust", "#[visible]", "let visible = \"visible\";", "```"]; 113 | 114 | #[test] 115 | fn hide_line_in_rust_code_block() { 116 | let result = process_docs(INPUT_HIDDEN_LINE, true); 117 | assert_eq!(result, EXPECTED_HIDDEN_LINE); 118 | } 119 | 120 | const INPUT_NOT_HIDDEN_LINE: &[&str] = &[ 121 | "```", 122 | "let visible = \"visible\";", 123 | "# let hidden = \"hidden\";", 124 | "```", 125 | "", 126 | "```python", 127 | "# this line is visible", 128 | "visible = True", 129 | "```", 130 | ]; 131 | 132 | const EXPECTED_NOT_HIDDEN_LINE: &[&str] = &[ 133 | "```rust", 134 | "let visible = \"visible\";", 135 | "```", 136 | "", 137 | "```python", 138 | "# this line is visible", 139 | "visible = True", 140 | "```", 141 | ]; 142 | 143 | #[test] 144 | fn do_not_hide_line_in_code_block() { 145 | let result = process_docs(INPUT_NOT_HIDDEN_LINE, true); 146 | assert_eq!(result, EXPECTED_NOT_HIDDEN_LINE); 147 | } 148 | 149 | const INPUT_RUST_CODE_BLOCK: &[&str] = &[ 150 | "```", 151 | "let block = \"simple code block\";", 152 | "```", 153 | "", 154 | "```no_run", 155 | "let run = false;", 156 | "```", 157 | "", 158 | "```ignore", 159 | "let ignore = true;", 160 | "```", 161 | "", 162 | "```should_panic", 163 | "panic!(\"at the disco\");", 164 | "```", 165 | "", 166 | "```C", 167 | "int i = 0; // no rust code", 168 | "```", 169 | ]; 170 | 171 | const EXPECTED_RUST_CODE_BLOCK: &[&str] = &[ 172 | "```rust", 173 | "let block = \"simple code block\";", 174 | "```", 175 | "", 176 | "```rust", 177 | "let run = false;", 178 | "```", 179 | "", 180 | "```rust", 181 | "let ignore = true;", 182 | "```", 183 | "", 184 | "```rust", 185 | "panic!(\"at the disco\");", 186 | "```", 187 | "", 188 | "```C", 189 | "int i = 0; // no rust code", 190 | "```", 191 | ]; 192 | 193 | #[test] 194 | fn transform_rust_code_block() { 195 | let result = process_docs(INPUT_RUST_CODE_BLOCK, true); 196 | assert_eq!(result, EXPECTED_RUST_CODE_BLOCK); 197 | } 198 | 199 | const INPUT_RUST_CODE_BLOCK_RUST_PREFIX: &[&str] = &[ 200 | "```rust", 201 | "let block = \"simple code block\";", 202 | "```", 203 | "", 204 | "```rust,no_run", 205 | "let run = false;", 206 | "```", 207 | "", 208 | "```rust,ignore", 209 | "let ignore = true;", 210 | "```", 211 | "", 212 | "```rust,should_panic", 213 | "panic!(\"at the disco\");", 214 | "```", 215 | "", 216 | "```C", 217 | "int i = 0; // no rust code", 218 | "```", 219 | ]; 220 | 221 | #[test] 222 | fn transform_rust_code_block_with_prefix() { 223 | let result = process_docs(INPUT_RUST_CODE_BLOCK_RUST_PREFIX, true); 224 | assert_eq!(result, EXPECTED_RUST_CODE_BLOCK); 225 | } 226 | 227 | const INPUT_TEXT_BLOCK: &[&str] = &["```text", "this is text", "```"]; 228 | 229 | const EXPECTED_TEXT_BLOCK: &[&str] = &["```", "this is text", "```"]; 230 | 231 | #[test] 232 | fn transform_text_block() { 233 | let result = process_docs(INPUT_TEXT_BLOCK, true); 234 | assert_eq!(result, EXPECTED_TEXT_BLOCK); 235 | } 236 | 237 | const INPUT_OTHER_CODE_BLOCK_WITH_SYMBOLS: &[&str] = &[ 238 | "```html,django", 239 | "{% if True %}True{% endif %}", 240 | "```", 241 | "", 242 | "```html+django", 243 | "{% if True %}True{% endif %}", 244 | "```", 245 | ]; 246 | 247 | #[test] 248 | fn transform_other_code_block_with_symbols() { 249 | let result = process_docs(INPUT_OTHER_CODE_BLOCK_WITH_SYMBOLS, true); 250 | assert_eq!(result, INPUT_OTHER_CODE_BLOCK_WITH_SYMBOLS); 251 | } 252 | 253 | const INPUT_INDENT_HEADINGS: &[&str] = &[ 254 | "# heading 1", 255 | "some text", 256 | "## heading 2", 257 | "some other text", 258 | ]; 259 | 260 | const EXPECTED_INDENT_HEADINGS: &[&str] = &[ 261 | "## heading 1", 262 | "some text", 263 | "### heading 2", 264 | "some other text", 265 | ]; 266 | 267 | #[test] 268 | fn indent_markdown_headings() { 269 | let result = process_docs(INPUT_INDENT_HEADINGS, true); 270 | assert_eq!(result, EXPECTED_INDENT_HEADINGS); 271 | } 272 | 273 | #[test] 274 | fn do_not_indent_markdown_headings() { 275 | let result = process_docs(INPUT_INDENT_HEADINGS, false); 276 | assert_eq!(result, INPUT_INDENT_HEADINGS); 277 | } 278 | 279 | const INPUT_ALTERNATE_DELIMITER_4_BACKTICKS: &[&str] = &["````", "let i = 1;", "````"]; 280 | 281 | const EXPECTED_ALTERNATE_DELIMITER_4_BACKTICKS: &[&str] = &["````rust", "let i = 1;", "````"]; 282 | 283 | #[test] 284 | fn alternate_delimiter_4_backticks() { 285 | let result = process_docs(INPUT_ALTERNATE_DELIMITER_4_BACKTICKS, false); 286 | assert_eq!(result, EXPECTED_ALTERNATE_DELIMITER_4_BACKTICKS); 287 | } 288 | 289 | const INPUT_ALTERNATE_DELIMITER_4_BACKTICKS_NESTED: &[&str] = &[ 290 | "````", 291 | "//! ```", 292 | "//! let i = 1;", 293 | "//! ```", 294 | "```python", 295 | "i = 1", 296 | "```", 297 | "````", 298 | ]; 299 | 300 | const EXPECTED_ALTERNATE_DELIMITER_4_BACKTICKS_NESTED: &[&str] = &[ 301 | "````rust", 302 | "//! ```", 303 | "//! let i = 1;", 304 | "//! ```", 305 | "```python", 306 | "i = 1", 307 | "```", 308 | "````", 309 | ]; 310 | 311 | #[test] 312 | fn alternate_delimiter_4_backticks_nested() { 313 | let result = process_docs(INPUT_ALTERNATE_DELIMITER_4_BACKTICKS_NESTED, false); 314 | assert_eq!(result, EXPECTED_ALTERNATE_DELIMITER_4_BACKTICKS_NESTED); 315 | } 316 | 317 | const INPUT_ALTERNATE_DELIMITER_3_TILDES: &[&str] = &["~~~", "let i = 1;", "~~~"]; 318 | 319 | const EXPECTED_ALTERNATE_DELIMITER_3_TILDES: &[&str] = &["~~~rust", "let i = 1;", "~~~"]; 320 | 321 | #[test] 322 | fn alternate_delimiter_3_tildes() { 323 | let result = process_docs(INPUT_ALTERNATE_DELIMITER_3_TILDES, false); 324 | assert_eq!(result, EXPECTED_ALTERNATE_DELIMITER_3_TILDES); 325 | } 326 | 327 | const INPUT_ALTERNATE_DELIMITER_4_TILDES: &[&str] = &["~~~~", "let i = 1;", "~~~~"]; 328 | 329 | const EXPECTED_ALTERNATE_DELIMITER_4_TILDES: &[&str] = &["~~~~rust", "let i = 1;", "~~~~"]; 330 | 331 | #[test] 332 | fn alternate_delimiter_4_tildes() { 333 | let result = process_docs(INPUT_ALTERNATE_DELIMITER_4_TILDES, false); 334 | assert_eq!(result, EXPECTED_ALTERNATE_DELIMITER_4_TILDES); 335 | } 336 | 337 | const INPUT_ALTERNATE_DELIMITER_MIXED: &[&str] = &[ 338 | "```", 339 | "let i = 1;", 340 | "```", 341 | "````", 342 | "//! ```", 343 | "//! let i = 1;", 344 | "//! ```", 345 | "```python", 346 | "i = 1", 347 | "```", 348 | "````", 349 | "~~~markdown", 350 | "```python", 351 | "i = 1", 352 | "```", 353 | "~~~", 354 | ]; 355 | 356 | const EXPECTED_ALTERNATE_DELIMITER_MIXED: &[&str] = &[ 357 | "```rust", 358 | "let i = 1;", 359 | "```", 360 | "````rust", 361 | "//! ```", 362 | "//! let i = 1;", 363 | "//! ```", 364 | "```python", 365 | "i = 1", 366 | "```", 367 | "````", 368 | "~~~markdown", 369 | "```python", 370 | "i = 1", 371 | "```", 372 | "~~~", 373 | ]; 374 | 375 | #[test] 376 | fn alternate_delimiter_mixed() { 377 | let result = process_docs(INPUT_ALTERNATE_DELIMITER_MIXED, false); 378 | assert_eq!(result, EXPECTED_ALTERNATE_DELIMITER_MIXED); 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /src/readme/template.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Manifest; 2 | 3 | /// Renders the template 4 | /// 5 | /// This is not a real template engine, it just processes a few substitutions. 6 | pub fn render( 7 | template: Option, 8 | readme: String, 9 | cargo: &Manifest, 10 | add_title: bool, 11 | add_badges: bool, 12 | add_license: bool, 13 | ) -> Result { 14 | let title: &str = &cargo.name; 15 | 16 | let badges: Vec<&str> = cargo.badges.iter().map(AsRef::as_ref).collect(); 17 | let badges: &[&str] = badges.as_ref(); 18 | 19 | let license: Option<&str> = cargo.license.as_ref().map(AsRef::as_ref); 20 | 21 | let version: &str = cargo.version.as_ref(); 22 | 23 | if let Some(template) = template { 24 | process_template(template, readme, title, badges, license, version) 25 | } else { 26 | process_string( 27 | readme, 28 | title, 29 | badges, 30 | license, 31 | add_title, 32 | add_badges, 33 | add_license, 34 | ) 35 | } 36 | } 37 | 38 | /// Process the substitutions of the template 39 | /// 40 | /// Available variable: 41 | /// - `{{readme}}` documentation extracted from the rust docs 42 | /// - `{{crate}}` crate name defined in `Cargo.toml` 43 | /// - `{{badges}}` badges defined in `Cargo.toml` 44 | /// - `{{license}}` license defined in `Cargo.toml` 45 | /// - `{{version}}` version defined in `Cargo.toml` 46 | fn process_template( 47 | mut template: String, 48 | readme: String, 49 | title: &str, 50 | badges: &[&str], 51 | license: Option<&str>, 52 | version: &str, 53 | ) -> Result { 54 | template = template.trim_end_matches("\n").to_owned(); 55 | 56 | if !template.contains("{{readme}}") { 57 | return Err("Missing `{{readme}}` in template".to_owned()); 58 | } 59 | 60 | if template.contains("{{crate}}") { 61 | template = template.replace("{{crate}}", &title); 62 | } 63 | 64 | if template.contains("{{badges}}") { 65 | if badges.is_empty() { 66 | return Err( 67 | "`{{badges}}` was found in template but no badges were provided".to_owned(), 68 | ); 69 | } 70 | let badges = badges.join("\n"); 71 | template = template.replace("{{badges}}", &badges); 72 | } 73 | 74 | if template.contains("{{license}}") { 75 | if let Some(license) = license { 76 | template = template.replace("{{license}}", &license); 77 | } else { 78 | return Err( 79 | "`{{license}}` was found in template but no license was provided".to_owned(), 80 | ); 81 | } 82 | } 83 | 84 | template = template.replace("{{version}}", version); 85 | 86 | let result = template.replace("{{readme}}", &readme); 87 | Ok(result) 88 | } 89 | 90 | /// Process output without template 91 | fn process_string( 92 | mut readme: String, 93 | title: &str, 94 | badges: &[&str], 95 | license: Option<&str>, 96 | add_title: bool, 97 | add_badges: bool, 98 | add_license: bool, 99 | ) -> Result { 100 | if add_title { 101 | readme = prepend_title(readme, title); 102 | } 103 | 104 | if add_badges { 105 | readme = prepend_badges(readme, badges); 106 | } 107 | 108 | if add_license { 109 | if let Some(license) = license { 110 | readme = append_license(readme, license); 111 | } 112 | } 113 | 114 | Ok(readme) 115 | } 116 | 117 | /// Prepend badges to output string 118 | fn prepend_badges(readme: String, badges: &[&str]) -> String { 119 | if badges.len() > 0 { 120 | let badges = badges.join("\n"); 121 | if !readme.is_empty() { 122 | format!("{}\n\n{}", badges, readme) 123 | } else { 124 | badges 125 | } 126 | } else { 127 | readme 128 | } 129 | } 130 | 131 | /// Prepend title (crate name) to output string 132 | fn prepend_title(readme: String, crate_name: &str) -> String { 133 | let title = format!("# {}", crate_name); 134 | if !readme.trim().is_empty() { 135 | format!("{}\n\n{}", title, readme) 136 | } else { 137 | title 138 | } 139 | } 140 | 141 | /// Append license to output string 142 | fn append_license(readme: String, license: &str) -> String { 143 | let license = format!("License: {}", license); 144 | if !readme.trim().is_empty() { 145 | format!("{}\n\n{}", readme, license) 146 | } else { 147 | license 148 | } 149 | } 150 | 151 | #[cfg(test)] 152 | mod tests { 153 | const TEMPLATE_MINIMAL: &str = "{{readme}}"; 154 | const TEMPLATE_WITH_TITLE: &str = "# {{crate}}\n\n{{readme}}"; 155 | const TEMPLATE_WITH_BADGES: &str = "{{badges}}\n\n{{readme}}"; 156 | const TEMPLATE_WITH_LICENSE: &str = "{{readme}}\n\n{{license}}"; 157 | const TEMPLATE_WITH_VERSION: &str = "{{readme}}\n\n{{version}}"; 158 | const TEMPLATE_FULL: &str = 159 | "{{badges}}\n\n# {{crate}}\n\n{{readme}}\n\n{{license}}\n\n{{version}}"; 160 | 161 | // process template 162 | #[test] 163 | fn template_without_readme_should_fail() { 164 | let result = super::process_template(String::new(), String::new(), "", &[], None, ""); 165 | assert!(result.is_err()); 166 | assert_eq!("Missing `{{readme}}` in template", result.unwrap_err()); 167 | } 168 | 169 | #[test] 170 | fn template_with_badge_tag_but_missing_badges_should_fail() { 171 | let result = super::process_template( 172 | TEMPLATE_WITH_BADGES.to_owned(), 173 | String::new(), 174 | "", 175 | &[], 176 | None, 177 | "", 178 | ); 179 | assert!(result.is_err()); 180 | assert_eq!( 181 | "`{{badges}}` was found in template but no badges were provided", 182 | result.unwrap_err() 183 | ); 184 | } 185 | 186 | #[test] 187 | fn template_with_license_tag_but_missing_license_should_fail() { 188 | let result = super::process_template( 189 | TEMPLATE_WITH_LICENSE.to_owned(), 190 | String::new(), 191 | "", 192 | &[], 193 | None, 194 | "", 195 | ); 196 | assert!(result.is_err()); 197 | assert_eq!( 198 | "`{{license}}` was found in template but no license was provided", 199 | result.unwrap_err() 200 | ); 201 | } 202 | 203 | #[test] 204 | fn template_minimal() { 205 | let result = super::process_template( 206 | TEMPLATE_MINIMAL.to_owned(), 207 | "readme".to_owned(), 208 | "", 209 | &[], 210 | None, 211 | "", 212 | ); 213 | assert!(result.is_ok()); 214 | assert_eq!("readme", result.unwrap()); 215 | } 216 | 217 | #[test] 218 | fn template_with_title() { 219 | let result = super::process_template( 220 | TEMPLATE_WITH_TITLE.to_owned(), 221 | "readme".to_owned(), 222 | "title", 223 | &[], 224 | None, 225 | "", 226 | ); 227 | assert!(result.is_ok()); 228 | assert_eq!("# title\n\nreadme", result.unwrap()); 229 | } 230 | 231 | #[test] 232 | fn template_with_badges() { 233 | let result = super::process_template( 234 | TEMPLATE_WITH_BADGES.to_owned(), 235 | "readme".to_owned(), 236 | "", 237 | &["badge1", "badge2"], 238 | None, 239 | "", 240 | ); 241 | assert!(result.is_ok()); 242 | assert_eq!("badge1\nbadge2\n\nreadme", result.unwrap()); 243 | } 244 | 245 | #[test] 246 | fn template_with_license() { 247 | let result = super::process_template( 248 | TEMPLATE_WITH_LICENSE.to_owned(), 249 | "readme".to_owned(), 250 | "", 251 | &[], 252 | Some("license"), 253 | "", 254 | ); 255 | assert!(result.is_ok()); 256 | assert_eq!("readme\n\nlicense", result.unwrap()); 257 | } 258 | 259 | #[test] 260 | fn template_with_version() { 261 | let result = super::process_template( 262 | TEMPLATE_WITH_VERSION.to_owned(), 263 | "readme".to_owned(), 264 | "", 265 | &[], 266 | None, 267 | "3.0.1", 268 | ); 269 | assert!(result.is_ok()); 270 | assert_eq!("readme\n\n3.0.1", result.unwrap()); 271 | } 272 | 273 | #[test] 274 | fn template_full() { 275 | let result = super::process_template( 276 | TEMPLATE_FULL.to_owned(), 277 | "readme".to_owned(), 278 | "title", 279 | &["badge1", "badge2"], 280 | Some("license"), 281 | "3.0.2", 282 | ); 283 | assert!(result.is_ok()); 284 | assert_eq!( 285 | "badge1\nbadge2\n\n# title\n\nreadme\n\nlicense\n\n3.0.2", 286 | result.unwrap() 287 | ); 288 | } 289 | 290 | // process string 291 | #[test] 292 | fn render_minimal() { 293 | let result = super::process_string("readme".to_owned(), "", &[], None, false, false, false); 294 | assert!(result.is_ok()); 295 | assert_eq!("readme", result.unwrap()); 296 | } 297 | 298 | #[test] 299 | fn render_title() { 300 | let result = 301 | super::process_string("readme".to_owned(), "title", &[], None, true, false, false); 302 | assert!(result.is_ok()); 303 | assert_eq!("# title\n\nreadme", result.unwrap()); 304 | } 305 | 306 | #[test] 307 | fn render_badges() { 308 | let result = super::process_string( 309 | "readme".to_owned(), 310 | "", 311 | &["badge1", "badge2"], 312 | None, 313 | false, 314 | true, 315 | false, 316 | ); 317 | assert!(result.is_ok()); 318 | assert_eq!("badge1\nbadge2\n\nreadme", result.unwrap()); 319 | } 320 | 321 | #[test] 322 | fn render_license() { 323 | let result = super::process_string( 324 | "readme".to_owned(), 325 | "", 326 | &[], 327 | Some("license"), 328 | false, 329 | false, 330 | true, 331 | ); 332 | assert!(result.is_ok()); 333 | assert_eq!("readme\n\nLicense: license", result.unwrap()); 334 | } 335 | 336 | #[test] 337 | fn render_full() { 338 | let result = super::process_string( 339 | "readme".to_owned(), 340 | "title", 341 | &["badge1", "badge2"], 342 | Some("license"), 343 | true, 344 | true, 345 | true, 346 | ); 347 | assert!(result.is_ok()); 348 | assert_eq!( 349 | "badge1\nbadge2\n\n# title\n\nreadme\n\nLicense: license", 350 | result.unwrap() 351 | ); 352 | } 353 | 354 | #[test] 355 | fn render_nothing() { 356 | let result = super::process_string( 357 | "readme".to_owned(), 358 | "title", 359 | &["badge1", "badge2"], 360 | Some("license"), 361 | false, 362 | false, 363 | false, 364 | ); 365 | assert!(result.is_ok()); 366 | assert_eq!("readme", result.unwrap()); 367 | } 368 | 369 | // prepend badges 370 | #[test] 371 | fn prepend_badges_with_filled_readme_and_non_empty_badges() { 372 | let result = super::prepend_badges("readme".into(), &["badge1", "badge2"]); 373 | assert_eq!("badge1\nbadge2\n\nreadme", result); 374 | } 375 | 376 | #[test] 377 | fn prepend_badges_with_empty_readme_and_non_empty_badges() { 378 | let result = super::prepend_badges("".into(), &["badge1", "badge2"]); 379 | assert_eq!("badge1\nbadge2", result); 380 | } 381 | 382 | #[test] 383 | fn prepend_badges_with_filled_readme_and_empty_badges() { 384 | let result = super::prepend_badges("readme".into(), &[]); 385 | assert_eq!("readme", result); 386 | } 387 | 388 | #[test] 389 | fn prepend_badges_with_empty_readme_and_empty_badges() { 390 | let result = super::prepend_badges("".into(), &[]); 391 | assert_eq!("", result); 392 | } 393 | 394 | // prepend title 395 | #[test] 396 | fn prepend_title_with_filled_readme() { 397 | let result = super::prepend_title("readme".into(), "title"); 398 | assert_eq!("# title\n\nreadme", result); 399 | } 400 | 401 | #[test] 402 | fn prepend_title_with_empty_readme() { 403 | let result = super::prepend_title("".into(), "title"); 404 | assert_eq!("# title", result); 405 | } 406 | 407 | // append license 408 | #[test] 409 | fn append_license_with_filled_readme() { 410 | let result = super::append_license("readme".into(), "license"); 411 | assert_eq!("readme\n\nLicense: license", result); 412 | } 413 | 414 | #[test] 415 | fn append_license_with_empty_readme() { 416 | let result = super::append_license("".into(), "license"); 417 | assert_eq!("License: license", result); 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | -------------------------------------------------------------------------------- /tests/alternate-input.rs: -------------------------------------------------------------------------------- 1 | use assert_cli::Assert; 2 | 3 | #[test] 4 | fn alternate_input_empty_docs() { 5 | let args = [ 6 | "readme", 7 | "--project-root", 8 | "tests/test-project", 9 | "--no-template", 10 | "--no-badges", 11 | "--input", 12 | "src/no_docs.rs", 13 | ]; 14 | 15 | Assert::main_binary() 16 | .with_args(&args) 17 | .succeeds() 18 | .and() 19 | .stdout() 20 | .is("# readme-test\n\nLicense: MIT") 21 | .unwrap(); 22 | } 23 | 24 | #[test] 25 | fn alternate_input_single_line() { 26 | let args = [ 27 | "readme", 28 | "--project-root", 29 | "tests/test-project", 30 | "--no-template", 31 | "--no-badges", 32 | "--input", 33 | "src/single_line.rs", 34 | ]; 35 | 36 | let expected = r#" 37 | # readme-test 38 | 39 | Test crate for cargo-readme 40 | 41 | License: MIT 42 | "#; 43 | 44 | Assert::main_binary() 45 | .with_args(&args) 46 | .succeeds() 47 | .and() 48 | .stdout() 49 | .is(expected) 50 | .unwrap(); 51 | } 52 | 53 | #[test] 54 | fn alternate_input_a_little_bit_longer() { 55 | let args = [ 56 | "readme", 57 | "--project-root", 58 | "tests/test-project", 59 | "--no-template", 60 | "--no-badges", 61 | "--input", 62 | "src/other.rs", 63 | ]; 64 | 65 | let expected = r#" 66 | # readme-test 67 | 68 | Test crate for cargo-readme 69 | 70 | ## Level 1 heading should become level 2 71 | 72 | License: MIT 73 | "#; 74 | 75 | Assert::main_binary() 76 | .with_args(&args) 77 | .succeeds() 78 | .and() 79 | .stdout() 80 | .is(expected) 81 | .unwrap(); 82 | } 83 | -------------------------------------------------------------------------------- /tests/alternate-template.rs: -------------------------------------------------------------------------------- 1 | use assert_cli::Assert; 2 | 3 | const EXPECTED: &str = r#" 4 | # readme-test 5 | 6 | Other readme template. 7 | 8 | Test crate for cargo-readme 9 | 10 | ## Level 1 heading should become level 2 11 | 12 | ```rust 13 | // This is standard doc test and should be output as ```rust 14 | let condition = true; 15 | if condition { 16 | // Some conditional code here 17 | if condition { 18 | // Some nested conditional code here 19 | } 20 | } 21 | ``` 22 | 23 | ### Level 2 heading should become level 3 24 | 25 | ```rust 26 | // This also should output as ```rust 27 | ``` 28 | #### Level 3 heading should become level 4 29 | 30 | ```rust 31 | // This also should output as ```rust 32 | ``` 33 | 34 | ```rust 35 | // This should output as ```rust too 36 | ``` 37 | 38 | ```rust 39 | // And also this should output as ```rust 40 | ``` 41 | 42 | ```python 43 | # This should be on the output 44 | ``` 45 | "#; 46 | 47 | #[test] 48 | fn alternate_template() { 49 | let args = [ 50 | "readme", 51 | "--project-root", 52 | "tests/test-project", 53 | "--template", 54 | "NOTITLE.tpl", 55 | ]; 56 | 57 | Assert::main_binary() 58 | .with_args(&args) 59 | .succeeds() 60 | .and() 61 | .stdout() 62 | .is(EXPECTED) 63 | .unwrap(); 64 | } 65 | -------------------------------------------------------------------------------- /tests/append-license.rs: -------------------------------------------------------------------------------- 1 | use assert_cli::Assert; 2 | 3 | const EXPECTED: &str = r#" 4 | # readme-test 5 | 6 | Test crate for cargo-readme 7 | 8 | ## Level 1 heading should become level 2 9 | 10 | ```rust 11 | // This is standard doc test and should be output as ```rust 12 | let condition = true; 13 | if condition { 14 | // Some conditional code here 15 | if condition { 16 | // Some nested conditional code here 17 | } 18 | } 19 | ``` 20 | 21 | ### Level 2 heading should become level 3 22 | 23 | ```rust 24 | // This also should output as ```rust 25 | ``` 26 | #### Level 3 heading should become level 4 27 | 28 | ```rust 29 | // This also should output as ```rust 30 | ``` 31 | 32 | ```rust 33 | // This should output as ```rust too 34 | ``` 35 | 36 | ```rust 37 | // And also this should output as ```rust 38 | ``` 39 | 40 | ```python 41 | # This should be on the output 42 | ``` 43 | "#; 44 | 45 | #[test] 46 | fn append_license() { 47 | let args = [ 48 | "readme", 49 | "--project-root", 50 | "tests/test-project", 51 | "--no-template", 52 | "--no-badges", 53 | ]; 54 | 55 | let expected = format!("{}\n\n{}", EXPECTED.trim(), "License: MIT"); 56 | 57 | Assert::main_binary() 58 | .with_args(&args) 59 | .succeeds() 60 | .and() 61 | .stdout() 62 | .is(&*expected) 63 | .unwrap(); 64 | } 65 | 66 | #[test] 67 | fn no_append_license() { 68 | let args = [ 69 | "readme", 70 | "--project-root", 71 | "tests/test-project", 72 | "--no-template", 73 | "--no-badges", 74 | "--no-license", 75 | ]; 76 | 77 | Assert::main_binary() 78 | .with_args(&args) 79 | .succeeds() 80 | .and() 81 | .stdout() 82 | .is(EXPECTED) 83 | .unwrap(); 84 | } 85 | -------------------------------------------------------------------------------- /tests/badges.rs: -------------------------------------------------------------------------------- 1 | use assert_cli::Assert; 2 | 3 | const EXPECTED: &str = r#" 4 | [![Build Status](https://ci.appveyor.com/api/projects/status/github/cargo-readme/test?branch=master&svg=true)](https://ci.appveyor.com/project/cargo-readme/test/branch/master) 5 | [![Build Status](https://circleci.com/gh/cargo-readme/test/tree/master.svg?style=shield)](https://circleci.com/gh/cargo-readme/test/tree/master) 6 | [![Build Status](https://gitlab.com/cargo-readme/test/badges/master/pipeline.svg)](https://gitlab.com/cargo-readme/test/commits/master) 7 | [![Build Status](https://travis-ci.org/cargo-readme/test.svg?branch=master)](https://travis-ci.org/cargo-readme/test) 8 | [![Coverage Status](https://codecov.io/gh/cargo-readme/test/branch/master/graph/badge.svg)](https://codecov.io/gh/cargo-readme/test) 9 | [![Coverage Status](https://coveralls.io/repos/github/cargo-readme/test/badge.svg?branch=branch)](https://coveralls.io/github/cargo-readme/test?branch=master) 10 | [![Average time to resolve an issue](https://isitmaintained.com/badge/resolution/cargo-readme/test.svg)](https://isitmaintained.com/project/cargo-readme/test "Average time to resolve an issue") 11 | [![Percentage of issues still open](https://isitmaintained.com/badge/open/cargo-readme/test.svg)](https://isitmaintained.com/project/cargo-readme/test "Percentage of issues still open") 12 | 13 | # readme-test 14 | 15 | Test crate for cargo-readme 16 | 17 | License: MIT 18 | "#; 19 | 20 | #[test] 21 | fn badges() { 22 | let args = ["readme", "--project-root", "tests/badges"]; 23 | 24 | Assert::main_binary() 25 | .with_args(&args) 26 | .succeeds() 27 | .and() 28 | .stdout() 29 | .is(EXPECTED) 30 | .unwrap(); 31 | } 32 | -------------------------------------------------------------------------------- /tests/badges/.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | -------------------------------------------------------------------------------- /tests/badges/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "readme-test" 3 | version = "0.1.0" 4 | authors = ["Livio Ribeiro "] 5 | license = "MIT" 6 | 7 | [badges] 8 | appveyor = { repository = "cargo-readme/test" } 9 | circle-ci = { repository = "cargo-readme/test" } 10 | gitlab = { repository = "cargo-readme/test" } 11 | travis-ci = { repository = "cargo-readme/test" } 12 | codecov = { repository = "cargo-readme/test" } 13 | coveralls = { repository = "cargo-readme/test" } 14 | is-it-maintained-issue-resolution = { repository = "cargo-readme/test" } 15 | is-it-maintained-open-issues = { repository = "cargo-readme/test" } -------------------------------------------------------------------------------- /tests/badges/README.tpl: -------------------------------------------------------------------------------- 1 | {{badges}} 2 | 3 | # {{crate}} 4 | 5 | {{readme}} 6 | 7 | License: {{license}} -------------------------------------------------------------------------------- /tests/badges/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Test crate for cargo-readme -------------------------------------------------------------------------------- /tests/default-behavior.rs: -------------------------------------------------------------------------------- 1 | use assert_cli::Assert; 2 | 3 | const EXPECTED: &str = r#" 4 | [![Build Status](https://travis-ci.org/livioribeiro/cargo-readme.svg?branch=master)](https://travis-ci.org/livioribeiro/cargo-readme) 5 | 6 | # readme-test 7 | 8 | Some text here 9 | 10 | Test crate for cargo-readme 11 | 12 | ## Level 1 heading should become level 2 13 | 14 | ```rust 15 | // This is standard doc test and should be output as ```rust 16 | let condition = true; 17 | if condition { 18 | // Some conditional code here 19 | if condition { 20 | // Some nested conditional code here 21 | } 22 | } 23 | ``` 24 | 25 | ### Level 2 heading should become level 3 26 | 27 | ```rust 28 | // This also should output as ```rust 29 | ``` 30 | #### Level 3 heading should become level 4 31 | 32 | ```rust 33 | // This also should output as ```rust 34 | ``` 35 | 36 | ```rust 37 | // This should output as ```rust too 38 | ``` 39 | 40 | ```rust 41 | // And also this should output as ```rust 42 | ``` 43 | 44 | ```python 45 | # This should be on the output 46 | ``` 47 | "#; 48 | 49 | #[test] 50 | fn default_behavior() { 51 | let args = ["readme", "--project-root", "tests/test-project"]; 52 | 53 | Assert::main_binary() 54 | .with_args(&args) 55 | .succeeds() 56 | .and() 57 | .stdout() 58 | .is(EXPECTED) 59 | .unwrap(); 60 | } 61 | -------------------------------------------------------------------------------- /tests/entrypoint-resolution.rs: -------------------------------------------------------------------------------- 1 | use assert_cli::Assert; 2 | 3 | #[test] 4 | fn entrypoint_resolution_main() { 5 | let args = [ 6 | "readme", 7 | "--project-root", 8 | "tests/entrypoint-resolution/main", 9 | "--no-title", 10 | "--no-license", 11 | ]; 12 | 13 | Assert::main_binary() 14 | .with_args(&args) 15 | .succeeds() 16 | .and() 17 | .stdout() 18 | .is("main") 19 | .unwrap(); 20 | } 21 | 22 | #[test] 23 | fn entrypoint_resolution_lib() { 24 | let args = [ 25 | "readme", 26 | "--project-root", 27 | "tests/entrypoint-resolution/lib", 28 | "--no-title", 29 | "--no-license", 30 | ]; 31 | 32 | Assert::main_binary() 33 | .with_args(&args) 34 | .succeeds() 35 | .and() 36 | .stdout() 37 | .is("lib") 38 | .unwrap(); 39 | } 40 | 41 | #[test] 42 | fn entrypoint_resolution_cargo_lib() { 43 | let args = [ 44 | "readme", 45 | "--project-root", 46 | "tests/entrypoint-resolution/cargo-lib", 47 | "--no-title", 48 | "--no-license", 49 | ]; 50 | 51 | Assert::main_binary() 52 | .with_args(&args) 53 | .succeeds() 54 | .and() 55 | .stdout() 56 | .is("cargo lib") 57 | .unwrap(); 58 | } 59 | 60 | #[test] 61 | fn entrypoint_resolution_cargo_bin() { 62 | let args = [ 63 | "readme", 64 | "--project-root", 65 | "tests/entrypoint-resolution/cargo-bin", 66 | "--no-title", 67 | "--no-license", 68 | ]; 69 | 70 | Assert::main_binary() 71 | .with_args(&args) 72 | .succeeds() 73 | .and() 74 | .stdout() 75 | .is("cargo bin") 76 | .unwrap(); 77 | } 78 | -------------------------------------------------------------------------------- /tests/entrypoint-resolution/cargo-bin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "readme-test" 3 | version = "0.1.0" 4 | authors = ["Livio Ribeiro "] 5 | license = "MIT" 6 | 7 | [[bin]] 8 | name = "bin" 9 | path = "src/bin/bin.rs" 10 | -------------------------------------------------------------------------------- /tests/entrypoint-resolution/cargo-bin/src/bin/bin.rs: -------------------------------------------------------------------------------- 1 | //! cargo bin 2 | 3 | fn main() {} 4 | -------------------------------------------------------------------------------- /tests/entrypoint-resolution/cargo-lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "readme-test" 3 | version = "0.1.0" 4 | authors = ["Livio Ribeiro "] 5 | license = "MIT" 6 | 7 | [lib] 8 | path = "src/alt.rs" 9 | 10 | [[bin]] 11 | name = "bin" 12 | path = "src/bin/bin.rs" 13 | -------------------------------------------------------------------------------- /tests/entrypoint-resolution/cargo-lib/src/alt.rs: -------------------------------------------------------------------------------- 1 | //! cargo lib 2 | -------------------------------------------------------------------------------- /tests/entrypoint-resolution/cargo-lib/src/bin/bin.rs: -------------------------------------------------------------------------------- 1 | //! cargo bin 2 | 3 | fn main() {} 4 | -------------------------------------------------------------------------------- /tests/entrypoint-resolution/lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "readme-test" 3 | version = "0.1.0" 4 | authors = ["Livio Ribeiro "] 5 | license = "MIT" 6 | 7 | [[bin]] 8 | name = "bin" 9 | path = "src/bin/bin.rs" 10 | -------------------------------------------------------------------------------- /tests/entrypoint-resolution/lib/src/bin/bin.rs: -------------------------------------------------------------------------------- 1 | //! cargo bin 2 | 3 | fn main() {} 4 | -------------------------------------------------------------------------------- /tests/entrypoint-resolution/lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! lib 2 | -------------------------------------------------------------------------------- /tests/entrypoint-resolution/lib/src/main.rs: -------------------------------------------------------------------------------- 1 | //! main 2 | 3 | fn main() { 4 | println!("hello, world!"); 5 | } 6 | -------------------------------------------------------------------------------- /tests/entrypoint-resolution/main/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "readme-test" 3 | version = "0.1.0" 4 | authors = ["Livio Ribeiro "] 5 | license = "MIT" 6 | -------------------------------------------------------------------------------- /tests/entrypoint-resolution/main/src/main.rs: -------------------------------------------------------------------------------- 1 | //! main 2 | 3 | fn main() { 4 | println!("hello, world!"); 5 | } 6 | -------------------------------------------------------------------------------- /tests/multiline-doc.rs: -------------------------------------------------------------------------------- 1 | use assert_cli::Assert; 2 | 3 | const EXPECTED: &str = r#" 4 | [![Build Status](https://travis-ci.org/livioribeiro/cargo-readme.svg?branch=master)](https://travis-ci.org/livioribeiro/cargo-readme) 5 | 6 | # readme-test 7 | 8 | Some text here 9 | 10 | Test crate for cargo-readme 11 | 12 | ## Level 1 heading should become level 2 13 | 14 | ```rust 15 | // This is standard doc test and should be output as ```rust 16 | let condition = true; 17 | if condition { 18 | // Some conditional code here 19 | if condition { 20 | // Some nested conditional code here 21 | } 22 | } 23 | ``` 24 | 25 | ### Level 2 heading should become level 3 26 | 27 | ```rust 28 | // This also should output as ```rust 29 | ``` 30 | #### Level 3 heading should become level 4 31 | 32 | ```rust 33 | // This also should output as ```rust 34 | ``` 35 | 36 | ```rust 37 | // This should output as ```rust too 38 | ``` 39 | 40 | ```rust 41 | // And also this should output as ```rust 42 | ``` 43 | 44 | ```python 45 | # This should be on the output 46 | ``` 47 | "#; 48 | 49 | #[test] 50 | fn multiline_doc() { 51 | let args = [ 52 | "readme", 53 | "--project-root", 54 | "tests/test-project", 55 | "--input", 56 | "src/multiline.rs", 57 | ]; 58 | 59 | Assert::main_binary() 60 | .with_args(&args) 61 | .succeeds() 62 | .and() 63 | .stdout() 64 | .is(EXPECTED) 65 | .unwrap(); 66 | } 67 | -------------------------------------------------------------------------------- /tests/multiple-bin-fail.rs: -------------------------------------------------------------------------------- 1 | use assert_cli::Assert; 2 | 3 | const EXPECTED: &str = "Error: Multiple binaries found, choose one: [src/entry1.rs, src/entry2.rs]"; 4 | 5 | #[test] 6 | fn multiple_bin_fail() { 7 | let args = ["readme", "--project-root", "tests/multiple-bin-fail"]; 8 | 9 | Assert::main_binary() 10 | .with_args(&args) 11 | .fails() 12 | .and() 13 | .stderr() 14 | .is(EXPECTED) 15 | .unwrap(); 16 | } 17 | -------------------------------------------------------------------------------- /tests/multiple-bin-fail/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "multiple-bin" 3 | version = "0.1.0" 4 | [[bin]] 5 | name = "entry1" 6 | path = "src/entry1.rs" 7 | [[bin]] 8 | name = "entry2" 9 | path = "src/entry2.rs" -------------------------------------------------------------------------------- /tests/no-entrypoint-fail.rs: -------------------------------------------------------------------------------- 1 | use assert_cli::Assert; 2 | 3 | const EXPECTED: &str = "Error: No entrypoint found"; 4 | 5 | #[test] 6 | fn no_entrypoint_fail() { 7 | let args = ["readme", "--project-root", "tests/no-entrypoint-fail"]; 8 | 9 | Assert::main_binary() 10 | .with_args(&args) 11 | .fails() 12 | .and() 13 | .stderr() 14 | .is(EXPECTED) 15 | .unwrap(); 16 | } 17 | -------------------------------------------------------------------------------- /tests/no-entrypoint-fail/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "no-entrypoint" 3 | version = "0.1.0" -------------------------------------------------------------------------------- /tests/no-template.rs: -------------------------------------------------------------------------------- 1 | use assert_cli::Assert; 2 | 3 | const EXPECTED: &str = r#" 4 | # readme-test 5 | 6 | Test crate for cargo-readme 7 | 8 | ## Level 1 heading should become level 2 9 | 10 | ```rust 11 | // This is standard doc test and should be output as ```rust 12 | let condition = true; 13 | if condition { 14 | // Some conditional code here 15 | if condition { 16 | // Some nested conditional code here 17 | } 18 | } 19 | ``` 20 | 21 | ### Level 2 heading should become level 3 22 | 23 | ```rust 24 | // This also should output as ```rust 25 | ``` 26 | #### Level 3 heading should become level 4 27 | 28 | ```rust 29 | // This also should output as ```rust 30 | ``` 31 | 32 | ```rust 33 | // This should output as ```rust too 34 | ``` 35 | 36 | ```rust 37 | // And also this should output as ```rust 38 | ``` 39 | 40 | ```python 41 | # This should be on the output 42 | ``` 43 | 44 | License: MIT 45 | "#; 46 | 47 | #[test] 48 | fn no_template() { 49 | let args = [ 50 | "readme", 51 | "--project-root", 52 | "tests/test-project", 53 | "--no-template", 54 | "--no-badges", 55 | ]; 56 | 57 | Assert::main_binary() 58 | .with_args(&args) 59 | .succeeds() 60 | .and() 61 | .stdout() 62 | .is(EXPECTED) 63 | .unwrap(); 64 | } 65 | -------------------------------------------------------------------------------- /tests/project-with-version.rs: -------------------------------------------------------------------------------- 1 | use assert_cli::Assert; 2 | 3 | const EXPECTED: &str = "# project-with-version 4 | 5 | Current version: 0.1.0 6 | 7 | A test project with a version provided."; 8 | 9 | #[test] 10 | fn template_with_version() { 11 | let args = [ 12 | "readme", 13 | "--project-root", 14 | "tests/project-with-version", 15 | "--template", 16 | "README.tpl", 17 | ]; 18 | 19 | Assert::main_binary() 20 | .with_args(&args) 21 | .succeeds() 22 | .and() 23 | .stdout() 24 | .is(EXPECTED) 25 | .unwrap(); 26 | } 27 | -------------------------------------------------------------------------------- /tests/project-with-version/.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | -------------------------------------------------------------------------------- /tests/project-with-version/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "project-with-version" 3 | version = "0.1.0" 4 | authors = ["mexus "] 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /tests/project-with-version/README.tpl: -------------------------------------------------------------------------------- 1 | # {{crate}} 2 | 3 | Current version: {{version}} 4 | 5 | {{readme}} 6 | -------------------------------------------------------------------------------- /tests/project-with-version/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A test project with a version provided. 2 | -------------------------------------------------------------------------------- /tests/test-project/.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | -------------------------------------------------------------------------------- /tests/test-project/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "readme-test" 3 | version = "0.1.0" 4 | authors = ["Livio Ribeiro "] 5 | license = "MIT" 6 | 7 | [badges] 8 | travis-ci = { repository = "livioribeiro/cargo-readme" } -------------------------------------------------------------------------------- /tests/test-project/NOTITLE.tpl: -------------------------------------------------------------------------------- 1 | # {{crate}} 2 | 3 | Other readme template. 4 | 5 | {{readme}} 6 | -------------------------------------------------------------------------------- /tests/test-project/OTHER.tpl: -------------------------------------------------------------------------------- 1 | Other readme template. 2 | 3 | {{readme}} 4 | -------------------------------------------------------------------------------- /tests/test-project/README.tpl: -------------------------------------------------------------------------------- 1 | {{badges}} 2 | 3 | # {{crate}} 4 | 5 | Some text here 6 | 7 | {{readme}} 8 | -------------------------------------------------------------------------------- /tests/test-project/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Misleading first comment 2 | 3 | //! Test crate for cargo-readme 4 | //! 5 | //! # Level 1 heading should become level 2 6 | //! 7 | //! ``` 8 | //! // This is standard doc test and should be output as ```rust 9 | //! # This should NOT be on the output 10 | //! let condition = true; 11 | //! if condition { 12 | //! // Some conditional code here 13 | //! if condition { 14 | //! // Some nested conditional code here 15 | //! } 16 | //! } 17 | //! ``` 18 | //! 19 | //! ## Level 2 heading should become level 3 20 | //! 21 | //! ```ignore 22 | //! // This also should output as ```rust 23 | //! ``` 24 | //! ### Level 3 heading should become level 4 25 | //! 26 | //! ```ignore 27 | //! // This also should output as ```rust 28 | //! ``` 29 | //! 30 | //! ```no_run 31 | //! // This should output as ```rust too 32 | //! ``` 33 | //! 34 | //! ```should_panic 35 | //! // And also this should output as ```rust 36 | //! ``` 37 | //! 38 | //! ```python 39 | //! # This should be on the output 40 | //! ``` 41 | -------------------------------------------------------------------------------- /tests/test-project/src/multiline.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Test crate for cargo-readme 3 | 4 | # Level 1 heading should become level 2 5 | 6 | ``` 7 | // This is standard doc test and should be output as ```rust 8 | # This should NOT be on the output 9 | let condition = true; 10 | if condition { 11 | // Some conditional code here 12 | if condition { 13 | // Some nested conditional code here 14 | } 15 | } 16 | ``` 17 | 18 | ## Level 2 heading should become level 3 19 | 20 | ```ignore 21 | // This also should output as ```rust 22 | ``` 23 | ### Level 3 heading should become level 4 24 | 25 | ```ignore 26 | // This also should output as ```rust 27 | ``` 28 | 29 | ```no_run 30 | // This should output as ```rust too 31 | ``` 32 | 33 | ```should_panic 34 | // And also this should output as ```rust 35 | ``` 36 | 37 | ```python 38 | # This should be on the output 39 | ``` 40 | */ -------------------------------------------------------------------------------- /tests/test-project/src/no_docs.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webern/cargo-readme/e0683641c02856a2171ee18a306a8c571cace0ec/tests/test-project/src/no_docs.rs -------------------------------------------------------------------------------- /tests/test-project/src/other.rs: -------------------------------------------------------------------------------- 1 | //! Test crate for cargo-readme 2 | //! 3 | //! # Level 1 heading should become level 2 4 | -------------------------------------------------------------------------------- /tests/test-project/src/single_line.rs: -------------------------------------------------------------------------------- 1 | //! Test crate for cargo-readme 2 | --------------------------------------------------------------------------------