Repository: pulldown-cmark/pulldown-cmark
Pull request #887 (2024-05-13 11:22:03)

Branch 0.11
The development has been slowed down, so let's release the 0.11 version and leave description lists and #881 for the 0.12 version.


Patch:
diff --git a/pulldown-cmark-escape/src/lib.rs b/pulldown-cmark-escape/src/lib.rs
index 4fc2d51..7ee03ae 100644
--- a/pulldown-cmark-escape/src/lib.rs
+++ b/pulldown-cmark-escape/src/lib.rs
@@ -21,8 +21,8 @@
 //! Utility functions for HTML escaping. Only useful when building your own
 //! HTML renderer.
 
-use std::fmt::{Arguments, Write as FmtWrite};
-use std::io::{self, ErrorKind, Write};
+use std::fmt::{self, Arguments};
+use std::io::{self, Write};
 use std::str::from_utf8;
 
 #[rustfmt::skip]
@@ -46,20 +46,23 @@ static SINGLE_QUOTE_ESCAPE: &str = "&#x27;";
 /// `W: StrWrite`. Since we need the latter a lot, we choose to wrap
 /// `Write` types.
 #[derive(Debug)]
-pub struct WriteWrapper<W>(pub W);
+pub struct IoWriter<W>(pub W);
 
 /// Trait that allows writing string slices. This is basically an extension
 /// of `std::io::Write` in order to include `String`.
 pub trait StrWrite {
-    fn write_str(&mut self, s: &str) -> io::Result<()>;
+    type Error;
 
-    fn write_fmt(&mut self, args: Arguments) -> io::Result<()>;
+    fn write_str(&mut self, s: &str) -> Result<(), Self::Error>;
+    fn write_fmt(&mut self, args: Arguments) -> Result<(), Self::Error>;
 }
 
-impl<W> StrWrite for WriteWrapper<W>
+impl<W> StrWrite for IoWriter<W>
 where
     W: Write,
 {
+    type Error = io::Error;
+
     #[inline]
     fn write_str(&mut self, s: &str) -> io::Result<()> {
         self.0.write_all(s.as_bytes())
@@ -71,17 +74,42 @@ where
     }
 }
 
+/// This wrapper exists because we can't have both a blanket implementation
+/// for all types implementing `io::Write` and types of the form `&mut W` where
+/// `W: StrWrite`. Since we need the latter a lot, we choose to wrap
+/// `Write` types.
+#[derive(Debug)]
+pub struct FmtWriter<W>(pub W);
+
+impl<W> StrWrite for FmtWriter<W>
+where
+    W: fmt::Write,
+{
+    type Error = fmt::Error;
+
+    #[inline]
+    fn write_str(&mut self, s: &str) -> fmt::Result {
+        self.0.write_str(s)
+    }
+
+    #[inline]
+    fn write_fmt(&mut self, args: Arguments) -> fmt::Result {
+        self.0.write_fmt(args)
+    }
+}
+
 impl StrWrite for String {
+    type Error = fmt::Error;
+
     #[inline]
-    fn write_str(&mut self, s: &str) -> io::Result<()> {
+    fn write_str(&mut self, s: &str) -> fmt::Result {
         self.push_str(s);
         Ok(())
     }
 
     #[inline]
-    fn write_fmt(&mut self, args: Arguments) -> io::Result<()> {
-        // FIXME: translate fmt error to io error?
-        FmtWrite::write_fmt(self, args).map_err(|_| ErrorKind::Other.into())
+    fn write_fmt(&mut self, args: Arguments) -> fmt::Result {
+        fmt::Write::write_fmt(self, args)
     }
 }
 
@@ -89,19 +117,21 @@ impl<W> StrWrite for &'_ mut W
 where
     W: StrWrite,
 {
+    type Error = W::Error;
+
     #[inline]
-    fn write_str(&mut self, s: &str) -> io::Result<()> {
+    fn write_str(&mut self, s: &str) -> Result<(), Self::Error> {
         (**self).write_str(s)
     }
 
     #[inline]
-    fn write_fmt(&mut self, args: Arguments) -> io::Result<()> {
+    fn write_fmt(&mut self, args: Arguments) -> Result<(), Self::Error> {
         (**self).write_fmt(args)
     }
 }
 
 /// Writes an href to the buffer, escaping href unsafe bytes.
-pub fn escape_href<W>(mut w: W, s: &str) -> io::Result<()>
+pub fn escape_href<W>(mut w: W, s: &str) -> Result<(), W::Error>
 where
     W: StrWrite,
 {
@@ -171,7 +201,7 @@ static HTML_ESCAPES: [&str; 6] = ["", "&amp;", "&lt;", "&gt;", "&quot;", "&#39;"
 /// // This is not okay.
 /// //let not_ok = format!("<a title={value}>test</a>");
 /// ````
-pub fn escape_html<W: StrWrite>(w: W, s: &str) -> io::Result<()> {
+pub fn escape_html<W: StrWrite>(w: W, s: &str) -> Result<(), W::Error> {
     #[cfg(all(target_arch = "x86_64", feature = "simd"))]
     {
         simd::escape_html(w, s, &HTML_ESCAPE_TABLE)
@@ -200,7 +230,7 @@ pub fn escape_html<W: StrWrite>(w: W, s: &str) -> io::Result<()> {
 /// It should always be correct, but will produce larger output.
 ///
 /// </div>
-pub fn escape_html_body_text<W: StrWrite>(w: W, s: &str) -> io::Result<()> {
+pub fn escape_html_body_text<W: StrWrite>(w: W, s: &str) -> Result<(), W::Error> {
     #[cfg(all(target_arch = "x86_64", feature = "simd"))]
     {
         simd::escape_html(w, s, &HTML_BODY_TEXT_ESCAPE_TABLE)
@@ -211,7 +241,11 @@ pub fn escape_html_body_text<W: StrWrite>(w: W, s: &str) -> io::Result<()> {
     }
 }
 
-fn escape_html_scalar<W: StrWrite>(mut w: W, s: &str, table: &'static [u8; 256]) -> io::Result<()> {
+fn escape_html_scalar<W: StrWrite>(
+    mut w: W,
+    s: &str,
+    table: &'static [u8; 256],
+) -> Result<(), W::Error> {
     let bytes = s.as_bytes();
     let mut mark = 0;
     let mut i = 0;
@@ -237,7 +271,6 @@ fn escape_html_scalar<W: StrWrite>(mut w: W, s: &str, table: &'static [u8; 256])
 mod simd {
     use super::StrWrite;
     use std::arch::x86_64::*;
-    use std::io;
     use std::mem::size_of;
 
     const VECTOR_SIZE: usize = size_of::<__m128i>();
@@ -246,7 +279,7 @@ mod simd {
         mut w: W,
         s: &str,
         table: &'static [u8; 256],
-    ) -> io::Result<()> {
+    ) -> Result<(), W::Error> {
         // The SIMD accelerated code uses the PSHUFB instruction, which is part
         // of the SSSE3 instruction set. Further, we can only use this code if
         // the buffer is at least one VECTOR_SIZE in length to prevent reading
@@ -327,13 +360,13 @@ mod simd {
     /// Make sure to only call this when `bytes.len() >= 16`, undefined behaviour may
     /// occur otherwise.
     #[target_feature(enable = "ssse3")]
-    unsafe fn foreach_special_simd<F>(
+    unsafe fn foreach_special_simd<E, F>(
         bytes: &[u8],
         mut offset: usize,
         mut callback: F,
-    ) -> io::Result<()>
+    ) -> Result<(), E>
     where
-        F: FnMut(usize) -> io::Result<()>,
+        F: FnMut(usize) -> Result<(), E>,
     {
         // The strategy here is to walk the byte buffer in chunks of VECTOR_SIZE (16)
         // bytes at a time starting at the given offset. For each chunk, we compute a
@@ -376,7 +409,7 @@ mod simd {
             unsafe {
                 super::foreach_special_simd("&aXaaaa.a'aa9a<>aab&".as_bytes(), 0, |ix| {
                     #[allow(clippy::unit_arg)]
-                    Ok(vec.push(ix))
+                    Ok::<_, std::fmt::Error>(vec.push(ix))
                 })
                 .unwrap();
             }
@@ -393,7 +426,7 @@ mod simd {
                 unsafe {
                     super::foreach_special_simd(&vek, 0, |_| {
                         match_count += 1;
-                        Ok(())
+                        Ok::<_, std::fmt::Error>(())
                     })
                     .unwrap();
                 }
diff --git a/pulldown-cmark/examples/event-filter.rs b/pulldown-cmark/examples/event-filter.rs
index a67e121..c2e2557 100644
--- a/pulldown-cmark/examples/event-filter.rs
+++ b/pulldown-cmark/examples/event-filter.rs
@@ -23,5 +23,5 @@ fn main() {
     let stdout = std::io::stdout();
     let mut handle = stdout.lock();
     handle.write_all(b"\nHTML output:\n").unwrap();
-    html::write_html(&mut handle, parser).unwrap();
+    html::write_html_io(&mut handle, parser).unwrap();
 }
diff --git a/pulldown-cmark/examples/footnote-rewrite.rs b/pulldown-cmark/examples/footnote-rewrite.rs
index 8d9e164..a550c3e 100644
--- a/pulldown-cmark/examples/footnote-rewrite.rs
+++ b/pulldown-cmark/examples/footnote-rewrite.rs
@@ -53,7 +53,7 @@ fn main() {
     let stdout = std::io::stdout();
     let mut handle = stdout.lock();
     handle.write_all(b"\nHTML output:\n").unwrap();
-    html::write_html(&mut handle, parser).unwrap();
+    html::write_html_io(&mut handle, parser).unwrap();
 
     // To make the footnotes look right, we need to sort them by their appearance order, not by
     // the in-tree order of their actual definitions. Unused items are omitted entirely.
@@ -88,7 +88,7 @@ fn main() {
         handle
             .write_all(b"<hr><ol class=\"footnotes-list\">\n")
             .unwrap();
-        html::write_html(
+        html::write_html_io(
             &mut handle,
             footnotes.into_iter().flat_map(|fl| {
                 // To write backrefs, the name needs kept until the end of the footnote definition.
diff --git a/pulldown-cmark/examples/parser-map-event-print.rs b/pulldown-cmark/examples/parser-map-event-print.rs
index 45be827..84b1651 100644
--- a/pulldown-cmark/examples/parser-map-event-print.rs
+++ b/pulldown-cmark/examples/parser-map-event-print.rs
@@ -19,6 +19,8 @@ fn main() {
             Event::InlineHtml(s) => println!("InlineHtml: {:?}", s),
             Event::Text(s) => println!("Text: {:?}", s),
             Event::Code(s) => println!("Code: {:?}", s),
+            Event::DisplayMath(s) => println!("DisplayMath: {:?}", s),
+            Event::InlineMath(s) => println!("Math: {:?}", s),
             Event::FootnoteReference(s) => println!("FootnoteReference: {:?}", s),
             Event::TaskListMarker(b) => println!("TaskListMarker: {:?}", b),
             Event::SoftBreak => println!("SoftBreak"),
diff --git a/pulldown-cmark/examples/parser-map-tag-print.rs b/pulldown-cmark/examples/parser-map-tag-print.rs
index 6f77f31..30b1919 100644
--- a/pulldown-cmark/examples/parser-map-tag-print.rs
+++ b/pulldown-cmark/examples/parser-map-tag-print.rs
@@ -64,7 +64,7 @@ fn main() {
                 Tag::Emphasis => println!("Emphasis (this is a span tag)"),
                 Tag::Strong => println!("Strong (this is a span tag)"),
                 Tag::Strikethrough => println!("Strikethrough (this is a span tag)"),
-                Tag::BlockQuote => println!("BlockQuote"),
+                Tag::BlockQuote(kind) => println!("BlockQuote ({:?})", kind),
                 Tag::CodeBlock(code_block_kind) => {
                     println!("CodeBlock code_block_kind: {:?}", code_block_kind)
                 }
diff --git a/pulldown-cmark/specs/blockquotes_tags.txt b/pulldown-cmark/specs/blockquotes_tags.txt
new file mode 100644
index 0000000..98d1610
--- /dev/null
+++ b/pulldown-cmark/specs/blockquotes_tags.txt
@@ -0,0 +1,193 @@
+Run this with `cargo test --features gen-tests suite::blockquotes_tags`.
+
+Blockquotes can optionally have one of the following tags:
+
+- [!NOTE]
+- [!TIP]
+- [!IMPORTANT]
+- [!WARNING]
+- [!CAUTION]
+
+Using one of these tags adds a class with the same name but in lowercase
+(note, tip, etc.).
+
+
+```````````````````````````````` example
+> This is a normal blockquote without tag.
+.
+<blockquote><p>This is a normal blockquote without tag.</p></blockquote>
+````````````````````````````````
+
+```````````````````````````````` example
+> [!NOTE]
+> Note blockquote
+.
+<blockquote class="markdown-alert-note"><p>Note blockquote</p></blockquote>
+````````````````````````````````
+
+```````````````````````````````` example
+> [!TIP]
+> Tip blockquote
+.
+<blockquote class="markdown-alert-tip"><p>Tip blockquote</p></blockquote>
+````````````````````````````````
+
+```````````````````````````````` example
+> [!IMPORTANT]
+> Important blockquote
+.
+<blockquote class="markdown-alert-important"><p>Important blockquote</p></blockquote>
+````````````````````````````````
+
+```````````````````````````````` example
+> [!WARNING]
+> Warning blockquote
+.
+<blockquote class="markdown-alert-warning"><p>Warning blockquote</p></blockquote>
+````````````````````````````````
+
+```````````````````````````````` example
+> [!CAUTION]
+> Caution blockquote
+.
+<blockquote class="markdown-alert-caution"><p>Caution blockquote</p></blockquote>
+````````````````````````````````
+
+A blockquote with tag can be empty:
+```````````````````````````````` example
+> [!CAUTION]
+.
+<blockquote class="markdown-alert-caution"></blockquote>
+````````````````````````````````
+
+An a blockquote can have several lines:
+```````````````````````````````` example
+> [!CAUTION]
+> Line 1.
+> Line 2.
+.
+<blockquote class="markdown-alert-caution"><p>Line 1.
+Line 2.</p></blockquote>
+````````````````````````````````
+
+Tags are ignored in subsequent lines, literally written:
+```````````````````````````````` example
+> [!CAUTION]
+> Line 1.
+> [!CAUTION]
+> Line 2.
+.
+<blockquote class="markdown-alert-caution"><p>Line 1.
+[!CAUTION]
+Line 2.</p></blockquote>
+````````````````````````````````
+
+But nested blockquotes can have their own tag:
+```````````````````````````````` example
+> [!CAUTION]
+> Line 1.
+> > [!TIP]
+> Line 2.
+.
+<blockquote class="markdown-alert-caution"><p>Line 1.</p><blockquote class="markdown-alert-tip"><p>Line 2.</p></blockquote></blockquote>
+````````````````````````````````
+
+And consecutive blockquotes too:
+```````````````````````````````` example
+> [!CAUTION]
+> Line 1.
+
+
+> [!TIP]
+> Line 2.
+.
+<blockquote class="markdown-alert-caution"><p>Line 1.</p></blockquote><blockquote class="markdown-alert-tip"><p>Line 2.</p></blockquote>
+````````````````````````````````
+
+Tags also work in inner blockquotes:
+```````````````````````````````` example
+> > [!CAUTION]
+> > Line 1.
+> Line 2.
+.
+<blockquote><blockquote class="markdown-alert-caution"><p>Line 1.
+Line 2.</p></blockquote></blockquote>
+````````````````````````````````
+
+Tags can be followed by spaces (works in most implementations):
+```````````````````````````````` example
+> [!CAUTION]→
+> Line 1.
+> > [!NOTE]      
+> > Line 2.
+.
+<blockquote class="markdown-alert-caution"><p>Line 1.</p>
+<blockquote class="markdown-alert-note"><p>Line 2.</p></blockquote>
+</blockquote>
+````````````````````````````````
+
+Tags are case-insensitive (works in most implementations):
+```````````````````````````````` example
+> [!caution]→
+> Line 1.
+> > [!note]      
+> > Line 2.
+.
+<blockquote class="markdown-alert-caution"><p>Line 1.</p>
+<blockquote class="markdown-alert-note"><p>Line 2.</p></blockquote>
+</blockquote>
+````````````````````````````````
+
+Tags work inside lists (doesn't work in GitHub, but works elsewhere):
+```````````````````````````````` example
+  * loose lists
+
+    > [!NOTE]
+    > sink ships
+.
+<ul><li><p>loose lists</p>
+<blockquote class="markdown-alert-note"><p>sink ships</p></blockquote>
+</li></ul>
+````````````````````````````````
+
+Tags can act as lazy paragraph continuations (works in GitHub, not commonmark-hs):
+```````````````````````````````` example
+> [!NOTE]
+sink ships
+
+> [!NOTE]
+> sink ships
+
+  * loose lists
+
+    > [!NOTE]
+    sink ships
+
+    > [!NOTE]
+    > sink ships
+.
+<blockquote class="markdown-alert-note"><p>sink ships</p></blockquote>
+<blockquote class="markdown-alert-note"><p>sink ships</p></blockquote>
+<ul><li><p>loose lists</p>
+<blockquote class="markdown-alert-note"><p>sink ships</p></blockquote>
+<blockquote class="markdown-alert-note"><p>sink ships</p></blockquote>
+</li></ul>
+````````````````````````````````
+
+Like lazy continuations, the blockquote marker is still needed to make nested blocks:
+```````````````````````````````` example
+  * loose lists
+
+    > [!NOTE]
+    - sink ships
+
+    > [!NOTE]
+    > - sink ships
+.
+<ul><li><p>loose lists</p>
+<blockquote class="markdown-alert-note"></blockquote>
+<ul><li>sink ships</li></ul>
+<blockquote class="markdown-alert-note">
+<ul><li>sink ships</li></ul></blockquote>
+</li></ul>
+````````````````````````````````
diff --git a/pulldown-cmark/specs/footnotes.txt b/pulldown-cmark/specs/footnotes.txt
index 2a4ca55..dd341d9 100644
--- a/pulldown-cmark/specs/footnotes.txt
+++ b/pulldown-cmark/specs/footnotes.txt
@@ -1,4 +1,4 @@
-Run this with `cargo test --features gen-tests suite::gfm_footnotes`.
+Run this with `cargo test --features gen-tests suite::footnotes`.
 
 Parts of this test case are based on
 <https://github.com/github/cmark-gfm/blob/1e230827a584ebc9938c3eadc5059c55ef3c9abf/test/extensions.txt#L702>.
diff --git a/pulldown-cmark/specs/heading_attrs.txt b/pulldown-cmark/specs/heading_attrs.txt
index 1a2706b..f9f21f2 100644
--- a/pulldown-cmark/specs/heading_attrs.txt
+++ b/pulldown-cmark/specs/heading_attrs.txt
@@ -1,3 +1,5 @@
+Run this with `cargo test --features gen-tests suite::heading_attrs`.
+
 Examples and edge cases for attribute blocks for headings.
 
 # Basic usage
diff --git a/pulldown-cmark/specs/math.txt b/pulldown-cmark/specs/math.txt
new file mode 100644
index 0000000..b16453b
--- /dev/null
+++ b/pulldown-cmark/specs/math.txt
@@ -0,0 +1,861 @@
+Run this with `cargo test --features gen-tests suite::math`.
+
+# `$`-delimited LaTeX Math in pulldown-cmark
+
+Mathematical expressions extension. Syntax based on
+<https://github.com/jgm/commonmark-hs/blob/master/commonmark-extensions/test/math.md>.
+
+Inline mode mathematical expressions:
+
+```````````````````````````````` example
+This sentence uses `$` delimiters to show math inline: $\sqrt{3x-1}+(1+x)^2$
+$\sum_{k=1}^n a_k b_k$: Mathematical expression at head of line
+
+`\` may follow just after the first `$`: $\{1, 2, 3\}$
+.
+<p>This sentence uses <code>$</code> delimiters to show math inline: <span class="math math-inline">\sqrt{3x-1}+(1+x)^2</span>
+<span class="math math-inline">\sum_{k=1}^n a_k b_k</span>: Mathematical expression at head of line</p>
+<p><code>\</code> may follow just after the first <code>$</code>: <span class="math math-inline">\{1, 2, 3\}</span></p>
+````````````````````````````````
+
+Display mode mathematical expressions:
+
+```````````````````````````````` example
+**The Cauchy-Schwarz Inequality**
+
+$$\left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)$$
+.
+<p><strong>The Cauchy-Schwarz Inequality</strong></p>
+<p><span class="math math-display">\left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)</span></p>
+````````````````````````````````
+
+Inline math expressions cannot be empty, but display mode expressions can.
+
+```````````````````````````````` example
+Oops empty $$ expression.
+
+$$$$
+.
+<p>Oops empty $$ expression.</p>
+<p><span class="math math-display"></span></p>
+````````````````````````````````
+
+This is a greedy, left-to-right parser.
+
+```````````````````````````````` example
+$x$$$$$$$y$$
+
+$x$$$$$$y$$
+
+$$x$$$$$$y$$
+.
+<p><span class="math math-inline">x</span><span class="math math-display"></span><span class="math math-display">y</span></p>
+<p><span class="math math-inline">x</span><span class="math math-display"></span><span class="math math-inline">y</span>$</p>
+<p><span class="math math-display">x</span><span class="math math-display"></span>y$$</p>
+````````````````````````````````
+
+Math expressions pass their content through as-is, ignoring any other inline
+Markdown constructs:
+
+```````````````````````````````` example
+$a<b>c</b>$
+
+$${a*b*c} _c_ d$$
+
+$not `code`$
+
+$![not an](/image)$
+
+$<https://not.a.link/>$
+
+$&alpha;$
+.
+<p><span class="math math-inline">a&lt;b&gt;c&lt;/b&gt;</span></p>
+<p><span class="math math-display">{a*b*c} _c_ d</span></p>
+<p><span class="math math-inline">not `code`</span></p>
+<p><span class="math math-inline">![not an](/image)</span></p>
+<p><span class="math math-inline">&lt;https://not.a.link/&gt;</span></p>
+<p><span class="math math-inline">&amp;alpha;</span></p>
+````````````````````````````````
+
+Sole `$` characters without a matching pair in the same block element
+are handled as normal text.
+
+```````````````````````````````` example
+Hello $world.
+
+Dollar at end of line$
+.
+<p>Hello $world.</p>
+<p>Dollar at end of line$</p>
+````````````````````````````````
+
+Mathematical expressions can continue across multiple lines:
+
+```````````````````````````````` example
+$5x + 2 =
+17$
+
+$$\left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right)
+\left( \sum_{k=1}^n b_k^2 \right)$$
+.
+<p><span class="math math-inline">5x + 2 =
+17</span></p>
+<p><span class="math math-display">\left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right)
+\left( \sum_{k=1}^n b_k^2 \right)</span></p>
+````````````````````````````````
+
+Markdown hard breaks are also not recognized inside math expressions:
+
+```````````````````````````````` example
+$not a\
+hard break  
+either$
+.
+<p><span class="math math-inline">not a\
+hard break  
+either</span></p>
+````````````````````````````````
+
+
+`$` character can be escaped with backslash in mathematical expressions:
+
+```````````````````````````````` example
+$\$$
+
+$$y = \$ x$$
+.
+<p><span class="math math-inline">\$</span></p>
+<p><span class="math math-display">y = \$ x</span></p>
+````````````````````````````````
+
+Inline mode math expressions cannot contain unescaped `$` characters.
+Neither can display math.
+
+```````````````````````````````` example
+$x $ x$
+
+$$ $ $$
+.
+<p>$x $ x$</p>
+<p>$$ $ $$</p>
+````````````````````````````````
+
+```````````````````````````````` example
+alpha$$beta$gamma$$delta
+.
+<p>alpha$<span class="math math-inline">beta</span>gamma$$delta</p>
+````````````````````````````````
+
+Inline math expressions cannot start or end with whitespace, including newlines:
+
+```````````````````````````````` example
+these are not math texts: $ y=x$, $y=x $, $
+y=x$ and $y=x
+$
+
+>The start of a line counts as whitespace $2 +
+>$
+
+While displays can start with whitespace, {${
+they should not allow inlines to do that $$2 +
+$*$
+.
+<p>these are not math texts: $ y=x$, $y=x $, $
+y=x$ and $y=x
+$</p>
+<blockquote>
+<p>The start of a line counts as whitespace $2 +
+$</p>
+</blockquote>
+<p>While displays can start with whitespace, {${
+they should not allow inlines to do that $$2 +
+<span class="math math-inline">*</span></p>
+````````````````````````````````
+
+Inline math expressions do not need to be surrounded with whitespace:
+
+```````````````````````````````` example
+these are math texts: foo$y=x$bar and $y=x$bar and foo$y=x$ bar
+.
+<p>these are math texts: foo<span class="math math-inline">y=x</span>bar and <span class="math math-inline">y=x</span>bar and foo<span class="math math-inline">y=x</span> bar</p>
+````````````````````````````````
+
+Inline math expressions can be surrounded by punctuation:
+
+```````````````````````````````` example
+math texts: $x=y$! and $x=y$? and $x=y$: and $x=y$. and $x=y$"
+
+also math texts: !$x=y$! and ?$x=y$? and :$x=y$: and .$x=y$. and "$x=y$"
+
+braces: ($x=y$) [$x=y$] {$x=y$}
+.
+<p>math texts: <span class="math math-inline">x=y</span>! and <span class="math math-inline">x=y</span>? and <span class="math math-inline">x=y</span>: and <span class="math math-inline">x=y</span>. and <span class="math math-inline">x=y</span>"</p>
+<p>also math texts: !<span class="math math-inline">x=y</span>! and ?<span class="math math-inline">x=y</span>? and :<span class="math math-inline">x=y</span>: and .<span class="math math-inline">x=y</span>. and "<span class="math math-inline">x=y</span>"</p>
+<p>braces: (<span class="math math-inline">x=y</span>) [<span class="math math-inline">x=y</span>] {<span class="math math-inline">x=y</span>}</p>
+````````````````````````````````
+
+Math expression as only item on a line:
+
+```````````````````````````````` example
+$x=y$
+.
+<p><span class="math math-inline">x=y</span></p>
+````````````````````````````````
+
+Math expressions can be immediately followed by other math expressions:
+
+```````````````````````````````` example
+$a$$b$
+
+$a$$$b$$
+
+$$a$$$b$
+
+$$a$$$$b$$
+.
+<p><span class="math math-inline">a</span><span class="math math-inline">b</span></p>
+<p><span class="math math-inline">a</span><span class="math math-display">b</span></p>
+<p><span class="math math-display">a</span><span class="math math-inline">b</span></p>
+<p><span class="math math-display">a</span><span class="math math-display">b</span></p>
+````````````````````````````````
+
+Both inline and display mode math expressions are inline elements with the same
+precedence as code spans. The leftmost valid element takes priority:
+
+```````````````````````````````` example
+$Inline `first$ then` code
+
+`Code $first` then$ inline
+
+$$ Display `first $$ then` code
+
+`Code $$ first` then $$ display
+.
+<p><span class="math math-inline">Inline `first</span> then` code</p>
+<p><code>Code $first</code> then$ inline</p>
+<p><span class="math math-display"> Display `first </span> then` code</p>
+<p><code>Code $$ first</code> then $$ display</p>
+````````````````````````````````
+
+Indicators of block structure take precedence over math expressions:
+
+```````````````````````````````` example
+$x + y - z$
+
+$x + y
+- z$
+
+$$ x + y
+> z $$
+.
+<p><span class="math math-inline">x + y - z</span></p>
+<p>$x + y</p>
+<ul>
+<li>z$</li>
+</ul>
+<p>$$ x + y</p>
+<blockquote>
+<p>z $$</p>
+</blockquote>
+````````````````````````````````
+
+This also means that math expressions cannot contain empty lines, since they
+start a new paragraph:
+
+```````````````````````````````` example
+$not
+
+math$
+
+$$
+not
+
+math
+$$
+.
+<p>$not</p>
+<p>math$</p>
+<p>$$
+not</p>
+<p>math
+$$</p>
+````````````````````````````````
+
+It also implies that math notation has lower
+parsing power than block elements.
+
+```````````````````````````````` example
+- $not
+    - *
+  math$
+.
+<ul>
+<li>$not
+<ul>
+<li>
+<ul>
+<li>
+</li>
+</ul>
+</li>
+</ul>
+math$</li>
+</ul>
+````````````````````````````````
+
+Note that math can contain embedded math.  In scanning
+for a closing delimiter, we skip material in balanced
+curly braces:
+
+```````````````````````````````` example
+This is display math:
+$$
+\text{Hello $x^2$}
+$$
+And this is inline math:
+$\text{Hello $x$ there!}$
+.
+<p>This is display math:
+<span class="math math-display">
+\text{Hello $x^2$}
+</span>
+And this is inline math:
+<span class="math math-inline">\text{Hello $x$ there!}</span></p>
+````````````````````````````````
+
+Math expressions must be nested within balanced curly braces.
+Backslash-escaped braces do not count.
+
+```````````````````````````````` example
+This is not valid math: $}{$
+
+Neither is this: { $}{$ }
+
+This is: $\}\{$
+
+This is: $\}$
+
+Math environment contains 2+2: $}$2+2$
+
+Math environment contains y: $x {$ $ } $y$
+.
+<p>This is not valid math: $}{$</p>
+<p>Neither is this: { $}{$ }</p>
+<p>This is: <span class="math math-inline">\}\{</span></p>
+<p>This is: <span class="math math-inline">\}</span></p>
+<p>Math environment contains 2+2: $}<span class="math math-inline">2+2</span></p>
+<p>Math environment contains y: $x {$ $ } <span class="math math-inline">y</span></p>
+````````````````````````````````
+
+Math expressions must contain properly nested braces.
+
+```````````````````````````````` example
+This is not display math. It is inline math:
+
+$$\text{first $$ second}$
+
+$$$\text{first $$ second}$
+
+This is display math:
+
+$$\text{first $$ second}$$
+
+$$$\text{first $$ second}$$
+
+This is also display math, but (counterintuitively) it's allowed to be empty
+and expected to be as short as possible:
+
+$$$$\text{first $$ second}$$
+.
+<p>This is not display math. It is inline math:</p>
+<p>$<span class="math math-inline">\text{first $$ second}</span></p>
+<p>$$<span class="math math-inline">\text{first $$ second}</span></p>
+<p>This is display math:</p>
+<p><span class="math math-display">\text{first $$ second}</span></p>
+<p>$<span class="math math-display">\text{first $$ second}</span></p>
+<p>This is also display math, but (counterintuitively) it's allowed to be empty
+and expected to be as short as possible:</p>
+<p><span class="math math-display"></span>\text{first $$ second}$$</p>
+````````````````````````````````
+
+Dollar signs must also be backslash-escaped if they occur within math:
+
+```````````````````````````````` example
+$\text{\$}$
+
+$$x$x$$
+
+${$^$$
+
+$}$$$$
+
+$}$] $$
+.
+<p><span class="math math-inline">\text{\$}</span></p>
+<p>$<span class="math math-inline">x</span>x$$</p>
+<p>${<span class="math math-inline">^</span>$</p>
+<p>$}<span class="math math-display"></span></p>
+<p>$}$] $$</p>
+````````````````````````````````
+
+## Edge case tests comparison with GitHub
+
+Test cases
+https://raw.githubusercontent.com/nschloe/github-math-bugs/db938ff690ab7c534d8195fe4a1a5163c20b1134/README.md
+
+Inline math wrapped in quotes
+
+```````````````````````````````` example
+$x$ $`y`$
+.
+<p><span class="math math-inline">x</span> <span class="math math-inline">`y`</span></p>
+````````````````````````````````
+
+Inline and display math in the same list
+
+```````````````````````````````` example
+- $a$
+
+  ```math
+  a
+  ```
+
+  $$
+  a
+  $$
+
+- ```math
+  b
+  ```
+
+  $$
+  b
+  $$
+.
+<ul>
+<li>
+<p><span class="math math-inline">a</span></p>
+<pre><code class="language-math">a
+</code></pre>
+<p><span class="math math-display">
+a
+</span></p>
+</li>
+<li>
+<pre><code class="language-math">b
+</code></pre>
+<p><span class="math math-display">
+b
+</span></p>
+</li>
+</ul>
+````````````````````````````````
+
+Images and math in the same list
+
+```````````````````````````````` example
+- ![node logo](https://nodejs.org/static/images/logo.svg)
+- $x$
+.
+<ul>
+<li><img src="https://nodejs.org/static/images/logo.svg" alt="node logo" /></li>
+<li><span class="math math-inline">x</span></li>
+</ul>
+````````````````````````````````
+
+Inline and display math in `<details>`
+
+```````````````````````````````` example
+<details>
+
+$A = 5$
+
+$$
+A = 5
+$$
+
+</details>
+.
+<details>
+<p><span class="math math-inline">A = 5</span></p>
+<p><span class="math math-display">
+A = 5
+</span></p>
+</details>
+````````````````````````````````
+
+`<` without surrounding whitespace
+
+```````````````````````````````` example
+$a<b$
+
+$$a<b$$
+.
+<p><span class="math math-inline">a&lt;b</span></p>
+<p><span class="math math-display">a&lt;b</span></p>
+````````````````````````````````
+
+Math in footnotes
+
+```````````````````````````````` example
+[^a]
+
+[^a]: Lorem $a$
+.
+<p><sup class="footnote-reference"><a href="#a">1</a></sup></p>
+<div class="footnote-definition" id="a"><sup class="footnote-definition-label">1</sup>
+<p>Lorem <span class="math math-inline">a</span></p>
+</div>
+````````````````````````````````
+
+Math in links
+
+```````````````````````````````` example
+[$a$](x)
+.
+<p>
+<a href="x"><span class="math math-inline">a</span></a>
+</p>
+````````````````````````````````
+
+Math preceded by an alphabetical character
+
+```````````````````````````````` example
+a$x$
+
+-$x$
+
+1$x$
+.
+<p>a<span class="math math-inline">x</span></p>
+<p>-<span class="math math-inline">x</span></p>
+<p>1<span class="math math-inline">x</span></p>
+````````````````````````````````
+
+Inline math at the end of italic text
+
+```````````````````````````````` example
+_$a$ equals $b$_
+
+_$a$ equals $b$_
+
+**$a$ equals $b$**
+.
+<p><em><span class="math math-inline">a</span> equals <span class="math math-inline">b</span></em></p>
+<p><em><span class="math math-inline">a</span> equals <span class="math math-inline">b</span></em></p>
+<p><strong><span class="math math-inline">a</span> equals <span class="math math-inline">b</span></strong></p>
+````````````````````````````````
+
+Dollar in `\text`
+
+```````````````````````````````` example
+$$
+a
+$$
+
+- $$
+  \text{$b$}
+  $$
+.
+<p><span class="math math-display">
+a
+</span>
+</p><ul>
+<li><span class="math math-display">
+\text{$b$}
+</span></li>
+</ul>
+````````````````````````````````
+
+Backslashes in `$`-math
+
+```````````````````````````````` example
+$\{a\,b\}$
+.
+<p><span class="math math-inline">\{a\,b\}</span></p>
+````````````````````````````````
+
+Math vs. HTML mix-up
+
+```````````````````````````````` example
+$a <b > c$
+
+$[(a+b)c](d+e)$
+
+${a}_b c_{d}$
+.
+<p><span class="math math-inline">a &lt;b &gt; c</span></p>
+<p><span class="math math-inline">[(a+b)c](d+e)</span></p>
+<p><span class="math math-inline">{a}_b c_{d}</span></p>
+````````````````````````````````
+
+Dollar-math with spaces
+
+```````````````````````````````` example
+When $a \ne 0$, there are two solutions to $(ax^2 + bx + c = 0)$ and they are
+$$ x = {-b \pm \sqrt{b^2-4ac} \over 2a} $$
+.
+<p>When <span class="math math-inline">a \ne 0</span>, there are two solutions to <span class="math math-inline">(ax^2 + bx + c = 0)</span> and they are
+<span class="math math-display"> x = {-b \pm \sqrt{b^2-4ac} \over 2a} </span></p>
+````````````````````````````````
+
+Spacing around dollar sign in math mode
+
+```````````````````````````````` example
+$x = \$$
+.
+<p><span class="math math-inline">x = \$</span></p>
+````````````````````````````````
+
+Math in italic text
+
+```````````````````````````````` example
+_Equation $\Omega(69)$ in italic text_
+.
+<p><em>Equation <span class="math math-inline">\Omega(69)</span> in italic text</em></p>
+````````````````````````````````
+
+Inline math can't be preceded by brackets, quotation marks etc.
+
+```````````````````````````````` example
+$\pi$
+'$\pi$
+"$\pi$
+($\pi$
+[$\pi$
+{$\pi$
+/$\pi$
+.
+<p>
+<span class="math math-inline">\pi</span>
+'<span class="math math-inline">\pi</span>
+"<span class="math math-inline">\pi</span>
+(<span class="math math-inline">\pi</span>
+[<span class="math math-inline">\pi</span>
+{<span class="math math-inline">\pi</span>
+/<span class="math math-inline">\pi</span>
+</p>
+````````````````````````````````
+
+## Relationship with tables
+
+As a block element, tables parsing is stronger than math.
+
+```````````````````````````````` example
+| first $|$ second |
+|--------|---------|
+| a ${   | }$ b    |
+.
+<table><thead>
+<tr><th>first $</th><th>$ second</th></tr>
+</thead><tbody>
+<tr><td>a ${</td><td>}$ b</td></tr>
+</tbody></table>
+````````````````````````````````
+
+As a special case, pipes in math environments in tables are escaped
+with backslashes. Though backslash-escaped characters in math
+environments are normally passed through verbatim to the LaTeX
+engine, escaped pipes in tables are an exception like they
+are in code spans.
+
+The behavior of the table parser should be as-if it found the bounds
+of the table cell in a separate pass that only looked for the
+strings `|` and `\|`, treating pipes as boundaries and removing the
+escaping backslash before passing the string to the inline parser.
+
+```````````````````````````````` example
+| first $\|$ second |
+|-------------------|
+| a ${   \| }$ b    |
+.
+<table><thead>
+<tr><th>first <span class="math math-inline">|</span> second</th></tr>
+</thead><tbody>
+<tr><td>a <span class="math math-inline">{   | }</span> b</td></tr>
+</tbody></table>
+````````````````````````````````
+
+```````````````````````````````` example
+| Description | Test case |
+|-------------|-----------|
+| Single      | $\$       |
+| Double      | $\\$      |
+| Basic test  | $\|$      |
+| Basic test 2| $\|\|\$   |
+| Basic test 3| $x\|y\|z\$|
+| Not pipe    | $\.$      |
+| Combo       | $\.\|$    |
+| Combo 2     | $\.\|\$   |
+| Extra       | $\\\.$    |
+| Wait, what? | $\\|$     |
+| Wait, what? | $\\\|$    |
+| Wait, what? | $\\\\|$   |
+| Wait, what? | $\\\\\|$  |
+.
+<table><thead><tr><th>Description</th><th>Test case</th></tr></thead><tbody>
+<tr><td>Single</td><td>$$</td></tr>
+<tr><td>Double</td><td><span class="math math-inline">\\</span></td></tr>
+<tr><td>Basic test</td><td><span class="math math-inline">|</span></td></tr>
+<tr><td>Basic test 2</td><td>$||$</td></tr>
+<tr><td>Basic test 3</td><td>$x|y|z$</td></tr>
+<tr><td>Not pipe</td><td><span class="math math-inline">\.</span></td></tr>
+<tr><td>Combo</td><td><span class="math math-inline">\.|</span></td></tr>
+<tr><td>Combo 2</td><td>$.|$</td></tr>
+<tr><td>Extra</td><td><span class="math math-inline">\\\.</span></td></tr>
+<tr><td>Wait, what?</td><td><span class="math math-inline">\|</span></td></tr>
+<tr><td>Wait, what?</td><td><span class="math math-inline">\\|</span></td></tr>
+<tr><td>Wait, what?</td><td><span class="math math-inline">\\\|</span></td></tr>
+<tr><td>Wait, what?</td><td><span class="math math-inline">\\\\|</span></td></tr>
+</tbody></table>
+````````````````````````````````
+
+## Implementation limits
+
+Implementations may impose limits on brace nesting to avoid performance issues,
+but at least three levels of nesting should be supported.
+
+Pulldown-cmark imposes the following limits:
+
+1. At 25 levels of nesting, it switches from tracking nested pairs to simply
+   counting the number of braces. This means the below example will spurriously
+   recognize a math environment with the correct number of braces, but not
+   nested correctly.
+
+```````````````````````````````` example
+This is not an inline math environment: $}{$
+But, because it's nested too deeply, this is parsed as an inline math environment:
+{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{
+improperly $}{$ nested
+}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
+But this still isn't, because the braces are still counted: $}{$
+.
+<p>This is not an inline math environment: $}{$
+But, because it's nested too deeply, this is parsed as an inline math environment:
+{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{
+improperly <span class="math math-inline">}{</span> nested
+}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
+But this still isn't, because the braces are still counted: $}{$</p>
+````````````````````````````````
+
+```````````````````````````````` example
+This is also deeply nested, but, unlike the first example,
+they don't have an equal number of close braces and open braces,
+so aren't detected as math.
+{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{
+improperly $}$ nested ${$ example
+}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
+This, however, is detected ${}$
+
+${{{{{{{{{{{{{{{{{{{{{{{{{{{{{{
+another improperly nested example
+}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}$
+.
+<p>This is also deeply nested, but, unlike the first example,
+they don't have an equal number of close braces and open braces,
+so aren't detected as math.
+{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{
+improperly $}$ nested ${$ example
+}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
+This, however, is detected <span class="math math-inline">{}</span></p>
+<p><span class="math math-inline">{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{
+another improperly nested example
+}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}</span></p>
+````````````````````````````````
+
+2. At 255 distinct brace-delimited groups, the counter rolls over. This means
+   the below example will spurriously recognize an incorrectly-nested
+   inline math environment.
+
+```````````````````````````````` example
+${}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}  20 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}  40 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}  60 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}  80 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 100 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 120 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 140 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 160 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 180 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 200 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 220 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 240 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{$ 255 brace pairs and one unclosed brace
+.
+<p><span class="math math-inline">{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}  20 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}  40 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}  60 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}  80 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 100 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 120 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 140 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 160 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 180 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 200 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 220 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 240 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{</span> 255 brace pairs and one unclosed brace</p>
+````````````````````````````````
+
+3. Thanks to rule 1, though, deeply-nested structures won't chew through all of
+   the ID space. This means that the below example, even though it nests 255
+   levels deep, parses correctly anyway.
+
+```````````````````````````````` example
+${{{{{{{{{{{{{{{{{{{{ 20 open braces
+{{{{{{{{{{{{{{{{{{{{  40 open braces
+{{{{{{{{{{{{{{{{{{{{  60 open braces
+{{{{{{{{{{{{{{{{{{{{  80 open braces
+{{{{{{{{{{{{{{{{{{{{ 100 open braces
+{{{{{{{{{{{{{{{{{{{{ 110 open braces
+{{{{{{{{{{{{{{{{{{{{ 120 open braces
+{{{{{{{{{{{{{{{{{{{{ 140 open braces
+{{{{{{{{{{{{{{{{{{{{ 160 open braces
+{{{{{{{{{{{{{{{{{{{{ 180 open braces
+{{{{{{{{{{{{{{{{{{{{ 200 open braces
+{{{{{{{{{{{{{{{{{{{{ 220 open braces
+{{{{{{{{{{{{{{{{{{{{ 240 open braces
+{{{{{{{{{{{{{{{ 255 open braces
+}}}}}}}}}}}}}}}}}}}}  20 close braces
+}}}}}}}}}}}}}}}}}}}}  40 close braces
+}}}}}}}}}}}}}}}}}}}}  60 close braces
+}}}}}}}}}}}}}}}}}}}}  80 close braces
+}}}}}}}}}}}}}}}}}}}} 100 close braces
+}}}}}}}}}}}}}}}}}}}} 120 close braces
+}}}}}}}}}}}}}}}}}}}} 140 close braces
+}}}}}}}}}}}}}}}}}}}} 160 close braces
+}}}}}}}}}}}}}}}}}}}} 180 close braces
+}}}}}}}}}}}}}}}}}}}} 200 close braces
+}}}}}}}}}}}}}}}}}}}} 220 close braces
+}}}}}}}}}}}}}}}}}}}} 240 close braces
+}}}}}}}}}}}}}}}{$ 255 close braces and one open brace
+.
+<p>${{{{{{{{{{{{{{{{{{{{ 20 open braces
+{{{{{{{{{{{{{{{{{{{{  40 open braces
+{{{{{{{{{{{{{{{{{{{{  60 open braces
+{{{{{{{{{{{{{{{{{{{{  80 open braces
+{{{{{{{{{{{{{{{{{{{{ 100 open braces
+{{{{{{{{{{{{{{{{{{{{ 110 open braces
+{{{{{{{{{{{{{{{{{{{{ 120 open braces
+{{{{{{{{{{{{{{{{{{{{ 140 open braces
+{{{{{{{{{{{{{{{{{{{{ 160 open braces
+{{{{{{{{{{{{{{{{{{{{ 180 open braces
+{{{{{{{{{{{{{{{{{{{{ 200 open braces
+{{{{{{{{{{{{{{{{{{{{ 220 open braces
+{{{{{{{{{{{{{{{{{{{{ 240 open braces
+{{{{{{{{{{{{{{{ 255 open braces
+}}}}}}}}}}}}}}}}}}}}  20 close braces
+}}}}}}}}}}}}}}}}}}}}  40 close braces
+}}}}}}}}}}}}}}}}}}}}  60 close braces
+}}}}}}}}}}}}}}}}}}}}  80 close braces
+}}}}}}}}}}}}}}}}}}}} 100 close braces
+}}}}}}}}}}}}}}}}}}}} 120 close braces
+}}}}}}}}}}}}}}}}}}}} 140 close braces
+}}}}}}}}}}}}}}}}}}}} 160 close braces
+}}}}}}}}}}}}}}}}}}}} 180 close braces
+}}}}}}}}}}}}}}}}}}}} 200 close braces
+}}}}}}}}}}}}}}}}}}}} 220 close braces
+}}}}}}}}}}}}}}}}}}}} 240 close braces
+}}}}}}}}}}}}}}}{$ 255 close braces and one open brace</p>
+````````````````````````````````
diff --git a/pulldown-cmark/specs/metadata_blocks.txt b/pulldown-cmark/specs/metadata_blocks.txt
index 7aa9b97..ac61aa0 100644
--- a/pulldown-cmark/specs/metadata_blocks.txt
+++ b/pulldown-cmark/specs/metadata_blocks.txt
@@ -1,3 +1,5 @@
+Run this with `cargo test --features gen-tests suite::metadata_blocks`.
+
 Examples and edge cases for metadata blocks.
 
 YAML-style and pluses-style metadata blocks are supported.
diff --git a/pulldown-cmark/specs/old_footnotes.txt b/pulldown-cmark/specs/old_footnotes.txt
index 3f4b028..32f7ab4 100644
--- a/pulldown-cmark/specs/old_footnotes.txt
+++ b/pulldown-cmark/specs/old_footnotes.txt
@@ -1,4 +1,4 @@
-Run this with `cargo test --features gen-tests suite::footnotes`.
+Run this with `cargo test --features gen-tests suite::old_footnotes`.
 
 This is how footnotes are basically used.
 
diff --git a/pulldown-cmark/src/firstpass.rs b/pulldown-cmark/src/firstpass.rs
index 614d9d5..c9ecc0e 100644
--- a/pulldown-cmark/src/firstpass.rs
+++ b/pulldown-cmark/src/firstpass.rs
@@ -5,7 +5,8 @@ use std::cmp::max;
 use std::ops::Range;
 
 use crate::parse::{
-    scan_containers, Allocations, FootnoteDef, HeadingAttributes, Item, ItemBody, LinkDef, LINK_MAX_NESTED_PARENS,
+    scan_containers, Allocations, FootnoteDef, HeadingAttributes, Item, ItemBody, LinkDef,
+    LINK_MAX_NESTED_PARENS,
 };
 use crate::strings::CowStr;
 use crate::tree::{Tree, TreeIndex};
@@ -34,10 +35,23 @@ pub(crate) fn run_first_pass(text: &str, options: Options) -> (Tree<Item>, Alloc
         options,
         lookup_table,
         next_paragraph_task: None,
+        brace_context_next: 0,
+        brace_context_stack: Vec::new(),
     };
     first_pass.run()
 }
 
+// Each level of brace nesting adds another entry to a hash table.
+// To limit the amount of memory consumed, do not create a new brace
+// context beyond some amount deep.
+//
+// There are actually two limits at play here: this one,
+// and the one where the maximum amount of distinct contexts passes
+// the 255 item limit imposed by using `u8`. When over 255 distinct
+// contexts are created, it wraps around, while this one instead makes it
+// saturate, which is a better behavior.
+const MATH_BRACE_CONTEXT_MAX_NESTING: usize = 25;
+
 /// State for the first parsing pass.
 struct FirstPass<'a, 'b> {
     text: &'a str,
@@ -50,6 +64,9 @@ struct FirstPass<'a, 'b> {
     /// This is `Some(item)` when the next paragraph
     /// starts with a task list marker.
     next_paragraph_task: Option<Item>,
+    /// Math environment brace nesting.
+    brace_context_stack: Vec<u8>,
+    brace_context_next: usize,
 }
 
 impl<'a, 'b> FirstPass<'a, 'b> {
@@ -69,6 +86,10 @@ impl<'a, 'b> FirstPass<'a, 'b> {
         let bytes = self.text.as_bytes();
         let mut line_start = LineStart::new(&bytes[start_ix..]);
 
+        // math spans and their braces are tracked only within a single block
+        self.brace_context_stack.clear();
+        self.brace_context_next = 0;
+
         let i = scan_containers(
             &self.tree,
             &mut line_start,
@@ -145,13 +166,45 @@ impl<'a, 'b> FirstPass<'a, 'b> {
                         });
                 }
             } else if line_start.scan_blockquote_marker() {
+                let kind = if self.options.contains(Options::ENABLE_GFM) {
+                    line_start.scan_blockquote_tag()
+                } else {
+                    None
+                };
                 self.finish_list(start_ix);
                 self.tree.append(Item {
                     start: container_start,
                     end: 0, // will get set later
-                    body: ItemBody::BlockQuote,
+                    body: ItemBody::BlockQuote(kind),
                 });
                 self.tree.push();
+                if kind.is_some() {
+                    // blockquote tag leaves us at the end of the line
+                    // we need to scan through all the container syntax for the next line
+                    // and break out if we can't re-scan all of them
+                    let ix = start_ix + line_start.bytes_scanned();
+                    let mut lazy_line_start = LineStart::new(&bytes[ix..]);
+                    let current_container = scan_containers(
+                        &self.tree,
+                        &mut lazy_line_start,
+                        self.options.has_gfm_footnotes(),
+                    ) == self.tree.spine_len();
+                    if !lazy_line_start.scan_space(4)
+                        && self.scan_paragraph_interrupt(
+                            &bytes[ix + lazy_line_start.bytes_scanned()..],
+                            current_container,
+                        )
+                    {
+                        return ix;
+                    } else {
+                        // blockquote tags act as if they were nested in a paragraph
+                        // so you can lazily continue the imaginary paragraph off of them
+                        line_start = lazy_line_start;
+                        line_start.scan_all_space();
+                        start_ix = ix;
+                        break;
+                    }
+                }
             } else {
                 break;
             }
@@ -162,7 +215,7 @@ impl<'a, 'b> FirstPass<'a, 'b> {
         if let Some(n) = scan_blank_line(&bytes[ix..]) {
             if let Some(node_ix) = self.tree.peek_up() {
                 match &mut self.tree[node_ix].item.body {
-                    ItemBody::BlockQuote => (),
+                    ItemBody::BlockQuote(..) => (),
                     ItemBody::ListItem(indent) if self.begin_list_item.is_some() => {
                         self.last_line_blank = true;
                         // This is a blank list item.
@@ -304,20 +357,15 @@ impl<'a, 'b> FirstPass<'a, 'b> {
         // https://github.com/raphlinus/pulldown-cmark/issues/832
         let mut missing_empty_cells = 0;
         // parse header. this shouldn't fail because we made sure the table header is ok
-        let (_sep_start, thead_ix) = self.parse_table_row_inner(
-            head_start,
-            table_cols,
-            &mut missing_empty_cells,
-        )?;
+        let (_sep_start, thead_ix) =
+            self.parse_table_row_inner(head_start, table_cols, &mut missing_empty_cells)?;
         self.tree[thead_ix].item.body = ItemBody::TableHead;
 
         // parse body
         let mut ix = body_start;
-        while let Some((next_ix, _row_ix)) = self.parse_table_row(
-            ix,
-            table_cols,
-            &mut missing_empty_cells,
-        ) {
+        while let Some((next_ix, _row_ix)) =
+            self.parse_table_row(ix, table_cols, &mut missing_empty_cells)
+        {
             ix = next_ix;
         }
 
@@ -772,6 +820,93 @@ impl<'a, 'b> FirstPass<'a, 'b> {
                     }
                     LoopInstruction::ContinueAndSkip(count - 1)
                 }
+                b'$' => {
+                    let string_suffix = &self.text[ix..];
+                    let can_open = !string_suffix[1..]
+                        .as_bytes()
+                        .first()
+                        .copied()
+                        .map_or(true, is_ascii_whitespace);
+                    let can_close = ix > start
+                        && !self.text[..ix]
+                            .as_bytes()
+                            .last()
+                            .copied()
+                            .map_or(true, is_ascii_whitespace);
+
+                    // 0xFFFF_FFFF... represents the root brace context. Using None would require
+                    // storing Option<u8>, which is bigger than u8.
+                    //
+                    // These shouldn't conflict unless you have 255 levels of nesting, which is
+                    // past the intended limit anyway.
+                    //
+                    // Unbalanced braces will cause the root to be changed, which is why it gets
+                    // stored here.
+                    let brace_context =
+                        if self.brace_context_stack.len() > MATH_BRACE_CONTEXT_MAX_NESTING {
+                            self.brace_context_next as u8
+                        } else {
+                            self.brace_context_stack.last().copied().unwrap_or_else(|| {
+                                self.brace_context_stack.push(!0);
+                                !0
+                            })
+                        };
+
+                    self.tree.append_text(begin_text, ix, backslash_escaped);
+                    self.tree.append(Item {
+                        start: ix,
+                        end: ix + 1,
+                        body: ItemBody::MaybeMath(can_open, can_close, brace_context),
+                    });
+                    begin_text = ix + 1;
+                    LoopInstruction::ContinueAndSkip(0)
+                }
+                b'{' => {
+                    if self.brace_context_stack.len() == MATH_BRACE_CONTEXT_MAX_NESTING {
+                        self.brace_context_stack.push(self.brace_context_next as u8);
+                        self.brace_context_next = MATH_BRACE_CONTEXT_MAX_NESTING;
+                    } else if self.brace_context_stack.len() > MATH_BRACE_CONTEXT_MAX_NESTING {
+                        // When we reach the limit of nesting, switch from actually matching
+                        // braces to just counting them.
+                        self.brace_context_next += 1;
+                    } else if !self.brace_context_stack.is_empty() {
+                        // Store nothing if no math environment has been reached yet.
+                        self.brace_context_stack.push(self.brace_context_next as u8);
+                        self.brace_context_next += 1;
+                    }
+                    LoopInstruction::ContinueAndSkip(0)
+                }
+                b'}' => {
+                    if let &mut [ref mut top_level_context] = &mut self.brace_context_stack[..] {
+                        // Unbalanced Braces
+                        //
+                        // The initial, root top-level brace context is -1, but this is changed whenever an unbalanced
+                        // close brace is encountered:
+                        //
+                        //     This is not a math environment: $}$
+                        //     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^|^
+                        //     -1                               |-2
+                        //
+                        // To ensure this can't get parsed as math, each side of the unbalanced
+                        // brace is an irreversibly separate brace context. As long as the math
+                        // environment itself contains balanced braces, they should share a top level context.
+                        //
+                        //     Math environment contains 2+2: $}$2+2$
+                        //                                       ^^^ this is a math environment
+                        *top_level_context = top_level_context.wrapping_sub(1);
+                    } else if self.brace_context_stack.len() > MATH_BRACE_CONTEXT_MAX_NESTING {
+                        // When we exceed 25 levels of nesting, switch from accurately balancing braces
+                        // to just counting them. When we dip back below the limit, switch back.
+                        if self.brace_context_next <= MATH_BRACE_CONTEXT_MAX_NESTING {
+                            self.brace_context_stack.pop();
+                        } else {
+                            self.brace_context_next -= 1;
+                        }
+                    } else {
+                        self.brace_context_stack.pop();
+                    }
+                    LoopInstruction::ContinueAndSkip(0)
+                }
                 b'`' => {
                     self.tree.append_text(begin_text, ix, backslash_escaped);
                     backslash_escaped = false;
@@ -1454,13 +1589,7 @@ impl<'a, 'b> FirstPass<'a, 'b> {
             // 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..],
-                &|_| {
-                    None
-                },
-                self.tree.is_in_table(),
-            )?
+            scan_link_label_rest(&self.text[start + 2..], &|_| None, self.tree.is_in_table())?
         } else {
             self.parse_refdef_label(start + 2)?
         };
@@ -2122,6 +2251,11 @@ fn special_bytes(options: &Options) -> [bool; 256] {
     if options.contains(Options::ENABLE_STRIKETHROUGH) {
         bytes[b'~' as usize] = true;
     }
+    if options.contains(Options::ENABLE_MATH) {
+        bytes[b'$' as usize] = true;
+        bytes[b'{' as usize] = true;
+        bytes[b'}' as usize] = true;
+    }
     if options.contains(Options::ENABLE_SMART_PUNCTUATION) {
         for &byte in &[b'.', b'-', b'"', b'\''] {
             bytes[byte as usize] = true;
@@ -2357,6 +2491,11 @@ mod simd {
         if options.contains(Options::ENABLE_STRIKETHROUGH) {
             add_lookup_byte(&mut lookup, b'~');
         }
+        if options.contains(Options::ENABLE_MATH) {
+            add_lookup_byte(&mut lookup, b'$');
+            add_lookup_byte(&mut lookup, b'{');
+            add_lookup_byte(&mut lookup, b'}');
+        }
         if options.contains(Options::ENABLE_SMART_PUNCTUATION) {
             for &byte in &[b'.', b'-', b'"', b'\''] {
                 add_lookup_byte(&mut lookup, byte);
@@ -2503,6 +2642,7 @@ mod simd {
 
         fn check_expected_indices(bytes: &[u8], expected: &[usize], skip: usize) {
             let mut opts = Options::empty();
+            opts.insert(Options::ENABLE_MATH);
             opts.insert(Options::ENABLE_TABLES);
             opts.insert(Options::ENABLE_FOOTNOTES);
             opts.insert(Options::ENABLE_STRIKETHROUGH);
@@ -2548,6 +2688,7 @@ mod simd {
         fn exhaustive_search() {
             let chars = [
                 b'\n', b'\r', b'*', b'_', b'~', b'|', b'&', b'\\', b'[', b']', b'<', b'!', b'`',
+                b'$', b'{', b'}',
             ];
 
             for &c in &chars {
diff --git a/pulldown-cmark/src/html.rs b/pulldown-cmark/src/html.rs
index 1bcd18d..54005bd 100644
--- a/pulldown-cmark/src/html.rs
+++ b/pulldown-cmark/src/html.rs
@@ -21,13 +21,12 @@
 //! HTML renderer that takes an iterator of events as input.
 
 use std::collections::HashMap;
-use std::io::{self, Write};
 
 use crate::strings::CowStr;
 use crate::Event::*;
-use crate::{Alignment, CodeBlockKind, Event, LinkType, Tag, TagEnd};
+use crate::{Alignment, BlockQuoteKind, CodeBlockKind, Event, LinkType, Tag, TagEnd};
 use pulldown_cmark_escape::{
-    escape_href, escape_html, escape_html_body_text, StrWrite, WriteWrapper,
+    escape_href, escape_html, escape_html_body_text, FmtWriter, IoWriter, StrWrite,
 };
 
 enum TableState {
@@ -73,14 +72,15 @@ where
     }
 
     /// Writes a new line.
-    fn write_newline(&mut self) -> io::Result<()> {
+    #[inline]
+    fn write_newline(&mut self) -> Result<(), W::Error> {
         self.end_newline = true;
         self.writer.write_str("\n")
     }
 
     /// Writes a buffer, and tracks whether or not a newline was written.
     #[inline]
-    fn write(&mut self, s: &str) -> io::Result<()> {
+    fn write(&mut self, s: &str) -> Result<(), W::Error> {
         self.writer.write_str(s)?;
 
         if !s.is_empty() {
@@ -89,7 +89,7 @@ where
         Ok(())
     }
 
-    fn run(mut self) -> io::Result<()> {
+    fn run(mut self) -> Result<(), W::Error> {
         while let Some(event) = self.iter.next() {
             match event {
                 Start(tag) => {
@@ -109,6 +109,16 @@ where
                     escape_html_body_text(&mut self.writer, &text)?;
                     self.write("</code>")?;
                 }
+                InlineMath(text) => {
+                    self.write(r#"<span class="math math-inline">"#)?;
+                    escape_html(&mut self.writer, &text)?;
+                    self.write("</span>")?;
+                }
+                DisplayMath(text) => {
+                    self.write(r#"<span class="math math-display">"#)?;
+                    escape_html(&mut self.writer, &text)?;
+                    self.write("</span>")?;
+                }
                 Html(html) | InlineHtml(html) => {
                     self.write(&html)?;
                 }
@@ -146,7 +156,7 @@ where
     }
 
     /// Writes the start of an HTML tag.
-    fn start_tag(&mut self, tag: Tag<'a>) -> io::Result<()> {
+    fn start_tag(&mut self, tag: Tag<'a>) -> Result<(), W::Error> {
         match tag {
             Tag::HtmlBlock => Ok(()),
             Tag::Paragraph => {
@@ -225,11 +235,21 @@ where
                     _ => self.write(">"),
                 }
             }
-            Tag::BlockQuote => {
+            Tag::BlockQuote(kind) => {
+                let class_str = match kind {
+                    None => "",
+                    Some(kind) => match kind {
+                        BlockQuoteKind::Note => " class=\"markdown-alert-note\"",
+                        BlockQuoteKind::Tip => " class=\"markdown-alert-tip\"",
+                        BlockQuoteKind::Important => " class=\"markdown-alert-important\"",
+                        BlockQuoteKind::Warning => " class=\"markdown-alert-warning\"",
+                        BlockQuoteKind::Caution => " class=\"markdown-alert-caution\"",
+                    },
+                };
                 if self.end_newline {
-                    self.write("<blockquote>\n")
+                    self.write(&format!("<blockquote{}>\n", class_str))
                 } else {
-                    self.write("\n<blockquote>\n")
+                    self.write(&format!("\n<blockquote{}>\n", class_str))
                 }
             }
             Tag::CodeBlock(info) => {
@@ -347,7 +367,7 @@ where
         }
     }
 
-    fn end_tag(&mut self, tag: TagEnd) -> io::Result<()> {
+    fn end_tag(&mut self, tag: TagEnd) -> Result<(), W::Error> {
         match tag {
             TagEnd::HtmlBlock => {}
             TagEnd::Paragraph => {
@@ -418,7 +438,7 @@ where
     }
 
     // run raw text, consuming end tag
-    fn raw_text(&mut self) -> io::Result<()> {
+    fn raw_text(&mut self) -> Result<(), W::Error> {
         let mut nest = 0;
         while let Some(event) = self.iter.next() {
             match event {
@@ -436,6 +456,16 @@ where
                     escape_html(&mut self.writer, &text)?;
                     self.end_newline = text.ends_with('\n');
                 }
+                InlineMath(text) => {
+                    self.write("$")?;
+                    escape_html(&mut self.writer, &text)?;
+                    self.write("$")?;
+                }
+                DisplayMath(text) => {
+                    self.write("$$")?;
+                    escape_html(&mut self.writer, &text)?;
+                    self.write("$$")?;
+                }
                 SoftBreak | HardBreak | Rule => {
                     self.write(" ")?;
                 }
@@ -483,11 +513,11 @@ pub fn push_html<'a, I>(s: &mut String, iter: I)
 where
     I: Iterator<Item = Event<'a>>,
 {
-    HtmlWriter::new(iter, s).run().unwrap();
+    write_html_fmt(s, iter).unwrap()
 }
 
 /// Iterate over an `Iterator` of `Event`s, generate HTML for each `Event`, and
-/// write it out to a writable stream.
+/// write it out to an I/O stream.
 ///
 /// **Note**: using this function with an unbuffered writer like a file or socket
 /// will result in poor performance. Wrap these in a
@@ -510,7 +540,7 @@ where
 /// let mut bytes = Vec::new();
 /// let parser = Parser::new(markdown_str);
 ///
-/// html::write_html(Cursor::new(&mut bytes), parser);
+/// html::write_html_io(Cursor::new(&mut bytes), parser);
 ///
 /// assert_eq!(&String::from_utf8_lossy(&bytes)[..], r#"<h1>hello</h1>
 /// <ul>
@@ -519,10 +549,45 @@ where
 /// </ul>
 /// "#);
 /// ```
-pub fn write_html<'a, I, W>(writer: W, iter: I) -> io::Result<()>
+pub fn write_html_io<'a, I, W>(writer: W, iter: I) -> std::io::Result<()>
+where
+    I: Iterator<Item = Event<'a>>,
+    W: std::io::Write,
+{
+    HtmlWriter::new(iter, IoWriter(writer)).run()
+}
+
+/// Iterate over an `Iterator` of `Event`s, generate HTML for each `Event`, and
+/// write it into Unicode-accepting buffer or stream.
+///
+/// # Examples
+///
+/// ```
+/// use pulldown_cmark::{html, Parser};
+///
+/// let markdown_str = r#"
+/// hello
+/// =====
+///
+/// * alpha
+/// * beta
+/// "#;
+/// let mut buf = String::new();
+/// let parser = Parser::new(markdown_str);
+///
+/// html::write_html_fmt(&mut buf, parser);
+///
+/// assert_eq!(buf, r#"<h1>hello</h1>
+/// <ul>
+/// <li>alpha</li>
+/// <li>beta</li>
+/// </ul>
+/// "#);
+/// ```
+pub fn write_html_fmt<'a, I, W>(writer: W, iter: I) -> std::fmt::Result
 where
     I: Iterator<Item = Event<'a>>,
-    W: Write,
+    W: std::fmt::Write,
 {
-    HtmlWriter::new(iter, WriteWrapper(writer)).run()
+    HtmlWriter::new(iter, FmtWriter(writer)).run()
 }
diff --git a/pulldown-cmark/src/lib.rs b/pulldown-cmark/src/lib.rs
index aabf394..82c28fc 100644
--- a/pulldown-cmark/src/lib.rs
+++ b/pulldown-cmark/src/lib.rs
@@ -122,6 +122,18 @@ impl<'a> CodeBlockKind<'a> {
     }
 }
 
+/// BlockQuote kind (Note, Tip, Important, Warning, Caution).
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub enum BlockQuoteKind {
+    Note,
+    Tip,
+    Important,
+    Warning,
+    Caution,
+}
+
+/// Metadata block kind.
 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 pub enum MetadataBlockKind {
@@ -148,7 +160,7 @@ pub enum Tag<'a> {
         attrs: Vec<(CowStr<'a>, Option<CowStr<'a>>)>,
     },
 
-    BlockQuote,
+    BlockQuote(Option<BlockQuoteKind>),
     /// A code block.
     CodeBlock(CodeBlockKind<'a>),
 
@@ -207,7 +219,7 @@ impl<'a> Tag<'a> {
         match self {
             Tag::Paragraph => TagEnd::Paragraph,
             Tag::Heading { level, .. } => TagEnd::Heading(*level),
-            Tag::BlockQuote => TagEnd::BlockQuote,
+            Tag::BlockQuote(_) => TagEnd::BlockQuote,
             Tag::CodeBlock(_) => TagEnd::CodeBlock,
             Tag::HtmlBlock => TagEnd::HtmlBlock,
             Tag::List(number) => TagEnd::List(number.is_some()),
@@ -365,6 +377,12 @@ pub enum Event<'a> {
     /// An inline code node.
     #[cfg_attr(feature = "serde", serde(borrow))]
     Code(CowStr<'a>),
+    /// An inline math environment node.
+    #[cfg_attr(feature = "serde", serde(borrow))]
+    InlineMath(CowStr<'a>),
+    /// A display math environment node.
+    #[cfg_attr(feature = "serde", serde(borrow))]
+    DisplayMath(CowStr<'a>),
     /// An HTML node.
     #[cfg_attr(feature = "serde", serde(borrow))]
     Html(CowStr<'a>),
@@ -461,6 +479,13 @@ bitflags::bitflags! {
         /// literal text [^4]. In old syntax, it creates a dangling link.
         /// ```
         const ENABLE_OLD_FOOTNOTES = (1 << 9) | (1 << 2);
+        /// With this feature enabled, two events `Event::InlineMath` and `Event::DisplayMath`
+        /// are emitted that conventionally contain TeX formulas.
+        const ENABLE_MATH = 1 << 10;
+        /// Misc GitHub Flavored Markdown features not supported in CommonMark.
+        /// The following features are currently behind this tag:
+        /// - Blockquote tags ([!NOTE], [!TIP], [!IMPORTANT], [!WARNING], [!CAUTION]).
+        const ENABLE_GFM = 1 << 11;
     }
 }
 
diff --git a/pulldown-cmark/src/main.rs b/pulldown-cmark/src/main.rs
index 7a1e316..c0c2628 100644
--- a/pulldown-cmark/src/main.rs
+++ b/pulldown-cmark/src/main.rs
@@ -72,6 +72,7 @@ pub fn main() -> std::io::Result<()> {
     opts.optflag("d", "dry-run", "dry run, produce no output");
     opts.optflag("e", "events", "print event sequence instead of rendering");
     opts.optflag("T", "enable-tables", "enable GitHub-style tables");
+    opts.optflag("m", "enable-math", "enable LaTeX-style math");
     opts.optflag("F", "enable-footnotes", "enable GitHub-style footnotes");
     opts.optflag("", "enable-old-footnotes", "enable Hoedown-style footnotes");
     opts.optflag(
@@ -108,6 +109,9 @@ pub fn main() -> std::io::Result<()> {
     if matches.opt_present("enable-tables") {
         opts.insert(Options::ENABLE_TABLES);
     }
+    if matches.opt_present("enable-math") {
+        opts.insert(Options::ENABLE_MATH);
+    }
     if matches.opt_present("enable-footnotes") {
         opts.insert(Options::ENABLE_FOOTNOTES);
     }
@@ -183,5 +187,5 @@ pub fn pulldown_cmark(input: &str, opts: Options, broken_links: &mut Vec<BrokenL
     );
     let stdio = io::stdout();
     let buffer = std::io::BufWriter::with_capacity(1024 * 1024, stdio.lock());
-    let _ = html::write_html(buffer, &mut p);
+    let _ = html::write_html_io(buffer, &mut p);
 }
diff --git a/pulldown-cmark/src/parse.rs b/pulldown-cmark/src/parse.rs
index 70f7b87..e9189d3 100644
--- a/pulldown-cmark/src/parse.rs
+++ b/pulldown-cmark/src/parse.rs
@@ -30,10 +30,13 @@ use unicase::UniCase;
 
 use crate::firstpass::run_first_pass;
 use crate::linklabel::{scan_link_label_rest, FootnoteLabel, LinkLabel, ReferenceLabel};
+use crate::scanners::*;
 use crate::strings::CowStr;
 use crate::tree::{Tree, TreeIndex};
-use crate::{scanners::*, MetadataBlockKind};
-use crate::{Alignment, CodeBlockKind, Event, HeadingLevel, LinkType, Options, Tag, TagEnd};
+use crate::{
+    Alignment, BlockQuoteKind, CodeBlockKind, Event, HeadingLevel, LinkType, MetadataBlockKind,
+    Options, Tag, TagEnd,
+};
 
 // Allowing arbitrary depth nested parentheses inside link destinations
 // can create denial of service vulnerabilities if we're not careful.
@@ -49,11 +52,12 @@ pub(crate) struct Item {
     pub body: ItemBody,
 }
 
-#[derive(Debug, PartialEq, Clone, Copy)]
-#[derive(Default)]
+#[derive(Debug, PartialEq, Clone, Copy, Default)]
 pub(crate) enum ItemBody {
     Paragraph,
-    Text { backslash_escaped: bool },
+    Text {
+        backslash_escaped: bool,
+    },
     SoftBreak,
     // true = is backlash
     HardBreak(bool),
@@ -62,6 +66,8 @@ pub(crate) enum ItemBody {
 
     // repeats, can_open, can_close
     MaybeEmphasis(usize, bool, bool),
+    // can_open, can_close, brace context
+    MaybeMath(bool, bool, u8),
     // quote byte, can_open, can_close
     MaybeSmartQuote(u8, bool, bool),
     MaybeCode(usize, bool), // number of backticks, preceded by backslash
@@ -75,6 +81,7 @@ pub(crate) enum ItemBody {
     Emphasis,
     Strong,
     Strikethrough,
+    Math(CowIndex, bool), // true for display math
     Code(CowIndex),
     Link(LinkIndex),
     Image(LinkIndex),
@@ -89,7 +96,7 @@ pub(crate) enum ItemBody {
     InlineHtml,
     Html,
     OwnedHtml(CowIndex),
-    BlockQuote,
+    BlockQuote(Option<BlockQuoteKind>),
     List(bool, u8, u64), // is_tight, list character, list start index
     ListItem(usize),     // indent level
     SynthesizeText(CowIndex),
@@ -113,6 +120,7 @@ impl ItemBody {
         matches!(
             *self,
             ItemBody::MaybeEmphasis(..)
+                | ItemBody::MaybeMath(..)
                 | ItemBody::MaybeSmartQuote(..)
                 | ItemBody::MaybeHtml
                 | ItemBody::MaybeCode(..)
@@ -125,7 +133,7 @@ impl ItemBody {
         matches!(
             *self,
             ItemBody::Paragraph
-                | ItemBody::BlockQuote
+                | ItemBody::BlockQuote(..)
                 | ItemBody::List(..)
                 | ItemBody::ListItem(..)
                 | ItemBody::HtmlBlock
@@ -139,8 +147,6 @@ impl ItemBody {
     }
 }
 
-
-
 #[derive(Debug)]
 pub struct BrokenLink<'a> {
     pub span: std::ops::Range<usize>,
@@ -278,9 +284,9 @@ impl<'input, F: BrokenLinkCallback<'input>> Parser<'input, F> {
         &mut self,
         link_label: CowStr<'input>,
         span: Range<usize>,
-        link_type: LinkType
+        link_type: LinkType,
     ) -> Option<(LinkType, CowStr<'input>, CowStr<'input>)> {
-        if self.link_ref_expansion_limit <= 0 {
+        if self.link_ref_expansion_limit == 0 {
             return None;
         }
 
@@ -298,30 +304,29 @@ impl<'input, F: BrokenLinkCallback<'input>> Parser<'input, F> {
                 let url = matching_def.dest.clone();
                 (link_type, url, title)
             })
-        .or_else(|| {
-            match self.broken_link_callback.as_mut() {
-                Some(callback) => {
-                    // Construct a BrokenLink struct, which will be passed to the callback
-                    let broken_link = BrokenLink {
-                        span,
-                        link_type,
-                        reference: link_label,
-                    };
+            .or_else(|| {
+                match self.broken_link_callback.as_mut() {
+                    Some(callback) => {
+                        // Construct a BrokenLink struct, which will be passed to the callback
+                        let broken_link = BrokenLink {
+                            span,
+                            link_type,
+                            reference: link_label,
+                        };
 
-                    callback.handle_broken_link(broken_link).map(
-                        |(url, title)| {
-                            (link_type.to_unknown(), url, title)
-                        },
-                    )
+                        callback
+                            .handle_broken_link(broken_link)
+                            .map(|(url, title)| (link_type.to_unknown(), url, title))
+                    }
+                    None => None,
                 }
-                None => None,
-            }
-        })?;
+            })?;
 
         // Limit expansion from link references.
         // This isn't a problem for footnotes, because multiple references to the same one
         // reuse the same node, but links/images get their HREF/SRC copied.
-        self.link_ref_expansion_limit = self.link_ref_expansion_limit
+        self.link_ref_expansion_limit = self
+            .link_ref_expansion_limit
             .saturating_sub(url.len() + title.len());
 
         Some((link_type, url, title))
@@ -345,6 +350,7 @@ impl<'input, F: BrokenLinkCallback<'input>> Parser<'input, F> {
     /// precedence, because the URL of links must not be processed.
     fn handle_inline_pass1(&mut self) {
         let mut code_delims = CodeDelims::new();
+        let mut math_delims = MathDelims::new();
         let mut cur = self.tree.cur();
         let mut prev = None;
 
@@ -366,7 +372,9 @@ impl<'input, F: BrokenLinkCallback<'input>> Parser<'input, F> {
                         let text_node = self.tree.create_node(Item {
                             start: self.tree[cur_ix].item.start + 1,
                             end: ix - 1,
-                            body: ItemBody::Text { backslash_escaped: false },
+                            body: ItemBody::Text {
+                                backslash_escaped: false,
+                            },
                         });
                         let link_ix =
                             self.allocs
@@ -410,13 +418,100 @@ impl<'input, F: BrokenLinkCallback<'input>> Parser<'input, F> {
                             continue;
                         }
                     }
-                    self.tree[cur_ix].item.body = ItemBody::Text { backslash_escaped: false };
+                    self.tree[cur_ix].item.body = ItemBody::Text {
+                        backslash_escaped: false,
+                    };
+                }
+                ItemBody::MaybeMath(can_open, _can_close, brace_context) => {
+                    if !can_open {
+                        self.tree[cur_ix].item.body = ItemBody::Text {
+                            backslash_escaped: false,
+                        };
+                        prev = cur;
+                        cur = self.tree[cur_ix].next;
+                        continue;
+                    }
+                    let is_display = self.tree[cur_ix].next.map_or(false, |next_ix| {
+                        matches!(
+                            self.tree[next_ix].item.body,
+                            ItemBody::MaybeMath(_can_open, _can_close, _brace_context)
+                        )
+                    });
+                    let result = if math_delims.is_populated() {
+                        // we have previously scanned all math environment delimiters,
+                        // so we can reuse that work
+                        math_delims.find(&self.tree, cur_ix, is_display, brace_context)
+                    } else {
+                        // we haven't previously scanned all math delimiters,
+                        // so walk the AST
+                        let mut scan = self.tree[cur_ix].next;
+                        if is_display {
+                            // a display delimiter, `$$`, is actually two delimiters
+                            // skip the second one
+                            scan = self.tree[scan.unwrap()].next;
+                        }
+                        let mut invalid = false;
+                        while let Some(scan_ix) = scan {
+                            if let ItemBody::MaybeMath(_can_open, can_close, delim_brace_context) =
+                                self.tree[scan_ix].item.body
+                            {
+                                let delim_is_display =
+                                    self.tree[scan_ix].next.map_or(false, |next_ix| {
+                                        matches!(
+                                            self.tree[next_ix].item.body,
+                                            ItemBody::MaybeMath(
+                                                _can_open,
+                                                _can_close,
+                                                _brace_context
+                                            )
+                                        )
+                                    });
+                                if !invalid && delim_brace_context == brace_context {
+                                    if (!is_display && can_close) || (is_display && delim_is_display) {
+                                        // This will skip ahead past everything we
+                                        // just inserted. Needed for correctness to
+                                        // ensure that a new scan is done after this item.
+                                        math_delims.clear();
+                                        break;
+                                    } else {
+                                        // Math cannot contain $, so the current item
+                                        // is invalid. Keep scanning to fill math_delims.
+                                        invalid = true;
+                                    }
+                                }
+                                math_delims.insert(
+                                    delim_is_display,
+                                    delim_brace_context,
+                                    scan_ix,
+                                    can_close,
+                                );
+                            }
+                            if self.tree[scan_ix].item.body.is_block() {
+                                // If this is a tight list, blocks and inlines might be
+                                // siblings. Inlines can't cross the boundary like that.
+                                scan = None;
+                                break;
+                            }
+                            scan = self.tree[scan_ix].next;
+                        }
+                        scan
+                    };
+
+                    if let Some(scan_ix) = result {
+                        self.make_math_span(cur_ix, scan_ix);
+                    } else {
+                        self.tree[cur_ix].item.body = ItemBody::Text {
+                            backslash_escaped: false,
+                        };
+                    }
                 }
                 ItemBody::MaybeCode(mut search_count, preceded_by_backslash) => {
                     if preceded_by_backslash {
                         search_count -= 1;
                         if search_count == 0 {
-                            self.tree[cur_ix].item.body = ItemBody::Text { backslash_escaped: false };
+                            self.tree[cur_ix].item.body = ItemBody::Text {
+                                backslash_escaped: false,
+                            };
                             prev = cur;
                             cur = self.tree[cur_ix].next;
                             continue;
@@ -429,7 +524,9 @@ impl<'input, F: BrokenLinkCallback<'input>> Parser<'input, F> {
                         if let Some(scan_ix) = code_delims.find(cur_ix, search_count) {
                             self.make_code_span(cur_ix, scan_ix, preceded_by_backslash);
                         } else {
-                            self.tree[cur_ix].item.body = ItemBody::Text { backslash_escaped: false };
+                            self.tree[cur_ix].item.body = ItemBody::Text {
+                                backslash_escaped: false,
+                            };
                         }
                     } else {
                         // we haven't previously scanned all codeblock delimiters,
@@ -460,26 +557,34 @@ impl<'input, F: BrokenLinkCallback<'input>> Parser<'input, F> {
                             scan = self.tree[scan_ix].next;
                         }
                         if scan.is_none() {
-                            self.tree[cur_ix].item.body = ItemBody::Text { backslash_escaped: false };
+                            self.tree[cur_ix].item.body = ItemBody::Text {
+                                backslash_escaped: false,
+                            };
                         }
                     }
                 }
                 ItemBody::MaybeLinkOpen => {
-                    self.tree[cur_ix].item.body = ItemBody::Text { backslash_escaped: false };
+                    self.tree[cur_ix].item.body = ItemBody::Text {
+                        backslash_escaped: false,
+                    };
                     self.link_stack.push(LinkStackEl {
                         node: cur_ix,
                         ty: LinkStackTy::Link,
                     });
                 }
                 ItemBody::MaybeImage => {
-                    self.tree[cur_ix].item.body = ItemBody::Text { backslash_escaped: false };
+                    self.tree[cur_ix].item.body = ItemBody::Text {
+                        backslash_escaped: false,
+                    };
                     self.link_stack.push(LinkStackEl {
                         node: cur_ix,
                         ty: LinkStackTy::Image,
                     });
                 }
                 ItemBody::MaybeLinkClose(could_be_ref) => {
-                    self.tree[cur_ix].item.body = ItemBody::Text { backslash_escaped: false };
+                    self.tree[cur_ix].item.body = ItemBody::Text {
+                        backslash_escaped: false,
+                    };
                     if let Some(tos) = self.link_stack.pop() {
                         if tos.ty == LinkStackTy::Disabled {
                             continue;
@@ -589,7 +694,9 @@ impl<'input, F: BrokenLinkCallback<'input>> Parser<'input, F> {
                             };
 
                             let id = match &label {
-                                Some((ReferenceLabel::Link(l), _) | (ReferenceLabel::Footnote(l), _)) => l.clone(),
+                                Some(
+                                    (ReferenceLabel::Link(l), _) | (ReferenceLabel::Footnote(l), _),
+                                ) => l.clone(),
                                 None => "".into(),
                             };
 
@@ -631,11 +738,13 @@ impl<'input, F: BrokenLinkCallback<'input>> Parser<'input, F> {
                                     continue;
                                 }
                             } else if let Some((ReferenceLabel::Link(link_label), end)) = label {
-                                if let Some((def_link_type, url, title)) = self.fetch_link_type_url_title(
-                                    link_label,
-                                    (self.tree[tos.node].item.start)..end,
-                                    link_type,
-                                ) {
+                                if let Some((def_link_type, url, title)) = self
+                                    .fetch_link_type_url_title(
+                                        link_label,
+                                        (self.tree[tos.node].item.start)..end,
+                                        link_type,
+                                    )
+                                {
                                     let link_ix =
                                         self.allocs.allocate_link(def_link_type, url, title, id);
                                     self.tree[tos.node].item.body = if tos.ty == LinkStackTy::Image
@@ -706,7 +815,8 @@ impl<'input, F: BrokenLinkCallback<'input>> Parser<'input, F> {
                     let both = can_open && can_close;
                     if can_close {
                         while let Some(el) =
-                            self.inline_stack.find_match(&mut self.tree, c, run_length, both)
+                            self.inline_stack
+                                .find_match(&mut self.tree, c, run_length, both)
                         {
                             // have a match!
                             if let Some(prev_ix) = prev {
@@ -775,7 +885,9 @@ impl<'input, F: BrokenLinkCallback<'input>> Parser<'input, F> {
                             });
                         } else {
                             for i in 0..count {
-                                self.tree[cur_ix + i].item.body = ItemBody::Text { backslash_escaped: false };
+                                self.tree[cur_ix + i].item.body = ItemBody::Text {
+                                    backslash_escaped: false,
+                                };
                             }
                         }
                         prev_ix = cur_ix + count - 1;
@@ -937,7 +1049,12 @@ impl<'input, F: BrokenLinkCallback<'input>> Parser<'input, F> {
                     continue;
                 }
             }
-            if self.tree.is_in_table() && c == b'\\' && i + 2 < bytes.len() && bytes[i + 1] == b'\\' && bytes[i + 2] == b'|' {
+            if self.tree.is_in_table()
+                && c == b'\\'
+                && i + 2 < bytes.len()
+                && bytes[i + 1] == b'\\'
+                && bytes[i + 2] == b'|'
+            {
                 // this runs if there are an even number of pipes in a table
                 // if it's odd, then it gets parsed as normal
                 title.push_str(&text[mark..i]);
@@ -956,6 +1073,81 @@ impl<'input, F: BrokenLinkCallback<'input>> Parser<'input, F> {
         None
     }
 
+    fn make_math_span(&mut self, open: TreeIndex, mut close: TreeIndex) {
+        let start_is_display = self.tree[open].next.filter(|&next_ix| {
+            next_ix != close
+                && matches!(
+                    self.tree[next_ix].item.body,
+                    ItemBody::MaybeMath(_can_open, _can_close, _brace_context)
+                )
+        });
+        let end_is_display = self.tree[close].next.filter(|&next_ix| {
+            matches!(
+                self.tree[next_ix].item.body,
+                ItemBody::MaybeMath(_can_open, _can_close, _brace_context)
+            )
+        });
+        let is_display = start_is_display.is_some() && end_is_display.is_some();
+        if is_display {
+            // This unwrap() can't panic, because if the next variable were None, end_is_display would be None
+            close = self.tree[close].next.unwrap();
+            self.tree[open].next = Some(close);
+            self.tree[open].item.end += 1;
+            self.tree[close].item.start -= 1;
+        } else {
+            if self.tree[open].item.end == self.tree[close].item.start {
+                // inline math spans cannot be empty
+                self.tree[open].item.body = ItemBody::Text {
+                    backslash_escaped: false,
+                };
+                return;
+            }
+            self.tree[open].next = Some(close);
+        }
+        let span_start = self.tree[open].item.end;
+        let span_end = self.tree[close].item.start;
+
+        let bytes = self.text.as_bytes();
+        let mut buf: Option<String> = None;
+
+        let mut start_ix = span_start;
+        let mut ix = span_start;
+        while ix < span_end {
+            let c = bytes[ix];
+            if c == b'\r' || c == b'\n' {
+                ix += 1;
+                let buf = buf.get_or_insert_with(|| String::with_capacity(ix - span_start));
+                buf.push_str(&self.text[start_ix..ix]);
+                let mut line_start = LineStart::new(&bytes[ix..]);
+                let _ = scan_containers(
+                    &self.tree,
+                    &mut line_start,
+                    self.options.has_gfm_footnotes(),
+                );
+                ix += line_start.bytes_scanned();
+                start_ix = ix;
+            } else if c == b'\\' && bytes.get(ix + 1) == Some(&b'|') && self.tree.is_in_table() {
+                let buf = buf.get_or_insert_with(|| String::with_capacity(ix + 1 - span_start));
+                buf.push_str(&self.text[start_ix..ix]);
+                buf.push('|');
+                ix += 2;
+                start_ix = ix;
+            } else {
+                ix += 1;
+            }
+        }
+
+        let cow = if let Some(mut buf) = buf {
+            buf.push_str(&self.text[start_ix..span_end]);
+            buf.into()
+        } else {
+            self.text[span_start..span_end].into()
+        };
+
+        self.tree[open].item.body = ItemBody::Math(self.allocs.allocate_cow(cow), is_display);
+        self.tree[open].item.end = self.tree[close].item.end;
+        self.tree[open].next = self.tree[close].next;
+    }
 
     /// Make a code span.
     ///
@@ -993,7 +1185,7 @@ impl<'input, F: BrokenLinkCallback<'input>> Parser<'input, F> {
                 ix += 1;
             }
         }
-        
+
         let (opening, closing, all_spaces) = {
             let s = if let Some(buf) = &mut buf {
                 buf.push_str(&self.text[start_ix..span_end]);
@@ -1004,7 +1196,7 @@ impl<'input, F: BrokenLinkCallback<'input>> Parser<'input, F> {
             (
                 s.as_bytes().first() == Some(&b' '),
                 s.as_bytes().last() == Some(&b' '),
-                s.bytes().all(|b| b == b' ')
+                s.bytes().all(|b| b == b' '),
             )
         };
 
@@ -1025,7 +1217,9 @@ impl<'input, F: BrokenLinkCallback<'input>> Parser<'input, F> {
         };
 
         if preceding_backslash {
-            self.tree[open].item.body = ItemBody::Text { backslash_escaped: true };
+            self.tree[open].item.body = ItemBody::Text {
+                backslash_escaped: true,
+            };
             self.tree[open].item.end = self.tree[open].item.start + 1;
             self.tree[open].next = Some(close);
             self.tree[close].item.body = ItemBody::Code(self.allocs.allocate_cow(cow));
@@ -1087,7 +1281,7 @@ pub(crate) fn scan_containers(
     let mut i = 0;
     for &node_ix in tree.walk_spine() {
         match tree[node_ix].item.body {
-            ItemBody::BlockQuote => {
+            ItemBody::BlockQuote(..) => {
                 // `scan_blockquote_marker` saves & restores internally
                 if !line_start.scan_blockquote_marker() {
                     break;
@@ -1118,7 +1312,8 @@ impl Tree<Item> {
     pub(crate) fn append_text(&mut self, start: usize, end: usize, backslash_escaped: bool) {
         if end > start {
             if let Some(ix) = self.cur() {
-                if matches!(self[ix].item.body, ItemBody::Text { .. }) && self[ix].item.end == start {
+                if matches!(self[ix].item.body, ItemBody::Text { .. }) && self[ix].item.end == start
+                {
                     self[ix].item.end = end;
                     return;
                 }
@@ -1191,7 +1386,9 @@ impl InlineStack {
     fn pop_all(&mut self, tree: &mut Tree<Item>) {
         for el in self.stack.drain(..) {
             for i in 0..el.count {
-                tree[el.start + i].item.body = ItemBody::Text { backslash_escaped: false};
+                tree[el.start + i].item.body = ItemBody::Text {
+                    backslash_escaped: false,
+                };
             }
         }
         self.lower_bounds = [0; 9];
@@ -1265,14 +1462,19 @@ impl InlineStack {
                 if c == b'~' && run_length != el.run_length {
                     return false;
                 }
-                el.c == c && (!both && !el.both || (run_length + el.run_length) % 3 != 0 || run_length % 3 == 0)
+                el.c == c
+                    && (!both && !el.both
+                        || (run_length + el.run_length) % 3 != 0
+                        || run_length % 3 == 0)
             });
 
         if let Some((matching_ix, matching_el)) = res {
             let matching_ix = matching_ix + lowerbound;
             for el in &self.stack[(matching_ix + 1)..] {
                 for i in 0..el.count {
-                    tree[el.start + i].item.body = ItemBody::Text { backslash_escaped: false };
+                    tree[el.start + i].item.body = ItemBody::Text {
+                        backslash_escaped: false,
+                    };
                 }
             }
             self.truncate(matching_ix);
@@ -1341,15 +1543,18 @@ fn scan_link_label<'text>(
     };
     if allow_footnote_refs && b'^' == bytes[1] && bytes.get(2) != Some(&b']') {
         let linebreak_handler: &dyn Fn(&[u8]) -> Option<usize> = if gfm_footnotes {
-            &|_|  None
+            &|_| None
         } else {
             &linebreak_handler
         };
-        if let Some((byte_index, cow)) = scan_link_label_rest(&text[2..], linebreak_handler, tree.is_in_table()) {
+        if let Some((byte_index, cow)) =
+            scan_link_label_rest(&text[2..], linebreak_handler, tree.is_in_table())
+        {
             return Some((byte_index + 2, ReferenceLabel::Footnote(cow)));
         }
     }
-    let (byte_index, cow) = scan_link_label_rest(&text[1..], &linebreak_handler, tree.is_in_table())?;
+    let (byte_index, cow) =
+        scan_link_label_rest(&text[1..], &linebreak_handler, tree.is_in_table())?;
     Some((byte_index + 1, ReferenceLabel::Link(cow)))
 }
 
@@ -1457,10 +1662,7 @@ impl CodeDelims {
 
     fn insert(&mut self, count: usize, ix: TreeIndex) {
         if self.seen_first {
-            self.inner
-                .entry(count)
-                .or_default()
-                .push_back(ix);
+            self.inner.entry(count).or_default().push_back(ix);
         } else {
             // Skip the first insert, since that delimiter will always
             // be an opener and not a closer.
@@ -1487,6 +1689,69 @@ impl CodeDelims {
     }
 }
 
+/// Tracks brace contexts and delimiter length for math delimiters.
+/// Provides amortized constant-time lookups.
+struct MathDelims {
+    inner: HashMap<u8, VecDeque<(TreeIndex, bool, bool)>>,
+}
+
+impl MathDelims {
+    fn new() -> Self {
+        Self {
+            inner: Default::default(),
+        }
+    }
+
+    fn insert(
+        &mut self,
+        delim_is_display: bool,
+        brace_context: u8,
+        ix: TreeIndex,
+        can_close: bool,
+    ) {
+        self.inner.entry(brace_context).or_default().push_back((
+            ix,
+            can_close,
+            delim_is_display,
+        ));
+    }
+
+    fn is_populated(&self) -> bool {
+        !self.inner.is_empty()
+    }
+
+    fn find(
+        &mut self,
+        tree: &Tree<Item>,
+        open_ix: TreeIndex,
+        is_display: bool,
+        brace_context: u8,
+    ) -> Option<TreeIndex> {
+        while let Some((ix, can_close, delim_is_display)) =
+            self.inner.get_mut(&brace_context)?.pop_front()
+        {
+            if ix <= open_ix || (is_display && tree[open_ix].next == Some(ix)) {
+                continue;
+            }
+            let can_close = can_close && tree[open_ix].item.end != tree[ix].item.start;
+            if (!is_display && can_close) || (is_display && delim_is_display) {
+                return Some(ix);
+            }
+            // if we can't use it, leave it in the queue as a tombstone for the next
+            // thing that tries to match it
+            self.inner
+                .get_mut(&brace_context)?
+                .push_front((ix, can_close, delim_is_display));
+            break;
+        }
+        None
+    }
+
+    fn clear(&mut self) {
+        self.inner.clear();
+    }
+}
+
 #[derive(Copy, Clone, PartialEq, Eq, Debug)]
 pub(crate) struct LinkIndex(usize);
 
@@ -1764,7 +2029,7 @@ fn body_to_tag_end(body: &ItemBody) -> TagEnd {
         ItemBody::Image(..) => TagEnd::Image,
         ItemBody::Heading(level, _) => TagEnd::Heading(level),
         ItemBody::IndentCodeBlock | ItemBody::FencedCodeBlock(..) => TagEnd::CodeBlock,
-        ItemBody::BlockQuote => TagEnd::BlockQuote,
+        ItemBody::BlockQuote(..) => TagEnd::BlockQuote,
         ItemBody::HtmlBlock => TagEnd::HtmlBlock,
         ItemBody::List(_, c, _) => {
             let is_ordered = c == b'.' || c == b')';
@@ -1839,7 +2104,7 @@ fn item_to_event<'a>(item: Item, text: &'a str, allocs: &mut Allocations<'a>) ->
             Tag::CodeBlock(CodeBlockKind::Fenced(allocs.take_cow(cow_ix)))
         }
         ItemBody::IndentCodeBlock => Tag::CodeBlock(CodeBlockKind::Indented),
-        ItemBody::BlockQuote => Tag::BlockQuote,
+        ItemBody::BlockQuote(kind) => Tag::BlockQuote(kind),
         ItemBody::List(_, c, listitem_start) => {
             if c == b'.' || c == b')' {
                 Tag::List(Some(listitem_start))
@@ -1854,6 +2119,13 @@ fn item_to_event<'a>(item: Item, text: &'a str, allocs: &mut Allocations<'a>) ->
         ItemBody::Table(alignment_ix) => Tag::Table(allocs.take_alignment(alignment_ix)),
         ItemBody::FootnoteDefinition(cow_ix) => Tag::FootnoteDefinition(allocs.take_cow(cow_ix)),
         ItemBody::MetadataBlock(kind) => Tag::MetadataBlock(kind),
+        ItemBody::Math(cow_ix, is_display) => {
+            return if is_display {
+                Event::DisplayMath(allocs.take_cow(cow_ix))
+            } else {
+                Event::InlineMath(allocs.take_cow(cow_ix))
+            }
+        }
         _ => panic!("unexpected item body {:?}", item.body),
     };
 
diff --git a/pulldown-cmark/src/scanners.rs b/pulldown-cmark/src/scanners.rs
index a7eb8be..15cdd27 100644
--- a/pulldown-cmark/src/scanners.rs
+++ b/pulldown-cmark/src/scanners.rs
@@ -25,7 +25,7 @@ use std::char;
 use crate::parse::HtmlScanGuard;
 pub(crate) use crate::puncttable::{is_ascii_punctuation, is_punctuation};
 use crate::strings::CowStr;
-use crate::{entities, HeadingLevel};
+use crate::{entities, BlockQuoteKind, HeadingLevel};
 use crate::{Alignment, LinkType};
 
 use memchr::memchr;
@@ -212,6 +212,56 @@ impl<'a> LineStart<'a> {
         }
     }
 
+    fn scan_case_insensitive(&mut self, tag: &[u8]) -> bool {
+        if self.bytes.len() - self.ix < tag.len() {
+            return false;
+        }
+        let prefix = &self.bytes[self.ix..self.ix + tag.len()];
+        let ok = prefix.eq_ignore_ascii_case(tag);
+        if ok {
+            self.ix += tag.len();
+        }
+        ok
+    }
+
+    pub(crate) fn scan_blockquote_tag(&mut self) -> Option<BlockQuoteKind> {
+        let saved_ix = self.ix;
+        let tag = if self.scan_ch(b'[') && self.scan_ch(b'!') {
+            let tag = if self.scan_case_insensitive(b"note") {
+                Some(BlockQuoteKind::Note)
+            } else if self.scan_case_insensitive(b"tip") {
+                Some(BlockQuoteKind::Tip)
+            } else if self.scan_case_insensitive(b"important") {
+                Some(BlockQuoteKind::Important)
+            } else if self.scan_case_insensitive(b"warning") {
+                Some(BlockQuoteKind::Warning)
+            } else if self.scan_case_insensitive(b"caution") {
+                Some(BlockQuoteKind::Caution)
+            } else {
+                None
+            };
+            if tag.is_some() && self.scan_ch(b']') {
+                if let Some(nl) = scan_blank_line(&self.bytes[self.ix..]) {
+                    self.ix += nl;
+                    while self.scan_ch(b'>') {
+                        self.scan_space(1);
+                    }
+                    tag
+                } else {
+                    None
+                }
+            } else {
+                None
+            }
+        } else {
+            None
+        };
+        if tag.is_none() {
+            self.ix = saved_ix;
+        }
+        tag
+    }
+
     pub(crate) fn scan_blockquote_marker(&mut self) -> bool {
         let save = self.clone();
         let _ = self.scan_space(3);


Test patch:
diff --git a/pulldown-cmark/tests/lib.rs b/pulldown-cmark/tests/lib.rs
index efba374..4c16ae0 100644
--- a/pulldown-cmark/tests/lib.rs
+++ b/pulldown-cmark/tests/lib.rs
@@ -15,9 +15,11 @@ pub fn test_markdown_html(
     let mut s = String::new();
 
     let mut opts = Options::empty();
+    opts.insert(Options::ENABLE_MATH);
     opts.insert(Options::ENABLE_TABLES);
     opts.insert(Options::ENABLE_STRIKETHROUGH);
     opts.insert(Options::ENABLE_TASKLISTS);
+    opts.insert(Options::ENABLE_GFM);
     if old_footnotes {
         opts.insert(Options::ENABLE_OLD_FOOTNOTES);
     } else {
diff --git a/pulldown-cmark/tests/suite/blockquotes_tags.rs b/pulldown-cmark/tests/suite/blockquotes_tags.rs
new file mode 100644
index 0000000..77a1932
--- /dev/null
+++ b/pulldown-cmark/tests/suite/blockquotes_tags.rs
@@ -0,0 +1,241 @@
+// This file is auto-generated by the build script
+// Please, do not modify it manually
+
+use super::test_markdown_html;
+
+#[test]
+fn blockquotes_tags_test_1() {
+    let original = r##"> This is a normal blockquote without tag.
+"##;
+    let expected = r##"<blockquote><p>This is a normal blockquote without tag.</p></blockquote>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn blockquotes_tags_test_2() {
+    let original = r##"> [!NOTE]
+> Note blockquote
+"##;
+    let expected = r##"<blockquote class="markdown-alert-note"><p>Note blockquote</p></blockquote>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn blockquotes_tags_test_3() {
+    let original = r##"> [!TIP]
+> Tip blockquote
+"##;
+    let expected = r##"<blockquote class="markdown-alert-tip"><p>Tip blockquote</p></blockquote>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn blockquotes_tags_test_4() {
+    let original = r##"> [!IMPORTANT]
+> Important blockquote
+"##;
+    let expected = r##"<blockquote class="markdown-alert-important"><p>Important blockquote</p></blockquote>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn blockquotes_tags_test_5() {
+    let original = r##"> [!WARNING]
+> Warning blockquote
+"##;
+    let expected = r##"<blockquote class="markdown-alert-warning"><p>Warning blockquote</p></blockquote>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn blockquotes_tags_test_6() {
+    let original = r##"> [!CAUTION]
+> Caution blockquote
+"##;
+    let expected = r##"<blockquote class="markdown-alert-caution"><p>Caution blockquote</p></blockquote>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn blockquotes_tags_test_7() {
+    let original = r##"> [!CAUTION]
+"##;
+    let expected = r##"<blockquote class="markdown-alert-caution"></blockquote>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn blockquotes_tags_test_8() {
+    let original = r##"> [!CAUTION]
+> Line 1.
+> Line 2.
+"##;
+    let expected = r##"<blockquote class="markdown-alert-caution"><p>Line 1.
+Line 2.</p></blockquote>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn blockquotes_tags_test_9() {
+    let original = r##"> [!CAUTION]
+> Line 1.
+> [!CAUTION]
+> Line 2.
+"##;
+    let expected = r##"<blockquote class="markdown-alert-caution"><p>Line 1.
+[!CAUTION]
+Line 2.</p></blockquote>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn blockquotes_tags_test_10() {
+    let original = r##"> [!CAUTION]
+> Line 1.
+> > [!TIP]
+> Line 2.
+"##;
+    let expected = r##"<blockquote class="markdown-alert-caution"><p>Line 1.</p><blockquote class="markdown-alert-tip"><p>Line 2.</p></blockquote></blockquote>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn blockquotes_tags_test_11() {
+    let original = r##"> [!CAUTION]
+> Line 1.
+
+
+> [!TIP]
+> Line 2.
+"##;
+    let expected = r##"<blockquote class="markdown-alert-caution"><p>Line 1.</p></blockquote><blockquote class="markdown-alert-tip"><p>Line 2.</p></blockquote>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn blockquotes_tags_test_12() {
+    let original = r##"> > [!CAUTION]
+> > Line 1.
+> Line 2.
+"##;
+    let expected = r##"<blockquote><blockquote class="markdown-alert-caution"><p>Line 1.
+Line 2.</p></blockquote></blockquote>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn blockquotes_tags_test_13() {
+    let original = r##"> [!CAUTION]	
+> Line 1.
+> > [!NOTE]      
+> > Line 2.
+"##;
+    let expected = r##"<blockquote class="markdown-alert-caution"><p>Line 1.</p>
+<blockquote class="markdown-alert-note"><p>Line 2.</p></blockquote>
+</blockquote>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn blockquotes_tags_test_14() {
+    let original = r##"> [!caution]	
+> Line 1.
+> > [!note]      
+> > Line 2.
+"##;
+    let expected = r##"<blockquote class="markdown-alert-caution"><p>Line 1.</p>
+<blockquote class="markdown-alert-note"><p>Line 2.</p></blockquote>
+</blockquote>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn blockquotes_tags_test_15() {
+    let original = r##"  * loose lists
+
+    > [!NOTE]
+    > sink ships
+"##;
+    let expected = r##"<ul><li><p>loose lists</p>
+<blockquote class="markdown-alert-note"><p>sink ships</p></blockquote>
+</li></ul>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn blockquotes_tags_test_16() {
+    let original = r##"> [!NOTE]
+sink ships
+
+> [!NOTE]
+> sink ships
+
+  * loose lists
+
+    > [!NOTE]
+    sink ships
+
+    > [!NOTE]
+    > sink ships
+"##;
+    let expected = r##"<blockquote class="markdown-alert-note"><p>sink ships</p></blockquote>
+<blockquote class="markdown-alert-note"><p>sink ships</p></blockquote>
+<ul><li><p>loose lists</p>
+<blockquote class="markdown-alert-note"><p>sink ships</p></blockquote>
+<blockquote class="markdown-alert-note"><p>sink ships</p></blockquote>
+</li></ul>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn blockquotes_tags_test_17() {
+    let original = r##"  * loose lists
+
+    > [!NOTE]
+    - sink ships
+
+    > [!NOTE]
+    > - sink ships
+"##;
+    let expected = r##"<ul><li><p>loose lists</p>
+<blockquote class="markdown-alert-note"></blockquote>
+<ul><li>sink ships</li></ul>
+<blockquote class="markdown-alert-note">
+<ul><li>sink ships</li></ul></blockquote>
+</li></ul>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
diff --git a/pulldown-cmark/tests/suite/math.rs b/pulldown-cmark/tests/suite/math.rs
new file mode 100644
index 0000000..856b1f9
--- /dev/null
+++ b/pulldown-cmark/tests/suite/math.rs
@@ -0,0 +1,919 @@
+// This file is auto-generated by the build script
+// Please, do not modify it manually
+
+use super::test_markdown_html;
+
+#[test]
+fn math_test_1() {
+    let original = r##"This sentence uses `$` delimiters to show math inline: $\sqrt{3x-1}+(1+x)^2$
+$\sum_{k=1}^n a_k b_k$: Mathematical expression at head of line
+
+`\` may follow just after the first `$`: $\{1, 2, 3\}$
+"##;
+    let expected = r##"<p>This sentence uses <code>$</code> delimiters to show math inline: <span class="math math-inline">\sqrt{3x-1}+(1+x)^2</span>
+<span class="math math-inline">\sum_{k=1}^n a_k b_k</span>: Mathematical expression at head of line</p>
+<p><code>\</code> may follow just after the first <code>$</code>: <span class="math math-inline">\{1, 2, 3\}</span></p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_2() {
+    let original = r##"**The Cauchy-Schwarz Inequality**
+
+$$\left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)$$
+"##;
+    let expected = r##"<p><strong>The Cauchy-Schwarz Inequality</strong></p>
+<p><span class="math math-display">\left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)</span></p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_3() {
+    let original = r##"Oops empty $$ expression.
+
+$$$$
+"##;
+    let expected = r##"<p>Oops empty $$ expression.</p>
+<p><span class="math math-display"></span></p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_4() {
+    let original = r##"$x$$$$$$$y$$
+
+$x$$$$$$y$$
+
+$$x$$$$$$y$$
+"##;
+    let expected = r##"<p><span class="math math-inline">x</span><span class="math math-display"></span><span class="math math-display">y</span></p>
+<p><span class="math math-inline">x</span><span class="math math-display"></span><span class="math math-inline">y</span>$</p>
+<p><span class="math math-display">x</span><span class="math math-display"></span>y$$</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_5() {
+    let original = r##"$a<b>c</b>$
+
+$${a*b*c} _c_ d$$
+
+$not `code`$
+
+$![not an](/image)$
+
+$<https://not.a.link/>$
+
+$&alpha;$
+"##;
+    let expected = r##"<p><span class="math math-inline">a&lt;b&gt;c&lt;/b&gt;</span></p>
+<p><span class="math math-display">{a*b*c} _c_ d</span></p>
+<p><span class="math math-inline">not `code`</span></p>
+<p><span class="math math-inline">![not an](/image)</span></p>
+<p><span class="math math-inline">&lt;https://not.a.link/&gt;</span></p>
+<p><span class="math math-inline">&amp;alpha;</span></p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_6() {
+    let original = r##"Hello $world.
+
+Dollar at end of line$
+"##;
+    let expected = r##"<p>Hello $world.</p>
+<p>Dollar at end of line$</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_7() {
+    let original = r##"$5x + 2 =
+17$
+
+$$\left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right)
+\left( \sum_{k=1}^n b_k^2 \right)$$
+"##;
+    let expected = r##"<p><span class="math math-inline">5x + 2 =
+17</span></p>
+<p><span class="math math-display">\left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right)
+\left( \sum_{k=1}^n b_k^2 \right)</span></p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_8() {
+    let original = r##"$not a\
+hard break  
+either$
+"##;
+    let expected = r##"<p><span class="math math-inline">not a\
+hard break  
+either</span></p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_9() {
+    let original = r##"$\$$
+
+$$y = \$ x$$
+"##;
+    let expected = r##"<p><span class="math math-inline">\$</span></p>
+<p><span class="math math-display">y = \$ x</span></p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_10() {
+    let original = r##"$x $ x$
+
+$$ $ $$
+"##;
+    let expected = r##"<p>$x $ x$</p>
+<p>$$ $ $$</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_11() {
+    let original = r##"alpha$$beta$gamma$$delta
+"##;
+    let expected = r##"<p>alpha$<span class="math math-inline">beta</span>gamma$$delta</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_12() {
+    let original = r##"these are not math texts: $ y=x$, $y=x $, $
+y=x$ and $y=x
+$
+
+>The start of a line counts as whitespace $2 +
+>$
+
+While displays can start with whitespace, {${
+they should not allow inlines to do that $$2 +
+$*$
+"##;
+    let expected = r##"<p>these are not math texts: $ y=x$, $y=x $, $
+y=x$ and $y=x
+$</p>
+<blockquote>
+<p>The start of a line counts as whitespace $2 +
+$</p>
+</blockquote>
+<p>While displays can start with whitespace, {${
+they should not allow inlines to do that $$2 +
+<span class="math math-inline">*</span></p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_13() {
+    let original = r##"these are math texts: foo$y=x$bar and $y=x$bar and foo$y=x$ bar
+"##;
+    let expected = r##"<p>these are math texts: foo<span class="math math-inline">y=x</span>bar and <span class="math math-inline">y=x</span>bar and foo<span class="math math-inline">y=x</span> bar</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_14() {
+    let original = r##"math texts: $x=y$! and $x=y$? and $x=y$: and $x=y$. and $x=y$"
+
+also math texts: !$x=y$! and ?$x=y$? and :$x=y$: and .$x=y$. and "$x=y$"
+
+braces: ($x=y$) [$x=y$] {$x=y$}
+"##;
+    let expected = r##"<p>math texts: <span class="math math-inline">x=y</span>! and <span class="math math-inline">x=y</span>? and <span class="math math-inline">x=y</span>: and <span class="math math-inline">x=y</span>. and <span class="math math-inline">x=y</span>"</p>
+<p>also math texts: !<span class="math math-inline">x=y</span>! and ?<span class="math math-inline">x=y</span>? and :<span class="math math-inline">x=y</span>: and .<span class="math math-inline">x=y</span>. and "<span class="math math-inline">x=y</span>"</p>
+<p>braces: (<span class="math math-inline">x=y</span>) [<span class="math math-inline">x=y</span>] {<span class="math math-inline">x=y</span>}</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_15() {
+    let original = r##"$x=y$
+"##;
+    let expected = r##"<p><span class="math math-inline">x=y</span></p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_16() {
+    let original = r##"$a$$b$
+
+$a$$$b$$
+
+$$a$$$b$
+
+$$a$$$$b$$
+"##;
+    let expected = r##"<p><span class="math math-inline">a</span><span class="math math-inline">b</span></p>
+<p><span class="math math-inline">a</span><span class="math math-display">b</span></p>
+<p><span class="math math-display">a</span><span class="math math-inline">b</span></p>
+<p><span class="math math-display">a</span><span class="math math-display">b</span></p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_17() {
+    let original = r##"$Inline `first$ then` code
+
+`Code $first` then$ inline
+
+$$ Display `first $$ then` code
+
+`Code $$ first` then $$ display
+"##;
+    let expected = r##"<p><span class="math math-inline">Inline `first</span> then` code</p>
+<p><code>Code $first</code> then$ inline</p>
+<p><span class="math math-display"> Display `first </span> then` code</p>
+<p><code>Code $$ first</code> then $$ display</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_18() {
+    let original = r##"$x + y - z$
+
+$x + y
+- z$
+
+$$ x + y
+> z $$
+"##;
+    let expected = r##"<p><span class="math math-inline">x + y - z</span></p>
+<p>$x + y</p>
+<ul>
+<li>z$</li>
+</ul>
+<p>$$ x + y</p>
+<blockquote>
+<p>z $$</p>
+</blockquote>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_19() {
+    let original = r##"$not
+
+math$
+
+$$
+not
+
+math
+$$
+"##;
+    let expected = r##"<p>$not</p>
+<p>math$</p>
+<p>$$
+not</p>
+<p>math
+$$</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_20() {
+    let original = r##"- $not
+    - *
+  math$
+"##;
+    let expected = r##"<ul>
+<li>$not
+<ul>
+<li>
+<ul>
+<li>
+</li>
+</ul>
+</li>
+</ul>
+math$</li>
+</ul>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_21() {
+    let original = r##"This is display math:
+$$
+\text{Hello $x^2$}
+$$
+And this is inline math:
+$\text{Hello $x$ there!}$
+"##;
+    let expected = r##"<p>This is display math:
+<span class="math math-display">
+\text{Hello $x^2$}
+</span>
+And this is inline math:
+<span class="math math-inline">\text{Hello $x$ there!}</span></p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_22() {
+    let original = r##"This is not valid math: $}{$
+
+Neither is this: { $}{$ }
+
+This is: $\}\{$
+
+This is: $\}$
+
+Math environment contains 2+2: $}$2+2$
+
+Math environment contains y: $x {$ $ } $y$
+"##;
+    let expected = r##"<p>This is not valid math: $}{$</p>
+<p>Neither is this: { $}{$ }</p>
+<p>This is: <span class="math math-inline">\}\{</span></p>
+<p>This is: <span class="math math-inline">\}</span></p>
+<p>Math environment contains 2+2: $}<span class="math math-inline">2+2</span></p>
+<p>Math environment contains y: $x {$ $ } <span class="math math-inline">y</span></p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_23() {
+    let original = r##"This is not display math. It is inline math:
+
+$$\text{first $$ second}$
+
+$$$\text{first $$ second}$
+
+This is display math:
+
+$$\text{first $$ second}$$
+
+$$$\text{first $$ second}$$
+
+This is also display math, but (counterintuitively) it's allowed to be empty
+and expected to be as short as possible:
+
+$$$$\text{first $$ second}$$
+"##;
+    let expected = r##"<p>This is not display math. It is inline math:</p>
+<p>$<span class="math math-inline">\text{first $$ second}</span></p>
+<p>$$<span class="math math-inline">\text{first $$ second}</span></p>
+<p>This is display math:</p>
+<p><span class="math math-display">\text{first $$ second}</span></p>
+<p>$<span class="math math-display">\text{first $$ second}</span></p>
+<p>This is also display math, but (counterintuitively) it's allowed to be empty
+and expected to be as short as possible:</p>
+<p><span class="math math-display"></span>\text{first $$ second}$$</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_24() {
+    let original = r##"$\text{\$}$
+
+$$x$x$$
+
+${$^$$
+
+$}$$$$
+
+$}$] $$
+"##;
+    let expected = r##"<p><span class="math math-inline">\text{\$}</span></p>
+<p>$<span class="math math-inline">x</span>x$$</p>
+<p>${<span class="math math-inline">^</span>$</p>
+<p>$}<span class="math math-display"></span></p>
+<p>$}$] $$</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_25() {
+    let original = r##"$x$ $`y`$
+"##;
+    let expected = r##"<p><span class="math math-inline">x</span> <span class="math math-inline">`y`</span></p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_26() {
+    let original = r##"- $a$
+
+  ```math
+  a
+  ```
+
+  $$
+  a
+  $$
+
+- ```math
+  b
+  ```
+
+  $$
+  b
+  $$
+"##;
+    let expected = r##"<ul>
+<li>
+<p><span class="math math-inline">a</span></p>
+<pre><code class="language-math">a
+</code></pre>
+<p><span class="math math-display">
+a
+</span></p>
+</li>
+<li>
+<pre><code class="language-math">b
+</code></pre>
+<p><span class="math math-display">
+b
+</span></p>
+</li>
+</ul>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_27() {
+    let original = r##"- ![node logo](https://nodejs.org/static/images/logo.svg)
+- $x$
+"##;
+    let expected = r##"<ul>
+<li><img src="https://nodejs.org/static/images/logo.svg" alt="node logo" /></li>
+<li><span class="math math-inline">x</span></li>
+</ul>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_28() {
+    let original = r##"<details>
+
+$A = 5$
+
+$$
+A = 5
+$$
+
+</details>
+"##;
+    let expected = r##"<details>
+<p><span class="math math-inline">A = 5</span></p>
+<p><span class="math math-display">
+A = 5
+</span></p>
+</details>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_29() {
+    let original = r##"$a<b$
+
+$$a<b$$
+"##;
+    let expected = r##"<p><span class="math math-inline">a&lt;b</span></p>
+<p><span class="math math-display">a&lt;b</span></p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_30() {
+    let original = r##"[^a]
+
+[^a]: Lorem $a$
+"##;
+    let expected = r##"<p><sup class="footnote-reference"><a href="#a">1</a></sup></p>
+<div class="footnote-definition" id="a"><sup class="footnote-definition-label">1</sup>
+<p>Lorem <span class="math math-inline">a</span></p>
+</div>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_31() {
+    let original = r##"[$a$](x)
+"##;
+    let expected = r##"<p>
+<a href="x"><span class="math math-inline">a</span></a>
+</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_32() {
+    let original = r##"a$x$
+
+-$x$
+
+1$x$
+"##;
+    let expected = r##"<p>a<span class="math math-inline">x</span></p>
+<p>-<span class="math math-inline">x</span></p>
+<p>1<span class="math math-inline">x</span></p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_33() {
+    let original = r##"_$a$ equals $b$_
+
+_$a$ equals $b$_
+
+**$a$ equals $b$**
+"##;
+    let expected = r##"<p><em><span class="math math-inline">a</span> equals <span class="math math-inline">b</span></em></p>
+<p><em><span class="math math-inline">a</span> equals <span class="math math-inline">b</span></em></p>
+<p><strong><span class="math math-inline">a</span> equals <span class="math math-inline">b</span></strong></p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_34() {
+    let original = r##"$$
+a
+$$
+
+- $$
+  \text{$b$}
+  $$
+"##;
+    let expected = r##"<p><span class="math math-display">
+a
+</span>
+</p><ul>
+<li><span class="math math-display">
+\text{$b$}
+</span></li>
+</ul>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_35() {
+    let original = r##"$\{a\,b\}$
+"##;
+    let expected = r##"<p><span class="math math-inline">\{a\,b\}</span></p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_36() {
+    let original = r##"$a <b > c$
+
+$[(a+b)c](d+e)$
+
+${a}_b c_{d}$
+"##;
+    let expected = r##"<p><span class="math math-inline">a &lt;b &gt; c</span></p>
+<p><span class="math math-inline">[(a+b)c](d+e)</span></p>
+<p><span class="math math-inline">{a}_b c_{d}</span></p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_37() {
+    let original = r##"When $a \ne 0$, there are two solutions to $(ax^2 + bx + c = 0)$ and they are
+$$ x = {-b \pm \sqrt{b^2-4ac} \over 2a} $$
+"##;
+    let expected = r##"<p>When <span class="math math-inline">a \ne 0</span>, there are two solutions to <span class="math math-inline">(ax^2 + bx + c = 0)</span> and they are
+<span class="math math-display"> x = {-b \pm \sqrt{b^2-4ac} \over 2a} </span></p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_38() {
+    let original = r##"$x = \$$
+"##;
+    let expected = r##"<p><span class="math math-inline">x = \$</span></p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_39() {
+    let original = r##"_Equation $\Omega(69)$ in italic text_
+"##;
+    let expected = r##"<p><em>Equation <span class="math math-inline">\Omega(69)</span> in italic text</em></p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_40() {
+    let original = r##"$\pi$
+'$\pi$
+"$\pi$
+($\pi$
+[$\pi$
+{$\pi$
+/$\pi$
+"##;
+    let expected = r##"<p>
+<span class="math math-inline">\pi</span>
+'<span class="math math-inline">\pi</span>
+"<span class="math math-inline">\pi</span>
+(<span class="math math-inline">\pi</span>
+[<span class="math math-inline">\pi</span>
+{<span class="math math-inline">\pi</span>
+/<span class="math math-inline">\pi</span>
+</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_41() {
+    let original = r##"| first $|$ second |
+|--------|---------|
+| a ${   | }$ b    |
+"##;
+    let expected = r##"<table><thead>
+<tr><th>first $</th><th>$ second</th></tr>
+</thead><tbody>
+<tr><td>a ${</td><td>}$ b</td></tr>
+</tbody></table>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_42() {
+    let original = r##"| first $\|$ second |
+|-------------------|
+| a ${   \| }$ b    |
+"##;
+    let expected = r##"<table><thead>
+<tr><th>first <span class="math math-inline">|</span> second</th></tr>
+</thead><tbody>
+<tr><td>a <span class="math math-inline">{   | }</span> b</td></tr>
+</tbody></table>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_43() {
+    let original = r##"| Description | Test case |
+|-------------|-----------|
+| Single      | $\$       |
+| Double      | $\\$      |
+| Basic test  | $\|$      |
+| Basic test 2| $\|\|\$   |
+| Basic test 3| $x\|y\|z\$|
+| Not pipe    | $\.$      |
+| Combo       | $\.\|$    |
+| Combo 2     | $\.\|\$   |
+| Extra       | $\\\.$    |
+| Wait, what? | $\\|$     |
+| Wait, what? | $\\\|$    |
+| Wait, what? | $\\\\|$   |
+| Wait, what? | $\\\\\|$  |
+"##;
+    let expected = r##"<table><thead><tr><th>Description</th><th>Test case</th></tr></thead><tbody>
+<tr><td>Single</td><td>$$</td></tr>
+<tr><td>Double</td><td><span class="math math-inline">\\</span></td></tr>
+<tr><td>Basic test</td><td><span class="math math-inline">|</span></td></tr>
+<tr><td>Basic test 2</td><td>$||$</td></tr>
+<tr><td>Basic test 3</td><td>$x|y|z$</td></tr>
+<tr><td>Not pipe</td><td><span class="math math-inline">\.</span></td></tr>
+<tr><td>Combo</td><td><span class="math math-inline">\.|</span></td></tr>
+<tr><td>Combo 2</td><td>$.|$</td></tr>
+<tr><td>Extra</td><td><span class="math math-inline">\\\.</span></td></tr>
+<tr><td>Wait, what?</td><td><span class="math math-inline">\|</span></td></tr>
+<tr><td>Wait, what?</td><td><span class="math math-inline">\\|</span></td></tr>
+<tr><td>Wait, what?</td><td><span class="math math-inline">\\\|</span></td></tr>
+<tr><td>Wait, what?</td><td><span class="math math-inline">\\\\|</span></td></tr>
+</tbody></table>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_44() {
+    let original = r##"This is not an inline math environment: $}{$
+But, because it's nested too deeply, this is parsed as an inline math environment:
+{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{
+improperly $}{$ nested
+}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
+But this still isn't, because the braces are still counted: $}{$
+"##;
+    let expected = r##"<p>This is not an inline math environment: $}{$
+But, because it's nested too deeply, this is parsed as an inline math environment:
+{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{
+improperly <span class="math math-inline">}{</span> nested
+}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
+But this still isn't, because the braces are still counted: $}{$</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_45() {
+    let original = r##"This is also deeply nested, but, unlike the first example,
+they don't have an equal number of close braces and open braces,
+so aren't detected as math.
+{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{
+improperly $}$ nested ${$ example
+}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
+This, however, is detected ${}$
+
+${{{{{{{{{{{{{{{{{{{{{{{{{{{{{{
+another improperly nested example
+}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}$
+"##;
+    let expected = r##"<p>This is also deeply nested, but, unlike the first example,
+they don't have an equal number of close braces and open braces,
+so aren't detected as math.
+{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{
+improperly $}$ nested ${$ example
+}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
+This, however, is detected <span class="math math-inline">{}</span></p>
+<p><span class="math math-inline">{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{
+another improperly nested example
+}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}</span></p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_46() {
+    let original = r##"${}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}  20 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}  40 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}  60 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}  80 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 100 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 120 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 140 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 160 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 180 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 200 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 220 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 240 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{$ 255 brace pairs and one unclosed brace
+"##;
+    let expected = r##"<p><span class="math math-inline">{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}  20 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}  40 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}  60 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}  80 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 100 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 120 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 140 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 160 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 180 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 200 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 220 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 240 brace pairs
+{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{</span> 255 brace pairs and one unclosed brace</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn math_test_47() {
+    let original = r##"${{{{{{{{{{{{{{{{{{{{ 20 open braces
+{{{{{{{{{{{{{{{{{{{{  40 open braces
+{{{{{{{{{{{{{{{{{{{{  60 open braces
+{{{{{{{{{{{{{{{{{{{{  80 open braces
+{{{{{{{{{{{{{{{{{{{{ 100 open braces
+{{{{{{{{{{{{{{{{{{{{ 110 open braces
+{{{{{{{{{{{{{{{{{{{{ 120 open braces
+{{{{{{{{{{{{{{{{{{{{ 140 open braces
+{{{{{{{{{{{{{{{{{{{{ 160 open braces
+{{{{{{{{{{{{{{{{{{{{ 180 open braces
+{{{{{{{{{{{{{{{{{{{{ 200 open braces
+{{{{{{{{{{{{{{{{{{{{ 220 open braces
+{{{{{{{{{{{{{{{{{{{{ 240 open braces
+{{{{{{{{{{{{{{{ 255 open braces
+}}}}}}}}}}}}}}}}}}}}  20 close braces
+}}}}}}}}}}}}}}}}}}}}  40 close braces
+}}}}}}}}}}}}}}}}}}}}  60 close braces
+}}}}}}}}}}}}}}}}}}}}  80 close braces
+}}}}}}}}}}}}}}}}}}}} 100 close braces
+}}}}}}}}}}}}}}}}}}}} 120 close braces
+}}}}}}}}}}}}}}}}}}}} 140 close braces
+}}}}}}}}}}}}}}}}}}}} 160 close braces
+}}}}}}}}}}}}}}}}}}}} 180 close braces
+}}}}}}}}}}}}}}}}}}}} 200 close braces
+}}}}}}}}}}}}}}}}}}}} 220 close braces
+}}}}}}}}}}}}}}}}}}}} 240 close braces
+}}}}}}}}}}}}}}}{$ 255 close braces and one open brace
+"##;
+    let expected = r##"<p>${{{{{{{{{{{{{{{{{{{{ 20 open braces
+{{{{{{{{{{{{{{{{{{{{  40 open braces
+{{{{{{{{{{{{{{{{{{{{  60 open braces
+{{{{{{{{{{{{{{{{{{{{  80 open braces
+{{{{{{{{{{{{{{{{{{{{ 100 open braces
+{{{{{{{{{{{{{{{{{{{{ 110 open braces
+{{{{{{{{{{{{{{{{{{{{ 120 open braces
+{{{{{{{{{{{{{{{{{{{{ 140 open braces
+{{{{{{{{{{{{{{{{{{{{ 160 open braces
+{{{{{{{{{{{{{{{{{{{{ 180 open braces
+{{{{{{{{{{{{{{{{{{{{ 200 open braces
+{{{{{{{{{{{{{{{{{{{{ 220 open braces
+{{{{{{{{{{{{{{{{{{{{ 240 open braces
+{{{{{{{{{{{{{{{ 255 open braces
+}}}}}}}}}}}}}}}}}}}}  20 close braces
+}}}}}}}}}}}}}}}}}}}}  40 close braces
+}}}}}}}}}}}}}}}}}}}}  60 close braces
+}}}}}}}}}}}}}}}}}}}}  80 close braces
+}}}}}}}}}}}}}}}}}}}} 100 close braces
+}}}}}}}}}}}}}}}}}}}} 120 close braces
+}}}}}}}}}}}}}}}}}}}} 140 close braces
+}}}}}}}}}}}}}}}}}}}} 160 close braces
+}}}}}}}}}}}}}}}}}}}} 180 close braces
+}}}}}}}}}}}}}}}}}}}} 200 close braces
+}}}}}}}}}}}}}}}}}}}} 220 close braces
+}}}}}}}}}}}}}}}}}}}} 240 close braces
+}}}}}}}}}}}}}}}{$ 255 close braces and one open brace</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
diff --git a/pulldown-cmark/tests/suite/mod.rs b/pulldown-cmark/tests/suite/mod.rs
index 7ee7e64..0f4a250 100644
--- a/pulldown-cmark/tests/suite/mod.rs
+++ b/pulldown-cmark/tests/suite/mod.rs
@@ -3,11 +3,13 @@
 
 pub use super::test_markdown_html;
 
+mod blockquotes_tags;
 mod footnotes;
 mod gfm_strikethrough;
 mod gfm_table;
 mod gfm_tasklist;
 mod heading_attrs;
+mod math;
 mod metadata_blocks;
 mod old_footnotes;
 mod regression;