diff --git a/src/cm.rs b/src/cm.rs index 4f362588..aa2989e1 100644 --- a/src/cm.rs +++ b/src/cm.rs @@ -397,6 +397,7 @@ impl<'a, 'o> CommonMarkFormatter<'a, 'o> { NodeValue::Math(ref math) => self.format_math(math, allow_wrap, entering), NodeValue::WikiLink(ref nl) => return self.format_wikilink(nl, entering), NodeValue::Underline => self.format_underline(), + NodeValue::Subscript => self.format_subscript(), NodeValue::SpoileredText => self.format_spoiler(), NodeValue::EscapedTag(ref net) => self.format_escaped_tag(net), }; @@ -712,7 +713,7 @@ impl<'a, 'o> CommonMarkFormatter<'a, 'o> { } fn format_strikethrough(&mut self) { - write!(self, "~").unwrap(); + write!(self, "~~").unwrap(); } fn format_superscript(&mut self) { @@ -723,6 +724,10 @@ impl<'a, 'o> CommonMarkFormatter<'a, 'o> { write!(self, "__").unwrap(); } + fn format_subscript(&mut self) { + write!(self, "~").unwrap(); + } + fn format_spoiler(&mut self) { write!(self, "||").unwrap(); } diff --git a/src/html.rs b/src/html.rs index c82abe6f..72fd1f0d 100644 --- a/src/html.rs +++ b/src/html.rs @@ -1139,6 +1139,18 @@ impl<'o> HtmlFormatter<'o> { self.output.write_all(b"")?; } } + NodeValue::Subscript => { + // Unreliable sourcepos. + if entering { + self.output.write_all(b"")?; + } else { + self.output.write_all(b"")?; + } + } NodeValue::SpoileredText => { // Unreliable sourcepos. if entering { diff --git a/src/main.rs b/src/main.rs index ac59dab3..f9bd64ad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -184,6 +184,7 @@ enum Extension { WikilinksTitleAfterPipe, WikilinksTitleBeforePipe, Underline, + Subscript, Spoiler, Greentext, } @@ -267,6 +268,7 @@ fn main() -> Result<(), Box> { .wikilinks_title_after_pipe(exts.contains(&Extension::WikilinksTitleAfterPipe)) .wikilinks_title_before_pipe(exts.contains(&Extension::WikilinksTitleBeforePipe)) .underline(exts.contains(&Extension::Underline)) + .subscript(exts.contains(&Extension::Subscript)) .spoiler(exts.contains(&Extension::Spoiler)) .greentext(exts.contains(&Extension::Greentext)) .maybe_front_matter_delimiter(cli.front_matter_delimiter); diff --git a/src/nodes.rs b/src/nodes.rs index 2051badd..ec6e549c 100644 --- a/src/nodes.rs +++ b/src/nodes.rs @@ -189,6 +189,9 @@ pub enum NodeValue { /// **Inline**. Underline. Enabled with `underline` option. Underline, + /// **Inline**. Subscript. Enabled with `subscript` options. + Subscript, + /// **Inline**. Spoilered text. Enabled with `spoiler` option. SpoileredText, @@ -514,6 +517,7 @@ impl NodeValue { NodeValue::Math(..) => "math", NodeValue::WikiLink(..) => "wikilink", NodeValue::Underline => "underline", + NodeValue::Subscript => "subscript", NodeValue::SpoileredText => "spoiler", NodeValue::EscapedTag(_) => "escaped_tag", } @@ -764,6 +768,7 @@ pub fn can_contain_type<'a>(node: &'a AstNode<'a>, child: &NodeValue) -> bool { | NodeValue::Superscript | NodeValue::SpoileredText | NodeValue::Underline + | NodeValue::Subscript // XXX: this is quite a hack: the EscapedTag _contains_ whatever was // possibly going to fall into the spoiler. This should be fixed in // inlines. @@ -791,6 +796,7 @@ pub fn can_contain_type<'a>(node: &'a AstNode<'a>, child: &NodeValue) -> bool { | NodeValue::Superscript | NodeValue::SpoileredText | NodeValue::Underline + | NodeValue::Subscript ), #[cfg(feature = "shortcodes")] @@ -810,6 +816,7 @@ pub fn can_contain_type<'a>(node: &'a AstNode<'a>, child: &NodeValue) -> bool { | NodeValue::Superscript | NodeValue::SpoileredText | NodeValue::Underline + | NodeValue::Subscript | NodeValue::ShortCode(..) ), diff --git a/src/parser/inlines.rs b/src/parser/inlines.rs index 5601cfb7..34e97405 100644 --- a/src/parser/inlines.rs +++ b/src/parser/inlines.rs @@ -149,7 +149,7 @@ impl<'a, 'r, 'o, 'd, 'i> Subject<'a, 'r, 'o, 'd, 'i> { s.special_chars[b':' as usize] = true; s.special_chars[b'w' as usize] = true; } - if options.extension.strikethrough { + if options.extension.strikethrough || options.extension.subscript { s.special_chars[b'~' as usize] = true; s.skip_chars[b'~' as usize] = true; } @@ -281,7 +281,9 @@ impl<'a, 'r, 'o, 'd, 'i> Subject<'a, 'r, 'o, 'd, 'i> { )) } } - '~' if self.options.extension.strikethrough => Some(self.handle_delim(b'~')), + '~' if self.options.extension.strikethrough || self.options.extension.subscript => { + Some(self.handle_delim(b'~')) + } '^' if self.options.extension.superscript && !self.within_brackets => { Some(self.handle_delim(b'^')) } @@ -333,7 +335,7 @@ impl<'a, 'r, 'o, 'd, 'i> Subject<'a, 'r, 'o, 'd, 'i> { // After parsing a block (and sometimes during), this function traverses the // stack of `Delimiters`, tokens ("*", "_", etc.) that may delimit regions - // of text for special rendering: emphasis, strong, superscript, + // of text for special rendering: emphasis, strong, superscript, subscript, // spoilertext; looking for pairs of opening and closing delimiters, // with the goal of placing the intervening nodes into new emphasis, // etc AST nodes. @@ -461,7 +463,8 @@ impl<'a, 'r, 'o, 'd, 'i> Subject<'a, 'r, 'o, 'd, 'i> { // both get passed. if c.delim_char == b'*' || c.delim_char == b'_' - || (self.options.extension.strikethrough && c.delim_char == b'~') + || ((self.options.extension.strikethrough || self.options.extension.subscript) + && c.delim_char == b'~') || (self.options.extension.superscript && c.delim_char == b'^') || (self.options.extension.spoiler && c.delim_char == b'|') { @@ -1068,7 +1071,7 @@ impl<'a, 'r, 'o, 'd, 'i> Subject<'a, 'r, 'o, 'd, 'i> { opener_num_chars -= use_delims; closer_num_chars -= use_delims; - if self.options.extension.strikethrough + if (self.options.extension.strikethrough || self.options.extension.subscript) && opener_char == b'~' && (opener_num_chars != closer_num_chars || opener_num_chars > 0) { @@ -1101,8 +1104,19 @@ impl<'a, 'r, 'o, 'd, 'i> Subject<'a, 'r, 'o, 'd, 'i> { } let emph = self.make_inline( - if self.options.extension.strikethrough && opener_char == b'~' { - NodeValue::Strikethrough + if self.options.extension.subscript && opener_char == b'~' && use_delims == 1 { + NodeValue::Subscript + } else if opener_char == b'~' { + // Not emphasis + // Unlike for |, these cases have to be handled because they will match + // in the event subscript but not strikethrough is enabled + if self.options.extension.strikethrough { + NodeValue::Strikethrough + } else if use_delims == 1 { + NodeValue::EscapedTag("~".to_owned()) + } else { + NodeValue::EscapedTag("~~".to_owned()) + } } else if self.options.extension.superscript && opener_char == b'^' { NodeValue::Superscript } else if self.options.extension.spoiler && opener_char == b'|' { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5b13fc42..e08fdc01 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -515,6 +515,26 @@ pub struct ExtensionOptions { #[builder(default)] pub underline: bool, + /// Enables subscript text using single tildes. + /// + /// If the strikethrough option is also enabled, this overrides the single + /// tilde case to output subscript text. + /// + /// ```md + /// H~2~O + /// ``` + /// + /// ``` + /// # use comrak::{markdown_to_html, Options}; + /// let mut options = Options::default(); + /// options.extension.subscript = true; + /// + /// assert_eq!(markdown_to_html("H~2~O", &options), + /// "

H2O

\n"); + /// ``` + #[builder(default)] + pub subscript: bool, + /// Enables spoilers using double vertical bars /// /// ```md diff --git a/src/tests.rs b/src/tests.rs index 8d8fc965..575310be 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -25,6 +25,7 @@ mod rewriter; mod shortcodes; mod spoiler; mod strikethrough; +mod subscript; mod superscript; mod table; mod tagfilter; diff --git a/src/tests/api.rs b/src/tests/api.rs index 07808209..cfd0c785 100644 --- a/src/tests/api.rs +++ b/src/tests/api.rs @@ -72,6 +72,7 @@ fn exercise_full_api() { .wikilinks_title_after_pipe(true) .wikilinks_title_before_pipe(true) .underline(true) + .subscript(true) .spoiler(true) .greentext(true); @@ -271,6 +272,7 @@ fn exercise_full_api() { let _: String = nl.url; } nodes::NodeValue::Underline => {} + nodes::NodeValue::Subscript => {} nodes::NodeValue::SpoileredText => {} nodes::NodeValue::EscapedTag(data) => { let _: &String = data; diff --git a/src/tests/subscript.rs b/src/tests/subscript.rs new file mode 100644 index 00000000..fb3ebb8a --- /dev/null +++ b/src/tests/subscript.rs @@ -0,0 +1,28 @@ +use super::*; + +#[test] +fn subscript() { + html_opts!( + [extension.subscript], + concat!("H~2~O\n"), + concat!("

H2O

\n"), + ); +} + +#[test] +fn strikethrough_and_subscript() { + html_opts!( + [extension.subscript, extension.strikethrough], + concat!("~~H~2~O~~\n"), + concat!("

H2O

\n"), + ); +} + +#[test] +fn no_strikethrough_when_only_subscript() { + html_opts!( + [extension.subscript], + concat!("~~H~2~O~~\n"), + concat!("

~~H2O~~

\n"), + ); +} diff --git a/src/xml.rs b/src/xml.rs index 19721d2e..0dceabf3 100644 --- a/src/xml.rs +++ b/src/xml.rs @@ -285,6 +285,7 @@ impl<'o> XmlFormatter<'o> { self.output.write_all(b"\"")?; } NodeValue::Underline => {} + NodeValue::Subscript => {} NodeValue::SpoileredText => {} NodeValue::EscapedTag(ref data) => { self.output.write_all(data.as_bytes())?;