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())?;