bunch more lexicon stuff and buildout

+3419 -195
+2
.gitignore
··· 10 10 11 11 .db/ 12 12 **/.db/ 13 + 14 + **/.claude/settings.local.json
+10
.zed/settings.json
··· 11 11 "command": ["alejandra", "--quiet", "--"] // or ["nixfmt"] 12 12 } 13 13 } 14 + }, 15 + "rust-analyzer": { 16 + "binary": { 17 + "path": "/nix/store/qs17clvcja2b5wy5wmcqd5mqly6wypax-rust-default-1.89.0-nightly-2025-05-21/bin/rust-analyzer" 18 + }, 19 + "initialization_options": { 20 + "cargo": { 21 + "allFeatures": true 22 + } 23 + } 14 24 } 15 25 } 16 26 }
+228 -81
Cargo.lock
··· 121 121 122 122 [[package]] 123 123 name = "anstyle-wincon" 124 - version = "3.0.7" 124 + version = "3.0.8" 125 125 source = "registry+https://github.com/rust-lang/crates.io-index" 126 - checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 126 + checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" 127 127 dependencies = [ 128 128 "anstyle", 129 - "once_cell", 129 + "once_cell_polyfill", 130 130 "windows-sys 0.59.0", 131 131 ] 132 132 ··· 185 185 186 186 [[package]] 187 187 name = "atrium-api" 188 - version = "0.25.3" 188 + version = "0.25.4" 189 189 source = "registry+https://github.com/rust-lang/crates.io-index" 190 - checksum = "7225f0ca3c78564b784828e3db3e92619cf6e786530c3468df73f49deebc0bd4" 190 + checksum = "46355d3245edc7b3160b2a45fe55d09a6963ebd3eee0252feb6b72fb0eb71463" 191 191 dependencies = [ 192 192 "atrium-common", 193 193 "atrium-xrpc", ··· 221 221 222 222 [[package]] 223 223 name = "atrium-identity" 224 - version = "0.1.4" 224 + version = "0.1.5" 225 225 source = "registry+https://github.com/rust-lang/crates.io-index" 226 - checksum = "84939a5e3a0a442467d6a000f586c092bb763a31c2d38ec1648f5179a720395a" 226 + checksum = "c9e2d42bb4dbea038f4f5f45e3af2a89d61a9894a75f06aa550b74a60d2be380" 227 227 dependencies = [ 228 228 "atrium-api", 229 229 "atrium-common", ··· 247 247 248 248 [[package]] 249 249 name = "atrium-oauth" 250 - version = "0.1.2" 250 + version = "0.1.3" 251 251 source = "registry+https://github.com/rust-lang/crates.io-index" 252 - checksum = "1f4f210da8f2d7199b15e6b02c0628175791ae5caaf506cfcafe45a20917f37c" 252 + checksum = "ca22dc4eaf77fd9bf050b21192ac58cd654a437d28e000ec114ebd93a51d36f5" 253 253 dependencies = [ 254 254 "atrium-api", 255 255 "atrium-common", ··· 422 422 ] 423 423 424 424 [[package]] 425 + name = "bit-set" 426 + version = "0.5.3" 427 + source = "registry+https://github.com/rust-lang/crates.io-index" 428 + checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" 429 + dependencies = [ 430 + "bit-vec", 431 + ] 432 + 433 + [[package]] 434 + name = "bit-vec" 435 + version = "0.6.3" 436 + source = "registry+https://github.com/rust-lang/crates.io-index" 437 + checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" 438 + 439 + [[package]] 425 440 name = "bitflags" 426 441 version = "1.3.2" 427 442 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 470 485 ] 471 486 472 487 [[package]] 488 + name = "bstr" 489 + version = "1.12.0" 490 + source = "registry+https://github.com/rust-lang/crates.io-index" 491 + checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" 492 + dependencies = [ 493 + "memchr", 494 + "serde", 495 + ] 496 + 497 + [[package]] 473 498 name = "buf_redux" 474 499 version = "0.8.4" 475 500 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 517 542 518 543 [[package]] 519 544 name = "cc" 520 - version = "1.2.23" 545 + version = "1.2.24" 521 546 source = "registry+https://github.com/rust-lang/crates.io-index" 522 - checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766" 547 + checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7" 523 548 dependencies = [ 524 549 "shlex", 525 550 ] ··· 573 598 574 599 [[package]] 575 600 name = "clap" 576 - version = "4.5.38" 601 + version = "4.5.39" 577 602 source = "registry+https://github.com/rust-lang/crates.io-index" 578 - checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" 603 + checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" 579 604 dependencies = [ 580 605 "clap_builder", 581 606 "clap_derive", ··· 583 608 584 609 [[package]] 585 610 name = "clap_builder" 586 - version = "4.5.38" 611 + version = "4.5.39" 587 612 source = "registry+https://github.com/rust-lang/crates.io-index" 588 - checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" 613 + checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" 589 614 dependencies = [ 590 615 "anstream", 591 616 "anstyle", ··· 682 707 683 708 [[package]] 684 709 name = "cordyceps" 685 - version = "0.3.3" 710 + version = "0.3.4" 686 711 source = "registry+https://github.com/rust-lang/crates.io-index" 687 - checksum = "a0392f465ceba1713d30708f61c160ebf4dc1cf86bb166039d16b11ad4f3b5b6" 712 + checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a" 688 713 dependencies = [ 689 714 "loom", 690 715 "tracing", ··· 739 764 source = "registry+https://github.com/rust-lang/crates.io-index" 740 765 checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" 741 766 dependencies = [ 767 + "crossbeam-utils", 768 + ] 769 + 770 + [[package]] 771 + name = "crossbeam-deque" 772 + version = "0.8.6" 773 + source = "registry+https://github.com/rust-lang/crates.io-index" 774 + checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 775 + dependencies = [ 776 + "crossbeam-epoch", 742 777 "crossbeam-utils", 743 778 ] 744 779 ··· 1086 1121 ] 1087 1122 1088 1123 [[package]] 1124 + name = "dynosaur" 1125 + version = "0.2.0" 1126 + source = "registry+https://github.com/rust-lang/crates.io-index" 1127 + checksum = "277b2cb52d2df4acece06bb16bc0bb0a006970c7bf504eac2d310927a6f65890" 1128 + dependencies = [ 1129 + "dynosaur_derive", 1130 + "trait-variant", 1131 + ] 1132 + 1133 + [[package]] 1134 + name = "dynosaur_derive" 1135 + version = "0.2.0" 1136 + source = "registry+https://github.com/rust-lang/crates.io-index" 1137 + checksum = "7a4102713839a8c01c77c165bc38ef2e83948f6397fa1e1dcfacec0f07b149d3" 1138 + dependencies = [ 1139 + "proc-macro2", 1140 + "quote", 1141 + "syn", 1142 + ] 1143 + 1144 + [[package]] 1089 1145 name = "ecdsa" 1090 1146 version = "0.16.9" 1091 1147 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1232 1288 checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" 1233 1289 1234 1290 [[package]] 1291 + name = "fancy-regex" 1292 + version = "0.11.0" 1293 + source = "registry+https://github.com/rust-lang/crates.io-index" 1294 + checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" 1295 + dependencies = [ 1296 + "bit-set", 1297 + "regex", 1298 + ] 1299 + 1300 + [[package]] 1235 1301 name = "fastrand" 1236 1302 version = "2.3.0" 1237 1303 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1485 1551 checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 1486 1552 1487 1553 [[package]] 1554 + name = "globset" 1555 + version = "0.4.16" 1556 + source = "registry+https://github.com/rust-lang/crates.io-index" 1557 + checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" 1558 + dependencies = [ 1559 + "aho-corasick", 1560 + "bstr", 1561 + "log", 1562 + "regex-automata 0.4.9", 1563 + "regex-syntax 0.8.5", 1564 + ] 1565 + 1566 + [[package]] 1488 1567 name = "group" 1489 1568 version = "0.13.0" 1490 1569 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1567 1646 ] 1568 1647 1569 1648 [[package]] 1649 + name = "hashlink" 1650 + version = "0.10.0" 1651 + source = "registry+https://github.com/rust-lang/crates.io-index" 1652 + checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" 1653 + dependencies = [ 1654 + "hashbrown 0.15.3", 1655 + ] 1656 + 1657 + [[package]] 1570 1658 name = "heck" 1571 1659 version = "0.4.1" 1572 1660 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1719 1807 1720 1808 [[package]] 1721 1809 name = "hyper-rustls" 1722 - version = "0.27.5" 1810 + version = "0.27.6" 1723 1811 source = "registry+https://github.com/rust-lang/crates.io-index" 1724 - checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" 1812 + checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d" 1725 1813 dependencies = [ 1726 - "futures-util", 1727 1814 "http", 1728 1815 "hyper", 1729 1816 "hyper-util", ··· 1732 1819 "tokio", 1733 1820 "tokio-rustls", 1734 1821 "tower-service", 1735 - "webpki-roots 0.26.11", 1822 + "webpki-roots", 1736 1823 ] 1737 1824 1738 1825 [[package]] ··· 1753 1840 1754 1841 [[package]] 1755 1842 name = "hyper-util" 1756 - version = "0.1.11" 1843 + version = "0.1.13" 1757 1844 source = "registry+https://github.com/rust-lang/crates.io-index" 1758 - checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" 1845 + checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" 1759 1846 dependencies = [ 1847 + "base64 0.22.1", 1760 1848 "bytes", 1761 1849 "futures-channel", 1850 + "futures-core", 1762 1851 "futures-util", 1763 1852 "http", 1764 1853 "http-body", 1765 1854 "hyper", 1855 + "ipnet", 1766 1856 "libc", 1857 + "percent-encoding", 1767 1858 "pin-project-lite", 1768 1859 "socket2", 1860 + "system-configuration", 1769 1861 "tokio", 1770 1862 "tower-service", 1771 1863 "tracing", 1864 + "windows-registry", 1772 1865 ] 1773 1866 1774 1867 [[package]] ··· 1844 1937 1845 1938 [[package]] 1846 1939 name = "icu_properties" 1847 - version = "2.0.0" 1940 + version = "2.0.1" 1848 1941 source = "registry+https://github.com/rust-lang/crates.io-index" 1849 - checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a" 1942 + checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 1850 1943 dependencies = [ 1851 1944 "displaydoc", 1852 1945 "icu_collections", ··· 1860 1953 1861 1954 [[package]] 1862 1955 name = "icu_properties_data" 1863 - version = "2.0.0" 1956 + version = "2.0.1" 1864 1957 source = "registry+https://github.com/rust-lang/crates.io-index" 1865 - checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04" 1958 + checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" 1866 1959 1867 1960 [[package]] 1868 1961 name = "icu_provider" ··· 1909 2002 ] 1910 2003 1911 2004 [[package]] 2005 + name = "ignore" 2006 + version = "0.4.23" 2007 + source = "registry+https://github.com/rust-lang/crates.io-index" 2008 + checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" 2009 + dependencies = [ 2010 + "crossbeam-deque", 2011 + "globset", 2012 + "log", 2013 + "memchr", 2014 + "regex-automata 0.4.9", 2015 + "same-file", 2016 + "walkdir", 2017 + "winapi-util", 2018 + ] 2019 + 2020 + [[package]] 1912 2021 name = "indexmap" 1913 2022 version = "1.9.3" 1914 2023 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1977 2086 version = "2.11.0" 1978 2087 source = "registry+https://github.com/rust-lang/crates.io-index" 1979 2088 checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 2089 + 2090 + [[package]] 2091 + name = "iri-string" 2092 + version = "0.7.8" 2093 + source = "registry+https://github.com/rust-lang/crates.io-index" 2094 + checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" 2095 + dependencies = [ 2096 + "memchr", 2097 + "serde", 2098 + ] 1980 2099 1981 2100 [[package]] 1982 2101 name = "is_ci" ··· 2159 2278 2160 2279 [[package]] 2161 2280 name = "libsqlite3-sys" 2162 - version = "0.30.1" 2281 + version = "0.33.0" 2163 2282 source = "registry+https://github.com/rust-lang/crates.io-index" 2164 - checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" 2283 + checksum = "947e6816f7825b2b45027c2c32e7085da9934defa535de4a6a46b10a4d5257fa" 2165 2284 dependencies = [ 2166 2285 "pkg-config", 2167 2286 "vcpkg", ··· 2241 2360 [[package]] 2242 2361 name = "markdown-weaver" 2243 2362 version = "0.13.0" 2244 - source = "git+https://github.com/rsform/markdown-weaver#593f968ec3c4515c23273715eaeb33801342439b" 2363 + source = "git+https://github.com/rsform/markdown-weaver#5a93522bc6b69058a34e7eb46f89b39d1b6be006" 2245 2364 dependencies = [ 2246 2365 "bitflags 2.9.1", 2247 2366 "getopts", ··· 2253 2372 [[package]] 2254 2373 name = "markdown-weaver-escape" 2255 2374 version = "0.11.0" 2256 - source = "git+https://github.com/rsform/markdown-weaver#593f968ec3c4515c23273715eaeb33801342439b" 2375 + source = "git+https://github.com/rsform/markdown-weaver#5a93522bc6b69058a34e7eb46f89b39d1b6be006" 2257 2376 2258 2377 [[package]] 2259 2378 name = "matchers" ··· 2324 2443 checksum = "7c36f61da594ecad0ed986ceeb5061eba47a36fcf839576ce525c7e4ff08f3fa" 2325 2444 dependencies = [ 2326 2445 "merde_core", 2327 - "yaml-rust2", 2446 + "yaml-rust2 0.8.1", 2328 2447 ] 2329 2448 2330 2449 [[package]] ··· 2425 2544 2426 2545 [[package]] 2427 2546 name = "mio" 2428 - version = "1.0.3" 2547 + version = "1.0.4" 2429 2548 source = "registry+https://github.com/rust-lang/crates.io-index" 2430 - checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 2549 + checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" 2431 2550 dependencies = [ 2432 2551 "libc", 2433 2552 "log", 2434 2553 "wasi 0.11.0+wasi-snapshot-preview1", 2435 - "windows-sys 0.52.0", 2554 + "windows-sys 0.59.0", 2436 2555 ] 2437 2556 2438 2557 [[package]] ··· 2658 2777 checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 2659 2778 2660 2779 [[package]] 2780 + name = "once_cell_polyfill" 2781 + version = "1.70.1" 2782 + source = "registry+https://github.com/rust-lang/crates.io-index" 2783 + checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 2784 + 2785 + [[package]] 2661 2786 name = "onig" 2662 - version = "6.4.0" 2787 + version = "6.5.1" 2663 2788 source = "registry+https://github.com/rust-lang/crates.io-index" 2664 - checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f" 2789 + checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" 2665 2790 dependencies = [ 2666 - "bitflags 1.3.2", 2791 + "bitflags 2.9.1", 2667 2792 "libc", 2668 2793 "once_cell", 2669 2794 "onig_sys", ··· 2671 2796 2672 2797 [[package]] 2673 2798 name = "onig_sys" 2674 - version = "69.8.1" 2799 + version = "69.9.1" 2675 2800 source = "registry+https://github.com/rust-lang/crates.io-index" 2676 - checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7" 2801 + checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" 2677 2802 dependencies = [ 2678 2803 "cc", 2679 2804 "pkg-config", ··· 2811 2936 "smallvec", 2812 2937 "windows-targets 0.52.6", 2813 2938 ] 2939 + 2940 + [[package]] 2941 + name = "pathdiff" 2942 + version = "0.2.3" 2943 + source = "registry+https://github.com/rust-lang/crates.io-index" 2944 + checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" 2814 2945 2815 2946 [[package]] 2816 2947 name = "pem-rfc7468" ··· 3244 3375 3245 3376 [[package]] 3246 3377 name = "reqwest" 3247 - version = "0.12.15" 3378 + version = "0.12.16" 3248 3379 source = "registry+https://github.com/rust-lang/crates.io-index" 3249 - checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" 3380 + checksum = "2bf597b113be201cb2269b4c39b39a804d01b99ee95a4278f0ed04e45cff1c71" 3250 3381 dependencies = [ 3251 3382 "async-compression", 3252 3383 "base64 0.22.1", ··· 3272 3403 "pin-project-lite", 3273 3404 "quinn", 3274 3405 "rustls 0.23.27", 3275 - "rustls-pemfile 2.2.0", 3276 3406 "rustls-pki-types", 3277 3407 "serde", 3278 3408 "serde_json", 3279 3409 "serde_urlencoded", 3280 3410 "sync_wrapper", 3281 - "system-configuration", 3282 3411 "tokio", 3283 3412 "tokio-native-tls", 3284 3413 "tokio-rustls", 3285 3414 "tokio-util", 3286 3415 "tower", 3416 + "tower-http", 3287 3417 "tower-service", 3288 3418 "url", 3289 3419 "wasm-bindgen", 3290 3420 "wasm-bindgen-futures", 3291 3421 "web-sys", 3292 - "webpki-roots 0.26.11", 3293 - "windows-registry", 3422 + "webpki-roots", 3294 3423 ] 3295 3424 3296 3425 [[package]] ··· 3452 3581 ] 3453 3582 3454 3583 [[package]] 3455 - name = "rustls-pemfile" 3456 - version = "2.2.0" 3457 - source = "registry+https://github.com/rust-lang/crates.io-index" 3458 - checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" 3459 - dependencies = [ 3460 - "rustls-pki-types", 3461 - ] 3462 - 3463 - [[package]] 3464 3584 name = "rustls-pki-types" 3465 3585 version = "1.12.0" 3466 3586 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3483 3603 3484 3604 [[package]] 3485 3605 name = "rustversion" 3486 - version = "1.0.20" 3606 + version = "1.0.21" 3487 3607 source = "registry+https://github.com/rust-lang/crates.io-index" 3488 - checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 3608 + checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" 3489 3609 3490 3610 [[package]] 3491 3611 name = "ryu" ··· 3849 3969 3850 3970 [[package]] 3851 3971 name = "socket2" 3852 - version = "0.5.9" 3972 + version = "0.5.10" 3853 3973 source = "registry+https://github.com/rust-lang/crates.io-index" 3854 - checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" 3974 + checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" 3855 3975 dependencies = [ 3856 3976 "libc", 3857 3977 "windows-sys 0.52.0", ··· 3974 4094 dependencies = [ 3975 4095 "bincode", 3976 4096 "bitflags 1.3.2", 4097 + "fancy-regex", 3977 4098 "flate2", 3978 4099 "fnv", 3979 4100 "once_cell", ··· 4151 4272 "httpdate", 4152 4273 "log", 4153 4274 "rustls 0.20.9", 4154 - "rustls-pemfile 0.2.1", 4275 + "rustls-pemfile", 4155 4276 "zeroize", 4156 4277 ] 4157 4278 ··· 4182 4303 4183 4304 [[package]] 4184 4305 name = "tokio" 4185 - version = "1.45.0" 4306 + version = "1.45.1" 4186 4307 source = "registry+https://github.com/rust-lang/crates.io-index" 4187 - checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" 4308 + checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" 4188 4309 dependencies = [ 4189 4310 "backtrace", 4190 4311 "bytes", ··· 4358 4479 "http-body-util", 4359 4480 "http-range-header", 4360 4481 "httpdate", 4482 + "iri-string", 4361 4483 "mime", 4362 4484 "mime_guess", 4363 4485 "percent-encoding", 4364 4486 "pin-project-lite", 4365 4487 "tokio", 4366 4488 "tokio-util", 4489 + "tower", 4367 4490 "tower-layer", 4368 4491 "tower-service", 4369 4492 "tracing", ··· 4602 4725 4603 4726 [[package]] 4604 4727 name = "uuid" 4605 - version = "1.16.0" 4728 + version = "1.17.0" 4606 4729 source = "registry+https://github.com/rust-lang/crates.io-index" 4607 - checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" 4730 + checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" 4608 4731 dependencies = [ 4609 4732 "getrandom 0.3.3", 4733 + "js-sys", 4610 4734 "serde", 4735 + "wasm-bindgen", 4611 4736 ] 4612 4737 4613 4738 [[package]] ··· 4818 4943 "http", 4819 4944 "jose-jwk", 4820 4945 "markdown-weaver", 4946 + "markdown-weaver-escape", 4821 4947 "merde", 4822 4948 "miette", 4823 4949 "minijinja", 4824 4950 "multibase", 4825 4951 "n0-future", 4826 4952 "owo-colors", 4953 + "regex", 4827 4954 "reqwest", 4828 4955 "serde", 4829 4956 "serde_bytes", ··· 4844 4971 name = "weaver-renderer" 4845 4972 version = "0.1.0" 4846 4973 dependencies = [ 4974 + "async-trait", 4847 4975 "atrium-api", 4976 + "bitflags 2.9.1", 4848 4977 "compact_string", 4978 + "dashmap", 4979 + "dynosaur", 4849 4980 "http", 4981 + "ignore", 4982 + "markdown-weaver", 4983 + "markdown-weaver-escape", 4984 + "miette", 4850 4985 "n0-future", 4986 + "pathdiff", 4987 + "pin-project", 4988 + "pin-utils", 4989 + "regex", 4990 + "syntect", 4991 + "thiserror 2.0.12", 4992 + "tokio", 4993 + "tokio-util", 4994 + "unicode-normalization", 4851 4995 "url", 4852 4996 "weaver-common", 4853 4997 "weaver-workspace-hack", 4998 + "yaml-rust2 0.10.2", 4854 4999 ] 4855 5000 4856 5001 [[package]] ··· 4950 5095 4951 5096 [[package]] 4952 5097 name = "webpki-roots" 4953 - version = "0.26.11" 4954 - source = "registry+https://github.com/rust-lang/crates.io-index" 4955 - checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" 4956 - dependencies = [ 4957 - "webpki-roots 1.0.0", 4958 - ] 4959 - 4960 - [[package]] 4961 - name = "webpki-roots" 4962 5098 version = "1.0.0" 4963 5099 source = "registry+https://github.com/rust-lang/crates.io-index" 4964 5100 checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" ··· 5038 5174 5039 5175 [[package]] 5040 5176 name = "windows-core" 5041 - version = "0.61.1" 5177 + version = "0.61.2" 5042 5178 source = "registry+https://github.com/rust-lang/crates.io-index" 5043 - checksum = "46ec44dc15085cea82cf9c78f85a9114c463a369786585ad2882d1ff0b0acf40" 5179 + checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" 5044 5180 dependencies = [ 5045 5181 "windows-implement", 5046 5182 "windows-interface", 5047 5183 "windows-link", 5048 5184 "windows-result", 5049 - "windows-strings 0.4.1", 5185 + "windows-strings 0.4.2", 5050 5186 ] 5051 5187 5052 5188 [[package]] ··· 5111 5247 5112 5248 [[package]] 5113 5249 name = "windows-result" 5114 - version = "0.3.3" 5250 + version = "0.3.4" 5115 5251 source = "registry+https://github.com/rust-lang/crates.io-index" 5116 - checksum = "4b895b5356fc36103d0f64dd1e94dfa7ac5633f1c9dd6e80fe9ec4adef69e09d" 5252 + checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" 5117 5253 dependencies = [ 5118 5254 "windows-link", 5119 5255 ] ··· 5129 5265 5130 5266 [[package]] 5131 5267 name = "windows-strings" 5132 - version = "0.4.1" 5268 + version = "0.4.2" 5133 5269 source = "registry+https://github.com/rust-lang/crates.io-index" 5134 - checksum = "2a7ab927b2637c19b3dbe0965e75d8f2d30bdd697a1516191cad2ec4df8fb28a" 5270 + checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 5135 5271 dependencies = [ 5136 5272 "windows-link", 5137 5273 ] ··· 5407 5543 checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" 5408 5544 dependencies = [ 5409 5545 "arraydeque", 5410 - "hashlink", 5546 + "hashlink 0.8.4", 5547 + ] 5548 + 5549 + [[package]] 5550 + name = "yaml-rust2" 5551 + version = "0.10.2" 5552 + source = "registry+https://github.com/rust-lang/crates.io-index" 5553 + checksum = "18b783b2c2789414f8bb84ca3318fc9c2d7e7be1c22907d37839a58dedb369d3" 5554 + dependencies = [ 5555 + "arraydeque", 5556 + "encoding_rs", 5557 + "hashlink 0.10.0", 5411 5558 ] 5412 5559 5413 5560 [[package]]
+2 -1
Cargo.toml
··· 30 30 miette = { version = "7.6" } 31 31 owo-colors = { version = "4.2.0" } 32 32 thiserror = "2.0" 33 - syntect = "5.2.0" 33 + syntect = { version = "5.2.0", default-features = false } 34 34 jane-eyre = "0.6.12" 35 35 n0-future = "=0.1.3" 36 36 tracing = { version = "0.1.41", default-features = false, features = ["std"] } ··· 38 38 "serde-codec", 39 39 ] } 40 40 markdown-weaver = { git = "https://github.com/rsform/markdown-weaver" } 41 + markdown-weaver-escape = { git = "https://github.com/rsform/markdown-weaver" } 41 42 42 43 esquema-codegen = { git = "https://github.com/fatfingers23/esquema.git", branch = "main" } 43 44 atrium-lex = { git = "https://github.com/sugyan/atrium.git", rev = "f162f815a04b5ecb0421b390d521c883c41d5f75" }
+2
crates/weaver-common/Cargo.toml
··· 60 60 tower-layer = "0.3.3" 61 61 multibase = "0.9.1" 62 62 dirs = "6.0.0" 63 + regex = "1.11.1" 64 + markdown-weaver-escape = { workspace = true, features = ["std"] } 63 65 64 66 65 67
+74 -4
crates/weaver-common/src/agent.rs
··· 3 3 use crate::resolver::HickoryDnsTxtResolver; 4 4 use crate::sh::weaver::actor::defs::ProfileDataViewInnerRefs; 5 5 use atrium_api::agent::{CloneWithProxy, Configure}; 6 - use atrium_api::types::string::{Cid, Did, Handle, Nsid, RecordKey}; 7 - use atrium_api::types::{Collection, Union, Unknown}; 6 + use atrium_api::types::string::{Cid, Did, Handle, Nsid, RecordKey, Tid}; 7 + use atrium_api::types::{BlobRef, Collection, LimitedU32, TryIntoUnknown, Union, Unknown}; 8 8 use atrium_api::{agent::SessionManager, types::string::AtIdentifier}; 9 9 use atrium_common::resolver::Resolver; 10 10 use atrium_identity::did::{CommonDidResolver, CommonDidResolverConfig, DEFAULT_PLC_DIRECTORY_URL}; ··· 60 60 AtprotoHandleResolver<HickoryDnsTxtResolver, WeaverHttpClient>, 61 61 >, 62 62 >, 63 + pub client: Arc<WeaverHttpClient>, 63 64 pub api: Service<Wrapper<M>>, 64 65 } 65 66 ··· 86 87 Self { 87 88 session_manager, 88 89 resolver: Arc::new(IdentityResolver::new(resolver_config)), 90 + client: Arc::clone(&http_client), 89 91 api, 90 92 } 91 93 } ··· 94 96 95 97 pub async fn did(&self) -> Option<Did> { 96 98 self.session_manager.did().await 99 + } 100 + 101 + pub fn pds(&self) -> String { 102 + self.session_manager.base_uri() 97 103 } 98 104 } 99 105 ··· 173 179 } 174 180 } 175 181 } 182 + pub async fn create_record( 183 + &self, 184 + collection: Nsid, 185 + record: Unknown, 186 + repo: AtIdentifier, 187 + rkey: Option<RecordKey>, 188 + ) -> Result<crate::com::atproto::repo::create_record::OutputData, Error> { 189 + use crate::com::atproto::repo::create_record::*; 190 + let result = self 191 + .api 192 + .com 193 + .atproto 194 + .repo 195 + .create_record( 196 + InputData { 197 + collection, 198 + record, 199 + repo, 200 + rkey, 201 + swap_commit: None, 202 + validate: None, 203 + } 204 + .into(), 205 + ) 206 + .await?; 207 + Ok(result.data) 208 + } 176 209 177 210 pub async fn put_record( 178 211 &self, ··· 249 282 Ok(result.data) 250 283 } 251 284 285 + pub async fn publish_blob( 286 + &self, 287 + input: Vec<u8>, 288 + ) -> Result< 289 + ( 290 + BlobRef, 291 + crate::com::atproto::repo::create_record::OutputData, 292 + ), 293 + Error, 294 + > { 295 + use crate::sh::weaver::publish; 296 + let blobref = self 297 + .api 298 + .com 299 + .atproto 300 + .repo 301 + .upload_blob(input.into()) 302 + .await? 303 + .blob 304 + .clone(); 305 + let upload_record = publish::blob::RecordData { 306 + upload: blobref.clone(), 307 + }; 308 + let at_identifier = AtIdentifier::try_from(self.did().await.expect("valid did")) 309 + .expect("valid did should be valid at identifier"); 310 + let result = self 311 + .create_record( 312 + Nsid::new(publish::Blob::NSID.to_string()) 313 + .expect("sh.weaver.publish.blob should be a valid NSID"), 314 + upload_record 315 + .try_into_unknown() 316 + .expect("this should just work"), 317 + at_identifier, 318 + None, 319 + ) 320 + .await?; 321 + Ok((blobref, result)) 322 + } 323 + 252 324 pub async fn upload_artifact( 253 325 &self, 254 326 content: String, ··· 313 385 M: CloneWithProxy + SessionManager + Send + Sync, 314 386 { 315 387 /// Configures the atproto-proxy header to be applied on requests. 316 - 317 388 /// 318 - 319 389 /// Returns a new client service with the proxy header configured. 320 390 321 391 pub fn api_with_proxy(&self, did: Did, service_type: impl AsRef<str>) -> Service<Wrapper<M>> {
+10
crates/weaver-common/src/lexicons/app/bsky/feed/defs.rs
··· 39 39 pub reason: core::option::Option<atrium_api::types::Union<FeedViewPostReasonRefs>>, 40 40 #[serde(skip_serializing_if = "core::option::Option::is_none")] 41 41 pub reply: core::option::Option<ReplyRef>, 42 + ///Unique identifier per request that may be passed back alongside interactions. 43 + #[serde(skip_serializing_if = "core::option::Option::is_none")] 44 + pub req_id: core::option::Option<String>, 42 45 } 43 46 pub type FeedViewPost = atrium_api::types::Object<FeedViewPostData>; 44 47 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] ··· 87 90 pub feed_context: core::option::Option<String>, 88 91 #[serde(skip_serializing_if = "core::option::Option::is_none")] 89 92 pub item: core::option::Option<String>, 93 + ///Unique identifier per request that may be passed back alongside interactions. 94 + #[serde(skip_serializing_if = "core::option::Option::is_none")] 95 + pub req_id: core::option::Option<String>, 90 96 } 91 97 pub type Interaction = atrium_api::types::Object<InteractionData>; 92 98 ///User liked the feed item ··· 142 148 #[serde(rename_all = "camelCase")] 143 149 pub struct ReasonRepostData { 144 150 pub by: crate::app::bsky::actor::defs::ProfileViewBasic, 151 + #[serde(skip_serializing_if = "core::option::Option::is_none")] 152 + pub cid: core::option::Option<atrium_api::types::string::Cid>, 145 153 pub indexed_at: atrium_api::types::string::Datetime, 154 + #[serde(skip_serializing_if = "core::option::Option::is_none")] 155 + pub uri: core::option::Option<String>, 146 156 } 147 157 pub type ReasonRepost = atrium_api::types::Object<ReasonRepostData>; 148 158 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
+3
crates/weaver-common/src/lexicons/app/bsky/feed/get_feed_skeleton.rs
··· 18 18 #[serde(skip_serializing_if = "core::option::Option::is_none")] 19 19 pub cursor: core::option::Option<String>, 20 20 pub feed: Vec<crate::app::bsky::feed::defs::SkeletonFeedPost>, 21 + ///Unique identifier per request that may be passed back alongside interactions. 22 + #[serde(skip_serializing_if = "core::option::Option::is_none")] 23 + pub req_id: core::option::Option<String>, 21 24 } 22 25 pub type Output = atrium_api::types::Object<OutputData>; 23 26 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
+2
crates/weaver-common/src/lexicons/app/bsky/feed/like.rs
··· 6 6 pub struct RecordData { 7 7 pub created_at: atrium_api::types::string::Datetime, 8 8 pub subject: crate::com::atproto::repo::strong_ref::Main, 9 + #[serde(skip_serializing_if = "core::option::Option::is_none")] 10 + pub via: core::option::Option<crate::com::atproto::repo::strong_ref::Main>, 9 11 } 10 12 pub type Record = atrium_api::types::Object<RecordData>; 11 13 impl From<atrium_api::types::Unknown> for RecordData {
+2
crates/weaver-common/src/lexicons/app/bsky/feed/repost.rs
··· 6 6 pub struct RecordData { 7 7 pub created_at: atrium_api::types::string::Datetime, 8 8 pub subject: crate::com::atproto::repo::strong_ref::Main, 9 + #[serde(skip_serializing_if = "core::option::Option::is_none")] 10 + pub via: core::option::Option<crate::com::atproto::repo::strong_ref::Main>, 9 11 } 10 12 pub type Record = atrium_api::types::Object<RecordData>; 11 13 impl From<atrium_api::types::Unknown> for RecordData {
+1 -1
crates/weaver-common/src/lexicons/app/bsky/notification/list_notifications.rs
··· 46 46 pub is_read: bool, 47 47 #[serde(skip_serializing_if = "core::option::Option::is_none")] 48 48 pub labels: core::option::Option<Vec<crate::com::atproto::label::defs::Label>>, 49 - ///Expected values are 'like', 'repost', 'follow', 'mention', 'reply', 'quote', 'starterpack-joined', 'verified', and 'unverified'. 49 + ///The reason why this notification was delivered - e.g. your post was liked, or you received a new follower. 50 50 pub reason: String, 51 51 #[serde(skip_serializing_if = "core::option::Option::is_none")] 52 52 pub reason_subject: core::option::Option<String>,
+1 -1
crates/weaver-common/src/lexicons/client.rs
··· 1150 1150 _ => Err(atrium_xrpc::Error::UnexpectedResponseType), 1151 1151 } 1152 1152 } 1153 - ///Find posts matching search criteria, returning views of those posts. 1153 + ///Find posts matching search criteria, returning views of those posts. Note that this API endpoint may require authentication (eg, not public) for some service providers and implementations. 1154 1154 pub async fn search_posts( 1155 1155 &self, 1156 1156 params: crate::app::bsky::feed::search_posts::Parameters,
+54 -90
crates/weaver-common/src/lexicons/record.rs
··· 8 8 #[serde(rename = "app.bsky.actor.status")] 9 9 LexiconsAppBskyActorStatus(Box<crate::lexicons::app::bsky::actor::status::Record>), 10 10 #[serde(rename = "app.bsky.feed.generator")] 11 - LexiconsAppBskyFeedGenerator( 12 - Box<crate::lexicons::app::bsky::feed::generator::Record>, 13 - ), 11 + LexiconsAppBskyFeedGenerator(Box<crate::lexicons::app::bsky::feed::generator::Record>), 14 12 #[serde(rename = "app.bsky.feed.like")] 15 13 LexiconsAppBskyFeedLike(Box<crate::lexicons::app::bsky::feed::like::Record>), 16 14 #[serde(rename = "app.bsky.feed.post")] ··· 20 18 #[serde(rename = "app.bsky.feed.repost")] 21 19 LexiconsAppBskyFeedRepost(Box<crate::lexicons::app::bsky::feed::repost::Record>), 22 20 #[serde(rename = "app.bsky.feed.threadgate")] 23 - LexiconsAppBskyFeedThreadgate( 24 - Box<crate::lexicons::app::bsky::feed::threadgate::Record>, 25 - ), 21 + LexiconsAppBskyFeedThreadgate(Box<crate::lexicons::app::bsky::feed::threadgate::Record>), 26 22 #[serde(rename = "app.bsky.graph.block")] 27 23 LexiconsAppBskyGraphBlock(Box<crate::lexicons::app::bsky::graph::block::Record>), 28 24 #[serde(rename = "app.bsky.graph.follow")] ··· 30 26 #[serde(rename = "app.bsky.graph.list")] 31 27 LexiconsAppBskyGraphList(Box<crate::lexicons::app::bsky::graph::list::Record>), 32 28 #[serde(rename = "app.bsky.graph.listblock")] 33 - LexiconsAppBskyGraphListblock( 34 - Box<crate::lexicons::app::bsky::graph::listblock::Record>, 35 - ), 29 + LexiconsAppBskyGraphListblock(Box<crate::lexicons::app::bsky::graph::listblock::Record>), 36 30 #[serde(rename = "app.bsky.graph.listitem")] 37 - LexiconsAppBskyGraphListitem( 38 - Box<crate::lexicons::app::bsky::graph::listitem::Record>, 39 - ), 31 + LexiconsAppBskyGraphListitem(Box<crate::lexicons::app::bsky::graph::listitem::Record>), 40 32 #[serde(rename = "app.bsky.graph.starterpack")] 41 - LexiconsAppBskyGraphStarterpack( 42 - Box<crate::lexicons::app::bsky::graph::starterpack::Record>, 43 - ), 33 + LexiconsAppBskyGraphStarterpack(Box<crate::lexicons::app::bsky::graph::starterpack::Record>), 44 34 #[serde(rename = "app.bsky.graph.verification")] 45 - LexiconsAppBskyGraphVerification( 46 - Box<crate::lexicons::app::bsky::graph::verification::Record>, 47 - ), 35 + LexiconsAppBskyGraphVerification(Box<crate::lexicons::app::bsky::graph::verification::Record>), 48 36 #[serde(rename = "app.bsky.labeler.service")] 49 - LexiconsAppBskyLabelerService( 50 - Box<crate::lexicons::app::bsky::labeler::service::Record>, 51 - ), 37 + LexiconsAppBskyLabelerService(Box<crate::lexicons::app::bsky::labeler::service::Record>), 52 38 #[serde(rename = "chat.bsky.actor.declaration")] 53 - LexiconsChatBskyActorDeclaration( 54 - Box<crate::lexicons::chat::bsky::actor::declaration::Record>, 55 - ), 39 + LexiconsChatBskyActorDeclaration(Box<crate::lexicons::chat::bsky::actor::declaration::Record>), 56 40 #[serde(rename = "com.atproto.lexicon.schema")] 57 - LexiconsComAtprotoLexiconSchema( 58 - Box<crate::lexicons::com::atproto::lexicon::schema::Record>, 59 - ), 41 + LexiconsComAtprotoLexiconSchema(Box<crate::lexicons::com::atproto::lexicon::schema::Record>), 60 42 #[serde(rename = "sh.tangled.actor.profile")] 61 - LexiconsShTangledActorProfile( 62 - Box<crate::lexicons::sh::tangled::actor::profile::Record>, 63 - ), 43 + LexiconsShTangledActorProfile(Box<crate::lexicons::sh::tangled::actor::profile::Record>), 64 44 #[serde(rename = "sh.weaver.actor.profile")] 65 - LexiconsShWeaverActorProfile( 66 - Box<crate::lexicons::sh::weaver::actor::profile::Record>, 67 - ), 45 + LexiconsShWeaverActorProfile(Box<crate::lexicons::sh::weaver::actor::profile::Record>), 68 46 #[serde(rename = "sh.weaver.edit.cursor")] 69 47 LexiconsShWeaverEditCursor(Box<crate::lexicons::sh::weaver::edit::cursor::Record>), 70 48 #[serde(rename = "sh.weaver.edit.diff")] ··· 72 50 #[serde(rename = "sh.weaver.edit.root")] 73 51 LexiconsShWeaverEditRoot(Box<crate::lexicons::sh::weaver::edit::root::Record>), 74 52 #[serde(rename = "sh.weaver.notebook.authors")] 75 - LexiconsShWeaverNotebookAuthors( 76 - Box<crate::lexicons::sh::weaver::notebook::authors::Record>, 77 - ), 53 + LexiconsShWeaverNotebookAuthors(Box<crate::lexicons::sh::weaver::notebook::authors::Record>), 78 54 #[serde(rename = "sh.weaver.notebook.book")] 79 - LexiconsShWeaverNotebookBook( 80 - Box<crate::lexicons::sh::weaver::notebook::book::Record>, 81 - ), 55 + LexiconsShWeaverNotebookBook(Box<crate::lexicons::sh::weaver::notebook::book::Record>), 56 + #[serde(rename = "sh.weaver.notebook.chapter")] 57 + LexiconsShWeaverNotebookChapter(Box<crate::lexicons::sh::weaver::notebook::chapter::Record>), 82 58 #[serde(rename = "sh.weaver.notebook.entry")] 83 - LexiconsShWeaverNotebookEntry( 84 - Box<crate::lexicons::sh::weaver::notebook::entry::Record>, 85 - ), 59 + LexiconsShWeaverNotebookEntry(Box<crate::lexicons::sh::weaver::notebook::entry::Record>), 60 + #[serde(rename = "sh.weaver.publish.blob")] 61 + LexiconsShWeaverPublishBlob(Box<crate::lexicons::sh::weaver::publish::blob::Record>), 86 62 } 87 63 impl From<crate::lexicons::app::bsky::actor::profile::Record> for KnownRecord { 88 64 fn from(record: crate::lexicons::app::bsky::actor::profile::Record) -> Self { ··· 90 66 } 91 67 } 92 68 impl From<crate::lexicons::app::bsky::actor::profile::RecordData> for KnownRecord { 93 - fn from( 94 - record_data: crate::lexicons::app::bsky::actor::profile::RecordData, 95 - ) -> Self { 69 + fn from(record_data: crate::lexicons::app::bsky::actor::profile::RecordData) -> Self { 96 70 KnownRecord::LexiconsAppBskyActorProfile(Box::new(record_data.into())) 97 71 } 98 72 } ··· 112 86 } 113 87 } 114 88 impl From<crate::lexicons::app::bsky::feed::generator::RecordData> for KnownRecord { 115 - fn from( 116 - record_data: crate::lexicons::app::bsky::feed::generator::RecordData, 117 - ) -> Self { 89 + fn from(record_data: crate::lexicons::app::bsky::feed::generator::RecordData) -> Self { 118 90 KnownRecord::LexiconsAppBskyFeedGenerator(Box::new(record_data.into())) 119 91 } 120 92 } ··· 144 116 } 145 117 } 146 118 impl From<crate::lexicons::app::bsky::feed::postgate::RecordData> for KnownRecord { 147 - fn from( 148 - record_data: crate::lexicons::app::bsky::feed::postgate::RecordData, 149 - ) -> Self { 119 + fn from(record_data: crate::lexicons::app::bsky::feed::postgate::RecordData) -> Self { 150 120 KnownRecord::LexiconsAppBskyFeedPostgate(Box::new(record_data.into())) 151 121 } 152 122 } ··· 166 136 } 167 137 } 168 138 impl From<crate::lexicons::app::bsky::feed::threadgate::RecordData> for KnownRecord { 169 - fn from( 170 - record_data: crate::lexicons::app::bsky::feed::threadgate::RecordData, 171 - ) -> Self { 139 + fn from(record_data: crate::lexicons::app::bsky::feed::threadgate::RecordData) -> Self { 172 140 KnownRecord::LexiconsAppBskyFeedThreadgate(Box::new(record_data.into())) 173 141 } 174 142 } ··· 208 176 } 209 177 } 210 178 impl From<crate::lexicons::app::bsky::graph::listblock::RecordData> for KnownRecord { 211 - fn from( 212 - record_data: crate::lexicons::app::bsky::graph::listblock::RecordData, 213 - ) -> Self { 179 + fn from(record_data: crate::lexicons::app::bsky::graph::listblock::RecordData) -> Self { 214 180 KnownRecord::LexiconsAppBskyGraphListblock(Box::new(record_data.into())) 215 181 } 216 182 } ··· 220 186 } 221 187 } 222 188 impl From<crate::lexicons::app::bsky::graph::listitem::RecordData> for KnownRecord { 223 - fn from( 224 - record_data: crate::lexicons::app::bsky::graph::listitem::RecordData, 225 - ) -> Self { 189 + fn from(record_data: crate::lexicons::app::bsky::graph::listitem::RecordData) -> Self { 226 190 KnownRecord::LexiconsAppBskyGraphListitem(Box::new(record_data.into())) 227 191 } 228 192 } ··· 232 196 } 233 197 } 234 198 impl From<crate::lexicons::app::bsky::graph::starterpack::RecordData> for KnownRecord { 235 - fn from( 236 - record_data: crate::lexicons::app::bsky::graph::starterpack::RecordData, 237 - ) -> Self { 199 + fn from(record_data: crate::lexicons::app::bsky::graph::starterpack::RecordData) -> Self { 238 200 KnownRecord::LexiconsAppBskyGraphStarterpack(Box::new(record_data.into())) 239 201 } 240 202 } ··· 244 206 } 245 207 } 246 208 impl From<crate::lexicons::app::bsky::graph::verification::RecordData> for KnownRecord { 247 - fn from( 248 - record_data: crate::lexicons::app::bsky::graph::verification::RecordData, 249 - ) -> Self { 209 + fn from(record_data: crate::lexicons::app::bsky::graph::verification::RecordData) -> Self { 250 210 KnownRecord::LexiconsAppBskyGraphVerification(Box::new(record_data.into())) 251 211 } 252 212 } ··· 256 216 } 257 217 } 258 218 impl From<crate::lexicons::app::bsky::labeler::service::RecordData> for KnownRecord { 259 - fn from( 260 - record_data: crate::lexicons::app::bsky::labeler::service::RecordData, 261 - ) -> Self { 219 + fn from(record_data: crate::lexicons::app::bsky::labeler::service::RecordData) -> Self { 262 220 KnownRecord::LexiconsAppBskyLabelerService(Box::new(record_data.into())) 263 221 } 264 222 } ··· 268 226 } 269 227 } 270 228 impl From<crate::lexicons::chat::bsky::actor::declaration::RecordData> for KnownRecord { 271 - fn from( 272 - record_data: crate::lexicons::chat::bsky::actor::declaration::RecordData, 273 - ) -> Self { 229 + fn from(record_data: crate::lexicons::chat::bsky::actor::declaration::RecordData) -> Self { 274 230 KnownRecord::LexiconsChatBskyActorDeclaration(Box::new(record_data.into())) 275 231 } 276 232 } ··· 280 236 } 281 237 } 282 238 impl From<crate::lexicons::com::atproto::lexicon::schema::RecordData> for KnownRecord { 283 - fn from( 284 - record_data: crate::lexicons::com::atproto::lexicon::schema::RecordData, 285 - ) -> Self { 239 + fn from(record_data: crate::lexicons::com::atproto::lexicon::schema::RecordData) -> Self { 286 240 KnownRecord::LexiconsComAtprotoLexiconSchema(Box::new(record_data.into())) 287 241 } 288 242 } ··· 292 246 } 293 247 } 294 248 impl From<crate::lexicons::sh::tangled::actor::profile::RecordData> for KnownRecord { 295 - fn from( 296 - record_data: crate::lexicons::sh::tangled::actor::profile::RecordData, 297 - ) -> Self { 249 + fn from(record_data: crate::lexicons::sh::tangled::actor::profile::RecordData) -> Self { 298 250 KnownRecord::LexiconsShTangledActorProfile(Box::new(record_data.into())) 299 251 } 300 252 } ··· 304 256 } 305 257 } 306 258 impl From<crate::lexicons::sh::weaver::actor::profile::RecordData> for KnownRecord { 307 - fn from( 308 - record_data: crate::lexicons::sh::weaver::actor::profile::RecordData, 309 - ) -> Self { 259 + fn from(record_data: crate::lexicons::sh::weaver::actor::profile::RecordData) -> Self { 310 260 KnownRecord::LexiconsShWeaverActorProfile(Box::new(record_data.into())) 311 261 } 312 262 } ··· 346 296 } 347 297 } 348 298 impl From<crate::lexicons::sh::weaver::notebook::authors::RecordData> for KnownRecord { 349 - fn from( 350 - record_data: crate::lexicons::sh::weaver::notebook::authors::RecordData, 351 - ) -> Self { 299 + fn from(record_data: crate::lexicons::sh::weaver::notebook::authors::RecordData) -> Self { 352 300 KnownRecord::LexiconsShWeaverNotebookAuthors(Box::new(record_data.into())) 353 301 } 354 302 } ··· 358 306 } 359 307 } 360 308 impl From<crate::lexicons::sh::weaver::notebook::book::RecordData> for KnownRecord { 361 - fn from( 362 - record_data: crate::lexicons::sh::weaver::notebook::book::RecordData, 363 - ) -> Self { 309 + fn from(record_data: crate::lexicons::sh::weaver::notebook::book::RecordData) -> Self { 364 310 KnownRecord::LexiconsShWeaverNotebookBook(Box::new(record_data.into())) 365 311 } 366 312 } 313 + impl From<crate::lexicons::sh::weaver::notebook::chapter::Record> for KnownRecord { 314 + fn from(record: crate::lexicons::sh::weaver::notebook::chapter::Record) -> Self { 315 + KnownRecord::LexiconsShWeaverNotebookChapter(Box::new(record)) 316 + } 317 + } 318 + impl From<crate::lexicons::sh::weaver::notebook::chapter::RecordData> for KnownRecord { 319 + fn from(record_data: crate::lexicons::sh::weaver::notebook::chapter::RecordData) -> Self { 320 + KnownRecord::LexiconsShWeaverNotebookChapter(Box::new(record_data.into())) 321 + } 322 + } 367 323 impl From<crate::lexicons::sh::weaver::notebook::entry::Record> for KnownRecord { 368 324 fn from(record: crate::lexicons::sh::weaver::notebook::entry::Record) -> Self { 369 325 KnownRecord::LexiconsShWeaverNotebookEntry(Box::new(record)) 370 326 } 371 327 } 372 328 impl From<crate::lexicons::sh::weaver::notebook::entry::RecordData> for KnownRecord { 373 - fn from( 374 - record_data: crate::lexicons::sh::weaver::notebook::entry::RecordData, 375 - ) -> Self { 329 + fn from(record_data: crate::lexicons::sh::weaver::notebook::entry::RecordData) -> Self { 376 330 KnownRecord::LexiconsShWeaverNotebookEntry(Box::new(record_data.into())) 331 + } 332 + } 333 + impl From<crate::lexicons::sh::weaver::publish::blob::Record> for KnownRecord { 334 + fn from(record: crate::lexicons::sh::weaver::publish::blob::Record) -> Self { 335 + KnownRecord::LexiconsShWeaverPublishBlob(Box::new(record)) 336 + } 337 + } 338 + impl From<crate::lexicons::sh::weaver::publish::blob::RecordData> for KnownRecord { 339 + fn from(record_data: crate::lexicons::sh::weaver::publish::blob::RecordData) -> Self { 340 + KnownRecord::LexiconsShWeaverPublishBlob(Box::new(record_data.into())) 377 341 } 378 342 } 379 343 impl Into<atrium_api::types::Unknown> for KnownRecord {
+1
crates/weaver-common/src/lexicons/sh/weaver.rs
··· 4 4 pub mod edit; 5 5 pub mod embed; 6 6 pub mod notebook; 7 + pub mod publish;
+11
crates/weaver-common/src/lexicons/sh/weaver/actor/defs.rs
··· 1 1 // @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT. 2 2 //!Definitions for the `sh.weaver.actor.defs` namespace. 3 + ///A single author in a Weaver notebook. 4 + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] 5 + #[serde(rename_all = "camelCase")] 6 + pub struct AuthorData { 7 + pub did: atrium_api::types::string::Did, 8 + #[serde(skip_serializing_if = "core::option::Option::is_none")] 9 + pub display_name: core::option::Option<String>, 10 + #[serde(skip_serializing_if = "core::option::Option::is_none")] 11 + pub handle: core::option::Option<atrium_api::types::string::Handle>, 12 + } 13 + pub type Author = atrium_api::types::Object<AuthorData>; 3 14 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] 4 15 #[serde(rename_all = "camelCase")] 5 16 pub struct ProfileDataViewData {
+7
crates/weaver-common/src/lexicons/sh/weaver/notebook.rs
··· 2 2 //!Definitions for the `sh.weaver.notebook` namespace. 3 3 pub mod authors; 4 4 pub mod book; 5 + pub mod chapter; 5 6 pub mod defs; 6 7 pub mod entry; 7 8 #[derive(Debug)] ··· 15 16 impl atrium_api::types::Collection for Book { 16 17 const NSID: &'static str = "sh.weaver.notebook.book"; 17 18 type Record = book::Record; 19 + } 20 + #[derive(Debug)] 21 + pub struct Chapter; 22 + impl atrium_api::types::Collection for Chapter { 23 + const NSID: &'static str = "sh.weaver.notebook.chapter"; 24 + type Record = chapter::Record; 18 25 } 19 26 #[derive(Debug)] 20 27 pub struct Entry;
+2 -2
crates/weaver-common/src/lexicons/sh/weaver/notebook/book.rs
··· 4 4 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] 5 5 #[serde(rename_all = "camelCase")] 6 6 pub struct RecordData { 7 - pub authors: crate::sh::weaver::notebook::defs::AuthorListView, 7 + pub authors: Vec<crate::sh::weaver::actor::defs::Author>, 8 8 ///Client-declared timestamp when this was originally created. 9 9 #[serde(skip_serializing_if = "core::option::Option::is_none")] 10 10 pub created_at: core::option::Option<atrium_api::types::string::Datetime>, 11 - pub entry_list: Vec<crate::sh::weaver::notebook::defs::BookEntryView>, 11 + pub entry_list: Vec<crate::com::atproto::repo::strong_ref::Main>, 12 12 #[serde(skip_serializing_if = "core::option::Option::is_none")] 13 13 pub tags: core::option::Option<crate::sh::weaver::notebook::defs::Tags>, 14 14 #[serde(skip_serializing_if = "core::option::Option::is_none")]
+24
crates/weaver-common/src/lexicons/sh/weaver/notebook/chapter.rs
··· 1 + // @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT. 2 + //!Definitions for the `sh.weaver.notebook.chapter` namespace. 3 + use atrium_api::types::TryFromUnknown; 4 + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] 5 + #[serde(rename_all = "camelCase")] 6 + pub struct RecordData { 7 + pub authors: Vec<crate::sh::weaver::actor::defs::Author>, 8 + ///Client-declared timestamp when this was originally created. 9 + #[serde(skip_serializing_if = "core::option::Option::is_none")] 10 + pub created_at: core::option::Option<atrium_api::types::string::Datetime>, 11 + pub entry_list: Vec<crate::com::atproto::repo::strong_ref::Main>, 12 + ///The notebook this chapter belongs to. 13 + pub notebook: crate::com::atproto::repo::strong_ref::Main, 14 + #[serde(skip_serializing_if = "core::option::Option::is_none")] 15 + pub tags: core::option::Option<crate::sh::weaver::notebook::defs::Tags>, 16 + #[serde(skip_serializing_if = "core::option::Option::is_none")] 17 + pub title: core::option::Option<crate::sh::weaver::notebook::defs::Title>, 18 + } 19 + pub type Record = atrium_api::types::Object<RecordData>; 20 + impl From<atrium_api::types::Unknown> for RecordData { 21 + fn from(value: atrium_api::types::Unknown) -> Self { 22 + Self::try_from_unknown(value).unwrap() 23 + } 24 + }
+9
crates/weaver-common/src/lexicons/sh/weaver/notebook/defs.rs
··· 30 30 pub prev: core::option::Option<BookEntryRef>, 31 31 } 32 32 pub type BookEntryView = atrium_api::types::Object<BookEntryViewData>; 33 + ///The format of the content. This is used to determine how to render the content. 34 + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] 35 + #[serde(rename_all = "camelCase")] 36 + pub struct ContentFormatData { 37 + ///The format of the content. This is used to determine how to render the content. 38 + #[serde(skip_serializing_if = "core::option::Option::is_none")] 39 + pub markdown: core::option::Option<String>, 40 + } 41 + pub type ContentFormat = atrium_api::types::Object<ContentFormatData>; 33 42 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] 34 43 #[serde(rename_all = "camelCase")] 35 44 pub struct EntryViewData {
+10
crates/weaver-common/src/lexicons/sh/weaver/publish.rs
··· 1 + // @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT. 2 + //!Definitions for the `sh.weaver.publish` namespace. 3 + pub mod blob; 4 + pub mod defs; 5 + #[derive(Debug)] 6 + pub struct Blob; 7 + impl atrium_api::types::Collection for Blob { 8 + const NSID: &'static str = "sh.weaver.publish.blob"; 9 + type Record = blob::Record; 10 + }
+15
crates/weaver-common/src/lexicons/sh/weaver/publish/blob.rs
··· 1 + // @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT. 2 + //!Definitions for the `sh.weaver.publish.blob` namespace. 3 + use atrium_api::types::TryFromUnknown; 4 + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] 5 + #[serde(rename_all = "camelCase")] 6 + pub struct RecordData { 7 + ///Reference to the uploaded file 8 + pub upload: atrium_api::types::BlobRef, 9 + } 10 + pub type Record = atrium_api::types::Object<RecordData>; 11 + impl From<atrium_api::types::Unknown> for RecordData { 12 + fn from(value: atrium_api::types::Unknown) -> Self { 13 + Self::try_from_unknown(value).unwrap() 14 + } 15 + }
+2
crates/weaver-common/src/lexicons/sh/weaver/publish/defs.rs
··· 1 + // @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT. 2 + //!Definitions for the `sh.weaver.publish.defs` namespace.
+85 -1
crates/weaver-common/src/lib.rs
··· 8 8 pub mod oauth; 9 9 pub mod resolver; 10 10 pub mod xrpc_server; 11 - use atrium_api::types::{BlobRef, TypedBlobRef, string::Did}; 11 + use std::sync::OnceLock; 12 + 13 + pub use atrium_api::types::*; 12 14 pub use lexicons::*; 15 + use regex::Regex; 16 + use string::Did; 13 17 14 18 pub use crate::error::{Error, IoError, ParseError, SerDeError}; 15 19 ··· 56 60 mime_type.strip_prefix("image/").unwrap_or(mime_type) 57 61 ) 58 62 } 63 + 64 + pub fn blob_url(did: &Did, pds: &str, blob_ref: &BlobRef) -> String { 65 + let cid = match blob_ref { 66 + BlobRef::Typed(TypedBlobRef::Blob(b)) => atrium_api::types::string::Cid::new(b.r#ref.0) 67 + .as_ref() 68 + .to_string(), 69 + 70 + BlobRef::Untyped(r) => r.cid.clone(), 71 + }; 72 + format!( 73 + "https://{}/xrpc/com.atproto.repo.getBlob?did={}&cid={}", 74 + pds, 75 + did.as_str(), 76 + cid, 77 + ) 78 + } 79 + 80 + pub fn match_identifier(maybe_identifier: &str) -> Option<&str> { 81 + static RE_HANDLE: OnceLock<Regex> = OnceLock::new(); 82 + static RE_DID: OnceLock<Regex> = OnceLock::new(); 83 + if maybe_identifier.len() > 253 { 84 + None 85 + } else if !RE_DID.get_or_init(|| Regex::new(r"^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$").unwrap()) 86 + .is_match(&maybe_identifier) && !RE_HANDLE 87 + .get_or_init(|| Regex::new(r"^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$").unwrap()) 88 + .is_match(&maybe_identifier) 89 + { 90 + None 91 + } else { 92 + Some(maybe_identifier) 93 + } 94 + } 95 + 96 + pub fn match_nsid(maybe_nsid: &str) -> Option<&str> { 97 + static RE_NSID: OnceLock<Regex> = OnceLock::new(); 98 + if maybe_nsid.len() > 317 { 99 + None 100 + } else if !RE_NSID 101 + .get_or_init(|| Regex::new(r"^[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(\.[a-zA-Z]([a-zA-Z0-9]{0,62}[a-zA-Z0-9])?)$").unwrap()) 102 + .is_match(&maybe_nsid) 103 + { 104 + None 105 + } else { 106 + Some(maybe_nsid) 107 + } 108 + } 109 + 110 + /// Convert an ATURI to a HTTP URL 111 + /// Currently has some failure modes and should restrict the NSIDs to a known subset 112 + pub fn aturi_to_http<'s>(aturi: &'s str, appview: &'s str) -> Option<markdown_weaver::CowStr<'s>> { 113 + use markdown_weaver::CowStr; 114 + 115 + if aturi.starts_with("at://") { 116 + let rest = aturi.strip_prefix("at:://").unwrap(); 117 + let mut split = rest.splitn(2, '/'); 118 + let maybe_identifier = split.next()?; 119 + let maybe_nsid = split.next()?; 120 + let maybe_rkey = split.next()?; 121 + 122 + // https://atproto.com/specs/handle#handle-identifier-syntax 123 + let identifier = match_identifier(maybe_identifier)?; 124 + 125 + let nsid = if let Some(nsid) = match_nsid(maybe_nsid) { 126 + // Last part of the nsid is generally the middle component of the URL 127 + // TODO: check for bsky ones specifically, because those are the ones where this is valid 128 + nsid.rsplitn(1, '.').next()? 129 + } else { 130 + return None; 131 + }; 132 + Some(CowStr::Boxed( 133 + format!( 134 + "https://{}/profile/{}/{}/{}", 135 + appview, identifier, nsid, maybe_rkey 136 + ) 137 + .into_boxed_str(), 138 + )) 139 + } else { 140 + Some(CowStr::Borrowed(aturi)) 141 + } 142 + }
+21 -2
crates/weaver-renderer/Cargo.toml
··· 6 6 publish = false 7 7 8 8 [dependencies] 9 - n0-future = { workspace = true } 9 + n0-future.workspace = true 10 10 atrium-api = { version = "0.25.3", default-features = false } 11 11 12 12 weaver-common = { path = "../weaver-common" } 13 - 13 + markdown-weaver = { workspace = true } 14 14 weaver-workspace-hack = { version = "0.1", path = "../weaver-workspace-hack" } 15 15 compact_string = "0.1.0" 16 16 http = "1.3.1" 17 17 url = "2.5.4" 18 + syntect = { workspace = true, default-features = false, features = ["default-fancy"]} 19 + markdown-weaver-escape = { workspace = true, features = ["std"] } 20 + thiserror.workspace = true 21 + miette.workspace = true 22 + pathdiff = "0.2.3" 23 + unicode-normalization = "0.1.24" 24 + yaml-rust2 = { version = "0.10.2" } 25 + bitflags = "2.9.1" 26 + ignore = "0.4.23" 27 + dashmap = "6.1.0" 28 + regex = "1.11.1" 29 + pin-utils = "0.1.0" 30 + pin-project = "1.1.10" 31 + dynosaur = "0.2.0" 32 + async-trait = "0.1.88" 33 + 34 + [target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dependencies] 35 + tokio = { version = "1.28", features = ["rt", "time"] } 36 + tokio-util = { version = "0.7.14", features = ["rt"] }
+18
crates/weaver-renderer/src/atproto.rs
··· 1 + //! Atproto renderer 2 + //! 3 + //! This mode of the renderer renders either an entire notebook or entries in it to files suitable for inclusion 4 + //! in a single-page app and uploads them to your Atproto PDS 5 + //! It can be accessed via the appview at {your-handle}.weaver.sh/{notebook-name}. 6 + //! 7 + //! It can also be edited there. 8 + //! 9 + //! Link altering logic: 10 + //! - Option 1: leave (non-embed) links the same as in the markdown, have the CSM deal with them via some means 11 + //! such as adding "data-did" and "data-cid" attributes to the `<a/>` tag containing the DID and CID 12 + //! Pushes toward having the SPA/Appview do a bit more, but makes this step MUCH simpler 13 + //! - In this scenario, the rendering step can happen upon access (and then be cached) in the appview 14 + //! - More flexible in some ways, less in others 15 + //! - Option 2: alter links to point to other rendered blobs. Requires a certain amount of work to handle 16 + //! scenarios with a complex mesh of internal links, as the CID is altered by the editing of the link. 17 + //! Such cycles are handled in the simplest way, by rendering an absolute url which will make a call to the appview. 18 + //!
+693
crates/weaver-renderer/src/base_html.rs
··· 1 + // use std::{string::String, vec::Vec}; 2 + // #[cfg(all(feature = "std", not(feature = "hashbrown")))] 3 + // use std::collections::HashMap; 4 + 5 + // #[cfg(feature = "hashbrown")] 6 + // use hashbrown::HashMap; 7 + // 8 + use std::collections::HashMap; 9 + //#[cfg(feature = "std")] 10 + use markdown_weaver_escape::IoWriter; 11 + use markdown_weaver_escape::{ 12 + FmtWriter, StrWrite, escape_href, escape_html, escape_html_body_text, 13 + }; 14 + 15 + use markdown_weaver::{ 16 + Alignment, BlockQuoteKind, CodeBlockKind, CowStr, Event, Event::*, LinkType, Tag, TagEnd, 17 + }; 18 + 19 + pub enum TableState { 20 + Head, 21 + Body, 22 + } 23 + 24 + struct HtmlWriter<'a, I, W> { 25 + /// Iterator supplying events. 26 + iter: I, 27 + 28 + /// Writer to write to. 29 + writer: W, 30 + 31 + /// Whether or not the last write wrote a newline. 32 + end_newline: bool, 33 + 34 + /// Whether if inside a metadata block (text should not be written) 35 + in_non_writing_block: bool, 36 + 37 + table_state: TableState, 38 + table_alignments: Vec<Alignment>, 39 + table_cell_index: usize, 40 + numbers: HashMap<CowStr<'a>, usize>, 41 + } 42 + 43 + impl<'a, I, W> HtmlWriter<'a, I, W> 44 + where 45 + I: Iterator<Item = Event<'a>>, 46 + W: StrWrite, 47 + { 48 + fn new(iter: I, writer: W) -> Self { 49 + Self { 50 + iter, 51 + writer, 52 + end_newline: true, 53 + in_non_writing_block: false, 54 + table_state: TableState::Head, 55 + table_alignments: vec![], 56 + table_cell_index: 0, 57 + numbers: HashMap::new(), 58 + } 59 + } 60 + 61 + /// Writes a new line. 62 + #[inline] 63 + fn write_newline(&mut self) -> Result<(), W::Error> { 64 + self.end_newline = true; 65 + self.writer.write_str("\n") 66 + } 67 + 68 + /// Writes a buffer, and tracks whether or not a newline was written. 69 + #[inline] 70 + fn write(&mut self, s: &str) -> Result<(), W::Error> { 71 + self.writer.write_str(s)?; 72 + 73 + if !s.is_empty() { 74 + self.end_newline = s.ends_with('\n'); 75 + } 76 + Ok(()) 77 + } 78 + 79 + fn run(mut self) -> Result<(), W::Error> { 80 + while let Some(event) = self.iter.next() { 81 + match event { 82 + Start(tag) => { 83 + self.start_tag(tag)?; 84 + } 85 + End(tag) => { 86 + self.end_tag(tag)?; 87 + } 88 + Text(text) => { 89 + if !self.in_non_writing_block { 90 + escape_html_body_text(&mut self.writer, &text)?; 91 + self.end_newline = text.ends_with('\n'); 92 + } 93 + } 94 + Code(text) => { 95 + self.write("<code>")?; 96 + escape_html_body_text(&mut self.writer, &text)?; 97 + self.write("</code>")?; 98 + } 99 + InlineMath(text) => { 100 + self.write(r#"<span class="math math-inline">"#)?; 101 + escape_html(&mut self.writer, &text)?; 102 + self.write("</span>")?; 103 + } 104 + DisplayMath(text) => { 105 + self.write(r#"<span class="math math-display">"#)?; 106 + escape_html(&mut self.writer, &text)?; 107 + self.write("</span>")?; 108 + } 109 + Html(html) | InlineHtml(html) => { 110 + self.write(&html)?; 111 + } 112 + SoftBreak => { 113 + self.write_newline()?; 114 + } 115 + HardBreak => { 116 + self.write("<br />\n")?; 117 + } 118 + Rule => { 119 + if self.end_newline { 120 + self.write("<hr />\n")?; 121 + } else { 122 + self.write("\n<hr />\n")?; 123 + } 124 + } 125 + FootnoteReference(name) => { 126 + let len = self.numbers.len() + 1; 127 + self.write("<sup class=\"footnote-reference\"><a href=\"#")?; 128 + escape_html(&mut self.writer, &name)?; 129 + self.write("\">")?; 130 + let number = *self.numbers.entry(name).or_insert(len); 131 + write!(&mut self.writer, "{}", number)?; 132 + self.write("</a></sup>")?; 133 + } 134 + TaskListMarker(true) => { 135 + self.write("<input disabled=\"\" type=\"checkbox\" checked=\"\"/>\n")?; 136 + } 137 + TaskListMarker(false) => { 138 + self.write("<input disabled=\"\" type=\"checkbox\"/>\n")?; 139 + } 140 + WeaverBlock(_text) => {} 141 + } 142 + } 143 + Ok(()) 144 + } 145 + 146 + /// Writes the start of an HTML tag. 147 + fn start_tag(&mut self, tag: Tag<'a>) -> Result<(), W::Error> { 148 + match tag { 149 + Tag::HtmlBlock => Ok(()), 150 + Tag::Paragraph => { 151 + if self.end_newline { 152 + self.write("<p>") 153 + } else { 154 + self.write("\n<p>") 155 + } 156 + } 157 + Tag::Heading { 158 + level, 159 + id, 160 + classes, 161 + attrs, 162 + } => { 163 + if self.end_newline { 164 + self.write("<")?; 165 + } else { 166 + self.write("\n<")?; 167 + } 168 + write!(&mut self.writer, "{}", level)?; 169 + if let Some(id) = id { 170 + self.write(" id=\"")?; 171 + escape_html(&mut self.writer, &id)?; 172 + self.write("\"")?; 173 + } 174 + let mut classes = classes.iter(); 175 + if let Some(class) = classes.next() { 176 + self.write(" class=\"")?; 177 + escape_html(&mut self.writer, class)?; 178 + for class in classes { 179 + self.write(" ")?; 180 + escape_html(&mut self.writer, class)?; 181 + } 182 + self.write("\"")?; 183 + } 184 + for (attr, value) in attrs { 185 + self.write(" ")?; 186 + escape_html(&mut self.writer, &attr)?; 187 + if let Some(val) = value { 188 + self.write("=\"")?; 189 + escape_html(&mut self.writer, &val)?; 190 + self.write("\"")?; 191 + } else { 192 + self.write("=\"\"")?; 193 + } 194 + } 195 + self.write(">") 196 + } 197 + Tag::Table(alignments) => { 198 + self.table_alignments = alignments; 199 + self.write("<table>") 200 + } 201 + Tag::TableHead => { 202 + self.table_state = TableState::Head; 203 + self.table_cell_index = 0; 204 + self.write("<thead><tr>") 205 + } 206 + Tag::TableRow => { 207 + self.table_cell_index = 0; 208 + self.write("<tr>") 209 + } 210 + Tag::TableCell => { 211 + match self.table_state { 212 + TableState::Head => { 213 + self.write("<th")?; 214 + } 215 + TableState::Body => { 216 + self.write("<td")?; 217 + } 218 + } 219 + match self.table_alignments.get(self.table_cell_index) { 220 + Some(&Alignment::Left) => self.write(" style=\"text-align: left\">"), 221 + Some(&Alignment::Center) => self.write(" style=\"text-align: center\">"), 222 + Some(&Alignment::Right) => self.write(" style=\"text-align: right\">"), 223 + _ => self.write(">"), 224 + } 225 + } 226 + Tag::BlockQuote(kind) => { 227 + let class_str = match kind { 228 + None => "", 229 + Some(kind) => match kind { 230 + BlockQuoteKind::Note => " class=\"markdown-alert-note\"", 231 + BlockQuoteKind::Tip => " class=\"markdown-alert-tip\"", 232 + BlockQuoteKind::Important => " class=\"markdown-alert-important\"", 233 + BlockQuoteKind::Warning => " class=\"markdown-alert-warning\"", 234 + BlockQuoteKind::Caution => " class=\"markdown-alert-caution\"", 235 + }, 236 + }; 237 + if self.end_newline { 238 + self.write(&format!("<blockquote{}>\n", class_str)) 239 + } else { 240 + self.write(&format!("\n<blockquote{}>\n", class_str)) 241 + } 242 + } 243 + Tag::CodeBlock(info) => { 244 + if !self.end_newline { 245 + self.write_newline()?; 246 + } 247 + match info { 248 + CodeBlockKind::Fenced(info) => { 249 + let lang = info.split(' ').next().unwrap(); 250 + if lang.is_empty() { 251 + self.write("<pre><code>") 252 + } else { 253 + self.write("<pre><code class=\"language-")?; 254 + escape_html(&mut self.writer, lang)?; 255 + self.write("\">") 256 + } 257 + } 258 + CodeBlockKind::Indented => self.write("<pre><code>"), 259 + } 260 + } 261 + Tag::List(Some(1)) => { 262 + if self.end_newline { 263 + self.write("<ol>\n") 264 + } else { 265 + self.write("\n<ol>\n") 266 + } 267 + } 268 + Tag::List(Some(start)) => { 269 + if self.end_newline { 270 + self.write("<ol start=\"")?; 271 + } else { 272 + self.write("\n<ol start=\"")?; 273 + } 274 + write!(&mut self.writer, "{}", start)?; 275 + self.write("\">\n") 276 + } 277 + Tag::List(None) => { 278 + if self.end_newline { 279 + self.write("<ul>\n") 280 + } else { 281 + self.write("\n<ul>\n") 282 + } 283 + } 284 + Tag::Item => { 285 + if self.end_newline { 286 + self.write("<li>") 287 + } else { 288 + self.write("\n<li>") 289 + } 290 + } 291 + Tag::DefinitionList => { 292 + if self.end_newline { 293 + self.write("<dl>\n") 294 + } else { 295 + self.write("\n<dl>\n") 296 + } 297 + } 298 + Tag::DefinitionListTitle => { 299 + if self.end_newline { 300 + self.write("<dt>") 301 + } else { 302 + self.write("\n<dt>") 303 + } 304 + } 305 + Tag::DefinitionListDefinition => { 306 + if self.end_newline { 307 + self.write("<dd>") 308 + } else { 309 + self.write("\n<dd>") 310 + } 311 + } 312 + Tag::Subscript => self.write("<sub>"), 313 + Tag::Superscript => self.write("<sup>"), 314 + Tag::Emphasis => self.write("<em>"), 315 + Tag::Strong => self.write("<strong>"), 316 + Tag::Strikethrough => self.write("<del>"), 317 + Tag::Link { 318 + link_type: LinkType::Email, 319 + dest_url, 320 + title, 321 + id: _, 322 + } => { 323 + self.write("<a href=\"mailto:")?; 324 + escape_href(&mut self.writer, &dest_url)?; 325 + if !title.is_empty() { 326 + self.write("\" title=\"")?; 327 + escape_html(&mut self.writer, &title)?; 328 + } 329 + self.write("\">") 330 + } 331 + Tag::Link { 332 + link_type: _, 333 + dest_url, 334 + title, 335 + id: _, 336 + } => { 337 + self.write("<a href=\"")?; 338 + escape_href(&mut self.writer, &dest_url)?; 339 + if !title.is_empty() { 340 + self.write("\" title=\"")?; 341 + escape_html(&mut self.writer, &title)?; 342 + } 343 + self.write("\">") 344 + } 345 + Tag::Image { 346 + link_type: _, 347 + dest_url, 348 + title, 349 + id: _, 350 + attrs, 351 + } => { 352 + self.write("<img src=\"")?; 353 + escape_href(&mut self.writer, &dest_url)?; 354 + if let Some(attrs) = attrs { 355 + if !attrs.classes.is_empty() { 356 + self.write("\" class=\"")?; 357 + for class in &attrs.classes { 358 + escape_html(&mut self.writer, class)?; 359 + self.write(" ")?; 360 + } 361 + self.write("\" ")?; 362 + } else { 363 + self.write("\" ")?; 364 + } 365 + if !attrs.attrs.is_empty() { 366 + for (attr, value) in &attrs.attrs { 367 + escape_html(&mut self.writer, attr)?; 368 + self.write("=\"")?; 369 + escape_html(&mut self.writer, value)?; 370 + self.write("\" ")?; 371 + } 372 + } 373 + } else { 374 + self.write("\" ")?; 375 + } 376 + self.write("alt=\"")?; 377 + self.raw_text()?; 378 + if !title.is_empty() { 379 + self.write("\" title=\"")?; 380 + escape_html(&mut self.writer, &title)?; 381 + } 382 + self.write("\" />") 383 + } 384 + Tag::Embed { 385 + embed_type: _, 386 + dest_url, 387 + title, 388 + id, 389 + attrs, 390 + } => { 391 + // rewrite this to work correctly 392 + self.write("<iframe src=\"")?; 393 + escape_href(&mut self.writer, &dest_url)?; 394 + self.write("\" title=\"")?; 395 + escape_html(&mut self.writer, &title)?; 396 + if !id.is_empty() { 397 + self.write("\" id=\"")?; 398 + escape_html(&mut self.writer, &id)?; 399 + self.write("\"")?; 400 + } 401 + if let Some(attrs) = attrs { 402 + self.write(" ")?; 403 + if !attrs.classes.is_empty() { 404 + self.write("class=\"")?; 405 + for class in &attrs.classes { 406 + escape_html(&mut self.writer, class)?; 407 + self.write(" ")?; 408 + } 409 + self.write("\" ")?; 410 + } 411 + if !attrs.attrs.is_empty() { 412 + for (attr, value) in &attrs.attrs { 413 + escape_html(&mut self.writer, attr)?; 414 + self.write("=\"")?; 415 + escape_html(&mut self.writer, value)?; 416 + self.write("\" ")?; 417 + } 418 + } 419 + } 420 + self.write("/>") 421 + } 422 + Tag::WeaverBlock(_, _attrs) => { 423 + println!("Weaver block"); 424 + self.in_non_writing_block = true; 425 + Ok(()) 426 + } 427 + Tag::FootnoteDefinition(name) => { 428 + if self.end_newline { 429 + self.write("<div class=\"footnote-definition\" id=\"")?; 430 + } else { 431 + self.write("\n<div class=\"footnote-definition\" id=\"")?; 432 + } 433 + escape_html(&mut self.writer, &name)?; 434 + self.write("\"><sup class=\"footnote-definition-label\">")?; 435 + let len = self.numbers.len() + 1; 436 + let number = *self.numbers.entry(name).or_insert(len); 437 + write!(&mut self.writer, "{}", number)?; 438 + self.write("</sup>") 439 + } 440 + Tag::MetadataBlock(_) => { 441 + self.in_non_writing_block = true; 442 + Ok(()) 443 + } 444 + } 445 + } 446 + 447 + fn end_tag(&mut self, tag: TagEnd) -> Result<(), W::Error> { 448 + match tag { 449 + TagEnd::HtmlBlock => {} 450 + TagEnd::Paragraph => { 451 + self.write("</p>\n")?; 452 + } 453 + TagEnd::Heading(level) => { 454 + self.write("</")?; 455 + write!(&mut self.writer, "{}", level)?; 456 + self.write(">\n")?; 457 + } 458 + TagEnd::Table => { 459 + self.write("</tbody></table>\n")?; 460 + } 461 + TagEnd::TableHead => { 462 + self.write("</tr></thead><tbody>\n")?; 463 + self.table_state = TableState::Body; 464 + } 465 + TagEnd::TableRow => { 466 + self.write("</tr>\n")?; 467 + } 468 + TagEnd::TableCell => { 469 + match self.table_state { 470 + TableState::Head => { 471 + self.write("</th>")?; 472 + } 473 + TableState::Body => { 474 + self.write("</td>")?; 475 + } 476 + } 477 + self.table_cell_index += 1; 478 + } 479 + TagEnd::BlockQuote(_) => { 480 + self.write("</blockquote>\n")?; 481 + } 482 + TagEnd::CodeBlock => { 483 + self.write("</code></pre>\n")?; 484 + } 485 + TagEnd::List(true) => { 486 + self.write("</ol>\n")?; 487 + } 488 + TagEnd::List(false) => { 489 + self.write("</ul>\n")?; 490 + } 491 + TagEnd::Item => { 492 + self.write("</li>\n")?; 493 + } 494 + TagEnd::DefinitionList => { 495 + self.write("</dl>\n")?; 496 + } 497 + TagEnd::DefinitionListTitle => { 498 + self.write("</dt>\n")?; 499 + } 500 + TagEnd::DefinitionListDefinition => { 501 + self.write("</dd>\n")?; 502 + } 503 + TagEnd::Emphasis => { 504 + self.write("</em>")?; 505 + } 506 + TagEnd::Superscript => { 507 + self.write("</sup>")?; 508 + } 509 + TagEnd::Subscript => { 510 + self.write("</sub>")?; 511 + } 512 + TagEnd::Strong => { 513 + self.write("</strong>")?; 514 + } 515 + TagEnd::Strikethrough => { 516 + self.write("</del>")?; 517 + } 518 + TagEnd::Link => { 519 + self.write("</a>")?; 520 + } 521 + TagEnd::Image => (), // shouldn't happen, handled in start 522 + TagEnd::Embed => (), // shouldn't happen, handled in start 523 + TagEnd::WeaverBlock(_) => { 524 + self.in_non_writing_block = false; 525 + } 526 + TagEnd::FootnoteDefinition => { 527 + self.write("</div>\n")?; 528 + } 529 + TagEnd::MetadataBlock(_) => { 530 + self.in_non_writing_block = false; 531 + } 532 + } 533 + Ok(()) 534 + } 535 + 536 + // run raw text, consuming end tag 537 + fn raw_text(&mut self) -> Result<(), W::Error> { 538 + let mut nest = 0; 539 + while let Some(event) = self.iter.next() { 540 + match event { 541 + Start(_) => nest += 1, 542 + End(_) => { 543 + if nest == 0 { 544 + break; 545 + } 546 + nest -= 1; 547 + } 548 + Html(_) => {} 549 + InlineHtml(text) | Code(text) | Text(text) => { 550 + // Don't use escape_html_body_text here. 551 + // The output of this function is used in the `alt` attribute. 552 + escape_html(&mut self.writer, &text)?; 553 + self.end_newline = text.ends_with('\n'); 554 + } 555 + InlineMath(text) => { 556 + self.write("$")?; 557 + escape_html(&mut self.writer, &text)?; 558 + self.write("$")?; 559 + } 560 + DisplayMath(text) => { 561 + self.write("$$")?; 562 + escape_html(&mut self.writer, &text)?; 563 + self.write("$$")?; 564 + } 565 + SoftBreak | HardBreak | Rule => { 566 + self.write(" ")?; 567 + } 568 + FootnoteReference(name) => { 569 + let len = self.numbers.len() + 1; 570 + let number = *self.numbers.entry(name).or_insert(len); 571 + write!(&mut self.writer, "[{}]", number)?; 572 + } 573 + TaskListMarker(true) => self.write("[x]")?, 574 + TaskListMarker(false) => self.write("[ ]")?, 575 + WeaverBlock(_) => { 576 + println!("Weaver block internal"); 577 + } 578 + } 579 + } 580 + Ok(()) 581 + } 582 + } 583 + 584 + /// Iterate over an `Iterator` of `Event`s, generate HTML for each `Event`, and 585 + /// push it to a `String`. 586 + /// 587 + /// # Examples 588 + /// 589 + /// ``` 590 + /// use pulldown_cmark::{html, Parser}; 591 + /// 592 + /// let markdown_str = r#" 593 + /// hello 594 + /// ===== 595 + /// 596 + /// * alpha 597 + /// * beta 598 + /// "#; 599 + /// let parser = Parser::new(markdown_str); 600 + /// 601 + /// let mut html_buf = String::new(); 602 + /// html::push_html(&mut html_buf, parser); 603 + /// 604 + /// assert_eq!(html_buf, r#"<h1>hello</h1> 605 + /// <ul> 606 + /// <li>alpha</li> 607 + /// <li>beta</li> 608 + /// </ul> 609 + /// "#); 610 + /// ``` 611 + pub fn push_html<'a, I>(s: &mut String, iter: I) 612 + where 613 + I: Iterator<Item = Event<'a>>, 614 + { 615 + write_html_fmt(s, iter).unwrap() 616 + } 617 + 618 + /// Iterate over an `Iterator` of `Event`s, generate HTML for each `Event`, and 619 + /// write it out to an I/O stream. 620 + /// 621 + /// **Note**: using this function with an unbuffered writer like a file or socket 622 + /// will result in poor performance. Wrap these in a 623 + /// [`BufWriter`](https://doc.rust-lang.org/std/io/struct.BufWriter.html) to 624 + /// prevent unnecessary slowdowns. 625 + /// 626 + /// # Examples 627 + /// 628 + /// ``` 629 + /// use pulldown_cmark::{html, Parser}; 630 + /// use std::io::Cursor; 631 + /// 632 + /// let markdown_str = r#" 633 + /// hello 634 + /// ===== 635 + /// 636 + /// * alpha 637 + /// * beta 638 + /// "#; 639 + /// let mut bytes = Vec::new(); 640 + /// let parser = Parser::new(markdown_str); 641 + /// 642 + /// html::write_html_io(Cursor::new(&mut bytes), parser); 643 + /// 644 + /// assert_eq!(&String::from_utf8_lossy(&bytes)[..], r#"<h1>hello</h1> 645 + /// <ul> 646 + /// <li>alpha</li> 647 + /// <li>beta</li> 648 + /// </ul> 649 + /// "#); 650 + /// ``` 651 + //#[cfg(feature = "std")] 652 + pub fn write_html_io<'a, I, W>(writer: W, iter: I) -> std::io::Result<()> 653 + where 654 + I: Iterator<Item = Event<'a>>, 655 + W: std::io::Write, 656 + { 657 + HtmlWriter::new(iter, IoWriter(writer)).run() 658 + } 659 + 660 + /// Iterate over an `Iterator` of `Event`s, generate HTML for each `Event`, and 661 + /// write it into Unicode-accepting buffer or stream. 662 + /// 663 + /// # Examples 664 + /// 665 + /// ``` 666 + /// use pulldown_cmark::{html, Parser}; 667 + /// 668 + /// let markdown_str = r#" 669 + /// hello 670 + /// ===== 671 + /// 672 + /// * alpha 673 + /// * beta 674 + /// "#; 675 + /// let mut buf = String::new(); 676 + /// let parser = Parser::new(markdown_str); 677 + /// 678 + /// html::write_html_fmt(&mut buf, parser); 679 + /// 680 + /// assert_eq!(buf, r#"<h1>hello</h1> 681 + /// <ul> 682 + /// <li>alpha</li> 683 + /// <li>beta</li> 684 + /// </ul> 685 + /// "#); 686 + /// ``` 687 + pub fn write_html_fmt<'a, I, W>(writer: W, iter: I) -> core::fmt::Result 688 + where 689 + I: Iterator<Item = Event<'a>>, 690 + W: core::fmt::Write, 691 + { 692 + HtmlWriter::new(iter, FmtWriter(writer)).run() 693 + }
+49
crates/weaver-renderer/src/code_pretty.rs
··· 1 + use markdown_weaver_escape::StrWrite; 2 + // use syntect::highlighting::ThemeSet; 3 + // use syntect::html::css_for_theme_with_class_style; 4 + use syntect::html::{ClassStyle, ClassedHTMLGenerator}; 5 + use syntect::parsing::SyntaxSet; 6 + use syntect::util::LinesWithEndings; 7 + 8 + /// Perform syntax highlighting on a code block. 9 + /// This requires an external stylesheet, also generated by syntect to be loaded by the page. 10 + /// The syntect SyntaxSet is also provided, so that it is not re-created on every call. 11 + pub fn highlight<M>( 12 + syn_set: SyntaxSet, 13 + lang: Option<&str>, 14 + code: impl AsRef<str>, 15 + writer: &mut M, 16 + ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> 17 + where 18 + M: StrWrite, 19 + <M as StrWrite>::Error: std::error::Error + Send + Sync + 'static, 20 + { 21 + let lang_syn = if let Some(lang) = lang { 22 + syn_set 23 + .find_syntax_by_token(lang) 24 + .unwrap_or_else(|| syn_set.find_syntax_plain_text()) 25 + } else { 26 + syn_set 27 + .find_syntax_by_first_line(code.as_ref()) 28 + .unwrap_or_else(|| syn_set.find_syntax_plain_text()) 29 + }; 30 + writer.write_str("<pre><code class=\"language-")?; 31 + writer.write_str(&lang_syn.name)?; 32 + writer.write_str("\">")?; 33 + 34 + let mut html_gen = ClassedHTMLGenerator::new_with_class_style( 35 + lang_syn, 36 + &syn_set, 37 + ClassStyle::SpacedPrefixed { prefix: CSS_PREFIX }, 38 + ); 39 + for line in LinesWithEndings::from(code.as_ref()) { 40 + html_gen 41 + .parse_html_for_line_which_includes_newline(line) 42 + .unwrap(); 43 + } 44 + writer.write_str(&html_gen.finalize())?; 45 + writer.write_str("</code></pre>")?; 46 + Ok(()) 47 + } 48 + 49 + pub const CSS_PREFIX: &str = "wvrcode-";
+323
crates/weaver-renderer/src/lib.rs
··· 3 3 //! This crate works with the weaver-markdown crate to render and optionally upload markdown notebooks to your Atproto PDS. 4 4 //! 5 5 6 + use async_trait::async_trait; 7 + use markdown_weaver::CowStr; 8 + use markdown_weaver::Event; 9 + use markdown_weaver::LinkType; 10 + use markdown_weaver::Tag; 11 + use n0_future::Stream; 12 + use n0_future::StreamExt; 13 + use n0_future::pin; 14 + use n0_future::stream::once_future; 15 + use yaml_rust2::Yaml; 16 + use yaml_rust2::YamlLoader; 17 + 18 + use regex::Regex; 19 + use std::iter::Iterator; 20 + use std::path::PathBuf; 21 + use std::pin::Pin; 22 + use std::sync::Arc; 23 + use std::sync::LazyLock; 24 + use std::sync::RwLock; 25 + use std::task::Poll; 26 + 27 + pub mod atproto; 28 + pub mod base_html; 29 + pub mod code_pretty; 30 + pub mod static_site; 6 31 pub mod types; 32 + pub mod utils; 33 + pub mod walker; 34 + 35 + pub static OBSIDIAN_NOTE_LINK_RE: LazyLock<Regex> = LazyLock::new(|| { 36 + Regex::new(r"^(?P<file>[^#|]+)??(#(?P<section>.+?))??(\|(?P<label>.+?))??$").unwrap() 37 + }); 38 + 39 + #[derive(Debug, Default)] 40 + pub struct ContextIterator<'a, I: Iterator<Item = Event<'a>>> { 41 + pub context: Option<EventContext>, 42 + pub iter: I, 43 + _phantom: std::marker::PhantomData<&'a ()>, 44 + } 45 + 46 + impl<'a, I: Iterator<Item = Event<'a>>> ContextIterator<'a, I> { 47 + pub fn new(context: EventContext, iter: I) -> Self { 48 + Self { 49 + context: Some(context), 50 + iter, 51 + _phantom: std::marker::PhantomData, 52 + } 53 + } 54 + 55 + pub fn default(iter: I) -> Self { 56 + Self { 57 + context: None, 58 + iter, 59 + _phantom: std::marker::PhantomData, 60 + } 61 + } 62 + } 63 + 64 + impl<'a, I: Iterator<Item = Event<'a>>> Iterator for ContextIterator<'a, I> { 65 + type Item = (Event<'a>, EventContext); 66 + 67 + fn next(&mut self) -> Option<Self::Item> { 68 + if let Some(next) = self.iter.next() { 69 + let ctxt = EventContext::get_context(&next, self.context.as_ref()); 70 + self.context = Some(ctxt); 71 + Some((next, ctxt)) 72 + } else { 73 + None 74 + } 75 + } 76 + } 77 + 78 + #[derive(Debug, Default)] 79 + #[pin_project::pin_project] 80 + pub struct NotebookProcessor<'a, I: Iterator<Item = Event<'a>>, CTX> { 81 + context: CTX, 82 + iter: ContextIterator<'a, I>, 83 + } 84 + 85 + impl<'a, I: Iterator<Item = Event<'a>>, CTX> NotebookProcessor<'a, I, CTX> { 86 + pub fn new(ctx: CTX, iter: ContextIterator<'a, I>) -> Self { 87 + Self { context: ctx, iter } 88 + } 89 + } 90 + 91 + impl<'a, I: Iterator<Item = Event<'a>>, CTX: NotebookContext> Stream 92 + for NotebookProcessor<'a, I, CTX> 93 + { 94 + type Item = Event<'a>; 95 + 96 + fn size_hint(&self) -> (usize, Option<usize>) { 97 + self.iter.size_hint() 98 + } 99 + fn poll_next( 100 + self: Pin<&mut Self>, 101 + cx: &mut std::task::Context<'_>, 102 + ) -> Poll<Option<Self::Item>> { 103 + let this = self.project(); 104 + let iter: &mut ContextIterator<'a, I> = this.iter; 105 + if let Some((event, ctxt)) = iter.next() { 106 + match ctxt { 107 + EventContext::EmbedLink => match event { 108 + Event::Start(ref tag) => match tag { 109 + Tag::Embed { .. } => { 110 + let fut = once_future(this.context.handle_embed(tag.clone())); 111 + pin!(fut); 112 + fut.poll_next(cx) 113 + .map(|tag| tag.map(|t| Event::Start(t.into_static()))) 114 + } 115 + _ => Poll::Ready(Some(event)), 116 + }, 117 + _ => Poll::Ready(Some(event)), 118 + }, 119 + EventContext::CodeBlock => Poll::Ready(Some(event)), 120 + EventContext::Text => Poll::Ready(Some(event)), 121 + EventContext::Html => Poll::Ready(Some(event)), 122 + EventContext::Heading => Poll::Ready(Some(event)), 123 + EventContext::Reference => match event { 124 + Event::Start(ref tag) => match tag { 125 + Tag::Link { .. } => { 126 + let fut = once_future(this.context.handle_link(tag.clone())); 127 + pin!(fut); 128 + fut.poll_next(cx).map(|tag| tag.map(|t| Event::Start(t))) 129 + } 130 + _ => Poll::Ready(Some(event)), 131 + }, 132 + Event::FootnoteReference(ref name) => { 133 + this.context.handle_reference(name.clone()); 134 + Poll::Ready(Some(event)) 135 + } 136 + _ => Poll::Ready(Some(event)), 137 + }, 138 + EventContext::RefDef => match event { 139 + Event::Start(ref tag) => match tag { 140 + Tag::FootnoteDefinition(name) => { 141 + this.context.add_reference(name.clone()); 142 + Poll::Ready(Some(event)) 143 + } 144 + _ => Poll::Ready(Some(event)), 145 + }, 146 + _ => Poll::Ready(Some(event)), 147 + }, 148 + EventContext::Link => match event { 149 + Event::Start(ref tag) => match tag { 150 + Tag::Link { .. } => { 151 + let fut = once_future(this.context.handle_link(tag.clone())); 152 + pin!(fut); 153 + fut.poll_next(cx).map(|tag| tag.map(|t| Event::Start(t))) 154 + } 155 + _ => Poll::Ready(Some(event)), 156 + }, 157 + _ => Poll::Ready(Some(event)), 158 + }, 159 + EventContext::Image => match event { 160 + Event::Start(ref tag) => match tag { 161 + Tag::Image { .. } => { 162 + let fut = once_future(this.context.handle_image(tag.clone())); 163 + pin!(fut); 164 + fut.poll_next(cx).map(|tag| tag.map(|t| Event::Start(t))) 165 + } 166 + _ => Poll::Ready(Some(event)), 167 + }, 168 + _ => Poll::Ready(Some(event)), 169 + }, 170 + 171 + EventContext::Table => Poll::Ready(Some(event)), 172 + EventContext::Metadata => match event { 173 + Event::Text(ref text) => { 174 + let frontmatter = Frontmatter::new(&text); 175 + this.context.set_frontmatter(frontmatter); 176 + Poll::Ready(Some(event)) 177 + } 178 + _ => Poll::Ready(Some(event)), 179 + }, 180 + EventContext::Other => Poll::Ready(Some(event)), 181 + EventContext::None => Poll::Ready(Some(event)), 182 + } 183 + } else { 184 + Poll::Ready(None) 185 + } 186 + } 187 + } 188 + 189 + #[async_trait] 190 + pub trait NotebookContext { 191 + fn set_entry_title(&self, title: CowStr<'_>); 192 + fn entry_title(&self) -> CowStr<'_>; 193 + fn normalized_entry_title(&self) -> CowStr<'_> { 194 + let title = self.entry_title(); 195 + let mut normalized = String::new(); 196 + for c in title.chars() { 197 + if c.is_ascii_alphanumeric() { 198 + normalized.push(c); 199 + } else if c.is_whitespace() && !normalized.is_empty() && !(c == '\n' || c == '\r') { 200 + normalized.push('-'); 201 + } else if c == '\n' { 202 + normalized.push('_'); 203 + } else if c == '\r' { 204 + continue; 205 + } else if !crate::utils::AVOID_URL_CHARS.contains(&c) { 206 + normalized.push(c); 207 + } 208 + } 209 + CowStr::Boxed(normalized.into_boxed_str()) 210 + } 211 + fn frontmatter(&self) -> Frontmatter; 212 + fn set_frontmatter(&self, frontmatter: Frontmatter); 213 + async fn handle_link<'s>(&self, link: Tag<'s>) -> Tag<'s>; 214 + async fn handle_image<'s>(&self, image: Tag<'s>) -> Tag<'s>; 215 + async fn handle_embed<'s>(&self, embed: Tag<'s>) -> Tag<'s>; 216 + fn handle_reference(&self, reference: CowStr<'_>) -> CowStr<'_>; 217 + fn add_reference(&self, reference: CowStr<'_>); 218 + } 219 + 220 + #[derive(Debug, Clone)] 221 + pub struct Frontmatter { 222 + yaml: Arc<RwLock<Vec<Yaml>>>, 223 + } 224 + 225 + impl Frontmatter { 226 + pub fn new(text: &str) -> Self { 227 + let yaml = YamlLoader::load_from_str(text).unwrap_or_else(|_| vec![Yaml::BadValue]); 228 + Self { 229 + yaml: Arc::new(RwLock::new(yaml)), 230 + } 231 + } 232 + 233 + pub fn contents(&self) -> Arc<RwLock<Vec<Yaml>>> { 234 + self.yaml.clone() 235 + } 236 + } 237 + 238 + impl Default for Frontmatter { 239 + fn default() -> Self { 240 + Frontmatter { 241 + yaml: Arc::new(RwLock::new(vec![])), 242 + } 243 + } 244 + } 245 + 246 + #[derive(thiserror::Error, Debug, miette::Diagnostic)] 247 + pub enum RenderError { 248 + #[error("WalkDir error at {}", path.display())] 249 + #[diagnostic(code(crate::static_site::walker))] 250 + WalkDirError { path: PathBuf, msg: String }, 251 + } 252 + 253 + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] 254 + pub enum EventContext { 255 + EmbedLink, 256 + CodeBlock, 257 + #[default] 258 + Text, 259 + Html, 260 + Heading, 261 + Reference, 262 + RefDef, 263 + Image, 264 + Link, 265 + Table, 266 + Metadata, 267 + Other, 268 + None, 269 + } 270 + 271 + impl EventContext { 272 + pub fn get_context<'a>(event: &Event<'a>, prev: Option<&Self>) -> Self { 273 + match event { 274 + Event::Start(tag) => match tag { 275 + Tag::Paragraph => Self::Text, 276 + Tag::Heading { .. } => Self::Heading, 277 + Tag::BlockQuote(_block_quote_kind) => Self::Text, 278 + Tag::CodeBlock(_code_block_kind) => Self::CodeBlock, 279 + Tag::HtmlBlock => Self::Text, 280 + Tag::List(_) => Self::Other, 281 + Tag::Item => Self::Other, 282 + Tag::FootnoteDefinition(_cow_str) => Self::RefDef, 283 + Tag::DefinitionList => Self::Other, 284 + Tag::DefinitionListTitle => Self::Other, 285 + Tag::DefinitionListDefinition => Self::Other, 286 + Tag::Table(_alignments) => Self::Table, 287 + Tag::TableHead => Self::Table, 288 + Tag::TableRow => Self::Table, 289 + Tag::TableCell => Self::Table, 290 + Tag::Emphasis => Self::Text, 291 + Tag::Strong => Self::Text, 292 + Tag::Strikethrough => Self::Text, 293 + Tag::Superscript => Self::Text, 294 + Tag::Subscript => Self::Text, 295 + Tag::Link { .. } => Self::Link, 296 + Tag::Image { .. } => Self::Image, 297 + Tag::Embed { .. } => Self::EmbedLink, 298 + Tag::WeaverBlock(_weaver_block_kind, _weaver_attributes) => Self::Metadata, 299 + Tag::MetadataBlock(_metadata_block_kind) => Self::Metadata, 300 + }, 301 + Event::End(_tag_end) => Self::None, 302 + Event::Text(_cow_str) => match prev { 303 + Some(ctxt) => match ctxt { 304 + EventContext::None => Self::Text, 305 + _ => *ctxt, 306 + }, 307 + None => Self::Text, 308 + }, 309 + Event::Code(_cow_str) => Self::CodeBlock, 310 + Event::InlineMath(_cow_str) => Self::Other, 311 + Event::DisplayMath(_cow_str) => Self::Other, 312 + Event::Html(_cow_str) => Self::Html, 313 + Event::InlineHtml(_cow_str) => Self::Html, 314 + Event::FootnoteReference(_cow_str) => Self::Reference, 315 + Event::SoftBreak => Self::Other, 316 + Event::HardBreak => Self::Other, 317 + Event::Rule => Self::Other, 318 + Event::TaskListMarker(_cow_str) => Self::Other, 319 + Event::WeaverBlock(_cow_str) => Self::Other, 320 + } 321 + } 322 + 323 + pub fn is_non_writing_block(&self) -> bool { 324 + match self { 325 + Self::Metadata => true, 326 + _ => false, 327 + } 328 + } 329 + }
+1245
crates/weaver-renderer/src/static_site.rs
··· 1 + //! Static renderer 2 + //! 3 + //! This mode of the renderer creates a static html and css website from a notebook in a local directory. 4 + //! It does not upload it to the PDS by default (though it can ). This is good for testing and for self-hosting. 5 + //! URLs in the notebook are mostly unaltered. It is compatible with GitHub or Cloudflare Pages 6 + //! and other similar static hosting services. 7 + 8 + use std::{ 9 + path::{Path, PathBuf}, 10 + sync::Arc, 11 + }; 12 + 13 + use crate::{ContextIterator, NotebookProcessor, base_html::TableState, walker::WalkOptions}; 14 + use async_trait::async_trait; 15 + use atrium_api::agent::{Configure, SessionManager}; 16 + use bitflags::bitflags; 17 + use dashmap::DashMap; 18 + use markdown_weaver::{ 19 + Alignment, BlockQuoteKind, BrokenLink, CodeBlockKind, CowStr, EmbedType, Event, LinkType, 20 + Parser, Tag, WeaverAttributes, 21 + }; 22 + use markdown_weaver_escape::{ 23 + FmtWriter, StrWrite, escape_href, escape_html, escape_html_body_text, 24 + }; 25 + use miette::IntoDiagnostic; 26 + use n0_future::StreamExt; 27 + #[cfg(all(target_family = "wasm", target_os = "unknown"))] 28 + use n0_future::io::AsyncWriteExt; 29 + #[cfg(not(all(target_family = "wasm", target_os = "unknown")))] 30 + use tokio::io::AsyncWriteExt; 31 + use unicode_normalization::UnicodeNormalization; 32 + use weaver_common::{ 33 + agent::{WeaverAgent, WeaverHttpClient}, 34 + aturi_to_http, 35 + }; 36 + use yaml_rust2::Yaml; 37 + 38 + use crate::{Frontmatter, NotebookContext}; 39 + 40 + bitflags! { 41 + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 42 + pub struct StaticSiteOptions:u32 { 43 + const FLATTEN_STRUCTURE = 1 << 1; 44 + const UPLOAD_BLOBS = 1 << 2; 45 + const INLINE_EMBEDS = 1 << 3; 46 + const ADD_LINK_PREVIEWS = 1 << 4; 47 + const RESOLVE_AT_IDENTIFIERS = 1 << 5; 48 + const RESOLVE_AT_URIS = 1 << 6; 49 + const ADD_BSKY_COMMENTS_EMBED = 1 << 7; 50 + const CREATE_INDEX = 1 << 8; 51 + const CREATE_CHAPTERS_BY_DIRECTORY = 1 << 9; 52 + const CREATE_PAGES_BY_TITLE = 1 << 10; 53 + const NORMALIZE_DIR_NAMES = 1 << 11; 54 + const ADD_TOC_TO_PAGES = 1 << 12; 55 + } 56 + } 57 + 58 + impl Default for StaticSiteOptions { 59 + fn default() -> Self { 60 + Self::FLATTEN_STRUCTURE 61 + | Self::UPLOAD_BLOBS 62 + | Self::RESOLVE_AT_IDENTIFIERS 63 + | Self::RESOLVE_AT_URIS 64 + | Self::CREATE_INDEX 65 + | Self::CREATE_CHAPTERS_BY_DIRECTORY 66 + | Self::CREATE_PAGES_BY_TITLE 67 + | Self::NORMALIZE_DIR_NAMES 68 + } 69 + } 70 + 71 + pub fn default_md_options() -> markdown_weaver::Options { 72 + markdown_weaver::Options::ENABLE_WIKILINKS 73 + | markdown_weaver::Options::ENABLE_FOOTNOTES 74 + | markdown_weaver::Options::ENABLE_TABLES 75 + | markdown_weaver::Options::ENABLE_GFM 76 + | markdown_weaver::Options::ENABLE_STRIKETHROUGH 77 + | markdown_weaver::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS 78 + | markdown_weaver::Options::ENABLE_OBSIDIAN_EMBEDS 79 + | markdown_weaver::Options::ENABLE_MATH 80 + | markdown_weaver::Options::ENABLE_HEADING_ATTRIBUTES 81 + } 82 + 83 + pub struct StaticSiteContext<'a, M: SessionManager + Send + Sync> { 84 + options: StaticSiteOptions, 85 + md_options: markdown_weaver::Options, 86 + pub bsky_appview: CowStr<'a>, 87 + root: PathBuf, 88 + pub destination: PathBuf, 89 + start_at: PathBuf, 90 + pub frontmatter: Arc<DashMap<PathBuf, Frontmatter>>, 91 + dir_contents: Option<Arc<[PathBuf]>>, 92 + reference_map: Arc<DashMap<CowStr<'a>, PathBuf>>, 93 + titles: Arc<DashMap<PathBuf, CowStr<'a>>>, 94 + position: usize, 95 + agent: Option<Arc<WeaverAgent<M>>>, 96 + } 97 + 98 + impl<M: SessionManager + Send + Sync> StaticSiteContext<'_, M> { 99 + pub fn clone_with_dir_contents(&self, dir_contents: &[PathBuf]) -> Self { 100 + Self { 101 + start_at: self.start_at.clone(), 102 + root: self.root.clone(), 103 + bsky_appview: self.bsky_appview.clone(), 104 + options: self.options.clone(), 105 + md_options: self.md_options.clone(), 106 + frontmatter: self.frontmatter.clone(), 107 + dir_contents: Some(Arc::from(dir_contents)), 108 + destination: self.destination.clone(), 109 + reference_map: self.reference_map.clone(), 110 + titles: self.titles.clone(), 111 + position: self.position, 112 + agent: self.agent.clone(), 113 + } 114 + } 115 + 116 + pub fn clone_with_path(&self, path: impl AsRef<Path>) -> Self { 117 + let position = if let Some(dir_contents) = &self.dir_contents { 118 + dir_contents 119 + .iter() 120 + .position(|p| p == path.as_ref()) 121 + .unwrap_or(0) 122 + } else { 123 + 0 124 + }; 125 + Self { 126 + start_at: self.start_at.clone(), 127 + root: self.root.clone(), 128 + bsky_appview: self.bsky_appview.clone(), 129 + options: self.options.clone(), 130 + md_options: self.md_options.clone(), 131 + frontmatter: self.frontmatter.clone(), 132 + dir_contents: self.dir_contents.clone(), 133 + destination: self.destination.clone(), 134 + reference_map: self.reference_map.clone(), 135 + titles: self.titles.clone(), 136 + position, 137 + agent: self.agent.clone(), 138 + } 139 + } 140 + pub fn new(root: PathBuf, destination: PathBuf, session: Option<M>) -> Self { 141 + Self { 142 + start_at: root.clone(), 143 + root, 144 + bsky_appview: CowStr::Borrowed("deer.social"), 145 + options: StaticSiteOptions::default(), 146 + md_options: default_md_options(), 147 + frontmatter: Arc::new(DashMap::new()), 148 + dir_contents: None, 149 + destination, 150 + reference_map: Arc::new(DashMap::new()), 151 + titles: Arc::new(DashMap::new()), 152 + position: 0, 153 + agent: session.map(|session| Arc::new(WeaverAgent::new(session))), 154 + } 155 + } 156 + 157 + pub fn current_path(&self) -> &PathBuf { 158 + if let Some(dir_contents) = &self.dir_contents { 159 + &dir_contents[self.position] 160 + } else { 161 + &self.start_at 162 + } 163 + } 164 + 165 + #[inline] 166 + pub fn handle_link_aturi<'s>(&self, link: Tag<'s>) -> Tag<'s> { 167 + let link = crate::utils::resolve_at_ident_or_uri(&link, &self.bsky_appview); 168 + self.handle_link_normal(link) 169 + } 170 + 171 + pub async fn handle_embed_aturi<'s>(&self, embed: Tag<'s>) -> Tag<'s> { 172 + if let Some(agent) = &self.agent { 173 + match &embed { 174 + Tag::Embed { 175 + embed_type, 176 + dest_url, 177 + title, 178 + id, 179 + attrs, 180 + } => { 181 + if dest_url.starts_with("at://") { 182 + let width = if let Some(attrs) = attrs { 183 + let mut width = 600; 184 + for attr in &attrs.attrs { 185 + if attr.0 == CowStr::Borrowed("width".into()) { 186 + width = attr.1.parse::<usize>().unwrap_or(600); 187 + break; 188 + } 189 + } 190 + width 191 + } else { 192 + 600 193 + }; 194 + let html = if let Ok(resp) = agent 195 + .client 196 + .client 197 + .get("https://embed.bsky.app/oembed") 198 + .query(&[ 199 + ("url", dest_url.clone().into_string()), 200 + ("maxwidth", width.to_string()), 201 + ]) 202 + .send() 203 + .await 204 + { 205 + resp.text().await.ok() 206 + } else { 207 + None 208 + }; 209 + if let Some(html) = html { 210 + let link = aturi_to_http(&dest_url, &self.bsky_appview) 211 + .expect("assuming the at-uri is valid rn"); 212 + let mut attrs = if let Some(attrs) = attrs { 213 + attrs.clone() 214 + } else { 215 + WeaverAttributes { 216 + classes: vec![], 217 + attrs: vec![], 218 + } 219 + }; 220 + attrs.attrs.push(("content".into(), html.into())); 221 + Tag::Embed { 222 + embed_type: EmbedType::Comments, // change this when i update markdown-weaver 223 + dest_url: link.into_static(), 224 + title: title.clone(), 225 + id: id.clone(), 226 + attrs: Some(attrs), 227 + } 228 + } else { 229 + self.handle_embed_normal(embed).await 230 + } 231 + } else { 232 + self.handle_embed_normal(embed).await 233 + } 234 + } 235 + _ => embed, 236 + } 237 + } else { 238 + self.handle_embed_normal(embed).await 239 + } 240 + } 241 + 242 + pub async fn handle_embed_normal<'s>(&self, embed: Tag<'s>) -> Tag<'s> { 243 + // This option will REALLY slow down iteration over events. 244 + if self.options.contains(StaticSiteOptions::INLINE_EMBEDS) { 245 + match &embed { 246 + Tag::Embed { 247 + embed_type: _, 248 + dest_url, 249 + title, 250 + id, 251 + attrs, 252 + } => { 253 + let mut attrs = if let Some(attrs) = attrs { 254 + attrs.clone() 255 + } else { 256 + WeaverAttributes { 257 + classes: vec![], 258 + attrs: vec![], 259 + } 260 + }; 261 + let contents = if crate::utils::is_local_path(dest_url) { 262 + let file_path = if crate::utils::is_relative_link(dest_url) { 263 + let root_path = self.root.clone(); 264 + root_path.join(Path::new(&dest_url as &str)) 265 + } else { 266 + PathBuf::from(&dest_url as &str) 267 + }; 268 + crate::utils::inline_file(&file_path).await 269 + } else if let Some(agent) = &self.agent { 270 + if let Ok(resp) = agent 271 + .client 272 + .client 273 + .get(dest_url.clone().into_string()) 274 + .send() 275 + .await 276 + { 277 + resp.text().await.ok() 278 + } else { 279 + None 280 + } 281 + } else { 282 + None 283 + }; 284 + if let Some(contents) = contents { 285 + attrs.attrs.push(("content".into(), contents.into())); 286 + Tag::Embed { 287 + embed_type: EmbedType::Markdown, // change this when i update markdown-weaver 288 + dest_url: dest_url.clone(), 289 + title: title.clone(), 290 + id: id.clone(), 291 + attrs: Some(attrs), 292 + } 293 + } else { 294 + embed 295 + } 296 + } 297 + _ => embed, 298 + } 299 + } else { 300 + embed 301 + } 302 + } 303 + 304 + /// This is a no-op for the static site renderer currently. 305 + #[inline] 306 + pub fn handle_link_normal<'s>(&self, link: Tag<'s>) -> Tag<'s> { 307 + link 308 + } 309 + 310 + /// This is a no-op for the static site renderer currently. 311 + #[inline] 312 + pub fn handle_image_normal<'s>(&self, image: Tag<'s>) -> Tag<'s> { 313 + image 314 + } 315 + } 316 + 317 + impl<M: SessionManager + Configure + Send + Sync> StaticSiteContext<'_, M> { 318 + pub async fn upload_image<'s>(&self, image: Tag<'s>) -> Tag<'s> { 319 + if let Some(agent) = &self.agent { 320 + match &image { 321 + Tag::Image { 322 + link_type, 323 + dest_url, 324 + title, 325 + id, 326 + attrs, 327 + } => { 328 + if crate::utils::is_local_path(&dest_url) { 329 + let root_path = self.root.clone(); 330 + let file_path = root_path.join(Path::new(&dest_url as &str)); 331 + if let Ok(bytes) = std::fs::read(&file_path) { 332 + if let Ok(blob_data) = agent.upload_blob(bytes).await { 333 + let url = weaver_common::blob_url( 334 + agent.did().await.as_ref().unwrap(), 335 + &agent.pds(), 336 + &blob_data.blob, 337 + ); 338 + return Tag::Image { 339 + link_type: *link_type, 340 + dest_url: url.into(), 341 + title: title.clone(), 342 + id: id.clone(), 343 + attrs: attrs.clone(), 344 + }; 345 + } 346 + } 347 + } 348 + } 349 + _ => {} 350 + } 351 + } 352 + image 353 + } 354 + } 355 + 356 + #[async_trait] 357 + impl<M: SessionManager + Configure + Send + Sync> NotebookContext for StaticSiteContext<'_, M> { 358 + fn set_entry_title(&self, title: CowStr<'_>) { 359 + let path = self.current_path(); 360 + self.titles 361 + .insert(path.clone(), title.clone().into_static()); 362 + self.frontmatter.get_mut(path).map(|frontmatter| { 363 + if let Ok(mut yaml) = frontmatter.yaml.write() { 364 + if yaml.get(0).is_some_and(|y| y.is_hash()) { 365 + let map = yaml.get_mut(0).unwrap().as_mut_hash().unwrap(); 366 + map.insert( 367 + Yaml::String("title".into()), 368 + Yaml::String(title.into_static().into()), 369 + ); 370 + } 371 + } 372 + }); 373 + } 374 + fn entry_title(&self) -> CowStr<'_> { 375 + let path = self.current_path(); 376 + self.titles.get(path).unwrap().clone() 377 + } 378 + 379 + fn frontmatter(&self) -> Frontmatter { 380 + let path = self.current_path(); 381 + self.frontmatter.get(path).unwrap().value().clone() 382 + } 383 + 384 + fn set_frontmatter(&self, frontmatter: Frontmatter) { 385 + let path = self.current_path(); 386 + self.frontmatter.insert(path.clone(), frontmatter); 387 + } 388 + 389 + async fn handle_link<'s>(&self, link: Tag<'s>) -> Tag<'s> { 390 + bitflags::bitflags_match!(self.options, { 391 + // Split this somehow or just combine the options 392 + StaticSiteOptions::RESOLVE_AT_URIS | StaticSiteOptions::RESOLVE_AT_IDENTIFIERS => { 393 + self.handle_link_aturi(link) 394 + } 395 + _ => match &link { 396 + Tag::Link { link_type, dest_url, title, id } => { 397 + if self.options.contains(StaticSiteOptions::FLATTEN_STRUCTURE) { 398 + let (parent, filename) = crate::utils::flatten_dir_to_just_one_parent(&dest_url); 399 + let dest_url = if crate::utils::is_relative_link(&dest_url) 400 + && self.options.contains(StaticSiteOptions::CREATE_CHAPTERS_BY_DIRECTORY) { 401 + CowStr::Boxed(format!("./{}/{}", parent, filename).into_boxed_str()) 402 + } else { 403 + CowStr::Boxed(format!("./entry/{}", filename).into_boxed_str()) 404 + }; 405 + Tag::Link { 406 + link_type: *link_type, 407 + dest_url, 408 + title: title.clone(), 409 + id: id.clone(), 410 + } 411 + } else { 412 + link 413 + 414 + } 415 + }, 416 + _ => link, 417 + } 418 + }) 419 + } 420 + 421 + async fn handle_image<'s>(&self, image: Tag<'s>) -> Tag<'s> { 422 + if self.options.contains(StaticSiteOptions::UPLOAD_BLOBS) { 423 + self.upload_image(image).await 424 + } else { 425 + self.handle_image_normal(image) 426 + } 427 + } 428 + 429 + async fn handle_embed<'s>(&self, embed: Tag<'s>) -> Tag<'s> { 430 + if self.options.contains(StaticSiteOptions::RESOLVE_AT_URIS) 431 + || self.options.contains(StaticSiteOptions::ADD_LINK_PREVIEWS) 432 + { 433 + self.handle_embed_aturi(embed).await 434 + } else { 435 + self.handle_embed_normal(embed).await 436 + } 437 + } 438 + 439 + fn handle_reference(&self, reference: CowStr<'_>) -> CowStr<'_> { 440 + let reference = reference.into_static(); 441 + if let Some(reference) = self.reference_map.get(&reference) { 442 + let path = reference.value().clone(); 443 + CowStr::Boxed(path.to_string_lossy().into_owned().into_boxed_str()) 444 + } else { 445 + reference 446 + } 447 + } 448 + 449 + fn add_reference(&self, reference: CowStr<'_>) { 450 + let path = self.current_path(); 451 + self.reference_map 452 + .insert(reference.into_static(), path.clone()); 453 + } 454 + } 455 + 456 + pub struct StaticSiteWriter<'a, M> 457 + where 458 + M: SessionManager + Send + Sync, 459 + { 460 + context: StaticSiteContext<'a, M>, 461 + } 462 + 463 + impl<'a, M> StaticSiteWriter<'a, M> 464 + where 465 + M: SessionManager + Send + Sync, 466 + { 467 + pub fn new(root: PathBuf, destination: PathBuf, session: Option<M>) -> Self { 468 + let context = StaticSiteContext::new(root, destination, session); 469 + Self { context } 470 + } 471 + } 472 + 473 + impl<'a, M> StaticSiteWriter<'a, M> 474 + where 475 + M: SessionManager + Configure + Send + Sync, 476 + { 477 + pub async fn run(self) -> Result<(), miette::Report> { 478 + todo!() 479 + } 480 + 481 + pub async fn export_page<'s, 'input>( 482 + &'s self, 483 + contents: &'input str, 484 + context: StaticSiteContext<'s, M>, 485 + ) -> Result<String, miette::Report> { 486 + let callback = if let Some(dir_contents) = context.dir_contents.clone() { 487 + Some(VaultBrokenLinkCallback { 488 + vault_contents: dir_contents, 489 + }) 490 + } else { 491 + None 492 + }; 493 + let parser = Parser::new_with_broken_link_callback(&contents, context.md_options, callback); 494 + let iterator = ContextIterator::default(parser); 495 + let mut output = String::new(); 496 + let writer = StaticPageWriter::new( 497 + NotebookProcessor::new(context, iterator), 498 + FmtWriter(&mut output), 499 + ); 500 + writer.run().await.into_diagnostic()?; 501 + Ok(output) 502 + } 503 + 504 + pub async fn write_page(&'a self, path: PathBuf) -> Result<(), miette::Report> { 505 + let contents = tokio::fs::read_to_string(&path).await.into_diagnostic()?; 506 + let mut output_file = crate::utils::create_file(&path).await?; 507 + let context = self.context.clone_with_path(&path); 508 + let output = self.export_page(&contents, context).await?; 509 + output_file 510 + .write_all(output.as_bytes()) 511 + .await 512 + .into_diagnostic()?; 513 + Ok(()) 514 + } 515 + } 516 + 517 + pub struct StaticPageWriter< 518 + 'a, 519 + 'input, 520 + I: Iterator<Item = Event<'input>>, 521 + M: SessionManager + Send + Sync, 522 + W: StrWrite, 523 + > { 524 + context: NotebookProcessor<'input, I, StaticSiteContext<'a, M>>, 525 + writer: W, 526 + /// Whether or not the last write wrote a newline. 527 + end_newline: bool, 528 + 529 + /// Whether if inside a metadata block (text should not be written) 530 + in_non_writing_block: bool, 531 + 532 + table_state: TableState, 533 + table_alignments: Vec<Alignment>, 534 + table_cell_index: usize, 535 + numbers: DashMap<CowStr<'a>, usize>, 536 + } 537 + 538 + impl<'a, 'input, I: Iterator<Item = Event<'input>>, M: SessionManager + Send + Sync, W: StrWrite> 539 + StaticPageWriter<'a, 'input, I, M, W> 540 + { 541 + pub fn new(context: NotebookProcessor<'input, I, StaticSiteContext<'a, M>>, writer: W) -> Self { 542 + Self { 543 + context, 544 + writer, 545 + end_newline: true, 546 + in_non_writing_block: false, 547 + table_state: TableState::Head, 548 + table_alignments: vec![], 549 + table_cell_index: 0, 550 + numbers: DashMap::new(), 551 + } 552 + } 553 + 554 + /// Writes a new line. 555 + #[inline] 556 + fn write_newline(&mut self) -> Result<(), W::Error> { 557 + self.end_newline = true; 558 + self.writer.write_str("\n") 559 + } 560 + 561 + /// Writes a buffer, and tracks whether or not a newline was written. 562 + #[inline] 563 + fn write(&mut self, s: &str) -> Result<(), W::Error> { 564 + self.writer.write_str(s)?; 565 + 566 + if !s.is_empty() { 567 + self.end_newline = s.ends_with('\n'); 568 + } 569 + Ok(()) 570 + } 571 + 572 + fn end_tag(&mut self, tag: markdown_weaver::TagEnd) -> Result<(), W::Error> { 573 + use markdown_weaver::TagEnd; 574 + match tag { 575 + TagEnd::HtmlBlock => {} 576 + TagEnd::Paragraph => { 577 + self.write("</p>\n")?; 578 + } 579 + TagEnd::Heading(level) => { 580 + self.write("</")?; 581 + write!(&mut self.writer, "{}", level)?; 582 + self.write(">\n")?; 583 + } 584 + TagEnd::Table => { 585 + self.write("</tbody></table>\n")?; 586 + } 587 + TagEnd::TableHead => { 588 + self.write("</tr></thead><tbody>\n")?; 589 + self.table_state = TableState::Body; 590 + } 591 + TagEnd::TableRow => { 592 + self.write("</tr>\n")?; 593 + } 594 + TagEnd::TableCell => { 595 + match self.table_state { 596 + TableState::Head => { 597 + self.write("</th>")?; 598 + } 599 + TableState::Body => { 600 + self.write("</td>")?; 601 + } 602 + } 603 + self.table_cell_index += 1; 604 + } 605 + TagEnd::BlockQuote(_) => { 606 + self.write("</blockquote>\n")?; 607 + } 608 + TagEnd::CodeBlock => { 609 + self.write("</code></pre>\n")?; 610 + } 611 + TagEnd::List(true) => { 612 + self.write("</ol>\n")?; 613 + } 614 + TagEnd::List(false) => { 615 + self.write("</ul>\n")?; 616 + } 617 + TagEnd::Item => { 618 + self.write("</li>\n")?; 619 + } 620 + TagEnd::DefinitionList => { 621 + self.write("</dl>\n")?; 622 + } 623 + TagEnd::DefinitionListTitle => { 624 + self.write("</dt>\n")?; 625 + } 626 + TagEnd::DefinitionListDefinition => { 627 + self.write("</dd>\n")?; 628 + } 629 + TagEnd::Emphasis => { 630 + self.write("</em>")?; 631 + } 632 + TagEnd::Superscript => { 633 + self.write("</sup>")?; 634 + } 635 + TagEnd::Subscript => { 636 + self.write("</sub>")?; 637 + } 638 + TagEnd::Strong => { 639 + self.write("</strong>")?; 640 + } 641 + TagEnd::Strikethrough => { 642 + self.write("</del>")?; 643 + } 644 + TagEnd::Link => { 645 + self.write("</a>")?; 646 + } 647 + TagEnd::Image => (), // shouldn't happen, handled in start 648 + TagEnd::Embed => (), // shouldn't happen, handled in start 649 + TagEnd::WeaverBlock(_) => { 650 + self.in_non_writing_block = false; 651 + } 652 + TagEnd::FootnoteDefinition => { 653 + self.write("</div>\n")?; 654 + } 655 + TagEnd::MetadataBlock(_) => { 656 + self.in_non_writing_block = false; 657 + } 658 + } 659 + Ok(()) 660 + } 661 + } 662 + 663 + impl< 664 + 'a, 665 + 'input, 666 + I: Iterator<Item = Event<'input>>, 667 + M: SessionManager + Configure + Send + Sync, 668 + W: StrWrite, 669 + > StaticPageWriter<'a, 'input, I, M, W> 670 + { 671 + async fn run(mut self) -> Result<(), W::Error> { 672 + while let Some(event) = self.context.next().await { 673 + self.process_event(event).await? 674 + } 675 + Ok(()) 676 + } 677 + 678 + async fn process_event(&mut self, event: Event<'input>) -> Result<(), W::Error> { 679 + use markdown_weaver::Event::*; 680 + match event { 681 + Start(tag) => { 682 + self.start_tag(tag).await?; 683 + } 684 + End(tag) => { 685 + self.end_tag(tag)?; 686 + } 687 + Text(text) => { 688 + if !self.in_non_writing_block { 689 + escape_html_body_text(&mut self.writer, &text)?; 690 + self.end_newline = text.ends_with('\n'); 691 + } 692 + } 693 + Code(text) => { 694 + self.write("<code>")?; 695 + escape_html_body_text(&mut self.writer, &text)?; 696 + self.write("</code>")?; 697 + } 698 + InlineMath(text) => { 699 + self.write(r#"<span class="math math-inline">"#)?; 700 + escape_html(&mut self.writer, &text)?; 701 + self.write("</span>")?; 702 + } 703 + DisplayMath(text) => { 704 + self.write(r#"<span class="math math-display">"#)?; 705 + escape_html(&mut self.writer, &text)?; 706 + self.write("</span>")?; 707 + } 708 + Html(html) | InlineHtml(html) => { 709 + self.write(&html)?; 710 + } 711 + SoftBreak => { 712 + self.write_newline()?; 713 + } 714 + HardBreak => { 715 + self.write("<br />\n")?; 716 + } 717 + Rule => { 718 + if self.end_newline { 719 + self.write("<hr />\n")?; 720 + } else { 721 + self.write("\n<hr />\n")?; 722 + } 723 + } 724 + FootnoteReference(name) => { 725 + let len = self.numbers.len() + 1; 726 + self.write("<sup class=\"footnote-reference\"><a href=\"#")?; 727 + escape_html(&mut self.writer, &name)?; 728 + self.write("\">")?; 729 + let number = *self.numbers.entry(name.into_static()).or_insert(len); 730 + write!(&mut self.writer, "{}", number)?; 731 + self.write("</a></sup>")?; 732 + } 733 + TaskListMarker(true) => { 734 + self.write("<input disabled=\"\" type=\"checkbox\" checked=\"\"/>\n")?; 735 + } 736 + TaskListMarker(false) => { 737 + self.write("<input disabled=\"\" type=\"checkbox\"/>\n")?; 738 + } 739 + WeaverBlock(_text) => {} 740 + } 741 + Ok(()) 742 + } 743 + 744 + // run raw text, consuming end tag 745 + async fn raw_text(&mut self) -> Result<(), W::Error> { 746 + use markdown_weaver::Event::*; 747 + let mut nest = 0; 748 + while let Some(event) = self.context.next().await { 749 + match event { 750 + Start(_) => nest += 1, 751 + End(_) => { 752 + if nest == 0 { 753 + break; 754 + } 755 + nest -= 1; 756 + } 757 + Html(_) => {} 758 + InlineHtml(text) | Code(text) | Text(text) => { 759 + // Don't use escape_html_body_text here. 760 + // The output of this function is used in the `alt` attribute. 761 + escape_html(&mut self.writer, &text)?; 762 + self.end_newline = text.ends_with('\n'); 763 + } 764 + InlineMath(text) => { 765 + self.write("$")?; 766 + escape_html(&mut self.writer, &text)?; 767 + self.write("$")?; 768 + } 769 + DisplayMath(text) => { 770 + self.write("$$")?; 771 + escape_html(&mut self.writer, &text)?; 772 + self.write("$$")?; 773 + } 774 + SoftBreak | HardBreak | Rule => { 775 + self.write(" ")?; 776 + } 777 + FootnoteReference(name) => { 778 + let len = self.numbers.len() + 1; 779 + let number = *self.numbers.entry(name.into_static()).or_insert(len); 780 + write!(&mut self.writer, "[{}]", number)?; 781 + } 782 + TaskListMarker(true) => self.write("[x]")?, 783 + TaskListMarker(false) => self.write("[ ]")?, 784 + WeaverBlock(_) => { 785 + println!("Weaver block internal"); 786 + } 787 + } 788 + } 789 + Ok(()) 790 + } 791 + 792 + /// Writes the start of an HTML tag. 793 + async fn start_tag(&mut self, tag: Tag<'input>) -> Result<(), W::Error> { 794 + match tag { 795 + Tag::HtmlBlock => Ok(()), 796 + Tag::Paragraph => { 797 + if self.end_newline { 798 + self.write("<p>") 799 + } else { 800 + self.write("\n<p>") 801 + } 802 + } 803 + Tag::Heading { 804 + level, 805 + id, 806 + classes, 807 + attrs, 808 + } => { 809 + if self.end_newline { 810 + self.write("<")?; 811 + } else { 812 + self.write("\n<")?; 813 + } 814 + write!(&mut self.writer, "{}", level)?; 815 + if let Some(id) = id { 816 + self.write(" id=\"")?; 817 + escape_html(&mut self.writer, &id)?; 818 + self.write("\"")?; 819 + } 820 + let mut classes = classes.iter(); 821 + if let Some(class) = classes.next() { 822 + self.write(" class=\"")?; 823 + escape_html(&mut self.writer, class)?; 824 + for class in classes { 825 + self.write(" ")?; 826 + escape_html(&mut self.writer, class)?; 827 + } 828 + self.write("\"")?; 829 + } 830 + for (attr, value) in attrs { 831 + self.write(" ")?; 832 + escape_html(&mut self.writer, &attr)?; 833 + if let Some(val) = value { 834 + self.write("=\"")?; 835 + escape_html(&mut self.writer, &val)?; 836 + self.write("\"")?; 837 + } else { 838 + self.write("=\"\"")?; 839 + } 840 + } 841 + self.write(">") 842 + } 843 + Tag::Table(alignments) => { 844 + self.table_alignments = alignments; 845 + self.write("<table>") 846 + } 847 + Tag::TableHead => { 848 + self.table_state = TableState::Head; 849 + self.table_cell_index = 0; 850 + self.write("<thead><tr>") 851 + } 852 + Tag::TableRow => { 853 + self.table_cell_index = 0; 854 + self.write("<tr>") 855 + } 856 + Tag::TableCell => { 857 + match self.table_state { 858 + TableState::Head => { 859 + self.write("<th")?; 860 + } 861 + TableState::Body => { 862 + self.write("<td")?; 863 + } 864 + } 865 + match self.table_alignments.get(self.table_cell_index) { 866 + Some(&Alignment::Left) => self.write(" style=\"text-align: left\">"), 867 + Some(&Alignment::Center) => self.write(" style=\"text-align: center\">"), 868 + Some(&Alignment::Right) => self.write(" style=\"text-align: right\">"), 869 + _ => self.write(">"), 870 + } 871 + } 872 + Tag::BlockQuote(kind) => { 873 + let class_str = match kind { 874 + None => "", 875 + Some(kind) => match kind { 876 + BlockQuoteKind::Note => " class=\"markdown-alert-note\"", 877 + BlockQuoteKind::Tip => " class=\"markdown-alert-tip\"", 878 + BlockQuoteKind::Important => " class=\"markdown-alert-important\"", 879 + BlockQuoteKind::Warning => " class=\"markdown-alert-warning\"", 880 + BlockQuoteKind::Caution => " class=\"markdown-alert-caution\"", 881 + }, 882 + }; 883 + if self.end_newline { 884 + self.write(&format!("<blockquote{}>\n", class_str)) 885 + } else { 886 + self.write(&format!("\n<blockquote{}>\n", class_str)) 887 + } 888 + } 889 + Tag::CodeBlock(info) => { 890 + if !self.end_newline { 891 + self.write_newline()?; 892 + } 893 + match info { 894 + CodeBlockKind::Fenced(info) => { 895 + let lang = info.split(' ').next().unwrap(); 896 + if lang.is_empty() { 897 + self.write("<pre><code>") 898 + } else { 899 + self.write("<pre><code class=\"language-")?; 900 + escape_html(&mut self.writer, lang)?; 901 + self.write("\">") 902 + } 903 + } 904 + CodeBlockKind::Indented => self.write("<pre><code>"), 905 + } 906 + } 907 + Tag::List(Some(1)) => { 908 + if self.end_newline { 909 + self.write("<ol>\n") 910 + } else { 911 + self.write("\n<ol>\n") 912 + } 913 + } 914 + Tag::List(Some(start)) => { 915 + if self.end_newline { 916 + self.write("<ol start=\"")?; 917 + } else { 918 + self.write("\n<ol start=\"")?; 919 + } 920 + write!(&mut self.writer, "{}", start)?; 921 + self.write("\">\n") 922 + } 923 + Tag::List(None) => { 924 + if self.end_newline { 925 + self.write("<ul>\n") 926 + } else { 927 + self.write("\n<ul>\n") 928 + } 929 + } 930 + Tag::Item => { 931 + if self.end_newline { 932 + self.write("<li>") 933 + } else { 934 + self.write("\n<li>") 935 + } 936 + } 937 + Tag::DefinitionList => { 938 + if self.end_newline { 939 + self.write("<dl>\n") 940 + } else { 941 + self.write("\n<dl>\n") 942 + } 943 + } 944 + Tag::DefinitionListTitle => { 945 + if self.end_newline { 946 + self.write("<dt>") 947 + } else { 948 + self.write("\n<dt>") 949 + } 950 + } 951 + Tag::DefinitionListDefinition => { 952 + if self.end_newline { 953 + self.write("<dd>") 954 + } else { 955 + self.write("\n<dd>") 956 + } 957 + } 958 + Tag::Subscript => self.write("<sub>"), 959 + Tag::Superscript => self.write("<sup>"), 960 + Tag::Emphasis => self.write("<em>"), 961 + Tag::Strong => self.write("<strong>"), 962 + Tag::Strikethrough => self.write("<del>"), 963 + Tag::Link { 964 + link_type: LinkType::Email, 965 + dest_url, 966 + title, 967 + id: _, 968 + } => { 969 + self.write("<a href=\"mailto:")?; 970 + escape_href(&mut self.writer, &dest_url)?; 971 + if !title.is_empty() { 972 + self.write("\" title=\"")?; 973 + escape_html(&mut self.writer, &title)?; 974 + } 975 + self.write("\">") 976 + } 977 + Tag::Link { 978 + link_type: _, 979 + dest_url, 980 + title, 981 + id: _, 982 + } => { 983 + self.write("<a href=\"")?; 984 + escape_href(&mut self.writer, &dest_url)?; 985 + if !title.is_empty() { 986 + self.write("\" title=\"")?; 987 + escape_html(&mut self.writer, &title)?; 988 + } 989 + self.write("\">") 990 + } 991 + Tag::Image { 992 + link_type, 993 + dest_url, 994 + title, 995 + id, 996 + attrs, 997 + } => { 998 + self.write_image(Tag::Image { 999 + link_type, 1000 + dest_url, 1001 + title, 1002 + id, 1003 + attrs, 1004 + }) 1005 + .await 1006 + } 1007 + Tag::Embed { 1008 + embed_type, 1009 + dest_url, 1010 + title, 1011 + id, 1012 + attrs, 1013 + } => { 1014 + if let Some(attrs) = attrs { 1015 + if let Some((_, content)) = attrs 1016 + .attrs 1017 + .iter() 1018 + .find(|(attr, _)| attr.as_ref() == "content") 1019 + { 1020 + match embed_type { 1021 + EmbedType::Image => { 1022 + self.write_image(Tag::Image { 1023 + link_type: LinkType::Inline, 1024 + dest_url, 1025 + title, 1026 + id, 1027 + attrs: Some(attrs.clone()), 1028 + }) 1029 + .await? 1030 + } 1031 + EmbedType::Comments => { 1032 + self.write("leaflet would go here\n")?; 1033 + } 1034 + EmbedType::Post => { 1035 + // Bluesky post embed, basically just render the raw html we got 1036 + self.write(content)?; 1037 + self.write_newline()?; 1038 + } 1039 + EmbedType::Markdown => { 1040 + // let context = self 1041 + // .context 1042 + // .context 1043 + // .clone_with_path(&Path::new(&dest_url.to_string())); 1044 + // let callback = 1045 + // if let Some(dir_contents) = context.dir_contents.clone() { 1046 + // Some(VaultBrokenLinkCallback { 1047 + // vault_contents: dir_contents, 1048 + // }) 1049 + // } else { 1050 + // None 1051 + // }; 1052 + // let parser = Parser::new_with_broken_link_callback( 1053 + // &content, 1054 + // context.md_options, 1055 + // callback, 1056 + // ); 1057 + // let iterator = ContextIterator::default(parser); 1058 + // let mut stream = NotebookProcessor::new(context, iterator); 1059 + // while let Some(event) = stream.next().await { 1060 + // self.process_event(event).await?; 1061 + // } 1062 + // 1063 + self.write("markdown embed would go here\n")?; 1064 + } 1065 + EmbedType::Leaflet => { 1066 + self.write("leaflet would go here\n")?; 1067 + } 1068 + EmbedType::Other => { 1069 + self.write("other embed would go here\n")?; 1070 + } 1071 + } 1072 + } 1073 + } else { 1074 + self.write("<iframe src=\"")?; 1075 + escape_href(&mut self.writer, &dest_url)?; 1076 + self.write("\" title=\"")?; 1077 + escape_html(&mut self.writer, &title)?; 1078 + if !id.is_empty() { 1079 + self.write("\" id=\"")?; 1080 + escape_html(&mut self.writer, &id)?; 1081 + self.write("\"")?; 1082 + } 1083 + if let Some(attrs) = attrs { 1084 + self.write(" ")?; 1085 + if !attrs.classes.is_empty() { 1086 + self.write("class=\"")?; 1087 + for class in &attrs.classes { 1088 + escape_html(&mut self.writer, class)?; 1089 + self.write(" ")?; 1090 + } 1091 + self.write("\" ")?; 1092 + } 1093 + if !attrs.attrs.is_empty() { 1094 + for (attr, value) in &attrs.attrs { 1095 + escape_html(&mut self.writer, attr)?; 1096 + self.write("=\"")?; 1097 + escape_html(&mut self.writer, value)?; 1098 + self.write("\" ")?; 1099 + } 1100 + } 1101 + } 1102 + self.write("/>")?; 1103 + } 1104 + Ok(()) 1105 + } 1106 + Tag::WeaverBlock(_, _attrs) => { 1107 + println!("Weaver block"); 1108 + self.in_non_writing_block = true; 1109 + Ok(()) 1110 + } 1111 + Tag::FootnoteDefinition(name) => { 1112 + if self.end_newline { 1113 + self.write("<div class=\"footnote-definition\" id=\"")?; 1114 + } else { 1115 + self.write("\n<div class=\"footnote-definition\" id=\"")?; 1116 + } 1117 + escape_html(&mut self.writer, &name)?; 1118 + self.write("\"><sup class=\"footnote-definition-label\">")?; 1119 + let len = self.numbers.len() + 1; 1120 + let number = *self.numbers.entry(name.into_static()).or_insert(len); 1121 + write!(&mut self.writer, "{}", number)?; 1122 + self.write("</sup>") 1123 + } 1124 + Tag::MetadataBlock(_) => { 1125 + self.in_non_writing_block = true; 1126 + Ok(()) 1127 + } 1128 + } 1129 + } 1130 + 1131 + async fn write_image(&mut self, tag: Tag<'input>) -> Result<(), W::Error> { 1132 + if let Tag::Image { 1133 + link_type: _, 1134 + dest_url, 1135 + title, 1136 + id: _, 1137 + attrs, 1138 + } = tag 1139 + { 1140 + self.write("<img src=\"")?; 1141 + escape_href(&mut self.writer, &dest_url)?; 1142 + if let Some(attrs) = attrs { 1143 + if !attrs.classes.is_empty() { 1144 + self.write("\" class=\"")?; 1145 + for class in &attrs.classes { 1146 + escape_html(&mut self.writer, class)?; 1147 + self.write(" ")?; 1148 + } 1149 + self.write("\" ")?; 1150 + } else { 1151 + self.write("\" ")?; 1152 + } 1153 + if !attrs.attrs.is_empty() { 1154 + for (attr, value) in &attrs.attrs { 1155 + escape_html(&mut self.writer, attr)?; 1156 + self.write("=\"")?; 1157 + escape_html(&mut self.writer, value)?; 1158 + self.write("\" ")?; 1159 + } 1160 + } 1161 + } else { 1162 + self.write("\" ")?; 1163 + } 1164 + self.write("alt=\"")?; 1165 + self.raw_text().await?; 1166 + if !title.is_empty() { 1167 + self.write("\" title=\"")?; 1168 + escape_html(&mut self.writer, &title)?; 1169 + } 1170 + self.write("\" />") 1171 + } else { 1172 + self.write_newline() 1173 + } 1174 + } 1175 + } 1176 + 1177 + /// Path lookup in an Obsidian vault 1178 + /// 1179 + /// Credit to https://github.com/zoni 1180 + /// 1181 + /// Taken from https://github.com/zoni/obsidian-export/blob/main/src/lib.rs.rs on 2025-05-21 1182 + /// 1183 + pub fn lookup_filename_in_vault<'a>( 1184 + filename: &str, 1185 + vault_contents: &'a [PathBuf], 1186 + ) -> Option<&'a PathBuf> { 1187 + let filename = PathBuf::from(filename); 1188 + let filename_normalized: String = filename.to_string_lossy().nfc().collect(); 1189 + 1190 + vault_contents.iter().find(|path| { 1191 + let path_normalized_str: String = path.to_string_lossy().nfc().collect(); 1192 + let path_normalized = PathBuf::from(&path_normalized_str); 1193 + let path_normalized_lowered = PathBuf::from(&path_normalized_str.to_lowercase()); 1194 + 1195 + // It would be convenient if we could just do `filename.set_extension("md")` at the start 1196 + // of this funtion so we don't need multiple separate + ".md" match cases here, however 1197 + // that would break with a reference of `[[Note.1]]` linking to `[[Note.1.md]]`. 1198 + 1199 + path_normalized.ends_with(&filename_normalized) 1200 + || path_normalized.ends_with(filename_normalized.clone() + ".md") 1201 + || path_normalized_lowered.ends_with(filename_normalized.to_lowercase()) 1202 + || path_normalized_lowered.ends_with(filename_normalized.to_lowercase() + ".md") 1203 + }) 1204 + } 1205 + 1206 + pub struct VaultBrokenLinkCallback { 1207 + vault_contents: Arc<[PathBuf]>, 1208 + } 1209 + 1210 + impl<'input> markdown_weaver::BrokenLinkCallback<'input> for VaultBrokenLinkCallback { 1211 + fn handle_broken_link( 1212 + &mut self, 1213 + link: BrokenLink<'input>, 1214 + ) -> Option<(CowStr<'input>, CowStr<'input>)> { 1215 + let text = link.reference; 1216 + let captures = crate::OBSIDIAN_NOTE_LINK_RE 1217 + .captures(&text) 1218 + .expect("note link regex didn't match - bad input?"); 1219 + let file = captures.name("file").map(|v| v.as_str().trim()); 1220 + let label = captures.name("label").map(|v| v.as_str()); 1221 + let section = captures.name("section").map(|v| v.as_str().trim()); 1222 + 1223 + if let Some(file) = file { 1224 + if let Some(path) = lookup_filename_in_vault(file, self.vault_contents.as_ref()) { 1225 + let mut link_text = String::from(path.to_string_lossy()); 1226 + if let Some(section) = section { 1227 + link_text.push('#'); 1228 + link_text.push_str(section); 1229 + if let Some(label) = label { 1230 + let label = label.to_string(); 1231 + Some((CowStr::from(link_text), CowStr::from(label))) 1232 + } else { 1233 + Some((link_text.into(), format!("{} > {}", file, section).into())) 1234 + } 1235 + } else { 1236 + Some((link_text.into(), format!("{}", file).into())) 1237 + } 1238 + } else { 1239 + None 1240 + } 1241 + } else { 1242 + None 1243 + } 1244 + } 1245 + }
+244
crates/weaver-renderer/src/utils.rs
··· 1 + use std::{fmt::Arguments, path::Path, sync::OnceLock}; 2 + 3 + use markdown_weaver::{CodeBlockKind, CowStr, Event, Tag}; 4 + use markdown_weaver_escape::StrWrite; 5 + use miette::IntoDiagnostic; 6 + use n0_future::TryFutureExt; 7 + use n0_future::io::AsyncWrite; 8 + use n0_future::io::AsyncWriteExt; 9 + use regex::Regex; 10 + 11 + #[cfg(not(all(target_family = "wasm", target_os = "unknown")))] 12 + pub async fn inline_file(path: impl AsRef<Path>) -> Option<String> { 13 + tokio::fs::read_to_string(path).await.ok() 14 + } 15 + #[cfg(all(target_family = "wasm", target_os = "unknown"))] 16 + pub async fn inline_file(path: impl AsRef<Path>) -> Option<String> { 17 + todo!() 18 + } 19 + 20 + pub const AVOID_URL_CHARS: &[char] = &[ 21 + '!', '#', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '@', '%', '[', ']', '?', '/', 22 + '~', '|', '{', '}', '^', '`', 23 + ]; 24 + 25 + pub fn resolve_at_ident_or_uri<'s>( 26 + link: &markdown_weaver::Tag<'s>, 27 + appview: &str, 28 + ) -> markdown_weaver::Tag<'s> { 29 + use markdown_weaver::Tag; 30 + match link { 31 + Tag::Link { 32 + link_type, 33 + dest_url, 34 + title, 35 + id, 36 + } => { 37 + if dest_url.starts_with("at://") { 38 + // Make the appview string swappable 39 + let at_uri = weaver_common::aturi_to_http(dest_url.as_ref(), appview); 40 + if let Some(at_uri) = at_uri { 41 + Tag::Link { 42 + link_type: *link_type, 43 + dest_url: at_uri.into_static(), 44 + title: title.clone(), 45 + id: id.clone(), 46 + } 47 + } else { 48 + link.clone() 49 + } 50 + } else if dest_url.starts_with("@") { 51 + let maybe_identifier = dest_url.strip_prefix("@").unwrap(); 52 + if let Some(identifier) = weaver_common::match_identifier(maybe_identifier) { 53 + let link = CowStr::Boxed( 54 + format!("https://{}/profile/{}", appview, identifier).into_boxed_str(), 55 + ); 56 + Tag::Link { 57 + link_type: *link_type, 58 + dest_url: link, 59 + title: title.clone(), 60 + id: id.clone(), 61 + } 62 + } else { 63 + link.clone() 64 + } 65 + } else { 66 + link.clone() 67 + } 68 + } 69 + _ => link.clone(), 70 + } 71 + } 72 + 73 + /// Rough and ready check if a path is a local path. 74 + /// Basically checks if the path is absolute and if so, is it accessible. 75 + /// Relative paths are assumed to be local 76 + pub fn is_local_path(path: &str) -> bool { 77 + let path = Path::new(path); 78 + path.is_relative() || path.try_exists().unwrap_or(false) 79 + } 80 + 81 + /// Is this link relative to somewhere? 82 + /// Rust has built-in checks for file paths, so this just wraps that. 83 + pub fn is_relative_link(link: &str) -> bool { 84 + let path = Path::new(link); 85 + path.is_relative() 86 + } 87 + 88 + /// Flatten a directory path to just the parent and filename, if present. 89 + /// Maybe worth to swap to using the Path tools, but this works. 90 + pub fn flatten_dir_to_just_one_parent(path: &str) -> (&str, &str) { 91 + static RE_PARENT_DIR: OnceLock<Regex> = OnceLock::new(); 92 + let caps = RE_PARENT_DIR 93 + .get_or_init(|| { 94 + Regex::new(r".*[/\\](?P<parent>[^/\\]+)[/\\](?P<filename>[^/\\]+)$").unwrap() 95 + }) 96 + .captures(path); 97 + if let Some(caps) = caps { 98 + if let Some(parent) = caps.name("parent") { 99 + if let Some(filename) = caps.name("filename") { 100 + return (parent.as_str(), filename.as_str()); 101 + } 102 + return (parent.as_str(), ""); 103 + } 104 + if let Some(filename) = caps.name("filename") { 105 + return ("", filename.as_str()); 106 + } 107 + } 108 + ("", path) 109 + } 110 + 111 + fn event_to_owned<'a>(event: Event<'a>) -> Event<'a> { 112 + match event { 113 + Event::Start(tag) => Event::Start(tag_to_owned(tag)), 114 + Event::End(tag) => Event::End(tag), 115 + Event::Text(cowstr) => Event::Text(CowStr::from(cowstr.into_string())), 116 + Event::Code(cowstr) => Event::Code(CowStr::from(cowstr.into_string())), 117 + Event::Html(cowstr) => Event::Html(CowStr::from(cowstr.into_string())), 118 + Event::InlineHtml(cowstr) => Event::InlineHtml(CowStr::from(cowstr.into_string())), 119 + Event::FootnoteReference(cowstr) => { 120 + Event::FootnoteReference(CowStr::from(cowstr.into_string())) 121 + } 122 + Event::SoftBreak => Event::SoftBreak, 123 + Event::HardBreak => Event::HardBreak, 124 + Event::Rule => Event::Rule, 125 + Event::TaskListMarker(checked) => Event::TaskListMarker(checked), 126 + Event::InlineMath(cowstr) => Event::InlineMath(CowStr::from(cowstr.into_string())), 127 + Event::DisplayMath(cowstr) => Event::DisplayMath(CowStr::from(cowstr.into_string())), 128 + Event::WeaverBlock(cow_str) => todo!(), 129 + } 130 + } 131 + 132 + fn tag_to_owned<'a>(tag: Tag<'a>) -> Tag<'a> { 133 + match tag { 134 + Tag::Paragraph => Tag::Paragraph, 135 + Tag::Heading { 136 + level: heading_level, 137 + id, 138 + classes, 139 + attrs, 140 + } => Tag::Heading { 141 + level: heading_level, 142 + id: id.map(|cowstr| CowStr::from(cowstr.into_string())), 143 + classes: classes 144 + .into_iter() 145 + .map(|cowstr| CowStr::from(cowstr.into_string())) 146 + .collect(), 147 + attrs: attrs 148 + .into_iter() 149 + .map(|(attr, value)| { 150 + ( 151 + CowStr::from(attr.into_string()), 152 + value.map(|cowstr| CowStr::from(cowstr.into_string())), 153 + ) 154 + }) 155 + .collect(), 156 + }, 157 + Tag::BlockQuote(blockquote_kind) => Tag::BlockQuote(blockquote_kind), 158 + Tag::CodeBlock(codeblock_kind) => Tag::CodeBlock(codeblock_kind_to_owned(codeblock_kind)), 159 + Tag::List(optional) => Tag::List(optional), 160 + Tag::Item => Tag::Item, 161 + Tag::FootnoteDefinition(cowstr) => { 162 + Tag::FootnoteDefinition(CowStr::from(cowstr.into_string())) 163 + } 164 + Tag::Table(alignment_vector) => Tag::Table(alignment_vector), 165 + Tag::TableHead => Tag::TableHead, 166 + Tag::TableRow => Tag::TableRow, 167 + Tag::TableCell => Tag::TableCell, 168 + Tag::Emphasis => Tag::Emphasis, 169 + Tag::Strong => Tag::Strong, 170 + Tag::Strikethrough => Tag::Strikethrough, 171 + Tag::Link { 172 + link_type, 173 + dest_url, 174 + title, 175 + id, 176 + } => Tag::Link { 177 + link_type, 178 + dest_url: CowStr::from(dest_url.into_string()), 179 + title: CowStr::from(title.into_string()), 180 + id: CowStr::from(id.into_string()), 181 + }, 182 + Tag::Embed { 183 + embed_type, 184 + dest_url, 185 + title, 186 + id, 187 + attrs, 188 + } => Tag::Embed { 189 + embed_type, 190 + dest_url: CowStr::from(dest_url.into_string()), 191 + title: CowStr::from(title.into_string()), 192 + id: CowStr::from(id.into_string()), 193 + attrs, 194 + }, 195 + Tag::Image { 196 + link_type, 197 + dest_url, 198 + title, 199 + id, 200 + attrs, 201 + } => Tag::Image { 202 + link_type, 203 + dest_url: CowStr::from(dest_url.into_string()), 204 + title: CowStr::from(title.into_string()), 205 + id: CowStr::from(id.into_string()), 206 + attrs, 207 + }, 208 + Tag::HtmlBlock => Tag::HtmlBlock, 209 + Tag::MetadataBlock(metadata_block_kind) => Tag::MetadataBlock(metadata_block_kind), 210 + Tag::DefinitionList => Tag::DefinitionList, 211 + Tag::DefinitionListTitle => Tag::DefinitionListTitle, 212 + Tag::DefinitionListDefinition => Tag::DefinitionListDefinition, 213 + Tag::Superscript => todo!(), 214 + Tag::Subscript => todo!(), 215 + Tag::WeaverBlock(weaver_block_kind, weaver_attributes) => todo!(), 216 + } 217 + } 218 + 219 + fn codeblock_kind_to_owned<'a>(codeblock_kind: CodeBlockKind<'_>) -> CodeBlockKind<'a> { 220 + match codeblock_kind { 221 + CodeBlockKind::Indented => CodeBlockKind::Indented, 222 + CodeBlockKind::Fenced(cowstr) => CodeBlockKind::Fenced(CowStr::from(cowstr.into_string())), 223 + } 224 + } 225 + 226 + #[cfg(not(all(target_family = "wasm", target_os = "unknown")))] 227 + use tokio::fs::{self, File}; 228 + 229 + pub async fn create_file(dest: &Path) -> miette::Result<File> { 230 + let file = File::create(dest) 231 + .or_else(async |err| { 232 + { 233 + if err.kind() == std::io::ErrorKind::NotFound { 234 + let parent = dest.parent().expect("file should have a parent directory"); 235 + fs::create_dir_all(parent).await? 236 + } 237 + File::create(dest) 238 + } 239 + .await 240 + }) 241 + .await 242 + .into_diagnostic()?; 243 + Ok(file) 244 + }
+113
crates/weaver-renderer/src/walker.rs
··· 1 + /// Credit to https://github.com/zoni 2 + /// 3 + /// Modified from https://github.com/zoni/obsidian-export/blob/main/src/walker.rs on 2025-05-21 4 + /// 5 + use std::fmt; 6 + use std::path::{Path, PathBuf}; 7 + 8 + use ignore::{DirEntry, Walk, WalkBuilder}; 9 + 10 + use crate::RenderError; 11 + 12 + type FilterFn = dyn Fn(&DirEntry) -> bool + Send + Sync + 'static; 13 + 14 + /// `WalkOptions` specifies how an Obsidian vault directory is scanned for eligible files to export. 15 + #[derive(Clone)] 16 + #[allow(clippy::exhaustive_structs)] 17 + pub struct WalkOptions<'a> { 18 + /// The filename for ignore files, following the 19 + /// [gitignore](https://git-scm.com/docs/gitignore) syntax. 20 + /// 21 + /// By default `.export-ignore` is used. 22 + pub ignore_filename: &'a str, 23 + /// Whether to ignore hidden files. 24 + /// 25 + /// This is enabled by default. 26 + pub ignore_hidden: bool, 27 + /// Whether to honor git's ignore rules (`.gitignore` files, `.git/config/exclude`, etc) if 28 + /// the target is within a git repository. 29 + /// 30 + /// This is enabled by default. 31 + pub honor_gitignore: bool, 32 + /// An optional custom filter function which is called for each directory entry to determine if 33 + /// it should be included or not. 34 + /// 35 + /// This is passed to [`ignore::WalkBuilder::filter_entry`]. 36 + pub filter_fn: Option<&'static FilterFn>, 37 + } 38 + 39 + impl<'a> fmt::Debug for WalkOptions<'a> { 40 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 41 + let filter_fn_fmt = match self.filter_fn { 42 + Some(_) => "<function set>", 43 + None => "<not set>", 44 + }; 45 + f.debug_struct("WalkOptions") 46 + .field("ignore_filename", &self.ignore_filename) 47 + .field("ignore_hidden", &self.ignore_hidden) 48 + .field("honor_gitignore", &self.honor_gitignore) 49 + .field("filter_fn", &filter_fn_fmt) 50 + .finish() 51 + } 52 + } 53 + 54 + impl<'a> WalkOptions<'a> { 55 + /// Create a new set of options using default values. 56 + #[must_use] 57 + pub fn new() -> Self { 58 + WalkOptions { 59 + ignore_filename: ".export-ignore", 60 + ignore_hidden: true, 61 + honor_gitignore: true, 62 + filter_fn: None, 63 + } 64 + } 65 + 66 + fn build_walker(self, path: &Path) -> Walk { 67 + let mut walker = WalkBuilder::new(path); 68 + walker 69 + .standard_filters(false) 70 + .parents(true) 71 + .hidden(self.ignore_hidden) 72 + .add_custom_ignore_filename(self.ignore_filename) 73 + .require_git(true) 74 + .git_ignore(self.honor_gitignore) 75 + .git_global(self.honor_gitignore) 76 + .git_exclude(self.honor_gitignore); 77 + 78 + if let Some(filter) = self.filter_fn { 79 + walker.filter_entry(filter); 80 + } 81 + walker.build() 82 + } 83 + } 84 + 85 + impl<'a> Default for WalkOptions<'a> { 86 + fn default() -> Self { 87 + Self::new() 88 + } 89 + } 90 + 91 + /// `vault_contents` returns all of the files in an Obsidian vault located at `path` which would be 92 + /// exported when using the given [`WalkOptions`]. 93 + pub fn vault_contents(root: &Path, opts: WalkOptions<'_>) -> Result<Vec<PathBuf>, RenderError> { 94 + let mut contents = Vec::new(); 95 + let walker = opts.build_walker(root); 96 + for entry in walker { 97 + let entry = entry.map_err(|e| RenderError::WalkDirError { 98 + path: root.to_path_buf(), 99 + msg: e.to_string(), 100 + })?; 101 + let path = entry.path(); 102 + let metadata = entry.metadata().map_err(|e| RenderError::WalkDirError { 103 + path: root.to_path_buf(), 104 + msg: e.to_string(), 105 + })?; 106 + 107 + if metadata.is_dir() { 108 + continue; 109 + } 110 + contents.push(path.to_path_buf()); 111 + } 112 + Ok(contents) 113 + }
+8
crates/weaver-server/Cargo.toml
··· 5 5 license.workspace = true 6 6 publish = false 7 7 8 + [[bin]] 9 + name = "weaver-server" 10 + path = "src/main.rs" 11 + 12 + [lib] 13 + name = "weaver_server" 14 + path = "src/lib.rs" 15 + 8 16 [dependencies] 9 17 weaver-common = { path = "../weaver-common" } 10 18 weaver-workspace-hack = { version = "0.1", path = "../weaver-workspace-hack" }
+1
crates/weaver-server/src/lib.rs
··· 1 +
+6 -6
flake.lock
··· 51 51 }, 52 52 "nixpkgs": { 53 53 "locked": { 54 - "lastModified": 1747467164, 55 - "narHash": "sha256-JBXbjJ0t6T6BbVc9iPVquQI9XSXCGQJD8c8SgnUquus=", 54 + "lastModified": 1747728033, 55 + "narHash": "sha256-NnXFQu7g4LnvPIPfJmBuZF7LFy/fey2g2+LCzjQhTUk=", 56 56 "owner": "NixOS", 57 57 "repo": "nixpkgs", 58 - "rev": "3fcbdcfc707e0aa42c541b7743e05820472bdaec", 58 + "rev": "2f9173bde1d3fbf1ad26ff6d52f952f9e9da52ea", 59 59 "type": "github" 60 60 }, 61 61 "original": { ··· 95 95 "nixpkgs": "nixpkgs_2" 96 96 }, 97 97 "locked": { 98 - "lastModified": 1747535902, 99 - "narHash": "sha256-rKWBtLcqQeu8PpfKIBn1ORXS2udSH/MwnQFwfSpFOLg=", 98 + "lastModified": 1747795013, 99 + "narHash": "sha256-c7i0xJ+xFhgjO9SWHYu5dF/7lq63RPDvwKAdjc6VCE4=", 100 100 "owner": "oxalica", 101 101 "repo": "rust-overlay", 102 - "rev": "b7a99615d26b82c39b73ccc9026545c3f3403b71", 102 + "rev": "6b1cf12374361859242a562e1933a7930649131a", 103 103 "type": "github" 104 104 }, 105 105 "original": {
+1 -2
flake.nix
··· 37 37 p.rust-bin.selectLatestNightlyWith(toolchain: toolchain.default.override { 38 38 # Set the build targets supported by the toolchain, 39 39 # wasm32-unknown-unknown is required for trunk. 40 - #targets = ["wasm32-unknown-unknown"]; 40 + targets = ["wasm32-unknown-unknown"]; 41 41 extensions = [ 42 - "llvm-tools" 43 42 "rust-src" 44 43 "rust-analyzer" 45 44 "clippy"
+14
lexicons/sh/weaver/actor/defs.json
··· 59 59 } 60 60 } 61 61 }, 62 + "author": { 63 + "type": "object", 64 + "description": "A single author in a Weaver notebook.", 65 + "required": ["did"], 66 + "properties": { 67 + "did": { "type": "string", "format": "did" }, 68 + "handle": { "type": "string", "format": "handle" }, 69 + "displayName": { 70 + "type": "string", 71 + "maxGraphemes": 64, 72 + "maxLength": 640 73 + } 74 + } 75 + }, 62 76 "tangledProfileView": { 63 77 "type": "object", 64 78 "required": ["bluesky", "did", "handle"],
+1 -1
lexicons/sh/weaver/notebook/authors.json
··· 5 5 "main": { 6 6 "type": "record", 7 7 "description": "Authors of a Weaver notebook.", 8 - "key": "literal:self", 8 + "key": "tid", 9 9 "record": { 10 10 "type": "object", 11 11 "required": ["authorList"],
+6 -3
lexicons/sh/weaver/notebook/book.json
··· 13 13 "title": { "type": "ref", "ref": "sh.weaver.notebook.defs#title" }, 14 14 "tags": { "type": "ref", "ref": "sh.weaver.notebook.defs#tags" }, 15 15 "authors": { 16 - "type": "ref", 17 - "ref": "sh.weaver.notebook.defs#authorListView" 16 + "type": "array", 17 + "items": { 18 + "type": "ref", 19 + "ref": "sh.weaver.actor.defs#author" 20 + } 18 21 }, 19 22 "entryList": { 20 23 "type": "array", 21 24 "items": { 22 25 "type": "ref", 23 - "ref": "sh.weaver.notebook.defs#bookEntryView" 26 + "ref": "com.atproto.repo.strongRef" 24 27 } 25 28 }, 26 29 "createdAt": {
+43
lexicons/sh/weaver/notebook/chapter.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "sh.weaver.notebook.chapter", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A grouping of entries in a notebook.", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": ["notebook", "authors", "entryList"], 12 + "properties": { 13 + "title": { "type": "ref", "ref": "sh.weaver.notebook.defs#title" }, 14 + "tags": { "type": "ref", "ref": "sh.weaver.notebook.defs#tags" }, 15 + "notebook": { 16 + "type": "ref", 17 + "ref": "com.atproto.repo.strongRef", 18 + "description": "The notebook this chapter belongs to." 19 + }, 20 + "authors": { 21 + "type": "array", 22 + "items": { 23 + "type": "ref", 24 + "ref": "sh.weaver.actor.defs#author" 25 + } 26 + }, 27 + "entryList": { 28 + "type": "array", 29 + "items": { 30 + "type": "ref", 31 + "ref": "com.atproto.repo.strongRef" 32 + } 33 + }, 34 + "createdAt": { 35 + "type": "string", 36 + "format": "datetime", 37 + "description": "Client-declared timestamp when this was originally created." 38 + } 39 + } 40 + } 41 + } 42 + } 43 + }
+12
lexicons/sh/weaver/notebook/defs.json
··· 112 112 }, 113 113 "maxLength": 10, 114 114 "description": "An array of tags associated with the notebook entry. Tags can help categorize and organize entries." 115 + }, 116 + "contentFormat": { 117 + "type": "object", 118 + "description": "The format of the content. This is used to determine how to render the content.", 119 + "properties": { 120 + "markdown": { 121 + "type": "string", 122 + "description": "The format of the content. This is used to determine how to render the content.", 123 + "enum": ["commonmark", "gfm", "obsidian", "weaver"], 124 + "default": "weaver" 125 + } 126 + } 115 127 } 116 128 } 117 129 }
+36
lexicons/sh/weaver/notebook/page.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "sh.weaver.notebook.chapter", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A grouping of entries in a notebook, intended to be displayed as a single page.", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": ["notebook", "authors", "entryList"], 12 + "properties": { 13 + "title": { "type": "ref", "ref": "sh.weaver.notebook.defs#title" }, 14 + "tags": { "type": "ref", "ref": "sh.weaver.notebook.defs#tags" }, 15 + "notebook": { 16 + "type": "ref", 17 + "ref": "com.atproto.repo.strongRef", 18 + "description": "The notebook this page belongs to." 19 + }, 20 + "entryList": { 21 + "type": "array", 22 + "items": { 23 + "type": "ref", 24 + "ref": "com.atproto.repo.strongRef" 25 + } 26 + }, 27 + "createdAt": { 28 + "type": "string", 29 + "format": "datetime", 30 + "description": "Client-declared timestamp when this was originally created." 31 + } 32 + } 33 + } 34 + } 35 + } 36 + }
+23
lexicons/sh/weaver/publish/blob.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "sh.weaver.publish.blob", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A simple record referencing a file hosted on a PDS", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": ["upload"], 12 + "properties": { 13 + "upload": { 14 + "type": "blob", 15 + "description": "Reference to the uploaded file", 16 + "accept": ["*/*"], 17 + "maxSize": 10000000 18 + } 19 + } 20 + } 21 + } 22 + } 23 + }
+5
lexicons/sh/weaver/publish/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "sh.weaver.publish.defs", 4 + "defs": {} 5 + }