toolkit for mdBook [mirror of my GitHub repo] docs.tonywu.dev/mdbookkit/
permalinks rust-analyzer mdbook
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: move into separate crates (WIP)

Tony Wu 71ff67e2 9f78ba9d

+3648 -3436
+1 -1
.vscode/settings.json
··· 7 7 "[markdown]": { 8 8 "editor.defaultFormatter": "esbenp.prettier-vscode" 9 9 }, 10 - "cSpell.words": ["cmark", "mdbookkit", "unmark"], 10 + "cSpell.words": ["cmark", "mdbookkit", "rstest", "unmark"], 11 11 "deno.enable": true, 12 12 "editor.formatOnSave": true, 13 13 "files.associations": {
+219 -126
Cargo.lock
··· 99 99 100 100 [[package]] 101 101 name = "anyhow" 102 - version = "1.0.97" 102 + version = "1.0.100" 103 103 source = "registry+https://github.com/rust-lang/crates.io-index" 104 - checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" 104 + checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" 105 105 106 106 [[package]] 107 107 name = "arbitrary" ··· 304 304 "android-tzdata", 305 305 "iana-time-zone", 306 306 "num-traits", 307 - "windows-link", 307 + "windows-link 0.1.0", 308 308 ] 309 309 310 310 [[package]] 311 311 name = "clap" 312 - version = "4.5.32" 312 + version = "4.5.53" 313 313 source = "registry+https://github.com/rust-lang/crates.io-index" 314 - checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83" 314 + checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" 315 315 dependencies = [ 316 316 "clap_builder", 317 317 "clap_derive", ··· 319 319 320 320 [[package]] 321 321 name = "clap_builder" 322 - version = "4.5.32" 322 + version = "4.5.53" 323 323 source = "registry+https://github.com/rust-lang/crates.io-index" 324 - checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8" 324 + checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" 325 325 dependencies = [ 326 326 "anstream", 327 327 "anstyle", ··· 332 332 333 333 [[package]] 334 334 name = "clap_complete" 335 - version = "4.5.46" 335 + version = "4.5.61" 336 336 source = "registry+https://github.com/rust-lang/crates.io-index" 337 - checksum = "f5c5508ea23c5366f77e53f5a0070e5a84e51687ec3ef9e0464c86dc8d13ce98" 337 + checksum = "39615915e2ece2550c0149addac32fb5bd312c657f43845bb9088cb9c8a7c992" 338 338 dependencies = [ 339 339 "clap", 340 340 ] 341 341 342 342 [[package]] 343 343 name = "clap_derive" 344 - version = "4.5.32" 344 + version = "4.5.49" 345 345 source = "registry+https://github.com/rust-lang/crates.io-index" 346 - checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 346 + checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" 347 347 dependencies = [ 348 348 "heck", 349 349 "proc-macro2", ··· 495 495 ] 496 496 497 497 [[package]] 498 - name = "dbus" 499 - version = "0.9.7" 500 - source = "registry+https://github.com/rust-lang/crates.io-index" 501 - checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" 502 - dependencies = [ 503 - "libc", 504 - "libdbus-sys", 505 - "winapi", 506 - ] 507 - 508 - [[package]] 509 498 name = "derive_arbitrary" 510 499 version = "1.4.1" 511 500 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 594 583 "libc", 595 584 "option-ext", 596 585 "redox_users", 597 - "windows-sys 0.59.0", 586 + "windows-sys 0.60.2", 598 587 ] 599 588 600 589 [[package]] ··· 819 808 checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 820 809 821 810 [[package]] 811 + name = "futures-timer" 812 + version = "3.0.3" 813 + source = "registry+https://github.com/rust-lang/crates.io-index" 814 + checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" 815 + 816 + [[package]] 822 817 name = "futures-util" 823 818 version = "0.3.31" 824 819 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1013 1008 ] 1014 1009 1015 1010 [[package]] 1011 + name = "hashbrown" 1012 + version = "0.16.1" 1013 + source = "registry+https://github.com/rust-lang/crates.io-index" 1014 + checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" 1015 + 1016 + [[package]] 1016 1017 name = "heck" 1017 1018 version = "0.5.0" 1018 1019 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1338 1339 1339 1340 [[package]] 1340 1341 name = "indexmap" 1341 - version = "2.8.0" 1342 + version = "2.12.1" 1342 1343 source = "registry+https://github.com/rust-lang/crates.io-index" 1343 - checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" 1344 + checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" 1344 1345 dependencies = [ 1345 1346 "equivalent", 1346 - "hashbrown", 1347 + "hashbrown 0.16.1", 1347 1348 ] 1348 1349 1349 1350 [[package]] ··· 1448 1449 checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" 1449 1450 1450 1451 [[package]] 1451 - name = "libdbus-sys" 1452 - version = "0.2.5" 1453 - source = "registry+https://github.com/rust-lang/crates.io-index" 1454 - checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" 1455 - dependencies = [ 1456 - "cc", 1457 - "pkg-config", 1458 - ] 1459 - 1460 - [[package]] 1461 1452 name = "libgit2-sys" 1462 1453 version = "0.18.1+1.9.0" 1463 1454 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1537 1528 "cfg-if", 1538 1529 "cssparser", 1539 1530 "encoding_rs", 1540 - "hashbrown", 1531 + "hashbrown 0.15.2", 1541 1532 "memchr", 1542 1533 "mime", 1543 1534 "selectors", ··· 1565 1556 1566 1557 [[package]] 1567 1558 name = "mdbook" 1568 - version = "0.4.48" 1559 + version = "0.4.52" 1569 1560 source = "registry+https://github.com/rust-lang/crates.io-index" 1570 - checksum = "8b6fbb4ac2d9fd7aa987c3510309ea3c80004a968d063c42f0d34fea070817c1" 1561 + checksum = "93c284d2855916af7c5919cf9ad897cfc77d3c2db6f55429c7cfb769182030ec" 1571 1562 dependencies = [ 1572 1563 "anyhow", 1573 1564 "chrono", ··· 1578 1569 "hex", 1579 1570 "log", 1580 1571 "memchr", 1581 - "once_cell", 1582 1572 "opener", 1583 1573 "pulldown-cmark 0.10.3", 1584 1574 "regex", ··· 1592 1582 ] 1593 1583 1594 1584 [[package]] 1595 - name = "mdbookkit" 1585 + name = "mdbook-link-forever" 1586 + version = "1.1.2" 1587 + dependencies = [ 1588 + "anyhow", 1589 + "assert_cmd", 1590 + "clap", 1591 + "console", 1592 + "git2", 1593 + "gix-url", 1594 + "insta", 1595 + "log", 1596 + "mdbook", 1597 + "mdbookkit", 1598 + "miette", 1599 + "percent-encoding", 1600 + "predicates", 1601 + "pulldown-cmark 0.13.0", 1602 + "rstest", 1603 + "serde", 1604 + "tap", 1605 + "tempfile", 1606 + "thiserror 2.0.12", 1607 + "url", 1608 + ] 1609 + 1610 + [[package]] 1611 + name = "mdbook-rustdoc-link" 1596 1612 version = "1.1.2" 1597 1613 dependencies = [ 1598 1614 "anyhow", 1599 1615 "assert_cmd", 1600 1616 "async-lsp", 1601 - "cargo-run-bin", 1602 1617 "cargo_toml", 1603 - "chrono", 1604 1618 "clap", 1605 1619 "console", 1606 1620 "dirs", 1607 - "env_logger", 1608 - "git2", 1609 - "gix-url", 1610 - "indicatif", 1611 1621 "insta", 1612 1622 "log", 1613 1623 "lsp-types", 1614 1624 "mdbook", 1625 + "mdbookkit", 1615 1626 "miette", 1616 - "owo-colors", 1617 - "percent-encoding", 1618 1627 "predicates", 1619 1628 "proc-macro2", 1620 1629 "pulldown-cmark 0.13.0", 1621 - "pulldown-cmark-to-cmark", 1630 + "rstest", 1622 1631 "serde", 1623 1632 "serde_json", 1624 1633 "sha2", ··· 1627 1636 "syn 2.0.100", 1628 1637 "tap", 1629 1638 "tempfile", 1630 - "thiserror 2.0.12", 1631 1639 "tokio", 1632 1640 "tokio-util", 1633 - "toml 0.5.11", 1634 1641 "tower", 1642 + ] 1643 + 1644 + [[package]] 1645 + name = "mdbookkit" 1646 + version = "1.1.2" 1647 + dependencies = [ 1648 + "anyhow", 1649 + "cargo-run-bin", 1650 + "chrono", 1651 + "clap", 1652 + "console", 1653 + "env_logger", 1654 + "indicatif", 1655 + "insta", 1656 + "log", 1657 + "mdbook", 1658 + "miette", 1659 + "owo-colors", 1660 + "pulldown-cmark 0.13.0", 1661 + "pulldown-cmark-to-cmark", 1662 + "serde", 1663 + "serde_json", 1664 + "tap", 1665 + "toml 0.5.11", 1635 1666 "url", 1636 - "util-testing", 1637 1667 ] 1638 1668 1639 1669 [[package]] ··· 1655 1685 1656 1686 [[package]] 1657 1687 name = "memchr" 1658 - version = "2.7.4" 1688 + version = "2.7.6" 1659 1689 source = "registry+https://github.com/rust-lang/crates.io-index" 1660 - checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 1690 + checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 1661 1691 1662 1692 [[package]] 1663 1693 name = "miette" ··· 1797 1827 1798 1828 [[package]] 1799 1829 name = "opener" 1800 - version = "0.7.2" 1830 + version = "0.8.3" 1801 1831 source = "registry+https://github.com/rust-lang/crates.io-index" 1802 - checksum = "d0812e5e4df08da354c851a3376fead46db31c2214f849d3de356d774d057681" 1832 + checksum = "cb9024962ab91e00c89d2a14352a8d0fc1a64346bf96f1839b45c09149564e47" 1803 1833 dependencies = [ 1804 1834 "bstr", 1805 - "dbus", 1806 1835 "normpath", 1807 - "windows-sys 0.59.0", 1836 + "windows-sys 0.60.2", 1808 1837 ] 1809 1838 1810 1839 [[package]] ··· 2108 2137 ] 2109 2138 2110 2139 [[package]] 2140 + name = "proc-macro-crate" 2141 + version = "3.4.0" 2142 + source = "registry+https://github.com/rust-lang/crates.io-index" 2143 + checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" 2144 + dependencies = [ 2145 + "toml_edit 0.23.7", 2146 + ] 2147 + 2148 + [[package]] 2111 2149 name = "proc-macro-hack" 2112 2150 version = "0.5.20+deprecated" 2113 2151 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2161 2199 2162 2200 [[package]] 2163 2201 name = "pulldown-cmark-to-cmark" 2164 - version = "21.0.0" 2202 + version = "21.1.0" 2165 2203 source = "registry+https://github.com/rust-lang/crates.io-index" 2166 - checksum = "e5b6a0769a491a08b31ea5c62494a8f144ee0987d86d670a8af4df1e1b7cde75" 2204 + checksum = "8246feae3db61428fd0bb94285c690b460e4517d83152377543ca802357785f1" 2167 2205 dependencies = [ 2168 2206 "pulldown-cmark 0.13.0", 2169 2207 ] ··· 2277 2315 2278 2316 [[package]] 2279 2317 name = "regex" 2280 - version = "1.11.1" 2318 + version = "1.12.2" 2281 2319 source = "registry+https://github.com/rust-lang/crates.io-index" 2282 - checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 2320 + checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" 2283 2321 dependencies = [ 2284 2322 "aho-corasick", 2285 2323 "memchr", ··· 2289 2327 2290 2328 [[package]] 2291 2329 name = "regex-automata" 2292 - version = "0.4.9" 2330 + version = "0.4.13" 2293 2331 source = "registry+https://github.com/rust-lang/crates.io-index" 2294 - checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 2332 + checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" 2295 2333 dependencies = [ 2296 2334 "aho-corasick", 2297 2335 "memchr", ··· 2305 2343 checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 2306 2344 2307 2345 [[package]] 2346 + name = "relative-path" 2347 + version = "1.9.3" 2348 + source = "registry+https://github.com/rust-lang/crates.io-index" 2349 + checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" 2350 + 2351 + [[package]] 2308 2352 name = "reqwest" 2309 2353 version = "0.12.15" 2310 2354 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2361 2405 "libc", 2362 2406 "untrusted", 2363 2407 "windows-sys 0.52.0", 2408 + ] 2409 + 2410 + [[package]] 2411 + name = "rstest" 2412 + version = "0.26.1" 2413 + source = "registry+https://github.com/rust-lang/crates.io-index" 2414 + checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" 2415 + dependencies = [ 2416 + "futures-timer", 2417 + "futures-util", 2418 + "rstest_macros", 2419 + ] 2420 + 2421 + [[package]] 2422 + name = "rstest_macros" 2423 + version = "0.26.1" 2424 + source = "registry+https://github.com/rust-lang/crates.io-index" 2425 + checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" 2426 + dependencies = [ 2427 + "cfg-if", 2428 + "glob", 2429 + "proc-macro-crate", 2430 + "proc-macro2", 2431 + "quote", 2432 + "regex", 2433 + "relative-path", 2434 + "rustc_version", 2435 + "syn 2.0.100", 2436 + "unicode-ident", 2364 2437 ] 2365 2438 2366 2439 [[package]] ··· 2513 2586 2514 2587 [[package]] 2515 2588 name = "serde" 2516 - version = "1.0.219" 2589 + version = "1.0.228" 2517 2590 source = "registry+https://github.com/rust-lang/crates.io-index" 2518 - checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 2591 + checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 2592 + dependencies = [ 2593 + "serde_core", 2594 + "serde_derive", 2595 + ] 2596 + 2597 + [[package]] 2598 + name = "serde_core" 2599 + version = "1.0.228" 2600 + source = "registry+https://github.com/rust-lang/crates.io-index" 2601 + checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 2519 2602 dependencies = [ 2520 2603 "serde_derive", 2521 2604 ] 2522 2605 2523 2606 [[package]] 2524 2607 name = "serde_derive" 2525 - version = "1.0.219" 2608 + version = "1.0.228" 2526 2609 source = "registry+https://github.com/rust-lang/crates.io-index" 2527 - checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 2610 + checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 2528 2611 dependencies = [ 2529 2612 "proc-macro2", 2530 2613 "quote", ··· 2533 2616 2534 2617 [[package]] 2535 2618 name = "serde_json" 2536 - version = "1.0.140" 2619 + version = "1.0.145" 2537 2620 source = "registry+https://github.com/rust-lang/crates.io-index" 2538 - checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 2621 + checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" 2539 2622 dependencies = [ 2540 2623 "itoa", 2541 2624 "memchr", 2542 2625 "ryu", 2543 2626 "serde", 2627 + "serde_core", 2544 2628 ] 2545 2629 2546 2630 [[package]] ··· 2587 2671 2588 2672 [[package]] 2589 2673 name = "sha2" 2590 - version = "0.10.8" 2674 + version = "0.10.9" 2591 2675 source = "registry+https://github.com/rust-lang/crates.io-index" 2592 - checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 2676 + checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 2593 2677 dependencies = [ 2594 2678 "cfg-if", 2595 2679 "cpufeatures", ··· 2604 2688 2605 2689 [[package]] 2606 2690 name = "signal-hook-registry" 2607 - version = "1.4.2" 2691 + version = "1.4.7" 2608 2692 source = "registry+https://github.com/rust-lang/crates.io-index" 2609 - checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 2693 + checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" 2610 2694 dependencies = [ 2611 2695 "libc", 2612 2696 ] ··· 2764 2848 2765 2849 [[package]] 2766 2850 name = "tempfile" 2767 - version = "3.19.0" 2851 + version = "3.23.0" 2768 2852 source = "registry+https://github.com/rust-lang/crates.io-index" 2769 - checksum = "488960f40a3fd53d72c2a29a58722561dee8afdd175bd88e3db4677d7b2ba600" 2853 + checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" 2770 2854 dependencies = [ 2771 2855 "fastrand", 2772 2856 "getrandom 0.3.1", 2773 2857 "once_cell", 2774 2858 "rustix 1.0.2", 2775 - "windows-sys 0.59.0", 2859 + "windows-sys 0.60.2", 2776 2860 ] 2777 2861 2778 2862 [[package]] ··· 2930 3014 dependencies = [ 2931 3015 "serde", 2932 3016 "serde_spanned", 2933 - "toml_datetime", 3017 + "toml_datetime 0.6.8", 2934 3018 "toml_edit 0.22.24", 2935 3019 ] 2936 3020 ··· 2944 3028 ] 2945 3029 2946 3030 [[package]] 3031 + name = "toml_datetime" 3032 + version = "0.7.3" 3033 + source = "registry+https://github.com/rust-lang/crates.io-index" 3034 + checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" 3035 + dependencies = [ 3036 + "serde_core", 3037 + ] 3038 + 3039 + [[package]] 2947 3040 name = "toml_edit" 2948 3041 version = "0.19.15" 2949 3042 source = "registry+https://github.com/rust-lang/crates.io-index" 2950 3043 checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" 2951 3044 dependencies = [ 2952 3045 "indexmap", 2953 - "toml_datetime", 3046 + "toml_datetime 0.6.8", 2954 3047 "winnow 0.5.40", 2955 3048 ] 2956 3049 ··· 2963 3056 "indexmap", 2964 3057 "serde", 2965 3058 "serde_spanned", 2966 - "toml_datetime", 2967 - "winnow 0.7.4", 3059 + "toml_datetime 0.6.8", 3060 + "winnow 0.7.14", 3061 + ] 3062 + 3063 + [[package]] 3064 + name = "toml_edit" 3065 + version = "0.23.7" 3066 + source = "registry+https://github.com/rust-lang/crates.io-index" 3067 + checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" 3068 + dependencies = [ 3069 + "indexmap", 3070 + "toml_datetime 0.7.3", 3071 + "toml_parser", 3072 + "winnow 0.7.14", 3073 + ] 3074 + 3075 + [[package]] 3076 + name = "toml_parser" 3077 + version = "1.0.4" 3078 + source = "registry+https://github.com/rust-lang/crates.io-index" 3079 + checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" 3080 + dependencies = [ 3081 + "winnow 0.7.14", 2968 3082 ] 2969 3083 2970 3084 [[package]] ··· 3164 3278 ] 3165 3279 3166 3280 [[package]] 3167 - name = "util-testing" 3168 - version = "0.1.0" 3169 - dependencies = [ 3170 - "anyhow", 3171 - "cargo-run-bin", 3172 - "insta", 3173 - "log", 3174 - "once_cell", 3175 - "serde", 3176 - "serde_json", 3177 - "tap", 3178 - "url", 3179 - ] 3180 - 3181 - [[package]] 3182 3281 name = "vcpkg" 3183 3282 version = "0.2.15" 3184 3283 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3343 3442 ] 3344 3443 3345 3444 [[package]] 3346 - name = "winapi" 3347 - version = "0.3.9" 3348 - source = "registry+https://github.com/rust-lang/crates.io-index" 3349 - checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 3350 - dependencies = [ 3351 - "winapi-i686-pc-windows-gnu", 3352 - "winapi-x86_64-pc-windows-gnu", 3353 - ] 3354 - 3355 - [[package]] 3356 - name = "winapi-i686-pc-windows-gnu" 3357 - version = "0.4.0" 3358 - source = "registry+https://github.com/rust-lang/crates.io-index" 3359 - checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 3360 - 3361 - [[package]] 3362 - name = "winapi-x86_64-pc-windows-gnu" 3363 - version = "0.4.0" 3364 - source = "registry+https://github.com/rust-lang/crates.io-index" 3365 - checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 3366 - 3367 - [[package]] 3368 3445 name = "windows-core" 3369 3446 version = "0.52.0" 3370 3447 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3380 3457 checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" 3381 3458 3382 3459 [[package]] 3460 + name = "windows-link" 3461 + version = "0.2.1" 3462 + source = "registry+https://github.com/rust-lang/crates.io-index" 3463 + checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 3464 + 3465 + [[package]] 3383 3466 name = "windows-registry" 3384 3467 version = "0.4.0" 3385 3468 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3387 3470 dependencies = [ 3388 3471 "windows-result", 3389 3472 "windows-strings", 3390 - "windows-targets 0.53.0", 3473 + "windows-targets 0.53.5", 3391 3474 ] 3392 3475 3393 3476 [[package]] ··· 3396 3479 source = "registry+https://github.com/rust-lang/crates.io-index" 3397 3480 checksum = "06374efe858fab7e4f881500e6e86ec8bc28f9462c47e5a9941a0142ad86b189" 3398 3481 dependencies = [ 3399 - "windows-link", 3482 + "windows-link 0.1.0", 3400 3483 ] 3401 3484 3402 3485 [[package]] ··· 3405 3488 source = "registry+https://github.com/rust-lang/crates.io-index" 3406 3489 checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" 3407 3490 dependencies = [ 3408 - "windows-link", 3491 + "windows-link 0.1.0", 3409 3492 ] 3410 3493 3411 3494 [[package]] ··· 3427 3510 ] 3428 3511 3429 3512 [[package]] 3513 + name = "windows-sys" 3514 + version = "0.60.2" 3515 + source = "registry+https://github.com/rust-lang/crates.io-index" 3516 + checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 3517 + dependencies = [ 3518 + "windows-targets 0.53.5", 3519 + ] 3520 + 3521 + [[package]] 3430 3522 name = "windows-targets" 3431 3523 version = "0.52.6" 3432 3524 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3444 3536 3445 3537 [[package]] 3446 3538 name = "windows-targets" 3447 - version = "0.53.0" 3539 + version = "0.53.5" 3448 3540 source = "registry+https://github.com/rust-lang/crates.io-index" 3449 - checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" 3541 + checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" 3450 3542 dependencies = [ 3543 + "windows-link 0.2.1", 3451 3544 "windows_aarch64_gnullvm 0.53.0", 3452 3545 "windows_aarch64_msvc 0.53.0", 3453 3546 "windows_i686_gnu 0.53.0", ··· 3565 3658 3566 3659 [[package]] 3567 3660 name = "winnow" 3568 - version = "0.7.4" 3661 + version = "0.7.14" 3569 3662 source = "registry+https://github.com/rust-lang/crates.io-index" 3570 - checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" 3663 + checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" 3571 3664 dependencies = [ 3572 3665 "memchr", 3573 3666 ]
+6 -4
Cargo.toml
··· 5 5 license = "MIT OR Apache-2.0" 6 6 repository = "https://github.com/tonywu6/mdbookkit" 7 7 8 - edition = "2021" 9 - rust-version = "1.81.0" 8 + edition = "2024" 9 + rust-version = "1.85.0" 10 10 11 11 [profile.dev.package] 12 12 insta.opt-level = 3 ··· 22 22 assert_cmd = "2.0.16" 23 23 cargo-run-bin = { version = "1.7.4", default-features = false } 24 24 clap = { version = "4.5.31", features = ["derive"] } 25 + console = { version = "0.15.11" } 25 26 env_logger = "0.11.6" 26 27 insta = { version = "1.40.0", features = ["yaml", "filters"] } 27 28 log = "0.4.26" 28 29 mdbook = { version = "0.4.48", default-features = false } 30 + mdbookkit = { path = "crates/mdbookkit" } 29 31 miette = { version = "7.5.0", features = [ 30 32 "fancy-no-backtrace", 31 33 ], default-features = false } ··· 33 35 predicates = "3.1.3" 34 36 pulldown-cmark = "0.13.0" 35 37 pulldown-cmark-to-cmark = "21.0.0" 38 + rstest = "0.26.1" 36 39 serde = { version = "1", features = ["derive"] } 37 40 serde_json = "1.0.139" 38 41 shlex = "1.3.0" ··· 40 43 tap = "1.0.1" 41 44 tempfile = "3.18.0" 42 45 thiserror = "2.0.12" 43 - tokio = { version = "1", features = ["macros"] } 46 + tokio = { version = "1", default-features = false } 44 47 toml = "0.5" 45 48 url = "2.5.4" 46 - util-testing = { path = "utils/testing" } 47 49 48 50 [workspace.metadata.bin] 49 51 mdbook = { version = "0.4.48" }
+57
crates/mdbook-rustdoc-link/Cargo.toml
··· 1 + [package] 2 + name = "mdbook-rustdoc-link" 3 + publish = true 4 + version = "1.1.2" 5 + 6 + edition.workspace = true 7 + rust-version.workspace = true 8 + 9 + authors.workspace = true 10 + license.workspace = true 11 + repository.workspace = true 12 + 13 + # categories = ["command-line-utilities"] 14 + # description = "toolkit for mdBook 📖" 15 + # documentation = "https://tonywu6.github.io/mdbookkit/" 16 + # keywords = ["mdbook", "documentation", "rust-analyzer", "lsp", "permalink"] 17 + # readme = "README.md" 18 + 19 + [dependencies] 20 + anyhow = { workspace = true } 21 + async-lsp = { version = "0.2.2" } 22 + cargo_toml = { version = "0.21.0" } 23 + clap = { workspace = true } 24 + console = { workspace = true } 25 + dirs = { version = "6.0.0" } 26 + log = { workspace = true } 27 + lsp-types = { version = "0.95.0" } 28 + mdbook = { workspace = true } 29 + mdbookkit = { workspace = true } 30 + miette = { workspace = true } 31 + proc-macro2 = { version = "1.0.94", features = ["span-locations"] } 32 + pulldown-cmark = { workspace = true } 33 + serde = { workspace = true, features = ["rc"] } 34 + serde_json = { workspace = true } 35 + sha2 = { version = "0.10.8" } 36 + shlex = { workspace = true } 37 + syn = { version = "2.0.99" } 38 + tap = { workspace = true } 39 + tempfile = { workspace = true } 40 + tokio = { workspace = true, features = [ 41 + "fs", 42 + "macros", 43 + "process", 44 + "rt-multi-thread", 45 + "time", 46 + ] } 47 + tokio-util = { version = "0.7.13", features = ["compat"] } 48 + tower = { version = "0.5.2" } 49 + 50 + [dev-dependencies] 51 + assert_cmd = { workspace = true } 52 + insta = { workspace = true } 53 + mdbookkit = { workspace = true, features = ["_testing"] } 54 + predicates = { workspace = true } 55 + rstest = { workspace = true } 56 + similar = { workspace = true } 57 + tempfile = { workspace = true }
+363
crates/mdbook-rustdoc-link/src/main.rs
··· 1 + use std::{ 2 + borrow::Borrow, 3 + collections::HashMap, 4 + hash::Hash, 5 + io::{Read, Write}, 6 + sync::Arc, 7 + }; 8 + 9 + use anyhow::{Context, Result}; 10 + use clap::{Parser, Subcommand}; 11 + use console::colors_enabled_stderr; 12 + use log::LevelFilter; 13 + use lsp_types::Position; 14 + use mdbook::preprocess::PreprocessorContext; 15 + use tap::{Pipe, TapFallible}; 16 + use tokio::task::JoinSet; 17 + 18 + use mdbookkit::{ 19 + book::{ 20 + book_from_stdin, book_into_stdout, config_from_book, for_each_chapter_mut, iter_chapters, 21 + smart_punctuation, 22 + }, 23 + diagnostics::Issue, 24 + log_debug, log_warning, 25 + logging::{ConsoleLogger, is_logging, spinner}, 26 + styled, 27 + }; 28 + 29 + use self::{ 30 + cache::{Cache, FileCache}, 31 + client::Client, 32 + env::{Config, Environment, RustAnalyzer}, 33 + item::Item, 34 + link::ItemLinks, 35 + page::Pages, 36 + url::UrlToPath, 37 + }; 38 + 39 + mod cache; 40 + mod client; 41 + mod env; 42 + mod item; 43 + mod link; 44 + mod markdown; 45 + mod page; 46 + mod sync; 47 + #[cfg(test)] 48 + mod tests; 49 + mod url; 50 + 51 + /// Type that can provide links. 52 + /// 53 + /// Resolvers should modify the provided [`Pages`] in place. 54 + /// 55 + /// This is currently an abstraction over two sources of links: 56 + /// 57 + /// - [`Client`], which invokes rust-analyzer 58 + /// - [`Cache`] implementations 59 + /// 60 + /// [`Cache`]: crate::bin::rustdoc_link::cache::Cache 61 + trait Resolver { 62 + async fn resolve<K>(&self, pages: &mut Pages<'_, K>) -> Result<()> 63 + where 64 + K: Eq + Hash; 65 + } 66 + 67 + impl Resolver for Client { 68 + async fn resolve<K>(&self, pages: &mut Pages<'_, K>) -> Result<()> 69 + where 70 + K: Eq + Hash, 71 + { 72 + let request = pages.items(); 73 + 74 + if request.is_empty() { 75 + return Ok(()); 76 + } 77 + 78 + let main = std::fs::read_to_string(self.env().entrypoint.to_path()?)?; 79 + 80 + let (context, request) = { 81 + let mut context = format!("{main}\nfn {UNIQUE_ID} () {{\n"); 82 + 83 + let line = context.chars().filter(|&c| c == '\n').count(); 84 + 85 + let request = request 86 + .iter() 87 + .scan(line, |line, (key, item)| { 88 + build(&mut context, line, item).map(|cursors| (key.clone(), cursors)) 89 + }) 90 + .collect::<Vec<_>>(); 91 + 92 + fn build(context: &mut String, line: &mut usize, item: &Item) -> Option<Vec<Position>> { 93 + use std::fmt::Write; 94 + let _ = writeln!(context, "{}", item.stmt); 95 + let cursors = item 96 + .cursor 97 + .as_ref() 98 + .iter() 99 + .map(|&col| Position::new(*line as _, col as _)) 100 + .collect::<Vec<_>>(); 101 + *line += 1; 102 + Some(cursors) 103 + } 104 + 105 + context.push('}'); 106 + 107 + (context, request) 108 + }; 109 + 110 + log::debug!("request context\n\n{context}\n"); 111 + 112 + let document = self 113 + .open(self.env().entrypoint.clone(), context) 114 + .await? 115 + .pipe(Arc::new); 116 + 117 + spinner().create("resolve", Some(request.len() as _)); 118 + 119 + let tasks: JoinSet<Option<(String, ItemLinks)>> = request 120 + .into_iter() 121 + .map(|(key, pos)| { 122 + let key = key.to_string(); 123 + let doc = document.clone(); 124 + resolve(doc, key, pos) 125 + }) 126 + .collect(); 127 + 128 + async fn resolve( 129 + doc: Arc<client::OpenDocument>, 130 + key: String, 131 + pos: Vec<Position>, 132 + ) -> Option<(String, ItemLinks)> { 133 + let _task = spinner().task("resolve", &key); 134 + for p in pos { 135 + let resolved = doc 136 + .resolve(p) 137 + .await 138 + .with_context(|| format!("{p:?}")) 139 + .context("failed to resolve symbol:") 140 + .tap_err(log_debug!()) 141 + .ok(); 142 + if let Some(resolved) = resolved { 143 + return Some((key, resolved)); 144 + } 145 + } 146 + None 147 + } 148 + 149 + let resolved = tasks 150 + .join_all() 151 + .await 152 + .into_iter() 153 + .flatten() 154 + .collect::<HashMap<_, _>>(); 155 + 156 + spinner().finish("resolve", styled!(("done").green())); 157 + 158 + pages.apply(&resolved); 159 + 160 + Ok(()) 161 + } 162 + } 163 + 164 + impl<K> Resolver for HashMap<K, ItemLinks> 165 + where 166 + K: Borrow<str> + Eq + Hash, 167 + { 168 + async fn resolve<P>(&self, pages: &mut Pages<'_, P>) -> Result<()> 169 + where 170 + P: Eq + Hash, 171 + { 172 + pages.apply(self); 173 + Ok(()) 174 + } 175 + } 176 + 177 + #[tokio::main] 178 + async fn main() -> Result<()> { 179 + ConsoleLogger::install(env!("CARGO_PKG_NAME")); 180 + match Program::parse().command { 181 + Some(Command::Supports { .. }) => Ok(()), 182 + Some(Command::Markdown(options)) => markdown(options).await, 183 + Some(Command::RustAnalyzer) => which(), 184 + None => mdbook().await, 185 + } 186 + } 187 + 188 + #[derive(Parser, Debug, Clone)] 189 + struct Program { 190 + #[command(subcommand)] 191 + command: Option<Command>, 192 + } 193 + 194 + #[derive(Subcommand, Debug, Clone)] 195 + enum Command { 196 + /// Convert rustdoc-style Markdown links found in stdin. 197 + Markdown(Config), 198 + 199 + /// Show which `rust-analyzer` is being used. 200 + RustAnalyzer, 201 + 202 + /// Support command for mdbook. 203 + /// 204 + /// See <https://rust-lang.github.io/mdBook/for_developers/preprocessors.html#hooking-into-mdbook> 205 + #[clap(hide = true)] 206 + Supports { renderer: String }, 207 + } 208 + 209 + async fn mdbook() -> Result<()> { 210 + let (context, mut book) = book_from_stdin().context("failed to parse book content")?; 211 + 212 + let config = config(&context).context("failed to read preprocessor config from book.toml")?; 213 + 214 + let client = Environment::new(config) 215 + .context("failed to initialize `mdbook-rustdoc-link`")? 216 + .pipe(Client::new); 217 + 218 + let cached = FileCache::load(client.env()).await.ok(); 219 + 220 + let mut content = Pages::default(); 221 + 222 + for (path, ch) in iter_chapters(&book) { 223 + let stream = client.env().markdown(&ch.content).into_offset_iter(); 224 + content 225 + .read(path.clone(), &ch.content, stream) 226 + .with_context(|| path.display().to_string()) 227 + .context("failed to parse Markdown source:")?; 228 + } 229 + 230 + if let Some(cached) = cached { 231 + cached.resolve(&mut content).await.ok(); 232 + } 233 + 234 + client 235 + .resolve(&mut content) 236 + .await 237 + .context("failed to resolve some links")?; 238 + 239 + let mut result = iter_chapters(&book) 240 + .filter_map(|(path, _)| { 241 + let output = content 242 + .emit(path, &client.env().emit_config()) 243 + .tap_err(log_warning!()) 244 + .ok()?; 245 + Some((path.clone(), output.to_string())) 246 + }) 247 + .collect::<HashMap<_, _>>(); 248 + 249 + let env = client.stop().await; 250 + 251 + let status = content 252 + .reporter() 253 + .names(|path| path.display().to_string()) 254 + .level(LevelFilter::Warn) 255 + .logging(is_logging()) 256 + .colored(colors_enabled_stderr()) 257 + .build() 258 + .to_stderr() 259 + .to_status(); 260 + 261 + if content.modified() { 262 + FileCache::save(&env, &content).await.ok(); 263 + } 264 + 265 + for_each_chapter_mut(&mut book, |path, ch| { 266 + if let Some(output) = result.remove(&path) { 267 + ch.content = output 268 + } 269 + }); 270 + 271 + book_into_stdout(&book)?; 272 + 273 + env.config.fail_on_warnings.check(status.level())?; 274 + 275 + Ok(()) 276 + } 277 + 278 + async fn markdown(config: Config) -> Result<()> { 279 + let client = Environment::new(config) 280 + .context("failed to initialize")? 281 + .pipe(Client::new); 282 + 283 + let source = string_from_stdin().context("failed to read Markdown source from stdin")?; 284 + 285 + let stream = client.env().markdown(&source).into_offset_iter(); 286 + 287 + let mut content = Pages::one(&source, stream).context("failed to parse Markdown source")?; 288 + 289 + if let Ok(cached) = FileCache::load(client.env()).await { 290 + cached.resolve(&mut content).await.ok(); 291 + } 292 + 293 + client 294 + .resolve(&mut content) 295 + .await 296 + .context("failed to resolve some links")?; 297 + 298 + let env = client.stop().await; 299 + 300 + let status = content 301 + .reporter() 302 + .names(|_| "<stdin>".into()) 303 + .level(LevelFilter::Warn) 304 + .logging(is_logging()) 305 + .colored(colors_enabled_stderr()) 306 + .build() 307 + .to_stderr() 308 + .to_status(); 309 + 310 + if content.modified() { 311 + FileCache::save(&env, &content).await.ok(); 312 + } 313 + 314 + content 315 + .get(&env.emit_config()) 316 + .map(|emit| emit.to_string()) 317 + .and_then(|output| Ok(std::io::stdout().write_all(output.as_bytes())?))?; 318 + 319 + env.config.fail_on_warnings.check(status.level())?; 320 + 321 + Ok(()) 322 + } 323 + 324 + fn which() -> Result<()> { 325 + let env = Environment::new(Default::default())?; 326 + 327 + match env.which() { 328 + RustAnalyzer::Custom(cmd) => println!("using a custom command for rust-analyzer: {cmd:?}"), 329 + RustAnalyzer::VsCode(cmd) => println!( 330 + "using rust-analyzer from VS Code extension: {}", 331 + cmd.display() 332 + ), 333 + RustAnalyzer::Path => println!("using rust-analyzer on PATH (run `which rust-analyzer`)"), 334 + } 335 + 336 + Ok(()) 337 + } 338 + 339 + fn config(context: &PreprocessorContext) -> Result<Config> { 340 + let mut config = config_from_book::<Config>(&context.config, "rustdoc-link")?; 341 + 342 + if let Some(path) = config.manifest_dir { 343 + config.manifest_dir = Some(context.root.join(path)) 344 + } else { 345 + config.manifest_dir = Some(context.root.clone()) 346 + } 347 + 348 + if let Some(path) = config.cache_dir { 349 + config.cache_dir = Some(context.root.join(path)) 350 + } 351 + 352 + config.smart_punctuation = smart_punctuation(&context.config); 353 + 354 + Ok(config) 355 + } 356 + 357 + fn string_from_stdin() -> Result<String> { 358 + Vec::new() 359 + .pipe(|mut buf| std::io::stdin().read_to_end(&mut buf).and(Ok(buf)))? 360 + .pipe(|buf| Ok(String::from_utf8(buf)?)) 361 + } 362 + 363 + const UNIQUE_ID: &str = "__ded48f4d_0c4f_4950_b17d_55fd3b2a0c86__";
+136
crates/mdbook-rustdoc-link/src/tests.rs
··· 1 + use anyhow::{Context, Result, bail}; 2 + use lsp_types::Url; 3 + use rstest::*; 4 + use similar::{ChangeTag, TextDiff}; 5 + use tap::Pipe; 6 + 7 + use mdbookkit::{portable_snapshots, test_document, testing::TestDocument}; 8 + 9 + use crate::{ 10 + Resolver, 11 + client::Client, 12 + env::{Config, Environment}, 13 + page::Pages, 14 + }; 15 + 16 + struct TestOutput { 17 + pages: Pages<'static, Url>, 18 + env: Environment, 19 + } 20 + 21 + #[fixture] 22 + #[once] 23 + fn test_output() -> TestOutput { 24 + let client = Config { 25 + rust_analyzer: Some("cargo run --package util-rust-analyzer -- analyzer".into()), 26 + ..Default::default() 27 + } 28 + .pipe(Environment::new) 29 + .context("failed to initialize environment") 30 + .unwrap() 31 + .pipe(Client::new); 32 + 33 + let mut pages = Pages::default(); 34 + 35 + for doc in TEST_DOCUMENTS { 36 + let stream = client.env().markdown(doc.content).into_offset_iter(); 37 + pages 38 + .read(doc.url(), doc.content, stream) 39 + .context("failed to parse source") 40 + .unwrap(); 41 + } 42 + 43 + tokio::runtime::Builder::new_multi_thread() 44 + .enable_all() 45 + .build() 46 + .unwrap() 47 + .block_on(client.resolve(&mut pages)) 48 + .context("failed to resolve links") 49 + .unwrap(); 50 + 51 + let env = client.env().clone(); 52 + 53 + TestOutput { env, pages } 54 + } 55 + 56 + fn assert_output(doc: TestDocument, TestOutput { pages, env }: &TestOutput) -> Result<()> { 57 + let output = pages.emit(&doc.url(), &env.emit_config())?; 58 + portable_snapshots!().test(|| insta::assert_snapshot!(doc.name(), output))?; 59 + Ok(()) 60 + } 61 + 62 + fn assert_report(doc: TestDocument, TestOutput { pages, .. }: &TestOutput) -> Result<()> { 63 + let report = pages 64 + .reporter() 65 + .level(log::LevelFilter::Info) 66 + .named(|u| u == &doc.url()) 67 + .names(|_| doc.name()) 68 + .colored(false) 69 + .logging(false) 70 + .build() 71 + .to_report(); 72 + portable_snapshots!() 73 + .test(|| insta::assert_snapshot!(format!("{}.stderr", doc.name()), report))?; 74 + Ok(()) 75 + } 76 + 77 + fn assert_whitespace_unchanged( 78 + doc: TestDocument, 79 + TestOutput { pages, env }: &TestOutput, 80 + ) -> Result<()> { 81 + let output = pages.emit(&doc.url(), &env.emit_config())?; 82 + 83 + let changed_lines = TextDiff::from_words(doc.content, &output) 84 + .iter_all_changes() 85 + .filter_map(|change| { 86 + if matches!(change.tag(), ChangeTag::Equal) { 87 + return None; 88 + } 89 + if change.value().contains('\n') { 90 + Some(change.value()) 91 + } else { 92 + None 93 + } 94 + }) 95 + .collect::<Vec<_>>(); 96 + 97 + if !changed_lines.is_empty() { 98 + bail!("unexpected whitespace change: {changed_lines:?}") 99 + } else { 100 + Ok(()) 101 + } 102 + } 103 + 104 + macro_rules! test_documents { 105 + ( $($path:literal,)+ ) => { 106 + static TEST_DOCUMENTS: &[TestDocument] = &[ 107 + $(test_document!($path),)* 108 + ]; 109 + 110 + #[rstest] 111 + $(#[case(test_document!($path))])* 112 + fn assert_outputs(#[case] doc: TestDocument, test_output: &TestOutput) -> Result<()> { 113 + assert_output(doc, test_output) 114 + } 115 + 116 + #[rstest] 117 + $(#[case(test_document!($path))])* 118 + fn assert_reports(#[case] doc: TestDocument, test_output: &TestOutput) -> Result<()> { 119 + assert_report(doc, test_output) 120 + } 121 + 122 + #[rstest] 123 + $(#[case(test_document!($path))])* 124 + fn check_whitespace(#[case] doc: TestDocument, test_output: &TestOutput) -> Result<()> { 125 + assert_whitespace_unchanged(doc, test_output) 126 + } 127 + }; 128 + } 129 + 130 + test_documents![ 131 + "../../../docs/src/rustdoc-link/index.md", 132 + "../../../docs/src/rustdoc-link/getting-started.md", 133 + "../../../docs/src/rustdoc-link/supported-syntax.md", 134 + "../../../docs/src/rustdoc-link/known-issues.md", 135 + "tests/ra-known-quirks.md", 136 + ];
+172
crates/mdbook-rustdoc-link/tests/env.rs
··· 1 + use std::io::Write; 2 + 3 + use anyhow::{Context, Result}; 4 + use assert_cmd::{Command, assert::OutputAssertExt}; 5 + use predicates::prelude::*; 6 + use tap::Pipe; 7 + use tempfile::TempDir; 8 + 9 + use mdbookkit::testing::{not_in_ci, setup_logging, setup_paths}; 10 + 11 + #[test] 12 + #[ignore = "should run in a dedicated environment"] 13 + fn test_minimum_env() -> Result<()> { 14 + setup_logging(env!("CARGO_PKG_NAME")); 15 + 16 + log::info!("setup: compile self"); 17 + Command::new("cargo") 18 + .args([ 19 + "build", 20 + "--package", 21 + env!("CARGO_PKG_NAME"), 22 + "--all-features", 23 + "--bin", 24 + "mdbook-rustdoc-link", 25 + ]) 26 + .arg(if cfg!(debug_assertions) { 27 + "--profile=dev" 28 + } else { 29 + "--profile=release" 30 + }) 31 + .assert() 32 + .success(); 33 + 34 + let path = setup_paths()?; 35 + 36 + let root = TempDir::new()?; 37 + 38 + log::debug!("{root:?}"); 39 + 40 + log::info!("given: a book"); 41 + Command::new("mdbook") 42 + .args(["init", "--force"]) 43 + .env("PATH", &path) 44 + .current_dir(&root) 45 + .unwrap() 46 + .assert() 47 + .success(); 48 + 49 + log::info!("given: preprocessor is enabled"); 50 + std::fs::File::options() 51 + .append(true) 52 + .open(root.path().join("book.toml"))? 53 + .pipe(|mut file| file.write_all("[preprocessor.rustdoc-link]\n".as_bytes()))?; 54 + 55 + log::info!("when: book is not a Cargo project"); 56 + log::info!("then: preprocessor fails"); 57 + Command::new("mdbook") 58 + .arg("build") 59 + .env("PATH", &path) 60 + .current_dir(&root) 61 + .assert() 62 + .failure() 63 + .stderr(predicate::str::contains( 64 + "failed to determine the current Cargo project", 65 + )); 66 + 67 + log::info!("given: book is a Cargo project"); 68 + Command::new("cargo") 69 + .arg("init") 70 + .args(["--name", "temp"]) 71 + .env("PATH", &path) 72 + .current_dir(&root) 73 + .assert() 74 + .success(); 75 + 76 + if Command::new("mdbook-rustdoc-link") 77 + .arg("rust-analyzer") 78 + .env("PATH", &path) 79 + .current_dir(&root) 80 + .assert() 81 + .try_stdout(predicate::str::contains("VS Code extension")) 82 + .is_ok() 83 + && not_in_ci("rust-analyzer code extension is already installed") 84 + { 85 + log::info!("when: book has item links"); 86 + std::fs::File::options() 87 + .append(true) 88 + .open(root.path().join("src/chapter_1.md"))? 89 + .pipe(|mut file| file.write_all("\n[std::thread]\n".as_bytes()))?; 90 + 91 + log::info!("then: book builds without errors"); 92 + Command::new("mdbook") 93 + .arg("build") 94 + .env("PATH", &path) 95 + .current_dir(&root) 96 + .assert() 97 + .success(); 98 + } else if Command::new("rust-analyzer") 99 + .arg("--version") 100 + .assert() 101 + .try_success() 102 + .is_ok() 103 + && not_in_ci("rust-analyzer is already available") 104 + { 105 + log::info!("skip testing mdbook build without rust-analyzer") 106 + } else { 107 + log::info!("when: rust-analyzer is not configured"); 108 + 109 + log::info!("when: book has no item links"); 110 + 111 + log::info!("then: book builds without errors"); 112 + Command::new("mdbook") 113 + .arg("build") 114 + .env("PATH", &path) 115 + .current_dir(&root) 116 + .assert() 117 + .success(); 118 + 119 + log::info!("when: book has item links"); 120 + std::fs::File::options() 121 + .append(true) 122 + .open(root.path().join("src/chapter_1.md"))? 123 + .pipe(|mut file| file.write_all("\n[std]\n".as_bytes()))?; 124 + 125 + log::info!("then: preprocessor fails"); 126 + Command::new("mdbook") 127 + .arg("build") 128 + .env("PATH", &path) 129 + .current_dir(&root) 130 + .assert() 131 + .failure() 132 + .stderr( 133 + predicate::str::contains("failed to spawn rust-analyzer") 134 + // https://github.com/rust-lang/rustup/issues/3846 135 + // rustup shims rust-analyzer when it's not installed 136 + .or(predicate::str::contains("Unknown binary 'rust-analyzer")), 137 + // ^ doesn't have a closing `'` because on windows it says 'rust-analyzer.exe' 138 + ); 139 + 140 + log::info!("when: code extension is installed"); 141 + 142 + let extension_dir = tempfile::Builder::new() 143 + .prefix(".vscode") 144 + .suffix("") 145 + .rand_bytes(0) 146 + .tempdir_in(dirs::home_dir().context("failed to get home dir")?)?; 147 + 148 + let ra_executable = extension_dir 149 + .path() 150 + .join("extensions/rust-lang.rust-analyzer-lorem-ipsum") 151 + .join("server/rust-analyzer"); 152 + 153 + Command::new("cargo") 154 + .args(["run", "--package", "util-rust-analyzer", "--"]) 155 + .arg("--ra-path") 156 + .arg(ra_executable) 157 + .arg("download") 158 + .unwrap() 159 + .assert() 160 + .success(); 161 + 162 + log::info!("then: book builds without errors"); 163 + Command::new("mdbook") 164 + .arg("build") 165 + .env("PATH", &path) 166 + .current_dir(&root) 167 + .assert() 168 + .success(); 169 + } 170 + 171 + Ok(()) 172 + }
+14 -112
crates/mdbookkit/Cargo.toml
··· 16 16 keywords = ["mdbook", "documentation", "rust-analyzer", "lsp", "permalink"] 17 17 readme = "README.md" 18 18 19 - autobins = false 20 - autotests = false 21 - 22 19 [dependencies] 23 20 anyhow = { workspace = true } 24 - async-lsp = { version = "0.2.2", optional = true } 25 - cargo_toml = { version = "0.21.0", optional = true } 26 - chrono = { version = "0.4.40", features = [ 27 - "clock", 28 - ], default-features = false, optional = true } 29 - clap = { workspace = true, optional = true } 30 - console = { version = "0.15.11", optional = true } 31 - dirs = { version = "6.0.0", optional = true } 32 - env_logger = { workspace = true, optional = true } 33 - git2 = { version = "0.20.1", default-features = false, optional = true } 34 - gix-url = { version = "0.30.0", optional = true } 35 - indicatif = { version = "0.17.11", optional = true } 21 + chrono = { version = "0.4.40", features = ["clock"], default-features = false } 22 + clap = { workspace = true } 23 + console = { version = "0.15.11" } 24 + env_logger = { workspace = true } 25 + indicatif = { version = "0.17.11" } 36 26 log = { workspace = true } 37 - lsp-types = { version = "0.95.0", optional = true } 38 - mdbook = { workspace = true, optional = true } 39 - miette = { workspace = true, optional = true } 40 - owo-colors = { version = "4.2.0", optional = true } 41 - percent-encoding = { version = "2.3.1", optional = true } 42 - proc-macro2 = { version = "1.0.94", features = [ 43 - "span-locations", 44 - ], optional = true } 27 + mdbook = { workspace = true } 28 + miette = { workspace = true } 29 + owo-colors = { version = "4.2.0" } 45 30 pulldown-cmark = { workspace = true } 46 31 pulldown-cmark-to-cmark = { workspace = true } 47 - serde = { workspace = true, features = ["rc"] } 32 + serde = { workspace = true } 48 33 serde_json = { workspace = true } 49 - sha2 = { version = "0.10.8", optional = true } 50 - shlex = { workspace = true, optional = true } 51 - syn = { version = "2.0.99", optional = true } 52 34 tap = { workspace = true } 53 - tempfile = { workspace = true, optional = true } 54 - thiserror = { workspace = true } 55 - tokio = { workspace = true, optional = true } 56 - tokio-util = { version = "0.7.13", features = ["compat"], optional = true } 57 - toml = { workspace = true, optional = true } 58 - tower = { version = "0.5.2", optional = true } 59 - url = { workspace = true, features = ["serde"], optional = true } 35 + toml = { workspace = true } 60 36 61 - [dev-dependencies] 62 - assert_cmd = { workspace = true } 63 - cargo-run-bin = { workspace = true } 64 - insta = { workspace = true } 65 - predicates = { workspace = true } 66 - similar = { workspace = true } 67 - util-testing = { workspace = true } 37 + cargo-run-bin = { workspace = true, optional = true } 38 + insta = { workspace = true, optional = true } 39 + url = { workspace = true, optional = true } 68 40 69 41 [features] 70 - lib-rustdoc-link = [ 71 - "dep:async-lsp", 72 - "dep:cargo_toml", 73 - "dep:dirs", 74 - "dep:lsp-types", 75 - "dep:proc-macro2", 76 - "dep:sha2", 77 - "dep:shlex", 78 - "dep:syn", 79 - "dep:tempfile", 80 - "dep:tokio", 81 - "dep:tokio-util", 82 - "dep:tower", 83 - "tokio/process", 84 - "tokio/rt", 85 - "tokio/time", 86 - ] 87 - rustdoc-link = [ 88 - "lib-rustdoc-link", 89 - "dep:toml", 90 - "tokio/fs", 91 - "tokio/rt-multi-thread", 92 - "common-cli", 93 - "common-logger", 94 - ] 95 - 96 - lib-link-forever = ["dep:mdbook", "dep:percent-encoding", "dep:url"] 97 - link-forever = [ 98 - "lib-link-forever", 99 - "dep:git2", 100 - "dep:gix-url", 101 - "common-cli", 102 - "common-logger", 103 - ] 104 - 105 - common-logger = [ 106 - "dep:chrono", 107 - "dep:console", 108 - "dep:env_logger", 109 - "dep:indicatif", 110 - "dep:miette", 111 - "dep:owo-colors", 112 - ] 113 - 114 - common-cli = ["dep:clap", "dep:mdbook", "dep:toml"] 115 - 116 - default = [] 117 - 118 - # aliases 119 - mdbook-link-forever = ["link-forever"] 120 - mdbook-rustdoc-link = ["rustdoc-link"] 121 - 122 - [[bin]] 123 - name = "mdbook-rustdoc-link" 124 - path = "src/bin/rustdoc_link/main.rs" 125 - required-features = ["rustdoc-link"] 126 - 127 - [[test]] 128 - name = "rustdoc-link" 129 - path = "tests/rustdoc_link.rs" 130 - required-features = ["rustdoc-link"] 131 - 132 - [[bin]] 133 - name = "mdbook-link-forever" 134 - path = "src/bin/link_forever/main.rs" 135 - required-features = ["link-forever"] 136 - 137 - [[test]] 138 - name = "link-forever" 139 - path = "tests/link_forever.rs" 140 - required-features = ["link-forever"] 42 + _testing = ["dep:cargo-run-bin", "dep:insta", "dep:url"] 141 43 142 44 [package.metadata.docs.rs] 143 45 all-features = true
-5
crates/mdbookkit/src/bin/mod.rs
··· 1 - #[cfg(feature = "lib-rustdoc-link")] 2 - pub mod rustdoc_link; 3 - 4 - #[cfg(feature = "lib-link-forever")] 5 - pub mod link_forever;
+4 -4
crates/mdbookkit/src/bin/rustdoc_link/cache.rs crates/mdbook-rustdoc-link/src/cache.rs
··· 5 5 iter, 6 6 }; 7 7 8 - use anyhow::{bail, Context, Result}; 8 + use anyhow::{Context, Result, bail}; 9 9 use lsp_types::Url; 10 - use serde::{de::DeserializeOwned, Deserialize, Serialize}; 10 + use serde::{Deserialize, Serialize, de::DeserializeOwned}; 11 11 use sha2::{Digest, Sha256}; 12 12 use tap::{Pipe, Tap, TapFallible}; 13 13 use tokio::task::JoinSet; 14 14 15 - use crate::log_debug; 15 + use mdbookkit::log_debug; 16 16 17 - use super::{env::Environment, link::ItemLinks, page::Pages, url::UrlToPath, Resolver}; 17 + use super::{Resolver, env::Environment, link::ItemLinks, page::Pages, url::UrlToPath}; 18 18 19 19 #[allow(async_fn_in_trait)] 20 20 pub trait Cache: DeserializeOwned + Serialize {
+9 -25
crates/mdbookkit/src/bin/rustdoc_link/client.rs crates/mdbook-rustdoc-link/src/client.rs
··· 7 7 time::Duration, 8 8 }; 9 9 10 - use anyhow::{anyhow, bail, Context, Result}; 11 - use async_lsp::{ 12 - concurrency::ConcurrencyLayer, panic::CatchUnwindLayer, router::Router, LanguageServer, 13 - MainLoop, ServerSocket, 14 - }; 10 + use anyhow::{Context, Result, anyhow, bail}; 11 + use async_lsp::{LanguageServer, MainLoop, ServerSocket, router::Router}; 15 12 use lsp_types::{ 16 - notification::{LogMessage, Progress, PublishDiagnostics, ShowMessage}, 17 - request::Request, 18 13 ClientCapabilities, ClientInfo, DidCloseTextDocumentParams, DidOpenTextDocumentParams, 19 14 GeneralClientCapabilities, GotoDefinitionParams, GotoDefinitionResponse, InitializeParams, 20 15 InitializeResult, InitializedParams, LogMessageParams, MessageType, NumberOrString, Position, 21 16 PositionEncodingKind, ProgressParams, ProgressParamsValue, ServerInfo, ShowMessageParams, 22 17 TextDocumentIdentifier, TextDocumentItem, TextDocumentPositionParams, Url, 23 18 WindowClientCapabilities, WorkDoneProgress, WorkspaceFolder, 19 + notification::{LogMessage, Progress, PublishDiagnostics, ShowMessage}, 20 + request::Request, 24 21 }; 25 22 use serde::{Deserialize, Serialize}; 26 23 use serde_json::json; 27 24 use tap::{Pipe, TapFallible}; 28 25 use tokio::{ 29 - sync::{mpsc, OnceCell, OwnedSemaphorePermit, Semaphore}, 26 + sync::{OnceCell, OwnedSemaphorePermit, Semaphore, mpsc}, 30 27 task::JoinHandle, 31 28 }; 32 29 use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt}; 33 30 use tower::ServiceBuilder; 34 31 35 - use crate::{log_debug, log_warning, logging::spinner}; 32 + use mdbookkit::{log_debug, log_warning, logging::spinner}; 36 33 37 34 use super::{ 38 35 env::Environment, ··· 88 85 /// Shutdown the server if it was spawned. 89 86 /// 90 87 /// Returns the [`Environment`] struct for further use. 91 - /// 92 - /// This moves `self`. To shutdown a [`Client`] that's in an [`Arc`], 93 - /// use [`client.drop`][Client::drop]. 94 88 pub async fn stop(self) -> Environment { 95 89 if let Some(server) = self.server.into_inner() { 96 90 server ··· 101 95 .ok(); 102 96 } 103 97 self.env 104 - } 105 - 106 - /// See [`client.stop`][Client::stop]. 107 - pub async fn drop(self: Arc<Self>) -> Result<Environment> { 108 - let Some(this) = Arc::into_inner(self) else { 109 - bail!("attempted to shutdown a client that is still referenced") 110 - }; 111 - Ok(this.stop().await) 112 98 } 113 99 } 114 100 ··· 272 258 }) 273 259 .event::<StopEvent>(|_, _| ControlFlow::Break(Ok(()))); 274 260 275 - ServiceBuilder::new() 276 - .layer(CatchUnwindLayer::default()) 277 - .layer(ConcurrencyLayer::default()) 278 - .service(router) 261 + ServiceBuilder::new().service(router) 279 262 }); 280 263 281 264 let proc = env 265 + .which() 282 266 .command()? 283 267 .current_dir(env.crate_dir.to_path()?) 284 268 .stdin(Stdio::piped()) 285 269 .stdout(Stdio::piped()) 286 270 // TODO: managed subcommand stderr 287 - .stderr(Stdio::inherit()) 271 + .stderr(Stdio::null()) 288 272 .spawn() 289 273 .context("failed to spawn rust-analyzer")?; 290 274
+56 -49
crates/mdbookkit/src/bin/rustdoc_link/env.rs crates/mdbook-rustdoc-link/src/env.rs
··· 5 5 time::Duration, 6 6 }; 7 7 8 - use anyhow::{anyhow, bail, Context, Result}; 8 + use anyhow::{Context, Result, anyhow, bail}; 9 9 use cargo_toml::{Manifest, Product}; 10 10 use lsp_types::Url; 11 11 use pulldown_cmark::Options; 12 - use serde::{de::DeserializeOwned, Deserialize, Serialize}; 12 + use serde::{Deserialize, Serialize, de::DeserializeOwned}; 13 13 use shlex::Shlex; 14 14 use tap::Pipe; 15 15 use tokio::process::Command; 16 16 17 - use crate::{env::ErrorHandling, markdown::mdbook_markdown}; 17 + use mdbookkit::{error::OnWarning, markdown::mdbook_markdown_options}; 18 18 19 19 use super::markdown; 20 20 ··· 23 23 /// This is both deserialized from book.toml and parsed from the command line. 24 24 /// 25 25 /// Doc comments for attributes populate the `configuration.md` page in the docs. 26 - #[derive(Deserialize, Debug, Default, Clone)] 27 - #[cfg_attr(feature = "common-cli", derive(clap::Parser))] 26 + #[derive(clap::Parser, Deserialize, Debug, Default, Clone)] 28 27 #[serde(rename_all = "kebab-case", deny_unknown_fields)] 29 28 pub struct Config { 30 29 /// Command to use for spawning rust-analyzer. ··· 35 34 /// 36 35 /// The command string will be tokenized by [shlex], so you can include arguments in it. 37 36 #[serde(default)] 38 - #[cfg_attr( 39 - feature = "common-cli", 40 - arg( 41 - long, 42 - value_name("COMMAND"), 43 - value_hint(clap::ValueHint::CommandString) 44 - ) 37 + #[arg( 38 + long, 39 + value_name("COMMAND"), 40 + value_hint(clap::ValueHint::CommandString) 45 41 )] 46 42 pub rust_analyzer: Option<String>, 47 43 ··· 55 51 /// comma-separated values, or specify multiple times; to enable all features, 56 52 /// specify `--cargo-features all`. 57 53 #[serde(default)] 58 - #[cfg_attr( 59 - feature = "common-cli", 60 - arg(long, value_delimiter(','), value_name("FEATURES")) 61 - )] 54 + #[arg(long, value_delimiter(','), value_name("FEATURES"))] 62 55 pub cargo_features: Vec<String>, 63 56 64 57 /// Directory from which to search for a Cargo project. ··· 69 62 /// The processor requires the Cargo.toml of a package to work. If you are working 70 63 /// on a Cargo workspace, set this to the relative path to a member crate. 71 64 #[serde(default)] 72 - #[cfg_attr( 73 - feature = "common-cli", 74 - arg(long, value_name("PATH"), value_hint(clap::ValueHint::DirPath)) 75 - )] 65 + #[arg(long, value_name("PATH"), value_hint(clap::ValueHint::DirPath))] 76 66 pub manifest_dir: Option<PathBuf>, 77 67 78 68 /// Directory in which to persist build cache. 79 69 /// 80 70 /// Setting this will enable caching. Will skip rust-analyzer if cache hits. 81 71 #[serde(default)] 82 - #[cfg_attr( 83 - feature = "common-cli", 84 - arg(long, value_name("PATH"), value_hint(clap::ValueHint::DirPath)) 85 - )] 72 + #[arg(long, value_name("PATH"), value_hint(clap::ValueHint::DirPath))] 86 73 pub cache_dir: Option<PathBuf>, 87 74 88 75 /// Exit with a non-zero status code when some links fail to resolve. 89 76 /// 90 77 /// Warnings are always printed to the console regardless of this option. 91 78 #[serde(default)] 92 - #[cfg_attr(feature = "common-cli", arg(long, value_enum, value_name("MODE"), default_value_t = Default::default()))] 93 - pub fail_on_warnings: ErrorHandling, 79 + #[arg(long, value_enum, value_name("MODE"), default_value_t = Default::default())] 80 + pub fail_on_warnings: OnWarning, 94 81 95 82 /// Whether to enable punctuations like smart quotes `“”`. 96 83 /// ··· 101 88 /// **In `book.toml`** — this option is not needed because 102 89 /// `output.html.smart-punctuation` is honored. 103 90 #[serde(default)] 104 - #[cfg_attr(feature = "common-cli", arg(long))] 91 + #[arg(long)] 105 92 pub smart_punctuation: bool, 106 93 107 94 #[serde(default)] 108 - #[cfg_attr(feature = "common-cli", arg(long, hide = true))] 95 + #[arg(long, hide = true)] 109 96 pub prefer_local_links: bool, 110 97 111 98 /// Timeout in seconds to wait for rust-analyzer to finish indexing. 112 99 #[serde(default)] 113 - #[cfg_attr( 114 - feature = "common-cli", 115 - arg(long, value_name("SECONDS"), default_value("60")) 116 - )] 100 + #[arg(long, value_name("SECONDS"), default_value("60"))] 117 101 pub rust_analyzer_timeout: Option<u64>, 118 102 119 103 #[allow(unused)] 120 104 #[serde(default)] 121 105 #[doc(hidden)] 122 - #[cfg_attr(feature = "common-cli", arg(skip))] 106 + #[arg(skip)] 123 107 pub after: Option<Vec<String>>, 124 108 125 109 #[allow(unused)] 126 110 #[serde(default)] 127 111 #[doc(hidden)] 128 - #[cfg_attr(feature = "common-cli", arg(skip))] 112 + #[arg(skip)] 129 113 pub before: Option<Vec<String>>, 130 114 131 115 #[allow(unused)] 132 116 #[serde(default)] 133 117 #[doc(hidden)] 134 - #[cfg_attr(feature = "common-cli", arg(skip))] 118 + #[arg(skip)] 135 119 pub renderers: Option<Vec<String>>, 136 120 137 121 #[allow(unused)] 138 122 #[serde(default)] 139 123 #[doc(hidden)] 140 - #[cfg_attr(feature = "common-cli", arg(skip))] 124 + #[arg(skip)] 141 125 pub command: Option<String>, 142 126 } 143 127 ··· 233 217 }) 234 218 } 235 219 236 - pub fn command(&self) -> Result<Command> { 220 + pub fn which(&self) -> RustAnalyzer<'_> { 237 221 if let Some(command) = self.config.rust_analyzer.as_deref() { 238 - let mut words = Shlex::new(command); 239 - let executable = words 240 - .next() 241 - .context("unexpected empty string for option `rust-analyzer`")?; 242 - let mut cmd = Command::new(executable); 243 - cmd.args(words); 244 - Ok(cmd) 245 - } else if let Some(extension) = find_code_extension() { 246 - log::debug!("using rust-analyzer from {}", extension.display()); 247 - Ok(Command::new(extension)) 222 + RustAnalyzer::Custom(command) 223 + } else if let Some(command) = find_code_extension() { 224 + RustAnalyzer::VsCode(command) 248 225 } else { 249 - Ok(Command::new("rust-analyzer")) 226 + RustAnalyzer::Path 250 227 } 251 228 } 252 229 ··· 256 233 } else { 257 234 Options::empty() 258 235 }; 259 - markdown::stream(source, options.union(mdbook_markdown())) 236 + markdown::stream(source, options.union(mdbook_markdown_options())) 260 237 } 261 238 262 239 pub fn emit_config(&self) -> EmitConfig { ··· 342 319 .pipe(Ok) 343 320 } else { 344 321 bail!(String::from_utf8_lossy(&output.stderr).into_owned()); 322 + } 323 + } 324 + } 325 + 326 + pub enum RustAnalyzer<'a> { 327 + Custom(&'a str), 328 + VsCode(PathBuf), 329 + Path, 330 + } 331 + 332 + impl<'a> RustAnalyzer<'a> { 333 + pub fn command(self) -> Result<Command> { 334 + match self { 335 + Self::Custom(cmd) => { 336 + let mut words = Shlex::new(cmd); 337 + let executable = words 338 + .next() 339 + .context("unexpected empty string for option `rust-analyzer`")?; 340 + let mut cmd = Command::new(executable); 341 + cmd.args(words); 342 + Ok(cmd) 343 + } 344 + Self::VsCode(cmd) => { 345 + log::debug!("using rust-analyzer from {}", cmd.display()); 346 + Ok(Command::new(cmd)) 347 + } 348 + Self::Path => { 349 + log::debug!("using rust-analyzer on PATH"); 350 + Ok(Command::new("rust-analyzer")) 351 + } 345 352 } 346 353 } 347 354 }
crates/mdbookkit/src/bin/rustdoc_link/item.rs crates/mdbook-rustdoc-link/src/item.rs
+2 -3
crates/mdbookkit/src/bin/rustdoc_link/link.rs crates/mdbook-rustdoc-link/src/link.rs
··· 1 1 use std::{borrow::Cow, ops::Range, sync::Arc}; 2 2 3 - use anyhow::{bail, Result}; 3 + use anyhow::{Result, bail}; 4 4 use lsp_types::Url; 5 5 use pulldown_cmark::{CowStr, Event, LinkType, Tag, TagEnd}; 6 6 use serde::{Deserialize, Serialize}; 7 7 use tap::{Pipe, Tap, TapFallible}; 8 8 9 - use crate::log_trace; 9 + use mdbookkit::log_trace; 10 10 11 11 use super::{env::EmitConfig, item::Item}; 12 12 13 - #[cfg(feature = "common-logger")] 14 13 pub mod diagnostic; 15 14 16 15 #[derive(Debug)]
+2 -2
crates/mdbookkit/src/bin/rustdoc_link/link/diagnostic.rs crates/mdbook-rustdoc-link/src/link/diagnostic.rs
··· 3 3 use log::Level; 4 4 use miette::LabeledSpan; 5 5 6 - use crate::diagnostics::{Issue, Problem}; 6 + use mdbookkit::diagnostics::{Issue, IssueItem}; 7 7 8 8 use super::{Link, LinkState}; 9 9 ··· 20 20 Debug, 21 21 } 22 22 23 - impl Problem for LinkDiagnostic { 23 + impl IssueItem for LinkDiagnostic { 24 24 type Kind = LinkStatus; 25 25 26 26 fn issue(&self) -> Self::Kind {
-186
crates/mdbookkit/src/bin/rustdoc_link/main.rs
··· 1 - use std::{collections::HashMap, io::Write}; 2 - 3 - use anyhow::{Context, Result}; 4 - use clap::{Parser, Subcommand}; 5 - use console::colors_enabled_stderr; 6 - use log::LevelFilter; 7 - use mdbook::preprocess::PreprocessorContext; 8 - use tap::{Pipe, TapFallible}; 9 - 10 - use mdbookkit::{ 11 - bin::rustdoc_link::{ 12 - cache::{Cache, FileCache}, 13 - env::{Config, Environment}, 14 - Client, Pages, Resolver, 15 - }, 16 - diagnostics::Issue, 17 - env::{ 18 - book_from_stdin, book_into_stdout, config_from_book, for_each_chapter_mut, iter_chapters, 19 - smart_punctuation, string_from_stdin, 20 - }, 21 - log_warning, 22 - logging::{is_logging, ConsoleLogger}, 23 - }; 24 - 25 - #[tokio::main] 26 - async fn main() -> Result<()> { 27 - ConsoleLogger::install("rustdoc-link"); 28 - match Program::parse().command { 29 - Some(Command::Supports { .. }) => Ok(()), 30 - Some(Command::Markdown(options)) => markdown(options).await, 31 - None => mdbook().await, 32 - } 33 - } 34 - 35 - #[derive(Parser, Debug, Clone)] 36 - struct Program { 37 - #[command(subcommand)] 38 - command: Option<Command>, 39 - } 40 - 41 - #[derive(Subcommand, Debug, Clone)] 42 - enum Command { 43 - /// Link to Rust documentation à la rustdoc. 44 - /// 45 - /// Markdown is read from stdin and written to stdout. 46 - Markdown(Config), 47 - 48 - /// Supporting command for mdbook. 49 - /// 50 - /// See <https://rust-lang.github.io/mdBook/for_developers/preprocessors.html#hooking-into-mdbook> 51 - #[clap(hide = true)] 52 - Supports { renderer: String }, 53 - } 54 - 55 - async fn mdbook() -> Result<()> { 56 - let (context, mut book) = book_from_stdin().context("failed to parse book content")?; 57 - 58 - let config = config(&context).context("failed to read preprocessor config from book.toml")?; 59 - 60 - let client = Environment::new(config) 61 - .context("failed to initialize `mdbook-rustdoc-link`")? 62 - .pipe(Client::new); 63 - 64 - let cached = FileCache::load(client.env()).await.ok(); 65 - 66 - let mut content = Pages::default(); 67 - 68 - for (path, ch) in iter_chapters(&book) { 69 - let stream = client.env().markdown(&ch.content).into_offset_iter(); 70 - content 71 - .read(path.clone(), &ch.content, stream) 72 - .with_context(|| path.display().to_string()) 73 - .context("failed to parse Markdown source:")?; 74 - } 75 - 76 - if let Some(cached) = cached { 77 - cached.resolve(&mut content).await.ok(); 78 - } 79 - 80 - client 81 - .resolve(&mut content) 82 - .await 83 - .context("failed to resolve some links")?; 84 - 85 - let mut result = iter_chapters(&book) 86 - .filter_map(|(path, _)| { 87 - let output = content 88 - .emit(path, &client.env().emit_config()) 89 - .tap_err(log_warning!()) 90 - .ok()?; 91 - Some((path.clone(), output.to_string())) 92 - }) 93 - .collect::<HashMap<_, _>>(); 94 - 95 - let env = client.stop().await; 96 - 97 - let status = content 98 - .reporter() 99 - .names(|path| path.display().to_string()) 100 - .level(LevelFilter::Warn) 101 - .logging(is_logging()) 102 - .colored(colors_enabled_stderr()) 103 - .build() 104 - .to_stderr() 105 - .to_status(); 106 - 107 - if content.modified() { 108 - FileCache::save(&env, &content).await.ok(); 109 - } 110 - 111 - for_each_chapter_mut(&mut book, |path, ch| { 112 - if let Some(output) = result.remove(&path) { 113 - ch.content = output 114 - } 115 - }); 116 - 117 - book_into_stdout(&book)?; 118 - 119 - env.config.fail_on_warnings.check(status.level())?; 120 - 121 - Ok(()) 122 - } 123 - 124 - async fn markdown(config: Config) -> Result<()> { 125 - let client = Environment::new(config) 126 - .context("failed to initialize")? 127 - .pipe(Client::new); 128 - 129 - let source = string_from_stdin().context("failed to read Markdown source from stdin")?; 130 - 131 - let stream = client.env().markdown(&source).into_offset_iter(); 132 - 133 - let mut content = Pages::one(&source, stream).context("failed to parse Markdown source")?; 134 - 135 - if let Ok(cached) = FileCache::load(client.env()).await { 136 - cached.resolve(&mut content).await.ok(); 137 - } 138 - 139 - client 140 - .resolve(&mut content) 141 - .await 142 - .context("failed to resolve some links")?; 143 - 144 - let env = client.stop().await; 145 - 146 - let status = content 147 - .reporter() 148 - .names(|_| "<stdin>".into()) 149 - .level(LevelFilter::Warn) 150 - .logging(is_logging()) 151 - .colored(colors_enabled_stderr()) 152 - .build() 153 - .to_stderr() 154 - .to_status(); 155 - 156 - if content.modified() { 157 - FileCache::save(&env, &content).await.ok(); 158 - } 159 - 160 - content 161 - .get(&env.emit_config()) 162 - .map(|emit| emit.to_string()) 163 - .and_then(|output| Ok(std::io::stdout().write_all(output.as_bytes())?))?; 164 - 165 - env.config.fail_on_warnings.check(status.level())?; 166 - 167 - Ok(()) 168 - } 169 - 170 - fn config(context: &PreprocessorContext) -> Result<Config> { 171 - let mut config = config_from_book::<Config>(&context.config, "rustdoc-link")?; 172 - 173 - if let Some(path) = config.manifest_dir { 174 - config.manifest_dir = Some(context.root.join(path)) 175 - } else { 176 - config.manifest_dir = Some(context.root.clone()) 177 - } 178 - 179 - if let Some(path) = config.cache_dir { 180 - config.cache_dir = Some(context.root.join(path)) 181 - } 182 - 183 - config.smart_punctuation = smart_punctuation(&context.config); 184 - 185 - Ok(config) 186 - }
+2 -2
crates/mdbookkit/src/bin/rustdoc_link/markdown.rs crates/mdbook-rustdoc-link/src/markdown.rs
··· 1 1 use pulldown_cmark::{BrokenLink, BrokenLinkCallback, CowStr, Event, Options, Parser}; 2 2 use tap::Pipe; 3 3 4 - use crate::markdown::mdbook_markdown; 4 + use mdbookkit::markdown::mdbook_markdown_options; 5 5 6 6 pub fn stream(text: &str, options: Options) -> MarkdownStream<'_> { 7 7 Parser::new_with_broken_link_callback(text, options, Some(ItemLinks)) ··· 24 24 // Explicitly disable smart punctuation to prevent quotes from being changed 25 25 // or else things like lifetimes may become invalid 26 26 const OPTIONS: pulldown_cmark::Options = 27 - mdbook_markdown().intersection(Options::ENABLE_SMART_PUNCTUATION.complement()); 27 + mdbook_markdown_options().intersection(Options::ENABLE_SMART_PUNCTUATION.complement()); 28 28 } 29 29 30 30 impl<'input> BrokenLinkCallback<'input> for ItemLinks {
-152
crates/mdbookkit/src/bin/rustdoc_link/mod.rs
··· 1 - use std::{borrow::Borrow, collections::HashMap, hash::Hash, sync::Arc}; 2 - 3 - use anyhow::{Context, Result}; 4 - use lsp_types::Position; 5 - use tap::{Pipe, TapFallible}; 6 - use tokio::task::JoinSet; 7 - 8 - mod client; 9 - pub mod env; 10 - mod item; 11 - mod link; 12 - mod markdown; 13 - mod page; 14 - mod sync; 15 - mod url; 16 - 17 - #[cfg(feature = "rustdoc-link")] 18 - pub mod cache; 19 - 20 - use crate::{log_debug, logging::spinner, styled}; 21 - 22 - pub use self::{client::Client, page::Pages}; 23 - use self::{item::Item, link::ItemLinks, url::UrlToPath}; 24 - 25 - /// Type that can provide links. 26 - /// 27 - /// Resolvers should modify the provided [`Pages`] in place. 28 - /// 29 - /// This is currently an abstraction over two sources of links: 30 - /// 31 - /// - [`Client`], which invokes rust-analyzer 32 - /// - [`Cache`] implementations 33 - /// 34 - /// [`Cache`]: crate::bin::rustdoc_link::cache::Cache 35 - #[allow(async_fn_in_trait)] 36 - pub trait Resolver { 37 - async fn resolve<K>(&self, pages: &mut Pages<'_, K>) -> Result<()> 38 - where 39 - K: Eq + Hash; 40 - } 41 - 42 - impl Resolver for Client { 43 - async fn resolve<K>(&self, pages: &mut Pages<'_, K>) -> Result<()> 44 - where 45 - K: Eq + Hash, 46 - { 47 - let request = pages.items(); 48 - 49 - if request.is_empty() { 50 - return Ok(()); 51 - } 52 - 53 - let main = std::fs::read_to_string(self.env().entrypoint.to_path()?)?; 54 - 55 - let (context, request) = { 56 - let mut context = format!("{main}\nfn {UNIQUE_ID} () {{\n"); 57 - 58 - let line = context.chars().filter(|&c| c == '\n').count(); 59 - 60 - let request = request 61 - .iter() 62 - .scan(line, |line, (key, item)| { 63 - build(&mut context, line, item).map(|cursors| (key.clone(), cursors)) 64 - }) 65 - .collect::<Vec<_>>(); 66 - 67 - fn build(context: &mut String, line: &mut usize, item: &Item) -> Option<Vec<Position>> { 68 - use std::fmt::Write; 69 - let _ = writeln!(context, "{}", item.stmt); 70 - let cursors = item 71 - .cursor 72 - .as_ref() 73 - .iter() 74 - .map(|&col| Position::new(*line as _, col as _)) 75 - .collect::<Vec<_>>(); 76 - *line += 1; 77 - Some(cursors) 78 - } 79 - 80 - context.push('}'); 81 - 82 - (context, request) 83 - }; 84 - 85 - log::debug!("request context\n\n{context}\n"); 86 - 87 - let document = self 88 - .open(self.env().entrypoint.clone(), context) 89 - .await? 90 - .pipe(Arc::new); 91 - 92 - spinner().create("resolve", Some(request.len() as _)); 93 - 94 - let tasks: JoinSet<Option<(String, ItemLinks)>> = request 95 - .into_iter() 96 - .map(|(key, pos)| { 97 - let key = key.to_string(); 98 - let doc = document.clone(); 99 - resolve(doc, key, pos) 100 - }) 101 - .collect(); 102 - 103 - async fn resolve( 104 - doc: Arc<client::OpenDocument>, 105 - key: String, 106 - pos: Vec<Position>, 107 - ) -> Option<(String, ItemLinks)> { 108 - let _task = spinner().task("resolve", &key); 109 - for p in pos { 110 - let resolved = doc 111 - .resolve(p) 112 - .await 113 - .with_context(|| format!("{p:?}")) 114 - .context("failed to resolve symbol:") 115 - .tap_err(log_debug!()) 116 - .ok(); 117 - if let Some(resolved) = resolved { 118 - return Some((key, resolved)); 119 - } 120 - } 121 - None 122 - } 123 - 124 - let resolved = tasks 125 - .join_all() 126 - .await 127 - .into_iter() 128 - .flatten() 129 - .collect::<HashMap<_, _>>(); 130 - 131 - spinner().finish("resolve", styled!(("done").green())); 132 - 133 - pages.apply(&resolved); 134 - 135 - Ok(()) 136 - } 137 - } 138 - 139 - impl<K> Resolver for HashMap<K, ItemLinks> 140 - where 141 - K: Borrow<str> + Eq + Hash, 142 - { 143 - async fn resolve<P>(&self, pages: &mut Pages<'_, P>) -> Result<()> 144 - where 145 - P: Eq + Hash, 146 - { 147 - pages.apply(self); 148 - Ok(()) 149 - } 150 - } 151 - 152 - const UNIQUE_ID: &str = "__ded48f4d_0c4f_4950_b17d_55fd3b2a0c86__";
+12 -4
crates/mdbookkit/src/bin/rustdoc_link/page.rs crates/mdbook-rustdoc-link/src/page.rs
··· 5 5 hash::Hash, 6 6 }; 7 7 8 - use anyhow::{bail, Context, Result}; 8 + use anyhow::{Context, Result, bail}; 9 9 use pulldown_cmark::{CowStr, Event, Tag, TagEnd}; 10 10 use tap::Pipe; 11 11 12 - use crate::markdown::{PatchStream, Spanned}; 12 + use mdbookkit::markdown::{PatchStream, Spanned}; 13 13 14 14 use super::{ 15 15 env::EmitConfig, ··· 17 17 link::{ItemLinks, Link, LinkState}, 18 18 }; 19 19 20 - #[cfg(feature = "common-logger")] 21 20 mod diagnostic; 22 21 23 - #[derive(Debug, Default)] 22 + #[derive(Debug)] 24 23 pub struct Pages<'a, K> { 25 24 pages: HashMap<K, Page<'a>>, 26 25 modified: bool, ··· 98 97 99 98 pub fn get(&self, options: &EmitConfig) -> Result<String> { 100 99 self.emit(&(), options) 100 + } 101 + } 102 + 103 + impl<'a, K> Default for Pages<'a, K> { 104 + fn default() -> Self { 105 + Self { 106 + pages: Default::default(), 107 + modified: Default::default(), 108 + } 101 109 } 102 110 } 103 111
+1 -1
crates/mdbookkit/src/bin/rustdoc_link/page/diagnostic.rs crates/mdbook-rustdoc-link/src/page/diagnostic.rs
··· 1 1 use std::{fmt::Debug, hash::Hash}; 2 2 3 - use crate::diagnostics::{Diagnostics, ReportBuilder}; 3 + use mdbookkit::diagnostics::{Diagnostics, ReportBuilder}; 4 4 5 5 use super::{super::link::diagnostic::LinkDiagnostic, Pages}; 6 6
crates/mdbookkit/src/bin/rustdoc_link/sync.rs crates/mdbook-rustdoc-link/src/sync.rs
crates/mdbookkit/src/bin/rustdoc_link/url.rs crates/mdbook-rustdoc-link/src/url.rs
+69
crates/mdbookkit/src/book.rs
··· 1 + use std::{ 2 + io::{Read, Write}, 3 + path::PathBuf, 4 + }; 5 + 6 + use anyhow::{Context, Result}; 7 + use mdbook::{ 8 + BookItem, 9 + book::{Book, Chapter}, 10 + preprocess::PreprocessorContext, 11 + }; 12 + use serde::de::DeserializeOwned; 13 + use tap::Pipe; 14 + 15 + pub fn book_from_stdin() -> Result<(PreprocessorContext, Book)> { 16 + Ok(Vec::new() 17 + .pipe(|mut buf| std::io::stdin().read_to_end(&mut buf).and(Ok(buf)))? 18 + .pipe(String::from_utf8)? 19 + .pipe_as_ref(serde_json::from_str)?) 20 + } 21 + 22 + pub fn config_from_book<T>(config: &mdbook::Config, name: &str) -> Result<T> 23 + where 24 + T: DeserializeOwned + Default, 25 + { 26 + if let Some(config) = config.get_preprocessor(name) { 27 + T::deserialize(toml::Value::Table(config.clone()))? 28 + } else { 29 + Default::default() 30 + } 31 + .pipe(Ok) 32 + } 33 + 34 + pub fn smart_punctuation(config: &mdbook::Config) -> bool { 35 + config 36 + .get_deserialized_opt::<bool, _>("output.html.smart-punctuation") 37 + .unwrap_or_default() 38 + .unwrap_or(true) 39 + } 40 + 41 + pub fn iter_chapters(book: &Book) -> impl Iterator<Item = (&PathBuf, &Chapter)> { 42 + book.iter().filter_map(|item| { 43 + let BookItem::Chapter(ch) = item else { 44 + return None; 45 + }; 46 + let Some(path) = &ch.source_path else { 47 + return None; 48 + }; 49 + Some((path, ch)) 50 + }) 51 + } 52 + 53 + pub fn for_each_chapter_mut<F>(book: &mut Book, mut func: F) 54 + where 55 + F: FnMut(PathBuf, &mut Chapter), 56 + { 57 + book.for_each_mut(|item| { 58 + let BookItem::Chapter(ch) = item else { return }; 59 + let Some(path) = &ch.source_path else { return }; 60 + func(path.clone(), ch) 61 + }); 62 + } 63 + 64 + pub fn book_into_stdout(book: &Book) -> Result<()> { 65 + serde_json::to_string(&book) 66 + .context("failed to serialize book") 67 + .and_then(|output| Ok(std::io::stdout().write_all(output.as_bytes())?)) 68 + .context("failed to write book to stdout") 69 + }
+28 -16
crates/mdbookkit/src/diagnostics.rs
··· 1 1 //! Error reporting for preprocessors. 2 2 3 - use std::fmt::{self, Debug, Display, Write}; 3 + use std::{ 4 + borrow::Borrow, 5 + fmt::{self, Debug, Display, Write}, 6 + }; 4 7 5 8 use log::{Level, LevelFilter}; 6 9 use miette::{ ··· 12 15 13 16 /// Trait for Markdown diagnostics. This will eventually be printed to stderr. 14 17 /// 15 - /// Each [`Problem`] represents a specific message, such as a warning, associated with 18 + /// Each [`IssueItem`] represents a specific message, such as a warning, associated with 16 19 /// an [`Issue`] (the type and severity of the issue) and a location in the Markdown 17 20 /// source, represented by [`LabeledSpan`]. 18 - pub trait Problem: Send + Sync { 21 + pub trait IssueItem: Send + Sync { 19 22 type Kind: Issue; 20 23 fn issue(&self) -> Self::Kind; 21 24 fn label(&self) -> LabeledSpan; ··· 40 43 impl<K, P> Diagnostics<'_, K, P> 41 44 where 42 45 K: Title, 43 - P: Problem, 46 + P: IssueItem, 44 47 { 45 48 /// Render a report of the diagnostics using [miette]'s graphical reporting 46 49 pub fn to_report(&self, colored: bool) -> String { ··· 86 89 87 90 impl<'a, K, P> Diagnostics<'a, K, P> 88 91 where 89 - P: Problem, 92 + P: IssueItem, 90 93 { 91 94 pub fn new(text: &'a str, name: K, issues: Vec<P>) -> Self { 92 95 Self { text, name, issues } ··· 105 108 } 106 109 } 107 110 111 + pub fn name(&self) -> &K { 112 + &self.name 113 + } 114 + 108 115 fn status(&self) -> P::Kind { 109 116 self.issues 110 117 .iter() ··· 117 124 impl<K, P> Diagnostic for Diagnostics<'_, K, P> 118 125 where 119 126 K: Title, 120 - P: Problem, 127 + P: IssueItem, 121 128 { 122 129 fn severity(&self) -> Option<Severity> { 123 130 match self.status().level() { ··· 173 180 } 174 181 } 175 182 176 - impl<K, P: Problem> Debug for Diagnostics<'_, K, P> { 183 + impl<K, P: IssueItem> Debug for Diagnostics<'_, K, P> { 177 184 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 178 185 fmt::Debug::fmt(&self.status(), f) 179 186 } 180 187 } 181 188 182 - impl<K, P: Problem> Display for Diagnostics<'_, K, P> { 189 + impl<K, P: IssueItem> Display for Diagnostics<'_, K, P> { 183 190 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 184 191 fmt::Display::fmt(&self.status(), f) 185 192 } 186 193 } 187 194 188 - impl<K, P: Problem> std::error::Error for Diagnostics<'_, K, P> {} 195 + impl<K, P: IssueItem> std::error::Error for Diagnostics<'_, K, P> {} 189 196 190 197 /// Builder for printing diagnostics over multiple files. 191 198 pub struct ReportBuilder<'a, K, P, F> { ··· 238 245 self 239 246 } 240 247 248 + pub fn named<Q>(mut self, mut f: impl FnMut(&K) -> bool) -> Self 249 + where 250 + K: Borrow<Q>, 251 + Q: Eq + ?Sized, 252 + { 253 + self.items.retain(|d| f(&d.name)); 254 + self 255 + } 256 + 241 257 pub fn logging(mut self, logging: bool) -> Self { 242 258 self.logging = logging; 243 259 self ··· 246 262 247 263 impl<'a, K, P, F> ReportBuilder<'a, K, P, F> 248 264 where 249 - P: Problem, 265 + P: IssueItem, 250 266 { 251 267 pub fn build(self) -> Reporter<'a, P> 252 268 where ··· 293 309 294 310 impl<P> Reporter<'_, P> 295 311 where 296 - P: Problem, 312 + P: IssueItem, 297 313 { 298 314 pub fn to_status(&self) -> P::Kind { 299 315 self.items ··· 408 424 409 425 impl StyleCompat for Style { 410 426 fn toggle(self, enabled: bool) -> Self { 411 - if enabled { 412 - self 413 - } else { 414 - Style::new() 415 - } 427 + if enabled { self } else { Style::new() } 416 428 } 417 429 } 418 430
-150
crates/mdbookkit/src/env.rs
··· 1 - use std::io::Read; 2 - 3 - use anyhow::{anyhow, Result}; 4 - use serde::Deserialize; 5 - use tap::Pipe; 6 - 7 - pub fn is_ci() -> Option<String> { 8 - let ci = std::env::var("CI").unwrap_or("".into()); 9 - if matches!(ci.as_str(), "" | "0" | "false") { 10 - None 11 - } else { 12 - Some(ci) 13 - } 14 - } 15 - 16 - /// Flag indicating how the program should proceed when there are warnings. 17 - /// 18 - /// Used in preprocessor options. 19 - /// 20 - /// Doc comments for variants in this enum will show up in autogenerated docs. 21 - #[cfg_attr(feature = "common-cli", derive(clap::ValueEnum))] 22 - #[derive(Deserialize, Debug, Default, Clone, Copy)] 23 - #[serde(rename_all = "lowercase")] 24 - pub enum ErrorHandling { 25 - /// Fail if the environment variable `CI` is set to a value other than `0` or `false`. 26 - /// Environments like GitHub Actions configure this automatically. 27 - #[default] 28 - #[serde(rename = "ci")] 29 - #[cfg_attr(feature = "common-cli", clap(name = "ci"))] 30 - Env, 31 - 32 - /// Fail as long as there are warnings, even in local use. 33 - Always, 34 - } 35 - 36 - impl ErrorHandling { 37 - pub fn check(&self, level: log::Level) -> Result<()> { 38 - match level { 39 - log::Level::Error => Err(anyhow!("preprocessor has errors")), 40 - log::Level::Warn => match self { 41 - Self::Always => { 42 - anyhow!("treating warnings as errors because fail-on-unresolved is \"always\"") 43 - .context("preprocessor has errors") 44 - .pipe(Err) 45 - } 46 - Self::Env => { 47 - let Some(ci) = Self::warning_as_error() else { 48 - return Ok(()); 49 - }; 50 - anyhow!("treating warnings as errors because fail-on-unresolved is \"ci\" and CI={ci}") 51 - .context("preprocessor has errors") 52 - .pipe(Err) 53 - } 54 - }, 55 - _ => Ok(()), 56 - } 57 - } 58 - 59 - pub fn adjusted<T, E>(&self, result: Result<Result<T, E>, E>) -> Result<Result<T, E>, E> { 60 - match result { 61 - Ok(Err(error)) if Self::warning_as_error().is_some() => Err(error), 62 - result => result, 63 - } 64 - } 65 - 66 - fn warning_as_error() -> Option<String> { 67 - is_ci() 68 - } 69 - } 70 - 71 - pub fn string_from_stdin() -> Result<String> { 72 - Vec::new() 73 - .pipe(|mut buf| std::io::stdin().read_to_end(&mut buf).and(Ok(buf)))? 74 - .pipe(|buf| Ok(String::from_utf8(buf)?)) 75 - } 76 - 77 - #[cfg(feature = "common-cli")] 78 - pub use book::*; 79 - #[cfg(feature = "common-cli")] 80 - mod book { 81 - use std::{ 82 - io::{Read, Write}, 83 - path::PathBuf, 84 - }; 85 - 86 - use anyhow::{Context, Result}; 87 - use mdbook::{ 88 - book::{Book, Chapter}, 89 - preprocess::PreprocessorContext, 90 - BookItem, 91 - }; 92 - use serde::de::DeserializeOwned; 93 - use tap::Pipe; 94 - 95 - pub fn book_from_stdin() -> Result<(PreprocessorContext, Book)> { 96 - Ok(Vec::new() 97 - .pipe(|mut buf| std::io::stdin().read_to_end(&mut buf).and(Ok(buf)))? 98 - .pipe(String::from_utf8)? 99 - .pipe_as_ref(serde_json::from_str)?) 100 - } 101 - 102 - pub fn config_from_book<T>(config: &mdbook::Config, name: &str) -> Result<T> 103 - where 104 - T: DeserializeOwned + Default, 105 - { 106 - if let Some(config) = config.get_preprocessor(name) { 107 - T::deserialize(toml::Value::Table(config.clone()))? 108 - } else { 109 - Default::default() 110 - } 111 - .pipe(Ok) 112 - } 113 - 114 - pub fn smart_punctuation(config: &mdbook::Config) -> bool { 115 - config 116 - .get_deserialized_opt::<bool, _>("output.html.smart-punctuation") 117 - .unwrap_or_default() 118 - .unwrap_or(true) 119 - } 120 - 121 - pub fn iter_chapters(book: &Book) -> impl Iterator<Item = (&PathBuf, &Chapter)> { 122 - book.iter().filter_map(|item| { 123 - let BookItem::Chapter(ch) = item else { 124 - return None; 125 - }; 126 - let Some(path) = &ch.source_path else { 127 - return None; 128 - }; 129 - Some((path, ch)) 130 - }) 131 - } 132 - 133 - pub fn for_each_chapter_mut<F>(book: &mut Book, mut func: F) 134 - where 135 - F: FnMut(PathBuf, &mut Chapter), 136 - { 137 - book.for_each_mut(|item| { 138 - let BookItem::Chapter(ch) = item else { return }; 139 - let Some(path) = &ch.source_path else { return }; 140 - func(path.clone(), ch) 141 - }); 142 - } 143 - 144 - pub fn book_into_stdout(book: &Book) -> Result<()> { 145 - serde_json::to_string(&book) 146 - .context("failed to serialize book") 147 - .and_then(|output| Ok(std::io::stdout().write_all(output.as_bytes())?)) 148 - .context("failed to write book to stdout") 149 - } 150 - }
+66
crates/mdbookkit/src/error.rs
··· 1 + use anyhow::{Result, anyhow}; 2 + use serde::Deserialize; 3 + use tap::Pipe; 4 + 5 + pub fn is_ci() -> Option<String> { 6 + let ci = std::env::var("CI").unwrap_or("".into()); 7 + if matches!(ci.as_str(), "" | "0" | "false") { 8 + None 9 + } else { 10 + Some(ci) 11 + } 12 + } 13 + 14 + /// Flag indicating how the program should proceed when there are warnings. 15 + /// 16 + /// Used in preprocessor options. 17 + /// 18 + /// Doc comments for variants in this enum will show up in autogenerated docs. 19 + #[derive(clap::ValueEnum, Deserialize, Debug, Default, Clone, Copy)] 20 + #[serde(rename_all = "lowercase")] 21 + pub enum OnWarning { 22 + /// Fail if the environment variable `CI` is set to a value other than `0` or `false`. 23 + /// Environments like GitHub Actions configure this automatically. 24 + #[default] 25 + #[serde(rename = "ci")] 26 + #[clap(name = "ci")] 27 + FailInCi, 28 + 29 + /// Fail as long as there are warnings, even in local use. 30 + AlwaysFail, 31 + } 32 + 33 + impl OnWarning { 34 + pub fn check(&self, level: log::Level) -> Result<()> { 35 + match level { 36 + log::Level::Error => Err(anyhow!("preprocessor has errors")), 37 + log::Level::Warn => match self { 38 + Self::AlwaysFail => { 39 + anyhow!("treating warnings as errors because fail-on-unresolved is \"always\"") 40 + .context("preprocessor has errors") 41 + .pipe(Err) 42 + } 43 + Self::FailInCi => { 44 + let Some(ci) = Self::warning_as_error() else { 45 + return Ok(()); 46 + }; 47 + anyhow!("treating warnings as errors because fail-on-unresolved is \"ci\" and CI={ci}") 48 + .context("preprocessor has errors") 49 + .pipe(Err) 50 + } 51 + }, 52 + _ => Ok(()), 53 + } 54 + } 55 + 56 + pub fn adjusted<T, E>(&self, result: Result<Result<T, E>, E>) -> Result<Result<T, E>, E> { 57 + match result { 58 + Ok(Err(error)) if Self::warning_as_error().is_some() => Err(error), 59 + result => result, 60 + } 61 + } 62 + 63 + fn warning_as_error() -> Option<String> { 64 + is_ci() 65 + } 66 + }
+4 -4
crates/mdbookkit/src/lib.rs
··· 8 8 //! 9 9 //! [preprocessors]: https://rust-lang.github.io/mdBook/format/configuration/preprocessors.html 10 10 11 - #[cfg(feature = "common-logger")] 11 + pub mod book; 12 12 pub mod diagnostics; 13 - pub mod env; 13 + pub mod error; 14 14 pub mod logging; 15 15 pub mod markdown; 16 - 17 - pub mod bin; 16 + #[cfg(feature = "_testing")] 17 + pub mod testing;
+328 -35
crates/mdbookkit/src/logging.rs
··· 1 1 //! Progress reporting and logging for preprocessors. 2 2 3 - use std::{fmt, sync::mpsc}; 3 + use std::{ 4 + collections::BTreeSet, 5 + fmt, io, 6 + sync::{OnceLock, mpsc}, 7 + thread, 8 + time::{Duration, Instant}, 9 + }; 10 + 11 + use anyhow::{Context, Result}; 12 + use console::{StyledObject, Term, colors_enabled_stderr, set_colors_enabled}; 13 + use env_logger::Logger; 14 + use indicatif::{HumanDuration, ProgressBar, ProgressDrawTarget, ProgressStyle}; 15 + use log::{Level, LevelFilter, Log}; 16 + use tap::{Pipe, Tap}; 4 17 5 18 pub fn spinner() -> SpinnerHandle { 6 19 SpinnerHandle ··· 19 32 let prefix = prefix.into(); 20 33 let msg = Message::Create { prefix, total }; 21 34 22 - #[cfg(feature = "common-logger")] 23 - if let Some(terminal::Spinner { tx, .. }) = terminal::SPINNER.get() { 35 + if let Some(Spinner { tx, .. }) = SPINNER.get() { 24 36 tx.send(msg).ok(); 25 37 } else { 26 38 spinner_log!(info!("{msg}")); 27 39 } 28 40 29 - #[cfg(not(feature = "common-logger"))] 30 - spinner_log!(info!("{msg}")); 31 - 32 41 self 33 42 } 34 43 ··· 37 46 let update = update.to_string(); 38 47 let msg = Message::Update { key, update }; 39 48 40 - #[cfg(feature = "common-logger")] 41 - if let Some(terminal::Spinner { tx, .. }) = terminal::SPINNER.get() { 49 + if let Some(Spinner { tx, .. }) = SPINNER.get() { 42 50 tx.send(msg).ok(); 43 51 } else { 44 52 spinner_log!(info!("{msg}")); 45 53 } 46 - 47 - #[cfg(not(feature = "common-logger"))] 48 - spinner_log!(info!("{msg}")); 49 54 50 55 self 51 56 } ··· 60 65 }; 61 66 let done = Some(Message::Done { key, task }); 62 67 63 - #[cfg(feature = "common-logger")] 64 - if let Some(terminal::Spinner { tx, .. }) = terminal::SPINNER.get() { 68 + if let Some(Spinner { tx, .. }) = SPINNER.get() { 65 69 tx.send(open).ok(); 66 70 let spin = Some(tx.clone()); 67 71 return TaskHandle { spin, done }; ··· 77 81 let update = update.to_string(); 78 82 let msg = Message::Finish { key, update }; 79 83 80 - #[cfg(feature = "common-logger")] 81 - if let Some(terminal::Spinner { tx, .. }) = terminal::SPINNER.get() { 84 + if let Some(Spinner { tx, .. }) = SPINNER.get() { 82 85 tx.send(msg).ok(); 83 86 } else { 84 87 spinner_log!(info!("{msg}")); 85 88 } 86 - 87 - #[cfg(not(feature = "common-logger"))] 88 - spinner_log!(info!("{msg}")); 89 89 } 90 90 } 91 91 ··· 107 107 } 108 108 109 109 #[derive(Debug)] 110 - #[cfg_attr(not(feature = "common-logger"), allow(unused))] 111 110 enum Message { 112 111 Create { prefix: String, total: Option<u64> }, 113 112 Update { key: String, update: String }, ··· 149 148 } 150 149 } 151 150 152 - #[cfg(feature = "common-logger")] 153 151 pub fn styled<D>(val: D) -> console::StyledObject<D> { 154 - if let Some(terminal::Spinner { term, .. }) = terminal::SPINNER.get() { 152 + if let Some(Spinner { term, .. }) = SPINNER.get() { 155 153 term.style() 156 154 } else { 157 155 console::Style::new().for_stderr() ··· 162 160 #[macro_export] 163 161 macro_rules! styled { 164 162 ( ( $($display:tt)+ ) . $($style:tt)+ ) => {{ 165 - #[cfg(feature = "common-logger")] 166 - { 167 - $crate::logging::styled( $($display)* ) . $($style)* 168 - } 169 - #[cfg(not(feature = "common-logger"))] 170 - { 171 - $($display)* 172 - } 163 + $crate::logging::styled( $($display)* ) . $($style)* 173 164 }}; 174 165 } 175 166 176 - #[cfg(feature = "common-logger")] 177 167 pub fn is_logging() -> bool { 178 - terminal::SPINNER.get().is_none() 168 + SPINNER.get().is_none() 179 169 } 180 170 181 171 #[macro_export] ··· 208 198 }; 209 199 } 210 200 211 - #[cfg(feature = "common-logger")] 212 - mod terminal; 201 + /// Either a [`console::Term`] or an [`env_logger::Logger`]. 202 + /// 203 + /// This is automatically detected upon installation as the global logger. The logic is: 204 + /// 205 + /// - If the `RUST_LOG` env var is set, this will use [`env_logger`]. 206 + /// - If stderr is not "user-attended", as determined by [`console::user_attended_stderr()`], 207 + /// like if stderr is piped to a file, this will use [`env_logger`]. 208 + /// - Otherwise, this will use [`console`]. 209 + /// 210 + /// When this is a [`console::Term`], logs are handled by the global [`indicatif`] spinner. 211 + /// 212 + /// When this is an [`env_logger::Logger`], there will not be a spinner, and progress 213 + /// reports are printed as logs instead. 214 + pub enum ConsoleLogger { 215 + Console(Term), 216 + Logger(Logger), 217 + } 218 + 219 + impl ConsoleLogger { 220 + /// Install a [`ConsoleLogger`] as the global [`log`] logger. 221 + pub fn install(name: &str) { 222 + Self::try_install(name).expect("logger should not have been set"); 223 + } 224 + 225 + pub fn try_install(name: &str) -> Result<()> { 226 + log::set_boxed_logger(Box::new(Self::new(name)))?; 227 + log::set_max_level(LevelFilter::max()); 228 + Ok(()) 229 + } 230 + 231 + fn new(name: &str) -> Self { 232 + match maybe_logging() { 233 + Some(LevelFilter::Off) => SPINNER 234 + .get_or_init(|| spawn_spinner(name)) 235 + .term 236 + .clone() 237 + .pipe(Self::Console), 238 + level => env_logger::Builder::new() 239 + .format(log_format) 240 + .parse_default_env() 241 + .tap_mut(|builder| { 242 + if let Some(level) = level { 243 + builder.filter_level(level); 244 + } 245 + }) 246 + .build() 247 + .pipe(Self::Logger), 248 + } 249 + } 250 + } 251 + 252 + fn maybe_logging() -> Option<LevelFilter> { 253 + if std::env::var("RUST_LOG") 254 + .map(|v| !v.is_empty()) 255 + .unwrap_or(false) 256 + { 257 + // RUST_LOG to be parsed by env_logger 258 + None 259 + } else if !console::user_attended_stderr() { 260 + // RUST_LOG not set but stderr isn't a terminal 261 + // log info and above 262 + Some(LevelFilter::Info) 263 + } else { 264 + // use spinner instead 265 + Some(LevelFilter::Off) 266 + } 267 + } 268 + 269 + impl Log for ConsoleLogger { 270 + fn enabled(&self, metadata: &log::Metadata) -> bool { 271 + match self { 272 + ConsoleLogger::Logger(logger) => logger.enabled(metadata), 273 + ConsoleLogger::Console(_) => { 274 + if metadata.target().starts_with(env!("CARGO_CRATE_NAME")) { 275 + metadata.level() <= Level::Info 276 + } else { 277 + metadata.level() <= Level::Warn 278 + } 279 + } 280 + } 281 + } 282 + 283 + fn log(&self, record: &log::Record) { 284 + match self { 285 + ConsoleLogger::Logger(logger) => logger.log(record), 286 + ConsoleLogger::Console(term) => { 287 + if !self.enabled(record.metadata()) { 288 + return; 289 + } 290 + let Ok(message) = Vec::<u8>::new() 291 + .pipe(|mut buf| log_format(&mut buf, record).and(Ok(buf))) 292 + .context("failed to emit log message") 293 + .and_then(|buf| Ok(String::from_utf8(buf)?)) 294 + else { 295 + return; 296 + }; 297 + let message = styled_log(message.trim_end(), record); 298 + term.write_line(&message.to_string()).ok(); 299 + } 300 + } 301 + } 302 + 303 + fn flush(&self) { 304 + match self { 305 + ConsoleLogger::Console(term) => { 306 + term.flush().ok(); 307 + } 308 + ConsoleLogger::Logger(logger) => { 309 + logger.flush(); 310 + } 311 + } 312 + } 313 + } 314 + 315 + pub static SPINNER: OnceLock<Spinner> = OnceLock::new(); 316 + 317 + pub struct Spinner { 318 + tx: mpsc::Sender<Message>, 319 + term: Term, 320 + } 321 + 322 + fn spawn_spinner(name: &str) -> Spinner { 323 + // https://github.com/console-rs/indicatif/issues/698 324 + set_colors_enabled(colors_enabled_stderr()); 325 + 326 + let (tx, rx) = mpsc::channel(); 327 + 328 + let term = Term::stderr(); 329 + 330 + let target = term.clone(); 331 + let template = format!("{{spinner:.cyan}} [{name}] {{prefix}} ... {{msg}}",); 332 + 333 + // this thread is detached. this is okay in usage because SPINNER.get_or_init 334 + // guarantees this function is called at most once 335 + 336 + thread::spawn(move || { 337 + struct Bar { 338 + prefix: String, 339 + bar: ProgressBar, 340 + } 341 + 342 + let mut current: Option<Bar> = None; 343 + 344 + let mut tasks = BTreeSet::<String>::new(); 345 + let mut task_idx = 0; 346 + let mut interval = Instant::now(); 347 + 348 + loop { 349 + match rx.recv_timeout(Duration::from_millis(100)) { 350 + Err(mpsc::RecvTimeoutError::Timeout) => {} 351 + 352 + Err(mpsc::RecvTimeoutError::Disconnected) => break, 353 + 354 + Ok(Message::Create { prefix, total }) => { 355 + if let Some(bar) = current { 356 + bar.bar.abandon() 357 + } 358 + 359 + let style = ProgressStyle::with_template(&template) 360 + .unwrap() 361 + .tick_chars("⠇⠋⠙⠸⠴⠦⠿"); 362 + 363 + let bar = ProgressDrawTarget::term(target.clone(), 20) 364 + .pipe(|target| ProgressBar::with_draw_target(total, target)) 365 + .with_prefix(prefix.clone()) 366 + .with_style(style); 367 + 368 + bar.enable_steady_tick(Duration::from_millis(100)); 369 + 370 + current = Some(Bar { prefix, bar }); 371 + } 372 + 373 + Ok(Message::Update { key, update }) => { 374 + let Some(Bar { 375 + ref bar, 376 + ref prefix, 377 + }) = current 378 + else { 379 + continue; 380 + }; 381 + 382 + if &key != prefix { 383 + continue; 384 + } 385 + 386 + bar.set_message(update); 387 + bar.tick(); 388 + } 389 + 390 + Ok(Message::Finish { key, update }) => { 391 + let Some(Bar { 392 + ref bar, 393 + ref prefix, 394 + }) = current 395 + else { 396 + continue; 397 + }; 398 + 399 + if &key != prefix { 400 + continue; 401 + } 402 + 403 + bar.finish_with_message(update); 404 + current = None; 405 + } 406 + 407 + Ok(Message::Task { key, task }) => { 408 + let Some(Bar { 409 + ref bar, 410 + ref prefix, 411 + }) = current 412 + else { 413 + continue; 414 + }; 415 + 416 + if &key != prefix { 417 + continue; 418 + } 419 + 420 + if let Some(length) = bar.length() { 421 + let counter = styled(format!("({}/{length})", bar.position())).dim(); 422 + bar.set_prefix(format!("{prefix} {counter}")) 423 + } 424 + 425 + bar.set_message(styled(&task).magenta().to_string()); 426 + bar.tick(); 427 + 428 + tasks.insert(task); 429 + interval = Instant::now(); 430 + } 431 + 432 + Ok(Message::Done { key, task }) => { 433 + let Some(Bar { 434 + ref bar, 435 + ref prefix, 436 + }) = current 437 + else { 438 + continue; 439 + }; 440 + 441 + if &key != prefix { 442 + continue; 443 + } 444 + 445 + bar.inc(1); 446 + 447 + if let Some(length) = bar.length() { 448 + let counter = styled(format!("({}/{length})", bar.position())).dim(); 449 + bar.set_prefix(format!("{prefix} {counter}")) 450 + } 213 451 214 - #[cfg(feature = "common-logger")] 215 - pub use self::terminal::ConsoleLogger; 452 + bar.set_message(styled(&task).green().to_string()); 453 + bar.tick(); 454 + 455 + tasks.insert(task); 456 + interval = Instant::now(); 457 + } 458 + } 459 + 460 + if let Some(Bar { 461 + ref prefix, 462 + ref bar, 463 + }) = current 464 + { 465 + let now = Instant::now(); 466 + 467 + if now - interval > Duration::from_secs(10) { 468 + interval = now; 469 + if task_idx >= tasks.len() { 470 + task_idx = 0 471 + } 472 + if let Some(task) = tasks.iter().nth(task_idx) { 473 + spinner_log!(warn!( 474 + "task {prefix} - {task} has been running for more than {}", 475 + HumanDuration(bar.elapsed()) 476 + )); 477 + bar.set_message(styled(task).magenta().to_string()); 478 + task_idx += 1; 479 + } 480 + } 481 + } 482 + } 483 + }); 484 + 485 + Spinner { tx, term } 486 + } 487 + 488 + /// <https://github.com/rust-lang/mdBook/blob/07b25cdb643899aeca2307fbab7690fa7eeec36b/src/main.rs#L100-L109> 489 + fn log_format<W: io::Write>(formatter: &mut W, record: &log::Record) -> io::Result<()> { 490 + let message = format!( 491 + "{} [{}] ({}): {}", 492 + chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), 493 + record.level(), 494 + record.target(), 495 + record.args() 496 + ); 497 + let message = styled_log(message, record); 498 + writeln!(formatter, "{message}",) 499 + } 500 + 501 + fn styled_log<D>(message: D, record: &log::Record) -> StyledObject<D> { 502 + match record.level() { 503 + Level::Warn => styled(message).yellow(), 504 + Level::Error => styled(message).red(), 505 + Level::Info => styled(message), 506 + _ => styled(message).dim(), 507 + } 508 + }
-325
crates/mdbookkit/src/logging/terminal.rs
··· 1 - use std::{ 2 - collections::BTreeSet, 3 - io, 4 - sync::{mpsc, OnceLock}, 5 - thread, 6 - time::{Duration, Instant}, 7 - }; 8 - 9 - use anyhow::{Context, Result}; 10 - use console::{colors_enabled_stderr, set_colors_enabled, StyledObject, Term}; 11 - use env_logger::Logger; 12 - use indicatif::{HumanDuration, ProgressBar, ProgressDrawTarget, ProgressStyle}; 13 - use log::{Level, LevelFilter, Log}; 14 - use tap::{Pipe, Tap}; 15 - 16 - use super::{styled, Message}; 17 - 18 - /// Either a [`console::Term`] or an [`env_logger::Logger`]. 19 - /// 20 - /// This is automatically detected upon installation as the global logger. The logic is: 21 - /// 22 - /// - If the `RUST_LOG` env var is set, this will use [`env_logger`]. 23 - /// - If stderr is not "user-attended", as determined by [`console::user_attended_stderr()`], 24 - /// like if stderr is piped to a file, this will use [`env_logger`]. 25 - /// - Otherwise, this will use [`console`]. 26 - /// 27 - /// When this is a [`console::Term`], logs are handled by the global [`indicatif`] spinner. 28 - /// 29 - /// When this is an [`env_logger::Logger`], there will not be a spinner, and progress 30 - /// reports are printed as logs instead. 31 - pub enum ConsoleLogger { 32 - Console(Term), 33 - Logger(Logger), 34 - } 35 - 36 - impl ConsoleLogger { 37 - /// Install a [`ConsoleLogger`] as the global [`log`] logger. 38 - pub fn install(name: &str) { 39 - Self::try_install(name).expect("logger should not have been set"); 40 - } 41 - 42 - pub fn try_install(name: &str) -> Result<()> { 43 - log::set_boxed_logger(Box::new(Self::new(name)))?; 44 - log::set_max_level(LevelFilter::max()); 45 - Ok(()) 46 - } 47 - 48 - fn new(name: &str) -> Self { 49 - match maybe_logging() { 50 - Some(LevelFilter::Off) => SPINNER 51 - .get_or_init(|| spawn_spinner(name)) 52 - .term 53 - .clone() 54 - .pipe(Self::Console), 55 - level => env_logger::Builder::new() 56 - .format(log_format) 57 - .parse_default_env() 58 - .tap_mut(|builder| { 59 - if let Some(level) = level { 60 - builder.filter_level(level); 61 - } 62 - }) 63 - .build() 64 - .pipe(Self::Logger), 65 - } 66 - } 67 - } 68 - 69 - fn maybe_logging() -> Option<LevelFilter> { 70 - if std::env::var("RUST_LOG") 71 - .map(|v| !v.is_empty()) 72 - .unwrap_or(false) 73 - { 74 - // RUST_LOG to be parsed by env_logger 75 - None 76 - } else if !console::user_attended_stderr() { 77 - // RUST_LOG not set but stderr isn't a terminal 78 - // log info and above 79 - Some(LevelFilter::Info) 80 - } else { 81 - // use spinner instead 82 - Some(LevelFilter::Off) 83 - } 84 - } 85 - 86 - impl Log for ConsoleLogger { 87 - fn enabled(&self, metadata: &log::Metadata) -> bool { 88 - match self { 89 - ConsoleLogger::Logger(logger) => logger.enabled(metadata), 90 - ConsoleLogger::Console(_) => { 91 - if metadata.target().starts_with(env!("CARGO_CRATE_NAME")) { 92 - metadata.level() <= Level::Info 93 - } else { 94 - metadata.level() <= Level::Warn 95 - } 96 - } 97 - } 98 - } 99 - 100 - fn log(&self, record: &log::Record) { 101 - match self { 102 - ConsoleLogger::Logger(logger) => logger.log(record), 103 - ConsoleLogger::Console(term) => { 104 - if !self.enabled(record.metadata()) { 105 - return; 106 - } 107 - let Ok(message) = Vec::<u8>::new() 108 - .pipe(|mut buf| log_format(&mut buf, record).and(Ok(buf))) 109 - .context("failed to emit log message") 110 - .and_then(|buf| Ok(String::from_utf8(buf)?)) 111 - else { 112 - return; 113 - }; 114 - let message = styled_log(message.trim_end(), record); 115 - term.write_line(&message.to_string()).ok(); 116 - } 117 - } 118 - } 119 - 120 - fn flush(&self) { 121 - match self { 122 - ConsoleLogger::Console(term) => { 123 - term.flush().ok(); 124 - } 125 - ConsoleLogger::Logger(logger) => { 126 - logger.flush(); 127 - } 128 - } 129 - } 130 - } 131 - 132 - pub static SPINNER: OnceLock<Spinner> = OnceLock::new(); 133 - 134 - pub struct Spinner { 135 - pub tx: mpsc::Sender<Message>, 136 - pub term: Term, 137 - } 138 - 139 - fn spawn_spinner(name: &str) -> Spinner { 140 - // https://github.com/console-rs/indicatif/issues/698 141 - set_colors_enabled(colors_enabled_stderr()); 142 - 143 - let (tx, rx) = mpsc::channel(); 144 - 145 - let term = Term::stderr(); 146 - 147 - let target = term.clone(); 148 - let template = format!("{{spinner:.cyan}} [{name}] {{prefix}} ... {{msg}}",); 149 - 150 - // this thread is detached. this is okay in usage because SPINNER.get_or_init 151 - // guarantees this function is called at most once 152 - 153 - thread::spawn(move || { 154 - struct Bar { 155 - prefix: String, 156 - bar: ProgressBar, 157 - } 158 - 159 - let mut current: Option<Bar> = None; 160 - 161 - let mut tasks = BTreeSet::<String>::new(); 162 - let mut task_idx = 0; 163 - let mut interval = Instant::now(); 164 - 165 - loop { 166 - match rx.recv_timeout(Duration::from_millis(100)) { 167 - Err(mpsc::RecvTimeoutError::Timeout) => {} 168 - 169 - Err(mpsc::RecvTimeoutError::Disconnected) => break, 170 - 171 - Ok(Message::Create { prefix, total }) => { 172 - if let Some(bar) = current { 173 - bar.bar.abandon() 174 - } 175 - 176 - let style = ProgressStyle::with_template(&template) 177 - .unwrap() 178 - .tick_chars("⠇⠋⠙⠸⠴⠦⠿"); 179 - 180 - let bar = ProgressDrawTarget::term(target.clone(), 20) 181 - .pipe(|target| ProgressBar::with_draw_target(total, target)) 182 - .with_prefix(prefix.clone()) 183 - .with_style(style); 184 - 185 - bar.enable_steady_tick(Duration::from_millis(100)); 186 - 187 - current = Some(Bar { prefix, bar }); 188 - } 189 - 190 - Ok(Message::Update { key, update }) => { 191 - let Some(Bar { 192 - ref bar, 193 - ref prefix, 194 - }) = current 195 - else { 196 - continue; 197 - }; 198 - 199 - if &key != prefix { 200 - continue; 201 - } 202 - 203 - bar.set_message(update); 204 - bar.tick(); 205 - } 206 - 207 - Ok(Message::Finish { key, update }) => { 208 - let Some(Bar { 209 - ref bar, 210 - ref prefix, 211 - }) = current 212 - else { 213 - continue; 214 - }; 215 - 216 - if &key != prefix { 217 - continue; 218 - } 219 - 220 - bar.finish_with_message(update); 221 - current = None; 222 - } 223 - 224 - Ok(Message::Task { key, task }) => { 225 - let Some(Bar { 226 - ref bar, 227 - ref prefix, 228 - }) = current 229 - else { 230 - continue; 231 - }; 232 - 233 - if &key != prefix { 234 - continue; 235 - } 236 - 237 - if let Some(length) = bar.length() { 238 - let counter = styled(format!("({}/{length})", bar.position())).dim(); 239 - bar.set_prefix(format!("{prefix} {counter}")) 240 - } 241 - 242 - bar.set_message(styled(&task).magenta().to_string()); 243 - bar.tick(); 244 - 245 - tasks.insert(task); 246 - interval = Instant::now(); 247 - } 248 - 249 - Ok(Message::Done { key, task }) => { 250 - let Some(Bar { 251 - ref bar, 252 - ref prefix, 253 - }) = current 254 - else { 255 - continue; 256 - }; 257 - 258 - if &key != prefix { 259 - continue; 260 - } 261 - 262 - bar.inc(1); 263 - 264 - if let Some(length) = bar.length() { 265 - let counter = styled(format!("({}/{length})", bar.position())).dim(); 266 - bar.set_prefix(format!("{prefix} {counter}")) 267 - } 268 - 269 - bar.set_message(styled(&task).green().to_string()); 270 - bar.tick(); 271 - 272 - tasks.insert(task); 273 - interval = Instant::now(); 274 - } 275 - } 276 - 277 - if let Some(Bar { 278 - ref prefix, 279 - ref bar, 280 - }) = current 281 - { 282 - let now = Instant::now(); 283 - 284 - if now - interval > Duration::from_secs(10) { 285 - interval = now; 286 - if task_idx >= tasks.len() { 287 - task_idx = 0 288 - } 289 - if let Some(task) = tasks.iter().nth(task_idx) { 290 - spinner_log!(warn!( 291 - "task {prefix} - {task} has been running for more than {}", 292 - HumanDuration(bar.elapsed()) 293 - )); 294 - bar.set_message(styled(task).magenta().to_string()); 295 - task_idx += 1; 296 - } 297 - } 298 - } 299 - } 300 - }); 301 - 302 - Spinner { tx, term } 303 - } 304 - 305 - /// <https://github.com/rust-lang/mdBook/blob/07b25cdb643899aeca2307fbab7690fa7eeec36b/src/main.rs#L100-L109> 306 - fn log_format<W: io::Write>(formatter: &mut W, record: &log::Record) -> io::Result<()> { 307 - let message = format!( 308 - "{} [{}] ({}): {}", 309 - chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), 310 - record.level(), 311 - record.target(), 312 - record.args() 313 - ); 314 - let message = styled_log(message, record); 315 - writeln!(formatter, "{message}",) 316 - } 317 - 318 - fn styled_log<D>(message: D, record: &log::Record) -> StyledObject<D> { 319 - match record.level() { 320 - Level::Warn => styled(message).yellow(), 321 - Level::Error => styled(message).red(), 322 - Level::Info => styled(message), 323 - _ => styled(message).dim(), 324 - } 325 - }
+2 -2
crates/mdbookkit/src/markdown.rs
··· 3 3 use std::{borrow::Cow, fmt::Write, ops::Range}; 4 4 5 5 use pulldown_cmark::{Event, Options}; 6 - use pulldown_cmark_to_cmark::{cmark, Error}; 6 + use pulldown_cmark_to_cmark::{Error, cmark}; 7 7 use tap::Pipe; 8 8 9 9 /// _Patch_ a Markdown string, instead of regenerating it entirely, in order to preserve ··· 102 102 } 103 103 104 104 /// <https://github.com/rust-lang/mdBook/blob/v0.4.47/src/utils/mod.rs#L197-L208> 105 - pub const fn mdbook_markdown() -> Options { 105 + pub const fn mdbook_markdown_options() -> Options { 106 106 Options::empty() 107 107 .union(Options::ENABLE_TABLES) 108 108 .union(Options::ENABLE_FOOTNOTES)
-277
crates/mdbookkit/tests/rustdoc_link.rs
··· 1 - use std::{io::Write, sync::Arc}; 2 - 3 - use anyhow::{bail, Context, Result}; 4 - 5 - use assert_cmd::{prelude::*, Command}; 6 - use predicates::prelude::*; 7 - use similar::{ChangeTag, TextDiff}; 8 - use tap::Pipe; 9 - use tempfile::TempDir; 10 - use tokio::task::JoinSet; 11 - 12 - use mdbookkit::bin::rustdoc_link::{ 13 - env::{find_code_extension, Config, Environment}, 14 - Client, Pages, Resolver, 15 - }; 16 - use util_testing::{may_skip, portable_snapshots, setup_paths, test_document, TestDocument}; 17 - 18 - mod util; 19 - 20 - async fn snapshot( 21 - client: Arc<Client>, 22 - TestDocument { source, name, .. }: TestDocument, 23 - ) -> Result<()> { 24 - let stream = client.env().markdown(source).into_offset_iter(); 25 - 26 - let mut page = Pages::one(source, stream)?; 27 - 28 - client.resolve(&mut page).await?; 29 - 30 - let output = page.get(&client.env().emit_config())?.to_string(); 31 - 32 - portable_snapshots!().test(|| insta::assert_snapshot!(name.clone(), output))?; 33 - 34 - assert_no_whitespace_change(source, &output)?; 35 - 36 - let report = page 37 - .reporter() 38 - .level(log::LevelFilter::Info) 39 - .names(|_| name.clone()) 40 - .colored(false) 41 - .logging(false) 42 - .build() 43 - .to_report(); 44 - 45 - portable_snapshots!().test(|| insta::assert_snapshot!(format!("{name}.stderr"), report))?; 46 - 47 - Ok(()) 48 - } 49 - 50 - fn assert_no_whitespace_change(source: &str, output: &str) -> Result<()> { 51 - let changed_lines = TextDiff::from_words(source, output) 52 - .iter_all_changes() 53 - .filter_map(|change| { 54 - if matches!(change.tag(), ChangeTag::Equal) { 55 - return None; 56 - } 57 - if change.value().contains('\n') { 58 - Some(change.value()) 59 - } else { 60 - None 61 - } 62 - }) 63 - .collect::<Vec<_>>(); 64 - 65 - if !changed_lines.is_empty() { 66 - bail!("unexpected whitespace change: {changed_lines:?}") 67 - } else { 68 - Ok(()) 69 - } 70 - } 71 - 72 - #[tokio::test] 73 - async fn test_snapshots() -> Result<()> { 74 - util::setup_logging(); 75 - 76 - let client = client()?; 77 - 78 - let tests = [ 79 - test_document!("../../../docs/src/rustdoc-link/supported-syntax.md"), 80 - test_document!("../../../docs/src/rustdoc-link/known-issues.md"), 81 - test_document!("../../../docs/src/rustdoc-link/getting-started.md"), 82 - test_document!("../../../docs/src/rustdoc-link/index.md"), 83 - test_document!("tests/ra-known-quirks.md"), 84 - ]; 85 - 86 - let errors = tests 87 - .map(|test| snapshot(client.clone(), test)) 88 - .into_iter() 89 - .collect::<JoinSet<_>>() 90 - .join_all() 91 - .await 92 - .into_iter() 93 - .filter_map(Result::err) 94 - .collect::<Vec<_>>(); 95 - 96 - if !errors.is_empty() { 97 - let errors = errors 98 - .iter() 99 - .map(|e| format!("{e:?}")) 100 - .collect::<Vec<_>>() 101 - .join("\n"); 102 - panic!("{errors}") 103 - } 104 - 105 - client.drop().await?; 106 - 107 - Ok(()) 108 - } 109 - 110 - fn client() -> Result<Arc<Client>> { 111 - Config { 112 - rust_analyzer: Some("cargo run --package util-rust-analyzer -- analyzer".into()), 113 - cargo_features: vec!["rustdoc-link".into()], 114 - ..Default::default() 115 - } 116 - .pipe(Environment::new)? 117 - .pipe(Client::new) 118 - .pipe(Arc::new) 119 - .pipe(Ok) 120 - } 121 - 122 - #[test] 123 - #[ignore = "should run in a dedicated environment"] 124 - fn test_minimum_env() -> Result<()> { 125 - util::setup_logging(); 126 - 127 - log::info!("setup: compile self"); 128 - Command::new("cargo") 129 - .args([ 130 - "build", 131 - "--package", 132 - env!("CARGO_PKG_NAME"), 133 - "--all-features", 134 - "--bin", 135 - "mdbook-rustdoc-link", 136 - ]) 137 - .arg(if cfg!(debug_assertions) { 138 - "--profile=dev" 139 - } else { 140 - "--profile=release" 141 - }) 142 - .assert() 143 - .success(); 144 - 145 - let path = setup_paths()?; 146 - 147 - let root = TempDir::new()?; 148 - 149 - log::debug!("{root:?}"); 150 - 151 - log::info!("given: a book"); 152 - Command::new("mdbook") 153 - .args(["init", "--force"]) 154 - .env("PATH", &path) 155 - .current_dir(&root) 156 - .unwrap() 157 - .assert() 158 - .success(); 159 - 160 - log::info!("given: preprocessor is enabled"); 161 - std::fs::File::options() 162 - .append(true) 163 - .open(root.path().join("book.toml"))? 164 - .pipe(|mut file| file.write_all("[preprocessor.rustdoc-link]\n".as_bytes()))?; 165 - 166 - log::info!("when: book is not a Cargo project"); 167 - log::info!("then: preprocessor fails"); 168 - Command::new("mdbook") 169 - .arg("build") 170 - .env("PATH", &path) 171 - .current_dir(&root) 172 - .assert() 173 - .failure() 174 - .stderr(predicate::str::contains( 175 - "failed to determine the current Cargo project", 176 - )); 177 - 178 - log::info!("given: book is a Cargo project"); 179 - Command::new("cargo") 180 - .arg("init") 181 - .args(["--name", "temp"]) 182 - .env("PATH", &path) 183 - .current_dir(&root) 184 - .assert() 185 - .success(); 186 - 187 - if find_code_extension().is_some() 188 - && may_skip("rust-analyzer code extension is already installed") 189 - { 190 - log::info!("when: book has item links"); 191 - std::fs::File::options() 192 - .append(true) 193 - .open(root.path().join("src/chapter_1.md"))? 194 - .pipe(|mut file| file.write_all("\n[std::thread]\n".as_bytes()))?; 195 - 196 - log::info!("then: book builds without errors"); 197 - Command::new("mdbook") 198 - .arg("build") 199 - .env("PATH", &path) 200 - .current_dir(&root) 201 - .assert() 202 - .success(); 203 - } else if Command::new("rust-analyzer") 204 - .arg("--version") 205 - .assert() 206 - .try_success() 207 - .is_ok() 208 - && may_skip("rust-analyzer is already available") 209 - { 210 - log::info!("skip testing mdbook build without rust-analyzer") 211 - } else { 212 - log::info!("when: rust-analyzer is not configured"); 213 - 214 - log::info!("when: book has no item links"); 215 - 216 - log::info!("then: book builds without errors"); 217 - Command::new("mdbook") 218 - .arg("build") 219 - .env("PATH", &path) 220 - .current_dir(&root) 221 - .assert() 222 - .success(); 223 - 224 - log::info!("when: book has item links"); 225 - std::fs::File::options() 226 - .append(true) 227 - .open(root.path().join("src/chapter_1.md"))? 228 - .pipe(|mut file| file.write_all("\n[std]\n".as_bytes()))?; 229 - 230 - log::info!("then: preprocessor fails"); 231 - Command::new("mdbook") 232 - .arg("build") 233 - .env("PATH", &path) 234 - .current_dir(&root) 235 - .assert() 236 - .failure() 237 - .stderr( 238 - predicate::str::contains("failed to spawn rust-analyzer") 239 - // https://github.com/rust-lang/rustup/issues/3846 240 - // rustup shims rust-analyzer when it's not installed 241 - .or(predicate::str::contains("Unknown binary 'rust-analyzer")), 242 - // ^ doesn't have a closing `'` because on windows it says 'rust-analyzer.exe' 243 - ); 244 - 245 - log::info!("when: code extension is installed"); 246 - 247 - let extension_dir = tempfile::Builder::new() 248 - .prefix(".vscode") 249 - .suffix("") 250 - .rand_bytes(0) 251 - .tempdir_in(dirs::home_dir().context("failed to get home dir")?)?; 252 - 253 - let ra_executable = extension_dir 254 - .path() 255 - .join("extensions/rust-lang.rust-analyzer-lorem-ipsum") 256 - .join("server/rust-analyzer"); 257 - 258 - Command::new("cargo") 259 - .args(["run", "--package", "util-rust-analyzer", "--"]) 260 - .arg("--ra-path") 261 - .arg(ra_executable) 262 - .arg("download") 263 - .unwrap() 264 - .assert() 265 - .success(); 266 - 267 - log::info!("then: book builds without errors"); 268 - Command::new("mdbook") 269 - .arg("build") 270 - .env("PATH", &path) 271 - .current_dir(&root) 272 - .assert() 273 - .success(); 274 - } 275 - 276 - Ok(()) 277 - }
crates/mdbookkit/tests/snaps/rustdoc_link/getting-started.snap crates/mdbook-rustdoc-link/src/tests/snaps/getting-started.snap
crates/mdbookkit/tests/snaps/rustdoc_link/getting-started.stderr.snap crates/mdbook-rustdoc-link/src/tests/snaps/getting-started.stderr.snap
crates/mdbookkit/tests/snaps/rustdoc_link/index.snap crates/mdbook-rustdoc-link/src/tests/snaps/index.snap
crates/mdbookkit/tests/snaps/rustdoc_link/index.stderr.snap crates/mdbook-rustdoc-link/src/tests/snaps/index.stderr.snap
crates/mdbookkit/tests/snaps/rustdoc_link/known-issues.snap crates/mdbook-rustdoc-link/src/tests/snaps/known-issues.snap
crates/mdbookkit/tests/snaps/rustdoc_link/known-issues.stderr.snap crates/mdbook-rustdoc-link/src/tests/snaps/known-issues.stderr.snap
crates/mdbookkit/tests/snaps/rustdoc_link/ra-known-quirks.snap crates/mdbook-rustdoc-link/src/tests/snaps/ra-known-quirks.snap
crates/mdbookkit/tests/snaps/rustdoc_link/ra-known-quirks.stderr.snap crates/mdbook-rustdoc-link/src/tests/snaps/ra-known-quirks.stderr.snap
crates/mdbookkit/tests/snaps/rustdoc_link/supported-syntax.snap crates/mdbook-rustdoc-link/src/tests/snaps/supported-syntax.snap
crates/mdbookkit/tests/snaps/rustdoc_link/supported-syntax.stderr.snap crates/mdbook-rustdoc-link/src/tests/snaps/supported-syntax.stderr.snap
crates/mdbookkit/tests/tests/ra-known-quirks.md crates/mdbook-rustdoc-link/src/tests/ra-known-quirks.md
-7
crates/mdbookkit/tests/util.rs
··· 1 - use log::LevelFilter; 2 - use mdbookkit::logging::ConsoleLogger; 3 - 4 - pub fn setup_logging() { 5 - ConsoleLogger::try_install(env!("CARGO_PKG_NAME")).ok(); 6 - log::set_max_level(LevelFilter::Debug); 7 - }
+1 -4
docs/Cargo.toml
··· 15 15 env_logger = { workspace = true } 16 16 gix-url = { version = "0.30.0" } 17 17 log = { workspace = true } 18 + mdbookkit = { workspace = true } 18 19 miette = { workspace = true } 19 20 serde = { workspace = true } 20 21 serde_json = { workspace = true } 21 22 shlex = { workspace = true } 22 23 tokio = { workspace = true } 23 - 24 - [dependencies.mdbookkit] 25 - features = ["lib-rustdoc-link"] 26 - path = "../crates/mdbookkit"
+1 -1
docs/src/lib.rs
··· 7 7 use mdbookkit::bin::rustdoc_link::Resolver; 8 8 9 9 mod env { 10 - pub use mdbookkit::{bin::rustdoc_link::env::Config, env::is_ci}; 10 + pub use mdbookkit::{bin::rustdoc_link::env::Config, error::is_ci}; 11 11 }
+1 -4
utils/clap-reflect/Cargo.toml
··· 13 13 anyhow = { workspace = true } 14 14 clap = { workspace = true } 15 15 mdbook = { workspace = true } 16 + mdbookkit = { workspace = true } 16 17 minijinja = { workspace = true } 17 18 serde = { workspace = true } 18 19 serde_json = { workspace = true } 19 20 tap = { workspace = true } 20 - 21 - [dependencies.mdbookkit] 22 - features = ["lib-rustdoc-link", "lib-link-forever", "common-cli"] 23 - path = "../../crates/mdbookkit"
-20
utils/testing/Cargo.toml
··· 1 - [package] 2 - name = "util-testing" 3 - version = "0.1.0" 4 - 5 - authors.workspace = true 6 - edition.workspace = true 7 - license.workspace = true 8 - publish.workspace = true 9 - repository.workspace = true 10 - 11 - [dependencies] 12 - anyhow = { workspace = true } 13 - cargo-run-bin = { workspace = true, default-features = false } 14 - insta = { workspace = true } 15 - log = { workspace = true } 16 - once_cell = "1.21.3" 17 - serde = { workspace = true } 18 - serde_json = { workspace = true } 19 - tap = { workspace = true } 20 - url = { workspace = true }
+82 -68
utils/testing/src/lib.rs crates/mdbookkit/src/testing.rs
··· 1 - //! Test helpers. 2 - 3 - use std::{ffi::OsString, path::Path}; 1 + use std::{ffi::OsString, path::Path, sync::LazyLock}; 4 2 5 3 use anyhow::Result; 6 - use once_cell::sync::Lazy; 4 + use log::LevelFilter; 7 5 use serde::Deserialize; 8 6 use tap::{Pipe, Tap}; 9 - pub use url; 10 7 use url::Url; 11 8 12 - pub static CARGO_WORKSPACE_DIR: Lazy<Url> = Lazy::new(|| { 13 - #[derive(Deserialize)] 14 - struct CargoManifest { 15 - workspace_root: String, 9 + use crate::logging::ConsoleLogger; 10 + 11 + #[derive(Debug, PartialEq, Eq, Hash)] 12 + pub struct TestDocument { 13 + pub source_path: &'static str, 14 + pub target_path: &'static str, 15 + pub content: &'static str, 16 + } 17 + 18 + #[macro_export] 19 + macro_rules! test_document { 20 + ($path:literal) => { 21 + $crate::testing::TestDocument { 22 + source_path: file!(), 23 + target_path: $path, 24 + content: include_str!($path), 25 + } 26 + }; 27 + } 28 + 29 + impl TestDocument { 30 + pub fn url(&self) -> Url { 31 + CARGO_WORKSPACE_DIR 32 + .join(self.source_path) 33 + .unwrap() 34 + .join(self.target_path) 35 + .unwrap() 36 + } 37 + 38 + pub fn name(&self) -> String { 39 + std::path::Path::new(self.target_path) 40 + .with_extension("") 41 + .file_name() 42 + .unwrap() 43 + .to_string_lossy() 44 + .into_owned() 16 45 } 17 - // https://github.com/mitsuhiko/insta/blob/b113499249584cb650150d2d01ed96ee66db6b30/src/runtime.rs#L67-L88 18 - std::process::Command::new(env!("CARGO")) 19 - .arg("metadata") 20 - .args(["--format-version=1", "--no-deps"]) 21 - .current_dir(env!("CARGO_MANIFEST_DIR")) 22 - .output() 23 - .unwrap() 24 - .pipe(|output| String::from_utf8(output.stdout)) 25 - .unwrap() 26 - .pipe(|output| serde_json::from_str::<CargoManifest>(&output)) 27 - .unwrap() 28 - .pipe(|manifest| Url::from_directory_path(manifest.workspace_root)) 29 - .unwrap() 30 - }); 46 + } 31 47 32 48 #[macro_export] 33 49 macro_rules! portable_snapshots { 34 50 () => { 35 - $crate::PortableSnapshots { 36 - manifest: env!("CARGO_MANIFEST_DIR"), 37 - module: module_path!(), 51 + $crate::testing::PortableSnapshots { 52 + file: std::path::Path::new(file!()), 38 53 } 39 54 }; 40 55 } ··· 42 57 #[derive(Debug)] 43 58 #[must_use] 44 59 pub struct PortableSnapshots { 45 - pub manifest: &'static str, 46 - pub module: &'static str, 60 + pub file: &'static Path, 47 61 } 48 62 49 63 impl PortableSnapshots { 50 64 pub fn test<T: FnOnce() -> R, R>(&self, cb: T) -> Result<R> { 51 - let Self { manifest, module } = self; 65 + let Self { file } = self; 52 66 53 - let snapshot_dir = Path::new(manifest).join("tests").join("snaps"); 54 - 55 - let path = module 56 - .split("::") 57 - .fold(snapshot_dir, |dir, path| dir.join(path)); 67 + let path = file.with_extension("").join("snaps"); 68 + let path = CARGO_WORKSPACE_DIR 69 + .join(&path.to_string_lossy()) 70 + .unwrap() 71 + .to_file_path() 72 + .unwrap(); 58 73 59 74 let result = insta::Settings::clone_current() 60 75 .tap_mut(|s| s.set_snapshot_path(path)) ··· 73 88 } 74 89 } 75 90 76 - pub struct TestDocument { 77 - pub source: &'static str, 78 - pub file: Url, 79 - pub name: String, 80 - } 81 - 82 - #[macro_export] 83 - macro_rules! test_document { 84 - ($path:literal) => { 85 - $crate::TestDocument { 86 - source: include_str!($path), 87 - file: $crate::CARGO_WORKSPACE_DIR 88 - .join(file!()) 89 - .unwrap() 90 - .join($path) 91 - .unwrap(), 92 - name: std::path::Path::new($path) 93 - .with_extension("") 94 - .file_name() 95 - .unwrap() 96 - .to_string_lossy() 97 - .into_owned(), 98 - } 99 - }; 100 - } 101 - 102 - pub fn may_skip<D: std::fmt::Display>(because: D) -> bool { 103 - let ci = std::env::var("CI").unwrap_or("".into()); 104 - if matches!(ci.as_str(), "" | "0" | "false") { 105 - log::info!("{because}"); 106 - true 107 - } else { 108 - panic!("{because} but CI={ci}") 109 - } 91 + pub fn setup_logging(name: &str) { 92 + ConsoleLogger::try_install(name).ok(); 93 + log::set_max_level(LevelFilter::Debug); 110 94 } 111 95 112 96 pub fn setup_paths() -> Result<OsString> { ··· 144 128 145 129 Ok(std::env::join_paths(path.into_iter().rev())?) 146 130 } 131 + 132 + pub static CARGO_WORKSPACE_DIR: LazyLock<Url> = LazyLock::new(|| { 133 + #[derive(Deserialize)] 134 + struct CargoManifest { 135 + workspace_root: String, 136 + } 137 + // https://github.com/mitsuhiko/insta/blob/b113499249584cb650150d2d01ed96ee66db6b30/src/runtime.rs#L67-L88 138 + std::process::Command::new(env!("CARGO")) 139 + .arg("metadata") 140 + .args(["--format-version=1", "--no-deps"]) 141 + .current_dir(env!("CARGO_MANIFEST_DIR")) 142 + .output() 143 + .unwrap() 144 + .pipe(|output| String::from_utf8(output.stdout)) 145 + .unwrap() 146 + .pipe(|output| serde_json::from_str::<CargoManifest>(&output)) 147 + .unwrap() 148 + .pipe(|manifest| Url::from_directory_path(manifest.workspace_root)) 149 + .unwrap() 150 + }); 151 + 152 + pub fn not_in_ci<D: std::fmt::Display>(because: D) -> bool { 153 + let ci = std::env::var("CI").unwrap_or("".into()); 154 + if matches!(ci.as_str(), "" | "0" | "false") { 155 + log::info!("{because}"); 156 + true 157 + } else { 158 + panic!("{because} but CI={ci}") 159 + } 160 + }