Repository: raphlinus/pulldown-cmark (rust)

Issue:
`<@1>` parsed as inline link
Events from pulldown-cmark:

```rust
"<@1>" -> [
  Start(Paragraph)
    Start(Link { link_type: Inline, dest_url: Borrowed("@1"), title: Borrowed(""), id: Borrowed("") })
      Text(Borrowed("@1"))
    End(Link)
  End(Paragraph)
]
```

Events from commonmark.js:

```rust
"<@1>" -> [
  Start(Paragraph)
    Text(Boxed("<@1>"))
  End(Paragraph)
]
```

XML from [commonmark.js](https://spec.commonmark.org/dingus/?text=%3C%401%3E):

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE document SYSTEM "CommonMark.dtd">
<document xmlns="http://commonmark.org/xml/1.0">
  <paragraph>
    <text>&lt;</text>
    <text>@1&gt;</text>
  </paragraph>
</document>
```

I looked at the [section on autolinks in the spec](https://spec.commonmark.org/0.30/#autolinks) and the problem seems to be that `scan_email` accepts `@1` as a valid email address. However, the spec says

> An [email address](https://spec.commonmark.org/0.30/#email-address), for these purposes, is anything that matches the [non-normative regex from the HTML5 spec](https://html.spec.whatwg.org/multipage/forms.html#e-mail-state-(type=email)):
>
>     /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?
>     (?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/

There is a comment at the start of `scan_line` saying that the text is scanned by hand to avoid using a regex library. I suppose we could rewrite the code there to parse the regex more faithfully, or perhaps we could pull in a regex library?

Pull request description:
Fix incorrect scan of bad email address
The old scanner mistakenly accepted an email address with an empty label before the @ symbol.

This commit also adds some hand-written tests. I also ran a bit of property testing against the regex.

Also renumbers regression tests that were incorrectly numbered (probably due to merge conflict).

Fixes #687

Patch:
diff --git a/specs/regression.txt b/specs/regression.txt
index d5b6d9f..bec7df7 100644
--- a/specs/regression.txt
+++ b/specs/regression.txt
@@ -1305,3 +1305,11 @@ The below example adds a trailing space.
 %</p>
 ````````````````````````````````
 
+
+ISSUE 687
+
+```````````````````````````````` example
+<@1>
+.
+<p>&lt;@1&gt;</p>
+````````````````````````````````
diff --git a/src/scanners.rs b/src/scanners.rs
index 2284d33..6532bc7 100644
--- a/src/scanners.rs
+++ b/src/scanners.rs
@@ -1259,7 +1259,7 @@ fn scan_email(text: &str, start_ix: usize) -> Option<(usize, CowStr<'_>)> {
             c if is_ascii_alphanumeric(c) => (),
             b'.' | b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+' | b'/' | b'=' | b'?'
             | b'^' | b'_' | b'`' | b'{' | b'|' | b'}' | b'~' | b'-' => (),
-            b'@' => break,
+            b'@' if i > 1 => break,
             _ => return None,
         }
     }
@@ -1398,4 +1398,33 @@ mod test {
     fn overflow_by_addition() {
         assert!(scan_listitem(b"1844674407370955161615!").is_none());
     }
+
+    #[test]
+    fn good_emails() {
+        const EMAILS: &[&str] = &[
+            "<a@b.c>",
+            "<a@b>",
+            "<a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-@example.com>",
+            "<a@sixty-three-letters-in-this-identifier-----------------------63>",
+        ];
+        for email in EMAILS {
+            assert!(scan_email(email, 1).is_some());
+        }
+    }
+
+    #[test]
+    fn bad_emails() {
+        const EMAILS: &[&str] = &[
+            "<@b.c>",
+            "<foo@-example.com>",
+            "<foo@example-.com>",
+            "<a@notrailingperiod.>",
+            "<a(noparens)@example.com>",
+            "<\"noquotes\"@example.com>",
+            "<a@sixty-four-letters-in-this-identifier-------------------------64>",
+        ];
+        for email in EMAILS {
+            assert!(scan_email(email, 1).is_none());
+        }
+    }
 }


Test patch:
diff --git a/tests/suite/regression.rs b/tests/suite/regression.rs
index 19121a3..a9ad186 100644
--- a/tests/suite/regression.rs
+++ b/tests/suite/regression.rs
@@ -1523,7 +1523,7 @@ fn regression_test_102() {
 }
 
 #[test]
-fn regression_test_101() {
+fn regression_test_103() {
     let original = r##";
 *
 %
@@ -1537,7 +1537,7 @@ fn regression_test_101() {
 }
 
 #[test]
-fn regression_test_102() {
+fn regression_test_104() {
     let original = r##";
 * 
 %
@@ -1549,3 +1549,13 @@ fn regression_test_102() {
 
     test_markdown_html(original, expected, false, false, false);
 }
+
+#[test]
+fn regression_test_105() {
+    let original = r##"<@1>
+"##;
+    let expected = r##"<p>&lt;@1&gt;</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}