├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── NOTES.md ├── README.md ├── examples └── syn.rs ├── src ├── extract.rs ├── lib.rs ├── locate.rs ├── main.rs ├── parse.rs └── pprint.rs └── test_resources └── foo ├── enum.elon.html ├── fn.foo.html ├── index.html ├── macro.makrow.html ├── primative.ug.html ├── struct.structural.html └── trait.fooable.html /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | 14 | jobs: 15 | build: 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Build 22 | run: cargo build --verbose 23 | - name: Run tests 24 | run: cargo test --verbose 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /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 = "aho-corasick" 7 | version = "0.7.13" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "atty" 16 | version = "0.2.14" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 19 | dependencies = [ 20 | "hermit-abi", 21 | "libc", 22 | "winapi", 23 | ] 24 | 25 | [[package]] 26 | name = "autocfg" 27 | version = "1.0.0" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" 30 | 31 | [[package]] 32 | name = "base64" 33 | version = "0.12.3" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" 36 | 37 | [[package]] 38 | name = "bit-set" 39 | version = "0.5.1" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "e84c238982c4b1e1ee668d136c510c67a13465279c0cb367ea6baf6310620a80" 42 | dependencies = [ 43 | "bit-vec", 44 | ] 45 | 46 | [[package]] 47 | name = "bit-vec" 48 | version = "0.5.1" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "f59bbe95d4e52a6398ec21238d31577f2b28a9d86807f06ca59d191d8440d0bb" 51 | 52 | [[package]] 53 | name = "bitflags" 54 | version = "1.2.1" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 57 | 58 | [[package]] 59 | name = "bstr" 60 | version = "0.2.13" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "31accafdb70df7871592c058eca3985b71104e15ac32f64706022c58867da931" 63 | dependencies = [ 64 | "lazy_static", 65 | "memchr", 66 | "regex-automata", 67 | ] 68 | 69 | [[package]] 70 | name = "bytecount" 71 | version = "0.6.0" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "b0017894339f586ccb943b01b9555de56770c11cda818e7e3d8bd93f4ed7f46e" 74 | 75 | [[package]] 76 | name = "byteorder" 77 | version = "1.3.4" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" 80 | 81 | [[package]] 82 | name = "cfg-if" 83 | version = "0.1.10" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 86 | 87 | [[package]] 88 | name = "cfg-if" 89 | version = "1.0.0" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 92 | 93 | [[package]] 94 | name = "clap" 95 | version = "3.0.0-beta.2" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "4bd1061998a501ee7d4b6d449020df3266ca3124b941ec56cf2005c3779ca142" 98 | dependencies = [ 99 | "atty", 100 | "bitflags", 101 | "clap_derive", 102 | "indexmap", 103 | "lazy_static", 104 | "os_str_bytes", 105 | "strsim", 106 | "termcolor", 107 | "textwrap", 108 | "unicode-width", 109 | "vec_map", 110 | ] 111 | 112 | [[package]] 113 | name = "clap_derive" 114 | version = "3.0.0-beta.2" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "370f715b81112975b1b69db93e0b56ea4cd4e5002ac43b2da8474106a54096a1" 117 | dependencies = [ 118 | "heck", 119 | "proc-macro-error", 120 | "proc-macro2", 121 | "quote", 122 | "syn", 123 | ] 124 | 125 | [[package]] 126 | name = "colored" 127 | version = "1.9.3" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "f4ffc801dacf156c5854b9df4f425a626539c3a6ef7893cc0c5084a23f0b6c59" 130 | dependencies = [ 131 | "atty", 132 | "lazy_static", 133 | "winapi", 134 | ] 135 | 136 | [[package]] 137 | name = "encoding_rs" 138 | version = "0.8.24" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "a51b8cf747471cb9499b6d59e59b0444f4c90eba8968c4e44874e92b5b64ace2" 141 | dependencies = [ 142 | "cfg-if 0.1.10", 143 | ] 144 | 145 | [[package]] 146 | name = "encoding_rs_io" 147 | version = "0.1.7" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83" 150 | dependencies = [ 151 | "encoding_rs", 152 | ] 153 | 154 | [[package]] 155 | name = "fnv" 156 | version = "1.0.7" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 159 | 160 | [[package]] 161 | name = "futf" 162 | version = "0.1.4" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "7c9c1ce3fa9336301af935ab852c437817d14cd33690446569392e65170aac3b" 165 | dependencies = [ 166 | "mac", 167 | "new_debug_unreachable", 168 | ] 169 | 170 | [[package]] 171 | name = "getrandom" 172 | version = "0.1.14" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" 175 | dependencies = [ 176 | "cfg-if 0.1.10", 177 | "libc", 178 | "wasi", 179 | ] 180 | 181 | [[package]] 182 | name = "globset" 183 | version = "0.4.5" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "7ad1da430bd7281dde2576f44c84cc3f0f7b475e7202cd503042dff01a8c8120" 186 | dependencies = [ 187 | "aho-corasick", 188 | "bstr", 189 | "fnv", 190 | "log", 191 | "regex", 192 | ] 193 | 194 | [[package]] 195 | name = "grep" 196 | version = "0.2.7" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "582e93ba59eae0f9607f53832326f351f87a5175f17ff123b3986b927a0e7e68" 199 | dependencies = [ 200 | "grep-cli", 201 | "grep-matcher", 202 | "grep-printer", 203 | "grep-regex", 204 | "grep-searcher", 205 | ] 206 | 207 | [[package]] 208 | name = "grep-cli" 209 | version = "0.1.5" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "3592784f7791f5f0bfc2e4b5595dc2d4e34a79099d7228d121b954bae73db182" 212 | dependencies = [ 213 | "atty", 214 | "bstr", 215 | "globset", 216 | "lazy_static", 217 | "log", 218 | "regex", 219 | "same-file", 220 | "termcolor", 221 | "winapi-util", 222 | ] 223 | 224 | [[package]] 225 | name = "grep-matcher" 226 | version = "0.1.4" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "fdf0e1fd5af17008a918fd868e63ec0226e96ce88b832f00c7fb041e014b9350" 229 | dependencies = [ 230 | "memchr", 231 | ] 232 | 233 | [[package]] 234 | name = "grep-printer" 235 | version = "0.1.5" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "7f34d40c840fe8e413a94598345d8a4bb25fdbb4f3860a6c00a88bb296d89d00" 238 | dependencies = [ 239 | "base64", 240 | "bstr", 241 | "grep-matcher", 242 | "grep-searcher", 243 | "serde", 244 | "serde_derive", 245 | "serde_json", 246 | "termcolor", 247 | ] 248 | 249 | [[package]] 250 | name = "grep-regex" 251 | version = "0.1.8" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "a7ba5e492a049950acbd60f5d183011fdca66c248663f6b4e118d591eeada3d2" 254 | dependencies = [ 255 | "aho-corasick", 256 | "bstr", 257 | "grep-matcher", 258 | "log", 259 | "regex", 260 | "regex-syntax", 261 | "thread_local", 262 | ] 263 | 264 | [[package]] 265 | name = "grep-searcher" 266 | version = "0.1.7" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "19897320890970db77fb9e8110aeafdf5a74f4ee32756db16d44a96d8f454b1b" 269 | dependencies = [ 270 | "bstr", 271 | "bytecount", 272 | "encoding_rs", 273 | "encoding_rs_io", 274 | "grep-matcher", 275 | "log", 276 | "memmap", 277 | ] 278 | 279 | [[package]] 280 | name = "heck" 281 | version = "0.3.1" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" 284 | dependencies = [ 285 | "unicode-segmentation", 286 | ] 287 | 288 | [[package]] 289 | name = "hermit-abi" 290 | version = "0.1.6" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "eff2656d88f158ce120947499e971d743c05dbcbed62e5bd2f38f1698bbc3772" 293 | dependencies = [ 294 | "libc", 295 | ] 296 | 297 | [[package]] 298 | name = "html5ever" 299 | version = "0.25.1" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "aafcf38a1a36118242d29b92e1b08ef84e67e4a5ed06e0a80be20e6a32bfed6b" 302 | dependencies = [ 303 | "log", 304 | "mac", 305 | "markup5ever", 306 | "proc-macro2", 307 | "quote", 308 | "syn", 309 | ] 310 | 311 | [[package]] 312 | name = "indexmap" 313 | version = "1.4.0" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "c398b2b113b55809ceb9ee3e753fcbac793f1956663f3c36549c1346015c2afe" 316 | dependencies = [ 317 | "autocfg", 318 | ] 319 | 320 | [[package]] 321 | name = "itoa" 322 | version = "0.4.5" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" 325 | 326 | [[package]] 327 | name = "lazy_static" 328 | version = "1.4.0" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 331 | 332 | [[package]] 333 | name = "libc" 334 | version = "0.2.66" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558" 337 | 338 | [[package]] 339 | name = "log" 340 | version = "0.4.8" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" 343 | dependencies = [ 344 | "cfg-if 0.1.10", 345 | ] 346 | 347 | [[package]] 348 | name = "mac" 349 | version = "0.1.1" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" 352 | 353 | [[package]] 354 | name = "markup5ever" 355 | version = "0.10.0" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "aae38d669396ca9b707bfc3db254bc382ddb94f57cc5c235f34623a669a01dab" 358 | dependencies = [ 359 | "log", 360 | "phf", 361 | "phf_codegen", 362 | "serde", 363 | "serde_derive", 364 | "serde_json", 365 | "string_cache", 366 | "string_cache_codegen", 367 | "tendril", 368 | ] 369 | 370 | [[package]] 371 | name = "markup5ever_rcdom" 372 | version = "0.1.0" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "f015da43bcd8d4f144559a3423f4591d69b8ce0652c905374da7205df336ae2b" 375 | dependencies = [ 376 | "html5ever", 377 | "markup5ever", 378 | "tendril", 379 | "xml5ever", 380 | ] 381 | 382 | [[package]] 383 | name = "memchr" 384 | version = "2.3.3" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" 387 | 388 | [[package]] 389 | name = "memmap" 390 | version = "0.7.0" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b" 393 | dependencies = [ 394 | "libc", 395 | "winapi", 396 | ] 397 | 398 | [[package]] 399 | name = "new_debug_unreachable" 400 | version = "1.0.4" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" 403 | 404 | [[package]] 405 | name = "os_str_bytes" 406 | version = "2.3.1" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "06de47b848347d8c4c94219ad8ecd35eb90231704b067e67e6ae2e36ee023510" 409 | 410 | [[package]] 411 | name = "phf" 412 | version = "0.8.0" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" 415 | dependencies = [ 416 | "phf_shared", 417 | ] 418 | 419 | [[package]] 420 | name = "phf_codegen" 421 | version = "0.8.0" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" 424 | dependencies = [ 425 | "phf_generator", 426 | "phf_shared", 427 | ] 428 | 429 | [[package]] 430 | name = "phf_generator" 431 | version = "0.8.0" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" 434 | dependencies = [ 435 | "phf_shared", 436 | "rand", 437 | ] 438 | 439 | [[package]] 440 | name = "phf_shared" 441 | version = "0.8.0" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" 444 | dependencies = [ 445 | "siphasher", 446 | ] 447 | 448 | [[package]] 449 | name = "ppv-lite86" 450 | version = "0.2.8" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea" 453 | 454 | [[package]] 455 | name = "precomputed-hash" 456 | version = "0.1.1" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" 459 | 460 | [[package]] 461 | name = "proc-macro-error" 462 | version = "1.0.4" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 465 | dependencies = [ 466 | "proc-macro-error-attr", 467 | "proc-macro2", 468 | "quote", 469 | "syn", 470 | "version_check", 471 | ] 472 | 473 | [[package]] 474 | name = "proc-macro-error-attr" 475 | version = "1.0.4" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 478 | dependencies = [ 479 | "proc-macro2", 480 | "quote", 481 | "version_check", 482 | ] 483 | 484 | [[package]] 485 | name = "proc-macro2" 486 | version = "1.0.36" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" 489 | dependencies = [ 490 | "unicode-xid", 491 | ] 492 | 493 | [[package]] 494 | name = "quote" 495 | version = "1.0.14" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d" 498 | dependencies = [ 499 | "proc-macro2", 500 | ] 501 | 502 | [[package]] 503 | name = "rand" 504 | version = "0.7.3" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 507 | dependencies = [ 508 | "getrandom", 509 | "libc", 510 | "rand_chacha", 511 | "rand_core", 512 | "rand_hc", 513 | "rand_pcg", 514 | ] 515 | 516 | [[package]] 517 | name = "rand_chacha" 518 | version = "0.2.2" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" 521 | dependencies = [ 522 | "ppv-lite86", 523 | "rand_core", 524 | ] 525 | 526 | [[package]] 527 | name = "rand_core" 528 | version = "0.5.1" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 531 | dependencies = [ 532 | "getrandom", 533 | ] 534 | 535 | [[package]] 536 | name = "rand_hc" 537 | version = "0.2.0" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 540 | dependencies = [ 541 | "rand_core", 542 | ] 543 | 544 | [[package]] 545 | name = "rand_pcg" 546 | version = "0.2.1" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" 549 | dependencies = [ 550 | "rand_core", 551 | ] 552 | 553 | [[package]] 554 | name = "redox_syscall" 555 | version = "0.1.57" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" 558 | 559 | [[package]] 560 | name = "regex" 561 | version = "1.3.9" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6" 564 | dependencies = [ 565 | "aho-corasick", 566 | "memchr", 567 | "regex-syntax", 568 | "thread_local", 569 | ] 570 | 571 | [[package]] 572 | name = "regex-automata" 573 | version = "0.1.9" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4" 576 | dependencies = [ 577 | "byteorder", 578 | ] 579 | 580 | [[package]] 581 | name = "regex-syntax" 582 | version = "0.6.18" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" 585 | 586 | [[package]] 587 | name = "rocdoc" 588 | version = "0.1.2" 589 | dependencies = [ 590 | "clap", 591 | "colored", 592 | "grep", 593 | "quote", 594 | "select", 595 | "syn", 596 | "term_size", 597 | "test-case", 598 | ] 599 | 600 | [[package]] 601 | name = "ryu" 602 | version = "1.0.2" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8" 605 | 606 | [[package]] 607 | name = "same-file" 608 | version = "1.0.6" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 611 | dependencies = [ 612 | "winapi-util", 613 | ] 614 | 615 | [[package]] 616 | name = "select" 617 | version = "0.5.0" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "8ee061f90afcc8678bef7a78d0d121683f0ba753f740ff7005f833ec445876b7" 620 | dependencies = [ 621 | "bit-set", 622 | "html5ever", 623 | "markup5ever_rcdom", 624 | ] 625 | 626 | [[package]] 627 | name = "serde" 628 | version = "1.0.104" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449" 631 | 632 | [[package]] 633 | name = "serde_derive" 634 | version = "1.0.104" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "128f9e303a5a29922045a830221b8f78ec74a5f544944f3d5984f8ec3895ef64" 637 | dependencies = [ 638 | "proc-macro2", 639 | "quote", 640 | "syn", 641 | ] 642 | 643 | [[package]] 644 | name = "serde_json" 645 | version = "1.0.48" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "9371ade75d4c2d6cb154141b9752cf3781ec9c05e0e5cf35060e1e70ee7b9c25" 648 | dependencies = [ 649 | "itoa", 650 | "ryu", 651 | "serde", 652 | ] 653 | 654 | [[package]] 655 | name = "siphasher" 656 | version = "0.3.3" 657 | source = "registry+https://github.com/rust-lang/crates.io-index" 658 | checksum = "fa8f3741c7372e75519bd9346068370c9cdaabcc1f9599cbcf2a2719352286b7" 659 | 660 | [[package]] 661 | name = "string_cache" 662 | version = "0.8.0" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "2940c75beb4e3bf3a494cef919a747a2cb81e52571e212bfbd185074add7208a" 665 | dependencies = [ 666 | "lazy_static", 667 | "new_debug_unreachable", 668 | "phf_shared", 669 | "precomputed-hash", 670 | "serde", 671 | ] 672 | 673 | [[package]] 674 | name = "string_cache_codegen" 675 | version = "0.5.1" 676 | source = "registry+https://github.com/rust-lang/crates.io-index" 677 | checksum = "f24c8e5e19d22a726626f1a5e16fe15b132dcf21d10177fa5a45ce7962996b97" 678 | dependencies = [ 679 | "phf_generator", 680 | "phf_shared", 681 | "proc-macro2", 682 | "quote", 683 | ] 684 | 685 | [[package]] 686 | name = "strsim" 687 | version = "0.10.0" 688 | source = "registry+https://github.com/rust-lang/crates.io-index" 689 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 690 | 691 | [[package]] 692 | name = "syn" 693 | version = "1.0.84" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "ecb2e6da8ee5eb9a61068762a32fa9619cc591ceb055b3687f4cd4051ec2e06b" 696 | dependencies = [ 697 | "proc-macro2", 698 | "quote", 699 | "unicode-xid", 700 | ] 701 | 702 | [[package]] 703 | name = "tendril" 704 | version = "0.4.1" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "707feda9f2582d5d680d733e38755547a3e8fb471e7ba11452ecfd9ce93a5d3b" 707 | dependencies = [ 708 | "futf", 709 | "mac", 710 | "utf-8", 711 | ] 712 | 713 | [[package]] 714 | name = "term_size" 715 | version = "0.3.2" 716 | source = "registry+https://github.com/rust-lang/crates.io-index" 717 | checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" 718 | dependencies = [ 719 | "libc", 720 | "winapi", 721 | ] 722 | 723 | [[package]] 724 | name = "termcolor" 725 | version = "1.1.0" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" 728 | dependencies = [ 729 | "winapi-util", 730 | ] 731 | 732 | [[package]] 733 | name = "test-case" 734 | version = "1.2.1" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "c7cad0a06f9a61e94355aa3b3dc92d85ab9c83406722b1ca5e918d4297c12c23" 737 | dependencies = [ 738 | "cfg-if 1.0.0", 739 | "proc-macro2", 740 | "quote", 741 | "syn", 742 | "version_check", 743 | ] 744 | 745 | [[package]] 746 | name = "textwrap" 747 | version = "0.12.1" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789" 750 | dependencies = [ 751 | "unicode-width", 752 | ] 753 | 754 | [[package]] 755 | name = "thread_local" 756 | version = "1.0.1" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" 759 | dependencies = [ 760 | "lazy_static", 761 | ] 762 | 763 | [[package]] 764 | name = "time" 765 | version = "0.1.42" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" 768 | dependencies = [ 769 | "libc", 770 | "redox_syscall", 771 | "winapi", 772 | ] 773 | 774 | [[package]] 775 | name = "unicode-segmentation" 776 | version = "1.6.0" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" 779 | 780 | [[package]] 781 | name = "unicode-width" 782 | version = "0.1.7" 783 | source = "registry+https://github.com/rust-lang/crates.io-index" 784 | checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" 785 | 786 | [[package]] 787 | name = "unicode-xid" 788 | version = "0.2.0" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" 791 | 792 | [[package]] 793 | name = "utf-8" 794 | version = "0.7.5" 795 | source = "registry+https://github.com/rust-lang/crates.io-index" 796 | checksum = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7" 797 | 798 | [[package]] 799 | name = "vec_map" 800 | version = "0.8.1" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" 803 | 804 | [[package]] 805 | name = "version_check" 806 | version = "0.9.4" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 809 | 810 | [[package]] 811 | name = "wasi" 812 | version = "0.9.0+wasi-snapshot-preview1" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 815 | 816 | [[package]] 817 | name = "winapi" 818 | version = "0.3.8" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" 821 | dependencies = [ 822 | "winapi-i686-pc-windows-gnu", 823 | "winapi-x86_64-pc-windows-gnu", 824 | ] 825 | 826 | [[package]] 827 | name = "winapi-i686-pc-windows-gnu" 828 | version = "0.4.0" 829 | source = "registry+https://github.com/rust-lang/crates.io-index" 830 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 831 | 832 | [[package]] 833 | name = "winapi-util" 834 | version = "0.1.5" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 837 | dependencies = [ 838 | "winapi", 839 | ] 840 | 841 | [[package]] 842 | name = "winapi-x86_64-pc-windows-gnu" 843 | version = "0.4.0" 844 | source = "registry+https://github.com/rust-lang/crates.io-index" 845 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 846 | 847 | [[package]] 848 | name = "xml5ever" 849 | version = "0.16.1" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "0b1b52e6e8614d4a58b8e70cf51ec0cc21b256ad8206708bcff8139b5bbd6a59" 852 | dependencies = [ 853 | "log", 854 | "mac", 855 | "markup5ever", 856 | "time", 857 | ] 858 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rocdoc" 3 | version = "0.1.2" 4 | authors = [ 5 | "sminez ", 6 | "mchlrhw <4028654+mchlrhw@users.noreply.github.com>" 7 | ] 8 | license-file = "LICENSE" 9 | repository = "https://github.com/sminez/roc" 10 | documentation = "https://docs.rs/rocdoc" 11 | readme = "README.md" 12 | edition = "2018" 13 | description = """ 14 | Command line rust documentation searching in the style of godoc 15 | """ 16 | 17 | [[bin]] 18 | doc = false 19 | name = "roc" 20 | path = "src/main.rs" 21 | 22 | [dependencies] 23 | clap = "3.0.0-beta.2" 24 | colored = "1.9.3" 25 | select = "0.5.0" 26 | term_size = "0.3.2" 27 | grep = "0.2" 28 | syn = { version = "1.0.84", features = ["full", "visit"] } 29 | quote = "1.0" 30 | 31 | [dev-dependencies] 32 | test-case = "1.2" 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Innes Anderson-Morrison, mchlrhw 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 | -------------------------------------------------------------------------------- /NOTES.md: -------------------------------------------------------------------------------- 1 | # roc - notes on desired behaviour and implementation decisions 2 | 3 | ### Why parse rustdoc HTML output? 4 | In theory we _should_ be able to use the rust stdlib code directly (libsyntax?) 5 | to parse Rust source files and generate the documentation directly. While this 6 | does feel like the "right" way to go about this there are two major drawbacks: 7 | 1) We need to make sure that our parsing of Rust source is always up to date 8 | with language changes and that we can handle both different editions and minor 9 | point versions of the language. 10 | 2) Parsing from source is both a lot more complicated and potentially slower: 11 | `racer` frequently hangs / crashes in Vim for me and genreating full `rustdoc` 12 | output is painfully slow (on a par with compile times as opposed to near 13 | instant feedback as with `go doc`) 14 | 15 | So, if instead we require that the user has generated documentation (via 16 | `rustdoc` / `cargo doc`) which we then parse in order to generate our stubbed 17 | output, we gain in a couple of key areas: 18 | 1) Parsing HTML is stable so all we need to worry about is any changes to 19 | `rustdoc` that alter the tags/headings used in the HTML output which breaks 20 | our parsing / scraping of the `rustdoc` output. In the case where there is a 21 | breaking change for us, we are altering tags that we pull out not worrying 22 | about parsing rust source. 23 | 2) While `rustdoc` itself is slow, we can treat the output as a cache that we 24 | then read directly from disk. The format (at time or writing) prepends 'type' 25 | information to the file names (e.g. `struct.mystruct.html`) that allows us to 26 | use a simple walk of the doc output directory to resolve a large number of 27 | common queries and also determine what we need to pull out of each file to 28 | generate the output for `roc`. 29 | 30 | 31 | ### Query semantics 32 | The following outlines what we expect to see returned (/printed) as the result 33 | of each category of query when using `roc` on the command line. In cases where 34 | it is possible to make a direct comparison to `go doc`, `racer` or `rustdoc` the 35 | output from those commands will be marked with their source. 36 | 37 | Note that the `cut` command used for `racer` output is used to make the output 38 | easier to parse visually here, it removes some of the output that is useful for 39 | programatic use in auto-completion (which is what racer is for!) 40 | 41 | 42 | 1) Crate/module level 43 | ```bash 44 | $ roc [std|crate]::SomeModule 45 | 46 | # racer will list the _source files_ under a given module but only if you append 47 | # a trailing '::', without that it will simply report the location of the crate 48 | # root itself. Output is an ordered, typed list of module contents including 49 | # signatures 50 | $ racer complete std::fs:: | cut -d, -f5- 51 | 52 | # Output is an example of how to import this module, summary documentation about 53 | # the module and an ordered, typed list of module contents including signatures 54 | $ go doc os 55 | ``` 56 | 57 | Sometimes the top level summary for a crate/module is a bit much to blat out 58 | onto the command line so we may want a flag to toggle between "just give me the 59 | names and signatures" and "summarise what each item is". 60 | 61 | 62 | 2) A struct 63 | ```bash 64 | $ roc [std|crate]::SomeModule::SomeStruct 65 | 66 | # With racer, no trailing '::' will give the details for the struct itself (no 67 | # description of fields though) and trailing '::' will give details for methods. 68 | $ racer complete std::fs::File | cut -d, -f5- 69 | $ racer complete std::fs::File:: | cut -d, -f5- 70 | 71 | # Again, shows how to import, the struct definition, functions that _return_ the 72 | # struct and methods. Methods and functions get their signatures but not their 73 | # docstrings. 74 | $ go doc os File 75 | ``` 76 | 77 | We want to mirror the `go doc` behaviour of defaulting to showing all methods 78 | (static and instance) along with any top level documentation on the struct and 79 | public data members itself. 80 | 81 | 82 | 3) A method on a struct (static and instance based) or top level function 83 | ```bash 84 | $ roc [std|crate]::SomeModule::SomeStruct::SomeStaticMethod 85 | $ roc [std|crate]::SomeModule::SomeStruct.SomeInstanceMethod 86 | $ roc [std|crate]::SomeModule::SomeFunction 87 | 88 | # Only gives the signature 89 | $ racer complete std::fs::File::open | cut -d, -f5- 90 | 91 | # Signature and docstring 92 | $ go doc os File.Name 93 | ``` 94 | 95 | 96 | 4) Top level constant, variable or interface/trait 97 | ```bash 98 | $ roc [std|crate]::SomeModule::SomeConstant 99 | $ roc [std|crate]::SomeModule::SomeVar 100 | $ roc [std|crate]::SomeModule::SomeTrait 101 | 102 | $ racer ? 103 | 104 | # Declaration showing the value along with docstring. For interfaces, go doc 105 | # will show the signature of each instance method as well (effectively the raw 106 | # source definition) 107 | $ go doc os DevNull 108 | ``` 109 | 110 | We want to copy `go doc` 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | roc -- cli rust documentation that rocks 2 | ---------------------------------------- 3 | [![Build](https://github.com/sminez/roc/workflows/Build/badge.svg?branch=master)](https://github.com/sminez/roc/actions?query=workflow%3ABuild) 4 | 5 | `go doc` style command line searching through documentation for rust crates. 6 | 7 | `roc` piggybacks off of the HTML documentation generated by rustdoc, so you will 8 | need to run `cargo doc` before using roc if you want to look at anything other 9 | that the standard library. `roc` only aims to provide a quick interface to track 10 | down documentation summaries: most top level details will be shown but you 11 | should pass the `-o` flag to your query if you need to read the full 12 | documentation. This will open the local doc page using your default web browser. 13 | 14 | ### Some caveats 15 | * This is very much a work in progress! There are multiple features that need 16 | implementing (grepping for partial matches, bash/zsh completion scripts, 17 | hoogle style searching by signature etc) and several known bugs, mostly in 18 | output formatting. If you have a use case that is not currenly covered or a 19 | ideas for functionality that could be added, please raise an issue on the 20 | GitHub repo or a PR if you are happy to implement the features yourself. 21 | * `roc` assumes that you are using rustup and that you have stdlib docs downloaded. 22 | If not, you will be unable to search the docs of anything in `std`. 23 | * `roc` requires that you build any dependency crate docs before they can be found. 24 | The simplest workflow is to run `cargo doc` whenever you update your 25 | `Cargo.toml` in order to ensure that your local generated docs are up to date. 26 | 27 | ### Example usage 28 | ```bash 29 | # show summary comment for the Eq trait from stdlib 30 | $ roc std::cmp::Eq 31 | Trait for equality comparisons which are equivalence relations. 32 | 33 | This means, that in addition to a == b and a != b being strict inverses, the equality must 34 | be (for all a, b and c) 35 | 36 | 37 | # generate the documentation for roc itself 38 | $ cd roc && cargo doc 39 | 40 | 41 | # list out all of the known crates that we can find from this directory 42 | $ roc . 43 | :: known crates 44 | atty bit_set bit_vec bitflags cfg_if 45 | clap clap_derive colored debug_unreachable futf 46 | heck html5ever implementors indexmap lazy_static 47 | libc log mac markup5ever markup5ever_rcdom 48 | os_str_bytes phf phf_shared precomputed_hash proc_macro2 49 | proc_macro_error proc_macro_error_attr quote rocdoc select 50 | serde siphasher std string_cache strsim 51 | syn syn_mid tendril term_size termcolor 52 | textwrap time unicode_segmentation unicode_width unicode_xid 53 | utf8 vec_map xml5ever 54 | 55 | 56 | # show top level summary details for the roc crate 57 | $ roc rocdoc 58 | roc - command line doucmentation that rocks 59 | 60 | roc is an attempt at bringing godoc style quick docs searching to the command 61 | line for rust. It doesn\'t generate any documentation itself, instead it relies 62 | entirely on the local HTML output created by running cargo doc in the root of 63 | your crate. You will need to have rust installed via rustup and have the std lib 64 | docs downloaded in order to look at std lib. 65 | 66 | :: modules 67 | locate Locate the generated docs that we have available within the current workspace 68 | parse Parse the contents of rustdoc generated HTML files 69 | pprint Formatted output and pretty printing 70 | 71 | 72 | # show specific information about the Locator struct 73 | $ roc rocdoc::locate::Locator 74 | pub struct Locator { /* fields omitted */ } 75 | 76 | A Locator handles mapping a user query string from the command line to a file 77 | location on disk. It also provides information about what kind of documentation 78 | file it has found so that the appropriate parsing of the file contents can be 79 | carried out. 80 | 81 | pub fn new(query: String) -> Self 82 | pub fn target_file_path(&self) -> Option 83 | pub fn determine_tagged_path(&self) -> Option 84 | ``` 85 | 86 | ### Curent flags 87 | ``` 88 | -l, --list list out modules under the current path 89 | -o, --open open the selected doc page in the browser (local copy) 90 | ``` 91 | 92 | ### Local file system doc locations 93 | ``` 94 | std::* -> $(rustc --print sysroot)/share/doc/rust/html/std 95 | * -> $(dirname Cargo.toml)/target/doc 96 | ``` 97 | -------------------------------------------------------------------------------- /examples/syn.rs: -------------------------------------------------------------------------------- 1 | use rocdoc::extract::extract_items; 2 | use std::fs::read_to_string; 3 | 4 | const FNAME: &str = "src/extract.rs"; 5 | 6 | fn main() { 7 | let contents = read_to_string(FNAME).expect("wrong file path"); 8 | let items = extract_items("extract", &contents).unwrap(); 9 | 10 | println!("{}", items.render_all()); 11 | } 12 | -------------------------------------------------------------------------------- /src/extract.rs: -------------------------------------------------------------------------------- 1 | //! Use syn to extract definitions and documentation from Rust source files. 2 | //! 3 | //! The primary entrypoint for this module is the extract_items function which returns the parsed 4 | //! items found within the target module. 5 | // TODO: handle impl blocks being written in different files. 6 | use quote::ToTokens; 7 | use std::fmt; 8 | use syn::{ 9 | visit::{self, Visit}, 10 | Attribute, File, ItemFn, Lit, Meta, Result, Signature, 11 | }; 12 | 13 | #[derive(Debug, Clone, Default)] 14 | pub struct Module { 15 | name: String, 16 | docs: Option, 17 | } 18 | 19 | impl fmt::Display for Module { 20 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 21 | if let Some(s) = &self.docs { 22 | write!(f, "[{}]\n{}", self.name, s) 23 | } else { 24 | write!(f, "[{}]", self.name) 25 | } 26 | } 27 | } 28 | 29 | #[derive(Debug, Clone)] 30 | pub struct Function { 31 | // name: String, 32 | // vis: String, 33 | sig: String, 34 | docs: Option, 35 | } 36 | 37 | impl fmt::Display for Function { 38 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 39 | if let Some(s) = &self.docs { 40 | write!(f, "{}\n{}", s, self.sig) 41 | } else { 42 | write!(f, "{}", self.sig) 43 | } 44 | } 45 | } 46 | 47 | #[derive(Debug, Clone, Default)] 48 | pub struct DocItems { 49 | module: Module, 50 | fns: Vec, 51 | } 52 | 53 | impl DocItems { 54 | pub fn render_all(&self) -> String { 55 | format!( 56 | "{}\n\n[Functions]\n{}", 57 | self.module, 58 | self.fns 59 | .iter() 60 | .map(|f| format!("{}\n", f)) 61 | .collect::() 62 | ) 63 | } 64 | } 65 | 66 | #[derive(Debug, Clone, Default)] 67 | struct Extractor { 68 | module: String, 69 | items: DocItems, 70 | } 71 | 72 | pub fn extract_items(module: &str, contents: &str) -> Result { 73 | let syntax = syn::parse_file(contents)?; 74 | let mut ex = Extractor { 75 | module: module.to_string(), 76 | items: Default::default(), 77 | }; 78 | ex.visit_file(&syntax); 79 | 80 | Ok(ex.items) 81 | } 82 | 83 | impl<'ast> Visit<'ast> for Extractor { 84 | fn visit_file(&mut self, node: &'ast File) { 85 | self.items.module = Module { 86 | name: self.module.clone(), 87 | docs: extract_docs(&node.attrs).unwrap_or_default(), 88 | }; 89 | 90 | visit::visit_file(self, node); 91 | } 92 | 93 | fn visit_item_fn(&mut self, node: &'ast ItemFn) { 94 | self.items.fns.push(Function { 95 | // name: node.sig.ident.to_string(), 96 | // vis: format!("{}", node.vis.clone().into_token_stream()), 97 | sig: format_sig(node.sig.clone()), 98 | docs: extract_docs(&node.attrs).unwrap_or_default(), 99 | }); 100 | } 101 | } 102 | 103 | fn extract_docs(attrs: &[Attribute]) -> Result> { 104 | let mut docs = vec![]; 105 | 106 | for attr in attrs { 107 | match attr.parse_meta()? { 108 | Meta::NameValue(nv) if nv.path.is_ident("doc") => { 109 | if let Lit::Str(s) = nv.lit { 110 | docs.push(s.value().trim().to_string()); 111 | } 112 | } 113 | _ => continue, 114 | } 115 | } 116 | 117 | Ok(if docs.is_empty() { 118 | None 119 | } else { 120 | Some(docs.join("\n")) 121 | }) 122 | } 123 | 124 | fn format_sig(sig: Signature) -> String { 125 | format!("{}", sig.into_token_stream()) 126 | .replace(" (", "(") 127 | .replace(" < ", "<") 128 | .replace(" > ", ">") 129 | .replace(" >", ">") 130 | .replace("& ", "&") 131 | .replace(" ,", ",") 132 | .replace(" :", ":") 133 | } 134 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! roc - command line doucmentation that rocks 2 | //! 3 | //! roc is an attempt at bringing godoc style quick docs searching to the command 4 | //! line for rust. It doesn't generate any documentation itself, instead it relies 5 | //! entirely on the local HTML output created by running `cargo doc` in the root of 6 | //! your crate. You will need to have rust installed via rustup and have the std lib 7 | //! docs downloaded in order to look at std lib. 8 | pub mod extract; 9 | mod locate; 10 | mod parse; 11 | mod pprint; 12 | -------------------------------------------------------------------------------- /src/locate.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | * Locate the generated docs that we have available within the current workspace 3 | */ 4 | use crate::pprint::{header, pprint_as_columns, CRATE_LIST_HEADING_COLOR}; 5 | use std::fs; 6 | use std::{env, ffi, path, process}; 7 | 8 | /// Determine the known crates under this path 9 | pub fn list_known_crates() -> std::io::Result<()> { 10 | let mut dirs = vec![String::from("std")]; 11 | 12 | if let Some(root) = get_doc_root(&CrateType::Cargo) { 13 | for res in root.read_dir()? { 14 | let entry = res?; 15 | let meta = entry.metadata()?; 16 | if meta.is_dir() && entry.file_name() != "src" { 17 | dirs.push(String::from(entry.file_name().to_str().unwrap())); 18 | } 19 | } 20 | } 21 | 22 | dirs.sort(); 23 | let title = header("known crates", CRATE_LIST_HEADING_COLOR); 24 | println!("{}\n{}", title, pprint_as_columns(dirs)); 25 | Ok(()) 26 | } 27 | 28 | /// Each of the various documentation types we can be asked to locate 29 | #[derive(PartialEq, Eq, Debug, Clone)] 30 | pub enum Tag { 31 | /// A public constant 32 | Constant, 33 | /// An enum and its variants 34 | Enum, 35 | /// A top level function 36 | Function, 37 | /// An exported macro 38 | Macro, 39 | /// A child module under the current search path 40 | Module, 41 | /// A stdlib primative data type 42 | Primative, 43 | /// A struct (we wont see hidden fields) 44 | Struct, 45 | /// A trait definition 46 | Trait, 47 | /// Methods on a struct if that's what the current path points to 48 | Method, 49 | /// Something we don't know how to handle yet 50 | Unknown, 51 | } 52 | 53 | impl From for Tag { 54 | fn from(path_buf: path::PathBuf) -> Tag { 55 | let file_name = path_buf.file_name().unwrap(); 56 | if file_name == ffi::OsString::from("index.html") { 57 | return Tag::Module; 58 | } 59 | 60 | match file_name.to_os_string().into_string() { 61 | Err(_) => Tag::Unknown, 62 | Ok(s) => match s.split('.').collect::>()[0] { 63 | "constant" => Tag::Constant, 64 | "enum" => Tag::Enum, 65 | "fn" => Tag::Function, 66 | "macro" => Tag::Macro, 67 | "primative" => Tag::Primative, 68 | "struct" => Tag::Struct, 69 | "trait" => Tag::Trait, 70 | _ => Tag::Unknown, 71 | }, 72 | } 73 | } 74 | } 75 | 76 | /// A filesystem path generated by the search query issued by the user 77 | #[derive(PartialEq, Eq, Debug, Clone)] 78 | pub struct TaggedPath { 79 | path_buf: path::PathBuf, 80 | without_prefix: Option, 81 | /// The file name that was located 82 | pub file_name: String, 83 | /// A method name within this file if the file itself is for the parent struct 84 | pub method_name: Option, 85 | /// A Tag indicating what type of file this is 86 | pub tag: Tag, 87 | } 88 | 89 | impl TaggedPath { 90 | /// The underlying abs path as a String 91 | pub fn path(&self) -> String { 92 | String::from(self.path_buf.to_str().unwrap()) 93 | } 94 | 95 | fn dir(&self) -> String { 96 | let mut dir = self.path_buf.clone(); 97 | dir.pop(); 98 | String::from(dir.to_str().unwrap()) 99 | } 100 | 101 | /// Returns a vec of child directory leaf names next to this path 102 | pub fn sibling_dirs(&self) -> std::io::Result> { 103 | let mut dirs = Vec::new(); 104 | for res in fs::read_dir(self.dir())? { 105 | let entry = res?; 106 | let meta = entry.metadata()?; 107 | if meta.is_dir() { 108 | dirs.push(String::from(entry.file_name().to_str().unwrap())); 109 | } 110 | } 111 | 112 | Ok(dirs) 113 | } 114 | } 115 | 116 | impl From for TaggedPath { 117 | fn from(path_buf: path::PathBuf) -> TaggedPath { 118 | let file_name = match path_buf.file_name().unwrap().to_str() { 119 | Some(s) => String::from(s), 120 | None => panic!("file path is not valid utf8"), 121 | }; 122 | let tag = Tag::from(path_buf.clone()); 123 | let method_name = None; 124 | let without_prefix = match tag { 125 | Tag::Module | Tag::Unknown => None, 126 | _ => Some(ffi::OsString::from( 127 | file_name.splitn(2, '.').collect::>()[1], 128 | )), 129 | }; 130 | 131 | TaggedPath { 132 | path_buf, 133 | file_name, 134 | without_prefix, 135 | method_name, 136 | tag, 137 | } 138 | } 139 | } 140 | 141 | /// Local documentation is stored in different locations depending on whether or 142 | /// not the query resolves to something that is from the standard library or a 143 | /// third party crate that the user is pulling in via Cargo. 144 | #[derive(PartialEq, Eq, Debug)] 145 | enum CrateType { 146 | StdLib, 147 | Cargo, 148 | } 149 | 150 | /// We can't resolve all cases when we parse the Query but it is useful to know 151 | /// if the query is for a method or a concrete symbol that will have its own 152 | /// documentation file 153 | #[derive(PartialEq, Eq, Debug)] 154 | enum QueryType { 155 | InstanceMethod, 156 | Unknown, 157 | } 158 | 159 | /// A Locator handles mapping a user query string from the command line to a file 160 | /// location on disk. It also provides information about what kind of documentation 161 | /// file it has found so that the appropriate parsing of the file contents can be 162 | /// carried out. 163 | #[derive(PartialEq, Eq, Debug)] 164 | pub struct Locator { 165 | root: path::PathBuf, 166 | crate_type: CrateType, 167 | query_type: QueryType, 168 | components: Vec, 169 | } 170 | 171 | impl Locator { 172 | /// Create a new Locator based on the given user query path entered at the command line 173 | pub fn new(query: String) -> Self { 174 | let components: Vec = query 175 | .split("::") 176 | .flat_map(|s| s.split('.')) 177 | .filter(|s| !s.is_empty()) 178 | .map(String::from) 179 | .collect(); 180 | 181 | let crate_type = if components[0] == "std" { 182 | CrateType::StdLib 183 | } else { 184 | CrateType::Cargo 185 | }; 186 | 187 | let query_type = if query.contains('.') { 188 | QueryType::InstanceMethod 189 | } else { 190 | QueryType::Unknown 191 | }; 192 | 193 | let root = get_doc_root(&crate_type).expect("unable to locate documentation root"); 194 | 195 | return Locator { 196 | root, 197 | crate_type, 198 | query_type, 199 | components, 200 | }; 201 | } 202 | 203 | /// The resolved local file path if we were able to determine one 204 | pub fn target_file_path(&self) -> Option { 205 | self.determine_tagged_path().map(|p| p.path()) 206 | } 207 | 208 | /// The resolved local TaggedPath (path with added metadata) 209 | pub fn determine_tagged_path(&self) -> Option { 210 | let mut search_path = self.root.clone(); 211 | search_path.extend(self.query_dir_as_path_buf().iter()); 212 | 213 | // Check to see if we are targeting a module and grab index.html 214 | // if we are. 215 | search_path.push(self.last_component().clone()); 216 | if search_path.is_dir() { 217 | search_path.push("index.html"); 218 | return Some(TaggedPath::from(search_path)); 219 | } else { 220 | search_path.pop(); 221 | } 222 | 223 | let target_filename = self.query_filename(); 224 | let target_filename_for_method = self.query_filename_for_method(); 225 | 226 | while search_path != self.root { 227 | if let Ok(entries) = search_path.read_dir() { 228 | for entry in entries.filter_map(|p| p.ok()) { 229 | let mut tagged = TaggedPath::from(entry.path()); 230 | if let Some(without_prefix) = &tagged.without_prefix { 231 | if without_prefix == &target_filename { 232 | return Some(tagged); 233 | } 234 | 235 | if let Some(ref target) = target_filename_for_method { 236 | if without_prefix == target { 237 | tagged.tag = Tag::Method; 238 | tagged.method_name = 239 | Some(String::from(self.last_component().to_str().unwrap())); 240 | return Some(tagged); 241 | } 242 | } 243 | } 244 | } 245 | }; 246 | 247 | if let Some(p) = search_path.file_name() { 248 | if p == target_filename { 249 | search_path.push("index.html"); 250 | return Some(TaggedPath::from(search_path)); 251 | } 252 | } 253 | 254 | search_path.pop(); 255 | } 256 | return None; 257 | } 258 | 259 | fn query_dir_as_path_buf(&self) -> path::PathBuf { 260 | let mut buf = path::PathBuf::new(); 261 | buf.extend(self.components.iter()); 262 | buf.pop(); 263 | 264 | return buf; 265 | } 266 | 267 | fn query_filename_for_method(&self) -> Option { 268 | if self.components.len() > 2 { 269 | let comp = self.components[self.components.len() - 2].clone(); 270 | Some(ffi::OsString::from(comp + ".html")) 271 | } else { 272 | None 273 | } 274 | } 275 | 276 | fn query_filename(&self) -> ffi::OsString { 277 | if let Some(s) = self.components.last() { 278 | ffi::OsString::from(String::from(s) + ".html") 279 | } else { 280 | panic!("no last component in method query") 281 | } 282 | } 283 | 284 | fn last_component(&self) -> ffi::OsString { 285 | if let Some(s) = self.components.last() { 286 | ffi::OsString::from(s) 287 | } else { 288 | panic!("no last component in method query") 289 | } 290 | } 291 | } 292 | 293 | fn get_doc_root(crate_type: &CrateType) -> Option { 294 | match crate_type { 295 | CrateType::StdLib => get_sys_root().map(|r| r.join(path::Path::new("share/doc/rust/html"))), 296 | CrateType::Cargo => get_crate_root().map(|r| r.join(path::Path::new("target/doc"))), 297 | } 298 | } 299 | 300 | fn get_sys_root() -> Option { 301 | process::Command::new("rustc") 302 | .arg("--print") 303 | .arg("sysroot") 304 | .output() 305 | .ok() 306 | .and_then(|out| String::from_utf8(out.stdout).ok()) 307 | .map(|s| path::Path::new(s.trim()).to_path_buf()) 308 | } 309 | 310 | fn get_crate_root() -> Option { 311 | let mut cur_dir = env::current_dir().ok().unwrap(); 312 | let cargo_toml = ffi::OsStr::new("Cargo.toml"); 313 | let file_system_root = path::Path::new("/"); 314 | 315 | while cur_dir != file_system_root { 316 | if let Ok(paths) = cur_dir.read_dir() { 317 | for entry in paths { 318 | if let Ok(entry) = entry { 319 | if let Some(fname) = entry.path().as_path().file_name() { 320 | if fname == cargo_toml { 321 | return Some(cur_dir); 322 | } 323 | } 324 | }; 325 | } 326 | }; 327 | cur_dir.pop(); 328 | } 329 | return None; 330 | } 331 | 332 | #[cfg(test)] 333 | mod tests { 334 | use super::*; 335 | use test_case::test_case; 336 | 337 | #[test_case("test_resources/foo/enum.elon.html", Tag::Enum)] 338 | #[test_case("test_resources/foo/fn.foo.html", Tag::Function)] 339 | #[test_case("test_resources/foo/macro.makrow.html", Tag::Macro)] 340 | #[test_case("test_resources/foo/index.html", Tag::Module)] 341 | #[test_case("test_resources/foo/primative.ug.html", Tag::Primative)] 342 | #[test_case("test_resources/foo/struct.structural.html", Tag::Struct)] 343 | #[test_case("test_resources/foo/trait.fooable.html", Tag::Trait)] 344 | #[test_case("test_resources/foo/some_other_unknown.html", Tag::Unknown)] 345 | fn path_buf_into_symbol_type(path: &str, expected: Tag) { 346 | let path_buf = path::PathBuf::from(path); 347 | let symbol_type = Tag::from(path_buf); 348 | 349 | assert_eq!(symbol_type, expected); 350 | } 351 | 352 | #[test_case("std::fs::File", CrateType::StdLib, QueryType::Unknown, vec!["std", "fs", "File"])] 353 | #[test_case("std::path::PathBuf.file_name", CrateType::StdLib, QueryType::InstanceMethod, vec!["std", "path", "PathBuf", "file_name"])] 354 | #[test_case("foo::Foo.bar", CrateType::Cargo, QueryType::InstanceMethod, vec!["foo", "Foo", "bar"])] 355 | fn locator_from_input( 356 | path: &str, 357 | crate_type: CrateType, 358 | query_type: QueryType, 359 | comps: Vec<&str>, 360 | ) { 361 | let root = get_doc_root(&crate_type).unwrap(); 362 | assert_eq!( 363 | Locator::new(String::from(path)), 364 | Locator { 365 | root: root, 366 | crate_type: crate_type, 367 | query_type: query_type, 368 | components: comps.iter().map(|c| String::from(*c)).collect() 369 | } 370 | ) 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Clap; 2 | use rocdoc::locate; 3 | use rocdoc::parse; 4 | use std::process; 5 | 6 | const CRATE_ROOT_QUERIES: &[&'static str] = &[".", "crate"]; 7 | 8 | /** 9 | * roc :: command lines rust documentation that rocks 10 | */ 11 | #[derive(Clap, Debug)] 12 | struct Options { 13 | /// list out child modules under the given path 14 | #[clap(short = 'l', long = "list")] 15 | list: bool, 16 | 17 | /// open the selected doc page in the browser using full rustdoc 18 | #[clap(short = 'o', long = "open")] 19 | open_in_browser: bool, 20 | 21 | /// grep the resulting output to only show lines matching this query 22 | #[clap(short = 'g', long = "grep")] 23 | grep: Option, 24 | 25 | /// [::[.]] 26 | query: String, 27 | } 28 | 29 | fn main() { 30 | let opts: Options = Options::parse(); 31 | 32 | if CRATE_ROOT_QUERIES.contains(&opts.query.as_ref()) { 33 | if let Err(e) = locate::list_known_crates() { 34 | eprintln!("{}", e); 35 | } 36 | return; 37 | } 38 | 39 | let locator = locate::Locator::new(opts.query); 40 | let tagged_path = match locator.determine_tagged_path() { 41 | Some(p) => p, 42 | None => { 43 | println!("unable to resolve query path"); 44 | process::exit(1); 45 | } 46 | }; 47 | 48 | if opts.open_in_browser { 49 | open_in_browser(tagged_path); 50 | } else if opts.list { 51 | parse::DocParser::new(tagged_path).show_child_modules(); 52 | } else { 53 | parse::DocParser::new(tagged_path).parse_and_print(opts.grep); 54 | } 55 | } 56 | 57 | fn open_in_browser(tp: locate::TaggedPath) { 58 | let path = tp.path(); 59 | let res = process::Command::new("xdg-open").arg(&path).spawn(); 60 | if let Err(e) = res { 61 | match e.kind() { 62 | std::io::ErrorKind::NotFound => { 63 | process::Command::new("open") 64 | .arg(path) 65 | .spawn() 66 | .expect("failed to open using the 'open' command"); 67 | } 68 | _ => panic!("failed to open using the 'xdg-open' command"), 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/parse.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | * Parse the contents of rustdoc generated HTML files 3 | */ 4 | use crate::{ 5 | locate, pprint, 6 | pprint::{header, ENUM_HEADING_COLOR, SECTION_HEADING_COLOR}, 7 | }; 8 | use grep::{ 9 | regex::RegexMatcher, 10 | searcher::{sinks::UTF8, Searcher}, 11 | }; 12 | use select::{ 13 | document::Document, 14 | node::Node, 15 | predicate::{And, Class, Name, Not}, 16 | }; 17 | use std::{error::Error, fs}; 18 | 19 | /** 20 | * Parses generated HTML output from rustdoc to give summarised results. 21 | */ 22 | pub struct DocParser { 23 | contents: Document, 24 | tag: locate::Tag, 25 | method_name: Option, 26 | } 27 | 28 | impl DocParser { 29 | /// Create a new DocParser rooted at the given tagged search path 30 | pub fn new(tagged_path: locate::TaggedPath) -> Self { 31 | let file_name = tagged_path.file_name.clone(); 32 | let file = fs::File::open(tagged_path.path()) 33 | .expect(&format!("unable to open file: {}", file_name)); 34 | let contents = Document::from_read(file).expect(&format!( 35 | "unable to parse rustdoc generated HTML file: {}", 36 | file_name 37 | )); 38 | 39 | return DocParser { 40 | contents, 41 | tag: tagged_path.tag.clone(), 42 | method_name: tagged_path.method_name.clone(), 43 | }; 44 | } 45 | 46 | /// Instead of parsing the contents of the search result, show child modules instead 47 | pub fn show_child_modules(&self) { 48 | let s = if let Some(ms) = self.table_with_header("modules", &None) { 49 | ms 50 | } else { 51 | "No child modules found".into() 52 | }; 53 | 54 | println!("{}", s); 55 | } 56 | 57 | /// Parse the contents of a located doc file and pretty print them to the terminal 58 | pub fn parse_and_print(&self, grep: Option) { 59 | let mut sections: Vec = vec![]; 60 | 61 | match self.tag { 62 | locate::Tag::Module => { 63 | if let Some(s) = self.extract_summary() { 64 | sections.push(s) 65 | }; 66 | if let Some(s) = self.table_with_header("modules", &grep) { 67 | sections.push(s) 68 | }; 69 | if let Some(s) = self.table_with_header("traits", &grep) { 70 | sections.push(s) 71 | }; 72 | if let Some(s) = self.table_with_header("constants", &grep) { 73 | sections.push(s) 74 | }; 75 | if let Some(s) = self.table_with_header("structs", &grep) { 76 | sections.push(s) 77 | }; 78 | if let Some(s) = self.table_with_header("enums", &grep) { 79 | sections.push(s) 80 | }; 81 | if let Some(s) = self.table_with_header("functions", &grep) { 82 | sections.push(s) 83 | }; 84 | if let Some(s) = self.table_with_header("macros", &grep) { 85 | sections.push(s) 86 | }; 87 | } 88 | 89 | locate::Tag::Struct => { 90 | sections.push(self.extract_type_declaration()); 91 | if let Some(s) = self.extract_summary() { 92 | sections.push(s) 93 | } 94 | sections.push(self.extract_method_signatures(&grep)); 95 | } 96 | 97 | locate::Tag::Method => { 98 | let s = match self.extract_method() { 99 | Some(s) => s, 100 | None => format!("{} is not method", self.method_name.clone().unwrap()), 101 | }; 102 | sections.push(s) 103 | } 104 | 105 | locate::Tag::Enum => { 106 | if let Some(s) = self.extract_summary() { 107 | sections.push(s) 108 | } 109 | if let Some(s) = self.extract_enum_variants(&grep) { 110 | sections.push(s) 111 | }; 112 | } 113 | 114 | _ => { 115 | if let Some(s) = self.extract_summary() { 116 | sections.push(s) 117 | } 118 | } 119 | } 120 | 121 | // TODO: Current parsing leaves '[src]' at the end of a lot of lines 122 | // This is a quick hack to tidy that up but we should do this in 123 | // a smarter way really... 124 | sections.retain(|s| s.len() > 0); 125 | println!("{}", sections.join("\n\n").replace("[src]", "")); 126 | } 127 | 128 | fn extract_summary(&self) -> Option { 129 | let docblock = self 130 | .contents 131 | .find(And(Class("docblock"), Not(Class("type-decl")))) 132 | .next()?; 133 | 134 | let mut paragraphs: Vec = vec![]; 135 | for node in docblock.children() { 136 | if node.is(Name("p")) { 137 | paragraphs.push(node.text()); 138 | } else if node.text() == "\n" { 139 | continue; 140 | } else { 141 | break; 142 | } 143 | } 144 | return Some(paragraphs.join("\n\n")); 145 | } 146 | 147 | // Not Option-al as all structs must have a type declaration 148 | fn extract_type_declaration(&self) -> String { 149 | self.contents 150 | .find(Class("type-decl")) 151 | .map(|n| n.text()) 152 | .collect::>() 153 | .join("\n") 154 | } 155 | 156 | fn extract_method_signatures(&self, grep: &Option) -> String { 157 | let impl_block = self.contents.find(Class("impl-items")).next().unwrap(); 158 | let mut methods: Vec = vec![]; 159 | 160 | for node in impl_block.children() { 161 | if node.is(Class("method")) { 162 | methods.push(node.text()); 163 | } else { 164 | continue; 165 | } 166 | } 167 | 168 | let raw = methods.join("\n"); 169 | match grep { 170 | Some(grep_str) => matching_lines(raw, grep_str).unwrap(), 171 | None => raw, 172 | } 173 | } 174 | 175 | fn extract_method(&self) -> Option { 176 | let mut sections: Vec = vec![]; 177 | let id = format!("method.{}", self.method_name.clone()?); 178 | let node = self 179 | .contents 180 | .find(|n: &Node| n.attr("id").map_or(false, |i| i == id)) 181 | .next()?; 182 | 183 | sections.push(node.text()); 184 | 185 | if let Some(n) = node.next() { 186 | if n.is(Class("docblock")) { 187 | // TODO: the raw formatting here isn't great as it becomes one big blob 188 | // probably want to try our own iteration over the children? 189 | sections.push(n.text()); 190 | } 191 | } 192 | 193 | return Some(sections.join("\n\n")); 194 | } 195 | 196 | fn extract_enum_variants(&self, grep: &Option) -> Option { 197 | let raw = self 198 | .contents 199 | .find(|n: &Node| n.attr("id").map_or(false, |i| i.starts_with("variant."))) 200 | .map(|n| { 201 | let mut lines = vec![header(&n.text(), ENUM_HEADING_COLOR)]; 202 | if let Some(n) = n.next() { 203 | if n.is(Class("docblock")) { 204 | lines.push(n.text()); 205 | } 206 | } 207 | lines.join("\n") 208 | }) 209 | .collect::>() 210 | .join("\n"); 211 | 212 | match grep { 213 | Some(grep_str) => matching_lines(raw, grep_str).ok(), 214 | None => Some(raw), 215 | } 216 | } 217 | 218 | fn table_after_header(&self, header: &str) -> Option { 219 | Some( 220 | pprint::Table::from_rows( 221 | self.contents 222 | .find(And(Class("section-header"), |n: &Node| { 223 | n.attr("id").map_or(false, |i| i == header) 224 | })) 225 | .next()? // the header itself 226 | .next()? // a newline... 227 | .next()? // the table 228 | .first_child()? // tbody 229 | .children() 230 | .map(|n| { 231 | n.children() 232 | .map(|c| String::from(c.text().replace("\n", " ").trim_end())) 233 | .collect::>() 234 | }) 235 | .collect::>>(), 236 | ) 237 | .as_string(), 238 | ) 239 | } 240 | 241 | fn table_with_header(&self, header_str: &str, grep: &Option) -> Option { 242 | self.table_after_header(header_str).map(|t| { 243 | let s = match grep { 244 | Some(grep_str) => match matching_lines(t, grep_str) { 245 | Ok(lines) => lines, 246 | Err(e) => panic!("{}", e), 247 | }, 248 | None => t, 249 | }; 250 | format!("{}\n{}", header(header_str, SECTION_HEADING_COLOR), s) 251 | }) 252 | } 253 | } 254 | 255 | fn matching_lines(s: String, pattern: &str) -> Result> { 256 | let matcher = RegexMatcher::new(pattern)?; 257 | let mut matches: Vec = vec![]; 258 | let lines: Vec<&str> = s.split('\n').collect(); 259 | 260 | Searcher::new().search_slice( 261 | &matcher, 262 | s.as_bytes(), 263 | UTF8(|lnum, _| { 264 | matches.push(lines[lnum as usize].to_string()); 265 | Ok(true) 266 | }), 267 | )?; 268 | 269 | Ok(matches.join("\n")) 270 | } 271 | -------------------------------------------------------------------------------- /src/pprint.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | * Formatted output and pretty printing 3 | */ 4 | use colored::*; 5 | use std::cmp; 6 | use term_size; 7 | 8 | // Colors used when outputting with color 9 | pub(crate) const CRATE_LIST_HEADING_COLOR: &'static str = "blue"; 10 | pub(crate) const SECTION_HEADING_COLOR: &'static str = "yellow"; 11 | pub(crate) const ENUM_HEADING_COLOR: &'static str = "green"; 12 | 13 | // Space between columns when pretty printing 14 | const SPACER: &'static str = " "; 15 | 16 | // Default maximum output width when pretty printing 17 | const DEFAULT_TERM_WIDTH: usize = 90; 18 | 19 | /// A simple colored header for sections 20 | pub(crate) fn header(s: &str, color: &str) -> String { 21 | format!("{} {}", "::".color(color).bold(), s.bold()) 22 | } 23 | 24 | /// Print a vec of strings as column spaced rows 25 | pub(crate) fn pprint_as_columns(elems: Vec) -> String { 26 | let col_width = elems.iter().map(String::len).max().unwrap(); 27 | let per_row = max_width() / (col_width + 1); 28 | 29 | elems 30 | .chunks(per_row) 31 | .map(|chunk| { 32 | chunk 33 | .iter() 34 | .map(|c| format!("{:01$}", c, col_width)) 35 | .collect::>() 36 | .join(" ") 37 | }) 38 | .collect::>() 39 | .join("\n") 40 | } 41 | 42 | fn max_width() -> usize { 43 | if let Some((w, _)) = term_size::dimensions() { 44 | w 45 | } else { 46 | DEFAULT_TERM_WIDTH 47 | } 48 | } 49 | 50 | // A single row within a table layout 51 | struct Row { 52 | cells: Vec, 53 | } 54 | 55 | impl Row { 56 | fn formatted(&self, column_widths: &Vec) -> String { 57 | self.cells 58 | .iter() 59 | .zip(column_widths) 60 | .map(|e| format!("{:01$}", e.0, e.1)) 61 | .collect::>() 62 | .join(SPACER) 63 | } 64 | 65 | fn two_column_wrapped(&self, column_widths: &Vec) -> String { 66 | if self.cells.len() != 2 { 67 | panic!( 68 | "two_column_wrapped called on row with {} columns", 69 | self.cells.len() 70 | ); 71 | } 72 | 73 | let max = max_width(); 74 | if column_widths.iter().sum::() + SPACER.len() <= max { 75 | return self.formatted(&column_widths); 76 | } 77 | 78 | let mut buf = String::new(); 79 | let mut current = String::new(); 80 | let offset = vec![" "; column_widths[0] + SPACER.len() + 1].join(""); 81 | current.push_str(&(format!("{:01$}", self.cells[0], column_widths[0]) + SPACER)); 82 | 83 | for word in self.cells[1].split_whitespace() { 84 | if current.len() + word.len() + 1 < max { 85 | current.push_str(&format!(" {}", word)); 86 | } else { 87 | buf.push_str(&format!("{}\n", current)); 88 | current.clear(); 89 | current.push_str(&format!("{}{}", offset, word)); 90 | } 91 | } 92 | 93 | buf.push_str(¤t); 94 | return buf; 95 | } 96 | } 97 | 98 | /// A very simple plain text table that knows the width of each of its columns. 99 | pub(crate) struct Table { 100 | rows: Vec, 101 | column_widths: Vec, 102 | } 103 | 104 | impl Table { 105 | /// Create a new empty Table without any rows 106 | pub fn new() -> Self { 107 | Table { 108 | rows: vec![], 109 | column_widths: vec![], 110 | } 111 | } 112 | 113 | /** 114 | * Create a new Table from a vec of vecs as a one time operation. 115 | * This is a convenience method that is intended to be used at the 116 | * end of an iterator chain that make use of 'add_row' to construct 117 | * the table from the input. 118 | */ 119 | pub fn from_rows(rows: Vec>) -> Self { 120 | let mut t = Table::new(); 121 | rows.iter().for_each(|r| t.add_row(r.clone())); 122 | return t; 123 | } 124 | 125 | /// Add a single row to an existing table and update column widths if needed. 126 | pub fn add_row(&mut self, cells: Vec) { 127 | let diff = cells.len() - self.column_widths.len(); 128 | if diff > 0 { 129 | self.column_widths.extend(vec![0; diff]) 130 | } 131 | 132 | cells.iter().enumerate().for_each(|(i, cell)| { 133 | self.column_widths[i] = cmp::max(cell.len(), self.column_widths[i]) 134 | }); 135 | 136 | self.rows.push(Row { cells }); 137 | } 138 | 139 | /// Convert this table to a left justified, column aligned string 140 | pub fn as_string(&self) -> String { 141 | self.rows 142 | .iter() 143 | .map(|r| r.two_column_wrapped(&self.column_widths)) 144 | .collect::>() 145 | .join("\n") 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /test_resources/foo/enum.elon.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sminez/roc/b075ef6ed01a825e79056349f0903e5599ab259d/test_resources/foo/enum.elon.html -------------------------------------------------------------------------------- /test_resources/foo/fn.foo.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sminez/roc/b075ef6ed01a825e79056349f0903e5599ab259d/test_resources/foo/fn.foo.html -------------------------------------------------------------------------------- /test_resources/foo/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sminez/roc/b075ef6ed01a825e79056349f0903e5599ab259d/test_resources/foo/index.html -------------------------------------------------------------------------------- /test_resources/foo/macro.makrow.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sminez/roc/b075ef6ed01a825e79056349f0903e5599ab259d/test_resources/foo/macro.makrow.html -------------------------------------------------------------------------------- /test_resources/foo/primative.ug.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sminez/roc/b075ef6ed01a825e79056349f0903e5599ab259d/test_resources/foo/primative.ug.html -------------------------------------------------------------------------------- /test_resources/foo/struct.structural.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sminez/roc/b075ef6ed01a825e79056349f0903e5599ab259d/test_resources/foo/struct.structural.html -------------------------------------------------------------------------------- /test_resources/foo/trait.fooable.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sminez/roc/b075ef6ed01a825e79056349f0903e5599ab259d/test_resources/foo/trait.fooable.html --------------------------------------------------------------------------------