From ac5685d473d0245796f8bcb764ed0a730f70d0db Mon Sep 17 00:00:00 2001 From: Orual Date: Sun, 9 Nov 2025 19:45:52 -0500 Subject: [PATCH] pretty record view --- Cargo.lock | 11 + crates/weaver-app/Cargo.toml | 2 + .../weaver-app/assets/styling/record-view.css | 332 +++++++++++++ crates/weaver-app/src/components/mod.rs | 2 +- crates/weaver-app/src/main.rs | 4 +- crates/weaver-app/src/views/mod.rs | 3 + crates/weaver-app/src/views/record.rs | 436 ++++++++++++++++++ crates/weaver-renderer/src/code_pretty.rs | 38 ++ crates/weaver-renderer/src/css.rs | 48 ++ 9 files changed, 874 insertions(+), 2 deletions(-) create mode 100644 crates/weaver-app/assets/styling/record-view.css create mode 100644 crates/weaver-app/src/views/record.rs diff --git a/Cargo.lock b/Cargo.lock index 37ca3ec..dee9b57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3658,6 +3658,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + [[package]] name = "hyper" version = "1.7.0" @@ -8634,6 +8643,8 @@ dependencies = [ "dioxus", "dioxus-free-icons", "dioxus-primitives", + "hex_fmt", + "humansize", "jacquard", "jacquard-axum", "js-sys", diff --git a/crates/weaver-app/Cargo.toml b/crates/weaver-app/Cargo.toml index 4e7f550..7a97abc 100644 --- a/crates/weaver-app/Cargo.toml +++ b/crates/weaver-app/Cargo.toml @@ -37,6 +37,8 @@ mime-sniffer = {version = "^0.1"} chrono = { version = "0.4" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +hex_fmt = "0.3" +humansize = "2.0.0" reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } dioxus-free-icons = { version = "0.10.0" } diesel = { version = "2.3", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "chrono", "serde_json"] } diff --git a/crates/weaver-app/assets/styling/record-view.css b/crates/weaver-app/assets/styling/record-view.css new file mode 100644 index 0000000..b12caeb --- /dev/null +++ b/crates/weaver-app/assets/styling/record-view.css @@ -0,0 +1,332 @@ +.record-view-container { + font-family: var(--font-mono); + max-width: 1200px; + margin: 2rem auto; + padding: 0 1rem; +} + +.record-header { + margin-bottom: 2rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--color-border); +} + +.record-header h1 { + font-family: var(--font-mono); + font-size: 1.2rem; + font-weight: 400; + margin-bottom: 1rem; + color: var(--color-text); + text-transform: uppercase; + letter-spacing: 0.15em; +} + +.record-metadata { + display: flex-wrap; + flex-direction: column; + gap: 0.75rem; +} + +.metadata-row { + display: flex; + gap: 0.5rem; + padding: 0.35rem; + align-items: baseline; +} + +.metadata-label { + color: var(--color-muted); + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.1em; + white-space: nowrap; +} + +.metadata-label::after { + content: ":"; + font-size: 0.8rem; + margin-left: 0.25rem; +} + +.metadata-value { + font-family: var(--font-mono); + color: var(--color-text); + font-size: 1rem; + word-break: break-all; +} + +.uri-link { + text-decoration: none; +} + +.uri-link:hover .aturi-scheme, +.uri-link:hover .aturi-authority, +.uri-link:hover .aturi-collection, +.uri-link:hover .aturi-rkey, +.uri-link:hover .uri-scheme, +.uri-link:hover .uri-authority, +.uri-link:hover .uri-path { + text-decoration: underline; +} + +.tab-bar { + display: flex; + gap: 0; + border-bottom: 1px solid var(--color-border); + margin-bottom: 1.5rem; + margin-top: 1.5rem; +} + +.tab-button { + font-family: var(--font-mono); + background: transparent; + border: none; + padding: 0.5rem 1rem; + cursor: pointer; + color: var(--color-muted); + text-transform: uppercase; + font-size: 0.9rem; + letter-spacing: 0.1em; + border-bottom: 2px solid transparent; + margin-bottom: -1px; + transition: all 0.2s; +} + +.tab-button:hover { + color: var(--color-text); +} + +.tab-button.active { + color: var(--color-text); + border-bottom-color: var(--color-primary); +} + +.tab-content { + min-height: 300px; +} + +.pretty-record { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.record-field { + display: flex; + flex-direction: column; + padding: 0rem 0 0rem 1rem; + + padding-right: 1rem; + border-left: 2px solid var(--color-secondary); + border-bottom: 1px dashed var(--color-muted); +} + +.field-label { + font-family: var(--font-mono); + color: var(--color-subtle); + font-size: 0.7rem; + padding-top: 0.5rem; +} + +.field-value { + font-family: var(--font-mono); + color: var(--color-text); + font-size: 1rem; + padding-top: 0.2rem; + padding-bottom: 0.1rem; + word-break: break-word; +} + +.field-value.muted { + color: var(--color-subtle); + font-style: italic; +} + +.field-value.mime { + color: var(--color-subtle); + font-style: italic; + font-size: 0.85rem; +} + +.field-value.bytes { + color: var(--color-subtle); + font-size: 0.75rem; +} + +.record-section { + padding-left: 1.5rem; + position: relative; + border-left: 1px dashed var(--color-border); +} + +.section-label { + font-family: var(--font-mono); + color: var(--color-primary); + font-size: 1rem; + font-weight: 600; + padding-left: 1rem; + padding-top: 0.5rem; + padding-bottom: 0.25rem; + border-left: 2px solid var(--color-primary); + border-bottom: 1px dashed var(--color-muted); + margin-left: -1.51rem; +} + +.section-content .section-label { + font-family: var(--font-mono); + color: var(--color-tertiary); + font-size: 0.9rem; + font-weight: 600; + padding-left: 1rem; + padding-top: 0.5rem; + border-left: 2px solid var(--color-secondary); +} + +.section-content { + display: flex; + flex-direction: column; + align-items: flex-start; + padding-right: 1rem; +} + +.section-content .record-field { + border-left-color: var(--color-secondary); + opacity: 0.95; +} + +.blob-image { + max-width: 600px; + max-height: 400px; + margin-top: 0.5rem; + border: 1px solid var(--color-border); + margin-bottom: 0.5rem; +} + +.string-type-tag { + font-size: 0.7rem; + color: var(--color-muted); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.string-did, +.string-handle, +.string-at-identifier { + color: var(--color-primary); +} + +.string-at-uri, +.string-uri { + color: var(--color-secondary); +} + +.string-cid, +.string-tid { + color: var(--color-tertiary); + font-family: var(--font-mono); +} + +.string-nsid { + color: var(--color-emphasis); +} + +.string-datetime { + color: var(--color-text); + font-style: italic; +} + +/* NSID highlighting */ +.nsid-dot { + color: var(--color-muted); + opacity: 0.6; +} + +.nsid-segment-0 { + color: var(--color-primary); +} + +.nsid-segment-1 { + color: var(--color-secondary); +} + +.nsid-segment-2 { + color: var(--color-tertiary); +} + +/* DID highlighting */ +.did-scheme { + color: var(--color-muted); + opacity: 0.7; +} + +.did-method { + color: var(--color-secondary); + font-weight: 500; +} + +.did-separator { + color: var(--color-muted); + opacity: 0.6; +} + +.did-identifier { + color: var(--color-primary); +} + +/* Handle highlighting */ +.handle-dot { + color: var(--color-muted); + opacity: 0.6; +} + +.handle-segment-0 { + color: var(--color-primary); + font-weight: 500; +} + +.handle-segment-1 { + color: var(--color-secondary); +} + +/* AT URI highlighting */ +.aturi-scheme { + color: var(--color-muted); + opacity: 0.7; +} + +.aturi-authority { + color: var(--color-primary); +} + +.aturi-slash { + color: var(--color-muted); + opacity: 0.6; +} + +.aturi-collection { + color: var(--color-secondary); +} + +.aturi-rkey { + color: var(--color-tertiary); +} + +/* URI highlighting */ +.uri-scheme { + color: var(--color-muted); + opacity: 0.7; + font-weight: 500; +} + +.uri-separator { + color: var(--color-muted); + opacity: 0.6; +} + +.uri-authority { + color: var(--color-primary); +} + +.uri-path { + color: var(--color-secondary); +} diff --git a/crates/weaver-app/src/components/mod.rs b/crates/weaver-app/src/components/mod.rs index 990f82b..4f4597e 100644 --- a/crates/weaver-app/src/components/mod.rs +++ b/crates/weaver-app/src/components/mod.rs @@ -6,7 +6,7 @@ pub mod css; pub use css::NotebookCss; mod entry; -pub use entry::{Entry, EntryCard}; +pub use entry::{Entry, EntryCard, EntryMarkdown}; pub mod identity; pub use identity::{NotebookCard, Repository, RepositoryIndex}; diff --git a/crates/weaver-app/src/main.rs b/crates/weaver-app/src/main.rs index 6330089..56dc7bb 100644 --- a/crates/weaver-app/src/main.rs +++ b/crates/weaver-app/src/main.rs @@ -17,7 +17,7 @@ use jacquard::{ use std::sync::Arc; #[allow(unused)] -use views::{Home, Navbar, Notebook, NotebookIndex, NotebookPage}; +use views::{Home, Navbar, Notebook, NotebookIndex, NotebookPage, RecordView}; #[cfg(feature = "server")] mod blobcache; @@ -46,6 +46,8 @@ enum Route { #[route("/")] Home {}, #[layout(ErrorLayout)] + #[route("/record#:uri")] + RecordView { uri: SmolStr }, #[nest("/:ident")] #[layout(Repository)] #[route("/")] diff --git a/crates/weaver-app/src/views/mod.rs b/crates/weaver-app/src/views/mod.rs index c1739f2..fbc1808 100644 --- a/crates/weaver-app/src/views/mod.rs +++ b/crates/weaver-app/src/views/mod.rs @@ -19,3 +19,6 @@ pub use notebookpage::NotebookPage; mod notebook; pub use notebook::{Notebook, NotebookIndex}; + +mod record; +pub use record::RecordView; diff --git a/crates/weaver-app/src/views/record.rs b/crates/weaver-app/src/views/record.rs new file mode 100644 index 0000000..0254dca --- /dev/null +++ b/crates/weaver-app/src/views/record.rs @@ -0,0 +1,436 @@ +use crate::fetch::CachedFetcher; +use dioxus::prelude::*; +use hex_fmt::HexFmt; +use humansize::format_size; +use jacquard::{ + client::AgentSessionExt, + common::{Data, IntoStatic}, + smol_str::SmolStr, + types::aturi::AtUri, +}; +use weaver_renderer::{code_pretty::highlight_code, css::generate_default_css}; + +#[derive(Clone, Copy, PartialEq)] +enum ViewMode { + Pretty, + Json, +} + +#[component] +pub fn RecordView(uri: SmolStr) -> Element { + let fetcher = use_context::(); + let at_uri = AtUri::new_owned(uri.clone()); + if let Err(err) = &at_uri { + let error = format!("{:?}", err); + return rsx! { + div { + h1 { "Record View" } + p { "URI: {uri}" } + p { "Error: {error}" } + } + }; + } + let uri = use_signal(|| at_uri.unwrap()); + let mut view_mode = use_signal(|| ViewMode::Pretty); + let record = use_resource(move || { + let fetcher = fetcher.clone(); + async move { fetcher.client.fetch_record_slingshot(&uri()).await } + }); + if let Some(Ok(record)) = &*record.read_unchecked() { + let record_value = record.value.clone().into_static(); + let json = serde_json::to_string_pretty(&record_value).unwrap(); + rsx! { + document::Stylesheet { href: asset!("/assets/styling/record-view.css") } + div { + class: "record-view-container", + div { + class: "record-header", + h1 { "Record Inspector" } + div { + class: "record-metadata", + div { class: "metadata-row", + span { class: "metadata-label", "URI" } + span { class: "metadata-value", + HighlightedUri { uri: uri().clone() } + } + } + if let Some(cid) = &record.cid { + div { class: "metadata-row", + span { class: "metadata-label", "CID" } + code { class: "metadata-value", "{cid}" } + } + } + } + } + div { + class: "tab-bar", + button { + class: if view_mode() == ViewMode::Pretty { "tab-button active" } else { "tab-button" }, + onclick: move |_| view_mode.set(ViewMode::Pretty), + "View" + } + button { + class: if view_mode() == ViewMode::Json { "tab-button active" } else { "tab-button" }, + onclick: move |_| view_mode.set(ViewMode::Json), + "JSON" + } + } + div { + class: "tab-content", + match view_mode() { + ViewMode::Pretty => rsx! { + PrettyRecordView { record: record_value.clone(), uri: uri().clone() } + }, + ViewMode::Json => rsx! { + CodeView { + code: use_signal(|| json.clone()), + lang: Some("json".to_string()), + } + }, + } + } + } + } + } else { + rsx! { + div { + class: "record-view-container", + h1 { "Record Inspector" } + p { "URI: {uri}" } + p { "Loading..." } + } + } + } +} + +#[component] +fn PrettyRecordView(record: Data<'static>, uri: AtUri<'static>) -> Element { + let did = uri.authority().to_string(); + rsx! { + div { + class: "pretty-record", + DataView { data: record, path: String::new(), did } + } + } +} +fn get_hex_rep(byte_array: &mut [u8]) -> String { + let build_string_vec: Vec = byte_array + .chunks(2) + .enumerate() + .map(|(i, c)| { + let sep = if i % 16 == 0 && i > 0 { + "\n" + } else if i == 0 { + "" + } else { + " " + }; + if c.len() == 2 { + format!("{}{:02x}{:02x}", sep, c[0], c[1]) + } else { + format!("{}{:02x}", sep, c[0]) + } + }) + .collect(); + build_string_vec.join("") +} + +#[component] +fn DataView(data: Data<'static>, path: String, did: String) -> Element { + match &data { + Data::Null => rsx! { + div { class: "record-field", + span { class: "field-label", "{path}" } + span { class: "field-value muted", "null" } + } + }, + Data::Boolean(b) => rsx! { + div { class: "record-field", + span { class: "field-label", "{path}" } + span { class: "field-value", "{b}" } + } + }, + Data::Integer(i) => rsx! { + div { class: "record-field", + span { class: "field-label", "{path}" } + span { class: "field-value", "{i}" } + } + }, + Data::String(s) => { + use jacquard::types::string::AtprotoStr; + + let type_label = match s { + AtprotoStr::Datetime(_) => "datetime", + AtprotoStr::Language(_) => "language", + AtprotoStr::Tid(_) => "tid", + AtprotoStr::Nsid(_) => "nsid", + AtprotoStr::Did(_) => "did", + AtprotoStr::Handle(_) => "handle", + AtprotoStr::AtIdentifier(_) => "at-identifier", + AtprotoStr::AtUri(_) => "at-uri", + AtprotoStr::Uri(_) => "uri", + AtprotoStr::Cid(_) => "cid", + AtprotoStr::RecordKey(_) => "record-key", + AtprotoStr::String(_) => "string", + }; + + rsx! { + div { class: "record-field", + span { class: "field-label", "{path}" } + span { class: "field-value", + + HighlightedString { string_type: s.clone() } + if type_label != "string" { + span { class: "string-type-tag", " [{type_label}]" } + } + } + } + } + } + Data::Bytes(b) => { + let hex_string = get_hex_rep(&mut b.to_vec()); + let byte_size = if b.len() > 128 { + format_size(b.len(), humansize::BINARY) + } else { + format!("{} bytes", b.len()) + }; + rsx! { + div { class: "record-field", + span { class: "field-label", "{path}" } + pre { class: "field-value bytes", "{hex_string} [{byte_size}]" } + } + } + } + Data::CidLink(cid) => rsx! { + div { class: "record-field", + span { class: "field-label", "{path}" } + span { class: "field-value", "{cid}" } + } + }, + Data::Array(arr) => rsx! { + div { class: "record-section", + div { class: "section-label", "{path}" span { class: "field-label", "[{arr.len()}] " } } + + div { class: "section-content", + for (idx, item) in arr.iter().enumerate() { + DataView { + data: item.clone(), + path: format!("{}[{}]", path, idx), + did: did.clone() + } + } + } + } + }, + Data::Object(obj) => rsx! { + for (key, value) in obj.iter() { + { + let new_path = if path.is_empty() { + key.to_string() + } else { + format!("{}.{}", path, key) + }; + let did_clone = did.clone(); + + match value { + Data::Object(_) | Data::Array(_) => rsx! { + div { class: "record-section", + div { class: "section-label", "{key}" } + div { class: "section-content", + DataView { data: value.clone(), path: new_path, did: did_clone } + } + } + }, + _ => rsx! { + DataView { data: value.clone(), path: new_path, did: did_clone } + } + } + } + } + }, + Data::Blob(blob) => { + let is_image = blob.mime_type.starts_with("image/"); + let format = blob.mime_type.strip_prefix("image/").unwrap_or("jpeg"); + let image_url = format!( + "https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}@{}", + did, + blob.cid(), + format + ); + + let blob_size = format_size(blob.size, humansize::BINARY); + rsx! { + div { class: "record-field", + span { class: "field-label", "{path}" } + span { class: "field-value mime", "[mimeType: {blob.mime_type}, size: {blob_size}]" } + if is_image { + img { + src: "{image_url}", + alt: "Blob image", + class: "blob-image", + } + } + } + } + } + } +} + +#[component] +fn HighlightedUri(uri: AtUri<'static>) -> Element { + let s = uri.as_str(); + let link = format!("/record#{}", s); + + if let Some(rest) = s.strip_prefix("at://") { + let parts: Vec<&str> = rest.splitn(3, '/').collect(); + return rsx! { + a { + href: "{link}", + class: "uri-link", + span { class: "string-at-uri", + span { class: "aturi-scheme", "at://" } + span { class: "aturi-authority", "{uri.authority()}" } + + if parts.len() > 1 { + span { class: "aturi-slash", "/" } + if let Some(collection) = uri.collection() { + span { class: "aturi-collection", "{collection.as_ref()}" } + } + } + if parts.len() > 2 { + span { class: "aturi-slash", "/" } + if let Some(rkey) = uri.rkey() { + span { class: "aturi-rkey", "{rkey.as_ref()}" } + } + } + } + } + }; + } + + rsx! { span { class: "string-at-uri", "{s}" } } +} + +#[component] +fn HighlightedString(string_type: jacquard::types::string::AtprotoStr<'static>) -> Element { + use jacquard::types::string::AtprotoStr; + + match &string_type { + AtprotoStr::Nsid(nsid) => { + let parts: Vec<&str> = nsid.as_str().split('.').collect(); + rsx! { + span { class: "string-nsid", + for (i, part) in parts.iter().enumerate() { + span { class: "nsid-segment nsid-segment-{i % 3}", "{part}" } + if i < parts.len() - 1 { + span { class: "nsid-dot", "." } + } + } + } + } + } + AtprotoStr::Did(did) => { + let s = did.as_str(); + if let Some(rest) = s.strip_prefix("did:") { + if let Some((method, identifier)) = rest.split_once(':') { + return rsx! { + span { class: "string-did", + span { class: "did-scheme", "did:" } + span { class: "did-method", "{method}" } + span { class: "did-separator", ":" } + span { class: "did-identifier", "{identifier}" } + } + }; + } + } + rsx! { span { class: "string-did", "{s}" } } + } + AtprotoStr::Handle(handle) => { + let parts: Vec<&str> = handle.as_str().split('.').collect(); + rsx! { + span { class: "string-handle", + for (i, part) in parts.iter().enumerate() { + span { class: "handle-segment handle-segment-{i % 2}", "{part}" } + if i < parts.len() - 1 { + span { class: "handle-dot", "." } + } + } + } + } + } + AtprotoStr::AtUri(uri) => { + rsx! { + HighlightedUri { uri: uri.clone().into_static() } + } + } + AtprotoStr::Uri(uri) => { + let s = uri.as_str(); + if let Ok(at_uri) = AtUri::new(s) { + return rsx! { + HighlightedUri { uri: at_uri.into_static() } + }; + } + + // Try to parse scheme + if let Some((scheme, rest)) = s.split_once("://") { + // Split authority and path + let (authority, path) = if let Some(idx) = rest.find('/') { + (&rest[..idx], &rest[idx..]) + } else { + (rest, "") + }; + + return rsx! { + a { + href: "{s}", + target: "_blank", + rel: "noopener noreferrer", + class: "uri-link", + span { class: "string-uri", + span { class: "uri-scheme", "{scheme}" } + span { class: "uri-separator", "://" } + span { class: "uri-authority", "{authority}" } + if !path.is_empty() { + span { class: "uri-path", "{path}" } + } + } + } + }; + } + + rsx! { span { class: "string-uri", "{s}" } } + } + _ => { + let value = string_type.as_str(); + rsx! { "{value}" } + } + } +} + +#[derive(Props, Clone, PartialEq)] +pub struct CodeViewProps { + #[props(default)] + id: Signal, + #[props(default)] + class: Signal, + code: ReadSignal, + lang: Option, +} + +/// Render some text as markdown. +#[component] +pub fn CodeView(props: CodeViewProps) -> Element { + let code = &*props.code.read(); + + let mut html_buf = String::new(); + highlight_code(props.lang.as_deref(), code, &mut html_buf).unwrap(); + + rsx! { + document::Style { {generate_default_css().unwrap()}} + div { + id: "{&*props.id.read()}", + class: "{&*props.class.read()}", + dangerous_inner_html: "{html_buf}" + } + } +} diff --git a/crates/weaver-renderer/src/code_pretty.rs b/crates/weaver-renderer/src/code_pretty.rs index 08a0675..aa3ab73 100644 --- a/crates/weaver-renderer/src/code_pretty.rs +++ b/crates/weaver-renderer/src/code_pretty.rs @@ -47,3 +47,41 @@ where } pub const CSS_PREFIX: &str = "wvc-"; + +pub fn highlight_code( + lang: Option<&str>, + code: impl AsRef, + writer: &mut M, +) -> Result<(), Box> +where + M: StrWrite, + ::Error: std::error::Error + Send + Sync + 'static, +{ + let syn_set = SyntaxSet::load_defaults_newlines(); + let lang_syn = if let Some(lang) = lang { + syn_set + .find_syntax_by_token(lang) + .unwrap_or_else(|| syn_set.find_syntax_plain_text()) + } else { + syn_set + .find_syntax_by_first_line(code.as_ref()) + .unwrap_or_else(|| syn_set.find_syntax_plain_text()) + }; + writer.write_str("
")?;
+
+    let mut html_gen = ClassedHTMLGenerator::new_with_class_style(
+        lang_syn,
+        &syn_set,
+        ClassStyle::SpacedPrefixed { prefix: CSS_PREFIX },
+    );
+    for line in LinesWithEndings::from(code.as_ref()) {
+        html_gen
+            .parse_html_for_line_which_includes_newline(line)
+            .unwrap();
+    }
+    writer.write_str(&html_gen.finalize())?;
+    writer.write_str("
")?; + Ok(()) +} diff --git a/crates/weaver-renderer/src/css.rs b/crates/weaver-renderer/src/css.rs index 7787f67..2ae766d 100644 --- a/crates/weaver-renderer/src/css.rs +++ b/crates/weaver-renderer/src/css.rs @@ -461,3 +461,51 @@ pub async fn generate_syntax_css(theme: &ResolvedTheme<'_>) -> miette::Result miette::Result { + let mut theme_set = ThemeSet::load_defaults(); + let rose_pine = { + let mut cursor = Cursor::new(ROSE_PINE_THEME.as_bytes()); + ThemeSet::load_from_reader(&mut cursor) + .into_diagnostic() + .map_err(|e| miette::miette!("Failed to load embedded rose-pine theme: {}", e))? + }; + let rose_pine_dawn = { + let mut cursor = Cursor::new(ROSE_PINE_DAWN_THEME.as_bytes()); + ThemeSet::load_from_reader(&mut cursor) + .into_diagnostic() + .map_err(|e| miette::miette!("Failed to load embedded rose-pine-dawn theme: {}", e))? + }; + theme_set.themes.insert("rose-pine".to_string(), rose_pine); + theme_set + .themes + .insert("rose-pine-dawn".to_string(), rose_pine_dawn); + // Generate dark mode CSS (default) + let dark_css = css_for_theme_with_class_style( + theme_set.themes.get("rose-pine").unwrap(), + ClassStyle::SpacedPrefixed { + prefix: crate::code_pretty::CSS_PREFIX, + }, + ) + .into_diagnostic()?; + + // Generate light mode CSS + let light_css = css_for_theme_with_class_style( + theme_set.themes.get("rose-pine-dawn").unwrap(), + ClassStyle::SpacedPrefixed { + prefix: crate::code_pretty::CSS_PREFIX, + }, + ) + .into_diagnostic()?; + + // Combine with media queries + let mut result = String::new(); + result.push_str("/* Syntax highlighting - Light Mode (default) */\n"); + result.push_str(&light_css); + result.push_str("\n\n/* Syntax highlighting - Dark Mode */\n"); + result.push_str("@media (prefers-color-scheme: dark) {\n"); + result.push_str(&dark_css); + result.push_str("}\n"); + + Ok(result) +} -- knotmirror 0.0.1