From 4939d5a58f41ee339791677fa7a9472d58917082 Mon Sep 17 00:00:00 2001 From: Borodinov Ilya Date: Sun, 8 Dec 2024 23:53:36 +0300 Subject: [PATCH] tailwind yay --- .gitignore | 1 + Cargo.lock | 259 ++++++++- Cargo.toml | 4 +- bun.lockb | Bin 0 -> 23334 bytes package.json | 11 + src/app.css | 101 ++++ src/main.rs | 14 +- tailwind.config.ts | 125 +++++ vespid/Cargo.toml | 7 + vespid/macros/src/lib.rs | 8 +- vespid/src/{axum_compat.rs => axum.rs} | 0 vespid/src/icons.rs | 28 + vespid/src/lib.rs | 17 +- vespid/src/text.rs | 748 +++++++++++++++++++++++++ watch.sh | 3 + 15 files changed, 1314 insertions(+), 12 deletions(-) create mode 100755 bun.lockb create mode 100644 package.json create mode 100644 src/app.css create mode 100644 tailwind.config.ts rename vespid/src/{axum_compat.rs => axum.rs} (100%) create mode 100644 vespid/src/icons.rs create mode 100644 vespid/src/text.rs create mode 100755 watch.sh diff --git a/.gitignore b/.gitignore index 47e996a..3923422 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target .devenv .direnv +/node_modules diff --git a/Cargo.lock b/Cargo.lock index d34b4a6..1cddc00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "aho-corasick" version = "1.1.3" @@ -26,6 +32,37 @@ 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 = "async-compression" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522" +dependencies = [ + "brotli", + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "zstd", + "zstd-safe", +] + [[package]] name = "async-trait" version = "0.1.83" @@ -108,17 +145,44 @@ dependencies = [ "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.7.4", "object", "rustc-demangle", ] +[[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 = "brotli" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +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 = "bytes" version = "1.9.0" @@ -178,6 +242,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + [[package]] name = "crusto" version = "0.1.0" @@ -186,7 +259,9 @@ dependencies = [ "color-eyre", "eyre", "git2", + "icondata", "tokio", + "tower-http", "tracing", "tracing-error", "tracing-subscriber", @@ -225,6 +300,16 @@ dependencies = [ "once_cell", ] +[[package]] +name = "flate2" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +dependencies = [ + "crc32fast", + "miniz_oxide 0.8.0", +] + [[package]] name = "fnv" version = "1.0.7" @@ -292,6 +377,17 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "gimli" version = "0.28.1" @@ -362,6 +458,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + [[package]] name = "httparse" version = "1.9.5" @@ -409,6 +511,31 @@ dependencies = [ "tower-service", ] +[[package]] +name = "icondata" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "805a2a031c06a768407b774240192b0ad82ddb94554bb5b52053453f2f6b0bf1" +dependencies = [ + "icondata_core", + "icondata_lu", +] + +[[package]] +name = "icondata_core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c97be924215abd5e630d84e95a47c710138a6559b4c55039f4f33aa897fa859" + +[[package]] +name = "icondata_lu" +version = "0.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d552c45cc3ab1d1bf88cc0201004eb92418141e5454e9e0e46c4b4a4faf66248" +dependencies = [ + "icondata_core", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -554,6 +681,16 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" +[[package]] +name = "iri-string" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0f0a572e8ffe56e2ff4f769f32ffe919282c3916799f8b68688b6030063bea" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "itoa" version = "1.0.14" @@ -670,6 +807,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.7.4" @@ -679,6 +826,15 @@ dependencies = [ "adler", ] +[[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" @@ -933,7 +1089,7 @@ dependencies = [ "quote", "syn", "syn_derive", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -1121,7 +1277,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47" +dependencies = [ + "thiserror-impl 2.0.6", ] [[package]] @@ -1135,6 +1300,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.8" @@ -1215,6 +1391,37 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" +dependencies = [ + "async-compression", + "base64", + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "iri-string", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower", + "tower-layer", + "tower-service", + "tracing", + "uuid", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -1319,6 +1526,12 @@ dependencies = [ "syn", ] +[[package]] +name = "unicase" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" + [[package]] name = "unicode-ident" version = "1.0.14" @@ -1360,6 +1573,15 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "getrandom", +] + [[package]] name = "valuable" version = "0.1.0" @@ -1384,6 +1606,9 @@ version = "0.1.0" dependencies = [ "axum", "html-escape", + "icondata_core", + "serde", + "thiserror 2.0.6", "tokio", "tokio-util", "typed-builder", @@ -1589,3 +1814,31 @@ dependencies = [ "quote", "syn", ] + +[[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 3b1cffe..51052e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ version = "0.1.0" edition = "2021" [dependencies] -vespid.workspace = true +vespid = { workspace = true, features = ["icons"] } tokio.workspace = true axum.workspace = true eyre = "0.6.12" @@ -25,3 +25,5 @@ tracing = "0.1.41" tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } tracing-error = "0.2.1" git2 = "0.19.0" +icondata = { version = "0.5", default-features = false, features = ["lucide"] } +tower-http = { version = "0.6.2", features = ["full"] } diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..cd083ad983cd480c0f7cd4a168cb986ab767b192 GIT binary patch literal 23334 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 i3xa(CRq^wd@SW{&li]:mt-2; + } + .ty h1 { + @apply scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl; + } + .ty h2 { + @apply mt-10 scroll-m-20 pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0; + } + .ty h3 { + @apply scroll-m-20 text-2xl font-semibold tracking-tight; + } + .ty h4 { + @apply scroll-m-20 text-xl font-semibold tracking-tight mt-2; + } + .ty p { + @apply leading-7 [&:not(:first-child)]:mt-4; + } + .ty blockquote { + @apply mt-6 border-l-2 pl-6 italic; + } + .ty code { + @apply relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold; + } + .ty hr { + @apply my-6 border-border/60 rounded-lg; + } + .ty a:not([m-button]) { + @apply text-info underline; + } + + .lead { + @apply text-xl text-muted-foreground; + } + .large { + @apply text-lg font-semibold; + } + .small { + @apply text-sm font-medium leading-none; + } + .muted { + @apply text-sm text-muted-foreground; + } + + /* Hide scrollbar for Chrome, Safari and Opera */ + .no-scrollbar::-webkit-scrollbar { + display: none; + } + /* Hide scrollbar for IE, Edge and Firefox */ + .no-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } +} diff --git a/src/main.rs b/src/main.rs index 6c23977..27bb3e8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ #![allow(non_snake_case)] + #[macro_use] extern crate tracing; @@ -7,7 +8,7 @@ use std::sync::{atomic::AtomicU64, Arc}; use axum::{response::Html, routing::get}; use tracing::level_filters::LevelFilter; use tracing_subscriber::layer::SubscriberExt; -use vespid::{axum_compat::render, *}; +use vespid::{axum::render, prelude::*}; #[component] fn Shell(children: String) -> String { @@ -19,6 +20,7 @@ fn Shell(children: String) -> String { Crusto + {children} @@ -34,9 +36,12 @@ async fn index() -> Html {

"Hello to Crusto!"

"Index"

- + -
+
+ +
+
} @@ -59,6 +64,7 @@ async fn main() -> eyre::Result<()> { )?; let amount_of_refreshes = Arc::new(AtomicU64::new(0)); + let app = axum::Router::new().route("/", get(index)).route("/widget", get(|| async move { render(async move { view! { @@ -69,8 +75,10 @@ async fn main() -> eyre::Result<()> { } }).await })); + let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?; info!("listening on {}", listener.local_addr()?); axum::serve(listener, app).await?; + Ok(()) } diff --git a/tailwind.config.ts b/tailwind.config.ts new file mode 100644 index 0000000..e90b4cc --- /dev/null +++ b/tailwind.config.ts @@ -0,0 +1,125 @@ +import catppuccin from '@catppuccin/tailwindcss'; +import type { Config } from 'tailwindcss'; +import { fontFamily } from 'tailwindcss/defaultTheme'; + +export default { + content: [ + './src/**/*.{html,ts,scss,css}', + '../magic/src/**/*.{html,ts,scss,css}', + ], + darkMode: ['selector', ':not(.latte)'], + theme: { + container: { + center: true, + padding: '2rem', + screens: { + '2xl': '1400px', + }, + }, + extend: { + /** + --background: var(--ctp-base); + --foreground: var(--ctp-text); + + --muted: var(--ctp-overlay0); + --muted-foreground: var(--ctp-overlay1); + + --popover: var(--ctp-mantle); + --popover-foreground: var(--ctp-text); + + --border: var(--ctp-surface1); + --input: var(--ctp-surface1); + + --card: var(--ctp-mantle); + --card-foreground: var(--ctp-text); + + --primary: var(--ctp-mauve); + --primary-foreground: var(--ctp-base); + + --secondary: var(--ctp-surface0); + --secondary-foreground: var(--ctp-text); + + --accent: var(--ctp-base); + --accent-foreground: var(--ctp-text); + + --success: var(--ctp-green); + --success-foreground: var(--ctp-crust); + + --info: var(--ctp-blue); + --info-foreground: var(--ctp-crust); + + --warning: var(--ctp-peach); + --warning-foreground: var(--ctp-crust); + + --destructive: var(--ctp-red); + --destructive-foreground: var(--ctp-crust); + + --ring: var(--ctp-text); + */ + colors: { + border: 'rgba(var(--ctp-overlay1), )', + input: 'rgba(var(--ctp-surface2), )', + ring: 'rgba(var(--ctp-text), )', + background: 'rgba(var(--ctp-base), )', + foreground: 'rgba(var(--ctp-text), )', + primary: { + DEFAULT: 'rgba(var(--ctp-lavender), )', + foreground: 'rgba(var(--ctp-base), )', + }, + secondary: { + DEFAULT: 'rgba(var(--ctp-surface0), )', + foreground: 'rgba(var(--ctp-text), )', + }, + destructive: { + DEFAULT: 'rgba(var(--ctp-red), )', + foreground: 'rgba(var(--ctp-crust), )', + }, + warning: { + DEFAULT: 'rgba(var(--ctp-peach), )', + foreground: 'rgba(var(--ctp-crust), )', + }, + info: { + DEFAULT: 'rgba(var(--ctp-blue), )', + foreground: 'rgba(var(--info-foreground), )', + }, + success: { + DEFAULT: 'rgba(var(--ctp-green), )', + foreground: 'rgba(var(--ctp-crust), )', + }, + muted: { + DEFAULT: 'rgba(var(--ctp-overlay0), )', + foreground: 'rgba(var(--ctp-overlay1), )', + }, + invariant: { + DEFAULT: 'rgba(var(--ctp-subtext0), )', + foreground: 'rgba(var(--ctp-mantle), )', + }, + accent: { + DEFAULT: 'rgba(var(--ctp-base), )', + foreground: 'rgba(var(--ctp-text), )', + }, + popover: { + DEFAULT: 'rgba(var(--ctp-mantle), )', + foreground: 'rgba(var(--ctp-text), )', + }, + card: { + DEFAULT: 'rgba(var(--ctp-mantle), )', + foreground: 'rgba(var(--ctp-text), )', + }, + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', + }, + fontFamily: { + sans: ['Inter', ...fontFamily.sans], + }, + }, + }, + plugins: [ + catppuccin({ + defaultFlavour: 'mocha', + }), + ], +} satisfies Config; diff --git a/vespid/Cargo.toml b/vespid/Cargo.toml index a37dba5..e290ca5 100644 --- a/vespid/Cargo.toml +++ b/vespid/Cargo.toml @@ -10,3 +10,10 @@ tokio-util = { version = "0.7.13", features = ["rt"] } axum.workspace = true vespid_macros.path = "macros" typed-builder = "0.20.0" + +icondata_core = { version = "0.1", optional = true } +serde = "1.0.215" +thiserror = "2.0.6" + +[features] +icons = ["dep:icondata_core"] diff --git a/vespid/macros/src/lib.rs b/vespid/macros/src/lib.rs index 37812da..f4507e6 100644 --- a/vespid/macros/src/lib.rs +++ b/vespid/macros/src/lib.rs @@ -2,6 +2,7 @@ use std::collections::HashSet; use proc_macro::TokenStream; use proc_macro2_diagnostics::Diagnostic; +use proc_macro_error2::{abort, OptionExt}; use quote::{quote, quote_spanned, ToTokens}; use rstml::{ node::{KeyedAttribute, Node, NodeAttribute, NodeElement, NodeName}, @@ -287,7 +288,6 @@ fn walk_attribute(attribute: &KeyedAttribute) -> (String, Option { - panic!("receiver arguments unsupported"); + abort!(i, "builders do not support self"); } FnArg::Typed(mut t) => { if t.attrs.is_empty() { @@ -480,7 +480,7 @@ impl ToTokens for ComponentFn { .iter() .map(|i| match i { FnArg::Receiver(_) => { - panic!("receiver arguments unsupported"); + abort!(i, "builders do not support self"); } FnArg::Typed(t) => &t.pat, }) diff --git a/vespid/src/axum_compat.rs b/vespid/src/axum.rs similarity index 100% rename from vespid/src/axum_compat.rs rename to vespid/src/axum.rs diff --git a/vespid/src/icons.rs b/vespid/src/icons.rs new file mode 100644 index 0000000..2aa27f7 --- /dev/null +++ b/vespid/src/icons.rs @@ -0,0 +1,28 @@ +#![allow(non_snake_case)] + +use super::*; +pub use icondata_core::Icon; + +#[vespid_macros::component] +pub fn Icon( + icon: Icon, + #[builder(default, setter(into))] width: MaybeText, + #[builder(default, setter(into))] height: MaybeText, + #[builder(default, setter(into))] class: MaybeText, + #[builder(default, setter(into))] style: MaybeText, +) -> String { + let width = format!("width=\"{}\"", width.get().unwrap_or("1em")); + let height = format!("height=\"{}\"", height.get().unwrap_or("1em")); + let class = class.get().map_or_else(String::new, |class| format!("class=\"{}\"", class)); + let style = style.get().map_or_else(String::new, |style| format!("style=\"{}\"", style)); + let x = icon.x.map_or_else(String::new, |x| format!("x=\"{}\"", x)); + let y = icon.y.map_or_else(String::new, |y| format!("y=\"{}\"", y)); + let viewbox = icon.view_box.map_or_else(String::new, |viewbox| format!("viewBox=\"{}\"", viewbox)); + let stroke_linecap = icon.stroke_linecap.map_or_else(String::new, |stroke_linecap| format!("stroke-linecap=\"{}\"", stroke_linecap)); + let stroke_linejoin = icon.stroke_linejoin.map_or_else(String::new, |stroke_linejoin| format!("stroke-linejoin=\"{}\"", stroke_linejoin)); + let stroke_width = icon.stroke_width.map_or_else(String::new, |stroke_width| format!("stroke-width=\"{}\"", stroke_width)); + let stroke = icon.stroke.map_or_else(String::new, |stroke| format!("stroke=\"{}\"", stroke)); + let fill = format!("fill=\"{}\"", icon.fill.unwrap_or("currentColor")); + let data = icon.data; + format!("{data}") +} diff --git a/vespid/src/lib.rs b/vespid/src/lib.rs index 4d48e50..c18c19d 100644 --- a/vespid/src/lib.rs +++ b/vespid/src/lib.rs @@ -1,3 +1,5 @@ +extern crate self as vespid; + mod escape_attribute; pub use escape_attribute::*; @@ -14,7 +16,9 @@ pub use render_adapter::*; mod context; pub use context::*; -pub mod axum_compat; +mod text; +pub use text::*; +pub mod axum; pub use vespid_macros::*; @@ -22,3 +26,14 @@ pub use vespid_macros::*; pub extern crate html_escape; #[doc(hidden)] pub extern crate typed_builder; + +#[cfg(feature = "icons")] +pub mod icons; + +pub mod prelude { + pub use {html_escape, typed_builder}; + pub use vespid_macros::*; + pub use crate::{context::*, render::Render, render_adapter::*, text::*}; + #[cfg(feature = "icons")] + pub use crate::icons::*; +} diff --git a/vespid/src/text.rs b/vespid/src/text.rs new file mode 100644 index 0000000..8045bd7 --- /dev/null +++ b/vespid/src/text.rs @@ -0,0 +1,748 @@ +//! This module contains the `Oco` (Owned Clones Once) smart pointer, +//! which is used to store immutable references to values. +//! This is useful for storing, for example, strings. + +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::{ + borrow::{Borrow, Cow}, + ffi::{CStr, OsStr}, + fmt, + hash::Hash, + ops::{Add, Deref}, + path::Path, + sync::Arc, +}; + +pub type MaybeText = OptionText<'static>; + +pub enum OptionText<'a> { + Text(Oco<'a, str>), + None +} + +impl<'a> OptionText<'a> { + pub fn get(&self) -> Option<&str> { + match self { + OptionText::Text(text) => Some(text.as_ref()), + OptionText::None => None + } + } +} + +impl<'a> Default for OptionText<'a> { + fn default() -> Self { + OptionText::None + } +} + +impl<'a> From> for OptionText<'a> { + fn from(text: Oco<'a, str>) -> Self { + OptionText::Text(text) + } +} + +impl<'a> From<&'a str> for OptionText<'a> { + fn from(text: &'a str) -> Self { + OptionText::Text(Oco::Borrowed(text)) + } +} + +impl<'a> From for OptionText<'a> { + fn from(text: String) -> Self { + OptionText::Text(Oco::Owned(text)) + } +} + +pub type Text = Oco<'static, str>; + +/// "Owned Clones Once" - a smart pointer that can be either a reference, +/// an owned value, or a reference counted pointer. This is useful for +/// storing immutable values, such as strings, in a way that is cheap to +/// clone and pass around. +/// +/// The `Clone` implementation is amortized `O(1)`. Cloning the [`Oco::Borrowed`] +/// variant simply copies the references (`O(1)`). Cloning the [`Oco::Counted`] +/// variant increments a reference count (`O(1)`). Cloning the [`Oco::Owned`] +/// variant upgrades it to [`Oco::Counted`], which requires an `O(n)` clone of the +/// data, but all subsequent clones will be `O(1)`. +pub enum Oco<'a, T: ?Sized + ToOwned + 'a> { + /// A static reference to a value. + Borrowed(&'a T), + /// A reference counted pointer to a value. + Counted(Arc), + /// An owned value. + Owned(::Owned), +} + +impl<'a, T: ?Sized + ToOwned> Oco<'a, T> { + /// Converts the value into an owned value. + pub fn into_owned(self) -> ::Owned { + match self { + Oco::Borrowed(v) => v.to_owned(), + Oco::Counted(v) => v.as_ref().to_owned(), + Oco::Owned(v) => v, + } + } + + /// Checks if the value is [`Oco::Borrowed`]. + /// # Examples + /// ``` + /// # use std::sync::Arc; + /// # use oco_ref::Oco; + /// assert!(Oco::::Borrowed("Hello").is_borrowed()); + /// assert!(!Oco::::Counted(Arc::from("Hello")).is_borrowed()); + /// assert!(!Oco::::Owned("Hello".to_string()).is_borrowed()); + /// ``` + pub const fn is_borrowed(&self) -> bool { + matches!(self, Oco::Borrowed(_)) + } + + /// Checks if the value is [`Oco::Counted`]. + /// # Examples + /// ``` + /// # use std::sync::Arc; + /// # use oco_ref::Oco; + /// assert!(Oco::::Counted(Arc::from("Hello")).is_counted()); + /// assert!(!Oco::::Borrowed("Hello").is_counted()); + /// assert!(!Oco::::Owned("Hello".to_string()).is_counted()); + /// ``` + pub const fn is_counted(&self) -> bool { + matches!(self, Oco::Counted(_)) + } + + /// Checks if the value is [`Oco::Owned`]. + /// # Examples + /// ``` + /// # use std::sync::Arc; + /// # use oco_ref::Oco; + /// assert!(Oco::::Owned("Hello".to_string()).is_owned()); + /// assert!(!Oco::::Borrowed("Hello").is_owned()); + /// assert!(!Oco::::Counted(Arc::from("Hello")).is_owned()); + /// ``` + pub const fn is_owned(&self) -> bool { + matches!(self, Oco::Owned(_)) + } +} + +impl Deref for Oco<'_, T> { + type Target = T; + + fn deref(&self) -> &T { + match self { + Oco::Borrowed(v) => v, + Oco::Owned(v) => v.borrow(), + Oco::Counted(v) => v, + } + } +} + +impl Borrow for Oco<'_, T> { + #[inline(always)] + fn borrow(&self) -> &T { + self.deref() + } +} + +impl AsRef for Oco<'_, T> { + #[inline(always)] + fn as_ref(&self) -> &T { + self.deref() + } +} + +impl AsRef for Oco<'_, str> { + #[inline(always)] + fn as_ref(&self) -> &Path { + self.as_str().as_ref() + } +} + +impl AsRef for Oco<'_, OsStr> { + #[inline(always)] + fn as_ref(&self) -> &Path { + self.as_os_str().as_ref() + } +} + +// -------------------------------------- +// pub fn as_{slice}(&self) -> &{slice} +// -------------------------------------- + +impl Oco<'_, str> { + /// Returns a `&str` slice of this [`Oco`]. + /// # Examples + /// ``` + /// # use oco_ref::Oco; + /// let oco = Oco::::Borrowed("Hello"); + /// let s: &str = oco.as_str(); + /// assert_eq!(s, "Hello"); + /// ``` + #[inline(always)] + pub fn as_str(&self) -> &str { + self + } +} + +impl Oco<'_, CStr> { + /// Returns a `&CStr` slice of this [`Oco`]. + /// # Examples + /// ``` + /// # use oco_ref::Oco; + /// use std::ffi::CStr; + /// + /// let oco = + /// Oco::::Borrowed(CStr::from_bytes_with_nul(b"Hello\0").unwrap()); + /// let s: &CStr = oco.as_c_str(); + /// assert_eq!(s, CStr::from_bytes_with_nul(b"Hello\0").unwrap()); + /// ``` + #[inline(always)] + pub fn as_c_str(&self) -> &CStr { + self + } +} + +impl Oco<'_, OsStr> { + /// Returns a `&OsStr` slice of this [`Oco`]. + /// # Examples + /// ``` + /// # use oco_ref::Oco; + /// use std::ffi::OsStr; + /// + /// let oco = Oco::::Borrowed(OsStr::new("Hello")); + /// let s: &OsStr = oco.as_os_str(); + /// assert_eq!(s, OsStr::new("Hello")); + /// ``` + #[inline(always)] + pub fn as_os_str(&self) -> &OsStr { + self + } +} + +impl Oco<'_, Path> { + /// Returns a `&Path` slice of this [`Oco`]. + /// # Examples + /// ``` + /// # use oco_ref::Oco; + /// use std::path::Path; + /// + /// let oco = Oco::::Borrowed(Path::new("Hello")); + /// let s: &Path = oco.as_path(); + /// assert_eq!(s, Path::new("Hello")); + /// ``` + #[inline(always)] + pub fn as_path(&self) -> &Path { + self + } +} + +impl Oco<'_, [T]> +where + [T]: ToOwned, +{ + /// Returns a `&[T]` slice of this [`Oco`]. + /// # Examples + /// ``` + /// # use oco_ref::Oco; + /// let oco = Oco::<[u8]>::Borrowed(b"Hello"); + /// let s: &[u8] = oco.as_slice(); + /// assert_eq!(s, b"Hello"); + /// ``` + #[inline(always)] + pub fn as_slice(&self) -> &[T] { + self + } +} + +impl<'a, T> Clone for Oco<'a, T> +where + T: ?Sized + ToOwned + 'a, + for<'b> Arc: From<&'b T>, +{ + /// Returns a new [`Oco`] with the same value as this one. + /// If the value is [`Oco::Owned`], this will convert it into + /// [`Oco::Counted`], so that the next clone will be O(1). + /// # Examples + /// [`String`] : + /// ``` + /// # use oco_ref::Oco; + /// let oco = Oco::::Owned("Hello".to_string()); + /// let oco2 = oco.clone(); + /// assert_eq!(oco, oco2); + /// assert!(oco2.is_counted()); + /// ``` + /// [`Vec`] : + /// ``` + /// # use oco_ref::Oco; + /// let oco = Oco::<[u8]>::Owned(b"Hello".to_vec()); + /// let oco2 = oco.clone(); + /// assert_eq!(oco, oco2); + /// assert!(oco2.is_counted()); + /// ``` + fn clone(&self) -> Self { + match self { + Self::Borrowed(v) => Self::Borrowed(v), + Self::Counted(v) => Self::Counted(Arc::clone(v)), + Self::Owned(v) => Self::Counted(Arc::from(v.borrow())), + } + } +} + +impl<'a, T> Oco<'a, T> +where + T: ?Sized + ToOwned + 'a, + for<'b> Arc: From<&'b T>, +{ + /// Clones the value with inplace conversion into [`Oco::Counted`] if it + /// was previously [`Oco::Owned`]. + /// # Examples + /// ``` + /// # use oco_ref::Oco; + /// let mut oco1 = Oco::::Owned("Hello".to_string()); + /// let oco2 = oco1.clone_inplace(); + /// assert_eq!(oco1, oco2); + /// assert!(oco1.is_counted()); + /// assert!(oco2.is_counted()); + /// ``` + pub fn clone_inplace(&mut self) -> Self { + match &*self { + Self::Borrowed(v) => Self::Borrowed(v), + Self::Counted(v) => Self::Counted(Arc::clone(v)), + Self::Owned(v) => { + let rc = Arc::from(v.borrow()); + *self = Self::Counted(rc.clone()); + Self::Counted(rc) + } + } + } + + /// Converts the value into its cheaply-clonable form in place. + /// In other words, if it is currently [`Oco::Owned`], converts into [`Oco::Counted`] + /// in an `O(n)` operation, so that all future clones are `O(1)`. + /// + /// # Examples + /// ``` + /// # use oco_ref::Oco; + /// let mut oco = Oco::::Owned("Hello".to_string()); + /// oco.upgrade_inplace(); + /// assert!(oco.is_counted()); + /// ``` + pub fn upgrade_inplace(&mut self) { + if let Self::Owned(v) = &*self { + let rc = Arc::from(v.borrow()); + *self = Self::Counted(rc); + } + } +} + +impl Default for Oco<'_, T> +where + T: ToOwned, + T::Owned: Default, +{ + fn default() -> Self { + Oco::Owned(T::Owned::default()) + } +} + +impl<'a, 'b, A: ?Sized, B: ?Sized> PartialEq> for Oco<'a, A> +where + A: PartialEq, + A: ToOwned, + B: ToOwned, +{ + fn eq(&self, other: &Oco<'b, B>) -> bool { + **self == **other + } +} + +impl Eq for Oco<'_, T> {} + +impl<'a, 'b, A: ?Sized, B: ?Sized> PartialOrd> for Oco<'a, A> +where + A: PartialOrd, + A: ToOwned, + B: ToOwned, +{ + fn partial_cmp(&self, other: &Oco<'b, B>) -> Option { + (**self).partial_cmp(&**other) + } +} + +impl Ord for Oco<'_, T> +where + T: ToOwned, +{ + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + (**self).cmp(&**other) + } +} + +impl Hash for Oco<'_, T> +where + T: ToOwned, +{ + fn hash(&self, state: &mut H) { + (**self).hash(state) + } +} + +impl fmt::Debug for Oco<'_, T> +where + T: ToOwned, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + (**self).fmt(f) + } +} + +impl fmt::Display for Oco<'_, T> +where + T: ToOwned, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + (**self).fmt(f) + } +} + +impl<'a, T: ?Sized> From<&'a T> for Oco<'a, T> +where + T: ToOwned, +{ + fn from(v: &'a T) -> Self { + Oco::Borrowed(v) + } +} + +impl<'a, T: ?Sized> From> for Oco<'a, T> +where + T: ToOwned, +{ + fn from(v: Cow<'a, T>) -> Self { + match v { + Cow::Borrowed(v) => Oco::Borrowed(v), + Cow::Owned(v) => Oco::Owned(v), + } + } +} + +impl<'a, T: ?Sized> From> for Cow<'a, T> +where + T: ToOwned, +{ + fn from(value: Oco<'a, T>) -> Self { + match value { + Oco::Borrowed(v) => Cow::Borrowed(v), + Oco::Owned(v) => Cow::Owned(v), + Oco::Counted(v) => Cow::Owned(v.as_ref().to_owned()), + } + } +} + +impl From> for Oco<'_, T> +where + T: ToOwned, +{ + fn from(v: Arc) -> Self { + Oco::Counted(v) + } +} + +impl From> for Oco<'_, T> +where + T: ToOwned, +{ + fn from(v: Box) -> Self { + Oco::Counted(v.into()) + } +} + +impl From for Oco<'_, str> { + fn from(v: String) -> Self { + Oco::Owned(v) + } +} + +impl From> for String { + fn from(v: Oco<'_, str>) -> Self { + match v { + Oco::Borrowed(v) => v.to_owned(), + Oco::Counted(v) => v.as_ref().to_owned(), + Oco::Owned(v) => v, + } + } +} + +impl From> for Oco<'_, [T]> +where + [T]: ToOwned>, +{ + fn from(v: Vec) -> Self { + Oco::Owned(v) + } +} + +impl<'a, T, const N: usize> From<&'a [T; N]> for Oco<'a, [T]> +where + [T]: ToOwned, +{ + fn from(v: &'a [T; N]) -> Self { + Oco::Borrowed(v) + } +} + +impl<'a> From> for Oco<'a, [u8]> { + fn from(v: Oco<'a, str>) -> Self { + match v { + Oco::Borrowed(v) => Oco::Borrowed(v.as_bytes()), + Oco::Owned(v) => Oco::Owned(v.into_bytes()), + Oco::Counted(v) => Oco::Counted(v.into()), + } + } +} + +/// Error returned from `Oco::try_from` for unsuccessful +/// conversion from `Oco<'_, [u8]>` to `Oco<'_, str>`. +#[derive(Debug, Clone, thiserror::Error)] +#[error("invalid utf-8 sequence: {_0}")] +pub enum FromUtf8Error { + /// Error for conversion of [`Oco::Borrowed`] and [`Oco::Counted`] variants + /// (`&[u8]` to `&str`). + #[error("{_0}")] + StrFromBytes( + #[source] + #[from] + std::str::Utf8Error, + ), + /// Error for conversion of [`Oco::Owned`] variant (`Vec` to `String`). + #[error("{_0}")] + StringFromBytes( + #[source] + #[from] + std::string::FromUtf8Error, + ), +} + +macro_rules! impl_slice_eq { + ([$($g:tt)*] $((where $($w:tt)+))?, $lhs:ty, $rhs: ty) => { + impl<$($g)*> PartialEq<$rhs> for $lhs + $(where + $($w)*)? + { + #[inline] + fn eq(&self, other: &$rhs) -> bool { + PartialEq::eq(&self[..], &other[..]) + } + } + + impl<$($g)*> PartialEq<$lhs> for $rhs + $(where + $($w)*)? + { + #[inline] + fn eq(&self, other: &$lhs) -> bool { + PartialEq::eq(&self[..], &other[..]) + } + } + }; +} + +impl_slice_eq!([], Oco<'_, str>, str); +impl_slice_eq!(['a, 'b], Oco<'a, str>, &'b str); +impl_slice_eq!([], Oco<'_, str>, String); +impl_slice_eq!(['a, 'b], Oco<'a, str>, Cow<'b, str>); + +impl_slice_eq!([T: PartialEq] (where [T]: ToOwned), Oco<'_, [T]>, [T]); +impl_slice_eq!(['a, 'b, T: PartialEq] (where [T]: ToOwned), Oco<'a, [T]>, &'b [T]); +impl_slice_eq!([T: PartialEq] (where [T]: ToOwned), Oco<'_, [T]>, Vec); +impl_slice_eq!(['a, 'b, T: PartialEq] (where [T]: ToOwned), Oco<'a, [T]>, Cow<'b, [T]>); + +impl<'a, 'b> Add<&'b str> for Oco<'a, str> { + type Output = Oco<'static, str>; + + fn add(self, rhs: &'b str) -> Self::Output { + Oco::Owned(String::from(self) + rhs) + } +} + +impl<'a, 'b> Add> for Oco<'a, str> { + type Output = Oco<'static, str>; + + fn add(self, rhs: Cow<'b, str>) -> Self::Output { + Oco::Owned(String::from(self) + rhs.as_ref()) + } +} + +impl<'a, 'b> Add> for Oco<'a, str> { + type Output = Oco<'static, str>; + + fn add(self, rhs: Oco<'b, str>) -> Self::Output { + Oco::Owned(String::from(self) + rhs.as_ref()) + } +} + +impl<'a> FromIterator> for String { + fn from_iter>>(iter: T) -> Self { + iter.into_iter().fold(String::new(), |mut acc, item| { + acc.push_str(item.as_ref()); + acc + }) + } +} + +impl<'a, T> Deserialize<'a> for Oco<'static, T> +where + T: ?Sized + ToOwned + 'a, + T::Owned: DeserializeOwned, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'a>, + { + ::deserialize(deserializer).map(Oco::Owned) + } +} + +impl<'a, T> Serialize for Oco<'a, T> +where + T: ?Sized + ToOwned + 'a, + for<'b> &'b T: Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.as_ref().serialize(serializer) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn debug_fmt_should_display_quotes_for_strings() { + let s: Oco = Oco::Borrowed("hello"); + assert_eq!(format!("{:?}", s), "\"hello\""); + let s: Oco = Oco::Counted(Arc::from("hello")); + assert_eq!(format!("{:?}", s), "\"hello\""); + } + + #[test] + fn partial_eq_should_compare_str_to_str() { + let s: Oco = Oco::Borrowed("hello"); + assert_eq!(s, "hello"); + assert_eq!("hello", s); + assert_eq!(s, String::from("hello")); + assert_eq!(String::from("hello"), s); + assert_eq!(s, Cow::from("hello")); + assert_eq!(Cow::from("hello"), s); + } + + #[test] + fn partial_eq_should_compare_slice_to_slice() { + let s: Oco<[i32]> = Oco::Borrowed([1, 2, 3].as_slice()); + assert_eq!(s, [1, 2, 3].as_slice()); + assert_eq!([1, 2, 3].as_slice(), s); + assert_eq!(s, vec![1, 2, 3]); + assert_eq!(vec![1, 2, 3], s); + assert_eq!(s, Cow::<'_, [i32]>::Borrowed(&[1, 2, 3])); + assert_eq!(Cow::<'_, [i32]>::Borrowed(&[1, 2, 3]), s); + } + + #[test] + fn add_should_concatenate_strings() { + let s: Oco = Oco::Borrowed("hello"); + assert_eq!(s.clone() + " world", "hello world"); + assert_eq!(s.clone() + Cow::from(" world"), "hello world"); + assert_eq!(s + Oco::from(" world"), "hello world"); + } + + #[test] + fn as_str_should_return_a_str() { + let s: Oco = Oco::Borrowed("hello"); + assert_eq!(s.as_str(), "hello"); + let s: Oco = Oco::Counted(Arc::from("hello")); + assert_eq!(s.as_str(), "hello"); + } + + #[test] + fn as_slice_should_return_a_slice() { + let s: Oco<[i32]> = Oco::Borrowed([1, 2, 3].as_slice()); + assert_eq!(s.as_slice(), [1, 2, 3].as_slice()); + let s: Oco<[i32]> = Oco::Counted(Arc::from([1, 2, 3])); + assert_eq!(s.as_slice(), [1, 2, 3].as_slice()); + } + + #[test] + fn default_for_str_should_return_an_empty_string() { + let s: Oco = Default::default(); + assert!(s.is_empty()); + } + + #[test] + fn default_for_slice_should_return_an_empty_slice() { + let s: Oco<[i32]> = Default::default(); + assert!(s.is_empty()); + } + + #[test] + fn default_for_any_option_should_return_none() { + let s: Oco> = Default::default(); + assert!(s.is_none()); + } + + #[test] + fn cloned_owned_string_should_make_counted_str() { + let s: Oco = Oco::Owned(String::from("hello")); + assert!(s.clone().is_counted()); + } + + #[test] + fn cloned_borrowed_str_should_make_borrowed_str() { + let s: Oco = Oco::Borrowed("hello"); + assert!(s.clone().is_borrowed()); + } + + #[test] + fn cloned_counted_str_should_make_counted_str() { + let s: Oco = Oco::Counted(Arc::from("hello")); + assert!(s.clone().is_counted()); + } + + #[test] + fn cloned_inplace_owned_string_should_make_counted_str_and_become_counted() + { + let mut s: Oco = Oco::Owned(String::from("hello")); + assert!(s.clone_inplace().is_counted()); + assert!(s.is_counted()); + } + + #[test] + fn cloned_inplace_borrowed_str_should_make_borrowed_str_and_remain_borrowed( + ) { + let mut s: Oco = Oco::Borrowed("hello"); + assert!(s.clone_inplace().is_borrowed()); + assert!(s.is_borrowed()); + } + + #[test] + fn cloned_inplace_counted_str_should_make_counted_str_and_remain_counted() { + let mut s: Oco = Oco::Counted(Arc::from("hello")); + assert!(s.clone_inplace().is_counted()); + assert!(s.is_counted()); + } + + #[test] + fn serialization_works() { + let s = serde_json::to_string(&Oco::Borrowed("foo")) + .expect("should serialize string"); + assert_eq!(s, "\"foo\""); + } + + #[test] + fn deserialization_works() { + let s: Oco = serde_json::from_str("\"bar\"") + .expect("should deserialize from string"); + assert_eq!(s, Oco::from(String::from("bar"))); + } +} diff --git a/watch.sh b/watch.sh new file mode 100755 index 0000000..f2e10db --- /dev/null +++ b/watch.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +cargo watch -s "bun tailwind:build; cargo run"