Rust library to generate static websites
5
fork

Configure Feed

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

feat: make dev server better (#40)

* feat: make dev server better

* fix: rework the world

* fix: cancel builds

* feat: error overlay

* fix: js tweaks

* feat: the great renaming

* feat: some more

* refactor: js overlay

* feat: some more renames

* chore: changeset

authored by

Erika and committed by
GitHub
2bfa8a87 8d20f51d

+2105 -1963
+7
.sampo/changesets/jovial-witch-aino.md
··· 1 + --- 2 + maudit-cli: minor 3 + --- 4 + 5 + Improve general hot-reloading experience. 6 + 7 + The Maudit CLI will now output errors encountered during hot-reloading to the terminal and in the browser.
+20
.sampo/changesets/stern-duke-loviatar.md
··· 1 + --- 2 + maudit: minor 3 + maudit-macros: minor 4 + --- 5 + 6 + Rename (almost) all instances of Routes to Pages and vice versa. 7 + 8 + Previously, in Maudit, a _page_ referred to the struct you'd pass to `coronate` and a page could have multiple routes if it was dynamic. In my opinion, the reverse is more intuitive: a _route_ is the struct you define, and a route can have multiple _pages_ if it's dynamic. This also applies to every other types that had "Route" or "Page" in their name. 9 + 10 + As such, the following renames were made: 11 + 12 + - `Route` -> `Page` 13 + - `FullRoute` -> `FullPage` 14 + - `RouteContext` -> `PageContext` 15 + - `RouteParams` -> `PageParams` 16 + - `Routes` -> `Pages` 17 + - `fn routes` -> `fn pages` 18 + - `maudit::page` -> `maudit::route` (including the prelude, which is now `maudit::route::prelude`) 19 + 20 + And probably some others I forgot.
+2 -1
.vscode/settings.json
··· 4 4 }, 5 5 "tailwindCSS.experimental.classRegex": [ 6 6 ["[\\w-]+((?:\\.\\s*\\S+\\s*)*)", "\\.\"?([^.\"]+)\"?"] 7 - ] 7 + ], 8 + "biome.enabled": false 8 9 }
+33 -718
Cargo.lock
··· 157 157 checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" 158 158 159 159 [[package]] 160 - name = "async-priority-channel" 161 - version = "0.2.0" 162 - source = "registry+https://github.com/rust-lang/crates.io-index" 163 - checksum = "acde96f444d31031f760c5c43dc786b97d3e1cb2ee49dd06898383fe9a999758" 164 - dependencies = [ 165 - "event-listener", 166 - ] 167 - 168 - [[package]] 169 - name = "async-recursion" 170 - version = "1.1.1" 171 - source = "registry+https://github.com/rust-lang/crates.io-index" 172 - checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" 173 - dependencies = [ 174 - "proc-macro2", 175 - "quote", 176 - "syn 2.0.106", 177 - ] 178 - 179 - [[package]] 180 160 name = "async-scoped" 181 161 version = "0.9.0" 182 162 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 197 177 "quote", 198 178 "syn 2.0.106", 199 179 ] 200 - 201 - [[package]] 202 - name = "atomic-take" 203 - version = "1.1.0" 204 - source = "registry+https://github.com/rust-lang/crates.io-index" 205 - checksum = "a8ab6b55fe97976e46f91ddbed8d147d966475dc29b2032757ba47e02376fbc3" 206 180 207 181 [[package]] 208 182 name = "atomic-waker" ··· 768 742 dependencies = [ 769 743 "brk_rolldown_error", 770 744 "notify 8.2.0", 771 - "notify-debouncer-full", 745 + "notify-debouncer-full 0.6.0", 772 746 ] 773 747 774 748 [[package]] ··· 780 754 "oxc_index", 781 755 "oxc_sourcemap", 782 756 "rustc-hash", 783 - "serde", 784 - ] 785 - 786 - [[package]] 787 - name = "bstr" 788 - version = "1.12.0" 789 - source = "registry+https://github.com/rust-lang/crates.io-index" 790 - checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" 791 - dependencies = [ 792 - "memchr", 793 - "regex-automata 0.4.10", 794 757 "serde", 795 758 ] 796 759 ··· 952 915 "colored", 953 916 "glob", 954 917 "libc", 955 - "nix 0.29.0", 918 + "nix", 956 919 "serde", 957 920 "serde_json", 958 921 "statrs", ··· 1046 1009 ] 1047 1010 1048 1011 [[package]] 1049 - name = "concurrent-queue" 1050 - version = "2.5.0" 1051 - source = "registry+https://github.com/rust-lang/crates.io-index" 1052 - checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" 1053 - dependencies = [ 1054 - "crossbeam-utils", 1055 - ] 1056 - 1057 - [[package]] 1058 1012 name = "concurrent_lru" 1059 1013 version = "0.2.0" 1060 1014 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1441 1395 ] 1442 1396 1443 1397 [[package]] 1444 - name = "event-listener" 1445 - version = "4.0.3" 1446 - source = "registry+https://github.com/rust-lang/crates.io-index" 1447 - checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" 1448 - dependencies = [ 1449 - "concurrent-queue", 1450 - "parking", 1451 - "pin-project-lite", 1452 - ] 1453 - 1454 - [[package]] 1455 1398 name = "exr" 1456 1399 version = "1.73.0" 1457 1400 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1484 1427 checksum = "3d26eec0ae9682c457cb0f85de67ad417b716ae852736a5d94c2ad6e92a997c9" 1485 1428 dependencies = [ 1486 1429 "arrayvec", 1487 - ] 1488 - 1489 - [[package]] 1490 - name = "faster-hex" 1491 - version = "0.10.0" 1492 - source = "registry+https://github.com/rust-lang/crates.io-index" 1493 - checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" 1494 - dependencies = [ 1495 - "heapless", 1496 - "serde", 1497 1430 ] 1498 1431 1499 1432 [[package]] ··· 1776 1709 checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 1777 1710 1778 1711 [[package]] 1779 - name = "gix-actor" 1780 - version = "0.35.4" 1781 - source = "registry+https://github.com/rust-lang/crates.io-index" 1782 - checksum = "2d36dcf9efe32b51b12dfa33cedff8414926124e760a32f9e7a6b5580d280967" 1783 - dependencies = [ 1784 - "bstr", 1785 - "gix-date", 1786 - "gix-utils", 1787 - "itoa", 1788 - "thiserror 2.0.16", 1789 - "winnow", 1790 - ] 1791 - 1792 - [[package]] 1793 - name = "gix-config" 1794 - version = "0.45.1" 1795 - source = "registry+https://github.com/rust-lang/crates.io-index" 1796 - checksum = "48f3c8f357ae049bfb77493c2ec9010f58cfc924ae485e1116c3718fc0f0d881" 1797 - dependencies = [ 1798 - "bstr", 1799 - "gix-config-value", 1800 - "gix-features", 1801 - "gix-glob", 1802 - "gix-path", 1803 - "gix-ref", 1804 - "gix-sec", 1805 - "memchr", 1806 - "once_cell", 1807 - "smallvec", 1808 - "thiserror 2.0.16", 1809 - "unicode-bom", 1810 - "winnow", 1811 - ] 1812 - 1813 - [[package]] 1814 - name = "gix-config-value" 1815 - version = "0.15.1" 1816 - source = "registry+https://github.com/rust-lang/crates.io-index" 1817 - checksum = "9f012703eb67e263c6c1fc96649fec47694dd3e5d2a91abfc65e4a6a6dc85309" 1818 - dependencies = [ 1819 - "bitflags 2.9.4", 1820 - "bstr", 1821 - "gix-path", 1822 - "libc", 1823 - "thiserror 2.0.16", 1824 - ] 1825 - 1826 - [[package]] 1827 - name = "gix-date" 1828 - version = "0.10.5" 1829 - source = "registry+https://github.com/rust-lang/crates.io-index" 1830 - checksum = "996b6b90bafb287330af92b274c3e64309dc78359221d8612d11cd10c8b9fe1c" 1831 - dependencies = [ 1832 - "bstr", 1833 - "itoa", 1834 - "jiff", 1835 - "smallvec", 1836 - "thiserror 2.0.16", 1837 - ] 1838 - 1839 - [[package]] 1840 - name = "gix-features" 1841 - version = "0.42.1" 1842 - source = "registry+https://github.com/rust-lang/crates.io-index" 1843 - checksum = "56f4399af6ec4fd9db84dd4cf9656c5c785ab492ab40a7c27ea92b4241923fed" 1844 - dependencies = [ 1845 - "gix-path", 1846 - "gix-trace", 1847 - "gix-utils", 1848 - "libc", 1849 - "prodash", 1850 - "walkdir", 1851 - ] 1852 - 1853 - [[package]] 1854 - name = "gix-fs" 1855 - version = "0.15.0" 1856 - source = "registry+https://github.com/rust-lang/crates.io-index" 1857 - checksum = "67a0637149b4ef24d3ea55f81f77231401c8463fae6da27331c987957eb597c7" 1858 - dependencies = [ 1859 - "bstr", 1860 - "fastrand", 1861 - "gix-features", 1862 - "gix-path", 1863 - "gix-utils", 1864 - "thiserror 2.0.16", 1865 - ] 1866 - 1867 - [[package]] 1868 - name = "gix-glob" 1869 - version = "0.20.1" 1870 - source = "registry+https://github.com/rust-lang/crates.io-index" 1871 - checksum = "90181472925b587f6079698f79065ff64786e6d6c14089517a1972bca99fb6e9" 1872 - dependencies = [ 1873 - "bitflags 2.9.4", 1874 - "bstr", 1875 - "gix-features", 1876 - "gix-path", 1877 - ] 1878 - 1879 - [[package]] 1880 - name = "gix-hash" 1881 - version = "0.18.0" 1882 - source = "registry+https://github.com/rust-lang/crates.io-index" 1883 - checksum = "8d4900562c662852a6b42e2ef03442eccebf24f047d8eab4f23bc12ef0d785d8" 1884 - dependencies = [ 1885 - "faster-hex", 1886 - "gix-features", 1887 - "sha1-checked", 1888 - "thiserror 2.0.16", 1889 - ] 1890 - 1891 - [[package]] 1892 - name = "gix-hashtable" 1893 - version = "0.8.1" 1894 - source = "registry+https://github.com/rust-lang/crates.io-index" 1895 - checksum = "b5b5cb3c308b4144f2612ff64e32130e641279fcf1a84d8d40dad843b4f64904" 1896 - dependencies = [ 1897 - "gix-hash", 1898 - "hashbrown 0.14.5", 1899 - "parking_lot", 1900 - ] 1901 - 1902 - [[package]] 1903 - name = "gix-lock" 1904 - version = "17.1.0" 1905 - source = "registry+https://github.com/rust-lang/crates.io-index" 1906 - checksum = "570f8b034659f256366dc90f1a24924902f20acccd6a15be96d44d1269e7a796" 1907 - dependencies = [ 1908 - "gix-tempfile", 1909 - "gix-utils", 1910 - "thiserror 2.0.16", 1911 - ] 1912 - 1913 - [[package]] 1914 - name = "gix-object" 1915 - version = "0.49.1" 1916 - source = "registry+https://github.com/rust-lang/crates.io-index" 1917 - checksum = "d957ca3640c555d48bb27f8278c67169fa1380ed94f6452c5590742524c40fbb" 1918 - dependencies = [ 1919 - "bstr", 1920 - "gix-actor", 1921 - "gix-date", 1922 - "gix-features", 1923 - "gix-hash", 1924 - "gix-hashtable", 1925 - "gix-path", 1926 - "gix-utils", 1927 - "gix-validate", 1928 - "itoa", 1929 - "smallvec", 1930 - "thiserror 2.0.16", 1931 - "winnow", 1932 - ] 1933 - 1934 - [[package]] 1935 - name = "gix-path" 1936 - version = "0.10.20" 1937 - source = "registry+https://github.com/rust-lang/crates.io-index" 1938 - checksum = "06d37034a4c67bbdda76f7bcd037b2f7bc0fba0c09a6662b19697a5716e7b2fd" 1939 - dependencies = [ 1940 - "bstr", 1941 - "gix-trace", 1942 - "gix-validate", 1943 - "home", 1944 - "once_cell", 1945 - "thiserror 2.0.16", 1946 - ] 1947 - 1948 - [[package]] 1949 - name = "gix-ref" 1950 - version = "0.52.1" 1951 - source = "registry+https://github.com/rust-lang/crates.io-index" 1952 - checksum = "d1b7985657029684d759f656b09abc3e2c73085596d5cdb494428823970a7762" 1953 - dependencies = [ 1954 - "gix-actor", 1955 - "gix-features", 1956 - "gix-fs", 1957 - "gix-hash", 1958 - "gix-lock", 1959 - "gix-object", 1960 - "gix-path", 1961 - "gix-tempfile", 1962 - "gix-utils", 1963 - "gix-validate", 1964 - "memmap2", 1965 - "thiserror 2.0.16", 1966 - "winnow", 1967 - ] 1968 - 1969 - [[package]] 1970 - name = "gix-sec" 1971 - version = "0.11.0" 1972 - source = "registry+https://github.com/rust-lang/crates.io-index" 1973 - checksum = "d0dabbc78c759ecc006b970339394951b2c8e1e38a37b072c105b80b84c308fd" 1974 - dependencies = [ 1975 - "bitflags 2.9.4", 1976 - "gix-path", 1977 - "libc", 1978 - "windows-sys 0.59.0", 1979 - ] 1980 - 1981 - [[package]] 1982 - name = "gix-tempfile" 1983 - version = "17.1.0" 1984 - source = "registry+https://github.com/rust-lang/crates.io-index" 1985 - checksum = "c750e8c008453a2dba67a2b0d928b7716e05da31173a3f5e351d5457ad4470aa" 1986 - dependencies = [ 1987 - "gix-fs", 1988 - "libc", 1989 - "once_cell", 1990 - "parking_lot", 1991 - "tempfile", 1992 - ] 1993 - 1994 - [[package]] 1995 - name = "gix-trace" 1996 - version = "0.1.13" 1997 - source = "registry+https://github.com/rust-lang/crates.io-index" 1998 - checksum = "e2ccaf54b0b1743a695b482ca0ab9d7603744d8d10b2e5d1a332fef337bee658" 1999 - 2000 - [[package]] 2001 - name = "gix-utils" 2002 - version = "0.3.0" 2003 - source = "registry+https://github.com/rust-lang/crates.io-index" 2004 - checksum = "5351af2b172caf41a3728eb4455326d84e0d70fe26fc4de74ab0bd37df4191c5" 2005 - dependencies = [ 2006 - "fastrand", 2007 - "unicode-normalization", 2008 - ] 2009 - 2010 - [[package]] 2011 - name = "gix-validate" 2012 - version = "0.10.0" 2013 - source = "registry+https://github.com/rust-lang/crates.io-index" 2014 - checksum = "77b9e00cacde5b51388d28ed746c493b18a6add1f19b5e01d686b3b9ece66d4d" 2015 - dependencies = [ 2016 - "bstr", 2017 - "thiserror 2.0.16", 2018 - ] 2019 - 2020 - [[package]] 2021 1712 name = "glob" 2022 1713 version = "0.3.3" 2023 1714 source = "registry+https://github.com/rust-lang/crates.io-index" 2024 1715 checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" 2025 1716 2026 1717 [[package]] 2027 - name = "globset" 2028 - version = "0.4.16" 2029 - source = "registry+https://github.com/rust-lang/crates.io-index" 2030 - checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" 2031 - dependencies = [ 2032 - "aho-corasick", 2033 - "bstr", 2034 - "log", 2035 - "regex-automata 0.4.10", 2036 - "regex-syntax 0.8.6", 2037 - ] 2038 - 2039 - [[package]] 2040 1718 name = "half" 2041 1719 version = "2.6.0" 2042 1720 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2047 1725 ] 2048 1726 2049 1727 [[package]] 2050 - name = "hash32" 2051 - version = "0.3.1" 2052 - source = "registry+https://github.com/rust-lang/crates.io-index" 2053 - checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" 2054 - dependencies = [ 2055 - "byteorder", 2056 - ] 2057 - 2058 - [[package]] 2059 1728 name = "hashbrown" 2060 1729 version = "0.14.5" 2061 1730 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2082 1751 ] 2083 1752 2084 1753 [[package]] 2085 - name = "heapless" 2086 - version = "0.8.0" 2087 - source = "registry+https://github.com/rust-lang/crates.io-index" 2088 - checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" 2089 - dependencies = [ 2090 - "hash32", 2091 - "stable_deref_trait", 2092 - ] 2093 - 2094 - [[package]] 2095 1754 name = "heck" 2096 1755 version = "0.5.0" 2097 1756 source = "registry+https://github.com/rust-lang/crates.io-index" 2098 1757 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 2099 1758 2100 1759 [[package]] 2101 - name = "home" 2102 - version = "0.5.11" 2103 - source = "registry+https://github.com/rust-lang/crates.io-index" 2104 - checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" 2105 - dependencies = [ 2106 - "windows-sys 0.59.0", 2107 - ] 2108 - 2109 - [[package]] 2110 1760 name = "http" 2111 1761 version = "1.3.1" 2112 1762 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2207 1857 "js-sys", 2208 1858 "log", 2209 1859 "wasm-bindgen", 2210 - "windows-core 0.62.0", 1860 + "windows-core", 2211 1861 ] 2212 1862 2213 1863 [[package]] ··· 2327 1977 ] 2328 1978 2329 1979 [[package]] 2330 - name = "ignore" 2331 - version = "0.4.23" 2332 - source = "registry+https://github.com/rust-lang/crates.io-index" 2333 - checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" 2334 - dependencies = [ 2335 - "crossbeam-deque", 2336 - "globset", 2337 - "log", 2338 - "memchr", 2339 - "regex-automata 0.4.10", 2340 - "same-file", 2341 - "walkdir", 2342 - "winapi-util", 2343 - ] 2344 - 2345 - [[package]] 2346 - name = "ignore-files" 2347 - version = "3.0.4" 2348 - source = "registry+https://github.com/rust-lang/crates.io-index" 2349 - checksum = "834d78be07a00bd65bdf068027f6b0118ef98d3779e1629edb6571616e28f60d" 2350 - dependencies = [ 2351 - "dunce", 2352 - "futures", 2353 - "gix-config", 2354 - "ignore", 2355 - "miette", 2356 - "normalize-path", 2357 - "project-origins", 2358 - "radix_trie", 2359 - "thiserror 2.0.16", 2360 - "tokio", 2361 - "tracing", 2362 - ] 2363 - 2364 - [[package]] 2365 1980 name = "image" 2366 1981 version = "0.25.8" 2367 1982 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2529 2144 checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" 2530 2145 dependencies = [ 2531 2146 "jiff-static", 2532 - "jiff-tzdb-platform", 2533 2147 "log", 2534 2148 "portable-atomic", 2535 2149 "portable-atomic-util", 2536 2150 "serde", 2537 - "windows-sys 0.59.0", 2538 2151 ] 2539 2152 2540 2153 [[package]] ··· 2549 2162 ] 2550 2163 2551 2164 [[package]] 2552 - name = "jiff-tzdb" 2553 - version = "0.1.4" 2554 - source = "registry+https://github.com/rust-lang/crates.io-index" 2555 - checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524" 2556 - 2557 - [[package]] 2558 - name = "jiff-tzdb-platform" 2559 - version = "0.1.3" 2560 - source = "registry+https://github.com/rust-lang/crates.io-index" 2561 - checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" 2562 - dependencies = [ 2563 - "jiff-tzdb", 2564 - ] 2565 - 2566 - [[package]] 2567 2165 name = "jobserver" 2568 2166 version = "0.1.34" 2569 2167 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2826 2424 version = "0.4.5" 2827 2425 dependencies = [ 2828 2426 "axum", 2427 + "brk_rolldown", 2829 2428 "chrono", 2830 2429 "clap", 2831 2430 "colored", ··· 2833 2432 "futures", 2834 2433 "inquire", 2835 2434 "local-ip-address", 2435 + "notify 6.1.1", 2436 + "notify-debouncer-full 0.3.2", 2836 2437 "quanta", 2837 2438 "rand 0.9.2", 2439 + "serde_json", 2838 2440 "spinach", 2839 2441 "tar", 2840 2442 "tokio", 2443 + "tokio-util", 2841 2444 "toml_edit", 2842 2445 "tower-http", 2843 2446 "tracing", 2844 2447 "tracing-subscriber", 2845 2448 "ureq", 2846 - "watchexec", 2847 - "watchexec-events", 2848 2449 ] 2849 2450 2850 2451 [[package]] ··· 2947 2548 checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" 2948 2549 2949 2550 [[package]] 2950 - name = "memmap2" 2951 - version = "0.9.8" 2952 - source = "registry+https://github.com/rust-lang/crates.io-index" 2953 - checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" 2954 - dependencies = [ 2955 - "libc", 2956 - ] 2957 - 2958 - [[package]] 2959 - name = "miette" 2960 - version = "7.6.0" 2961 - source = "registry+https://github.com/rust-lang/crates.io-index" 2962 - checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" 2963 - dependencies = [ 2964 - "cfg-if", 2965 - "miette-derive", 2966 - "unicode-width 0.1.14", 2967 - ] 2968 - 2969 - [[package]] 2970 - name = "miette-derive" 2971 - version = "7.6.0" 2972 - source = "registry+https://github.com/rust-lang/crates.io-index" 2973 - checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" 2974 - dependencies = [ 2975 - "proc-macro2", 2976 - "quote", 2977 - "syn 2.0.106", 2978 - ] 2979 - 2980 - [[package]] 2981 2551 name = "mime" 2982 2552 version = "0.3.17" 2983 2553 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3105 2675 ] 3106 2676 3107 2677 [[package]] 3108 - name = "nix" 3109 - version = "0.30.1" 3110 - source = "registry+https://github.com/rust-lang/crates.io-index" 3111 - checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" 3112 - dependencies = [ 3113 - "bitflags 2.9.4", 3114 - "cfg-if", 3115 - "cfg_aliases", 3116 - "libc", 3117 - ] 3118 - 3119 - [[package]] 3120 2678 name = "nom" 3121 2679 version = "7.1.3" 3122 2680 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3148 2706 checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" 3149 2707 3150 2708 [[package]] 3151 - name = "normalize-path" 3152 - version = "0.2.1" 3153 - source = "registry+https://github.com/rust-lang/crates.io-index" 3154 - checksum = "f5438dd2b2ff4c6df6e1ce22d825ed2fa93ee2922235cc45186991717f0a892d" 3155 - 3156 - [[package]] 3157 2709 name = "notify" 3158 2710 version = "6.1.1" 3159 2711 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3192 2744 3193 2745 [[package]] 3194 2746 name = "notify-debouncer-full" 2747 + version = "0.3.2" 2748 + source = "registry+https://github.com/rust-lang/crates.io-index" 2749 + checksum = "fb7fd166739789c9ff169e654dc1501373db9d80a4c3f972817c8a4d7cf8f34e" 2750 + dependencies = [ 2751 + "crossbeam-channel", 2752 + "file-id", 2753 + "log", 2754 + "notify 6.1.1", 2755 + "parking_lot", 2756 + "walkdir", 2757 + ] 2758 + 2759 + [[package]] 2760 + name = "notify-debouncer-full" 3195 2761 version = "0.6.0" 3196 2762 source = "registry+https://github.com/rust-lang/crates.io-index" 3197 2763 checksum = "375bd3a138be7bfeff3480e4a623df4cbfb55b79df617c055cd810ba466fa078" ··· 3687 3253 "thiserror 2.0.16", 3688 3254 "tracing", 3689 3255 "url", 3690 - "windows 0.62.0", 3256 + "windows", 3691 3257 ] 3692 3258 3693 3259 [[package]] ··· 3863 3429 ] 3864 3430 3865 3431 [[package]] 3866 - name = "parking" 3867 - version = "2.2.1" 3868 - source = "registry+https://github.com/rust-lang/crates.io-index" 3869 - checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" 3870 - 3871 - [[package]] 3872 3432 name = "parking_lot" 3873 3433 version = "0.12.4" 3874 3434 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4186 3746 ] 4187 3747 4188 3748 [[package]] 4189 - name = "process-wrap" 4190 - version = "8.2.1" 4191 - source = "registry+https://github.com/rust-lang/crates.io-index" 4192 - checksum = "a3ef4f2f0422f23a82ec9f628ea2acd12871c81a9362b02c43c1aa86acfc3ba1" 4193 - dependencies = [ 4194 - "futures", 4195 - "indexmap", 4196 - "nix 0.30.1", 4197 - "tokio", 4198 - "tracing", 4199 - "windows 0.61.3", 4200 - ] 4201 - 4202 - [[package]] 4203 - name = "prodash" 4204 - version = "29.0.2" 4205 - source = "registry+https://github.com/rust-lang/crates.io-index" 4206 - checksum = "f04bb108f648884c23b98a0e940ebc2c93c0c3b89f04dbaf7eb8256ce617d1bc" 4207 - dependencies = [ 4208 - "log", 4209 - "parking_lot", 4210 - ] 4211 - 4212 - [[package]] 4213 3749 name = "profiling" 4214 3750 version = "1.0.17" 4215 3751 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4229 3765 ] 4230 3766 4231 3767 [[package]] 4232 - name = "project-origins" 4233 - version = "1.4.2" 4234 - source = "registry+https://github.com/rust-lang/crates.io-index" 4235 - checksum = "e42382141d102db809df94324b513c388b047ebc47926eec5417623b88781527" 4236 - dependencies = [ 4237 - "futures", 4238 - "tokio", 4239 - "tokio-stream", 4240 - ] 4241 - 4242 - [[package]] 4243 3768 name = "pulldown-cmark" 4244 3769 version = "0.12.2" 4245 3770 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4845 4370 ] 4846 4371 4847 4372 [[package]] 4848 - name = "sha1-checked" 4849 - version = "0.10.0" 4850 - source = "registry+https://github.com/rust-lang/crates.io-index" 4851 - checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" 4852 - dependencies = [ 4853 - "digest", 4854 - "sha1", 4855 - ] 4856 - 4857 - [[package]] 4858 4373 name = "sharded-slab" 4859 4374 version = "0.1.7" 4860 4375 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5114 4629 checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" 5115 4630 5116 4631 [[package]] 5117 - name = "tempfile" 5118 - version = "3.22.0" 5119 - source = "registry+https://github.com/rust-lang/crates.io-index" 5120 - checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" 5121 - dependencies = [ 5122 - "fastrand", 5123 - "getrandom 0.3.3", 5124 - "once_cell", 5125 - "rustix", 5126 - "windows-sys 0.61.0", 5127 - ] 5128 - 5129 - [[package]] 5130 4632 name = "termcolor" 5131 4633 version = "1.4.1" 5132 4634 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5266 4768 ] 5267 4769 5268 4770 [[package]] 5269 - name = "tinyvec" 5270 - version = "1.10.0" 5271 - source = "registry+https://github.com/rust-lang/crates.io-index" 5272 - checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" 5273 - dependencies = [ 5274 - "tinyvec_macros", 5275 - ] 5276 - 5277 - [[package]] 5278 - name = "tinyvec_macros" 5279 - version = "0.1.1" 5280 - source = "registry+https://github.com/rust-lang/crates.io-index" 5281 - checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 5282 - 5283 - [[package]] 5284 4771 name = "tokio" 5285 4772 version = "1.47.1" 5286 4773 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5311 4798 ] 5312 4799 5313 4800 [[package]] 5314 - name = "tokio-stream" 5315 - version = "0.1.17" 5316 - source = "registry+https://github.com/rust-lang/crates.io-index" 5317 - checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" 5318 - dependencies = [ 5319 - "futures-core", 5320 - "pin-project-lite", 5321 - "tokio", 5322 - ] 5323 - 5324 - [[package]] 5325 4801 name = "tokio-tungstenite" 5326 4802 version = "0.26.2" 5327 4803 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5587 5063 version = "2.8.1" 5588 5064 source = "registry+https://github.com/rust-lang/crates.io-index" 5589 5065 checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" 5590 - 5591 - [[package]] 5592 - name = "unicode-bom" 5593 - version = "2.0.3" 5594 - source = "registry+https://github.com/rust-lang/crates.io-index" 5595 - checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" 5596 5066 5597 5067 [[package]] 5598 5068 name = "unicode-id-start" ··· 5613 5083 checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" 5614 5084 5615 5085 [[package]] 5616 - name = "unicode-normalization" 5617 - version = "0.1.24" 5618 - source = "registry+https://github.com/rust-lang/crates.io-index" 5619 - checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" 5620 - dependencies = [ 5621 - "tinyvec", 5622 - ] 5623 - 5624 - [[package]] 5625 5086 name = "unicode-segmentation" 5626 5087 version = "1.12.0" 5627 5088 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5884 5345 ] 5885 5346 5886 5347 [[package]] 5887 - name = "watchexec" 5888 - version = "5.0.0" 5889 - source = "registry+https://github.com/rust-lang/crates.io-index" 5890 - checksum = "81e682bb1fe9526a6c78ffcfc6bb662ab36c213764fdd173babfbaf05cc56254" 5891 - dependencies = [ 5892 - "async-priority-channel", 5893 - "async-recursion", 5894 - "atomic-take", 5895 - "futures", 5896 - "ignore-files", 5897 - "miette", 5898 - "nix 0.29.0", 5899 - "normalize-path", 5900 - "notify 6.1.1", 5901 - "once_cell", 5902 - "process-wrap", 5903 - "project-origins", 5904 - "thiserror 1.0.69", 5905 - "tokio", 5906 - "tracing", 5907 - "watchexec-events", 5908 - "watchexec-signals", 5909 - "watchexec-supervisor", 5910 - ] 5911 - 5912 - [[package]] 5913 - name = "watchexec-events" 5914 - version = "4.0.0" 5915 - source = "registry+https://github.com/rust-lang/crates.io-index" 5916 - checksum = "2404ed3aa5e4a8f6139a2ee137926886c9144234c945102143ef9bf65309a751" 5917 - dependencies = [ 5918 - "nix 0.29.0", 5919 - "notify 6.1.1", 5920 - "watchexec-signals", 5921 - ] 5922 - 5923 - [[package]] 5924 - name = "watchexec-signals" 5925 - version = "4.0.1" 5926 - source = "registry+https://github.com/rust-lang/crates.io-index" 5927 - checksum = "8834ddd08f1ce18ea85e4ccbdafaea733851c7dc6afefd50037aea17845a861a" 5928 - dependencies = [ 5929 - "miette", 5930 - "nix 0.29.0", 5931 - "thiserror 2.0.16", 5932 - ] 5933 - 5934 - [[package]] 5935 - name = "watchexec-supervisor" 5936 - version = "3.0.0" 5937 - source = "registry+https://github.com/rust-lang/crates.io-index" 5938 - checksum = "6026815bdc9653d7820f6499b83ecadacd97a804dfabf2b2c55b061557f5f1f4" 5939 - dependencies = [ 5940 - "futures", 5941 - "nix 0.29.0", 5942 - "process-wrap", 5943 - "tokio", 5944 - "tracing", 5945 - "watchexec-events", 5946 - "watchexec-signals", 5947 - ] 5948 - 5949 - [[package]] 5950 5348 name = "web-sys" 5951 5349 version = "0.3.78" 5952 5350 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 6014 5412 6015 5413 [[package]] 6016 5414 name = "windows" 6017 - version = "0.61.3" 6018 - source = "registry+https://github.com/rust-lang/crates.io-index" 6019 - checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" 6020 - dependencies = [ 6021 - "windows-collections 0.2.0", 6022 - "windows-core 0.61.2", 6023 - "windows-future 0.2.1", 6024 - "windows-link 0.1.3", 6025 - "windows-numerics 0.2.0", 6026 - ] 6027 - 6028 - [[package]] 6029 - name = "windows" 6030 5415 version = "0.62.0" 6031 5416 source = "registry+https://github.com/rust-lang/crates.io-index" 6032 5417 checksum = "9579d0e6970fd5250aa29aba5994052385ff55cf7b28a059e484bb79ea842e42" 6033 5418 dependencies = [ 6034 - "windows-collections 0.3.0", 6035 - "windows-core 0.62.0", 6036 - "windows-future 0.3.0", 5419 + "windows-collections", 5420 + "windows-core", 5421 + "windows-future", 6037 5422 "windows-link 0.2.0", 6038 - "windows-numerics 0.3.0", 6039 - ] 6040 - 6041 - [[package]] 6042 - name = "windows-collections" 6043 - version = "0.2.0" 6044 - source = "registry+https://github.com/rust-lang/crates.io-index" 6045 - checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" 6046 - dependencies = [ 6047 - "windows-core 0.61.2", 5423 + "windows-numerics", 6048 5424 ] 6049 5425 6050 5426 [[package]] ··· 6053 5429 source = "registry+https://github.com/rust-lang/crates.io-index" 6054 5430 checksum = "a90dd7a7b86859ec4cdf864658b311545ef19dbcf17a672b52ab7cefe80c336f" 6055 5431 dependencies = [ 6056 - "windows-core 0.62.0", 6057 - ] 6058 - 6059 - [[package]] 6060 - name = "windows-core" 6061 - version = "0.61.2" 6062 - source = "registry+https://github.com/rust-lang/crates.io-index" 6063 - checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" 6064 - dependencies = [ 6065 - "windows-implement", 6066 - "windows-interface", 6067 - "windows-link 0.1.3", 6068 - "windows-result 0.3.4", 6069 - "windows-strings 0.4.2", 5432 + "windows-core", 6070 5433 ] 6071 5434 6072 5435 [[package]] ··· 6078 5441 "windows-implement", 6079 5442 "windows-interface", 6080 5443 "windows-link 0.2.0", 6081 - "windows-result 0.4.0", 6082 - "windows-strings 0.5.0", 6083 - ] 6084 - 6085 - [[package]] 6086 - name = "windows-future" 6087 - version = "0.2.1" 6088 - source = "registry+https://github.com/rust-lang/crates.io-index" 6089 - checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" 6090 - dependencies = [ 6091 - "windows-core 0.61.2", 6092 - "windows-link 0.1.3", 6093 - "windows-threading 0.1.0", 5444 + "windows-result", 5445 + "windows-strings", 6094 5446 ] 6095 5447 6096 5448 [[package]] ··· 6099 5451 source = "registry+https://github.com/rust-lang/crates.io-index" 6100 5452 checksum = "b2194dee901458cb79e1148a4e9aac2b164cc95fa431891e7b296ff0b2f1d8a6" 6101 5453 dependencies = [ 6102 - "windows-core 0.62.0", 5454 + "windows-core", 6103 5455 "windows-link 0.2.0", 6104 - "windows-threading 0.2.0", 5456 + "windows-threading", 6105 5457 ] 6106 5458 6107 5459 [[package]] ··· 6140 5492 6141 5493 [[package]] 6142 5494 name = "windows-numerics" 6143 - version = "0.2.0" 6144 - source = "registry+https://github.com/rust-lang/crates.io-index" 6145 - checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" 6146 - dependencies = [ 6147 - "windows-core 0.61.2", 6148 - "windows-link 0.1.3", 6149 - ] 6150 - 6151 - [[package]] 6152 - name = "windows-numerics" 6153 5495 version = "0.3.0" 6154 5496 source = "registry+https://github.com/rust-lang/crates.io-index" 6155 5497 checksum = "2ce3498fe0aba81e62e477408383196b4b0363db5e0c27646f932676283b43d8" 6156 5498 dependencies = [ 6157 - "windows-core 0.62.0", 5499 + "windows-core", 6158 5500 "windows-link 0.2.0", 6159 5501 ] 6160 5502 6161 5503 [[package]] 6162 5504 name = "windows-result" 6163 - version = "0.3.4" 6164 - source = "registry+https://github.com/rust-lang/crates.io-index" 6165 - checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" 6166 - dependencies = [ 6167 - "windows-link 0.1.3", 6168 - ] 6169 - 6170 - [[package]] 6171 - name = "windows-result" 6172 5505 version = "0.4.0" 6173 5506 source = "registry+https://github.com/rust-lang/crates.io-index" 6174 5507 checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" 6175 5508 dependencies = [ 6176 5509 "windows-link 0.2.0", 6177 - ] 6178 - 6179 - [[package]] 6180 - name = "windows-strings" 6181 - version = "0.4.2" 6182 - source = "registry+https://github.com/rust-lang/crates.io-index" 6183 - checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 6184 - dependencies = [ 6185 - "windows-link 0.1.3", 6186 5510 ] 6187 5511 6188 5512 [[package]] ··· 6285 5609 "windows_x86_64_gnu 0.53.0", 6286 5610 "windows_x86_64_gnullvm 0.53.0", 6287 5611 "windows_x86_64_msvc 0.53.0", 6288 - ] 6289 - 6290 - [[package]] 6291 - name = "windows-threading" 6292 - version = "0.1.0" 6293 - source = "registry+https://github.com/rust-lang/crates.io-index" 6294 - checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" 6295 - dependencies = [ 6296 - "windows-link 0.1.3", 6297 5612 ] 6298 5613 6299 5614 [[package]]
+6 -6
benchmarks/md-benchmark/src/page.rs
··· 1 - use maudit::{content::UntypedMarkdownContent, page::prelude::*}; 1 + use maudit::{content::UntypedMarkdownContent, route::prelude::*}; 2 2 3 3 #[route("/[file]")] 4 4 pub struct Article; ··· 8 8 file: String, 9 9 } 10 10 11 - impl Page<Params> for Article { 12 - fn routes(&self, context: &DynamicRouteContext) -> Routes<Params> { 11 + impl Route<Params> for Article { 12 + fn pages(&self, context: &mut DynamicRouteContext) -> Pages<Params> { 13 13 context 14 14 .content 15 15 .get_source::<UntypedMarkdownContent>("articles") 16 - .into_routes(|entry| { 17 - Route::from_params(Params { 16 + .into_pages(|entry| { 17 + Page::from_params(Params { 18 18 file: entry.id.clone(), 19 19 }) 20 20 }) 21 21 } 22 22 23 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 23 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 24 24 let params = ctx.params::<Params>(); 25 25 let entry = ctx 26 26 .content
+10 -4
crates/maudit-cli/Cargo.toml
··· 13 13 chrono = "0.4.39" 14 14 colored = "2.2.0" 15 15 clap = { version = "4.5.23", features = ["derive"] } 16 - tokio = { version = "1", features = ["macros", "rt-multi-thread"] } 16 + tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] } 17 17 axum = { version = "0.8.1", features = ["ws"] } 18 18 futures = "0.3" 19 19 tower-http = { version = "0.6.2", features = ["fs", "trace"] } ··· 22 22 "env-filter", 23 23 "chrono", 24 24 ] } 25 - watchexec = "5.0.0" 26 - watchexec-events = "4.0.0" 25 + notify = "6.1.1" 26 + notify-debouncer-full = "0.3.1" 27 27 inquire = "0.7.5" 28 28 rand = "0.9.0" 29 29 spinach = "3" ··· 32 32 toml_edit = "0.22.23" 33 33 local-ip-address = "0.6.3" 34 34 flate2 = "1.0.35" 35 - quanta = "0.12.6" 35 + quanta = "0.12.6" 36 + serde_json = "1.0" 37 + tokio-util = "0.7" 38 + 39 + [build-dependencies] 40 + rolldown = { package = "brk_rolldown", version = "0.1.4" } 41 + tokio = { version = "1", features = ["rt"] }
+43
crates/maudit-cli/build.rs
··· 1 + use rolldown::{Bundler, BundlerOptions, InputItem}; 2 + use std::path::PathBuf; 3 + 4 + fn main() { 5 + // Tell Cargo to rerun if any of our JS/TS files change 6 + println!("cargo:rerun-if-changed=src/dev/js"); 7 + println!("cargo:rerun-if-changed=tsconfig.json"); 8 + 9 + // Only bundle during regular builds, not during cargo check or similar 10 + if std::env::var("CARGO_CFG_TARGET_ARCH").is_err() { 11 + return; 12 + } 13 + 14 + let js_src_dir = PathBuf::from("src/dev/js"); 15 + let js_dist_dir = js_src_dir.join("dist"); 16 + 17 + // Ensure the dist directory exists 18 + std::fs::create_dir_all(&js_dist_dir).expect("Failed to create dist directory"); 19 + 20 + // Configure Rolldown bundler input 21 + let input_items = vec![InputItem { 22 + name: Some("client".to_string()), 23 + import: js_src_dir.join("client.ts").to_string_lossy().to_string(), 24 + }]; 25 + 26 + let bundler_options = BundlerOptions { 27 + input: Some(input_items), 28 + dir: Some(js_dist_dir.to_string_lossy().to_string()), 29 + format: Some(rolldown::OutputFormat::Esm), 30 + ..Default::default() 31 + }; 32 + 33 + // Create and run the bundler 34 + let runtime = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime"); 35 + 36 + runtime.block_on(async { 37 + let mut bundler = Bundler::new(bundler_options); 38 + if let Err(e) = bundler.write().await { 39 + panic!("Failed to bundle JavaScript: {:?}", e); 40 + } 41 + println!("Successfully bundled JavaScript files"); 42 + }); 43 + }
+230 -89
crates/maudit-cli/src/dev.rs
··· 1 - use std::io::{self}; 2 - 3 1 pub(crate) mod server; 4 2 5 3 mod filterer; 6 4 7 - use filterer::DevServerFilterer; 5 + use colored::Colorize; 6 + use filterer::should_watch_path; 7 + use notify::{event::ModifyKind, EventKind, RecursiveMode, Watcher}; 8 + use notify_debouncer_full::{new_debouncer, DebounceEventResult, DebouncedEvent}; 8 9 use quanta::Instant; 9 - use server::WebSocketMessage; 10 + use server::{update_status, WebSocketMessage}; 11 + use std::path::Path; 10 12 use std::sync::Arc; 11 13 use tokio::sync::broadcast; 14 + use tokio_util::sync::CancellationToken; 12 15 use tracing::{debug, error, info}; 13 - use watchexec::{ 14 - command::{Command, Program, Shell}, 15 - job::CommandState, 16 - Watchexec, 17 - }; 18 16 19 17 use crate::logging::{format_elapsed_time, FormatElapsedTimeOptions}; 20 18 21 - pub async fn start_dev_env(cwd: &str, host: bool) -> io::Result<()> { 19 + fn should_rebuild_for_event(event: &DebouncedEvent) -> bool { 20 + event.paths.iter().any(|path| { 21 + should_watch_path(path) 22 + && match event.kind { 23 + // Only rebuild on actual content modifications, not metadata changes 24 + EventKind::Modify(ModifyKind::Data(_)) => true, 25 + EventKind::Modify(ModifyKind::Name(_)) => true, 26 + EventKind::Modify(ModifyKind::Any) => true, 27 + EventKind::Modify(ModifyKind::Other) => true, 28 + // Skip metadata-only changes (permissions, timestamps, etc.) 29 + EventKind::Modify(ModifyKind::Metadata(_)) => false, 30 + // Include file creation and removal 31 + EventKind::Create(_) => true, 32 + EventKind::Remove(_) => true, 33 + // Skip other event types 34 + _ => false, 35 + } 36 + }) 37 + } 38 + 39 + pub async fn start_dev_env(cwd: &str, host: bool) -> Result<(), Box<dyn std::error::Error>> { 22 40 let start_time = Instant::now(); 23 41 info!(name: "dev", "Preparing dev environment…"); 24 42 25 43 // Do initial sync build 26 44 info!(name: "build", "Doing initial build…"); 27 - let command = std::process::Command::new("cargo") 45 + 46 + let child = std::process::Command::new("cargo") 28 47 .args(["run", "--quiet"]) 29 - .envs([("MAUDIT_DEV", "true"), ("MAUDIT_QUIET", "true")]) 30 - .output() 48 + .envs([ 49 + ("MAUDIT_DEV", "true"), 50 + ("MAUDIT_QUIET", "true"), 51 + ("CARGO_TERM_COLOR", "always"), 52 + ("RUSTFLAGS", "-Awarnings"), 53 + ]) 54 + .stderr(std::process::Stdio::piped()) 55 + .spawn() 31 56 .unwrap(); 57 + 58 + // Start a timer task to show warning after X seconds 59 + let warning_task = tokio::spawn(async { 60 + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; // Adjust timeout as needed 61 + info!(name: "build", "{}", "This can take some time on the first run, or if there are uncached dependencies or assets..".dimmed()); 62 + }); 63 + 64 + // Wait for the command to finish 65 + let output = child.wait_with_output().unwrap(); 66 + 67 + // Cancel the warning task since the command finished 68 + warning_task.abort(); 69 + 70 + let stderr = String::from_utf8_lossy(&output.stderr); 71 + 32 72 let duration = start_time.elapsed(); 33 73 let formatted_elasped_time = 34 74 format_elapsed_time(duration, &FormatElapsedTimeOptions::default_dev()); 35 75 36 - if command.status.success() { 76 + if output.status.success() { 37 77 info!(name: "build", "Initial build finished {}", formatted_elasped_time); 38 78 } else { 79 + error!(name: "build", "{}", stderr); 39 80 error!(name: "build", "Initial build failed with errors {}", formatted_elasped_time); 40 81 } 41 82 42 83 let (sender_websocket, _) = broadcast::channel::<WebSocketMessage>(100); 43 84 44 - let web_server_thread: tokio::task::JoinHandle<()> = tokio::spawn( 45 - server::start_dev_web_server(start_time, sender_websocket.clone(), host), 46 - ); 85 + // Create shared status state 86 + let current_status = Arc::new(tokio::sync::RwLock::new(if !output.status.success() { 87 + Some(stderr.to_string()) 88 + } else { 89 + None 90 + })); 47 91 48 - let wx = Watchexec::new_async(move |mut action| { 49 - Box::new({ 50 - let browser_websocket = sender_websocket.clone(); 92 + // Track the current build's cancellation token 93 + let current_build_cancel = 94 + Arc::new(tokio::sync::RwLock::new(Option::<CancellationToken>::None)); 51 95 52 - async move { 53 - if action.signals().next().is_some() { 54 - action.quit(); 55 - return action; 56 - } else { 57 - info!(name: "build", "Detected changes. Rebuilding…"); 96 + let web_server_thread: tokio::task::JoinHandle<()> = 97 + tokio::spawn(server::start_dev_web_server( 98 + start_time, 99 + sender_websocket.clone(), 100 + host, 101 + if !output.status.success() { 102 + Some(stderr.to_string()) 103 + } else { 104 + None 105 + }, 106 + current_status.clone(), 107 + )); 58 108 59 - // TODO: This kinda sucks but watchexec doesn't support setting env vars on commands 60 - // Maybe I need to use something else than watchexec 61 - let (shell, command) = if cfg!(windows) { 62 - ("cmd", "/C set MAUDIT_DEV=true && set MAUDIT_QUIET=true && cargo run --quiet") 63 - } else { 64 - ("sh", "MAUDIT_DEV=true MAUDIT_QUIET=true cargo run --quiet") 65 - }; 109 + // Set up file watching with debouncer 110 + let (tx, mut rx) = tokio::sync::mpsc::channel::<DebounceEventResult>(100); 66 111 67 - let (_, job) = action.create_job(Arc::new(Command { 68 - program: Program::Shell { 69 - shell: Shell::new(shell), 70 - command: command.into(), 71 - args: vec![], 72 - }, 73 - options: Default::default(), 74 - })); 75 - job.set_error_handler(|err| { 76 - eprintln!("Error: {:?}", err); 77 - }); 78 - job.start(); 79 - job.to_wait().await; 112 + let mut debouncer = new_debouncer( 113 + std::time::Duration::from_millis(100), 114 + None, 115 + move |result: DebounceEventResult| { 116 + tx.blocking_send(result).unwrap_or(()); 117 + }, 118 + )?; 80 119 81 - // TODO: Find a way to extract the stdout and stderr from the job and show it to the user other than 82 - // cargo logging 120 + debouncer 121 + .watcher() 122 + .watch(Path::new(cwd), RecursiveMode::Recursive)?; 83 123 84 - job.run(move |context| { 85 - let CommandState::Finished { 86 - status, 87 - started, 88 - finished, 89 - } = context.current 90 - else { 91 - return; 92 - }; 124 + // Handle file events 125 + tokio::spawn(async move { 126 + let browser_websocket = sender_websocket.clone(); 127 + let current_status = current_status.clone(); 128 + let build_cancel_ref = current_build_cancel.clone(); 129 + 130 + while let Some(result) = rx.recv().await { 131 + match result { 132 + Ok(events) => { 133 + // Filter events that should trigger a rebuild 134 + let triggering_events: Vec<_> = events 135 + .iter() 136 + .filter(|event| should_rebuild_for_event(event)) 137 + .collect(); 138 + 139 + if !triggering_events.is_empty() { 140 + debug!("File events: {} valid changes", triggering_events.len()); 141 + for event in &triggering_events { 142 + for path in &event.paths { 143 + debug!(" {:?}: {}", event.kind, path.display()); 144 + } 145 + } 93 146 147 + info!(name: "build", "Detected changes. Rebuilding…"); 94 148 95 - let duration = *finished - *started; 96 - let formatted_elasped_time = 97 - format_elapsed_time(duration, &FormatElapsedTimeOptions::default_dev()); 149 + // Cancel any ongoing build 150 + { 151 + let mut current_cancel = build_cancel_ref.write().await; 152 + if let Some(cancel_token) = current_cancel.take() { 153 + cancel_token.cancel(); 154 + debug!("Cancelled previous build"); 155 + } 156 + } 98 157 99 - match status { 100 - watchexec_events::ProcessEnd::ExitError(_) => { 101 - error!(name: "build", "Rebuild failed with errors {}", formatted_elasped_time); 102 - }, 103 - watchexec_events::ProcessEnd::Success => { 104 - info!(name: "build", "Rebuild finished {}", formatted_elasped_time); 105 - }, 106 - // TODO: Log the other statuses 107 - _ => {} 158 + // Create new cancellation token for this build 159 + let new_cancel_token = CancellationToken::new(); 160 + { 161 + let mut current_cancel = build_cancel_ref.write().await; 162 + *current_cancel = Some(new_cancel_token.clone()); 108 163 } 109 164 110 - match browser_websocket.send(WebSocketMessage { 111 - data: "done".into(), 112 - }) { 113 - Ok(_) => {} 165 + let start_time = Instant::now(); 166 + 167 + // Run the build command 168 + let child = std::process::Command::new("cargo") 169 + .args(["run", "--quiet"]) 170 + .envs([ 171 + ("MAUDIT_DEV", "true"), 172 + ("MAUDIT_QUIET", "true"), 173 + ("CARGO_TERM_COLOR", "always"), 174 + ("RUSTFLAGS", "-Awarnings"), 175 + ]) 176 + .stdout(std::process::Stdio::inherit()) 177 + .stderr(std::process::Stdio::piped()) 178 + .spawn(); 179 + 180 + match child { 181 + Ok(child_process) => { 182 + // Spawn the build in a separate task so we can cancel it 183 + let build_task = tokio::task::spawn_blocking(move || { 184 + child_process.wait_with_output() 185 + }); 186 + 187 + // Wait for either process completion or cancellation 188 + let output_result = tokio::select! { 189 + output = build_task => { 190 + match output { 191 + Ok(result) => Some(result), 192 + Err(e) => { 193 + error!(name: "build", "Failed to join build task: {}", e); 194 + None 195 + } 196 + } 197 + } 198 + _ = new_cancel_token.cancelled() => { 199 + debug!("Build was cancelled by new file changes"); 200 + None 201 + } 202 + }; 203 + 204 + // Clear the cancellation token since build is done/cancelled 205 + { 206 + let mut current_cancel = build_cancel_ref.write().await; 207 + *current_cancel = None; 208 + } 209 + 210 + if let Some(output) = output_result { 211 + match output { 212 + Ok(output) => { 213 + let duration = start_time.elapsed(); 214 + let formatted_elapsed_time = format_elapsed_time( 215 + duration, 216 + &FormatElapsedTimeOptions::default_dev(), 217 + ); 218 + 219 + if output.status.success() { 220 + info!(name: "build", "Rebuild finished {}", formatted_elapsed_time); 221 + 222 + // Update status and send success message to browser 223 + let websocket = browser_websocket.clone(); 224 + let status = current_status.clone(); 225 + tokio::spawn(async move { 226 + update_status( 227 + &websocket, status, "success", "", 228 + ) 229 + .await; 230 + }); 231 + } else { 232 + // TODO: It'd be great to somehow be able to get structured errors here (and in the initial build) 233 + // You can get some sort of structured errors from cargo with `--message-format=json`, but: 234 + // - You get an absurd amount of output, including non-error messages, at least when running `cargo run` 235 + // - You don't get the normal human-friendly output anymore, which would be great to have still 236 + // - You can print the rendered output to the console from the JSON, but then you don't have colors 237 + // - It'd only work for rustc errors, not sure how we'd make it work with runtime errors. 238 + // ... So until then, we just send the raw stderr output and hopefully the user can make sense of it. 239 + let stderr = 240 + String::from_utf8_lossy(&output.stderr) 241 + .to_string(); 242 + error!(name: "build", "{}", stderr); 243 + error!(name: "build", "Rebuild failed with errors {}", formatted_elapsed_time); 244 + 245 + // Update status and send error message to browser 246 + let websocket = browser_websocket.clone(); 247 + let status = current_status.clone(); 248 + tokio::spawn(async move { 249 + update_status( 250 + &websocket, status, "error", &stderr, 251 + ) 252 + .await; 253 + }); 254 + } 255 + } 256 + Err(e) => { 257 + error!(name: "build", "Failed to wait for build process: {}", e); 258 + } 259 + } 260 + } 261 + } 114 262 Err(e) => { 115 - debug!("Error sending message to browser: {:?}", e); 263 + error!(name: "build", "Failed to spawn build process: {}", e); 116 264 } 117 265 } 118 - }); 266 + } 119 267 } 120 - 121 - for event in action.events.iter() { 122 - debug!("EVENT: {event:?}"); 268 + Err(errors) => { 269 + for error in errors { 270 + error!("File watch error: {:?}", error); 271 + } 123 272 } 124 - 125 - action 126 273 } 127 - }) 128 - }) 129 - .unwrap(); 274 + } 275 + }); 130 276 131 - wx.config.pathset([cwd]); 132 - wx.config.filterer(DevServerFilterer); 133 - 134 - let _ = wx.main().await; 135 - 136 - // Wait for the build process to finish 277 + // Wait for the web server to finish (this will run indefinitely) 137 278 web_server_thread.await.unwrap(); 138 279 139 280 Ok(())
-30
crates/maudit-cli/src/dev/client.js
··· 1 - /** 2 - * TODO: This is a quite naive implementation, without necessarily thinking about complex HMR and stuff 3 - * It might be better to use a more sophisticated approach, using some sort of diffing, handling reconnecting, etc. 4 - */ 5 - const debounceReload = (time) => { 6 - let timer; 7 - return () => { 8 - if (timer) { 9 - clearTimeout(timer); 10 - timer = null; 11 - } 12 - timer = setTimeout(() => { 13 - location.reload(); 14 - }, time); 15 - }; 16 - }; 17 - const pageReload = debounceReload(50); 18 - 19 - const socket = new WebSocket("ws://{SERVER_ADDRESS}/ws"); 20 - 21 - socket.addEventListener("open", (event) => { 22 - console.log("Connected to server"); 23 - socket.send("Hello Server!"); 24 - }); 25 - 26 - socket.addEventListener("message", (event) => { 27 - if (event.data === "done") { 28 - pageReload(); 29 - } 30 - });
+16 -49
crates/maudit-cli/src/dev/filterer.rs
··· 1 - use watchexec::filter::Filterer; 2 - use watchexec_events::{ 3 - filekind::{FileEventKind, ModifyKind}, 4 - Tag, 5 - }; 1 + use std::path::Path; 6 2 7 - #[derive(Debug)] 8 - pub struct DevServerFilterer; 9 - 10 - impl Filterer for DevServerFilterer { 11 - fn check_event( 12 - &self, 13 - event: &watchexec_events::Event, 14 - _: watchexec_events::Priority, 15 - ) -> Result<bool, watchexec::error::RuntimeError> { 16 - let mut result = true; 17 - 18 - for tag in &event.tags { 19 - // NOTE: This happens whenever the watch gets dropped and re-added, you get something like `rescan: user dropped` 20 - // It's probable that this needs to be used to do some sort of action on the watch, not sure what yet 21 - if let Tag::FileEventKind(FileEventKind::Other) = tag { 22 - result = false; 23 - break; 24 - } 25 - 26 - if let Tag::Path { path, file_type: _ } = tag { 27 - if let Some(file_name) = path.file_name() { 28 - if file_name == ".DS_Store" { 29 - result = false; 30 - break; 31 - } 32 - } 33 - 34 - // TODO: Customizable dist path 35 - if path.ancestors().any(|p| p.ends_with("dist")) { 36 - result = false; 37 - break; 38 - } 39 - 40 - if path.ancestors().any(|p| p.ends_with("target")) { 41 - result = false; 42 - break; 43 - } 44 - } 45 - 46 - if let Tag::FileEventKind(FileEventKind::Modify(ModifyKind::Metadata(_))) = tag { 47 - result = false; 48 - break; 49 - } 3 + /// Simple file path filter for the dev server 4 + pub fn should_watch_path(path: &Path) -> bool { 5 + // Skip .DS_Store files 6 + if let Some(file_name) = path.file_name() { 7 + if file_name == ".DS_Store" { 8 + return false; 50 9 } 10 + } 51 11 52 - Ok(result) 12 + // Skip dist and target directories 13 + if path 14 + .ancestors() 15 + .any(|p| p.ends_with("dist") || p.ends_with("target")) 16 + { 17 + return false; 53 18 } 19 + 20 + true 54 21 }
+55
crates/maudit-cli/src/dev/js/client.ts
··· 1 + /** 2 + * TODO: This is a quite naive implementation, without necessarily thinking about complex HMR and stuff 3 + * It might be better to use a more sophisticated approach, using some sort of diffing, handling reconnecting, etc. 4 + */ 5 + 6 + import { AnsiUp } from "ansi_up"; 7 + import { createErrorOverlay } from "./overlay"; 8 + import { error, log } from "./utils"; 9 + 10 + const WS_SERVER_ADDRESS = "{SERVER_ADDRESS}"; 11 + 12 + const ansiUp = new AnsiUp(); 13 + 14 + export interface Message { 15 + type: "success" | "error"; 16 + message: string; 17 + } 18 + 19 + const debounceReload = (time: number | undefined) => { 20 + let timer: number | null | undefined; 21 + return () => { 22 + if (timer) { 23 + clearTimeout(timer); 24 + timer = null; 25 + } 26 + timer = setTimeout(() => { 27 + location.reload(); 28 + }, time); 29 + }; 30 + }; 31 + const pageReload = debounceReload(50); 32 + 33 + const socket = new WebSocket(`ws://${WS_SERVER_ADDRESS}/ws`); 34 + 35 + socket.addEventListener("open", (event) => { 36 + console.log("Connected to server"); 37 + socket.send("Hello Server!"); 38 + }); 39 + 40 + socket.addEventListener("message", (event) => { 41 + try { 42 + const message = JSON.parse(event.data) as Message; 43 + 44 + if (message.type === "success") { 45 + log("Build successful:", message.message); 46 + pageReload(); 47 + } else if (message.type === "error") { 48 + error("Build error:", message.message); 49 + 50 + createErrorOverlay(ansiUp.ansi_to_html(message.message)); 51 + } 52 + } catch (e) { 53 + error("Failed to parse WebSocket message", event.data, e); 54 + } 55 + });
+76
crates/maudit-cli/src/dev/js/overlay.ts
··· 1 + import type { Message } from "./client"; 2 + 3 + export const overlayTagName = "maudit-error-overlay"; 4 + 5 + const template = /*html*/ ` 6 + <style> 7 + #maudit-error-overlay { 8 + position: fixed; 9 + z-index: 99999; 10 + top: 0; 11 + left: 0; 12 + width: 100%; 13 + height: 100%; 14 + background: rgba(0, 0, 0, 0.90); 15 + margin: 0; 16 + color: white; 17 + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 18 + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", 19 + "Helvetica Neue", sans-serif; 20 + overflow-y: scroll; 21 + padding: 20px; 22 + box-sizing: border-box; 23 + direction: ltr; 24 + } 25 + 26 + #maudit-error-overlay pre { 27 + background-color: #070707; 28 + padding: 1rem; 29 + border-radius: 4px; 30 + overflow-x: scroll; 31 + word-break: break-word; 32 + font-size: 16px; 33 + line-height: 1.4; 34 + } 35 + </style> 36 + <div id="maudit-error-overlay"> 37 + <h1>Build Error</h1> 38 + <pre id="maudit-error-message"></pre> 39 + </div> 40 + `; 41 + 42 + class MauditErrorOverlay extends HTMLElement { 43 + root: ShadowRoot; 44 + 45 + constructor(err: Message["message"]) { 46 + super(); 47 + this.root = this.attachShadow({ mode: "open" }); 48 + this.root.innerHTML = template; 49 + 50 + // Set the error message 51 + const messageElement = this.root.querySelector("#maudit-error-message"); 52 + if (messageElement) { 53 + messageElement.innerHTML = err.trim(); 54 + } 55 + } 56 + 57 + close(): void { 58 + this.parentNode?.removeChild(this); 59 + } 60 + } 61 + 62 + const { customElements } = globalThis; 63 + if (customElements && !customElements.get(overlayTagName)) { 64 + customElements.define(overlayTagName, MauditErrorOverlay); 65 + } 66 + 67 + export function createErrorOverlay(err: Message["message"]) { 68 + clearErrorOverlay(); 69 + document.body.appendChild(new MauditErrorOverlay(err)); 70 + } 71 + 72 + export function clearErrorOverlay() { 73 + document 74 + .querySelectorAll<MauditErrorOverlay>(overlayTagName) 75 + .forEach((n) => n.close()); 76 + }
+30
crates/maudit-cli/src/dev/js/utils.ts
··· 1 + const ansiPattern = new RegExp( 2 + "(?:\\u001B\\][\\s\\S]*?(?:\\u0007|\\u001B\\u005C|\\u009C))|[\\u001B\\u009B][[\\]()#;?]*(?:\\d{1,4}(?:[;:]\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]", 3 + "g" 4 + ); 5 + 6 + export function stripAnsi(str: string): string { 7 + return str.replace(ansiPattern, ""); 8 + } 9 + 10 + export function log(...args: unknown[]) { 11 + mauditMessage("log", args); 12 + } 13 + 14 + export function warn(...args: unknown[]) { 15 + mauditMessage("warn", args); 16 + } 17 + 18 + export function error(...args: unknown[]) { 19 + mauditMessage("error", args); 20 + } 21 + 22 + function mauditMessage(level: "log" | "warn" | "error", message: unknown[]) { 23 + console[level]( 24 + "%cMaudit", 25 + "background: #ba1f33; color: white; padding-inline: 4px; border-radius: 2px; font-family: serif;", 26 + ...message.map((m) => 27 + typeof m === "string" ? stripAnsi(m) : JSON.stringify(m, null, 2) 28 + ) 29 + ); 30 + }
+68 -3
crates/maudit-cli/src/dev/server.rs
··· 12 12 Router, 13 13 }; 14 14 use quanta::Instant; 15 + use serde_json::json; 15 16 use tokio::{net::TcpSocket, signal, sync::broadcast}; 16 17 use tracing::{debug, Level}; 17 18 18 19 use std::net::{IpAddr, SocketAddr}; 20 + use std::sync::Arc; 19 21 use tower_http::{ 20 22 services::ServeDir, 21 23 trace::{DefaultMakeSpan, TraceLayer}, ··· 37 39 #[derive(Clone)] 38 40 struct AppState { 39 41 tx: broadcast::Sender<WebSocketMessage>, 42 + current_status: Arc<tokio::sync::RwLock<Option<String>>>, 40 43 } 41 44 42 45 fn inject_live_reload_script(html_content: &str, socket_addr: SocketAddr, host: bool) -> String { 43 46 let mut content = html_content.to_string(); 44 47 45 - let script_content = include_str!("./client.js").replace( 48 + let script_content = include_str!("./js/dist/client.js").replace( 46 49 "{SERVER_ADDRESS}", 47 50 &format!( 48 51 "{}:{}", ··· 63 66 start_time: Instant, 64 67 tx: broadcast::Sender<WebSocketMessage>, 65 68 host: bool, 69 + initial_error: Option<String>, 70 + current_status: Arc<tokio::sync::RwLock<Option<String>>>, 66 71 ) { 67 72 // TODO: The dist dir should be configurable 68 73 let dist_dir = "dist"; 74 + 75 + // Send initial error if present 76 + if let Some(error) = initial_error { 77 + let _ = tx.send(WebSocketMessage { 78 + data: json!({ 79 + "type": "error", 80 + "message": error 81 + }) 82 + .to_string(), 83 + }); 84 + } 69 85 70 86 async fn handle_404(socket_addr: SocketAddr, host: bool, dist_dir: &str) -> impl IntoResponse { 71 87 let content = match fs::read_to_string(format!("{}/404.html", dist_dir)).await { ··· 117 133 .make_span_with(DefaultMakeSpan::new().level(Level::INFO)) 118 134 .on_response(CustomOnResponse), 119 135 ) 120 - .with_state(AppState { tx }); 136 + .with_state(AppState { 137 + tx: tx.clone(), 138 + current_status: current_status.clone(), 139 + }); 121 140 122 141 log_server_start( 123 142 start_time, ··· 135 154 .unwrap(); 136 155 } 137 156 157 + pub async fn update_status( 158 + tx: &broadcast::Sender<WebSocketMessage>, 159 + current_status: Arc<tokio::sync::RwLock<Option<String>>>, 160 + status_type: &str, 161 + message: &str, 162 + ) { 163 + let status_message = if status_type == "success" { 164 + None // Clear the status on success 165 + } else { 166 + Some(message.to_string()) 167 + }; 168 + 169 + // Update the stored status 170 + { 171 + let mut status = current_status.write().await; 172 + *status = status_message; 173 + } 174 + 175 + // Send the message 176 + let _ = tx.send(WebSocketMessage { 177 + data: json!({ 178 + "type": status_type, 179 + "message": message 180 + }) 181 + .to_string(), 182 + }); 183 + } 184 + 138 185 async fn add_dev_client_script( 139 186 req: Request, 140 187 next: Next, ··· 177 224 debug!("`{addr} connected."); 178 225 // finalize the upgrade process by returning upgrade callback. 179 226 // we can customize the callback by sending additional info such as address. 180 - ws.on_upgrade(move |socket| handle_socket(socket, addr, state.tx)) 227 + ws.on_upgrade(move |socket| handle_socket(socket, addr, state.tx, state.current_status)) 181 228 } 182 229 183 230 async fn handle_socket( 184 231 socket: WebSocket, 185 232 who: SocketAddr, 186 233 tx: broadcast::Sender<WebSocketMessage>, 234 + current_status: Arc<tokio::sync::RwLock<Option<String>>>, 187 235 ) { 188 236 let (mut sender, mut receiver) = socket.split(); 237 + 238 + // Send current status to new connection if there is one 239 + { 240 + let status = current_status.read().await; 241 + if let Some(error_message) = status.as_ref() { 242 + let _ = sender 243 + .send(Message::Text( 244 + json!({ 245 + "type": "error", 246 + "message": error_message 247 + }) 248 + .to_string() 249 + .into(), 250 + )) 251 + .await; 252 + } 253 + } 189 254 190 255 let mut rx = tx.subscribe(); 191 256
+26
crates/maudit-cli/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "rootDir": "./src/dev/js", 4 + "noEmit": true, 5 + 6 + "module": "esnext", 7 + "target": "es2020", 8 + "lib": ["dom", "dom.iterable", "es2020"], 9 + "moduleResolution": "bundler", 10 + 11 + // Other Outputs 12 + "sourceMap": true, 13 + 14 + // Stricter Typechecking Options 15 + "noUncheckedIndexedAccess": true, 16 + "exactOptionalPropertyTypes": true, 17 + 18 + // Recommended Options 19 + "strict": true, 20 + "verbatimModuleSyntax": true, 21 + "isolatedModules": true, 22 + "noUncheckedSideEffectImports": true, 23 + "moduleDetection": "force", 24 + "skipLibCheck": true, 25 + } 26 + }
+41 -23
crates/maudit-macros/src/lib.rs
··· 25 25 let path = &attrs.path; 26 26 27 27 let expanded = quote! { 28 - impl maudit::page::InternalPage for #struct_name { 28 + impl maudit::route::InternalRoute for #struct_name { 29 29 fn route_raw(&self) -> String { 30 30 #path.to_string() 31 31 } 32 32 } 33 33 34 - impl maudit::page::FullPage for #struct_name { 35 - fn render_internal(&self, ctx: &mut maudit::page::RouteContext) -> maudit::page::RenderResult { 34 + impl maudit::route::FullRoute for #struct_name { 35 + fn render_internal(&self, ctx: &mut maudit::route::PageContext) -> maudit::route::RenderResult { 36 36 self.render(ctx).into() 37 37 } 38 38 39 - fn routes_internal(&self, ctx: &maudit::page::DynamicRouteContext) -> Vec<(maudit::page::RouteParams, Box<dyn std::any::Any + Send + Sync>, Box<dyn std::any::Any + Send + Sync>)> { 40 - self.routes(ctx) 39 + fn pages_internal(&self, ctx: &mut maudit::route::DynamicRouteContext) -> Vec<(maudit::route::PageParams, Box<dyn std::any::Any + Send + Sync>, Box<dyn std::any::Any + Send + Sync>)> { 40 + self.pages(ctx) 41 41 .into_iter() 42 42 .map(|route| { 43 - let raw_params: maudit::page::RouteParams = (&route.params).into(); 43 + let raw_params: maudit::route::PageParams = (&route.params).into(); 44 44 let typed_params: Box<dyn std::any::Any + Send + Sync> = Box::new(route.params); 45 45 let props: Box<dyn std::any::Any + Send + Sync> = Box::new(route.props); 46 46 (raw_params, typed_params, props) ··· 60 60 let item_struct = syn::parse_macro_input!(item as ItemStruct); 61 61 let struct_name = &item_struct.ident; 62 62 63 - let fields = match &item_struct.fields { 63 + let field_conversions = match &item_struct.fields { 64 64 syn::Fields::Named(fields) => fields 65 65 .named 66 66 .iter() 67 - .map(|f| f.ident.as_ref().unwrap()) 67 + .map(|field| { 68 + let field_name = field.ident.as_ref().unwrap(); 69 + let field_name_str = field_name.to_string(); 70 + 71 + // Check if the field type is Option<T> 72 + if is_option_type(&field.ty) { 73 + quote! { 74 + map.insert( 75 + #field_name_str.to_string(), 76 + self.#field_name.as_ref().map_or("__MAUDIT_NONE__".to_string(), |v| v.to_string()) 77 + ); 78 + } 79 + } else { 80 + quote! { 81 + map.insert(#field_name_str.to_string(), self.#field_name.to_string()); 82 + } 83 + } 84 + }) 68 85 .collect::<Vec<_>>(), 69 86 _ => panic!("Only named fields are supported"), 70 87 }; 71 88 72 - // Add a from Hashmap conversion 73 89 let expanded = quote! { 74 - impl Into<RouteParams> for #struct_name { 75 - fn into(self) -> RouteParams { 76 - let mut map = maudit::FxHashMap::default(); 77 - #( 78 - map.insert(stringify!(#fields).to_string(), self.#fields.to_string()); 79 - )* 80 - RouteParams(map) 90 + impl Into<PageParams> for #struct_name { 91 + fn into(self) -> PageParams { 92 + (&self).into() 81 93 } 82 94 } 83 95 84 - impl Into<RouteParams> for &#struct_name { 85 - fn into(self) -> RouteParams { 96 + impl Into<PageParams> for &#struct_name { 97 + fn into(self) -> PageParams { 86 98 let mut map = maudit::FxHashMap::default(); 87 - #( 88 - map.insert(stringify!(#fields).to_string(), self.#fields.to_string()); 89 - )* 90 - RouteParams(map) 99 + #(#field_conversions)* 100 + PageParams(map) 91 101 } 92 102 } 93 - 94 103 }; 95 104 96 105 TokenStream::from(expanded) 106 + } 107 + 108 + fn is_option_type(ty: &syn::Type) -> bool { 109 + if let syn::Type::Path(type_path) = ty { 110 + if let Some(segment) = type_path.path.segments.last() { 111 + return segment.ident == "Option"; 112 + } 113 + } 114 + false 97 115 } 98 116 99 117 #[proc_macro_attribute]
+8 -8
crates/maudit/CHANGELOG.md
··· 71 71 To process an image, add it using `ctx.assets.add_image_with_options` in your page's `render` method, specifying the desired transformations. 72 72 73 73 ```rs 74 - use maudit::page::prelude::*; 74 + use maudit::route::prelude::*; 75 75 76 76 #[route("/image")] 77 77 pub struct ImagePage; 78 78 79 - impl Page for ImagePage { 80 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 79 + impl Route for ImagePage { 80 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 81 81 let image = ctx.assets.add_image_with_options( 82 82 "path/to/image.jpg", 83 83 ImageOptions { ··· 98 98 - [52eda9e](https://github.com/bruits/maudit/commit/52eda9ea4eac8efd3efd945d00f39a1b99f284ab) Adds support for dynamic routes with properties. In addition to its parameters, a dynamic route can now provide additional properties that can be used during rendering. 99 99 100 100 ```rs 101 - use maudit::page::prelude::*; 101 + use maudit::route::prelude::*; 102 102 103 103 #[route("/posts/[slug]")] 104 104 pub struct Post; ··· 114 114 pub content: String, 115 115 } 116 116 117 - impl Page<Params, Props> for Post { 118 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 117 + impl Route<Params, Props> for Post { 118 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 119 119 let params = ctx.params::<Params>(); 120 120 let props = ctx.props::<Props>(); 121 121 ··· 125 125 ).into() 126 126 } 127 127 128 - fn routes(&self, ctx: &DynamicRouteContext) -> Routes<Params, Props> { 129 - vec![Route::from_params_and_props( 128 + fn pages(&self, ctx: &mut DynamicRouteContext) -> Routes<Params, Props> { 129 + vec![Page::from_params_and_props( 130 130 Params { 131 131 slug: "hello-world".to_string(), 132 132 },
+22 -22
crates/maudit/src/assets.rs
··· 18 18 use crate::{AssetHashingStrategy, BuildOptions}; 19 19 20 20 #[derive(Default)] 21 - pub struct PageAssets { 21 + pub struct RouteAssets { 22 22 pub images: FxHashSet<Image>, 23 23 pub scripts: FxHashSet<Script>, 24 24 pub styles: FxHashSet<Style>, 25 25 26 - pub(crate) options: PageAssetsOptions, 26 + pub(crate) options: RouteAssetsOptions, 27 27 } 28 28 29 29 #[derive(Clone)] 30 - pub struct PageAssetsOptions { 30 + pub struct RouteAssetsOptions { 31 31 pub assets_dir: PathBuf, 32 32 pub output_assets_dir: PathBuf, 33 33 pub hashing_strategy: AssetHashingStrategy, 34 34 } 35 35 36 - impl Default for PageAssetsOptions { 36 + impl Default for RouteAssetsOptions { 37 37 fn default() -> Self { 38 38 let default_build_options = BuildOptions::default(); 39 39 let page_assets_optiosn = default_build_options.page_assets_options(); ··· 46 46 } 47 47 } 48 48 49 - impl PageAssets { 50 - pub fn new(assets_options: &PageAssetsOptions) -> Self { 49 + impl RouteAssets { 50 + pub fn new(assets_options: &RouteAssetsOptions) -> Self { 51 51 Self { 52 52 options: assets_options.clone(), 53 53 ..Default::default() ··· 118 118 /// Add a script to the page assets, causing the file to be created in the output directory. The script is resolved relative to the current working directory. 119 119 /// 120 120 /// The script will not automatically be included in the page, but can be included through the `.url()` method on the returned `Script` object. 121 - /// Alternatively, a script can be included automatically using the [PageAssets::include_script] method instead. 121 + /// Alternatively, a script can be included automatically using the [RouteAssets::include_script] method instead. 122 122 /// 123 123 /// Subsequent calls to this function using the same path will return the same script, as such, the value returned by this function can be cloned and used multiple times without issue. 124 124 pub fn add_script<P>(&mut self, script_path: P) -> Script ··· 149 149 /// Add a style to the page assets, causing the file to be created in the output directory. The style is resolved relative to the current working directory. 150 150 /// 151 151 /// The style will not automatically be included in the page, but can be included through the `.url()` method on the returned `Style` object. 152 - /// Alternatively, a style can be included automatically using the [PageAssets::include_style] method instead. 152 + /// Alternatively, a style can be included automatically using the [RouteAssets::include_style] method instead. 153 153 /// 154 154 /// Subsequent calls to this method using the same path will return the same style, as such, the value returned by this method can be cloned and used multiple times without issue. this method is equivalent to calling `add_style_with_options` with the default `StyleOptions` and is purely provided for convenience. 155 155 pub fn add_style<P>(&mut self, style_path: P) -> Style ··· 357 357 #[test] 358 358 fn test_add_style() { 359 359 let temp_dir = setup_temp_dir(); 360 - let mut page_assets = PageAssets::default(); 360 + let mut page_assets = RouteAssets::default(); 361 361 page_assets.add_style(temp_dir.join("style.css")); 362 362 363 363 assert!(page_assets.styles.len() == 1); ··· 366 366 #[test] 367 367 fn test_include_style() { 368 368 let temp_dir = setup_temp_dir(); 369 - let mut page_assets = PageAssets::default(); 369 + let mut page_assets = RouteAssets::default(); 370 370 371 371 page_assets.include_style(temp_dir.join("style.css")); 372 372 ··· 377 377 #[test] 378 378 fn test_add_script() { 379 379 let temp_dir = setup_temp_dir(); 380 - let mut page_assets = PageAssets::default(); 380 + let mut page_assets = RouteAssets::default(); 381 381 382 382 page_assets.add_script(temp_dir.join("script.js")); 383 383 assert!(page_assets.scripts.len() == 1); ··· 386 386 #[test] 387 387 fn test_include_script() { 388 388 let temp_dir = setup_temp_dir(); 389 - let mut page_assets = PageAssets::default(); 389 + let mut page_assets = RouteAssets::default(); 390 390 391 391 page_assets.include_script(temp_dir.join("script.js")); 392 392 ··· 397 397 #[test] 398 398 fn test_add_image() { 399 399 let temp_dir = setup_temp_dir(); 400 - let mut page_assets = PageAssets::default(); 400 + let mut page_assets = RouteAssets::default(); 401 401 402 402 page_assets.add_image(temp_dir.join("image.png")); 403 403 assert!(page_assets.images.len() == 1); ··· 406 406 #[test] 407 407 fn test_asset_has_leading_slash() { 408 408 let temp_dir = setup_temp_dir(); 409 - let mut page_assets = PageAssets::default(); 409 + let mut page_assets = RouteAssets::default(); 410 410 411 411 let image = page_assets.add_image(temp_dir.join("image.png")); 412 412 assert_eq!(image.url().unwrap().chars().next(), Some('/')); ··· 421 421 #[test] 422 422 fn test_asset_url_include_hash() { 423 423 let temp_dir = setup_temp_dir(); 424 - let mut page_assets = PageAssets::default(); 424 + let mut page_assets = RouteAssets::default(); 425 425 426 426 let image = page_assets.add_image(temp_dir.join("image.png")); 427 427 assert!(image.url().unwrap().contains(&image.hash)); ··· 436 436 #[test] 437 437 fn test_asset_path_include_hash() { 438 438 let temp_dir = setup_temp_dir(); 439 - let mut page_assets = PageAssets::default(); 439 + let mut page_assets = RouteAssets::default(); 440 440 441 441 let image = page_assets.add_image(temp_dir.join("image.png")); 442 442 assert!(image.build_path().to_string_lossy().contains(&image.hash)); ··· 463 463 ]; 464 464 std::fs::write(&image_path, png_data).unwrap(); 465 465 466 - let mut page_assets = PageAssets::default(); 466 + let mut page_assets = RouteAssets::default(); 467 467 468 468 // Test that different options produce different hashes 469 469 let image_default = page_assets.add_image(&image_path); ··· 526 526 ]; 527 527 std::fs::write(&image_path, png_data).unwrap(); 528 528 529 - let mut page_assets = PageAssets::default(); 529 + let mut page_assets = RouteAssets::default(); 530 530 531 531 // Same options should produce same hash 532 532 let image1 = page_assets.add_image_with_options( ··· 558 558 let temp_dir = setup_temp_dir(); 559 559 let style_path = temp_dir.join("style.css"); 560 560 561 - let mut page_assets = PageAssets::new(&PageAssetsOptions::default()); 561 + let mut page_assets = RouteAssets::new(&RouteAssetsOptions::default()); 562 562 563 563 // Test that different tailwind options produce different hashes 564 564 let style_default = page_assets.add_style(&style_path); ··· 583 583 std::fs::write(&style1_path, content).unwrap(); 584 584 std::fs::write(&style2_path, content).unwrap(); 585 585 586 - let mut page_assets = PageAssets::new(&PageAssetsOptions::default()); 586 + let mut page_assets = RouteAssets::new(&RouteAssetsOptions::default()); 587 587 588 588 let style1 = page_assets.add_style(&style1_path); 589 589 let style2 = page_assets.add_style(&style2_path); ··· 599 599 let temp_dir = setup_temp_dir(); 600 600 let style_path = temp_dir.join("dynamic_style.css"); 601 601 602 - let assets_options = PageAssetsOptions::default(); 603 - let mut page_assets = PageAssets::new(&assets_options); 602 + let assets_options = RouteAssetsOptions::default(); 603 + let mut page_assets = RouteAssets::new(&assets_options); 604 604 605 605 // Write first content and get hash 606 606 std::fs::write(&style_path, "body { background: red; }").unwrap();
+2 -2
crates/maudit/src/assets/image.rs
··· 8 8 9 9 use super::image_cache::ImageCache; 10 10 use crate::assets::{ 11 - HashAssetType, HashConfig, PageAssetsOptions, calculate_hash, make_filename, make_final_path, 11 + HashAssetType, HashConfig, RouteAssetsOptions, calculate_hash, make_filename, make_final_path, 12 12 make_final_url, 13 13 }; 14 14 use crate::is_dev; ··· 78 78 pub fn new( 79 79 path: PathBuf, 80 80 image_options: Option<ImageOptions>, 81 - page_assets_options: &PageAssetsOptions, 81 + page_assets_options: &RouteAssetsOptions, 82 82 ) -> Self { 83 83 let hash = calculate_hash( 84 84 &path,
+2 -2
crates/maudit/src/assets/script.rs
··· 1 1 use std::path::PathBuf; 2 2 3 3 use crate::assets::{ 4 - HashAssetType, HashConfig, PageAssetsOptions, calculate_hash, make_filename, make_final_path, 4 + HashAssetType, HashConfig, RouteAssetsOptions, calculate_hash, make_filename, make_final_path, 5 5 make_final_url, 6 6 }; 7 7 ··· 18 18 } 19 19 20 20 impl Script { 21 - pub fn new(path: PathBuf, included: bool, page_assets_options: &PageAssetsOptions) -> Self { 21 + pub fn new(path: PathBuf, included: bool, page_assets_options: &RouteAssetsOptions) -> Self { 22 22 let hash = calculate_hash( 23 23 &path, 24 24 Some(&HashConfig {
+2 -2
crates/maudit/src/assets/style.rs
··· 1 1 use std::path::PathBuf; 2 2 3 3 use crate::assets::{ 4 - HashAssetType, HashConfig, PageAssetsOptions, calculate_hash, make_filename, make_final_path, 4 + HashAssetType, HashConfig, RouteAssetsOptions, calculate_hash, make_filename, make_final_path, 5 5 make_final_url, 6 6 }; 7 7 ··· 28 28 path: PathBuf, 29 29 included: bool, 30 30 style_options: &StyleOptions, 31 - page_assets_options: &PageAssetsOptions, 31 + page_assets_options: &RouteAssetsOptions, 32 32 ) -> Self { 33 33 let hash = calculate_hash( 34 34 &path,
+29 -28
crates/maudit/src/build.rs
··· 12 12 use crate::{ 13 13 BuildOptions, BuildOutput, 14 14 assets::{ 15 - self, PageAssets, 15 + self, RouteAssets, 16 16 image_cache::{IMAGE_CACHE_DIR, ImageCache}, 17 17 }, 18 18 build::images::process_image, 19 - content::{ContentSources, PageContent}, 19 + content::{ContentSources, RouteContent}, 20 20 errors::BuildError, 21 21 is_dev, 22 22 logging::print_title, 23 - page::{DynamicRouteContext, FullPage, RenderResult, RouteContext, RouteParams, RouteType}, 23 + route::{DynamicRouteContext, FullRoute, PageContext, PageParams, RenderResult, RouteType}, 24 24 }; 25 25 use colored::{ColoredString, Colorize}; 26 - use log::{debug, info, trace}; 26 + use log::{debug, info, trace, warn}; 27 27 use oxc_sourcemap::SourceMap; 28 28 use rolldown::{ 29 29 Bundler, BundlerOptions, InputItem, ModuleType, ··· 137 137 } 138 138 139 139 pub fn execute_build( 140 - routes: &[&dyn FullPage], 140 + routes: &[&dyn FullRoute], 141 141 content_sources: &mut ContentSources, 142 142 options: &BuildOptions, 143 143 async_runtime: &tokio::runtime::Runtime, ··· 146 146 } 147 147 148 148 pub async fn build( 149 - routes: &[&dyn FullPage], 149 + routes: &[&dyn FullRoute], 150 150 content_sources: &mut ContentSources, 151 151 options: &BuildOptions, 152 152 ) -> Result<BuildOutput, Box<dyn std::error::Error>> { ··· 228 228 RouteType::Static => { 229 229 let route_start = Instant::now(); 230 230 231 - let content = PageContent::new(content_sources); 232 - let mut page_assets = PageAssets::new(&page_assets_options); 231 + let content = RouteContent::new(content_sources); 232 + let mut page_assets = RouteAssets::new(&page_assets_options); 233 233 234 - let params = RouteParams::default(); 234 + let params = PageParams::default(); 235 235 let url = route.url(&params); 236 236 237 - let result = route.build(&mut RouteContext::from_static_route( 237 + let result = route.build(&mut PageContext::from_static_route( 238 238 &content, 239 239 &mut page_assets, 240 240 &url, ··· 259 259 page_count += 1; 260 260 } 261 261 RouteType::Dynamic => { 262 - let routes = route.get_routes(&DynamicRouteContext { 263 - content: &PageContent::new(content_sources), 262 + let content = RouteContent::new(content_sources); 263 + let mut page_assets = RouteAssets::new(&page_assets_options); 264 + 265 + let pages = route.get_pages(&mut DynamicRouteContext { 266 + content: &content, 267 + assets: &mut page_assets, 264 268 }); 265 269 266 - if routes.is_empty() { 267 - info!(target: "build", "{} is a dynamic route, but its implementation of Page::routes returned an empty Vec. No pages will be generated for this route.", route.route_raw().to_string().bold()); 270 + if pages.is_empty() { 271 + warn!(target: "build", "{} is a dynamic route, but its implementation of Route::pages returned an empty Vec. No pages will be generated for this route.", route.route_raw().to_string().bold()); 268 272 continue; 269 273 } else { 270 274 info!(target: "build", "{}", route.route_raw().to_string().bold()); 271 275 } 272 276 273 - let content = PageContent::new(content_sources); 274 - for dynamic_route in routes { 277 + for page in pages { 275 278 let route_start = Instant::now(); 276 279 277 - let mut page_assets = PageAssets::new(&page_assets_options); 278 - 279 - let url = route.url(&dynamic_route.0); 280 + let url = route.url(&page.0); 280 281 281 - let content = route.build(&mut RouteContext::from_dynamic_route( 282 - &dynamic_route, 282 + let content = route.build(&mut PageContext::from_dynamic_route( 283 + &page, 283 284 &content, 284 285 &mut page_assets, 285 286 &url, 286 287 ))?; 287 288 288 - let file_path = route.file_path(&dynamic_route.0, &options.output_dir); 289 + let file_path = route.file_path(&page.0, &options.output_dir); 289 290 290 291 write_route_file(&content, &file_path)?; 291 292 292 293 info!(target: "pages", "├─ {} {}", file_path.to_string_lossy().dimmed(), format_elapsed_time(route_start.elapsed(), &route_format_options)); 293 - 294 - build_pages_images.extend(page_assets.images); 295 - build_pages_scripts.extend(page_assets.scripts); 296 - build_pages_styles.extend(page_assets.styles); 297 294 298 295 build_metadata.add_page( 299 296 route.route_raw().to_string(), 300 297 file_path.to_string_lossy().to_string(), 301 - Some(dynamic_route.0.0), 298 + Some(page.0.0), 302 299 ); 303 300 304 301 page_count += 1; 305 302 } 303 + 304 + build_pages_images.extend(page_assets.images); 305 + build_pages_scripts.extend(page_assets.scripts); 306 + build_pages_styles.extend(page_assets.styles); 306 307 } 307 308 } 308 309 } ··· 509 510 510 511 pub fn finish_route( 511 512 render_result: RenderResult, 512 - page_assets: &assets::PageAssets, 513 + page_assets: &assets::RouteAssets, 513 514 route: String, 514 515 ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { 515 516 match render_result {
+4 -4
crates/maudit/src/build/options.rs
··· 1 1 use std::path::PathBuf; 2 2 3 - use crate::{assets::PageAssetsOptions, is_dev}; 3 + use crate::{assets::RouteAssetsOptions, is_dev}; 4 4 5 5 /// Maudit build options. Should be passed to [`coronate()`](crate::coronate()). 6 6 /// ··· 57 57 impl BuildOptions { 58 58 /// Returns the fully resolved assets options, with the `output_assets_dir` property resolved to be inside `output_dir`. 59 59 /// e.g. if `output_dir` is `dist` and `assets.assets_dir` is `_maudit`, `output_assets_dir` will return `dist/_maudit`. The user-entered `assets.assets_dir` is also available and unchanged. 60 - pub fn page_assets_options(&self) -> PageAssetsOptions { 61 - PageAssetsOptions { 60 + pub fn page_assets_options(&self) -> RouteAssetsOptions { 61 + RouteAssetsOptions { 62 62 assets_dir: self.assets.assets_dir.clone(), 63 63 output_assets_dir: self.output_dir.join(&self.assets.assets_dir), 64 64 hashing_strategy: self.assets.hashing_strategy, ··· 76 76 /// Directory inside the output directory to place built assets in. 77 77 /// Defaults to `_maudit`. 78 78 /// 79 - /// Note that this value is not automatically joined with the `output_dir` in `BuildOptions`. Use [`BuildOptions::page_assets_options()`] to get a `PageAssetsOptions` with the correct final path. 79 + /// Note that this value is not automatically joined with the `output_dir` in `BuildOptions`. Use [`BuildOptions::route_assets_options()`] to get a `RouteAssetsOptions` with the correct final path. 80 80 pub assets_dir: PathBuf, 81 81 82 82 /// Strategy to use when hashing assets for fingerprinting.
+53 -25
crates/maudit/src/content.rs
··· 9 9 pub mod markdown; 10 10 mod slugger; 11 11 12 - use crate::page::{RouteContext, RouteParams}; 12 + use crate::{ 13 + assets::RouteAssets, 14 + route::{DynamicRouteContext, PageContext, PageParams}, 15 + }; 13 16 pub use markdown::{ 14 17 components::{ 15 18 BlockQuoteKind, BlockquoteComponent, CodeComponent, EmphasisComponent, HardBreakComponent, ··· 108 111 /// 109 112 /// In a page: 110 113 /// ```rs 111 - /// use maudit::page::prelude::*; 114 + /// use maudit::route::prelude::*; 112 115 /// # use maudit::content::markdown_entry; 113 116 /// # 114 117 /// # #[markdown_entry] ··· 125 128 /// pub article: String, 126 129 /// } 127 130 /// 128 - /// impl Page<ArticleParams> for Article { 129 - /// fn render(&self, ctx: &mut RouteContext) -> RenderResult { 131 + /// impl Route<ArticleParams> for Article { 132 + /// fn render(&self, ctx: &mut PageContext) -> RenderResult { 130 133 /// let params = ctx.params::<ArticleParams>(); 131 134 /// let articles = ctx.content.get_source::<ArticleContent>("articles"); 132 135 /// let article = articles.get_entry(&params.article); 133 136 /// article.render(ctx).into() 134 137 /// } 135 138 /// 136 - /// fn routes(&self, ctx: &DynamicRouteContext) -> Vec<ArticleParams> { 139 + /// fn pages(&self, ctx: &mut DynamicRouteContext) -> Vec<ArticleParams> { 137 140 /// let articles = ctx.content.get_source::<ArticleContent>("articles"); 138 141 /// 139 142 /// articles.into_params(|entry| ArticleParams { ··· 142 145 /// } 143 146 /// } 144 147 /// ``` 145 - pub struct PageContent<'a> { 148 + pub struct RouteContent<'a> { 146 149 sources: &'a [Box<dyn ContentSourceInternal>], 147 150 } 148 151 149 - impl PageContent<'_> { 150 - pub fn new(sources: &'_ ContentSources) -> PageContent<'_> { 151 - PageContent { 152 + impl RouteContent<'_> { 153 + pub fn new(sources: &'_ ContentSources) -> RouteContent<'_> { 154 + RouteContent { 152 155 sources: sources.sources(), 153 156 } 154 157 } ··· 200 203 /// 201 204 /// ## Example 202 205 /// ```rs 203 - /// use maudit::page::prelude::*; 206 + /// use maudit::route::prelude::*; 204 207 /// # use maudit::content::markdown_entry; 205 208 /// # 206 209 /// # #[markdown_entry] ··· 217 220 /// pub article: String, 218 221 /// } 219 222 /// 220 - /// impl Page for Article { 221 - /// fn render(&self, ctx: &mut RouteContext) -> RenderResult { 223 + /// impl Route for Article { 224 + /// fn render(&self, ctx: &mut PageContext) -> RenderResult { 222 225 /// let articles = ctx.content.get_source::<ArticleContent>("articles"); 223 226 /// let article = articles.get_entry("my-article"); // returns a ContentEntry 224 227 /// article.render(ctx).into() ··· 229 232 pub id: String, 230 233 render: OptionalContentRenderFn, 231 234 pub raw_content: Option<String>, 232 - data_loader: OptionalDataLoadingFn<T>, 235 + data_loader: Option<DataLoadingFn<T>>, 233 236 cached_data: std::sync::OnceLock<T>, 234 237 pub file_path: Option<PathBuf>, 235 238 } 236 239 237 - type OptionalDataLoadingFn<T> = 238 - Option<Box<dyn Fn(&mut crate::page::RouteContext) -> T + Send + Sync>>; 240 + /// Trait for contexts that can provide access to content 241 + pub trait ContentContext { 242 + fn content(&self) -> &RouteContent<'_>; 243 + fn assets(&mut self) -> &mut RouteAssets; 244 + } 245 + 246 + impl ContentContext for PageContext<'_> { 247 + fn content(&self) -> &RouteContent<'_> { 248 + self.content 249 + } 250 + 251 + fn assets(&mut self) -> &mut RouteAssets { 252 + self.assets 253 + } 254 + } 255 + 256 + impl ContentContext for DynamicRouteContext<'_> { 257 + fn content(&self) -> &RouteContent<'_> { 258 + self.content 259 + } 260 + 261 + fn assets(&mut self) -> &mut RouteAssets { 262 + self.assets 263 + } 264 + } 265 + 266 + type DataLoadingFn<T> = Box<dyn Fn(&mut dyn ContentContext) -> T + Send + Sync>; 239 267 240 268 type OptionalContentRenderFn = 241 - Option<Box<dyn Fn(&str, &mut crate::page::RouteContext) -> String + Send + Sync>>; 269 + Option<Box<dyn Fn(&str, &mut crate::route::PageContext) -> String + Send + Sync>>; 242 270 243 271 impl<T> ContentEntry<T> { 244 272 pub fn new( ··· 262 290 id: String, 263 291 render: OptionalContentRenderFn, 264 292 raw_content: Option<String>, 265 - data_loader: Box<dyn Fn(&mut crate::page::RouteContext) -> T + Send + Sync>, 293 + data_loader: DataLoadingFn<T>, 266 294 file_path: Option<PathBuf>, 267 295 ) -> Self { 268 296 Self { ··· 275 303 } 276 304 } 277 305 278 - pub fn data(&self, ctx: &mut RouteContext) -> &T { 306 + pub fn data<C: ContentContext>(&self, ctx: &mut C) -> &T { 279 307 self.cached_data.get_or_init(|| { 280 308 if let Some(ref loader) = self.data_loader { 281 309 loader(ctx) ··· 285 313 }) 286 314 } 287 315 288 - pub fn render(&self, ctx: &mut RouteContext) -> String { 316 + pub fn render(&self, ctx: &mut PageContext) -> String { 289 317 (self.render.as_ref().unwrap())(self.raw_content.as_ref().unwrap(), ctx) 290 318 } 291 319 } ··· 299 327 /// 300 328 /// ## Example 301 329 /// ```rs 302 - /// use maudit::page::prelude::*; 330 + /// use maudit::route::prelude::*; 303 331 /// use maudit::content::{glob_markdown, ContentSources}; 304 332 /// use maudit::content_sources; 305 333 /// # use maudit::content::markdown_entry; ··· 375 403 376 404 pub fn into_params<P>(&self, cb: impl Fn(&ContentEntry<T>) -> P) -> Vec<P> 377 405 where 378 - P: Into<RouteParams>, 406 + P: Into<PageParams>, 379 407 { 380 408 self.entries.iter().map(cb).collect() 381 409 } 382 410 383 - pub fn into_routes<Params, Props>( 411 + pub fn into_pages<Params, Props>( 384 412 &self, 385 - cb: impl Fn(&ContentEntry<T>) -> crate::page::Route<Params, Props>, 386 - ) -> Vec<crate::page::Route<Params, Props>> 413 + cb: impl Fn(&ContentEntry<T>) -> crate::route::Page<Params, Props>, 414 + ) -> crate::route::Pages<Params, Props> 387 415 where 388 - Params: Into<RouteParams>, 416 + Params: Into<PageParams>, 389 417 { 390 418 self.entries.iter().map(cb).collect() 391 419 }
+12 -8
crates/maudit/src/content/markdown.rs
··· 12 12 13 13 use crate::{ 14 14 assets::Asset, 15 - content::shortcodes::{MarkdownShortcodes, preprocess_shortcodes}, 16 - page::RouteContext, 15 + content::{ 16 + ContentContext, 17 + shortcodes::{MarkdownShortcodes, preprocess_shortcodes}, 18 + }, 19 + route::PageContext, 17 20 }; 18 21 19 22 use super::{ContentEntry, highlight::CodeBlock, slugger}; ··· 27 30 /// 28 31 /// ## Example 29 32 /// ```rs 30 - /// use maudit::page::prelude::*; 33 + /// use maudit::route::prelude::*; 31 34 /// use maud::{html, Markup}; 32 35 /// # use maudit::content::markdown_entry; 33 36 /// # ··· 40 43 /// #[route("/articles/my-article")] 41 44 /// pub struct Article; 42 45 /// 43 - /// impl Page<RouteParams, Markup> for Article { 44 - /// fn render(&self, ctx: &mut RouteContext) -> Markup { 46 + /// impl Route<PageParams, Markup> for Article { 47 + /// fn render(&self, ctx: &mut PageContext) -> Markup { 45 48 /// let articles = ctx.content.get_source::<ArticleContent>("articles"); 46 49 /// let article = articles.get_entry("my-article"); 47 50 /// let headings = article.data(ctx).get_headings(); // returns a Vec<MarkdownHeading> ··· 209 212 210 213 // Clone content for the closure 211 214 let content_clone = content.clone(); 212 - let data_loader = 213 - Box::new(move |_: &mut RouteContext| parse_markdown_with_frontmatter(&content_clone)); 215 + let data_loader = Box::new(move |_: &mut dyn ContentContext| { 216 + parse_markdown_with_frontmatter(&content_clone) 217 + }); 214 218 215 219 // Perhaps not ideal, but I don't know better. We're at the "get it working" stage - erika, 2025-08-24 216 220 // Ideally, we'd at least avoid the allocation here whenever `options` is None, not sure how to do that ergonomically ··· 306 310 content: &str, 307 311 options: Option<&MarkdownOptions>, 308 312 path: Option<&Path>, 309 - mut route_ctx: Option<&mut RouteContext>, 313 + mut route_ctx: Option<&mut PageContext>, 310 314 ) -> String { 311 315 let content = if let Some(shortcodes) = options.map(|o| &o.shortcodes) 312 316 && !shortcodes.is_empty()
+4 -4
crates/maudit/src/content/markdown/shortcodes.rs
··· 1 1 use rustc_hash::FxHashMap; 2 2 use std::str::FromStr; 3 3 4 - use crate::page::RouteContext; 4 + use crate::route::PageContext; 5 5 6 6 pub type ShortcodeFn = 7 - Box<dyn Fn(&ShortcodeArgs, Option<&mut RouteContext>) -> String + Send + Sync>; 7 + Box<dyn Fn(&ShortcodeArgs, Option<&mut PageContext>) -> String + Send + Sync>; 8 8 9 9 #[derive(Default)] 10 10 pub struct MarkdownShortcodes(FxHashMap<String, ShortcodeFn>); ··· 16 16 17 17 pub fn register<F>(&mut self, name: &str, func: F) 18 18 where 19 - F: Fn(&ShortcodeArgs, Option<&mut RouteContext>) -> String + Send + Sync + 'static, 19 + F: Fn(&ShortcodeArgs, Option<&mut PageContext>) -> String + Send + Sync + 'static, 20 20 { 21 21 self.0.insert(name.to_string(), Box::new(func)); 22 22 } ··· 58 58 pub fn preprocess_shortcodes( 59 59 content: &str, 60 60 shortcodes: &MarkdownShortcodes, 61 - mut route_ctx: Option<&mut RouteContext>, 61 + mut route_ctx: Option<&mut PageContext>, 62 62 markdown_path: Option<&str>, 63 63 ) -> Result<String, String> { 64 64 let mut output = String::new();
+12 -12
crates/maudit/src/content/markdown/shortcodes_tests.rs
··· 1 1 #[cfg(test)] 2 2 mod tests { 3 3 use crate::{ 4 - assets::PageAssetsOptions, 4 + assets::RouteAssetsOptions, 5 5 content::shortcodes::{MarkdownShortcodes, preprocess_shortcodes}, 6 - page::RouteContext, 6 + route::PageContext, 7 7 }; 8 8 9 9 fn create_test_shortcodes() -> MarkdownShortcodes { ··· 52 52 shortcodes 53 53 } 54 54 55 - // Helper function to create a minimal RouteContext for testing 55 + // Helper function to create a minimal PageContext for testing 56 56 fn with_test_route_context<F, R>(f: F) -> R 57 57 where 58 - F: for<'a> FnOnce(&mut RouteContext<'a>) -> R, 58 + F: for<'a> FnOnce(&mut PageContext<'a>) -> R, 59 59 { 60 60 use crate::{ 61 - assets::PageAssets, 62 - content::{ContentSources, PageContent}, 61 + assets::RouteAssets, 62 + content::{ContentSources, RouteContent}, 63 63 }; 64 64 65 65 let content_sources = ContentSources::new(vec![]); 66 - let content = PageContent::new(&content_sources); 67 - let mut page_assets = PageAssets::new(&PageAssetsOptions { 66 + let content = RouteContent::new(&content_sources); 67 + let mut page_assets = RouteAssets::new(&RouteAssetsOptions { 68 68 assets_dir: "assets".into(), 69 69 ..Default::default() 70 70 }); 71 71 72 - let mut ctx = RouteContext { 72 + let mut ctx = PageContext { 73 73 content: &content, 74 74 assets: &mut page_assets, 75 75 current_url: &"/test".to_string(), ··· 80 80 f(&mut ctx) 81 81 } 82 82 83 - // Helper function for tests that don't need RouteContext 83 + // Helper function for tests that don't need PageContext 84 84 fn preprocess_shortcodes_simple( 85 85 content: &str, 86 86 shortcodes: &MarkdownShortcodes, ··· 88 88 preprocess_shortcodes(content, shortcodes, None, None) 89 89 } 90 90 91 - // Helper function that automatically wraps RouteContext in Some() for existing tests 91 + // Helper function that automatically wraps PageContext in Some() for existing tests 92 92 fn preprocess_shortcodes_with_ctx( 93 93 content: &str, 94 94 shortcodes: &MarkdownShortcodes, 95 - route_ctx: &mut RouteContext, 95 + route_ctx: &mut PageContext, 96 96 ) -> Result<String, String> { 97 97 preprocess_shortcodes(content, shortcodes, Some(route_ctx), None) 98 98 }
+14 -14
crates/maudit/src/lib.rs
··· 9 9 pub mod assets; 10 10 pub mod content; 11 11 pub mod errors; 12 - pub mod page; 12 + pub mod route; 13 13 14 - mod route; 14 + mod routing; 15 15 16 16 // Exports for end-users 17 17 pub use build::metadata::{BuildOutput, PageOutput, StaticAssetOutput}; ··· 31 31 //! 32 32 //! ## Example 33 33 //! ```rs 34 - //! use maudit::page::prelude::*; 34 + //! use maudit::route::prelude::*; 35 35 //! use maud::{html, Markup}; 36 36 //! 37 37 //! #[route("/")] 38 38 //! pub struct Index; 39 39 //! 40 - //! impl Page<RouteParams, (), Markup> for Index { 41 - //! fn render(&self, ctx: &mut RouteContext) -> Markup { 40 + //! impl Route<PageParams, (), Markup> for Index { 41 + //! fn render(&self, ctx: &mut PageContext) -> Markup { 42 42 //! html! { 43 43 //! h1 { "Hello, world!" } 44 44 //! } ··· 56 56 use build::execute_build; 57 57 use content::ContentSources; 58 58 use logging::init_logging; 59 - use page::FullPage; 59 + use route::FullRoute; 60 60 61 61 /// Returns whether Maudit is running in development mode (through `maudit dev`). 62 62 /// ··· 79 79 /// content_sources, coronate, routes, BuildOptions, BuildOutput, 80 80 /// }; 81 81 /// 82 - /// # mod pages { 83 - /// # use maudit::page::prelude::*; 82 + /// # mod routes { 83 + /// # use maudit::route::prelude::*; 84 84 /// # 85 85 /// # #[route("/")] 86 86 /// # pub struct Index; 87 - /// # impl Page<RouteParams, (), String> for Index { 88 - /// # fn render(&self, _ctx: &mut RouteContext) -> String { 87 + /// # impl Route<PageParams, (), String> for Index { 88 + /// # fn render(&self, _ctx: &mut PageContext) -> String { 89 89 /// # "Hello, world!".to_string() 90 90 /// # } 91 91 /// # } 92 92 /// # #[route("/article")] 93 93 /// # pub struct Article; 94 94 /// # 95 - /// # impl Page<RouteParams, (), String> for Article { 96 - /// # fn render(&self, _ctx: &mut RouteContext) -> String { 95 + /// # impl Route<PageParams, (), String> for Article { 96 + /// # fn render(&self, _ctx: &mut PageContext) -> String { 97 97 /// # "Hello, world!".to_string() 98 98 /// # } 99 99 /// # } ··· 101 101 /// 102 102 /// fn main() -> Result<BuildOutput, Box<dyn std::error::Error>> { 103 103 /// coronate( 104 - /// routes![pages::Index, pages::Article], 104 + /// routes![routes::Index, routes::Article], 105 105 /// content_sources![], 106 106 /// BuildOptions::default(), 107 107 /// ) ··· 202 202 /// } 203 203 /// ``` 204 204 pub fn coronate( 205 - routes: &[&dyn FullPage], 205 + routes: &[&dyn FullRoute], 206 206 mut content_sources: ContentSources, 207 207 options: BuildOptions, 208 208 ) -> Result<BuildOutput, Box<dyn std::error::Error>> {
-530
crates/maudit/src/page.rs
··· 1 - //! Core traits and structs to define the pages of your website. 2 - //! 3 - //! Every page must implement the [`Page`] trait. Then, pages can be passed to [`coronate()`](crate::coronate), through the [`routes!`](crate::routes) macro, to be built. 4 - use crate::assets::PageAssets; 5 - use crate::build::finish_route; 6 - use crate::content::PageContent; 7 - use crate::route::{ 8 - extract_params_from_raw_route, get_route_type_from_route_params, guess_if_route_is_endpoint, 9 - }; 10 - use rustc_hash::FxHashMap; 11 - use std::any::Any; 12 - use std::path::{Path, PathBuf}; 13 - 14 - /// The result of a page render, can be either text or raw bytes. 15 - /// 16 - /// Typically used through the [`Into<RenderResult>`](std::convert::Into) and [`From<RenderResult>`](std::convert::From) implementations for common types. 17 - /// End users should rarely need to interact with this enum directly. 18 - /// 19 - /// ## Example 20 - /// ```rs 21 - /// use maudit::page::prelude::*; 22 - /// 23 - /// #[route("/")] 24 - /// pub struct Index; 25 - /// 26 - /// impl Page for Index { 27 - /// fn render(&self, ctx: &mut RouteContext) -> RenderResult { 28 - /// "<h1>Hello, world!</h1>".into() 29 - /// } 30 - /// } 31 - /// ``` 32 - pub enum RenderResult { 33 - Text(String), 34 - Raw(Vec<u8>), 35 - } 36 - 37 - impl From<maud::Markup> for RenderResult { 38 - fn from(val: maud::Markup) -> Self { 39 - RenderResult::Text(val.into_string()) 40 - } 41 - } 42 - 43 - impl From<String> for RenderResult { 44 - fn from(val: String) -> Self { 45 - RenderResult::Text(val) 46 - } 47 - } 48 - 49 - impl From<&str> for RenderResult { 50 - fn from(val: &str) -> Self { 51 - RenderResult::Text(val.to_string()) 52 - } 53 - } 54 - 55 - impl From<Vec<u8>> for RenderResult { 56 - fn from(val: Vec<u8>) -> Self { 57 - RenderResult::Raw(val) 58 - } 59 - } 60 - 61 - impl From<&[u8]> for RenderResult { 62 - fn from(val: &[u8]) -> Self { 63 - RenderResult::Raw(val.to_vec()) 64 - } 65 - } 66 - 67 - pub type Routes<Params = RouteParams, Props = ()> = Vec<Route<Params, Props>>; 68 - 69 - /// Represents a route with its parameters and associated props 70 - #[derive(Debug, Clone)] 71 - pub struct Route<Params = RouteParams, Props = ()> 72 - where 73 - Params: Into<RouteParams>, 74 - { 75 - pub params: Params, 76 - pub props: Props, 77 - } 78 - 79 - impl<Params, Props> Route<Params, Props> 80 - where 81 - Params: Into<RouteParams>, 82 - { 83 - pub fn new(params: Params, props: Props) -> Self { 84 - Self { params, props } 85 - } 86 - } 87 - 88 - impl<Params> Route<Params, ()> 89 - where 90 - Params: Into<RouteParams>, 91 - { 92 - pub fn from_params(params: Params) -> Self { 93 - Self { params, props: () } 94 - } 95 - } 96 - 97 - /// Pagination metadata for routes 98 - #[derive(Debug, Clone)] 99 - pub struct PaginationMeta { 100 - pub page: usize, 101 - pub per_page: usize, 102 - pub total_items: usize, 103 - pub total_pages: usize, 104 - pub has_next: bool, 105 - pub has_prev: bool, 106 - pub next_page: Option<usize>, 107 - pub prev_page: Option<usize>, 108 - pub start_index: usize, 109 - pub end_index: usize, 110 - } 111 - 112 - impl PaginationMeta { 113 - pub fn new(page: usize, per_page: usize, total_items: usize) -> Self { 114 - let total_pages = if total_items == 0 { 115 - 1 116 - } else { 117 - total_items.div_ceil(per_page) 118 - }; 119 - let start_index = page * per_page; 120 - let end_index = ((page + 1) * per_page).min(total_items); 121 - 122 - Self { 123 - page, 124 - per_page, 125 - total_items, 126 - total_pages, 127 - has_next: page < total_pages - 1, 128 - has_prev: page > 0, 129 - next_page: if page < total_pages - 1 { 130 - Some(page + 1) 131 - } else { 132 - None 133 - }, 134 - prev_page: if page > 0 { Some(page - 1) } else { None }, 135 - start_index, 136 - end_index, 137 - } 138 - } 139 - } 140 - 141 - /// Helper function to create paginated routes from a content source 142 - pub fn paginate_content<T, Params>( 143 - entries: &[crate::content::ContentEntry<T>], 144 - per_page: usize, 145 - mut params_fn: impl FnMut(usize) -> Params, 146 - ) -> Routes<Params, PaginationMeta> 147 - where 148 - Params: Into<RouteParams>, 149 - { 150 - if entries.is_empty() { 151 - return vec![]; 152 - } 153 - 154 - let total_items = entries.len(); 155 - let total_pages = total_items.div_ceil(per_page); 156 - let mut routes = Vec::new(); 157 - 158 - for page in 0..total_pages { 159 - let params = params_fn(page); 160 - let props = PaginationMeta::new(page, per_page, total_items); 161 - 162 - routes.push(Route::new(params, props)); 163 - } 164 - 165 - routes 166 - } 167 - 168 - /// Helper to get paginated slice from content entries 169 - pub fn get_page_slice<'a, T>( 170 - entries: &'a [crate::content::ContentEntry<T>], 171 - pagination: &'a PaginationMeta, 172 - ) -> &'a [crate::content::ContentEntry<T>] { 173 - &entries[pagination.start_index..pagination.end_index] 174 - } 175 - 176 - /// Allows to access various data and assets in a [`Page`] implementation. 177 - /// 178 - /// ## Example 179 - /// ```rs 180 - /// use maudit::page::prelude::*; 181 - /// use maud::html; 182 - /// # use maudit::content::markdown_entry; 183 - /// # 184 - /// # #[markdown_entry] 185 - /// # pub struct ArticleContent { 186 - /// # pub title: String, 187 - /// # pub description: String, 188 - /// # } 189 - /// 190 - /// #[route("/")] 191 - /// pub struct Index; 192 - /// 193 - /// impl Page for Index { 194 - /// fn render(&self, ctx: &mut RouteContext) -> RenderResult { 195 - /// let logo = ctx.assets.add_image("logo.png"); 196 - /// let last_entries = &ctx.content.get_source::<ArticleContent>("articles").entries; 197 - /// html! { 198 - /// main { 199 - /// (logo) 200 - /// ul { 201 - /// @for entry in last_entries { 202 - /// li { (entry.data(ctx).title) } 203 - /// } 204 - /// } 205 - /// } 206 - /// }.into() 207 - /// } 208 - /// } 209 - pub struct RouteContext<'a> { 210 - pub params: &'a dyn Any, 211 - pub props: &'a dyn Any, 212 - pub content: &'a PageContent<'a>, 213 - pub assets: &'a mut PageAssets, 214 - pub current_url: &'a String, 215 - } 216 - 217 - impl<'a> RouteContext<'a> { 218 - pub fn from_static_route( 219 - content: &'a PageContent, 220 - assets: &'a mut PageAssets, 221 - current_url: &'a String, 222 - ) -> Self { 223 - Self { 224 - params: &(), 225 - props: &(), 226 - content, 227 - assets, 228 - current_url, 229 - } 230 - } 231 - 232 - pub fn from_dynamic_route( 233 - dynamic_route: &'a RouteResult, 234 - content: &'a PageContent, 235 - assets: &'a mut PageAssets, 236 - current_url: &'a String, 237 - ) -> Self { 238 - Self { 239 - params: dynamic_route.1.as_ref(), 240 - props: dynamic_route.2.as_ref(), 241 - content, 242 - assets, 243 - current_url, 244 - } 245 - } 246 - 247 - pub fn params<T: 'static + Clone>(&self) -> T { 248 - self.params 249 - .downcast_ref::<T>() 250 - .unwrap_or_else(|| panic!("Params type mismatch: got {}", std::any::type_name::<T>())) 251 - .clone() 252 - } 253 - 254 - pub fn props<T: 'static + Clone>(&self) -> T { 255 - self.props 256 - .downcast_ref::<T>() 257 - .unwrap_or_else(|| panic!("Props type mismatch: got {}", std::any::type_name::<T>())) 258 - .clone() 259 - } 260 - 261 - pub fn params_ref<T: 'static>(&self) -> &T { 262 - self.params 263 - .downcast_ref::<T>() 264 - .unwrap_or_else(|| panic!("Params type mismatch: got {}", std::any::type_name::<T>())) 265 - } 266 - 267 - pub fn props_ref<T: 'static>(&self) -> &T { 268 - self.props 269 - .downcast_ref::<T>() 270 - .unwrap_or_else(|| panic!("Props type mismatch: got {}", std::any::type_name::<T>())) 271 - } 272 - } 273 - 274 - /// Allows to access the content source in the [`Page::routes`] method. 275 - /// 276 - /// ## Example 277 - /// ```rs 278 - /// use maudit::page::prelude::*; 279 - /// # use maudit::content::markdown_entry; 280 - /// # 281 - /// # #[markdown_entry] 282 - /// # pub struct ArticleContent { 283 - /// # pub title: String, 284 - /// # pub description: String, 285 - /// # } 286 - /// 287 - /// #[route("/articles/[article]")] 288 - /// pub struct Article; 289 - /// 290 - /// #[derive(Params)] 291 - /// pub struct ArticleParams { 292 - /// pub article: String, 293 - /// } 294 - /// 295 - /// impl Page<ArticleParams> for Article { 296 - /// fn render(&self, ctx: &mut RouteContext) -> RenderResult { 297 - /// let params = ctx.params::<ArticleParams>(); 298 - /// let articles = ctx.content.get_source::<ArticleContent>("articles"); 299 - /// let article = articles.get_entry(&params.article); 300 - /// article.render().into() 301 - /// } 302 - /// 303 - /// fn routes(&self, ctx: &DynamicRouteContext) -> Vec<ArticleParams> { 304 - /// let articles = ctx.content.get_source::<ArticleContent>("articles"); 305 - /// 306 - /// articles.into_params(|entry| ArticleParams { 307 - /// article: entry.id.clone(), 308 - /// }) 309 - /// } 310 - /// } 311 - /// ``` 312 - pub struct DynamicRouteContext<'a> { 313 - pub content: &'a PageContent<'a>, 314 - } 315 - 316 - /// Must be implemented for every page of your website. 317 - /// 318 - /// The page struct implementing this trait can be passed to [`coronate()`](crate::coronate), through the [`routes!`](crate::routes) macro, to be built. 319 - /// 320 - /// ## Example 321 - /// ```rs 322 - /// use maudit::page::prelude::*; 323 - /// 324 - /// #[route("/")] 325 - /// pub struct Index; 326 - /// 327 - /// impl Page for Index { 328 - /// fn render(&self, ctx: &mut RouteContext) -> RenderResult { 329 - /// "<h1>Hello, world!</h1>".into() 330 - /// } 331 - /// } 332 - /// ``` 333 - pub trait Page<Params = RouteParams, Props = (), T = RenderResult> 334 - where 335 - Params: Into<RouteParams>, 336 - Props: 'static, 337 - T: Into<RenderResult>, 338 - { 339 - fn routes(&self, _ctx: &DynamicRouteContext) -> Routes<Params, Props> { 340 - Vec::new() 341 - } 342 - fn render(&self, ctx: &mut RouteContext) -> T; 343 - } 344 - 345 - /// Raw representation of a route's parameters. 346 - /// 347 - /// Can be accessed through [`RouteContext`]'s `raw_params`. 348 - #[derive(Clone, Default, Debug)] 349 - pub struct RouteParams(pub FxHashMap<String, String>); 350 - 351 - impl RouteParams { 352 - pub fn from_vec<T>(params: Vec<T>) -> Vec<RouteParams> 353 - where 354 - T: Into<RouteParams>, 355 - { 356 - params.into_iter().map(|p| p.into()).collect() 357 - } 358 - } 359 - 360 - impl From<&RouteParams> for RouteParams { 361 - fn from(params: &RouteParams) -> Self { 362 - params.clone() 363 - } 364 - } 365 - 366 - impl<T> FromIterator<T> for RouteParams 367 - where 368 - T: Into<RouteParams>, 369 - { 370 - fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self { 371 - let mut map = FxHashMap::default(); 372 - for item in iter { 373 - let item = item.into(); 374 - map.extend(item.0); 375 - } 376 - RouteParams(map) 377 - } 378 - } 379 - 380 - #[derive(PartialEq, Eq, Debug)] 381 - /// Used internally by Maudit and should not be implemented by the user. 382 - /// We expose it because [`maudit_macros::route`] implements it for the user behind the scenes. 383 - pub enum RouteType { 384 - Static, 385 - Dynamic, 386 - } 387 - 388 - #[doc(hidden)] 389 - /// Used internally by Maudit and should not be implemented by the user. 390 - /// We expose it because the derive macro implements it for the user behind the scenes. 391 - pub trait InternalPage { 392 - fn route_raw(&self) -> String; 393 - fn is_endpoint(&self) -> bool { 394 - guess_if_route_is_endpoint(&self.route_raw()) 395 - } 396 - fn route_type(&self) -> RouteType { 397 - let params_def = extract_params_from_raw_route(&self.route_raw()); 398 - 399 - get_route_type_from_route_params(&params_def) 400 - } 401 - 402 - fn url(&self, params: &RouteParams) -> String { 403 - let params_def = extract_params_from_raw_route(&self.route_raw()); 404 - 405 - // Replace every param_def with the value from the params hashmap for said key 406 - // So, ex: "/articles/[article]" (params: Hashmap {article: "truc"}) -> "/articles/truc" 407 - let mut route = self.route_raw(); 408 - 409 - for param_def in params_def { 410 - let value = params.0.get(&param_def.key); 411 - 412 - match value { 413 - Some(value) => { 414 - route.replace_range(param_def.index..param_def.index + param_def.length, value); 415 - } 416 - None => { 417 - panic!( 418 - "Route {:?} is missing parameter {:?}", 419 - self.route_raw(), 420 - param_def.key 421 - ); 422 - } 423 - } 424 - } 425 - 426 - route 427 - } 428 - 429 - fn file_path(&self, params: &RouteParams, output_dir: &Path) -> PathBuf { 430 - let params_def = extract_params_from_raw_route(&self.route_raw()); 431 - let mut route = self.route_raw(); 432 - 433 - for param_def in params_def { 434 - let value = params.0.get(&param_def.key); 435 - 436 - match value { 437 - Some(value) => { 438 - route.replace_range(param_def.index..param_def.index + param_def.length, value); 439 - } 440 - None => { 441 - panic!( 442 - "Route {:?} is missing parameter {:?}", 443 - self.route_raw(), 444 - param_def.key 445 - ); 446 - } 447 - } 448 - } 449 - 450 - let cleaned_raw_route = route.trim_start_matches('/').to_string(); 451 - 452 - output_dir.join(match self.is_endpoint() { 453 - true => cleaned_raw_route, 454 - false => match cleaned_raw_route.is_empty() { 455 - true => "index.html".into(), 456 - false => format!("{}/index.html", cleaned_raw_route), 457 - }, 458 - }) 459 - } 460 - } 461 - 462 - /// Extension trait providing generic convenience methods on an instance of a page 463 - pub trait PageExt<Params = RouteParams, Props = (), T = RenderResult>: 464 - Page<Params, Props, T> + InternalPage 465 - where 466 - Params: Into<RouteParams>, 467 - Props: 'static, 468 - T: Into<RenderResult>, 469 - { 470 - /// Get the URL for this page with the given parameters 471 - /// 472 - /// Note that this method merely generates the URL based on the route pattern and parameters, it does not verify if a corresponding route actually exists. 473 - fn url(&self, params: Params) -> String { 474 - InternalPage::url(self, &params.into()) 475 - } 476 - } 477 - 478 - // Blanket implementation for all Page implementors that also implement InternalPage 479 - impl<U, Params, Props, T> PageExt<Params, Props, T> for U 480 - where 481 - U: Page<Params, Props, T> + InternalPage, 482 - Params: Into<RouteParams>, 483 - Props: 'static, 484 - T: Into<RenderResult>, 485 - { 486 - } 487 - 488 - /// Used internally by Maudit and should not be implemented by the user. 489 - /// We expose it because [`maudit_macros::route`] implements it for the user behind the scenes. 490 - pub trait FullPage: InternalPage + Sync + Send { 491 - #[doc(hidden)] 492 - fn render_internal(&self, ctx: &mut RouteContext) -> RenderResult; 493 - #[doc(hidden)] 494 - fn routes_internal(&self, context: &DynamicRouteContext) -> RoutesResult; 495 - 496 - fn get_routes(&self, context: &DynamicRouteContext) -> RoutesResult { 497 - self.routes_internal(context) 498 - } 499 - 500 - fn build(&self, ctx: &mut RouteContext) -> Result<Vec<u8>, Box<dyn std::error::Error>> { 501 - let result = self.render_internal(ctx); 502 - let bytes = finish_route(result, ctx.assets, self.route_raw())?; 503 - 504 - Ok(bytes) 505 - } 506 - } 507 - 508 - pub type RouteResult = (RouteParams, RouteProps, RouteTypedParams); 509 - pub type RoutesResult = Vec<RouteResult>; 510 - 511 - pub type RouteProps = Box<dyn Any + Send + Sync>; 512 - pub type RouteTypedParams = Box<dyn Any + Send + Sync>; 513 - 514 - pub mod prelude { 515 - //! Re-exports of the most commonly used types and traits for defining pages. 516 - //! 517 - //! This module is meant to be glob imported in your page files. 518 - //! 519 - //! ## Example 520 - //! ```rs 521 - //! use maudit::page::prelude::*; 522 - //! ``` 523 - pub use super::{ 524 - DynamicRouteContext, FullPage, Page, PageExt, PaginationMeta, RenderResult, Route, 525 - RouteContext, RouteParams, Routes, get_page_slice, paginate_content, 526 - }; 527 - pub use crate::assets::{Asset, Image, Style, StyleOptions}; 528 - pub use crate::content::MarkdownContent; 529 - pub use maudit_macros::{Params, route}; 530 - }
+808 -123
crates/maudit/src/route.rs
··· 1 - use std::path::Path; 1 + //! Core traits and structs to define the pages of your website. 2 + //! 3 + //! Every route must implement the [`Route`] trait. Then, pages can be passed to [`coronate()`](crate::coronate), through the [`routes!`](crate::routes) macro, to be built. 4 + use crate::assets::RouteAssets; 5 + use crate::build::finish_route; 6 + use crate::content::RouteContent; 7 + use crate::routing::{ 8 + extract_params_from_raw_route, get_route_type_from_route_params, guess_if_route_is_endpoint, 9 + }; 10 + use rustc_hash::FxHashMap; 11 + use std::any::Any; 12 + use std::path::{Path, PathBuf}; 13 + 14 + /// The result of a page render, can be either text or raw bytes. 15 + /// 16 + /// Typically used through the [`Into<RenderResult>`](std::convert::Into) and [`From<RenderResult>`](std::convert::From) implementations for common types. 17 + /// End users should rarely need to interact with this enum directly. 18 + /// 19 + /// ## Example 20 + /// ```rs 21 + /// use maudit::route::prelude::*; 22 + /// 23 + /// #[route("/")] 24 + /// pub struct Index; 25 + /// 26 + /// impl Route for Index { 27 + /// fn render(&self, ctx: &mut PageContext) -> RenderResult { 28 + /// "<h1>Hello, world!</h1>".into() 29 + /// } 30 + /// } 31 + /// ``` 32 + pub enum RenderResult { 33 + Text(String), 34 + Raw(Vec<u8>), 35 + } 36 + 37 + impl From<String> for RenderResult { 38 + fn from(val: String) -> Self { 39 + RenderResult::Text(val) 40 + } 41 + } 42 + 43 + impl From<&str> for RenderResult { 44 + fn from(val: &str) -> Self { 45 + RenderResult::Text(val.to_string()) 46 + } 47 + } 48 + 49 + impl From<Vec<u8>> for RenderResult { 50 + fn from(val: Vec<u8>) -> Self { 51 + RenderResult::Raw(val) 52 + } 53 + } 54 + 55 + impl From<&[u8]> for RenderResult { 56 + fn from(val: &[u8]) -> Self { 57 + RenderResult::Raw(val.to_vec()) 58 + } 59 + } 2 60 3 - use crate::page::RouteType; 61 + pub type Pages<Params = PageParams, Props = ()> = Vec<Page<Params, Props>>; 4 62 5 - #[derive(Debug, PartialEq)] 6 - pub struct ParameterDef { 7 - pub(crate) key: String, 8 - pub(crate) index: usize, 9 - pub(crate) length: usize, 63 + /// Represents a route with its parameters and associated props 64 + #[derive(Debug, Clone)] 65 + pub struct Page<Params = PageParams, Props = ()> 66 + where 67 + Params: Into<PageParams>, 68 + { 69 + pub params: Params, 70 + pub props: Props, 10 71 } 11 72 12 - pub fn extract_params_from_raw_route(raw_route: &str) -> Vec<ParameterDef> { 13 - let mut params = Vec::new(); 14 - let mut start = 0; 73 + impl<Params, Props> Page<Params, Props> 74 + where 75 + Params: Into<PageParams>, 76 + { 77 + pub fn new(params: Params, props: Props) -> Self { 78 + Self { params, props } 79 + } 80 + } 15 81 16 - while let Some(bracket_pos) = raw_route[start..].find('[') { 17 - let abs_pos = start + bracket_pos; 82 + impl<Params> Page<Params, ()> 83 + where 84 + Params: Into<PageParams>, 85 + { 86 + pub fn from_params(params: Params) -> Self { 87 + Self { params, props: () } 88 + } 89 + } 18 90 19 - // Check if escaped by counting preceding backslashes 20 - let backslash_count = raw_route[..abs_pos] 21 - .chars() 22 - .rev() 23 - .take_while(|&c| c == '\\') 24 - .count(); 91 + /// Pagination page for any type of items 92 + pub struct PaginationPage<'a, T> { 93 + pub page: usize, 94 + pub per_page: usize, 95 + pub total_items: usize, 96 + pub total_pages: usize, 97 + pub has_next: bool, 98 + pub has_prev: bool, 99 + pub next_page: Option<usize>, 100 + pub prev_page: Option<usize>, 101 + pub start_index: usize, 102 + pub end_index: usize, 103 + pub items: &'a [T], 104 + } 105 + 106 + impl<'a, T> PaginationPage<'a, T> { 107 + pub fn new(page: usize, per_page: usize, total_items: usize, items: &'a [T]) -> Self { 108 + let total_pages = if total_items == 0 { 109 + 1 110 + } else { 111 + total_items.div_ceil(per_page) 112 + }; 113 + let start_index = page * per_page; 114 + let end_index = ((page + 1) * per_page).min(total_items); 25 115 26 - if backslash_count % 2 == 1 { 27 - start = abs_pos + 1; 28 - continue; 116 + Self { 117 + page, 118 + per_page, 119 + total_items, 120 + total_pages, 121 + has_next: page < total_pages - 1, 122 + has_prev: page > 0, 123 + next_page: if page < total_pages - 1 { 124 + Some(page + 1) 125 + } else { 126 + None 127 + }, 128 + prev_page: if page > 0 { Some(page - 1) } else { None }, 129 + start_index, 130 + end_index, 131 + items: &items[start_index..end_index], 29 132 } 133 + } 134 + } 30 135 31 - if let Some(end_bracket) = raw_route[abs_pos + 1..].find(']') { 32 - let end_pos = abs_pos + 1 + end_bracket; 33 - let key = raw_route[abs_pos + 1..end_pos].to_string(); 136 + impl<'a, T> std::fmt::Debug for PaginationPage<'a, T> { 137 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 138 + f.debug_struct("PaginationPage") 139 + .field("page", &self.page) 140 + .field("per_page", &self.per_page) 141 + .field("total_items", &self.total_items) 142 + .field("total_pages", &self.total_pages) 143 + .field("has_next", &self.has_next) 144 + .field("has_prev", &self.has_prev) 145 + .field("next_page", &self.next_page) 146 + .field("prev_page", &self.prev_page) 147 + .field("start_index", &self.start_index) 148 + .field("end_index", &self.end_index) 149 + // I don't really want to force users to implement Debug for T, so just show the length of items 150 + .field("items", &format!("[{} items]", self.items.len())) 151 + .finish() 152 + } 153 + } 34 154 35 - params.push(ParameterDef { 36 - key, 37 - index: abs_pos, 38 - length: end_pos - abs_pos + 1, 39 - }); 155 + /// Helper function to create paginated routes from any slice 156 + pub fn paginate<T, Params>( 157 + items: &[T], 158 + per_page: usize, 159 + mut params_fn: impl FnMut(usize) -> Params, 160 + ) -> Pages<Params, PaginationPage<'_, T>> 161 + where 162 + Params: Into<PageParams>, 163 + { 164 + if items.is_empty() { 165 + return vec![]; 166 + } 167 + 168 + let total_items = items.len(); 169 + let total_pages = total_items.div_ceil(per_page); 170 + let mut routes = Vec::new(); 171 + 172 + for page in 0..total_pages { 173 + let params = params_fn(page); 174 + let props = PaginationPage::new(page, per_page, total_items, items); 175 + 176 + routes.push(Page::new(params, props)); 177 + } 178 + 179 + routes 180 + } 181 + 182 + /// Allows to access various data and assets in a [`Route`] implementation. 183 + /// 184 + /// ## Example 185 + /// ```rs 186 + /// use maudit::route::prelude::*; 187 + /// use maud::html; 188 + /// # use maudit::content::markdown_entry; 189 + /// # 190 + /// # #[markdown_entry] 191 + /// # pub struct ArticleContent { 192 + /// # pub title: String, 193 + /// # pub description: String, 194 + /// # } 195 + /// 196 + /// #[route("/")] 197 + /// pub struct Index; 198 + /// 199 + /// impl Route for Index { 200 + /// fn render(&self, ctx: &mut PageContext) -> RenderResult { 201 + /// let logo = ctx.assets.add_image("logo.png"); 202 + /// let last_entries = &ctx.content.get_source::<ArticleContent>("articles").entries; 203 + /// html! { 204 + /// main { 205 + /// (logo) 206 + /// ul { 207 + /// @for entry in last_entries { 208 + /// li { (entry.data(ctx).title) } 209 + /// } 210 + /// } 211 + /// } 212 + /// }.into() 213 + /// } 214 + /// } 215 + pub struct PageContext<'a> { 216 + pub params: &'a dyn Any, 217 + pub props: &'a dyn Any, 218 + pub content: &'a RouteContent<'a>, 219 + pub assets: &'a mut RouteAssets, 220 + pub current_url: &'a String, 221 + } 222 + 223 + impl<'a> PageContext<'a> { 224 + pub fn from_static_route( 225 + content: &'a RouteContent, 226 + assets: &'a mut RouteAssets, 227 + current_url: &'a String, 228 + ) -> Self { 229 + Self { 230 + params: &(), 231 + props: &(), 232 + content, 233 + assets, 234 + current_url, 235 + } 236 + } 237 + 238 + pub fn from_dynamic_route( 239 + dynamic_page: &'a PagesResult, 240 + content: &'a RouteContent, 241 + assets: &'a mut RouteAssets, 242 + current_url: &'a String, 243 + ) -> Self { 244 + Self { 245 + params: dynamic_page.1.as_ref(), 246 + props: dynamic_page.2.as_ref(), 247 + content, 248 + assets, 249 + current_url, 250 + } 251 + } 252 + 253 + pub fn params<T: 'static + Clone>(&self) -> T { 254 + self.params 255 + .downcast_ref::<T>() 256 + .unwrap_or_else(|| panic!("Params type mismatch: got {}", std::any::type_name::<T>())) 257 + .clone() 258 + } 259 + 260 + pub fn props<T: 'static + Clone>(&self) -> T { 261 + self.props 262 + .downcast_ref::<T>() 263 + .unwrap_or_else(|| panic!("Props type mismatch: got {}", std::any::type_name::<T>())) 264 + .clone() 265 + } 266 + 267 + pub fn params_ref<T: 'static>(&self) -> &T { 268 + self.params 269 + .downcast_ref::<T>() 270 + .unwrap_or_else(|| panic!("Params type mismatch: got {}", std::any::type_name::<T>())) 271 + } 272 + 273 + pub fn props_ref<T: 'static>(&self) -> &T { 274 + self.props 275 + .downcast_ref::<T>() 276 + .unwrap_or_else(|| panic!("Props type mismatch: got {}", std::any::type_name::<T>())) 277 + } 278 + } 279 + 280 + /// Allows to access the content source in the [`Page::pages`] method. 281 + /// 282 + /// ## Example 283 + /// ```rs 284 + /// use maudit::route::prelude::*; 285 + /// # use maudit::content::markdown_entry; 286 + /// # 287 + /// # #[markdown_entry] 288 + /// # pub struct ArticleContent { 289 + /// # pub title: String, 290 + /// # pub description: String, 291 + /// # } 292 + /// 293 + /// #[route("/articles/[article]")] 294 + /// pub struct Article; 295 + /// 296 + /// #[derive(Params)] 297 + /// pub struct ArticleParams { 298 + /// pub article: String, 299 + /// } 300 + /// 301 + /// impl Route<ArticleParams> for Article { 302 + /// fn render(&self, ctx: &mut PageContext) -> RenderResult { 303 + /// let params = ctx.params::<ArticleParams>(); 304 + /// let articles = ctx.content.get_source::<ArticleContent>("articles"); 305 + /// let article = articles.get_entry(&params.article); 306 + /// article.render().into() 307 + /// } 308 + /// 309 + /// fn pages(&self, ctx: &mut DynamicRouteContext) -> Vec<ArticleParams> { 310 + /// let articles = ctx.content.get_source::<ArticleContent>("articles"); 311 + /// 312 + /// articles.into_params(|entry| ArticleParams { 313 + /// article: entry.id.clone(), 314 + /// }) 315 + /// } 316 + /// } 317 + /// ``` 318 + pub struct DynamicRouteContext<'a> { 319 + pub content: &'a RouteContent<'a>, 320 + pub assets: &'a mut RouteAssets, 321 + } 322 + 323 + /// Must be implemented for every page of your website. 324 + /// 325 + /// The page struct implementing this trait can be passed to [`coronate()`](crate::coronate), through the [`routes!`](crate::routes) macro, to be built. 326 + /// 327 + /// ## Example 328 + /// ```rs 329 + /// use maudit::route::prelude::*; 330 + /// 331 + /// #[route("/")] 332 + /// pub struct Index; 333 + /// 334 + /// impl Route for Index { 335 + /// fn render(&self, ctx: &mut PageContext) -> RenderResult { 336 + /// "<h1>Hello, world!</h1>".into() 337 + /// } 338 + /// } 339 + /// ``` 340 + pub trait Route<Params = PageParams, Props = (), T = RenderResult> 341 + where 342 + Params: Into<PageParams>, 343 + Props: 'static, 344 + T: Into<RenderResult>, 345 + { 346 + fn pages(&self, _ctx: &mut DynamicRouteContext) -> Pages<Params, Props> { 347 + Vec::new() 348 + } 349 + fn render(&self, ctx: &mut PageContext) -> T; 350 + } 351 + 352 + /// Raw representation of the parameters passed to a page. 353 + /// 354 + /// Can be accessed through [`PageContext`]'s `raw_params`. 355 + #[derive(Clone, Default, Debug)] 356 + pub struct PageParams(pub FxHashMap<String, String>); 357 + 358 + impl PageParams { 359 + pub fn from_vec<T>(params: Vec<T>) -> Vec<PageParams> 360 + where 361 + T: Into<PageParams>, 362 + { 363 + params.into_iter().map(|p| p.into()).collect() 364 + } 365 + } 366 + 367 + impl From<&PageParams> for PageParams { 368 + fn from(params: &PageParams) -> Self { 369 + params.clone() 370 + } 371 + } 372 + 373 + impl<T> FromIterator<T> for PageParams 374 + where 375 + T: Into<PageParams>, 376 + { 377 + fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self { 378 + let mut map = FxHashMap::default(); 379 + for item in iter { 380 + let item = item.into(); 381 + map.extend(item.0); 382 + } 383 + PageParams(map) 384 + } 385 + } 386 + 387 + #[derive(PartialEq, Eq, Debug)] 388 + /// Used internally by Maudit and should not be implemented by the user. 389 + /// We expose it because [`maudit_macros::route`] implements it for the user behind the scenes. 390 + pub enum RouteType { 391 + Static, 392 + Dynamic, 393 + } 394 + 395 + #[doc(hidden)] 396 + /// Used internally by Maudit and should not be implemented by the user. 397 + /// We expose it because the derive macro implements it for the user behind the scenes. 398 + pub trait InternalRoute { 399 + fn route_raw(&self) -> String; 400 + fn is_endpoint(&self) -> bool { 401 + guess_if_route_is_endpoint(&self.route_raw()) 402 + } 403 + fn route_type(&self) -> RouteType { 404 + let params_def = extract_params_from_raw_route(&self.route_raw()); 405 + 406 + get_route_type_from_route_params(&params_def) 407 + } 408 + 409 + fn url(&self, params: &PageParams) -> String { 410 + let mut params_def = extract_params_from_raw_route(&self.route_raw()); 411 + 412 + // Replace every param_def with the value from the params hashmap for said key 413 + // So, ex: "/articles/[article]" (params: Hashmap {article: "truc"}) -> "/articles/truc" 414 + let mut route = self.route_raw(); 415 + 416 + // Sort params by index in reverse order to avoid index shifting issues 417 + params_def.sort_by(|a, b| b.index.cmp(&a.index)); 418 + 419 + for param_def in params_def { 420 + let value = params.0.get(&param_def.key); 421 + 422 + match value { 423 + Some(value) => { 424 + route.replace_range(param_def.index..param_def.index + param_def.length, value); 425 + } 426 + None => { 427 + panic!( 428 + "Route {:?} is missing parameter {:?}", 429 + self.route_raw(), 430 + param_def.key 431 + ); 432 + } 433 + } 434 + } 435 + 436 + route 437 + } 438 + 439 + fn file_path(&self, params: &PageParams, output_dir: &Path) -> PathBuf { 440 + let mut params_def = extract_params_from_raw_route(&self.route_raw()); 441 + let mut route = self.route_raw(); 442 + 443 + // Sort params by index in reverse order to avoid index shifting issues 444 + params_def.sort_by(|a, b| b.index.cmp(&a.index)); 445 + 446 + for param_def in params_def { 447 + let value = params.0.get(&param_def.key); 40 448 41 - start = end_pos + 1; 42 - } else { 43 - break; 449 + match value { 450 + Some(value) => { 451 + route.replace_range(param_def.index..param_def.index + param_def.length, value); 452 + } 453 + None => { 454 + panic!( 455 + "Route {:?} is missing parameter {:?}", 456 + self.route_raw(), 457 + param_def.key 458 + ); 459 + } 460 + } 44 461 } 462 + 463 + let cleaned_raw_route = route.trim_start_matches('/').to_string(); 464 + 465 + output_dir.join(match self.is_endpoint() { 466 + true => cleaned_raw_route, 467 + false => match cleaned_raw_route.is_empty() { 468 + true => "index.html".into(), 469 + false => format!("{}/index.html", cleaned_raw_route), 470 + }, 471 + }) 45 472 } 473 + } 46 474 47 - params 475 + /// Extension trait providing generic convenience methods on an instance of a route 476 + pub trait RouteExt<Params = PageParams, Props = (), T = RenderResult>: 477 + Route<Params, Props, T> + InternalRoute 478 + where 479 + Params: Into<PageParams>, 480 + Props: 'static, 481 + T: Into<RenderResult>, 482 + { 483 + /// Get the URL for this page with the given parameters 484 + /// 485 + /// Note that this method merely generates the URL based on the route pattern and parameters, it does not verify if a corresponding route actually exists. 486 + fn url(&self, params: Params) -> String { 487 + InternalRoute::url(self, &params.into()) 488 + } 489 + } 490 + 491 + // Blanket implementation for all Page implementors that also implement InternalPage 492 + impl<U, Params, Props, T> RouteExt<Params, Props, T> for U 493 + where 494 + U: Route<Params, Props, T> + InternalRoute, 495 + Params: Into<PageParams>, 496 + Props: 'static, 497 + T: Into<RenderResult>, 498 + { 48 499 } 49 500 50 - pub fn get_route_type_from_route_params(params_def: &[ParameterDef]) -> RouteType { 51 - if params_def.is_empty() { 52 - RouteType::Static 53 - } else { 54 - RouteType::Dynamic 501 + /// Used internally by Maudit and should not be implemented by the user. 502 + /// We expose it because [`maudit_macros::route`] implements it for the user behind the scenes. 503 + pub trait FullRoute: InternalRoute + Sync + Send { 504 + #[doc(hidden)] 505 + fn render_internal(&self, ctx: &mut PageContext) -> RenderResult; 506 + #[doc(hidden)] 507 + fn pages_internal(&self, context: &mut DynamicRouteContext) -> PagesResults; 508 + 509 + fn get_pages(&self, context: &mut DynamicRouteContext) -> PagesResults { 510 + self.pages_internal(context) 511 + } 512 + 513 + fn build(&self, ctx: &mut PageContext) -> Result<Vec<u8>, Box<dyn std::error::Error>> { 514 + let result = self.render_internal(ctx); 515 + let bytes = finish_route(result, ctx.assets, self.route_raw())?; 516 + 517 + Ok(bytes) 55 518 } 56 519 } 57 520 58 - pub fn guess_if_route_is_endpoint(raw_route: &str) -> bool { 59 - let real_path = Path::new(&raw_route); 521 + pub type PagesResult = (PageParams, PageProps, PageTypedParams); 522 + pub type PagesResults = Vec<PagesResult>; 523 + 524 + pub type PageProps = Box<dyn Any + Send + Sync>; 525 + pub type PageTypedParams = Box<dyn Any + Send + Sync>; 60 526 61 - real_path.extension().is_some() 527 + pub mod prelude { 528 + //! Re-exports of the most commonly used types and traits for defining routes. 529 + //! 530 + //! This module is meant to be glob imported in your routes files. 531 + //! 532 + //! ## Example 533 + //! ```rs 534 + //! use maudit::route::prelude::*; 535 + //! ``` 536 + pub use super::{ 537 + DynamicRouteContext, FullRoute, Page, PageContext, PageParams, Pages, PaginationPage, 538 + RenderResult, Route, RouteExt, paginate, 539 + }; 540 + pub use crate::assets::{Asset, Image, Style, StyleOptions}; 541 + pub use crate::content::MarkdownContent; 542 + pub use maudit_macros::{Params, route}; 62 543 } 63 544 64 545 #[cfg(test)] 65 546 mod tests { 66 - use crate::{ 67 - page::RouteType, 68 - route::{ParameterDef, extract_params_from_raw_route, get_route_type_from_route_params}, 69 - }; 547 + use super::*; 548 + use rustc_hash::FxHashMap; 549 + use std::path::Path; 550 + 551 + // Test struct implementing InternalPage for testing 552 + struct TestPage { 553 + route: String, 554 + } 555 + 556 + impl InternalRoute for TestPage { 557 + fn route_raw(&self) -> String { 558 + self.route.clone() 559 + } 560 + } 561 + 562 + #[test] 563 + fn test_url_single_parameter() { 564 + let page = TestPage { 565 + route: "/articles/[slug]".to_string(), 566 + }; 567 + 568 + let mut params = FxHashMap::default(); 569 + params.insert("slug".to_string(), "hello-world".to_string()); 570 + let route_params = PageParams(params); 571 + 572 + assert_eq!(page.url(&route_params), "/articles/hello-world"); 573 + } 70 574 71 575 #[test] 72 - fn test_extract_params() { 73 - let input = "/articles/[article]"; 74 - let expected = vec![ParameterDef { 75 - key: "article".to_string(), 76 - index: 10, 77 - length: 9, 78 - }]; 576 + fn test_url_multiple_parameters() { 577 + let page = TestPage { 578 + route: "/articles/tags/[tag]/[page]".to_string(), 579 + }; 79 580 80 - assert_eq!(extract_params_from_raw_route(input), expected); 581 + let mut params = FxHashMap::default(); 582 + params.insert("tag".to_string(), "rust".to_string()); 583 + params.insert("page".to_string(), "2".to_string()); 584 + let route_params = PageParams(params); 585 + 586 + assert_eq!(page.url(&route_params), "/articles/tags/rust/2"); 81 587 } 82 588 83 589 #[test] 84 - fn test_extract_params_multiple() { 85 - let input = "/articles/[article]/[id]"; 86 - let expected = vec![ 87 - ParameterDef { 88 - key: "article".to_string(), 89 - index: 10, 90 - length: 9, 91 - }, 92 - ParameterDef { 93 - key: "id".to_string(), 94 - index: 20, 95 - length: 4, 96 - }, 97 - ]; 590 + fn test_url_multiple_parameters_different_lengths() { 591 + // This specifically tests the bug we fixed where parameter replacement 592 + // would create invalid indices for subsequent parameters 593 + let page = TestPage { 594 + route: "/articles/tags/[tag]/[page]".to_string(), 595 + }; 596 + 597 + let mut params = FxHashMap::default(); 598 + params.insert("tag".to_string(), "development-experience".to_string()); // Long replacement 599 + params.insert("page".to_string(), "1".to_string()); // Short replacement 600 + let route_params = PageParams(params); 98 601 99 - assert_eq!(extract_params_from_raw_route(input), expected); 602 + assert_eq!( 603 + page.url(&route_params), 604 + "/articles/tags/development-experience/1" 605 + ); 100 606 } 101 607 102 608 #[test] 103 - fn test_extract_params_no_params() { 104 - let input = "/articles"; 105 - let expected: Vec<ParameterDef> = Vec::new(); 609 + fn test_url_no_parameters() { 610 + let page = TestPage { 611 + route: "/about".to_string(), 612 + }; 613 + 614 + let route_params = PageParams(FxHashMap::default()); 615 + 616 + assert_eq!(page.url(&route_params), "/about"); 617 + } 618 + 619 + #[test] 620 + fn test_url_parameter_at_start() { 621 + let page = TestPage { 622 + route: "/[lang]/about".to_string(), 623 + }; 106 624 107 - assert_eq!(extract_params_from_raw_route(input), expected); 625 + let mut params = FxHashMap::default(); 626 + params.insert("lang".to_string(), "en".to_string()); 627 + let route_params = PageParams(params); 628 + 629 + assert_eq!(page.url(&route_params), "/en/about"); 108 630 } 109 631 110 632 #[test] 111 - fn test_extract_params_escaped() { 112 - let input = "/articles/\\[article\\]"; 113 - let expected: Vec<ParameterDef> = Vec::new(); 633 + fn test_url_parameter_at_end() { 634 + let page = TestPage { 635 + route: "/api/users/[id]".to_string(), 636 + }; 637 + 638 + let mut params = FxHashMap::default(); 639 + params.insert("id".to_string(), "123".to_string()); 640 + let route_params = PageParams(params); 641 + 642 + assert_eq!(page.url(&route_params), "/api/users/123"); 643 + } 644 + 645 + #[test] 646 + fn test_file_path_single_parameter_non_endpoint() { 647 + let page = TestPage { 648 + route: "/articles/[slug]".to_string(), 649 + }; 650 + 651 + let mut params = FxHashMap::default(); 652 + params.insert("slug".to_string(), "hello-world".to_string()); 653 + let route_params = PageParams(params); 654 + 655 + let output_dir = Path::new("/dist"); 656 + let expected = Path::new("/dist/articles/hello-world/index.html"); 657 + 658 + assert_eq!(page.file_path(&route_params, output_dir), expected); 659 + } 660 + 661 + #[test] 662 + fn test_file_path_multiple_parameters_non_endpoint() { 663 + let page = TestPage { 664 + route: "/articles/tags/[tag]/[page]".to_string(), 665 + }; 666 + 667 + let mut params = FxHashMap::default(); 668 + params.insert("tag".to_string(), "rust".to_string()); 669 + params.insert("page".to_string(), "2".to_string()); 670 + let route_params = PageParams(params); 671 + 672 + let output_dir = Path::new("/dist"); 673 + let expected = Path::new("/dist/articles/tags/rust/2/index.html"); 114 674 115 - assert_eq!(extract_params_from_raw_route(input), expected); 675 + assert_eq!(page.file_path(&route_params, output_dir), expected); 116 676 } 117 677 118 678 #[test] 119 - fn test_extract_params_escaped_brackets() { 120 - let input = "/articles/\\[article\\]/\\[id\\]"; 121 - let expected: Vec<ParameterDef> = Vec::new(); 679 + fn test_file_path_root_route() { 680 + let page = TestPage { 681 + route: "/".to_string(), 682 + }; 122 683 123 - assert_eq!(extract_params_from_raw_route(input), expected); 684 + let route_params = PageParams(FxHashMap::default()); 685 + let output_dir = Path::new("/dist"); 686 + let expected = Path::new("/dist/index.html"); 687 + 688 + assert_eq!(page.file_path(&route_params, output_dir), expected); 124 689 } 125 690 126 691 #[test] 127 - fn test_extract_params_escaped_brackets_with_params() { 128 - let input = "/articles/\\[article\\]/[id]"; 129 - let expected = vec![ParameterDef { 130 - key: "id".to_string(), 131 - index: 22, 132 - length: 4, 133 - }]; 692 + fn test_file_path_endpoint() { 693 + let page = TestPage { 694 + route: "/api/data.json".to_string(), 695 + }; 696 + 697 + let route_params = PageParams(FxHashMap::default()); 698 + let output_dir = Path::new("/dist"); 699 + let expected = Path::new("/dist/api/data.json"); 134 700 135 - assert_eq!(extract_params_from_raw_route(input), expected); 701 + assert_eq!(page.file_path(&route_params, output_dir), expected); 136 702 } 137 703 138 704 #[test] 139 - fn test_route_type_static() { 140 - let input = "/articles"; 141 - let params_def = extract_params_from_raw_route(input); 142 - assert_eq!( 143 - get_route_type_from_route_params(&params_def), 144 - RouteType::Static 145 - ); 705 + #[should_panic(expected = "Route \"/articles/[slug]\" is missing parameter \"slug\"")] 706 + fn test_url_missing_parameter_panics() { 707 + let page = TestPage { 708 + route: "/articles/[slug]".to_string(), 709 + }; 710 + 711 + let route_params = PageParams(FxHashMap::default()); 712 + 713 + // This should panic because we're missing the "slug" parameter 714 + page.url(&route_params); 146 715 } 147 716 148 717 #[test] 149 - fn test_route_type_dynamic() { 150 - let input = "/articles/[article]"; 151 - let params_def = extract_params_from_raw_route(input); 152 - assert_eq!( 153 - get_route_type_from_route_params(&params_def), 154 - RouteType::Dynamic 155 - ); 718 + #[should_panic(expected = "Route \"/articles/tags/[tag]/[page]\" is missing parameter \"tag\"")] 719 + fn test_file_path_missing_parameter_panics() { 720 + let page = TestPage { 721 + route: "/articles/tags/[tag]/[page]".to_string(), 722 + }; 723 + 724 + let mut params = FxHashMap::default(); 725 + params.insert("page".to_string(), "1".to_string()); 726 + let route_params = PageParams(params); 727 + 728 + let output_dir = Path::new("/dist"); 729 + 730 + // This should panic because we're missing the "tag" parameter 731 + page.file_path(&route_params, output_dir); 156 732 } 157 733 158 734 #[test] 159 - fn test_route_type_dynamic_multiple() { 160 - let input = "/articles/[article]/[id]"; 161 - let params_def = extract_params_from_raw_route(input); 162 - assert_eq!( 163 - get_route_type_from_route_params(&params_def), 164 - RouteType::Dynamic 165 - ); 735 + fn test_pagination_page_with_entries() { 736 + // Create some mock content entries 737 + use crate::content::ContentEntry; 738 + use std::path::PathBuf; 739 + 740 + let entries = vec![ 741 + ContentEntry::new( 742 + "entry1".to_string(), 743 + None, 744 + Some("content1".to_string()), 745 + (), 746 + Some(PathBuf::from("file1.md")), 747 + ), 748 + ContentEntry::new( 749 + "entry2".to_string(), 750 + None, 751 + Some("content2".to_string()), 752 + (), 753 + Some(PathBuf::from("file2.md")), 754 + ), 755 + ContentEntry::new( 756 + "entry3".to_string(), 757 + None, 758 + Some("content3".to_string()), 759 + (), 760 + Some(PathBuf::from("file3.md")), 761 + ), 762 + ContentEntry::new( 763 + "entry4".to_string(), 764 + None, 765 + Some("content4".to_string()), 766 + (), 767 + Some(PathBuf::from("file4.md")), 768 + ), 769 + ContentEntry::new( 770 + "entry5".to_string(), 771 + None, 772 + Some("content5".to_string()), 773 + (), 774 + Some(PathBuf::from("file5.md")), 775 + ), 776 + ]; 777 + 778 + let pagination = PaginationPage::new(1, 2, 5, &entries); 779 + 780 + assert_eq!(pagination.page, 1); 781 + assert_eq!(pagination.per_page, 2); 782 + assert_eq!(pagination.total_items, 5); 783 + assert_eq!(pagination.total_pages, 3); 784 + assert!(pagination.has_next); 785 + assert!(pagination.has_prev); 786 + assert_eq!(pagination.start_index, 2); 787 + assert_eq!(pagination.end_index, 4); 788 + assert_eq!(pagination.items.len(), 2); 789 + assert_eq!(pagination.items[0].id, "entry3"); 790 + assert_eq!(pagination.items[1].id, "entry4"); 166 791 } 167 792 168 793 #[test] 169 - fn test_route_type_dynamic_escaped() { 170 - let input = "/articles/\\[article\\]"; 171 - let params_def = extract_params_from_raw_route(input); 172 - assert_eq!( 173 - get_route_type_from_route_params(&params_def), 174 - RouteType::Static 175 - ); 794 + fn test_paginate_content_function() { 795 + use crate::content::ContentEntry; 796 + use std::path::PathBuf; 797 + 798 + let entries = vec![ 799 + ContentEntry::new( 800 + "entry1".to_string(), 801 + None, 802 + Some("content1".to_string()), 803 + (), 804 + Some(PathBuf::from("file1.md")), 805 + ), 806 + ContentEntry::new( 807 + "entry2".to_string(), 808 + None, 809 + Some("content2".to_string()), 810 + (), 811 + Some(PathBuf::from("file2.md")), 812 + ), 813 + ContentEntry::new( 814 + "entry3".to_string(), 815 + None, 816 + Some("content3".to_string()), 817 + (), 818 + Some(PathBuf::from("file3.md")), 819 + ), 820 + ]; 821 + 822 + let routes = paginate(&entries, 2, |page| { 823 + let mut params = FxHashMap::default(); 824 + params.insert("page".to_string(), page.to_string()); 825 + PageParams(params) 826 + }); 827 + 828 + assert_eq!(routes.len(), 2); 829 + 830 + // First page 831 + assert_eq!(routes[0].props.page, 0); 832 + assert_eq!(routes[0].props.items.len(), 2); 833 + assert_eq!(routes[0].props.items[0].id, "entry1"); 834 + assert_eq!(routes[0].props.items[1].id, "entry2"); 835 + 836 + // Second page 837 + assert_eq!(routes[1].props.page, 1); 838 + assert_eq!(routes[1].props.items.len(), 1); 839 + assert_eq!(routes[1].props.items[0].id, "entry3"); 176 840 } 177 841 178 842 #[test] 179 - fn test_route_type_dynamic_mixed_escaped_brackets() { 180 - let input = "/articles/\\[article\\]/[id]"; 181 - let params_def = extract_params_from_raw_route(input); 182 - assert_eq!( 183 - get_route_type_from_route_params(&params_def), 184 - RouteType::Dynamic 185 - ); 843 + fn test_paginate_generic_function() { 844 + // Test with simple strings 845 + let tags = vec!["rust", "javascript", "python", "go", "typescript"]; 846 + 847 + let routes = paginate(&tags, 2, |page| { 848 + let mut params = FxHashMap::default(); 849 + params.insert("page".to_string(), page.to_string()); 850 + PageParams(params) 851 + }); 852 + 853 + assert_eq!(routes.len(), 3); 854 + 855 + // First page 856 + assert_eq!(routes[0].props.page, 0); 857 + assert_eq!(routes[0].props.items.len(), 2); 858 + assert_eq!(routes[0].props.items[0], "rust"); 859 + assert_eq!(routes[0].props.items[1], "javascript"); 860 + 861 + // Second page 862 + assert_eq!(routes[1].props.page, 1); 863 + assert_eq!(routes[1].props.items.len(), 2); 864 + assert_eq!(routes[1].props.items[0], "python"); 865 + assert_eq!(routes[1].props.items[1], "go"); 866 + 867 + // Third page 868 + assert_eq!(routes[2].props.page, 2); 869 + assert_eq!(routes[2].props.items.len(), 1); 870 + assert_eq!(routes[2].props.items[0], "typescript"); 186 871 } 187 872 }
+187
crates/maudit/src/routing.rs
··· 1 + use std::path::Path; 2 + 3 + use crate::route::RouteType; 4 + 5 + #[derive(Debug, PartialEq)] 6 + pub struct ParameterDef { 7 + pub(crate) key: String, 8 + pub(crate) index: usize, 9 + pub(crate) length: usize, 10 + } 11 + 12 + pub fn extract_params_from_raw_route(raw_route: &str) -> Vec<ParameterDef> { 13 + let mut params = Vec::new(); 14 + let mut start = 0; 15 + 16 + while let Some(bracket_pos) = raw_route[start..].find('[') { 17 + let abs_pos = start + bracket_pos; 18 + 19 + // Check if escaped by counting preceding backslashes 20 + let backslash_count = raw_route[..abs_pos] 21 + .chars() 22 + .rev() 23 + .take_while(|&c| c == '\\') 24 + .count(); 25 + 26 + if backslash_count % 2 == 1 { 27 + start = abs_pos + 1; 28 + continue; 29 + } 30 + 31 + if let Some(end_bracket) = raw_route[abs_pos + 1..].find(']') { 32 + let end_pos = abs_pos + 1 + end_bracket; 33 + let key = raw_route[abs_pos + 1..end_pos].to_string(); 34 + 35 + params.push(ParameterDef { 36 + key, 37 + index: abs_pos, 38 + length: end_pos - abs_pos + 1, 39 + }); 40 + 41 + start = end_pos + 1; 42 + } else { 43 + break; 44 + } 45 + } 46 + 47 + params 48 + } 49 + 50 + pub fn get_route_type_from_route_params(params_def: &[ParameterDef]) -> RouteType { 51 + if params_def.is_empty() { 52 + RouteType::Static 53 + } else { 54 + RouteType::Dynamic 55 + } 56 + } 57 + 58 + pub fn guess_if_route_is_endpoint(raw_route: &str) -> bool { 59 + let real_path = Path::new(&raw_route); 60 + 61 + real_path.extension().is_some() 62 + } 63 + 64 + #[cfg(test)] 65 + mod tests { 66 + use crate::{ 67 + route::RouteType, 68 + routing::{ParameterDef, extract_params_from_raw_route, get_route_type_from_route_params}, 69 + }; 70 + 71 + #[test] 72 + fn test_extract_params() { 73 + let input = "/articles/[article]"; 74 + let expected = vec![ParameterDef { 75 + key: "article".to_string(), 76 + index: 10, 77 + length: 9, 78 + }]; 79 + 80 + assert_eq!(extract_params_from_raw_route(input), expected); 81 + } 82 + 83 + #[test] 84 + fn test_extract_params_multiple() { 85 + let input = "/articles/[article]/[id]"; 86 + let expected = vec![ 87 + ParameterDef { 88 + key: "article".to_string(), 89 + index: 10, 90 + length: 9, 91 + }, 92 + ParameterDef { 93 + key: "id".to_string(), 94 + index: 20, 95 + length: 4, 96 + }, 97 + ]; 98 + 99 + assert_eq!(extract_params_from_raw_route(input), expected); 100 + } 101 + 102 + #[test] 103 + fn test_extract_params_no_params() { 104 + let input = "/articles"; 105 + let expected: Vec<ParameterDef> = Vec::new(); 106 + 107 + assert_eq!(extract_params_from_raw_route(input), expected); 108 + } 109 + 110 + #[test] 111 + fn test_extract_params_escaped() { 112 + let input = "/articles/\\[article\\]"; 113 + let expected: Vec<ParameterDef> = Vec::new(); 114 + 115 + assert_eq!(extract_params_from_raw_route(input), expected); 116 + } 117 + 118 + #[test] 119 + fn test_extract_params_escaped_brackets() { 120 + let input = "/articles/\\[article\\]/\\[id\\]"; 121 + let expected: Vec<ParameterDef> = Vec::new(); 122 + 123 + assert_eq!(extract_params_from_raw_route(input), expected); 124 + } 125 + 126 + #[test] 127 + fn test_extract_params_escaped_brackets_with_params() { 128 + let input = "/articles/\\[article\\]/[id]"; 129 + let expected = vec![ParameterDef { 130 + key: "id".to_string(), 131 + index: 22, 132 + length: 4, 133 + }]; 134 + 135 + assert_eq!(extract_params_from_raw_route(input), expected); 136 + } 137 + 138 + #[test] 139 + fn test_route_type_static() { 140 + let input = "/articles"; 141 + let params_def = extract_params_from_raw_route(input); 142 + assert_eq!( 143 + get_route_type_from_route_params(&params_def), 144 + RouteType::Static 145 + ); 146 + } 147 + 148 + #[test] 149 + fn test_route_type_dynamic() { 150 + let input = "/articles/[article]"; 151 + let params_def = extract_params_from_raw_route(input); 152 + assert_eq!( 153 + get_route_type_from_route_params(&params_def), 154 + RouteType::Dynamic 155 + ); 156 + } 157 + 158 + #[test] 159 + fn test_route_type_dynamic_multiple() { 160 + let input = "/articles/[article]/[id]"; 161 + let params_def = extract_params_from_raw_route(input); 162 + assert_eq!( 163 + get_route_type_from_route_params(&params_def), 164 + RouteType::Dynamic 165 + ); 166 + } 167 + 168 + #[test] 169 + fn test_route_type_dynamic_escaped() { 170 + let input = "/articles/\\[article\\]"; 171 + let params_def = extract_params_from_raw_route(input); 172 + assert_eq!( 173 + get_route_type_from_route_params(&params_def), 174 + RouteType::Static 175 + ); 176 + } 177 + 178 + #[test] 179 + fn test_route_type_dynamic_mixed_escaped_brackets() { 180 + let input = "/articles/\\[article\\]/[id]"; 181 + let params_def = extract_params_from_raw_route(input); 182 + assert_eq!( 183 + get_route_type_from_route_params(&params_def), 184 + RouteType::Dynamic 185 + ); 186 + } 187 + }
+7
crates/maudit/src/templating/maud_ext.rs
··· 3 3 use crate::{ 4 4 GENERATOR, 5 5 assets::{Asset, Image, Script, Style}, 6 + route::RenderResult, 6 7 }; 7 8 8 9 impl Render for Style { ··· 36 37 meta name="generator" content=(GENERATOR); 37 38 } 38 39 } 40 + 41 + impl From<maud::Markup> for RenderResult { 42 + fn from(val: maud::Markup) -> Self { 43 + RenderResult::Text(val.into_string()) 44 + } 45 + }
+9 -9
crates/oubli/src/archetypes/blog.rs
··· 3 3 use crate::layouts::layout; 4 4 use maud::{html, Markup}; 5 5 use maudit::content::markdown_entry; 6 - use maudit::page::prelude::*; 7 - use maudit::page::FullPage; 6 + use maudit::route::prelude::*; 7 + use maudit::route::FullRoute; 8 8 9 - pub fn blog_index_content<T: FullPage>( 10 - route: impl FullPage, 11 - ctx: &mut RouteContext, 9 + pub fn blog_index_content<T: FullRoute>( 10 + route: impl FullRoute, 11 + ctx: &mut PageContext, 12 12 name: &str, 13 13 stringified_ident: &str, 14 14 ) -> Markup { ··· 43 43 pub entry: String, 44 44 } 45 45 46 - pub fn blog_entry_routes(ctx: &DynamicRouteContext, name: &str) -> Routes<BlogEntryParams> { 46 + pub fn blog_entry_routes(ctx: &mut DynamicRouteContext, name: &str) -> Pages<BlogEntryParams> { 47 47 let blog_entries = ctx.content.get_source::<BlogEntryContent>(name); 48 48 49 - blog_entries.into_routes(|entry| { 50 - Route::from_params(BlogEntryParams { 49 + blog_entries.into_pages(|entry| { 50 + Page::from_params(BlogEntryParams { 51 51 entry: entry.id.clone(), 52 52 }) 53 53 }) 54 54 } 55 55 56 - pub fn blog_entry_render(ctx: &mut RouteContext, name: &str, stringified_ident: &str) -> Markup { 56 + pub fn blog_entry_render(ctx: &mut PageContext, name: &str, stringified_ident: &str) -> Markup { 57 57 let params = ctx.params::<BlogEntryParams>(); 58 58 let blog_entries = ctx 59 59 .content
+12 -12
crates/oubli/src/lib.rs
··· 1 1 use maudit::content::ContentEntry; 2 - use maudit::page::prelude::*; 2 + use maudit::route::prelude::*; 3 3 4 4 use maudit::{ 5 5 content::{ContentSourceInternal, ContentSources}, 6 6 coronate, 7 - page::{prelude::Params, FullPage}, 7 + route::{prelude::Params, FullRoute}, 8 8 }; 9 9 10 10 // Re-expose Maudit's public API. ··· 65 65 ); 66 66 // Generate the pages 67 67 mod $ident { 68 - use maudit::page::prelude::*; 68 + use maudit::route::prelude::*; 69 69 use oubli::archetypes::blog::*; 70 70 71 71 #[route(stringify!($ident))] 72 72 pub struct Index; 73 - impl Page for Index { 74 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 73 + impl Route for Index { 74 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 75 75 blog_index_content::<Entry>(Entry, ctx, $name, stringify!($ident)).into() 76 76 } 77 77 } 78 78 79 79 #[route(concat!(stringify!($ident), "/[entry]"))] 80 80 pub struct Entry; 81 - impl Page<BlogEntryParams> for Entry { 82 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 81 + impl Route<BlogEntryParams> for Entry { 82 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 83 83 blog_entry_render(ctx, $name, stringify!($ident)).into() 84 84 } 85 85 86 - fn routes(&self, ctx: &DynamicRouteContext) -> Routes<BlogEntryParams> { 86 + fn pages(&self, ctx: &mut DynamicRouteContext) -> Pages<BlogEntryParams> { 87 87 blog_entry_routes(ctx, stringify!($ident)) 88 88 } 89 89 } 90 90 } 91 - ($name, stringify!($ident), vec![&$ident::Index as &dyn maudit::page::FullPage, &$ident::Entry as &dyn maudit::page::FullPage], Box::new(content_source) as Box<dyn maudit::content::ContentSourceInternal>) 91 + ($name, stringify!($ident), vec![&$ident::Index as &dyn maudit::route::FullRoute, &$ident::Entry as &dyn maudit::route::FullRoute], Box::new(content_source) as Box<dyn maudit::content::ContentSourceInternal>) 92 92 }, 93 93 oubli::Archetype::MarkdownDoc => { 94 94 todo!(); ··· 127 127 archetypes: Vec<( 128 128 &str, 129 129 &str, 130 - Vec<&dyn FullPage>, 130 + Vec<&dyn FullRoute>, 131 131 Box<dyn ContentSourceInternal>, 132 132 )>, 133 - routes: &[&dyn FullPage], 133 + routes: &[&dyn FullRoute], 134 134 mut content_sources: ContentSources, 135 135 options: BuildOptions, 136 136 ) -> Result<BuildOutput, Box<dyn std::error::Error>> { ··· 194 194 type ArchetypeTuple<'a> = ( 195 195 &'a str, 196 196 &'a str, 197 - Vec<&'a dyn FullPage>, 197 + Vec<&'a dyn FullRoute>, 198 198 Box<dyn ContentSourceInternal>, 199 199 );
+2 -2
examples/basics/src/main.rs
··· 2 2 3 3 use maudit::{coronate, routes, BuildOptions, BuildOutput}; 4 4 5 - mod pages { 5 + mod routes { 6 6 mod index; 7 7 pub use index::Index; 8 8 } 9 9 10 - pub use pages::Index; 10 + pub use routes::Index; 11 11 12 12 fn main() -> Result<BuildOutput, Box<dyn std::error::Error>> { 13 13 coronate(routes![Index], vec![].into(), BuildOptions::default())
+3 -3
examples/basics/src/pages/index.rs examples/basics/src/routes/index.rs
··· 1 1 use crate::layout::layout; 2 2 use maud::html; 3 - use maudit::page::prelude::*; 3 + use maudit::route::prelude::*; 4 4 5 5 #[route("/")] 6 6 pub struct Index; 7 7 8 - impl Page for Index { 9 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 8 + impl Route for Index { 9 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 10 10 let logo = ctx.assets.add_image("images/logo.svg"); 11 11 12 12 layout(html! {
+2 -2
examples/blog/src/main.rs
··· 5 5 content::glob_markdown, content_sources, coronate, routes, BuildOptions, BuildOutput, 6 6 }; 7 7 8 - mod pages { 8 + mod routes { 9 9 mod article; 10 10 mod index; 11 11 pub use article::Article; ··· 14 14 15 15 fn main() -> Result<BuildOutput, Box<dyn std::error::Error>> { 16 16 coronate( 17 - routes![pages::Index, pages::Article], 17 + routes![routes::Index, routes::Article], 18 18 content_sources![ 19 19 "articles" => glob_markdown::<ArticleContent>("content/articles/*.md", None) 20 20 ],
+6 -6
examples/blog/src/pages/article.rs examples/blog/src/routes/article.rs
··· 1 - use maudit::page::prelude::*; 1 + use maudit::route::prelude::*; 2 2 3 3 use crate::{content::ArticleContent, layout::layout}; 4 4 ··· 10 10 pub article: String, 11 11 } 12 12 13 - impl Page<ArticleParams> for Article { 14 - fn routes(&self, ctx: &DynamicRouteContext) -> Routes<ArticleParams> { 13 + impl Route<ArticleParams> for Article { 14 + fn pages(&self, ctx: &mut DynamicRouteContext) -> Pages<ArticleParams> { 15 15 let articles = ctx.content.get_source::<ArticleContent>("articles"); 16 16 17 - articles.into_routes(|entry| { 18 - Route::from_params(ArticleParams { 17 + articles.into_pages(|entry| { 18 + Page::from_params(ArticleParams { 19 19 article: entry.id.clone(), 20 20 }) 21 21 }) 22 22 } 23 23 24 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 24 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 25 25 let params = ctx.params::<ArticleParams>(); 26 26 let articles = ctx.content.get_source::<ArticleContent>("articles"); 27 27 let article = articles.get_entry(&params.article);
+4 -4
examples/blog/src/pages/index.rs examples/blog/src/routes/index.rs
··· 1 1 use maud::html; 2 - use maudit::page::prelude::*; 2 + use maudit::route::prelude::*; 3 3 4 4 use crate::{ 5 5 content::ArticleContent, 6 6 layout::layout, 7 - pages::{article::ArticleParams, Article}, 7 + routes::{article::ArticleParams, Article}, 8 8 }; 9 9 10 10 #[route("/")] 11 11 pub struct Index; 12 12 13 - impl Page for Index { 14 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 13 + impl Route for Index { 14 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 15 15 let articles = ctx.content.get_source::<ArticleContent>("articles"); 16 16 17 17 let markup = html! {
+2 -2
examples/empty/src/main.rs
··· 1 1 use maudit::{content_sources, coronate, routes, BuildOptions, BuildOutput}; 2 2 3 - mod pages { 3 + mod routes { 4 4 mod index; 5 5 pub use index::Index; 6 6 } 7 7 8 8 fn main() -> Result<BuildOutput, Box<dyn std::error::Error>> { 9 9 coronate( 10 - routes![pages::Index], 10 + routes![routes::Index], 11 11 content_sources![], 12 12 BuildOptions::default(), 13 13 )
-10
examples/empty/src/pages/index.rs
··· 1 - use maudit::page::prelude::*; 2 - 3 - #[route("/")] 4 - pub struct Index; 5 - 6 - impl Page for Index { 7 - fn render(&self, _ctx: &mut RouteContext) -> RenderResult { 8 - "Hello, world!".into() 9 - } 10 - }
+10
examples/empty/src/routes/index.rs
··· 1 + use maudit::route::prelude::*; 2 + 3 + #[route("/")] 4 + pub struct Index; 5 + 6 + impl Route for Index { 7 + fn render(&self, _ctx: &mut PageContext) -> RenderResult { 8 + "Hello, world!".into() 9 + } 10 + }
+2 -2
examples/image-processing/src/main.rs
··· 2 2 3 3 use maudit::{coronate, routes, BuildOptions, BuildOutput}; 4 4 5 - mod pages { 5 + mod routes { 6 6 mod index; 7 7 pub use index::Index; 8 8 } 9 9 10 - pub use pages::Index; 10 + pub use routes::Index; 11 11 12 12 fn main() -> Result<BuildOutput, Box<dyn std::error::Error>> { 13 13 coronate(routes![Index], vec![].into(), BuildOptions::default())
+3 -3
examples/image-processing/src/pages/index.rs examples/image-processing/src/routes/index.rs
··· 1 1 use crate::layout::layout; 2 2 use maud::html; 3 - use maudit::{assets::ImageOptions, page::prelude::*}; 3 + use maudit::{assets::ImageOptions, route::prelude::*}; 4 4 5 5 #[route("/")] 6 6 pub struct Index; 7 7 8 - impl Page for Index { 9 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 8 + impl Route for Index { 9 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 10 10 let logo = ctx.assets.add_image("images/logo.svg"); 11 11 let walrus = ctx.assets.add_image_with_options( 12 12 "images/walrus.jpg",
+2 -2
examples/kitchen-sink/src/main.rs
··· 1 1 use maudit::{coronate, routes, AssetsOptions, BuildOptions, BuildOutput}; 2 2 3 - mod pages { 3 + mod routes { 4 4 mod dynamic; 5 5 mod endpoint; 6 6 mod index; ··· 11 11 12 12 fn main() -> Result<BuildOutput, Box<dyn std::error::Error>> { 13 13 coronate( 14 - routes![pages::Index, pages::DynamicExample, pages::Endpoint], 14 + routes![routes::Index, routes::DynamicExample, routes::Endpoint], 15 15 vec![].into(), 16 16 BuildOptions { 17 17 assets: AssetsOptions {
+5 -5
examples/kitchen-sink/src/pages/dynamic.rs examples/kitchen-sink/src/routes/dynamic.rs
··· 1 - use maudit::page::prelude::*; 1 + use maudit::route::prelude::*; 2 2 3 3 use maud::html; 4 4 ··· 10 10 pub page: u128, 11 11 } 12 12 13 - impl Page<Params> for DynamicExample { 14 - fn routes(&self, _: &DynamicRouteContext) -> Routes<Params> { 13 + impl Route<Params> for DynamicExample { 14 + fn pages(&self, _: &mut DynamicRouteContext) -> Pages<Params> { 15 15 (0..1) 16 - .map(|i| Route::from_params(Params { page: i })) 16 + .map(|i| Page::from_params(Params { page: i })) 17 17 .collect() 18 18 } 19 19 20 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 20 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 21 21 let params = ctx.params::<Params>(); 22 22 let image = ctx.assets.add_image("data/social-card.png"); 23 23 ctx.assets
+3 -3
examples/kitchen-sink/src/pages/endpoint.rs examples/kitchen-sink/src/routes/endpoint.rs
··· 1 - use maudit::page::prelude::*; 1 + use maudit::route::prelude::*; 2 2 3 3 #[route("/catalogue/data.json")] 4 4 pub struct Endpoint; 5 5 6 - impl Page for Endpoint { 7 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 6 + impl Route for Endpoint { 7 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 8 8 let image = ctx.assets.add_image("data/logo.svg"); 9 9 let some_script = ctx.assets.add_script("data/script.js"); 10 10 ctx.assets
+3 -3
examples/kitchen-sink/src/pages/index.rs examples/kitchen-sink/src/routes/index.rs
··· 1 - use maudit::page::prelude::*; 1 + use maudit::route::prelude::*; 2 2 3 3 use maud::html; 4 4 ··· 7 7 #[route("/")] 8 8 pub struct Index; 9 9 10 - impl Page for Index { 11 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 10 + impl Route for Index { 11 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 12 12 let image = ctx.assets.add_image("data/logo.svg"); 13 13 let script = ctx.assets.add_script("data/some_other_script.js"); 14 14 let style = ctx
+32 -35
examples/library/src/build.rs
··· 1 1 use std::fs; 2 2 3 3 use maudit::{ 4 - assets::PageAssets, 5 - content::{ContentSources, PageContent}, 6 - page::{DynamicRouteContext, FullPage, RouteContext, RouteParams, RouteType}, 4 + assets::RouteAssets, 5 + content::{ContentSources, RouteContent}, 6 + route::{DynamicRouteContext, FullRoute, PageContext, PageParams, RouteType}, 7 7 BuildOptions, 8 8 }; 9 9 10 10 pub fn build_website( 11 - routes: &[&dyn FullPage], 11 + routes: &[&dyn FullRoute], 12 12 mut content_sources: ContentSources, 13 13 options: BuildOptions, 14 14 ) -> Result<(), Box<dyn std::error::Error>> { 15 15 // Initialize all the content sources; 16 16 content_sources.init_all(); 17 17 18 - // Options we'll be passing to PageAssets instances. 18 + // Options we'll be passing to RouteAssets instances. 19 19 // This value automatically has the paths joined based on the output directory in BuildOptions for us, so we don't have to do it ourselves. 20 20 let page_assets_options = options.page_assets_options(); 21 21 ··· 26 26 match route.route_type() { 27 27 RouteType::Static => { 28 28 // Our page does not include content or assets, but we'll set those up for future use. 29 - let content = PageContent::new(&content_sources); 30 - let mut page_assets = PageAssets::new(&page_assets_options); 29 + let content = RouteContent::new(&content_sources); 30 + let mut page_assets = RouteAssets::new(&page_assets_options); 31 31 32 32 // Static and dynamic routes share the same interface for building, but static routes do not require any parameters. 33 - // As such, we can just pass an empty set of parameters (the default for RouteParams). 34 - let params = RouteParams::default(); 33 + // As such, we can just pass an empty set of parameters (the default for PageParams). 34 + let params = PageParams::default(); 35 35 36 - // Every page has a RouteContext, which contains information about the current route, as well as access to content and assets. 36 + // Every page has a PageContext, which contains information about the current page, as well as access to content and assets. 37 37 let url = route.url(&params); 38 - let mut ctx = RouteContext::from_static_route(&content, &mut page_assets, &url); 38 + let mut ctx = PageContext::from_static_route(&content, &mut page_assets, &url); 39 39 40 40 let content = route.build(&mut ctx)?; 41 41 42 - let route_filepath = route.file_path(&params, &options.output_dir); 42 + let page_filepath = route.file_path(&params, &options.output_dir); 43 43 44 44 // On some platforms, creating a file in a nested directory requires that the directory already exists or the file creation will fail. 45 - if let Some(parent_dir) = route_filepath.parent() { 45 + if let Some(parent_dir) = page_filepath.parent() { 46 46 fs::create_dir_all(parent_dir)? 47 47 } 48 48 49 - fs::write(route_filepath, content)?; 49 + fs::write(page_filepath, content)?; 50 50 51 51 // Copy all assets used by this page. 52 52 for asset in page_assets.assets() { ··· 54 54 } 55 55 } 56 56 RouteType::Dynamic => { 57 - // The `get_routes` method returns all the possible routes for this page, along with their parameters and properties. 57 + // The `get_pages` method returns all the possible pages for this route, along with their parameters and properties. 58 58 // It is very common for dynamic pages to be based on content, for instance a blog post page that has one route per blog post. 59 - // As such, we create a mini RouteContext that includes the content sources, so that the page can use them to generate its routes. 59 + // As such, we create a mini PageContext that includes the content sources, so that the route can use them to generate its pages. 60 + 61 + // Every page of a route may share a reference to the same RouteContent and RouteAssets instance, as it can help with caching. 62 + // However, it is not stricly necessary, and you may want to instead create a new instance of RouteAssets especially if you were to parallelize the building of pages. 63 + let content = RouteContent::new(&content_sources); 64 + let mut page_assets = RouteAssets::new(&page_assets_options); 60 65 61 - let dynamic_ctx = DynamicRouteContext { 62 - content: &PageContent::new(&content_sources), 66 + let mut dynamic_ctx = DynamicRouteContext { 67 + content: &content, 68 + assets: &mut page_assets, 63 69 }; 64 70 65 - let routes = route.get_routes(&dynamic_ctx); 71 + let routes = route.get_pages(&mut dynamic_ctx); 66 72 67 - // Every page can share a reference to the same PageContent instance, as it is just a view into the content sources. 68 - let content = PageContent::new(&content_sources); 73 + let content = RouteContent::new(&content_sources); 69 74 70 - for dynamic_route in routes { 71 - // However, since page assets is a mutable structure that tracks which assets have been used, we need a new instance for each route. 72 - // This is especially relevant if we were to parallelize this loop in the future. 73 - let mut page_assets = PageAssets::new(&page_assets_options); 75 + for page in routes { 76 + // The dynamic route includes the parameters for this specific page. 77 + let params = &page.0; 74 78 75 - // The dynamic route includes the parameters for this specific route. 76 - let params = &dynamic_route.0; 77 - 78 - // Here the context is created from a dynamic route, as the context has to include the route parameters and properties. 79 + // Here the context is created from a dynamic route, as the context has to include the page parameters and properties. 79 80 let url = route.url(params); 80 - let mut ctx = RouteContext::from_dynamic_route( 81 - &dynamic_route, 82 - &content, 83 - &mut page_assets, 84 - &url, 85 - ); 81 + let mut ctx = 82 + PageContext::from_dynamic_route(&page, &content, &mut page_assets, &url); 86 83 87 84 // Everything below here is the same as for static routes. 88 85
+2 -2
examples/library/src/main.rs
··· 5 5 6 6 use crate::build::build_website; 7 7 8 - mod pages { 8 + mod routes { 9 9 mod article; 10 10 mod index; 11 11 pub use article::Article; ··· 16 16 17 17 fn main() -> Result<(), Box<dyn std::error::Error>> { 18 18 build_website( 19 - routes![pages::Index], 19 + routes![routes::Index], 20 20 content_sources![ 21 21 "articles" => glob_markdown::<ArticleContent>("content/articles/*.md", None) 22 22 ],
+6 -6
examples/library/src/pages/article.rs examples/library/src/routes/article.rs
··· 1 - use maudit::page::prelude::*; 1 + use maudit::route::prelude::*; 2 2 3 3 use crate::{content::ArticleContent, layout::layout}; 4 4 ··· 10 10 pub article: String, 11 11 } 12 12 13 - impl Page<ArticleParams> for Article { 14 - fn routes(&self, ctx: &DynamicRouteContext) -> Routes<ArticleParams> { 13 + impl Route<ArticleParams> for Article { 14 + fn pages(&self, ctx: &mut DynamicRouteContext) -> Pages<ArticleParams> { 15 15 let articles = ctx.content.get_source::<ArticleContent>("articles"); 16 16 17 - articles.into_routes(|entry| { 18 - Route::from_params(ArticleParams { 17 + articles.into_pages(|entry| { 18 + Page::from_params(ArticleParams { 19 19 article: entry.id.clone(), 20 20 }) 21 21 }) 22 22 } 23 23 24 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 24 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 25 25 let params = ctx.params::<ArticleParams>(); 26 26 let articles = ctx.content.get_source::<ArticleContent>("articles"); 27 27 let article = articles.get_entry(&params.article);
+4 -4
examples/library/src/pages/index.rs examples/library/src/routes/index.rs
··· 1 1 use maud::html; 2 - use maudit::page::prelude::*; 2 + use maudit::route::prelude::*; 3 3 4 4 use crate::{ 5 5 content::ArticleContent, 6 6 layout::layout, 7 - pages::{article::ArticleParams, Article}, 7 + routes::{article::ArticleParams, Article}, 8 8 }; 9 9 10 10 #[route("/")] 11 11 pub struct Index; 12 12 13 - impl Page for Index { 14 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 13 + impl Route for Index { 14 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 15 15 let articles = ctx.content.get_source::<ArticleContent>("articles"); 16 16 let logo = ctx.assets.add_image("images/logo.svg"); 17 17
+2 -2
examples/markdown-components/src/main.rs
··· 2 2 use maudit::{content_sources, coronate, routes, BuildOptions, BuildOutput}; 3 3 4 4 mod components; 5 - mod pages; 5 + mod routes; 6 6 7 7 use components::*; 8 - use pages::{ComponentExample, IndexPage}; 8 + use routes::{ComponentExample, IndexPage}; 9 9 10 10 fn main() -> Result<BuildOutput, Box<dyn std::error::Error>> { 11 11 coronate(
+3 -3
examples/markdown-components/src/pages.rs examples/markdown-components/src/routes.rs
··· 1 1 use maud::{html, PreEscaped, DOCTYPE}; 2 2 use maudit::content::markdown_entry; 3 - use maudit::page::prelude::*; 3 + use maudit::route::prelude::*; 4 4 5 5 #[markdown_entry] 6 6 pub struct ComponentExample {} ··· 8 8 #[route("/")] 9 9 pub struct IndexPage; 10 10 11 - impl Page for IndexPage { 12 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 11 + impl Route for IndexPage { 12 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 13 13 let examples = ctx.content.get_source::<ComponentExample>("examples"); 14 14 let example = examples.get_entry("showcase"); 15 15
+2 -2
examples/oubli-basics/src/main.rs
··· 2 2 3 3 use oubli::{archetypes, forget, routes, Archetype, BuildOptions, BuildOutput}; 4 4 5 - mod pages { 5 + mod routes { 6 6 mod index; 7 7 pub use index::Index; 8 8 } 9 9 10 - pub use pages::Index; 10 + pub use routes::Index; 11 11 12 12 fn main() -> Result<BuildOutput, Box<dyn std::error::Error>> { 13 13 forget(
+3 -3
examples/oubli-basics/src/pages/index.rs examples/oubli-basics/src/routes/index.rs
··· 1 1 use crate::layout::layout; 2 2 use maud::html; 3 - use maudit::page::prelude::*; 3 + use maudit::route::prelude::*; 4 4 5 5 #[route("/")] 6 6 pub struct Index; 7 7 8 - impl Page for Index { 9 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 8 + impl Route for Index { 9 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 10 10 let logo = ctx.assets.add_image("images/logo.svg"); 11 11 12 12 let archetype_store = ctx
+5
package.json
··· 1 1 { 2 2 "name": "root", 3 + "type": "module", 3 4 "private": true, 4 5 "packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c", 5 6 "dependencies": { ··· 7 8 "@tailwindcss/typography": "^0.5.15", 8 9 "tailwindcss": "^4.0.0", 9 10 "thumbhash": "^0.1.1" 11 + }, 12 + "devDependencies": { 13 + "ansi_up": "^6.0.6", 14 + "typescript": "^5.9.2" 10 15 } 11 16 }
+19
pnpm-lock.yaml
··· 20 20 thumbhash: 21 21 specifier: ^0.1.1 22 22 version: 0.1.1 23 + devDependencies: 24 + ansi_up: 25 + specifier: ^6.0.6 26 + version: 6.0.6 27 + typescript: 28 + specifier: ^5.9.2 29 + version: 5.9.2 23 30 24 31 packages: 25 32 ··· 186 193 resolution: {integrity: sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA==} 187 194 peerDependencies: 188 195 tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20' 196 + 197 + ansi_up@6.0.6: 198 + resolution: {integrity: sha512-yIa1x3Ecf8jWP4UWEunNjqNX6gzE4vg2gGz+xqRGY+TBSucnYp6RRdPV4brmtg6bQ1ljD48mZ5iGSEj7QEpRKA==} 189 199 190 200 braces@3.0.3: 191 201 resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} ··· 337 347 resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 338 348 engines: {node: '>=8.0'} 339 349 350 + typescript@5.9.2: 351 + resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} 352 + engines: {node: '>=14.17'} 353 + hasBin: true 354 + 340 355 util-deprecate@1.0.2: 341 356 resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 342 357 ··· 474 489 postcss-selector-parser: 6.0.10 475 490 tailwindcss: 4.0.0 476 491 492 + ansi_up@6.0.6: {} 493 + 477 494 braces@3.0.3: 478 495 dependencies: 479 496 fill-range: 7.1.1 ··· 581 598 to-regex-range@5.0.1: 582 599 dependencies: 583 600 is-number: 7.0.0 601 + 602 + typescript@5.9.2: {} 584 603 585 604 util-deprecate@1.0.2: {}
+7 -7
website/content/docs/content.md
··· 41 41 42 42 ## Using a content source in pages 43 43 44 - Once a content source is defined, it can be accessed in pages through the `RouteContext#content` property. 44 + Once a content source is defined, it can be accessed in pages through the `PageContext#content` property. 45 45 46 46 ```rs 47 - use maudit::page::prelude::*; 47 + use maudit::route::prelude::*; 48 48 use maud::{html, PreEscaped}; 49 49 50 50 #[route("/some-article")] 51 51 pub struct SomeArticlePage; 52 52 53 - impl Page for SomeArticlePage { 54 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 53 + impl Route for SomeArticlePage { 54 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 55 55 let entry = ctx 56 56 .content 57 57 .get_source::<BlogPost>("source_name") ··· 127 127 and then in pages, you could access the data like this: 128 128 129 129 ```rs 130 - use maudit::page::prelude::*; 130 + use maudit::route::prelude::*; 131 131 132 132 #[route("/data")] 133 133 pub struct DataPage; 134 134 135 - impl Page for DataPage { 136 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 135 + impl Route for DataPage { 136 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 137 137 let entry = ctx 138 138 .content 139 139 .get_source::<MyType>("my_data")
+2 -2
website/content/docs/entrypoint.md
··· 10 10 11 11 ```rs 12 12 use maudit::{coronate, routes, BuildOptions, BuildOutput}; 13 - use pages::Index; 13 + use routes::Index; 14 14 15 15 fn main() -> Result<BuildOutput, Box<dyn std::error::Error>> { 16 16 coronate(routes![Index], vec![].into(), BuildOptions::default()) ··· 24 24 The first argument to the `coronate` function is a `Vec` of all the routes that should be built. For the sake of ergonomics, the `routes!` macro can be used to create this list. 25 25 26 26 ```rs 27 - use pages::Index; 27 + use routes::Index; 28 28 29 29 coronate( 30 30 routes![Index],
+6 -6
website/content/docs/images.md
··· 15 15 To use an image in a page, add it anywhere in your project's directory, and use the `ctx.assets.add_image()` method to add it to a page's assets. 16 16 17 17 ```rs 18 - use maudit::page::prelude::*; 18 + use maudit::route::prelude::*; 19 19 20 20 #[route("/blog")] 21 21 pub struct Blog; 22 22 23 - impl Page for Blog { 24 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 23 + impl Route for Blog { 24 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 25 25 let image = ctx.assets.add_image("logo.png"); 26 26 27 27 format!("", image.url).into() ··· 46 46 Images added to pages can be transformed by using `ctx.assets.add_image_with_options()`. 47 47 48 48 ```rs 49 - use maudit::page::prelude::*; 49 + use maudit::route::prelude::*; 50 50 51 51 #[route("/image")] 52 52 pub struct ImagePage; 53 53 54 - impl Page for ImagePage { 55 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 54 + impl Route for ImagePage { 55 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 56 56 let image = ctx.assets.add_image_with_options( 57 57 "path/to/image.jpg", 58 58 ImageOptions {
+4 -4
website/content/docs/javascript.md
··· 7 7 JavaScript and TypeScript files can be added to pages using the `ctx.assets.add_script()` method. 8 8 9 9 ```rs 10 - use maudit::page::prelude::*; 10 + use maudit::route::prelude::*; 11 11 use maud::{html, Markup}; 12 12 13 13 #[route("/blog")] 14 14 pub struct Blog; 15 15 16 - impl Page<RouteParams, Markup> for Blog { 17 - fn render(&self, ctx: &mut RouteContext) -> Markup { 16 + impl Route<PageParams, Markup> for Blog { 17 + fn render(&self, ctx: &mut PageContext) -> Markup { 18 18 let script = ctx.assets.add_script("script.js"); 19 19 20 20 html! { ··· 27 27 The `include_script()` method can be used to automatically include the script in the page, which can be useful when using layouts or other shared templates. 28 28 29 29 ```rs 30 - fn render(&self, ctx: &mut RouteContext) -> Markup { 30 + fn render(&self, ctx: &mut PageContext) -> Markup { 31 31 ctx.assets.include_script("script.js"); 32 32 33 33 layout(
+23 -24
website/content/docs/library.md
··· 12 12 13 13 ## Function signature 14 14 15 - The built-in `coronate` function takes a list of routes (which all implements the [FullPage](https://docs.rs/maudit/latest/maudit/page/trait.FullPage.html) trait), content sources, and some build options. We'll do the same. 15 + The built-in `coronate` function takes a list of routes (which all implements the [FullRoute](https://docs.rs/maudit/latest/maudit/page/trait.FullRoute.html) trait), content sources, and some build options. We'll do the same. 16 16 17 17 ```rs 18 18 use maudit::{ 19 19 content::ContentSources, 20 - page::{FullPage, PageAssets, PageContent}, 21 - route::{DynamicRouteContext, RouteContext, RouteParams, RouteType}, 20 + page::{FullRoute, RouteAssets, RouteContent}, 21 + routing::{DynamicRouteContext, PageContext, PageParams, RouteType}, 22 22 BuildOptions, 23 23 }; 24 24 25 25 pub fn build_website( 26 - routes: &[&dyn FullPage], 26 + routes: &[&dyn FullRoute], 27 27 mut content_sources: ContentSources, 28 28 options: BuildOptions 29 29 ) -> Result<(), Box<dyn std::error::Error>> { ··· 40 40 41 41 ```rs 42 42 pub fn build_website( 43 - routes: &[&dyn FullPage], 43 + routes: &[&dyn FullRoute], 44 44 mut content_sources: ContentSources, 45 45 options: BuildOptions, 46 46 ) -> Result<(), Box<dyn std::error::Error>> { 47 47 48 - // Options we'll be passing to PageAssets instances. 48 + // Options we'll be passing to RouteAssets instances. 49 49 // This value automatically has the paths joined based on the output directory in BuildOptions for us, so we don't have to do it ourselves. 50 50 let page_assets_options = options.page_assets_options(); 51 51 ··· 53 53 match route.route_type() { 54 54 RouteType::Static => { 55 55 // Our page does not include content or assets, but we'll set those up for future use. 56 - let content = PageContent::new(&content_sources); 57 - let mut page_assets = PageAssets::new(&page_assets_options); 56 + let content = RouteContent::new(&content_sources); 57 + let mut page_assets = RouteAssets::new(&page_assets_options); 58 58 59 59 // Static and dynamic routes share the same interface for building, but static routes do not require any parameters. 60 - // As such, we can just pass an empty set of parameters (the default for RouteParams). 61 - let params = RouteParams::default(); 60 + // As such, we can just pass an empty set of parameters (the default for PageParams). 61 + let params = PageParams::default(); 62 62 63 - // Every page has a RouteContext, which contains information about the current route, as well as access to content and assets. 63 + // Every page has a PageContext, which contains information about the current route, as well as access to content and assets. 64 64 let url = route.url(&params); 65 - let mut ctx = RouteContext::from_static_route(&content, &mut page_assets, &url); 65 + let mut ctx = PageContext::from_static_route(&content, &mut page_assets, &url); 66 66 67 67 let content = route.build(&mut ctx)?; 68 68 ··· 127 127 // No changes before this block. 128 128 129 129 RouteType::Dynamic => { 130 - // The `get_routes` method returns all the possible routes for this page, along with their parameters and properties. 130 + // The `get_pages` method returns all the possible pages for this route, along with their parameters and properties. 131 131 // It is very common for dynamic pages to be based on content, for instance a blog post page that has one route per blog post. 132 - // As such, we create essentially a mini `RouteContext` through `DynamicRouteContext` that includes the content sources, so that the page can use them to generate its routes. 132 + // As such, we create essentially a mini `PageContext` through `DynamicRouteContext` that includes the content sources, so that the page can use them to generate its routes. 133 + 134 + // Every page of a route may share a reference to the same RouteContent and RouteAssets instance, as it can help with caching. 135 + // However, it is not stricly necessary, and you may want to instead create a new instance of RouteAssets especially if you were to parallelize the building of pages. 136 + let mut page_assets = RouteAssets::new(&page_assets_options); 137 + let content = RouteContent::new(&content_sources); 133 138 134 139 let dynamic_ctx = DynamicRouteContext { 135 - content: &PageContent::new(&content_sources), 140 + content: &content, 141 + assets: &mut page_assets, 136 142 }; 137 143 138 - let routes = route.get_routes(&dynamic_ctx); 139 - 140 - // Every page can share a reference to the same PageContent instance, as it is just a view into the content sources. 141 - let content = PageContent::new(&content_sources); 144 + let routes = route.get_pages(&dynamic_ctx); 142 145 143 146 for dynamic_route in routes { 144 - // However, since page assets is a mutable structure that tracks which assets have been used, we need a new instance for each route. 145 - // This is especially relevant if we were to parallelize this loop in the future. 146 - let mut page_assets = PageAssets::new(&page_assets_options); 147 - 148 147 // The dynamic route includes the parameters for this specific route. 149 148 let params = &dynamic_route.0; 150 149 151 150 // Here the context is created from a dynamic route, as the context has to include the route parameters and properties. 152 151 let url = route.url(params); 153 - let mut ctx = RouteContext::from_dynamic_route( 152 + let mut ctx = PageContext::from_dynamic_route( 154 153 &dynamic_route, 155 154 &content, 156 155 &mut page_assets,
+6 -6
website/content/docs/quick-start.md
··· 45 45 46 46 To create a page, create a `.rs` file with a public struct using the `route` attribute, which take the path of the route as sole parameter. 47 47 48 - All of Maudit's useful imports for pages can be imported using the prelude from `maudit::page`. 48 + All of Maudit's useful imports for pages can be imported using the prelude from `maudit::route`. 49 49 50 50 ```rs 51 - use maudit::page::prelude::*; 51 + use maudit::route::prelude::*; 52 52 53 53 #[route("/hello-world")] 54 54 pub struct HelloWorld; 55 55 ``` 56 56 57 - Every page must `impl` the `Page` trait, with the required method `render`. 57 + Every page must `impl` the `Route` trait, with the required method `render`. 58 58 59 59 ```rs 60 - impl Page for HelloWorld { 61 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 60 + impl Route for HelloWorld { 61 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 62 62 "Hello, world!".into() 63 63 } 64 64 } ··· 67 67 Finally, pages' struct must be passed to the `coronate` function in the project's `main.rs` 68 68 69 69 ```rs 70 - use pages::HelloWorld; 70 + use routes::HelloWorld; 71 71 use maudit::{coronate, routes, content_sources, BuildOptions, BuildOutput}; 72 72 73 73 fn main() -> Result<BuildOutput, Box<dyn std::error::Error>> {
+29 -29
website/content/docs/routing.md
··· 6 6 7 7 ## Static Routes 8 8 9 - To create a new page in your Maudit project, create a struct and implement the `Page` trait for it, adding the `#[route]` attribute to the struct definition with the path of the route as an argument. The path can be any Rust expression, as long as it returns a `String`. 9 + To create a new page in your Maudit project, create a struct and implement the `Route` trait for it, adding the `#[route]` attribute to the struct definition with the path of the route as an argument. The path can be any Rust expression, as long as it returns a `String`. 10 10 11 11 ```rs 12 - use maudit::page::prelude::*; 12 + use maudit::route::prelude::*; 13 13 14 14 #[route("/hello-world")] 15 15 pub struct HelloWorld; 16 16 17 - impl Page for HelloWorld { 18 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 17 + impl Route for HelloWorld { 18 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 19 19 RenderResult::Text("Hello, world!".to_string()) 20 20 } 21 21 } 22 22 ``` 23 23 24 - The `Page` trait requires the implementation of a `render` method that returns a `RenderResult`. This method is called when the page is built and should return the content that will be displayed. In most cases, you'll be using a templating library to create HTML content. 24 + The `Route` trait requires the implementation of a `render` method that returns a `RenderResult`. This method is called when the page is built and should return the content that will be displayed. In most cases, you'll be using a templating library to create HTML content. 25 25 26 26 Finally, make sure to [register the page](#registering-routes) in the `coronate` function for it to be built. 27 27 28 28 ## Ergonomic returns 29 29 30 - The `Page` trait accepts a generic parameter in third position for the return type of the `render` method. This type must implement `Into<RenderResult>`, enabling more ergonomic returns in certain cases. 30 + The `Route` trait accepts a generic parameter in third position for the return type of the `render` method. This type must implement `Into<RenderResult>`, enabling more ergonomic returns in certain cases. 31 31 32 32 ```rs 33 - impl Page<(), (), String> for HelloWorld { 34 - fn render(&self, ctx: &mut RouteContext) -> String { 33 + impl Route<(), (), String> for HelloWorld { 34 + fn render(&self, ctx: &mut PageContext) -> String { 35 35 "Hello, world!".to_string() 36 36 } 37 37 } ··· 51 51 In addition to the `render` method, dynamic routes must implement a `routes` method for Page. The `routes` method returns a list of all the possible values for each parameter in the route's path, so that Maudit can generate all the necessary pages. 52 52 53 53 ```rs 54 - use maudit::page::prelude::*; 54 + use maudit::route::prelude::*; 55 55 56 56 #[route("/posts/[slug]")] 57 57 pub struct Post; ··· 61 61 pub slug: String, 62 62 } 63 63 64 - impl Page<Params> for Post { 65 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 64 + impl Route<Params> for Post { 65 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 66 66 let params = ctx.params::<Params>(); 67 67 RenderResult::Text(format!("Hello, {}!", params.slug)) 68 68 } 69 69 70 - fn routes(&self, ctx: &DynamicRouteContext) -> Routes<Params> { 71 - vec![Route::from_params(Params { 70 + fn pages(&self, ctx: &mut DynamicRouteContext) -> Routes<Params> { 71 + vec![Page::from_params(Params { 72 72 slug: "hello-world".to_string(), 73 73 })] 74 74 } 75 75 } 76 76 ``` 77 77 78 - The route parameters are automatically extracted from the URL and made available through the `ctx.params::<T>()` method in the `RouteContext` struct, providing type-safe access to the values. 78 + The route parameters are automatically extracted from the URL and made available through the `ctx.params::<T>()` method in the `PageContext` struct, providing type-safe access to the values. 79 79 80 80 ```rs 81 - use maudit::page::prelude::*; 81 + use maudit::route::prelude::*; 82 82 83 83 #[route("/posts/[slug]")] 84 84 pub struct Post; ··· 88 88 pub slug: String, 89 89 } 90 90 91 - impl Page for Post { 92 - fn render(&self, ctx: &mut RouteContext) -> String { 91 + impl Route for Post { 92 + fn render(&self, ctx: &mut PageContext) -> String { 93 93 let slug = ctx.params::<Params>().slug; 94 94 format!("Hello, {}!", slug) 95 95 } 96 96 97 - fn routes(&self, ctx: &DynamicRouteContext) -> Routes<Params> { 98 - vec![Route::from_params(Params { 97 + fn pages(&self, ctx: &mut DynamicRouteContext) -> Routes<Params> { 98 + vec![Page::from_params(Params { 99 99 slug: "hello-world".to_string(), 100 100 })] 101 101 } 102 102 } 103 103 ``` 104 104 105 - The struct used for the parameters must implement `Into<RouteParams>`, which can be done automatically by deriving the `Params` trait. The fields of the struct must implement the `Display` trait, as they will be converted to strings to be used in the final URLs and file paths. 105 + The struct used for the parameters must implement `Into<PageParams>`, which can be done automatically by deriving the `Params` trait. The fields of the struct must implement the `Display` trait, as they will be converted to strings to be used in the final URLs and file paths. 106 106 107 107 Like static routes, dynamic routes must be [registered](#registering-routes) in the `coronate` function in order for them to be built. 108 108 ··· 111 111 Maudit supports returning other types of content besides HTML, such as JSON, plain text or binary data. To do this, simply add a file extension to the route path and return the content in the `render` method. 112 112 113 113 ```rs 114 - use maudit::page::prelude::*; 114 + use maudit::route::prelude::*; 115 115 116 116 #[route("/api.json")] 117 117 pub struct HelloWorldJson; 118 118 119 - impl Page for HelloWorldJson { 120 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 119 + impl Route for HelloWorldJson { 120 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 121 121 RenderResult::Text(r#"{"message": "Hello, world!"}"#.to_string()) 122 122 } 123 123 } ··· 126 126 Dynamic routes can also return different types of content. For example, to return a JSON response with the post's content, you could write: 127 127 128 128 ```rs 129 - use maudit::page::prelude::*; 129 + use maudit::route::prelude::*; 130 130 131 131 #[route("/api/[slug].json")] 132 132 pub struct PostJson; ··· 136 136 pub slug: String, 137 137 } 138 138 139 - impl Page<Params> for PostJson { 140 - fn routes(&self, ctx: &DynamicRouteContext) -> Routes<Params> { 141 - vec![Route::from_params(Params { 139 + impl Route<Params> for PostJson { 140 + fn pages(&self, ctx: &mut DynamicRouteContext) -> Routes<Params> { 141 + vec![Page::from_params(Params { 142 142 slug: "hello-world".to_string() 143 143 })] 144 144 } 145 145 146 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 146 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 147 147 let params = ctx.params::<Params>(); 148 148 149 149 RenderResult::Text(format!(r#"{{"message": "Hello, {}!"}}"#, params.slug)) ··· 160 160 The first argument to the `coronate` function is a `Vec` of all the routes that should be built. This list can be created using the `routes!` macro to make it more concise. 161 161 162 162 ```rs 163 - use pages::Index; 163 + use routes::Index; 164 164 use maudit::{coronate, routes, BuildOptions, BuildOutput}; 165 165 166 166 fn main() -> Result<BuildOutput, Box<dyn std::error::Error>> {
+5 -5
website/content/docs/styling.md
··· 11 11 In [supported templating languages](/docs/templating/), the return value of `ctx.assets.add_style()` can be used directly in the template. 12 12 13 13 ```rs 14 - use maudit::page::prelude::*; 14 + use maudit::route::prelude::*; 15 15 use maud::{html, Markup}; 16 16 17 17 #[route("/blog")] 18 18 pub struct Blog; 19 19 20 - impl Page<RouteParams, Markup> for Blog { 21 - fn render(&self, ctx: &mut RouteContext) -> Markup { 20 + impl Route<PageParams, Markup> for Blog { 21 + fn render(&self, ctx: &mut PageContext) -> Markup { 22 22 let style = ctx.assets.add_style("style.css"); 23 23 24 24 html! { ··· 31 31 Alternatively, the `include_style()` method can be used to automatically include the stylesheet in the page, without needing to manually add it to the template. Note that, at this time, pages without a `head` tag won't have the stylesheet included. 32 32 33 33 ```rs 34 - fn render(&self, ctx: &mut RouteContext) -> Markup { 34 + fn render(&self, ctx: &mut PageContext) -> Markup { 35 35 ctx.assets.include_style("style.css"); 36 36 37 37 layout( ··· 49 49 Maudit includes built-in support for [Tailwind CSS](https://tailwindcss.com/). To use it, use `add_style_with_options()` or `include_style_with_options()` with the `StyleOptions { tailwind: true }` option. 50 50 51 51 ```rs 52 - fn render(&self, ctx: &mut RouteContext) -> Markup { 52 + fn render(&self, ctx: &mut PageContext) -> Markup { 53 53 ctx.assets.add_style_with_options("style.css", StyleOptions { tailwind: true }); 54 54 55 55 html! {
+6 -6
website/content/docs/templating.md
··· 14 14 15 15 ```rs 16 16 use maud::{html, Markup}; 17 - use maudit::page::prelude::*; 17 + use maudit::route::prelude::*; 18 18 19 19 #[route("/")] 20 20 pub struct Index; 21 21 22 - impl Page<RouteParams, Markup> for Index { 23 - fn render(&self, _: &mut RouteContext) -> Markup { 22 + impl Route<PageParams, Markup> for Index { 23 + fn render(&self, _: &mut PageContext) -> Markup { 24 24 html! { 25 25 h1 { "Hello, world!" } 26 26 } ··· 32 32 33 33 ```rs 34 34 use maud::{html, Markup}; 35 - use maudit::page::prelude::*; 35 + use maudit::route::prelude::*; 36 36 37 37 #[route("/")] 38 38 pub struct Index; 39 39 40 - impl Page<RouteParams, Markup> for Index { 41 - fn render(&self, ctx: &mut RouteContext) -> Markup { 40 + impl Route<PageParams, Markup> for Index { 41 + fn render(&self, ctx: &mut PageContext) -> Markup { 42 42 let logo = ctx.add_image("./logo.png"); 43 43 44 44 html! {
+4 -4
website/content/news/maudit01.md
··· 25 25 26 26 ## Not quite your overlord 27 27 28 - Maudit is built like a library, not a framework. A Maudit project is a normal Rust project, nothing more, nothing less. A Maudit page is a normal Rust struct in a normal `.rs` file. 28 + [Maudit is built like a library, not a framework.](/docs/philosophy/#maudit-is-a-library-not-a-framework) A Maudit project is a normal Rust project, nothing more, nothing less. A Maudit page is a normal Rust struct in a normal `.rs` file. 29 29 30 30 ```rs 31 - use maudit::page::prelude::*; 31 + use maudit::route::prelude::*; 32 32 33 33 #[route("/")] 34 34 pub struct Index; 35 35 36 - impl Page for Index { 37 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 36 + impl Route for Index { 37 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 38 38 "Hello World".into() 39 39 } 40 40 }
+3 -3
website/src/layout.rs
··· 8 8 use maudit::assets::StyleOptions; 9 9 use maudit::content::MarkdownHeading; 10 10 use maudit::maud::generator; 11 - use maudit::page::{RenderResult, RouteContext}; 11 + use maudit::route::{RenderResult, PageContext}; 12 12 13 13 pub fn docs_layout( 14 14 main: Markup, 15 - ctx: &mut RouteContext, 15 + ctx: &mut PageContext, 16 16 headings: &[MarkdownHeading], 17 17 ) -> RenderResult { 18 18 layout( ··· 39 39 main: Markup, 40 40 bottom_border: bool, 41 41 licenses: bool, 42 - ctx: &mut RouteContext, 42 + ctx: &mut PageContext, 43 43 ) -> RenderResult { 44 44 ctx.assets 45 45 .include_style_with_options("assets/prin.css", StyleOptions { tailwind: true });
+2 -2
website/src/layout/docs_sidebars.rs
··· 1 1 use maud::{html, Markup}; 2 - use maudit::{content::MarkdownHeading, page::RouteContext}; 2 + use maudit::{content::MarkdownHeading, route::PageContext}; 3 3 4 4 use crate::content::{DocsContent, DocsSection}; 5 5 6 - pub fn left_sidebar(ctx: &mut RouteContext) -> Markup { 6 + pub fn left_sidebar(ctx: &mut PageContext) -> Markup { 7 7 let content = ctx.content.get_source::<DocsContent>("docs"); 8 8 9 9 let mut sections = std::collections::HashMap::new();
+2 -2
website/src/layout/header.rs
··· 1 1 use maud::html; 2 2 use maud::Markup; 3 3 use maud::PreEscaped; 4 - use maudit::page::RouteContext; 4 + use maudit::route::PageContext; 5 5 6 - pub fn header(_: &mut RouteContext, bottom_border: bool) -> Markup { 6 + pub fn header(_: &mut PageContext, bottom_border: bool) -> Markup { 7 7 let border = if bottom_border { "border-b" } else { "" }; 8 8 9 9 html! {
+2 -2
website/src/main.rs
··· 3 3 4 4 mod content; 5 5 mod layout; 6 - mod pages; 6 + mod routes; 7 7 8 - use pages::*; 8 + use routes::*; 9 9 10 10 fn main() -> Result<BuildOutput, Box<dyn std::error::Error>> { 11 11 coronate(
+3 -3
website/src/pages/404.rs website/src/routes/404.rs
··· 1 1 use maud::{html, PreEscaped}; 2 - use maudit::page::prelude::*; 2 + use maudit::route::prelude::*; 3 3 4 4 use crate::layout::layout; 5 5 6 6 #[route("404.html")] 7 7 pub struct NotFound; 8 8 9 - impl Page for NotFound { 10 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 9 + impl Route for NotFound { 10 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 11 11 layout( 12 12 html! { 13 13 div.container.mx-auto.text-center.my-50.flex.items-center.flex-col."gap-y-4"."px-8"."sm:px-0" {
+3 -3
website/src/pages/chat.rs website/src/routes/chat.rs
··· 1 1 use maud::html; 2 - use maudit::page::prelude::*; 2 + use maudit::route::prelude::*; 3 3 4 4 #[route("/chat")] 5 5 pub struct ChatRedirect; 6 6 7 7 pub const DISCORD_INVITE: &str = "https://discord.gg/84pd4QtmzA"; 8 8 9 - impl Page for ChatRedirect { 10 - fn render(&self, _: &mut RouteContext) -> RenderResult { 9 + impl Route for ChatRedirect { 10 + fn render(&self, _: &mut PageContext) -> RenderResult { 11 11 html! { 12 12 head { 13 13 meta http-equiv="refresh" content=(format!("0;url={}", DISCORD_INVITE));
+3 -3
website/src/pages/contribute.rs website/src/routes/contribute.rs
··· 1 1 use maud::html; 2 - use maudit::page::prelude::*; 2 + use maudit::route::prelude::*; 3 3 4 4 use crate::layout::layout; 5 5 6 6 #[route("/contribute")] 7 7 pub struct Contribute; 8 8 9 - impl Page for Contribute { 10 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 9 + impl Route for Contribute { 10 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 11 11 layout( 12 12 html!( 13 13 div.container.w-full.max-w-larger-prose.mx-auto.my-14.flex.flex-col."gap-y-12"."px-8"."sm:px-0" {
+9 -9
website/src/pages/docs.rs website/src/routes/docs.rs
··· 1 1 use maud::{html, Markup, PreEscaped}; 2 - use maudit::{content::ContentEntry, page::prelude::*}; 2 + use maudit::{content::ContentEntry, route::prelude::*}; 3 3 4 4 use crate::{content::DocsContent, layout::docs_layout}; 5 5 6 6 #[route("/docs")] 7 7 pub struct DocsIndex; 8 8 9 - impl Page for DocsIndex { 10 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 9 + impl Route for DocsIndex { 10 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 11 11 let index_page = ctx 12 12 .content 13 13 .get_source::<DocsContent>("docs") ··· 19 19 } 20 20 } 21 21 22 - fn render_entry(entry: &ContentEntry<DocsContent>, ctx: &mut RouteContext) -> Markup { 22 + fn render_entry(entry: &ContentEntry<DocsContent>, ctx: &mut PageContext) -> Markup { 23 23 html! { 24 24 section.mb-4.border-b."border-[#e9e9e7]".pb-2 { 25 25 @if let Some(section) = &entry.data(ctx).section { ··· 44 44 slug: String, 45 45 } 46 46 47 - impl Page<DocsPageParams> for DocsPage { 48 - fn routes(&self, ctx: &DynamicRouteContext) -> Routes<DocsPageParams> { 47 + impl Route<DocsPageParams> for DocsPage { 48 + fn pages(&self, ctx: &mut DynamicRouteContext) -> Pages<DocsPageParams> { 49 49 let content = ctx.content.get_source::<DocsContent>("docs"); 50 50 51 - content.into_routes(|entry| { 52 - Route::from_params(DocsPageParams { 51 + content.into_pages(|entry| { 52 + Page::from_params(DocsPageParams { 53 53 slug: entry.id.clone(), 54 54 }) 55 55 }) 56 56 } 57 57 58 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 58 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 59 59 let slug = ctx.params::<DocsPageParams>().slug.clone(); 60 60 let entry = ctx 61 61 .content
+3 -3
website/src/pages/index.rs website/src/routes/index.rs
··· 1 1 use maud::html; 2 2 use maud::PreEscaped; 3 - use maudit::page::prelude::*; 3 + use maudit::route::prelude::*; 4 4 5 5 use crate::layout::layout; 6 6 7 7 #[route("/")] 8 8 pub struct Index; 9 9 10 - impl Page for Index { 11 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 10 + impl Route for Index { 11 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 12 12 let features = [ 13 13 ("Performant", "Generate a site with thousands of pages in seconds using minimal resources."), 14 14 ("Content", "Bring your content to life with built-in support for Markdown, custom components, syntax highlighting, and more."),
website/src/pages/mod.rs website/src/routes/mod.rs
+8 -8
website/src/pages/news.rs website/src/routes/news.rs
··· 1 1 use maud::html; 2 2 use maud::PreEscaped; 3 - use maudit::page::prelude::*; 3 + use maudit::route::prelude::*; 4 4 use std::collections::BTreeMap; 5 5 6 6 use crate::content::NewsContent; ··· 9 9 #[route("/news")] 10 10 pub struct NewsIndex; 11 11 12 - impl Page for NewsIndex { 13 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 12 + impl Route for NewsIndex { 13 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 14 14 let content = ctx.content.get_source::<NewsContent>("news"); 15 15 16 16 // Group articles by year ··· 86 86 slug: String, 87 87 } 88 88 89 - impl Page<NewsPageParams> for NewsPage { 90 - fn routes(&self, ctx: &DynamicRouteContext) -> Routes<NewsPageParams> { 89 + impl Route<NewsPageParams> for NewsPage { 90 + fn pages(&self, ctx: &mut DynamicRouteContext) -> Pages<NewsPageParams> { 91 91 let content = ctx.content.get_source::<NewsContent>("news"); 92 92 93 - content.into_routes(|entry| { 94 - Route::from_params(NewsPageParams { 93 + content.into_pages(|entry| { 94 + Page::from_params(NewsPageParams { 95 95 slug: entry.id.clone(), 96 96 }) 97 97 }) 98 98 } 99 99 100 - fn render(&self, ctx: &mut RouteContext) -> RenderResult { 100 + fn render(&self, ctx: &mut PageContext) -> RenderResult { 101 101 let slug = ctx.params::<NewsPageParams>().slug.clone(); 102 102 let entry = ctx 103 103 .content