Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions fuzz/fuzz_targets/all_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ fuzz_target!(|s: &str| {
render.escape = true;
render.list_style = ListStyleType::Star;
render.sourcepos = true;
render.escaped_char_spans = true;

markdown_to_html(
s,
Expand Down
2 changes: 2 additions & 0 deletions fuzz/fuzz_targets/quadratic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ struct FuzzRenderOptions {
escape: bool,
list_style: ListStyleType,
sourcepos: bool,
escaped_char_spans: bool,
}

impl FuzzRenderOptions {
Expand All @@ -256,6 +257,7 @@ impl FuzzRenderOptions {
render.escape = self.escape;
render.list_style = self.list_style;
render.sourcepos = self.sourcepos;
render.escaped_char_spans = self.escaped_char_spans;
render
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/cm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,9 @@ impl<'a, 'o> CommonMarkFormatter<'a, 'o> {
self.format_footnote_reference(nfr.name.as_bytes(), entering)
}
NodeValue::MultilineBlockQuote(..) => self.format_block_quote(entering),
NodeValue::Escaped => {
// noop - automatic escaping is already being done
}
};
true
}
Expand Down
11 changes: 11 additions & 0 deletions src/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1004,6 +1004,17 @@ impl<'o> HtmlFormatter<'o> {
self.output.write_all(b"</blockquote>\n")?;
}
}
NodeValue::Escaped => {
if self.options.render.escaped_char_spans {
if entering {
self.output.write_all(b"<span data-escaped-char")?;
self.render_sourcepos(node)?;
self.output.write_all(b">")?;
} else {
self.output.write_all(b"</span>")?;
}
}
}
}
Ok(false)
}
Expand Down
5 changes: 5 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ struct Cli {
#[arg(long)]
escape: bool,

/// Wrap escaped characters in span tags
#[arg(long)]
escaped_char_spans: bool,

/// Specify extension name(s) to use
///
/// Multiple extensions can be delimited with ",", e.g. --extension strikethrough,table
Expand Down Expand Up @@ -237,6 +241,7 @@ fn main() -> Result<(), Box<dyn Error>> {
.escape(cli.escape)
.list_style(cli.list_style.into())
.sourcepos(cli.sourcepos)
.escaped_char_spans(cli.escaped_char_spans)
.build()?;

let options = Options {
Expand Down
4 changes: 4 additions & 0 deletions src/nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ pub enum NodeValue {
/// >>>
/// ```
MultilineBlockQuote(NodeMultilineBlockQuote),

/// **Inline**. A character that has been [escaped](https://github.github.com/gfm/#backslash-escapes)
Escaped,
}

/// Alignment of a single table cell.
Expand Down Expand Up @@ -481,6 +484,7 @@ impl NodeValue {
#[cfg(feature = "shortcodes")]
NodeValue::ShortCode(_) => "shortcode",
NodeValue::MultilineBlockQuote(_) => "multiline_block_quote",
NodeValue::Escaped => "escaped",
}
}
}
Expand Down
15 changes: 13 additions & 2 deletions src/parser/inlines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -929,13 +929,24 @@ impl<'a, 'r, 'o, 'd, 'i, 'c, 'subj> Subject<'a, 'r, 'o, 'd, 'i, 'c, 'subj> {
pub fn handle_backslash(&mut self) -> &'a AstNode<'a> {
let startpos = self.pos;
self.pos += 1;

if self.peek_char().map_or(false, |&c| ispunct(c)) {
let inl;
self.pos += 1;
self.make_inline(

let inline_text = self.make_inline(
NodeValue::Text(String::from_utf8(vec![self.input[self.pos - 1]]).unwrap()),
self.pos - 2,
self.pos - 1,
)
);

if self.options.render.escaped_char_spans {
inl = self.make_inline(NodeValue::Escaped, self.pos - 2, self.pos - 1);
inl.append(inline_text);
inl
} else {
inline_text
}
} else if !self.eof() && self.skip_line_end() {
self.make_inline(NodeValue::LineBreak, startpos, self.pos - 1)
} else {
Expand Down
17 changes: 17 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,23 @@ pub struct RenderOptions {
/// assert!(xml.contains("<emph sourcepos=\"1:7-1:13\">"));
/// ```
pub sourcepos: bool,

/// Wrap escaped characters in a `<span>` to allow any
/// post-processing to recognize them.
///
/// ```rust
/// # use comrak::{markdown_to_html, Options};
/// let mut options = Options::default();
/// let input = "Notify user \\@example";
///
/// assert_eq!(markdown_to_html(input, &options),
/// "<p>Notify user @example</p>\n");
///
/// options.render.escaped_char_spans = true;
/// assert_eq!(markdown_to_html(input, &options),
/// "<p>Notify user <span data-escaped-char>@</span>example</p>\n");
/// ```
pub escaped_char_spans: bool,
}

#[non_exhaustive]
Expand Down
53 changes: 44 additions & 9 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ mod autolink;
mod commonmark;
mod core;
mod description_lists;
mod escaped_char_spans;
mod footnotes;
mod fuzz;
mod header_ids;
Expand All @@ -27,9 +28,13 @@ mod tasklist;
mod xml;

#[track_caller]
fn compare_strs(output: &str, expected: &str, kind: &str) {
fn compare_strs(output: &str, expected: &str, kind: &str, original_input: &str) {
if output != expected {
println!("Running {} test", kind);
println!("Original Input:");
println!("==============================");
println!("{}", original_input);
println!("==============================");
Comment on lines +34 to +37
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kivikakk added this so it's easier to see the original input that caused the error, rather than referring to the actual test.

println!("Got:");
println!("==============================");
println!("{}", output);
Expand All @@ -53,7 +58,12 @@ fn commonmark(input: &str, expected: &str, opts: Option<&Options>) {
let root = parse_document(&arena, input, options);
let mut output = vec![];
cm::format_document(root, options, &mut output).unwrap();
compare_strs(&String::from_utf8(output).unwrap(), expected, "regular");
compare_strs(
&String::from_utf8(output).unwrap(),
expected,
"regular",
input,
);
}

#[track_caller]
Expand All @@ -79,21 +89,29 @@ fn html_opts_w(input: &str, expected: &str, options: &Options) {
let root = parse_document(&arena, input, &options);
let mut output = vec![];
html::format_document(root, &options, &mut output).unwrap();
compare_strs(&String::from_utf8(output).unwrap(), expected, "regular");
compare_strs(
&String::from_utf8(output).unwrap(),
expected,
"regular",
input,
);

if options.render.sourcepos {
return;
}

let mut md = vec![];
cm::format_document(root, &options, &mut md).unwrap();
let root = parse_document(&arena, &String::from_utf8(md).unwrap(), &options);

let md_string = &String::from_utf8(md).unwrap();
let root = parse_document(&arena, md_string, &options);
let mut output_from_rt = vec![];
html::format_document(root, &options, &mut output_from_rt).unwrap();
compare_strs(
&String::from_utf8(output_from_rt).unwrap(),
expected,
"roundtrip",
md_string,
);
}

Expand Down Expand Up @@ -137,6 +155,7 @@ macro_rules! html_opts {
escape: true,
list_style: $crate::ListStyleType::Star,
sourcepos: true,
escaped_char_spans: true,
},
});
}
Expand All @@ -152,21 +171,29 @@ fn html_plugins(input: &str, expected: &str, plugins: &Plugins) {
let root = parse_document(&arena, input, &options);
let mut output = vec![];
html::format_document_with_plugins(root, &options, &mut output, plugins).unwrap();
compare_strs(&String::from_utf8(output).unwrap(), expected, "regular");
compare_strs(
&String::from_utf8(output).unwrap(),
expected,
"regular",
input,
);

if options.render.sourcepos {
return;
}

let mut md = vec![];
cm::format_document(root, &options, &mut md).unwrap();
let root = parse_document(&arena, &String::from_utf8(md).unwrap(), &options);

let md_string = &String::from_utf8(md).unwrap();
let root = parse_document(&arena, md_string, &options);
let mut output_from_rt = vec![];
html::format_document_with_plugins(root, &options, &mut output_from_rt, plugins).unwrap();
compare_strs(
&String::from_utf8(output_from_rt).unwrap(),
expected,
"roundtrip",
md_string,
);
}

Expand All @@ -187,21 +214,29 @@ where
let root = parse_document(&arena, input, &options);
let mut output = vec![];
crate::xml::format_document(root, &options, &mut output).unwrap();
compare_strs(&String::from_utf8(output).unwrap(), expected, "regular");
compare_strs(
&String::from_utf8(output).unwrap(),
expected,
"regular",
input,
);

if options.render.sourcepos {
return;
}

let mut md = vec![];
cm::format_document(root, &options, &mut md).unwrap();
let root = parse_document(&arena, &String::from_utf8(md).unwrap(), &options);

let md_string = &String::from_utf8(md).unwrap();
let root = parse_document(&arena, md_string, &options);
let mut output_from_rt = vec![];
crate::xml::format_document(root, &options, &mut output_from_rt).unwrap();
compare_strs(
&String::from_utf8(output_from_rt).unwrap(),
expected,
"roundtrip",
md_string,
);
}

Expand All @@ -214,7 +249,7 @@ fn asssert_node_eq<'a>(node: &'a AstNode<'a>, location: &[usize], expected: &Nod
let actual = format!("{:?}", data.value);
let expected = format!("{:?}", expected);

compare_strs(&actual, &expected, "ast comparison");
compare_strs(&actual, &expected, "ast comparison", "ast node");
}

macro_rules! sourcepos {
Expand Down
2 changes: 2 additions & 0 deletions src/tests/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ fn exercise_full_api() {
render.escape(false);
render.list_style(ListStyleType::Dash);
render.sourcepos(false);
render.escaped_char_spans(false);

pub struct MockAdapter {}
impl SyntaxHighlighterAdapter for MockAdapter {
Expand Down Expand Up @@ -217,5 +218,6 @@ fn exercise_full_api() {
let _: usize = mbc.fence_length;
let _: usize = mbc.fence_offset;
}
nodes::NodeValue::Escaped => {}
}
}
21 changes: 21 additions & 0 deletions src/tests/escaped_char_spans.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use super::*;
use ntest::test_case;

// html_opts! does a roundtrip check unless sourcepos is set.
// These cases don't work roundtrip, because converting to commonmark
// automatically escapes certain characters.
#[test_case("\\@user", "<p data-sourcepos=\"1:1-1:6\"><span data-escaped-char data-sourcepos=\"1:1-1:2\">@</span>user</p>\n")]
#[test_case("This\\@that", "<p data-sourcepos=\"1:1-1:10\">This<span data-escaped-char data-sourcepos=\"1:5-1:6\">@</span>that</p>\n")]
fn escaped_char_spans(markdown: &str, html: &str) {
html_opts!(
[render.escaped_char_spans, render.sourcepos],
markdown,
html
);
}

#[test_case("\\@user", "<p>@user</p>\n")]
#[test_case("This\\@that", "<p>This@that</p>\n")]
fn disabled_escaped_char_spans(markdown: &str, expected: &str) {
html(markdown, expected);
}
1 change: 1 addition & 0 deletions src/tests/propfuzz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ fn propfuzz_doesnt_crash(md: String) {
escape: false,
list_style: ListStyleType::Dash,
sourcepos: true,
escaped_char_spans: true,
},
};

Expand Down
3 changes: 3 additions & 0 deletions src/xml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,9 @@ impl<'o> XmlFormatter<'o> {
self.escape(nsc.shortcode().as_bytes())?;
self.output.write_all(b"\"")?;
}
NodeValue::Escaped => {
// noop
}
}

if node.first_child().is_some() {
Expand Down