tailwind yay

This commit is contained in:
Borodinov Ilya 2024-12-08 23:53:36 +03:00
parent 91eaf8f6e3
commit 4939d5a58f
Signed by: noth
GPG key ID: 75503B2EF596D1BD
15 changed files with 1314 additions and 12 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
/target
.devenv
.direnv
/node_modules

259
Cargo.lock generated
View file

@ -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",
]

View file

@ -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"] }

BIN
bun.lockb Executable file

Binary file not shown.

11
package.json Normal file
View file

@ -0,0 +1,11 @@
{
"name": "crusto",
"devDependencies": {
"@catppuccin/tailwindcss": "^0.1.6",
"@tailwindcss/cli": "^4.0.0-beta.6",
"tailwindcss": "^4.0.0-beta.6"
},
"scripts": {
"tailwind:build": "tailwindcss -i ./src/app.css -o ./target/app.css"
}
}

101
src/app.css Normal file
View file

@ -0,0 +1,101 @@
@import "tailwindcss";
@config "../tailwind.config.ts";
html {
@apply w-full h-full;
font-size: 100%;
}
select,
textarea,
input,
button {
font: inherit;
}
body {
@apply bg-background text-foreground shadow-accent w-full h-full;
cursor: auto !important;
font-feature-settings:
"rlig" 1,
"calt" 1;
}
* {
@apply border-border;
}
@layer base {
:root {
--radius: 0.5rem;
}
}
@layer utilities {
.hstack {
@apply flex flex-row items-center;
}
.vstack {
@apply flex flex-col;
}
.fullcenter {
@apply items-center justify-center;
}
.ty ol,
.ty ul {
@apply my-6 ml-6 list-disc [&>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 */
}
}

View file

@ -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 {
<meta charset="utf-8" />
<title>Crusto</title>
<script src="https://unpkg.com/htmx.org@2.0.3" integrity="sha384-0895/pl2MU10Hqc6jd4RvrthNlDiE9U1tWmX7WRESftEDRosgxNsQG/Ze9YMRzHq" crossorigin="anonymous"></script>
<style>{include_str!("../target/app.css")}</style>
</head>
<body>
{children}
@ -34,9 +36,12 @@ async fn index() -> Html<String> {
<h1>"Hello to Crusto!"</h1>
<p>"Index"</p>
<button hx-get="/widget" hx-swap="outerHTML" hx-target="#widget">"Get widget"</button>
<button hx-get="/widget" hx-swap="outerHTML" hx-target="#widget" hx-trigger="load delay:1s, click" hx-indicator="#widget_container > .spinner">"Get widget"</button>
<div id="widget">
<div id="widget_container" class="flex flex-col items-center justify-center">
<Icon icon=icondata::LuLoader2 class="animate-spin spinner htmx-indicator" />
<div id="widget">
</div>
</div>
</Shell>
}
@ -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(())
}

125
tailwind.config.ts Normal file
View file

@ -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), <alpha-value>)',
input: 'rgba(var(--ctp-surface2), <alpha-value>)',
ring: 'rgba(var(--ctp-text), <alpha-value>)',
background: 'rgba(var(--ctp-base), <alpha-value>)',
foreground: 'rgba(var(--ctp-text), <alpha-value>)',
primary: {
DEFAULT: 'rgba(var(--ctp-lavender), <alpha-value>)',
foreground: 'rgba(var(--ctp-base), <alpha-value>)',
},
secondary: {
DEFAULT: 'rgba(var(--ctp-surface0), <alpha-value>)',
foreground: 'rgba(var(--ctp-text), <alpha-value>)',
},
destructive: {
DEFAULT: 'rgba(var(--ctp-red), <alpha-value>)',
foreground: 'rgba(var(--ctp-crust), <alpha-value>)',
},
warning: {
DEFAULT: 'rgba(var(--ctp-peach), <alpha-value>)',
foreground: 'rgba(var(--ctp-crust), <alpha-value>)',
},
info: {
DEFAULT: 'rgba(var(--ctp-blue), <alpha-value>)',
foreground: 'rgba(var(--info-foreground), <alpha-value>)',
},
success: {
DEFAULT: 'rgba(var(--ctp-green), <alpha-value>)',
foreground: 'rgba(var(--ctp-crust), <alpha-value>)',
},
muted: {
DEFAULT: 'rgba(var(--ctp-overlay0), <alpha-value>)',
foreground: 'rgba(var(--ctp-overlay1), <alpha-value>)',
},
invariant: {
DEFAULT: 'rgba(var(--ctp-subtext0), <alpha-value>)',
foreground: 'rgba(var(--ctp-mantle), <alpha-value>)',
},
accent: {
DEFAULT: 'rgba(var(--ctp-base), <alpha-value>)',
foreground: 'rgba(var(--ctp-text), <alpha-value>)',
},
popover: {
DEFAULT: 'rgba(var(--ctp-mantle), <alpha-value>)',
foreground: 'rgba(var(--ctp-text), <alpha-value>)',
},
card: {
DEFAULT: 'rgba(var(--ctp-mantle), <alpha-value>)',
foreground: 'rgba(var(--ctp-text), <alpha-value>)',
},
},
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;

View file

@ -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"]

View file

@ -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<proc_macro2::To
static_format.push_str(r#"="{}""#);
format_value = Some(
quote! {{
// (#value).escape_attribute()
::vespid::EscapeAttribute::escape_attribute(&#value)
}}
.into_token_stream(),
@ -400,7 +400,7 @@ impl ToTokens for PropsStruct {
let has_attributes = item
.fields
.iter()
.any(|field| field.ident.as_ref().unwrap().to_string() == "attributes");
.any(|field| field.ident.as_ref().expect_or_abort("field.ident == None").to_string() == "attributes");
if has_attributes {
tokens.extend(quote! {
@ -463,7 +463,7 @@ impl ToTokens for ComponentFn {
.into_iter()
.map(|i| match i {
FnArg::Receiver(_) => {
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,
})

28
vespid/src/icons.rs Normal file
View file

@ -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!("<svg {width} {height} {class} {style} {x} {y} {viewbox} {stroke_linecap} {stroke_linejoin} {stroke_width} {stroke} {fill}>{data}</svg>")
}

View file

@ -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::*;
}

748
vespid/src/text.rs Normal file
View file

@ -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<Oco<'a, str>> 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<String> 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<T>),
/// An owned value.
Owned(<T as ToOwned>::Owned),
}
impl<'a, T: ?Sized + ToOwned> Oco<'a, T> {
/// Converts the value into an owned value.
pub fn into_owned(self) -> <T as ToOwned>::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::<str>::Borrowed("Hello").is_borrowed());
/// assert!(!Oco::<str>::Counted(Arc::from("Hello")).is_borrowed());
/// assert!(!Oco::<str>::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::<str>::Counted(Arc::from("Hello")).is_counted());
/// assert!(!Oco::<str>::Borrowed("Hello").is_counted());
/// assert!(!Oco::<str>::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::<str>::Owned("Hello".to_string()).is_owned());
/// assert!(!Oco::<str>::Borrowed("Hello").is_owned());
/// assert!(!Oco::<str>::Counted(Arc::from("Hello")).is_owned());
/// ```
pub const fn is_owned(&self) -> bool {
matches!(self, Oco::Owned(_))
}
}
impl<T: ?Sized + ToOwned> 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<T: ?Sized + ToOwned> Borrow<T> for Oco<'_, T> {
#[inline(always)]
fn borrow(&self) -> &T {
self.deref()
}
}
impl<T: ?Sized + ToOwned> AsRef<T> for Oco<'_, T> {
#[inline(always)]
fn as_ref(&self) -> &T {
self.deref()
}
}
impl AsRef<Path> for Oco<'_, str> {
#[inline(always)]
fn as_ref(&self) -> &Path {
self.as_str().as_ref()
}
}
impl AsRef<Path> 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::<str>::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::<CStr>::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::<OsStr>::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::<Path>::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<T> 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<T>: 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::<str>::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<T>: 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::<str>::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::<str>::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<T: ?Sized> 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<Oco<'b, B>> for Oco<'a, A>
where
A: PartialEq<B>,
A: ToOwned,
B: ToOwned,
{
fn eq(&self, other: &Oco<'b, B>) -> bool {
**self == **other
}
}
impl<T: ?Sized + ToOwned + Eq> Eq for Oco<'_, T> {}
impl<'a, 'b, A: ?Sized, B: ?Sized> PartialOrd<Oco<'b, B>> for Oco<'a, A>
where
A: PartialOrd<B>,
A: ToOwned,
B: ToOwned,
{
fn partial_cmp(&self, other: &Oco<'b, B>) -> Option<std::cmp::Ordering> {
(**self).partial_cmp(&**other)
}
}
impl<T: ?Sized + Ord> Ord for Oco<'_, T>
where
T: ToOwned,
{
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
(**self).cmp(&**other)
}
}
impl<T: ?Sized + Hash> Hash for Oco<'_, T>
where
T: ToOwned,
{
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
(**self).hash(state)
}
}
impl<T: ?Sized + fmt::Debug> fmt::Debug for Oco<'_, T>
where
T: ToOwned,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
(**self).fmt(f)
}
}
impl<T: ?Sized + fmt::Display> 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<Cow<'a, T>> 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<Oco<'a, T>> 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<T: ?Sized> From<Arc<T>> for Oco<'_, T>
where
T: ToOwned,
{
fn from(v: Arc<T>) -> Self {
Oco::Counted(v)
}
}
impl<T: ?Sized> From<Box<T>> for Oco<'_, T>
where
T: ToOwned,
{
fn from(v: Box<T>) -> Self {
Oco::Counted(v.into())
}
}
impl From<String> for Oco<'_, str> {
fn from(v: String) -> Self {
Oco::Owned(v)
}
}
impl From<Oco<'_, str>> 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<T> From<Vec<T>> for Oco<'_, [T]>
where
[T]: ToOwned<Owned = Vec<T>>,
{
fn from(v: Vec<T>) -> 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<Oco<'a, str>> 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<u8>` 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<T>);
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<Cow<'b, str>> 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<Oco<'b, str>> 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<Oco<'a, str>> for String {
fn from_iter<T: IntoIterator<Item = Oco<'a, str>>>(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<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'a>,
{
<T::Owned>::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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<str> = Oco::Borrowed("hello");
assert_eq!(format!("{:?}", s), "\"hello\"");
let s: Oco<str> = Oco::Counted(Arc::from("hello"));
assert_eq!(format!("{:?}", s), "\"hello\"");
}
#[test]
fn partial_eq_should_compare_str_to_str() {
let s: Oco<str> = 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<str> = 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<str> = Oco::Borrowed("hello");
assert_eq!(s.as_str(), "hello");
let s: Oco<str> = 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<str> = 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<Option<i32>> = Default::default();
assert!(s.is_none());
}
#[test]
fn cloned_owned_string_should_make_counted_str() {
let s: Oco<str> = Oco::Owned(String::from("hello"));
assert!(s.clone().is_counted());
}
#[test]
fn cloned_borrowed_str_should_make_borrowed_str() {
let s: Oco<str> = Oco::Borrowed("hello");
assert!(s.clone().is_borrowed());
}
#[test]
fn cloned_counted_str_should_make_counted_str() {
let s: Oco<str> = 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<str> = 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<str> = 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<str> = 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<str> = serde_json::from_str("\"bar\"")
.expect("should deserialize from string");
assert_eq!(s, Oco::from(String::from("bar")));
}
}

3
watch.sh Executable file
View file

@ -0,0 +1,3 @@
#!/usr/bin/env bash
cargo watch -s "bun tailwind:build; cargo run"