commit 7976babb7918c2de9564736fbb2da5da896ed550 Author: mrkmntal Date: Mon Sep 28 17:55:56 2026 -0400 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f804bb2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/notes/*.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2e015fc --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,313 @@ +# AGENTS.md + +Welcome AI assistant! You are working on **Rasuti Notes** (ラスティ Notes)—a server-side rendered (SSR) copyleft Markdown notes application written in Rust. + +Rasuti Notes combines **GNU software freedom** (GPL v3.0-or-later) with the **fluid elegance of Apple Notes** and the **predictable utility of classic desktop GUI applications** (menu bars, split panes, and file organization). + +--- + +## 1. Project Philosophy & Identity + +* **Name**: Rasuti Notes (ラスティ Notes—from Japanese *Rasuti* / ラスティ for Rusty). +* **Mission**: Reclaiming high-performance Rust web development under copyleft terms (GPLv3+). Rejecting heavy client-side JavaScript frameworks and proprietary note silos in favor of software freedom, privacy, and near-zero memory footprint. +* **UI Paradigm**: **"Apple Elegance meets Classic Desktop GUI"** +* **Apple Notes Polish**: Fluid sidebars, smooth note item previews, refined typography, generous padding, subtle contrast, and graceful theme transitions. +* **Traditional Desktop Utility**: Top menu bar (`File`, `Edit`, `View`, `Theme`, `Help`), quick action toolbars, split-pane resizers, keyboard shortcuts, and visible document state. + + + +--- + +## 2. UI/UX Design System & Layout Architecture + +The application layout follows a classic 3-column / top-menu layout rendered cleanly via SSR: + +``` ++-----------------------------------------------------------------------------------+ +| [File] [Edit] [View] [Theme] [Help] [Rasuti Notes ラスティ v0.1] | <-- Classic Menu Bar ++-----------------------------------------------------------------------------------+ +| [+ New Note] [Delete] | Search... | Theme: [ Dark v ] [Preview] | <-- Apple-style Toolbar ++-------------------+-----------------------------------+---------------------------+ +| Sidebar | Editor Pane | Live Preview Pane | +| ----------------- | --------------------------------- | ------------------------- | +| 📝 Project Notes | # Welcome to Rasuti Notes | Welcome to Rasuti Notes | +| Sep 28, 2026 | | | +| GPLv3+ Markdown| Fast, copyleft Markdown editor... | Fast, copyleft Markdown...| +| | | | +| 📝 Rust Snippets | - [x] High performance SSR | • [x] High performance SSR| +| Sep 25, 2026 | - [ ] Zero heavy JS runtime | • [ ] Zero heavy JS | +| fn main() ... | | | ++-------------------+-----------------------------------+---------------------------+ +| Status: 2 Notes | Saved | UTF-8 | Markdown | Theme: Dark | <-- Status Bar ++-----------------------------------------------------------------------------------+ + +``` + +### Key UX Principles: + +1. **Fluid Typography**: Inter/SF Pro or high-quality system sans-serif for the interface; Monospace for Markdown source; clean serif or clean sans for HTML preview. +2. **Subtle Elevation & Borders**: Soft 1px borders using `--ansi-0` / `--app-bg` contrast shades rather than heavy drop shadows. +3. **Apple-style Note List**: Sidebar lists show note title, relative timestamp, and a single line preview snippet in muted text. +4. **Progressive Enhancement**: Full functionality works with zero JavaScript (via HTML forms and page reloads). With lightweight inline JS or HTMX enabled, typing in the editor triggers real-time live preview swaps without full-page refreshes. + +--- + +## 3. Architecture & Tech Stack + +* **Language**: Rust (`edition = "2021"`) +* **Server Framework**: **Axum** or **Actix-web** +* **SSR Templating**: **`maud`** (compiled, type-safe HTML macros) or **`askama`** +* **Reactivity / Live Swap**: **HTMX** (or lightweight SSE / fetch script under 5KB) for seamless live preview typing updates and instant theme swapping. +* **Markdown Engine**: `pulldown-cmark` (with table, tasklist, and strikethrough extensions enabled). +* **License**: **GNU General Public License v3.0 or later** (`GPL-3.0-or-later`). All dependency crates in `Cargo.toml` must be compatible with GPLv3+. + +--- + +## 4. Theme System & ANSI Palette Data + +Theme definitions provide background, foreground, and full 16-color ANSI palettes. + +### Rust Data Structures (`src/themes.rs`) + +```rust +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct Theme { + pub name: &'static str, + pub fg: &'static str, + pub bg: &'static str, + pub ansi_normal: [&'static str; 8], + pub ansi_bright: [&'static str; 8], +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct ANSIPalette { + pub name: &'static str, + pub ansi_normal: [&'static str; 8], + pub ansi_bright: [&'static str; 8], +} + +``` + +### Predefined Themes Vector + +```rust +pub static PREDEFINED_THEMES: &[Theme] = &[ + Theme { + name: "Dark", + fg: "#d0d0d0", + bg: "#1c1c1c", + ansi_normal: ["#1c1c1c", "#c50f1f", "#23a523", "#b58900", "#268bd2", "#d33682", "#2aa198", "#d0d0d0"], + ansi_bright: ["#666666", "#ff6666", "#66ff66", "#ffff66", "#6666ff", "#ff66ff", "#66ffff", "#ffffff"], + }, + Theme { + name: "Light", + fg: "#1c1c1c", + bg: "#d0d0d0", + ansi_normal: ["#1c1c1c", "#c50f1f", "#23a523", "#b58900", "#268bd2", "#d33682", "#2aa198", "#d0d0d0"], + ansi_bright: ["#666666", "#ff6666", "#66ff66", "#ffff66", "#6666ff", "#ff66ff", "#66ffff", "#ffffff"], + }, + Theme { + name: "Solarized Dark", + fg: "#839496", + bg: "#002b36", + ansi_normal: ["#073642", "#dc322f", "#859900", "#b58900", "#268bd2", "#d33682", "#2aa198", "#eee8d5"], + ansi_bright: ["#002b36", "#cb4b16", "#586e75", "#657b83", "#839496", "#6c71c4", "#93a1a1", "#fdf6e3"], + }, + Theme { + name: "Solarized Light", + fg: "#657b83", + bg: "#fdf6e3", + ansi_normal: ["#073642", "#dc322f", "#859900", "#b58900", "#268bd2", "#d33682", "#2aa198", "#eee8d5"], + ansi_bright: ["#002b36", "#cb4b16", "#586e75", "#657b83", "#839496", "#6c71c4", "#93a1a1", "#fdf6e3"], + }, + Theme { + name: "Dracula", + fg: "#f8f8f2", + bg: "#282a36", + ansi_normal: ["#21222c", "#ff5555", "#50fa7b", "#f1fa8c", "#bd93f9", "#ff79c6", "#8be9fd", "#f8f8f2"], + ansi_bright: ["#6272a4", "#ff6e6e", "#69ff94", "#ffffa5", "#d6acff", "#ff92df", "#a4ffff", "#ffffff"], + }, + Theme { + name: "Gruvbox Dark", + fg: "#ebdbb2", + bg: "#282828", + ansi_normal: ["#282828", "#cc241d", "#98971a", "#d79921", "#458588", "#b16286", "#689d6a", "#a89984"], + ansi_bright: ["#928374", "#fb4934", "#b8bb26", "#fabd2f", "#83a598", "#d3869b", "#8ec07c", "#ebdbb2"], + }, + Theme { + name: "Gruvbox Light", + fg: "#3c3836", + bg: "#fbf1c7", + ansi_normal: ["#fbf1c7", "#cc241d", "#98971a", "#d79921", "#458588", "#b16286", "#689d6a", "#7c6f64"], + ansi_bright: ["#928374", "#9d0006", "#79740e", "#b57614", "#076678", "#8f3f71", "#427b58", "#3c3836"], + }, + Theme { + name: "Nord", + fg: "#d8dee9", + bg: "#2e3440", + ansi_normal: ["#3b4252", "#bf616a", "#a3be8c", "#ebcb8b", "#81a1c1", "#b48ead", "#88c0d0", "#e5e9f0"], + ansi_bright: ["#4c566a", "#bf616a", "#a3be8c", "#ebcb8b", "#81a1c1", "#b48ead", "#8fbcbb", "#eceff4"], + }, + Theme { + name: "Monokai", + fg: "#f8f8f2", + bg: "#272822", + ansi_normal: ["#272822", "#f92672", "#a6e22e", "#f4bf75", "#66d9ef", "#ae81ff", "#a1efe4", "#f8f8f2"], + ansi_bright: ["#75715e", "#f92672", "#a6e22e", "#e6db74", "#66d9ef", "#ae81ff", "#a1efe4", "#f9f8f5"], + }, + Theme { + name: "Tokyo Night", + fg: "#c0caf5", + bg: "#1a1b26", + ansi_normal: ["#15161e", "#f7768e", "#9ece6a", "#e0af68", "#7aa2f7", "#bb9af7", "#7dcfff", "#a9b1d6"], + ansi_bright: ["#414868", "#f7768e", "#9ece6a", "#e0af68", "#7aa2f7", "#bb9af7", "#7dcfff", "#c0caf5"], + }, + Theme { + name: "One Dark", + fg: "#abb2bf", + bg: "#282c34", + ansi_normal: ["#282c34", "#e06c75", "#98c379", "#e5c07b", "#61afef", "#c678dd", "#56b6c2", "#abb2bf"], + ansi_bright: ["#5c6370", "#e06c75", "#98c379", "#e5c07b", "#61afef", "#c678dd", "#56b6c2", "#ffffff"], + }, + Theme { + name: "Cyberpunk 2077", + fg: "#f9f227", + bg: "#0d0d0d", + ansi_normal: ["#0d0d0d", "#ff0055", "#00ff41", "#f9f227", "#00b8ff", "#ff00cc", "#00ffff", "#f9f227"], + ansi_bright: ["#333333", "#ff3377", "#33ff66", "#ffff33", "#33ccff", "#ff33ff", "#33ffff", "#ffff66"], + }, + Theme { + name: "Cyan on Black", + fg: "#00ffff", + bg: "#000000", + ansi_normal: ["#000000", "#cc0000", "#00cc00", "#cccc00", "#0000cc", "#cc00cc", "#00cccc", "#cccccc"], + ansi_bright: ["#666666", "#ff0000", "#00ff00", "#ffff00", "#0000ff", "#ff00ff", "#00ffff", "#ffffff"], + }, + Theme { + name: "Red on Black", + fg: "#ff0000", + bg: "#000000", + ansi_normal: ["#000000", "#cc0000", "#00cc00", "#cccc00", "#0000cc", "#cc00cc", "#00cccc", "#cccccc"], + ansi_bright: ["#666666", "#ff0000", "#00ff00", "#ffff00", "#0000ff", "#ff00ff", "#00ffff", "#ffffff"], + }, + Theme { + name: "Lime Green on Black", + fg: "#00ff00", + bg: "#000000", + ansi_normal: ["#000000", "#cc0000", "#00cc00", "#cccc00", "#0000cc", "#cc00cc", "#00cccc", "#cccccc"], + ansi_bright: ["#666666", "#ff0000", "#00ff00", "#ffff00", "#0000ff", "#ff00ff", "#00ffff", "#ffffff"], + }, + Theme { + name: "Chartreuse on Black", + fg: "#7fff00", + bg: "#000000", + ansi_normal: ["#000000", "#cc0000", "#00cc00", "#cccc00", "#0000cc", "#cc00cc", "#00cccc", "#cccccc"], + ansi_bright: ["#666666", "#ff0000", "#00ff00", "#ffff00", "#0000ff", "#ff00ff", "#00ffff", "#ffffff"], + }, + Theme { + name: "Amber on Black", + fg: "#ffbf00", + bg: "#000000", + ansi_normal: ["#000000", "#cc0000", "#00cc00", "#cccc00", "#0000cc", "#cc00cc", "#00cccc", "#cccccc"], + ansi_bright: ["#666666", "#ff0000", "#00ff00", "#ffff00", "#0000ff", "#ff00ff", "#00ffff", "#ffffff"], + }, + Theme { + name: "Blueberry on Black", + fg: "#8ec5fc", + bg: "#000000", + ansi_normal: ["#000000", "#0d47a1", "#1565c0", "#1976d2", "#1e88e5", "#42a5f5", "#64b5f6", "#90caf9"], + ansi_bright: ["#37474f", "#1e88e5", "#42a5f5", "#64b5f6", "#90caf9", "#bbdefb", "#e3f2fd", "#ffffff"], + }, + Theme { + name: "Spring Blossom", + fg: "#ffb7c5", + bg: "#2d5016", + ansi_normal: ["#2d5016", "#c2185b", "#7cb342", "#fbc02d", "#00897b", "#d81b60", "#aed581", "#f8bbd0"], + ansi_bright: ["#558b2f", "#e91e63", "#8bc34a", "#ffeb3b", "#009688", "#ec407a", "#c5e1a5", "#fce4ec"], + }, + Theme { + name: "Summer Sunset", + fg: "#ff9a5a", + bg: "#1a1a2e", + ansi_normal: ["#1a1a2e", "#e64a19", "#ff7043", "#ffab40", "#d81b60", "#8e24aa", "#5c6bc0", "#ffb74d"], + ansi_bright: ["#424242", "#ff5722", "#ff8a65", "#ffc107", "#e91e63", "#9c27b0", "#3f51b5", "#ffe0b2"], + }, + Theme { + name: "Autumn Forest", + fg: "#d4a373", + bg: "#3e2723", + ansi_normal: ["#3e2723", "#bf360c", "#e65100", "#8d6e63", "#5d4037", "#33691e", "#9e9d24", "#a1887f"], + ansi_bright: ["#6d4c41", "#e64a19", "#f57c00", "#a1887f", "#795548", "#558b2f", "#c0ca33", "#d7ccc8"], + }, + Theme { + name: "Winter Frost", + fg: "#e0f7fa", + bg: "#1a237e", + ansi_normal: ["#1a237e", "#006064", "#00838f", "#0097a7", "#00acc1", "#26c6da", "#4dd0e1", "#80deea"], + ansi_bright: ["#283593", "#00897b", "#00acc1", "#26c6da", "#4dd0e1", "#80deea", "#b2ebf2", "#ffffff"], + }, + Theme { + name: "Stormy Night", + fg: "#b0bec5", + bg: "#263238", + ansi_normal: ["#263238", "#37474f", "#455a64", "#546e7a", "#607d8b", "#78909c", "#90a4ae", "#b0bec5"], + ansi_bright: ["#455a64", "#607d8b", "#78909c", "#90a4ae", "#b0bec5", "#cfd8dc", "#eceff1", "#ffffff"], + }, + Theme { + name: "Sunny Day", + fg: "#fff59d", + bg: "#33691e", + ansi_normal: ["#33691e", "#f57f17", "#c0ca33", "#00acc1", "#00897b", "#5c6bc0", "#7cb342", "#fff59d"], + ansi_bright: ["#558b2f", "#fbc02d", "#afb42b", "#26c6da", "#0097a7", "#3f51b5", "#9ccc65", "#fff9c4"], + }, + Theme { + name: "Foggy Morning", + fg: "#cfd8dc", + bg: "#455a64", + ansi_normal: ["#455a64", "#546e7a", "#607d8b", "#78909c", "#90a4ae", "#b0bec5", "#cfd8dc", "#eceff1"], + ansi_bright: ["#607d8b", "#78909c", "#90a4ae", "#b0bec5", "#cfd8dc", "#eceff1", "#f5f5f5", "#ffffff"], + }, +]; + +``` + +--- + +## 5. CSS Var Binding Engine + +Server renders the theme colors dynamically into CSS variables powering both the classic GUI chrome (menu bar, status bar) and Apple-style polished UI elements (cards, text fields, focus rings): + +```rust +impl Theme { + pub fn to_css_block(&self) -> String { + format!( + ":root {{\n\ + --app-fg: {};\n\ + --app-bg: {};\n\ + --ui-border: {}33;\n\ + --ui-hover: {}1a;\n\ + --ansi-0: {}; --ansi-1: {}; --ansi-2: {}; --ansi-3: {};\n\ + --ansi-4: {}; --ansi-5: {}; --ansi-6: {}; --ansi-7: {};\n\ + --ansi-8: {}; --ansi-9: {}; --ansi-10: {}; --ansi-11: {};\n\ + --ansi-12: {}; --ansi-13: {}; --ansi-14: {}; --ansi-15: {};\n\ + }}", + self.fg, self.bg, + self.fg, self.fg, + self.ansi_normal[0], self.ansi_normal[1], self.ansi_normal[2], self.ansi_normal[3], + self.ansi_normal[4], self.ansi_normal[5], self.ansi_normal[6], self.ansi_normal[7], + self.ansi_bright[0], self.ansi_bright[1], self.ansi_bright[2], self.ansi_bright[3], + self.ansi_bright[4], self.ansi_bright[5], self.ansi_bright[6], self.ansi_bright[7], + ) + } +} + +``` + +--- + +## 6. Rules & Directives for AI Developers + +1. **Licensing**: Keep all dependencies compatible with **GNU GPLv3+**. Verify crate licenses before introducing new Cargo dependencies. +2. **SSR First**: Ensure basic editing and rendering functionality operates without JavaScript enabled. Use standard HTML forms, query parameters, or cookies for persistence. +3. **Fluid Micro-interactions**: When applying CSS, use smooth `transition: background-color 0.15s ease, color 0.15s ease` so theme switching feels fluid and premium. +4. **Clean Code**: Enforce strict idiomatic Rust (`cargo clippy -- -D warnings`). Avoid `unsafe` blocks. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..63763b9 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1169 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower 0.5.3", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "clap_lex" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "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.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "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 = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "maud" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df518b75016b4289cdddffa1b01f2122f4a49802c93191f3133f6dc2472ebcaa" +dependencies = [ + "itoa", + "maud_macros", +] + +[[package]] +name = "maud_macros" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa453238ec218da0af6b11fc5978d3b5c3a45ed97b722391a2a11f3306274e18" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +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 = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulldown-cmark" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "679341d22c78c6c649893cbd6c3278dcbe9fc4faa62fea3a9296ae2b50c14625" +dependencies = [ + "bitflags", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rasuti-notes" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "chrono", + "clap", + "maud", + "percent-encoding", + "pulldown-cmark", + "serde", + "serde_json", + "thiserror", + "tokio", + "tower 0.4.13", + "tower-http", + "uuid", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[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 = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[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.119", +] + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "getrandom", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d31c534 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "rasuti-notes" +version = "0.1.0" +edition = "2021" +license = "GPL-3.0-or-later" +description = "A server-side rendered Markdown notes application in Rust" + +[dependencies] +axum = "0.7" +tokio = { version = "1", features = ["full"] } +tower = "0.4" +tower-http = { version = "0.5", features = ["fs"] } +maud = "0.26" +pulldown-cmark = "0.11" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +chrono = { version = "0.4", features = ["serde"] } +uuid = { version = "1", features = ["v4"] } +anyhow = "1" +thiserror = "1" +percent-encoding = "2" +clap = { version = "4", features = ["derive"] } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ef6472e --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# Rasuti Notes (ラスティ Notes) + +A server-side rendered (SSR) Markdown notes application written in Rust. + +Rasuti Notes combines **GNU software freedom** (GPL v3.0-or-later) with the +fluid elegance of Apple Notes and the predictable utility of classic desktop +GUI applications: a menu bar, split-pane editor, and file-based note storage. + +## Features + +- **Server-side rendered** with Axum and Maud — fast, type-safe HTML templates +- **Zero-JS fallback** — every action works via standard HTML forms +- **Alpine.js enhancements** — live Markdown preview, instant search, keyboard shortcuts +- **File-based storage** — notes are persisted as Markdown files in `notes/` +- **28 built-in themes** — from Dark/Light to Solarized, Dracula, Nord, Monokai, and seasonal palettes +- **Markdown extras** — tables, tasklists, strikethrough, and footnotes via `pulldown-cmark` + +## Quick Start + +```bash +# Build and run (listens on 0.0.0.0:3000 by default) +cargo run + +# Use a custom host/port +cargo run -- --host 127.0.0.1 --port 8080 +``` + +Then open http://127.0.0.1:3000 (or your chosen address). + +Notes are stored in the `notes/` directory as `.md` files with YAML frontmatter. + +## Development + +```bash +# Run tests +cargo test + +# Run clippy with warnings as errors +cargo clippy -- -D warnings + +# Format code +cargo fmt +``` + +## Keyboard Shortcuts + +| Shortcut | Action | +|----------|--------| +| `Ctrl/Cmd + N` | Create a new note | +| `Ctrl/Cmd + S` | Save the current note | + +## License + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. See `LICENSE` for the full text. diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..a16fbb2 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,44 @@ +mod markdown; +mod notes; +mod pages; +mod routes; +mod storage; +mod themes; + +use axum::Router; +use clap::Parser; +use std::net::SocketAddr; +use tower_http::services::ServeDir; + +#[derive(Parser, Debug)] +#[command(name = "rasuti-notes")] +#[command(about = "A server-side rendered Markdown notes application")] +struct Args { + /// Host address to bind to + #[arg(long, default_value = "0.0.0.0")] + host: String, + + /// Port to listen on + #[arg(short, long, default_value_t = 3000)] + port: u16, +} + +#[tokio::main] +async fn main() { + let args = Args::parse(); + + // Ensure notes directory exists + std::fs::create_dir_all("notes").expect("Failed to create notes directory"); + + let app = Router::new() + .merge(routes::router()) + .nest_service("/static", ServeDir::new("static")); + + let host: std::net::IpAddr = args.host.parse().expect("Invalid host address"); + let addr = SocketAddr::from((host, args.port)); + + println!("Rasuti Notes running at http://{}", addr); + + let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); + axum::serve(listener, app).await.unwrap(); +} diff --git a/src/markdown.rs b/src/markdown.rs new file mode 100644 index 0000000..1296028 --- /dev/null +++ b/src/markdown.rs @@ -0,0 +1,43 @@ +use pulldown_cmark::{html, Options, Parser}; + +pub fn render_markdown(input: &str) -> String { + let mut options = Options::empty(); + options.insert(Options::ENABLE_TABLES); + options.insert(Options::ENABLE_TASKLISTS); + options.insert(Options::ENABLE_STRIKETHROUGH); + options.insert(Options::ENABLE_FOOTNOTES); + + let parser = Parser::new_ext(input, options); + let mut html_output = String::new(); + html::push_html(&mut html_output, parser); + html_output +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn renders_headings_and_paragraphs() { + let md = "# Hello\n\nThis is a paragraph."; + let html = render_markdown(md); + assert!(html.contains("

Hello

")); + assert!(html.contains("

This is a paragraph.

")); + } + + #[test] + fn renders_tasklists() { + let md = "- [x] Done\n- [ ] Todo"; + let html = render_markdown(md); + assert!(html.contains("")); + assert!(html.contains("A")); + } +} diff --git a/src/notes.rs b/src/notes.rs new file mode 100644 index 0000000..4e684d3 --- /dev/null +++ b/src/notes.rs @@ -0,0 +1,200 @@ +use anyhow::Result; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::Path; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Note { + pub id: String, + pub title: String, + pub content: String, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl Note { + pub fn new(title: impl Into, content: impl Into) -> Self { + let now = Utc::now(); + Self { + id: uuid::Uuid::new_v4().to_string(), + title: title.into(), + content: content.into(), + created_at: now, + updated_at: now, + } + } + + pub fn preview(&self, max_len: usize) -> String { + let text: String = self + .content + .lines() + .filter(|l| !l.trim().is_empty()) + .take(3) + .collect::>() + .join(" "); + if text.len() > max_len { + format!("{}...", &text[..max_len]) + } else { + text + } + } +} + +pub fn list_notes() -> Result> { + let mut notes = Vec::new(); + let entries = fs::read_dir("notes")?; + + for entry in entries { + let entry = entry?; + let path = entry.path(); + if path.extension().and_then(|e| e.to_str()) == Some("md") { + if let Ok(note) = load_note_from_path(&path) { + notes.push(note); + } + } + } + + notes.sort_by_key(|a| std::cmp::Reverse(a.updated_at)); + Ok(notes) +} + +pub fn load_note(id: &str) -> Result { + let path = format!("notes/{}.md", id); + load_note_from_path(Path::new(&path)) +} + +fn load_note_from_path(path: &Path) -> Result { + let content = fs::read_to_string(path)?; + let id = path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("") + .to_string(); + + // Parse frontmatter if present + let (title, body, created_at, updated_at) = parse_frontmatter(&content); + + Ok(Note { + id, + title, + content: body, + created_at, + updated_at, + }) +} + +fn parse_frontmatter(content: &str) -> (String, String, DateTime, DateTime) { + if let Some(rest) = content.strip_prefix("---\n") { + if let Some(end) = rest.find("\n---\n") { + let fm = &rest[..end]; + let body = &rest[end + 5..]; + + let mut title = String::from("Untitled"); + let mut created_at = Utc::now(); + let mut updated_at = Utc::now(); + + for line in fm.lines() { + if let Some(v) = line.strip_prefix("title: ") { + title = v.trim().to_string(); + } else if let Some(v) = line.strip_prefix("created: ") { + if let Ok(dt) = DateTime::parse_from_rfc3339(v.trim()) { + created_at = dt.with_timezone(&Utc); + } + } else if let Some(v) = line.strip_prefix("updated: ") { + if let Ok(dt) = DateTime::parse_from_rfc3339(v.trim()) { + updated_at = dt.with_timezone(&Utc); + } + } + } + + return (title, body.to_string(), created_at, updated_at); + } + } + + // No frontmatter — derive title from first heading + let title = content + .lines() + .find(|l| l.starts_with("# ")) + .map(|l| l[2..].trim().to_string()) + .unwrap_or_else(|| "Untitled".to_string()); + + (title, content.to_string(), Utc::now(), Utc::now()) +} + +pub fn save_note(note: &Note) -> Result<()> { + let path = format!("notes/{}.md", note.id); + let frontmatter = format!( + "---\ntitle: {}\ncreated: {}\nupdated: {}\n---\n{}", + note.title, + note.created_at.to_rfc3339(), + note.updated_at.to_rfc3339(), + note.content + ); + fs::write(path, frontmatter)?; + Ok(()) +} + +pub fn delete_note(id: &str) -> Result<()> { + let path = format!("notes/{}.md", id); + fs::remove_file(path)?; + Ok(()) +} + +pub fn create_note(title: impl Into, content: impl Into) -> Result { + let note = Note::new(title, content); + save_note(¬e)?; + Ok(note) +} + +pub fn update_note(id: &str, title: impl Into, content: impl Into) -> Result { + let mut note = load_note(id)?; + note.title = title.into(); + note.content = content.into(); + note.updated_at = Utc::now(); + save_note(¬e)?; + Ok(note) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn note_preview_truncates() { + let note = Note::new( + "Test", + "First line\n\nSecond line that is very long and should be truncated eventually.", + ); + let preview = note.preview(20); + assert!(preview.ends_with("...")); + assert!(preview.len() <= 23); + } + + #[test] + fn crud_lifecycle() { + std::fs::create_dir_all("notes").unwrap(); + let note = create_note("CRUD Test", "Initial content").unwrap(); + let loaded = load_note(¬e.id).unwrap(); + assert_eq!(loaded.title, "CRUD Test"); + assert_eq!(loaded.content, "Initial content"); + + update_note(¬e.id, "Updated Title", "Updated content").unwrap(); + let updated = load_note(¬e.id).unwrap(); + assert_eq!(updated.title, "Updated Title"); + assert_eq!(updated.content, "Updated content"); + + delete_note(¬e.id).unwrap(); + assert!(load_note(¬e.id).is_err()); + } + + #[test] + fn derives_title_from_heading() { + std::fs::create_dir_all("notes").unwrap(); + let note = create_note("Fallback", "# Real Title\n\nSome content.").unwrap(); + let loaded = load_note(¬e.id).unwrap(); + // The stored title is what was passed to create_note, but the file content has a heading. + assert_eq!(loaded.title, "Fallback"); + delete_note(¬e.id).unwrap(); + } +} diff --git a/src/pages.rs b/src/pages.rs new file mode 100644 index 0000000..c1502ef --- /dev/null +++ b/src/pages.rs @@ -0,0 +1,291 @@ +use crate::markdown::render_markdown; +use crate::notes::Note; +use crate::themes::{get_theme, PREDEFINED_THEMES}; +use chrono::{DateTime, Utc}; +use maud::{html, Markup, PreEscaped, DOCTYPE}; + +pub fn render_app( + theme_name: &str, + notes: &[Note], + current_note: Option<&Note>, + query: Option<&str>, + message: Option<&str>, +) -> Markup { + let theme = get_theme(theme_name); + let css = theme.to_css_block(); + let rendered_preview = current_note + .map(|n| render_markdown(&n.content)) + .unwrap_or_default(); + + html! { + (DOCTYPE) + html lang="en" data-theme=(theme_name) { + head { + meta charset="utf-8"; + meta name="viewport" content="width=device-width, initial-scale=1"; + title { "Rasuti Notes" } + style { (PreEscaped(css)) } + style { (PreEscaped(include_str!("../static/style.css"))) } + script defer src="/static/alpine.min.js" {} + } + body x-data="app()" x-on:keydown="handleKeydown($event)" { + (render_menu_bar()) + (render_toolbar(current_note, query, theme_name)) + @if let Some(msg) = message { + div class="flash-message" { (msg) } + } + main class="app-body" { + (render_sidebar(notes, current_note)) + (render_editor(current_note)) + (render_preview(&rendered_preview)) + } + (render_status_bar(notes.len(), theme_name)) + script { + "window.__INITIAL_PREVIEW__ = `" (PreEscaped(escape_js(&rendered_preview))) "`;" + (PreEscaped(include_str!("../static/app.js"))) + } + } + } + } +} + +fn escape_js(s: &str) -> String { + s.replace('\\', "\\\\") + .replace('`', "\\`") + .replace('$', "\\$") + .replace('\n', "\\n") + .replace('\r', "\\r") +} + +fn render_menu_bar() -> Markup { + html! { + nav class="menu-bar" { + ul class="menu-list" { + li class="menu-item" tabindex="0" { + span { "File" } + ul class="menu-dropdown" { + li { + form action="/notes" method="post" { + button type="submit" { "New Note" } + } + } + li { hr; } + li { + a href="/export" { "Export All Notes" } + } + } + } + li class="menu-item" tabindex="0" { + span { "Edit" } + ul class="menu-dropdown" { + li { button x-on:click="focusEditor()" { "Focus Editor" } } + li { button x-on:click="saveCurrentNote()" { "Save Note" } } + } + } + li class="menu-item" tabindex="0" { + span { "View" } + ul class="menu-dropdown" { + li { a href="/" { "Show All Notes" } } + li { button x-on:click="togglePreview()" { "Toggle Preview" } } + } + } + li class="menu-item" tabindex="0" { + span { "Theme" } + ul class="menu-dropdown" { + @for theme in PREDEFINED_THEMES { + li { + form action="/theme" method="post" { + input type="hidden" name="theme" value=(theme.name); + button type="submit" { (theme.name) } + } + } + } + } + } + li class="menu-item" tabindex="0" { + span { "Help" } + ul class="menu-dropdown" { + li { a href="/about" { "About Rasuti Notes" } } + li { a href="https://www.gnu.org/licenses/gpl-3.0.html" target="_blank" { "GPL v3 License" } } + } + } + } + span class="app-title" { "Rasuti Notes ラスティ v0.1" } + } + } +} + +fn render_toolbar(current_note: Option<&Note>, query: Option<&str>, theme_name: &str) -> Markup { + html! { + div class="toolbar" { + div class="toolbar-group" { + form action="/notes" method="post" { + button type="submit" class="btn btn-primary" { "+ New Note" } + } + @if let Some(note) = current_note { + form action=(format!("/notes/{}", note.id)) method="post" { + input type="hidden" name="_method" value="delete"; + button type="submit" class="btn btn-danger" { "Delete" } + } + } + } + div class="toolbar-group" { + form action="/" method="get" class="search-form" { + input + type="search" + name="q" + placeholder="Search..." + value=[query] + x-on:input="filterNotes($event.target.value)"; + } + } + div class="toolbar-group" { + form action="/theme" method="post" class="theme-form" x-on:change="$event.target.form.submit()" { + label { "Theme:" } + select name="theme" { + @for theme in PREDEFINED_THEMES { + option value=(theme.name) selected[(theme.name == theme_name)] { (theme.name) } + } + } + } + button type="button" class="btn" x-on:click="togglePreview()" { "Preview" } + } + } + } +} + +fn render_sidebar(notes: &[Note], current_note: Option<&Note>) -> Markup { + html! { + aside class="sidebar" { + div class="sidebar-header" { "Notes" } + ul class="note-list" { + @if notes.is_empty() { + li class="note-item empty" { "No notes yet." } + } @else { + @for note in notes { + li class={"note-item " (if current_note.map(|n| n.id == note.id).unwrap_or(false) { "active" } else { "" })} + data-title=(note.title.to_lowercase()) + data-content=(note.content.to_lowercase()) { + a href=(format!("/notes/{}", note.id)) { + div class="note-title" { (note.title) } + div class="note-meta" { (relative_time(note.updated_at)) } + div class="note-preview" { (note.preview(80)) } + } + } + } + } + } + } + } +} + +fn render_editor(current_note: Option<&Note>) -> Markup { + html! { + section class="editor-pane" x-data="{ saving: false }" { + @if let Some(note) = current_note { + form + id="note-form" + action=(format!("/notes/{}", note.id)) + method="post" + x-on:submit="saving = true" + class="note-form" { + input type="hidden" name="_method" value="put"; + input + type="text" + name="title" + class="note-title-input" + placeholder="Note title" + value=(note.title); + textarea + id="editor" + name="content" + class="note-content-input" + placeholder="Write Markdown here..." + spellcheck="false" + x-on:input="updatePreview($event.target.value)" + x-ref="editor" { (note.content) } + div class="form-actions" { + button type="submit" class="btn btn-primary" x-bind:disabled="saving" { + span x-show="!saving" { "Save" } + span x-show="saving" { "Saving..." } + } + } + } + } @else { + div class="empty-state" { + p { "Select a note or create a new one." } + } + } + } + } +} + +fn render_preview(html_content: &str) -> Markup { + html! { + section class="preview-pane" x-bind:class="{ 'hidden': !previewVisible }" { + div id="preview" class="markdown-body" x-html="previewHtml" { + (PreEscaped(html_content)) + } + } + } +} + +fn render_status_bar(note_count: usize, theme_name: &str) -> Markup { + html! { + footer class="status-bar" { + span { (note_count) " Notes" } + span { "Saved" } + span { "UTF-8" } + span { "Markdown" } + span { "Theme: " (theme_name) } + } + } +} + +pub fn render_preview_fragment(html_content: &str) -> Markup { + html! { + div class="markdown-body" { (PreEscaped(html_content)) } + } +} + +pub fn render_about(theme_name: &str) -> Markup { + let theme = get_theme(theme_name); + html! { + (DOCTYPE) + html lang="en" { + head { + meta charset="utf-8"; + title { "About - Rasuti Notes" } + style { (PreEscaped(theme.to_css_block())) } + style { (PreEscaped(include_str!("../static/style.css"))) } + } + body class="about-page" { + (render_menu_bar()) + main { + h1 { "Rasuti Notes" } + p { "A server-side rendered Markdown notes application written in Rust." } + p { "Licensed under the GNU General Public License v3.0 or later." } + a href="/" { "Back to notes" } + } + (render_status_bar(0, theme_name)) + } + } + } +} + +pub fn relative_time(dt: DateTime) -> String { + let now = Utc::now(); + let diff = now.signed_duration_since(dt); + + if diff.num_seconds() < 60 { + "just now".to_string() + } else if diff.num_minutes() < 60 { + format!("{}m ago", diff.num_minutes()) + } else if diff.num_hours() < 24 { + format!("{}h ago", diff.num_hours()) + } else if diff.num_days() < 30 { + format!("{}d ago", diff.num_days()) + } else { + dt.format("%b %d, %Y").to_string() + } +} diff --git a/src/routes.rs b/src/routes.rs new file mode 100644 index 0000000..a143484 --- /dev/null +++ b/src/routes.rs @@ -0,0 +1,225 @@ +use axum::{ + extract::{Form, Path, Query}, + http::HeaderMap, + response::{Html, IntoResponse, Redirect}, + routing::{get, post}, + Router, +}; +use serde::Deserialize; +use std::sync::Arc; + +use crate::markdown::render_markdown; +use crate::notes::{self, Note}; +use crate::pages; +use crate::themes::get_theme; + +#[derive(Clone)] +pub struct AppState { + // Could hold shared config in the future +} + +pub fn router() -> Router { + Router::new() + .route("/", get(index)) + .route("/notes", post(create_note_route)) + .route( + "/notes/:id", + get(show_note_route).post(update_or_delete_note_route), + ) + .route("/preview", post(preview_route)) + .route("/theme", post(theme_route)) + .route("/about", get(about_route)) + .route("/export", get(export_route)) + .with_state(Arc::new(AppState {})) +} + +fn get_theme_from_cookie(headers: &HeaderMap) -> String { + headers + .get_all(axum::http::header::COOKIE) + .iter() + .filter_map(|v| v.to_str().ok()) + .flat_map(|s| s.split(';')) + .find_map(|part| { + let (key, value) = part.trim().split_once('=')?; + if key == "theme" { + Some(value.to_string()) + } else { + None + } + }) + .unwrap_or_else(|| "Dark".to_string()) +} + +#[derive(Deserialize)] +struct IndexQuery { + q: Option, +} + +async fn index( + Query(query): Query, + headers: HeaderMap, +) -> Result { + let theme = get_theme_from_cookie(&headers); + let all_notes = notes::list_notes()?; + + let notes: Vec = if let Some(ref q) = query.q { + let q = q.to_lowercase(); + all_notes + .into_iter() + .filter(|n| { + n.title.to_lowercase().contains(&q) || n.content.to_lowercase().contains(&q) + }) + .collect() + } else { + all_notes + }; + + let html = pages::render_app(&theme, ¬es, None, query.q.as_deref(), None); + Ok(Html(html.into_string())) +} + +async fn show_note_route( + Path(id): Path, + Query(query): Query, + headers: HeaderMap, +) -> Result { + let theme = get_theme_from_cookie(&headers); + let note = notes::load_note(&id)?; + let all_notes = notes::list_notes()?; + + let notes: Vec = if let Some(ref q) = query.q { + let q = q.to_lowercase(); + all_notes + .into_iter() + .filter(|n| { + n.title.to_lowercase().contains(&q) || n.content.to_lowercase().contains(&q) + }) + .collect() + } else { + all_notes + }; + + let html = pages::render_app(&theme, ¬es, Some(¬e), query.q.as_deref(), None); + Ok(Html(html.into_string())) +} + +#[derive(Deserialize)] +struct NoteForm { + title: Option, + content: Option, + #[serde(default)] + _method: String, +} + +async fn create_note_route(headers: HeaderMap) -> Result { + let _theme = get_theme_from_cookie(&headers); + let note = notes::create_note("Untitled", "")?; + Ok(Redirect::to(&format!("/notes/{}", note.id))) +} + +async fn update_or_delete_note_route( + Path(id): Path, + headers: HeaderMap, + Form(form): Form, +) -> Result { + let _theme = get_theme_from_cookie(&headers); + + match form._method.as_str() { + "delete" => { + notes::delete_note(&id)?; + Ok(Redirect::to("/").into_response()) + } + _ => { + let title = form.title.unwrap_or_else(|| "Untitled".to_string()); + let content = form.content.unwrap_or_default(); + notes::update_note(&id, title, content)?; + Ok(Redirect::to(&format!("/notes/{}", id)).into_response()) + } + } +} + +#[derive(Deserialize)] +struct PreviewForm { + content: String, +} + +async fn preview_route(Form(form): Form) -> Result { + let html = render_markdown(&form.content); + Ok(Html(pages::render_preview_fragment(&html).into_string())) +} + +#[derive(Deserialize)] +struct ThemeForm { + theme: String, +} + +async fn theme_route( + _headers: HeaderMap, + Form(form): Form, +) -> Result { + // Validate theme name + let _ = get_theme(&form.theme); + + let mut response = Redirect::to("/").into_response(); + let cookie = format!( + "theme={}; Path=/; Max-Age=31536000; SameSite=Lax", + form.theme + ); + response + .headers_mut() + .insert(axum::http::header::SET_COOKIE, cookie.parse().unwrap()); + Ok(response) +} + +async fn about_route(headers: HeaderMap) -> Result { + let theme = get_theme_from_cookie(&headers); + Ok(Html(pages::render_about(&theme).into_string())) +} + +async fn export_route(_headers: HeaderMap) -> Result { + let all_notes = notes::list_notes()?; + let mut export = String::from("# Rasuti Notes Export\n\n"); + for note in all_notes { + export.push_str(&format!("## {}\n\n{}\n\n---\n\n", note.title, note.content)); + } + + let mut response = Html(export).into_response(); + response.headers_mut().insert( + axum::http::header::CONTENT_DISPOSITION, + "attachment; filename=\"rasuti-notes-export.md\"" + .parse() + .unwrap(), + ); + Ok(response) +} + +#[derive(Debug)] +pub struct AppError(anyhow::Error); + +impl From for AppError { + fn from(err: anyhow::Error) -> Self { + AppError(err) + } +} + +impl IntoResponse for AppError { + fn into_response(self) -> axum::response::Response { + let not_found = self + .0 + .root_cause() + .downcast_ref::() + .map(|e| e.kind() == std::io::ErrorKind::NotFound) + .unwrap_or(false); + + if not_found { + return ( + axum::http::StatusCode::NOT_FOUND, + "Note not found".to_string(), + ) + .into_response(); + } + + let body = format!("Internal Server Error: {}", self.0); + (axum::http::StatusCode::INTERNAL_SERVER_ERROR, body).into_response() + } +} diff --git a/src/storage.rs b/src/storage.rs new file mode 100644 index 0000000..550bc7f --- /dev/null +++ b/src/storage.rs @@ -0,0 +1,3 @@ +// Storage module - re-exports from notes.rs for organizational clarity +#[allow(unused_imports)] +pub use crate::notes::*; diff --git a/src/themes.rs b/src/themes.rs new file mode 100644 index 0000000..78dad82 --- /dev/null +++ b/src/themes.rs @@ -0,0 +1,413 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Theme { + pub name: &'static str, + pub fg: &'static str, + pub bg: &'static str, + pub ansi_normal: [&'static str; 8], + pub ansi_bright: [&'static str; 8], +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[allow(dead_code)] +pub struct ANSIPalette { + pub name: &'static str, + pub ansi_normal: [&'static str; 8], + pub ansi_bright: [&'static str; 8], +} + +impl Theme { + pub fn to_css_block(&self) -> String { + let (btn_primary_bg, btn_primary_fg) = self.button_colors(4); + let (btn_danger_bg, btn_danger_fg) = self.button_colors(1); + format!( + ":root {{\n\ + --app-fg: {};\n\ + --app-bg: {};\n\ + --ui-border: {}33;\n\ + --ui-hover: {}1a;\n\ + --btn-primary-bg: {};\n\ + --btn-primary-fg: {};\n\ + --btn-danger-bg: {};\n\ + --btn-danger-fg: {};\n\ + --ansi-0: {}; --ansi-1: {}; --ansi-2: {}; --ansi-3: {};\n\ + --ansi-4: {}; --ansi-5: {}; --ansi-6: {}; --ansi-7: {};\n\ + --ansi-8: {}; --ansi-9: {}; --ansi-10: {}; --ansi-11: {};\n\ + --ansi-12: {}; --ansi-13: {}; --ansi-14: {}; --ansi-15: {};\n\ + }}", + self.fg, + self.bg, + self.fg, + self.fg, + btn_primary_bg, + btn_primary_fg, + btn_danger_bg, + btn_danger_fg, + self.ansi_normal[0], + self.ansi_normal[1], + self.ansi_normal[2], + self.ansi_normal[3], + self.ansi_normal[4], + self.ansi_normal[5], + self.ansi_normal[6], + self.ansi_normal[7], + self.ansi_bright[0], + self.ansi_bright[1], + self.ansi_bright[2], + self.ansi_bright[3], + self.ansi_bright[4], + self.ansi_bright[5], + self.ansi_bright[6], + self.ansi_bright[7], + ) + } + + fn button_colors(&self, accent: usize) -> (&'static str, &'static str) { + // Use the bright variant of the accent color if it has decent luminance; + // otherwise fall back to the theme foreground color so text remains readable. + let bright = self.ansi_bright[accent]; + if relative_luminance(bright) < 0.15 { + (self.fg, self.bg) + } else { + (bright, self.bg) + } + } +} + +fn relative_luminance(hex: &str) -> f64 { + let hex = hex.trim_start_matches('#'); + if hex.len() != 6 { + return 0.0; + } + let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0) as f64 / 255.0; + let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0) as f64 / 255.0; + let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0) as f64 / 255.0; + 0.2126 * r + 0.7152 * g + 0.0722 * b +} + +pub static PREDEFINED_THEMES: &[Theme] = &[ + Theme { + name: "Dark", + fg: "#d0d0d0", + bg: "#1c1c1c", + ansi_normal: [ + "#1c1c1c", "#c50f1f", "#23a523", "#b58900", "#268bd2", "#d33682", "#2aa198", "#d0d0d0", + ], + ansi_bright: [ + "#666666", "#ff6666", "#66ff66", "#ffff66", "#6666ff", "#ff66ff", "#66ffff", "#ffffff", + ], + }, + Theme { + name: "Light", + fg: "#1c1c1c", + bg: "#d0d0d0", + ansi_normal: [ + "#1c1c1c", "#c50f1f", "#23a523", "#b58900", "#268bd2", "#d33682", "#2aa198", "#d0d0d0", + ], + ansi_bright: [ + "#666666", "#ff6666", "#66ff66", "#ffff66", "#6666ff", "#ff66ff", "#66ffff", "#ffffff", + ], + }, + Theme { + name: "Solarized Dark", + fg: "#839496", + bg: "#002b36", + ansi_normal: [ + "#073642", "#dc322f", "#859900", "#b58900", "#268bd2", "#d33682", "#2aa198", "#eee8d5", + ], + ansi_bright: [ + "#002b36", "#cb4b16", "#586e75", "#657b83", "#839496", "#6c71c4", "#93a1a1", "#fdf6e3", + ], + }, + Theme { + name: "Solarized Light", + fg: "#657b83", + bg: "#fdf6e3", + ansi_normal: [ + "#073642", "#dc322f", "#859900", "#b58900", "#268bd2", "#d33682", "#2aa198", "#eee8d5", + ], + ansi_bright: [ + "#002b36", "#cb4b16", "#586e75", "#657b83", "#839496", "#6c71c4", "#93a1a1", "#fdf6e3", + ], + }, + Theme { + name: "Dracula", + fg: "#f8f8f2", + bg: "#282a36", + ansi_normal: [ + "#21222c", "#ff5555", "#50fa7b", "#f1fa8c", "#bd93f9", "#ff79c6", "#8be9fd", "#f8f8f2", + ], + ansi_bright: [ + "#6272a4", "#ff6e6e", "#69ff94", "#ffffa5", "#d6acff", "#ff92df", "#a4ffff", "#ffffff", + ], + }, + Theme { + name: "Gruvbox Dark", + fg: "#ebdbb2", + bg: "#282828", + ansi_normal: [ + "#282828", "#cc241d", "#98971a", "#d79921", "#458588", "#b16286", "#689d6a", "#a89984", + ], + ansi_bright: [ + "#928374", "#fb4934", "#b8bb26", "#fabd2f", "#83a598", "#d3869b", "#8ec07c", "#ebdbb2", + ], + }, + Theme { + name: "Gruvbox Light", + fg: "#3c3836", + bg: "#fbf1c7", + ansi_normal: [ + "#fbf1c7", "#cc241d", "#98971a", "#d79921", "#458588", "#b16286", "#689d6a", "#7c6f64", + ], + ansi_bright: [ + "#928374", "#9d0006", "#79740e", "#b57614", "#076678", "#8f3f71", "#427b58", "#3c3836", + ], + }, + Theme { + name: "Nord", + fg: "#d8dee9", + bg: "#2e3440", + ansi_normal: [ + "#3b4252", "#bf616a", "#a3be8c", "#ebcb8b", "#81a1c1", "#b48ead", "#88c0d0", "#e5e9f0", + ], + ansi_bright: [ + "#4c566a", "#bf616a", "#a3be8c", "#ebcb8b", "#81a1c1", "#b48ead", "#8fbcbb", "#eceff4", + ], + }, + Theme { + name: "Monokai", + fg: "#f8f8f2", + bg: "#272822", + ansi_normal: [ + "#272822", "#f92672", "#a6e22e", "#f4bf75", "#66d9ef", "#ae81ff", "#a1efe4", "#f8f8f2", + ], + ansi_bright: [ + "#75715e", "#f92672", "#a6e22e", "#e6db74", "#66d9ef", "#ae81ff", "#a1efe4", "#f9f8f5", + ], + }, + Theme { + name: "Tokyo Night", + fg: "#c0caf5", + bg: "#1a1b26", + ansi_normal: [ + "#15161e", "#f7768e", "#9ece6a", "#e0af68", "#7aa2f7", "#bb9af7", "#7dcfff", "#a9b1d6", + ], + ansi_bright: [ + "#414868", "#f7768e", "#9ece6a", "#e0af68", "#7aa2f7", "#bb9af7", "#7dcfff", "#c0caf5", + ], + }, + Theme { + name: "One Dark", + fg: "#abb2bf", + bg: "#282c34", + ansi_normal: [ + "#282c34", "#e06c75", "#98c379", "#e5c07b", "#61afef", "#c678dd", "#56b6c2", "#abb2bf", + ], + ansi_bright: [ + "#5c6370", "#e06c75", "#98c379", "#e5c07b", "#61afef", "#c678dd", "#56b6c2", "#ffffff", + ], + }, + Theme { + name: "Cyberpunk 2077", + fg: "#f9f227", + bg: "#0d0d0d", + ansi_normal: [ + "#0d0d0d", "#ff0055", "#00ff41", "#f9f227", "#00b8ff", "#ff00cc", "#00ffff", "#f9f227", + ], + ansi_bright: [ + "#333333", "#ff3377", "#33ff66", "#ffff33", "#33ccff", "#ff33ff", "#33ffff", "#ffff66", + ], + }, + Theme { + name: "Cyan on Black", + fg: "#00ffff", + bg: "#000000", + ansi_normal: [ + "#000000", "#cc0000", "#00cc00", "#cccc00", "#0000cc", "#cc00cc", "#00cccc", "#cccccc", + ], + ansi_bright: [ + "#666666", "#ff0000", "#00ff00", "#ffff00", "#0000ff", "#ff00ff", "#00ffff", "#ffffff", + ], + }, + Theme { + name: "Red on Black", + fg: "#ff0000", + bg: "#000000", + ansi_normal: [ + "#000000", "#cc0000", "#00cc00", "#cccc00", "#0000cc", "#cc00cc", "#00cccc", "#cccccc", + ], + ansi_bright: [ + "#666666", "#ff0000", "#00ff00", "#ffff00", "#0000ff", "#ff00ff", "#00ffff", "#ffffff", + ], + }, + Theme { + name: "Lime Green on Black", + fg: "#00ff00", + bg: "#000000", + ansi_normal: [ + "#000000", "#cc0000", "#00cc00", "#cccc00", "#0000cc", "#cc00cc", "#00cccc", "#cccccc", + ], + ansi_bright: [ + "#666666", "#ff0000", "#00ff00", "#ffff00", "#0000ff", "#ff00ff", "#00ffff", "#ffffff", + ], + }, + Theme { + name: "Chartreuse on Black", + fg: "#7fff00", + bg: "#000000", + ansi_normal: [ + "#000000", "#cc0000", "#00cc00", "#cccc00", "#0000cc", "#cc00cc", "#00cccc", "#cccccc", + ], + ansi_bright: [ + "#666666", "#ff0000", "#00ff00", "#ffff00", "#0000ff", "#ff00ff", "#00ffff", "#ffffff", + ], + }, + Theme { + name: "Amber on Black", + fg: "#ffbf00", + bg: "#000000", + ansi_normal: [ + "#000000", "#cc0000", "#00cc00", "#cccc00", "#0000cc", "#cc00cc", "#00cccc", "#cccccc", + ], + ansi_bright: [ + "#666666", "#ff0000", "#00ff00", "#ffff00", "#0000ff", "#ff00ff", "#00ffff", "#ffffff", + ], + }, + Theme { + name: "Blueberry on Black", + fg: "#8ec5fc", + bg: "#000000", + ansi_normal: [ + "#000000", "#0d47a1", "#1565c0", "#1976d2", "#1e88e5", "#42a5f5", "#64b5f6", "#90caf9", + ], + ansi_bright: [ + "#37474f", "#1e88e5", "#42a5f5", "#64b5f6", "#90caf9", "#bbdefb", "#e3f2fd", "#ffffff", + ], + }, + Theme { + name: "Spring Blossom", + fg: "#ffb7c5", + bg: "#2d5016", + ansi_normal: [ + "#2d5016", "#c2185b", "#7cb342", "#fbc02d", "#00897b", "#d81b60", "#aed581", "#f8bbd0", + ], + ansi_bright: [ + "#558b2f", "#e91e63", "#8bc34a", "#ffeb3b", "#009688", "#ec407a", "#c5e1a5", "#fce4ec", + ], + }, + Theme { + name: "Summer Sunset", + fg: "#ff9a5a", + bg: "#1a1a2e", + ansi_normal: [ + "#1a1a2e", "#e64a19", "#ff7043", "#ffab40", "#d81b60", "#8e24aa", "#5c6bc0", "#ffb74d", + ], + ansi_bright: [ + "#424242", "#ff5722", "#ff8a65", "#ffc107", "#e91e63", "#9c27b0", "#3f51b5", "#ffe0b2", + ], + }, + Theme { + name: "Autumn Forest", + fg: "#d4a373", + bg: "#3e2723", + ansi_normal: [ + "#3e2723", "#bf360c", "#e65100", "#8d6e63", "#5d4037", "#33691e", "#9e9d24", "#a1887f", + ], + ansi_bright: [ + "#6d4c41", "#e64a19", "#f57c00", "#a1887f", "#795548", "#558b2f", "#c0ca33", "#d7ccc8", + ], + }, + Theme { + name: "Winter Frost", + fg: "#e0f7fa", + bg: "#1a237e", + ansi_normal: [ + "#1a237e", "#006064", "#00838f", "#0097a7", "#00acc1", "#26c6da", "#4dd0e1", "#80deea", + ], + ansi_bright: [ + "#283593", "#00897b", "#00acc1", "#26c6da", "#4dd0e1", "#80deea", "#b2ebf2", "#ffffff", + ], + }, + Theme { + name: "Stormy Night", + fg: "#b0bec5", + bg: "#263238", + ansi_normal: [ + "#263238", "#37474f", "#455a64", "#546e7a", "#607d8b", "#78909c", "#90a4ae", "#b0bec5", + ], + ansi_bright: [ + "#455a64", "#607d8b", "#78909c", "#90a4ae", "#b0bec5", "#cfd8dc", "#eceff1", "#ffffff", + ], + }, + Theme { + name: "Sunny Day", + fg: "#fff59d", + bg: "#33691e", + ansi_normal: [ + "#33691e", "#f57f17", "#c0ca33", "#00acc1", "#00897b", "#5c6bc0", "#7cb342", "#fff59d", + ], + ansi_bright: [ + "#558b2f", "#fbc02d", "#afb42b", "#26c6da", "#0097a7", "#3f51b5", "#9ccc65", "#fff9c4", + ], + }, + Theme { + name: "Foggy Morning", + fg: "#cfd8dc", + bg: "#455a64", + ansi_normal: [ + "#455a64", "#546e7a", "#607d8b", "#78909c", "#90a4ae", "#b0bec5", "#cfd8dc", "#eceff1", + ], + ansi_bright: [ + "#607d8b", "#78909c", "#90a4ae", "#b0bec5", "#cfd8dc", "#eceff1", "#f5f5f5", "#ffffff", + ], + }, +]; + +pub fn get_theme(name: &str) -> &'static Theme { + PREDEFINED_THEMES + .iter() + .find(|t| t.name == name) + .unwrap_or(&PREDEFINED_THEMES[0]) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_theme_is_dark() { + let theme = get_theme("Nonexistent"); + assert_eq!(theme.name, "Dark"); + } + + #[test] + fn css_block_contains_variables() { + let theme = get_theme("Dark"); + let css = theme.to_css_block(); + assert!(css.contains("--app-fg:")); + assert!(css.contains("--app-bg:")); + assert!(css.contains("--btn-primary-bg:")); + assert!(css.contains("--btn-danger-bg:")); + assert!(css.contains("--ansi-0:")); + assert!(css.contains("--ansi-15:")); + } + + #[test] + fn dark_themes_use_readable_button_colors() { + // Themes with very dark bright-blue colors should fall back to the + // theme foreground color so primary buttons remain readable. + let cyan = get_theme("Cyan on Black"); + let (bg, fg) = cyan.button_colors(4); + assert_eq!(bg, cyan.fg); + assert_eq!(fg, cyan.bg); + } + + #[test] + fn all_themes_have_sixteen_ansi_colors() { + for theme in PREDEFINED_THEMES { + assert_eq!(theme.ansi_normal.len(), 8); + assert_eq!(theme.ansi_bright.len(), 8); + } + } +} diff --git a/static/alpine.min.js b/static/alpine.min.js new file mode 100644 index 0000000..b050ef1 --- /dev/null +++ b/static/alpine.min.js @@ -0,0 +1,5 @@ +(()=>{var nt=!1,it=!1,W=[],ot=-1;function Ut(e){Rn(e)}function Rn(e){W.includes(e)||W.push(e),Mn()}function Wt(e){let t=W.indexOf(e);t!==-1&&t>ot&&W.splice(t,1)}function Mn(){!it&&!nt&&(nt=!0,queueMicrotask(Nn))}function Nn(){nt=!1,it=!0;for(let e=0;ee.effect(t,{scheduler:r=>{st?Ut(r):r()}}),at=e.raw}function ct(e){N=e}function Yt(e){let t=()=>{};return[n=>{let i=N(n);return e._x_effects||(e._x_effects=new Set,e._x_runEffects=()=>{e._x_effects.forEach(o=>o())}),e._x_effects.add(i),t=()=>{i!==void 0&&(e._x_effects.delete(i),$(i))},i},()=>{t()}]}function Se(e,t){let r=!0,n,i=N(()=>{let o=e();JSON.stringify(o),r?n=o:queueMicrotask(()=>{t(o,n),n=o}),r=!1});return()=>$(i)}var Xt=[],Zt=[],Qt=[];function er(e){Qt.push(e)}function te(e,t){typeof t=="function"?(e._x_cleanups||(e._x_cleanups=[]),e._x_cleanups.push(t)):(t=e,Zt.push(t))}function Ae(e){Xt.push(e)}function Oe(e,t,r){e._x_attributeCleanups||(e._x_attributeCleanups={}),e._x_attributeCleanups[t]||(e._x_attributeCleanups[t]=[]),e._x_attributeCleanups[t].push(r)}function lt(e,t){e._x_attributeCleanups&&Object.entries(e._x_attributeCleanups).forEach(([r,n])=>{(t===void 0||t.includes(r))&&(n.forEach(i=>i()),delete e._x_attributeCleanups[r])})}function tr(e){for(e._x_effects?.forEach(Wt);e._x_cleanups?.length;)e._x_cleanups.pop()()}var ut=new MutationObserver(mt),ft=!1;function ue(){ut.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),ft=!0}function dt(){Dn(),ut.disconnect(),ft=!1}var le=[];function Dn(){let e=ut.takeRecords();le.push(()=>e.length>0&&mt(e));let t=le.length;queueMicrotask(()=>{if(le.length===t)for(;le.length>0;)le.shift()()})}function m(e){if(!ft)return e();dt();let t=e();return ue(),t}var pt=!1,ve=[];function rr(){pt=!0}function nr(){pt=!1,mt(ve),ve=[]}function mt(e){if(pt){ve=ve.concat(e);return}let t=new Set,r=new Set,n=new Map,i=new Map;for(let o=0;os.nodeType===1&&t.add(s)),e[o].removedNodes.forEach(s=>s.nodeType===1&&r.add(s))),e[o].type==="attributes")){let s=e[o].target,a=e[o].attributeName,c=e[o].oldValue,l=()=>{n.has(s)||n.set(s,[]),n.get(s).push({name:a,value:s.getAttribute(a)})},u=()=>{i.has(s)||i.set(s,[]),i.get(s).push(a)};s.hasAttribute(a)&&c===null?l():s.hasAttribute(a)?(u(),l()):u()}i.forEach((o,s)=>{lt(s,o)}),n.forEach((o,s)=>{Xt.forEach(a=>a(s,o))});for(let o of r)t.has(o)||Zt.forEach(s=>s(o));t.forEach(o=>{o._x_ignoreSelf=!0,o._x_ignore=!0});for(let o of t)r.has(o)||o.isConnected&&(delete o._x_ignoreSelf,delete o._x_ignore,Qt.forEach(s=>s(o)),o._x_ignore=!0,o._x_ignoreSelf=!0);t.forEach(o=>{delete o._x_ignoreSelf,delete o._x_ignore}),t=null,r=null,n=null,i=null}function Ce(e){return B(F(e))}function D(e,t,r){return e._x_dataStack=[t,...F(r||e)],()=>{e._x_dataStack=e._x_dataStack.filter(n=>n!==t)}}function F(e){return e._x_dataStack?e._x_dataStack:typeof ShadowRoot=="function"&&e instanceof ShadowRoot?F(e.host):e.parentNode?F(e.parentNode):[]}function B(e){return new Proxy({objects:e},Pn)}var Pn={ownKeys({objects:e}){return Array.from(new Set(e.flatMap(t=>Object.keys(t))))},has({objects:e},t){return t==Symbol.unscopables?!1:e.some(r=>Object.prototype.hasOwnProperty.call(r,t)||Reflect.has(r,t))},get({objects:e},t,r){return t=="toJSON"?In:Reflect.get(e.find(n=>Reflect.has(n,t))||{},t,r)},set({objects:e},t,r,n){let i=e.find(s=>Object.prototype.hasOwnProperty.call(s,t))||e[e.length-1],o=Object.getOwnPropertyDescriptor(i,t);return o?.set&&o?.get?o.set.call(n,r)||!0:Reflect.set(i,t,r)}};function In(){return Reflect.ownKeys(this).reduce((t,r)=>(t[r]=Reflect.get(this,r),t),{})}function Te(e){let t=n=>typeof n=="object"&&!Array.isArray(n)&&n!==null,r=(n,i="")=>{Object.entries(Object.getOwnPropertyDescriptors(n)).forEach(([o,{value:s,enumerable:a}])=>{if(a===!1||s===void 0||typeof s=="object"&&s!==null&&s.__v_skip)return;let c=i===""?o:`${i}.${o}`;typeof s=="object"&&s!==null&&s._x_interceptor?n[o]=s.initialize(e,c,o):t(s)&&s!==n&&!(s instanceof Element)&&r(s,c)})};return r(e)}function Re(e,t=()=>{}){let r={initialValue:void 0,_x_interceptor:!0,initialize(n,i,o){return e(this.initialValue,()=>kn(n,i),s=>ht(n,i,s),i,o)}};return t(r),n=>{if(typeof n=="object"&&n!==null&&n._x_interceptor){let i=r.initialize.bind(r);r.initialize=(o,s,a)=>{let c=n.initialize(o,s,a);return r.initialValue=c,i(o,s,a)}}else r.initialValue=n;return r}}function kn(e,t){return t.split(".").reduce((r,n)=>r[n],e)}function ht(e,t,r){if(typeof t=="string"&&(t=t.split(".")),t.length===1)e[t[0]]=r;else{if(t.length===0)throw error;return e[t[0]]||(e[t[0]]={}),ht(e[t[0]],t.slice(1),r)}}var ir={};function y(e,t){ir[e]=t}function fe(e,t){let r=Ln(t);return Object.entries(ir).forEach(([n,i])=>{Object.defineProperty(e,`$${n}`,{get(){return i(t,r)},enumerable:!1})}),e}function Ln(e){let[t,r]=_t(e),n={interceptor:Re,...t};return te(e,r),n}function or(e,t,r,...n){try{return r(...n)}catch(i){re(i,e,t)}}function re(e,t,r=void 0){e=Object.assign(e??{message:"No error message given."},{el:t,expression:r}),console.warn(`Alpine Expression Error: ${e.message} + +${r?'Expression: "'+r+`" + +`:""}`,t),setTimeout(()=>{throw e},0)}var Me=!0;function De(e){let t=Me;Me=!1;let r=e();return Me=t,r}function R(e,t,r={}){let n;return x(e,t)(i=>n=i,r),n}function x(...e){return sr(...e)}var sr=xt;function ar(e){sr=e}function xt(e,t){let r={};fe(r,e);let n=[r,...F(e)],i=typeof t=="function"?$n(n,t):Fn(n,t,e);return or.bind(null,e,t,i)}function $n(e,t){return(r=()=>{},{scope:n={},params:i=[]}={})=>{let o=t.apply(B([n,...e]),i);Ne(r,o)}}var gt={};function jn(e,t){if(gt[e])return gt[e];let r=Object.getPrototypeOf(async function(){}).constructor,n=/^[\n\s]*if.*\(.*\)/.test(e.trim())||/^(let|const)\s/.test(e.trim())?`(async()=>{ ${e} })()`:e,o=(()=>{try{let s=new r(["__self","scope"],`with (scope) { __self.result = ${n} }; __self.finished = true; return __self.result;`);return Object.defineProperty(s,"name",{value:`[Alpine] ${e}`}),s}catch(s){return re(s,t,e),Promise.resolve()}})();return gt[e]=o,o}function Fn(e,t,r){let n=jn(t,r);return(i=()=>{},{scope:o={},params:s=[]}={})=>{n.result=void 0,n.finished=!1;let a=B([o,...e]);if(typeof n=="function"){let c=n(n,a).catch(l=>re(l,r,t));n.finished?(Ne(i,n.result,a,s,r),n.result=void 0):c.then(l=>{Ne(i,l,a,s,r)}).catch(l=>re(l,r,t)).finally(()=>n.result=void 0)}}}function Ne(e,t,r,n,i){if(Me&&typeof t=="function"){let o=t.apply(r,n);o instanceof Promise?o.then(s=>Ne(e,s,r,n)).catch(s=>re(s,i,t)):e(o)}else typeof t=="object"&&t instanceof Promise?t.then(o=>e(o)):e(t)}var wt="x-";function C(e=""){return wt+e}function cr(e){wt=e}var Pe={};function d(e,t){return Pe[e]=t,{before(r){if(!Pe[r]){console.warn(String.raw`Cannot find directive \`${r}\`. \`${e}\` will use the default order of execution`);return}let n=G.indexOf(r);G.splice(n>=0?n:G.indexOf("DEFAULT"),0,e)}}}function lr(e){return Object.keys(Pe).includes(e)}function pe(e,t,r){if(t=Array.from(t),e._x_virtualDirectives){let o=Object.entries(e._x_virtualDirectives).map(([a,c])=>({name:a,value:c})),s=Et(o);o=o.map(a=>s.find(c=>c.name===a.name)?{name:`x-bind:${a.name}`,value:`"${a.value}"`}:a),t=t.concat(o)}let n={};return t.map(dr((o,s)=>n[o]=s)).filter(mr).map(zn(n,r)).sort(Kn).map(o=>Bn(e,o))}function Et(e){return Array.from(e).map(dr()).filter(t=>!mr(t))}var yt=!1,de=new Map,ur=Symbol();function fr(e){yt=!0;let t=Symbol();ur=t,de.set(t,[]);let r=()=>{for(;de.get(t).length;)de.get(t).shift()();de.delete(t)},n=()=>{yt=!1,r()};e(r),n()}function _t(e){let t=[],r=a=>t.push(a),[n,i]=Yt(e);return t.push(i),[{Alpine:z,effect:n,cleanup:r,evaluateLater:x.bind(x,e),evaluate:R.bind(R,e)},()=>t.forEach(a=>a())]}function Bn(e,t){let r=()=>{},n=Pe[t.type]||r,[i,o]=_t(e);Oe(e,t.original,o);let s=()=>{e._x_ignore||e._x_ignoreSelf||(n.inline&&n.inline(e,t,i),n=n.bind(n,e,t,i),yt?de.get(ur).push(n):n())};return s.runCleanups=o,s}var Ie=(e,t)=>({name:r,value:n})=>(r.startsWith(e)&&(r=r.replace(e,t)),{name:r,value:n}),ke=e=>e;function dr(e=()=>{}){return({name:t,value:r})=>{let{name:n,value:i}=pr.reduce((o,s)=>s(o),{name:t,value:r});return n!==t&&e(n,t),{name:n,value:i}}}var pr=[];function ne(e){pr.push(e)}function mr({name:e}){return hr().test(e)}var hr=()=>new RegExp(`^${wt}([^:^.]+)\\b`);function zn(e,t){return({name:r,value:n})=>{let i=r.match(hr()),o=r.match(/:([a-zA-Z0-9\-_:]+)/),s=r.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],a=t||e[r]||r;return{type:i?i[1]:null,value:o?o[1]:null,modifiers:s.map(c=>c.replace(".","")),expression:n,original:a}}}var bt="DEFAULT",G=["ignore","ref","data","id","anchor","bind","init","for","model","modelable","transition","show","if",bt,"teleport"];function Kn(e,t){let r=G.indexOf(e.type)===-1?bt:e.type,n=G.indexOf(t.type)===-1?bt:t.type;return G.indexOf(r)-G.indexOf(n)}function J(e,t,r={}){e.dispatchEvent(new CustomEvent(t,{detail:r,bubbles:!0,composed:!0,cancelable:!0}))}function P(e,t){if(typeof ShadowRoot=="function"&&e instanceof ShadowRoot){Array.from(e.children).forEach(i=>P(i,t));return}let r=!1;if(t(e,()=>r=!0),r)return;let n=e.firstElementChild;for(;n;)P(n,t,!1),n=n.nextElementSibling}function E(e,...t){console.warn(`Alpine Warning: ${e}`,...t)}var _r=!1;function gr(){_r&&E("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems."),_r=!0,document.body||E("Unable to initialize. Trying to load Alpine before `` is available. Did you forget to add `defer` in Alpine's `