diff --git a/crates/weaver-renderer/src/css.rs b/crates/weaver-renderer/src/css.rs index cf9e983..7c3564d 100644 --- a/crates/weaver-renderer/src/css.rs +++ b/crates/weaver-renderer/src/css.rs @@ -1,8 +1,13 @@ use crate::theme::Theme; use miette::IntoDiagnostic; -use syntect::highlighting::ThemeSet; +use syntect::highlighting::{Theme as SyntectTheme, ThemeSet}; use syntect::html::{ClassStyle, css_for_theme_with_class_style}; use syntect::parsing::SyntaxSet; +use std::io::Cursor; + +// Embed rose-pine themes at compile time +const ROSE_PINE_THEME: &str = include_str!("../themes/rose-pine.tmTheme"); +const ROSE_PINE_DAWN_THEME: &str = include_str!("../themes/rose-pine-dawn.tmTheme"); pub fn generate_base_css(theme: &Theme) -> String { format!( @@ -179,17 +184,43 @@ hr {{ } pub fn generate_syntax_css( - syntect_theme_name: &str, + theme: &Theme, _syntax_set: &SyntaxSet, ) -> miette::Result { - let theme_set = ThemeSet::load_defaults(); - let theme = theme_set - .themes - .get(syntect_theme_name) - .ok_or_else(|| miette::miette!("Theme '{}' not found", syntect_theme_name))?; + let syntect_theme = if let Some(custom_path) = &theme.custom_syntect_theme_path { + // Load custom theme from file + ThemeSet::get_theme(custom_path) + .into_diagnostic() + .map_err(|e| miette::miette!("Failed to load custom theme from {:?}: {}", custom_path, e))? + } else { + // Check for embedded themes first + match theme.syntect_theme_name.as_str() { + "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))? + } + "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))? + } + _ => { + // Fall back to syntect's built-in themes + let theme_set = ThemeSet::load_defaults(); + theme_set + .themes + .get(theme.syntect_theme_name.as_str()) + .ok_or_else(|| miette::miette!("Theme '{}' not found in defaults", theme.syntect_theme_name))? + .clone() + } + } + }; let css = css_for_theme_with_class_style( - theme, + &syntect_theme, ClassStyle::SpacedPrefixed { prefix: crate::code_pretty::CSS_PREFIX, }, diff --git a/crates/weaver-renderer/src/snapshots/weaver_renderer__static_site__tests__code_block_rendering.snap b/crates/weaver-renderer/src/snapshots/weaver_renderer__static_site__tests__code_block_rendering.snap index 25b2511..ceda8fc 100644 --- a/crates/weaver-renderer/src/snapshots/weaver_renderer__static_site__tests__code_block_rendering.snap +++ b/crates/weaver-renderer/src/snapshots/weaver_renderer__static_site__tests__code_block_rendering.snap @@ -2,7 +2,7 @@ source: crates/weaver-renderer/src/static_site.rs expression: output --- -
fn main() {
-    println!("Hello");
-}
-
+
fn main() {
+    println!("Hello");
+}
+
diff --git a/crates/weaver-renderer/src/static_site.rs b/crates/weaver-renderer/src/static_site.rs index a52e090..e1f3720 100644 --- a/crates/weaver-renderer/src/static_site.rs +++ b/crates/weaver-renderer/src/static_site.rs @@ -11,7 +11,11 @@ pub mod writer; use crate::{ ContextIterator, NotebookProcessor, - static_site::{context::StaticSiteContext, writer::StaticPageWriter}, + static_site::{ + context::StaticSiteContext, + document::{CssMode, write_document_footer, write_document_head}, + writer::StaticPageWriter, + }, utils::flatten_dir_to_just_one_parent, walker::{WalkOptions, vault_contents}, }; @@ -131,7 +135,8 @@ where } self.context.destination.clone() }; - return write_page(self.context.clone(), &self.context.start_at, dest).await; + // Use standalone writer for single file (inline CSS) + return write_page_standalone(self.context.clone(), &self.context.start_at, dest).await; } if !self.context.destination.exists() { @@ -141,6 +146,9 @@ where )); } + // Generate CSS files for multi-file mode + self.generate_css_files().await?; + let mut writers = Vec::with_capacity(self.context.dir_contents.clone().unwrap_or_default().len()); @@ -161,22 +169,65 @@ where .strip_prefix(context.start_at.clone()) .expect("file should always be nested under root") .to_path_buf(); + + // Check if this is a markdown file + let is_markdown = file.extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext == "md" || ext == "markdown") + .unwrap_or(false); + + if !is_markdown { + // Copy non-markdown files directly + let output_path = if context + .options + .contains(StaticSiteOptions::FLATTEN_STRUCTURE) + { + let path_str = relative_path.to_string_lossy(); + let (parent, fname) = flatten_dir_to_just_one_parent(&path_str); + let parent = if parent.is_empty() { "entry" } else { parent }; + context + .destination + .join(String::from(parent)) + .join(String::from(fname)) + } else { + context.destination.join(relative_path.clone()) + }; + + // Create parent directory if needed + if let Some(parent) = output_path.parent() { + tokio::fs::create_dir_all(parent).await.into_diagnostic()?; + } + + tokio::fs::copy(&file, &output_path).await.into_diagnostic()?; + return Ok(()); + } + + // Process markdown files + // Check if this is the designated index file + if let Some(index) = &context.index_file { + if &relative_path == index { + let output_path = context.destination.join("index.html"); + return write_page(context.clone(), file, output_path).await; + } + } + if context .options .contains(StaticSiteOptions::FLATTEN_STRUCTURE) { let path_str = relative_path.to_string_lossy(); - let (parent, file) = flatten_dir_to_just_one_parent(&path_str); + let (parent, fname) = flatten_dir_to_just_one_parent(&path_str); + let parent = if parent.is_empty() { "entry" } else { parent }; let output_path = context .destination .join(String::from(parent)) - .join(String::from(file)); + .join(String::from(fname)); - write_page(context.clone(), relative_path, output_path).await?; + write_page(context.clone(), file.clone(), output_path).await?; } else { let output_path = context.destination.join(relative_path.clone()); - write_page(context.clone(), relative_path, output_path).await?; + write_page(context.clone(), file.clone(), output_path).await?; } Ok::<(), miette::Report>(()) })); @@ -187,6 +238,103 @@ where for fut in n0_future::join_all(writers).await.into_iter() { fut.into_diagnostic()??; } + + // Generate default index if requested and no custom index specified + if self + .context + .options + .contains(StaticSiteOptions::CREATE_INDEX) + && self.context.index_file.is_none() + { + self.generate_default_index().await?; + } + + Ok(()) + } + + async fn generate_css_files(&self) -> Result<(), miette::Report> { + use crate::css::{generate_base_css, generate_syntax_css}; + use crate::theme::Theme; + + let css_dir = self.context.destination.join("css"); + tokio::fs::create_dir_all(&css_dir) + .await + .into_diagnostic()?; + + let default_theme = Theme::default(); + let theme = self.context.theme.as_deref().unwrap_or(&default_theme); + + // Write base.css + let base_css = generate_base_css(theme); + tokio::fs::write(css_dir.join("base.css"), base_css) + .await + .into_diagnostic()?; + + // Write syntax.css + let syntax_css = generate_syntax_css(theme, &self.context.syntax_set)?; + tokio::fs::write(css_dir.join("syntax.css"), syntax_css) + .await + .into_diagnostic()?; + + Ok(()) + } + + async fn generate_default_index(&self) -> Result<(), miette::Report> { + let index_path = self.context.destination.join("index.html"); + let mut index_file = crate::utils::create_file(&index_path).await?; + + // Write head + write_document_head(&self.context, &mut index_file, CssMode::Linked, &index_path).await?; + + // Write title and list + index_file + .write_all(b"

Index

\n\n").await.into_diagnostic()?; + + // Write footer + write_document_footer(&mut index_file).await?; + Ok(()) } } @@ -227,13 +375,58 @@ where let contents = tokio::fs::read_to_string(&input_path) .await .into_diagnostic()?; - let mut output_file = crate::utils::create_file(output_path.as_ref()).await?; + + // Change extension to .html + let output_path = output_path.as_ref().with_extension("html"); + let mut output_file = crate::utils::create_file(&output_path).await?; let context = context.clone_with_path(input_path); + + // Write document head + write_document_head(&context, &mut output_file, CssMode::Linked, &output_path).await?; + + // Write body content let output = export_page(&contents, context).await?; output_file .write_all(output.as_bytes()) .await .into_diagnostic()?; + + // Write document footer + write_document_footer(&mut output_file).await?; + + Ok(()) +} + +pub async fn write_page_standalone( + context: StaticSiteContext, + input_path: impl AsRef, + output_path: impl AsRef, +) -> Result<(), miette::Report> +where + A: AgentSession + IdentityResolver, +{ + let contents = tokio::fs::read_to_string(&input_path) + .await + .into_diagnostic()?; + + // Change extension to .html + let output_path = output_path.as_ref().with_extension("html"); + let mut output_file = crate::utils::create_file(&output_path).await?; + let context = context.clone_with_path(input_path); + + // Write document head with inline CSS + write_document_head(&context, &mut output_file, CssMode::Inline, &output_path).await?; + + // Write body content + let output = export_page(&contents, context).await?; + output_file + .write_all(output.as_bytes()) + .await + .into_diagnostic()?; + + // Write document footer + write_document_footer(&mut output_file).await?; + Ok(()) } diff --git a/crates/weaver-renderer/src/static_site/document.rs b/crates/weaver-renderer/src/static_site/document.rs index 1a5b777..697fe61 100644 --- a/crates/weaver-renderer/src/static_site/document.rs +++ b/crates/weaver-renderer/src/static_site/document.rs @@ -14,14 +14,19 @@ pub async fn write_document_head( context: &StaticSiteContext, writer: &mut (impl tokio::io::AsyncWrite + Unpin), css_mode: CssMode, + output_path: &std::path::Path, ) -> miette::Result<()> { use tokio::io::AsyncWriteExt; // Get title from frontmatter or current path - let title = if let Some(path) = context.dir_contents.as_ref() + let title = if let Some(path) = context + .dir_contents + .as_ref() .and_then(|contents| contents.get(context.position)) { - context.titles.get(path) + context + .titles + .get(path) .map(|t| t.value().to_string()) .unwrap_or_else(|| { path.file_stem() @@ -33,11 +38,35 @@ pub async fn write_document_head( "Untitled".to_string() }; - writer.write_all(b"\n").await.into_diagnostic()?; - writer.write_all(b"\n").await.into_diagnostic()?; + // Calculate relative path to root based on output file depth + let relative_to_root = if let Ok(rel) = output_path.strip_prefix(&context.destination) { + let depth = rel.components().count() - 1; // -1 because the file itself doesn't count + if depth == 0 { + ".".to_string() + } else { + "../".repeat(depth) + } + } else { + ".".to_string() + }; + + writer + .write_all(b"\n") + .await + .into_diagnostic()?; + writer + .write_all(b"\n") + .await + .into_diagnostic()?; writer.write_all(b"\n").await.into_diagnostic()?; - writer.write_all(b" \n").await.into_diagnostic()?; - writer.write_all(b" \n").await.into_diagnostic()?; + writer + .write_all(b" \n") + .await + .into_diagnostic()?; + writer + .write_all(b" \n") + .await + .into_diagnostic()?; // Title writer.write_all(b" ").await.into_diagnostic()?; @@ -47,20 +76,32 @@ pub async fn write_document_head<A: AgentSession>( // CSS match css_mode { CssMode::Linked => { - writer.write_all(b" <link rel=\"stylesheet\" href=\"/css/base.css\">\n").await.into_diagnostic()?; - writer.write_all(b" <link rel=\"stylesheet\" href=\"/css/syntax.css\">\n").await.into_diagnostic()?; + writer + .write_all(format!(" <link rel=\"stylesheet\" href=\"{}/css/base.css\">\n", relative_to_root).as_bytes()) + .await + .into_diagnostic()?; + writer + .write_all(format!(" <link rel=\"stylesheet\" href=\"{}/css/syntax.css\">\n", relative_to_root).as_bytes()) + .await + .into_diagnostic()?; } CssMode::Inline => { let default_theme = Theme::default(); let theme = context.theme.as_deref().unwrap_or(&default_theme); writer.write_all(b" <style>\n").await.into_diagnostic()?; - writer.write_all(generate_base_css(theme).as_bytes()).await.into_diagnostic()?; + writer + .write_all(generate_base_css(theme).as_bytes()) + .await + .into_diagnostic()?; writer.write_all(b" </style>\n").await.into_diagnostic()?; writer.write_all(b" <style>\n").await.into_diagnostic()?; - let syntax_css = generate_syntax_css(&theme.syntect_theme_name, &context.syntax_set)?; - writer.write_all(syntax_css.as_bytes()).await.into_diagnostic()?; + let syntax_css = generate_syntax_css(theme, &context.syntax_set)?; + writer + .write_all(syntax_css.as_bytes()) + .await + .into_diagnostic()?; writer.write_all(b" </style>\n").await.into_diagnostic()?; } } @@ -75,8 +116,26 @@ pub async fn write_document_head<A: AgentSession>( } KaTeXSource::Local(path) => { let path_str = path.to_string_lossy(); - writer.write_all(format!(" <link rel=\"stylesheet\" href=\"{}/katex.min.css\">\n", path_str).as_bytes()).await.into_diagnostic()?; - writer.write_all(format!(" <script defer src=\"{}/katex.min.js\"></script>\n", path_str).as_bytes()).await.into_diagnostic()?; + writer + .write_all( + format!( + " <link rel=\"stylesheet\" href=\"{}/katex.min.css\">\n", + path_str + ) + .as_bytes(), + ) + .await + .into_diagnostic()?; + writer + .write_all( + format!( + " <script defer src=\"{}/katex.min.js\"></script>\n", + path_str + ) + .as_bytes(), + ) + .await + .into_diagnostic()?; writer.write_all(format!(" <script defer src=\"{}/contrib/auto-render.min.js\" onload=\"renderMathInElement(document.body);\"></script>\n", path_str).as_bytes()).await.into_diagnostic()?; } } diff --git a/crates/weaver-renderer/src/static_site/writer.rs b/crates/weaver-renderer/src/static_site/writer.rs index 35e4f72..01659c3 100644 --- a/crates/weaver-renderer/src/static_site/writer.rs +++ b/crates/weaver-renderer/src/static_site/writer.rs @@ -109,7 +109,7 @@ impl<'input, I: Iterator<Item = Event<'input>>, A: AgentSession, W: StrWrite> ) { Ok(_) => { self.write(&temp_output)?; - }, + } Err(_) => { // Fallback to plain code block self.write("<pre><code class=\"language-")?; @@ -518,6 +518,7 @@ impl<'input, I: Iterator<Item = Event<'input>>, A: AgentSession + IdentityResolv id, attrs, } => { + println!("Image tag {}", dest_url); self.write_image(Tag::Image { link_type, dest_url, @@ -534,6 +535,7 @@ impl<'input, I: Iterator<Item = Event<'input>>, A: AgentSession + IdentityResolv id, attrs, } => { + println!("Embed {:?}: {} - {}", embed_type, title, dest_url); if let Some(attrs) = attrs { if let Some((_, content)) = attrs .attrs diff --git a/crates/weaver-renderer/src/theme.rs b/crates/weaver-renderer/src/theme.rs index ccebd2d..2c55dcb 100644 --- a/crates/weaver-renderer/src/theme.rs +++ b/crates/weaver-renderer/src/theme.rs @@ -1,4 +1,5 @@ use smol_str::SmolStr; +use std::path::PathBuf; #[derive(Debug, Clone)] pub struct Theme { @@ -6,6 +7,7 @@ pub struct Theme { pub fonts: FontScheme, pub spacing: SpacingScheme, pub syntect_theme_name: SmolStr, + pub custom_syntect_theme_path: Option<PathBuf>, } #[derive(Debug, Clone)] @@ -36,7 +38,8 @@ impl Default for Theme { colors: ColorScheme::default(), fonts: FontScheme::default(), spacing: SpacingScheme::default(), - syntect_theme_name: SmolStr::new("base16-ocean.dark"), + syntect_theme_name: SmolStr::new("rose-pine"), + custom_syntect_theme_path: None, } } } @@ -55,9 +58,15 @@ impl Default for ColorScheme { impl Default for FontScheme { fn default() -> Self { Self { - body: SmolStr::new("system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"), - heading: SmolStr::new("system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"), - monospace: SmolStr::new("'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, monospace"), + body: SmolStr::new( + "IBM Plex, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", + ), + heading: SmolStr::new( + "IBM Plex Sans, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", + ), + monospace: SmolStr::new( + "'IBM Plex Mono', 'Berkeley Mono', 'Cascadia Code', 'Roboto Mono', Consolas, monospace", + ), } } } diff --git a/crates/weaver-renderer/themes/rose-pine-dawn.tmTheme b/crates/weaver-renderer/themes/rose-pine-dawn.tmTheme new file mode 100644 index 0000000..4610ea5 --- /dev/null +++ b/crates/weaver-renderer/themes/rose-pine-dawn.tmTheme @@ -0,0 +1,316 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>name</key> + <string>Rosé Pine Dawn</string> + <key>settings</key> + <array> + <dict> + <key>settings</key> + <dict> + <key>background</key> + <string>#faf4ed</string> + <key>caret</key> + <string>#cecacd</string> + <key>foreground</key> + <string>#575279</string> + <key>invisibles</key> + <string>#fffaf3</string> + <key>lineHighlight</key> + <string>#f4ede8</string> + <key>selection</key> + <string>#dfdad9</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Comment</string> + <key>scope</key> + <string>comment</string> + <key>settings</key> + <dict> + <key>fontStyle</key> + <string>italic</string> + <key>foreground</key> + <string>#797593</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>String</string> + <key>scope</key> + <string>string, punctuation.definition.string</string> + <key>settings</key> + <dict> + <key>foreground</key> + <string>#ea9d34</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Number</string> + <key>scope</key> + <string>constant.numeric</string> + <key>settings</key> + <dict> + <key>foreground</key> + <string>#ea9d34</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Built-in constant</string> + <key>scope</key> + <string>constant.language</string> + <key>settings</key> + <dict> + <key>fontStyle</key> + <string>bold</string> + <key>foreground</key> + <string>#ea9d34</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>User-defined constant</string> + <key>scope</key> + <string>constant.character, constant.other</string> + <key>settings</key> + <dict> + <key>foreground</key> + <string>#ea9d34</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Variable</string> + <key>scope</key> + <string>variable</string> + <key>settings</key> + <dict> + <key>fontStyle</key> + <string>italic</string> + <key>foreground</key> + <string>#575279</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Keyword</string> + <key>scope</key> + <string>keyword</string> + <key>settings</key> + <dict> + <key>foreground</key> + <string>#286983</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Storage</string> + <key>scope</key> + <string>storage</string> + <key>settings</key> + <dict> + <key>fontStyle</key> + <string></string> + <key>foreground</key> + <string>#56949f</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Storage type</string> + <key>scope</key> + <string>storage.type</string> + <key>settings</key> + <dict> + <key>fontStyle</key> + <string></string> + <key>foreground</key> + <string>#56949f</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Class name</string> + <key>scope</key> + <string>entity.name.class</string> + <key>settings</key> + <dict> + <key>fontStyle</key> + <string>bold</string> + <key>foreground</key> + <string>#286983</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Inherited class</string> + <key>scope</key> + <string>entity.other.inherited-class</string> + <key>settings</key> + <dict> + <key>fontStyle</key> + <string>italic </string> + <key>foreground</key> + <string>#286983</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Function name</string> + <key>scope</key> + <string>entity.name.function</string> + <key>settings</key> + <dict> + <key>fontStyle</key> + <string>italic</string> + <key>foreground</key> + <string>#d7827e</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Function argument</string> + <key>scope</key> + <string>variable.parameter</string> + <key>settings</key> + <dict> + <key>fontStyle</key> + <string></string> + <key>foreground</key> + <string>#907aa9</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Tag name</string> + <key>scope</key> + <string>entity.name.tag</string> + <key>settings</key> + <dict> + <key>fontStyle</key> + <string>bold</string> + <key>foreground</key> + <string>#286983</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Tag attribute</string> + <key>scope</key> + <string>entity.other.attribute-name</string> + <key>settings</key> + <dict> + <key>fontStyle</key> + <string></string> + <key>foreground</key> + <string>#907aa9</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Library function</string> + <key>scope</key> + <string>support.function</string> + <key>settings</key> + <dict> + <key>fontStyle</key> + <string>bold</string> + <key>foreground</key> + <string>#d7827e</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Library constant</string> + <key>scope</key> + <string>support.constant</string> + <key>settings</key> + <dict> + <key>fontStyle</key> + <string>bold</string> + <key>foreground</key> + <string>#ea9d34</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Library class/type</string> + <key>scope</key> + <string>support.type, support.class</string> + <key>settings</key> + <dict> + <key>fontStyle</key> + <string>bold</string> + <key>foreground</key> + <string>#56949f</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Library variable</string> + <key>scope</key> + <string>support.other.variable</string> + <key>settings</key> + <dict> + <key>fontStyle</key> + <string>bold</string> + <key>foreground</key> + <string>#b4637a</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Invalid</string> + <key>scope</key> + <string>invalid</string> + <key>settings</key> + <dict> + <key>background</key> + <string>#b4637a</string> + <key>fontStyle</key> + <string></string> + <key>foreground</key> + <string>#575279</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Invalid deprecated</string> + <key>scope</key> + <string>invalid.deprecated</string> + <key>settings</key> + <dict> + <key>background</key> + <string>#907aa9</string> + <key>foreground</key> + <string>#575279</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Punctuation, Operators</string> + <key>scope</key> + <string>punctuation, keyword.operator</string> + <key>settings</key> + <dict> + <key>foreground</key> + <string>#797593</string> + </dict> + </dict> + </array> + <key>colorSpaceName</key> + <string>sRGB</string> + <key>semanticClass</key> + <string>theme.light.rosé-pine-dawn</string> + <key>author</key> + <string>arrrgi</string> + <key>comment</key> + <string>All natural pine, faux fur and a bit of soho vibes for the classy minimalist</string> + <key>uuid</key> + <string>BB4B4616-E742-41D5-BB5B-63D45FA614F</string> +</dict> +</plist> diff --git a/crates/weaver-renderer/themes/rose-pine.tmTheme b/crates/weaver-renderer/themes/rose-pine.tmTheme new file mode 100644 index 0000000..70ded3e --- /dev/null +++ b/crates/weaver-renderer/themes/rose-pine.tmTheme @@ -0,0 +1,316 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>name</key> + <string>Rosé Pine</string> + <key>settings</key> + <array> + <dict> + <key>settings</key> + <dict> + <key>background</key> + <string>#191724</string> + <key>caret</key> + <string>#524f67</string> + <key>foreground</key> + <string>#e0def4</string> + <key>invisibles</key> + <string>#1f1d2e</string> + <key>lineHighlight</key> + <string>#21202e</string> + <key>selection</key> + <string>#403d52</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Comment</string> + <key>scope</key> + <string>comment</string> + <key>settings</key> + <dict> + <key>fontStyle</key> + <string>italic</string> + <key>foreground</key> + <string>#908caa</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>String</string> + <key>scope</key> + <string>string, punctuation.definition.string</string> + <key>settings</key> + <dict> + <key>foreground</key> + <string>#f6c177</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Number</string> + <key>scope</key> + <string>constant.numeric</string> + <key>settings</key> + <dict> + <key>foreground</key> + <string>#f6c177</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Built-in constant</string> + <key>scope</key> + <string>constant.language</string> + <key>settings</key> + <dict> + <key>fontStyle</key> + <string>bold</string> + <key>foreground</key> + <string>#f6c177</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>User-defined constant</string> + <key>scope</key> + <string>constant.character, constant.other</string> + <key>settings</key> + <dict> + <key>foreground</key> + <string>#f6c177</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Variable</string> + <key>scope</key> + <string>variable</string> + <key>settings</key> + <dict> + <key>fontStyle</key> + <string>italic</string> + <key>foreground</key> + <string>#e0def4</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Keyword</string> + <key>scope</key> + <string>keyword</string> + <key>settings</key> + <dict> + <key>foreground</key> + <string>#31748f</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Storage</string> + <key>scope</key> + <string>storage</string> + <key>settings</key> + <dict> + <key>fontStyle</key> + <string></string> + <key>foreground</key> + <string>#9ccfd8</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Storage type</string> + <key>scope</key> + <string>storage.type</string> + <key>settings</key> + <dict> + <key>fontStyle</key> + <string></string> + <key>foreground</key> + <string>#9ccfd8</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Class name</string> + <key>scope</key> + <string>entity.name.class</string> + <key>settings</key> + <dict> + <key>fontStyle</key> + <string>bold</string> + <key>foreground</key> + <string>#31748f</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Inherited class</string> + <key>scope</key> + <string>entity.other.inherited-class</string> + <key>settings</key> + <dict> + <key>fontStyle</key> + <string>italic </string> + <key>foreground</key> + <string>#31748f</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Function name</string> + <key>scope</key> + <string>entity.name.function</string> + <key>settings</key> + <dict> + <key>fontStyle</key> + <string>italic</string> + <key>foreground</key> + <string>#ebbcba</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Function argument</string> + <key>scope</key> + <string>variable.parameter</string> + <key>settings</key> + <dict> + <key>fontStyle</key> + <string></string> + <key>foreground</key> + <string>#c4a7e7</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Tag name</string> + <key>scope</key> + <string>entity.name.tag</string> + <key>settings</key> + <dict> + <key>fontStyle</key> + <string>bold</string> + <key>foreground</key> + <string>#31748f</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Tag attribute</string> + <key>scope</key> + <string>entity.other.attribute-name</string> + <key>settings</key> + <dict> + <key>fontStyle</key> + <string></string> + <key>foreground</key> + <string>#c4a7e7</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Library function</string> + <key>scope</key> + <string>support.function</string> + <key>settings</key> + <dict> + <key>fontStyle</key> + <string>bold</string> + <key>foreground</key> + <string>#ebbcba</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Library constant</string> + <key>scope</key> + <string>support.constant</string> + <key>settings</key> + <dict> + <key>fontStyle</key> + <string>bold</string> + <key>foreground</key> + <string>#f6c177</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Library class/type</string> + <key>scope</key> + <string>support.type, support.class</string> + <key>settings</key> + <dict> + <key>fontStyle</key> + <string>bold</string> + <key>foreground</key> + <string>#9ccfd8</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Library variable</string> + <key>scope</key> + <string>support.other.variable</string> + <key>settings</key> + <dict> + <key>fontStyle</key> + <string>bold</string> + <key>foreground</key> + <string>#eb6f92</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Invalid</string> + <key>scope</key> + <string>invalid</string> + <key>settings</key> + <dict> + <key>background</key> + <string>#eb6f92</string> + <key>fontStyle</key> + <string></string> + <key>foreground</key> + <string>#e0def4</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Invalid deprecated</string> + <key>scope</key> + <string>invalid.deprecated</string> + <key>settings</key> + <dict> + <key>background</key> + <string>#c4a7e7</string> + <key>foreground</key> + <string>#e0def4</string> + </dict> + </dict> + <dict> + <key>name</key> + <string>Punctuation, Operators</string> + <key>scope</key> + <string>punctuation, keyword.operator</string> + <key>settings</key> + <dict> + <key>foreground</key> + <string>#908caa</string> + </dict> + </dict> + </array> + <key>colorSpaceName</key> + <string>sRGB</string> + <key>semanticClass</key> + <string>theme.dark.rosé-pine</string> + <key>author</key> + <string>arrrgi</string> + <key>comment</key> + <string>All natural pine, faux fur and a bit of soho vibes for the classy minimalist</string> + <key>uuid</key> + <string>14991673-80EB-41A2-BEFF-03216A233730</string> +</dict> +</plist> diff --git a/essay/css/base.css b/essay/css/base.css new file mode 100644 index 0000000..f9d8750 --- /dev/null +++ b/essay/css/base.css @@ -0,0 +1,158 @@ +/* CSS Reset */ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +/* CSS Variables */ +:root { + --color-background: #ffffff; + --color-foreground: #2b303b; + --color-link: #0366d6; + --color-link-hover: #0256b8; + + --font-body: IBM Plex, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --font-heading: IBM Plex Sans, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --font-mono: 'IBM Plex Mono', 'Berkeley Mono', 'Cascadia Code', 'Roboto Mono', Consolas, monospace; + + --spacing-base: 16px; + --spacing-line-height: 1.6; + --spacing-scale: 1.25; +} + +/* Base Styles */ +html { + font-size: var(--spacing-base); + line-height: var(--spacing-line-height); +} + +body { + font-family: var(--font-body); + color: var(--color-foreground); + background-color: var(--color-background); + max-width: 65ch; + margin: 0 auto; + padding: 2rem 1rem; +} + +/* Typography */ +h1, h2, h3, h4, h5, h6 { + font-family: var(--font-heading); + margin-top: calc(1rem * var(--spacing-scale)); + margin-bottom: 0.5rem; + line-height: 1.2; +} + +h1 { font-size: 2.5rem; } +h2 { font-size: 2rem; } +h3 { font-size: 1.5rem; } +h4 { font-size: 1.25rem; } +h5 { font-size: 1.125rem; } +h6 { font-size: 1rem; } + +p { + margin-bottom: 1rem; +} + +a { + color: var(--color-link); + text-decoration: none; +} + +a:hover { + color: var(--color-link-hover); + text-decoration: underline; +} + +/* Lists */ +ul, ol { + margin-left: 2rem; + margin-bottom: 1rem; +} + +li { + margin-bottom: 0.25rem; +} + +/* Code */ +code { + font-family: var(--font-mono); + background-color: rgba(0, 0, 0, 0.05); + padding: 0.125rem 0.25rem; + border-radius: 3px; + font-size: 0.9em; +} + +pre { + overflow-x: auto; + margin-bottom: 1rem; +} + +pre code { + display: block; + padding: 1rem; + background-color: rgba(0, 0, 0, 0.03); + border-radius: 5px; +} + +/* Math */ +.math { + font-family: var(--font-mono); +} + +.math-display { + display: block; + margin: 1rem 0; + text-align: center; +} + +/* Blockquotes */ +blockquote { + border-left: 4px solid var(--color-link); + padding-left: 1rem; + margin: 1rem 0; + font-style: italic; +} + +/* Tables */ +table { + border-collapse: collapse; + width: 100%; + margin-bottom: 1rem; +} + +th, td { + border: 1px solid rgba(0, 0, 0, 0.1); + padding: 0.5rem; + text-align: left; +} + +th { + background-color: rgba(0, 0, 0, 0.05); + font-weight: 600; +} + +/* Footnotes */ +.footnote-reference { + font-size: 0.8em; +} + +.footnote-definition { + margin-top: 2rem; + padding-top: 0.5rem; + border-top: 1px solid rgba(0, 0, 0, 0.1); + font-size: 0.9em; +} + +.footnote-definition-label { + font-weight: 600; + margin-right: 0.5rem; +} + +/* Horizontal Rule */ +hr { + border: none; + border-top: 1px solid rgba(0, 0, 0, 0.1); + margin: 2rem 0; +} diff --git a/essay/css/syntax.css b/essay/css/syntax.css new file mode 100644 index 0000000..de40384 --- /dev/null +++ b/essay/css/syntax.css @@ -0,0 +1,88 @@ +/* + * theme "Rosé Pine" generated by syntect + */ + +.wvrcode-code { + color: #e0def4; + background-color: #191724; +} + +.wvrcode-comment { + color: #908caa; +font-style: italic; +} +.wvrcode-string, .wvrcode-punctuation.wvrcode-definition.wvrcode-string { + color: #f6c177; +} +.wvrcode-constant.wvrcode-numeric { + color: #f6c177; +} +.wvrcode-constant.wvrcode-language { + color: #f6c177; +font-weight: bold; +} +.wvrcode-constant.wvrcode-character, .wvrcode-constant.wvrcode-other { + color: #f6c177; +} +.wvrcode-variable { + color: #e0def4; +font-style: italic; +} +.wvrcode-keyword { + color: #31748f; +} +.wvrcode-storage { + color: #9ccfd8; +} +.wvrcode-storage.wvrcode-type { + color: #9ccfd8; +} +.wvrcode-entity.wvrcode-name.wvrcode-class { + color: #31748f; +font-weight: bold; +} +.wvrcode-entity.wvrcode-other.wvrcode-inherited-class { + color: #31748f; +font-style: italic; +} +.wvrcode-entity.wvrcode-name.wvrcode-function { + color: #ebbcba; +font-style: italic; +} +.wvrcode-variable.wvrcode-parameter { + color: #c4a7e7; +} +.wvrcode-entity.wvrcode-name.wvrcode-tag { + color: #31748f; +font-weight: bold; +} +.wvrcode-entity.wvrcode-other.wvrcode-attribute-name { + color: #c4a7e7; +} +.wvrcode-support.wvrcode-function { + color: #ebbcba; +font-weight: bold; +} +.wvrcode-support.wvrcode-constant { + color: #f6c177; +font-weight: bold; +} +.wvrcode-support.wvrcode-type, .wvrcode-support.wvrcode-class { + color: #9ccfd8; +font-weight: bold; +} +.wvrcode-support.wvrcode-other.wvrcode-variable { + color: #eb6f92; +font-weight: bold; +} +.wvrcode-invalid { + color: #e0def4; + background-color: #eb6f92; +} +.wvrcode-invalid.wvrcode-deprecated { + color: #e0def4; + background-color: #c4a7e7; +} +.wvrcode-punctuation, .wvrcode-keyword.wvrcode-operator { + color: #908caa; +} diff --git a/essay/entry/Features/Allocator.html b/essay/entry/Features/Allocator.html new file mode 100644 index 0000000..86f3491 --- /dev/null +++ b/essay/entry/Features/Allocator.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <title>Allocator + + + + + + diff --git a/essay/entry/Features/Effects.html b/essay/entry/Features/Effects.html new file mode 100644 index 0000000..e98248c --- /dev/null +++ b/essay/entry/Features/Effects.html @@ -0,0 +1,204 @@ + + + + + + Effects + + + + +

Overview

+

Alloy's effect system provides explicit tracking of side effects at the type level. This allows for better reasoning about code behavior and enables powerful optimizations while maintaining clarity about what a function can do.

+

Core Concepts

+

Effect Declaration

+

Effects are declared using the effect keyword:

+
effect IO {
+	read_line() -> String
+	print(String)
+}
+
+effect State[T] {
+	get() -> T
+	set(T)
+}
+
+effect Random {
+	next_int(min: i32, max: i32) -> i32
+}
+
+

Using Effects

+

Functions must declare which effects they use with the with keyword:

+
fn get_user_name() -> String with IO {
+	print("Enter your name: ")
+	read_line()
+}
+
+fn generate_id() -> i32 with Random {
+	next_int(0, 1000)
+}
+
+

Multiple Effects

+

Functions can use multiple effects:

+
fn interactive_random() -> i32 with IO, Random {
+	print("Generating a random number...")
+	let num = next_int(0, 100)
+	print("Generated: {num}")
+	num
+}
+
+

Effect Handlers

+

Effects are implemented using handlers:

+
handler ConsoleIO for IO {
+	fn read_line() -> String {
+		// Implementation using actual console I/O
+	}
+	
+	fn print(message: String) {
+		// Implementation using actual console I/O
+	}
+}
+
+handler TestIO for IO {
+	let mut inputs: Vec[String]
+	let mut outputs: Vec[String]
+	
+	fn read_line() -> String {
+		inputs.pop().unwrap_or("Test Input")
+	}
+
+	fn print(message: String) {
+		outputs.push(messages)
+	}
+}
+
+

Running with Effects

+

Use the run keyword to execute code with effect handlers:

+
fn main() {
+	// Using real I/O
+	run with ConsoleIO {
+		let name = get_user_name()
+		print("Hello, {name}!")
+	}
+
+	// Using test I/O
+	let test_io = TestIO {
+		inputs: vec!["Test User"],
+		outputs: vec![]
+	}
+
+	run with test_io {
+		let name = get_user_name()
+		print("Hello, {name}!")
+	}
+
+	assert_eq(test_io.outputs, ["Enter your name: ", "Hello, Test User!"])
+}
+
+

Effect Composition

+

Combining Effects

+
effect Logger {
+	log(message: String)
+} 
+
+effect Database {
+	query(sql: String) -> Result[Row, DbError]
+}
+
+// Combined effect handler
+fn process_user() -> Result[(), Error] with Logger, Database, IO {
+	log("Starting user processing")
+	let user = match query("SELECT * FROM users LIMIT 1") {
+		Ok(row) -> row,
+		Err(e) -> {
+			log("Database error: {e}")
+			return Err(e.into())
+		}
+	}
+	print("Found user: {user.name}")
+	Ok(())
+}
+
+

Effect Polymorphism

+
// Generic over any effect that provides logging
+
+fn audit[E: Logger](action: String) -> () with E {
+	log("Audit: {action}")
+}
+
+

Best Practices

+
    +
  1. Effect Granularity
  2. +
+
    +
  • Keep effects focused and single-purpose
  • +
  • Split large effects into smaller, composable ones
  • +
+
    +
  1. Effect Isolation
  2. +
+
    +
  • Contain effects at the edges of your program
  • +
  • Keep core logic pure when possible
  • +
+
    +
  1. Testing with Effects
  2. +
+
    +
  • Create test handlers for effects
  • +
  • Use effect substitution to simulate different scenarios
  • +
+
    +
  1. Effect Documentation
  2. +
+
    +
  • Document effect requirements clearly
  • +
  • Explain effect interactions in complex systems
  • +
+
    +
  1. Effect Performance
  2. +
+
    +
  • Group related effects to reduce handler overhead
  • +
  • Consider effect batching for performance-critical code
  • +
+

Patterns

+

The Reader Effect

+
effect Reader[C] {
+	ask() -> C
+}
+  
+fn get_config[C]() -> C with Reader[C] {
+	ask()
+}
+
+fn use_config() -> String with Reader[Config] {
+	let config = ask()
+	config.base_url
+}
+
+

The Writer Effect

+
effect Writer[W] {
+	tell(W)
+	clear()
+}
+
+fn log_action(action: String) -> () with Writer[String] {
+	tell("[{timestamp()}] {action}")
+}
+
+

Combining with Pipelines

+
effect Transform {
+	validate(String) -> Result[String, Error]
+	normalize(String) -> String
+	encrypt(String) -> String
+}
+
+fn process_input(input: String) -> Result[String, Error] with Transform {
+	input
+		|> validate()
+		|> map(normalize)
+		|> map(encrypt)
+}
+
+ diff --git a/essay/entry/Features/Traits.html b/essay/entry/Features/Traits.html new file mode 100644 index 0000000..192a078 --- /dev/null +++ b/essay/entry/Features/Traits.html @@ -0,0 +1,211 @@ + + + + + + Traits + + + + +

Trait System Guide

+

Overview

+

Alloy's trait system builds on Rust's traits while adding powerful features like default implementations, template control, and better composition patterns.

+

Core Concepts

+

The basics follow Rust closely

+

Basic Trait Declaration

+
trait Display {
+	fn display(self) -> String // Note: no semicolon
+}
+
+

Implementation

+
struct Point {
+	x: f64
+	y: f64
+}
+
+impl Display for Point {
+	fn display(self) -> String {
+		"({self.x}, {self.y})" // Alloy builds formatting into Strings
+	}
+}
+
+

Generic Traits

+
trait Container[T] { // Alloy uses [] rather than < > for type parameters
+	fn add(self, item: T)
+	fn remove(self) -> Option[T]
+	fn is_empty(self) -> bool
+}  
+
+impl[T] Container[T] for Stack[T] {
+	fn add(self, item: T) {
+		self.push(item)
+	} 
+
+	fn remove(self) -> Option[T] {
+		self.pop()
+	}
+
+	fn is_empty(self) -> bool {
+		self.len() == 0
+	}
+}
+
+

Features

+

Default Implementations

+

Like in Rust, traits in Alloy can contain a mix of methods which include a default implementation and ones which require the implementer to provide an implementation.

+
trait Collection[T] {
+	fn is_empty(self) -> bool {
+		self.len() == 0
+	}
+  
+	fn len(self) -> usize  
+
+	fn contains(self, item: T) -> bool {
+		self.into_iter().any { x in x == item }
+	}
+}
+
+

Trait Defaults

+

Unlike in Rust, associated types in Alloy can carry a default value. Sensible defaults make writing simple things easy, without losing any flexibility.

+
// Define a trait with a default implementation strategy
+trait ToString {
+	default = JsonString
+	fn to_string(self) -> String
+}  
+
+// Implementations can override the default
+impl ToString for CustomType {
+	fn to_string(self) -> String {
+		// Custom implementation
+	}
+}
+  
+// Or use the default
+impl ToString for SimpleType default
+
+

Template Control

+

Defaults allow more organic composition and make code simpler.

+
trait Serialize[Format] {
+	default Format = Json
+	fn serialize(self) -> String
+}  
+
+// Use default format
+fn save[T: Serialize](item: T) {
+	item.serialize() // Uses Json format
+}  
+
+// Override default format
+fn save_xml[T: Serialize[Xml]](item: T) {
+	item.serialize() // Uses XML format
+}
+
+

Conditional Implementation

+
impl[T: Display] Display for Option[T] {
+	fn display(self) -> String {
+		match self {
+			Some(value) -> value.display()
+			None -> "None"
+		}
+	}
+}
+
+

The above snippet, if not defined in either the same crate as Display or the same crate as Option[T], would require a newtype wrapper in Rust due to the orphan rule, and a bunch of extra boilerplate to make it behave nicely. Alloy either avoids or elides all that for you. Types are types and traits are traits, and you should be able to work with them as you wish. The compiler will attempt to resolve them and handle any possible conflicts as expected, without surprises, but if it cannot, or if you want to choose the implementation, you can specify.

+

Trait Composition

+

Traits can also be composed quite freely using a few simple operators, to build up more complex types, in a way that is easy to understand.

+
trait Identified {
+	fn id(self) -> usize
+}
+  
+trait Named {
+	fn name(self) -> String
+}  
+
+// Compose traits
+trait Entity = Identified + Named {
+	// Additional requirements can be added
+	fn describe(self) -> String {
+		"{self.id()}: {self.name()}"
+	}
+}
+
+// Misha can you check my syntax here?
+trait Component = Identified + !Named // Marker trait
+
+trait GameObject = Entity | Component {
+	fn describe(self) -> String {
+		match typeof self {
+			Entity -> (self as Entity).describe()
+			Component -> "{(self as Identified).id()}"
+		}
+	}
+}
+
+

Best Practices

+
    +
  1. Trait Cohesion
  2. +
+
    +
  • Keep traits focused on a single responsibility
  • +
  • Use composition for complex behaviors
  • +
+
    +
  1. Default Implementations
  2. +
+
    +
  • Provide sensible defaults when possible
  • +
  • Make defaults efficient and general
  • +
+
    +
  1. Template Defaults
  2. +
+
    +
  • Choose widely applicable defaults
  • +
  • Document default behavior clearly
  • +
+
    +
  1. Trait Bounds
  2. +
+
    +
  • Use minimal trait bounds
  • +
  • Prefer composition over complex bounds
  • +
+
    +
  1. Documentation
  2. +
+
    +
  • Document required behavior clearly
  • +
  • Explain performance characteristics
  • +
+

Patterns

+

Builder Pattern

+
trait Builder[T] {
+	fn build(self) -> Result[T, BuildError]  
+
+	// Optional steps with defaults
+	fn with_name(self, name: String) -> Self {
+		self
+	}
+
+	fn with_id(self, id: usize) -> Self {
+		self
+	}
+}
+
+

Type State Pattern

+
trait State {
+	type Next: State
+	fn transition(self) -> Next
+}
+
+  
+implement State for Draft {
+	type Next = Review
+
+	fn transition(self) -> Review {
+		Review::new(self)
+	}
+}
+
+ diff --git a/essay/entry/Language Overview.html b/essay/entry/Language Overview.html new file mode 100644 index 0000000..7669994 --- /dev/null +++ b/essay/entry/Language Overview.html @@ -0,0 +1,86 @@ + + + + + + Language Overview + + + + +

.ac file extension

+

Design Philosophy

+

Safety First

+
    +
  • Thread-safe by default through Arc
  • +
  • Immutable by default
  • +
  • Explicit effect tracking
  • +
  • Comprehensive compile-time checks
  • +
  • No unsafe blocks
  • +
+

Performance

+
    +
  • Zero-cost abstractions where possible
  • +
  • Predictable performance
  • +
  • Efficient compilation to Rust
  • +
  • Optional no-atomic reference counting for embedded systems
  • +
  • Effect system enables compile-time optimizations
  • +
+

Ergonomics

+
    +
  • Clear, readable syntax
  • +
  • Powerful type inference
  • +
  • Pipeline operator for fluid data transformation
  • +
  • Pattern matching with exhaustive checking
  • +
  • Trailing closure syntax
  • +
+

Key Features

+

Type System

+
    +
  • Strong static typing with powerful type inference
  • +
  • Union types and type narrowing from TypeScript
  • +
  • Advanced trait system with defaults and template control
  • +
  • Square brackets [] for generics/templates instead of angle brackets
  • +
  • Algebraic data types with pattern matching
  • +
  • Optional chaining and null coalescing operators from Swift
  • +
+
// Union types and type narrowing
+type StringOrNumber = string | number
+
+fn process(value: StringOrNumber) {
+	match typeof value {
+		"string" -> handle_string(value as string)
+		"number" -> handle_number(value as number)
+	}
+}
+
+// Optional chaining and null coalescing
+let value = obj?.prop?.method() ?? default_value
+
+

Memory Management

+
    +
  • Implicit Arc (Atomic Reference Counting) by default
  • +
  • Optional non-atomic reference counting for platforms without atomics
  • +
  • Custom allocator support via the effect system
  • +
  • Everything immutable by default
  • +
  • Thread-safe by default through Arc
  • +
+

Example:

+
// Standard usage - implicit Arc
+let shared = ComplexType::new()
+
+// No-atomic reference counting for embedded platforms
+
+#[no_atomic]
+let basic = ComplexType::new()
+
+// Custom allocation
+
+let bump_allocator = BumpAllocator::new(1024)
+	run with bump_allocator as Alloc {
+	let temp = ComplexType::new()
+}
+
+

...

+ + diff --git a/essay/entry/Logo.svg b/essay/entry/Logo.svg new file mode 100644 index 0000000..27794da --- /dev/null +++ b/essay/entry/Logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/essay/entry/README.md.html b/essay/entry/README.md.html new file mode 100644 index 0000000..472eec4 --- /dev/null +++ b/essay/entry/README.md.html @@ -0,0 +1,56 @@ + + + + + + README.md + + + + +

Logo.svg

+

Alloy

+

Alloy is a language that fuses Rust's power, safety guarantees, and performance with Swift's ergonomics and TypeScript's type system innovations. It features unique systems like explicit effect tracking and an advanced trait system with defaults. It maintains portability and compatibility by transpiling to Rust code, meaning it runs pretty much anywhere Rust does, and can take advantage of the existing Rust ecosystem.

+

Key Features

+
    +
  • 🚀 Performance: (hopefully) Zero-cost abstractions and efficient compilation to Rust
  • +
  • 🛡️ Safety: Thread-safe by default with implicit Arc
  • +
  • 🎯 Effect Tracking: Explicit side effect management
  • +
  • 🧩 Advanced Traits: Flexible trait system with powerful defaults
  • +
  • 🔄 Ergonomic Syntax: Clean, expressive code
  • +
+

Example

+
effect IO {
+	read_line() -> String
+	print(String)
+}
+
+fn process(input: String) -> String {
+	input
+		|> trim()
+		|> to_uppercase()
+		|> add_exclamation()
+}
+
+fn main() -> Result[(), Error] with IO {
+	print("Enter your name: ")
+	let name = read_line()
+	let processed = process(name)
+	print("Hello, {processed}!")
+	Ok(())
+}
+
+

Documentation

+
+

Status

+

Contributing

+

License

+

Alloy is licensed under the MPL-2.0.

+

License: MPL 2.0

+ + diff --git a/essay/entry/Syntax Guide.html b/essay/entry/Syntax Guide.html new file mode 100644 index 0000000..ab84b5b --- /dev/null +++ b/essay/entry/Syntax Guide.html @@ -0,0 +1,356 @@ + + + + + + Syntax Guide + + + + +

Core Principles

+
    +
  1. Clean and consistent syntax that prioritizes readability
  2. +
  3. Pipeline-oriented flow where appropriate
  4. +
  5. Explicit over implicit
  6. +
  7. Thread-local and immutable by default
  8. +
  9. Square brackets for generics/templates
  10. +
  11. Minimal noise and ceremony
  12. +
  13. Predictable formatting rules
  14. +
+

Basic Syntax Elements

+

Keywords and Names

+

Functions

+
// Function declaration
+fn add(x: i32, y: i32) -> i32 {
+	x + y
+}
+
+// Async function
+fn fetch_data() -> Result[Data, Error] with Async {
+	// ...
+}
+
+// Generic function
+fn map[T, U](value: T, f: fn(T) -> U) -> U {
+	f(value)
+}
+
+

Variables

+

+// Immutable by default
+let x = 5
+
+// Mutable variables must be declared with 'let mut'
+let mut count = 0
+
+// Explicit type annotation
+let name: String = "Alice"
+
+// Pattern binding
+let Point(x, y) = point
+
+

Types and Traits

+
// Type declaration
+type Point {
+	x: f64
+	y: f64
+}
+
+// Enum (sum type)
+type Option[T] {
+	Some(T)
+	None
+}
+
+// Trait declaration
+trait Display {
+	fn display(self) -> String
+}
+
+// Generic trait with associated type
+trait Container[T] {
+	type Item
+	fn get(self) -> Option[T]
+	fn insert(self, item: T)
+}
+
+

Control Flow

+

Pattern Matching

+
// Basic match expression using pipeline
+let description = value
+	|> match {
+		Ok(n) if n > 0 -> "Positive"
+		Ok(n) if n < 0 -> "Negative"
+		Ok(0) -> "Zero"
+		Err(e) -> "Error: {e}"
+	}
+
+// Match with complex patterns
+let command = input
+	|> match {
+		Command.Quit -> exit(0)
+		Command.Move(x, y) -> move_cursor(x, y)
+		Command.Text(str) -> insert_text(str)
+		_ -> beep()
+	}
+
+

Conditionals

+
// If expression
+let status = if count > 0 {
+	"Active"
+} else {
+	"Inactive"
+}
+
+// Guard statement for early returns
+fn process(data: Option[Data]) -> Result[Output, Error] {
+	guard let unwrapped = data else {
+		return Err(Error.InvalidData)
+	}
+	process_unwrapped(unwrapped)
+}
+
+
+

Loops

+
// For-in loop
+for item in items {
+	process(item)
+}
+
+// While loop
+while condition {
+	// ...
+}
+
+// Loop with iterator methods
+items
+	|> filter(pred)
+	|> map(transform)
+	|> collect()
+
+

Error Handling

+

Results and Options

+
// Using Result type
+fn divide(x: f64, y: f64) -> Result[f64, DivisionError] {
+	if y == 0.0 {
+		Err(DivisionError.DivideByZero)
+	} else {
+		Ok(x / y)
+	}
+}
+
+// Chaining Results
+let result = value
+	|> first_operation()
+	|> and_then(second_operation)
+	|> map(third_operation)
+	|> map_err(CustomError)
+
+// Pattern matching on Result
+let output = result
+	|> match {
+		Ok(value) -> process(value)
+		Err(e) -> handle_error(e)
+	}
+
+

Effects and Side Effects

+

Effect Declaration and Usage

+
// Declaring effects
+effect IO {
+	read_line() -> String
+	print(str: String)
+}
+
+// Using effects
+fn greet() -> String with IO {
+	print("What's your name?")
+	let name = read_line()
+	print("Hello, {name}!")
+	name
+}
+
+// Handling effects
+fn main() {
+	run with IO = ConsoleIO {
+		let name = greet()
+		print("Goodbye, {name}")
+	}
+}
+
+

Generics and Traits

+

Generic Types

+
// Generic type declaration
+type List[T] {
+	head: Option[T]
+	tail: Option[Box[List[T]]]
+}
+
+// Generic trait bounds
+fn sort[T: Ord](list: &mut [T]) {
+	// Implementation
+}
+
+// Multiple bounds
+fn process[T: Display + Clone](item: T) {
+	// Implementation
+}
+
+

Trait Implementation

+
implement Display for Point {
+	fn display(self) -> String {
+		"Point({self.x}, {self.y})"
+	}
+}
+
+// Generic implementation
+implement[T: Display] Display for Option[T] {
+	fn display(self) -> String {
+		match self {
+			Some(value) -> "Some({value})"
+			None -> "None"
+		}
+	}
+}
+
+
+

Concurrency

+

Async/Await

+
// Async function declaration
+fn fetch_data(url: String) -> Result[Data, Error] with Async {
+	let response = http.get(url)?.await
+	let data = response.json()?.await
+	Ok(data)
+}
+
+// Using async functions
+fn process() with Async {
+	let data = fetch_data("example.com")?
+		|> await
+	process_data(data)
+		|> await
+}
+
+

Thread Safety

+
// Thread-local by default
+let counter = 0
+
+// Explicitly shared state
+shared let global_counter = AtomicInteger(0)
+
+// Shared function
+shared fn increment() {
+	global_counter.fetch_add(1)
+}
+
+

Pipeline Operator

+

Basic Usage

+
// Pipeline with simple transformations
+let result = value
+	|> transform1()
+	|> transform2()
+	|> transform3()
+	
+// Pipeline with closures
+let processed = data
+	|> map(x -> x * 2)
+	|> filter(x -> x > 0)
+	|> fold(0, (acc, x) -> acc + x)
+
+

Complex Pipelines

+
let result = initial_value
+	|> validate()
+	|> match {
+		Ok(value) -> process(value)
+		Err(e) -> handle_error(e)
+	}
+	|> transform()
+	|> finalize()
+
+

Standard Library Conventions

+

Iterator Methods

+
// Method chaining with iterators
+let sum = numbers
+	|> filter(n -> n > 0)
+	|> map(n -> n * n)
+	|> sum()
+
+// Collecting results
+let vec = iter
+	|> filter(pred)
+	|> map(transform)
+	|> collect()
+
+

String Formatting

+
// String interpolation
+let message = "Hello, {name}!"
+
+// Multi-line strings
+let text = """
+	This is a multi-line
+	string with proper
+	indentation handling
+"""
+
+

Module System

+

Module Declaration

+
// Module declaration
+module math {
+	pub fn add(x: i32, y: i32) -> i32 {
+		x + y
+	}
+}
+
+// Using modules
+use math.{add, subtract}
+use collections.{HashMap, Vector}
+
+

Comments and Documentation

+

Documentation Comments

+
/// Function documentation
+///
+/// # Arguments
+/// * `x` - First number
+/// * `y` - Second number
+///
+/// # Returns
+/// Sum of the two numbers
+
+fn add(x: i32, y: i32) -> i32 {
+	x + y
+}
+
+// Regular single-line comment
+
+/* Block comment
+spanning multiple
+lines */
+
+
+

Formatting Rules

+
    +
  1. Use 4 spaces for indentation
  2. +
  3. One statement per line
  4. +
  5. Blank line between function definitions
  6. +
  7. No semicolon for expressions that are the last in a block
  8. +
  9. Space after keywords
  10. +
  11. No space after function names in calls
  12. +
  13. Align match arms
  14. +
  15. Pipeline operators at the start of lines
  16. +
  17. Break long lines at sensible points (pipelines, binary operators)
  18. +
+

Best Practices

+
    +
  1. Prefer pipeline style for transformations and matches
  2. +
  3. Use pattern matching over if/else chains
  4. +
  5. Make effects explicit
  6. +
  7. Keep functions focused and small
  8. +
  9. Use meaningful and descriptive names
  10. +
  11. Document public interfaces
  12. +
  13. Utilize type inference where it aids readability
  14. +
  15. Make thread sharing explicit
  16. +
  17. Handle all error cases explicitly
  18. +
  19. Use appropriate abstraction levels
  20. +
+ + diff --git a/essay/entry/Untitled.html b/essay/entry/Untitled.html new file mode 100644 index 0000000..40a7c96 --- /dev/null +++ b/essay/entry/Untitled.html @@ -0,0 +1,37 @@ + + + + + + Untitled + + + + +

+struct Encoder {
+	
+}
+
+impl Encoder {
+	fn new(chars: &[char]) --> Self {
+		Self {
+			chars,
+			position: 0,
+			is_number: false,
+			number_start: 0
+		}
+	}
+}
+
+let chars = {}
+
+let enc = Encoder::new(chars)
+
+for char in enc.chars {
+	
+}
+
+
+
+ diff --git a/essay/entry/todo!.html b/essay/entry/todo!.html new file mode 100644 index 0000000..71e68c7 --- /dev/null +++ b/essay/entry/todo!.html @@ -0,0 +1,26 @@ + + + + + + todo! + + + + +
+

kanban-plugin: board

+
+

PLANNED

+

NOT STARTED

+

BLOCKED

+

IN PROGRESS

+

IN TESTING

+

DONE

+

Complete

+

%% kanban:settings

+
{"kanban-plugin":"board","list-collapse":[true,true,true,true,true,true]}
+
+

%%

+ + diff --git a/essay/index.html b/essay/index.html new file mode 100644 index 0000000..b6ea4a4 --- /dev/null +++ b/essay/index.html @@ -0,0 +1,24 @@ + + + + + + Language Overview + + + + +

Index

+ + + diff --git a/grant_proposal.html b/grant_proposal.html new file mode 100644 index 0000000..2cd7615 --- /dev/null +++ b/grant_proposal.html @@ -0,0 +1,287 @@ + + + + + + grant_proposal + + + + +

In a short paragraph, tell us about your project and what you're building.

+

Weaver is designed to be a highly flexible platform for medium and long-form writing on atproto. I was inspired by how weaver birds build their own homes, and by the notebooks, physical and virtual, that I create in the course of my work, to ideate, to document, and to inform. The initial proof-of-concept is a static site generator, able to turn a Markdown text file or a folder of Markdown files, such as an Obsidian vault or git repository documentation, into a static "notebook" site. The file is uploaded to your PDS, where it can be accessed, either directly, via a minimal appview layer that provides a friendlier web address than an XRPC request or CDN link, or hosted on a platform of your choice, be that your own server or any other static site hosting service. The intermediate goal is an elegant and intuitive writing platform with collaborative editing and straightforward, immediate publishing via a web-app. The ultimate goal is to build a platform suitable for professional writers and journalists, an open alternative to platforms like Substack, with ways for readers to support writers, all on the AT protocol. Currently it is in the earliest stages of development, working on core components such as the parsing and rendering backend, command-line client, and initial appview. The plan is to have the proof-of-concept ready for testing in June, and to immediately begin using it for my own writing, as well as information and updates about the project.

+

How do you plan to use this grant? Feel free to be specific.

+

I plan to use the grant money to help with hosting and/or hardware costs (such as VPS rental fees or upgrades to my homelab, like a backup internet connection), and potentially to offset the costs of incorporation. If I'm not able to dedicate enough time to this or bring other people onto the project, it won't get off the ground, and some help with expenses makes that possible.

+

Who are you? Please provide a brief bio or background about yourself.

+

I'm Orual, an electronics designer and software engineer who routinely works across the entire stack from bare metal microcontrollers all the way up to web services, UX development, and AI. I grew up getting mail delivered by single-engine Cessna airplane to a grass airstrip outside a tiny village in Congo and listening to my parents talk with people half-way across the country on a short-wave radio, later using an early satellite phone to send email, the ultimate decentralized information network, maybe. I wrote my first line of code when I was ten, started building robots soon after, got into university for engineering, designed and built electronic musical instruments, dropped out of university, transitioned my sex, learned a ton about programming and electronics, went back to college and got into research.

+

Currently, I work part-time as a principal investigator in a small applied research lab at a college, helping startups and other businesses with product development and prototyping. I'm a long-time enthusiast for blogging and other forms of extended online writing, but have always been dissatisfied with existing solutions, which is the impetus for Weaver, and because of my experience across the whole tech stack, I think I can deliver something really great.

+

What excites you about building in the ATProto/Bluesky ecosystem? This helps us understand what's driving you.

+

I'm a Utopian in the Terra Ignota sense. I want to see us flourish and go out into the universe. I want people to be free and able to create and build wonders, not beholden to any sovereigns, in whatever forms they wish. My vocation is engineering. It is the thing I cannot help but do, and I take great pleasure and pride in it. In the ATProto ecosystem I see a way to direct my skills toward creating the kinds of tools and platforms that empower people.

+

I got onto Bluesky early and was immediately hooked for a number of reasons. The first is the actual protocol itself. How it does identity, how the user maintains control over their identity and data, largely independent of hosting and services, and how no single service needs to do it all, you can mix and match. It's the embodiment of a lot of the ideas I've had about how the social web should work for a long time.

+

In addition to championing the atproto identity model and overall philosophy to clients at work, I’ve done some early explorations with private data and varied interaction modalities via embedded systems, such as a Rust XRPC client for microcontrollers like the ESP32. I've been watching projects like Whitewind and Leaflet explore the long-form atproto writing space, and I think there's room for a more flexible, extensible, and collaborative approach to the concept on the protocol.

+

The other primary staying factor has been the community. The Bluesky dev team has been so enthusiastic and welcoming for those who want to work on and with the protocol. One personal example is that of Morpho, an Android Bluesky client, which received an ATProtocol microgrant. I’ve made a lot of friends on Bluesky, and I want to grow the ecosystem which allowed that to happen, to help it flourish and become more than just a grand dream behind a Twitter clone.

+

How does your project align with our goal of accelerating growth in the ATProto/Bluesky ecosystem with developer tools?

+

The initial alpha version of Weaver is targeted at developers and people otherwise comfortable with a terminal. It aims to bring the process of documenting your tools, educating people, and otherwise writing and publishing things that don’t fit well into a Bluesky post or thread onto the protocol in a way that is highly flexible. If that is all that it does, that’s excellent and meets a need that I see in the community. It would provide a consistent way to make a blog built on ATProto without everyone having to reinvent part or all of the wheel, or do programming they don't want to do.

+

As Weaver develops further and targets a less technical audience, it will retain those more developer-focused modes of use, for people who want to do something more specialized with it, have a workflow they like which doesn’t mesh with the web editor (and if they want to write their own tooling on top of Weaver, more power to them), or just want to run it all themselves. Developers are also the initial audience because the alpha will inevitably be rough around the edges, and nobody provides quality feedback like other developers.

+

How does your project align with our goal of empowering communities with the tools to cultivate and sustain themselves?

+

Communities don’t just need microblogging posts and algorithmic feeds. They need places for longer-form content, less ephemeral works they can point to; stories, essays, rules, histories, articles, and so on. One of the biggest downsides of so many companies and communities moving onto closed platforms like Discord was the lack of durability of any content by default and the inability to easily access and search it from outside the platform, and one of the downsides of “identity-follower”-based platforms like Bluesky (where relationships are about following specific people) is that they produce communities highly focused on specific people as much as specific topics or interests. Weaver notebooks and entries within them can be throwaways, but they’re intended to stick around and remain accessible in a consistent way (unless taken down).

+

There’s a need for better tools for that kind of thing on the protocol right now, as currently a lot of stuff is bespoke, feels incomplete as an experience, or is just not really connected to the protocol and so misses on some of the data ownership of ATProto.

+

One aspect which makes Weaver as designed good for communities is simply that, unlike with Bluesky, where a follower is a follower and you only have one identity, you can have more than one notebook (and they can be shared between multiple authors), and more than one author profile. Someone might make a personal blog, or one for a project, or to write a piece of web fiction with a friend, either all under the same profile or under different ones, as fits their wants and needs. People have different contextual identities but don’t necessarily want to make alt accounts, and a writing platform for people should reflect that.

+

Collaborative editing is a great feature for communities as well. People can workshop something with their friends and then publish it instantly. You could build a wiki for a fandom community on Weaver. Two journalists could publish a newsletter together, and bring on an editor to tighten up their articles. And over time, Weaver would ultimately support payments, helping creators fund their work.

+

How does your project align with our goal of giving people direct, unmediated access to their audiences?

+

The first and maybe most powerful thing I’m going to give people that sort of direct and durable access is building complete self-hosting into it from day 1, inverting the pattern Bluesky followed. Weaver is designed to outlive Bluesky and even to some extent the AT Protocol. The failure mode is much the same as a tool like Obsidian, which is a direct inspiration. Because the core document format is Markdown, which is fundamentally just a text file you can load up in any editor, and the basic tools are designed to work without any ATProto authentication, people can keep using them and easily port or host the contents however they wish. You can go where your audience is.

+

Beyond that, the platform features are all about making the process of writing something and putting it out there for people to read as simple as possible, in a way that means you have ownership of that data, the same as anything else on the protocol, Even when collaborating on a document, you retain control, via much the same means that each person's repository uses to maintain consistency. And since it's designed to interoperate widely, you can easily bring notebooks and entries from Weaver to your audience on Bluesky or any other ATProto platform, and they can comment on them from Bluesky.

+

Other things to share?

+

Here's a bit more technical detail on what will underpin the collaborative features. A notebook entry on the protocol is a type of CRDT, a conflict-free replicated data type, built on top of Loro, stored as a series of your edit records on top of a root record in your own repository (with others’ edit records being stored in their own repositories), along with the contents of the most recently seen (and most recently published) versions.

+

With the combination of those pieces, if one person deletes a notebook or entry, everyone else in the list of authors retains the contents and can recreate most of the edit history, even if the Weaver appview doesn't have the deleted records cached.

+

The appview itself is a couple of components. One is a lightweight entryway service, which mostly provides that friendly external url for published notebooks, redirecting to an author's PDS or a CDN and handling some authentication (e.g. to ensure that, while an unpublished or controlled-audience notebook or entry does exist in one's PDS unencrypted, one cannot just navigate to it). The second component is the backend for the collaborative features and will host the web-based editing and navigation interface. All of this is designed to be used or run independently if desired.

+ +