From 0071167d21180a34769e524beda6fb2381a45e8a Mon Sep 17 00:00:00 2001 From: digitalMoksha Date: Thu, 25 May 2023 15:41:58 -0500 Subject: [PATCH 1/3] Escape footnote names --- .gitignore | 1 + src/html.rs | 21 ++++++++++++++------- src/tests/footnotes.rs | 22 ++++++++++++++++++++++ 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 4de538ee..cda7c229 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ target comrak-* .vscode +.idea diff --git a/src/html.rs b/src/html.rs index 0aa732bf..91e5016a 100644 --- a/src/html.rs +++ b/src/html.rs @@ -944,7 +944,9 @@ impl<'o> HtmlFormatter<'o> { self.footnote_ix += 1; self.output.write_all(b"", 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")?; @@ -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\">{}", - nfr.name, ref_id, nfr.ix, - )?; + + self.output + .write_all(b" class=\"footnote-ref\">{}", nfr.ix)?; } } NodeValue::TaskItem(symbol) => { @@ -1018,10 +1023,12 @@ impl<'o> HtmlFormatter<'o> { write!(self.output, " ")?; } + self.output.write_all(b"↩{}", - 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 {}{}\">↩{}", + ref_suffix, self.footnote_ix, ref_suffix, self.footnote_ix, ref_suffix, superscript )?; } Ok(true) diff --git a/src/tests/footnotes.rs b/src/tests/footnotes.rs index c16fa530..5d0a3646 100644 --- a/src/tests/footnotes.rs +++ b/src/tests/footnotes.rs @@ -147,6 +147,28 @@ 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!( + "

Here is a footnote reference.1

\n", + "
\n", + "
    \n", + "
  1. \n", + "

    Here is the footnote.

    \n", + "
  2. \n", + "
\n", + "
\n" + ), + ); +} + #[test] fn sourcepos() { assert_ast_match!( From d91c947054d40007d1979b204545f0a7c467ae46 Mon Sep 17 00:00:00 2001 From: digitalMoksha Date: Thu, 25 May 2023 15:44:24 -0500 Subject: [PATCH 2/3] Make footnote names case-insensitive but case-preserving --- src/parser/inlines.rs | 2 +- src/parser/mod.rs | 10 ++++++---- src/strings.rs | 34 ++++++++++++++++++++++++---------- src/tests/footnotes.rs | 22 ++++++++++++++++++++++ 4 files changed, 53 insertions(+), 15 deletions(-) diff --git a/src/parser/inlines.rs b/src/parser/inlines.rs index f56f82c6..3ed109a1 100644 --- a/src/parser/inlines.rs +++ b/src/parser/inlines.rs @@ -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 { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 4b309dd8..74bcc0b2 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -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, }, ); @@ -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 => { @@ -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()); } @@ -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(), diff --git a/src/strings.rs b/src/strings.rs index ec6b9feb..a5a6fb9b 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -237,7 +237,7 @@ 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())) }; @@ -245,15 +245,17 @@ pub fn normalize_label(i: &str) -> String { 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()); } } } @@ -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() { @@ -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"); + } } diff --git a/src/tests/footnotes.rs b/src/tests/footnotes.rs index 5d0a3646..116f95cf 100644 --- a/src/tests/footnotes.rs +++ b/src/tests/footnotes.rs @@ -169,6 +169,28 @@ fn footnote_escapes_name() { ); } +#[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!( + "

Here is a footnote reference.1 and 1

\n", + "
\n", + "
    \n", + "
  1. \n", + "

    Here is the footnote. 2

    \n", + "
  2. \n", + "
\n", + "
\n" + ), + ); +} + #[test] fn sourcepos() { assert_ast_match!( From 5593b7a91d3a07435ecaeda27158dd9b7e3615cd Mon Sep 17 00:00:00 2001 From: Cosmic Horror Date: Mon, 15 May 2023 17:31:10 -0600 Subject: [PATCH 3/3] Add in-doc labels for public facing features --- Cargo.toml | 4 ++++ src/lib.rs | 1 + src/parser/mod.rs | 4 ++-- src/plugins/mod.rs | 1 + 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index eee87153..4910572a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,10 @@ exclude = [ resolver = "2" edition = "2018" +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + [profile.release] lto = true diff --git a/src/lib.rs b/src/lib.rs index 268dfdd6..e4558382 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,6 +59,7 @@ //! # } //! ``` +#![cfg_attr(docsrs, feature(doc_cfg))] #![deny( missing_docs, missing_debug_implementations, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 74bcc0b2..7348464d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -329,8 +329,8 @@ pub struct ComrakExtensionOptions { pub front_matter_delimiter: Option, #[cfg(feature = "shortcodes")] - /// Available if "shortcodes" feature is enabled. Phrases wrapped inside of ':' blocks will be - /// replaced with emojis. + #[cfg_attr(docsrs, doc(cfg(feature = "shortcodes")))] + /// Phrases wrapped inside of ':' blocks will be replaced with emojis. /// /// ``` /// # use comrak::{markdown_to_html, ComrakOptions}; diff --git a/src/plugins/mod.rs b/src/plugins/mod.rs index 2fd02e26..18165982 100644 --- a/src/plugins/mod.rs +++ b/src/plugins/mod.rs @@ -1,4 +1,5 @@ //! Plugins for enhancing the default implementation of comrak can be defined in this module. #[cfg(feature = "syntect")] +#[cfg_attr(docsrs, doc(cfg(feature = "syntect")))] pub mod syntect;