From bf150d206fe08a6f36806a42fcd3255bddbde6ca Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 17 Nov 2024 08:06:46 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20html!{}=20and=20Tera=20templa?= =?UTF-8?q?tes=20for=20PageTop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 395 ++++++++- Cargo.toml | 1 + helpers/pagetop-build/Cargo.toml | 2 +- helpers/pagetop-macros/Cargo.toml | 2 + helpers/pagetop-macros/README.md | 14 +- helpers/pagetop-macros/src/lib.rs | 8 + helpers/pagetop-macros/src/maud.rs | 39 + helpers/pagetop-macros/src/maud/ast.rs | 221 +++++ helpers/pagetop-macros/src/maud/escape.rs | 34 + helpers/pagetop-macros/src/maud/generate.rs | 308 +++++++ helpers/pagetop-macros/src/maud/parse.rs | 752 ++++++++++++++++++ packages/drust/Cargo.toml | 4 +- packages/pagetop-aliner/Cargo.toml | 14 +- packages/pagetop-aliner/README.md | 12 +- packages/pagetop-aliner/_build.rs | 19 - packages/pagetop-aliner/build.rs | 7 + packages/pagetop-aliner/src/lib.rs | 218 +---- .../en-US/{inception.ftl => aliner.ftl} | 0 .../es-ES/{inception.ftl => aliner.ftl} | 0 packages/pagetop-aliner/static/css/styles.css | 356 +++++++++ packages/pagetop-aliner/tests/tera.rs | 23 + packages/pagetop-bootsier/Cargo.toml | 9 +- packages/pagetop-bootsier/src/lib.rs | 289 +++---- packages/pagetop/Cargo.toml | 33 +- packages/pagetop/src/html.rs | 4 + packages/pagetop/src/html/maud.rs | 350 ++++++++ packages/pagetop/src/html/maud/escape.rs | 34 + packages/pagetop/src/lib.rs | 14 +- packages/pagetop/src/prelude.rs | 2 + packages/pagetop/src/util.rs | 8 +- 30 files changed, 2756 insertions(+), 416 deletions(-) create mode 100644 helpers/pagetop-macros/src/maud.rs create mode 100644 helpers/pagetop-macros/src/maud/ast.rs create mode 100644 helpers/pagetop-macros/src/maud/escape.rs create mode 100644 helpers/pagetop-macros/src/maud/generate.rs create mode 100644 helpers/pagetop-macros/src/maud/parse.rs delete mode 100644 packages/pagetop-aliner/_build.rs create mode 100644 packages/pagetop-aliner/build.rs rename packages/pagetop-aliner/src/locale/en-US/{inception.ftl => aliner.ftl} (100%) rename packages/pagetop-aliner/src/locale/es-ES/{inception.ftl => aliner.ftl} (100%) create mode 100644 packages/pagetop-aliner/static/css/styles.css create mode 100644 packages/pagetop-aliner/tests/tera.rs create mode 100644 packages/pagetop/src/html.rs create mode 100644 packages/pagetop/src/html/maud.rs create mode 100644 packages/pagetop/src/html/maud/escape.rs diff --git a/Cargo.lock b/Cargo.lock index 60fad3de..5eca0cae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,7 +88,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn", + "syn 2.0.87", ] [[package]] @@ -222,7 +222,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn", + "syn 2.0.87", ] [[package]] @@ -330,6 +330,21 @@ version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.18" @@ -518,6 +533,40 @@ dependencies = [ "path-slash", ] +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "windows-targets 0.52.6", +] + +[[package]] +name = "chrono-tz" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + [[package]] name = "cipher" version = "0.4.4" @@ -607,6 +656,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.15" @@ -698,7 +753,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn", + "syn 2.0.87", ] [[package]] @@ -718,10 +773,16 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.87", "unicode-xid", ] +[[package]] +name = "deunicode" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00" + [[package]] name = "digest" version = "0.10.7" @@ -741,7 +802,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.87", ] [[package]] @@ -838,7 +899,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.87", "unic-langid", ] @@ -974,6 +1035,17 @@ dependencies = [ "regex-syntax 0.8.5", ] +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags", + "ignore", + "walkdir", +] + [[package]] name = "grass" version = "0.13.4" @@ -1087,6 +1159,38 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -1202,7 +1306,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.87", ] [[package]] @@ -1248,6 +1352,25 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae21c3177a27788957044151cc2800043d127acaa460a47ebb9b84dfa2c6aa0" +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "indexmap" version = "2.6.0" @@ -1343,6 +1466,12 @@ version = "0.2.162" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -1479,6 +1608,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.36.5" @@ -1519,6 +1657,7 @@ dependencies = [ "figlet-rs", "fluent-bundle", "fluent-templates", + "itoa", "nom", "pagetop-macros", "paste", @@ -1536,11 +1675,13 @@ dependencies = [ [[package]] name = "pagetop-aliner" -version = "0.0.1" +version = "0.0.9" dependencies = [ + "include_dir", "pagetop", "pagetop-build", "static-files", + "tera", ] [[package]] @@ -1565,9 +1706,11 @@ dependencies = [ name = "pagetop-macros" version = "0.0.13" dependencies = [ + "proc-macro-crate", + "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 2.0.87", ] [[package]] @@ -1593,6 +1736,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + [[package]] name = "paste" version = "1.0.15" @@ -1620,6 +1772,51 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879952a81a83930934cbf1786752d6dedc3b1f29e8f8fb2ad1d0a36f377cf442" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d214365f632b123a47fd913301e14c946c61d1c183ee245fa76eb752e59a02dd" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb55586734301717aea2ac313f50b2eb8f60d2fc3dc01d190eefa2e625f60c4e" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "pest_meta" +version = "2.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75da2a70cf4d9cb76833c990ac9cd3923c9a8905a8929789ce347c84564d03d" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + [[package]] name = "phf" version = "0.11.2" @@ -1630,6 +1827,16 @@ dependencies = [ "phf_shared", ] +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator", + "phf_shared", +] + [[package]] name = "phf_generator" version = "0.11.2" @@ -1650,7 +1857,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn", + "syn 2.0.87", ] [[package]] @@ -1679,7 +1886,7 @@ checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.87", ] [[package]] @@ -1727,6 +1934,39 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro-hack" version = "0.5.20+deprecated" @@ -1933,7 +2173,7 @@ checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.87", ] [[package]] @@ -2030,6 +2270,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slug" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + [[package]] name = "smallvec" version = "1.13.2" @@ -2093,6 +2343,16 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.87" @@ -2112,7 +2372,29 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.87", +] + +[[package]] +name = "tera" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab9d851b45e865f178319da0abdbfe6acbc4328759ff18dafc3a41c16b4cd2ee" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand", + "regex", + "serde", + "serde_json", + "slug", + "unic-segment", ] [[package]] @@ -2142,7 +2424,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.87", ] [[package]] @@ -2274,9 +2556,9 @@ dependencies = [ [[package]] name = "tracing-actix-web" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b87073920bcce23e9f5cb0d2671e9f01d6803bb5229c159b2f5ce6806d73ffc" +checksum = "54a9f5c1aca50ebebf074ee665b9f99f2e84906dcf6b993a0d0090edb835166d" dependencies = [ "actix-web", "mutually_exclusive_features", @@ -2305,7 +2587,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.87", ] [[package]] @@ -2375,6 +2657,33 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + [[package]] name = "unic-langid" version = "0.9.5" @@ -2414,10 +2723,39 @@ checksum = "1ed7f4237ba393424195053097c1516bd4590dc82b84f2f97c5c69e12704555b" dependencies = [ "proc-macro-hack", "quote", - "syn", + "syn 2.0.87", "unic-langid-impl", ] +[[package]] +name = "unic-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" +dependencies = [ + "unic-ucd-segment", +] + +[[package]] +name = "unic-ucd-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + [[package]] name = "unicase" version = "2.8.0" @@ -2540,7 +2878,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.87", "wasm-bindgen-shared", ] @@ -2562,7 +2900,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.87", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2604,6 +2942,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -2793,7 +3140,7 @@ checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.87", "synstructure", ] @@ -2815,7 +3162,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.87", ] [[package]] @@ -2835,7 +3182,7 @@ checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.87", "synstructure", ] @@ -2858,7 +3205,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.87", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 39d7caaf..effe459c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ authors = ["Manuel Cillero "] license = "MIT OR Apache-2.0" [workspace.dependencies] +include_dir = "0.7.4" serde = { version = "1.0", features = ["derive"] } static-files = "0.2.4" diff --git a/helpers/pagetop-build/Cargo.toml b/helpers/pagetop-build/Cargo.toml index cd58a95d..05d039b0 100644 --- a/helpers/pagetop-build/Cargo.toml +++ b/helpers/pagetop-build/Cargo.toml @@ -16,4 +16,4 @@ license = { workspace = true } [dependencies] grass = "0.13.4" -static-files = { workspace = true } +static-files.workspace = true diff --git a/helpers/pagetop-macros/Cargo.toml b/helpers/pagetop-macros/Cargo.toml index 5fc12212..e89c6193 100644 --- a/helpers/pagetop-macros/Cargo.toml +++ b/helpers/pagetop-macros/Cargo.toml @@ -19,5 +19,7 @@ proc-macro = true [dependencies] proc-macro2 = "1.0.89" +proc-macro-crate = "3.2.0" +proc-macro-error = "1.0.4" quote = "1.0.37" syn = { version = "2.0.87", features = ["full"] } diff --git a/helpers/pagetop-macros/README.md b/helpers/pagetop-macros/README.md index 58af0ca6..6dd80331 100644 --- a/helpers/pagetop-macros/README.md +++ b/helpers/pagetop-macros/README.md @@ -25,10 +25,16 @@ frequent changes. Production use is not recommended until version **0.1.0**. # 🔖 Credits -This crate includes an adapted version of [SmartDefault](https://crates.io/crates/smart_default) -(version 0.7.1) by [Jane Doe](https://crates.io/users/jane-doe), named `AutoDefault`, to streamline -Default implementations in **PageTop** projects and eliminate the need to explicitly add -`smart_default` to `Cargo.toml` files. +This crate includes an adapted version of [maud-macros](https://crates.io/crates/maud_macros), +version [0.25.0](https://github.com/lambda-fairy/maud/tree/v0.25.0/maud_macros), by +[Chris Wong](https://crates.io/users/lambda-fairy). + +It also includes an adapted version of [SmartDefault](https://crates.io/crates/smart_default) +(version 0.7.1) by [Jane Doe](https://crates.io/users/jane-doe), renamed as `AutoDefault`, to +streamline the implementation of `Default` in **PageTop** projects. + +Both adaptations eliminate the need to explicitly add `maud` or `smart_default` as dependencies in +`Cargo.toml` files. # 📜 License diff --git a/helpers/pagetop-macros/src/lib.rs b/helpers/pagetop-macros/src/lib.rs index 5d673945..006b92d4 100644 --- a/helpers/pagetop-macros/src/lib.rs +++ b/helpers/pagetop-macros/src/lib.rs @@ -1,9 +1,17 @@ +mod maud; mod smart_default; use proc_macro::TokenStream; +use proc_macro_error::proc_macro_error; use quote::quote; use syn::{parse_macro_input, DeriveInput}; +#[proc_macro] +#[proc_macro_error] +pub fn html(input: TokenStream) -> TokenStream { + maud::expand(input.into()).into() +} + #[proc_macro_derive(AutoDefault, attributes(default))] pub fn derive_auto_default(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); diff --git a/helpers/pagetop-macros/src/maud.rs b/helpers/pagetop-macros/src/maud.rs new file mode 100644 index 00000000..a4e7873f --- /dev/null +++ b/helpers/pagetop-macros/src/maud.rs @@ -0,0 +1,39 @@ +// #![doc(html_root_url = "https://docs.rs/maud_macros/0.25.0")] +// TokenStream values are reference counted, and the mental overhead of tracking +// lifetimes outweighs the marginal gains from explicit borrowing +// #![allow(clippy::needless_pass_by_value)] + +mod ast; +mod escape; +mod generate; +mod parse; + +use proc_macro2::{Ident, Span, TokenStream, TokenTree}; +use proc_macro_crate::{crate_name, FoundCrate}; +use quote::quote; + +pub fn expand(input: TokenStream) -> TokenStream { + let output_ident = TokenTree::Ident(Ident::new("__maud_output", Span::mixed_site())); + // Heuristic: the size of the resulting markup tends to correlate with the + // code size of the template itself + let size_hint = input.to_string().len(); + let markups = parse::parse(input); + let stmts = generate::generate(markups, output_ident.clone()); + + let found_crate = crate_name("pagetop").expect("pagetop is present in `Cargo.toml`"); + let pre_escaped = match found_crate { + FoundCrate::Itself => quote!( + crate::html::PreEscaped(#output_ident) + ), + _ => quote!( + pagetop::html::PreEscaped(#output_ident) + ), + }; + + quote!({ + extern crate alloc; + let mut #output_ident = alloc::string::String::with_capacity(#size_hint); + #stmts + #pre_escaped + }) +} diff --git a/helpers/pagetop-macros/src/maud/ast.rs b/helpers/pagetop-macros/src/maud/ast.rs new file mode 100644 index 00000000..cd8a2cef --- /dev/null +++ b/helpers/pagetop-macros/src/maud/ast.rs @@ -0,0 +1,221 @@ +use proc_macro2::{TokenStream, TokenTree}; +use proc_macro_error::SpanRange; + +#[derive(Debug)] +pub enum Markup { + /// Used as a placeholder value on parse error. + ParseError { + span: SpanRange, + }, + Block(Block), + Literal { + content: String, + span: SpanRange, + }, + Symbol { + symbol: TokenStream, + }, + Splice { + expr: TokenStream, + outer_span: SpanRange, + }, + Element { + name: TokenStream, + attrs: Vec, + body: ElementBody, + }, + Let { + at_span: SpanRange, + tokens: TokenStream, + }, + Special { + segments: Vec, + }, + Match { + at_span: SpanRange, + head: TokenStream, + arms: Vec, + arms_span: SpanRange, + }, +} + +impl Markup { + pub fn span(&self) -> SpanRange { + match *self { + Markup::ParseError { span } => span, + Markup::Block(ref block) => block.span(), + Markup::Literal { span, .. } => span, + Markup::Symbol { ref symbol } => span_tokens(symbol.clone()), + Markup::Splice { outer_span, .. } => outer_span, + Markup::Element { + ref name, ref body, .. + } => { + let name_span = span_tokens(name.clone()); + name_span.join_range(body.span()) + } + Markup::Let { + at_span, + ref tokens, + } => at_span.join_range(span_tokens(tokens.clone())), + Markup::Special { ref segments } => join_ranges(segments.iter().map(Special::span)), + Markup::Match { + at_span, arms_span, .. + } => at_span.join_range(arms_span), + } + } +} + +#[derive(Debug)] +pub enum Attr { + Class { + dot_span: SpanRange, + name: Markup, + toggler: Option, + }, + Id { + hash_span: SpanRange, + name: Markup, + }, + Named { + named_attr: NamedAttr, + }, +} + +impl Attr { + pub fn span(&self) -> SpanRange { + match *self { + Attr::Class { + dot_span, + ref name, + ref toggler, + } => { + let name_span = name.span(); + let dot_name_span = dot_span.join_range(name_span); + if let Some(toggler) = toggler { + dot_name_span.join_range(toggler.cond_span) + } else { + dot_name_span + } + } + Attr::Id { + hash_span, + ref name, + } => { + let name_span = name.span(); + hash_span.join_range(name_span) + } + Attr::Named { ref named_attr } => named_attr.span(), + } + } +} + +#[derive(Debug)] +pub enum ElementBody { + Void { semi_span: SpanRange }, + Block { block: Block }, +} + +impl ElementBody { + pub fn span(&self) -> SpanRange { + match *self { + ElementBody::Void { semi_span } => semi_span, + ElementBody::Block { ref block } => block.span(), + } + } +} + +#[derive(Debug)] +pub struct Block { + pub markups: Vec, + pub outer_span: SpanRange, +} + +impl Block { + pub fn span(&self) -> SpanRange { + self.outer_span + } +} + +#[derive(Debug)] +pub struct Special { + pub at_span: SpanRange, + pub head: TokenStream, + pub body: Block, +} + +impl Special { + pub fn span(&self) -> SpanRange { + let body_span = self.body.span(); + self.at_span.join_range(body_span) + } +} + +#[derive(Debug)] +pub struct NamedAttr { + pub name: TokenStream, + pub attr_type: AttrType, +} + +impl NamedAttr { + fn span(&self) -> SpanRange { + let name_span = span_tokens(self.name.clone()); + if let Some(attr_type_span) = self.attr_type.span() { + name_span.join_range(attr_type_span) + } else { + name_span + } + } +} + +#[derive(Debug)] +pub enum AttrType { + Normal { value: Markup }, + Optional { toggler: Toggler }, + Empty { toggler: Option }, +} + +impl AttrType { + fn span(&self) -> Option { + match *self { + AttrType::Normal { ref value } => Some(value.span()), + AttrType::Optional { ref toggler } => Some(toggler.span()), + AttrType::Empty { ref toggler } => toggler.as_ref().map(Toggler::span), + } + } +} + +#[derive(Debug)] +pub struct Toggler { + pub cond: TokenStream, + pub cond_span: SpanRange, +} + +impl Toggler { + fn span(&self) -> SpanRange { + self.cond_span + } +} + +#[derive(Debug)] +pub struct MatchArm { + pub head: TokenStream, + pub body: Block, +} + +pub fn span_tokens>(tokens: I) -> SpanRange { + join_ranges(tokens.into_iter().map(|s| SpanRange::single_span(s.span()))) +} + +pub fn join_ranges>(ranges: I) -> SpanRange { + let mut iter = ranges.into_iter(); + let first = match iter.next() { + Some(span) => span, + None => return SpanRange::call_site(), + }; + let last = iter.last().unwrap_or(first); + first.join_range(last) +} + +pub fn name_to_string(name: TokenStream) -> String { + name.into_iter().map(|token| token.to_string()).collect() +} diff --git a/helpers/pagetop-macros/src/maud/escape.rs b/helpers/pagetop-macros/src/maud/escape.rs new file mode 100644 index 00000000..49ece776 --- /dev/null +++ b/helpers/pagetop-macros/src/maud/escape.rs @@ -0,0 +1,34 @@ +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +// !!!!!!!! PLEASE KEEP THIS IN SYNC WITH `maud/src/escape.rs` !!!!!!!!! +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +extern crate alloc; + +use alloc::string::String; + +pub fn escape_to_string(input: &str, output: &mut String) { + for b in input.bytes() { + match b { + b'&' => output.push_str("&"), + b'<' => output.push_str("<"), + b'>' => output.push_str(">"), + b'"' => output.push_str("""), + _ => unsafe { output.as_mut_vec().push(b) }, + } + } +} + +#[cfg(test)] +mod test { + extern crate alloc; + + use super::escape_to_string; + use alloc::string::String; + + #[test] + fn it_works() { + let mut s = String::new(); + escape_to_string("", &mut s); + assert_eq!(s, "<script>launchMissiles()</script>"); + } +} diff --git a/helpers/pagetop-macros/src/maud/generate.rs b/helpers/pagetop-macros/src/maud/generate.rs new file mode 100644 index 00000000..be7946d0 --- /dev/null +++ b/helpers/pagetop-macros/src/maud/generate.rs @@ -0,0 +1,308 @@ +use proc_macro2::{Delimiter, Group, Ident, Literal, Span, TokenStream, TokenTree}; +use proc_macro_error::SpanRange; +use quote::quote; + +use crate::maud::{ast::*, escape}; + +use proc_macro_crate::{crate_name, FoundCrate}; + +pub fn generate(markups: Vec, output_ident: TokenTree) -> TokenStream { + let mut build = Builder::new(output_ident.clone()); + Generator::new(output_ident).markups(markups, &mut build); + build.finish() +} + +struct Generator { + output_ident: TokenTree, +} + +impl Generator { + fn new(output_ident: TokenTree) -> Generator { + Generator { output_ident } + } + + fn builder(&self) -> Builder { + Builder::new(self.output_ident.clone()) + } + + fn markups(&self, markups: Vec, build: &mut Builder) { + for markup in markups { + self.markup(markup, build); + } + } + + fn markup(&self, markup: Markup, build: &mut Builder) { + match markup { + Markup::ParseError { .. } => {} + Markup::Block(Block { + markups, + outer_span, + }) => { + if markups + .iter() + .any(|markup| matches!(*markup, Markup::Let { .. })) + { + self.block( + Block { + markups, + outer_span, + }, + build, + ); + } else { + self.markups(markups, build); + } + } + Markup::Literal { content, .. } => build.push_escaped(&content), + Markup::Symbol { symbol } => self.name(symbol, build), + Markup::Splice { expr, .. } => self.splice(expr, build), + Markup::Element { name, attrs, body } => self.element(name, attrs, body, build), + Markup::Let { tokens, .. } => build.push_tokens(tokens), + Markup::Special { segments } => { + for Special { head, body, .. } in segments { + build.push_tokens(head); + self.block(body, build); + } + } + Markup::Match { + head, + arms, + arms_span, + .. + } => { + let body = { + let mut build = self.builder(); + for MatchArm { head, body } in arms { + build.push_tokens(head); + self.block(body, &mut build); + } + build.finish() + }; + let mut body = TokenTree::Group(Group::new(Delimiter::Brace, body)); + body.set_span(arms_span.collapse()); + build.push_tokens(quote!(#head #body)); + } + } + } + + fn block( + &self, + Block { + markups, + outer_span, + }: Block, + build: &mut Builder, + ) { + let block = { + let mut build = self.builder(); + self.markups(markups, &mut build); + build.finish() + }; + let mut block = TokenTree::Group(Group::new(Delimiter::Brace, block)); + block.set_span(outer_span.collapse()); + build.push_tokens(TokenStream::from(block)); + } + + fn splice(&self, expr: TokenStream, build: &mut Builder) { + let output_ident = self.output_ident.clone(); + + let found_crate = crate_name("pagetop").expect("pagetop is present in `Cargo.toml`"); + build.push_tokens(match found_crate { + FoundCrate::Itself => quote!( + crate::html::html_private::render_to!(&#expr, &mut #output_ident); + ), + _ => quote!( + pagetop::html::html_private::render_to!(&#expr, &mut #output_ident); + ), + }); + } + + fn element(&self, name: TokenStream, attrs: Vec, body: ElementBody, build: &mut Builder) { + build.push_str("<"); + self.name(name.clone(), build); + self.attrs(attrs, build); + build.push_str(">"); + if let ElementBody::Block { block } = body { + self.markups(block.markups, build); + build.push_str(""); + } + } + + fn name(&self, name: TokenStream, build: &mut Builder) { + build.push_escaped(&name_to_string(name)); + } + + fn attrs(&self, attrs: Vec, build: &mut Builder) { + for NamedAttr { name, attr_type } in desugar_attrs(attrs) { + match attr_type { + AttrType::Normal { value } => { + build.push_str(" "); + self.name(name, build); + build.push_str("=\""); + self.markup(value, build); + build.push_str("\""); + } + AttrType::Optional { + toggler: Toggler { cond, .. }, + } => { + let inner_value = quote!(inner_value); + let body = { + let mut build = self.builder(); + build.push_str(" "); + self.name(name, &mut build); + build.push_str("=\""); + self.splice(inner_value.clone(), &mut build); + build.push_str("\""); + build.finish() + }; + build.push_tokens(quote!(if let Some(#inner_value) = (#cond) { #body })); + } + AttrType::Empty { toggler: None } => { + build.push_str(" "); + self.name(name, build); + } + AttrType::Empty { + toggler: Some(Toggler { cond, .. }), + } => { + let body = { + let mut build = self.builder(); + build.push_str(" "); + self.name(name, &mut build); + build.finish() + }; + build.push_tokens(quote!(if (#cond) { #body })); + } + } + } + } +} + +//////////////////////////////////////////////////////// + +fn desugar_attrs(attrs: Vec) -> Vec { + let mut classes_static = vec![]; + let mut classes_toggled = vec![]; + let mut ids = vec![]; + let mut named_attrs = vec![]; + for attr in attrs { + match attr { + Attr::Class { + name, + toggler: Some(toggler), + .. + } => classes_toggled.push((name, toggler)), + Attr::Class { + name, + toggler: None, + .. + } => classes_static.push(name), + Attr::Id { name, .. } => ids.push(name), + Attr::Named { named_attr } => named_attrs.push(named_attr), + } + } + let classes = desugar_classes_or_ids("class", classes_static, classes_toggled); + let ids = desugar_classes_or_ids("id", ids, vec![]); + classes.into_iter().chain(ids).chain(named_attrs).collect() +} + +fn desugar_classes_or_ids( + attr_name: &'static str, + values_static: Vec, + values_toggled: Vec<(Markup, Toggler)>, +) -> Option { + if values_static.is_empty() && values_toggled.is_empty() { + return None; + } + let mut markups = Vec::new(); + let mut leading_space = false; + for name in values_static { + markups.extend(prepend_leading_space(name, &mut leading_space)); + } + for (name, Toggler { cond, cond_span }) in values_toggled { + let body = Block { + markups: prepend_leading_space(name, &mut leading_space), + // TODO: is this correct? + outer_span: cond_span, + }; + markups.push(Markup::Special { + segments: vec![Special { + at_span: SpanRange::call_site(), + head: quote!(if (#cond)), + body, + }], + }); + } + Some(NamedAttr { + name: TokenStream::from(TokenTree::Ident(Ident::new(attr_name, Span::call_site()))), + attr_type: AttrType::Normal { + value: Markup::Block(Block { + markups, + outer_span: SpanRange::call_site(), + }), + }, + }) +} + +fn prepend_leading_space(name: Markup, leading_space: &mut bool) -> Vec { + let mut markups = Vec::new(); + if *leading_space { + markups.push(Markup::Literal { + content: " ".to_owned(), + span: name.span(), + }); + } + *leading_space = true; + markups.push(name); + markups +} + +//////////////////////////////////////////////////////// + +struct Builder { + output_ident: TokenTree, + tokens: Vec, + tail: String, +} + +impl Builder { + fn new(output_ident: TokenTree) -> Builder { + Builder { + output_ident, + tokens: Vec::new(), + tail: String::new(), + } + } + + fn push_str(&mut self, string: &str) { + self.tail.push_str(string); + } + + fn push_escaped(&mut self, string: &str) { + escape::escape_to_string(string, &mut self.tail); + } + + fn push_tokens(&mut self, tokens: TokenStream) { + self.cut(); + self.tokens.extend(tokens); + } + + fn cut(&mut self) { + if self.tail.is_empty() { + return; + } + let push_str_expr = { + let output_ident = self.output_ident.clone(); + let string = TokenTree::Literal(Literal::string(&self.tail)); + quote!(#output_ident.push_str(#string);) + }; + self.tail.clear(); + self.tokens.extend(push_str_expr); + } + + fn finish(mut self) -> TokenStream { + self.cut(); + self.tokens.into_iter().collect() + } +} diff --git a/helpers/pagetop-macros/src/maud/parse.rs b/helpers/pagetop-macros/src/maud/parse.rs new file mode 100644 index 00000000..d24cea6e --- /dev/null +++ b/helpers/pagetop-macros/src/maud/parse.rs @@ -0,0 +1,752 @@ +use proc_macro2::{Delimiter, Ident, Literal, Spacing, Span, TokenStream, TokenTree}; +use proc_macro_error::{abort, abort_call_site, emit_error, SpanRange}; +use std::collections::HashMap; + +use syn::Lit; + +use crate::maud::ast; + +pub fn parse(input: TokenStream) -> Vec { + Parser::new(input).markups() +} + +#[derive(Clone)] +struct Parser { + /// If we're inside an attribute, then this contains the attribute name. + current_attr: Option, + input: ::IntoIter, +} + +impl Iterator for Parser { + type Item = TokenTree; + + fn next(&mut self) -> Option { + self.input.next() + } +} + +impl Parser { + fn new(input: TokenStream) -> Parser { + Parser { + current_attr: None, + input: input.into_iter(), + } + } + + fn with_input(&self, input: TokenStream) -> Parser { + Parser { + current_attr: self.current_attr.clone(), + input: input.into_iter(), + } + } + + /// Returns the next token in the stream without consuming it. + fn peek(&mut self) -> Option { + self.clone().next() + } + + /// Returns the next two tokens in the stream without consuming them. + fn peek2(&mut self) -> Option<(TokenTree, Option)> { + let mut clone = self.clone(); + clone.next().map(|first| (first, clone.next())) + } + + /// Advances the cursor by one step. + fn advance(&mut self) { + self.next(); + } + + /// Advances the cursor by two steps. + fn advance2(&mut self) { + self.next(); + self.next(); + } + + /// Parses multiple blocks of markup. + fn markups(&mut self) -> Vec { + let mut result = Vec::new(); + loop { + match self.peek2() { + None => break, + Some((TokenTree::Punct(ref punct), _)) if punct.as_char() == ';' => self.advance(), + Some((TokenTree::Punct(ref punct), Some(TokenTree::Ident(ref ident)))) + if punct.as_char() == '@' && *ident == "let" => + { + self.advance2(); + let keyword = TokenTree::Ident(ident.clone()); + result.push(self.let_expr(punct.span(), keyword)); + } + _ => result.push(self.markup()), + } + } + result + } + + /// Parses a single block of markup. + fn markup(&mut self) -> ast::Markup { + let token = match self.peek() { + Some(token) => token, + None => { + abort_call_site!("unexpected end of input"); + } + }; + let markup = match token { + // Literal + TokenTree::Literal(literal) => { + self.advance(); + self.literal(literal) + } + // Special form + TokenTree::Punct(ref punct) if punct.as_char() == '@' => { + self.advance(); + let at_span = punct.span(); + match self.next() { + Some(TokenTree::Ident(ident)) => { + let keyword = TokenTree::Ident(ident.clone()); + match ident.to_string().as_str() { + "if" => { + let mut segments = Vec::new(); + self.if_expr(at_span, vec![keyword], &mut segments); + ast::Markup::Special { segments } + } + "while" => self.while_expr(at_span, keyword), + "for" => self.for_expr(at_span, keyword), + "match" => self.match_expr(at_span, keyword), + "let" => { + let span = SpanRange { + first: at_span, + last: ident.span(), + }; + abort!(span, "`@let` only works inside a block"); + } + other => { + let span = SpanRange { + first: at_span, + last: ident.span(), + }; + abort!(span, "unknown keyword `@{}`", other); + } + } + } + _ => { + abort!(at_span, "expected keyword after `@`"); + } + } + } + // Element + TokenTree::Ident(ident) => { + let ident_string = ident.to_string(); + match ident_string.as_str() { + "if" | "while" | "for" | "match" | "let" => { + abort!( + ident, + "found keyword `{}`", ident_string; + help = "should this be a `@{}`?", ident_string + ); + } + "true" | "false" => { + if let Some(attr_name) = &self.current_attr { + emit_error!( + ident, + r#"attribute value must be a string"#; + help = "to declare an empty attribute, omit the equals sign: `{}`", + attr_name; + help = "to toggle the attribute, use square brackets: `{}[some_boolean_flag]`", + attr_name; + ); + return ast::Markup::ParseError { + span: SpanRange::single_span(ident.span()), + }; + } + } + _ => {} + } + + // `.try_namespaced_name()` should never fail as we've + // already seen an `Ident` + let name = self.try_namespaced_name().expect("identifier"); + self.element(name) + } + // Div element shorthand + TokenTree::Punct(ref punct) if punct.as_char() == '.' || punct.as_char() == '#' => { + let name = TokenTree::Ident(Ident::new("div", punct.span())); + self.element(name.into()) + } + // Splice + TokenTree::Group(ref group) if group.delimiter() == Delimiter::Parenthesis => { + self.advance(); + ast::Markup::Splice { + expr: group.stream(), + outer_span: SpanRange::single_span(group.span()), + } + } + // Block + TokenTree::Group(ref group) if group.delimiter() == Delimiter::Brace => { + self.advance(); + ast::Markup::Block(self.block(group.stream(), SpanRange::single_span(group.span()))) + } + // ??? + token => { + abort!(token, "invalid syntax"); + } + }; + markup + } + + /// Parses a literal string. + fn literal(&mut self, literal: Literal) -> ast::Markup { + match Lit::new(literal.clone()) { + Lit::Str(lit_str) => { + return ast::Markup::Literal { + content: lit_str.value(), + span: SpanRange::single_span(literal.span()), + } + } + // Boolean literals are idents, so `Lit::Bool` is handled in + // `markup`, not here. + Lit::Int(..) | Lit::Float(..) => { + emit_error!(literal, r#"literal must be double-quoted: `"{}"`"#, literal); + } + Lit::Char(lit_char) => { + emit_error!( + literal, + r#"literal must be double-quoted: `"{}"`"#, + lit_char.value(), + ); + } + _ => { + emit_error!(literal, "expected string"); + } + } + ast::Markup::ParseError { + span: SpanRange::single_span(literal.span()), + } + } + + /// Parses an `@if` expression. + /// + /// The leading `@if` should already be consumed. + fn if_expr(&mut self, at_span: Span, prefix: Vec, segments: &mut Vec) { + let mut head = prefix; + let body = loop { + match self.next() { + Some(TokenTree::Group(ref block)) if block.delimiter() == Delimiter::Brace => { + break self.block(block.stream(), SpanRange::single_span(block.span())); + } + Some(token) => head.push(token), + None => { + let mut span = ast::span_tokens(head); + span.first = at_span; + abort!(span, "expected body for this `@if`"); + } + } + }; + segments.push(ast::Special { + at_span: SpanRange::single_span(at_span), + head: head.into_iter().collect(), + body, + }); + self.else_if_expr(segments) + } + + /// Parses an optional `@else if` or `@else`. + /// + /// The leading `@else if` or `@else` should *not* already be consumed. + fn else_if_expr(&mut self, segments: &mut Vec) { + match self.peek2() { + Some((TokenTree::Punct(ref punct), Some(TokenTree::Ident(ref else_keyword)))) + if punct.as_char() == '@' && *else_keyword == "else" => + { + self.advance2(); + let at_span = punct.span(); + let else_keyword = TokenTree::Ident(else_keyword.clone()); + match self.peek() { + // `@else if` + Some(TokenTree::Ident(ref if_keyword)) if *if_keyword == "if" => { + self.advance(); + let if_keyword = TokenTree::Ident(if_keyword.clone()); + self.if_expr(at_span, vec![else_keyword, if_keyword], segments) + } + // Just an `@else` + _ => match self.next() { + Some(TokenTree::Group(ref group)) + if group.delimiter() == Delimiter::Brace => + { + let body = + self.block(group.stream(), SpanRange::single_span(group.span())); + segments.push(ast::Special { + at_span: SpanRange::single_span(at_span), + head: vec![else_keyword].into_iter().collect(), + body, + }); + } + _ => { + let span = SpanRange { + first: at_span, + last: else_keyword.span(), + }; + abort!(span, "expected body for this `@else`"); + } + }, + } + } + // We didn't find an `@else`; stop + _ => {} + } + } + + /// Parses an `@while` expression. + /// + /// The leading `@while` should already be consumed. + fn while_expr(&mut self, at_span: Span, keyword: TokenTree) -> ast::Markup { + let keyword_span = keyword.span(); + let mut head = vec![keyword]; + let body = loop { + match self.next() { + Some(TokenTree::Group(ref block)) if block.delimiter() == Delimiter::Brace => { + break self.block(block.stream(), SpanRange::single_span(block.span())); + } + Some(token) => head.push(token), + None => { + let span = SpanRange { + first: at_span, + last: keyword_span, + }; + abort!(span, "expected body for this `@while`"); + } + } + }; + ast::Markup::Special { + segments: vec![ast::Special { + at_span: SpanRange::single_span(at_span), + head: head.into_iter().collect(), + body, + }], + } + } + + /// Parses a `@for` expression. + /// + /// The leading `@for` should already be consumed. + fn for_expr(&mut self, at_span: Span, keyword: TokenTree) -> ast::Markup { + let keyword_span = keyword.span(); + let mut head = vec![keyword]; + loop { + match self.next() { + Some(TokenTree::Ident(ref in_keyword)) if *in_keyword == "in" => { + head.push(TokenTree::Ident(in_keyword.clone())); + break; + } + Some(token) => head.push(token), + None => { + let span = SpanRange { + first: at_span, + last: keyword_span, + }; + abort!(span, "missing `in` in `@for` loop"); + } + } + } + let body = loop { + match self.next() { + Some(TokenTree::Group(ref block)) if block.delimiter() == Delimiter::Brace => { + break self.block(block.stream(), SpanRange::single_span(block.span())); + } + Some(token) => head.push(token), + None => { + let span = SpanRange { + first: at_span, + last: keyword_span, + }; + abort!(span, "expected body for this `@for`"); + } + } + }; + ast::Markup::Special { + segments: vec![ast::Special { + at_span: SpanRange::single_span(at_span), + head: head.into_iter().collect(), + body, + }], + } + } + + /// Parses a `@match` expression. + /// + /// The leading `@match` should already be consumed. + fn match_expr(&mut self, at_span: Span, keyword: TokenTree) -> ast::Markup { + let keyword_span = keyword.span(); + let mut head = vec![keyword]; + let (arms, arms_span) = loop { + match self.next() { + Some(TokenTree::Group(ref body)) if body.delimiter() == Delimiter::Brace => { + let span = SpanRange::single_span(body.span()); + break (self.with_input(body.stream()).match_arms(), span); + } + Some(token) => head.push(token), + None => { + let span = SpanRange { + first: at_span, + last: keyword_span, + }; + abort!(span, "expected body for this `@match`"); + } + } + }; + ast::Markup::Match { + at_span: SpanRange::single_span(at_span), + head: head.into_iter().collect(), + arms, + arms_span, + } + } + + fn match_arms(&mut self) -> Vec { + let mut arms = Vec::new(); + while let Some(arm) = self.match_arm() { + arms.push(arm); + } + arms + } + + fn match_arm(&mut self) -> Option { + let mut head = Vec::new(); + loop { + match self.peek2() { + Some((TokenTree::Punct(ref eq), Some(TokenTree::Punct(ref gt)))) + if eq.as_char() == '=' + && gt.as_char() == '>' + && eq.spacing() == Spacing::Joint => + { + self.advance2(); + head.push(TokenTree::Punct(eq.clone())); + head.push(TokenTree::Punct(gt.clone())); + break; + } + Some((token, _)) => { + self.advance(); + head.push(token); + } + None => { + if head.is_empty() { + return None; + } else { + let head_span = ast::span_tokens(head); + abort!(head_span, "unexpected end of @match pattern"); + } + } + } + } + let body = match self.next() { + // $pat => { $stmts } + Some(TokenTree::Group(ref body)) if body.delimiter() == Delimiter::Brace => { + let body = self.block(body.stream(), SpanRange::single_span(body.span())); + // Trailing commas are optional if the match arm is a braced block + if let Some(TokenTree::Punct(ref punct)) = self.peek() { + if punct.as_char() == ',' { + self.advance(); + } + } + body + } + // $pat => $expr + Some(first_token) => { + let mut span = SpanRange::single_span(first_token.span()); + let mut body = vec![first_token]; + loop { + match self.next() { + Some(TokenTree::Punct(ref punct)) if punct.as_char() == ',' => break, + Some(token) => { + span.last = token.span(); + body.push(token); + } + None => break, + } + } + self.block(body.into_iter().collect(), span) + } + None => { + let span = ast::span_tokens(head); + abort!(span, "unexpected end of @match arm"); + } + }; + Some(ast::MatchArm { + head: head.into_iter().collect(), + body, + }) + } + + /// Parses a `@let` expression. + /// + /// The leading `@let` should already be consumed. + fn let_expr(&mut self, at_span: Span, keyword: TokenTree) -> ast::Markup { + let mut tokens = vec![keyword]; + loop { + match self.next() { + Some(token) => match token { + TokenTree::Punct(ref punct) if punct.as_char() == '=' => { + tokens.push(token.clone()); + break; + } + _ => tokens.push(token), + }, + None => { + let mut span = ast::span_tokens(tokens); + span.first = at_span; + abort!(span, "unexpected end of `@let` expression"); + } + } + } + loop { + match self.next() { + Some(token) => match token { + TokenTree::Punct(ref punct) if punct.as_char() == ';' => { + tokens.push(token.clone()); + break; + } + _ => tokens.push(token), + }, + None => { + let mut span = ast::span_tokens(tokens); + span.first = at_span; + abort!( + span, + "unexpected end of `@let` expression"; + help = "are you missing a semicolon?" + ); + } + } + } + ast::Markup::Let { + at_span: SpanRange::single_span(at_span), + tokens: tokens.into_iter().collect(), + } + } + + /// Parses an element node. + /// + /// The element name should already be consumed. + fn element(&mut self, name: TokenStream) -> ast::Markup { + if self.current_attr.is_some() { + let span = ast::span_tokens(name); + abort!(span, "unexpected element"); + } + let attrs = self.attrs(); + let body = match self.peek() { + Some(TokenTree::Punct(ref punct)) + if punct.as_char() == ';' || punct.as_char() == '/' => + { + // Void element + self.advance(); + if punct.as_char() == '/' { + emit_error!( + punct, + "void elements must use `;`, not `/`"; + help = "change this to `;`"; + help = "see https://github.com/lambda-fairy/maud/pull/315 for details"; + ); + } + ast::ElementBody::Void { + semi_span: SpanRange::single_span(punct.span()), + } + } + Some(_) => match self.markup() { + ast::Markup::Block(block) => ast::ElementBody::Block { block }, + markup => { + let markup_span = markup.span(); + abort!( + markup_span, + "element body must be wrapped in braces"; + help = "see https://github.com/lambda-fairy/maud/pull/137 for details" + ); + } + }, + None => abort_call_site!("expected `;`, found end of macro"), + }; + ast::Markup::Element { name, attrs, body } + } + + /// Parses the attributes of an element. + fn attrs(&mut self) -> Vec { + let mut attrs = Vec::new(); + loop { + if let Some(name) = self.try_namespaced_name() { + // Attribute + match self.peek() { + // Non-empty attribute + Some(TokenTree::Punct(ref punct)) if punct.as_char() == '=' => { + self.advance(); + // Parse a value under an attribute context + assert!(self.current_attr.is_none()); + self.current_attr = Some(ast::name_to_string(name.clone())); + let attr_type = match self.attr_toggler() { + Some(toggler) => ast::AttrType::Optional { toggler }, + None => { + let value = self.markup(); + ast::AttrType::Normal { value } + } + }; + self.current_attr = None; + attrs.push(ast::Attr::Named { + named_attr: ast::NamedAttr { name, attr_type }, + }); + } + // Empty attribute (legacy syntax) + Some(TokenTree::Punct(ref punct)) if punct.as_char() == '?' => { + self.advance(); + let toggler = self.attr_toggler(); + attrs.push(ast::Attr::Named { + named_attr: ast::NamedAttr { + name: name.clone(), + attr_type: ast::AttrType::Empty { toggler }, + }, + }); + } + // Empty attribute (new syntax) + _ => { + let toggler = self.attr_toggler(); + attrs.push(ast::Attr::Named { + named_attr: ast::NamedAttr { + name: name.clone(), + attr_type: ast::AttrType::Empty { toggler }, + }, + }); + } + } + } else { + match self.peek() { + // Class shorthand + Some(TokenTree::Punct(ref punct)) if punct.as_char() == '.' => { + self.advance(); + let name = self.class_or_id_name(); + let toggler = self.attr_toggler(); + attrs.push(ast::Attr::Class { + dot_span: SpanRange::single_span(punct.span()), + name, + toggler, + }); + } + // ID shorthand + Some(TokenTree::Punct(ref punct)) if punct.as_char() == '#' => { + self.advance(); + let name = self.class_or_id_name(); + attrs.push(ast::Attr::Id { + hash_span: SpanRange::single_span(punct.span()), + name, + }); + } + // If it's not a valid attribute, backtrack and bail out + _ => break, + } + } + } + + let mut attr_map: HashMap> = HashMap::new(); + let mut has_class = false; + for attr in &attrs { + let name = match attr { + ast::Attr::Class { .. } => { + if has_class { + // Only check the first class to avoid spurious duplicates + continue; + } + has_class = true; + "class".to_string() + } + ast::Attr::Id { .. } => "id".to_string(), + ast::Attr::Named { named_attr } => named_attr + .name + .clone() + .into_iter() + .map(|token| token.to_string()) + .collect(), + }; + let entry = attr_map.entry(name).or_default(); + entry.push(attr.span()); + } + + for (name, spans) in attr_map { + if spans.len() > 1 { + let mut spans = spans.into_iter(); + let first_span = spans.next().expect("spans should be non-empty"); + abort!(first_span, "duplicate attribute `{}`", name); + } + } + + attrs + } + + /// Parses the name of a class or ID. + fn class_or_id_name(&mut self) -> ast::Markup { + if let Some(symbol) = self.try_name() { + ast::Markup::Symbol { symbol } + } else { + self.markup() + } + } + + /// Parses the `[cond]` syntax after an empty attribute or class shorthand. + fn attr_toggler(&mut self) -> Option { + match self.peek() { + Some(TokenTree::Group(ref group)) if group.delimiter() == Delimiter::Bracket => { + self.advance(); + Some(ast::Toggler { + cond: group.stream(), + cond_span: SpanRange::single_span(group.span()), + }) + } + _ => None, + } + } + + /// Parses an identifier, without dealing with namespaces. + fn try_name(&mut self) -> Option { + let mut result = Vec::new(); + if let Some(token @ TokenTree::Ident(_)) = self.peek() { + self.advance(); + result.push(token); + } else { + return None; + } + let mut expect_ident = false; + loop { + expect_ident = match self.peek() { + Some(TokenTree::Punct(ref punct)) if punct.as_char() == '-' => { + self.advance(); + result.push(TokenTree::Punct(punct.clone())); + true + } + Some(TokenTree::Ident(ref ident)) if expect_ident => { + self.advance(); + result.push(TokenTree::Ident(ident.clone())); + false + } + _ => break, + }; + } + Some(result.into_iter().collect()) + } + + /// Parses a HTML element or attribute name, along with a namespace + /// if necessary. + fn try_namespaced_name(&mut self) -> Option { + let mut result = vec![self.try_name()?]; + if let Some(TokenTree::Punct(ref punct)) = self.peek() { + if punct.as_char() == ':' { + self.advance(); + result.push(TokenStream::from(TokenTree::Punct(punct.clone()))); + result.push(self.try_name()?); + } + } + Some(result.into_iter().collect()) + } + + /// Parses the given token stream as a Maud expression. + fn block(&mut self, body: TokenStream, outer_span: SpanRange) -> ast::Block { + let markups = self.with_input(body).markups(); + ast::Block { + markups, + outer_span, + } + } +} diff --git a/packages/drust/Cargo.toml b/packages/drust/Cargo.toml index 2dd5751c..e44c6a3d 100644 --- a/packages/drust/Cargo.toml +++ b/packages/drust/Cargo.toml @@ -12,10 +12,10 @@ authors = { workspace = true } license = { workspace = true } [dependencies] -pagetop = { workspace = true } +pagetop.workspace = true # Packages. -pagetop-bootsier = { workspace = true } +pagetop-bootsier.workspace = true #pagetop-admin = { version = "0.0", path = "../pagetop-admin" } #pagetop-user = { version = "0.0", path = "../pagetop-user" } #pagetop-node = { version = "0.0", path = "../pagetop-node" } diff --git a/packages/pagetop-aliner/Cargo.toml b/packages/pagetop-aliner/Cargo.toml index 3c5187b3..c25be723 100644 --- a/packages/pagetop-aliner/Cargo.toml +++ b/packages/pagetop-aliner/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "pagetop-aliner" -version = "0.0.1" +version = "0.0.9" edition = "2021" description = """\ - PageTop default theme.\ + PageTop's default theme for schematic layouts, designed to be extended with subthemes.\ """ categories = ["web-programming", "gui"] keywords = ["pagetop", "theme", "css", "js"] @@ -15,8 +15,12 @@ authors = { workspace = true } license = { workspace = true } [dependencies] -pagetop = { workspace = true } -static-files = { workspace = true } +pagetop.workspace = true + +include_dir.workspace = true +static-files.workspace = true + +tera = "1.20.0" [build-dependencies] -pagetop-build = { workspace = true } +pagetop-build.workspace = true diff --git a/packages/pagetop-aliner/README.md b/packages/pagetop-aliner/README.md index 724f70eb..68634799 100644 --- a/packages/pagetop-aliner/README.md +++ b/packages/pagetop-aliner/README.md @@ -1,16 +1,18 @@
-

PageTop Bootsier

+

PageTop Aliner

-

PageTop theme that uses Bootstrap framework for versatile styles and components.

+

PageTop's default theme for schematic layouts, designed to be extended with subthemes.

[![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?style=for-the-badge)](#-license) -[![API Docs](https://img.shields.io/docsrs/pagetop-bootsier?label=API%20Docs&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop-bootsier) -[![Crates.io](https://img.shields.io/crates/v/pagetop-bootsier.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop-bootsier) -[![Downloads](https://img.shields.io/crates/d/pagetop-bootsier.svg?style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop-bootsier) +[![API Docs](https://img.shields.io/docsrs/pagetop-aliner?label=API%20Docs&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop-aliner) +[![Crates.io](https://img.shields.io/crates/v/pagetop-aliner.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop-aliner) +[![Downloads](https://img.shields.io/crates/d/pagetop-aliner.svg?style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop-aliner)
+PageTop Aliner is the default theme designed to provide schematic layout for easily extending. + # 📦 About PageTop [PageTop](https://docs.rs/pagetop) is an opinionated web framework to build modular *Server-Side diff --git a/packages/pagetop-aliner/_build.rs b/packages/pagetop-aliner/_build.rs deleted file mode 100644 index fd9dc778..00000000 --- a/packages/pagetop-aliner/_build.rs +++ /dev/null @@ -1,19 +0,0 @@ -use pagetop_build::StaticFilesBundle; - -use std::env; -use std::path::Path; - -fn main() -> std::io::Result<()> { - StaticFilesBundle::from_scss("./static/bootstrap-5.3.3/bootstrap.scss", "bootstrap.css") - .with_name("bootsier") - .build()?; - StaticFilesBundle::from_dir("./static/js", Some(bootstrap_js_files)) - .with_name("bootsier-js") - .build() -} - -fn bootstrap_js_files(path: &Path) -> bool { - // No filtering during development, only on "release" compilation. - env::var("PROFILE").unwrap_or_else(|_| "release".to_string()) != "release" - || path.file_name().map_or(false, |n| n == "bootstrap.min.js") -} diff --git a/packages/pagetop-aliner/build.rs b/packages/pagetop-aliner/build.rs new file mode 100644 index 00000000..26713f52 --- /dev/null +++ b/packages/pagetop-aliner/build.rs @@ -0,0 +1,7 @@ +use pagetop_build::StaticFilesBundle; + +fn main() -> std::io::Result<()> { + StaticFilesBundle::from_dir("./static", None) + .with_name("aliner") + .build() +} diff --git a/packages/pagetop-aliner/src/lib.rs b/packages/pagetop-aliner/src/lib.rs index 35276744..414e1a5a 100644 --- a/packages/pagetop-aliner/src/lib.rs +++ b/packages/pagetop-aliner/src/lib.rs @@ -1,8 +1,46 @@ use pagetop::prelude::*; +use include_dir::{include_dir, Dir}; +use tera::Tera; + +use std::sync::LazyLock; + static_locales!(LOCALES_ALINER); -//static_files!(bootsier); +static_files!(aliner); + +// ALINER THEME ************************************************************************************ + +pub const TEMPLATE_GLOB: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*.html"); +pub const TEMPLATE_BASE_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates"); + +/// Static instance of Tera used for rendering HTML templates for components. +/// +/// - In `debug` mode, templates are dynamically loaded from the file system, allowing for rapid +/// iteration. +/// - In `release` mode (`cargo build --release`), templates are embedded directly into the binary +/// for optimal performance and portability. +pub static ALINER_THEME: LazyLock = LazyLock::new(|| { + if cfg!(debug_assertions) { + // In debug mode, load templates directly from the file system. + Tera::new(TEMPLATE_GLOB).expect("Failed to initialize Tera from disk in debug mode") + } else { + // In release mode (cargo build --release), embed templates into the binary. + let mut tera = Tera::default(); + for file in TEMPLATE_BASE_DIR.files() { + if let Some(path) = file.path().to_str() { + let content = file + .contents_utf8() + .expect("Non UTF-8 content in template file"); + tera.add_raw_template(path, content) + .expect("Failed to add template to Tera"); + } + } + tera + } +}); + +// ALINER DEFINITION ******************************************************************************* pub struct Aliner; @@ -10,184 +48,10 @@ impl PackageTrait for Aliner { fn theme(&self) -> Option { Some(&Aliner) } - /* - fn actions(&self) -> Vec { - actions![ - action::theme::BeforePrepare::::new(&Self, before_prepare_icon), - action::theme::BeforePrepare::"; } + +caption::before { content: ""; } +caption::after { content: ""; } +canvas::before { content: ""; } +canvas::after { content: ""; } +center::before { content: "
"; } +center::after { content: "
"; } +cite::before { content: ""; } +cite::after { content: ""; } +code::before { content: ""; } +code::after { content: ""; } +col::before { content: ""; } +col::after { content: ""; } +colgroup::before { content: ""; } +colgroup::after { content: ""; } +command::before { content: ""; } +command::after { content: ""; } +content::before { content: ""; } +content::after { content: ""; } + +data::before { content: ""; } +data::after { content: ""; } +datalist::before { content: ""; } +datalist::after { content: ""; } +dd::before { content: "
"; } +dd::after { content: "
"; } +del::before { content: ""; } +del::after { content: ""; } +details::before { content: "
"; } +details::after { content: "
"; } +dfn::before { content: ""; } +dfn::after { content: ""; } +dialog::before { content: ""; } +dialog::after { content: ""; } +dir::before { content: ""; } +dir::after { content: ""; } +div::before { content: "
"; } +div::after { content: "
"; } +dl::before { content: "
"; } +dl::after { content: "
"; } +dt::before { content: "
"; } +dt::after { content: "
"; } + +element::before { content: ""; } +element::after { content: ""; } +em::before { content: ""; } +em::after { content: ""; } +embed::before { content: ""; } +embed::after { content: ""; } + +fieldset::before { content: "
"; } +fieldset::after { content: "
"; } +figcaption::before { content: "
"; } +figcaption::after { content: "
"; } +figure::before { content: "
"; } +figure::after { content: "
"; } +font::before { content: ""; } +font::after { content: ""; } +footer::before { content: "
"; } +footer::after { content: "
"; } +form::before { content: "
"; } +form::after { content: "
"; } +frame::before { content: ""; } +frame::after { content: ""; } +frameset::before { content: ""; } +frameset::after { content: ""; } + +h1::before { content: "

"; } +h1::after { content: "

"; } +h2::before { content: "

"; } +h2::after { content: "

"; } +h3::before { content: "

"; } +h3::after { content: "

"; } +h4::before { content: "

"; } +h4::after { content: "

"; } +h5::before { content: "
"; } +h5::after { content: "
"; } +h6::before { content: "
"; } +h6::after { content: "
"; } +head::before { content: ""; } +head::after { content: ""; } +header::before { content: "
"; } +header::after { content: "
"; } +hgroup::before { content: "
"; } +hgroup::after { content: "
"; } +hr::before { content: "
"; } +hr::after { content: ""; } +html::before { content: ""; } +html::after { content: ""; } + +i::before { content: ""; } +i::after { content: ""; } +iframe::before { content: ""; } +image::before { content: ""; } +image::after { content: ""; } +img::before { content: ""; } +img::after { content: ""; } +input::before { content: ""; } +input::after { content: ""; } +ins::before { content: ""; } +ins::after { content: ""; } +isindex::before { content: ""; } +isindex::after { content: ""; } + +kbd::before { content: ""; } +kbd::after { content: ""; } +keygen::before { content: ""; } +keygen::after { content: ""; } + +label::before { content: ""; } +legend::before { content: ""; } +legend::after { content: ""; } +li::before { content: "
  • "; } +li::after { content: "
  • "; } +link::before { content: ""; } +link::after { content: ""; } +listing::before { content: ""; } +listing::after { content: ""; } + +main::before { content: "
    "; } +main::after { content: "
    "; } +map::before { content: ""; } +map::after { content: ""; } +mark::before { content: ""; } +mark::after { content: ""; } +marquee::before { content: ""; } +marquee::after { content: ""; } +menu::before { content: ""; } +menu::after { content: ""; } +menuitem::before { content: ""; } +menuitem::after { content: ""; } +meta::before { content: ""; } +meta::after { content: ""; } +meter::before { content: ""; } +meter::after { content: ""; } +multicol::before { content: ""; } +multicol::after { content: ""; } + +nav::before { content: ""; } +nextid::before { content: ""; } +nextid::after { content: ""; } +nobr::before { content: ""; } +nobr::after { content: ""; } +noembed::before { content: ""; } +noembed::after { content: ""; } +noframes::before { content: ""; } +noframes::after { content: ""; } +noscript::before { content: ""; } + +object::before { content: ""; } +object::after { content: ""; } +ol::before { content: "
      "; } +ol::after { content: "
    "; } +optgroup::before { content: ""; } +optgroup::after { content: ""; } +option::before { content: ""; } +output::before { content: ""; } +output::after { content: ""; } + +p::before { content: "

    "; } +p::after { content: "

    "; } +param::before { content: ""; } +param::after { content: ""; } +picture::before { content: ""; } +picture::after { content: ""; } +plaintext::before { content: ""; } +plaintext::after { content: "</plaintext>"; } +pre::before { content: "<pre>"; } +pre::after { content: "</pre>"; } +progress::before { content: "<progress>"; } +progress::after { content: "</progress>"; } + +q::before { content: "<q>"; } +q::after { content: "</q>"; } + +rb::before { content: "<rb>"; } +rb::after { content: "</rb>"; } +rp::before { content: "<rp>"; } +rp::after { content: "</rp>"; } +rt::before { content: "<rt>"; } +rt::after { content: "</rt>"; } +rtc::before { content: "<rtc>"; } +rtc::after { content: "</rtc>"; } +ruby::before { content: "<ruby>"; } +ruby::after { content: "</ruby>"; } + +s::before { content: "<s>"; } +s::after { content: "</s>"; } +samp::before { content: "<samp>"; } +samp::after { content: "</samp>"; } +script::before { content: "<script>"; } +script::after { content: "</script>"; } +section::before { content: "<section>"; } +section::after { content: "</section>"; } +select::before { content: "<select>"; } +select::after { content: "</select>"; } +shadow::before { content: "<shadow>"; } +shadow::after { content: "</shadow>"; } +slot::before { content: "<slot>"; } +slot::after { content: "</slot>"; } +small::before { content: "<small>"; } +small::after { content: "</small>"; } +source::before { content: "<source>"; } +source::after { content: "</source>"; } +spacer::before { content: "<spacer>"; } +spacer::after { content: "</spacer>"; } +span::before { content: "<span>"; } +span::after { content: "</span>"; } +strike::before { content: "<strike>"; } +strike::after { content: "</strike>"; } +strong::before { content: "<strong>"; } +strong::after { content: "</strong>"; } +style::before { content: "<style>"; } +style::after { content: "<\/style>"; } +sub::before { content: "<sub>"; } +sub::after { content: "</sub>"; } +summary::before { content: "<summary>"; } +summary::after { content: "</summary>"; } +sup::before { content: "<sup>"; } +sup::after { content: "</sup>"; } + +table::before { content: "<table>"; } +table::after { content: "</table>"; } +tbody::before { content: "<tbody>"; } +tbody::after { content: "</tbody>"; } +td::before { content: "<td>"; } +td::after { content: "</td>"; } +template::before { content: "<template>"; } +template::after { content: "</template>"; } +textarea::before { content: "<textarea>"; } +textarea::after { content: "</textarea>"; } +tfoot::before { content: "<tfoot>"; } +tfoot::after { content: "</tfoot>"; } +th::before { content: "<th>"; } +th::after { content: "</th>"; } +thead::before { content: "<thead>"; } +thead::after { content: "</thead>"; } +time::before { content: "<time>"; } +time::after { content: "</time>"; } +title::before { content: "<title>"; } +title::after { content: "</title>"; } +tr::before { content: "<tr>"; } +tr::after { content: "</tr>"; } +track::before { content: "<track>"; } +track::after { content: "</track>"; } +tt::before { content: "<tt>"; } +tt::after { content: "</tt>"; } + +u::before { content: "<u>"; } +u::after { content: "</u>"; } +ul::before { content: "<ul>"; } +ul::after { content: "</ul>"; } + +var::before { content: "<var>"; } +var::after { content: "</var>"; } +video::before { content: "<video>"; } +video::after { content: "</video>"; } + +wbr::before { content: "<wbr>"; } +wbr::after { content: "</wbr>"; } + +xmp::before { content: "<xmp>"; } +xmp::after { content: "</xmp>"; } diff --git a/packages/pagetop-aliner/tests/tera.rs b/packages/pagetop-aliner/tests/tera.rs new file mode 100644 index 00000000..dceb0671 --- /dev/null +++ b/packages/pagetop-aliner/tests/tera.rs @@ -0,0 +1,23 @@ +use pagetop_aliner::{TEMPLATE_BASE_DIR, TEMPLATE_GLOB}; + +use tera::Tera; + +/// Test to ensure Tera can initialize templates from the file system in debug mode. +#[test] +fn aliner_initialization_in_debug_mode() { + let tera = Tera::new(TEMPLATE_GLOB); + assert!(tera.is_ok(), "Failed to initialize Tera in debug mode"); +} + +/// Test to ensure templates embedded in the binary can be properly loaded in release mode. +#[test] +fn aliner_initialization_in_release_mode() { + for file in TEMPLATE_BASE_DIR.files() { + let content = file.contents_utf8(); + assert!( + content.is_some(), + "File {:?} contains non-UTF-8 content", + file.path() + ); + } +} diff --git a/packages/pagetop-bootsier/Cargo.toml b/packages/pagetop-bootsier/Cargo.toml index 3c2b03ad..b09b0413 100644 --- a/packages/pagetop-bootsier/Cargo.toml +++ b/packages/pagetop-bootsier/Cargo.toml @@ -15,9 +15,10 @@ authors = { workspace = true } license = { workspace = true } [dependencies] -pagetop = { workspace = true } -pagetop-aliner = { workspace = true } -static-files = { workspace = true } +pagetop.workspace = true +pagetop-aliner.workspace = true + +static-files.workspace = true [build-dependencies] -pagetop-build = { workspace = true } +pagetop-build.workspace = true diff --git a/packages/pagetop-bootsier/src/lib.rs b/packages/pagetop-bootsier/src/lib.rs index f4f2ada2..7c78c2b1 100644 --- a/packages/pagetop-bootsier/src/lib.rs +++ b/packages/pagetop-bootsier/src/lib.rs @@ -30,157 +30,158 @@ impl PackageTrait for Bootsier { } */ } -impl ThemeTrait for Bootsier { /* - #[rustfmt::skip] - fn regions(&self) -> Vec<(&'static str, L10n)> { - vec![ - ("header", L10n::t("header", &LOCALES_BOOTSIER)), - ("nav_branding", L10n::t("nav_branding", &LOCALES_BOOTSIER)), - ("nav_main", L10n::t("nav_main", &LOCALES_BOOTSIER)), - ("nav_additional", L10n::t("nav_additional", &LOCALES_BOOTSIER)), - ("breadcrumb", L10n::t("breadcrumb", &LOCALES_BOOTSIER)), - ("content", L10n::t("breadcrumb", &LOCALES_BOOTSIER)), - ("sidebar_first", L10n::t("sidebar_first", &LOCALES_BOOTSIER)), - ("sidebar_second", L10n::t("sidebar_second", &LOCALES_BOOTSIER)), - ("footer", L10n::t("footer", &LOCALES_BOOTSIER)), - ] +impl ThemeTrait for Bootsier { + /* + #[rustfmt::skip] + fn regions(&self) -> Vec<(&'static str, L10n)> { + vec![ + ("header", L10n::t("header", &LOCALES_BOOTSIER)), + ("nav_branding", L10n::t("nav_branding", &LOCALES_BOOTSIER)), + ("nav_main", L10n::t("nav_main", &LOCALES_BOOTSIER)), + ("nav_additional", L10n::t("nav_additional", &LOCALES_BOOTSIER)), + ("breadcrumb", L10n::t("breadcrumb", &LOCALES_BOOTSIER)), + ("content", L10n::t("breadcrumb", &LOCALES_BOOTSIER)), + ("sidebar_first", L10n::t("sidebar_first", &LOCALES_BOOTSIER)), + ("sidebar_second", L10n::t("sidebar_second", &LOCALES_BOOTSIER)), + ("footer", L10n::t("footer", &LOCALES_BOOTSIER)), + ] + } + + fn prepare_body(&self, page: &mut Page) -> PrepareMarkup { + let skip_to_id = page.body_skip_to().get().unwrap_or("content".to_owned()); + + PrepareMarkup::With(html! { + body id=[page.body_id().get()] class=[page.body_classes().get()] { + @if let Some(skip) = L10n::l("skip_to_content").using(page.context().langid()) { + div class="skip__to_content" { + a href=(concat_string!("#", skip_to_id)) { (skip) } + } + } + (match page.context().layout() { + "admin" => flex::Container::new() + .add_item(flex::Item::region().with_id("top-menu")) + .add_item(flex::Item::region().with_id("side-menu")) + .add_item(flex::Item::region().with_id("content")), + _ => flex::Container::new() + .add_item(flex::Item::region().with_id("header")) + .add_item(flex::Item::region().with_id("nav_branding")) + .add_item(flex::Item::region().with_id("nav_main")) + .add_item(flex::Item::region().with_id("nav_additional")) + .add_item(flex::Item::region().with_id("breadcrumb")) + .add_item(flex::Item::region().with_id("content")) + .add_item(flex::Item::region().with_id("sidebar_first")) + .add_item(flex::Item::region().with_id("sidebar_second")) + .add_item(flex::Item::region().with_id("footer")), + }.render(page.context())) + } + }) + } + + fn after_prepare_body(&self, page: &mut Page) { + page.set_assets(AssetsOp::SetFaviconIfNone( + Favicon::new().with_icon("/base/favicon.ico"), + )) + .set_assets(AssetsOp::AddStyleSheet( + StyleSheet::from("/bootsier/css/bootstrap.min.css") + .with_version("5.1.3") + .with_weight(-99), + )) + .set_assets(AssetsOp::AddJavaScript( + JavaScript::defer("/bootsier/js/bootstrap.bundle.min.js") + .with_version("5.1.3") + .with_weight(-99), + )) + .set_assets(AssetsOp::AddBaseAssets) + .set_assets(AssetsOp::AddStyleSheet( + StyleSheet::from("/bootsier/css/styles.css").with_version("0.0.1"), + )); + } } - fn prepare_body(&self, page: &mut Page) -> PrepareMarkup { - let skip_to_id = page.body_skip_to().get().unwrap_or("content".to_owned()); + fn before_prepare_icon(i: &mut Icon, _cx: &mut Context) { + i.set_classes( + ClassesOp::Replace(i.font_size().to_string()), + with_font(i.font_size()), + ); + } - PrepareMarkup::With(html! { - body id=[page.body_id().get()] class=[page.body_classes().get()] { - @if let Some(skip) = L10n::l("skip_to_content").using(page.context().langid()) { - div class="skip__to_content" { - a href=(concat_string!("#", skip_to_id)) { (skip) } + #[rustfmt::skip] + fn before_prepare_button(b: &mut Button, _cx: &mut Context) { + b.set_classes(ClassesOp::Replace("button__tap".to_owned()), "btn"); + b.set_classes( + ClassesOp::Replace(b.style().to_string()), + match b.style() { + StyleBase::Default => "btn-primary", + StyleBase::Info => "btn-info", + StyleBase::Success => "btn-success", + StyleBase::Warning => "btn-warning", + StyleBase::Danger => "btn-danger", + StyleBase::Light => "btn-light", + StyleBase::Dark => "btn-dark", + StyleBase::Link => "btn-link", + }, + ); + b.set_classes( + ClassesOp::Replace(b.font_size().to_string()), + with_font(b.font_size()), + ); + } + + #[rustfmt::skip] + fn before_prepare_heading(h: &mut Heading, _cx: &mut Context) { + h.set_classes( + ClassesOp::Replace(h.size().to_string()), + match h.size() { + HeadingSize::ExtraLarge => "display-1", + HeadingSize::XxLarge => "display-2", + HeadingSize::XLarge => "display-3", + HeadingSize::Large => "display-4", + HeadingSize::Medium => "display-5", + _ => "", + }, + ); + } + + fn before_prepare_paragraph(p: &mut Paragraph, _cx: &mut Context) { + p.set_classes( + ClassesOp::Replace(p.font_size().to_string()), + with_font(p.font_size()), + ); + } + + fn render_error404(_: &Error404, cx: &mut Context) -> Option<Markup> { + Some(html! { + div class="jumbotron" { + div class="media" { + img + src="/bootsier/images/caution.png" + class="mr-4" + style="width: 20%; max-width: 188px" + alt="Caution!"; + div class="media-body" { + h1 class="display-4" { ("RESOURCE NOT FOUND") } + p class="lead" { + (L10n::t("e404-description", &LOCALES_BOOTSIER) + .escaped(cx.langid())) + } + hr class="my-4"; + p { + (L10n::t("e404-description", &LOCALES_BOOTSIER) + .escaped(cx.langid())) + } + a + class="btn btn-primary btn-lg" + href="/" + role="button" + { + (L10n::t("back-homepage", &LOCALES_BOOTSIER) + .escaped(cx.langid())) + } } } - (match page.context().layout() { - "admin" => flex::Container::new() - .add_item(flex::Item::region().with_id("top-menu")) - .add_item(flex::Item::region().with_id("side-menu")) - .add_item(flex::Item::region().with_id("content")), - _ => flex::Container::new() - .add_item(flex::Item::region().with_id("header")) - .add_item(flex::Item::region().with_id("nav_branding")) - .add_item(flex::Item::region().with_id("nav_main")) - .add_item(flex::Item::region().with_id("nav_additional")) - .add_item(flex::Item::region().with_id("breadcrumb")) - .add_item(flex::Item::region().with_id("content")) - .add_item(flex::Item::region().with_id("sidebar_first")) - .add_item(flex::Item::region().with_id("sidebar_second")) - .add_item(flex::Item::region().with_id("footer")), - }.render(page.context())) } }) - } - - fn after_prepare_body(&self, page: &mut Page) { - page.set_assets(AssetsOp::SetFaviconIfNone( - Favicon::new().with_icon("/base/favicon.ico"), - )) - .set_assets(AssetsOp::AddStyleSheet( - StyleSheet::from("/bootsier/css/bootstrap.min.css") - .with_version("5.1.3") - .with_weight(-99), - )) - .set_assets(AssetsOp::AddJavaScript( - JavaScript::defer("/bootsier/js/bootstrap.bundle.min.js") - .with_version("5.1.3") - .with_weight(-99), - )) - .set_assets(AssetsOp::AddBaseAssets) - .set_assets(AssetsOp::AddStyleSheet( - StyleSheet::from("/bootsier/css/styles.css").with_version("0.0.1"), - )); - } -} - -fn before_prepare_icon(i: &mut Icon, _cx: &mut Context) { - i.set_classes( - ClassesOp::Replace(i.font_size().to_string()), - with_font(i.font_size()), - ); -} - -#[rustfmt::skip] -fn before_prepare_button(b: &mut Button, _cx: &mut Context) { - b.set_classes(ClassesOp::Replace("button__tap".to_owned()), "btn"); - b.set_classes( - ClassesOp::Replace(b.style().to_string()), - match b.style() { - StyleBase::Default => "btn-primary", - StyleBase::Info => "btn-info", - StyleBase::Success => "btn-success", - StyleBase::Warning => "btn-warning", - StyleBase::Danger => "btn-danger", - StyleBase::Light => "btn-light", - StyleBase::Dark => "btn-dark", - StyleBase::Link => "btn-link", - }, - ); - b.set_classes( - ClassesOp::Replace(b.font_size().to_string()), - with_font(b.font_size()), - ); -} - -#[rustfmt::skip] -fn before_prepare_heading(h: &mut Heading, _cx: &mut Context) { - h.set_classes( - ClassesOp::Replace(h.size().to_string()), - match h.size() { - HeadingSize::ExtraLarge => "display-1", - HeadingSize::XxLarge => "display-2", - HeadingSize::XLarge => "display-3", - HeadingSize::Large => "display-4", - HeadingSize::Medium => "display-5", - _ => "", - }, - ); -} - -fn before_prepare_paragraph(p: &mut Paragraph, _cx: &mut Context) { - p.set_classes( - ClassesOp::Replace(p.font_size().to_string()), - with_font(p.font_size()), - ); -} - -fn render_error404(_: &Error404, cx: &mut Context) -> Option<Markup> { - Some(html! { - div class="jumbotron" { - div class="media" { - img - src="/bootsier/images/caution.png" - class="mr-4" - style="width: 20%; max-width: 188px" - alt="Caution!"; - div class="media-body" { - h1 class="display-4" { ("RESOURCE NOT FOUND") } - p class="lead" { - (L10n::t("e404-description", &LOCALES_BOOTSIER) - .escaped(cx.langid())) - } - hr class="my-4"; - p { - (L10n::t("e404-description", &LOCALES_BOOTSIER) - .escaped(cx.langid())) - } - a - class="btn btn-primary btn-lg" - href="/" - role="button" - { - (L10n::t("back-homepage", &LOCALES_BOOTSIER) - .escaped(cx.langid())) - } - } - } - } - }) -*/ + */ } /* #[rustfmt::skip] diff --git a/packages/pagetop/Cargo.toml b/packages/pagetop/Cargo.toml index ec996e94..67d8f12c 100644 --- a/packages/pagetop/Cargo.toml +++ b/packages/pagetop/Cargo.toml @@ -22,25 +22,26 @@ name = "pagetop" colored = "2.1.0" concat-string = "1.0.1" figlet-rs = "0.1.5" -fluent-bundle = "0.15" -fluent-templates = "0.11" -nom = "7.1" +fluent-bundle = "0.15.3" +fluent-templates = "0.11.0" +itoa = "1.0.11" +nom = "7.1.3" paste = "1.0.15" -substring = "1.4" +substring = "1.4.5" terminal_size = "0.4.0" toml = "0.8.19" -tracing = "0.1" -tracing-appender = "0.2" -tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] } -tracing-actix-web = "0.7" -unic-langid = { version = "0.9", features = ["macros"] } +tracing = "0.1.40" +tracing-appender = "0.2.3" +tracing-subscriber = { version = "0.3.18", features = ["json", "env-filter"] } +tracing-actix-web = "0.7.15" +unic-langid = { version = "0.9.5", features = ["macros"] } -actix-web = "4" -actix-web-files = { package = "actix-files", version = "0.6" } -actix-web-static-files = "4.0" -actix-session = { version = "0.10", features = ["cookie-session"] } +actix-web = "4.9.0" +actix-web-files = { package = "actix-files", version = "0.6.6" } +actix-web-static-files = "4.0.1" +actix-session = { version = "0.10.1", features = ["cookie-session"] } -serde = { workspace = true } -static-files = { workspace = true } +serde.workspace = true +static-files.workspace = true -pagetop-macros = { workspace = true } +pagetop-macros.workspace = true diff --git a/packages/pagetop/src/html.rs b/packages/pagetop/src/html.rs new file mode 100644 index 00000000..f81273bf --- /dev/null +++ b/packages/pagetop/src/html.rs @@ -0,0 +1,4 @@ +//! HTML in code. + +mod maud; +pub use maud::{html, html_private, Markup, PreEscaped, DOCTYPE}; diff --git a/packages/pagetop/src/html/maud.rs b/packages/pagetop/src/html/maud.rs new file mode 100644 index 00000000..801895e5 --- /dev/null +++ b/packages/pagetop/src/html/maud.rs @@ -0,0 +1,350 @@ +//#![no_std] + +//! A macro for writing HTML templates. +//! +//! This documentation only describes the runtime API. For a general +//! guide, check out the [book] instead. +//! +//! [book]: https://maud.lambda.xyz/ + +//#![doc(html_root_url = "https://docs.rs/maud/0.25.0")] + +extern crate alloc; + +use alloc::{borrow::Cow, boxed::Box, string::String}; +use core::fmt::{self, Arguments, Display, Write}; + +pub use pagetop_macros::html; + +mod escape; + +/// An adapter that escapes HTML special characters. +/// +/// The following characters are escaped: +/// +/// * `&` is escaped as `&amp;` +/// * `<` is escaped as `&lt;` +/// * `>` is escaped as `&gt;` +/// * `"` is escaped as `&quot;` +/// +/// All other characters are passed through unchanged. +/// +/// **Note:** In versions prior to 0.13, the single quote (`'`) was +/// escaped as well. +/// +/// # Example +/// +/// ```rust +/// use maud::Escaper; +/// use std::fmt::Write; +/// let mut s = String::new(); +/// write!(Escaper::new(&mut s), "<script>launchMissiles()</script>").unwrap(); +/// assert_eq!(s, "&lt;script&gt;launchMissiles()&lt;/script&gt;"); +/// ``` +pub struct Escaper<'a>(&'a mut String); + +impl<'a> Escaper<'a> { + /// Creates an `Escaper` from a `String`. + pub fn new(buffer: &'a mut String) -> Escaper<'a> { + Escaper(buffer) + } +} + +impl<'a> fmt::Write for Escaper<'a> { + fn write_str(&mut self, s: &str) -> fmt::Result { + escape::escape_to_string(s, self.0); + Ok(()) + } +} + +/// Represents a type that can be rendered as HTML. +/// +/// To implement this for your own type, override either the `.render()` +/// or `.render_to()` methods; since each is defined in terms of the +/// other, you only need to implement one of them. See the example below. +/// +/// # Minimal implementation +/// +/// An implementation of this trait must override at least one of +/// `.render()` or `.render_to()`. Since the default definitions of +/// these methods call each other, not doing this will result in +/// infinite recursion. +/// +/// # Example +/// +/// ```rust +/// use maud::{html, Markup, Render}; +/// +/// /// Provides a shorthand for linking to a CSS stylesheet. +/// pub struct Stylesheet(&'static str); +/// +/// impl Render for Stylesheet { +/// fn render(&self) -> Markup { +/// html! { +/// link rel="stylesheet" type="text/css" href=(self.0); +/// } +/// } +/// } +/// ``` +pub trait Render { + /// Renders `self` as a block of `Markup`. + fn render(&self) -> Markup { + let mut buffer = String::new(); + self.render_to(&mut buffer); + PreEscaped(buffer) + } + + /// Appends a representation of `self` to the given buffer. + /// + /// Its default implementation just calls `.render()`, but you may + /// override it with something more efficient. + /// + /// Note that no further escaping is performed on data written to + /// the buffer. If you override this method, you must make sure that + /// any data written is properly escaped, whether by hand or using + /// the [`Escaper`](struct.Escaper.html) wrapper struct. + fn render_to(&self, buffer: &mut String) { + buffer.push_str(&self.render().into_string()); + } +} + +impl Render for str { + fn render_to(&self, w: &mut String) { + escape::escape_to_string(self, w); + } +} + +impl Render for String { + fn render_to(&self, w: &mut String) { + str::render_to(self, w); + } +} + +impl<'a> Render for Cow<'a, str> { + fn render_to(&self, w: &mut String) { + str::render_to(self, w); + } +} + +impl<'a> Render for Arguments<'a> { + fn render_to(&self, w: &mut String) { + let _ = Escaper::new(w).write_fmt(*self); + } +} + +impl<'a, T: Render + ?Sized> Render for &'a T { + fn render_to(&self, w: &mut String) { + T::render_to(self, w); + } +} + +impl<'a, T: Render + ?Sized> Render for &'a mut T { + fn render_to(&self, w: &mut String) { + T::render_to(self, w); + } +} + +impl<T: Render + ?Sized> Render for Box<T> { + fn render_to(&self, w: &mut String) { + T::render_to(self, w); + } +} + +macro_rules! impl_render_with_display { + ($($ty:ty)*) => { + $( + impl Render for $ty { + fn render_to(&self, w: &mut String) { + // TODO: remove the explicit arg when Rust 1.58 is released + format_args!("{self}", self = self).render_to(w); + } + } + )* + }; +} + +impl_render_with_display! { + char f32 f64 +} + +macro_rules! impl_render_with_itoa { + ($($ty:ty)*) => { + $( + impl Render for $ty { + fn render_to(&self, w: &mut String) { + w.push_str(itoa::Buffer::new().format(*self)); + } + } + )* + }; +} + +impl_render_with_itoa! { + i8 i16 i32 i64 i128 isize + u8 u16 u32 u64 u128 usize +} + +/// Renders a value using its [`Display`] impl. +/// +/// # Example +/// +/// ```rust +/// use maud::html; +/// use std::net::Ipv4Addr; +/// +/// let ip_address = Ipv4Addr::new(127, 0, 0, 1); +/// +/// let markup = html! { +/// "My IP address is: " +/// (maud::display(ip_address)) +/// }; +/// +/// assert_eq!(markup.into_string(), "My IP address is: 127.0.0.1"); +/// ``` +pub fn display(value: impl Display) -> impl Render { + struct DisplayWrapper<T>(T); + + impl<T: Display> Render for DisplayWrapper<T> { + fn render_to(&self, w: &mut String) { + format_args!("{0}", self.0).render_to(w); + } + } + + DisplayWrapper(value) +} + +/// A wrapper that renders the inner value without escaping. +#[derive(Debug, Clone, Copy)] +pub struct PreEscaped<T: AsRef<str>>(pub T); + +impl<T: AsRef<str>> Render for PreEscaped<T> { + fn render_to(&self, w: &mut String) { + w.push_str(self.0.as_ref()); + } +} + +/// A block of markup is a string that does not need to be escaped. +/// +/// The `html!` macro expands to an expression of this type. +pub type Markup = PreEscaped<String>; + +impl Markup { + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl<T: AsRef<str> + Into<String>> PreEscaped<T> { + /// Converts the inner value to a string. + pub fn into_string(self) -> String { + self.0.into() + } +} + +impl<T: AsRef<str> + Into<String>> From<PreEscaped<T>> for String { + fn from(value: PreEscaped<T>) -> String { + value.into_string() + } +} + +impl<T: AsRef<str> + Default> Default for PreEscaped<T> { + fn default() -> Self { + Self(Default::default()) + } +} + +/// The literal string `<!DOCTYPE html>`. +/// +/// # Example +/// +/// A minimal web page: +/// +/// ```rust +/// use maud::{DOCTYPE, html}; +/// +/// let markup = html! { +/// (DOCTYPE) +/// html { +/// head { +/// meta charset="utf-8"; +/// title { "Test page" } +/// } +/// body { +/// p { "Hello, world!" } +/// } +/// } +/// }; +/// ``` +pub const DOCTYPE: PreEscaped<&'static str> = PreEscaped("<!DOCTYPE html>"); + +mod actix_support { + extern crate alloc; + + use crate::html::PreEscaped; + use actix_web::{http::header, HttpRequest, HttpResponse, Responder}; + use alloc::string::String; + + impl Responder for PreEscaped<String> { + type Body = String; + + fn respond_to(self, _req: &HttpRequest) -> HttpResponse<Self::Body> { + HttpResponse::Ok() + .content_type(header::ContentType::html()) + .message_body(self.0) + .unwrap() + } + } +} + +#[doc(hidden)] +pub mod html_private { + extern crate alloc; + + use super::{display, Render}; + use alloc::string::String; + use core::fmt::Display; + + #[doc(hidden)] + #[macro_export] + macro_rules! render_to { + ($x:expr, $buffer:expr) => {{ + use $crate::html::html_private::*; + match ChooseRenderOrDisplay($x) { + x => (&&x).implements_render_or_display().render_to(x.0, $buffer), + } + }}; + } + + pub use render_to; + + pub struct ChooseRenderOrDisplay<T>(pub T); + + pub struct ViaRenderTag; + pub struct ViaDisplayTag; + + pub trait ViaRender { + fn implements_render_or_display(&self) -> ViaRenderTag { + ViaRenderTag + } + } + pub trait ViaDisplay { + fn implements_render_or_display(&self) -> ViaDisplayTag { + ViaDisplayTag + } + } + + impl<T: Render> ViaRender for &ChooseRenderOrDisplay<T> {} + impl<T: Display> ViaDisplay for ChooseRenderOrDisplay<T> {} + + impl ViaRenderTag { + pub fn render_to<T: Render + ?Sized>(self, value: &T, buffer: &mut String) { + value.render_to(buffer); + } + } + + impl ViaDisplayTag { + pub fn render_to<T: Display + ?Sized>(self, value: &T, buffer: &mut String) { + display(value).render_to(buffer); + } + } +} diff --git a/packages/pagetop/src/html/maud/escape.rs b/packages/pagetop/src/html/maud/escape.rs new file mode 100644 index 00000000..94cdeec1 --- /dev/null +++ b/packages/pagetop/src/html/maud/escape.rs @@ -0,0 +1,34 @@ +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +// !!!!! PLEASE KEEP THIS IN SYNC WITH `maud_macros/src/escape.rs` !!!!! +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +extern crate alloc; + +use alloc::string::String; + +pub fn escape_to_string(input: &str, output: &mut String) { + for b in input.bytes() { + match b { + b'&' => output.push_str("&amp;"), + b'<' => output.push_str("&lt;"), + b'>' => output.push_str("&gt;"), + b'"' => output.push_str("&quot;"), + _ => unsafe { output.as_mut_vec().push(b) }, + } + } +} + +#[cfg(test)] +mod test { + extern crate alloc; + + use super::escape_to_string; + use alloc::string::String; + + #[test] + fn it_works() { + let mut s = String::new(); + escape_to_string("<script>launchMissiles()</script>", &mut s); + assert_eq!(s, "&lt;script&gt;launchMissiles()&lt;/script&gt;"); + } +} diff --git a/packages/pagetop/src/lib.rs b/packages/pagetop/src/lib.rs index 913ca22f..ecdffaf9 100644 --- a/packages/pagetop/src/lib.rs +++ b/packages/pagetop/src/lib.rs @@ -72,9 +72,7 @@ #![cfg_attr(docsrs, feature(doc_cfg))] -// ************************************************************************************************* -// RE-EXPORTED. -// ************************************************************************************************* +// RE-EXPORTED ************************************************************************************* pub use concat_string::concat_string as join; @@ -89,14 +87,14 @@ pub use std::any::TypeId; pub type Weight = i8; -// ************************************************************************************************* -// API. -// ************************************************************************************************* +// API ********************************************************************************************* // Useful functions and macros. pub mod util; // Application tracing and event logging. pub mod trace; +// HTML in code. +pub mod html; // Localization. pub mod locale; // Essential web framework. @@ -110,8 +108,6 @@ pub mod global; // Prepare and run the application. pub mod app; -// ************************************************************************************************* -// The PageTop Prelude. -// ************************************************************************************************* +// The PageTop Prelude ***************************************************************************** pub mod prelude; diff --git a/packages/pagetop/src/prelude.rs b/packages/pagetop/src/prelude.rs index 3b6bb561..9fdedbcb 100644 --- a/packages/pagetop/src/prelude.rs +++ b/packages/pagetop/src/prelude.rs @@ -23,6 +23,8 @@ pub use crate::util; pub use crate::trace; +pub use crate::html::*; + pub use crate::locale::*; pub use crate::service; diff --git a/packages/pagetop/src/util.rs b/packages/pagetop/src/util.rs index 6348487e..ed743491 100644 --- a/packages/pagetop/src/util.rs +++ b/packages/pagetop/src/util.rs @@ -15,9 +15,7 @@ use crate::trace; use std::io; use std::path::PathBuf; -// ************************************************************************************************* -// USEFUL FUNCTIONS. -// ************************************************************************************************* +// USEFUL FUNCTIONS ******************************************************************************** pub enum TypeInfo { FullName, @@ -150,9 +148,7 @@ pub fn absolute_dir( Ok(absolute_dir) } -// ************************************************************************************************* -// USEFUL MACROS. -// ************************************************************************************************* +// USEFUL MACROS *********************************************************************************** #[macro_export] /// Macro para construir grupos de pares clave-valor.