Skip to content

Commit

Permalink
feat: add markdown alert support (#423)
Browse files Browse the repository at this point in the history
This adds supports for markdown alerts (github/gitlab style) as support
for these was recently added to comrak. This for now looks close a block
quote except it also contains a title colored the same color as the
vertical bar prefix that shows up on the left of the block quote.

See kivikakk/comrak#519 and
kivikakk/comrak#521 for syntax but basically
this:

```markdown
> [!note]
> this is a note

> [!tip]
> this is a tip

> [!important]
> this is important

> [!warning]
> this is warning!

> [!caution]
> this advises caution!

>>> [!note] other title
ez
multiline
>>>
```

Renders like this:


![image](https://github.com/user-attachments/assets/219024ae-b635-4bf2-87ba-e252b64ebd67)
  • Loading branch information
mfontanini authored Jan 23, 2025
2 parents c8e413c + 388932f commit f6df7aa
Show file tree
Hide file tree
Showing 14 changed files with 240 additions and 13 deletions.
13 changes: 13 additions & 0 deletions src/markdown/elements.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -51,6 +52,18 @@ pub(crate) enum MarkdownElement {

/// A block quote containing a list of lines.
BlockQuote(Vec<Line>),

/// An alert.
Alert {
/// The alert's type.
alert_type: AlertType,

/// The optional title.
title: Option<String>,

/// The content lines in this alert.
lines: Vec<Line>,
},
}

#[derive(Clone, Copy, Debug, Default)]
Expand Down
36 changes: 28 additions & 8 deletions src/markdown/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand All @@ -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)
}
}
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -106,19 +108,19 @@ impl<'a> MarkdownParser<'a> {
}

fn parse_block_quote(&self, node: &'a AstNode<'a>) -> ParseResult<MarkdownElement> {
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<MarkdownElement> {
Expand All @@ -132,6 +134,11 @@ impl<'a> MarkdownParser<'a> {
})
}

fn parse_alert(&self, alert: &NodeAlert, node: &'a AstNode<'a>) -> ParseResult<MarkdownElement> {
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<MarkdownElement> {
let text = self.parse_text(node)?;
if heading.setext {
Expand Down Expand Up @@ -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);
}
}
36 changes: 31 additions & 5 deletions src/presentation/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -716,8 +718,30 @@ impl<'a> PresentationBuilder<'a> {

fn push_block_quote(&mut self, lines: Vec<Line>) {
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<String>, mut lines: Vec<Line>) {
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<Line>, prefix: String, base_colors: Colors, prefix_color: Option<Color>) {
let block_length = lines.iter().map(|line| line.width() + prefix.width()).max().unwrap_or(0) as u16;
let prefix = Text::new(
prefix,
TextStyle::default()
Expand All @@ -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 {
Expand All @@ -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();
}
Expand Down
59 changes: 59 additions & 0 deletions src/theme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<Color>,
}

/// The style of an alert.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub(crate) struct AlertStyle {
/// The alignment.
#[serde(flatten, default)]
pub(crate) alignment: Option<Alignment>,

/// 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<String>,

/// 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<Color>,

/// The color for tip type alerts.
#[serde(default)]
pub(crate) tip: Option<Color>,

/// The color for important type alerts.
#[serde(default)]
pub(crate) important: Option<Color>,

/// The color for warning type alerts.
#[serde(default)]
pub(crate) warning: Option<Color>,

/// The color for caution type alerts.
#[serde(default)]
pub(crate) caution: Option<Color>,
}

/// The style for the presentation introduction slide.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub(crate) struct IntroSlideStyle {
Expand Down
1 change: 1 addition & 0 deletions src/ui/footer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions themes/catppuccin-frappe.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
12 changes: 12 additions & 0 deletions themes/catppuccin-latte.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
12 changes: 12 additions & 0 deletions themes/catppuccin-macchiato.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
12 changes: 12 additions & 0 deletions themes/catppuccin-mocha.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
12 changes: 12 additions & 0 deletions themes/dark.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
12 changes: 12 additions & 0 deletions themes/light.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
12 changes: 12 additions & 0 deletions themes/terminal-dark.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading

0 comments on commit f6df7aa

Please sign in to comment.