Skip to content

Commit

Permalink
Add support for GitHub style alerts
Browse files Browse the repository at this point in the history
Also allows overriding the title by specifying a string
after the alert syntax.
  • Loading branch information
digitalmoksha committed Jan 18, 2025
1 parent 45c96a2 commit 8686e9e
Show file tree
Hide file tree
Showing 12 changed files with 1,144 additions and 6 deletions.
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 description-lists" \
|| 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
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"),
}
}
}
63 changes: 63 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,8 @@ pub struct ExtensionOptions<'c> {
#[cfg_attr(feature = "bon", builder(default))]
pub multiline_block_quotes: bool,

// #[cfg_attr(feature = "bon", builder(default))]
// pub alerts: bool,
/// Enables math using dollar syntax.
///
/// ``` md
Expand Down Expand Up @@ -1506,6 +1510,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 +1994,58 @@ where
true
}

fn detect_alert(&mut self, line: &[u8], indented: bool, alert_type: &mut AlertType) -> bool {
!indented
&& 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 +2062,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 +2456,7 @@ where
|| container.data.borrow().sourcepos.start.line != self.line_number
}
NodeValue::MultilineBlockQuote(..) => false,
NodeValue::Alert(..) => false,
_ => true,
};

Expand Down
16 changes: 16 additions & 0 deletions src/scanners.re
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
scheme = [A-Za-z][A-Za-z0-9.+-]{1,31};
*/

use crate::parser::alert::AlertType;

pub fn atx_heading_start(s: &[u8]) -> Option<usize> {
let mut cursor = 0;
let mut marker = 0;
Expand Down Expand Up @@ -120,6 +122,20 @@ pub fn html_block_end_5(s: &[u8]) -> bool {
*/
}

pub fn alert_start(s: &[u8]) -> Option<AlertType> {
let mut cursor = 0;
let mut marker = 0;
let len = s.len();
/*!re2c
'> [!note]' { return Some(AlertType::Note); }
'> [!tip]' { return Some(AlertType::Tip); }
'> [!important]' { return Some(AlertType::Important); }
'> [!warning]' { return Some(AlertType::Warning); }
'> [!caution]' { return Some(AlertType::Caution); }
* { return None; }
*/
}

pub fn open_code_fence(s: &[u8]) -> Option<usize> {
let mut cursor = 0;
let mut marker = 0;
Expand Down
Loading

0 comments on commit 8686e9e

Please sign in to comment.