Repository: pulldown-cmark/pulldown-cmark (rust)

Issue:
Superscript and subscript parsing inconsistent with Pandoc and markdown-it
`pulldown-cmark` requires whitespace around superscripted and subscripted text whereas [Pandoc](https://pandoc.org/try/?params=%7B%22text%22%3A%22H%7E2%7E0%22%2C%22to%22%3A%22html5%22%2C%22from%22%3A%22commonmark_x%22%2C%22standalone%22%3Afalse%2C%22embed-resources%22%3Afalse%2C%22table-of-contents%22%3Afalse%2C%22number-sections%22%3Afalse%2C%22citeproc%22%3Afalse%2C%22html-math-method%22%3A%22plain%22%2C%22wrap%22%3A%22auto%22%2C%22highlight-style%22%3Anull%2C%22files%22%3A%7B%7D%2C%22template%22%3Anull%7D) and [markdown-it](https://markdown-it.github.io/#md3=%7B%22source%22%3A%22H~2~0%22%2C%22defaults%22%3A%7B%22html%22%3Afalse%2C%22xhtmlOut%22%3Afalse%2C%22breaks%22%3Afalse%2C%22langPrefix%22%3A%22language-%22%2C%22linkify%22%3Atrue%2C%22typographer%22%3Atrue%2C%22_highlight%22%3Atrue%2C%22_strict%22%3Afalse%2C%22_view%22%3A%22html%22%7D%7D) do not.

```shell
$ echo 'H~2~0' | cargo r --quiet -- --enable-subscript -e
0..6: Start(Paragraph)
0..5: Text(Borrowed("H~2~0"))
0..6: End(Paragraph)
EOF
```

```shell
$ echo 'H ~2~ 0' | cargo r --quiet -- --enable-subscript -e
0..8: Start(Paragraph)
0..2: Text(Borrowed("H "))
2..5: Start(Subscript)
3..4: Text(Borrowed("2"))
2..5: End(Subscript)
5..7: Text(Borrowed(" 0"))
0..8: End(Paragraph)
EOF
```

Pull request description:
Make flanking behavior on `~` and `^` act closer to pandoc
Fixes #1022

Patch:
diff --git a/pulldown-cmark/specs/super_sub.txt b/pulldown-cmark/specs/super_sub.txt
index f60da2e..9cd7b9f 100644
--- a/pulldown-cmark/specs/super_sub.txt
+++ b/pulldown-cmark/specs/super_sub.txt
@@ -25,13 +25,19 @@ Backslash escapes:
 ```````````````````````````````` example_super_sub
 ~This~is~nothing~
 .
-<p><sub>This~is~nothing</sub></p>
+<p><sub>This</sub>is<sub>nothing</sub></p>
 ````````````````````````````````
 
 ```````````````````````````````` example_super_sub
-~This ~~is stricken.~
+~This ~~is not stricken.~
 .
-<p><sub>This ~~is stricken.</sub></p>
+<p><sub>This ~~is not stricken.</sub></p>
+````````````````````````````````
+
+```````````````````````````````` example_super_sub
+~~This ~is~~ stricken.~
+.
+<p><del>This ~is</del> stricken.~</p>
 ````````````````````````````````
 
 The first one wins.
@@ -41,3 +47,15 @@ The first one wins.
 .
 <p><sub>This ~~is stricken</sub> but this is not~~</p>
 ````````````````````````````````
+
+Though strikethrough requires left and right flanking, subscript does not.
+Neither does superscript.
+
+```````````````````````````````` example_super_sub
+H~2~O
+
+y=x^2^a+xb+c
+.
+<p>H<sub>2</sub>O</p>
+<p>y=x<sup>2</sup>a+xb+c</p>
+````````````````````````````````
diff --git a/pulldown-cmark/src/firstpass.rs b/pulldown-cmark/src/firstpass.rs
index 8994a27..31eae05 100644
--- a/pulldown-cmark/src/firstpass.rs
+++ b/pulldown-cmark/src/firstpass.rs
@@ -925,6 +925,7 @@ impl<'a, 'b> FirstPass<'a, 'b> {
                         count,
                         ix - start,
                         mode,
+                        self.options,
                     );
                     let can_close = delim_run_can_close(
                         &self.text[start..],
@@ -932,6 +933,7 @@ impl<'a, 'b> FirstPass<'a, 'b> {
                         count,
                         ix - start,
                         mode,
+                        self.options,
                     );
                     let is_valid_seq = (c != b'~' || count <= 2) || (c == b'~' && count == 2);
 
@@ -1176,14 +1178,21 @@ impl<'a, 'b> FirstPass<'a, 'b> {
                 }
                 c @ b'\'' | c @ b'"' => {
                     let string_suffix = &self.text[ix..];
-                    let can_open =
-                        delim_run_can_open(&self.text[start..], string_suffix, 1, ix - start, mode);
+                    let can_open = delim_run_can_open(
+                        &self.text[start..],
+                        string_suffix,
+                        1,
+                        ix - start,
+                        mode,
+                        self.options,
+                    );
                     let can_close = delim_run_can_close(
                         &self.text[start..],
                         string_suffix,
                         1,
                         ix - start,
                         mode,
+                        self.options,
                     );
 
                     self.tree.append_text(begin_text, ix, backslash_escaped);
@@ -2258,6 +2267,7 @@ fn delim_run_can_open(
     run_len: usize,
     ix: usize,
     mode: TableParseMode,
+    options: Options,
 ) -> bool {
     let next_char = if let Some(c) = suffix[run_len..].chars().next() {
         c
@@ -2279,15 +2289,18 @@ fn delim_run_can_open(
         }
     }
     let delim = suffix.bytes().next().unwrap();
-    // `*` and `~~` can be intraword, `_` and `~` cannot
-    if delim == b'*' && !is_punctuation(next_char) {
+    // `*`, `~~`, and `^` can be intraword, `~` can only be interword if it's subscript, `_` cannot
+    if (delim == b'*' || delim == b'^') && !is_punctuation(next_char) {
         return true;
     }
     if delim == b'~' && run_len > 1 {
         return true;
     }
     let prev_char = s[..ix].chars().last().unwrap();
-    if delim == b'~' && prev_char == '~' && !is_punctuation(next_char) {
+    if delim == b'~'
+        && (prev_char == '~' || options.contains(Options::ENABLE_SUBSCRIPT))
+        && !is_punctuation(next_char)
+    {
         return true;
     }
 
@@ -2304,6 +2317,7 @@ fn delim_run_can_close(
     run_len: usize,
     ix: usize,
     mode: TableParseMode,
+    options: Options,
 ) -> bool {
     if ix == 0 {
         return false;
@@ -2326,11 +2340,13 @@ fn delim_run_can_close(
         }
     }
     let delim = suffix.bytes().next().unwrap();
-    // `*` and `~~` can be intraword, `_` and `~` cannot
-    if (delim == b'*' || (delim == b'~' && run_len > 1)) && !is_punctuation(prev_char) {
+    // `*`, `~~`, and `^` can be intraword, `~` can only be interword if it's subscript, `_` cannot
+    if (delim == b'*' || delim == b'^' || (delim == b'~' && run_len > 1))
+        && !is_punctuation(prev_char)
+    {
         return true;
     }
-    if delim == b'~' && prev_char == '~' {
+    if delim == b'~' && (prev_char == '~' || options.contains(Options::ENABLE_SUBSCRIPT)) {
         return true;
     }
 


Test patch:
diff --git a/pulldown-cmark/tests/suite/super_sub.rs b/pulldown-cmark/tests/suite/super_sub.rs
index a9c0c84..6c2492b 100644
--- a/pulldown-cmark/tests/suite/super_sub.rs
+++ b/pulldown-cmark/tests/suite/super_sub.rs
@@ -37,7 +37,7 @@ fn super_sub_test_3() {
 fn super_sub_test_4() {
     let original = r##"~This~is~nothing~
 "##;
-    let expected = r##"<p><sub>This~is~nothing</sub></p>
+    let expected = r##"<p><sub>This</sub>is<sub>nothing</sub></p>
 "##;
 
     test_markdown_html(original, expected, false, false, false, true, false, false);
@@ -45,9 +45,9 @@ fn super_sub_test_4() {
 
 #[test]
 fn super_sub_test_5() {
-    let original = r##"~This ~~is stricken.~
+    let original = r##"~This ~~is not stricken.~
 "##;
-    let expected = r##"<p><sub>This ~~is stricken.</sub></p>
+    let expected = r##"<p><sub>This ~~is not stricken.</sub></p>
 "##;
 
     test_markdown_html(original, expected, false, false, false, true, false, false);
@@ -55,6 +55,16 @@ fn super_sub_test_5() {
 
 #[test]
 fn super_sub_test_6() {
+    let original = r##"~~This ~is~~ stricken.~
+"##;
+    let expected = r##"<p><del>This ~is</del> stricken.~</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false, true, false, false);
+}
+
+#[test]
+fn super_sub_test_7() {
     let original = r##"~This ~~is stricken~ but this is not~~
 "##;
     let expected = r##"<p><sub>This ~~is stricken</sub> but this is not~~</p>
@@ -62,3 +72,16 @@ fn super_sub_test_6() {
 
     test_markdown_html(original, expected, false, false, false, true, false, false);
 }
+
+#[test]
+fn super_sub_test_8() {
+    let original = r##"H~2~O
+
+y=x^2^a+xb+c
+"##;
+    let expected = r##"<p>H<sub>2</sub>O</p>
+<p>y=x<sup>2</sup>a+xb+c</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false, true, false, false);
+}