Skip to content

Commit 428ad9a

Browse files
Merge pull request #519 from kivikakk/bw-admonmition-blocks
Add GitHub style alerts / admonitions
2 parents 45c96a2 + 6aa5a73 commit 428ad9a

18 files changed

+1231
-8
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ Options:
117117
[possible values: strikethrough, tagfilter, table, autolink, tasklist, superscript,
118118
footnotes, description-lists, multiline-block-quotes, math-dollars, math-code,
119119
wikilinks-title-after-pipe, wikilinks-title-before-pipe, underline, subscript, spoiler,
120-
greentext]
120+
greentext, alerts]
121121
122122
-t, --to <FORMAT>
123123
Specify output format

fuzz/fuzz_targets/all_options.rs

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ fuzz_target!(|s: &str| {
2929
extension.underline = true;
3030
extension.spoiler = true;
3131
extension.greentext = true;
32+
extension.alerts = true;
3233

3334
let mut parse = ParseOptions::default();
3435
parse.smart = true;

fuzz/fuzz_targets/quadratic.rs

+8
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,10 @@ struct FuzzExtensionOptions {
197197
shortcodes: bool,
198198
wikilinks_title_after_pipe: bool,
199199
wikilinks_title_before_pipe: bool,
200+
underline: bool,
201+
spoiler: bool,
202+
greentext: bool,
203+
alerts: bool,
200204
}
201205

202206
impl FuzzExtensionOptions {
@@ -216,6 +220,10 @@ impl FuzzExtensionOptions {
216220
extension.shortcodes = self.shortcodes;
217221
extension.wikilinks_title_after_pipe = self.wikilinks_title_after_pipe;
218222
extension.wikilinks_title_before_pipe = self.wikilinks_title_before_pipe;
223+
extension.underline = self.underline;
224+
extension.spoiler = self.spoiler;
225+
extension.greentext = self.greentext;
226+
extension.alerts = self.alerts;
219227
extension.front_matter_delimiter = None;
220228
extension.header_ids = None;
221229
extension

script/cibuild

+2
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ python3 spec_tests.py --no-normalize --spec ../../../src/tests/fixtures/wikilink
5050
|| failed=1
5151
python3 spec_tests.py --no-normalize --spec ../../../src/tests/fixtures/description_lists.md "$PROGRAM_ARG -e description-lists" \
5252
|| failed=1
53+
python3 spec_tests.py --no-normalize --spec ../../../src/tests/fixtures/alerts.md "$PROGRAM_ARG -e alerts" \
54+
|| failed=1
5355

5456
python3 spec_tests.py --no-normalize --spec regression.txt "$PROGRAM_ARG" \
5557
|| failed=1

src/cm.rs

+26-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::ctype::{isalpha, isdigit, ispunct, isspace};
22
use crate::nodes::{
3-
AstNode, ListDelimType, ListType, NodeCodeBlock, NodeHeading, NodeHtmlBlock, NodeLink,
4-
NodeMath, NodeTable, NodeValue, NodeWikiLink,
3+
AstNode, ListDelimType, ListType, NodeAlert, NodeCodeBlock, NodeHeading, NodeHtmlBlock,
4+
NodeLink, NodeMath, NodeTable, NodeValue, NodeWikiLink,
55
};
66
use crate::nodes::{NodeList, TableAlignment};
77
#[cfg(feature = "shortcodes")]
@@ -401,6 +401,7 @@ impl<'a, 'o, 'c> CommonMarkFormatter<'a, 'o, 'c> {
401401
NodeValue::Subscript => self.format_subscript(),
402402
NodeValue::SpoileredText => self.format_spoiler(),
403403
NodeValue::EscapedTag(ref net) => self.format_escaped_tag(net),
404+
NodeValue::Alert(ref alert) => self.format_alert(alert, entering),
404405
};
405406
true
406407
}
@@ -904,6 +905,29 @@ impl<'a, 'o, 'c> CommonMarkFormatter<'a, 'o, 'c> {
904905
self.output(end_fence.as_bytes(), false, Escaping::Literal);
905906
}
906907
}
908+
909+
fn format_alert(&mut self, alert: &NodeAlert, entering: bool) {
910+
if entering {
911+
write!(
912+
self,
913+
"> [!{}]",
914+
alert.alert_type.default_title().to_uppercase()
915+
)
916+
.unwrap();
917+
if alert.title.is_some() {
918+
let title = alert.title.as_ref().unwrap();
919+
write!(self, " {}", title).unwrap();
920+
}
921+
writeln!(self).unwrap();
922+
write!(self, "> ").unwrap();
923+
self.begin_content = true;
924+
write!(self.prefix, "> ").unwrap();
925+
} else {
926+
let new_len = self.prefix.len() - 2;
927+
self.prefix.truncate(new_len);
928+
self.blankline();
929+
}
930+
}
907931
}
908932

909933
fn longest_char_sequence(literal: &[u8], ch: u8) -> usize {

src/html.rs

+23
Original file line numberDiff line numberDiff line change
@@ -1152,6 +1152,29 @@ where
11521152
// Nowhere to put sourcepos.
11531153
self.output.write_all(net.as_bytes())?;
11541154
}
1155+
NodeValue::Alert(ref alert) => {
1156+
if entering {
1157+
self.cr()?;
1158+
self.output.write_all(b"<div class=\"alert ")?;
1159+
self.output
1160+
.write_all(alert.alert_type.css_class().as_bytes())?;
1161+
self.output.write_all(b"\"")?;
1162+
self.render_sourcepos(node)?;
1163+
self.output.write_all(b">\n")?;
1164+
self.output.write_all(b"<p class=\"alert-title\">")?;
1165+
match alert.title {
1166+
Some(ref title) => self.escape(title.as_bytes())?,
1167+
None => {
1168+
self.output
1169+
.write_all(alert.alert_type.default_title().as_bytes())?;
1170+
}
1171+
}
1172+
self.output.write_all(b"</p>\n")?;
1173+
} else {
1174+
self.cr()?;
1175+
self.output.write_all(b"</div>\n")?;
1176+
}
1177+
}
11551178
}
11561179
Ok(false)
11571180
}

src/main.rs

+2
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ enum Extension {
187187
Subscript,
188188
Spoiler,
189189
Greentext,
190+
Alerts,
190191
}
191192

192193
#[derive(Clone, Copy, Debug, ValueEnum)]
@@ -271,6 +272,7 @@ fn main() -> Result<(), Box<dyn Error>> {
271272
.subscript(exts.contains(&Extension::Subscript))
272273
.spoiler(exts.contains(&Extension::Spoiler))
273274
.greentext(exts.contains(&Extension::Greentext))
275+
.alerts(exts.contains(&Extension::Alerts))
274276
.maybe_front_matter_delimiter(cli.front_matter_delimiter);
275277

276278
#[cfg(feature = "shortcodes")]

src/nodes.rs

+10
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::convert::TryFrom;
77
#[cfg(feature = "shortcodes")]
88
pub use crate::parser::shortcodes::NodeShortCode;
99

10+
pub use crate::parser::alert::{AlertType, NodeAlert};
1011
pub use crate::parser::math::NodeMath;
1112
pub use crate::parser::multiline_block_quote::NodeMultilineBlockQuote;
1213

@@ -204,6 +205,10 @@ pub enum NodeValue {
204205
/// **Inline**. Text surrounded by escaped markup. Enabled with `spoiler` option.
205206
/// The `String` is the tag to be escaped.
206207
EscapedTag(String),
208+
209+
/// **Block**. GitHub style alert boxes which uses a modified blockquote syntax.
210+
/// Enabled with the `alerts` option.
211+
Alert(NodeAlert),
207212
}
208213

209214
/// Alignment of a single table cell.
@@ -449,6 +454,7 @@ impl NodeValue {
449454
| NodeValue::TableCell
450455
| NodeValue::TaskItem(..)
451456
| NodeValue::MultilineBlockQuote(_)
457+
| NodeValue::Alert(_)
452458
)
453459
}
454460

@@ -531,6 +537,7 @@ impl NodeValue {
531537
NodeValue::Subscript => "subscript",
532538
NodeValue::SpoileredText => "spoiler",
533539
NodeValue::EscapedTag(_) => "escaped_tag",
540+
NodeValue::Alert(_) => "alert",
534541
}
535542
}
536543
}
@@ -835,6 +842,9 @@ pub fn can_contain_type<'a>(node: &'a AstNode<'a>, child: &NodeValue) -> bool {
835842
child.block() && !matches!(*child, NodeValue::Item(..) | NodeValue::TaskItem(..))
836843
}
837844

845+
NodeValue::Alert(_) => {
846+
child.block() && !matches!(*child, NodeValue::Item(..) | NodeValue::TaskItem(..))
847+
}
838848
_ => false,
839849
}
840850
}

src/parser/alert.rs

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/// The metadata of an Alert node.
2+
#[derive(Debug, Clone, PartialEq, Eq)]
3+
pub struct NodeAlert {
4+
/// Type of alert
5+
pub alert_type: AlertType,
6+
7+
/// Overridden title. If None, then use the default title.
8+
pub title: Option<String>,
9+
10+
/// Originated from a multiline blockquote.
11+
pub multiline: bool,
12+
}
13+
14+
/// The type of alert.
15+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16+
pub enum AlertType {
17+
/// Useful information that users should know, even when skimming content
18+
#[default]
19+
Note,
20+
21+
/// Helpful advice for doing things better or more easily
22+
Tip,
23+
24+
/// Key information users need to know to achieve their goal
25+
Important,
26+
27+
/// Urgent info that needs immediate user attention to avoid problems
28+
Warning,
29+
30+
/// Advises about risks or negative outcomes of certain actions
31+
Caution,
32+
}
33+
34+
impl AlertType {
35+
/// Returns the default title for an alert type
36+
pub(crate) fn default_title(&self) -> String {
37+
match *self {
38+
AlertType::Note => String::from("Note"),
39+
AlertType::Tip => String::from("Tip"),
40+
AlertType::Important => String::from("Important"),
41+
AlertType::Warning => String::from("Warning"),
42+
AlertType::Caution => String::from("Caution"),
43+
}
44+
}
45+
46+
/// Returns the CSS class to use for an alert type
47+
pub(crate) fn css_class(&self) -> String {
48+
match *self {
49+
AlertType::Note => String::from("alert-note"),
50+
AlertType::Tip => String::from("alert-tip"),
51+
AlertType::Important => String::from("alert-important"),
52+
AlertType::Warning => String::from("alert-warning"),
53+
AlertType::Caution => String::from("alert-caution"),
54+
}
55+
}
56+
}

src/parser/mod.rs

+79
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ mod inlines;
44
pub mod shortcodes;
55
mod table;
66

7+
pub mod alert;
78
pub mod math;
89
pub mod multiline_block_quote;
910

@@ -29,6 +30,7 @@ use std::sync::Arc;
2930
use typed_arena::Arena;
3031

3132
use crate::adapters::HeadingAdapter;
33+
use crate::parser::alert::{AlertType, NodeAlert};
3234
use crate::parser::multiline_block_quote::NodeMultilineBlockQuote;
3335

3436
#[cfg(feature = "bon")]
@@ -420,6 +422,23 @@ pub struct ExtensionOptions<'c> {
420422
#[cfg_attr(feature = "bon", builder(default))]
421423
pub multiline_block_quotes: bool,
422424

425+
/// Enables GitHub style alerts
426+
///
427+
/// ```md
428+
/// > [!note]
429+
/// > Something of note
430+
/// ```
431+
///
432+
/// ```
433+
/// # use comrak::{markdown_to_html, Options};
434+
/// let mut options = Options::default();
435+
/// options.extension.alerts = true;
436+
/// assert_eq!(markdown_to_html("> [!note]\n> Something of note", &options),
437+
/// "<div class=\"alert alert-note\">\n<p class=\"alert-title\">Note</p>\n<p>Something of note</p>\n</div>\n");
438+
/// ```
439+
#[cfg_attr(feature = "bon", builder(default))]
440+
pub alerts: bool,
441+
423442
/// Enables math using dollar syntax.
424443
///
425444
/// ``` md
@@ -1506,6 +1525,11 @@ where
15061525
return (false, container, should_continue);
15071526
}
15081527
}
1528+
NodeValue::Alert(..) => {
1529+
if !self.parse_block_quote_prefix(line) {
1530+
return (false, container, should_continue);
1531+
}
1532+
}
15091533
_ => {}
15101534
}
15111535
}
@@ -1985,6 +2009,59 @@ where
19852009
true
19862010
}
19872011

2012+
fn detect_alert(&mut self, line: &[u8], indented: bool, alert_type: &mut AlertType) -> bool {
2013+
!indented
2014+
&& self.options.extension.alerts
2015+
&& line[self.first_nonspace] == b'>'
2016+
&& unwrap_into(
2017+
scanners::alert_start(&line[self.first_nonspace..]),
2018+
alert_type,
2019+
)
2020+
}
2021+
2022+
fn handle_alert(
2023+
&mut self,
2024+
container: &mut &'a Node<'a, RefCell<Ast>>,
2025+
line: &[u8],
2026+
indented: bool,
2027+
) -> bool {
2028+
let mut alert_type: AlertType = Default::default();
2029+
2030+
if !self.detect_alert(line, indented, &mut alert_type) {
2031+
return false;
2032+
}
2033+
2034+
let alert_startpos = self.first_nonspace;
2035+
let mut title_startpos = self.first_nonspace;
2036+
2037+
while line[title_startpos] != b']' {
2038+
title_startpos += 1;
2039+
}
2040+
title_startpos += 1;
2041+
2042+
// anything remaining on this line is considered an alert title
2043+
let mut tmp = entity::unescape_html(&line[title_startpos..]);
2044+
strings::trim(&mut tmp);
2045+
strings::unescape(&mut tmp);
2046+
2047+
let na = NodeAlert {
2048+
alert_type,
2049+
multiline: false,
2050+
title: if tmp.is_empty() {
2051+
None
2052+
} else {
2053+
Some(String::from_utf8(tmp).unwrap())
2054+
},
2055+
};
2056+
2057+
let offset = self.curline_len - self.offset - 1;
2058+
self.advance_offset(line, offset, false);
2059+
2060+
*container = self.add_child(container, NodeValue::Alert(na), alert_startpos + 1);
2061+
2062+
true
2063+
}
2064+
19882065
fn open_new_blocks(&mut self, container: &mut &'a AstNode<'a>, line: &[u8], all_matched: bool) {
19892066
let mut matched: usize = 0;
19902067
let mut nl: NodeList = NodeList::default();
@@ -2001,6 +2078,7 @@ where
20012078
let indented = self.indent >= CODE_INDENT;
20022079

20032080
if self.handle_multiline_blockquote(container, line, indented, &mut matched)
2081+
|| self.handle_alert(container, line, indented)
20042082
|| self.handle_blockquote(container, line, indented)
20052083
|| self.handle_atx_heading(container, line, indented, &mut matched)
20062084
|| self.handle_code_fence(container, line, indented, &mut matched)
@@ -2394,6 +2472,7 @@ where
23942472
|| container.data.borrow().sourcepos.start.line != self.line_number
23952473
}
23962474
NodeValue::MultilineBlockQuote(..) => false,
2475+
NodeValue::Alert(..) => false,
23972476
_ => true,
23982477
};
23992478

0 commit comments

Comments
 (0)