Skip to content

Commit 9d434e5

Browse files
committed
Add a Highlight Extension ==for highlights==
1 parent 9a25834 commit 9d434e5

File tree

11 files changed

+108
-5
lines changed

11 files changed

+108
-5
lines changed

fuzz/fuzz_targets/all_options.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ fuzz_target!(|s: &str| {
3737
link_url_rewriter: Some(Arc::new(url_rewriter)),
3838
cjk_friendly_emphasis: true,
3939
subtext: true,
40+
highlight: true,
4041
};
4142

4243
let cb = |link_ref: options::BrokenLinkReference| {

src/cm.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ impl<'a, 'o, 'c> CommonMarkFormatter<'a, 'o, 'c> {
395395
NodeValue::Emph => self.format_emph(node)?,
396396
NodeValue::TaskItem(symbol) => self.format_task_item(symbol, node, entering)?,
397397
NodeValue::Strikethrough => self.format_strikethrough()?,
398+
NodeValue::Highlight => self.format_highlight()?,
398399
NodeValue::Superscript => self.format_superscript()?,
399400
NodeValue::Link(ref nl) => return self.format_link(node, nl, entering),
400401
NodeValue::Image(ref nl) => self.format_image(nl, allow_wrap, entering)?,
@@ -781,6 +782,11 @@ impl<'a, 'o, 'c> CommonMarkFormatter<'a, 'o, 'c> {
781782
Ok(())
782783
}
783784

785+
fn format_highlight(&mut self) -> fmt::Result {
786+
write!(self, "==")?;
787+
Ok(())
788+
}
789+
784790
fn format_superscript(&mut self) -> fmt::Result {
785791
write!(self, "^")?;
786792
Ok(())

src/html.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,7 @@ pub fn format_node_default<'a, T>(
440440
render_footnote_reference(context, node, entering, nfr)
441441
}
442442
NodeValue::Strikethrough => render_strikethrough(context, node, entering),
443+
NodeValue::Highlight => render_highlight(context, node, entering),
443444
NodeValue::Table(_) => render_table(context, node, entering),
444445
NodeValue::TableCell => render_table_cell(context, node, entering),
445446
NodeValue::TableRow(thead) => render_table_row(context, node, entering, thead),
@@ -1034,6 +1035,22 @@ fn render_strikethrough<'a, T>(
10341035
Ok(ChildRendering::HTML)
10351036
}
10361037

1038+
fn render_highlight<'a, T>(
1039+
context: &mut Context<T>,
1040+
node: Node<'a>,
1041+
entering: bool,
1042+
) -> Result<ChildRendering, fmt::Error> {
1043+
if entering {
1044+
context.write_str("<mark")?;
1045+
render_sourcepos(context, node)?;
1046+
context.write_str(">")?;
1047+
} else {
1048+
context.write_str("</mark>")?;
1049+
}
1050+
1051+
Ok(ChildRendering::HTML)
1052+
}
1053+
10371054
fn render_table<'a, T>(
10381055
context: &mut Context<T>,
10391056
node: Node<'a>,

src/main.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ enum Extension {
196196
Alerts,
197197
CjkFriendlyEmphasis,
198198
Subtext,
199+
Highlight,
199200
}
200201

201202
#[derive(Clone, Copy, Debug, ValueEnum)]
@@ -284,7 +285,8 @@ fn main() -> Result<(), Box<dyn Error>> {
284285
.alerts(exts.contains(&Extension::Alerts))
285286
.maybe_front_matter_delimiter(cli.front_matter_delimiter)
286287
.cjk_friendly_emphasis(exts.contains(&Extension::CjkFriendlyEmphasis))
287-
.subtext(exts.contains(&Extension::Subtext));
288+
.subtext(exts.contains(&Extension::Subtext))
289+
.highlight(exts.contains(&Extension::Highlight));
288290

289291
#[cfg(feature = "shortcodes")]
290292
let extension = extension.shortcodes(cli.gemoji);

src/nodes.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ pub enum NodeValue {
159159
/// per the GFM spec.
160160
Strikethrough,
161161

162+
/// ***Inline**. [Highlight](https://www.markdownlang.com/extended/highlight.html) / mark text
163+
Highlight,
164+
162165
/// **Inline**. Superscript. Enabled with `ext_superscript` option.
163166
Superscript,
164167

@@ -648,6 +651,7 @@ impl NodeValue {
648651
NodeValue::HtmlInline(..) => "html_inline",
649652
NodeValue::Raw(..) => "raw",
650653
NodeValue::Strikethrough => "strikethrough",
654+
NodeValue::Highlight => "highlight",
651655
NodeValue::FrontMatter(_) => "frontmatter",
652656
NodeValue::TaskItem { .. } => "taskitem",
653657
NodeValue::Superscript => "superscript",
@@ -899,6 +903,7 @@ impl<'a> arena_tree::Node<'a, RefCell<Ast>> {
899903
| NodeValue::Image(..)
900904
| NodeValue::WikiLink(..)
901905
| NodeValue::Strikethrough
906+
| NodeValue::Highlight
902907
| NodeValue::Superscript
903908
| NodeValue::SpoileredText
904909
| NodeValue::Underline
@@ -921,6 +926,7 @@ impl<'a> arena_tree::Node<'a, RefCell<Ast>> {
921926
| NodeValue::Link(..)
922927
| NodeValue::Image(..)
923928
| NodeValue::Strikethrough
929+
| NodeValue::Highlight
924930
| NodeValue::HtmlInline(..)
925931
| NodeValue::Math(..)
926932
| NodeValue::WikiLink(..)
@@ -943,6 +949,7 @@ impl<'a> arena_tree::Node<'a, RefCell<Ast>> {
943949
| NodeValue::Link(..)
944950
| NodeValue::Image(..)
945951
| NodeValue::Strikethrough
952+
| NodeValue::Highlight
946953
| NodeValue::HtmlInline(..)
947954
| NodeValue::Math(..)
948955
| NodeValue::WikiLink(..)

src/parser/inlines.rs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ impl<'a, 'r, 'o, 'd, 'c, 'p> Subject<'a, 'r, 'o, 'd, 'c, 'p> {
104104
s.special_chars[b'~' as usize] = true;
105105
s.skip_chars[b'~' as usize] = true;
106106
}
107+
if options.extension.highlight {
108+
s.special_chars[b'=' as usize] = true;
109+
s.skip_chars[b'=' as usize] = true;
110+
}
107111
if options.extension.superscript || options.extension.inline_footnotes {
108112
s.special_chars[b'^' as usize] = true;
109113
}
@@ -190,6 +194,7 @@ impl<'a, 'r, 'o, 'd, 'c, 'p> Subject<'a, 'r, 'o, 'd, 'c, 'p> {
190194
'\0' => return false,
191195
'\r' | '\n' => Some(self.handle_newline()),
192196
'`' => Some(self.handle_backticks(&ast.line_offsets)),
197+
'=' if self.options.extension.highlight => Some(self.handle_delim(b'=')),
193198
'\\' => Some(self.handle_backslash()),
194199
'&' => Some(self.handle_entity()),
195200
'<' => Some(self.handle_pointy_brace(&ast.line_offsets)),
@@ -635,15 +640,18 @@ impl<'a, 'r, 'o, 'd, 'c, 'p> Subject<'a, 'r, 'o, 'd, 'c, 'p> {
635640
self.pos - 1,
636641
);
637642

638-
let is_valid_strikethrough_delim = if c == b'~' && self.options.extension.strikethrough {
643+
let is_valid_double_delim_if_required = if (c == b'~'
644+
&& self.options.extension.strikethrough)
645+
|| (c == b'=' && self.options.extension.highlight)
646+
{
639647
numdelims <= 2
640648
} else {
641649
true
642650
};
643651

644652
if (can_open || can_close)
645653
&& (!(c == b'\'' || c == b'"') || self.options.parse.smart)
646-
&& is_valid_strikethrough_delim
654+
&& is_valid_double_delim_if_required
647655
{
648656
self.push_delimiter(c, can_open, can_close, inl);
649657
}
@@ -1116,7 +1124,7 @@ impl<'a, 'r, 'o, 'd, 'c, 'p> Subject<'a, 'r, 'o, 'd, 'c, 'p> {
11161124
// This array is an important optimization that prevents searching down
11171125
// the stack for openers we've previously searched for and know don't
11181126
// exist, preventing exponential blowup on pathological cases.
1119-
let mut openers_bottom: [usize; 12] = [stack_bottom; 12];
1127+
let mut openers_bottom: [usize; 13] = [stack_bottom; 13];
11201128

11211129
// This is traversing the stack from the top to the bottom, setting `closer` to
11221130
// the delimiter directly above `stack_bottom`. In the case where we are processing
@@ -1146,7 +1154,8 @@ impl<'a, 'r, 'o, 'd, 'c, 'p> Subject<'a, 'r, 'o, 'd, 'c, 'p> {
11461154
b'"' => 3,
11471155
b'\'' => 4,
11481156
b'_' => 5,
1149-
b'*' => 6 + (if c.can_open { 3 } else { 0 }) + (c.length % 3),
1157+
b'*' => 7 + (if c.can_open { 3 } else { 0 }) + (c.length % 3),
1158+
b'=' => 12,
11501159
_ => unreachable!(),
11511160
};
11521161

@@ -1197,6 +1206,7 @@ impl<'a, 'r, 'o, 'd, 'c, 'p> Subject<'a, 'r, 'o, 'd, 'c, 'p> {
11971206
&& c.delim_char == b'~')
11981207
|| (self.options.extension.superscript && c.delim_char == b'^')
11991208
|| (self.options.extension.spoiler && c.delim_char == b'|')
1209+
|| (self.options.extension.highlight && c.delim_char == b'=')
12001210
{
12011211
if opener_found {
12021212
// Finally, here's the happy case where the delimiters
@@ -1373,6 +1383,17 @@ impl<'a, 'r, 'o, 'd, 'c, 'p> Subject<'a, 'r, 'o, 'd, 'c, 'p> {
13731383
} else {
13741384
NodeValue::EscapedTag("~~".to_owned())
13751385
}
1386+
} else if opener_char == b'=' {
1387+
// Not emphasis
1388+
// Unlike for |, these cases have to be handled because they will match
1389+
// in the event subscript but not strikethrough is enabled
1390+
if self.options.extension.highlight {
1391+
NodeValue::Highlight
1392+
} else if use_delims == 1 {
1393+
NodeValue::EscapedTag("=".to_owned())
1394+
} else {
1395+
NodeValue::EscapedTag("==".to_owned())
1396+
}
13761397
} else if self.options.extension.superscript && opener_char == b'^' {
13771398
NodeValue::Superscript
13781399
} else if self.options.extension.spoiler && opener_char == b'|' {

src/parser/options.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,23 @@ pub struct Extension<'c> {
526526
/// ```
527527
#[cfg_attr(feature = "bon", builder(default))]
528528
pub subtext: bool,
529+
530+
/// Enables highlighting (mark) using `==`.
531+
///
532+
/// ```md
533+
/// Hey, ==this is important!==
534+
/// ```
535+
///
536+
/// ```rust
537+
/// # use comrak::{markdown_to_html, Options};
538+
/// let mut options = Options::default();
539+
/// options.extension.highlight = true;
540+
///
541+
/// assert_eq!(markdown_to_html("Hey, ==this is important!==", &options),
542+
/// "<p>Hey, <mark>this is important!</mark></p>\n");
543+
/// ```
544+
#[cfg_attr(feature = "bon", builder(default))]
545+
pub highlight: bool,
529546
}
530547

531548
impl<'c> Extension<'c> {

src/tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ mod front_matter;
2121
mod fuzz;
2222
mod greentext;
2323
mod header_ids;
24+
mod highlight;
2425
#[path = "tests/html.rs"]
2526
mod html_;
2627
mod inline_footnotes;

src/tests/highlight.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
use super::*;
2+
3+
#[test]
4+
fn highlight() {
5+
html_opts!(
6+
[extension.highlight],
7+
concat!(
8+
"This is ==important!==.\n",
9+
"\n",
10+
"This is ==very important== OK?\n",
11+
"\n",
12+
"==It is a \n shrubbery==\n",
13+
"\n",
14+
"Vendo ==Opel Corsa **em bom estado**==\n",
15+
),
16+
concat!(
17+
"<p>This is <mark>important!</mark>.</p>\n",
18+
"<p>This is <mark>very important</mark> OK?</p>\n",
19+
"<p><mark>It is a\nshrubbery</mark></p>\n",
20+
"<p>Vendo <mark>Opel Corsa <strong>em bom estado</strong></mark></p>\n"
21+
),
22+
);
23+
}

src/tests/sourcepos.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,11 @@ const STRIKETHROUGH: TestCase = (
298298
"hello ~~world~~ between ~~wo\nrld~~ after",
299299
);
300300

301+
const HIGHLIGHT: TestCase = (
302+
&[sourcepos!((1:7-1:15)), sourcepos!((1:25-2:5))],
303+
"hello ==world== between ==wo\nrld== after",
304+
);
305+
301306
const SUPERSCRIPT: TestCase = (
302307
&[sourcepos!((1:7-1:13)), sourcepos!((1:23-2:4))],
303308
"hello ^world^ between ^wo\nrld^ after",
@@ -475,6 +480,7 @@ fn node_values() -> HashMap<NodeValueDiscriminants, TestCase> {
475480
Emph => EMPH,
476481
Strong => STRONG,
477482
Strikethrough => STRIKETHROUGH,
483+
Highlight => HIGHLIGHT,
478484
Superscript => SUPERSCRIPT,
479485
Subscript => SUBSCRIPT,
480486
Link => LINK,
@@ -508,6 +514,7 @@ fn sourcepos() {
508514
options.extension.table = true;
509515
options.extension.tasklist = true;
510516
options.extension.strikethrough = true;
517+
options.extension.highlight = true;
511518
options.extension.superscript = true;
512519
options.extension.subscript = true;
513520
options.extension.autolink = true;

0 commit comments

Comments
 (0)