Skip to content

Commit f6df7aa

Browse files
authored
feat: add markdown alert support (#423)
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)
2 parents c8e413c + 388932f commit f6df7aa

14 files changed

+240
-13
lines changed

Diff for: src/markdown/elements.rs

+13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use super::text_style::TextStyle;
2+
use comrak::nodes::AlertType;
23
use std::{fmt, iter, path::PathBuf, str::FromStr};
34
use unicode_width::UnicodeWidthStr;
45

@@ -51,6 +52,18 @@ pub(crate) enum MarkdownElement {
5152

5253
/// A block quote containing a list of lines.
5354
BlockQuote(Vec<Line>),
55+
56+
/// An alert.
57+
Alert {
58+
/// The alert's type.
59+
alert_type: AlertType,
60+
61+
/// The optional title.
62+
title: Option<String>,
63+
64+
/// The content lines in this alert.
65+
lines: Vec<Line>,
66+
},
5467
}
5568

5669
#[derive(Clone, Copy, Debug, Default)]

Diff for: src/markdown/parse.rs

+28-8
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ use comrak::{
88
arena_tree::Node,
99
format_commonmark,
1010
nodes::{
11-
Ast, AstNode, ListDelimType, ListType, NodeCodeBlock, NodeHeading, NodeHtmlBlock, NodeList, NodeValue,
12-
Sourcepos,
11+
Ast, AstNode, ListDelimType, ListType, NodeAlert, NodeCodeBlock, NodeHeading, NodeHtmlBlock, NodeList,
12+
NodeValue, Sourcepos,
1313
},
1414
parse_document,
1515
};
@@ -31,6 +31,7 @@ impl Default for ParserOptions {
3131
options.extension.table = true;
3232
options.extension.strikethrough = true;
3333
options.extension.multiline_block_quotes = true;
34+
options.extension.alerts = true;
3435
Self(options)
3536
}
3637
}
@@ -76,6 +77,7 @@ impl<'a> MarkdownParser<'a> {
7677
NodeValue::ThematicBreak => MarkdownElement::ThematicBreak,
7778
NodeValue::HtmlBlock(block) => self.parse_html_block(block, data.sourcepos)?,
7879
NodeValue::BlockQuote | NodeValue::MultilineBlockQuote(_) => self.parse_block_quote(node)?,
80+
NodeValue::Alert(alert) => self.parse_alert(alert, node)?,
7981
other => return Err(ParseErrorKind::UnsupportedElement(other.identifier()).with_sourcepos(data.sourcepos)),
8082
};
8183
Ok(vec![element])
@@ -106,19 +108,19 @@ impl<'a> MarkdownParser<'a> {
106108
}
107109

108110
fn parse_block_quote(&self, node: &'a AstNode<'a>) -> ParseResult<MarkdownElement> {
109-
let mut elements = Vec::new();
111+
let mut lines = Vec::new();
110112
let inlines = InlinesParser::new(self.arena, SoftBreak::Newline, StringifyImages::Yes).parse(node)?;
111113
for inline in inlines {
112114
match inline {
113-
Inline::Text(text) => elements.push(text),
114-
Inline::LineBreak => elements.push(Line::from("")),
115+
Inline::Text(text) => lines.push(text),
116+
Inline::LineBreak => lines.push(Line::from("")),
115117
Inline::Image { .. } => {}
116118
}
117119
}
118-
if elements.last() == Some(&Line::from("")) {
119-
elements.pop();
120+
if lines.last() == Some(&Line::from("")) {
121+
lines.pop();
120122
}
121-
Ok(MarkdownElement::BlockQuote(elements))
123+
Ok(MarkdownElement::BlockQuote(lines))
122124
}
123125

124126
fn parse_code_block(block: &NodeCodeBlock, sourcepos: Sourcepos) -> ParseResult<MarkdownElement> {
@@ -132,6 +134,11 @@ impl<'a> MarkdownParser<'a> {
132134
})
133135
}
134136

137+
fn parse_alert(&self, alert: &NodeAlert, node: &'a AstNode<'a>) -> ParseResult<MarkdownElement> {
138+
let MarkdownElement::BlockQuote(lines) = self.parse_block_quote(node)? else { panic!("not a block quote") };
139+
Ok(MarkdownElement::Alert { alert_type: alert.alert_type, title: alert.title.clone(), lines })
140+
}
141+
135142
fn parse_heading(&self, heading: &NodeHeading, node: &'a AstNode<'a>) -> ParseResult<MarkdownElement> {
136143
let text = self.parse_text(node)?;
137144
if heading.setext {
@@ -973,4 +980,17 @@ mom
973980
let expected = format!("hi{nl}mom{nl}");
974981
assert_eq!(contents, &expected);
975982
}
983+
984+
#[test]
985+
fn parse_alert() {
986+
let input = r"
987+
> [!note]
988+
> hi mom
989+
> bye **mom**
990+
";
991+
let MarkdownElement::Alert { lines, .. } = parse_single(&input) else {
992+
panic!("not an alert");
993+
};
994+
assert_eq!(lines.len(), 2);
995+
}
976996
}

Diff for: src/presentation/builder.rs

+31-5
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ use crate::{
4343
separator::RenderSeparator,
4444
},
4545
};
46+
use comrak::nodes::AlertType;
4647
use image::DynamicImage;
4748
use serde::Deserialize;
4849
use std::{borrow::Cow, cell::RefCell, fmt::Display, iter, mem, path::PathBuf, rc::Rc, str::FromStr};
@@ -287,6 +288,7 @@ impl<'a> PresentationBuilder<'a> {
287288
MarkdownElement::Image { path, title, source_position } => {
288289
self.push_image_from_path(path, title, source_position)?
289290
}
291+
MarkdownElement::Alert { alert_type, title, lines } => self.push_alert(alert_type, title, lines),
290292
};
291293
if should_clear_last {
292294
self.slide_state.last_element = LastElement::Other;
@@ -716,8 +718,30 @@ impl<'a> PresentationBuilder<'a> {
716718

717719
fn push_block_quote(&mut self, lines: Vec<Line>) {
718720
let prefix = self.theme.block_quote.prefix.clone().unwrap_or_default();
719-
let block_length = lines.iter().map(|line| line.width() + prefix.width()).max().unwrap_or(0) as u16;
720721
let prefix_color = self.theme.block_quote.colors.prefix.or(self.theme.block_quote.colors.base.foreground);
722+
self.push_quoted_text(lines, prefix, self.theme.block_quote.colors.base, prefix_color);
723+
}
724+
725+
fn push_alert(&mut self, alert_type: AlertType, title: Option<String>, mut lines: Vec<Line>) {
726+
let (default_title, prefix_color) = match alert_type {
727+
AlertType::Note => ("Note", self.theme.alert.colors.types.note),
728+
AlertType::Tip => ("Tip", self.theme.alert.colors.types.tip),
729+
AlertType::Important => ("Important", self.theme.alert.colors.types.important),
730+
AlertType::Warning => ("Warning", self.theme.alert.colors.types.warning),
731+
AlertType::Caution => ("Caution", self.theme.alert.colors.types.caution),
732+
};
733+
let prefix_color = prefix_color.or(self.theme.alert.colors.base.foreground);
734+
let title = title.unwrap_or_else(|| default_title.to_string());
735+
let title_colors = Colors { foreground: prefix_color, background: self.theme.alert.colors.base.background };
736+
lines.insert(0, Line::from(Text::from("")));
737+
lines.insert(0, Line::from(Text::new(title, TextStyle::default().colors(title_colors))));
738+
739+
let prefix = self.theme.block_quote.prefix.clone().unwrap_or_default();
740+
self.push_quoted_text(lines, prefix, self.theme.alert.colors.base, prefix_color);
741+
}
742+
743+
fn push_quoted_text(&mut self, lines: Vec<Line>, prefix: String, base_colors: Colors, prefix_color: Option<Color>) {
744+
let block_length = lines.iter().map(|line| line.width() + prefix.width()).max().unwrap_or(0) as u16;
721745
let prefix = Text::new(
722746
prefix,
723747
TextStyle::default()
@@ -728,9 +752,11 @@ impl<'a> PresentationBuilder<'a> {
728752
for mut line in lines {
729753
// Apply our colors to each chunk in this line.
730754
for text in &mut line.0 {
731-
text.style.colors = self.theme.block_quote.colors.base;
732-
if text.style.is_code() {
733-
text.style.colors = self.theme.inline_code.colors;
755+
if text.style.colors.background.is_none() && text.style.colors.foreground.is_none() {
756+
text.style.colors = base_colors;
757+
if text.style.is_code() {
758+
text.style.colors = self.theme.inline_code.colors;
759+
}
734760
}
735761
}
736762
self.chunk_operations.push(RenderOperation::RenderBlockLine(BlockLine {
@@ -740,7 +766,7 @@ impl<'a> PresentationBuilder<'a> {
740766
text: line.into(),
741767
block_length,
742768
alignment: alignment.clone(),
743-
block_color: self.theme.block_quote.colors.base.background,
769+
block_color: base_colors.background,
744770
}));
745771
self.push_line_break();
746772
}

Diff for: src/theme.rs

+59
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,10 @@ pub struct PresentationTheme {
150150
#[serde(default)]
151151
pub(crate) block_quote: BlockQuoteStyle,
152152

153+
/// The style for an alert.
154+
#[serde(default)]
155+
pub(crate) alert: AlertStyle,
156+
153157
/// The default style.
154158
#[serde(rename = "default", default)]
155159
pub(crate) default_style: DefaultStyle,
@@ -325,9 +329,64 @@ pub(crate) struct BlockQuoteColors {
325329
pub(crate) base: Colors,
326330

327331
/// The color of the vertical bar that prefixes each line in the quote.
332+
#[serde(default)]
328333
pub(crate) prefix: Option<Color>,
329334
}
330335

336+
/// The style of an alert.
337+
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
338+
pub(crate) struct AlertStyle {
339+
/// The alignment.
340+
#[serde(flatten, default)]
341+
pub(crate) alignment: Option<Alignment>,
342+
343+
/// The prefix to be added to this block quote.
344+
///
345+
/// This allows adding something like a vertical bar before the text.
346+
#[serde(default)]
347+
pub(crate) prefix: Option<String>,
348+
349+
/// The colors to be used.
350+
#[serde(default)]
351+
pub(crate) colors: AlertColors,
352+
}
353+
354+
/// The colors of an alert.
355+
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
356+
pub(crate) struct AlertColors {
357+
/// The foreground/background colors.
358+
#[serde(flatten)]
359+
pub(crate) base: Colors,
360+
361+
/// The color of the vertical bar that prefixes each line in the quote.
362+
#[serde(default)]
363+
pub(crate) types: AlertTypeColors,
364+
}
365+
366+
/// The colors of each alert type.
367+
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
368+
pub(crate) struct AlertTypeColors {
369+
/// The color for note type alerts.
370+
#[serde(default)]
371+
pub(crate) note: Option<Color>,
372+
373+
/// The color for tip type alerts.
374+
#[serde(default)]
375+
pub(crate) tip: Option<Color>,
376+
377+
/// The color for important type alerts.
378+
#[serde(default)]
379+
pub(crate) important: Option<Color>,
380+
381+
/// The color for warning type alerts.
382+
#[serde(default)]
383+
pub(crate) warning: Option<Color>,
384+
385+
/// The color for caution type alerts.
386+
#[serde(default)]
387+
pub(crate) caution: Option<Color>,
388+
}
389+
331390
/// The style for the presentation introduction slide.
332391
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
333392
pub(crate) struct IntroSlideStyle {

Diff for: src/ui/footer.rs

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ impl FooterGenerator {
3838
colors: Colors,
3939
alignment: Alignment,
4040
) -> RenderOperation {
41+
#[allow(unknown_lints)]
4142
#[allow(clippy::literal_string_with_formatting_args)]
4243
let contents = template
4344
.replace("{current_slide}", current_slide)

Diff for: themes/catppuccin-frappe.yaml

+12
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,18 @@ block_quote:
103103
background: "414559"
104104
prefix: "e5c890"
105105

106+
alert:
107+
prefix: ""
108+
colors:
109+
foreground: "c6d0f5"
110+
background: "414559"
111+
types:
112+
note: "8caaee"
113+
tip: "a6d189"
114+
important: "ca9ee6"
115+
warning: "e5c890"
116+
caution: "e78284"
117+
106118
typst:
107119
colors:
108120
foreground: "c6d0f5"

Diff for: themes/catppuccin-latte.yaml

+12
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,18 @@ block_quote:
103103
background: "ccd0da"
104104
prefix: "df8e1d"
105105

106+
alert:
107+
prefix: ""
108+
colors:
109+
foreground: "4c4f69"
110+
background: "ccd0da"
111+
types:
112+
note: "1e66f5"
113+
tip: "40a02b"
114+
important: "8839ef"
115+
warning: "df8e1d"
116+
caution: "d20f39"
117+
106118
typst:
107119
colors:
108120
foreground: "4c4f69"

Diff for: themes/catppuccin-macchiato.yaml

+12
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,18 @@ block_quote:
103103
background: "363a4f"
104104
prefix: "eed49f"
105105

106+
alert:
107+
prefix: ""
108+
colors:
109+
foreground: "cad3f5"
110+
background: "363a4f"
111+
types:
112+
note: "8aadf4"
113+
tip: "a6da95"
114+
important: "c6a0f6"
115+
warning: "f5a97f"
116+
caution: "ed8796"
117+
106118
typst:
107119
colors:
108120
foreground: "cad3f5"

Diff for: themes/catppuccin-mocha.yaml

+12
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,18 @@ block_quote:
103103
background: "313244"
104104
prefix: "f9e2af"
105105

106+
alert:
107+
prefix: ""
108+
colors:
109+
foreground: "cdd6f4"
110+
background: "313244"
111+
types:
112+
note: "89b4fa"
113+
tip: "a6e3a1"
114+
important: "cba6f7"
115+
warning: "fab387"
116+
caution: "f38ba8"
117+
106118
typst:
107119
colors:
108120
foreground: "cdd6f4"

Diff for: themes/dark.yaml

+12
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,18 @@ block_quote:
104104
background: "292e42"
105105
prefix: "ee9322"
106106

107+
alert:
108+
prefix: ""
109+
colors:
110+
foreground: "f0f0f0"
111+
background: "292e42"
112+
types:
113+
note: "3085c3"
114+
tip: "a8df8e"
115+
important: "986ee2"
116+
warning: "ee9322"
117+
caution: "f78ca2"
118+
107119
typst:
108120
colors:
109121
foreground: "f0f0f0"

Diff for: themes/light.yaml

+12
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,18 @@ block_quote:
104104
background: "e9ecef"
105105
prefix: "f77f00"
106106

107+
alert:
108+
prefix: ""
109+
colors:
110+
foreground: "212529"
111+
background: "e9ecef"
112+
types:
113+
note: "1e66f5"
114+
tip: "40a02b"
115+
important: "8839ef"
116+
warning: "df8e1d"
117+
caution: "d20f39"
118+
107119
typst:
108120
colors:
109121
foreground: "212529"

Diff for: themes/terminal-dark.yaml

+12
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,18 @@ block_quote:
102102
background: black
103103
prefix: yellow
104104

105+
alert:
106+
prefix: ""
107+
colors:
108+
foreground: white
109+
background: black
110+
types:
111+
note: blue
112+
tip: green
113+
important: magenta
114+
warning: yellow
115+
caution: red
116+
105117
typst:
106118
colors:
107119
foreground: "f0f0f0"

0 commit comments

Comments
 (0)