From 388932f2b48ba8c97b598ce0196d1eddb2bd242e Mon Sep 17 00:00:00 2001 From: Matias Fontanini Date: Wed, 22 Jan 2025 17:55:50 -0800 Subject: [PATCH] feat: add markdown alert support --- src/markdown/elements.rs | 13 +++++++ src/markdown/parse.rs | 36 ++++++++++++++----- src/presentation/builder.rs | 36 ++++++++++++++++--- src/theme.rs | 59 ++++++++++++++++++++++++++++++++ src/ui/footer.rs | 1 + themes/catppuccin-frappe.yaml | 12 +++++++ themes/catppuccin-latte.yaml | 12 +++++++ themes/catppuccin-macchiato.yaml | 12 +++++++ themes/catppuccin-mocha.yaml | 12 +++++++ themes/dark.yaml | 12 +++++++ themes/light.yaml | 12 +++++++ themes/terminal-dark.yaml | 12 +++++++ themes/terminal-light.yaml | 12 +++++++ themes/tokyonight-storm.yaml | 12 +++++++ 14 files changed, 240 insertions(+), 13 deletions(-) diff --git a/src/markdown/elements.rs b/src/markdown/elements.rs index d0b0cb9a..c5908cfb 100644 --- a/src/markdown/elements.rs +++ b/src/markdown/elements.rs @@ -1,4 +1,5 @@ use super::text_style::TextStyle; +use comrak::nodes::AlertType; use std::{fmt, iter, path::PathBuf, str::FromStr}; use unicode_width::UnicodeWidthStr; @@ -51,6 +52,18 @@ pub(crate) enum MarkdownElement { /// A block quote containing a list of lines. BlockQuote(Vec), + + /// An alert. + Alert { + /// The alert's type. + alert_type: AlertType, + + /// The optional title. + title: Option, + + /// The content lines in this alert. + lines: Vec, + }, } #[derive(Clone, Copy, Debug, Default)] diff --git a/src/markdown/parse.rs b/src/markdown/parse.rs index 0573853c..14b9d5fc 100644 --- a/src/markdown/parse.rs +++ b/src/markdown/parse.rs @@ -8,8 +8,8 @@ use comrak::{ arena_tree::Node, format_commonmark, nodes::{ - Ast, AstNode, ListDelimType, ListType, NodeCodeBlock, NodeHeading, NodeHtmlBlock, NodeList, NodeValue, - Sourcepos, + Ast, AstNode, ListDelimType, ListType, NodeAlert, NodeCodeBlock, NodeHeading, NodeHtmlBlock, NodeList, + NodeValue, Sourcepos, }, parse_document, }; @@ -31,6 +31,7 @@ impl Default for ParserOptions { options.extension.table = true; options.extension.strikethrough = true; options.extension.multiline_block_quotes = true; + options.extension.alerts = true; Self(options) } } @@ -76,6 +77,7 @@ impl<'a> MarkdownParser<'a> { NodeValue::ThematicBreak => MarkdownElement::ThematicBreak, NodeValue::HtmlBlock(block) => self.parse_html_block(block, data.sourcepos)?, NodeValue::BlockQuote | NodeValue::MultilineBlockQuote(_) => self.parse_block_quote(node)?, + NodeValue::Alert(alert) => self.parse_alert(alert, node)?, other => return Err(ParseErrorKind::UnsupportedElement(other.identifier()).with_sourcepos(data.sourcepos)), }; Ok(vec![element]) @@ -106,19 +108,19 @@ impl<'a> MarkdownParser<'a> { } fn parse_block_quote(&self, node: &'a AstNode<'a>) -> ParseResult { - let mut elements = Vec::new(); + let mut lines = Vec::new(); let inlines = InlinesParser::new(self.arena, SoftBreak::Newline, StringifyImages::Yes).parse(node)?; for inline in inlines { match inline { - Inline::Text(text) => elements.push(text), - Inline::LineBreak => elements.push(Line::from("")), + Inline::Text(text) => lines.push(text), + Inline::LineBreak => lines.push(Line::from("")), Inline::Image { .. } => {} } } - if elements.last() == Some(&Line::from("")) { - elements.pop(); + if lines.last() == Some(&Line::from("")) { + lines.pop(); } - Ok(MarkdownElement::BlockQuote(elements)) + Ok(MarkdownElement::BlockQuote(lines)) } fn parse_code_block(block: &NodeCodeBlock, sourcepos: Sourcepos) -> ParseResult { @@ -132,6 +134,11 @@ impl<'a> MarkdownParser<'a> { }) } + fn parse_alert(&self, alert: &NodeAlert, node: &'a AstNode<'a>) -> ParseResult { + let MarkdownElement::BlockQuote(lines) = self.parse_block_quote(node)? else { panic!("not a block quote") }; + Ok(MarkdownElement::Alert { alert_type: alert.alert_type, title: alert.title.clone(), lines }) + } + fn parse_heading(&self, heading: &NodeHeading, node: &'a AstNode<'a>) -> ParseResult { let text = self.parse_text(node)?; if heading.setext { @@ -973,4 +980,17 @@ mom let expected = format!("hi{nl}mom{nl}"); assert_eq!(contents, &expected); } + + #[test] + fn parse_alert() { + let input = r" +> [!note] +> hi mom +> bye **mom** +"; + let MarkdownElement::Alert { lines, .. } = parse_single(&input) else { + panic!("not an alert"); + }; + assert_eq!(lines.len(), 2); + } } diff --git a/src/presentation/builder.rs b/src/presentation/builder.rs index f0dc5952..a6db9aa3 100644 --- a/src/presentation/builder.rs +++ b/src/presentation/builder.rs @@ -43,6 +43,7 @@ use crate::{ separator::RenderSeparator, }, }; +use comrak::nodes::AlertType; use image::DynamicImage; use serde::Deserialize; use std::{borrow::Cow, cell::RefCell, fmt::Display, iter, mem, path::PathBuf, rc::Rc, str::FromStr}; @@ -287,6 +288,7 @@ impl<'a> PresentationBuilder<'a> { MarkdownElement::Image { path, title, source_position } => { self.push_image_from_path(path, title, source_position)? } + MarkdownElement::Alert { alert_type, title, lines } => self.push_alert(alert_type, title, lines), }; if should_clear_last { self.slide_state.last_element = LastElement::Other; @@ -716,8 +718,30 @@ impl<'a> PresentationBuilder<'a> { fn push_block_quote(&mut self, lines: Vec) { let prefix = self.theme.block_quote.prefix.clone().unwrap_or_default(); - let block_length = lines.iter().map(|line| line.width() + prefix.width()).max().unwrap_or(0) as u16; let prefix_color = self.theme.block_quote.colors.prefix.or(self.theme.block_quote.colors.base.foreground); + self.push_quoted_text(lines, prefix, self.theme.block_quote.colors.base, prefix_color); + } + + fn push_alert(&mut self, alert_type: AlertType, title: Option, mut lines: Vec) { + let (default_title, prefix_color) = match alert_type { + AlertType::Note => ("Note", self.theme.alert.colors.types.note), + AlertType::Tip => ("Tip", self.theme.alert.colors.types.tip), + AlertType::Important => ("Important", self.theme.alert.colors.types.important), + AlertType::Warning => ("Warning", self.theme.alert.colors.types.warning), + AlertType::Caution => ("Caution", self.theme.alert.colors.types.caution), + }; + let prefix_color = prefix_color.or(self.theme.alert.colors.base.foreground); + let title = title.unwrap_or_else(|| default_title.to_string()); + let title_colors = Colors { foreground: prefix_color, background: self.theme.alert.colors.base.background }; + lines.insert(0, Line::from(Text::from(""))); + lines.insert(0, Line::from(Text::new(title, TextStyle::default().colors(title_colors)))); + + let prefix = self.theme.block_quote.prefix.clone().unwrap_or_default(); + self.push_quoted_text(lines, prefix, self.theme.alert.colors.base, prefix_color); + } + + fn push_quoted_text(&mut self, lines: Vec, prefix: String, base_colors: Colors, prefix_color: Option) { + let block_length = lines.iter().map(|line| line.width() + prefix.width()).max().unwrap_or(0) as u16; let prefix = Text::new( prefix, TextStyle::default() @@ -728,9 +752,11 @@ impl<'a> PresentationBuilder<'a> { for mut line in lines { // Apply our colors to each chunk in this line. for text in &mut line.0 { - text.style.colors = self.theme.block_quote.colors.base; - if text.style.is_code() { - text.style.colors = self.theme.inline_code.colors; + if text.style.colors.background.is_none() && text.style.colors.foreground.is_none() { + text.style.colors = base_colors; + if text.style.is_code() { + text.style.colors = self.theme.inline_code.colors; + } } } self.chunk_operations.push(RenderOperation::RenderBlockLine(BlockLine { @@ -740,7 +766,7 @@ impl<'a> PresentationBuilder<'a> { text: line.into(), block_length, alignment: alignment.clone(), - block_color: self.theme.block_quote.colors.base.background, + block_color: base_colors.background, })); self.push_line_break(); } diff --git a/src/theme.rs b/src/theme.rs index 3bf4127a..eb53252e 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -150,6 +150,10 @@ pub struct PresentationTheme { #[serde(default)] pub(crate) block_quote: BlockQuoteStyle, + /// The style for an alert. + #[serde(default)] + pub(crate) alert: AlertStyle, + /// The default style. #[serde(rename = "default", default)] pub(crate) default_style: DefaultStyle, @@ -325,9 +329,64 @@ pub(crate) struct BlockQuoteColors { pub(crate) base: Colors, /// The color of the vertical bar that prefixes each line in the quote. + #[serde(default)] pub(crate) prefix: Option, } +/// The style of an alert. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub(crate) struct AlertStyle { + /// The alignment. + #[serde(flatten, default)] + pub(crate) alignment: Option, + + /// The prefix to be added to this block quote. + /// + /// This allows adding something like a vertical bar before the text. + #[serde(default)] + pub(crate) prefix: Option, + + /// The colors to be used. + #[serde(default)] + pub(crate) colors: AlertColors, +} + +/// The colors of an alert. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub(crate) struct AlertColors { + /// The foreground/background colors. + #[serde(flatten)] + pub(crate) base: Colors, + + /// The color of the vertical bar that prefixes each line in the quote. + #[serde(default)] + pub(crate) types: AlertTypeColors, +} + +/// The colors of each alert type. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub(crate) struct AlertTypeColors { + /// The color for note type alerts. + #[serde(default)] + pub(crate) note: Option, + + /// The color for tip type alerts. + #[serde(default)] + pub(crate) tip: Option, + + /// The color for important type alerts. + #[serde(default)] + pub(crate) important: Option, + + /// The color for warning type alerts. + #[serde(default)] + pub(crate) warning: Option, + + /// The color for caution type alerts. + #[serde(default)] + pub(crate) caution: Option, +} + /// The style for the presentation introduction slide. #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub(crate) struct IntroSlideStyle { diff --git a/src/ui/footer.rs b/src/ui/footer.rs index 9b8cab93..8214332f 100644 --- a/src/ui/footer.rs +++ b/src/ui/footer.rs @@ -38,6 +38,7 @@ impl FooterGenerator { colors: Colors, alignment: Alignment, ) -> RenderOperation { + #[allow(unknown_lints)] #[allow(clippy::literal_string_with_formatting_args)] let contents = template .replace("{current_slide}", current_slide) diff --git a/themes/catppuccin-frappe.yaml b/themes/catppuccin-frappe.yaml index 0f8557f5..bd070e8a 100644 --- a/themes/catppuccin-frappe.yaml +++ b/themes/catppuccin-frappe.yaml @@ -103,6 +103,18 @@ block_quote: background: "414559" prefix: "e5c890" +alert: + prefix: "▍ " + colors: + foreground: "c6d0f5" + background: "414559" + types: + note: "8caaee" + tip: "a6d189" + important: "ca9ee6" + warning: "e5c890" + caution: "e78284" + typst: colors: foreground: "c6d0f5" diff --git a/themes/catppuccin-latte.yaml b/themes/catppuccin-latte.yaml index 6f686e5a..65f2c87d 100644 --- a/themes/catppuccin-latte.yaml +++ b/themes/catppuccin-latte.yaml @@ -103,6 +103,18 @@ block_quote: background: "ccd0da" prefix: "df8e1d" +alert: + prefix: "▍ " + colors: + foreground: "4c4f69" + background: "ccd0da" + types: + note: "1e66f5" + tip: "40a02b" + important: "8839ef" + warning: "df8e1d" + caution: "d20f39" + typst: colors: foreground: "4c4f69" diff --git a/themes/catppuccin-macchiato.yaml b/themes/catppuccin-macchiato.yaml index 52f3a4ba..0530fb96 100644 --- a/themes/catppuccin-macchiato.yaml +++ b/themes/catppuccin-macchiato.yaml @@ -103,6 +103,18 @@ block_quote: background: "363a4f" prefix: "eed49f" +alert: + prefix: "▍ " + colors: + foreground: "cad3f5" + background: "363a4f" + types: + note: "8aadf4" + tip: "a6da95" + important: "c6a0f6" + warning: "f5a97f" + caution: "ed8796" + typst: colors: foreground: "cad3f5" diff --git a/themes/catppuccin-mocha.yaml b/themes/catppuccin-mocha.yaml index 5bc96b51..241ca786 100644 --- a/themes/catppuccin-mocha.yaml +++ b/themes/catppuccin-mocha.yaml @@ -103,6 +103,18 @@ block_quote: background: "313244" prefix: "f9e2af" +alert: + prefix: "▍ " + colors: + foreground: "cdd6f4" + background: "313244" + types: + note: "89b4fa" + tip: "a6e3a1" + important: "cba6f7" + warning: "fab387" + caution: "f38ba8" + typst: colors: foreground: "cdd6f4" diff --git a/themes/dark.yaml b/themes/dark.yaml index 90d42c8f..7eba388c 100644 --- a/themes/dark.yaml +++ b/themes/dark.yaml @@ -104,6 +104,18 @@ block_quote: background: "292e42" prefix: "ee9322" +alert: + prefix: "▍ " + colors: + foreground: "f0f0f0" + background: "292e42" + types: + note: "3085c3" + tip: "a8df8e" + important: "986ee2" + warning: "ee9322" + caution: "f78ca2" + typst: colors: foreground: "f0f0f0" diff --git a/themes/light.yaml b/themes/light.yaml index aa7f7553..7b5f8a73 100644 --- a/themes/light.yaml +++ b/themes/light.yaml @@ -104,6 +104,18 @@ block_quote: background: "e9ecef" prefix: "f77f00" +alert: + prefix: "▍ " + colors: + foreground: "212529" + background: "e9ecef" + types: + note: "1e66f5" + tip: "40a02b" + important: "8839ef" + warning: "df8e1d" + caution: "d20f39" + typst: colors: foreground: "212529" diff --git a/themes/terminal-dark.yaml b/themes/terminal-dark.yaml index 4a246285..359c8e8b 100644 --- a/themes/terminal-dark.yaml +++ b/themes/terminal-dark.yaml @@ -102,6 +102,18 @@ block_quote: background: black prefix: yellow +alert: + prefix: "▍ " + colors: + foreground: white + background: black + types: + note: blue + tip: green + important: magenta + warning: yellow + caution: red + typst: colors: foreground: "f0f0f0" diff --git a/themes/terminal-light.yaml b/themes/terminal-light.yaml index 27612bd6..26af3acc 100644 --- a/themes/terminal-light.yaml +++ b/themes/terminal-light.yaml @@ -102,6 +102,18 @@ block_quote: background: grey prefix: dark_red +alert: + prefix: "▍ " + colors: + foreground: black + background: grey + types: + note: dark_blue + tip: dark_green + important: dark_magenta + warning: dark_yellow + caution: dark_red + typst: colors: foreground: "212529" diff --git a/themes/tokyonight-storm.yaml b/themes/tokyonight-storm.yaml index 58562945..a13248ee 100644 --- a/themes/tokyonight-storm.yaml +++ b/themes/tokyonight-storm.yaml @@ -104,6 +104,18 @@ block_quote: background: "545c7e" prefix: "e0af68" +alert: + prefix: "▍ " + colors: + foreground: "f0f0f0" + background: "545c7e" + types: + note: "7aa2f7" + tip: "9ece6a" + important: "bb9af7" + warning: "e0af68" + caution: "f7768e" + typst: colors: foreground: "f0f0f0"