diff --git a/Cargo.lock b/Cargo.lock index a6bdf40..352f5b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5787,6 +5787,7 @@ dependencies = [ "pin-utils", "regex", "reqwest", + "smol_str", "syntect", "thiserror 2.0.17", "tokio", diff --git a/crates/weaver-renderer/Cargo.toml b/crates/weaver-renderer/Cargo.toml index d135e50..f91d98f 100644 --- a/crates/weaver-renderer/Cargo.toml +++ b/crates/weaver-renderer/Cargo.toml @@ -28,6 +28,7 @@ pin-utils = "0.1.0" pin-project = "1.1.10" dynosaur = "0.2.0" async-trait = "0.1.88" +smol_str = { version = "0.3", features = ["serde"] } reqwest = { version = "0.12.7", default-features = false, features = [ "json", "rustls-tls", diff --git a/crates/weaver-renderer/src/code_pretty.rs b/crates/weaver-renderer/src/code_pretty.rs index 30308e1..28fbde5 100644 --- a/crates/weaver-renderer/src/code_pretty.rs +++ b/crates/weaver-renderer/src/code_pretty.rs @@ -9,7 +9,7 @@ use syntect::util::LinesWithEndings; /// This requires an external stylesheet, also generated by syntect to be loaded by the page. /// The syntect SyntaxSet is also provided, so that it is not re-created on every call. pub fn highlight( - syn_set: SyntaxSet, + syn_set: &SyntaxSet, lang: Option<&str>, code: impl AsRef, writer: &mut M, @@ -33,7 +33,7 @@ where let mut html_gen = ClassedHTMLGenerator::new_with_class_style( lang_syn, - &syn_set, + syn_set, ClassStyle::SpacedPrefixed { prefix: CSS_PREFIX }, ); for line in LinesWithEndings::from(code.as_ref()) { diff --git a/crates/weaver-renderer/src/css.rs b/crates/weaver-renderer/src/css.rs new file mode 100644 index 0000000..cf9e983 --- /dev/null +++ b/crates/weaver-renderer/src/css.rs @@ -0,0 +1,200 @@ +use crate::theme::Theme; +use miette::IntoDiagnostic; +use syntect::highlighting::ThemeSet; +use syntect::html::{ClassStyle, css_for_theme_with_class_style}; +use syntect::parsing::SyntaxSet; + +pub fn generate_base_css(theme: &Theme) -> String { + format!( + r#"/* CSS Reset */ +*, *::before, *::after {{ + box-sizing: border-box; + margin: 0; + padding: 0; +}} + +/* CSS Variables */ +:root {{ + --color-background: {}; + --color-foreground: {}; + --color-link: {}; + --color-link-hover: {}; + + --font-body: {}; + --font-heading: {}; + --font-mono: {}; + + --spacing-base: {}; + --spacing-line-height: {}; + --spacing-scale: {}; +}} + +/* 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; +}} +"#, + theme.colors.background, + theme.colors.foreground, + theme.colors.link, + theme.colors.link_hover, + theme.fonts.body, + theme.fonts.heading, + theme.fonts.monospace, + theme.spacing.base_font_size, + theme.spacing.line_height, + theme.spacing.scale, + ) +} + +pub fn generate_syntax_css( + syntect_theme_name: &str, + _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 css = css_for_theme_with_class_style( + theme, + ClassStyle::SpacedPrefixed { + prefix: crate::code_pretty::CSS_PREFIX, + }, + ) + .into_diagnostic()?; + + Ok(css) +} diff --git a/crates/weaver-renderer/src/lib.rs b/crates/weaver-renderer/src/lib.rs index de75b48..a4d18e4 100644 --- a/crates/weaver-renderer/src/lib.rs +++ b/crates/weaver-renderer/src/lib.rs @@ -27,7 +27,9 @@ use std::task::Poll; pub mod atproto; pub mod base_html; pub mod code_pretty; +pub mod css; pub mod static_site; +pub mod theme; pub mod types; pub mod utils; pub mod walker; diff --git a/crates/weaver-renderer/src/static_site.rs b/crates/weaver-renderer/src/static_site.rs index 3e01fe7..a52e090 100644 --- a/crates/weaver-renderer/src/static_site.rs +++ b/crates/weaver-renderer/src/static_site.rs @@ -5,45 +5,30 @@ //! URLs in the notebook are mostly unaltered. It is compatible with GitHub or Cloudflare Pages //! and other similar static hosting services. -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; +pub mod context; +pub mod document; +pub mod writer; use crate::{ ContextIterator, NotebookProcessor, - base_html::TableState, + static_site::{context::StaticSiteContext, writer::StaticPageWriter}, utils::flatten_dir_to_just_one_parent, walker::{WalkOptions, vault_contents}, }; use bitflags::bitflags; -use dashmap::DashMap; -use markdown_weaver::{ - Alignment, BlockQuoteKind, BrokenLink, CodeBlockKind, CowStr, EmbedType, Event, LinkType, - Parser, Tag, WeaverAttributes, -}; -use markdown_weaver_escape::{ - FmtWriter, StrWrite, escape_href, escape_html, escape_html_body_text, -}; +use markdown_weaver::{BrokenLink, CowStr, Parser}; +use markdown_weaver_escape::FmtWriter; use miette::IntoDiagnostic; #[cfg(all(target_family = "wasm", target_os = "unknown"))] use n0_future::io::AsyncWriteExt; -use n0_future::{IterExt, StreamExt}; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; #[cfg(not(all(target_family = "wasm", target_os = "unknown")))] use tokio::io::AsyncWriteExt; use unicode_normalization::UnicodeNormalization; -use weaver_common::{ - aturi_to_http, - jacquard::{ - client::{Agent, AgentSession, AgentSessionExt}, - identity::resolver::IdentityError, - prelude::*, - types::blob::MimeType, - }, -}; -use yaml_rust2::Yaml; - -use crate::{Frontmatter, NotebookContext}; +use weaver_common::jacquard::{client::AgentSession, prelude::*}; bitflags! { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -88,413 +73,14 @@ pub fn default_md_options() -> markdown_weaver::Options { | markdown_weaver::Options::ENABLE_HEADING_ATTRIBUTES } -pub struct StaticSiteContext<'a, A: AgentSession> { - options: StaticSiteOptions, - md_options: markdown_weaver::Options, - pub bsky_appview: CowStr<'a>, - root: PathBuf, - pub destination: PathBuf, - start_at: PathBuf, - pub frontmatter: Arc>, - dir_contents: Option>, - reference_map: Arc, PathBuf>>, - titles: Arc>>, - position: usize, - client: Option, - agent: Option>>, -} - -impl<'a, A: AgentSession> Clone for StaticSiteContext<'a, A> { - fn clone(&self) -> Self { - Self { - options: self.options.clone(), - md_options: self.md_options.clone(), - bsky_appview: self.bsky_appview.clone(), - root: self.root.clone(), - destination: self.destination.clone(), - start_at: self.start_at.clone(), - frontmatter: self.frontmatter.clone(), - dir_contents: self.dir_contents.clone(), - reference_map: self.reference_map.clone(), - titles: self.titles.clone(), - position: self.position.clone(), - client: self.client.clone(), - agent: self.agent.clone(), - } - } -} - -impl StaticSiteContext<'_, A> { - pub fn clone_with_dir_contents(&self, dir_contents: &[PathBuf]) -> Self { - Self { - start_at: self.start_at.clone(), - root: self.root.clone(), - bsky_appview: self.bsky_appview.clone(), - options: self.options.clone(), - md_options: self.md_options.clone(), - frontmatter: self.frontmatter.clone(), - dir_contents: Some(Arc::from(dir_contents)), - destination: self.destination.clone(), - reference_map: self.reference_map.clone(), - titles: self.titles.clone(), - position: self.position, - client: self.client.clone(), - agent: self.agent.clone(), - } - } - - pub fn clone_with_path(&self, path: impl AsRef) -> Self { - let position = if let Some(dir_contents) = &self.dir_contents { - dir_contents - .iter() - .position(|p| p == path.as_ref()) - .unwrap_or(0) - } else { - 0 - }; - Self { - start_at: self.start_at.clone(), - root: self.root.clone(), - bsky_appview: self.bsky_appview.clone(), - options: self.options.clone(), - md_options: self.md_options.clone(), - frontmatter: self.frontmatter.clone(), - dir_contents: self.dir_contents.clone(), - destination: self.destination.clone(), - reference_map: self.reference_map.clone(), - titles: self.titles.clone(), - position, - client: Some(reqwest::Client::default()), - agent: self.agent.clone(), - } - } - pub fn new(root: PathBuf, destination: PathBuf, session: Option) -> Self { - Self { - start_at: root.clone(), - root, - bsky_appview: CowStr::Borrowed("deer.social"), - options: StaticSiteOptions::default(), - md_options: default_md_options(), - frontmatter: Arc::new(DashMap::new()), - dir_contents: None, - destination, - reference_map: Arc::new(DashMap::new()), - titles: Arc::new(DashMap::new()), - position: 0, - client: Some(reqwest::Client::default()), - agent: session.map(|session| Arc::new(Agent::new(session))), - } - } - - pub fn current_path(&self) -> &PathBuf { - if let Some(dir_contents) = &self.dir_contents { - &dir_contents[self.position] - } else { - &self.start_at - } - } - - #[inline] - pub fn handle_link_aturi<'s>(&self, link: Tag<'s>) -> Tag<'s> { - let link = crate::utils::resolve_at_ident_or_uri(&link, &self.bsky_appview); - self.handle_link_normal(link) - } - - pub async fn handle_embed_aturi<'s>(&self, embed: Tag<'s>) -> Tag<'s> { - match &embed { - Tag::Embed { - embed_type, - dest_url, - title, - id, - attrs, - } => { - if dest_url.starts_with("at://") { - let width = if let Some(attrs) = attrs { - let mut width = 600; - for attr in &attrs.attrs { - if attr.0 == CowStr::Borrowed("width".into()) { - width = attr.1.parse::().unwrap_or(600); - break; - } - } - width - } else { - 600 - }; - let html = if let Some(client) = &self.client { - if let Ok(resp) = client - .get("https://embed.bsky.app/oembed") - .query(&[ - ("url", dest_url.clone().into_string()), - ("maxwidth", width.to_string()), - ]) - .send() - .await - { - resp.text().await.ok() - } else { - None - } - } else { - None - }; - if let Some(html) = html { - let link = aturi_to_http(&dest_url, &self.bsky_appview) - .expect("assuming the at-uri is valid rn"); - let mut attrs = if let Some(attrs) = attrs { - attrs.clone() - } else { - WeaverAttributes { - classes: vec![], - attrs: vec![], - } - }; - attrs.attrs.push(("content".into(), html.into())); - Tag::Embed { - embed_type: EmbedType::Comments, // change this when i update markdown-weaver - dest_url: link.into_static(), - title: title.clone(), - id: id.clone(), - attrs: Some(attrs), - } - } else { - self.handle_embed_normal(embed).await - } - } else { - self.handle_embed_normal(embed).await - } - } - _ => embed, - } - } - - pub async fn handle_embed_normal<'s>(&self, embed: Tag<'s>) -> Tag<'s> { - // This option will REALLY slow down iteration over events. - if self.options.contains(StaticSiteOptions::INLINE_EMBEDS) { - match &embed { - Tag::Embed { - embed_type: _, - dest_url, - title, - id, - attrs, - } => { - let mut attrs = if let Some(attrs) = attrs { - attrs.clone() - } else { - WeaverAttributes { - classes: vec![], - attrs: vec![], - } - }; - let contents = if crate::utils::is_local_path(dest_url) { - let file_path = if crate::utils::is_relative_link(dest_url) { - let root_path = self.root.clone(); - root_path.join(Path::new(&dest_url as &str)) - } else { - PathBuf::from(&dest_url as &str) - }; - crate::utils::inline_file(&file_path).await - } else if let Some(client) = &self.client { - if let Ok(resp) = client.get(dest_url.clone().into_string()).send().await { - resp.text().await.ok() - } else { - None - } - } else { - None - }; - if let Some(contents) = contents { - attrs.attrs.push(("content".into(), contents.into())); - Tag::Embed { - embed_type: EmbedType::Markdown, // change this when i update markdown-weaver - dest_url: dest_url.clone(), - title: title.clone(), - id: id.clone(), - attrs: Some(attrs), - } - } else { - embed - } - } - _ => embed, - } - } else { - embed - } - } - - /// This is a no-op for the static site renderer currently. - #[inline] - pub fn handle_link_normal<'s>(&self, link: Tag<'s>) -> Tag<'s> { - link - } - - /// This is a no-op for the static site renderer currently. - #[inline] - pub fn handle_image_normal<'s>(&self, image: Tag<'s>) -> Tag<'s> { - image - } -} - -impl StaticSiteContext<'_, A> { - /// TODO: rework this a bit, to not just do the same thing as whitewind - /// (also need to make a record to refer to them) that being said, doing - /// this with the static site renderer isn't *really* the standard workflow - pub async fn upload_image<'s>(&self, image: Tag<'s>) -> Tag<'s> { - if let Some(agent) = &self.agent { - match &image { - Tag::Image { - link_type, - dest_url, - title, - id, - attrs, - } => { - if crate::utils::is_local_path(&dest_url) { - let root_path = self.root.clone(); - let file_path = root_path.join(Path::new(&dest_url as &str)); - if let Ok(image_data) = std::fs::read(&file_path) { - if let Ok(blob) = agent - .upload_blob(image_data, MimeType::new_static("image/jpg")) - .await - { - let (did, _) = agent.info().await.unwrap(); - let url = weaver_common::blob_url( - &did, - agent.endpoint().await.as_str(), - &blob.r#ref.0, - ); - return Tag::Image { - link_type: *link_type, - dest_url: url.into(), - title: title.clone(), - id: id.clone(), - attrs: attrs.clone(), - }; - } - } - } - } - _ => {} - } - } - image - } -} - -impl NotebookContext for StaticSiteContext<'_, A> { - fn set_entry_title(&self, title: CowStr<'_>) { - let path = self.current_path(); - self.titles - .insert(path.clone(), title.clone().into_static()); - self.frontmatter.get_mut(path).map(|frontmatter| { - if let Ok(mut yaml) = frontmatter.yaml.write() { - if yaml.get(0).is_some_and(|y| y.is_hash()) { - let map = yaml.get_mut(0).unwrap().as_mut_hash().unwrap(); - map.insert( - Yaml::String("title".into()), - Yaml::String(title.into_static().into()), - ); - } - } - }); - } - fn entry_title(&self) -> CowStr<'_> { - let path = self.current_path(); - self.titles.get(path).unwrap().clone() - } - - fn frontmatter(&self) -> Frontmatter { - let path = self.current_path(); - self.frontmatter.get(path).unwrap().value().clone() - } - - fn set_frontmatter(&self, frontmatter: Frontmatter) { - let path = self.current_path(); - self.frontmatter.insert(path.clone(), frontmatter); - } - - async fn handle_link<'s>(&self, link: Tag<'s>) -> Tag<'s> { - bitflags::bitflags_match!(self.options, { - // Split this somehow or just combine the options - StaticSiteOptions::RESOLVE_AT_URIS | StaticSiteOptions::RESOLVE_AT_IDENTIFIERS => { - self.handle_link_aturi(link) - } - _ => match &link { - Tag::Link { link_type, dest_url, title, id } => { - if self.options.contains(StaticSiteOptions::FLATTEN_STRUCTURE) { - let (parent, filename) = crate::utils::flatten_dir_to_just_one_parent(&dest_url); - let dest_url = if crate::utils::is_relative_link(&dest_url) - && self.options.contains(StaticSiteOptions::CREATE_CHAPTERS_BY_DIRECTORY) { - if !parent.is_empty() { - CowStr::Boxed(format!("./{}/{}", parent, filename).into_boxed_str()) - } else { - CowStr::Boxed(format!("./{}", filename).into_boxed_str()) - } - } else { - CowStr::Boxed(format!("./entry/{}", filename).into_boxed_str()) - }; - Tag::Link { - link_type: *link_type, - dest_url, - title: title.clone(), - id: id.clone(), - } - } else { - link - - } - }, - _ => link, - } - }) - } - - async fn handle_image<'s>(&self, image: Tag<'s>) -> Tag<'s> { - if self.options.contains(StaticSiteOptions::UPLOAD_BLOBS) { - self.upload_image(image).await - } else { - self.handle_image_normal(image) - } - } - - async fn handle_embed<'s>(&self, embed: Tag<'s>) -> Tag<'s> { - if self.options.contains(StaticSiteOptions::RESOLVE_AT_URIS) - || self.options.contains(StaticSiteOptions::ADD_LINK_PREVIEWS) - { - self.handle_embed_aturi(embed).await - } else { - self.handle_embed_normal(embed).await - } - } - - fn handle_reference(&self, reference: CowStr<'_>) -> CowStr<'_> { - let reference = reference.into_static(); - if let Some(reference) = self.reference_map.get(&reference) { - let path = reference.value().clone(); - CowStr::Boxed(path.to_string_lossy().into_owned().into_boxed_str()) - } else { - reference - } - } - - fn add_reference(&self, reference: CowStr<'_>) { - let path = self.current_path(); - self.reference_map - .insert(reference.into_static(), path.clone()); - } -} - -pub struct StaticSiteWriter<'a, A> +pub struct StaticSiteWriter where A: AgentSession, { - context: StaticSiteContext<'a, A>, + context: StaticSiteContext, } -impl<'a, A> StaticSiteWriter<'a, A> +impl StaticSiteWriter where A: AgentSession, { @@ -504,9 +90,9 @@ where } } -impl<'a, A> StaticSiteWriter<'a, A> +impl StaticSiteWriter where - A: AgentSession + IdentityResolver + 'a, + A: AgentSession + IdentityResolver + 'static, { pub async fn run(mut self) -> Result<(), miette::Report> { if !self.context.root.exists() { @@ -555,6 +141,9 @@ where )); } + let mut writers = + Vec::with_capacity(self.context.dir_contents.clone().unwrap_or_default().len()); + for file in self .context .dir_contents @@ -565,35 +154,46 @@ where .filter(|file| file.starts_with(&self.context.start_at)) { let context = self.context.clone(); - let relative_path = file - .strip_prefix(context.start_at.clone()) - .expect("file should always be nested under root") - .to_path_buf(); - 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 output_path = context - .destination - .join(String::from(parent)) - .join(String::from(file)); + let file = file.clone(); + // we'll see if this is a problem or not + writers.push(n0_future::task::spawn(async move { + let relative_path = file + .strip_prefix(context.start_at.clone()) + .expect("file should always be nested under root") + .to_path_buf(); + 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 output_path = context + .destination + .join(String::from(parent)) + .join(String::from(file)); + + write_page(context.clone(), relative_path, output_path).await?; + } else { + let output_path = context.destination.join(relative_path.clone()); - write_page(context.clone(), relative_path, output_path).await?; - } else { - let output_path = context.destination.join(relative_path.clone()); + write_page(context.clone(), relative_path, output_path).await?; + } + Ok::<(), miette::Report>(()) + })); + } - write_page(context.clone(), relative_path, output_path).await?; - } + // def want to scope these so we wait until they all complete before we return + // and then we def want the errors, or at least the first error + for fut in n0_future::join_all(writers).await.into_iter() { + fut.into_diagnostic()??; } Ok(()) } } -pub async fn export_page<'s, 'input, A>( +pub async fn export_page<'input, A>( contents: &'input str, - context: StaticSiteContext<'s, A>, + context: StaticSiteContext, ) -> Result where A: AgentSession + IdentityResolver, @@ -616,8 +216,8 @@ where Ok(output) } -pub async fn write_page<'s, A>( - context: StaticSiteContext<'s, A>, +pub async fn write_page( + context: StaticSiteContext, input_path: impl AsRef, output_path: impl AsRef, ) -> Result<(), miette::Report> @@ -637,661 +237,6 @@ where Ok(()) } -pub struct StaticPageWriter< - 'a, - 'input, - I: Iterator>, - A: AgentSession, - W: StrWrite, -> { - context: NotebookProcessor<'input, I, StaticSiteContext<'a, A>>, - writer: W, - /// Whether or not the last write wrote a newline. - end_newline: bool, - - /// Whether if inside a metadata block (text should not be written) - in_non_writing_block: bool, - - table_state: TableState, - table_alignments: Vec, - table_cell_index: usize, - numbers: DashMap, usize>, -} - -impl<'a, 'input, I: Iterator>, A: AgentSession, W: StrWrite> - StaticPageWriter<'a, 'input, I, A, W> -{ - pub fn new(context: NotebookProcessor<'input, I, StaticSiteContext<'a, A>>, writer: W) -> Self { - Self { - context, - writer, - end_newline: true, - in_non_writing_block: false, - table_state: TableState::Head, - table_alignments: vec![], - table_cell_index: 0, - numbers: DashMap::new(), - } - } - - /// Writes a new line. - #[inline] - fn write_newline(&mut self) -> Result<(), W::Error> { - self.end_newline = true; - self.writer.write_str("\n") - } - - /// Writes a buffer, and tracks whether or not a newline was written. - #[inline] - fn write(&mut self, s: &str) -> Result<(), W::Error> { - self.writer.write_str(s)?; - - if !s.is_empty() { - self.end_newline = s.ends_with('\n'); - } - Ok(()) - } - - fn end_tag(&mut self, tag: markdown_weaver::TagEnd) -> Result<(), W::Error> { - use markdown_weaver::TagEnd; - match tag { - TagEnd::HtmlBlock => {} - TagEnd::Paragraph => { - self.write("

\n")?; - } - TagEnd::Heading(level) => { - self.write("\n")?; - } - TagEnd::Table => { - self.write("\n")?; - } - TagEnd::TableHead => { - self.write("\n")?; - self.table_state = TableState::Body; - } - TagEnd::TableRow => { - self.write("\n")?; - } - TagEnd::TableCell => { - match self.table_state { - TableState::Head => { - self.write("")?; - } - TableState::Body => { - self.write("")?; - } - } - self.table_cell_index += 1; - } - TagEnd::BlockQuote(_) => { - self.write("\n")?; - } - TagEnd::CodeBlock => { - self.write("\n")?; - } - TagEnd::List(true) => { - self.write("\n")?; - } - TagEnd::List(false) => { - self.write("\n")?; - } - TagEnd::Item => { - self.write("\n")?; - } - TagEnd::DefinitionList => { - self.write("\n")?; - } - TagEnd::DefinitionListTitle => { - self.write("\n")?; - } - TagEnd::DefinitionListDefinition => { - self.write("\n")?; - } - TagEnd::Emphasis => { - self.write("")?; - } - TagEnd::Superscript => { - self.write("")?; - } - TagEnd::Subscript => { - self.write("")?; - } - TagEnd::Strong => { - self.write("")?; - } - TagEnd::Strikethrough => { - self.write("")?; - } - TagEnd::Link => { - self.write("
")?; - } - TagEnd::Image => (), // shouldn't happen, handled in start - TagEnd::Embed => (), // shouldn't happen, handled in start - TagEnd::WeaverBlock(_) => { - self.in_non_writing_block = false; - } - TagEnd::FootnoteDefinition => { - self.write("\n")?; - } - TagEnd::MetadataBlock(_) => { - self.in_non_writing_block = false; - } - } - Ok(()) - } -} - -impl<'a, 'input, I: Iterator>, A: AgentSession + IdentityResolver, W: StrWrite> - StaticPageWriter<'a, 'input, I, A, W> -{ - async fn run(mut self) -> Result<(), W::Error> { - while let Some(event) = self.context.next().await { - self.process_event(event).await? - } - Ok(()) - } - - async fn process_event(&mut self, event: Event<'input>) -> Result<(), W::Error> { - use markdown_weaver::Event::*; - match event { - Start(tag) => { - self.start_tag(tag).await?; - } - End(tag) => { - self.end_tag(tag)?; - } - Text(text) => { - if !self.in_non_writing_block { - escape_html_body_text(&mut self.writer, &text)?; - self.end_newline = text.ends_with('\n'); - } - } - Code(text) => { - self.write("")?; - escape_html_body_text(&mut self.writer, &text)?; - self.write("")?; - } - InlineMath(text) => { - self.write(r#""#)?; - escape_html(&mut self.writer, &text)?; - self.write("")?; - } - DisplayMath(text) => { - self.write(r#""#)?; - escape_html(&mut self.writer, &text)?; - self.write("")?; - } - Html(html) | InlineHtml(html) => { - self.write(&html)?; - } - SoftBreak => { - self.write_newline()?; - } - HardBreak => { - self.write("
\n")?; - } - Rule => { - if self.end_newline { - self.write("
\n")?; - } else { - self.write("\n
\n")?; - } - } - FootnoteReference(name) => { - let len = self.numbers.len() + 1; - self.write("")?; - let number = *self.numbers.entry(name.into_static()).or_insert(len); - write!(&mut self.writer, "{}", number)?; - self.write("")?; - } - TaskListMarker(true) => { - self.write("\n")?; - } - TaskListMarker(false) => { - self.write("\n")?; - } - WeaverBlock(_text) => {} - } - Ok(()) - } - - // run raw text, consuming end tag - async fn raw_text(&mut self) -> Result<(), W::Error> { - use markdown_weaver::Event::*; - let mut nest = 0; - while let Some(event) = self.context.next().await { - match event { - Start(_) => nest += 1, - End(_) => { - if nest == 0 { - break; - } - nest -= 1; - } - Html(_) => {} - InlineHtml(text) | Code(text) | Text(text) => { - // Don't use escape_html_body_text here. - // The output of this function is used in the `alt` attribute. - escape_html(&mut self.writer, &text)?; - self.end_newline = text.ends_with('\n'); - } - InlineMath(text) => { - self.write("$")?; - escape_html(&mut self.writer, &text)?; - self.write("$")?; - } - DisplayMath(text) => { - self.write("$$")?; - escape_html(&mut self.writer, &text)?; - self.write("$$")?; - } - SoftBreak | HardBreak | Rule => { - self.write(" ")?; - } - FootnoteReference(name) => { - let len = self.numbers.len() + 1; - let number = *self.numbers.entry(name.into_static()).or_insert(len); - write!(&mut self.writer, "[{}]", number)?; - } - TaskListMarker(true) => self.write("[x]")?, - TaskListMarker(false) => self.write("[ ]")?, - WeaverBlock(_) => { - println!("Weaver block internal"); - } - } - } - Ok(()) - } - - /// Writes the start of an HTML tag. - async fn start_tag(&mut self, tag: Tag<'input>) -> Result<(), W::Error> { - match tag { - Tag::HtmlBlock => Ok(()), - Tag::Paragraph => { - if self.end_newline { - self.write("

") - } else { - self.write("\n

") - } - } - Tag::Heading { - level, - id, - classes, - attrs, - } => { - if self.end_newline { - self.write("<")?; - } else { - self.write("\n<")?; - } - write!(&mut self.writer, "{}", level)?; - if let Some(id) = id { - self.write(" id=\"")?; - escape_html(&mut self.writer, &id)?; - self.write("\"")?; - } - let mut classes = classes.iter(); - if let Some(class) = classes.next() { - self.write(" class=\"")?; - escape_html(&mut self.writer, class)?; - for class in classes { - self.write(" ")?; - escape_html(&mut self.writer, class)?; - } - self.write("\"")?; - } - for (attr, value) in attrs { - self.write(" ")?; - escape_html(&mut self.writer, &attr)?; - if let Some(val) = value { - self.write("=\"")?; - escape_html(&mut self.writer, &val)?; - self.write("\"")?; - } else { - self.write("=\"\"")?; - } - } - self.write(">") - } - Tag::Table(alignments) => { - self.table_alignments = alignments; - self.write("") - } - Tag::TableHead => { - self.table_state = TableState::Head; - self.table_cell_index = 0; - self.write("") - } - Tag::TableRow => { - self.table_cell_index = 0; - self.write("") - } - Tag::TableCell => { - match self.table_state { - TableState::Head => { - self.write(" { - self.write(" self.write(" style=\"text-align: left\">"), - Some(&Alignment::Center) => self.write(" style=\"text-align: center\">"), - Some(&Alignment::Right) => self.write(" style=\"text-align: right\">"), - _ => self.write(">"), - } - } - Tag::BlockQuote(kind) => { - let class_str = match kind { - None => "", - Some(kind) => match kind { - BlockQuoteKind::Note => " class=\"markdown-alert-note\"", - BlockQuoteKind::Tip => " class=\"markdown-alert-tip\"", - BlockQuoteKind::Important => " class=\"markdown-alert-important\"", - BlockQuoteKind::Warning => " class=\"markdown-alert-warning\"", - BlockQuoteKind::Caution => " class=\"markdown-alert-caution\"", - }, - }; - if self.end_newline { - self.write(&format!("\n", class_str)) - } else { - self.write(&format!("\n\n", class_str)) - } - } - Tag::CodeBlock(info) => { - if !self.end_newline { - self.write_newline()?; - } - match info { - CodeBlockKind::Fenced(info) => { - let lang = info.split(' ').next().unwrap(); - if lang.is_empty() { - self.write("
")
-                        } else {
-                            self.write("
")
-                        }
-                    }
-                    CodeBlockKind::Indented => self.write("
"),
-                }
-            }
-            Tag::List(Some(1)) => {
-                if self.end_newline {
-                    self.write("
    \n") - } else { - self.write("\n
      \n") - } - } - Tag::List(Some(start)) => { - if self.end_newline { - self.write("
        \n") - } - Tag::List(None) => { - if self.end_newline { - self.write("