diff --git a/Cargo.lock b/Cargo.lock index ba1df9e6..91370336 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,291 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "actix-codec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-files" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0773d59061dedb49a8aed04c67291b9d8cf2fe0b60130a381aab53c6dd86e9be" +dependencies = [ + "actix-http", + "actix-service", + "actix-utils", + "actix-web", + "bitflags", + "bytes", + "derive_more 0.99.18", + "futures-core", + "http-range", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "v_htmlescape", +] + +[[package]] +name = "actix-http" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d48f96fc3003717aeb9856ca3d02a8c7de502667ad76eeacd830b48d2e91fac4" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "ahash", + "base64 0.22.1", + "bitflags", + "brotli", + "bytes", + "bytestring", + "derive_more 0.99.18", + "encoding_rs", + "flate2", + "futures-core", + "h2", + "http", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn 2.0.90", +] + +[[package]] +name = "actix-router" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" +dependencies = [ + "bytestring", + "cfg-if", + "http", + "regex", + "regex-lite", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca2549781d8dd6d75c40cf6b6051260a2cc2f3c62343d761a969a0640646894" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +dependencies = [ + "futures-core", + "paste", + "pin-project-lite", +] + +[[package]] +name = "actix-session" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efe6976a74f34f1b6d07a6c05aadc0ed0359304a7781c367fa5b4029418db08f" +dependencies = [ + "actix-service", + "actix-utils", + "actix-web", + "anyhow", + "derive_more 1.0.0", + "rand", + "serde", + "serde_json", + "tracing", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9180d76e5cc7ccbc4d60a506f2c727730b154010262df5b910eb17dbe4b8cb38" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "ahash", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more 0.99.18", + "encoding_rs", + "futures-core", + "futures-util", + "impl-more", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "regex-lite", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2", + "time", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "actix-web-static-files" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf6d1ef6d7a60e084f9e0595e2a5234abda14e76c105ecf8e2d0e8800c41a1f" +dependencies = [ + "actix-web", + "derive_more 0.99.18", + "futures-util", + "static-files", +] + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.8.11" @@ -9,17 +294,57 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", + "getrandom", "once_cell", "version_check", "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "allocator-api2" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[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" @@ -56,7 +381,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -66,7 +391,92 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "windows-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "anyhow" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bstr" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" +dependencies = [ + "memchr", + "serde", ] [[package]] @@ -81,6 +491,32 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bytes" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" + +[[package]] +name = "bytestring" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" +dependencies = [ + "bytes", +] + +[[package]] +name = "cc" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -97,6 +533,30 @@ 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", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.6", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.5.21" @@ -136,12 +596,363 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "colored" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +dependencies = [ + "lazy_static", + "windows-sys 0.48.0", +] + +[[package]] +name = "concat-string" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7439becb5fafc780b6f4de382b1a7a3e70234afe783854a4702ee8adbb838609" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "aes-gcm", + "base64 0.20.0", + "hkdf", + "hmac", + "percent-encoding", + "rand", + "sha2", + "subtle", + "time", + "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.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_more" +version = "0.99.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.90", +] + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "figlet-rs" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4742a071cd9694fc86f9fa1a08fa3e53d40cc899d7ee532295da2d085639fbc5" + +[[package]] +name = "flate2" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fluent-bundle" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe0a21ee80050c678013f82edf4b705fe2f26f1f9877593d13198612503f493" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash", + "self_cell 0.10.3", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ad0989667548f06ccd0e306ed56b61bd4d35458d54df5ec7587c0e8ed5e94" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a530c4694a6a8d528794ee9bbd8ba0122e779629ac908d15ad5a7ae7763a33d" +dependencies = [ + "thiserror", +] + +[[package]] +name = "fluent-template-macros" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d176e568a4f73ad4225df02aa29ccfecffd8eda31ce78da0bc8b4b310f20a" +dependencies = [ + "flume", + "ignore", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.90", + "unic-langid", +] + +[[package]] +name = "fluent-templates" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74f22f61b2c8551163ea13c16a381484e5360b089401c6e47c4bfcf6b62bb7ac" +dependencies = [ + "fluent-bundle", + "fluent-langneg", + "fluent-syntax", + "fluent-template-macros", + "flume", + "ignore", + "intl-memoizer", + "log", + "once_cell", + "thiserror", + "unic-langid", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -155,12 +966,41 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "glob" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "globset" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + [[package]] name = "grass" version = "0.13.4" @@ -186,6 +1026,25 @@ dependencies = [ "rand", ] +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -202,6 +1061,237 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ignore" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata 0.4.9", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "impl-more" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae21c3177a27788957044151cc2800043d127acaa460a47ebb9b84dfa2c6aa0" + [[package]] name = "indexmap" version = "2.7.0" @@ -212,12 +1302,55 @@ dependencies = [ "hashbrown 0.15.2", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "intl-memoizer" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe22e020fce238ae18a6d5d8c502ee76a52a6e880d99477657e6acc30ec57bda" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +dependencies = [ + "unic-langid", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.74" @@ -228,6 +1361,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + [[package]] name = "lasso" version = "0.7.3" @@ -237,18 +1376,72 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.167" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + +[[package]] +name = "local-channel" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "memchr" version = "2.7.4" @@ -271,12 +1464,131 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "mutually_exclusive_features" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94e1e6445d314f972ff7395df2de295fe51b71821694f0b0e1e79c4f12c8577" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-conv" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "pagetop" +version = "0.0.57" +dependencies = [ + "actix-files", + "actix-session", + "actix-web", + "actix-web-static-files", + "chrono", + "colored", + "concat-string", + "figlet-rs", + "fluent-templates", + "itoa", + "nom", + "pagetop-build", + "pagetop-macros", + "paste", + "serde", + "static-files", + "substring", + "terminal_size", + "toml", + "tracing", + "tracing-actix-web", + "tracing-appender", + "tracing-subscriber", + "unic-langid", +] + [[package]] name = "pagetop-build" version = "0.0.12" @@ -296,6 +1608,35 @@ dependencies = [ "syn 2.0.90", ] +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "path-matchers" version = "1.0.2" @@ -311,6 +1652,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498a099351efa4becc6a19c72aa9270598e8fd274ca47052e37455241c88b696" +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + [[package]] name = "phf" version = "0.11.2" @@ -353,6 +1700,62 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -395,6 +1798,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.92" @@ -443,12 +1852,286 @@ dependencies = [ "getrandom", ] +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "self_cell" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d" +dependencies = [ + "self_cell 1.0.4", +] + +[[package]] +name = "self_cell" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "serde_json" +version = "1.0.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "siphasher" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static-files" version = "0.2.4" @@ -466,6 +2149,21 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "substring" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ee6433ecef213b2e72f587ef64a2f5943e7cd16fbd82dbe8bc07486c534c86" +dependencies = [ + "autocfg", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "1.0.109" @@ -487,11 +2185,148 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "terminal_size" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" +dependencies = [ + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.41.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -500,10 +2335,170 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", + "serde", + "serde_spanned", "toml_datetime", "winnow", ] +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-actix-web" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a9f5c1aca50ebebf074ee665b9f99f2e84906dcf6b993a0d0090edb835166d" +dependencies = [ + "actix-web", + "mutually_exclusive_features", + "pin-project", + "tracing", + "uuid", +] + +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "type-map" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb68604048ff8fa93347f02441e4487594adc20bb8a084f9e564d2b827a0a9f" +dependencies = [ + "rustc-hash", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unic-langid" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dd9d1e72a73b25e07123a80776aae3e7b0ec461ef94f9151eed6ec88005a44" +dependencies = [ + "unic-langid-impl", + "unic-langid-macros", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a5422c1f65949306c99240b81de9f3f15929f5a8bfe05bb44b034cc8bf593e5" +dependencies = [ + "tinystr", +] + +[[package]] +name = "unic-langid-macros" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0da1cd2c042d3c7569a1008806b02039e7a4a2bdf8f8e96bd3c792434a0e275e" +dependencies = [ + "proc-macro-hack", + "tinystr", + "unic-langid-impl", + "unic-langid-macros-impl", +] + +[[package]] +name = "unic-langid-macros-impl" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ed7f4237ba393424195053097c1516bd4590dc82b84f2f97c5c69e12704555b" +dependencies = [ + "proc-macro-hack", + "quote", + "syn 2.0.90", + "unic-langid-impl", +] + [[package]] name = "unicase" version = "2.8.0" @@ -516,18 +2511,88 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "getrandom", +] + +[[package]] +name = "v_htmlescape" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -589,13 +2654,86 @@ version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ee99da9c5ba11bd675621338ef6fa52296b76b83305e9b6e5c77d4c286d6d49" +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -604,28 +2742,46 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -638,24 +2794,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -671,6 +2851,42 @@ dependencies = [ "memchr", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -691,3 +2907,74 @@ dependencies = [ "quote", "syn 2.0.90", ] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "zstd" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.13+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 35e82a14..b10ee36a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = [ "helpers/pagetop-macros", # PageTop - #"pagetop", + "pagetop", # Packages #"packages/pagetop-aliner", @@ -25,7 +25,7 @@ authors = ["Manuel Cillero "] [workspace.dependencies] #include_dir = "0.7.4" -#serde = { version = "1.0", features = ["derive"] } +serde = { version = "1.0", features = ["derive"] } static-files = "0.2.4" # Helpers @@ -33,7 +33,7 @@ pagetop-build = { version = "0.0", path = "helpers/pagetop-build" } pagetop-macros = { version = "0.0", path = "helpers/pagetop-macros" } # PageTop -#pagetop = { version = "0.0", path = "pagetop" } +pagetop = { version = "0.0", path = "pagetop" } # Packages #pagetop-aliner = { version = "0.0", path = "packages/pagetop-aliner" } diff --git a/README.md b/README.md index 676f9ffc..e7f8731c 100644 --- a/README.md +++ b/README.md @@ -1 +1,114 @@ -# Un nuevo comienzo +
+ + + +

PageTop

+ +

Entorno de desarrollo para crear soluciones web modulares, extensibles y configurables.

+ +[![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](#-license) +[![Doc API](https://img.shields.io/docsrs/pagetop?label=Doc%20API&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop) +[![Crates.io](https://img.shields.io/crates/v/pagetop.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop) +[![Descargas](https://img.shields.io/crates/d/pagetop.svg?label=Descargas&style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop) + +
+ +## Presentación + +**PageTop** reúne algunos de los *crates* más estables y populares del ecosistema Rust para +proporcionar un conjunto completo de funcionalidades que pueden extenderse y adaptarse a las +necesidades específicas de cada aplicación web. + +PageTop reivindica la sencillez de la web clásica aplicando *renderizado en el servidor* (SSR), +HTML, CSS y JS, mediante acciones, componentes, paquetes y temas: + + * **Acciones**. Las funciones y procedimientos que incorporen *acciones* en su lógica de programa + estarán proporcionando a los desarrolladores herramientas para alterar su comportamiento interno + interceptando su flujo de ejecución. + * **Componentes**. Encapsulan HTML, CSS y JavaScript en unidades funcionales, configurables y bien + definidas. + * **Paquetes**. Extienden o personalizan funcionalidades existentes interactuando con las APIs de + PageTop o de paquetes de terceros. + * **Temas**. Permiten a los desarrolladores alterar la apariencia de las páginas y componentes sin + afectar su funcionalidad. + + +# ⚡️ Inicio rápido + +La aplicación más sencilla de PageTop se ve así: + +```rust +use pagetop::prelude::*; + +#[pagetop::main] +async fn main() -> std::io::Result<()> { + Application::new().run()?.await +} +``` + +Proporciona una página de bienvenida en `http://localhost:8088` según la configuración predefinida. +Para personalizar el servicio puedes crear un paquete de PageTop: + +```rust +use pagetop::prelude::*; + +struct HelloWorld; + +impl PackageTrait for HelloWorld { + fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { + scfg.route("/", service::web::get().to(hello_world)); + } +} + +async fn hello_world(request: HttpRequest) -> ResultPage { + Page::new(request) + .with_component(Html::with(html! { h1 { "Hello world!" } })) + .render() +} + +#[pagetop::main] +async fn main() -> std::io::Result<()> { + Application::prepare(&HelloWorld).run()?.await +} +``` + +Este programa prepara un paquete personalizado llamado `HelloWorld` que sirve una página web en la +ruta raíz (`/`) mostrando el mensaje "Hello world!" en un elemento HTML `

`. + + +# 📂 Crates de ayuda + + * [pagetop-macros](https://github.com/manuelcillero/pagetop/tree/latest/helpers/pagetop-macros): + Proporciona una colección de macros que mejoran la experiencia de desarrollo con PageTop. + + * [pagetop-build](https://github.com/manuelcillero/pagetop/tree/latest/helpers/pagetop-build): + Permite incluir fácilmente archivos estáticos o archivos SCSS compilados, directamente en el + binario de las aplicaciones PageTop. + + +# 🚧 Advertencia + +**PageTop** es un proyecto personal que se encuentra en desarrollo activo. Actualmente su API es +inestable y está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos hasta +que alcance la versión **0.1.0**. + + +# 📜 Licencia + +El código está disponible bajo una doble licencia: + + * **Licencia MIT** + ([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT) + + * **Licencia Apache, Versión 2.0** + ([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0) + +Puedes elegir la licencia que prefieras. Este enfoque de doble licencia es el estándar de facto en +el ecosistema Rust. + + +# ✨ Contribuciones + +Cualquier contribución para añadir al proyecto se considerará automáticamente bajo la doble licencia +indicada arriba (MIT o Apache v2.0), sin términos o condiciones adicionales, tal y como permite la +licencia *Apache v2.0*. diff --git a/helpers/pagetop-build/README.md b/helpers/pagetop-build/README.md index 133200e8..c5ecd712 100644 --- a/helpers/pagetop-build/README.md +++ b/helpers/pagetop-build/README.md @@ -14,7 +14,7 @@ # 📦 Sobre PageTop [PageTop](https://docs.rs/pagetop) es un entorno de desarrollo con convenciones que reivindican la -web clásica aplicando *renderizado en el servidor* (SSR), HTML, CSS y JS. +sencillez de la web clásica aplicando *renderizado en el servidor* (SSR), HTML, CSS y JS. # 🚧 Advertencia diff --git a/helpers/pagetop-macros/Cargo.toml b/helpers/pagetop-macros/Cargo.toml index 66d71014..1f107111 100644 --- a/helpers/pagetop-macros/Cargo.toml +++ b/helpers/pagetop-macros/Cargo.toml @@ -4,7 +4,7 @@ version = "0.0.14" edition = "2021" description = """\ - Una colección de macros que impulsan el desarrollo con PageTop.\ + Una colección de macros que mejoran la experiencia de desarrollo con PageTop.\ """ categories = ["development-tools::procedural-macro-helpers", "web-programming"] keywords = ["pagetop", "macros", "proc-macros", "codegen"] diff --git a/helpers/pagetop-macros/README.md b/helpers/pagetop-macros/README.md index da83f592..5f268c84 100644 --- a/helpers/pagetop-macros/README.md +++ b/helpers/pagetop-macros/README.md @@ -2,7 +2,7 @@

PageTop Macros

-

Una colección de macros que impulsan el desarrollo con PageTop.

+

Una colección de macros que mejoran la experiencia de desarrollo con PageTop.

[![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](#-license) [![Doc API](https://img.shields.io/docsrs/pagetop-macros?label=Doc%20API&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop-macros) @@ -14,7 +14,7 @@ # 📦 Sobre PageTop [PageTop](https://docs.rs/pagetop) es un entorno de desarrollo con convenciones que reivindican la -web clásica aplicando *renderizado en el servidor* (SSR), HTML, CSS y JS. +sencillez de la web clásica aplicando *renderizado en el servidor* (SSR), HTML, CSS y JS. # 🚧 Advertencia diff --git a/pagetop/Cargo.toml b/pagetop/Cargo.toml new file mode 100644 index 00000000..e60a8de7 --- /dev/null +++ b/pagetop/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "pagetop" +version = "0.0.57" +edition = "2021" + +description = """\ + Entorno de desarrollo para crear soluciones web modulares, extensibles y configurables.\ +""" +categories = ["web-programming", "gui", "development-tools", "asynchronous"] +keywords = ["pagetop", "web", "framework", "frontend", "ssr"] +readme = "../README.md" + +repository.workspace = true +homepage.workspace = true +license.workspace = true +authors.workspace = true + +[dependencies] +chrono = "0.4.38" +colored = "2.1.0" +concat-string = "1.0.1" +figlet-rs = "0.1.5" +itoa = "1.0.14" +nom = "7.1.3" +paste = "1.0.15" +serde.workspace = true +substring = "1.4.5" +terminal_size = "0.4.1" +toml = "0.8.19" + +tracing = "0.1.41" +tracing-appender = "0.2.3" +tracing-subscriber = { version = "0.3.19", features = ["json", "env-filter"] } +tracing-actix-web = "0.7.15" + +fluent-templates = "0.11.0" +unic-langid = { version = "0.9.5", features = ["macros"] } + +actix-web = "4.9.0" +actix-session = { version = "0.10.1", features = ["cookie-session"] } +actix-web-files = { package = "actix-files", version = "0.6.6" } +actix-web-static-files = "4.0.1" +static-files.workspace = true + +pagetop-macros.workspace = true + +[build-dependencies] +pagetop-build.workspace = true diff --git a/pagetop/build.rs b/pagetop/build.rs new file mode 100644 index 00000000..1450422c --- /dev/null +++ b/pagetop/build.rs @@ -0,0 +1,7 @@ +use pagetop_build::StaticFilesBundle; + +fn main() -> std::io::Result<()> { + StaticFilesBundle::from_dir("../static", None) + .with_name("assets") + .build() +} diff --git a/pagetop/config/common.toml b/pagetop/config/common.toml new file mode 100644 index 00000000..d6b30e57 --- /dev/null +++ b/pagetop/config/common.toml @@ -0,0 +1,6 @@ +[app] +name = "Samples" +#language = "es-ES" + +[log] +tracing = "Debug" diff --git a/pagetop/config/predefined-settings.toml b/pagetop/config/predefined-settings.toml new file mode 100644 index 00000000..08571a92 --- /dev/null +++ b/pagetop/config/predefined-settings.toml @@ -0,0 +1,40 @@ +[app] +name = "My App" +description = "Developed with the amazing PageTop framework." +# Default theme. +theme = "Default" +# Default language (localization). +language = "en-US" +# Default text direction: "ltr", "rtl", or "auto". +direction = "ltr" +# Startup banner: "Off", "Slant", "Small", "Speed", or "Starwars". +startup_banner = "Slant" + +[dev] +# Static files required by the app are integrated by default into the executable +# binary. However, during development, it can be useful to serve these files +# from their own directory to avoid recompiling every time they are modified. In +# this case, just indicate the full path to the project's root directory. +pagetop_project_dir = "" + +[log] +# Execution trace: "Error", "Warn", "Info", "Debug", or "Trace". +# For example: "Error,actix_server::builder=Info,tracing_actix_web=Debug". +tracing = "Info" +# In terminal ("Stdout") or files "Daily", "Hourly", "Minutely", or "Endless". +rolling = "Stdout" +# Directory for trace files (if rolling != "Stdout"). +path = "log" +# Prefix for trace files (if rolling != "Stdout"). +prefix = "tracing.log" +# Traces format: "Full", "Compact", "Pretty", or "Json". +format = "Full" + +[server] +# Web server config. +bind_address = "localhost" +bind_port = 8088 +# Session cookie duration (in seconds), i.e., the time from when the session is +# created until the cookie expires. A value of 0 indicates "until the browser is +# closed". By default, it is one week. +session_lifetime = 604800 diff --git a/pagetop/examples/app-basic.rs b/pagetop/examples/app-basic.rs new file mode 100644 index 00000000..53743870 --- /dev/null +++ b/pagetop/examples/app-basic.rs @@ -0,0 +1,6 @@ +use pagetop::prelude::*; + +#[pagetop::main] +async fn main() -> std::io::Result<()> { + Application::new().run()?.await +} diff --git a/pagetop/examples/hello-name.rs b/pagetop/examples/hello-name.rs new file mode 100644 index 00000000..3a03e8b1 --- /dev/null +++ b/pagetop/examples/hello-name.rs @@ -0,0 +1,25 @@ +use pagetop::prelude::*; + +struct HelloName; + +impl PackageTrait for HelloName { + fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { + scfg.service(hello_name); + } +} + +#[service::get("/hello/{name}")] +async fn hello_name( + request: HttpRequest, + path: service::web::Path, +) -> ResultPage { + let name = path.into_inner(); + Page::new(request) + .with_component(Html::with(html! { h1 { "Hello " (name) "!" } })) + .render() +} + +#[pagetop::main] +async fn main() -> std::io::Result<()> { + Application::prepare(&HelloName).run()?.await +} diff --git a/pagetop/examples/hello-world.rs b/pagetop/examples/hello-world.rs new file mode 100644 index 00000000..17c1e9d3 --- /dev/null +++ b/pagetop/examples/hello-world.rs @@ -0,0 +1,20 @@ +use pagetop::prelude::*; + +struct HelloWorld; + +impl PackageTrait for HelloWorld { + fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { + scfg.route("/", service::web::get().to(hello_world)); + } +} + +async fn hello_world(request: HttpRequest) -> ResultPage { + Page::new(request) + .with_component(Html::with(html! { h1 { "Hello World!" } })) + .render() +} + +#[pagetop::main] +async fn main() -> std::io::Result<()> { + Application::prepare(&HelloWorld).run()?.await +} diff --git a/pagetop/src/app.rs b/pagetop/src/app.rs new file mode 100644 index 00000000..7c5b8930 --- /dev/null +++ b/pagetop/src/app.rs @@ -0,0 +1,168 @@ +//! Prepare and run an application created with **Pagetop**. + +mod figfont; + +use crate::core::{package, package::PackageRef}; +use crate::html::Markup; +use crate::response::page::{ErrorPage, ResultPage}; +use crate::service::HttpRequest; +use crate::{global, locale, service, trace}; + +use actix_session::config::{BrowserSession, PersistentSession, SessionLifecycle}; +use actix_session::storage::CookieSessionStore; +use actix_session::SessionMiddleware; + +use substring::Substring; + +use std::io::Error; +use std::sync::LazyLock; + +pub struct Application; + +impl Default for Application { + fn default() -> Self { + Self::new() + } +} + +impl Application { + /// Creates a new application instance without any package. + pub fn new() -> Self { + Self::internal_prepare(None) + } + + /// Prepares an application instance with a specific package. + pub fn prepare(root_package: PackageRef) -> Self { + Self::internal_prepare(Some(root_package)) + } + + // Internal method to prepare the application, optionally with a package. + fn internal_prepare(root_package: Option) -> Self { + // On startup, show the application banner. + Self::show_banner(); + + // Starts logging and event tracing. + LazyLock::force(&trace::TRACING); + + // Validates the default language identifier. + LazyLock::force(&locale::DEFAULT_LANGID); + + // Registers the application's packages. + package::all::register_packages(root_package); + + // Registers package actions. + package::all::register_actions(); + + // Initializes the packages. + package::all::init_packages(); + + Self + } + + // Displays the application banner based on the configuration. + fn show_banner() { + use colored::Colorize; + use terminal_size::{terminal_size, Width}; + + if global::SETTINGS.app.startup_banner.to_lowercase() != "off" { + // Application name, formatted for the terminal width if necessary. + let mut app_ff = "".to_string(); + let app_name = &global::SETTINGS.app.name; + if let Some((Width(term_width), _)) = terminal_size() { + if term_width >= 80 { + let maxlen: usize = ((term_width / 10) - 2).into(); + let mut app = app_name.substring(0, maxlen).to_owned(); + if app_name.len() > maxlen { + app = format!("{app}..."); + } + if let Some(ff) = figfont::FIGFONT.convert(&app) { + app_ff = ff.to_string(); + } + } + } + if app_ff.is_empty() { + println!("\n{app_name}"); + } else { + print!("\n{app_ff}"); + } + + // Application description. + if !global::SETTINGS.app.description.is_empty() { + println!("{}", global::SETTINGS.app.description.cyan()); + }; + + // PageTop version. + println!( + "{} {}\n", + "Powered by PageTop".yellow(), + env!("CARGO_PKG_VERSION").yellow() + ); + } + } + + /// Starts the web server. + pub fn run(self) -> Result { + // Generate the cookie key. + let secret_key = service::cookie::Key::generate(); + + // Prepares the web server. + Ok(service::HttpServer::new(move || { + Self::service_app() + .wrap(tracing_actix_web::TracingLogger::default()) + .wrap( + SessionMiddleware::builder(CookieSessionStore::default(), secret_key.clone()) + .session_lifecycle(match global::SETTINGS.server.session_lifetime { + 0 => SessionLifecycle::BrowserSession(BrowserSession::default()), + _ => SessionLifecycle::PersistentSession( + PersistentSession::default().session_ttl( + service::cookie::time::Duration::seconds( + global::SETTINGS.server.session_lifetime, + ), + ), + ), + }) + .build(), + ) + }) + .bind(format!( + "{}:{}", + &global::SETTINGS.server.bind_address, + &global::SETTINGS.server.bind_port + ))? + .run()) + } + + /// Method for testing, returns a service application instance. + pub fn test( + self, + ) -> service::App< + impl service::Factory< + service::Request, + Config = (), + Response = service::Response, + Error = service::Error, + InitError = (), + >, + > { + Self::service_app() + } + + // Configures the service application. + fn service_app() -> service::App< + impl service::Factory< + service::Request, + Config = (), + Response = service::Response, + Error = service::Error, + InitError = (), + >, + > { + service::App::new() + .configure(package::all::configure_services) + .default_service(service::web::route().to(service_not_found)) + } +} + +async fn service_not_found(request: HttpRequest) -> ResultPage { + Err(ErrorPage::NotFound(request)) +} diff --git a/pagetop/src/app/figfont.rs b/pagetop/src/app/figfont.rs new file mode 100644 index 00000000..1da9d9b9 --- /dev/null +++ b/pagetop/src/app/figfont.rs @@ -0,0 +1,30 @@ +use crate::global; + +use std::sync::LazyLock; + +use figlet_rs::FIGfont; + +pub static FIGFONT: LazyLock = LazyLock::new(|| { + let slant = include_str!("slant.flf"); + let small = include_str!("small.flf"); + let speed = include_str!("speed.flf"); + let starwars = include_str!("starwars.flf"); + + FIGfont::from_content( + match global::SETTINGS.app.startup_banner.to_lowercase().as_str() { + "off" => slant, + "slant" => slant, + "small" => small, + "speed" => speed, + "starwars" => starwars, + _ => { + println!( + "\n FIGfont \"{}\" not found for banner. Using \"Slant\". Check settings files.", + global::SETTINGS.app.startup_banner, + ); + slant + } + }, + ) + .unwrap() +}); diff --git a/pagetop/src/app/slant.flf b/pagetop/src/app/slant.flf new file mode 100644 index 00000000..ea27b6ed --- /dev/null +++ b/pagetop/src/app/slant.flf @@ -0,0 +1,1295 @@ +flf2a$ 6 5 16 15 10 0 18319 +Slant by Glenn Chappell 3/93 -- based on Standard +Includes ISO Latin-1 +figlet release 2.1 -- 12 Aug 1994 +Permission is hereby given to modify this font, as long as the +modifier's name is placed on a comment line. + +Modified by Paul Burton 12/96 to include new parameter +supported by FIGlet and FIGWin. May also be slightly modified for better use +of new full-width/kern/smush alternatives, but default output is NOT changed. + + $$@ + $$ @ + $$ @ + $$ @ + $$ @ +$$ @@ + __@ + / /@ + / / @ + /_/ @ +(_) @ + @@ + _ _ @ +( | )@ +|/|/ @ + $ @ +$ @ + @@ + __ __ @ + __/ // /_@ + /_ _ __/@ +/_ _ __/ @ + /_//_/ @ + @@ + __@ + _/ /@ + / __/@ + (_ ) @ +/ _/ @ +/_/ @@ + _ __@ + (_)_/_/@ + _/_/ @ + _/_/_ @ +/_/ (_) @ + @@ + ___ @ + ( _ ) @ + / __ \/|@ +/ /_/ < @ +\____/\/ @ + @@ + _ @ + ( )@ + |/ @ + $ @ +$ @ + @@ + __@ + _/_/@ + / / @ + / / @ +/ / @ +|_| @@ + _ @ + | |@ + / /@ + / / @ + _/_/ @ +/_/ @@ + @ + __/|_@ + | /@ +/_ __| @ + |/ @ + @@ + @ + __ @ + __/ /_@ +/_ __/@ + /_/ @ + @@ + @ + @ + @ + _ @ +( )@ +|/ @@ + @ + @ + ______@ +/_____/@ + $ @ + @@ + @ + @ + @ + _ @ +(_)@ + @@ + __@ + _/_/@ + _/_/ @ + _/_/ @ +/_/ @ + @@ + ____ @ + / __ \@ + / / / /@ +/ /_/ / @ +\____/ @ + @@ + ___@ + < /@ + / / @ + / / @ +/_/ @ + @@ + ___ @ + |__ \@ + __/ /@ + / __/ @ +/____/ @ + @@ + _____@ + |__ /@ + /_ < @ + ___/ / @ +/____/ @ + @@ + __ __@ + / // /@ + / // /_@ +/__ __/@ + /_/ @ + @@ + ______@ + / ____/@ + /___ \ @ + ____/ / @ +/_____/ @ + @@ + _____@ + / ___/@ + / __ \ @ +/ /_/ / @ +\____/ @ + @@ + _____@ +/__ /@ + / / @ + / / @ +/_/ @ + @@ + ____ @ + ( __ )@ + / __ |@ +/ /_/ / @ +\____/ @ + @@ + ____ @ + / __ \@ + / /_/ /@ + \__, / @ +/____/ @ + @@ + @ + _ @ + (_)@ + _ @ +(_) @ + @@ + @ + _ @ + (_)@ + _ @ +( ) @ +|/ @@ + __@ + / /@ +/ / @ +\ \ @ + \_\@ + @@ + @ + _____@ + /____/@ +/____/ @ + $ @ + @@ +__ @ +\ \ @ + \ \@ + / /@ +/_/ @ + @@ + ___ @ + /__ \@ + / _/@ + /_/ @ +(_) @ + @@ + ______ @ + / ____ \@ + / / __ `/@ +/ / /_/ / @ +\ \__,_/ @ + \____/ @@ + ___ @ + / |@ + / /| |@ + / ___ |@ +/_/ |_|@ + @@ + ____ @ + / __ )@ + / __ |@ + / /_/ / @ +/_____/ @ + @@ + ______@ + / ____/@ + / / @ +/ /___ @ +\____/ @ + @@ + ____ @ + / __ \@ + / / / /@ + / /_/ / @ +/_____/ @ + @@ + ______@ + / ____/@ + / __/ @ + / /___ @ +/_____/ @ + @@ + ______@ + / ____/@ + / /_ @ + / __/ @ +/_/ @ + @@ + ______@ + / ____/@ + / / __ @ +/ /_/ / @ +\____/ @ + @@ + __ __@ + / / / /@ + / /_/ / @ + / __ / @ +/_/ /_/ @ + @@ + ____@ + / _/@ + / / @ + _/ / @ +/___/ @ + @@ + __@ + / /@ + __ / / @ +/ /_/ / @ +\____/ @ + @@ + __ __@ + / //_/@ + / ,< @ + / /| | @ +/_/ |_| @ + @@ + __ @ + / / @ + / / @ + / /___@ +/_____/@ + @@ + __ ___@ + / |/ /@ + / /|_/ / @ + / / / / @ +/_/ /_/ @ + @@ + _ __@ + / | / /@ + / |/ / @ + / /| / @ +/_/ |_/ @ + @@ + ____ @ + / __ \@ + / / / /@ +/ /_/ / @ +\____/ @ + @@ + ____ @ + / __ \@ + / /_/ /@ + / ____/ @ +/_/ @ + @@ + ____ @ + / __ \@ + / / / /@ +/ /_/ / @ +\___\_\ @ + @@ + ____ @ + / __ \@ + / /_/ /@ + / _, _/ @ +/_/ |_| @ + @@ + _____@ + / ___/@ + \__ \ @ + ___/ / @ +/____/ @ + @@ + ______@ + /_ __/@ + / / @ + / / @ +/_/ @ + @@ + __ __@ + / / / /@ + / / / / @ +/ /_/ / @ +\____/ @ + @@ + _ __@ +| | / /@ +| | / / @ +| |/ / @ +|___/ @ + @@ + _ __@ +| | / /@ +| | /| / / @ +| |/ |/ / @ +|__/|__/ @ + @@ + _ __@ + | |/ /@ + | / @ + / | @ +/_/|_| @ + @@ +__ __@ +\ \/ /@ + \ / @ + / / @ +/_/ @ + @@ + _____@ +/__ /@ + / / @ + / /__@ +/____/@ + @@ + ___@ + / _/@ + / / @ + / / @ + / / @ +/__/ @@ +__ @ +\ \ @ + \ \ @ + \ \ @ + \_\@ + @@ + ___@ + / /@ + / / @ + / / @ + _/ / @ +/__/ @@ + //|@ + |/||@ + $ @ + $ @ +$ @ + @@ + @ + @ + @ + @ + ______@ +/_____/@@ + _ @ + ( )@ + V @ + $ @ +$ @ + @@ + @ + ____ _@ + / __ `/@ +/ /_/ / @ +\__,_/ @ + @@ + __ @ + / /_ @ + / __ \@ + / /_/ /@ +/_.___/ @ + @@ + @ + _____@ + / ___/@ +/ /__ @ +\___/ @ + @@ + __@ + ____/ /@ + / __ / @ +/ /_/ / @ +\__,_/ @ + @@ + @ + ___ @ + / _ \@ +/ __/@ +\___/ @ + @@ + ____@ + / __/@ + / /_ @ + / __/ @ +/_/ @ + @@ + @ + ____ _@ + / __ `/@ + / /_/ / @ + \__, / @ +/____/ @@ + __ @ + / /_ @ + / __ \@ + / / / /@ +/_/ /_/ @ + @@ + _ @ + (_)@ + / / @ + / / @ +/_/ @ + @@ + _ @ + (_)@ + / / @ + / / @ + __/ / @ +/___/ @@ + __ @ + / /__@ + / //_/@ + / ,< @ +/_/|_| @ + @@ + __@ + / /@ + / / @ + / / @ +/_/ @ + @@ + @ + ____ ___ @ + / __ `__ \@ + / / / / / /@ +/_/ /_/ /_/ @ + @@ + @ + ____ @ + / __ \@ + / / / /@ +/_/ /_/ @ + @@ + @ + ____ @ + / __ \@ +/ /_/ /@ +\____/ @ + @@ + @ + ____ @ + / __ \@ + / /_/ /@ + / .___/ @ +/_/ @@ + @ + ____ _@ + / __ `/@ +/ /_/ / @ +\__, / @ + /_/ @@ + @ + _____@ + / ___/@ + / / @ +/_/ @ + @@ + @ + _____@ + / ___/@ + (__ ) @ +/____/ @ + @@ + __ @ + / /_@ + / __/@ +/ /_ @ +\__/ @ + @@ + @ + __ __@ + / / / /@ +/ /_/ / @ +\__,_/ @ + @@ + @ + _ __@ +| | / /@ +| |/ / @ +|___/ @ + @@ + @ + _ __@ +| | /| / /@ +| |/ |/ / @ +|__/|__/ @ + @@ + @ + _ __@ + | |/_/@ + _> < @ +/_/|_| @ + @@ + @ + __ __@ + / / / /@ + / /_/ / @ + \__, / @ +/____/ @@ + @ + ____@ +/_ /@ + / /_@ +/___/@ + @@ + __@ + _/_/@ + _/_/ @ +< < @ +/ / @ +\_\ @@ + __@ + / /@ + / / @ + / / @ + / / @ +/_/ @@ + _ @ + | |@ + / /@ + _>_>@ + _/_/ @ +/_/ @@ + /\//@ + //\/ @ + $ @ + $ @ +$ @ + @@ + _ _ @ + (_)(_)@ + / _ | @ + / __ | @ +/_/ |_| @ + @@ + _ _ @ + (_)_(_)@ + / __ \ @ +/ /_/ / @ +\____/ @ + @@ + _ _ @ + (_) (_)@ + / / / / @ +/ /_/ / @ +\____/ @ + @@ + _ _ @ + (_)_(_)@ + / __ `/ @ +/ /_/ / @ +\__,_/ @ + @@ + _ _ @ + (_)_(_)@ + / __ \ @ +/ /_/ / @ +\____/ @ + @@ + _ _ @ + (_) (_)@ + / / / / @ +/ /_/ / @ +\__,_/ @ + @@ + ____ @ + / __ \@ + / / / /@ + / /_| | @ + / //__/ @ +/_/ @@ +160 NO-BREAK SPACE + $$@ + $$ @ + $$ @ + $$ @ + $$ @ +$$ @@ +161 INVERTED EXCLAMATION MARK + _ @ + (_)@ + / / @ + / / @ +/_/ @ + @@ +162 CENT SIGN + __@ + __/ /@ + / ___/@ +/ /__ @ +\ _/ @ +/_/ @@ +163 POUND SIGN + ____ @ + / ,__\@ + __/ /_ @ + _/ /___ @ +(_,____/ @ + @@ +164 CURRENCY SIGN + /|___/|@ + | __ / @ + / /_/ / @ + /___ | @ +|/ |/ @ + @@ +165 YEN SIGN + ____@ + _| / /@ + /_ __/@ +/_ __/ @ + /_/ @ + @@ +166 BROKEN BAR + __@ + / /@ + /_/ @ + __ @ + / / @ +/_/ @@ +167 SECTION SIGN + __ @ + _/ _)@ + / | | @ + | || | @ + | |_/ @ +(__/ @@ +168 DIAERESIS + _ _ @ + (_) (_)@ + $ $ @ + $ $ @ +$ $ @ + @@ +169 COPYRIGHT SIGN + ______ @ + / _____\ @ + / / ___/ |@ + / / /__ / @ +| \___/ / @ + \______/ @@ +170 FEMININE ORDINAL INDICATOR + ___ _@ + / _ `/@ + _\_,_/ @ +/____/ @ + $ @ + @@ +171 LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + ____@ + / / /@ +/ / / @ +\ \ \ @ + \_\_\@ + @@ +172 NOT SIGN + @ + ______@ +/___ /@ + /_/ @ + $ @ + @@ +173 SOFT HYPHEN + @ + @ + _____@ +/____/@ + $ @ + @@ +174 REGISTERED SIGN + ______ @ + / ___ \ @ + / / _ \ |@ + / / , _/ / @ +| /_/|_| / @ + \______/ @@ +175 MACRON + ______@ +/_____/@ + $ @ + $ @ +$ @ + @@ +176 DEGREE SIGN + ___ @ + / _ \@ +/ // /@ +\___/ @ + $ @ + @@ +177 PLUS-MINUS SIGN + __ @ + __/ /_@ + /_ __/@ + __/_/_ @ +/_____/ @ + @@ +178 SUPERSCRIPT TWO + ___ @ + |_ |@ + / __/ @ +/____/ @ + $ @ + @@ +179 SUPERSCRIPT THREE + ____@ + |_ /@ + _/_ < @ +/____/ @ + $ @ + @@ +180 ACUTE ACCENT + __@ + /_/@ + $ @ + $ @ +$ @ + @@ +181 MICRO SIGN + @ + __ __@ + / / / /@ + / /_/ / @ + / ._,_/ @ +/_/ @@ +182 PILCROW SIGN + _______@ + / _ /@ +/ (/ / / @ +\_ / / @ + /_/_/ @ + @@ +183 MIDDLE DOT + @ + _ @ +(_)@ + $ @ +$ @ + @@ +184 CEDILLA + @ + @ + @ + @ + _ @ +/_)@@ +185 SUPERSCRIPT ONE + ___@ + < /@ + / / @ +/_/ @ +$ @ + @@ +186 MASCULINE ORDINAL INDICATOR + ___ @ + / _ \@ + _\___/@ +/____/ @ + $ @ + @@ +187 RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK +____ @ +\ \ \ @ + \ \ \@ + / / /@ +/_/_/ @ + @@ +188 VULGAR FRACTION ONE QUARTER + ___ __ @ + < / _/_/ @ + / /_/_/___@ +/_//_// / /@ + /_/ /_ _/@ + /_/ @@ +189 VULGAR FRACTION ONE HALF + ___ __ @ + < / _/_/__ @ + / /_/_/|_ |@ +/_//_/ / __/ @ + /_/ /____/ @ + @@ +190 VULGAR FRACTION THREE QUARTERS + ____ __ @ + |_ / _/_/ @ + _/_ < _/_/___@ +/____//_// / /@ + /_/ /_ _/@ + /_/ @@ +191 INVERTED QUESTION MARK + _ @ + (_)@ + _/ / @ +/ _/_ @ +\___/ @ + @@ +192 LATIN CAPITAL LETTER A WITH GRAVE + __ @ + _\_\@ + / _ |@ + / __ |@ +/_/ |_|@ + @@ +193 LATIN CAPITAL LETTER A WITH ACUTE + __@ + _/_/@ + / _ |@ + / __ |@ +/_/ |_|@ + @@ +194 LATIN CAPITAL LETTER A WITH CIRCUMFLEX + //|@ + _|/||@ + / _ | @ + / __ | @ +/_/ |_| @ + @@ +195 LATIN CAPITAL LETTER A WITH TILDE + /\//@ + _//\/ @ + / _ | @ + / __ | @ +/_/ |_| @ + @@ +196 LATIN CAPITAL LETTER A WITH DIAERESIS + _ _ @ + (_)(_)@ + / _ | @ + / __ | @ +/_/ |_| @ + @@ +197 LATIN CAPITAL LETTER A WITH RING ABOVE + (())@ + / |@ + / /| |@ + / ___ |@ +/_/ |_|@ + @@ +198 LATIN CAPITAL LETTER AE + __________@ + / ____/@ + / /| __/ @ + / __ /___ @ +/_/ /_____/ @ + @@ +199 LATIN CAPITAL LETTER C WITH CEDILLA + ______@ + / ____/@ + / / @ +/ /___ @ +\____/ @ + /_) @@ +200 LATIN CAPITAL LETTER E WITH GRAVE + __ @ + _\_\@ + / __/@ + / _/ @ +/___/ @ + @@ +201 LATIN CAPITAL LETTER E WITH ACUTE + __@ + _/_/@ + / __/@ + / _/ @ +/___/ @ + @@ +202 LATIN CAPITAL LETTER E WITH CIRCUMFLEX + //|@ + _|/||@ + / __/ @ + / _/ @ +/___/ @ + @@ +203 LATIN CAPITAL LETTER E WITH DIAERESIS + _ _ @ + (_)(_)@ + / __/ @ + / _/ @ +/___/ @ + @@ +204 LATIN CAPITAL LETTER I WITH GRAVE + __ @ + _\_\@ + / _/@ + _/ / @ +/___/ @ + @@ +205 LATIN CAPITAL LETTER I WITH ACUTE + __@ + _/_/@ + / _/@ + _/ / @ +/___/ @ + @@ +206 LATIN CAPITAL LETTER I WITH CIRCUMFLEX + //|@ + _|/||@ + / _/ @ + _/ / @ +/___/ @ + @@ +207 LATIN CAPITAL LETTER I WITH DIAERESIS + _ _ @ + (_)(_)@ + / _/ @ + _/ / @ +/___/ @ + @@ +208 LATIN CAPITAL LETTER ETH + ____ @ + / __ \@ + __/ /_/ /@ +/_ __/ / @ + /_____/ @ + @@ +209 LATIN CAPITAL LETTER N WITH TILDE + /\//@ + _//\/ @ + / |/ / @ + / / @ +/_/|_/ @ + @@ +210 LATIN CAPITAL LETTER O WITH GRAVE + __ @ + __\_\@ + / __ \@ +/ /_/ /@ +\____/ @ + @@ +211 LATIN CAPITAL LETTER O WITH ACUTE + __@ + __/_/@ + / __ \@ +/ /_/ /@ +\____/ @ + @@ +212 LATIN CAPITAL LETTER O WITH CIRCUMFLEX + //|@ + _|/||@ + / __ \@ +/ /_/ /@ +\____/ @ + @@ +213 LATIN CAPITAL LETTER O WITH TILDE + /\//@ + _//\/ @ + / __ \ @ +/ /_/ / @ +\____/ @ + @@ +214 LATIN CAPITAL LETTER O WITH DIAERESIS + _ _ @ + (_)_(_)@ + / __ \ @ +/ /_/ / @ +\____/ @ + @@ +215 MULTIPLICATION SIGN + @ + @ + /|/|@ + > < @ +|/|/ @ + @@ +216 LATIN CAPITAL LETTER O WITH STROKE + _____ @ + / _// \@ + / //// /@ +/ //// / @ +\_//__/ @ + @@ +217 LATIN CAPITAL LETTER U WITH GRAVE + __ @ + __\_\_@ + / / / /@ +/ /_/ / @ +\____/ @ + @@ +218 LATIN CAPITAL LETTER U WITH ACUTE + __ @ + __/_/_@ + / / / /@ +/ /_/ / @ +\____/ @ + @@ +219 LATIN CAPITAL LETTER U WITH CIRCUMFLEX + //| @ + _|/||_@ + / / / /@ +/ /_/ / @ +\____/ @ + @@ +220 LATIN CAPITAL LETTER U WITH DIAERESIS + _ _ @ + (_) (_)@ + / / / / @ +/ /_/ / @ +\____/ @ + @@ +221 LATIN CAPITAL LETTER Y WITH ACUTE + __ @ +__/_/_@ +\ \/ /@ + \ / @ + /_/ @ + @@ +222 LATIN CAPITAL LETTER THORN + __ @ + / /_ @ + / __ \@ + / ____/@ +/_/ @ + @@ +223 LATIN SMALL LETTER SHARP S + ____ @ + / __ \@ + / / / /@ + / /_| | @ + / //__/ @ +/_/ @@ +224 LATIN SMALL LETTER A WITH GRAVE + __ @ + __\_\_@ + / __ `/@ +/ /_/ / @ +\__,_/ @ + @@ +225 LATIN SMALL LETTER A WITH ACUTE + __ @ + __/_/_@ + / __ `/@ +/ /_/ / @ +\__,_/ @ + @@ +226 LATIN SMALL LETTER A WITH CIRCUMFLEX + //| @ + _|/||_@ + / __ `/@ +/ /_/ / @ +\__,_/ @ + @@ +227 LATIN SMALL LETTER A WITH TILDE + /\//@ + _//\/_@ + / __ `/@ +/ /_/ / @ +\__,_/ @ + @@ +228 LATIN SMALL LETTER A WITH DIAERESIS + _ _ @ + (_)_(_)@ + / __ `/ @ +/ /_/ / @ +\__,_/ @ + @@ +229 LATIN SMALL LETTER A WITH RING ABOVE + __ @ + __(())@ + / __ `/@ +/ /_/ / @ +\__,_/ @ + @@ +230 LATIN SMALL LETTER AE + @ + ____ ___ @ + / __ ` _ \@ +/ /_/ __/@ +\__,_____/ @ + @@ +231 LATIN SMALL LETTER C WITH CEDILLA + @ + _____@ + / ___/@ +/ /__ @ +\___/ @ +/_) @@ +232 LATIN SMALL LETTER E WITH GRAVE + __ @ + _\_\@ + / _ \@ +/ __/@ +\___/ @ + @@ +233 LATIN SMALL LETTER E WITH ACUTE + __@ + _/_/@ + / _ \@ +/ __/@ +\___/ @ + @@ +234 LATIN SMALL LETTER E WITH CIRCUMFLEX + //|@ + _|/||@ + / _ \ @ +/ __/ @ +\___/ @ + @@ +235 LATIN SMALL LETTER E WITH DIAERESIS + _ _ @ + (_)(_)@ + / _ \ @ +/ __/ @ +\___/ @ + @@ +236 LATIN SMALL LETTER I WITH GRAVE + __ @ + \_\@ + / / @ + / / @ +/_/ @ + @@ +237 LATIN SMALL LETTER I WITH ACUTE + __@ + /_/@ + / / @ + / / @ +/_/ @ + @@ +238 LATIN SMALL LETTER I WITH CIRCUMFLEX + //|@ + |/||@ + / / @ + / / @ +/_/ @ + @@ +239 LATIN SMALL LETTER I WITH DIAERESIS + _ _ @ + (_)_(_)@ + / / @ + / / @ +/_/ @ + @@ +240 LATIN SMALL LETTER ETH + || @ + =||=@ + ___ || @ +/ __` | @ +\____/ @ + @@ +241 LATIN SMALL LETTER N WITH TILDE + /\//@ + _//\/ @ + / __ \ @ + / / / / @ +/_/ /_/ @ + @@ +242 LATIN SMALL LETTER O WITH GRAVE + __ @ + __\_\@ + / __ \@ +/ /_/ /@ +\____/ @ + @@ +243 LATIN SMALL LETTER O WITH ACUTE + __@ + __/_/@ + / __ \@ +/ /_/ /@ +\____/ @ + @@ +244 LATIN SMALL LETTER O WITH CIRCUMFLEX + //|@ + _|/||@ + / __ \@ +/ /_/ /@ +\____/ @ + @@ +245 LATIN SMALL LETTER O WITH TILDE + /\//@ + _//\/ @ + / __ \ @ +/ /_/ / @ +\____/ @ + @@ +246 LATIN SMALL LETTER O WITH DIAERESIS + _ _ @ + (_)_(_)@ + / __ \ @ +/ /_/ / @ +\____/ @ + @@ +247 DIVISION SIGN + @ + _ @ + __(_)_@ +/_____/@ + (_) @ + @@ +248 LATIN SMALL LETTER O WITH STROKE + @ + _____ @ + / _// \@ +/ //// /@ +\_//__/ @ + @@ +249 LATIN SMALL LETTER U WITH GRAVE + __ @ + __\_\_@ + / / / /@ +/ /_/ / @ +\__,_/ @ + @@ +250 LATIN SMALL LETTER U WITH ACUTE + __ @ + __/_/_@ + / / / /@ +/ /_/ / @ +\__,_/ @ + @@ +251 LATIN SMALL LETTER U WITH CIRCUMFLEX + //| @ + _|/||_@ + / / / /@ +/ /_/ / @ +\__,_/ @ + @@ +252 LATIN SMALL LETTER U WITH DIAERESIS + _ _ @ + (_) (_)@ + / / / / @ +/ /_/ / @ +\__,_/ @ + @@ +253 LATIN SMALL LETTER Y WITH ACUTE + __ @ + __/_/_@ + / / / /@ + / /_/ / @ + \__, / @ +/____/ @@ +254 LATIN SMALL LETTER THORN + __ @ + / /_ @ + / __ \@ + / /_/ /@ + / .___/ @ +/_/ @@ +255 LATIN SMALL LETTER Y WITH DIAERESIS + _ _ @ + (_) (_)@ + / / / / @ + / /_/ / @ + \__, / @ +/____/ @@ diff --git a/pagetop/src/app/small.flf b/pagetop/src/app/small.flf new file mode 100644 index 00000000..d2a2c49c --- /dev/null +++ b/pagetop/src/app/small.flf @@ -0,0 +1,1097 @@ +flf2a$ 5 4 13 15 10 0 22415 +Small by Glenn Chappell 4/93 -- based on Standard +Includes ISO Latin-1 +figlet release 2.1 -- 12 Aug 1994 +Permission is hereby given to modify this font, as long as the +modifier's name is placed on a comment line. + +Modified by Paul Burton 12/96 to include new parameter +supported by FIGlet and FIGWin. May also be slightly modified for better use +of new full-width/kern/smush alternatives, but default output is NOT changed. + + $@ + $@ + $@ + $@ + $@@ + _ @ + | |@ + |_|@ + (_)@ + @@ + _ _ @ + ( | )@ + V V @ + $ @ + @@ + _ _ @ + _| | |_ @ + |_ . _|@ + |_ _|@ + |_|_| @@ + @ + ||_@ + (_-<@ + / _/@ + || @@ + _ __ @ + (_)/ / @ + / /_ @ + /_/(_)@ + @@ + __ @ + / _|___ @ + > _|_ _|@ + \_____| @ + @@ + _ @ + ( )@ + |/ @ + $ @ + @@ + __@ + / /@ + | | @ + | | @ + \_\@@ + __ @ + \ \ @ + | |@ + | |@ + /_/ @@ + @ + _/\_@ + > <@ + \/ @ + @@ + _ @ + _| |_ @ + |_ _|@ + |_| @ + @@ + @ + @ + _ @ + ( )@ + |/ @@ + @ + ___ @ + |___|@ + $ @ + @@ + @ + @ + _ @ + (_)@ + @@ + __@ + / /@ + / / @ + /_/ @ + @@ + __ @ + / \ @ + | () |@ + \__/ @ + @@ + _ @ + / |@ + | |@ + |_|@ + @@ + ___ @ + |_ )@ + / / @ + /___|@ + @@ + ____@ + |__ /@ + |_ \@ + |___/@ + @@ + _ _ @ + | | | @ + |_ _|@ + |_| @ + @@ + ___ @ + | __|@ + |__ \@ + |___/@ + @@ + __ @ + / / @ + / _ \@ + \___/@ + @@ + ____ @ + |__ |@ + / / @ + /_/ @ + @@ + ___ @ + ( _ )@ + / _ \@ + \___/@ + @@ + ___ @ + / _ \@ + \_, /@ + /_/ @ + @@ + _ @ + (_)@ + _ @ + (_)@ + @@ + _ @ + (_)@ + _ @ + ( )@ + |/ @@ + __@ + / /@ + < < @ + \_\@ + @@ + @ + ___ @ + |___|@ + |___|@ + @@ + __ @ + \ \ @ + > >@ + /_/ @ + @@ + ___ @ + |__ \@ + /_/@ + (_) @ + @@ + ____ @ + / __ \ @ + / / _` |@ + \ \__,_|@ + \____/ @@ + _ @ + /_\ @ + / _ \ @ + /_/ \_\@ + @@ + ___ @ + | _ )@ + | _ \@ + |___/@ + @@ + ___ @ + / __|@ + | (__ @ + \___|@ + @@ + ___ @ + | \ @ + | |) |@ + |___/ @ + @@ + ___ @ + | __|@ + | _| @ + |___|@ + @@ + ___ @ + | __|@ + | _| @ + |_| @ + @@ + ___ @ + / __|@ + | (_ |@ + \___|@ + @@ + _ _ @ + | || |@ + | __ |@ + |_||_|@ + @@ + ___ @ + |_ _|@ + | | @ + |___|@ + @@ + _ @ + _ | |@ + | || |@ + \__/ @ + @@ + _ __@ + | |/ /@ + | ' < @ + |_|\_\@ + @@ + _ @ + | | @ + | |__ @ + |____|@ + @@ + __ __ @ + | \/ |@ + | |\/| |@ + |_| |_|@ + @@ + _ _ @ + | \| |@ + | .` |@ + |_|\_|@ + @@ + ___ @ + / _ \ @ + | (_) |@ + \___/ @ + @@ + ___ @ + | _ \@ + | _/@ + |_| @ + @@ + ___ @ + / _ \ @ + | (_) |@ + \__\_\@ + @@ + ___ @ + | _ \@ + | /@ + |_|_\@ + @@ + ___ @ + / __|@ + \__ \@ + |___/@ + @@ + _____ @ + |_ _|@ + | | @ + |_| @ + @@ + _ _ @ + | | | |@ + | |_| |@ + \___/ @ + @@ + __ __@ + \ \ / /@ + \ V / @ + \_/ @ + @@ + __ __@ + \ \ / /@ + \ \/\/ / @ + \_/\_/ @ + @@ + __ __@ + \ \/ /@ + > < @ + /_/\_\@ + @@ + __ __@ + \ \ / /@ + \ V / @ + |_| @ + @@ + ____@ + |_ /@ + / / @ + /___|@ + @@ + __ @ + | _|@ + | | @ + | | @ + |__|@@ + __ @ + \ \ @ + \ \ @ + \_\@ + @@ + __ @ + |_ |@ + | |@ + | |@ + |__|@@ + /\ @ + |/\|@ + $ @ + $ @ + @@ + @ + @ + @ + ___ @ + |___|@@ + _ @ + ( )@ + \|@ + $ @ + @@ + @ + __ _ @ + / _` |@ + \__,_|@ + @@ + _ @ + | |__ @ + | '_ \@ + |_.__/@ + @@ + @ + __ @ + / _|@ + \__|@ + @@ + _ @ + __| |@ + / _` |@ + \__,_|@ + @@ + @ + ___ @ + / -_)@ + \___|@ + @@ + __ @ + / _|@ + | _|@ + |_| @ + @@ + @ + __ _ @ + / _` |@ + \__, |@ + |___/ @@ + _ @ + | |_ @ + | ' \ @ + |_||_|@ + @@ + _ @ + (_)@ + | |@ + |_|@ + @@ + _ @ + (_)@ + | |@ + _/ |@ + |__/ @@ + _ @ + | |__@ + | / /@ + |_\_\@ + @@ + _ @ + | |@ + | |@ + |_|@ + @@ + @ + _ __ @ + | ' \ @ + |_|_|_|@ + @@ + @ + _ _ @ + | ' \ @ + |_||_|@ + @@ + @ + ___ @ + / _ \@ + \___/@ + @@ + @ + _ __ @ + | '_ \@ + | .__/@ + |_| @@ + @ + __ _ @ + / _` |@ + \__, |@ + |_|@@ + @ + _ _ @ + | '_|@ + |_| @ + @@ + @ + ___@ + (_-<@ + /__/@ + @@ + _ @ + | |_ @ + | _|@ + \__|@ + @@ + @ + _ _ @ + | || |@ + \_,_|@ + @@ + @ + __ __@ + \ V /@ + \_/ @ + @@ + @ + __ __ __@ + \ V V /@ + \_/\_/ @ + @@ + @ + __ __@ + \ \ /@ + /_\_\@ + @@ + @ + _ _ @ + | || |@ + \_, |@ + |__/ @@ + @ + ___@ + |_ /@ + /__|@ + @@ + __@ + / /@ + _| | @ + | | @ + \_\@@ + _ @ + | |@ + | |@ + | |@ + |_|@@ + __ @ + \ \ @ + | |_@ + | | @ + /_/ @@ + /\/|@ + |/\/ @ + $ @ + $ @ + @@ + _ _ @ + (_)(_)@ + /--\ @ + /_/\_\@ + @@ + _ _ @ + (_)(_)@ + / __ \@ + \____/@ + @@ + _ _ @ + (_) (_)@ + | |_| |@ + \___/ @ + @@ + _ _ @ + (_)(_)@ + / _` |@ + \__,_|@ + @@ + _ _ @ + (_)_(_)@ + / _ \ @ + \___/ @ + @@ + _ _ @ + (_)(_)@ + | || |@ + \_,_|@ + @@ + ___ @ + / _ \@ + | |< <@ + | ||_/@ + |_| @@ +160 NO-BREAK SPACE + $@ + $@ + $@ + $@ + $@@ +161 INVERTED EXCLAMATION MARK + _ @ + (_)@ + | |@ + |_|@ + @@ +162 CENT SIGN + @ + || @ + / _)@ + \ _)@ + || @@ +163 POUND SIGN + __ @ + _/ _\ @ + |_ _|_ @ + (_,___|@ + @@ +164 CURRENCY SIGN + /\_/\@ + \ . /@ + / _ \@ + \/ \/@ + @@ +165 YEN SIGN + __ __ @ + \ V / @ + |__ __|@ + |__ __|@ + |_| @@ +166 BROKEN BAR + _ @ + | |@ + |_|@ + | |@ + |_|@@ +167 SECTION SIGN + __ @ + / _)@ + /\ \ @ + \ \/ @ + (__/ @@ +168 DIAERESIS + _ _ @ + (_)(_)@ + $ $ @ + $ $ @ + @@ +169 COPYRIGHT SIGN + ____ @ + / __ \ @ + / / _| \@ + \ \__| /@ + \____/ @@ +170 FEMININE ORDINAL INDICATOR + __ _ @ + / _` |@ + \__,_|@ + |____|@ + @@ +171 LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + ____@ + / / /@ + < < < @ + \_\_\@ + @@ +172 NOT SIGN + ____ @ + |__ |@ + |_|@ + $ @ + @@ +173 SOFT HYPHEN + @ + __ @ + |__|@ + $ @ + @@ +174 REGISTERED SIGN + ____ @ + / __ \ @ + / | -) \@ + \ ||\\ /@ + \____/ @@ +175 MACRON + ___ @ + |___|@ + $ @ + $ @ + @@ +176 DEGREE SIGN + _ @ + /.\@ + \_/@ + $ @ + @@ +177 PLUS-MINUS SIGN + _ @ + _| |_ @ + |_ _|@ + _|_|_ @ + |_____|@@ +178 SUPERSCRIPT TWO + __ @ + |_ )@ + /__|@ + $ @ + @@ +179 SUPERSCRIPT THREE + ___@ + |_ /@ + |__)@ + $ @ + @@ +180 ACUTE ACCENT + __@ + /_/@ + $ @ + $ @ + @@ +181 MICRO SIGN + @ + _ _ @ + | || |@ + | .,_|@ + |_| @@ +182 PILCROW SIGN + ____ @ + / |@ + \_ | |@ + |_|_|@ + @@ +183 MIDDLE DOT + @ + _ @ + (_)@ + $ @ + @@ +184 CEDILLA + @ + @ + @ + _ @ + )_)@@ +185 SUPERSCRIPT ONE + _ @ + / |@ + |_|@ + $ @ + @@ +186 MASCULINE ORDINAL INDICATOR + ___ @ + / _ \@ + \___/@ + |___|@ + @@ +187 RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + ____ @ + \ \ \ @ + > > >@ + /_/_/ @ + @@ +188 VULGAR FRACTION ONE QUARTER + _ __ @ + / |/ /__ @ + |_/ /_' |@ + /_/ |_|@ + @@ +189 VULGAR FRACTION ONE HALF + _ __ @ + / |/ /_ @ + |_/ /_ )@ + /_//__|@ + @@ +190 VULGAR FRACTION THREE QUARTERS + ___ __ @ + |_ // /__ @ + |__) /_' |@ + /_/ |_|@ + @@ +191 INVERTED QUESTION MARK + _ @ + (_) @ + / /_ @ + \___|@ + @@ +192 LATIN CAPITAL LETTER A WITH GRAVE + __ @ + \_\ @ + /--\ @ + /_/\_\@ + @@ +193 LATIN CAPITAL LETTER A WITH ACUTE + __ @ + /_/ @ + /--\ @ + /_/\_\@ + @@ +194 LATIN CAPITAL LETTER A WITH CIRCUMFLEX + /\ @ + |/\| @ + /--\ @ + /_/\_\@ + @@ +195 LATIN CAPITAL LETTER A WITH TILDE + /\/|@ + |/\/ @ + /--\ @ + /_/\_\@ + @@ +196 LATIN CAPITAL LETTER A WITH DIAERESIS + _ _ @ + (_)(_)@ + /--\ @ + /_/\_\@ + @@ +197 LATIN CAPITAL LETTER A WITH RING ABOVE + __ @ + (()) @ + /--\ @ + /_/\_\@ + @@ +198 LATIN CAPITAL LETTER AE + ____ @ + /, __|@ + / _ _| @ + /_/|___|@ + @@ +199 LATIN CAPITAL LETTER C WITH CEDILLA + ___ @ + / __|@ + | (__ @ + \___|@ + )_) @@ +200 LATIN CAPITAL LETTER E WITH GRAVE + __ @ + \_\@ + | -<@ + |__<@ + @@ +201 LATIN CAPITAL LETTER E WITH ACUTE + __@ + /_/@ + | -<@ + |__<@ + @@ +202 LATIN CAPITAL LETTER E WITH CIRCUMFLEX + /\ @ + |/\|@ + | -<@ + |__<@ + @@ +203 LATIN CAPITAL LETTER E WITH DIAERESIS + _ _ @ + (_)(_)@ + | -< @ + |__< @ + @@ +204 LATIN CAPITAL LETTER I WITH GRAVE + __ @ + \_\ @ + |_ _|@ + |___|@ + @@ +205 LATIN CAPITAL LETTER I WITH ACUTE + __ @ + /_/ @ + |_ _|@ + |___|@ + @@ +206 LATIN CAPITAL LETTER I WITH CIRCUMFLEX + //\ @ + |/_\|@ + |_ _|@ + |___|@ + @@ +207 LATIN CAPITAL LETTER I WITH DIAERESIS + _ _ @ + (_)_(_)@ + |_ _| @ + |___| @ + @@ +208 LATIN CAPITAL LETTER ETH + ____ @ + | __ \ @ + |_ _|) |@ + |____/ @ + @@ +209 LATIN CAPITAL LETTER N WITH TILDE + /\/|@ + |/\/ @ + | \| |@ + |_|\_|@ + @@ +210 LATIN CAPITAL LETTER O WITH GRAVE + __ @ + \_\_ @ + / __ \@ + \____/@ + @@ +211 LATIN CAPITAL LETTER O WITH ACUTE + __ @ + _/_/ @ + / __ \@ + \____/@ + @@ +212 LATIN CAPITAL LETTER O WITH CIRCUMFLEX + /\ @ + |/\| @ + / __ \@ + \____/@ + @@ +213 LATIN CAPITAL LETTER O WITH TILDE + /\/|@ + |/\/ @ + / __ \@ + \____/@ + @@ +214 LATIN CAPITAL LETTER O WITH DIAERESIS + _ _ @ + (_)(_)@ + / __ \@ + \____/@ + @@ +215 MULTIPLICATION SIGN + @ + /\/\@ + > <@ + \/\/@ + @@ +216 LATIN CAPITAL LETTER O WITH STROKE + ____ @ + / _//\ @ + | (//) |@ + \//__/ @ + @@ +217 LATIN CAPITAL LETTER U WITH GRAVE + __ @ + _\_\_ @ + | |_| |@ + \___/ @ + @@ +218 LATIN CAPITAL LETTER U WITH ACUTE + __ @ + _/_/_ @ + | |_| |@ + \___/ @ + @@ +219 LATIN CAPITAL LETTER U WITH CIRCUMFLEX + //\ @ + |/ \| @ + | |_| |@ + \___/ @ + @@ +220 LATIN CAPITAL LETTER U WITH DIAERESIS + _ _ @ + (_) (_)@ + | |_| |@ + \___/ @ + @@ +221 LATIN CAPITAL LETTER Y WITH ACUTE + __ @ + _/_/_@ + \ V /@ + |_| @ + @@ +222 LATIN CAPITAL LETTER THORN + _ @ + | |_ @ + | -_)@ + |_| @ + @@ +223 LATIN SMALL LETTER SHARP S + ___ @ + / _ \@ + | |< <@ + | ||_/@ + |_| @@ +224 LATIN SMALL LETTER A WITH GRAVE + __ @ + \_\_ @ + / _` |@ + \__,_|@ + @@ +225 LATIN SMALL LETTER A WITH ACUTE + __ @ + _/_/ @ + / _` |@ + \__,_|@ + @@ +226 LATIN SMALL LETTER A WITH CIRCUMFLEX + /\ @ + |/\| @ + / _` |@ + \__,_|@ + @@ +227 LATIN SMALL LETTER A WITH TILDE + /\/|@ + |/\/ @ + / _` |@ + \__,_|@ + @@ +228 LATIN SMALL LETTER A WITH DIAERESIS + _ _ @ + (_)(_)@ + / _` |@ + \__,_|@ + @@ +229 LATIN SMALL LETTER A WITH RING ABOVE + __ @ + (()) @ + / _` |@ + \__,_|@ + @@ +230 LATIN SMALL LETTER AE + @ + __ ___ @ + / _` -_)@ + \__,___|@ + @@ +231 LATIN SMALL LETTER C WITH CEDILLA + @ + __ @ + / _|@ + \__|@ + )_)@@ +232 LATIN SMALL LETTER E WITH GRAVE + __ @ + \_\ @ + / -_)@ + \___|@ + @@ +233 LATIN SMALL LETTER E WITH ACUTE + __ @ + /_/ @ + / -_)@ + \___|@ + @@ +234 LATIN SMALL LETTER E WITH CIRCUMFLEX + //\ @ + |/_\|@ + / -_)@ + \___|@ + @@ +235 LATIN SMALL LETTER E WITH DIAERESIS + _ _ @ + (_)_(_)@ + / -_) @ + \___| @ + @@ +236 LATIN SMALL LETTER I WITH GRAVE + __ @ + \_\@ + | |@ + |_|@ + @@ +237 LATIN SMALL LETTER I WITH ACUTE + __@ + /_/@ + | |@ + |_|@ + @@ +238 LATIN SMALL LETTER I WITH CIRCUMFLEX + //\ @ + |/_\|@ + | | @ + |_| @ + @@ +239 LATIN SMALL LETTER I WITH DIAERESIS + _ _ @ + (_)_(_)@ + | | @ + |_| @ + @@ +240 LATIN SMALL LETTER ETH + \\/\ @ + \/\\ @ + / _` |@ + \___/ @ + @@ +241 LATIN SMALL LETTER N WITH TILDE + /\/| @ + |/\/ @ + | ' \ @ + |_||_|@ + @@ +242 LATIN SMALL LETTER O WITH GRAVE + __ @ + \_\ @ + / _ \@ + \___/@ + @@ +243 LATIN SMALL LETTER O WITH ACUTE + __ @ + /_/ @ + / _ \@ + \___/@ + @@ +244 LATIN SMALL LETTER O WITH CIRCUMFLEX + //\ @ + |/_\|@ + / _ \@ + \___/@ + @@ +245 LATIN SMALL LETTER O WITH TILDE + /\/|@ + |/\/ @ + / _ \@ + \___/@ + @@ +246 LATIN SMALL LETTER O WITH DIAERESIS + _ _ @ + (_)_(_)@ + / _ \ @ + \___/ @ + @@ +247 DIVISION SIGN + _ @ + (_) @ + |___|@ + (_) @ + @@ +248 LATIN SMALL LETTER O WITH STROKE + @ + ___ @ + / //\@ + \//_/@ + @@ +249 LATIN SMALL LETTER U WITH GRAVE + __ @ + \_\_ @ + | || |@ + \_,_|@ + @@ +250 LATIN SMALL LETTER U WITH ACUTE + __ @ + _/_/ @ + | || |@ + \_,_|@ + @@ +251 LATIN SMALL LETTER U WITH CIRCUMFLEX + /\ @ + |/\| @ + | || |@ + \_,_|@ + @@ +252 LATIN SMALL LETTER U WITH DIAERESIS + _ _ @ + (_)(_)@ + | || |@ + \_,_|@ + @@ +253 LATIN SMALL LETTER Y WITH ACUTE + __ @ + _/_/ @ + | || |@ + \_, |@ + |__/ @@ +254 LATIN SMALL LETTER THORN + _ @ + | |__ @ + | '_ \@ + | .__/@ + |_| @@ +255 LATIN SMALL LETTER Y WITH DIAERESIS + _ _ @ + (_)(_)@ + | || |@ + \_, |@ + |__/ @@ diff --git a/pagetop/src/app/speed.flf b/pagetop/src/app/speed.flf new file mode 100644 index 00000000..d294c52f --- /dev/null +++ b/pagetop/src/app/speed.flf @@ -0,0 +1,1301 @@ +flf2a$ 6 5 16 15 16 +Speed by Claude Martins 2/95 -- based on Slant +Includes ISO Latin-1 +figlet release 2.1 -- 12 Aug 1994 +Permission is hereby given to modify this font, as long as the +modifier's name is placed on a comment line. + +Explanation of first line: +flf2 - "magic number" for file identification +a - should always be `a', for now +$ - the "hardblank" -- prints as a blank, but can't be smushed +6 - height of a character +5 - height of a character, not including descenders +14 - max line length (excluding comment lines) + a fudge factor +15 - default smushmode for this font +16 - number of comment lines + + $$@ + $$ @ + $$ @ + $$ @ + $$ @ +$$ @@ +______@ +___ /@ +__ / @ + /_/ @ +(_) @ + @@ +___ _ @ +_( | )@ +_|/|/ @ + $ @ + $ @ + @@ +_______ __ @ +____/ // /_@ +_ _ _ __/@ +/_ _ __/ @ + /_//_/ @ + @@ +_______@ +____/ /@ +__ __/@ +_(_ ) @ +/ _/ @ +/_/ @@ +____ __@ +__(_)_/_/@ +____/_/ @ +__/_/_ @ +/_/ (_) @ + @@ +______ @ +__( _ ) @ +_ __ \/|@ +/ /_/ < @ +\____/\/ @ + @@ +___ @ +_( )@ +_|/ @ + $ @ +$ @ + @@ +_______@ +____/_/@ +__ / @ +_ / @ +/ / @ +|_| @@ +______ @ +____| |@ +____ /@ +___ / @ +__/_/ @ +/_/ @@ +_____ @ +____/|_@ +_| /@ +/_ __| @ + |/ @ + @@ + @ +______ @ +___/ /_@ +/_ __/@ + /_/ @ + @@ + @ + @ + @ +___ @ +_( )@ +_|/ @@ + @ + @ +________@ +_/_____/@ + $ @ + @@ + @ + @ + @ +___ @ +_(_)@ + @@ +_________@ +______/_/@ +____/_/ @ +__/_/ @ +/_/ @ + @@ +_______ @ +__ __ \@ +_ / / /@ +/ /_/ / @ +\____/ @ + @@ +______@ +__< /@ +__ / @ +_ / @ +/_/ @ + @@ +______ @ +__|__ \@ +____/ /@ +_ __/ @ +/____/ @ + @@ +________@ +__|__ /@ +___/_ < @ +____/ / @ +/____/ @ + @@ +_____ __@ +__ // /@ +_ // /_@ +/__ __/@ + /_/ @ + @@ +__________@ +___ ____/@ +______ \ @ + ____/ / @ +/_____/ @ + @@ +________@ +__ ___/@ +_ __ \ @ +/ /_/ / @ +\____/ @ + @@ +______@ +/__ /@ +__ / @ +_ / @ +/_/ @ + @@ +_______ @ +__( __ )@ +_ __ |@ +/ /_/ / @ +\____/ @ + @@ +_______ @ +__ __ \@ +_ /_/ /@ +_\__, / @ +/____/ @ + @@ + @ +_____ @ +___(_)@ +___ @ +_(_) @ + @@ + @ +_____ @ +___(_)@ +___ @ +_( ) @ +_|/ @@ +____@ +_ /@ +/ / @ +\ \ @ + \_\@ + @@ + @ +_______@ +_ ____/@ +/____/ @ + $ @ + @@ +___ @ +__ \ @ +___ \@ +__ /@ +_/_/ @ + @@ +_____ @ +_ __ \@ +__/ _/@ +_/_/ @ +(_) @ + @@ +_________ @ +__ ____ \@ +_ / __ `/@ +/ / /_/ / @ +\ \__,_/ @ + \____/ @@ +_______ @ +___ |@ +__ /| |@ +_ ___ |@ +/_/ |_|@ + @@ +________ @ +___ __ )@ +__ __ |@ +_ /_/ / @ +/_____/ @ + @@ +_________@ +__ ____/@ +_ / @ +/ /___ @ +\____/ @ + @@ +________ @ +___ __ \@ +__ / / /@ +_ /_/ / @ +/_____/ @ + @@ +__________@ +___ ____/@ +__ __/ @ +_ /___ @ +/_____/ @ + @@ +__________@ +___ ____/@ +__ /_ @ +_ __/ @ +/_/ @ + @@ +_________@ +__ ____/@ +_ / __ @ +/ /_/ / @ +\____/ @ + @@ +______ __@ +___ / / /@ +__ /_/ / @ +_ __ / @ +/_/ /_/ @ + @@ +________@ +____ _/@ + __ / @ +__/ / @ +/___/ @ + @@ +_________@ +______ /@ +___ _ / @ +/ /_/ / @ +\____/ @ + @@ +______ __@ +___ //_/@ +__ ,< @ +_ /| | @ +/_/ |_| @ + @@ +______ @ +___ / @ +__ / @ +_ /___@ +/_____/@ + @@ +______ ___@ +___ |/ /@ +__ /|_/ / @ +_ / / / @ +/_/ /_/ @ + @@ +_____ __@ +___ | / /@ +__ |/ / @ +_ /| / @ +/_/ |_/ @ + @@ +_______ @ +__ __ \@ +_ / / /@ +/ /_/ / @ +\____/ @ + @@ +________ @ +___ __ \@ +__ /_/ /@ +_ ____/ @ +/_/ @ + @@ +_______ @ +__ __ \@ +_ / / /@ +/ /_/ / @ +\___\_\ @ + @@ +________ @ +___ __ \@ +__ /_/ /@ +_ _, _/ @ +/_/ |_| @ + @@ +________@ +__ ___/@ +_____ \ @ +____/ / @ +/____/ @ + @@ +________@ +___ __/@ +__ / @ +_ / @ +/_/ @ + @@ +_____ __@ +__ / / /@ +_ / / / @ +/ /_/ / @ +\____/ @ + @@ +___ __@ +__ | / /@ +__ | / / @ +__ |/ / @ +_____/ @ + @@ +___ __@ +__ | / /@ +__ | /| / / @ +__ |/ |/ / @ +____/|__/ @ + @@ +____ __@ +__ |/ /@ +__ / @ +_ | @ +/_/|_| @ + @@ +__ __@ +_ \/ /@ +__ / @ +_ / @ +/_/ @ + @@ +______@ +___ /@ +__ / @ +_ /__@ +/____/@ + @@ +________@ +____ _/@ +___ / @ +__ / @ +_ / @ +/__/ @@ +___ @ +__ \ @ +___ \ @ +____ \ @ +______\@ + @@ +________@ +____/ /@ +____ / @ +___ / @ +__/ / @ +/__/ @@ +_ //|@ +_|/||@ + $ @ + $ @ +$ @ + @@ + @ + @ + @ + @ +________@ +_/_____/@@ +___ @ +_( )@ +__V @ + $ @ +$ @ + @@ + @ +______ _@ +_ __ `/@ +/ /_/ / @ +\__,_/ @ + @@ +______ @ +___ /_ @ +__ __ \@ +_ /_/ /@ +/_.___/ @ + @@ + @ +_______@ +_ ___/@ +/ /__ @ +\___/ @ + @@ +_________@ +______ /@ +_ __ / @ +/ /_/ / @ +\__,_/ @ + @@ + @ +_____ @ +_ _ \@ +/ __/@ +\___/ @ + @@ +________@ +___ __/@ +__ /_ @ +_ __/ @ +/_/ @ + @@ + @ +_______ _@ +__ __ `/@ +_ /_/ / @ +_\__, / @ +/____/ @@ +______ @ +___ /_ @ +__ __ \@ +_ / / /@ +/_/ /_/ @ + @@ +_____ @ +___(_)@ +__ / @ +_ / @ +/_/ @ + @@ +________ @ +______(_)@ +_____ / @ +____ / @ +___ / @ +/___/ @@ +______ @ +___ /__@ +__ //_/@ +_ ,< @ +/_/|_| @ + @@ +______@ +___ /@ +__ / @ +_ / @ +/_/ @ + @@ + @ +_______ ___ @ +__ __ `__ \@ +_ / / / / /@ +/_/ /_/ /_/ @ + @@ + @ +_______ @ +__ __ \@ +_ / / /@ +/_/ /_/ @ + @@ + @ +______ @ +_ __ \@ +/ /_/ /@ +\____/ @ + @@ + @ +________ @ +___ __ \@ +__ /_/ /@ +_ .___/ @ +/_/ @@ + @ +______ _@ +_ __ `/@ +/ /_/ / @ +\__, / @ + /_/ @@ + @ +________@ +__ ___/@ +_ / @ +/_/ @ + @@ + @ +________@ +__ ___/@ +_(__ ) @ +/____/ @ + @@ +_____ @ +__ /_@ +_ __/@ +/ /_ @ +\__/ @ + @@ + @ +____ __@ +_ / / /@ +/ /_/ / @ +\__,_/ @ + @@ + @ +___ __@ +__ | / /@ +__ |/ / @ +_____/ @ + @@ + @ +___ __@ +__ | /| / /@ +__ |/ |/ / @ +____/|__/ @ + @@ + @ +____ __@ +__ |/_/@ +__> < @ +/_/|_| @ + @@ + @ +_____ __@ +__ / / /@ +_ /_/ / @ +_\__, / @ +/____/ @@ + @ +______@ +___ /@ +__ /_@ +_____/@ + @@ +_______@ +____/_/@ +__/_/ @ +< < @ +/ / @ +\_\ @@ +_______@ +____ /@ +___ / @ +__ / @ +_ / @ +/_/ @@ +____ _ @ +____| |@ +____/ /@ +____>_>@ +__/_/ @ +/_/ @@ +__/\//@ +_//\/ @ + $ @ + $ @ +$ @ + @@ +_____ _ @ +___(_)(_)@ +__ _ | @ +_ __ | @ +/_/ |_| @ + @@ +____ _ @ +__(_)_(_)@ +_ __ \ @ +/ /_/ / @ +\____/ @ + @@ +____ _ @ +__(_) (_)@ +_ / / / @ +/ /_/ / @ +\____/ @ + @@ +____ _ @ +__(_)_(_)@ +_ __ `/ @ +/ /_/ / @ +\__,_/ @ + @@ +____ _ @ +__(_)_(_)@ +_ __ \ @ +/ /_/ / @ +\____/ @ + @@ +____ _ @ +__(_) (_)@ +_ / / / @ +/ /_/ / @ +\__,_/ @ + @@ +_________ @ +____ __ \@ +___ / / /@ +__ /_| | @ +_ //__/ @ +/_/ @@ +160 + $$@ + $$ @ + $$ @ + $$ @ + $$ @ +$$ @@ +161 +_____ @ +___(_)@ +__ / @ +_ / @ +/_/ @ + @@ +162 +_______@ +____/ /@ +_ ___/@ +/ /__ @ +\ _/ @ +/_/ @@ +163 +_________ @ +____ ,__\@ +___/ /_ @ +__/ /___ @ +(_,____/ @ + @@ +164 +___ /|___/|@ +___| __ / @ +__ /_/ / @ +_ ___ | @ +|/ |/ @ + @@ +165 +___ ____@ +___| / /@ +_ _ __/@ +/_ __/ @ + /_/ @ + @@ +166 +_______@ +____ /@ +_____/ @ +____ @ +_ / @ +/_/ @@ +167 +_______ @ +____/ _)@ +__ | | @ +_| || | @ +_| |_/ @ +(__/ @@ +168 +___ _ @ +_(_) (_)@ + $ $ @ + $ $ @ +$ $ @ + @@ +169 +__________ @ +___ _____\ @ +__ / ___/ |@ +_ / /__ / @ +| \___/ / @ + \______/ @@ +170 +______ _@ +__ _ `/@ +__\_,_/ @ +/____/ @ + $ @ + @@ +171 +______@ +_ / /@ +/ / / @ +\ \ \ @ + \_\_\@ + @@ +172 + @ +________@ +_/___ /@ + /_/ @ + $ @ + @@ +173 + @ + @ +_______@ +_/____/@ + $ @ + @@ +174 +__________ @ +___ ___ \ @ +__ / _ \ |@ +_ / , _/ / @ +| /_/|_| / @ + \______/ @@ +175 +________@ +_/_____/@ + $ @ + $ @ + $ @ + @@ +176 +_____ @ +_ _ \@ +/ // /@ +\___/ @ + $ @ + @@ +177 +________ @ +_____/ /_@ +____ __/@ +___/_/_ @ +/_____/ @ + @@ +178 +__ ___ @ +__|_ |@ +_ __/ @ +/____/ @ + $ @ + @@ +179 +__ ____@ +__|_ /@ +__/_ < @ +/____/ @ + $ @ + @@ +180 +____@ +_/_/@ + $ @ + $ @ +$ @ + @@ +181 + @ +______ __@ +___ / / /@ +__ /_/ / @ +_ ._,_/ @ +/_/ @@ +182 +_________@ +_ _ /@ +/ (/ / / @ +\_ / / @ + /_/_/ @ + @@ +183 + @ +___ @ +_(_)@ + $ @ + $ @ + @@ +184 + @ + @ + @ + @ +___ @ +_/_)@@ +185 +_____@ +_< /@ +_ / @ +/_/ @ +$ @ + @@ +186 +______ @ +__ _ \@ +__\___/@ +/____/ @ + $ @ + @@ +187 +_____ @ +__ \ \ @ +___ \ \@ +__ / /@ +___/_/ @ + @@ +188 +_____ __ @ +_< / _/_/ @ +_/ /_/_/___@ +/_//_// / /@ + /_/ /_ _/@ + /_/ @@ +189 +_____ __ @ +_< / _/_/__ @ +_/ /_/_/|_ |@ +/_//_/ / __/ @ + /_/ /____/ @ + @@ +190 +__ ____ __ @ +__|_ / _/_/ @ +__/_ < _/_/___@ +/____//_// / /@ + /_/ /_ _/@ + /_/ @@ +191 +___ _ @ +___(_)@ +__ / @ +/ _/_ @ +\___/ @ + @@ +192 +______ @ +____\_\@ +__ _ |@ +_ __ |@ +/_/ |_|@ + @@ +193 +_______@ +____/_/@ +__ _ |@ +_ __ |@ +/_/ |_|@ + @@ +194 +____ //|@ +____|/||@ +__ _ | @ +_ __ | @ +/_/ |_| @ + @@ +195 +_____/\//@ +____//\/ @ +__ _ | @ +_ __ | @ +/_/ |_| @ + @@ +196 +_____ _ @ +___(_)(_)@ +__ _ | @ +_ __ | @ +/_/ |_| @ + @@ +197 +____(())@ +___ |@ +__ /| |@ +_ ___ |@ +/_/ |_|@ + @@ +198 +______________@ +___ ____/@ +__ /| __/ @ +_ __ /___ @ +/_/ /_____/ @ + @@ +199 +_________@ +__ ____/@ +_ / @ +/ /___ @ +\____/ @ + /_) @@ +200 +______ @ +____\_\@ +__ __/@ +_ _/ @ +/___/ @ + @@ +201 +_______@ +____/_/@ +__ __/@ +_ _/ @ +/___/ @ + @@ +202 +____ //|@ +____|/||@ +__ __/ @ +_ _/ @ +/___/ @ + @@ +203 +_____ _ @ +___(_)(_)@ +__ __/ @ +_ _/ @ +/___/ @ + @@ +204 +______ @ +____\_\@ +__ _/@ +__/ / @ +/___/ @ + @@ +205 +_______@ +____/_/@ +__ _/@ +__/ / @ +/___/ @ + @@ +206 +____ //|@ +____|/||@ +__ _/ @ +__/ / @ +/___/ @ + @@ +207 +_____ _ @ +___(_)(_)@ +__ _/ @ +__/ / @ +/___/ @ + @@ +208 +_________ @ +____ __ \@ +___ /_/ /@ +/_ __/ / @ + /_____/ @ + @@ +209 +_____/\//@ +____//\/ @ +__ |/ / @ +_ / @ +/_/|_/ @ + @@ +210 +______ @ +____\_\@ +_ __ \@ +/ /_/ /@ +\____/ @ + @@ +211 +_______@ +____/_/@ +_ __ \@ +/ /_/ /@ +\____/ @ + @@ +212 +___ //|@ +___|/||@ +_ __ \@ +/ /_/ /@ +\____/ @ + @@ +213 +____/\//@ +___//\/ @ +_ __ \ @ +/ /_/ / @ +\____/ @ + @@ +214 +____ _ @ +__(_)_(_)@ +_ __ \ @ +/ /_/ / @ +\____/ @ + @@ +215 + @ +__ @ +_/|/|@ + > < @ +|/|/ @ + @@ +216 +________ @ +__ _// \@ +_ //// /@ +/ //// / @ +\_//__/ @ + @@ +217 +______ @ +____\_\_@ +_ / / /@ +/ /_/ / @ +\____/ @ + @@ +218 +_______ @ +____/_/_@ +_ / / /@ +/ /_/ / @ +\____/ @ + @@ +219 +___ //| @ +___|/||_@ +_ / / /@ +/ /_/ / @ +\____/ @ + @@ +220 +____ _ @ +__(_) (_)@ +_ / / / @ +/ /_/ / @ +\____/ @ + @@ +221 +______ @ +___/_/_@ +__ \/ /@ +___ / @ +__/_/ @ + @@ +222 +______ @ +___ /_ @ +__ __ \@ +_ ____/@ +/_/ @ + @@ +223 +_________ @ +____ __ \@ +___ / / /@ +__ /_| | @ +_ //__/ @ +/_/ @@ +224 +______ @ +____\_\_@ +_ __ `/@ +/ /_/ / @ +\__,_/ @ + @@ +225 +_______ @ +____/_/_@ +_ __ `/@ +/ /_/ / @ +\__,_/ @ + @@ +226 +___ //| @ +___|/||_@ +_ __ `/@ +/ /_/ / @ +\__,_/ @ + @@ +227 +____/\//@ +___//\/_@ +_ __ `/@ +/ /_/ / @ +\__,_/ @ + @@ +228 +____ _ @ +__(_)_(_)@ +_ __ `/ @ +/ /_/ / @ +\__,_/ @ + @@ +229 +_______ @ +____(())@ +_ __ `/@ +/ /_/ / @ +\__,_/ @ + @@ +230 + @ +______ ___ @ +_ __ ` _ \@ +/ /_/ __/@ +\__,_____/ @ + @@ +231 + @ +_______@ +_ ___/@ +/ /__ @ +\___/ @ +/_) @@ +232 +_____ @ +___\_\@ +_ _ \@ +/ __/@ +\___/ @ + @@ +233 +______@ +___/_/@ +_ _ \@ +/ __/@ +\___/ @ + @@ +234 +___ //|@ +___|/||@ +_ _ \ @ +/ __/ @ +\___/ @ + @@ +235 +____ _ @ +__(_)(_)@ +_ _ \ @ +/ __/ @ +\___/ @ + @@ +236 +_____ @ +___\_\@ +__ / @ +_ / @ +/_/ @ + @@ +237 +______@ +___/_/@ +__ / @ +_ / @ +/_/ @ + @@ +238 +___ //|@ +___|/||@ +__ / @ +_ / @ +/_/ @ + @@ +239 +_ _ _ @ +_(_)_(_)@ +__/ / @ +_ / @ +/_/ @ + @@ +240 +____ || @ +____=||=@ +____ || @ +/ __` | @ +\____/ @ + @@ +241 +_____/\//@ +____//\/ @ +__ __ \ @ +_ / / / @ +/_/ /_/ @ + @@ +242 +______ @ +____\_\@ +_ __ \@ +/ /_/ /@ +\____/ @ + @@ +243 +_______@ +____/_/@ +_ __ \@ +/ /_/ /@ +\____/ @ + @@ +244 +___ //|@ +___|/||@ +_ __ \@ +/ /_/ /@ +\____/ @ + @@ +245 +____/\//@ +___//\/ @ +_ __ \ @ +/ /_/ / @ +\____/ @ + @@ +246 +____ _ @ +__(_)_(_)@ +_ __ \ @ +/ /_/ / @ +\____/ @ + @@ +247 + @ +_____ @ +___(_)_@ +/_____/@ + (_) @ + @@ +248 + @ +_______ @ +_ _// \@ +/ //// /@ +\_//__/ @ + @@ +249 +______ @ +____\_\_@ +_ / / /@ +/ /_/ / @ +\__,_/ @ + @@ +250 +_______ @ +____/_/_@ +_ / / /@ +/ /_/ / @ +\__,_/ @ + @@ +251 +___ //| @ +___|/||_@ +_ / / /@ +/ /_/ / @ +\__,_/ @ + @@ +252 +____ _ @ +__(_) (_)@ +_ / / / @ +/ /_/ / @ +\__,_/ @ + @@ +253 +________ @ +_____/_/_@ +__ / / /@ +_ /_/ / @ +_\__, / @ +/____/ @@ +254 +_______ @ +____ /_ @ +___ __ \@ +__ /_/ /@ +_ .___/ @ +/_/ @@ +255 +_____ _ @ +___(_) (_)@ +__ / / / @ +_ /_/ / @ +_\__, / @ +/____/ @@ diff --git a/pagetop/src/app/starwars.flf b/pagetop/src/app/starwars.flf new file mode 100644 index 00000000..6ea55f02 --- /dev/null +++ b/pagetop/src/app/starwars.flf @@ -0,0 +1,719 @@ +flf2a$ 7 6 22 15 4 +starwars.flf by Ryan Youck (youck@cs.uregina.ca) Dec 25/1994 +I am not responsible for use of this font +Based on Big.flf by Glenn Chappell + +$ $@ +$ $@ +$ $@ +$ $@ +$ $@ +$ $@ +$ $@@ + __ $@ +| |$@ +| |$@ +| |$@ +|__|$@ +(__)$@ + $@@ + _ _ @ +( | )@ + V V @ + $ @ + $ @ + $ @ + @@ + _ _ @ + _| || |_$@ +|_ __ _|@ + _| || |_ @ +|_ __ _|@ + |_||_| $@ + @@ + __,--,_.@ + / |@ + | (----`@ + \ \ $@ +.----) | $@ +|_ __/ $@ + '--' $@@ + _ ___$ @ + / \ / /$ @ +( o ) / / $ @ + \_/ / / _$ @ + / / / \ @ + / / ( o )@ + /__/ \_/ @@ + @ + ___ @ + ( _ ) $@ + / _ \/\@ +| (_> <@ + \___/\/@ + $@@ + __ @ +(_ )@ + |/ @ + $ @ + $ @ + $ @ + @@ + ___@ + / /@ +| |$@ +| |$@ +| |$@ +| |$@ + \__\@@ +___ @ +\ \ @ + | |@ + | |@ + | |@ + | |@ +/__/ @@ + _ @ + /\| |/\ @ + \ ` ' /$@ +|_ _|@ + / , . \$@ + \/|_|\/ @ + @@ + @ + _ @ + _| |_$@ +|_ _|@ + |_| $@ + $ @ + @@ + @ + @ + $ @ + $ @ + __ @ +(_ )@ + |/ @@ + @ + @ + ______ @ +|______|@ + $ @ + $ @ + @@ + @ + @ + @ + $ @ + __ @ +(__)@ + @@ + ___@ + / /@ + / / @ + / /$ @ + / /$ @ +/__/$ @ + @@ + ___ $@ + / _ \ $@ +| | | |$@ +| | | |$@ +| |_| |$@ + \___/ $@ + $@@ + __ $@ +/_ |$@ + | |$@ + | |$@ + | |$@ + |_|$@ + $@@ + ___ $@ +|__ \ $@ + $) |$@ + / / $@ + / /_ $@ +|____|$@ + $@@ + ____ $@ +|___ \ $@ + __) |$@ + |__ < $@ + ___) |$@ +|____/ $@ + $@@ + _ _ $@ +| || | $@ +| || |_ $@ +|__ _|$@ + | | $@ + |_| $@ + $@@ + _____ $@ +| ____|$@ +| |__ $@ +|___ \ $@ + ___) |$@ +|____/ $@ + $@@ + __ $@ + / / $@ + / /_ $@ +| '_ \ $@ +| (_) |$@ + \___/ $@ + $@@ + ______ $@ +|____ |$@ + $/ / $@ + / / $@ + / / $@ + /_/ $@ + $@@ + ___ $@ + / _ \ $@ +| (_) |$@ + > _ < $@ +| (_) |$@ + \___/ $@ + $@@ + ___ $@ + / _ \ $@ +| (_) |$@ + \__, |$@ + / / $@ + /_/ $@ + $@@ + @ + _ @ +(_)@ + $ @ + _ @ +(_)@ + @@ + @ + _ @ +(_)@ + $ @ + _ @ +( )@ +|/ @@ + ___@ + / /@ + / /$@ +< <$ @ + \ \$@ + \__\@ + @@ + @ + ______ @ +|______|@ + ______ @ +|______|@ + @ + @@ +___ @ +\ \$ @ + \ \ @ + > >@ + / / @ +/__/$ @ + @@ + ______ $@ +| \ $@ +`----) |$@ + / / $@ + |__| $@ + __ $@ + (__) $@@ + ____ @ + / __ \ @ + / / _` |@ +| | (_| |@ + \ \__,_|@ + \____/ @ + @@ + ___ $ @ + / \ $ @ + / ^ \$ @ + / /_\ \$ @ + / _____ \$ @ +/__/ \__\$@ + $@@ +.______ $@ +| _ \ $@ +| |_) |$@ +| _ < $@ +| |_) |$@ +|______/ $@ + $@@ + ______$@ + / |@ +| ,----'@ +| | $@ +| `----.@ + \______|@ + $@@ + _______ $@ +| \$@ +| .--. |@ +| | | |@ +| '--' |@ +|_______/$@ + $@@ + _______ @ +| ____|@ +| |__ $@ +| __| $@ +| |____ @ +|_______|@ + @@ + _______ @ +| ____|@ +| |__ $@ +| __| $@ +| | $ @ +|__| @ + @@ + _______ @ + / _____|@ +| | __ $@ +| | |_ |$@ +| |__| |$@ + \______|$@ + $@@ + __ __ $@ +| | | |$@ +| |__| |$@ +| __ |$@ +| | | |$@ +|__| |__|$@ + $@@ + __ $@ +| |$@ +| |$@ +| |$@ +| |$@ +|__|$@ + $@@ + __ $@ + | |$@ + | |$@ +.--. | |$@ +| `--' |$@ + \______/ $@ + $@@ + __ ___$@ +| |/ /$@ +| ' / $@ +| < $@ +| . \ $@ +|__|\__\$@ + $@@ + __ $@ +| | $@ +| | $@ +| | $@ +| `----.@ +|_______|@ + $@@ +.___ ___.$@ +| \/ |$@ +| \ / |$@ +| |\/| |$@ +| | | |$@ +|__| |__|$@ + $@@ +.__ __.$@ +| \ | |$@ +| \| |$@ +| . ` |$@ +| |\ |$@ +|__| \__|$@ + $@@ + ______ $@ + / __ \ $@ +| | | |$@ +| | | |$@ +| `--' |$@ + \______/ $@ + $@@ +.______ $@ +| _ \ $@ +| |_) |$@ +| ___/ $@ +| | $ @ +| _| $ @ + $ @@ + ______ $ @ + / __ \ $ @ +| | | | $ @ +| | | | $ @ +| `--' '--. @ + \_____\_____\@ + $ @@ +.______ $ @ +| _ \ $ @ +| |_) | $ @ +| / $ @ +| |\ \----.@ +| _| `._____|@ + $@@ + _______.@ + / |@ + | (----`@ + \ \ $@ +.----) | $@ +|_______/ $@ + $@@ +.___________.@ +| |@ +`---| |----`@ + | | $ @ + | | $ @ + |__| $ @ + $ @@ + __ __ $@ +| | | |$@ +| | | |$@ +| | | |$@ +| `--' |$@ + \______/ $@ + $@@ +____ ____$@ +\ \ / /$@ + \ \/ /$ @ + \ /$ @ + \ /$ @ + \__/$ @ + $ @@ +____ __ ____$@ +\ \ / \ / /$@ + \ \/ \/ /$ @ + \ /$ @ + \ /\ /$ @ + \__/ \__/$ @ + $ @@ +___ ___$@ +\ \ / /$@ + \ V / $@ + > < $@ + / . \ $@ +/__/ \__\$@ + $@@ +____ ____$@ +\ \ / /$@ + \ \/ /$ @ + \_ _/$ @ + | |$ @ + |__|$ @ + $ @@ + ________ $@ +| / $@ +`---/ / $@ + / / $@ + / /----.@ + /________|@ + $@@ + ____ @ +| |@ +| |-`@ +| | $@ +| | $@ +| |-.@ +|____|@@ +___ @ +\ \ $ @ + \ \$ @ + \ \$ @ + \ \$@ + \__\@ + @@ + ____ @ +| |@ +`-| |@ + | |@ + | |@ +.-| |@ +|____|@@ + ___ @ + / \ @ +/--^--\@ + $@ + $@ + $@ + $@@ + @ + @ + @ + $ @ + $ @ + ______ @ +|______|@@ + __ @ +( _)@ + \| @ + $ @ + $ @ + $ @ + @@ + ___ $ @ + / \ $ @ + / ^ \$ @ + / /_\ \$ @ + / _____ \$ @ +/__/ \__\$@ + $@@ +.______ $@ +| _ \ $@ +| |_) |$@ +| _ < $@ +| |_) |$@ +|______/ $@ + $@@ + ______$@ + / |@ +| ,----'@ +| | $@ +| `----.@ + \______|@ + $@@ + _______ $@ +| \$@ +| .--. |@ +| | | |@ +| '--' |@ +|_______/$@ + $@@ + _______ @ +| ____|@ +| |__ $@ +| __| $@ +| |____ @ +|_______|@ + @@ + _______ @ +| ____|@ +| |__ $@ +| __| $@ +| | $ @ +|__| @ + @@ + _______ @ + / _____|@ +| | __ $@ +| | |_ |$@ +| |__| |$@ + \______|$@ + $@@ + __ __ $@ +| | | |$@ +| |__| |$@ +| __ |$@ +| | | |$@ +|__| |__|$@ + $@@ + __ $@ +| |$@ +| |$@ +| |$@ +| |$@ +|__|$@ + $@@ + __ $@ + | |$@ + | |$@ +.--. | |$@ +| `--' |$@ + \______/ $@ + $@@ + __ ___$@ +| |/ /$@ +| ' / $@ +| < $@ +| . \ $@ +|__|\__\$@ + $@@ + __ $@ +| | $@ +| | $@ +| | $@ +| `----.@ +|_______|@ + $@@ +.___ ___.$@ +| \/ |$@ +| \ / |$@ +| |\/| |$@ +| | | |$@ +|__| |__|$@ + $@@ +.__ __.$@ +| \ | |$@ +| \| |$@ +| . ` |$@ +| |\ |$@ +|__| \__|$@ + $@@ + ______ $@ + / __ \ $@ +| | | |$@ +| | | |$@ +| `--' |$@ + \______/ $@ + $@@ +.______ $@ +| _ \ $@ +| |_) |$@ +| ___/ $@ +| | $ @ +| _| $ @ + $ @@ + ______ $ @ + / __ \ $ @ +| | | | $ @ +| | | | $ @ +| `--' '--. @ + \_____\_____\@ + $ @@ +.______ $ @ +| _ \ $ @ +| |_) | $ @ +| / $ @ +| |\ \----.@ +| _| `._____|@ + $@@ + _______.@ + / |@ + | (----`@ + \ \ $@ +.----) | $@ +|_______/ $@ + $@@ +.___________.@ +| |@ +`---| |----`@ + | | $ @ + | | $ @ + |__| $ @ + $ @@ + __ __ $@ +| | | |$@ +| | | |$@ +| | | |$@ +| `--' |$@ + \______/ $@ + $@@ +____ ____$@ +\ \ / /$@ + \ \/ /$ @ + \ /$ @ + \ /$ @ + \__/$ @ + $ @@ +____ __ ____$@ +\ \ / \ / /$@ + \ \/ \/ /$ @ + \ /$ @ + \ /\ /$ @ + \__/ \__/$ @ + $ @@ +___ ___$@ +\ \ / /$@ + \ V / $@ + > < $@ + / . \ $@ +/__/ \__\$@ + $@@ +____ ____$@ +\ \ / /$@ + \ \/ /$ @ + \_ _/$ @ + | |$ @ + |__|$ @ + $ @@ + ________ $@ +| / $@ +`---/ / $@ + / / $@ + / /----.@ + /________|@ + $@@ + ___@ + / /@ + | |$@ +/ /$ @ +\ \$ @ + | |$@ + \__\@@ + __ $@ +| |$@ +| |$@ +| |$@ +| |$@ +| |$@ +|__|$@@ +___ @ +\ \$ @ + | | @ + \ \@ + / /@ + | | @ +/__/$ @@ + __ _ @ + / \/ |@ +|_/\__/ @ + $ @ + $ @ + $ @ + @@ + _ _ @ + (_)_(_) @ + / \ @ + / _ \ @ + / ___ \ @ +/_/ \_\@ + @@ + _ _ @ +(_)_(_)@ + / _ \ @ +| | | |@ +| |_| |@ + \___/ @ + @@ + _ _ @ +(_) (_)@ +| | | |@ +| | | |@ +| |_| |@ + \___/ @ + @@ + _ _ @ +(_) (_)@ + __ _ @ + / _` |@ +| (_| |@ + \__,_|@ + @@ + _ _ @ +(_) (_)@ + ___ @ + / _ \ @ +| (_) |@ + \___/ @ + @@ + _ _ @ +(_) (_)@ + _ _ @ +| | | |@ +| |_| |@ + \__,_|@ + @@ + ___ @ + / _ \ @ +| | ) |@ +| |< < @ +| | ) |@ +| ||_/ @ +|_| @@ diff --git a/pagetop/src/base.rs b/pagetop/src/base.rs new file mode 100644 index 00000000..196f119c --- /dev/null +++ b/pagetop/src/base.rs @@ -0,0 +1,9 @@ +//! Base actions, components, packages, and themes. + +pub mod action; + +pub mod component; + +pub mod package; + +pub mod theme; diff --git a/pagetop/src/base/action.rs b/pagetop/src/base/action.rs new file mode 100644 index 00000000..fe6741c5 --- /dev/null +++ b/pagetop/src/base/action.rs @@ -0,0 +1,9 @@ +use crate::prelude::*; + +pub type FnActionWithComponent = fn(component: &mut C, cx: &mut Context); + +pub mod page; + +pub mod theme; + +pub mod component; diff --git a/pagetop/src/base/action/component.rs b/pagetop/src/base/action/component.rs new file mode 100644 index 00000000..03eedf26 --- /dev/null +++ b/pagetop/src/base/action/component.rs @@ -0,0 +1,8 @@ +mod is_renderable; +pub use is_renderable::*; + +mod before_prepare_component; +pub use before_prepare_component::*; + +mod after_prepare_component; +pub use after_prepare_component::*; diff --git a/pagetop/src/base/action/component/after_prepare_component.rs b/pagetop/src/base/action/component/after_prepare_component.rs new file mode 100644 index 00000000..233c1a7b --- /dev/null +++ b/pagetop/src/base/action/component/after_prepare_component.rs @@ -0,0 +1,65 @@ +use crate::prelude::*; + +use crate::base::action::FnActionWithComponent; + +pub struct AfterPrepare { + f: FnActionWithComponent, + referer_type_id: Option, + referer_id: OptionId, + weight: Weight, +} + +impl ActionTrait for AfterPrepare { + fn referer_type_id(&self) -> Option { + self.referer_type_id + } + + fn referer_id(&self) -> Option { + self.referer_id.get() + } + + fn weight(&self) -> Weight { + self.weight + } +} + +impl AfterPrepare { + pub fn new(f: FnActionWithComponent) -> Self { + AfterPrepare { + f, + referer_type_id: Some(TypeId::of::()), + referer_id: OptionId::default(), + weight: 0, + } + } + + pub fn filter_by_referer_id(mut self, id: impl Into) -> Self { + self.referer_id.set_value(id); + self + } + + pub fn with_weight(mut self, value: Weight) -> Self { + self.weight = value; + self + } + + #[inline(always)] + #[allow(clippy::inline_always)] + pub(crate) fn dispatch(component: &mut C, cx: &mut Context) { + dispatch_actions( + &ActionKey::new(TypeId::of::(), None, Some(TypeId::of::()), None), + |action: &Self| (action.f)(component, cx), + ); + if let Some(id) = component.id() { + dispatch_actions( + &ActionKey::new( + TypeId::of::(), + None, + Some(TypeId::of::()), + Some(id), + ), + |action: &Self| (action.f)(component, cx), + ); + } + } +} diff --git a/pagetop/src/base/action/component/before_prepare_component.rs b/pagetop/src/base/action/component/before_prepare_component.rs new file mode 100644 index 00000000..cedc45db --- /dev/null +++ b/pagetop/src/base/action/component/before_prepare_component.rs @@ -0,0 +1,65 @@ +use crate::prelude::*; + +use crate::base::action::FnActionWithComponent; + +pub struct BeforePrepare { + f: FnActionWithComponent, + referer_type_id: Option, + referer_id: OptionId, + weight: Weight, +} + +impl ActionTrait for BeforePrepare { + fn referer_type_id(&self) -> Option { + self.referer_type_id + } + + fn referer_id(&self) -> Option { + self.referer_id.get() + } + + fn weight(&self) -> Weight { + self.weight + } +} + +impl BeforePrepare { + pub fn new(f: FnActionWithComponent) -> Self { + BeforePrepare { + f, + referer_type_id: Some(TypeId::of::()), + referer_id: OptionId::default(), + weight: 0, + } + } + + pub fn filter_by_referer_id(mut self, id: impl Into) -> Self { + self.referer_id.set_value(id); + self + } + + pub fn with_weight(mut self, value: Weight) -> Self { + self.weight = value; + self + } + + #[inline(always)] + #[allow(clippy::inline_always)] + pub(crate) fn dispatch(component: &mut C, cx: &mut Context) { + dispatch_actions( + &ActionKey::new(TypeId::of::(), None, Some(TypeId::of::()), None), + |action: &Self| (action.f)(component, cx), + ); + if let Some(id) = component.id() { + dispatch_actions( + &ActionKey::new( + TypeId::of::(), + None, + Some(TypeId::of::()), + Some(id), + ), + |action: &Self| (action.f)(component, cx), + ); + } + } +} diff --git a/pagetop/src/base/action/component/is_renderable.rs b/pagetop/src/base/action/component/is_renderable.rs new file mode 100644 index 00000000..93159237 --- /dev/null +++ b/pagetop/src/base/action/component/is_renderable.rs @@ -0,0 +1,77 @@ +use crate::prelude::*; + +pub type FnIsRenderable = fn(component: &C, cx: &mut Context) -> bool; + +pub struct IsRenderable { + f: FnIsRenderable, + referer_type_id: Option, + referer_id: OptionId, + weight: Weight, +} + +impl ActionTrait for IsRenderable { + fn referer_type_id(&self) -> Option { + self.referer_type_id + } + + fn referer_id(&self) -> Option { + self.referer_id.get() + } + + fn weight(&self) -> Weight { + self.weight + } +} + +impl IsRenderable { + pub fn new(f: FnIsRenderable) -> Self { + IsRenderable { + f, + referer_type_id: Some(TypeId::of::()), + referer_id: OptionId::default(), + weight: 0, + } + } + + pub fn filter_by_referer_id(mut self, id: impl Into) -> Self { + self.referer_id.set_value(id); + self + } + + pub fn with_weight(mut self, value: Weight) -> Self { + self.weight = value; + self + } + + #[inline(always)] + #[allow(clippy::inline_always)] + pub(crate) fn dispatch(component: &C, cx: &mut Context) -> bool { + let mut renderable = true; + dispatch_actions( + &ActionKey::new(TypeId::of::(), None, Some(TypeId::of::()), None), + |action: &Self| { + if renderable && !(action.f)(component, cx) { + renderable = false; + } + }, + ); + if renderable { + if let Some(id) = component.id() { + dispatch_actions( + &ActionKey::new( + TypeId::of::(), + None, + Some(TypeId::of::()), + Some(id), + ), + |action: &Self| { + if renderable && !(action.f)(component, cx) { + renderable = false; + } + }, + ); + } + } + renderable + } +} diff --git a/pagetop/src/base/action/page.rs b/pagetop/src/base/action/page.rs new file mode 100644 index 00000000..9c89e2d1 --- /dev/null +++ b/pagetop/src/base/action/page.rs @@ -0,0 +1,5 @@ +mod before_render_body; +pub use before_render_body::*; + +mod after_render_body; +pub use after_render_body::*; diff --git a/pagetop/src/base/action/page/after_render_body.rs b/pagetop/src/base/action/page/after_render_body.rs new file mode 100644 index 00000000..81a89d87 --- /dev/null +++ b/pagetop/src/base/action/page/after_render_body.rs @@ -0,0 +1,34 @@ +use crate::prelude::*; + +pub type FnAfterRenderBody = fn(page: &mut Page); + +pub struct AfterRenderBody { + f: FnAfterRenderBody, + weight: Weight, +} + +impl ActionTrait for AfterRenderBody { + fn weight(&self) -> Weight { + self.weight + } +} + +impl AfterRenderBody { + pub fn new(f: FnAfterRenderBody) -> Self { + AfterRenderBody { f, weight: 0 } + } + + pub fn with_weight(mut self, value: Weight) -> Self { + self.weight = value; + self + } + + #[inline(always)] + #[allow(clippy::inline_always)] + pub(crate) fn dispatch(page: &mut Page) { + dispatch_actions( + &ActionKey::new(TypeId::of::(), None, None, None), + |action: &Self| (action.f)(page), + ); + } +} diff --git a/pagetop/src/base/action/page/before_render_body.rs b/pagetop/src/base/action/page/before_render_body.rs new file mode 100644 index 00000000..e0a9d770 --- /dev/null +++ b/pagetop/src/base/action/page/before_render_body.rs @@ -0,0 +1,34 @@ +use crate::prelude::*; + +pub type FnBeforeRenderBody = fn(page: &mut Page); + +pub struct BeforeRenderBody { + f: FnBeforeRenderBody, + weight: Weight, +} + +impl ActionTrait for BeforeRenderBody { + fn weight(&self) -> Weight { + self.weight + } +} + +impl BeforeRenderBody { + pub fn new(f: FnBeforeRenderBody) -> Self { + BeforeRenderBody { f, weight: 0 } + } + + pub fn with_weight(mut self, value: Weight) -> Self { + self.weight = value; + self + } + + #[inline(always)] + #[allow(clippy::inline_always)] + pub(crate) fn dispatch(page: &mut Page) { + dispatch_actions( + &ActionKey::new(TypeId::of::(), None, None, None), + |action: &Self| (action.f)(page), + ); + } +} diff --git a/pagetop/src/base/action/theme.rs b/pagetop/src/base/action/theme.rs new file mode 100644 index 00000000..8f307d98 --- /dev/null +++ b/pagetop/src/base/action/theme.rs @@ -0,0 +1,8 @@ +mod before_prepare_component; +pub use before_prepare_component::*; + +mod after_prepare_component; +pub use after_prepare_component::*; + +mod render_component; +pub use render_component::*; diff --git a/pagetop/src/base/action/theme/after_prepare_component.rs b/pagetop/src/base/action/theme/after_prepare_component.rs new file mode 100644 index 00000000..7285aec1 --- /dev/null +++ b/pagetop/src/base/action/theme/after_prepare_component.rs @@ -0,0 +1,43 @@ +use crate::prelude::*; + +use crate::base::action::FnActionWithComponent; + +pub struct AfterPrepare { + f: FnActionWithComponent, + theme_type_id: Option, + referer_type_id: Option, +} + +impl ActionTrait for AfterPrepare { + fn theme_type_id(&self) -> Option { + self.theme_type_id + } + + fn referer_type_id(&self) -> Option { + self.referer_type_id + } +} + +impl AfterPrepare { + pub fn new(theme: ThemeRef, f: FnActionWithComponent) -> Self { + AfterPrepare { + f, + theme_type_id: Some(theme.type_id()), + referer_type_id: Some(TypeId::of::()), + } + } + + #[inline(always)] + #[allow(clippy::inline_always)] + pub(crate) fn dispatch(component: &mut C, cx: &mut Context) { + dispatch_actions( + &ActionKey::new( + TypeId::of::(), + Some(cx.theme().type_id()), + Some(TypeId::of::()), + None, + ), + |action: &Self| (action.f)(component, cx), + ); + } +} diff --git a/pagetop/src/base/action/theme/before_prepare_component.rs b/pagetop/src/base/action/theme/before_prepare_component.rs new file mode 100644 index 00000000..7c80a655 --- /dev/null +++ b/pagetop/src/base/action/theme/before_prepare_component.rs @@ -0,0 +1,43 @@ +use crate::prelude::*; + +use crate::base::action::FnActionWithComponent; + +pub struct BeforePrepare { + f: FnActionWithComponent, + theme_type_id: Option, + referer_type_id: Option, +} + +impl ActionTrait for BeforePrepare { + fn theme_type_id(&self) -> Option { + self.theme_type_id + } + + fn referer_type_id(&self) -> Option { + self.referer_type_id + } +} + +impl BeforePrepare { + pub fn new(theme: ThemeRef, f: FnActionWithComponent) -> Self { + BeforePrepare { + f, + theme_type_id: Some(theme.type_id()), + referer_type_id: Some(TypeId::of::()), + } + } + + #[inline(always)] + #[allow(clippy::inline_always)] + pub(crate) fn dispatch(component: &mut C, cx: &mut Context) { + dispatch_actions( + &ActionKey::new( + TypeId::of::(), + Some(cx.theme().type_id()), + Some(TypeId::of::()), + None, + ), + |action: &Self| (action.f)(component, cx), + ); + } +} diff --git a/pagetop/src/base/action/theme/render_component.rs b/pagetop/src/base/action/theme/render_component.rs new file mode 100644 index 00000000..e79c5c92 --- /dev/null +++ b/pagetop/src/base/action/theme/render_component.rs @@ -0,0 +1,49 @@ +use crate::prelude::*; + +pub type FnRenderComponent = fn(component: &C, cx: &mut Context) -> Option; + +pub struct RenderComponent { + f: FnRenderComponent, + theme_type_id: Option, + referer_type_id: Option, +} + +impl ActionTrait for RenderComponent { + fn theme_type_id(&self) -> Option { + self.theme_type_id + } + + fn referer_type_id(&self) -> Option { + self.referer_type_id + } +} + +impl RenderComponent { + pub fn new(theme: ThemeRef, f: FnRenderComponent) -> Self { + RenderComponent { + f, + theme_type_id: Some(theme.type_id()), + referer_type_id: Some(TypeId::of::()), + } + } + + #[inline(always)] + #[allow(clippy::inline_always)] + pub(crate) fn dispatch(component: &C, cx: &mut Context) -> Option { + let mut render_component: Option = None; + dispatch_actions( + &ActionKey::new( + TypeId::of::(), + Some(cx.theme().type_id()), + Some(TypeId::of::()), + None, + ), + |action: &Self| { + if render_component.is_none() { + render_component = (action.f)(component, cx); + } + }, + ); + render_component + } +} diff --git a/pagetop/src/base/component.rs b/pagetop/src/base/component.rs new file mode 100644 index 00000000..93b22fa1 --- /dev/null +++ b/pagetop/src/base/component.rs @@ -0,0 +1,11 @@ +mod html; +pub use html::Html; + +mod fluent; +pub use fluent::Fluent; + +mod error403; +pub use error403::Error403; + +mod error404; +pub use error404::Error404; diff --git a/pagetop/src/base/component/error403.rs b/pagetop/src/base/component/error403.rs new file mode 100644 index 00000000..172487d7 --- /dev/null +++ b/pagetop/src/base/component/error403.rs @@ -0,0 +1,17 @@ +use crate::prelude::*; + +pub struct Error403; + +impl ComponentTrait for Error403 { + fn new() -> Self { + Error403 + } + + fn prepare_component(&self, _cx: &mut Context) -> PrepareMarkup { + PrepareMarkup::With(html! { + div { + h1 { ("FORBIDDEN ACCESS") } + } + }) + } +} diff --git a/pagetop/src/base/component/error404.rs b/pagetop/src/base/component/error404.rs new file mode 100644 index 00000000..0292df8f --- /dev/null +++ b/pagetop/src/base/component/error404.rs @@ -0,0 +1,17 @@ +use crate::prelude::*; + +pub struct Error404; + +impl ComponentTrait for Error404 { + fn new() -> Self { + Error404 + } + + fn prepare_component(&self, _cx: &mut Context) -> PrepareMarkup { + PrepareMarkup::With(html! { + div { + h1 { ("RESOURCE NOT FOUND") } + } + }) + } +} diff --git a/pagetop/src/base/component/fluent.rs b/pagetop/src/base/component/fluent.rs new file mode 100644 index 00000000..9e7220fa --- /dev/null +++ b/pagetop/src/base/component/fluent.rs @@ -0,0 +1,25 @@ +use crate::prelude::*; + +#[derive(AutoDefault)] +pub struct Fluent(L10n); + +impl ComponentTrait for Fluent { + fn new() -> Self { + Fluent::default() + } + + fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup { + PrepareMarkup::With(self.0.escaped(cx.langid())) + } +} + +impl Fluent { + pub fn with(l10n: L10n) -> Self { + Fluent(l10n) + } + + pub fn set_l10n(&mut self, l10n: L10n) -> &mut Self { + self.0 = l10n; + self + } +} diff --git a/pagetop/src/base/component/html.rs b/pagetop/src/base/component/html.rs new file mode 100644 index 00000000..6d308438 --- /dev/null +++ b/pagetop/src/base/component/html.rs @@ -0,0 +1,25 @@ +use crate::prelude::*; + +#[derive(AutoDefault)] +pub struct Html(Markup); + +impl ComponentTrait for Html { + fn new() -> Self { + Html::default() + } + + fn prepare_component(&self, _cx: &mut Context) -> PrepareMarkup { + PrepareMarkup::With(html! { (self.0) }) + } +} + +impl Html { + pub fn with(html: Markup) -> Self { + Html(html) + } + + pub fn set_html(&mut self, html: Markup) -> &mut Self { + self.0 = html; + self + } +} diff --git a/pagetop/src/base/package.rs b/pagetop/src/base/package.rs new file mode 100644 index 00000000..7b633f33 --- /dev/null +++ b/pagetop/src/base/package.rs @@ -0,0 +1,133 @@ +use crate::prelude::*; + +pub struct Welcome; + +impl PackageTrait for Welcome { + fn name(&self) -> L10n { + L10n::l("welcome_package_name") + } + + fn description(&self) -> L10n { + L10n::l("welcome_package_description") + } + + fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { + scfg.route("/", service::web::get().to(homepage)); + } +} + +async fn homepage(request: HttpRequest) -> ResultPage { + Page::new(request) + .with_title(L10n::l("welcome_page")) + .with_assets(AssetsOp::Theme("Basic")) + .with_assets(AssetsOp::AddStyleSheet(StyleSheet::inline("styles", r##" + body { + background-color: #f3d060; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + font-size: 20px; + } + .skip__to_content { + display: none; + } + .wrapper { + max-width: 1200px; + width: 100%; + margin: 0 auto; + padding: 0; + } + .container { + padding: 0 16px; + } + .title { + font-size: clamp(3rem, 10vw, 10rem); + letter-spacing: -0.05em; + line-height: 1.2; + margin: 0; + } + .subtitle { + font-size: clamp(1.8rem, 2vw, 3rem); + letter-spacing: -0.02em; + line-height: 1.2; + margin: 0; + } + .powered { + margin: .5em 0 1em; + } + .box-container { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: stretch; + gap: 1.5em; + } + .box { + flex: 1 1 280px; + border: 3px solid #25282a; + box-shadow: 5px 5px 0px #25282a; + box-sizing: border-box; + padding: 0 16px; + } + footer { + margin-top: 5em; + font-size: 14px; + font-weight: 500; + color: #a5282c; + } + "##))) + .with_component(Html::with(html! { + div class="wrapper" { + div class="container" { + h1 class="title" { (L10n::l("welcome_title").markup()) } + + p class="subtitle" { + (L10n::l("welcome_intro").with_arg("app", format!( + "{}", + &global::SETTINGS.app.name + )).markup()) + } + p class="powered" { + (L10n::l("welcome_powered").with_arg("pagetop", format!( + "{}", + "https://crates.io/crates/pagetop", "PageTop" + )).markup()) + } + + h2 { (L10n::l("welcome_page").markup()) } + + div class="box-container" { + section class="box" style="background-color: #5eb0e5;" { + h3 { + (L10n::l("welcome_subtitle") + .with_arg("app", &global::SETTINGS.app.name) + .markup()) + } + p { (L10n::l("welcome_text1").markup()) } + p { (L10n::l("welcome_text2").markup()) } + } + section class="box" style="background-color: #aee1cd;" { + h3 { + (L10n::l("welcome_pagetop_title").markup()) + } + p { (L10n::l("welcome_pagetop_text1").markup()) } + p { (L10n::l("welcome_pagetop_text2").markup()) } + p { (L10n::l("welcome_pagetop_text3").markup()) } + } + section class="box" style="background-color: #ebebe3;" { + h3 { + (L10n::l("welcome_issues_title").markup()) + } + p { (L10n::l("welcome_issues_text1").markup()) } + p { + (L10n::l("welcome_issues_text2") + .with_arg("app", &global::SETTINGS.app.name) + .markup()) + } + } + } + + footer { "[ " (L10n::l("welcome_have_fun").markup()) " ]" } + } + } + })) + .render() +} diff --git a/pagetop/src/base/theme.rs b/pagetop/src/base/theme.rs new file mode 100644 index 00000000..c07ffc56 --- /dev/null +++ b/pagetop/src/base/theme.rs @@ -0,0 +1,11 @@ +use crate::prelude::*; + +pub struct Basic; + +impl PackageTrait for Basic { + fn theme(&self) -> Option { + Some(&Basic) + } +} + +impl ThemeTrait for Basic {} diff --git a/pagetop/src/config.rs b/pagetop/src/config.rs new file mode 100644 index 00000000..07924057 --- /dev/null +++ b/pagetop/src/config.rs @@ -0,0 +1,196 @@ +//! Load configuration settings. +//! +//! These settings are loaded from [TOML](https://toml.io) files as `key = value` pairs and mapped +//! into type-safe structures with predefined values. +//! +//! Following the [Twelve-Factor App](https://12factor.net/config) methodology, `PageTop` separates +//! code from configuration. This approach allows configurations to vary across deployments, such as +//! development, staging, or production, without changing the codebase. +//! +//! +//! # Loading configuration settings +//! +//! If your application requires configuration files, create a `config` directory in the root of +//! your project, at the same level as the *Cargo.toml* file or the application's binary. +//! +//! `PageTop` automatically loads configuration settings by reading the following TOML files in +//! order (all files are optional): +//! +//! 1. **config/common.toml**, for settings shared across all environments. This approach simplifies +//! maintenance by centralizing common configuration values. +//! +//! 2. **config/{rm}.toml**, where `{rm}` corresponds to the environment variable +//! `PAGETOP_RUN_MODE`: +//! +//! * If `PAGETOP_RUN_MODE` is not set, it defaults to `default`, and `PageTop` attempts to load +//! *config/default.toml* if available. +//! +//! * Useful for environment-specific configurations, ensuring that each environment +//! (e.g., development, staging, production) has its own settings without affecting others, +//! such as API keys, URLs, or performance-related adjustments. +//! +//! 3. **config/local.{rm}.toml**, useful for local machine-specific configurations: +//! +//! * This file allows you to add or override settings specific to the environment. For example, +//! `local.devel.toml` for development or `local.production.toml` for production tweaks. +//! +//! * It enables developers to tailor settings for their machines within a given environment and +//! is typically not shared or committed to version control systems. +//! +//! 4. **config/local.toml**, for general local settings across all environments, ideal for quick +//! adjustments or temporary values not tied to any specific environment. +//! +//! The configuration settings are merged in the order listed above, with later files overriding +//! earlier ones if there are conflicts. +//! +//! +//! # Adding configuration settings +//! +//! To give your **module** its own configuration settings, add [*serde*](https://docs.rs/serde) as +//! a dependency in your *Cargo.toml* file with the `derive` feature enabled: +//! +//! ```toml +//! [dependencies] +//! serde = { version = "1.0", features = ["derive"] } +//! ``` +//! +//! Then, use the [`include_config!`](crate::include_config) macro to initialize your settings with +//! type-safe structures and predefined values: +//! +//! ``` +//! use pagetop::prelude::*; +//! use serde::Deserialize; +//! +//! include_config!(SETTINGS: Settings => [ +//! // [myapp] +//! "myapp.name" => "Value Name", +//! "myapp.width" => 900, +//! "myapp.height" => 320, +//! ]); +//! +//! #[derive(Debug, Deserialize)] +//! pub struct Settings { +//! pub myapp: MyApp, +//! } +//! +//! #[derive(Debug, Deserialize)] +//! pub struct MyApp { +//! pub name: String, +//! pub description: Option, +//! pub width: u16, +//! pub height: u16, +//! } +//! ``` +//! +//! This is how global configuration settings are declared (see [`SETTINGS`](crate::global::SETTINGS)). +//! +//! You can add a new `[myapp]` section in the configuration files using the +//! [TOML syntax](https://toml.io/en/v1.0.0#table), just like the `[log]` or `[server]` sections in +//! the global settings (see [`Settings`](crate::global::Settings)). +//! +//! It is recommended to initialize all settings with predefined values or use `Option` for +//! optional settings handled within the code. +//! +//! If configuration settings fail to initialize correctly, the application will panic and stop +//! execution. +//! +//! Configuration settings are always read-only. +//! +//! +//! # Using your new configuration settings +//! +//! Access the settings directly in your code: +//! +//! ``` +//! use pagetop::prelude::*; +//! use crate::config; +//! +//! fn global_settings() { +//! println!("App name: {}", &global::SETTINGS.app.name); +//! println!("App description: {}", &global::SETTINGS.app.description); +//! println!("Value of PAGETOP_RUN_MODE: {}", &global::SETTINGS.app.run_mode); +//! } +//! +//! fn package_settings() { +//! println!("{} - {:?}", &config::SETTINGS.myapp.name, &config::SETTINGS.myapp.description); +//! println!("{}", &config::SETTINGS.myapp.width); +//! } +//! ``` + +mod data; +mod de; +mod error; +mod file; +mod path; +mod source; +mod value; + +use crate::concat_string; +use crate::config::data::ConfigData; +use crate::config::file::File; + +use std::sync::LazyLock; + +use std::env; +use std::path::Path; + +/// Original values read from configuration files in `key = value` pairs. +pub static CONFIG_VALUES: LazyLock = LazyLock::new(|| { + // Identify the configuration directory. + let config_dir = env::var("CARGO_MANIFEST_DIR") + .map(|manifest_dir| { + let manifest_config = Path::new(&manifest_dir).join("config"); + if manifest_config.exists() { + manifest_config.to_string_lossy().to_string() + } else { + "config".to_string() + } + }) + .unwrap_or_else(|_| "config".to_string()); + + // Execution mode based on the environment variable PAGETOP_RUN_MODE, defaults to 'default'. + let rm = env::var("PAGETOP_RUN_MODE").unwrap_or_else(|_| "default".into()); + + // Initialize config values. + let mut values = ConfigData::default(); + + // Merge (optional) configuration files and set the execution mode. + values + // First, add the common configuration for all environments. Defaults to 'common.toml'. + .merge(File::with_name(&concat_string!(config_dir, "/common.toml")).required(false)) + .expect("Failed to merge common configuration (common.toml)") + // Add the environment-specific configuration. Defaults to 'default.toml'. + .merge(File::with_name(&concat_string!(config_dir, "/", rm, ".toml")).required(false)) + .expect(&format!("Failed to merge {rm}.toml configuration")) + // Add reserved local configuration for the environment. Defaults to 'local.default.toml'. + .merge(File::with_name(&concat_string!(config_dir, "/local.", rm, ".toml")).required(false)) + .expect("Failed to merge reserved local environment configuration") + // Add common reserved local configuration. Defaults to 'local.toml'. + .merge(File::with_name(&concat_string!(config_dir, "/local.toml")).required(false)) + .expect("Failed to merge general reserved local configuration") + // Save the execution mode. + .set("app.run_mode", rm) + .expect("Failed to set application run mode"); + + values +}); + +#[macro_export] +macro_rules! include_config { + ( $SETTINGS:ident: $Settings:ty => [ $($key:literal => $value:literal),* $(,)? ] ) => { + #[doc = concat!( + "Assigned or predefined values for configuration settings associated to the ", + "[`", stringify!($Settings), "`] type." + )] + pub static $SETTINGS: std::sync::LazyLock<$Settings> = std::sync::LazyLock::new(|| { + let mut settings = $crate::config::CONFIG_VALUES.clone(); + $( + settings.set_default($key, $value).unwrap(); + )* + match settings.try_into() { + Ok(s) => s, + Err(e) => panic!("Error parsing settings: {}", e), + } + }); + }; +} diff --git a/pagetop/src/config/data.rs b/pagetop/src/config/data.rs new file mode 100644 index 00000000..22fe8359 --- /dev/null +++ b/pagetop/src/config/data.rs @@ -0,0 +1,136 @@ +use crate::config::error::*; +use crate::config::path; +use crate::config::source::Source; +use crate::config::value::Value; + +use serde::de::Deserialize; + +use std::collections::HashMap; +use std::fmt::Debug; + +#[derive(Clone, Debug)] +enum ConfigKind { + // A mutable configuration. This is the default. + Mutable { + defaults: HashMap, + overrides: HashMap, + sources: Vec>, + }, +} + +impl Default for ConfigKind { + fn default() -> Self { + ConfigKind::Mutable { + defaults: HashMap::new(), + overrides: HashMap::new(), + sources: Vec::new(), + } + } +} + +/// A prioritized configuration repository. It maintains a set of configuration sources, fetches +/// values to populate those, and provides them according to the source's priority. +#[derive(Default, Clone, Debug)] +pub struct ConfigData { + kind: ConfigKind, + /// Root of the cached configuration. + pub cache: Value, +} + +impl ConfigData { + /// Merge in a configuration property source. + pub fn merge(&mut self, source: T) -> Result<&mut ConfigData> + where + T: 'static, + T: Source + Send + Sync, + { + match self.kind { + ConfigKind::Mutable { + ref mut sources, .. + } => { + sources.push(Box::new(source)); + } + } + + self.refresh() + } + + /// Refresh the configuration cache with fresh data from added sources. + /// + /// Configuration is automatically refreshed after a mutation operation (`set`, `merge`, + /// `set_default`, etc.). + pub fn refresh(&mut self) -> Result<&mut ConfigData> { + self.cache = match self.kind { + // TODO: We need to actually merge in all the stuff. + ConfigKind::Mutable { + ref overrides, + ref sources, + ref defaults, + } => { + let mut cache: Value = HashMap::::new().into(); + + // Add defaults. + for (key, val) in defaults { + key.set(&mut cache, val.clone()); + } + + // Add sources. + sources.collect_to(&mut cache)?; + + // Add overrides. + for (key, val) in overrides { + key.set(&mut cache, val.clone()); + } + + cache + } + }; + + Ok(self) + } + + pub fn set_default(&mut self, key: &str, value: T) -> Result<&mut ConfigData> + where + T: Into, + { + match self.kind { + ConfigKind::Mutable { + ref mut defaults, .. + } => { + defaults.insert(key.parse()?, value.into()); + } + }; + + self.refresh() + } + + pub fn set(&mut self, key: &str, value: T) -> Result<&mut ConfigData> + where + T: Into, + { + match self.kind { + ConfigKind::Mutable { + ref mut overrides, .. + } => { + overrides.insert(key.parse()?, value.into()); + } + }; + + self.refresh() + } + + /// Attempt to deserialize the entire configuration into the requested type. + pub fn try_into<'de, T: Deserialize<'de>>(self) -> Result { + T::deserialize(self) + } +} + +impl Source for ConfigData { + fn clone_into_box(&self) -> Box { + Box::new((*self).clone()) + } + + fn collect(&self) -> Result> { + self.cache.clone().into_table() + } +} diff --git a/pagetop/src/config/de.rs b/pagetop/src/config/de.rs new file mode 100644 index 00000000..875219af --- /dev/null +++ b/pagetop/src/config/de.rs @@ -0,0 +1,462 @@ +use crate::config::data::ConfigData; +use crate::config::error::*; +use crate::config::value::{Table, Value, ValueKind}; + +use serde::de; +use serde::forward_to_deserialize_any; + +use std::collections::{HashMap, VecDeque}; +use std::iter::Enumerate; + +impl<'de> de::Deserializer<'de> for Value { + type Error = ConfigError; + + #[inline] + fn deserialize_any(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + // Deserialize based on the underlying type. + match self.kind { + ValueKind::Nil => visitor.visit_unit(), + ValueKind::Integer(i) => visitor.visit_i64(i), + ValueKind::Boolean(b) => visitor.visit_bool(b), + ValueKind::Float(f) => visitor.visit_f64(f), + ValueKind::String(s) => visitor.visit_string(s), + ValueKind::Array(values) => visitor.visit_seq(SeqAccess::new(values)), + ValueKind::Table(map) => visitor.visit_map(MapAccess::new(map)), + } + } + + #[inline] + fn deserialize_bool>(self, visitor: V) -> Result { + visitor.visit_bool(self.into_bool()?) + } + + #[inline] + fn deserialize_i8>(self, visitor: V) -> Result { + // FIXME: This should *fail* if the value does not fit in the requets integer type. + visitor.visit_i8(self.into_int()? as i8) + } + + #[inline] + fn deserialize_i16>(self, visitor: V) -> Result { + // FIXME: This should *fail* if the value does not fit in the requets integer type. + visitor.visit_i16(self.into_int()? as i16) + } + + #[inline] + fn deserialize_i32>(self, visitor: V) -> Result { + // FIXME: This should *fail* if the value does not fit in the requets integer type. + visitor.visit_i32(self.into_int()? as i32) + } + + #[inline] + fn deserialize_i64>(self, visitor: V) -> Result { + visitor.visit_i64(self.into_int()?) + } + + #[inline] + fn deserialize_u8>(self, visitor: V) -> Result { + // FIXME: This should *fail* if the value does not fit in the requets integer type. + visitor.visit_u8(self.into_int()? as u8) + } + + #[inline] + fn deserialize_u16>(self, visitor: V) -> Result { + // FIXME: This should *fail* if the value does not fit in the requets integer type. + visitor.visit_u16(self.into_int()? as u16) + } + + #[inline] + fn deserialize_u32>(self, visitor: V) -> Result { + // FIXME: This should *fail* if the value does not fit in the requets integer type. + visitor.visit_u32(self.into_int()? as u32) + } + + #[inline] + fn deserialize_u64>(self, visitor: V) -> Result { + // FIXME: This should *fail* if the value does not fit in the requets integer type. + visitor.visit_u64(self.into_int()? as u64) + } + + #[inline] + fn deserialize_f32>(self, visitor: V) -> Result { + visitor.visit_f32(self.into_float()? as f32) + } + + #[inline] + fn deserialize_f64>(self, visitor: V) -> Result { + visitor.visit_f64(self.into_float()?) + } + + #[inline] + fn deserialize_str>(self, visitor: V) -> Result { + visitor.visit_string(self.into_str()?) + } + + #[inline] + fn deserialize_string>(self, visitor: V) -> Result { + visitor.visit_string(self.into_str()?) + } + + #[inline] + fn deserialize_option(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + // Match an explicit nil as None and everything else as Some. + match self.kind { + ValueKind::Nil => visitor.visit_none(), + _ => visitor.visit_some(self), + } + } + + fn deserialize_newtype_struct(self, _name: &'static str, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + visitor.visit_newtype_struct(self) + } + + fn deserialize_enum( + self, + name: &'static str, + variants: &'static [&'static str], + visitor: V, + ) -> Result + where + V: de::Visitor<'de>, + { + visitor.visit_enum(EnumAccess { + value: self, + name, + variants, + }) + } + + forward_to_deserialize_any! { + char seq + bytes byte_buf map struct unit + identifier ignored_any unit_struct tuple_struct tuple + } +} + +struct StrDeserializer<'a>(&'a str); + +impl<'de, 'a> de::Deserializer<'de> for StrDeserializer<'a> { + type Error = ConfigError; + + #[inline] + fn deserialize_any>(self, visitor: V) -> Result { + visitor.visit_str(self.0) + } + + forward_to_deserialize_any! { + bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string seq + bytes byte_buf map struct unit enum newtype_struct + identifier ignored_any unit_struct tuple_struct tuple option + } +} + +struct SeqAccess { + elements: Enumerate<::std::vec::IntoIter>, +} + +impl SeqAccess { + fn new(elements: Vec) -> Self { + SeqAccess { + elements: elements.into_iter().enumerate(), + } + } +} + +impl<'de> de::SeqAccess<'de> for SeqAccess { + type Error = ConfigError; + + fn next_element_seed(&mut self, seed: T) -> Result> + where + T: de::DeserializeSeed<'de>, + { + match self.elements.next() { + Some((idx, value)) => seed + .deserialize(value) + .map(Some) + .map_err(|e| e.prepend_index(idx)), + None => Ok(None), + } + } + + fn size_hint(&self) -> Option { + match self.elements.size_hint() { + (lower, Some(upper)) if lower == upper => Some(upper), + _ => None, + } + } +} + +struct MapAccess { + elements: VecDeque<(String, Value)>, +} + +impl MapAccess { + fn new(table: HashMap) -> Self { + MapAccess { + elements: table.into_iter().collect(), + } + } +} + +impl<'de> de::MapAccess<'de> for MapAccess { + type Error = ConfigError; + + fn next_key_seed(&mut self, seed: K) -> Result> + where + K: de::DeserializeSeed<'de>, + { + if let Some((key_s, _)) = self.elements.front() { + let key_de = Value::new(None, key_s as &str); + let key = de::DeserializeSeed::deserialize(seed, key_de)?; + + Ok(Some(key)) + } else { + Ok(None) + } + } + + fn next_value_seed(&mut self, seed: V) -> Result + where + V: de::DeserializeSeed<'de>, + { + let (key, value) = self.elements.pop_front().unwrap(); + de::DeserializeSeed::deserialize(seed, value).map_err(|e| e.prepend_key(key)) + } +} + +struct EnumAccess { + value: Value, + name: &'static str, + variants: &'static [&'static str], +} + +impl EnumAccess { + fn variant_deserializer(&self, name: &str) -> Result { + self.variants + .iter() + .find(|&&s| s == name) + .map(|&s| StrDeserializer(s)) + .ok_or_else(|| self.no_constructor_error(name)) + } + + fn table_deserializer(&self, table: &Table) -> Result { + if table.len() == 1 { + self.variant_deserializer(table.iter().next().unwrap().0) + } else { + Err(self.structural_error()) + } + } + + fn no_constructor_error(&self, supposed_variant: &str) -> ConfigError { + ConfigError::Message(format!( + "enum {} does not have variant constructor {}", + self.name, supposed_variant + )) + } + + fn structural_error(&self) -> ConfigError { + ConfigError::Message(format!( + "value of enum {} should be represented by either string or table with exactly one key", + self.name + )) + } +} + +impl<'de> de::EnumAccess<'de> for EnumAccess { + type Error = ConfigError; + type Variant = Self; + + fn variant_seed(self, seed: V) -> Result<(V::Value, Self::Variant)> + where + V: de::DeserializeSeed<'de>, + { + let value = { + let deserializer = match self.value.kind { + ValueKind::String(ref s) => self.variant_deserializer(s), + ValueKind::Table(ref t) => self.table_deserializer(t), + _ => Err(self.structural_error()), + }?; + seed.deserialize(deserializer)? + }; + + Ok((value, self)) + } +} + +impl<'de> de::VariantAccess<'de> for EnumAccess { + type Error = ConfigError; + + fn unit_variant(self) -> Result<()> { + Ok(()) + } + + fn newtype_variant_seed(self, seed: T) -> Result + where + T: de::DeserializeSeed<'de>, + { + match self.value.kind { + ValueKind::Table(t) => seed.deserialize(t.into_iter().next().unwrap().1), + _ => unreachable!(), + } + } + + fn tuple_variant(self, _len: usize, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + match self.value.kind { + ValueKind::Table(t) => { + de::Deserializer::deserialize_seq(t.into_iter().next().unwrap().1, visitor) + } + _ => unreachable!(), + } + } + + fn struct_variant(self, _fields: &'static [&'static str], visitor: V) -> Result + where + V: de::Visitor<'de>, + { + match self.value.kind { + ValueKind::Table(t) => { + de::Deserializer::deserialize_map(t.into_iter().next().unwrap().1, visitor) + } + _ => unreachable!(), + } + } +} + +impl<'de> de::Deserializer<'de> for ConfigData { + type Error = ConfigError; + + #[inline] + fn deserialize_any(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + // Deserialize based on the underlying type. + match self.cache.kind { + ValueKind::Nil => visitor.visit_unit(), + ValueKind::Integer(i) => visitor.visit_i64(i), + ValueKind::Boolean(b) => visitor.visit_bool(b), + ValueKind::Float(f) => visitor.visit_f64(f), + ValueKind::String(s) => visitor.visit_string(s), + ValueKind::Array(values) => visitor.visit_seq(SeqAccess::new(values)), + ValueKind::Table(map) => visitor.visit_map(MapAccess::new(map)), + } + } + + #[inline] + fn deserialize_bool>(self, visitor: V) -> Result { + visitor.visit_bool(self.cache.into_bool()?) + } + + #[inline] + fn deserialize_i8>(self, visitor: V) -> Result { + // FIXME: This should *fail* if the value does not fit in the requets integer type. + visitor.visit_i8(self.cache.into_int()? as i8) + } + + #[inline] + fn deserialize_i16>(self, visitor: V) -> Result { + // FIXME: This should *fail* if the value does not fit in the requets integer type. + visitor.visit_i16(self.cache.into_int()? as i16) + } + + #[inline] + fn deserialize_i32>(self, visitor: V) -> Result { + // FIXME: This should *fail* if the value does not fit in the requets integer type. + visitor.visit_i32(self.cache.into_int()? as i32) + } + + #[inline] + fn deserialize_i64>(self, visitor: V) -> Result { + visitor.visit_i64(self.cache.into_int()?) + } + + #[inline] + fn deserialize_u8>(self, visitor: V) -> Result { + // FIXME: This should *fail* if the value does not fit in the requets integer type. + visitor.visit_u8(self.cache.into_int()? as u8) + } + + #[inline] + fn deserialize_u16>(self, visitor: V) -> Result { + // FIXME: This should *fail* if the value does not fit in the requets integer type. + visitor.visit_u16(self.cache.into_int()? as u16) + } + + #[inline] + fn deserialize_u32>(self, visitor: V) -> Result { + // FIXME: This should *fail* if the value does not fit in the requets integer type. + visitor.visit_u32(self.cache.into_int()? as u32) + } + + #[inline] + fn deserialize_u64>(self, visitor: V) -> Result { + // FIXME: This should *fail* if the value does not fit in the requets integer type. + visitor.visit_u64(self.cache.into_int()? as u64) + } + + #[inline] + fn deserialize_f32>(self, visitor: V) -> Result { + visitor.visit_f32(self.cache.into_float()? as f32) + } + + #[inline] + fn deserialize_f64>(self, visitor: V) -> Result { + visitor.visit_f64(self.cache.into_float()?) + } + + #[inline] + fn deserialize_str>(self, visitor: V) -> Result { + visitor.visit_string(self.cache.into_str()?) + } + + #[inline] + fn deserialize_string>(self, visitor: V) -> Result { + visitor.visit_string(self.cache.into_str()?) + } + + #[inline] + fn deserialize_option(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + // Match an explicit nil as None and everything else as Some. + match self.cache.kind { + ValueKind::Nil => visitor.visit_none(), + _ => visitor.visit_some(self), + } + } + + fn deserialize_enum( + self, + name: &'static str, + variants: &'static [&'static str], + visitor: V, + ) -> Result + where + V: de::Visitor<'de>, + { + visitor.visit_enum(EnumAccess { + value: self.cache, + name, + variants, + }) + } + + forward_to_deserialize_any! { + char seq + bytes byte_buf map struct unit newtype_struct + identifier ignored_any unit_struct tuple_struct tuple + } +} diff --git a/pagetop/src/config/error.rs b/pagetop/src/config/error.rs new file mode 100644 index 00000000..be3649c0 --- /dev/null +++ b/pagetop/src/config/error.rs @@ -0,0 +1,222 @@ +use nom; +use serde::de; +use serde::ser; + +use std::error::Error; +use std::fmt; +use std::result; + +#[derive(Debug)] +pub enum Unexpected { + Bool(bool), + Integer(i64), + Float(f64), + Str(String), + Unit, + Seq, + Map, +} + +impl fmt::Display for Unexpected { + fn fmt(&self, f: &mut fmt::Formatter) -> result::Result<(), fmt::Error> { + match *self { + Unexpected::Bool(b) => write!(f, "boolean `{}`", b), + Unexpected::Integer(i) => write!(f, "integer `{}`", i), + Unexpected::Float(v) => write!(f, "floating point `{}`", v), + Unexpected::Str(ref s) => write!(f, "string {:?}", s), + Unexpected::Unit => write!(f, "unit value"), + Unexpected::Seq => write!(f, "sequence"), + Unexpected::Map => write!(f, "map"), + } + } +} + +/// Represents all possible errors that can occur when working with configuration. +pub enum ConfigError { + /// Configuration is frozen and no further mutations can be made. + Frozen, + + /// Configuration property was not found. + NotFound(String), + + /// Configuration path could not be parsed. + PathParse(nom::error::ErrorKind), + + /// Configuration could not be parsed from file. + FileParse { + /// The URI used to access the file (if not loaded from a string). + /// Example: `/path/to/config.json` + uri: Option, + + /// The captured error from attempting to parse the file in its desired format. + /// This is the actual error object from the library used for the parsing. + cause: Box, + }, + + /// Value could not be converted into the requested type. + Type { + /// The URI that references the source that the value came from. + /// Example: `/path/to/config.json` or `Environment` or `etcd://localhost` + // TODO: Why is this called Origin but FileParse has a uri field? + origin: Option, + + /// What we found when parsing the value. + unexpected: Unexpected, + + /// What was expected when parsing the value. + expected: &'static str, + + /// The key in the configuration hash of this value (if available where the error is + /// generated). + key: Option, + }, + + /// Custom message. + Message(String), + + /// Unadorned error from a foreign origin. + Foreign(Box), +} + +impl ConfigError { + // FIXME: pub(crate). + #[doc(hidden)] + pub fn invalid_type( + origin: Option, + unexpected: Unexpected, + expected: &'static str, + ) -> Self { + ConfigError::Type { + origin, + unexpected, + expected, + key: None, + } + } + + // FIXME: pub(crate). + #[doc(hidden)] + pub fn extend_with_key(self, key: &str) -> Self { + match self { + ConfigError::Type { + origin, + unexpected, + expected, + .. + } => ConfigError::Type { + origin, + unexpected, + expected, + key: Some(key.into()), + }, + + _ => self, + } + } + + fn prepend(self, segment: String, add_dot: bool) -> Self { + let concat = |key: Option| { + let key = key.unwrap_or_default(); + let dot = if add_dot && key.as_bytes().first().unwrap_or(&b'[') != &b'[' { + "." + } else { + "" + }; + format!("{}{}{}", segment, dot, key) + }; + match self { + ConfigError::Type { + origin, + unexpected, + expected, + key, + } => ConfigError::Type { + origin, + unexpected, + expected, + key: Some(concat(key)), + }, + ConfigError::NotFound(key) => ConfigError::NotFound(concat(Some(key))), + _ => self, + } + } + + pub(crate) fn prepend_key(self, key: String) -> Self { + self.prepend(key, true) + } + + pub(crate) fn prepend_index(self, idx: usize) -> Self { + self.prepend(format!("[{}]", idx), false) + } +} + +/// Alias for a `Result` with the error type set to `ConfigError`. +pub type Result = result::Result; + +// Forward Debug to Display for readable panic! messages. +impl fmt::Debug for ConfigError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", *self) + } +} + +impl fmt::Display for ConfigError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + ConfigError::Frozen => write!(f, "configuration is frozen"), + + ConfigError::PathParse(ref kind) => write!(f, "{}", kind.description()), + + ConfigError::Message(ref s) => write!(f, "{}", s), + + ConfigError::Foreign(ref cause) => write!(f, "{}", cause), + + ConfigError::NotFound(ref key) => { + write!(f, "configuration property {:?} not found", key) + } + + ConfigError::Type { + ref origin, + ref unexpected, + expected, + ref key, + } => { + write!(f, "invalid type: {}, expected {}", unexpected, expected)?; + + if let Some(ref key) = *key { + write!(f, " for key `{}`", key)?; + } + + if let Some(ref origin) = *origin { + write!(f, " in {}", origin)?; + } + + Ok(()) + } + + ConfigError::FileParse { ref cause, ref uri } => { + write!(f, "{}", cause)?; + + if let Some(ref uri) = *uri { + write!(f, " in {}", uri)?; + } + + Ok(()) + } + } + } +} + +impl Error for ConfigError {} + +impl de::Error for ConfigError { + fn custom(msg: T) -> Self { + ConfigError::Message(msg.to_string()) + } +} + +impl ser::Error for ConfigError { + fn custom(msg: T) -> Self { + ConfigError::Message(msg.to_string()) + } +} diff --git a/pagetop/src/config/file.rs b/pagetop/src/config/file.rs new file mode 100644 index 00000000..00f0c34d --- /dev/null +++ b/pagetop/src/config/file.rs @@ -0,0 +1,85 @@ +mod source; +mod toml; + +use crate::config::error::*; +use crate::config::source::Source; +use crate::config::value::Value; + +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +use self::source::FileSource; + +#[derive(Clone, Debug)] +pub struct File +where + T: FileSource, +{ + source: T, + /// A required File will error if it cannot be found. + required: bool, +} + +impl File { + /// Given the basename of a file, will attempt to locate a file by setting its extension to a + /// registered format. + pub fn with_name(name: &str) -> Self { + File { + source: source::FileSourceFile::new(name.into()), + required: true, + } + } +} + +impl<'a> From<&'a Path> for File { + fn from(path: &'a Path) -> Self { + File { + source: source::FileSourceFile::new(path.to_path_buf()), + required: true, + } + } +} + +impl From for File { + fn from(path: PathBuf) -> Self { + File { + source: source::FileSourceFile::new(path), + required: true, + } + } +} + +impl File { + pub fn required(mut self, required: bool) -> Self { + self.required = required; + self + } +} + +impl Source for File +where + T: 'static, + T: Sync + Send, +{ + fn clone_into_box(&self) -> Box { + Box::new((*self).clone()) + } + + fn collect(&self) -> Result> { + // Coerce the file contents to a string. + let (uri, contents) = match self.source.resolve().map_err(ConfigError::Foreign) { + Ok((uri, contents)) => (uri, contents), + + Err(error) => { + if !self.required { + return Ok(HashMap::new()); + } + + return Err(error); + } + }; + + // Parse the string using the given format. + toml::parse(uri.as_ref(), &contents).map_err(|cause| ConfigError::FileParse { uri, cause }) + } +} diff --git a/pagetop/src/config/file/source.rs b/pagetop/src/config/file/source.rs new file mode 100644 index 00000000..46d9ff00 --- /dev/null +++ b/pagetop/src/config/file/source.rs @@ -0,0 +1,126 @@ +use std::env; +use std::error::Error; +use std::fmt::Debug; +use std::fs; +use std::io::{self, Read}; +use std::iter::Iterator; +use std::path::{Path, PathBuf}; + +/// Describes where the file is sourced. +pub trait FileSource: Debug + Clone { + fn resolve(&self) -> Result<(Option, String), Box>; +} + +/// Describes a file sourced from a file. +#[derive(Clone, Debug)] +pub struct FileSourceFile { + /// Path of configuration file. + name: PathBuf, +} + +impl FileSourceFile { + pub fn new(name: PathBuf) -> FileSourceFile { + FileSourceFile { name } + } + + fn find_file(&self) -> Result> { + // First check for an _exact_ match. + let mut filename = env::current_dir()?.as_path().join(self.name.clone()); + if filename.is_file() { + if ["toml"].contains( + &filename + .extension() + .unwrap_or_default() + .to_string_lossy() + .as_ref(), + ) { + return Ok(filename); + } + + Err(Box::new(io::Error::new( + io::ErrorKind::NotFound, + format!( + "configuration file \"{}\" is not of a registered file format", + filename.to_string_lossy() + ), + ))) + } else { + filename.set_extension("toml"); + + if filename.is_file() { + return Ok(filename); + } + + Err(Box::new(io::Error::new( + io::ErrorKind::NotFound, + format!( + "configuration file \"{}\" not found", + self.name.to_string_lossy() + ), + ))) + } + } +} + +impl FileSource for FileSourceFile { + fn resolve(&self) -> Result<(Option, String), Box> { + // Find file. + let filename = self.find_file()?; + + // Attempt to use a relative path for the URI. + let base = env::current_dir()?; + let uri = match path_relative_from(&filename, &base) { + Some(value) => value, + None => filename.clone(), + }; + + // Read contents from file. + let mut file = fs::File::open(filename)?; + let mut text = String::new(); + file.read_to_string(&mut text)?; + + Ok((Some(uri.to_string_lossy().into_owned()), text)) + } +} + +// TODO: This should probably be a crate. +// https://github.com/rust-lang/rust/blob/master/src/librustc_trans/back/rpath.rs#L128 +fn path_relative_from(path: &Path, base: &Path) -> Option { + use std::path::Component; + + if path.is_absolute() != base.is_absolute() { + if path.is_absolute() { + Some(PathBuf::from(path)) + } else { + None + } + } else { + let mut ita = path.components(); + let mut itb = base.components(); + let mut comps: Vec = vec![]; + loop { + match (ita.next(), itb.next()) { + (None, None) => break, + (Some(a), None) => { + comps.push(a); + comps.extend(ita.by_ref()); + break; + } + (None, _) => comps.push(Component::ParentDir), + (Some(a), Some(b)) if comps.is_empty() && a == b => (), + (Some(a), Some(Component::CurDir)) => comps.push(a), + (Some(_), Some(Component::ParentDir)) => return None, + (Some(a), Some(_)) => { + comps.push(Component::ParentDir); + for _ in itb { + comps.push(Component::ParentDir); + } + comps.push(a); + comps.extend(ita.by_ref()); + break; + } + } + } + Some(comps.iter().map(|c| c.as_os_str()).collect()) + } +} diff --git a/pagetop/src/config/file/toml.rs b/pagetop/src/config/file/toml.rs new file mode 100644 index 00000000..e8fa06c6 --- /dev/null +++ b/pagetop/src/config/file/toml.rs @@ -0,0 +1,51 @@ +use crate::config::value::{Value, ValueKind}; + +use toml; + +use std::collections::HashMap; +use std::error::Error; + +pub fn parse( + uri: Option<&String>, + text: &str, +) -> Result, Box> { + // Parse a TOML value from the provided text. + // TODO: Have a proper error fire if the root of a file is ever not a Table + let value = from_toml_value(uri, &toml::from_str(text)?); + match value.kind { + ValueKind::Table(map) => Ok(map), + + _ => Ok(HashMap::new()), + } +} + +fn from_toml_value(uri: Option<&String>, value: &toml::Value) -> Value { + match *value { + toml::Value::String(ref value) => Value::new(uri, value.to_string()), + toml::Value::Float(value) => Value::new(uri, value), + toml::Value::Integer(value) => Value::new(uri, value), + toml::Value::Boolean(value) => Value::new(uri, value), + + toml::Value::Table(ref table) => { + let mut m = HashMap::new(); + + for (key, value) in table { + m.insert(key.clone(), from_toml_value(uri, value)); + } + + Value::new(uri, m) + } + + toml::Value::Array(ref array) => { + let mut l = Vec::new(); + + for value in array { + l.push(from_toml_value(uri, value)); + } + + Value::new(uri, l) + } + + toml::Value::Datetime(ref datetime) => Value::new(uri, datetime.to_string()), + } +} diff --git a/pagetop/src/config/path.rs b/pagetop/src/config/path.rs new file mode 100644 index 00000000..72376a95 --- /dev/null +++ b/pagetop/src/config/path.rs @@ -0,0 +1,167 @@ +use crate::config::error::*; +use crate::config::value::{Value, ValueKind}; + +use std::collections::HashMap; +use std::str::FromStr; + +mod parser; + +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +pub enum Expression { + Identifier(String), + Child(Box, String), + Subscript(Box, isize), +} + +impl FromStr for Expression { + type Err = ConfigError; + + fn from_str(s: &str) -> Result { + parser::from_str(s).map_err(ConfigError::PathParse) + } +} + +fn sindex_to_uindex(index: isize, len: usize) -> usize { + if index >= 0 { + index as usize + } else { + len - (index.unsigned_abs()) + } +} + +impl Expression { + pub fn get_mut_forcibly<'a>(&self, root: &'a mut Value) -> Option<&'a mut Value> { + match *self { + Expression::Identifier(ref id) => match root.kind { + ValueKind::Table(ref mut map) => Some( + map.entry(id.clone()) + .or_insert_with(|| Value::new(None, ValueKind::Nil)), + ), + + _ => None, + }, + + Expression::Child(ref expr, ref key) => match expr.get_mut_forcibly(root) { + Some(value) => match value.kind { + ValueKind::Table(ref mut map) => Some( + map.entry(key.clone()) + .or_insert_with(|| Value::new(None, ValueKind::Nil)), + ), + + _ => { + *value = HashMap::::new().into(); + + if let ValueKind::Table(ref mut map) = value.kind { + Some( + map.entry(key.clone()) + .or_insert_with(|| Value::new(None, ValueKind::Nil)), + ) + } else { + unreachable!(); + } + } + }, + + _ => None, + }, + + Expression::Subscript(ref expr, index) => match expr.get_mut_forcibly(root) { + Some(value) => { + match value.kind { + ValueKind::Array(_) => (), + _ => *value = Vec::::new().into(), + } + + match value.kind { + ValueKind::Array(ref mut array) => { + let index = sindex_to_uindex(index, array.len()); + + if index >= array.len() { + array.resize(index + 1, Value::new(None, ValueKind::Nil)); + } + + Some(&mut array[index]) + } + + _ => None, + } + } + _ => None, + }, + } + } + + pub fn set(&self, root: &mut Value, value: Value) { + match *self { + Expression::Identifier(ref id) => { + // Ensure that root is a table. + match root.kind { + ValueKind::Table(_) => {} + + _ => { + *root = HashMap::::new().into(); + } + } + + match value.kind { + ValueKind::Table(ref incoming_map) => { + // Pull out another table. + let target = if let ValueKind::Table(ref mut map) = root.kind { + map.entry(id.clone()) + .or_insert_with(|| HashMap::::new().into()) + } else { + unreachable!(); + }; + + // Continue the deep merge. + for (key, val) in incoming_map { + Expression::Identifier(key.clone()).set(target, val.clone()); + } + } + + _ => { + if let ValueKind::Table(ref mut map) = root.kind { + // Just do a simple set. + map.insert(id.clone(), value); + } + } + } + } + + Expression::Child(ref expr, ref key) => { + if let Some(parent) = expr.get_mut_forcibly(root) { + match parent.kind { + ValueKind::Table(_) => { + Expression::Identifier(key.clone()).set(parent, value); + } + + _ => { + // Didn't find a table. Oh well. Make a table and do this anyway. + *parent = HashMap::::new().into(); + + Expression::Identifier(key.clone()).set(parent, value); + } + } + } + } + + Expression::Subscript(ref expr, index) => { + if let Some(parent) = expr.get_mut_forcibly(root) { + match parent.kind { + ValueKind::Array(_) => (), + _ => *parent = Vec::::new().into(), + } + + if let ValueKind::Array(ref mut array) = parent.kind { + let uindex = sindex_to_uindex(index, array.len()); + if uindex >= array.len() { + array.resize(uindex + 1, Value::new(None, ValueKind::Nil)); + } + + array[uindex] = value; + } + } + } + } + } +} diff --git a/pagetop/src/config/path/parser.rs b/pagetop/src/config/path/parser.rs new file mode 100644 index 00000000..6bc95276 --- /dev/null +++ b/pagetop/src/config/path/parser.rs @@ -0,0 +1,131 @@ +use super::Expression; + +use nom::{ + branch::alt, + bytes::complete::{is_a, tag}, + character::complete::{char, digit1, space0}, + combinator::{map, map_res, opt, recognize}, + error::ErrorKind, + sequence::{delimited, pair, preceded}, + Err, IResult, +}; + +use std::str::FromStr; + +fn raw_ident(i: &str) -> IResult<&str, String> { + map( + is_a( + "abcdefghijklmnopqrstuvwxyz \ + ABCDEFGHIJKLMNOPQRSTUVWXYZ \ + 0123456789 \ + _-", + ), + |s: &str| s.to_string(), + )(i) +} + +fn integer(i: &str) -> IResult<&str, isize> { + map_res( + delimited(space0, recognize(pair(opt(tag("-")), digit1)), space0), + FromStr::from_str, + )(i) +} + +fn ident(i: &str) -> IResult<&str, Expression> { + map(raw_ident, Expression::Identifier)(i) +} + +fn postfix<'a>(expr: Expression) -> impl FnMut(&'a str) -> IResult<&'a str, Expression> { + let e2 = expr.clone(); + let child = map(preceded(tag("."), raw_ident), move |id| { + Expression::Child(Box::new(expr.clone()), id) + }); + + let subscript = map(delimited(char('['), integer, char(']')), move |num| { + Expression::Subscript(Box::new(e2.clone()), num) + }); + + alt((child, subscript)) +} + +pub fn from_str(input: &str) -> Result { + match ident(input) { + Ok((mut rem, mut expr)) => { + while !rem.is_empty() { + match postfix(expr)(rem) { + Ok((rem_, expr_)) => { + rem = rem_; + expr = expr_; + } + + // Forward Incomplete and Error + result => { + return result.map(|(_, o)| o).map_err(to_error_kind); + } + } + } + + Ok(expr) + } + + // Forward Incomplete and Error + result => result.map(|(_, o)| o).map_err(to_error_kind), + } +} + +pub fn to_error_kind(e: Err>) -> ErrorKind { + match e { + Err::Incomplete(_) => ErrorKind::Complete, + Err::Failure(e) | Err::Error(e) => e.code, + } +} + +#[cfg(test)] +mod test { + use super::Expression::*; + use super::*; + + #[test] + fn test_id() { + let parsed: Expression = from_str("abcd").unwrap(); + assert_eq!(parsed, Identifier("abcd".into())); + } + + #[test] + fn test_id_dash() { + let parsed: Expression = from_str("abcd-efgh").unwrap(); + assert_eq!(parsed, Identifier("abcd-efgh".into())); + } + + #[test] + fn test_child() { + let parsed: Expression = from_str("abcd.efgh").unwrap(); + let expected = Child(Box::new(Identifier("abcd".into())), "efgh".into()); + + assert_eq!(parsed, expected); + + let parsed: Expression = from_str("abcd.efgh.ijkl").unwrap(); + let expected = Child( + Box::new(Child(Box::new(Identifier("abcd".into())), "efgh".into())), + "ijkl".into(), + ); + + assert_eq!(parsed, expected); + } + + #[test] + fn test_subscript() { + let parsed: Expression = from_str("abcd[12]").unwrap(); + let expected = Subscript(Box::new(Identifier("abcd".into())), 12); + + assert_eq!(parsed, expected); + } + + #[test] + fn test_subscript_neg() { + let parsed: Expression = from_str("abcd[-1]").unwrap(); + let expected = Subscript(Box::new(Identifier("abcd".into())), -1); + + assert_eq!(parsed, expected); + } +} diff --git a/pagetop/src/config/source.rs b/pagetop/src/config/source.rs new file mode 100644 index 00000000..5e693b68 --- /dev/null +++ b/pagetop/src/config/source.rs @@ -0,0 +1,87 @@ +use crate::config::error::*; +use crate::config::path; +use crate::config::value::{Value, ValueKind}; + +use std::collections::HashMap; +use std::fmt::Debug; +use std::str::FromStr; + +/// Describes a generic _source_ of configuration properties. +pub trait Source: Debug { + fn clone_into_box(&self) -> Box; + + /// Collect all configuration properties available from this source and return a HashMap. + fn collect(&self) -> Result>; + + fn collect_to(&self, cache: &mut Value) -> Result<()> { + let props = match self.collect() { + Ok(props) => props, + Err(error) => { + return Err(error); + } + }; + + for (key, val) in &props { + match path::Expression::from_str(key) { + // Set using the path. + Ok(expr) => expr.set(cache, val.clone()), + + // Set diretly anyway. + _ => path::Expression::Identifier(key.clone()).set(cache, val.clone()), + } + } + + Ok(()) + } +} + +impl Clone for Box { + fn clone(&self) -> Box { + self.clone_into_box() + } +} + +impl Source for Vec> { + fn clone_into_box(&self) -> Box { + Box::new((*self).clone()) + } + + fn collect(&self) -> Result> { + let mut cache: Value = HashMap::::new().into(); + + for source in self { + source.collect_to(&mut cache)?; + } + + if let ValueKind::Table(table) = cache.kind { + Ok(table) + } else { + unreachable!(); + } + } +} + +impl Source for Vec +where + T: Source + Sync + Send, + T: Clone, + T: 'static, +{ + fn clone_into_box(&self) -> Box { + Box::new((*self).clone()) + } + + fn collect(&self) -> Result> { + let mut cache: Value = HashMap::::new().into(); + + for source in self { + source.collect_to(&mut cache)?; + } + + if let ValueKind::Table(table) = cache.kind { + Ok(table) + } else { + unreachable!(); + } + } +} diff --git a/pagetop/src/config/value.rs b/pagetop/src/config/value.rs new file mode 100644 index 00000000..29d62cfe --- /dev/null +++ b/pagetop/src/config/value.rs @@ -0,0 +1,545 @@ +use crate::config::error::*; + +use serde::de::{Deserialize, Deserializer, Visitor}; + +use std::collections::HashMap; +use std::fmt; +use std::fmt::Display; + +/// Underlying kind of the configuration value. +#[derive(Clone, Debug, Default, PartialEq)] +pub enum ValueKind { + #[default] + Nil, + Boolean(bool), + Integer(i64), + Float(f64), + String(String), + Table(Table), + Array(Array), +} + +pub type Array = Vec; +pub type Table = HashMap; + +impl From> for ValueKind +where + T: Into, +{ + fn from(value: Option) -> Self { + match value { + Some(value) => value.into(), + None => ValueKind::Nil, + } + } +} + +impl From for ValueKind { + fn from(value: String) -> Self { + ValueKind::String(value) + } +} + +impl<'a> From<&'a str> for ValueKind { + fn from(value: &'a str) -> Self { + ValueKind::String(value.into()) + } +} + +impl From for ValueKind { + fn from(value: i64) -> Self { + ValueKind::Integer(value) + } +} + +impl From for ValueKind { + fn from(value: f64) -> Self { + ValueKind::Float(value) + } +} + +impl From for ValueKind { + fn from(value: bool) -> Self { + ValueKind::Boolean(value) + } +} + +impl From> for ValueKind +where + T: Into, +{ + fn from(values: HashMap) -> Self { + let mut r = HashMap::new(); + + for (k, v) in values { + r.insert(k.clone(), v.into()); + } + + ValueKind::Table(r) + } +} + +impl From> for ValueKind +where + T: Into, +{ + fn from(values: Vec) -> Self { + let mut l = Vec::new(); + + for v in values { + l.push(v.into()); + } + + ValueKind::Array(l) + } +} + +impl Display for ValueKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + ValueKind::String(ref value) => write!(f, "{}", value), + ValueKind::Boolean(value) => write!(f, "{}", value), + ValueKind::Integer(value) => write!(f, "{}", value), + ValueKind::Float(value) => write!(f, "{}", value), + ValueKind::Nil => write!(f, "nil"), + + // TODO: Figure out a nice Display for these + ValueKind::Table(ref table) => write!(f, "{:?}", table), + ValueKind::Array(ref array) => write!(f, "{:?}", array), + } + } +} + +/// A configuration value. +#[derive(Default, Debug, Clone, PartialEq)] +pub struct Value { + /// A description of the original location of the value. + /// + /// A Value originating from a File might contain: + /// ```text + /// Settings.toml + /// ``` + /// + /// A Value originating from the environment would contain: + /// ```text + /// the envrionment + /// ``` + /// + /// A Value originating from a remote source might contain: + /// ```text + /// etcd+http://127.0.0.1:2379 + /// ``` + origin: Option, + + /// Underlying kind of the configuration value. + pub kind: ValueKind, +} + +impl Value { + /// Create a new value instance that will remember its source uri. + pub fn new(origin: Option<&String>, kind: V) -> Self + where + V: Into, + { + Value { + origin: origin.cloned(), + kind: kind.into(), + } + } + + /// Attempt to deserialize this value into the requested type. + pub fn try_into<'de, T: Deserialize<'de>>(self) -> Result { + T::deserialize(self) + } + + /// Returns `self` as a bool, if possible. + // FIXME: Should this not be `try_into_*` ? + pub fn into_bool(self) -> Result { + match self.kind { + ValueKind::Boolean(value) => Ok(value), + ValueKind::Integer(value) => Ok(value != 0), + ValueKind::Float(value) => Ok(value != 0.0), + + ValueKind::String(ref value) => { + match value.to_lowercase().as_ref() { + "1" | "true" | "on" | "yes" => Ok(true), + "0" | "false" | "off" | "no" => Ok(false), + + // Unexpected string value + s => Err(ConfigError::invalid_type( + self.origin.clone(), + Unexpected::Str(s.into()), + "a boolean", + )), + } + } + + // Unexpected type + ValueKind::Nil => Err(ConfigError::invalid_type( + self.origin, + Unexpected::Unit, + "a boolean", + )), + ValueKind::Table(_) => Err(ConfigError::invalid_type( + self.origin, + Unexpected::Map, + "a boolean", + )), + ValueKind::Array(_) => Err(ConfigError::invalid_type( + self.origin, + Unexpected::Seq, + "a boolean", + )), + } + } + + /// Returns `self` into an i64, if possible. + // FIXME: Should this not be `try_into_*` ? + pub fn into_int(self) -> Result { + match self.kind { + ValueKind::Integer(value) => Ok(value), + + ValueKind::String(ref s) => { + match s.to_lowercase().as_ref() { + "true" | "on" | "yes" => Ok(1), + "false" | "off" | "no" => Ok(0), + _ => { + s.parse().map_err(|_| { + // Unexpected string + ConfigError::invalid_type( + self.origin.clone(), + Unexpected::Str(s.clone()), + "an integer", + ) + }) + } + } + } + + #[allow(clippy::bool_to_int_with_if)] + ValueKind::Boolean(value) => Ok(if value { 1 } else { 0 }), + + ValueKind::Float(value) => Ok(value.round() as i64), + + // Unexpected type + ValueKind::Nil => Err(ConfigError::invalid_type( + self.origin, + Unexpected::Unit, + "an integer", + )), + + ValueKind::Table(_) => Err(ConfigError::invalid_type( + self.origin, + Unexpected::Map, + "an integer", + )), + + ValueKind::Array(_) => Err(ConfigError::invalid_type( + self.origin, + Unexpected::Seq, + "an integer", + )), + } + } + + /// Returns `self` into a f64, if possible. + // FIXME: Should this not be `try_into_*` ? + pub fn into_float(self) -> Result { + match self.kind { + ValueKind::Float(value) => Ok(value), + + ValueKind::String(ref s) => { + match s.to_lowercase().as_ref() { + "true" | "on" | "yes" => Ok(1.0), + "false" | "off" | "no" => Ok(0.0), + _ => { + s.parse().map_err(|_| { + // Unexpected string + ConfigError::invalid_type( + self.origin.clone(), + Unexpected::Str(s.clone()), + "a floating point", + ) + }) + } + } + } + + ValueKind::Integer(value) => Ok(value as f64), + ValueKind::Boolean(value) => Ok(if value { 1.0 } else { 0.0 }), + + // Unexpected type. + ValueKind::Nil => Err(ConfigError::invalid_type( + self.origin, + Unexpected::Unit, + "a floating point", + )), + ValueKind::Table(_) => Err(ConfigError::invalid_type( + self.origin, + Unexpected::Map, + "a floating point", + )), + ValueKind::Array(_) => Err(ConfigError::invalid_type( + self.origin, + Unexpected::Seq, + "a floating point", + )), + } + } + + /// Returns `self` into a str, if possible. + // FIXME: Should this not be `try_into_*` ? + pub fn into_str(self) -> Result { + match self.kind { + ValueKind::String(value) => Ok(value), + + ValueKind::Boolean(value) => Ok(value.to_string()), + ValueKind::Integer(value) => Ok(value.to_string()), + ValueKind::Float(value) => Ok(value.to_string()), + + // Cannot convert. + ValueKind::Nil => Err(ConfigError::invalid_type( + self.origin, + Unexpected::Unit, + "a string", + )), + ValueKind::Table(_) => Err(ConfigError::invalid_type( + self.origin, + Unexpected::Map, + "a string", + )), + ValueKind::Array(_) => Err(ConfigError::invalid_type( + self.origin, + Unexpected::Seq, + "a string", + )), + } + } + + /// Returns `self` into an array, if possible. + // FIXME: Should this not be `try_into_*` ? + pub fn into_array(self) -> Result> { + match self.kind { + ValueKind::Array(value) => Ok(value), + + // Cannot convert. + ValueKind::Float(value) => Err(ConfigError::invalid_type( + self.origin, + Unexpected::Float(value), + "an array", + )), + ValueKind::String(value) => Err(ConfigError::invalid_type( + self.origin, + Unexpected::Str(value), + "an array", + )), + ValueKind::Integer(value) => Err(ConfigError::invalid_type( + self.origin, + Unexpected::Integer(value), + "an array", + )), + ValueKind::Boolean(value) => Err(ConfigError::invalid_type( + self.origin, + Unexpected::Bool(value), + "an array", + )), + ValueKind::Nil => Err(ConfigError::invalid_type( + self.origin, + Unexpected::Unit, + "an array", + )), + ValueKind::Table(_) => Err(ConfigError::invalid_type( + self.origin, + Unexpected::Map, + "an array", + )), + } + } + + /// If the `Value` is a Table, returns the associated Map. + // FIXME: Should this not be `try_into_*` ? + pub fn into_table(self) -> Result> { + match self.kind { + ValueKind::Table(value) => Ok(value), + + // Cannot convert. + ValueKind::Float(value) => Err(ConfigError::invalid_type( + self.origin, + Unexpected::Float(value), + "a map", + )), + ValueKind::String(value) => Err(ConfigError::invalid_type( + self.origin, + Unexpected::Str(value), + "a map", + )), + ValueKind::Integer(value) => Err(ConfigError::invalid_type( + self.origin, + Unexpected::Integer(value), + "a map", + )), + ValueKind::Boolean(value) => Err(ConfigError::invalid_type( + self.origin, + Unexpected::Bool(value), + "a map", + )), + ValueKind::Nil => Err(ConfigError::invalid_type( + self.origin, + Unexpected::Unit, + "a map", + )), + ValueKind::Array(_) => Err(ConfigError::invalid_type( + self.origin, + Unexpected::Seq, + "a map", + )), + } + } +} + +impl<'de> Deserialize<'de> for Value { + #[inline] + fn deserialize(deserializer: D) -> ::std::result::Result + where + D: Deserializer<'de>, + { + struct ValueVisitor; + + impl<'de> Visitor<'de> for ValueVisitor { + type Value = Value; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("any valid configuration value") + } + + #[inline] + fn visit_bool(self, value: bool) -> ::std::result::Result { + Ok(value.into()) + } + + #[inline] + fn visit_i8(self, value: i8) -> ::std::result::Result { + Ok((value as i64).into()) + } + + #[inline] + fn visit_i16(self, value: i16) -> ::std::result::Result { + Ok((value as i64).into()) + } + + #[inline] + fn visit_i32(self, value: i32) -> ::std::result::Result { + Ok((value as i64).into()) + } + + #[inline] + fn visit_i64(self, value: i64) -> ::std::result::Result { + Ok(value.into()) + } + + #[inline] + fn visit_u8(self, value: u8) -> ::std::result::Result { + Ok((value as i64).into()) + } + + #[inline] + fn visit_u16(self, value: u16) -> ::std::result::Result { + Ok((value as i64).into()) + } + + #[inline] + fn visit_u32(self, value: u32) -> ::std::result::Result { + Ok((value as i64).into()) + } + + #[inline] + fn visit_u64(self, value: u64) -> ::std::result::Result { + // FIXME: This is bad + Ok((value as i64).into()) + } + + #[inline] + fn visit_f64(self, value: f64) -> ::std::result::Result { + Ok(value.into()) + } + + #[inline] + fn visit_str(self, value: &str) -> ::std::result::Result + where + E: ::serde::de::Error, + { + self.visit_string(String::from(value)) + } + + #[inline] + fn visit_string(self, value: String) -> ::std::result::Result { + Ok(value.into()) + } + + #[inline] + fn visit_none(self) -> ::std::result::Result { + Ok(Value::new(None, ValueKind::Nil)) + } + + #[inline] + fn visit_some(self, deserializer: D) -> ::std::result::Result + where + D: Deserializer<'de>, + { + Deserialize::deserialize(deserializer) + } + + #[inline] + fn visit_unit(self) -> ::std::result::Result { + Ok(Value::new(None, ValueKind::Nil)) + } + + #[inline] + fn visit_seq(self, mut visitor: V) -> ::std::result::Result + where + V: ::serde::de::SeqAccess<'de>, + { + let mut vec = Array::new(); + + while let Some(elem) = visitor.next_element()? { + vec.push(elem); + } + + Ok(vec.into()) + } + + fn visit_map(self, mut visitor: V) -> ::std::result::Result + where + V: ::serde::de::MapAccess<'de>, + { + let mut values = Table::new(); + + while let Some((key, value)) = visitor.next_entry()? { + values.insert(key, value); + } + + Ok(values.into()) + } + } + + deserializer.deserialize_any(ValueVisitor) + } +} + +impl From for Value +where + T: Into, +{ + fn from(value: T) -> Self { + Value { + origin: None, + kind: value.into(), + } + } +} + +impl Display for Value { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.kind) + } +} diff --git a/pagetop/src/core.rs b/pagetop/src/core.rs new file mode 100644 index 00000000..2d65eadf --- /dev/null +++ b/pagetop/src/core.rs @@ -0,0 +1,87 @@ +//! Key types and functions for creating actions, components, packages, and themes. + +use crate::util::TypeInfo; + +use std::any::Any; + +/// A base trait that extends `Any` to provide metadata and dynamic type casting capabilities. +pub trait AnyBase: Any { + /// Returns the full name of the type. + fn type_name(&self) -> &'static str; + + /// Returns a short name for the type. + fn short_name(&self) -> &'static str; + + /// Returns a reference to `dyn Any` for dynamic type casting. + fn as_any_ref(&self) -> &dyn Any; + + /// Returns a mutable reference to `dyn Any` for dynamic type casting. + fn as_any_mut(&mut self) -> &mut dyn Any; +} + +#[allow(clippy::inline_always)] +impl AnyBase for T { + #[inline(always)] + fn type_name(&self) -> &'static str { + TypeInfo::FullName.of::() + } + + #[inline(always)] + fn short_name(&self) -> &'static str { + TypeInfo::ShortName.of::() + } + + #[inline(always)] + fn as_any_ref(&self) -> &dyn Any { + self + } + + #[inline(always)] + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + +/// A trait for advanced dynamic type manipulation and downcasting. +pub trait AnyTo: AnyBase { + /// Checks if the type is of the specified type `T`. + #[inline] + fn is(&self) -> bool + where + T: AnyBase, + { + self.as_any_ref().is::() + } + + /// Attempts to downcast a reference to the specified type `T`. + #[inline] + fn downcast_ref(&self) -> Option<&T> + where + T: AnyBase, + { + self.as_any_ref().downcast_ref() + } + + /// Attempts to downcast a mutable reference to the specified type `T`. + #[inline] + fn downcast_mut(&mut self) -> Option<&mut T> + where + T: AnyBase, + { + self.as_any_mut().downcast_mut() + } +} + +impl AnyTo for T {} + +// API to define functions that alter the predefined behavior of the code. +pub mod action; + +// API to build new components. +pub mod component; + +// API to add new features with packages. +pub mod package; + +// API to add new layouts with themes. +pub mod theme; diff --git a/pagetop/src/core/action.rs b/pagetop/src/core/action.rs new file mode 100644 index 00000000..b960e8b8 --- /dev/null +++ b/pagetop/src/core/action.rs @@ -0,0 +1,19 @@ +mod definition; +pub use definition::{ActionBase, ActionBox, ActionKey, ActionTrait}; + +mod list; +use list::ActionsList; + +mod all; +pub(crate) use all::add_action; +pub use all::dispatch_actions; + +#[macro_export] +macro_rules! actions { + () => { + Vec::::new() + }; + ( $($action:expr),+ $(,)? ) => {{ + vec![$(Box::new($action),)+] + }}; +} diff --git a/pagetop/src/core/action/all.rs b/pagetop/src/core/action/all.rs new file mode 100644 index 00000000..76256939 --- /dev/null +++ b/pagetop/src/core/action/all.rs @@ -0,0 +1,30 @@ +use crate::core::action::{ActionBox, ActionKey, ActionTrait, ActionsList}; + +use std::collections::HashMap; +use std::sync::{LazyLock, RwLock}; + +// Registered actions. +static ACTIONS: LazyLock>> = + LazyLock::new(|| RwLock::new(HashMap::new())); + +pub fn add_action(action: ActionBox) { + let key = action.key(); + let mut actions = ACTIONS.write().unwrap(); + if let Some(list) = actions.get_mut(&key) { + list.add(action); + } else { + let mut list = ActionsList::new(); + list.add(action); + actions.insert(key, list); + } +} + +pub fn dispatch_actions(key: &ActionKey, f: F) +where + A: ActionTrait, + F: FnMut(&A) -> B, +{ + if let Some(list) = ACTIONS.read().unwrap().get(key) { + list.iter_map(f); + } +} diff --git a/pagetop/src/core/action/definition.rs b/pagetop/src/core/action/definition.rs new file mode 100644 index 00000000..05ff8b79 --- /dev/null +++ b/pagetop/src/core/action/definition.rs @@ -0,0 +1,61 @@ +use crate::core::AnyBase; +use crate::{TypeId, Weight}; + +pub type ActionBox = Box; + +#[derive(Eq, PartialEq, Hash)] +pub struct ActionKey { + action_type_id: TypeId, + theme_type_id: Option, + referer_type_id: Option, + referer_id: Option, +} + +impl ActionKey { + pub fn new( + action_type_id: TypeId, + theme_type_id: Option, + referer_type_id: Option, + referer_id: Option, + ) -> Self { + ActionKey { + action_type_id, + theme_type_id, + referer_type_id, + referer_id, + } + } +} + +pub trait ActionBase { + fn key(&self) -> ActionKey; +} + +pub trait ActionTrait: ActionBase + AnyBase + Send + Sync { + fn theme_type_id(&self) -> Option { + None + } + + fn referer_type_id(&self) -> Option { + None + } + + fn referer_id(&self) -> Option { + None + } + + fn weight(&self) -> Weight { + 0 + } +} + +impl ActionBase for A { + fn key(&self) -> ActionKey { + ActionKey { + action_type_id: self.type_id(), + theme_type_id: self.theme_type_id(), + referer_type_id: self.referer_type_id(), + referer_id: self.referer_id(), + } + } +} diff --git a/pagetop/src/core/action/list.rs b/pagetop/src/core/action/list.rs new file mode 100644 index 00000000..fec25027 --- /dev/null +++ b/pagetop/src/core/action/list.rs @@ -0,0 +1,43 @@ +use crate::core::action::{ActionBox, ActionTrait}; +use crate::core::AnyTo; +use crate::trace; +use crate::AutoDefault; + +use std::sync::RwLock; + +#[derive(AutoDefault)] +pub struct ActionsList(RwLock>); + +impl ActionsList { + pub fn new() -> Self { + ActionsList::default() + } + + pub fn add(&mut self, action: ActionBox) { + let mut list = self.0.write().unwrap(); + list.push(action); + list.sort_by_key(|a| a.weight()); + } + + pub fn iter_map(&self, mut f: F) + where + Self: Sized, + A: ActionTrait, + F: FnMut(&A) -> B, + { + let _: Vec<_> = self + .0 + .read() + .unwrap() + .iter() + .rev() + .map(|a| { + if let Some(action) = (**a).downcast_ref::() { + f(action); + } else { + trace::error!("Failed to downcast action of type {}", (**a).type_name()); + } + }) + .collect(); + } +} diff --git a/pagetop/src/core/component.rs b/pagetop/src/core/component.rs new file mode 100644 index 00000000..5a02ee0e --- /dev/null +++ b/pagetop/src/core/component.rs @@ -0,0 +1,14 @@ +mod context; +pub use context::{AssetsOp, Context, ErrorParam}; +pub type FnContextualPath = fn(cx: &Context) -> &str; + +mod definition; +pub use definition::{ComponentBase, ComponentTrait}; + +mod classes; +pub use classes::{ComponentClasses, ComponentClassesOp}; + +mod children; +pub use children::Children; +pub use children::{ChildComponent, ChildOp}; +pub use children::{TypedComponent, TypedOp}; diff --git a/pagetop/src/core/component/children.rs b/pagetop/src/core/component/children.rs new file mode 100644 index 00000000..9a5db05b --- /dev/null +++ b/pagetop/src/core/component/children.rs @@ -0,0 +1,213 @@ +use crate::core::component::{ComponentTrait, Context}; +use crate::html::{html, Markup}; +use crate::{fn_builder, TypeId}; + +use std::sync::{Arc, RwLock}; + +#[derive(Clone)] +pub struct ChildComponent(Arc>); + +impl ChildComponent { + pub fn with(component: impl ComponentTrait) -> Self { + ChildComponent(Arc::new(RwLock::new(component))) + } + + // ChildComponent RENDER. + + pub fn render(&self, cx: &mut Context) -> Markup { + self.0.write().unwrap().render(cx) + } + + // ChildComponent HELPERS. + + fn type_id(&self) -> TypeId { + self.0.read().unwrap().type_id() + } + + fn id(&self) -> String { + self.0.read().unwrap().id().unwrap_or_default() + } +} + +// ************************************************************************************************* + +pub struct TypedComponent(Arc>); + +impl Clone for TypedComponent { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl TypedComponent { + pub fn with(component: C) -> Self { + TypedComponent(Arc::new(RwLock::new(component))) + } + + // TypedComponent RENDER. + + pub fn render(&self, cx: &mut Context) -> Markup { + self.0.write().unwrap().render(cx) + } + + // TypedComponent HELPERS. + + fn to_child(&self) -> ChildComponent { + ChildComponent(self.0.clone()) + } +} + +// ************************************************************************************************* + +pub enum ChildOp { + Add(ChildComponent), + InsertAfterId(&'static str, ChildComponent), + InsertBeforeId(&'static str, ChildComponent), + Prepend(ChildComponent), + RemoveById(&'static str), + ReplaceById(&'static str, ChildComponent), + Reset, +} + +pub enum TypedOp { + Add(TypedComponent), + InsertAfterId(&'static str, TypedComponent), + InsertBeforeId(&'static str, TypedComponent), + Prepend(TypedComponent), + RemoveById(&'static str), + ReplaceById(&'static str, TypedComponent), + Reset, +} + +#[derive(Clone, Default)] +pub struct Children(Vec); + +impl Children { + pub fn new() -> Self { + Children::default() + } + + pub fn with(child: ChildComponent) -> Self { + Children::default().with_value(ChildOp::Add(child)) + } + + pub(crate) fn merge(mixes: &[Option<&Children>]) -> Self { + let mut opt = Children::default(); + for m in mixes.iter().flatten() { + opt.0.append(&mut m.0.clone()); + } + opt + } + + // Children BUILDER. + + #[fn_builder] + pub fn set_value(&mut self, op: ChildOp) -> &mut Self { + match op { + ChildOp::Add(any) => self.add(any), + ChildOp::InsertAfterId(id, any) => self.insert_after_id(id, any), + ChildOp::InsertBeforeId(id, any) => self.insert_before_id(id, any), + ChildOp::Prepend(any) => self.prepend(any), + ChildOp::RemoveById(id) => self.remove_by_id(id), + ChildOp::ReplaceById(id, any) => self.replace_by_id(id, any), + ChildOp::Reset => self.reset(), + }; + self + } + + #[fn_builder] + pub fn set_typed(&mut self, op: TypedOp) -> &mut Self { + match op { + TypedOp::Add(typed) => self.add(typed.to_child()), + TypedOp::InsertAfterId(id, typed) => self.insert_after_id(id, typed.to_child()), + TypedOp::InsertBeforeId(id, typed) => self.insert_before_id(id, typed.to_child()), + TypedOp::Prepend(typed) => self.prepend(typed.to_child()), + TypedOp::RemoveById(id) => self.remove_by_id(id), + TypedOp::ReplaceById(id, typed) => self.replace_by_id(id, typed.to_child()), + TypedOp::Reset => self.reset(), + }; + self + } + + #[inline] + fn add(&mut self, child: ChildComponent) { + self.0.push(child); + } + + #[inline] + fn insert_after_id(&mut self, id: &str, child: ChildComponent) { + match self.0.iter().position(|c| c.id() == id) { + Some(index) => self.0.insert(index + 1, child), + _ => self.0.push(child), + }; + } + + #[inline] + fn insert_before_id(&mut self, id: &str, child: ChildComponent) { + match self.0.iter().position(|c| c.id() == id) { + Some(index) => self.0.insert(index, child), + _ => self.0.insert(0, child), + }; + } + + #[inline] + fn prepend(&mut self, child: ChildComponent) { + self.0.insert(0, child); + } + + #[inline] + fn remove_by_id(&mut self, id: &str) { + if let Some(index) = self.0.iter().position(|c| c.id() == id) { + self.0.remove(index); + } + } + + #[inline] + fn replace_by_id(&mut self, id: &str, child: ChildComponent) { + for c in &mut self.0 { + if c.id() == id { + *c = child; + break; + } + } + } + + #[inline] + fn reset(&mut self) { + self.0.clear(); + } + + // Children GETTERS. + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn get_by_id(&self, id: impl Into) -> Option<&ChildComponent> { + let id = id.into(); + self.0.iter().find(|c| c.id() == id) + } + + pub fn iter_by_id(&self, id: impl Into) -> impl Iterator { + let id = id.into(); + self.0.iter().filter(move |&c| c.id() == id) + } + + pub fn iter_by_type_id(&self, type_id: TypeId) -> impl Iterator { + self.0.iter().filter(move |&c| c.type_id() == type_id) + } + + // Children RENDER. + + pub fn render(&self, cx: &mut Context) -> Markup { + html! { + @for c in &self.0 { + (c.render(cx)) + } + } + } +} diff --git a/pagetop/src/core/component/classes.rs b/pagetop/src/core/component/classes.rs new file mode 100644 index 00000000..a61809c6 --- /dev/null +++ b/pagetop/src/core/component/classes.rs @@ -0,0 +1,19 @@ +use crate::core::component::ComponentBase; +use crate::html::{ClassesOp, OptionClasses}; + +pub trait ComponentClassesOp { + fn with_classes(self, op: ClassesOp, classes: impl Into) -> Self; +} + +pub trait ComponentClasses: ComponentBase + ComponentClassesOp { + fn set_classes(&mut self, op: ClassesOp, classes: impl Into) -> &mut Self; + + fn classes(&self) -> &OptionClasses; +} + +impl ComponentClassesOp for C { + fn with_classes(mut self, op: ClassesOp, classes: impl Into) -> Self { + self.set_classes(op, classes); + self + } +} diff --git a/pagetop/src/core/component/context.rs b/pagetop/src/core/component/context.rs new file mode 100644 index 00000000..45bdcb87 --- /dev/null +++ b/pagetop/src/core/component/context.rs @@ -0,0 +1,199 @@ +use crate::concat_string; +use crate::core::component::ChildOp; +use crate::core::theme::all::{theme_by_short_name, DEFAULT_THEME}; +use crate::core::theme::{ChildrenInRegions, ThemeRef}; +use crate::html::{html, Markup}; +use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; +use crate::locale::{LanguageIdentifier, DEFAULT_LANGID}; +use crate::service::HttpRequest; +use crate::util::TypeInfo; + +use std::collections::HashMap; +use std::error::Error; +use std::str::FromStr; + +use std::fmt; + +pub enum AssetsOp { + LangId(&'static LanguageIdentifier), + Theme(&'static str), + Layout(&'static str), + // Favicon. + SetFavicon(Option), + SetFaviconIfNone(Favicon), + // Stylesheets. + AddStyleSheet(StyleSheet), + RemoveStyleSheet(&'static str), + // JavaScripts. + AddJavaScript(JavaScript), + RemoveJavaScript(&'static str), +} + +#[derive(Debug)] +pub enum ErrorParam { + NotFound, + ParseError(String), +} + +impl fmt::Display for ErrorParam { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ErrorParam::NotFound => write!(f, "Parameter not found"), + ErrorParam::ParseError(e) => write!(f, "Parse error: {e}"), + } + } +} + +impl Error for ErrorParam {} + +#[rustfmt::skip] +pub struct Context { + request : HttpRequest, + langid : &'static LanguageIdentifier, + theme : ThemeRef, + layout : &'static str, + favicon : Option, + stylesheet: Assets, + javascript: Assets, + regions : ChildrenInRegions, + params : HashMap<&'static str, String>, + id_counter: usize, +} + +impl Context { + #[rustfmt::skip] + pub(crate) fn new(request: HttpRequest) -> Self { + Context { + request, + langid : &DEFAULT_LANGID, + theme : *DEFAULT_THEME, + layout : "default", + favicon : None, + stylesheet: Assets::::new(), + javascript: Assets::::new(), + regions : ChildrenInRegions::default(), + params : HashMap::<&str, String>::new(), + id_counter: 0, + } + } + + pub fn set_assets(&mut self, op: AssetsOp) -> &mut Self { + match op { + AssetsOp::LangId(langid) => { + self.langid = langid; + } + AssetsOp::Theme(theme_name) => { + self.theme = theme_by_short_name(theme_name).unwrap_or(*DEFAULT_THEME); + } + AssetsOp::Layout(layout) => { + self.layout = layout; + } + // Favicon. + AssetsOp::SetFavicon(favicon) => { + self.favicon = favicon; + } + AssetsOp::SetFaviconIfNone(icon) => { + if self.favicon.is_none() { + self.favicon = Some(icon); + } + } + // Stylesheets. + AssetsOp::AddStyleSheet(css) => { + self.stylesheet.add(css); + } + AssetsOp::RemoveStyleSheet(path) => { + self.stylesheet.remove(path); + } + // JavaScripts. + AssetsOp::AddJavaScript(js) => { + self.javascript.add(js); + } + AssetsOp::RemoveJavaScript(path) => { + self.javascript.remove(path); + } + } + self + } + + pub fn set_in_region(&mut self, region: &'static str, op: ChildOp) -> &mut Self { + self.regions.set_in_region(region, op); + self + } + + pub fn set_param(&mut self, key: &'static str, value: &T) -> &mut Self { + self.params.insert(key, value.to_string()); + self + } + + // Context GETTERS. + + pub fn request(&self) -> &HttpRequest { + &self.request + } + + pub fn langid(&self) -> &LanguageIdentifier { + self.langid + } + + pub fn theme(&self) -> ThemeRef { + self.theme + } + + pub fn layout(&self) -> &str { + self.layout + } + + pub fn regions(&self) -> &ChildrenInRegions { + &self.regions + } + + pub fn get_param(&self, key: &'static str) -> Result { + self.params + .get(key) + .ok_or(ErrorParam::NotFound) + .and_then(|v| T::from_str(v).map_err(|_| ErrorParam::ParseError(v.clone()))) + } + + // Context RENDER. + + pub fn render_assets(&mut self) -> Markup { + html! { + @if let Some(favicon) = &self.favicon { + (favicon.render()) + } + (self.stylesheet.render()) + (self.javascript.render()) + } + } + + pub fn render_region(&mut self, region: impl Into) -> Markup { + self.regions + .all_in_region(self.theme, ®ion.into()) + .render(self) + } + + // Context EXTRAS. + + pub fn remove_param(&mut self, key: &'static str) -> bool { + self.params.remove(key).is_some() + } + + pub fn required_id(&mut self, id: Option) -> String { + if let Some(id) = id { + id + } else { + let prefix = TypeInfo::ShortName + .of::() + .trim() + .replace(' ', "_") + .to_lowercase(); + let prefix = if prefix.is_empty() { + "prefix".to_owned() + } else { + prefix + }; + self.id_counter += 1; + concat_string!(prefix, "-", self.id_counter.to_string()) + } + } +} diff --git a/pagetop/src/core/component/definition.rs b/pagetop/src/core/component/definition.rs new file mode 100644 index 00000000..af33c943 --- /dev/null +++ b/pagetop/src/core/component/definition.rs @@ -0,0 +1,66 @@ +use crate::base::action; +use crate::core::component::Context; +use crate::core::AnyBase; +use crate::html::{html, Markup, PrepareMarkup}; +use crate::util::TypeInfo; + +pub trait ComponentBase { + fn render(&mut self, cx: &mut Context) -> Markup; +} + +pub trait ComponentTrait: AnyBase + ComponentBase + Send + Sync { + fn new() -> Self + where + Self: Sized; + + fn name(&self) -> &'static str { + TypeInfo::ShortName.of::() + } + + fn description(&self) -> Option { + None + } + + fn id(&self) -> Option { + None + } + + #[allow(unused_variables)] + fn setup_before_prepare(&mut self, cx: &mut Context) {} + + #[allow(unused_variables)] + fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup { + PrepareMarkup::None + } +} + +impl ComponentBase for C { + fn render(&mut self, cx: &mut Context) -> Markup { + if action::component::IsRenderable::dispatch(self, cx) { + // Comprueba el componente antes de prepararlo. + self.setup_before_prepare(cx); + + // Acciones del tema antes de preparar el componente. + action::theme::BeforePrepare::dispatch(self, cx); + + // Acciones de los módulos antes de preparar el componente. + action::component::BeforePrepare::dispatch(self, cx); + + // Renderiza el componente. + let markup = match action::theme::RenderComponent::dispatch(self, cx) { + Some(html) => html, + None => self.prepare_component(cx).render(), + }; + + // Acciones del tema después de preparar el componente. + action::theme::AfterPrepare::dispatch(self, cx); + + // Acciones de los módulos después de preparar el componente. + action::component::AfterPrepare::dispatch(self, cx); + + markup + } else { + html! {} + } + } +} diff --git a/pagetop/src/core/package.rs b/pagetop/src/core/package.rs new file mode 100644 index 00000000..e0c6a12e --- /dev/null +++ b/pagetop/src/core/package.rs @@ -0,0 +1,4 @@ +mod definition; +pub use definition::{PackageRef, PackageTrait}; + +pub(crate) mod all; diff --git a/pagetop/src/core/package/all.rs b/pagetop/src/core/package/all.rs new file mode 100644 index 00000000..301d3a02 --- /dev/null +++ b/pagetop/src/core/package/all.rs @@ -0,0 +1,134 @@ +use crate::core::action::add_action; +use crate::core::package::PackageRef; +use crate::core::theme::all::THEMES; +use crate::{global, include_files, include_files_service, service, trace}; + +use std::sync::{LazyLock, RwLock}; + +// PACKAGES **************************************************************************************** + +static ENABLED_PACKAGES: LazyLock>> = + LazyLock::new(|| RwLock::new(Vec::new())); + +static DROPPED_PACKAGES: LazyLock>> = + LazyLock::new(|| RwLock::new(Vec::new())); + +// REGISTER PACKAGES ******************************************************************************* + +pub fn register_packages(root_package: Option) { + // Initialize a list for packages to be enabled. + let mut enabled_list: Vec = Vec::new(); + + // Add default theme to the enabled list. + add_to_enabled(&mut enabled_list, &crate::base::theme::Basic); + + // If a root package is provided, add it to the enabled list. + if let Some(package) = root_package { + add_to_enabled(&mut enabled_list, package); + } + + // Add default welcome page package to the enabled list. + add_to_enabled(&mut enabled_list, &crate::base::package::Welcome); + + // Save the final list of enabled packages. + ENABLED_PACKAGES.write().unwrap().append(&mut enabled_list); + + // Initialize a list for packages to be dropped. + let mut dropped_list: Vec = Vec::new(); + // If a root package is provided, analyze its dropped list. + if let Some(package) = root_package { + add_to_dropped(&mut dropped_list, package); + } + // Save the final list of dropped packages. + DROPPED_PACKAGES.write().unwrap().append(&mut dropped_list); +} + +fn add_to_enabled(list: &mut Vec, package: PackageRef) { + // Check if the package is not already in the enabled list to avoid duplicates. + if !list.iter().any(|p| p.type_id() == package.type_id()) { + // Add the package dependencies in reverse order first. + for d in package.dependencies().iter().rev() { + add_to_enabled(list, *d); + } + + // Add the package itself to the enabled list. + list.push(package); + + // Check if the package has an associated theme to register. + if let Some(theme) = package.theme() { + let mut registered_themes = THEMES.write().unwrap(); + // Ensure the theme is not already registered to avoid duplicates. + if !registered_themes + .iter() + .any(|t| t.type_id() == theme.type_id()) + { + registered_themes.push(theme); + trace::debug!("Enabling \"{}\" theme", theme.short_name()); + } + } else { + trace::debug!("Enabling \"{}\" package", package.short_name()); + } + } +} + +fn add_to_dropped(list: &mut Vec, package: PackageRef) { + // Iterate through packages recommended to be dropped. + for d in &package.drop_packages() { + // Check if the package is not already in the dropped list. + if !list.iter().any(|p| p.type_id() == d.type_id()) { + // Check if the package is currently enabled. If so, log a warning. + if ENABLED_PACKAGES + .read() + .unwrap() + .iter() + .any(|p| p.type_id() == package.type_id()) + { + trace::warn!( + "Trying to drop \"{}\" package which is enabled", + package.short_name() + ); + } else { + // If the package is not enabled, add it to the dropped list and log the action. + list.push(*d); + trace::debug!("Package \"{}\" dropped", d.short_name()); + // Recursively add the dependencies of the dropped package to the dropped list. + // This ensures that all dependencies are also considered for dropping. + for dependency in &package.dependencies() { + add_to_dropped(list, *dependency); + } + } + } + } +} + +// REGISTER ACTIONS ******************************************************************************** + +pub fn register_actions() { + for m in ENABLED_PACKAGES.read().unwrap().iter() { + for a in m.actions().into_iter() { + add_action(a); + } + } +} + +// INIT PACKAGES *********************************************************************************** + +pub fn init_packages() { + trace::info!("Calling application bootstrap"); + for m in ENABLED_PACKAGES.read().unwrap().iter() { + m.init(); + } +} + +// CONFIGURE SERVICES ****************************************************************************** + +include_files!(assets); + +pub fn configure_services(scfg: &mut service::web::ServiceConfig) { + for m in ENABLED_PACKAGES.read().unwrap().iter() { + m.configure_service(scfg); + } + include_files_service!( + scfg, assets => "/", [&global::SETTINGS.dev.pagetop_project_dir, "static"] + ); +} diff --git a/pagetop/src/core/package/definition.rs b/pagetop/src/core/package/definition.rs new file mode 100644 index 00000000..b1d0a8c9 --- /dev/null +++ b/pagetop/src/core/package/definition.rs @@ -0,0 +1,39 @@ +use crate::core::action::ActionBox; +use crate::core::theme::ThemeRef; +use crate::core::AnyBase; +use crate::locale::L10n; +use crate::{actions, service}; + +pub type PackageRef = &'static dyn PackageTrait; + +/// Los paquetes deben implementar este *trait*. +pub trait PackageTrait: AnyBase + Send + Sync { + fn name(&self) -> L10n { + L10n::n(self.short_name()) + } + + fn description(&self) -> L10n { + L10n::default() + } + + fn theme(&self) -> Option { + None + } + + fn dependencies(&self) -> Vec { + vec![] + } + + fn drop_packages(&self) -> Vec { + vec![] + } + + fn actions(&self) -> Vec { + actions![] + } + + fn init(&self) {} + + #[allow(unused_variables)] + fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {} +} diff --git a/pagetop/src/core/theme.rs b/pagetop/src/core/theme.rs new file mode 100644 index 00000000..adb99d59 --- /dev/null +++ b/pagetop/src/core/theme.rs @@ -0,0 +1,8 @@ +mod definition; +pub use definition::{ThemeRef, ThemeTrait}; + +mod regions; +pub(crate) use regions::ChildrenInRegions; +pub use regions::InRegion; + +pub(crate) mod all; diff --git a/pagetop/src/core/theme/all.rs b/pagetop/src/core/theme/all.rs new file mode 100644 index 00000000..6e10eae7 --- /dev/null +++ b/pagetop/src/core/theme/all.rs @@ -0,0 +1,31 @@ +use crate::core::theme::ThemeRef; +use crate::global; + +use std::sync::{LazyLock, RwLock}; + +// THEMES ****************************************************************************************** + +pub static THEMES: LazyLock>> = LazyLock::new(|| RwLock::new(Vec::new())); + +// DEFAULT THEME *********************************************************************************** + +pub static DEFAULT_THEME: LazyLock = + LazyLock::new(|| match theme_by_short_name(&global::SETTINGS.app.theme) { + Some(theme) => theme, + None => &crate::base::theme::Basic, + }); + +// THEME BY NAME *********************************************************************************** + +pub fn theme_by_short_name(short_name: &str) -> Option { + let short_name = short_name.to_lowercase(); + match THEMES + .read() + .unwrap() + .iter() + .find(|t| t.short_name().to_lowercase() == short_name) + { + Some(theme) => Some(*theme), + _ => None, + } +} diff --git a/pagetop/src/core/theme/definition.rs b/pagetop/src/core/theme/definition.rs new file mode 100644 index 00000000..01e1cb04 --- /dev/null +++ b/pagetop/src/core/theme/definition.rs @@ -0,0 +1,59 @@ +use crate::core::package::PackageTrait; +use crate::global; +use crate::html::{html, Markup}; +use crate::locale::L10n; +use crate::response::page::Page; + +pub type ThemeRef = &'static dyn ThemeTrait; + +/// Los temas deben implementar este "trait". +pub trait ThemeTrait: PackageTrait + Send + Sync { + fn regions(&self) -> Vec<(&'static str, L10n)> { + vec![("content", L10n::l("content"))] + } + + fn render_head(&self, page: &mut Page) -> Markup { + let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no"; + html! { + head { + meta charset="utf-8"; + + @if let Some(title) = page.title() { + title { (global::SETTINGS.app.name) (" | ") (title) } + } @else { + title { (global::SETTINGS.app.name) } + } + + @if let Some(description) = page.description() { + meta name="description" content=(description); + } + + meta name="viewport" content=(viewport); + @for (name, content) in page.metadata() { + meta name=(name) content=(content) {} + } + + meta http-equiv="X-UA-Compatible" content="IE=edge"; + @for (property, content) in page.properties() { + meta property=(property) content=(content) {} + } + + (page.context().render_assets()) + } + } + } + + #[allow(unused_variables)] + fn before_render_body(&self, page: &mut Page) {} + + fn render_body(&self, page: &mut Page) -> Markup { + html! { + body id=[page.body_id().get()] class=[page.body_classes().get()] { + (page.context().render_region("content")) + } + } + } + + #[allow(unused_variables)] + fn after_render_body(&self, page: &mut Page) {} +} diff --git a/pagetop/src/core/theme/regions.rs b/pagetop/src/core/theme/regions.rs new file mode 100644 index 00000000..7e8128c5 --- /dev/null +++ b/pagetop/src/core/theme/regions.rs @@ -0,0 +1,78 @@ +use crate::core::component::{ChildComponent, ChildOp, Children}; +use crate::core::theme::ThemeRef; +use crate::{fn_builder, AutoDefault, TypeId}; + +use std::collections::HashMap; +use std::sync::{LazyLock, RwLock}; + +static THEME_REGIONS: LazyLock>> = + LazyLock::new(|| RwLock::new(HashMap::new())); + +static COMMON_REGIONS: LazyLock> = + LazyLock::new(|| RwLock::new(ChildrenInRegions::default())); + +#[derive(AutoDefault)] +pub struct ChildrenInRegions(HashMap<&'static str, Children>); + +impl ChildrenInRegions { + pub fn new() -> Self { + ChildrenInRegions::default() + } + + pub fn with(region: &'static str, child: ChildComponent) -> Self { + ChildrenInRegions::default().with_in_region(region, ChildOp::Add(child)) + } + + #[fn_builder] + pub fn set_in_region(&mut self, region: &'static str, op: ChildOp) -> &mut Self { + if let Some(region) = self.0.get_mut(region) { + region.set_value(op); + } else { + self.0.insert(region, Children::new().with_value(op)); + } + self + } + + pub fn all_in_region(&self, theme: ThemeRef, region: &str) -> Children { + let common = COMMON_REGIONS.read().unwrap(); + if let Some(r) = THEME_REGIONS.read().unwrap().get(&theme.type_id()) { + Children::merge(&[common.0.get(region), self.0.get(region), r.0.get(region)]) + } else { + Children::merge(&[common.0.get(region), self.0.get(region)]) + } + } +} + +pub enum InRegion { + Content, + Named(&'static str), + OfTheme(&'static str, ThemeRef), +} + +impl InRegion { + pub fn add(&self, child: ChildComponent) -> &Self { + match self { + InRegion::Content => { + COMMON_REGIONS + .write() + .unwrap() + .set_in_region("content", ChildOp::Add(child)); + } + InRegion::Named(name) => { + COMMON_REGIONS + .write() + .unwrap() + .set_in_region(name, ChildOp::Add(child)); + } + InRegion::OfTheme(region, theme) => { + let mut regions = THEME_REGIONS.write().unwrap(); + if let Some(r) = regions.get_mut(&theme.type_id()) { + r.set_in_region(region, ChildOp::Add(child)); + } else { + regions.insert(theme.type_id(), ChildrenInRegions::with(region, child)); + } + } + } + self + } +} diff --git a/pagetop/src/datetime.rs b/pagetop/src/datetime.rs new file mode 100644 index 00000000..732431df --- /dev/null +++ b/pagetop/src/datetime.rs @@ -0,0 +1,4 @@ +//! [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) date and time handling +//! ([chrono](https://docs.rs/chrono)). + +pub use chrono::prelude::*; diff --git a/pagetop/src/global.rs b/pagetop/src/global.rs new file mode 100644 index 00000000..e538958b --- /dev/null +++ b/pagetop/src/global.rs @@ -0,0 +1,121 @@ +//! Global settings. + +use crate::include_config; + +use serde::Deserialize; + +include_config!(SETTINGS: Settings => [ + // [app] + "app.name" => "My App", + "app.description" => "Developed with the amazing PageTop framework.", + "app.theme" => "", + "app.language" => "en-US", + "app.text_direction" => "ltr", + "app.startup_banner" => "Slant", + + // [dev] + "dev.pagetop_project_dir" => "", + + // [log] + "log.tracing" => "Info", + "log.rolling" => "Stdout", + "log.path" => "log", + "log.prefix" => "tracing.log", + "log.format" => "Full", + + // [server] + "server.bind_address" => "localhost", + "server.bind_port" => 8088, + "server.session_lifetime" => 604_800, +]); + +#[derive(Debug, Deserialize)] +/// Configuration settings for the global [`[app]`](App), [`[dev]`](Dev), [`[log]`](Log), and +/// [`[server]`](Server) sections (see [`SETTINGS`]). +pub struct Settings { + pub app: App, + pub dev: Dev, + pub log: Log, + pub server: Server, +} + +#[derive(Debug, Deserialize)] +/// Section `[app]` of the configuration settings. +/// +/// See [`Settings`]. +pub struct App { + /// The name of the application. + /// Default: *"My App"*. + pub name: String, + /// A brief description of the application. + /// Default: *"Developed with the amazing PageTop framework."*. + pub description: String, + /// Default theme. + /// Default: *""*. + pub theme: String, + /// Default language (localization). + /// Default: *"en-US"*. + pub language: String, + /// Default text direction: *"ltr"* (left-to-right), *"rtl"* (right-to-left), or *"auto"*. + /// Default: *"ltr"*. + pub text_direction: String, + /// ASCII banner printed at startup: *"Off"*, *"Slant"*, *"Small"*, *"Speed"*, or *"Starwars"*. + /// Default: *"Slant"*. + pub startup_banner: String, + /// Default: according to the `PAGETOP_RUN_MODE` environment variable, or *"default"* if unset. + pub run_mode: String, +} + +#[derive(Debug, Deserialize)] +/// Section `[dev]` of the configuration settings. +/// +/// See [`Settings`]. +pub struct Dev { + /// Static files required by the application are integrated by default into the executable + /// binary. However, during development, it can be useful to serve these files from their own + /// directory to avoid recompilation every time they are modified. In this case, specify the + /// full path to the project's root directory. + /// Default: *""*. + pub pagetop_project_dir: String, +} + +#[derive(Debug, Deserialize)] +/// Section `[log]` of the configuration settings. +/// +/// See [`Settings`]. +pub struct Log { + /// Filter, or a comma-separated combination of filters, for execution traces: *"Error"*, + /// *"Warn"*, *"Info"*, *"Debug"*, or *"Trace"*. + /// Example: "Error,actix_server::builder=Info,tracing_actix_web=Debug". + /// Default: *"Info"*. + pub tracing: String, + /// Displays traces in the terminal (*"Stdout"*) or logs them in files with rotation: *"Daily"*, + /// *"Hourly"*, *"Minutely"*, or *"Endless"*. + /// Default: *"Stdout"*. + pub rolling: String, + /// Directory for trace files (if `rolling` != *"Stdout"*). + /// Default: *"log"*. + pub path: String, + /// Prefix for trace files (if `rolling` != *"Stdout"*). + /// Default: *"tracing.log"*. + pub prefix: String, + /// Trace output format. Options are *"Full"*, *"Compact"*, *"Pretty"*, or *"Json"*. + /// Default: *"Full"*. + pub format: String, +} + +#[derive(Debug, Deserialize)] +/// Section `[server]` of the configuration settings. +/// +/// See [`Settings`]. +pub struct Server { + /// Web server bind address. + /// Default: *"localhost"*. + pub bind_address: String, + /// Web server bind port. + /// Default: *8088*. + pub bind_port: u16, + /// Session cookie duration in seconds (0 means "until the browser is closed"). + /// Default: *604800* (7 days). + pub session_lifetime: i64, +} diff --git a/pagetop/src/html.rs b/pagetop/src/html.rs new file mode 100644 index 00000000..939316b7 --- /dev/null +++ b/pagetop/src/html.rs @@ -0,0 +1,52 @@ +//! HTML in code. + +mod maud; +pub use maud::{html, html_private, Markup, PreEscaped, DOCTYPE}; + +mod assets; +pub use assets::favicon::Favicon; +pub use assets::javascript::JavaScript; +pub use assets::stylesheet::{StyleSheet, TargetMedia}; +pub(crate) use assets::Assets; + +mod opt_id; +pub use opt_id::OptionId; + +mod opt_name; +pub use opt_name::OptionName; + +mod opt_string; +pub use opt_string::OptionString; + +mod opt_translated; +pub use opt_translated::OptionTranslated; + +mod opt_classes; +pub use opt_classes::{ClassesOp, OptionClasses}; + +mod opt_component; +pub use opt_component::OptionComponent; + +pub mod unit; + +use crate::AutoDefault; + +#[derive(AutoDefault)] +pub enum PrepareMarkup { + #[default] + None, + Text(&'static str), + Escaped(String), + With(Markup), +} + +impl PrepareMarkup { + pub fn render(&self) -> Markup { + match self { + PrepareMarkup::None => html! {}, + PrepareMarkup::Text(text) => html! { (text) }, + PrepareMarkup::Escaped(string) => html! { (PreEscaped(string)) }, + PrepareMarkup::With(markup) => html! { (markup) }, + } + } +} diff --git a/pagetop/src/html/assets.rs b/pagetop/src/html/assets.rs new file mode 100644 index 00000000..c8b8f1b9 --- /dev/null +++ b/pagetop/src/html/assets.rs @@ -0,0 +1,53 @@ +pub mod favicon; +pub mod javascript; +pub mod stylesheet; + +use crate::html::{html, Markup}; +use crate::{AutoDefault, Weight}; + +pub trait AssetsTrait { + fn name(&self) -> &String; + + fn weight(&self) -> Weight; + + fn render(&self) -> Markup; +} + +#[derive(AutoDefault)] +pub(crate) struct Assets(Vec); + +impl Assets { + pub fn new() -> Self { + Assets::(Vec::::new()) + } + + pub fn add(&mut self, asset: T) -> &mut Self { + match self.0.iter().position(|x| x.name() == asset.name()) { + Some(index) => { + if self.0[index].weight() > asset.weight() { + self.0.remove(index); + self.0.push(asset); + } + } + _ => self.0.push(asset), + }; + self + } + + pub fn remove(&mut self, name: &'static str) -> &mut Self { + if let Some(index) = self.0.iter().position(|x| x.name() == name) { + self.0.remove(index); + }; + self + } + + pub fn render(&mut self) -> Markup { + let assets = &mut self.0; + assets.sort_by_key(AssetsTrait::weight); + html! { + @for a in assets { + (a.render()) + } + } + } +} diff --git a/pagetop/src/html/assets/favicon.rs b/pagetop/src/html/assets/favicon.rs new file mode 100644 index 00000000..366da4ff --- /dev/null +++ b/pagetop/src/html/assets/favicon.rs @@ -0,0 +1,93 @@ +use crate::html::{html, Markup}; +use crate::AutoDefault; + +#[derive(AutoDefault)] +pub struct Favicon(Vec); + +impl Favicon { + pub fn new() -> Self { + Favicon::default() + } + + // Favicon BUILDER. + + pub fn with_icon(self, image: &str) -> Self { + self.add_icon_item("icon", image, None, None) + } + + pub fn with_icon_for_sizes(self, image: &str, sizes: &str) -> Self { + self.add_icon_item("icon", image, Some(sizes), None) + } + + pub fn with_apple_touch_icon(self, image: &str, sizes: &str) -> Self { + self.add_icon_item("apple-touch-icon", image, Some(sizes), None) + } + + pub fn with_mask_icon(self, image: &str, color: &str) -> Self { + self.add_icon_item("mask-icon", image, None, Some(color)) + } + + pub fn with_manifest(self, file: &str) -> Self { + self.add_icon_item("manifest", file, None, None) + } + + pub fn with_theme_color(mut self, color: &str) -> Self { + self.0.push(html! { + meta name="theme-color" content=(color); + }); + self + } + + pub fn with_ms_tile_color(mut self, color: &str) -> Self { + self.0.push(html! { + meta name="msapplication-TileColor" content=(color); + }); + self + } + + pub fn with_ms_tile_image(mut self, image: &str) -> Self { + self.0.push(html! { + meta name="msapplication-TileImage" content=(image); + }); + self + } + + fn add_icon_item( + mut self, + icon_rel: &str, + icon_source: &str, + icon_sizes: Option<&str>, + icon_color: Option<&str>, + ) -> Self { + let icon_type = match icon_source.rfind('.') { + Some(i) => match icon_source[i..].to_owned().to_lowercase().as_str() { + ".gif" => Some("image/gif"), + ".ico" => Some("image/x-icon"), + ".jpg" => Some("image/jpg"), + ".png" => Some("image/png"), + ".svg" => Some("image/svg+xml"), + _ => None, + }, + _ => None, + }; + self.0.push(html! { + link + rel=(icon_rel) + type=[(icon_type)] + sizes=[(icon_sizes)] + color=[(icon_color)] + href=(icon_source); + }); + self + } + + // Favicon PREPARE. + + pub(crate) fn render(&self) -> Markup { + html! { + @for item in &self.0 { + (item) + } + } + } +} diff --git a/pagetop/src/html/assets/javascript.rs b/pagetop/src/html/assets/javascript.rs new file mode 100644 index 00000000..76d84673 --- /dev/null +++ b/pagetop/src/html/assets/javascript.rs @@ -0,0 +1,111 @@ +use crate::html::assets::AssetsTrait; +use crate::html::{html, Markup}; +use crate::{concat_string, AutoDefault, Weight}; + +#[derive(AutoDefault)] +enum Source { + #[default] + From(String), + Defer(String), + Async(String), + Inline(String, String), + OnLoad(String, String), +} + +#[rustfmt::skip] +#[derive(AutoDefault)] +pub struct JavaScript { + source : Source, + prefix : &'static str, + version: &'static str, + weight : Weight, +} + +impl AssetsTrait for JavaScript { + fn name(&self) -> &String { + match &self.source { + Source::From(path) => path, + Source::Defer(path) => path, + Source::Async(path) => path, + Source::Inline(name, _) => name, + Source::OnLoad(name, _) => name, + } + } + + fn weight(&self) -> Weight { + self.weight + } + + fn render(&self) -> Markup { + match &self.source { + Source::From(path) => html! { + script src=(concat_string!(path, self.prefix, self.version)) {}; + }, + Source::Defer(path) => html! { + script src=(concat_string!(path, self.prefix, self.version)) defer {}; + }, + Source::Async(path) => html! { + script src=(concat_string!(path, self.prefix, self.version)) async {}; + }, + Source::Inline(_, code) => html! { + script { (code) }; + }, + Source::OnLoad(_, code) => html! { (concat_string!( + "document.addEventListener('DOMContentLoaded',function(){", + code, + "});" + )) }, + } + } +} + +impl JavaScript { + pub fn from(path: impl Into) -> Self { + JavaScript { + source: Source::From(path.into()), + ..Default::default() + } + } + + pub fn defer(path: impl Into) -> Self { + JavaScript { + source: Source::Defer(path.into()), + ..Default::default() + } + } + + pub fn asynchronous(path: impl Into) -> Self { + JavaScript { + source: Source::Async(path.into()), + ..Default::default() + } + } + + pub fn inline(name: impl Into, script: impl Into) -> Self { + JavaScript { + source: Source::Inline(name.into(), script.into()), + ..Default::default() + } + } + + pub fn on_load(name: impl Into, script: impl Into) -> Self { + JavaScript { + source: Source::OnLoad(name.into(), script.into()), + ..Default::default() + } + } + + pub fn with_version(mut self, version: &'static str) -> Self { + (self.prefix, self.version) = if version.is_empty() { + ("", "") + } else { + ("?v=", version) + }; + self + } + + pub fn with_weight(mut self, value: Weight) -> Self { + self.weight = value; + self + } +} diff --git a/pagetop/src/html/assets/stylesheet.rs b/pagetop/src/html/assets/stylesheet.rs new file mode 100644 index 00000000..5dd65a97 --- /dev/null +++ b/pagetop/src/html/assets/stylesheet.rs @@ -0,0 +1,95 @@ +use crate::html::assets::AssetsTrait; +use crate::html::{html, Markup, PreEscaped}; +use crate::{concat_string, AutoDefault, Weight}; + +#[derive(AutoDefault)] +enum Source { + #[default] + From(String), + Inline(String, String), +} + +pub enum TargetMedia { + Default, + Print, + Screen, + Speech, +} + +#[rustfmt::skip] +#[derive(AutoDefault)] +pub struct StyleSheet { + source : Source, + prefix : &'static str, + version: &'static str, + media : Option<&'static str>, + weight : Weight, +} + +impl AssetsTrait for StyleSheet { + fn name(&self) -> &String { + match &self.source { + Source::From(path) => path, + Source::Inline(name, _) => name, + } + } + + fn weight(&self) -> Weight { + self.weight + } + + fn render(&self) -> Markup { + match &self.source { + Source::From(path) => html! { + link + rel="stylesheet" + href=(concat_string!(path, self.prefix, self.version)) + media=[self.media]; + }, + Source::Inline(_, code) => html! { + style { (PreEscaped(code)) }; + }, + } + } +} + +impl StyleSheet { + pub fn from(path: impl Into) -> Self { + StyleSheet { + source: Source::From(path.into()), + ..Default::default() + } + } + + pub fn inline(name: impl Into, styles: impl Into) -> Self { + StyleSheet { + source: Source::Inline(name.into(), styles.into()), + ..Default::default() + } + } + + pub fn with_version(mut self, version: &'static str) -> Self { + (self.prefix, self.version) = if version.is_empty() { + ("", "") + } else { + ("?v=", version) + }; + self + } + + pub fn with_weight(mut self, value: Weight) -> Self { + self.weight = value; + self + } + + #[rustfmt::skip] + pub fn for_media(mut self, media: &TargetMedia) -> Self { + self.media = match media { + TargetMedia::Default => None, + TargetMedia::Print => Some("print"), + TargetMedia::Screen => Some("screen"), + TargetMedia::Speech => Some("speech"), + }; + self + } +} diff --git a/pagetop/src/html/maud.rs b/pagetop/src/html/maud.rs new file mode 100644 index 00000000..801895e5 --- /dev/null +++ b/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 `&` +/// * `<` is escaped as `<` +/// * `>` is escaped as `>` +/// * `"` is escaped as `"` +/// +/// 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), "").unwrap(); +/// assert_eq!(s, "<script>launchMissiles()</script>"); +/// ``` +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 Render for Box { + 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); + + impl Render for DisplayWrapper { + 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>(pub T); + +impl> Render for PreEscaped { + 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; + +impl Markup { + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl + Into> PreEscaped { + /// Converts the inner value to a string. + pub fn into_string(self) -> String { + self.0.into() + } +} + +impl + Into> From> for String { + fn from(value: PreEscaped) -> String { + value.into_string() + } +} + +impl + Default> Default for PreEscaped { + fn default() -> Self { + Self(Default::default()) + } +} + +/// The literal string ``. +/// +/// # 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(""); + +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 { + type Body = String; + + fn respond_to(self, _req: &HttpRequest) -> HttpResponse { + 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(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 ViaRender for &ChooseRenderOrDisplay {} + impl ViaDisplay for ChooseRenderOrDisplay {} + + impl ViaRenderTag { + pub fn render_to(self, value: &T, buffer: &mut String) { + value.render_to(buffer); + } + } + + impl ViaDisplayTag { + pub fn render_to(self, value: &T, buffer: &mut String) { + display(value).render_to(buffer); + } + } +} diff --git a/pagetop/src/html/maud/escape.rs b/pagetop/src/html/maud/escape.rs new file mode 100644 index 00000000..94cdeec1 --- /dev/null +++ b/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("&"), + 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/pagetop/src/html/opt_classes.rs b/pagetop/src/html/opt_classes.rs new file mode 100644 index 00000000..453991cd --- /dev/null +++ b/pagetop/src/html/opt_classes.rs @@ -0,0 +1,111 @@ +//! **OptionClasses** implements a *helper* for dynamically adding class names to components. +//! +//! This *helper* differentiates between default classes (generally associated with styles provided +//! by the theme) and user classes (for customizing components based on application styles). +//! +//! Classes can be added using [Add]. Operations to [Remove], [Replace] or [Toggle] a class, as well +//! as [Clear] all classes, are also provided. +//! +//! **OptionClasses** assumes that the order of the classes is irrelevant +//! (), and duplicate classes will not be allowed. + +use crate::{fn_builder, AutoDefault}; + +pub enum ClassesOp { + Add, + Prepend, + Remove, + Replace(String), + Toggle, + Set, +} + +#[derive(AutoDefault)] +pub struct OptionClasses(Vec); + +impl OptionClasses { + pub fn new(classes: impl Into) -> Self { + OptionClasses::default().with_value(ClassesOp::Prepend, classes) + } + + // OptionClasses BUILDER. + + #[fn_builder] + pub fn set_value(&mut self, op: ClassesOp, classes: impl Into) -> &mut Self { + let classes: String = classes.into(); + let classes: Vec<&str> = classes.split_ascii_whitespace().collect(); + + if classes.is_empty() { + return self; + } + + match op { + ClassesOp::Add => { + self.add(&classes, self.0.len()); + } + ClassesOp::Prepend => { + self.add(&classes, 0); + } + ClassesOp::Remove => { + for class in classes { + self.0.retain(|c| c.ne(&class.to_string())); + } + } + ClassesOp::Replace(classes_to_replace) => { + let mut pos = self.0.len(); + let replace: Vec<&str> = classes_to_replace.split_ascii_whitespace().collect(); + for class in replace { + if let Some(replace_pos) = self.0.iter().position(|c| c.eq(class)) { + self.0.remove(replace_pos); + if pos > replace_pos { + pos = replace_pos; + } + } + } + self.add(&classes, pos); + } + ClassesOp::Toggle => { + for class in classes { + if !class.is_empty() { + if let Some(pos) = self.0.iter().position(|c| c.eq(class)) { + self.0.remove(pos); + } else { + self.0.push(class.to_string()); + } + } + } + } + ClassesOp::Set => { + self.0.clear(); + self.add(&classes, 0); + } + } + + self + } + + #[inline] + fn add(&mut self, classes: &[&str], mut pos: usize) { + for &class in classes { + if !class.is_empty() && !self.0.iter().any(|c| c == class) { + self.0.insert(pos, class.to_string()); + pos += 1; + } + } + } + + // OptionClasses GETTERS. + + pub fn get(&self) -> Option { + if self.0.is_empty() { + None + } else { + Some(self.0.join(" ")) + } + } + + pub fn contains(&self, class: impl Into) -> bool { + let class: String = class.into(); + self.0.iter().any(|c| c.eq(&class)) + } +} diff --git a/pagetop/src/html/opt_component.rs b/pagetop/src/html/opt_component.rs new file mode 100644 index 00000000..5082a939 --- /dev/null +++ b/pagetop/src/html/opt_component.rs @@ -0,0 +1,46 @@ +use crate::core::component::{ComponentTrait, Context, TypedComponent}; +use crate::fn_builder; +use crate::html::{html, Markup}; + +pub struct OptionComponent(Option>); + +impl Default for OptionComponent { + fn default() -> Self { + OptionComponent(None) + } +} + +impl OptionComponent { + pub fn new(component: C) -> Self { + OptionComponent::default().with_value(Some(component)) + } + + // OptionComponent BUILDER. + + #[fn_builder] + pub fn set_value(&mut self, component: Option) -> &mut Self { + if let Some(component) = component { + self.0 = Some(TypedComponent::with(component)); + } else { + self.0 = None; + } + self + } + + // OptionComponent GETTERS. + + pub fn get(&self) -> Option> { + if let Some(value) = &self.0 { + return Some(value.clone()); + } + None + } + + pub fn render(&self, cx: &mut Context) -> Markup { + if let Some(component) = &self.0 { + component.render(cx) + } else { + html! {} + } + } +} diff --git a/pagetop/src/html/opt_id.rs b/pagetop/src/html/opt_id.rs new file mode 100644 index 00000000..80e98325 --- /dev/null +++ b/pagetop/src/html/opt_id.rs @@ -0,0 +1,29 @@ +use crate::{fn_builder, AutoDefault}; + +#[derive(AutoDefault)] +pub struct OptionId(Option); + +impl OptionId { + pub fn new(value: impl Into) -> Self { + OptionId::default().with_value(value) + } + + // OptionId BUILDER. + + #[fn_builder] + pub fn set_value(&mut self, value: impl Into) -> &mut Self { + self.0 = Some(value.into().trim().replace(' ', "_")); + self + } + + // OptionId GETTERS. + + pub fn get(&self) -> Option { + if let Some(value) = &self.0 { + if !value.is_empty() { + return Some(value.to_owned()); + } + } + None + } +} diff --git a/pagetop/src/html/opt_name.rs b/pagetop/src/html/opt_name.rs new file mode 100644 index 00000000..5ba0c486 --- /dev/null +++ b/pagetop/src/html/opt_name.rs @@ -0,0 +1,29 @@ +use crate::{fn_builder, AutoDefault}; + +#[derive(AutoDefault)] +pub struct OptionName(Option); + +impl OptionName { + pub fn new(value: impl Into) -> Self { + OptionName::default().with_value(value) + } + + // OptionName BUILDER. + + #[fn_builder] + pub fn set_value(&mut self, value: impl Into) -> &mut Self { + self.0 = Some(value.into().trim().replace(' ', "_")); + self + } + + // OptionName GETTERS. + + pub fn get(&self) -> Option { + if let Some(value) = &self.0 { + if !value.is_empty() { + return Some(value.to_owned()); + } + } + None + } +} diff --git a/pagetop/src/html/opt_string.rs b/pagetop/src/html/opt_string.rs new file mode 100644 index 00000000..7de22486 --- /dev/null +++ b/pagetop/src/html/opt_string.rs @@ -0,0 +1,29 @@ +use crate::{fn_builder, AutoDefault}; + +#[derive(AutoDefault)] +pub struct OptionString(Option); + +impl OptionString { + pub fn new(value: impl Into) -> Self { + OptionString::default().with_value(value) + } + + // OptionString BUILDER. + + #[fn_builder] + pub fn set_value(&mut self, value: impl Into) -> &mut Self { + self.0 = Some(value.into().trim().to_owned()); + self + } + + // OptionString GETTERS. + + pub fn get(&self) -> Option { + if let Some(value) = &self.0 { + if !value.is_empty() { + return Some(value.to_owned()); + } + } + None + } +} diff --git a/pagetop/src/html/opt_translated.rs b/pagetop/src/html/opt_translated.rs new file mode 100644 index 00000000..e50a073f --- /dev/null +++ b/pagetop/src/html/opt_translated.rs @@ -0,0 +1,30 @@ +use crate::html::Markup; +use crate::locale::{L10n, LanguageIdentifier}; +use crate::{fn_builder, AutoDefault}; + +#[derive(AutoDefault)] +pub struct OptionTranslated(L10n); + +impl OptionTranslated { + pub fn new(value: L10n) -> Self { + OptionTranslated(value) + } + + // OptionTranslated BUILDER. + + #[fn_builder] + pub fn set_value(&mut self, value: L10n) -> &mut Self { + self.0 = value; + self + } + + // OptionTranslated GETTERS. + + pub fn using(&self, langid: &LanguageIdentifier) -> Option { + self.0.using(langid) + } + + pub fn escaped(&self, langid: &LanguageIdentifier) -> Markup { + self.0.escaped(langid) + } +} diff --git a/pagetop/src/html/unit.rs b/pagetop/src/html/unit.rs new file mode 100644 index 00000000..5a153c55 --- /dev/null +++ b/pagetop/src/html/unit.rs @@ -0,0 +1,56 @@ +use crate::AutoDefault; + +use std::fmt; + +// About pixels: Pixels (px) are relative to the viewing device. For low-dpi devices, 1px is one +// device pixel (dot) of the display. For printers and high resolution screens 1px implies multiple +// device pixels. + +// About em: 2em means 2 times the size of the current font. The em and rem units are practical in +// creating perfectly scalable layout! + +// About viewport: If the browser window size is 50cm wide, 1vw = 0.5cm. + +#[rustfmt::skip] +#[derive(AutoDefault)] +pub enum Value { + #[default] + None, + Auto, + + Cm(isize), // Centimeters. + In(isize), // Inches (1in = 96px = 2.54cm). + Mm(isize), // Millimeters. + Pc(isize), // Picas (1pc = 12pt). + Pt(isize), // Points (1pt = 1/72 of 1in). + Px(isize), // Pixels (1px = 1/96th of 1in). + + RelEm(f32), // Relative to the font-size of the element. + RelPct(f32), // Percentage relative to the parent element. + RelRem(f32), // Relative to font-size of the root element. + RelVh(f32), // Relative to 1% of the height of the viewport. + RelVw(f32), // Relative to 1% of the value of the viewport. +} + +#[rustfmt::skip] +impl fmt::Display for Value { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Value::None => write!(f, ""), + Value::Auto => write!(f, "auto"), + // Absolute value. + Value::Cm(av) => write!(f, "{av}cm"), + Value::In(av) => write!(f, "{av}in"), + Value::Mm(av) => write!(f, "{av}mm"), + Value::Pc(av) => write!(f, "{av}pc"), + Value::Pt(av) => write!(f, "{av}pt"), + Value::Px(av) => write!(f, "{av}px"), + // Relative value. + Value::RelEm(rv) => write!(f, "{rv}em"), + Value::RelPct(rv) => write!(f, "{rv}%"), + Value::RelRem(rv) => write!(f, "{rv}rem"), + Value::RelVh(rv) => write!(f, "{rv}vh"), + Value::RelVw(rv) => write!(f, "{rv}vw"), + } + } +} diff --git a/pagetop/src/lib.rs b/pagetop/src/lib.rs new file mode 100644 index 00000000..a5369718 --- /dev/null +++ b/pagetop/src/lib.rs @@ -0,0 +1,119 @@ +//!
+//! +//! +//! +//!

PageTop

+//! +//!

An opinionated web framework to build modular Server-Side Rendering web solutions.

+//! +//! [![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?style=for-the-badge)](https://github.com/manuelcillero/pagetop#-license) +//! [![API Docs](https://img.shields.io/docsrs/pagetop?label=API%20Docs&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop) +//! [![Crates.io](https://img.shields.io/crates/v/pagetop.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop) +//! [![Downloads](https://img.shields.io/crates/d/pagetop.svg?style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop) +//! +//!
+//!
+//! +//! The `PageTop` core API provides a comprehensive toolkit for extending its functionalities to +//! specific requirements and application scenarios through actions, components, packages, and +//! themes: +//! +//! * **Actions** serve as a mechanism to customize `PageTop`'s internal behavior by intercepting +//! its execution flow. +//! * **Components** encapsulate HTML, CSS, and JavaScript into functional, configurable, and +//! well-defined units. +//! * **Packages** extend or customize existing functionality by interacting with `PageTop` APIs +//! or third-party package APIs. +//! * **Themes** enable developers to alter the appearance of pages and components without +//! affecting their functionality. +//! +//! # ⚡️ Quick start +//! +//! ```rust +//! use pagetop::prelude::*; +//! +//! struct HelloWorld; +//! +//! impl PackageTrait for HelloWorld { +//! fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { +//! scfg.route("/", service::web::get().to(hello_world)); +//! } +//! } +//! +//! async fn hello_world(request: HttpRequest) -> ResultPage { +//! Page::new(request) +//! .with_component(Html::with(html! { h1 { "Hello World!" } })) +//! .render() +//! } +//! +//! #[pagetop::main] +//! async fn main() -> std::io::Result<()> { +//! Application::prepare(&HelloWorld).run()?.await +//! } +//! ``` +//! This program implements a package named `HelloWorld` with one service that returns a web page +//! that greets the world whenever it is accessed from the browser at `http://localhost:8088` (using +//! the [default configuration settings](`global::Server`)). You can find this code in the `PageTop` +//! [examples repository](https://github.com/manuelcillero/pagetop/tree/latest/examples). +//! +//! # 🧩 Dependency Management +//! +//! Projects leveraging `PageTop` will use `cargo` to resolve dependencies, similar to any other +//! Rust project. +//! +//! Nevertheless, it’s crucial that each package explicitly declares its +//! [dependencies](core::package::PackageTrait#method.dependencies), if any, to assist `PageTop` in +//! structuring and initializing the application in a modular fashion. +//! +//! # 🚧 Warning +//! +//! **`PageTop`** framework is currently in active development. The API is unstable and subject to +//! frequent changes. Production use is not recommended until version **0.1.0**. + +#![cfg_attr(docsrs, feature(doc_cfg))] + +// RE-EXPORTED ************************************************************************************* + +pub use concat_string::concat_string; + +/// Enables flexible identifier concatenation in macros, allowing new items with pasted identifiers. +pub use paste::paste; + +pub use pagetop_macros::{fn_builder, html, main, test, AutoDefault, ComponentClasses}; + +pub type StaticResources = std::collections::HashMap<&'static str, static_files::Resource>; + +pub use std::any::TypeId; + +pub type Weight = i8; + +// API ********************************************************************************************* + +// Useful functions and macros. +pub mod util; +// Load configuration settings. +pub mod config; +// Global settings. +pub mod global; +// Application tracing and event logging. +pub mod trace; +// HTML in code. +pub mod html; +// Localization. +pub mod locale; +// Date and time handling. +pub mod datetime; +// Essential web framework. +pub mod service; +// Key types and functions for creating actions, components, packages, and themes. +pub mod core; +// Web request response variants. +pub mod response; +// Base actions, components, packages, and themes. +pub mod base; +// Prepare and run the application. +pub mod app; + +// The PageTop Prelude ***************************************************************************** + +pub mod prelude; diff --git a/pagetop/src/locale.rs b/pagetop/src/locale.rs new file mode 100644 index 00000000..6cc79c9f --- /dev/null +++ b/pagetop/src/locale.rs @@ -0,0 +1,281 @@ +//! Localization (L10n). +//! +//! PageTop uses the [Fluent](https://www.projectfluent.org/) specifications for application +//! localization, leveraging the [fluent-templates](https://docs.rs/fluent-templates/) crate to +//! integrate translation resources directly into the application binary. +//! +//! # Fluent Syntax (FTL) +//! +//! The format used to describe the translation resources used by Fluent is called +//! [FTL](https://www.projectfluent.org/fluent/guide/). FTL is designed to be both readable and +//! expressive, enabling complex natural language constructs like gender, plurals, and conjugations. +//! +//! # Fluent Resources +//! +//! Localization resources are organized in the *src/locale* directory, with subdirectories for +//! each valid [Unicode Language Identifier](https://docs.rs/unic-langid/): +//! +//! ```text +//! src/locale/ +//! ├── common.ftl +//! ├── en-US/ +//! │ ├── default.ftl +//! │ └── main.ftl +//! ├── es-ES/ +//! │ ├── default.ftl +//! │ └── main.ftl +//! ├── es-MX/ +//! │ ├── default.ftl +//! │ └── main.ftl +//! └── fr/ +//! ├── default.ftl +//! └── main.ftl +//! ``` +//! +//! Example of a file *src/locale/en-US/main.ftl*: +//! +//! ```text +//! hello-world = Hello world! +//! hello-user = Hello, {$userName}! +//! shared-photos = +//! {$userName} {$photoCount -> +//! [one] added a new photo +//! *[other] added {$photoCount} new photos +//! } of {$userGender -> +//! [male] him and his family +//! [female] her and her family +//! *[other] the family +//! }. +//! ``` +//! +//! Example of the equivalent file *src/locale/es-ES/main.ftl*: +//! +//! ```text +//! hello-world = Hola mundo! +//! hello-user = ¡Hola, {$userName}! +//! shared-photos = +//! {$userName} {$photoCount -> +//! [one] ha añadido una nueva foto +//! *[other] ha añadido {$photoCount} nuevas fotos +//! } de {$userGender -> +//! [male] él y su familia +//! [female] ella y su familia +//! *[other] la familia +//! }. +//! ``` +//! +//! # How to apply localization in your code +//! +//! Once you have created your FTL resource directory, use the +//! [`include_locales!`](crate::include_locales) macro to integrate them into your module or +//! application. If your resources are located in the `"src/locale"` directory, simply declare: +//! +//! ``` +//! use pagetop::prelude::*; +//! +//! include_locales!(LOCALES_SAMPLE); +//! ``` +//! +//! But if they are in another directory, then you can use: +//! +//! ``` +//! use pagetop::prelude::*; +//! +//! include_locales!(LOCALES_SAMPLE from "path/to/locale"); +//! ``` + +use crate::html::{Markup, PreEscaped}; +use crate::{global, kv, AutoDefault}; + +pub use fluent_templates; +pub use unic_langid::{CharacterDirection, LanguageIdentifier}; + +use fluent_templates::Loader; +use fluent_templates::StaticLoader as Locales; + +use unic_langid::langid; + +use std::collections::HashMap; +use std::sync::LazyLock; + +use std::fmt; + +/// A mapping between language codes (e.g., "en-US") and their corresponding [`LanguageIdentifier`] +/// and locale key names. +static LANGUAGES: LazyLock> = LazyLock::new(|| { + kv![ + "en" => ( langid!("en-US"), "english" ), + "en-GB" => ( langid!("en-GB"), "english_british" ), + "en-US" => ( langid!("en-US"), "english_united_states" ), + "es" => ( langid!("es-ES"), "spanish" ), + "es-ES" => ( langid!("es-ES"), "spanish_spain" ), + ] +}); + +pub static FALLBACK_LANGID: LazyLock = LazyLock::new(|| langid!("en-US")); + +/// Sets the application's default +/// [Unicode Language Identifier](https://unicode.org/reports/tr35/tr35.html#Unicode_language_identifier) +/// through `SETTINGS.app.language`. +pub static DEFAULT_LANGID: LazyLock<&LanguageIdentifier> = + LazyLock::new(|| langid_for(&global::SETTINGS.app.language).unwrap_or(&FALLBACK_LANGID)); + +pub enum LangError { + EmptyLang, + UnknownLang(String), +} + +impl fmt::Display for LangError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + LangError::EmptyLang => write!(f, "The language identifier is empty."), + LangError::UnknownLang(lang) => write!(f, "Unknown language identifier: {lang}"), + } + } +} + +pub fn langid_for(language: impl Into) -> Result<&'static LanguageIdentifier, LangError> { + let language = language.into(); + if language.is_empty() { + return Err(LangError::EmptyLang); + } + // Attempt to match the full language code (e.g., "es-MX"). + if let Some(langid) = LANGUAGES.get(&language).map(|(langid, _)| langid) { + return Ok(langid); + } + // Fallback to the base language if no sublocale is found (e.g., "es"). + if let Some((base_lang, _)) = language.split_once('-') { + if let Some(langid) = LANGUAGES.get(base_lang).map(|(langid, _)| langid) { + return Ok(langid); + } + } + Err(LangError::UnknownLang(language)) +} + +#[macro_export] +/// Defines a set of localization elements and local translation texts, removing Unicode isolating +/// marks around arguments to improve readability and compatibility in certain rendering contexts. +macro_rules! include_locales { + ( $LOCALES:ident $(, $core_locales:literal)? ) => { + $crate::locale::fluent_templates::static_loader! { + static $LOCALES = { + locales: "src/locale", + $( core_locales: $core_locales, )? + fallback_language: "en-US", + // Removes unicode isolating marks around arguments. + customise: |bundle| bundle.set_use_isolating(false), + }; + } + }; + ( $LOCALES:ident from $dir_locales:literal $(, $core_locales:literal)? ) => { + $crate::locale::fluent_templates::static_loader! { + static $LOCALES = { + locales: $dir_locales, + $( core_locales: $core_locales, )? + fallback_language: "en-US", + // Removes unicode isolating marks around arguments. + customise: |bundle| bundle.set_use_isolating(false), + }; + } + }; +} + +include_locales!(LOCALES_PAGETOP); + +#[derive(AutoDefault)] +enum L10nOp { + #[default] + None, + Text(String), + Translate(String), +} + +#[derive(AutoDefault)] +pub struct L10n { + op: L10nOp, + #[default(&LOCALES_PAGETOP)] + locales: &'static Locales, + args: HashMap, +} + +impl L10n { + pub fn n(text: impl Into) -> Self { + L10n { + op: L10nOp::Text(text.into()), + ..Default::default() + } + } + + pub fn l(key: impl Into) -> Self { + L10n { + op: L10nOp::Translate(key.into()), + ..Default::default() + } + } + + pub fn t(key: impl Into, locales: &'static Locales) -> Self { + L10n { + op: L10nOp::Translate(key.into()), + locales, + ..Default::default() + } + } + + pub fn with_arg(mut self, arg: impl Into, value: impl Into) -> Self { + self.args.insert(arg.into(), value.into()); + self + } + + pub fn with_args(mut self, args: HashMap) -> Self { + for (k, v) in args { + self.args.insert(k, v); + } + self + } + + pub fn get(&self) -> Option { + self.using(&DEFAULT_LANGID) + } + + pub fn using(&self, langid: &LanguageIdentifier) -> Option { + match &self.op { + L10nOp::None => None, + L10nOp::Text(text) => Some(text.to_owned()), + L10nOp::Translate(key) => { + if self.args.is_empty() { + self.locales.try_lookup(langid, key) + } else { + self.locales.try_lookup_with_args( + langid, + key, + &self.args.iter().fold(HashMap::new(), |mut args, (k, v)| { + args.insert(k.to_string(), v.to_owned().into()); + args + }), + ) + } + } + } + } + + /// Escapes translated text using the default language identifier. + pub fn markup(&self) -> Markup { + PreEscaped(self.get().unwrap_or_default()) + } + + /// Escapes translated text using the specified language identifier. + pub fn escaped(&self, langid: &LanguageIdentifier) -> Markup { + PreEscaped(self.using(langid).unwrap_or_default()) + } +} + +impl fmt::Display for L10n { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let content = match &self.op { + L10nOp::None => "".to_string(), + L10nOp::Text(text) => text.clone(), + L10nOp::Translate(key) => self.get().unwrap_or_else(|| format!("No <{}>", key)), + }; + write!(f, "{content}") + } +} diff --git a/pagetop/src/locale/en-US/languages.ftl b/pagetop/src/locale/en-US/languages.ftl new file mode 100644 index 00000000..1e816605 --- /dev/null +++ b/pagetop/src/locale/en-US/languages.ftl @@ -0,0 +1,5 @@ +english = English +english_british = English (British) +english_united_states = English (United States) +spanish = Spanish +spanish_spain = Spanish (Spain) diff --git a/pagetop/src/locale/en-US/theme.ftl b/pagetop/src/locale/en-US/theme.ftl new file mode 100644 index 00000000..fd7f228d --- /dev/null +++ b/pagetop/src/locale/en-US/theme.ftl @@ -0,0 +1 @@ +content = Content diff --git a/pagetop/src/locale/en-US/welcome.ftl b/pagetop/src/locale/en-US/welcome.ftl new file mode 100644 index 00000000..fe45c852 --- /dev/null +++ b/pagetop/src/locale/en-US/welcome.ftl @@ -0,0 +1,23 @@ +welcome_package_name = Default homepage +welcome_package_description = Displays a landing page when none is configured. + +welcome_title = Hello world! + +welcome_intro = Verifying the installation of { $app }. +welcome_powered = A web solution powered by { $pagetop }. + +welcome_page = Welcome Page +welcome_subtitle = Are you a { $app } user? +welcome_text1 = If you don't know what this page is about, this probably means that the site is either experiencing problems or is undergoing routine maintenance. +welcome_text2 = If the issue persists, please contact your system administrator for assistance. + +welcome_pagetop_title = About PageTop +welcome_pagetop_text1 = If you can read this page, it means that the PageTop server is working properly, but has not yet been configured. +welcome_pagetop_text2 = PageTop is a
Rust-based web development framework designed to create modular, extensible, and configurable web solutions. +welcome_pagetop_text3 = For detailed information, please visit the official technical documentation. + +welcome_issues_title = Reporting Issues +welcome_issues_text1 = To report any issues with PageTop, please use GitHub. However, check the existing error reports to avoid duplicates. +welcome_issues_text2 = For issues specific to { $app }, please refer to its official repository or support channel, rather than directly to PageTop. + +welcome_have_fun = Coding is creating diff --git a/pagetop/src/locale/es-ES/languages.ftl b/pagetop/src/locale/es-ES/languages.ftl new file mode 100644 index 00000000..ee74ec26 --- /dev/null +++ b/pagetop/src/locale/es-ES/languages.ftl @@ -0,0 +1,5 @@ +english = Inglés +english_british = Inglés (Gran Bretaña) +english_united_states = Inglés (Estados Unidos) +spanish = Español +spanish_spain = Español (España) diff --git a/pagetop/src/locale/es-ES/theme.ftl b/pagetop/src/locale/es-ES/theme.ftl new file mode 100644 index 00000000..c2026c6f --- /dev/null +++ b/pagetop/src/locale/es-ES/theme.ftl @@ -0,0 +1 @@ +content = Contenido diff --git a/pagetop/src/locale/es-ES/welcome.ftl b/pagetop/src/locale/es-ES/welcome.ftl new file mode 100644 index 00000000..82dad6f0 --- /dev/null +++ b/pagetop/src/locale/es-ES/welcome.ftl @@ -0,0 +1,23 @@ +welcome_package_name = Página de inicio predeterminada +welcome_package_description = Muestra una página de inicio predeterminada cuando no hay ninguna configurada. + +welcome_title = ¡Hola mundo! + +welcome_intro = Verificando la instalación de { $app }. +welcome_powered = Una solución web creada con { $pagetop }. + +welcome_page = Página de Bienvenida +welcome_subtitle = ¿Eres usuario de { $app }? +welcome_text1 = Si no sabes por qué se muestra esta página probablemente significa que el sitio está experimentando problemas o está pasando por un mantenimiento de rutina. +welcome_text2 = Si el problema persiste, por favor póngase en contacto con el administrador del sistema. + +welcome_pagetop_title = Sobre PageTop +welcome_pagetop_text1 = Si puedes leer esta página significa que el servidor PageTop funciona correctamente, pero aún no se ha configurado. +welcome_pagetop_text2 = PageTop es un entorno de desarrollo web basado en Rust, diseñado para crear soluciones web modulares, extensibles y configurables. +welcome_pagetop_text3 = Para más información visita la documentación técnica oficial. + +welcome_issues_title = Informando Problemas +welcome_issues_text1 = Para comunicar cualquier problema con PageTop utiliza GitHub. No obstante, comprueba los informes de errores ya existentes para evitar duplicados. +welcome_issues_text2 = Si son fallos específicos de { $app }, por favor acude a su repositorio oficial o canal de soporte, y no al de PageTop directamente. + +welcome_have_fun = Programar es crear diff --git a/pagetop/src/prelude.rs b/pagetop/src/prelude.rs new file mode 100644 index 00000000..9d0d465f --- /dev/null +++ b/pagetop/src/prelude.rs @@ -0,0 +1,52 @@ +//! The `PageTop` Prelude. + +// RE-EXPORTED. + +pub use crate::{concat_string, fn_builder, html, main, paste, test}; + +pub use crate::{AutoDefault, ComponentClasses, StaticResources, TypeId, Weight}; + +// MACROS. + +// crate::util +pub use crate::kv; +// crate::config +pub use crate::include_config; +// crate::locale +pub use crate::include_locales; +// crate::service +pub use crate::{include_files, include_files_service}; +// crate::core::action +pub use crate::actions; + +// API. + +pub use crate::util; + +pub use crate::global; + +pub use crate::trace; + +pub use crate::html::*; + +pub use crate::locale::*; + +pub use crate::datetime::*; + +pub use crate::service; +pub use crate::service::{HttpMessage, HttpRequest}; + +pub use crate::core::{AnyBase, AnyTo}; + +pub use crate::core::action::*; +pub use crate::core::component::*; +pub use crate::core::package::*; +pub use crate::core::theme::*; + +pub use crate::response::{json::*, page::*, redirect::*, ResponseError}; + +pub use crate::base::action; +pub use crate::base::component::*; +pub use crate::base::theme; + +pub use crate::app::Application; diff --git a/pagetop/src/response.rs b/pagetop/src/response.rs new file mode 100644 index 00000000..e51974b1 --- /dev/null +++ b/pagetop/src/response.rs @@ -0,0 +1,9 @@ +//! Web request response variants. + +pub use actix_web::ResponseError; + +pub mod page; + +pub mod json; + +pub mod redirect; diff --git a/pagetop/src/response/json.rs b/pagetop/src/response/json.rs new file mode 100644 index 00000000..684d2c7b --- /dev/null +++ b/pagetop/src/response/json.rs @@ -0,0 +1 @@ +pub use actix_web::web::Json; diff --git a/pagetop/src/response/page.rs b/pagetop/src/response/page.rs new file mode 100644 index 00000000..340bd0c7 --- /dev/null +++ b/pagetop/src/response/page.rs @@ -0,0 +1,192 @@ +mod error; +pub use error::ErrorPage; + +pub use actix_web::Result as ResultPage; + +use crate::base::action; +use crate::core::component::{AssetsOp, Context}; +use crate::core::component::{ChildComponent, ChildOp, ComponentTrait}; +use crate::fn_builder; +use crate::html::{html, Markup, DOCTYPE}; +use crate::html::{ClassesOp, OptionClasses, OptionId, OptionTranslated}; +use crate::locale::L10n; +use crate::service::HttpRequest; + +use unic_langid::CharacterDirection; + +#[rustfmt::skip] +pub struct Page { + title : OptionTranslated, + description : OptionTranslated, + metadata : Vec<(&'static str, &'static str)>, + properties : Vec<(&'static str, &'static str)>, + context : Context, + body_id : OptionId, + body_classes: OptionClasses, + body_skip_to: OptionId, +} + +impl Page { + #[rustfmt::skip] + pub fn new(request: HttpRequest) -> Self { + Page { + title : OptionTranslated::default(), + description : OptionTranslated::default(), + metadata : Vec::default(), + properties : Vec::default(), + context : Context::new(request), + body_id : OptionId::default(), + body_classes: OptionClasses::default(), + body_skip_to: OptionId::default(), + } + } + + // Page BUILDER. + + #[fn_builder] + pub fn set_title(&mut self, title: L10n) -> &mut Self { + self.title.set_value(title); + self + } + + #[fn_builder] + pub fn set_description(&mut self, description: L10n) -> &mut Self { + self.description.set_value(description); + self + } + + #[fn_builder] + pub fn set_metadata(&mut self, name: &'static str, content: &'static str) -> &mut Self { + self.metadata.push((name, content)); + self + } + + #[fn_builder] + pub fn set_property(&mut self, property: &'static str, content: &'static str) -> &mut Self { + self.metadata.push((property, content)); + self + } + + #[fn_builder] + pub fn set_assets(&mut self, op: AssetsOp) -> &mut Self { + self.context.set_assets(op); + self + } + + #[fn_builder] + pub fn set_body_id(&mut self, id: impl Into) -> &mut Self { + self.body_id.set_value(id); + self + } + + #[fn_builder] + pub fn set_body_classes(&mut self, op: ClassesOp, classes: impl Into) -> &mut Self { + self.body_classes.set_value(op, classes); + self + } + + #[fn_builder] + pub fn set_body_skip_to(&mut self, id: impl Into) -> &mut Self { + self.body_skip_to.set_value(id); + self + } + + #[fn_builder] + pub fn set_layout(&mut self, layout: &'static str) -> &mut Self { + self.context.set_assets(AssetsOp::Layout(layout)); + self + } + + #[fn_builder] + pub fn set_in_region(&mut self, region: &'static str, op: ChildOp) -> &mut Self { + self.context.set_in_region(region, op); + self + } + + pub fn with_component(mut self, component: impl ComponentTrait) -> Self { + self.context + .set_in_region("content", ChildOp::Add(ChildComponent::with(component))); + self + } + + pub fn with_component_in( + mut self, + region: &'static str, + component: impl ComponentTrait, + ) -> Self { + self.context + .set_in_region(region, ChildOp::Add(ChildComponent::with(component))); + self + } + + // Page GETTERS. + + pub fn title(&mut self) -> Option { + self.title.using(self.context.langid()) + } + + pub fn description(&mut self) -> Option { + self.description.using(self.context.langid()) + } + + pub fn metadata(&self) -> &Vec<(&str, &str)> { + &self.metadata + } + + pub fn properties(&self) -> &Vec<(&str, &str)> { + &self.properties + } + + pub fn context(&mut self) -> &mut Context { + &mut self.context + } + + pub fn body_id(&self) -> &OptionId { + &self.body_id + } + + pub fn body_classes(&self) -> &OptionClasses { + &self.body_classes + } + + pub fn body_skip_to(&self) -> &OptionId { + &self.body_skip_to + } + + // Page RENDER. + + pub fn render(&mut self) -> ResultPage { + // Theme-specific operations before rendering the page body. + self.context.theme().before_render_body(self); + + // Execute package actions before rendering the page body. + action::page::BeforeRenderBody::dispatch(self); + + // Render the page body. + let body = self.context.theme().render_body(self); + + // Theme-specific operations after rendering the page body. + self.context.theme().after_render_body(self); + + // Execute package actions after rendering the page body. + action::page::AfterRenderBody::dispatch(self); + + // Render the page head. + let head = self.context.theme().render_head(self); + + // Render the full page with language and direction attributes. + let lang = &self.context.langid().language; + let dir = match self.context.langid().character_direction() { + CharacterDirection::LTR => "ltr", + CharacterDirection::RTL => "rtl", + CharacterDirection::TTB => "auto", + }; + Ok(html! { + (DOCTYPE) + html lang=(lang) dir=(dir) { + (head) + (body) + } + }) + } +} diff --git a/pagetop/src/response/page/error.rs b/pagetop/src/response/page/error.rs new file mode 100644 index 00000000..8493d58f --- /dev/null +++ b/pagetop/src/response/page/error.rs @@ -0,0 +1,86 @@ +use crate::base::component::{Error403, Error404}; +use crate::locale::L10n; +use crate::response::ResponseError; +use crate::service::http::{header::ContentType, StatusCode}; +use crate::service::{HttpRequest, HttpResponse}; + +use super::Page; + +use std::fmt; + +#[derive(Debug)] +pub enum ErrorPage { + NotModified(HttpRequest), + BadRequest(HttpRequest), + AccessDenied(HttpRequest), + NotFound(HttpRequest), + PreconditionFailed(HttpRequest), + InternalError(HttpRequest), + Timeout(HttpRequest), +} + +impl fmt::Display for ErrorPage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + // Error 304. + ErrorPage::NotModified(_) => write!(f, "Not Modified"), + // Error 400. + ErrorPage::BadRequest(_) => write!(f, "Bad Client Data"), + // Error 403. + ErrorPage::AccessDenied(request) => { + let error_page = Page::new(request.clone()); + if let Ok(page) = error_page + .with_title(L10n::n("Error FORBIDDEN")) + .with_layout("error") + .with_component(Error403) + .render() + { + write!(f, "{}", page.into_string()) + } else { + write!(f, "Access Denied") + } + } + // Error 404. + ErrorPage::NotFound(request) => { + let error_page = Page::new(request.clone()); + if let Ok(page) = error_page + .with_title(L10n::n("Error RESOURCE NOT FOUND")) + .with_layout("error") + .with_component(Error404) + .render() + { + write!(f, "{}", page.into_string()) + } else { + write!(f, "Not Found") + } + } + // Error 412. + ErrorPage::PreconditionFailed(_) => write!(f, "Precondition Failed"), + // Error 500. + ErrorPage::InternalError(_) => write!(f, "Internal Error"), + // Error 504. + ErrorPage::Timeout(_) => write!(f, "Timeout"), + } + } +} + +impl ResponseError for ErrorPage { + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()) + .insert_header(ContentType::html()) + .body(self.to_string()) + } + + #[rustfmt::skip] + fn status_code(&self) -> StatusCode { + match self { + ErrorPage::NotModified(_) => StatusCode::NOT_MODIFIED, + ErrorPage::BadRequest(_) => StatusCode::BAD_REQUEST, + ErrorPage::AccessDenied(_) => StatusCode::FORBIDDEN, + ErrorPage::NotFound(_) => StatusCode::NOT_FOUND, + ErrorPage::PreconditionFailed(_) => StatusCode::PRECONDITION_FAILED, + ErrorPage::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, + ErrorPage::Timeout(_) => StatusCode::GATEWAY_TIMEOUT, + } + } +} diff --git a/pagetop/src/response/redirect.rs b/pagetop/src/response/redirect.rs new file mode 100644 index 00000000..3b6f0646 --- /dev/null +++ b/pagetop/src/response/redirect.rs @@ -0,0 +1,76 @@ +//! Perform redirections in HTTP. +//! +//! **URL redirection**, also known as *URL forwarding*, is a technique to give more than one URL +//! address to a web resource. HTTP has a response called ***HTTP redirect*** for this operation +//! (see [Redirections in HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections)). +//! +//! There are several types of redirects, sorted into three categories: +//! +//! * **Permanent redirections**. These redirections are meant to last forever. They imply that +//! the original URL should no longer be used, and replaced with the new one. Search engine +//! robots, RSS readers, and other crawlers will update the original URL for the resource. +//! +//! * **Temporary redirections**. Sometimes the requested resource can't be accessed from its +//! canonical location, but it can be accessed from another place. In this case, a temporary +//! redirect can be used. Search engine robots and other crawlers don't memorize the new, +//! temporary URL. Temporary redirections are also used when creating, updating, or deleting +//! resources, to show temporary progress pages. +//! +//! * **Special redirections**. + +use crate::service::HttpResponse; + +pub struct Redirect; + +impl Redirect { + /// Permanent redirection. Status Code **301**. GET methods unchanged. Others may or may not be + /// changed to GET. Typical for reorganization of a website. + pub fn moved(redirect_to_url: &str) -> HttpResponse { + HttpResponse::MovedPermanently() + .append_header(("Location", redirect_to_url)) + .finish() + } + + /// Permanent redirection. Status Code **308**. Method and body not changed. Typical for + /// reorganization of a website, with non-GET links/operations. + pub fn permanent(redirect_to_url: &str) -> HttpResponse { + HttpResponse::PermanentRedirect() + .append_header(("Location", redirect_to_url)) + .finish() + } + + /// Temporary redirection. Status Code **302**. GET methods unchanged. Others may or may not be + /// changed to GET. Used when the web page is temporarily unavailable for unforeseen reasons. + pub fn found(redirect_to_url: &str) -> HttpResponse { + HttpResponse::Found() + .append_header(("Location", redirect_to_url)) + .finish() + } + + /// Temporary redirection. Status Code **303**. GET methods unchanged. Others changed to GET + /// (body lost). Used to redirect after a PUT or a POST, so that refreshing the result page + /// doesn't re-trigger the operation. + pub fn see_other(redirect_to_url: &str) -> HttpResponse { + HttpResponse::SeeOther() + .append_header(("Location", redirect_to_url)) + .finish() + } + + /// Temporary redirection. Status Code **307**. Method and body not changed. The web page is + /// temporarily unavailable for unforeseen reasons. Better than [`found()`](Self::found) when + /// non-GET operations are available on the site. + pub fn temporary(redirect_to_url: &str) -> HttpResponse { + HttpResponse::TemporaryRedirect() + .append_header(("Location", redirect_to_url)) + .finish() + } + + /// Special redirection. Status Code **304**. Redirects a page to the locally cached copy (that + /// was stale). Sent for revalidated conditional requests. Indicates that the cached response is + /// still fresh and can be used. + pub fn not_modified(redirect_to_url: &str) -> HttpResponse { + HttpResponse::NotModified() + .append_header(("Location", redirect_to_url)) + .finish() + } +} diff --git a/pagetop/src/service.rs b/pagetop/src/service.rs new file mode 100644 index 00000000..01d82d56 --- /dev/null +++ b/pagetop/src/service.rs @@ -0,0 +1,64 @@ +//! Essential web framework ([actix-web](https://docs.rs/actix-web)). + +pub use actix_session::Session; +pub use actix_web::body::BoxBody; +pub use actix_web::dev::Server; +pub use actix_web::dev::ServiceFactory as Factory; +pub use actix_web::dev::ServiceRequest as Request; +pub use actix_web::dev::ServiceResponse as Response; +pub use actix_web::{cookie, get, http, rt, test, web}; +pub use actix_web::{App, Error, HttpMessage, HttpRequest, HttpResponse, HttpServer, Responder}; + +pub use actix_web_files::Files as ActixFiles; +pub use actix_web_static_files::ResourceFiles; + +#[macro_export] +macro_rules! include_files { + ( $bundle:ident ) => { + $crate::paste! { + mod [] { + include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs")); + } + } + }; + ( $STATIC:ident => $bundle:ident ) => { + $crate::paste! { + mod [] { + include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs")); + } + pub static $STATIC: std::sync::LazyLock = std::sync::LazyLock::new( + []::$bundle + ); + } + }; +} + +#[macro_export] +macro_rules! include_files_service { + ( $scfg:ident, $bundle:ident => $path:expr $(, [$root:expr, $relative:expr])? ) => {{ + $crate::paste! { + let span = $crate::trace::debug_span!("Configuring static files ", path = $path); + let _ = span.in_scope(|| { + #[allow(unused_mut)] + let mut serve_embedded:bool = true; + $( + if !$root.is_empty() && !$relative.is_empty() { + if let Ok(absolute) = $crate::util::absolute_dir($root, $relative) { + $scfg.service($crate::service::ActixFiles::new( + $path, + absolute, + ).show_files_listing()); + serve_embedded = false + } + } + )? + if serve_embedded { + $scfg.service($crate::service::ResourceFiles::new( + $path, + []::$bundle(), + )); + } + }); + } + }}; +} diff --git a/pagetop/src/trace.rs b/pagetop/src/trace.rs new file mode 100644 index 00000000..6e42d790 --- /dev/null +++ b/pagetop/src/trace.rs @@ -0,0 +1,84 @@ +//! Application tracing and event logging. +//! +//! `PageTop` collects application diagnostic information in a structured and event-based manner. +//! +//! In asynchronous systems, interpreting traditional log messages often becomes complicated. +//! Individual tasks are multiplexed to the same thread, and associated events and log messages get +//! intermingled, making it difficult to follow the logical sequence. +//! +//! `PageTop` uses [`tracing`](https://docs.rs/tracing) to allow **applications** and **modules** to +//! log structured events with added information about *temporality* and *causality*. Unlike a log +//! message, a span has a start and end time, can enter and exit the execution flow, and can exist +//! within a nested tree of similar spans. Additionally, these spans are *structured*, with the +//! ability to record data types and text messages. + +use crate::global; + +pub use tracing::{debug, error, info, trace, warn}; +pub use tracing::{debug_span, error_span, info_span, trace_span, warn_span}; + +use tracing_appender::non_blocking::WorkerGuard; +use tracing_subscriber::EnvFilter; + +use std::sync::LazyLock; + +/// Application tracing and event logging. +/// +/// To increase performance, a dedicated thread uses a non-blocking writer system that acts +/// periodically instead of sending each trace or event instantly. If the program terminates +/// abruptly (e.g., due to a panic! or a `std::process::exit`), some traces or events might not be +/// sent. +/// +/// Since traces or events logged shortly before an application crash are often important for +/// diagnosing the cause of the failure, `Lazy` ensures that all stored logs are sent +/// before terminating execution. + +#[rustfmt::skip] +pub(crate) static TRACING: LazyLock = LazyLock::new(|| { + let env_filter = EnvFilter::try_new(&global::SETTINGS.log.tracing) + .unwrap_or_else(|_| EnvFilter::new("Info")); + + let rolling = global::SETTINGS.log.rolling.to_lowercase(); + + let (non_blocking, guard) = match rolling.as_str() { + "stdout" => tracing_appender::non_blocking(std::io::stdout()), + _ => tracing_appender::non_blocking({ + let path = &global::SETTINGS.log.path; + let prefix = &global::SETTINGS.log.prefix; + match rolling.as_str() { + "daily" => tracing_appender::rolling::daily(path, prefix), + "hourly" => tracing_appender::rolling::hourly(path, prefix), + "minutely" => tracing_appender::rolling::minutely(path, prefix), + "endless" => tracing_appender::rolling::never(path, prefix), + _ => { + println!( + "Rolling value \"{}\" not valid. Using \"daily\". Check the settings file.", + global::SETTINGS.log.rolling, + ); + tracing_appender::rolling::daily(path, prefix) + } + } + }), + }; + + let subscriber = tracing_subscriber::fmt() + .with_env_filter(env_filter) + .with_writer(non_blocking) + .with_ansi(rolling.as_str() == "stdout"); + + match global::SETTINGS.log.format.to_lowercase().as_str() { + "json" => subscriber.json().init(), + "full" => subscriber.init(), + "compact" => subscriber.compact().init(), + "pretty" => subscriber.pretty().init(), + _ => { + println!( + "Tracing format \"{}\" not valid. Using \"Full\". Check the settings file.", + global::SETTINGS.log.format, + ); + subscriber.init(); + } + } + + guard +}); diff --git a/pagetop/src/util.rs b/pagetop/src/util.rs new file mode 100644 index 00000000..a34ee599 --- /dev/null +++ b/pagetop/src/util.rs @@ -0,0 +1,161 @@ +//! Useful functions and macros. + +use crate::trace; + +use std::io; +use std::path::PathBuf; + +// USEFUL FUNCTIONS ******************************************************************************** + +pub enum TypeInfo { + FullName, + ShortName, + NameFrom(isize), + NameTo(isize), + PartialName(isize, isize), +} + +impl TypeInfo { + pub fn of(&self) -> &'static str { + let type_name = std::any::type_name::(); + match self { + TypeInfo::FullName => type_name, + TypeInfo::ShortName => Self::partial(type_name, -1, None), + TypeInfo::NameFrom(start) => Self::partial(type_name, *start, None), + TypeInfo::NameTo(end) => Self::partial(type_name, 0, Some(*end)), + TypeInfo::PartialName(start, end) => Self::partial(type_name, *start, Some(*end)), + } + } + + fn partial(type_name: &'static str, start: isize, end: Option) -> &'static str { + let maxlen = type_name.len(); + let mut segments = Vec::new(); + let mut segment_start = 0; // Start position of the current segment. + let mut angle_brackets = 0; // Counter for tracking '<' and '>'. + let mut previous_char = '\0'; // Initializes to a null character, no previous character. + + for (idx, c) in type_name.char_indices() { + match c { + ':' if angle_brackets == 0 => { + if previous_char == ':' { + if segment_start < idx - 1 { + segments.push((segment_start, idx - 1)); // Do not include last '::'. + } + segment_start = idx + 1; // Next segment starts after '::'. + } + } + '<' => angle_brackets += 1, + '>' => angle_brackets -= 1, + _ => {} + } + previous_char = c; + } + + // Include the last segment if there's any. + if segment_start < maxlen { + segments.push((segment_start, maxlen)); + } + + // Calculates the start position. + let start_pos = segments + .get(if start >= 0 { + start as usize + } else { + segments.len() - start.unsigned_abs() + }) + .map_or(0, |&(s, _)| s); + + // Calculates the end position. + let end_pos = segments + .get(if let Some(end) = end { + if end >= 0 { + end as usize + } else { + segments.len() - end.unsigned_abs() + } + } else { + segments.len() - 1 + }) + .map_or(maxlen, |&(_, e)| e); + + // Returns the partial string based on the calculated positions. + &type_name[start_pos..end_pos] + } +} + +/// Calculates the absolute directory given a root path and a relative path. +/// +/// # Arguments +/// +/// * `root_path` - A string slice that holds the root path. +/// * `relative_path` - A string slice that holds the relative path. +/// +/// # Returns +/// +/// * `Ok` - If the operation is successful, returns the absolute directory as a `String`. +/// * `Err` - If an I/O error occurs, returns an `io::Error`. +/// +/// # Errors +/// +/// This function will return an error if: +/// - The root path or relative path are invalid. +/// - There is an issue with file system operations, such as reading the directory. +/// +/// # Examples +/// +/// ``` +/// let root = "/home/user"; +/// let relative = "documents"; +/// let abs_dir = absolute_dir(root, relative).unwrap(); +/// println!("{}", abs_dir); +/// ``` +pub fn absolute_dir( + root_path: impl Into, + relative_path: impl Into, +) -> Result { + let root_path = PathBuf::from(root_path.into()); + let full_path = root_path.join(relative_path.into()); + let absolute_dir = full_path.to_string_lossy().into(); + + if !full_path.is_absolute() { + let message = format!("Path \"{absolute_dir}\" is not absolute"); + trace::warn!(message); + return Err(io::Error::new(io::ErrorKind::InvalidInput, message)); + } + + if !full_path.exists() { + let message = format!("Path \"{absolute_dir}\" does not exist"); + trace::warn!(message); + return Err(io::Error::new(io::ErrorKind::NotFound, message)); + } + + if !full_path.is_dir() { + let message = format!("Path \"{absolute_dir}\" is not a directory"); + trace::warn!(message); + return Err(io::Error::new(io::ErrorKind::InvalidInput, message)); + } + + Ok(absolute_dir) +} + +// USEFUL MACROS *********************************************************************************** + +#[macro_export] +/// Macro para construir grupos de pares clave-valor. +/// +/// ```rust#ignore +/// let args = kv![ +/// "userName" => "Roberto", +/// "photoCount" => 3, +/// "userGender" => "male", +/// ]; +/// ``` +macro_rules! kv { + ( $($key:expr => $value:expr),* $(,)? ) => {{ + let mut a = std::collections::HashMap::new(); + $( + a.insert($key.into(), $value.into()); + )* + a + }}; +} diff --git a/static/banner.png b/static/banner.png new file mode 100644 index 00000000..3ad51741 Binary files /dev/null and b/static/banner.png differ diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 00000000..95e1affa Binary files /dev/null and b/static/favicon.ico differ