Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GitHub style alerts / admonitions #519

Merged
merged 3 commits into from
Jan 19, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading