Skip to content

Commit

Permalink
Merge pull request #519 from kivikakk/bw-admonmition-blocks
Browse files Browse the repository at this point in the history
Add GitHub style alerts / admonitions
  • Loading branch information
digitalmoksha authored Jan 19, 2025
2 parents 45c96a2 + 6aa5a73 commit 428ad9a
Show file tree
Hide file tree
Showing 18 changed files with 1,231 additions and 8 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ Options:
[possible values: strikethrough, tagfilter, table, autolink, tasklist, superscript,
footnotes, description-lists, multiline-block-quotes, math-dollars, math-code,
wikilinks-title-after-pipe, wikilinks-title-before-pipe, underline, subscript, spoiler,
greentext]
greentext, alerts]
-t, --to <FORMAT>
Specify output format
Expand Down
1 change: 1 addition & 0 deletions fuzz/fuzz_targets/all_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ fuzz_target!(|s: &str| {
extension.underline = true;
extension.spoiler = true;
extension.greentext = true;
extension.alerts = true;

let mut parse = ParseOptions::default();
parse.smart = true;
Expand Down
8 changes: 8 additions & 0 deletions fuzz/fuzz_targets/quadratic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ struct FuzzExtensionOptions {
shortcodes: bool,
wikilinks_title_after_pipe: bool,
wikilinks_title_before_pipe: bool,
underline: bool,
spoiler: bool,
greentext: bool,
alerts: bool,
}

impl FuzzExtensionOptions {
Expand All @@ -216,6 +220,10 @@ impl FuzzExtensionOptions {
extension.shortcodes = self.shortcodes;
extension.wikilinks_title_after_pipe = self.wikilinks_title_after_pipe;
extension.wikilinks_title_before_pipe = self.wikilinks_title_before_pipe;
extension.underline = self.underline;
extension.spoiler = self.spoiler;
extension.greentext = self.greentext;
extension.alerts = self.alerts;
extension.front_matter_delimiter = None;
extension.header_ids = None;
extension
Expand Down
2 changes: 2 additions & 0 deletions script/cibuild
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ python3 spec_tests.py --no-normalize --spec ../../../src/tests/fixtures/wikilink
|| failed=1
python3 spec_tests.py --no-normalize --spec ../../../src/tests/fixtures/description_lists.md "$PROGRAM_ARG -e description-lists" \
|| failed=1
python3 spec_tests.py --no-normalize --spec ../../../src/tests/fixtures/alerts.md "$PROGRAM_ARG -e alerts" \
|| failed=1

python3 spec_tests.py --no-normalize --spec regression.txt "$PROGRAM_ARG" \
|| failed=1
Expand Down
28 changes: 26 additions & 2 deletions src/cm.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::ctype::{isalpha, isdigit, ispunct, isspace};
use crate::nodes::{
AstNode, ListDelimType, ListType, NodeCodeBlock, NodeHeading, NodeHtmlBlock, NodeLink,
NodeMath, NodeTable, NodeValue, NodeWikiLink,
AstNode, ListDelimType, ListType, NodeAlert, NodeCodeBlock, NodeHeading, NodeHtmlBlock,
NodeLink, NodeMath, NodeTable, NodeValue, NodeWikiLink,
};
use crate::nodes::{NodeList, TableAlignment};
#[cfg(feature = "shortcodes")]
Expand Down Expand Up @@ -401,6 +401,7 @@ impl<'a, 'o, 'c> CommonMarkFormatter<'a, 'o, 'c> {
NodeValue::Subscript => self.format_subscript(),
NodeValue::SpoileredText => self.format_spoiler(),
NodeValue::EscapedTag(ref net) => self.format_escaped_tag(net),
NodeValue::Alert(ref alert) => self.format_alert(alert, entering),
};
true
}
Expand Down Expand Up @@ -904,6 +905,29 @@ impl<'a, 'o, 'c> CommonMarkFormatter<'a, 'o, 'c> {
self.output(end_fence.as_bytes(), false, Escaping::Literal);
}
}

fn format_alert(&mut self, alert: &NodeAlert, entering: bool) {
if entering {
write!(
self,
"> [!{}]",
alert.alert_type.default_title().to_uppercase()
)
.unwrap();
if alert.title.is_some() {
let title = alert.title.as_ref().unwrap();
write!(self, " {}", title).unwrap();
}
writeln!(self).unwrap();
write!(self, "> ").unwrap();
self.begin_content = true;
write!(self.prefix, "> ").unwrap();
} else {
let new_len = self.prefix.len() - 2;
self.prefix.truncate(new_len);
self.blankline();
}
}
}

fn longest_char_sequence(literal: &[u8], ch: u8) -> usize {
Expand Down
23 changes: 23 additions & 0 deletions src/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1152,6 +1152,29 @@ where
// Nowhere to put sourcepos.
self.output.write_all(net.as_bytes())?;
}
NodeValue::Alert(ref alert) => {
if entering {
self.cr()?;
self.output.write_all(b"<div class=\"alert ")?;
self.output
.write_all(alert.alert_type.css_class().as_bytes())?;
self.output.write_all(b"\"")?;
self.render_sourcepos(node)?;
self.output.write_all(b">\n")?;
self.output.write_all(b"<p class=\"alert-title\">")?;
match alert.title {
Some(ref title) => self.escape(title.as_bytes())?,
None => {
self.output
.write_all(alert.alert_type.default_title().as_bytes())?;
}
}
self.output.write_all(b"</p>\n")?;
} else {
self.cr()?;
self.output.write_all(b"</div>\n")?;
}
}
}
Ok(false)
}
Expand Down
2 changes: 2 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ enum Extension {
Subscript,
Spoiler,
Greentext,
Alerts,
}

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

#[cfg(feature = "shortcodes")]
Expand Down
10 changes: 10 additions & 0 deletions src/nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::convert::TryFrom;
#[cfg(feature = "shortcodes")]
pub use crate::parser::shortcodes::NodeShortCode;

pub use crate::parser::alert::{AlertType, NodeAlert};
pub use crate::parser::math::NodeMath;
pub use crate::parser::multiline_block_quote::NodeMultilineBlockQuote;

Expand Down Expand Up @@ -204,6 +205,10 @@ pub enum NodeValue {
/// **Inline**. Text surrounded by escaped markup. Enabled with `spoiler` option.
/// The `String` is the tag to be escaped.
EscapedTag(String),

/// **Block**. GitHub style alert boxes which uses a modified blockquote syntax.
/// Enabled with the `alerts` option.
Alert(NodeAlert),
}

/// Alignment of a single table cell.
Expand Down Expand Up @@ -449,6 +454,7 @@ impl NodeValue {
| NodeValue::TableCell
| NodeValue::TaskItem(..)
| NodeValue::MultilineBlockQuote(_)
| NodeValue::Alert(_)
)
}

Expand Down Expand Up @@ -531,6 +537,7 @@ impl NodeValue {
NodeValue::Subscript => "subscript",
NodeValue::SpoileredText => "spoiler",
NodeValue::EscapedTag(_) => "escaped_tag",
NodeValue::Alert(_) => "alert",
}
}
}
Expand Down Expand Up @@ -835,6 +842,9 @@ pub fn can_contain_type<'a>(node: &'a AstNode<'a>, child: &NodeValue) -> bool {
child.block() && !matches!(*child, NodeValue::Item(..) | NodeValue::TaskItem(..))
}

NodeValue::Alert(_) => {
child.block() && !matches!(*child, NodeValue::Item(..) | NodeValue::TaskItem(..))
}
_ => false,
}
}
Expand Down
56 changes: 56 additions & 0 deletions src/parser/alert.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/// The metadata of an Alert node.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NodeAlert {
/// Type of alert
pub alert_type: AlertType,

/// Overridden title. If None, then use the default title.
pub title: Option<String>,

/// Originated from a multiline blockquote.
pub multiline: bool,
}

/// The type of alert.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AlertType {
/// Useful information that users should know, even when skimming content
#[default]
Note,

/// Helpful advice for doing things better or more easily
Tip,

/// Key information users need to know to achieve their goal
Important,

/// Urgent info that needs immediate user attention to avoid problems
Warning,

/// Advises about risks or negative outcomes of certain actions
Caution,
}

impl AlertType {
/// Returns the default title for an alert type
pub(crate) fn default_title(&self) -> String {
match *self {
AlertType::Note => String::from("Note"),
AlertType::Tip => String::from("Tip"),
AlertType::Important => String::from("Important"),
AlertType::Warning => String::from("Warning"),
AlertType::Caution => String::from("Caution"),
}
}

/// Returns the CSS class to use for an alert type
pub(crate) fn css_class(&self) -> String {
match *self {
AlertType::Note => String::from("alert-note"),
AlertType::Tip => String::from("alert-tip"),
AlertType::Important => String::from("alert-important"),
AlertType::Warning => String::from("alert-warning"),
AlertType::Caution => String::from("alert-caution"),
}
}
}
79 changes: 79 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod inlines;
pub mod shortcodes;
mod table;

pub mod alert;
pub mod math;
pub mod multiline_block_quote;

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

use crate::adapters::HeadingAdapter;
use crate::parser::alert::{AlertType, NodeAlert};
use crate::parser::multiline_block_quote::NodeMultilineBlockQuote;

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

/// Enables GitHub style alerts
///
/// ```md
/// > [!note]
/// > Something of note
/// ```
///
/// ```
/// # use comrak::{markdown_to_html, Options};
/// let mut options = Options::default();
/// options.extension.alerts = true;
/// assert_eq!(markdown_to_html("> [!note]\n> Something of note", &options),
/// "<div class=\"alert alert-note\">\n<p class=\"alert-title\">Note</p>\n<p>Something of note</p>\n</div>\n");
/// ```
#[cfg_attr(feature = "bon", builder(default))]
pub alerts: bool,

/// Enables math using dollar syntax.
///
/// ``` md
Expand Down Expand Up @@ -1506,6 +1525,11 @@ where
return (false, container, should_continue);
}
}
NodeValue::Alert(..) => {
if !self.parse_block_quote_prefix(line) {
return (false, container, should_continue);
}
}
_ => {}
}
}
Expand Down Expand Up @@ -1985,6 +2009,59 @@ where
true
}

fn detect_alert(&mut self, line: &[u8], indented: bool, alert_type: &mut AlertType) -> bool {
!indented
&& self.options.extension.alerts
&& line[self.first_nonspace] == b'>'
&& unwrap_into(
scanners::alert_start(&line[self.first_nonspace..]),
alert_type,
)
}

fn handle_alert(
&mut self,
container: &mut &'a Node<'a, RefCell<Ast>>,
line: &[u8],
indented: bool,
) -> bool {
let mut alert_type: AlertType = Default::default();

if !self.detect_alert(line, indented, &mut alert_type) {
return false;
}

let alert_startpos = self.first_nonspace;
let mut title_startpos = self.first_nonspace;

while line[title_startpos] != b']' {
title_startpos += 1;
}
title_startpos += 1;

// anything remaining on this line is considered an alert title
let mut tmp = entity::unescape_html(&line[title_startpos..]);
strings::trim(&mut tmp);
strings::unescape(&mut tmp);

let na = NodeAlert {
alert_type,
multiline: false,
title: if tmp.is_empty() {
None
} else {
Some(String::from_utf8(tmp).unwrap())
},
};

let offset = self.curline_len - self.offset - 1;
self.advance_offset(line, offset, false);

*container = self.add_child(container, NodeValue::Alert(na), alert_startpos + 1);

true
}

fn open_new_blocks(&mut self, container: &mut &'a AstNode<'a>, line: &[u8], all_matched: bool) {
let mut matched: usize = 0;
let mut nl: NodeList = NodeList::default();
Expand All @@ -2001,6 +2078,7 @@ where
let indented = self.indent >= CODE_INDENT;

if self.handle_multiline_blockquote(container, line, indented, &mut matched)
|| self.handle_alert(container, line, indented)
|| self.handle_blockquote(container, line, indented)
|| self.handle_atx_heading(container, line, indented, &mut matched)
|| self.handle_code_fence(container, line, indented, &mut matched)
Expand Down Expand Up @@ -2394,6 +2472,7 @@ where
|| container.data.borrow().sourcepos.start.line != self.line_number
}
NodeValue::MultilineBlockQuote(..) => false,
NodeValue::Alert(..) => false,
_ => true,
};

Expand Down
Loading

0 comments on commit 428ad9a

Please sign in to comment.