bunch more lexicon stuff and buildout

+3419 -195
+2
.gitignore
··· 10 11 .db/ 12 **/.db/
··· 10 11 .db/ 12 **/.db/ 13 + 14 + **/.claude/settings.local.json
+10
.zed/settings.json
··· 11 "command": ["alejandra", "--quiet", "--"] // or ["nixfmt"] 12 } 13 } 14 } 15 } 16 }
··· 11 "command": ["alejandra", "--quiet", "--"] // or ["nixfmt"] 12 } 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 + } 24 } 25 } 26 }
+228 -81
Cargo.lock
··· 121 122 [[package]] 123 name = "anstyle-wincon" 124 - version = "3.0.7" 125 source = "registry+https://github.com/rust-lang/crates.io-index" 126 - checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 127 dependencies = [ 128 "anstyle", 129 - "once_cell", 130 "windows-sys 0.59.0", 131 ] 132 ··· 185 186 [[package]] 187 name = "atrium-api" 188 - version = "0.25.3" 189 source = "registry+https://github.com/rust-lang/crates.io-index" 190 - checksum = "7225f0ca3c78564b784828e3db3e92619cf6e786530c3468df73f49deebc0bd4" 191 dependencies = [ 192 "atrium-common", 193 "atrium-xrpc", ··· 221 222 [[package]] 223 name = "atrium-identity" 224 - version = "0.1.4" 225 source = "registry+https://github.com/rust-lang/crates.io-index" 226 - checksum = "84939a5e3a0a442467d6a000f586c092bb763a31c2d38ec1648f5179a720395a" 227 dependencies = [ 228 "atrium-api", 229 "atrium-common", ··· 247 248 [[package]] 249 name = "atrium-oauth" 250 - version = "0.1.2" 251 source = "registry+https://github.com/rust-lang/crates.io-index" 252 - checksum = "1f4f210da8f2d7199b15e6b02c0628175791ae5caaf506cfcafe45a20917f37c" 253 dependencies = [ 254 "atrium-api", 255 "atrium-common", ··· 422 ] 423 424 [[package]] 425 name = "bitflags" 426 version = "1.3.2" 427 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 470 ] 471 472 [[package]] 473 name = "buf_redux" 474 version = "0.8.4" 475 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 517 518 [[package]] 519 name = "cc" 520 - version = "1.2.23" 521 source = "registry+https://github.com/rust-lang/crates.io-index" 522 - checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766" 523 dependencies = [ 524 "shlex", 525 ] ··· 573 574 [[package]] 575 name = "clap" 576 - version = "4.5.38" 577 source = "registry+https://github.com/rust-lang/crates.io-index" 578 - checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" 579 dependencies = [ 580 "clap_builder", 581 "clap_derive", ··· 583 584 [[package]] 585 name = "clap_builder" 586 - version = "4.5.38" 587 source = "registry+https://github.com/rust-lang/crates.io-index" 588 - checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" 589 dependencies = [ 590 "anstream", 591 "anstyle", ··· 682 683 [[package]] 684 name = "cordyceps" 685 - version = "0.3.3" 686 source = "registry+https://github.com/rust-lang/crates.io-index" 687 - checksum = "a0392f465ceba1713d30708f61c160ebf4dc1cf86bb166039d16b11ad4f3b5b6" 688 dependencies = [ 689 "loom", 690 "tracing", ··· 739 source = "registry+https://github.com/rust-lang/crates.io-index" 740 checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" 741 dependencies = [ 742 "crossbeam-utils", 743 ] 744 ··· 1086 ] 1087 1088 [[package]] 1089 name = "ecdsa" 1090 version = "0.16.9" 1091 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1232 checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" 1233 1234 [[package]] 1235 name = "fastrand" 1236 version = "2.3.0" 1237 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1485 checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 1486 1487 [[package]] 1488 name = "group" 1489 version = "0.13.0" 1490 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1567 ] 1568 1569 [[package]] 1570 name = "heck" 1571 version = "0.4.1" 1572 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1719 1720 [[package]] 1721 name = "hyper-rustls" 1722 - version = "0.27.5" 1723 source = "registry+https://github.com/rust-lang/crates.io-index" 1724 - checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" 1725 dependencies = [ 1726 - "futures-util", 1727 "http", 1728 "hyper", 1729 "hyper-util", ··· 1732 "tokio", 1733 "tokio-rustls", 1734 "tower-service", 1735 - "webpki-roots 0.26.11", 1736 ] 1737 1738 [[package]] ··· 1753 1754 [[package]] 1755 name = "hyper-util" 1756 - version = "0.1.11" 1757 source = "registry+https://github.com/rust-lang/crates.io-index" 1758 - checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" 1759 dependencies = [ 1760 "bytes", 1761 "futures-channel", 1762 "futures-util", 1763 "http", 1764 "http-body", 1765 "hyper", 1766 "libc", 1767 "pin-project-lite", 1768 "socket2", 1769 "tokio", 1770 "tower-service", 1771 "tracing", 1772 ] 1773 1774 [[package]] ··· 1844 1845 [[package]] 1846 name = "icu_properties" 1847 - version = "2.0.0" 1848 source = "registry+https://github.com/rust-lang/crates.io-index" 1849 - checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a" 1850 dependencies = [ 1851 "displaydoc", 1852 "icu_collections", ··· 1860 1861 [[package]] 1862 name = "icu_properties_data" 1863 - version = "2.0.0" 1864 source = "registry+https://github.com/rust-lang/crates.io-index" 1865 - checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04" 1866 1867 [[package]] 1868 name = "icu_provider" ··· 1909 ] 1910 1911 [[package]] 1912 name = "indexmap" 1913 version = "1.9.3" 1914 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1977 version = "2.11.0" 1978 source = "registry+https://github.com/rust-lang/crates.io-index" 1979 checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 1980 1981 [[package]] 1982 name = "is_ci" ··· 2159 2160 [[package]] 2161 name = "libsqlite3-sys" 2162 - version = "0.30.1" 2163 source = "registry+https://github.com/rust-lang/crates.io-index" 2164 - checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" 2165 dependencies = [ 2166 "pkg-config", 2167 "vcpkg", ··· 2241 [[package]] 2242 name = "markdown-weaver" 2243 version = "0.13.0" 2244 - source = "git+https://github.com/rsform/markdown-weaver#593f968ec3c4515c23273715eaeb33801342439b" 2245 dependencies = [ 2246 "bitflags 2.9.1", 2247 "getopts", ··· 2253 [[package]] 2254 name = "markdown-weaver-escape" 2255 version = "0.11.0" 2256 - source = "git+https://github.com/rsform/markdown-weaver#593f968ec3c4515c23273715eaeb33801342439b" 2257 2258 [[package]] 2259 name = "matchers" ··· 2324 checksum = "7c36f61da594ecad0ed986ceeb5061eba47a36fcf839576ce525c7e4ff08f3fa" 2325 dependencies = [ 2326 "merde_core", 2327 - "yaml-rust2", 2328 ] 2329 2330 [[package]] ··· 2425 2426 [[package]] 2427 name = "mio" 2428 - version = "1.0.3" 2429 source = "registry+https://github.com/rust-lang/crates.io-index" 2430 - checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 2431 dependencies = [ 2432 "libc", 2433 "log", 2434 "wasi 0.11.0+wasi-snapshot-preview1", 2435 - "windows-sys 0.52.0", 2436 ] 2437 2438 [[package]] ··· 2658 checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 2659 2660 [[package]] 2661 name = "onig" 2662 - version = "6.4.0" 2663 source = "registry+https://github.com/rust-lang/crates.io-index" 2664 - checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f" 2665 dependencies = [ 2666 - "bitflags 1.3.2", 2667 "libc", 2668 "once_cell", 2669 "onig_sys", ··· 2671 2672 [[package]] 2673 name = "onig_sys" 2674 - version = "69.8.1" 2675 source = "registry+https://github.com/rust-lang/crates.io-index" 2676 - checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7" 2677 dependencies = [ 2678 "cc", 2679 "pkg-config", ··· 2811 "smallvec", 2812 "windows-targets 0.52.6", 2813 ] 2814 2815 [[package]] 2816 name = "pem-rfc7468" ··· 3244 3245 [[package]] 3246 name = "reqwest" 3247 - version = "0.12.15" 3248 source = "registry+https://github.com/rust-lang/crates.io-index" 3249 - checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" 3250 dependencies = [ 3251 "async-compression", 3252 "base64 0.22.1", ··· 3272 "pin-project-lite", 3273 "quinn", 3274 "rustls 0.23.27", 3275 - "rustls-pemfile 2.2.0", 3276 "rustls-pki-types", 3277 "serde", 3278 "serde_json", 3279 "serde_urlencoded", 3280 "sync_wrapper", 3281 - "system-configuration", 3282 "tokio", 3283 "tokio-native-tls", 3284 "tokio-rustls", 3285 "tokio-util", 3286 "tower", 3287 "tower-service", 3288 "url", 3289 "wasm-bindgen", 3290 "wasm-bindgen-futures", 3291 "web-sys", 3292 - "webpki-roots 0.26.11", 3293 - "windows-registry", 3294 ] 3295 3296 [[package]] ··· 3452 ] 3453 3454 [[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 name = "rustls-pki-types" 3465 version = "1.12.0" 3466 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3483 3484 [[package]] 3485 name = "rustversion" 3486 - version = "1.0.20" 3487 source = "registry+https://github.com/rust-lang/crates.io-index" 3488 - checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 3489 3490 [[package]] 3491 name = "ryu" ··· 3849 3850 [[package]] 3851 name = "socket2" 3852 - version = "0.5.9" 3853 source = "registry+https://github.com/rust-lang/crates.io-index" 3854 - checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" 3855 dependencies = [ 3856 "libc", 3857 "windows-sys 0.52.0", ··· 3974 dependencies = [ 3975 "bincode", 3976 "bitflags 1.3.2", 3977 "flate2", 3978 "fnv", 3979 "once_cell", ··· 4151 "httpdate", 4152 "log", 4153 "rustls 0.20.9", 4154 - "rustls-pemfile 0.2.1", 4155 "zeroize", 4156 ] 4157 ··· 4182 4183 [[package]] 4184 name = "tokio" 4185 - version = "1.45.0" 4186 source = "registry+https://github.com/rust-lang/crates.io-index" 4187 - checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" 4188 dependencies = [ 4189 "backtrace", 4190 "bytes", ··· 4358 "http-body-util", 4359 "http-range-header", 4360 "httpdate", 4361 "mime", 4362 "mime_guess", 4363 "percent-encoding", 4364 "pin-project-lite", 4365 "tokio", 4366 "tokio-util", 4367 "tower-layer", 4368 "tower-service", 4369 "tracing", ··· 4602 4603 [[package]] 4604 name = "uuid" 4605 - version = "1.16.0" 4606 source = "registry+https://github.com/rust-lang/crates.io-index" 4607 - checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" 4608 dependencies = [ 4609 "getrandom 0.3.3", 4610 "serde", 4611 ] 4612 4613 [[package]] ··· 4818 "http", 4819 "jose-jwk", 4820 "markdown-weaver", 4821 "merde", 4822 "miette", 4823 "minijinja", 4824 "multibase", 4825 "n0-future", 4826 "owo-colors", 4827 "reqwest", 4828 "serde", 4829 "serde_bytes", ··· 4844 name = "weaver-renderer" 4845 version = "0.1.0" 4846 dependencies = [ 4847 "atrium-api", 4848 "compact_string", 4849 "http", 4850 "n0-future", 4851 "url", 4852 "weaver-common", 4853 "weaver-workspace-hack", 4854 ] 4855 4856 [[package]] ··· 4950 4951 [[package]] 4952 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 version = "1.0.0" 4963 source = "registry+https://github.com/rust-lang/crates.io-index" 4964 checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" ··· 5038 5039 [[package]] 5040 name = "windows-core" 5041 - version = "0.61.1" 5042 source = "registry+https://github.com/rust-lang/crates.io-index" 5043 - checksum = "46ec44dc15085cea82cf9c78f85a9114c463a369786585ad2882d1ff0b0acf40" 5044 dependencies = [ 5045 "windows-implement", 5046 "windows-interface", 5047 "windows-link", 5048 "windows-result", 5049 - "windows-strings 0.4.1", 5050 ] 5051 5052 [[package]] ··· 5111 5112 [[package]] 5113 name = "windows-result" 5114 - version = "0.3.3" 5115 source = "registry+https://github.com/rust-lang/crates.io-index" 5116 - checksum = "4b895b5356fc36103d0f64dd1e94dfa7ac5633f1c9dd6e80fe9ec4adef69e09d" 5117 dependencies = [ 5118 "windows-link", 5119 ] ··· 5129 5130 [[package]] 5131 name = "windows-strings" 5132 - version = "0.4.1" 5133 source = "registry+https://github.com/rust-lang/crates.io-index" 5134 - checksum = "2a7ab927b2637c19b3dbe0965e75d8f2d30bdd697a1516191cad2ec4df8fb28a" 5135 dependencies = [ 5136 "windows-link", 5137 ] ··· 5407 checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" 5408 dependencies = [ 5409 "arraydeque", 5410 - "hashlink", 5411 ] 5412 5413 [[package]]
··· 121 122 [[package]] 123 name = "anstyle-wincon" 124 + version = "3.0.8" 125 source = "registry+https://github.com/rust-lang/crates.io-index" 126 + checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" 127 dependencies = [ 128 "anstyle", 129 + "once_cell_polyfill", 130 "windows-sys 0.59.0", 131 ] 132 ··· 185 186 [[package]] 187 name = "atrium-api" 188 + version = "0.25.4" 189 source = "registry+https://github.com/rust-lang/crates.io-index" 190 + checksum = "46355d3245edc7b3160b2a45fe55d09a6963ebd3eee0252feb6b72fb0eb71463" 191 dependencies = [ 192 "atrium-common", 193 "atrium-xrpc", ··· 221 222 [[package]] 223 name = "atrium-identity" 224 + version = "0.1.5" 225 source = "registry+https://github.com/rust-lang/crates.io-index" 226 + checksum = "c9e2d42bb4dbea038f4f5f45e3af2a89d61a9894a75f06aa550b74a60d2be380" 227 dependencies = [ 228 "atrium-api", 229 "atrium-common", ··· 247 248 [[package]] 249 name = "atrium-oauth" 250 + version = "0.1.3" 251 source = "registry+https://github.com/rust-lang/crates.io-index" 252 + checksum = "ca22dc4eaf77fd9bf050b21192ac58cd654a437d28e000ec114ebd93a51d36f5" 253 dependencies = [ 254 "atrium-api", 255 "atrium-common", ··· 422 ] 423 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]] 440 name = "bitflags" 441 version = "1.3.2" 442 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 485 ] 486 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]] 498 name = "buf_redux" 499 version = "0.8.4" 500 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 542 543 [[package]] 544 name = "cc" 545 + version = "1.2.24" 546 source = "registry+https://github.com/rust-lang/crates.io-index" 547 + checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7" 548 dependencies = [ 549 "shlex", 550 ] ··· 598 599 [[package]] 600 name = "clap" 601 + version = "4.5.39" 602 source = "registry+https://github.com/rust-lang/crates.io-index" 603 + checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" 604 dependencies = [ 605 "clap_builder", 606 "clap_derive", ··· 608 609 [[package]] 610 name = "clap_builder" 611 + version = "4.5.39" 612 source = "registry+https://github.com/rust-lang/crates.io-index" 613 + checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" 614 dependencies = [ 615 "anstream", 616 "anstyle", ··· 707 708 [[package]] 709 name = "cordyceps" 710 + version = "0.3.4" 711 source = "registry+https://github.com/rust-lang/crates.io-index" 712 + checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a" 713 dependencies = [ 714 "loom", 715 "tracing", ··· 764 source = "registry+https://github.com/rust-lang/crates.io-index" 765 checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" 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", 777 "crossbeam-utils", 778 ] 779 ··· 1121 ] 1122 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]] 1145 name = "ecdsa" 1146 version = "0.16.9" 1147 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1288 checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" 1289 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]] 1301 name = "fastrand" 1302 version = "2.3.0" 1303 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1551 checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 1552 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]] 1567 name = "group" 1568 version = "0.13.0" 1569 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1646 ] 1647 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]] 1658 name = "heck" 1659 version = "0.4.1" 1660 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1807 1808 [[package]] 1809 name = "hyper-rustls" 1810 + version = "0.27.6" 1811 source = "registry+https://github.com/rust-lang/crates.io-index" 1812 + checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d" 1813 dependencies = [ 1814 "http", 1815 "hyper", 1816 "hyper-util", ··· 1819 "tokio", 1820 "tokio-rustls", 1821 "tower-service", 1822 + "webpki-roots", 1823 ] 1824 1825 [[package]] ··· 1840 1841 [[package]] 1842 name = "hyper-util" 1843 + version = "0.1.13" 1844 source = "registry+https://github.com/rust-lang/crates.io-index" 1845 + checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" 1846 dependencies = [ 1847 + "base64 0.22.1", 1848 "bytes", 1849 "futures-channel", 1850 + "futures-core", 1851 "futures-util", 1852 "http", 1853 "http-body", 1854 "hyper", 1855 + "ipnet", 1856 "libc", 1857 + "percent-encoding", 1858 "pin-project-lite", 1859 "socket2", 1860 + "system-configuration", 1861 "tokio", 1862 "tower-service", 1863 "tracing", 1864 + "windows-registry", 1865 ] 1866 1867 [[package]] ··· 1937 1938 [[package]] 1939 name = "icu_properties" 1940 + version = "2.0.1" 1941 source = "registry+https://github.com/rust-lang/crates.io-index" 1942 + checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 1943 dependencies = [ 1944 "displaydoc", 1945 "icu_collections", ··· 1953 1954 [[package]] 1955 name = "icu_properties_data" 1956 + version = "2.0.1" 1957 source = "registry+https://github.com/rust-lang/crates.io-index" 1958 + checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" 1959 1960 [[package]] 1961 name = "icu_provider" ··· 2002 ] 2003 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]] 2021 name = "indexmap" 2022 version = "1.9.3" 2023 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2086 version = "2.11.0" 2087 source = "registry+https://github.com/rust-lang/crates.io-index" 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 + ] 2099 2100 [[package]] 2101 name = "is_ci" ··· 2278 2279 [[package]] 2280 name = "libsqlite3-sys" 2281 + version = "0.33.0" 2282 source = "registry+https://github.com/rust-lang/crates.io-index" 2283 + checksum = "947e6816f7825b2b45027c2c32e7085da9934defa535de4a6a46b10a4d5257fa" 2284 dependencies = [ 2285 "pkg-config", 2286 "vcpkg", ··· 2360 [[package]] 2361 name = "markdown-weaver" 2362 version = "0.13.0" 2363 + source = "git+https://github.com/rsform/markdown-weaver#5a93522bc6b69058a34e7eb46f89b39d1b6be006" 2364 dependencies = [ 2365 "bitflags 2.9.1", 2366 "getopts", ··· 2372 [[package]] 2373 name = "markdown-weaver-escape" 2374 version = "0.11.0" 2375 + source = "git+https://github.com/rsform/markdown-weaver#5a93522bc6b69058a34e7eb46f89b39d1b6be006" 2376 2377 [[package]] 2378 name = "matchers" ··· 2443 checksum = "7c36f61da594ecad0ed986ceeb5061eba47a36fcf839576ce525c7e4ff08f3fa" 2444 dependencies = [ 2445 "merde_core", 2446 + "yaml-rust2 0.8.1", 2447 ] 2448 2449 [[package]] ··· 2544 2545 [[package]] 2546 name = "mio" 2547 + version = "1.0.4" 2548 source = "registry+https://github.com/rust-lang/crates.io-index" 2549 + checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" 2550 dependencies = [ 2551 "libc", 2552 "log", 2553 "wasi 0.11.0+wasi-snapshot-preview1", 2554 + "windows-sys 0.59.0", 2555 ] 2556 2557 [[package]] ··· 2777 checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 2778 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]] 2786 name = "onig" 2787 + version = "6.5.1" 2788 source = "registry+https://github.com/rust-lang/crates.io-index" 2789 + checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" 2790 dependencies = [ 2791 + "bitflags 2.9.1", 2792 "libc", 2793 "once_cell", 2794 "onig_sys", ··· 2796 2797 [[package]] 2798 name = "onig_sys" 2799 + version = "69.9.1" 2800 source = "registry+https://github.com/rust-lang/crates.io-index" 2801 + checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" 2802 dependencies = [ 2803 "cc", 2804 "pkg-config", ··· 2936 "smallvec", 2937 "windows-targets 0.52.6", 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" 2945 2946 [[package]] 2947 name = "pem-rfc7468" ··· 3375 3376 [[package]] 3377 name = "reqwest" 3378 + version = "0.12.16" 3379 source = "registry+https://github.com/rust-lang/crates.io-index" 3380 + checksum = "2bf597b113be201cb2269b4c39b39a804d01b99ee95a4278f0ed04e45cff1c71" 3381 dependencies = [ 3382 "async-compression", 3383 "base64 0.22.1", ··· 3403 "pin-project-lite", 3404 "quinn", 3405 "rustls 0.23.27", 3406 "rustls-pki-types", 3407 "serde", 3408 "serde_json", 3409 "serde_urlencoded", 3410 "sync_wrapper", 3411 "tokio", 3412 "tokio-native-tls", 3413 "tokio-rustls", 3414 "tokio-util", 3415 "tower", 3416 + "tower-http", 3417 "tower-service", 3418 "url", 3419 "wasm-bindgen", 3420 "wasm-bindgen-futures", 3421 "web-sys", 3422 + "webpki-roots", 3423 ] 3424 3425 [[package]] ··· 3581 ] 3582 3583 [[package]] 3584 name = "rustls-pki-types" 3585 version = "1.12.0" 3586 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3603 3604 [[package]] 3605 name = "rustversion" 3606 + version = "1.0.21" 3607 source = "registry+https://github.com/rust-lang/crates.io-index" 3608 + checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" 3609 3610 [[package]] 3611 name = "ryu" ··· 3969 3970 [[package]] 3971 name = "socket2" 3972 + version = "0.5.10" 3973 source = "registry+https://github.com/rust-lang/crates.io-index" 3974 + checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" 3975 dependencies = [ 3976 "libc", 3977 "windows-sys 0.52.0", ··· 4094 dependencies = [ 4095 "bincode", 4096 "bitflags 1.3.2", 4097 + "fancy-regex", 4098 "flate2", 4099 "fnv", 4100 "once_cell", ··· 4272 "httpdate", 4273 "log", 4274 "rustls 0.20.9", 4275 + "rustls-pemfile", 4276 "zeroize", 4277 ] 4278 ··· 4303 4304 [[package]] 4305 name = "tokio" 4306 + version = "1.45.1" 4307 source = "registry+https://github.com/rust-lang/crates.io-index" 4308 + checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" 4309 dependencies = [ 4310 "backtrace", 4311 "bytes", ··· 4479 "http-body-util", 4480 "http-range-header", 4481 "httpdate", 4482 + "iri-string", 4483 "mime", 4484 "mime_guess", 4485 "percent-encoding", 4486 "pin-project-lite", 4487 "tokio", 4488 "tokio-util", 4489 + "tower", 4490 "tower-layer", 4491 "tower-service", 4492 "tracing", ··· 4725 4726 [[package]] 4727 name = "uuid" 4728 + version = "1.17.0" 4729 source = "registry+https://github.com/rust-lang/crates.io-index" 4730 + checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" 4731 dependencies = [ 4732 "getrandom 0.3.3", 4733 + "js-sys", 4734 "serde", 4735 + "wasm-bindgen", 4736 ] 4737 4738 [[package]] ··· 4943 "http", 4944 "jose-jwk", 4945 "markdown-weaver", 4946 + "markdown-weaver-escape", 4947 "merde", 4948 "miette", 4949 "minijinja", 4950 "multibase", 4951 "n0-future", 4952 "owo-colors", 4953 + "regex", 4954 "reqwest", 4955 "serde", 4956 "serde_bytes", ··· 4971 name = "weaver-renderer" 4972 version = "0.1.0" 4973 dependencies = [ 4974 + "async-trait", 4975 "atrium-api", 4976 + "bitflags 2.9.1", 4977 "compact_string", 4978 + "dashmap", 4979 + "dynosaur", 4980 "http", 4981 + "ignore", 4982 + "markdown-weaver", 4983 + "markdown-weaver-escape", 4984 + "miette", 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", 4995 "url", 4996 "weaver-common", 4997 "weaver-workspace-hack", 4998 + "yaml-rust2 0.10.2", 4999 ] 5000 5001 [[package]] ··· 5095 5096 [[package]] 5097 name = "webpki-roots" 5098 version = "1.0.0" 5099 source = "registry+https://github.com/rust-lang/crates.io-index" 5100 checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" ··· 5174 5175 [[package]] 5176 name = "windows-core" 5177 + version = "0.61.2" 5178 source = "registry+https://github.com/rust-lang/crates.io-index" 5179 + checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" 5180 dependencies = [ 5181 "windows-implement", 5182 "windows-interface", 5183 "windows-link", 5184 "windows-result", 5185 + "windows-strings 0.4.2", 5186 ] 5187 5188 [[package]] ··· 5247 5248 [[package]] 5249 name = "windows-result" 5250 + version = "0.3.4" 5251 source = "registry+https://github.com/rust-lang/crates.io-index" 5252 + checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" 5253 dependencies = [ 5254 "windows-link", 5255 ] ··· 5265 5266 [[package]] 5267 name = "windows-strings" 5268 + version = "0.4.2" 5269 source = "registry+https://github.com/rust-lang/crates.io-index" 5270 + checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 5271 dependencies = [ 5272 "windows-link", 5273 ] ··· 5543 checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" 5544 dependencies = [ 5545 "arraydeque", 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", 5558 ] 5559 5560 [[package]]
+2 -1
Cargo.toml
··· 30 miette = { version = "7.6" } 31 owo-colors = { version = "4.2.0" } 32 thiserror = "2.0" 33 - syntect = "5.2.0" 34 jane-eyre = "0.6.12" 35 n0-future = "=0.1.3" 36 tracing = { version = "0.1.41", default-features = false, features = ["std"] } ··· 38 "serde-codec", 39 ] } 40 markdown-weaver = { git = "https://github.com/rsform/markdown-weaver" } 41 42 esquema-codegen = { git = "https://github.com/fatfingers23/esquema.git", branch = "main" } 43 atrium-lex = { git = "https://github.com/sugyan/atrium.git", rev = "f162f815a04b5ecb0421b390d521c883c41d5f75" }
··· 30 miette = { version = "7.6" } 31 owo-colors = { version = "4.2.0" } 32 thiserror = "2.0" 33 + syntect = { version = "5.2.0", default-features = false } 34 jane-eyre = "0.6.12" 35 n0-future = "=0.1.3" 36 tracing = { version = "0.1.41", default-features = false, features = ["std"] } ··· 38 "serde-codec", 39 ] } 40 markdown-weaver = { git = "https://github.com/rsform/markdown-weaver" } 41 + markdown-weaver-escape = { git = "https://github.com/rsform/markdown-weaver" } 42 43 esquema-codegen = { git = "https://github.com/fatfingers23/esquema.git", branch = "main" } 44 atrium-lex = { git = "https://github.com/sugyan/atrium.git", rev = "f162f815a04b5ecb0421b390d521c883c41d5f75" }
+2
crates/weaver-common/Cargo.toml
··· 60 tower-layer = "0.3.3" 61 multibase = "0.9.1" 62 dirs = "6.0.0" 63 64 65
··· 60 tower-layer = "0.3.3" 61 multibase = "0.9.1" 62 dirs = "6.0.0" 63 + regex = "1.11.1" 64 + markdown-weaver-escape = { workspace = true, features = ["std"] } 65 66 67
+74 -4
crates/weaver-common/src/agent.rs
··· 3 use crate::resolver::HickoryDnsTxtResolver; 4 use crate::sh::weaver::actor::defs::ProfileDataViewInnerRefs; 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}; 8 use atrium_api::{agent::SessionManager, types::string::AtIdentifier}; 9 use atrium_common::resolver::Resolver; 10 use atrium_identity::did::{CommonDidResolver, CommonDidResolverConfig, DEFAULT_PLC_DIRECTORY_URL}; ··· 60 AtprotoHandleResolver<HickoryDnsTxtResolver, WeaverHttpClient>, 61 >, 62 >, 63 pub api: Service<Wrapper<M>>, 64 } 65 ··· 86 Self { 87 session_manager, 88 resolver: Arc::new(IdentityResolver::new(resolver_config)), 89 api, 90 } 91 } ··· 94 95 pub async fn did(&self) -> Option<Did> { 96 self.session_manager.did().await 97 } 98 } 99 ··· 173 } 174 } 175 } 176 177 pub async fn put_record( 178 &self, ··· 249 Ok(result.data) 250 } 251 252 pub async fn upload_artifact( 253 &self, 254 content: String, ··· 313 M: CloneWithProxy + SessionManager + Send + Sync, 314 { 315 /// Configures the atproto-proxy header to be applied on requests. 316 - 317 /// 318 - 319 /// Returns a new client service with the proxy header configured. 320 321 pub fn api_with_proxy(&self, did: Did, service_type: impl AsRef<str>) -> Service<Wrapper<M>> {
··· 3 use crate::resolver::HickoryDnsTxtResolver; 4 use crate::sh::weaver::actor::defs::ProfileDataViewInnerRefs; 5 use atrium_api::agent::{CloneWithProxy, Configure}; 6 + use atrium_api::types::string::{Cid, Did, Handle, Nsid, RecordKey, Tid}; 7 + use atrium_api::types::{BlobRef, Collection, LimitedU32, TryIntoUnknown, Union, Unknown}; 8 use atrium_api::{agent::SessionManager, types::string::AtIdentifier}; 9 use atrium_common::resolver::Resolver; 10 use atrium_identity::did::{CommonDidResolver, CommonDidResolverConfig, DEFAULT_PLC_DIRECTORY_URL}; ··· 60 AtprotoHandleResolver<HickoryDnsTxtResolver, WeaverHttpClient>, 61 >, 62 >, 63 + pub client: Arc<WeaverHttpClient>, 64 pub api: Service<Wrapper<M>>, 65 } 66 ··· 87 Self { 88 session_manager, 89 resolver: Arc::new(IdentityResolver::new(resolver_config)), 90 + client: Arc::clone(&http_client), 91 api, 92 } 93 } ··· 96 97 pub async fn did(&self) -> Option<Did> { 98 self.session_manager.did().await 99 + } 100 + 101 + pub fn pds(&self) -> String { 102 + self.session_manager.base_uri() 103 } 104 } 105 ··· 179 } 180 } 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 + } 209 210 pub async fn put_record( 211 &self, ··· 282 Ok(result.data) 283 } 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 + 324 pub async fn upload_artifact( 325 &self, 326 content: String, ··· 385 M: CloneWithProxy + SessionManager + Send + Sync, 386 { 387 /// Configures the atproto-proxy header to be applied on requests. 388 /// 389 /// Returns a new client service with the proxy header configured. 390 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 pub reason: core::option::Option<atrium_api::types::Union<FeedViewPostReasonRefs>>, 40 #[serde(skip_serializing_if = "core::option::Option::is_none")] 41 pub reply: core::option::Option<ReplyRef>, 42 } 43 pub type FeedViewPost = atrium_api::types::Object<FeedViewPostData>; 44 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] ··· 87 pub feed_context: core::option::Option<String>, 88 #[serde(skip_serializing_if = "core::option::Option::is_none")] 89 pub item: core::option::Option<String>, 90 } 91 pub type Interaction = atrium_api::types::Object<InteractionData>; 92 ///User liked the feed item ··· 142 #[serde(rename_all = "camelCase")] 143 pub struct ReasonRepostData { 144 pub by: crate::app::bsky::actor::defs::ProfileViewBasic, 145 pub indexed_at: atrium_api::types::string::Datetime, 146 } 147 pub type ReasonRepost = atrium_api::types::Object<ReasonRepostData>; 148 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
··· 39 pub reason: core::option::Option<atrium_api::types::Union<FeedViewPostReasonRefs>>, 40 #[serde(skip_serializing_if = "core::option::Option::is_none")] 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>, 45 } 46 pub type FeedViewPost = atrium_api::types::Object<FeedViewPostData>; 47 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] ··· 90 pub feed_context: core::option::Option<String>, 91 #[serde(skip_serializing_if = "core::option::Option::is_none")] 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>, 96 } 97 pub type Interaction = atrium_api::types::Object<InteractionData>; 98 ///User liked the feed item ··· 148 #[serde(rename_all = "camelCase")] 149 pub struct ReasonRepostData { 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>, 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>, 156 } 157 pub type ReasonRepost = atrium_api::types::Object<ReasonRepostData>; 158 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
+3
crates/weaver-common/src/lexicons/app/bsky/feed/get_feed_skeleton.rs
··· 18 #[serde(skip_serializing_if = "core::option::Option::is_none")] 19 pub cursor: core::option::Option<String>, 20 pub feed: Vec<crate::app::bsky::feed::defs::SkeletonFeedPost>, 21 } 22 pub type Output = atrium_api::types::Object<OutputData>; 23 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
··· 18 #[serde(skip_serializing_if = "core::option::Option::is_none")] 19 pub cursor: core::option::Option<String>, 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>, 24 } 25 pub type Output = atrium_api::types::Object<OutputData>; 26 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
+2
crates/weaver-common/src/lexicons/app/bsky/feed/like.rs
··· 6 pub struct RecordData { 7 pub created_at: atrium_api::types::string::Datetime, 8 pub subject: crate::com::atproto::repo::strong_ref::Main, 9 } 10 pub type Record = atrium_api::types::Object<RecordData>; 11 impl From<atrium_api::types::Unknown> for RecordData {
··· 6 pub struct RecordData { 7 pub created_at: atrium_api::types::string::Datetime, 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>, 11 } 12 pub type Record = atrium_api::types::Object<RecordData>; 13 impl From<atrium_api::types::Unknown> for RecordData {
+2
crates/weaver-common/src/lexicons/app/bsky/feed/repost.rs
··· 6 pub struct RecordData { 7 pub created_at: atrium_api::types::string::Datetime, 8 pub subject: crate::com::atproto::repo::strong_ref::Main, 9 } 10 pub type Record = atrium_api::types::Object<RecordData>; 11 impl From<atrium_api::types::Unknown> for RecordData {
··· 6 pub struct RecordData { 7 pub created_at: atrium_api::types::string::Datetime, 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>, 11 } 12 pub type Record = atrium_api::types::Object<RecordData>; 13 impl From<atrium_api::types::Unknown> for RecordData {
+1 -1
crates/weaver-common/src/lexicons/app/bsky/notification/list_notifications.rs
··· 46 pub is_read: bool, 47 #[serde(skip_serializing_if = "core::option::Option::is_none")] 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'. 50 pub reason: String, 51 #[serde(skip_serializing_if = "core::option::Option::is_none")] 52 pub reason_subject: core::option::Option<String>,
··· 46 pub is_read: bool, 47 #[serde(skip_serializing_if = "core::option::Option::is_none")] 48 pub labels: core::option::Option<Vec<crate::com::atproto::label::defs::Label>>, 49 + ///The reason why this notification was delivered - e.g. your post was liked, or you received a new follower. 50 pub reason: String, 51 #[serde(skip_serializing_if = "core::option::Option::is_none")] 52 pub reason_subject: core::option::Option<String>,
+1 -1
crates/weaver-common/src/lexicons/client.rs
··· 1150 _ => Err(atrium_xrpc::Error::UnexpectedResponseType), 1151 } 1152 } 1153 - ///Find posts matching search criteria, returning views of those posts. 1154 pub async fn search_posts( 1155 &self, 1156 params: crate::app::bsky::feed::search_posts::Parameters,
··· 1150 _ => Err(atrium_xrpc::Error::UnexpectedResponseType), 1151 } 1152 } 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 pub async fn search_posts( 1155 &self, 1156 params: crate::app::bsky::feed::search_posts::Parameters,
+54 -90
crates/weaver-common/src/lexicons/record.rs
··· 8 #[serde(rename = "app.bsky.actor.status")] 9 LexiconsAppBskyActorStatus(Box<crate::lexicons::app::bsky::actor::status::Record>), 10 #[serde(rename = "app.bsky.feed.generator")] 11 - LexiconsAppBskyFeedGenerator( 12 - Box<crate::lexicons::app::bsky::feed::generator::Record>, 13 - ), 14 #[serde(rename = "app.bsky.feed.like")] 15 LexiconsAppBskyFeedLike(Box<crate::lexicons::app::bsky::feed::like::Record>), 16 #[serde(rename = "app.bsky.feed.post")] ··· 20 #[serde(rename = "app.bsky.feed.repost")] 21 LexiconsAppBskyFeedRepost(Box<crate::lexicons::app::bsky::feed::repost::Record>), 22 #[serde(rename = "app.bsky.feed.threadgate")] 23 - LexiconsAppBskyFeedThreadgate( 24 - Box<crate::lexicons::app::bsky::feed::threadgate::Record>, 25 - ), 26 #[serde(rename = "app.bsky.graph.block")] 27 LexiconsAppBskyGraphBlock(Box<crate::lexicons::app::bsky::graph::block::Record>), 28 #[serde(rename = "app.bsky.graph.follow")] ··· 30 #[serde(rename = "app.bsky.graph.list")] 31 LexiconsAppBskyGraphList(Box<crate::lexicons::app::bsky::graph::list::Record>), 32 #[serde(rename = "app.bsky.graph.listblock")] 33 - LexiconsAppBskyGraphListblock( 34 - Box<crate::lexicons::app::bsky::graph::listblock::Record>, 35 - ), 36 #[serde(rename = "app.bsky.graph.listitem")] 37 - LexiconsAppBskyGraphListitem( 38 - Box<crate::lexicons::app::bsky::graph::listitem::Record>, 39 - ), 40 #[serde(rename = "app.bsky.graph.starterpack")] 41 - LexiconsAppBskyGraphStarterpack( 42 - Box<crate::lexicons::app::bsky::graph::starterpack::Record>, 43 - ), 44 #[serde(rename = "app.bsky.graph.verification")] 45 - LexiconsAppBskyGraphVerification( 46 - Box<crate::lexicons::app::bsky::graph::verification::Record>, 47 - ), 48 #[serde(rename = "app.bsky.labeler.service")] 49 - LexiconsAppBskyLabelerService( 50 - Box<crate::lexicons::app::bsky::labeler::service::Record>, 51 - ), 52 #[serde(rename = "chat.bsky.actor.declaration")] 53 - LexiconsChatBskyActorDeclaration( 54 - Box<crate::lexicons::chat::bsky::actor::declaration::Record>, 55 - ), 56 #[serde(rename = "com.atproto.lexicon.schema")] 57 - LexiconsComAtprotoLexiconSchema( 58 - Box<crate::lexicons::com::atproto::lexicon::schema::Record>, 59 - ), 60 #[serde(rename = "sh.tangled.actor.profile")] 61 - LexiconsShTangledActorProfile( 62 - Box<crate::lexicons::sh::tangled::actor::profile::Record>, 63 - ), 64 #[serde(rename = "sh.weaver.actor.profile")] 65 - LexiconsShWeaverActorProfile( 66 - Box<crate::lexicons::sh::weaver::actor::profile::Record>, 67 - ), 68 #[serde(rename = "sh.weaver.edit.cursor")] 69 LexiconsShWeaverEditCursor(Box<crate::lexicons::sh::weaver::edit::cursor::Record>), 70 #[serde(rename = "sh.weaver.edit.diff")] ··· 72 #[serde(rename = "sh.weaver.edit.root")] 73 LexiconsShWeaverEditRoot(Box<crate::lexicons::sh::weaver::edit::root::Record>), 74 #[serde(rename = "sh.weaver.notebook.authors")] 75 - LexiconsShWeaverNotebookAuthors( 76 - Box<crate::lexicons::sh::weaver::notebook::authors::Record>, 77 - ), 78 #[serde(rename = "sh.weaver.notebook.book")] 79 - LexiconsShWeaverNotebookBook( 80 - Box<crate::lexicons::sh::weaver::notebook::book::Record>, 81 - ), 82 #[serde(rename = "sh.weaver.notebook.entry")] 83 - LexiconsShWeaverNotebookEntry( 84 - Box<crate::lexicons::sh::weaver::notebook::entry::Record>, 85 - ), 86 } 87 impl From<crate::lexicons::app::bsky::actor::profile::Record> for KnownRecord { 88 fn from(record: crate::lexicons::app::bsky::actor::profile::Record) -> Self { ··· 90 } 91 } 92 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 { 96 KnownRecord::LexiconsAppBskyActorProfile(Box::new(record_data.into())) 97 } 98 } ··· 112 } 113 } 114 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 { 118 KnownRecord::LexiconsAppBskyFeedGenerator(Box::new(record_data.into())) 119 } 120 } ··· 144 } 145 } 146 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 { 150 KnownRecord::LexiconsAppBskyFeedPostgate(Box::new(record_data.into())) 151 } 152 } ··· 166 } 167 } 168 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 { 172 KnownRecord::LexiconsAppBskyFeedThreadgate(Box::new(record_data.into())) 173 } 174 } ··· 208 } 209 } 210 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 { 214 KnownRecord::LexiconsAppBskyGraphListblock(Box::new(record_data.into())) 215 } 216 } ··· 220 } 221 } 222 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 { 226 KnownRecord::LexiconsAppBskyGraphListitem(Box::new(record_data.into())) 227 } 228 } ··· 232 } 233 } 234 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 { 238 KnownRecord::LexiconsAppBskyGraphStarterpack(Box::new(record_data.into())) 239 } 240 } ··· 244 } 245 } 246 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 { 250 KnownRecord::LexiconsAppBskyGraphVerification(Box::new(record_data.into())) 251 } 252 } ··· 256 } 257 } 258 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 { 262 KnownRecord::LexiconsAppBskyLabelerService(Box::new(record_data.into())) 263 } 264 } ··· 268 } 269 } 270 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 { 274 KnownRecord::LexiconsChatBskyActorDeclaration(Box::new(record_data.into())) 275 } 276 } ··· 280 } 281 } 282 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 { 286 KnownRecord::LexiconsComAtprotoLexiconSchema(Box::new(record_data.into())) 287 } 288 } ··· 292 } 293 } 294 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 { 298 KnownRecord::LexiconsShTangledActorProfile(Box::new(record_data.into())) 299 } 300 } ··· 304 } 305 } 306 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 { 310 KnownRecord::LexiconsShWeaverActorProfile(Box::new(record_data.into())) 311 } 312 } ··· 346 } 347 } 348 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 { 352 KnownRecord::LexiconsShWeaverNotebookAuthors(Box::new(record_data.into())) 353 } 354 } ··· 358 } 359 } 360 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 { 364 KnownRecord::LexiconsShWeaverNotebookBook(Box::new(record_data.into())) 365 } 366 } 367 impl From<crate::lexicons::sh::weaver::notebook::entry::Record> for KnownRecord { 368 fn from(record: crate::lexicons::sh::weaver::notebook::entry::Record) -> Self { 369 KnownRecord::LexiconsShWeaverNotebookEntry(Box::new(record)) 370 } 371 } 372 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 { 376 KnownRecord::LexiconsShWeaverNotebookEntry(Box::new(record_data.into())) 377 } 378 } 379 impl Into<atrium_api::types::Unknown> for KnownRecord {
··· 8 #[serde(rename = "app.bsky.actor.status")] 9 LexiconsAppBskyActorStatus(Box<crate::lexicons::app::bsky::actor::status::Record>), 10 #[serde(rename = "app.bsky.feed.generator")] 11 + LexiconsAppBskyFeedGenerator(Box<crate::lexicons::app::bsky::feed::generator::Record>), 12 #[serde(rename = "app.bsky.feed.like")] 13 LexiconsAppBskyFeedLike(Box<crate::lexicons::app::bsky::feed::like::Record>), 14 #[serde(rename = "app.bsky.feed.post")] ··· 18 #[serde(rename = "app.bsky.feed.repost")] 19 LexiconsAppBskyFeedRepost(Box<crate::lexicons::app::bsky::feed::repost::Record>), 20 #[serde(rename = "app.bsky.feed.threadgate")] 21 + LexiconsAppBskyFeedThreadgate(Box<crate::lexicons::app::bsky::feed::threadgate::Record>), 22 #[serde(rename = "app.bsky.graph.block")] 23 LexiconsAppBskyGraphBlock(Box<crate::lexicons::app::bsky::graph::block::Record>), 24 #[serde(rename = "app.bsky.graph.follow")] ··· 26 #[serde(rename = "app.bsky.graph.list")] 27 LexiconsAppBskyGraphList(Box<crate::lexicons::app::bsky::graph::list::Record>), 28 #[serde(rename = "app.bsky.graph.listblock")] 29 + LexiconsAppBskyGraphListblock(Box<crate::lexicons::app::bsky::graph::listblock::Record>), 30 #[serde(rename = "app.bsky.graph.listitem")] 31 + LexiconsAppBskyGraphListitem(Box<crate::lexicons::app::bsky::graph::listitem::Record>), 32 #[serde(rename = "app.bsky.graph.starterpack")] 33 + LexiconsAppBskyGraphStarterpack(Box<crate::lexicons::app::bsky::graph::starterpack::Record>), 34 #[serde(rename = "app.bsky.graph.verification")] 35 + LexiconsAppBskyGraphVerification(Box<crate::lexicons::app::bsky::graph::verification::Record>), 36 #[serde(rename = "app.bsky.labeler.service")] 37 + LexiconsAppBskyLabelerService(Box<crate::lexicons::app::bsky::labeler::service::Record>), 38 #[serde(rename = "chat.bsky.actor.declaration")] 39 + LexiconsChatBskyActorDeclaration(Box<crate::lexicons::chat::bsky::actor::declaration::Record>), 40 #[serde(rename = "com.atproto.lexicon.schema")] 41 + LexiconsComAtprotoLexiconSchema(Box<crate::lexicons::com::atproto::lexicon::schema::Record>), 42 #[serde(rename = "sh.tangled.actor.profile")] 43 + LexiconsShTangledActorProfile(Box<crate::lexicons::sh::tangled::actor::profile::Record>), 44 #[serde(rename = "sh.weaver.actor.profile")] 45 + LexiconsShWeaverActorProfile(Box<crate::lexicons::sh::weaver::actor::profile::Record>), 46 #[serde(rename = "sh.weaver.edit.cursor")] 47 LexiconsShWeaverEditCursor(Box<crate::lexicons::sh::weaver::edit::cursor::Record>), 48 #[serde(rename = "sh.weaver.edit.diff")] ··· 50 #[serde(rename = "sh.weaver.edit.root")] 51 LexiconsShWeaverEditRoot(Box<crate::lexicons::sh::weaver::edit::root::Record>), 52 #[serde(rename = "sh.weaver.notebook.authors")] 53 + LexiconsShWeaverNotebookAuthors(Box<crate::lexicons::sh::weaver::notebook::authors::Record>), 54 #[serde(rename = "sh.weaver.notebook.book")] 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>), 58 #[serde(rename = "sh.weaver.notebook.entry")] 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>), 62 } 63 impl From<crate::lexicons::app::bsky::actor::profile::Record> for KnownRecord { 64 fn from(record: crate::lexicons::app::bsky::actor::profile::Record) -> Self { ··· 66 } 67 } 68 impl From<crate::lexicons::app::bsky::actor::profile::RecordData> for KnownRecord { 69 + fn from(record_data: crate::lexicons::app::bsky::actor::profile::RecordData) -> Self { 70 KnownRecord::LexiconsAppBskyActorProfile(Box::new(record_data.into())) 71 } 72 } ··· 86 } 87 } 88 impl From<crate::lexicons::app::bsky::feed::generator::RecordData> for KnownRecord { 89 + fn from(record_data: crate::lexicons::app::bsky::feed::generator::RecordData) -> Self { 90 KnownRecord::LexiconsAppBskyFeedGenerator(Box::new(record_data.into())) 91 } 92 } ··· 116 } 117 } 118 impl From<crate::lexicons::app::bsky::feed::postgate::RecordData> for KnownRecord { 119 + fn from(record_data: crate::lexicons::app::bsky::feed::postgate::RecordData) -> Self { 120 KnownRecord::LexiconsAppBskyFeedPostgate(Box::new(record_data.into())) 121 } 122 } ··· 136 } 137 } 138 impl From<crate::lexicons::app::bsky::feed::threadgate::RecordData> for KnownRecord { 139 + fn from(record_data: crate::lexicons::app::bsky::feed::threadgate::RecordData) -> Self { 140 KnownRecord::LexiconsAppBskyFeedThreadgate(Box::new(record_data.into())) 141 } 142 } ··· 176 } 177 } 178 impl From<crate::lexicons::app::bsky::graph::listblock::RecordData> for KnownRecord { 179 + fn from(record_data: crate::lexicons::app::bsky::graph::listblock::RecordData) -> Self { 180 KnownRecord::LexiconsAppBskyGraphListblock(Box::new(record_data.into())) 181 } 182 } ··· 186 } 187 } 188 impl From<crate::lexicons::app::bsky::graph::listitem::RecordData> for KnownRecord { 189 + fn from(record_data: crate::lexicons::app::bsky::graph::listitem::RecordData) -> Self { 190 KnownRecord::LexiconsAppBskyGraphListitem(Box::new(record_data.into())) 191 } 192 } ··· 196 } 197 } 198 impl From<crate::lexicons::app::bsky::graph::starterpack::RecordData> for KnownRecord { 199 + fn from(record_data: crate::lexicons::app::bsky::graph::starterpack::RecordData) -> Self { 200 KnownRecord::LexiconsAppBskyGraphStarterpack(Box::new(record_data.into())) 201 } 202 } ··· 206 } 207 } 208 impl From<crate::lexicons::app::bsky::graph::verification::RecordData> for KnownRecord { 209 + fn from(record_data: crate::lexicons::app::bsky::graph::verification::RecordData) -> Self { 210 KnownRecord::LexiconsAppBskyGraphVerification(Box::new(record_data.into())) 211 } 212 } ··· 216 } 217 } 218 impl From<crate::lexicons::app::bsky::labeler::service::RecordData> for KnownRecord { 219 + fn from(record_data: crate::lexicons::app::bsky::labeler::service::RecordData) -> Self { 220 KnownRecord::LexiconsAppBskyLabelerService(Box::new(record_data.into())) 221 } 222 } ··· 226 } 227 } 228 impl From<crate::lexicons::chat::bsky::actor::declaration::RecordData> for KnownRecord { 229 + fn from(record_data: crate::lexicons::chat::bsky::actor::declaration::RecordData) -> Self { 230 KnownRecord::LexiconsChatBskyActorDeclaration(Box::new(record_data.into())) 231 } 232 } ··· 236 } 237 } 238 impl From<crate::lexicons::com::atproto::lexicon::schema::RecordData> for KnownRecord { 239 + fn from(record_data: crate::lexicons::com::atproto::lexicon::schema::RecordData) -> Self { 240 KnownRecord::LexiconsComAtprotoLexiconSchema(Box::new(record_data.into())) 241 } 242 } ··· 246 } 247 } 248 impl From<crate::lexicons::sh::tangled::actor::profile::RecordData> for KnownRecord { 249 + fn from(record_data: crate::lexicons::sh::tangled::actor::profile::RecordData) -> Self { 250 KnownRecord::LexiconsShTangledActorProfile(Box::new(record_data.into())) 251 } 252 } ··· 256 } 257 } 258 impl From<crate::lexicons::sh::weaver::actor::profile::RecordData> for KnownRecord { 259 + fn from(record_data: crate::lexicons::sh::weaver::actor::profile::RecordData) -> Self { 260 KnownRecord::LexiconsShWeaverActorProfile(Box::new(record_data.into())) 261 } 262 } ··· 296 } 297 } 298 impl From<crate::lexicons::sh::weaver::notebook::authors::RecordData> for KnownRecord { 299 + fn from(record_data: crate::lexicons::sh::weaver::notebook::authors::RecordData) -> Self { 300 KnownRecord::LexiconsShWeaverNotebookAuthors(Box::new(record_data.into())) 301 } 302 } ··· 306 } 307 } 308 impl From<crate::lexicons::sh::weaver::notebook::book::RecordData> for KnownRecord { 309 + fn from(record_data: crate::lexicons::sh::weaver::notebook::book::RecordData) -> Self { 310 KnownRecord::LexiconsShWeaverNotebookBook(Box::new(record_data.into())) 311 } 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 + } 323 impl From<crate::lexicons::sh::weaver::notebook::entry::Record> for KnownRecord { 324 fn from(record: crate::lexicons::sh::weaver::notebook::entry::Record) -> Self { 325 KnownRecord::LexiconsShWeaverNotebookEntry(Box::new(record)) 326 } 327 } 328 impl From<crate::lexicons::sh::weaver::notebook::entry::RecordData> for KnownRecord { 329 + fn from(record_data: crate::lexicons::sh::weaver::notebook::entry::RecordData) -> Self { 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())) 341 } 342 } 343 impl Into<atrium_api::types::Unknown> for KnownRecord {
+1
crates/weaver-common/src/lexicons/sh/weaver.rs
··· 4 pub mod edit; 5 pub mod embed; 6 pub mod notebook;
··· 4 pub mod edit; 5 pub mod embed; 6 pub mod notebook; 7 + pub mod publish;
+11
crates/weaver-common/src/lexicons/sh/weaver/actor/defs.rs
··· 1 // @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT. 2 //!Definitions for the `sh.weaver.actor.defs` namespace. 3 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] 4 #[serde(rename_all = "camelCase")] 5 pub struct ProfileDataViewData {
··· 1 // @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT. 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>; 14 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] 15 #[serde(rename_all = "camelCase")] 16 pub struct ProfileDataViewData {
+7
crates/weaver-common/src/lexicons/sh/weaver/notebook.rs
··· 2 //!Definitions for the `sh.weaver.notebook` namespace. 3 pub mod authors; 4 pub mod book; 5 pub mod defs; 6 pub mod entry; 7 #[derive(Debug)] ··· 15 impl atrium_api::types::Collection for Book { 16 const NSID: &'static str = "sh.weaver.notebook.book"; 17 type Record = book::Record; 18 } 19 #[derive(Debug)] 20 pub struct Entry;
··· 2 //!Definitions for the `sh.weaver.notebook` namespace. 3 pub mod authors; 4 pub mod book; 5 + pub mod chapter; 6 pub mod defs; 7 pub mod entry; 8 #[derive(Debug)] ··· 16 impl atrium_api::types::Collection for Book { 17 const NSID: &'static str = "sh.weaver.notebook.book"; 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; 25 } 26 #[derive(Debug)] 27 pub struct Entry;
+2 -2
crates/weaver-common/src/lexicons/sh/weaver/notebook/book.rs
··· 4 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] 5 #[serde(rename_all = "camelCase")] 6 pub struct RecordData { 7 - pub authors: crate::sh::weaver::notebook::defs::AuthorListView, 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::sh::weaver::notebook::defs::BookEntryView>, 12 #[serde(skip_serializing_if = "core::option::Option::is_none")] 13 pub tags: core::option::Option<crate::sh::weaver::notebook::defs::Tags>, 14 #[serde(skip_serializing_if = "core::option::Option::is_none")]
··· 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 #[serde(skip_serializing_if = "core::option::Option::is_none")] 13 pub tags: core::option::Option<crate::sh::weaver::notebook::defs::Tags>, 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 pub prev: core::option::Option<BookEntryRef>, 31 } 32 pub type BookEntryView = atrium_api::types::Object<BookEntryViewData>; 33 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] 34 #[serde(rename_all = "camelCase")] 35 pub struct EntryViewData {
··· 30 pub prev: core::option::Option<BookEntryRef>, 31 } 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>; 42 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] 43 #[serde(rename_all = "camelCase")] 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 pub mod oauth; 9 pub mod resolver; 10 pub mod xrpc_server; 11 - use atrium_api::types::{BlobRef, TypedBlobRef, string::Did}; 12 pub use lexicons::*; 13 14 pub use crate::error::{Error, IoError, ParseError, SerDeError}; 15 ··· 56 mime_type.strip_prefix("image/").unwrap_or(mime_type) 57 ) 58 }
··· 8 pub mod oauth; 9 pub mod resolver; 10 pub mod xrpc_server; 11 + use std::sync::OnceLock; 12 + 13 + pub use atrium_api::types::*; 14 pub use lexicons::*; 15 + use regex::Regex; 16 + use string::Did; 17 18 pub use crate::error::{Error, IoError, ParseError, SerDeError}; 19 ··· 60 mime_type.strip_prefix("image/").unwrap_or(mime_type) 61 ) 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 publish = false 7 8 [dependencies] 9 - n0-future = { workspace = true } 10 atrium-api = { version = "0.25.3", default-features = false } 11 12 weaver-common = { path = "../weaver-common" } 13 - 14 weaver-workspace-hack = { version = "0.1", path = "../weaver-workspace-hack" } 15 compact_string = "0.1.0" 16 http = "1.3.1" 17 url = "2.5.4"
··· 6 publish = false 7 8 [dependencies] 9 + n0-future.workspace = true 10 atrium-api = { version = "0.25.3", default-features = false } 11 12 weaver-common = { path = "../weaver-common" } 13 + markdown-weaver = { workspace = true } 14 weaver-workspace-hack = { version = "0.1", path = "../weaver-workspace-hack" } 15 compact_string = "0.1.0" 16 http = "1.3.1" 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 //! This crate works with the weaver-markdown crate to render and optionally upload markdown notebooks to your Atproto PDS. 4 //! 5 6 pub mod types;
··· 3 //! This crate works with the weaver-markdown crate to render and optionally upload markdown notebooks to your Atproto PDS. 4 //! 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; 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 license.workspace = true 6 publish = false 7 8 [dependencies] 9 weaver-common = { path = "../weaver-common" } 10 weaver-workspace-hack = { version = "0.1", path = "../weaver-workspace-hack" }
··· 5 license.workspace = true 6 publish = false 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 + 16 [dependencies] 17 weaver-common = { path = "../weaver-common" } 18 weaver-workspace-hack = { version = "0.1", path = "../weaver-workspace-hack" }
+1
crates/weaver-server/src/lib.rs
···
··· 1 +
+6 -6
flake.lock
··· 51 }, 52 "nixpkgs": { 53 "locked": { 54 - "lastModified": 1747467164, 55 - "narHash": "sha256-JBXbjJ0t6T6BbVc9iPVquQI9XSXCGQJD8c8SgnUquus=", 56 "owner": "NixOS", 57 "repo": "nixpkgs", 58 - "rev": "3fcbdcfc707e0aa42c541b7743e05820472bdaec", 59 "type": "github" 60 }, 61 "original": { ··· 95 "nixpkgs": "nixpkgs_2" 96 }, 97 "locked": { 98 - "lastModified": 1747535902, 99 - "narHash": "sha256-rKWBtLcqQeu8PpfKIBn1ORXS2udSH/MwnQFwfSpFOLg=", 100 "owner": "oxalica", 101 "repo": "rust-overlay", 102 - "rev": "b7a99615d26b82c39b73ccc9026545c3f3403b71", 103 "type": "github" 104 }, 105 "original": {
··· 51 }, 52 "nixpkgs": { 53 "locked": { 54 + "lastModified": 1747728033, 55 + "narHash": "sha256-NnXFQu7g4LnvPIPfJmBuZF7LFy/fey2g2+LCzjQhTUk=", 56 "owner": "NixOS", 57 "repo": "nixpkgs", 58 + "rev": "2f9173bde1d3fbf1ad26ff6d52f952f9e9da52ea", 59 "type": "github" 60 }, 61 "original": { ··· 95 "nixpkgs": "nixpkgs_2" 96 }, 97 "locked": { 98 + "lastModified": 1747795013, 99 + "narHash": "sha256-c7i0xJ+xFhgjO9SWHYu5dF/7lq63RPDvwKAdjc6VCE4=", 100 "owner": "oxalica", 101 "repo": "rust-overlay", 102 + "rev": "6b1cf12374361859242a562e1933a7930649131a", 103 "type": "github" 104 }, 105 "original": {
+1 -2
flake.nix
··· 37 p.rust-bin.selectLatestNightlyWith(toolchain: toolchain.default.override { 38 # Set the build targets supported by the toolchain, 39 # wasm32-unknown-unknown is required for trunk. 40 - #targets = ["wasm32-unknown-unknown"]; 41 extensions = [ 42 - "llvm-tools" 43 "rust-src" 44 "rust-analyzer" 45 "clippy"
··· 37 p.rust-bin.selectLatestNightlyWith(toolchain: toolchain.default.override { 38 # Set the build targets supported by the toolchain, 39 # wasm32-unknown-unknown is required for trunk. 40 + targets = ["wasm32-unknown-unknown"]; 41 extensions = [ 42 "rust-src" 43 "rust-analyzer" 44 "clippy"
+14
lexicons/sh/weaver/actor/defs.json
··· 59 } 60 } 61 }, 62 "tangledProfileView": { 63 "type": "object", 64 "required": ["bluesky", "did", "handle"],
··· 59 } 60 } 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 + }, 76 "tangledProfileView": { 77 "type": "object", 78 "required": ["bluesky", "did", "handle"],
+1 -1
lexicons/sh/weaver/notebook/authors.json
··· 5 "main": { 6 "type": "record", 7 "description": "Authors of a Weaver notebook.", 8 - "key": "literal:self", 9 "record": { 10 "type": "object", 11 "required": ["authorList"],
··· 5 "main": { 6 "type": "record", 7 "description": "Authors of a Weaver notebook.", 8 + "key": "tid", 9 "record": { 10 "type": "object", 11 "required": ["authorList"],
+6 -3
lexicons/sh/weaver/notebook/book.json
··· 13 "title": { "type": "ref", "ref": "sh.weaver.notebook.defs#title" }, 14 "tags": { "type": "ref", "ref": "sh.weaver.notebook.defs#tags" }, 15 "authors": { 16 - "type": "ref", 17 - "ref": "sh.weaver.notebook.defs#authorListView" 18 }, 19 "entryList": { 20 "type": "array", 21 "items": { 22 "type": "ref", 23 - "ref": "sh.weaver.notebook.defs#bookEntryView" 24 } 25 }, 26 "createdAt": {
··· 13 "title": { "type": "ref", "ref": "sh.weaver.notebook.defs#title" }, 14 "tags": { "type": "ref", "ref": "sh.weaver.notebook.defs#tags" }, 15 "authors": { 16 + "type": "array", 17 + "items": { 18 + "type": "ref", 19 + "ref": "sh.weaver.actor.defs#author" 20 + } 21 }, 22 "entryList": { 23 "type": "array", 24 "items": { 25 "type": "ref", 26 + "ref": "com.atproto.repo.strongRef" 27 } 28 }, 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 }, 113 "maxLength": 10, 114 "description": "An array of tags associated with the notebook entry. Tags can help categorize and organize entries." 115 } 116 } 117 }
··· 112 }, 113 "maxLength": 10, 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 + } 127 } 128 } 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 + }