Repository: raphlinus/pulldown-cmark (rust)

Issue:
[fuzz result]: footnote definitions are allowed to contain line breaks
[^foo\
\
]: this should not be a footnote, but pulldown-cmark thinks it is

GitHub doesn't allow that, and [neither does markdown-it](https://markdown-it.github.io/#md3=%7B%22source%22%3A%22%5B%5Efoo%5Cn%5Cn%5D%3A%20not%20a%20footnote%5Cn%5Cn%5B%5Ebar%5C%5C%5Cn%5C%5C%5Cn%5D%3A%20a%20f%5Cn%5Cn%5B%5Ebaz%20%5Cn%20%20%20%20%5Cn%5D%3A%20not%20a%20footnote%5Cn%5Cn%5B%5Equux%5D%3A%20a%20footnote%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). It's allowed for link definitions, but since the new-style footnotes are supposed to act like GitHub, we should follow it.

<details>

This sample doesn't really say much about the bug above, but testing it and a few variants vs GitHub happened after finding a discrepancy with Pandoc.

```rust
"[^^\n\n]:\n" -> [
  Start(Paragraph)
    Text(Boxed("[^^"))
  End(Paragraph)
  Start(Paragraph)
    Text(Boxed("]:"))
  End(Paragraph)
]
```

Events from pandoc:

```rust
"[^^\n\n]:\n" -> [
]
```


Pull request:
Do not accept footnote definitions with line breaks
Fixes #760

This change brings pulldown-cmark into alignment with [markdown-it] and hugo/goldmark. It's close to GitHub, but GitHub [doesn't parse the backslashes as hard line breaks][gist].

It's odd that link reference definitions are allowed to have line breaks and footnote definitions aren't, but it's not worth it to be gratuitously different.

[markdown-it]: https://markdown-it.github.io/#md3=%7B%22source%22%3A%22%5B%5Efoo%5C%5C%5Cnbar%5D%3A%20not%20a%20footnote%20definition%5Cn%5Cn%5Bbaz%5C%5C%5Cnquux%5D%3A%20https%3A%2F%2Frust-lang.org%5Cn%5Cn%5Bfirst%5Cnsecond%5D%3A%20https%3A%2F%2Frust-lang.org%5Cn%5Cn%5B%5Ethird%5Cnfourth%5D%3A%20not%20a%20footnote%20definition%5Cn%5Cn%5Bbaz%5C%5C%5Cnquux%5D%5Cn%5B%5Efoo%5C%5C%5Cnbar%5D%5Cn%5Bfirst%5Cnsecond%5D%5Cn%5B%5Ethird%5Cnfourth%5D%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

[gist]: https://gist.github.com/notriddle/7fd2410a2585123ad0f894b08c58b21f

Patch:
diff --git a/specs/footnotes.txt b/specs/footnotes.txt
index 913c499..510ca6c 100644
--- a/specs/footnotes.txt
+++ b/specs/footnotes.txt
@@ -553,6 +553,58 @@ Test [^] link
 <p>Test <a href="https://rust-lang.org">^</a> link</p>
 ````````````````````````````````
 
+Footnote labels cannot contain line breaks, even where link labels can.
+
+```````````````````````````````` example
+[^foo\
+bar]: not a footnote definition
+
+[baz\
+quux]: https://rust-lang.org
+
+[first
+second]: https://rust-lang.org
+
+[^third
+fourth]: not a footnote definition
+
+[baz\
+quux]
+[^foo\
+bar]
+[first
+second]
+[^third
+fourth]
+.
+<p>[^foo<br>
+bar]: not a footnote definition</p>
+<p>[^third
+fourth]: not a footnote definition</p>
+<p><a href="https://rust-lang.org">baz<br>
+quux</a>
+[^foo<br>
+bar]
+<a href="https://rust-lang.org">first
+second</a>
+[^third
+fourth]</p>
+````````````````````````````````
+
+The above rules imply a few cases where something starts with `^`, but actually
+parses as a link definition.
+
+```````````````````````````````` example
+[^foo
+]: https://rust-lang.org
+
+[^foo
+]
+.
+<p><a href="https://rust-lang.org">^foo
+</a></p>
+````````````````````````````````
+
 Footnote definitions can be indented up to three spaces.
 At four spaces, it becomes a code block.
 
diff --git a/src/firstpass.rs b/src/firstpass.rs
index 600d514..08661f0 100644
--- a/src/firstpass.rs
+++ b/src/firstpass.rs
@@ -1295,7 +1295,21 @@ impl<'a, 'b> FirstPass<'a, 'b> {
         if !bytes.starts_with(b"[^") {
             return None;
         }
-        let (mut i, label) = self.parse_refdef_label(start + 2)?;
+        let (mut i, label) = if self.options.has_gfm_footnotes() {
+            // GitHub doesn't allow footnote definition labels to contain line breaks.
+            // It actually does allow this for link definitions under certain circumstances,
+            // but for this it's simpler to avoid it.
+            scan_link_label_rest(&self.text[start + 2..], &|_| {
+                return None;
+            })?
+        } else {
+            self.parse_refdef_label(start + 2)?
+        };
+        if self.options.has_gfm_footnotes() && label.bytes().any(|b| b == b'\r' || b == b'\n') {
+            // GitHub doesn't allow footnote definition labels to contain line breaks,
+            // even if they're escaped.
+            return None;
+        }
         i += 2;
         if scan_ch(&bytes[i..], b':') == 0 {
             return None;
diff --git a/src/parse.rs b/src/parse.rs
index c3ce914..fe73879 100644
--- a/src/parse.rs
+++ b/src/parse.rs
@@ -1206,14 +1206,18 @@ fn scan_link_label<'text, 'tree>(
         let _ = scan_containers(tree, &mut line_start, gfm_footnotes);
         Some(line_start.bytes_scanned())
     };
-    let pair = if allow_footnote_refs && b'^' == bytes[1] && bytes.get(2) != Some(&b']') {
-        let (byte_index, cow) = scan_link_label_rest(&text[2..], &linebreak_handler)?;
-        (byte_index + 2, ReferenceLabel::Footnote(cow))
-    } else {
-        let (byte_index, cow) = scan_link_label_rest(&text[1..], &linebreak_handler)?;
-        (byte_index + 1, ReferenceLabel::Link(cow))
-    };
-    Some(pair)
+    if allow_footnote_refs && b'^' == bytes[1] && bytes.get(2) != Some(&b']') {
+        let linebreak_handler: &dyn Fn(&[u8]) -> Option<usize> = if gfm_footnotes {
+            &|_|  None
+        } else {
+            &linebreak_handler
+        };
+        if let Some((byte_index, cow)) = scan_link_label_rest(&text[2..], linebreak_handler) {
+            return Some((byte_index + 2, ReferenceLabel::Footnote(cow)));
+        }
+    }
+    let (byte_index, cow) = scan_link_label_rest(&text[1..], &linebreak_handler)?;
+    Some((byte_index + 1, ReferenceLabel::Link(cow)))
 }
 
 fn scan_reference<'a, 'b>(


Test patch:
diff --git a/tests/suite/footnotes.rs b/tests/suite/footnotes.rs
index fb2d924..e00f9bb 100644
--- a/tests/suite/footnotes.rs
+++ b/tests/suite/footnotes.rs
@@ -548,6 +548,61 @@ fn footnotes_test_20() {
 
 #[test]
 fn footnotes_test_21() {
+    let original = r##"[^foo\
+bar]: not a footnote definition
+
+[baz\
+quux]: https://rust-lang.org
+
+[first
+second]: https://rust-lang.org
+
+[^third
+fourth]: not a footnote definition
+
+[baz\
+quux]
+[^foo\
+bar]
+[first
+second]
+[^third
+fourth]
+"##;
+    let expected = r##"<p>[^foo<br>
+bar]: not a footnote definition</p>
+<p>[^third
+fourth]: not a footnote definition</p>
+<p><a href="https://rust-lang.org">baz<br>
+quux</a>
+[^foo<br>
+bar]
+<a href="https://rust-lang.org">first
+second</a>
+[^third
+fourth]</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn footnotes_test_22() {
+    let original = r##"[^foo
+]: https://rust-lang.org
+
+[^foo
+]
+"##;
+    let expected = r##"<p><a href="https://rust-lang.org">^foo
+</a></p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn footnotes_test_23() {
     let original = r##"footnote [^baz]
 footnote [^quux]