Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
81 changes: 78 additions & 3 deletions src/cm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,9 @@ impl<'a, 'o, 'c, 'w> CommonMarkFormatter<'a, 'o, 'c, 'w> {
NodeValue::HeexBlock(ref nhb) => self.format_heex_block(nhb, entering)?,
NodeValue::ThematicBreak => self.format_thematic_break(entering)?,
NodeValue::Paragraph => self.format_paragraph(entering),
NodeValue::Text(ref literal) => self.format_text(literal, entering, !text_in_cell)?,
NodeValue::Text(ref literal) => {
self.format_text(literal, node, entering, !text_in_cell)?
}
NodeValue::LineBreak => self.format_line_break(entering, next_is_block)?,
NodeValue::SoftBreak => self.format_soft_break(entering)?,
NodeValue::Code(ref code) => self.format_code(&code.literal, entering)?,
Expand Down Expand Up @@ -755,9 +757,67 @@ impl<'a, 'o, 'c, 'w> CommonMarkFormatter<'a, 'o, 'c, 'w> {
}
}

fn format_text(&mut self, literal: &str, entering: bool, wrap: bool) -> fmt::Result {
fn format_text(
&mut self,
literal: &str,
node: Node<'a>,
entering: bool,
wrap: bool,
) -> fmt::Result {
if entering {
self.output(literal, wrap, Escaping::Normal)?;
// Entity encode a leading or trailing whitespace character if this
// is the first or last child of an emphasis node respectively,
// otherwise flankingness rules will cause the emph to not be
// parsed! (Note that both is possible.)
let (leading, trailing) = if node
.parent()
.is_some_and(|n| is_emphasis_delimiter_node(&n.data().value))
{
(
if node.previous_sibling().is_none() {
literal
.chars()
.next()
.and_then(|c| if c.is_whitespace() { Some(c) } else { None })
} else {
None
},
if node.next_sibling().is_none() {
literal
.chars()
.next_back()
.and_then(|c| if c.is_whitespace() { Some(c) } else { None })
} else {
None
},
)
} else {
(None, None)
};

if let Some(leading) = leading {
write!(self, "&#{};", leading as u32)?;

if leading.len_utf8() == literal.len() {
// Deleterious case: the emphasis contains a text node which
// is a single whitespace character. Then,leading and trailing
// are actually the same (only) character, and the string slice
// below will fail.
// We can just stop!
return Ok(());
}
}

self.output(
&literal[leading.map_or(0, char::len_utf8)
..literal.len() - trailing.map_or(0, char::len_utf8)],
wrap,
Escaping::Normal,
)?;

if let Some(trailing) = trailing {
write!(self, "&#{};", trailing as u32)?;
}
}
Ok(())
}
Expand Down Expand Up @@ -1317,3 +1377,18 @@ pub fn escape_link_destination(url: &str) -> String {

result
}

#[inline]
fn is_emphasis_delimiter_node(value: &NodeValue) -> bool {
matches!(
value,
NodeValue::Emph
| NodeValue::Strong
| NodeValue::Strikethrough
| NodeValue::Highlight
| NodeValue::Superscript
| NodeValue::Subscript
| NodeValue::SpoileredText
| NodeValue::Underline
)
}
35 changes: 32 additions & 3 deletions src/tests/commonmark.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use self::nodes::{Ast, LineColumn, ListType, NodeList};
use ntest::test_case;
use pretty_assertions::assert_eq;

use crate::nodes::{Ast, LineColumn, ListType, NodeList};

use super::*;
use ntest::test_case;

#[test]
fn commonmark_removes_redundant_strong() {
Expand Down Expand Up @@ -64,7 +66,7 @@ fn commonmark_math(markdown: &str, cm: &str) {
options.extension.math_dollars = true;
options.extension.math_code = true;

commonmark(markdown, cm, None);
commonmark(markdown, cm, Some(&options));
}

#[test_case("\\(x^2\\) and \\[y^2\\]", "\\(x^2\\) and \\[y^2\\]\n")]
Expand Down Expand Up @@ -181,3 +183,30 @@ fn dont_wrap_table_cell() {
fn ol_marker_wonk() {
commonmark(">9)\r\u{b}", "> 9) \n\n&#11;\n", None);
}

#[test_case("**Hello&#32;**")]
#[test_case("**Hello &#32;**")]
#[test_case("*&#32;Hello*")]
#[test_case("*&#32; Hello*")]
#[test_case("*&#32;Hello&#32;*")]
#[test_case("*&#32;&#32;Hello&#32;&#32;*")]
#[test_case("~~Hello&#32;~~")]
#[test_case("_&#9;_")]
fn entity_roundtrips_fooled_by_whitespace(markdown: &str) {
let arena = Arena::new();
let mut options = Options::default();
if markdown.contains("~") {
options.extension.strikethrough = true;
}
let root = parse_document(&arena, markdown, &options);
let mut original_html = String::new();
html::format_document(root, &options, &mut original_html).unwrap();

let mut roundtripped = String::new();
cm::format_document(root, &options, &mut roundtripped).unwrap();
let roundtripped_root = parse_document(&arena, &roundtripped, &options);
let mut roundtripped_html = String::new();
html::format_document(roundtripped_root, &options, &mut roundtripped_html).unwrap();

assert_eq!(original_html, roundtripped_html);
}
Loading