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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
target
comrak-*
.vscode
.idea
21 changes: 14 additions & 7 deletions src/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -944,7 +944,9 @@ impl<'o> HtmlFormatter<'o> {
self.footnote_ix += 1;
self.output.write_all(b"<li")?;
self.render_sourcepos(node)?;
writeln!(self.output, " id=\"fn-{}\">", nfd.name)?;
self.output.write_all(b" id=\"fn-")?;
self.escape_href(nfd.name.as_bytes())?;
self.output.write_all(b"\">")?;
} else {
if self.put_footnote_backref(nfd)? {
self.output.write_all(b"\n")?;
Expand All @@ -962,10 +964,13 @@ impl<'o> HtmlFormatter<'o> {
if nfr.ref_num > 1 {
ref_id = format!("{}-{}", ref_id, nfr.ref_num);
}
write!(
self.output, " class=\"footnote-ref\"><a href=\"#fn-{}\" id=\"{}\" data-footnote-ref>{}</a></sup>",
nfr.name, ref_id, nfr.ix,
)?;

self.output
.write_all(b" class=\"footnote-ref\"><a href=\"#fn-")?;
self.escape_href(nfr.name.as_bytes())?;
self.output.write_all(b"\" id=\"")?;
self.escape_href(ref_id.as_bytes())?;
write!(self.output, "\" data-footnote-ref>{}</a></sup>", nfr.ix)?;
}
}
NodeValue::TaskItem(symbol) => {
Expand Down Expand Up @@ -1018,10 +1023,12 @@ impl<'o> HtmlFormatter<'o> {
write!(self.output, " ")?;
}

self.output.write_all(b"<a href=\"#fnref-")?;
self.escape_href(nfd.name.as_bytes())?;
write!(
self.output,
"<a href=\"#fnref-{}{}\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"{}{}\" aria-label=\"Back to reference {}{}\">↩{}</a>",
nfd.name, ref_suffix, self.footnote_ix, ref_suffix, self.footnote_ix, ref_suffix, superscript
"{}\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"{}{}\" aria-label=\"Back to reference {}{}\">↩{}</a>",
ref_suffix, self.footnote_ix, ref_suffix, self.footnote_ix, ref_suffix, superscript
)?;
}
Ok(true)
Expand Down
2 changes: 1 addition & 1 deletion src/parser/inlines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1197,7 +1197,7 @@ impl<'a, 'r, 'o, 'd, 'i, 'c, 'subj> Subject<'a, 'r, 'o, 'd, 'i, 'c, 'subj> {
}

// Need to normalize both to lookup in refmap and to call callback
let lab = strings::normalize_label(&lab);
let lab = strings::normalize_label(&lab, false);
let mut reff = if found_label {
self.refmap.lookup(&lab)
} else {
Expand Down
10 changes: 6 additions & 4 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1785,11 +1785,11 @@ impl<'a, 'o, 'c> Parser<'a, 'o, 'c> {
NodeValue::FootnoteDefinition(ref nfd) => {
node.detach();
map.insert(
strings::normalize_label(&nfd.name),
strings::normalize_label(&nfd.name, false),
FootnoteDefinition {
ix: None,
node,
name: strings::normalize_label(&nfd.name),
name: strings::normalize_label(&nfd.name, true),
total_references: 0,
},
);
Expand All @@ -1811,7 +1811,8 @@ impl<'a, 'o, 'c> Parser<'a, 'o, 'c> {
let mut replace = None;
match ast.value {
NodeValue::FootnoteReference(ref mut nfr) => {
if let Some(ref mut footnote) = map.get_mut(&nfr.name) {
let normalized = strings::normalize_label(&nfr.name, false);
if let Some(ref mut footnote) = map.get_mut(&normalized) {
let ix = match footnote.ix {
Some(ix) => ix,
None => {
Expand All @@ -1823,6 +1824,7 @@ impl<'a, 'o, 'c> Parser<'a, 'o, 'c> {
footnote.total_references += 1;
nfr.ref_num = footnote.total_references;
nfr.ix = ix;
nfr.name = strings::normalize_label(&footnote.name, true);
} else {
replace = Some(nfr.name.clone());
}
Expand Down Expand Up @@ -2023,7 +2025,7 @@ impl<'a, 'o, 'c> Parser<'a, 'o, 'c> {
}
}

lab = strings::normalize_label(&lab);
lab = strings::normalize_label(&lab, false);
if !lab.is_empty() {
subj.refmap.map.entry(lab).or_insert(Reference {
url: String::from_utf8(strings::clean_url(url)).unwrap(),
Expand Down
34 changes: 24 additions & 10 deletions src/strings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,23 +237,25 @@ pub fn is_blank(s: &[u8]) -> bool {
true
}

pub fn normalize_label(i: &str) -> String {
pub fn normalize_label(i: &str, preserve_case: bool) -> String {
// trim_slice only removes bytes from start and end that match isspace();
// result is UTF-8.
let i = unsafe { str::from_utf8_unchecked(trim_slice(i.as_bytes())) };

let mut v = String::with_capacity(i.len());
let mut last_was_whitespace = false;
for c in i.chars() {
for e in c.to_lowercase() {
if e.is_whitespace() {
if !last_was_whitespace {
last_was_whitespace = true;
v.push(' ');
}
if c.is_whitespace() {
if !last_was_whitespace {
last_was_whitespace = true;
v.push(' ');
}
} else {
last_was_whitespace = false;
if preserve_case {
v.push(c);
} else {
last_was_whitespace = false;
v.push(e);
v.push_str(&c.to_lowercase().to_string());
}
}
}
Expand Down Expand Up @@ -308,7 +310,7 @@ pub fn trim_start_match<'s>(s: &'s str, pat: &str) -> &'s str {

#[cfg(test)]
pub mod tests {
use super::{normalize_code, split_off_front_matter};
use super::{normalize_code, normalize_label, split_off_front_matter};

#[test]
fn normalize_code_handles_lone_newline() {
Expand Down Expand Up @@ -341,4 +343,16 @@ pub mod tests {
Some(("!@#\r\n\r\nfoo: \n!@# \r\nquux\n!@#\r\n\n", "\nYes!\n"))
);
}

#[test]
fn normalize_label_lowercase() {
assert_eq!(normalize_label(" Foo\u{A0}BAR ", false), "foo bar");
assert_eq!(normalize_label(" FooİBAR ", false), "fooi\u{307}bar");
}

#[test]
fn normalize_label_preserve() {
assert_eq!(normalize_label(" Foo\u{A0}BAR ", true), "Foo BAR");
assert_eq!(normalize_label(" FooİBAR ", true), "FooİBAR");
}
}
44 changes: 44 additions & 0 deletions src/tests/footnotes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,50 @@ fn footnote_with_superscript() {
);
}

#[test]
fn footnote_escapes_name() {
html_opts!(
[extension.footnotes],
concat!(
"Here is a footnote reference.[^😄ref]\n",
"\n",
"[^😄ref]: Here is the footnote.\n",
),
concat!(
"<p>Here is a footnote reference.<sup class=\"footnote-ref\"><a href=\"#fn-%F0%9F%98%84ref\" id=\"fnref-%F0%9F%98%84ref\" data-footnote-ref>1</a></sup></p>\n",
"<section class=\"footnotes\" data-footnotes>\n",
"<ol>\n",
"<li id=\"fn-%F0%9F%98%84ref\">\n",
"<p>Here is the footnote. <a href=\"#fnref-%F0%9F%98%84ref\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"1\" aria-label=\"Back to reference 1\">↩</a></p>\n",
"</li>\n",
"</ol>\n",
"</section>\n"
),
);
}

#[test]
fn footnote_case_insensitive_and_case_preserving() {
html_opts!(
[extension.footnotes],
concat!(
"Here is a footnote reference.[^AB] and [^ab]\n",
"\n",
"[^aB]: Here is the footnote.\n",
),
concat!(
"<p>Here is a footnote reference.<sup class=\"footnote-ref\"><a href=\"#fn-aB\" id=\"fnref-aB\" data-footnote-ref>1</a></sup> and <sup class=\"footnote-ref\"><a href=\"#fn-aB\" id=\"fnref-aB-2\" data-footnote-ref>1</a></sup></p>\n",
"<section class=\"footnotes\" data-footnotes>\n",
"<ol>\n",
"<li id=\"fn-aB\">\n",
"<p>Here is the footnote. <a href=\"#fnref-aB\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"1\" aria-label=\"Back to reference 1\">↩</a> <a href=\"#fnref-aB-2\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"1-2\" aria-label=\"Back to reference 1-2\">↩<sup class=\"footnote-ref\">2</sup></a></p>\n",
"</li>\n",
"</ol>\n",
"</section>\n"
),
);
}

#[test]
fn sourcepos() {
assert_ast_match!(
Expand Down