Merge pull request dart-lang/markdown#105 from dart-lang/strong

Find and fix a gnarly oversight with using the wrong 'Element' class
diff --git a/pkgs/markdown/lib/src/block_parser.dart b/pkgs/markdown/lib/src/block_parser.dart
index ee8ada4..6e1cf9f 100644
--- a/pkgs/markdown/lib/src/block_parser.dart
+++ b/pkgs/markdown/lib/src/block_parser.dart
@@ -60,6 +60,10 @@
   /// Index of the current line.
   int _pos = 0;
 
+  /// Whether the parser has encountered a blank line between two block-level
+  /// elements.
+  bool encounteredBlankLine = false;
+
   /// The collection of built-in block parsers.
   final List<BlockSyntax> standardBlockSyntaxes = [
     const EmptyBlockSyntax(),
@@ -128,6 +132,21 @@
     if (next == null) return false;
     return regex.firstMatch(next) != null;
   }
+
+  List<Node> parseLines() {
+    var blocks = <Node>[];
+    while (!isDone) {
+      for (var syntax in blockSyntaxes) {
+        if (syntax.canParse(this)) {
+          var block = syntax.parse(this);
+          if (block != null) blocks.add(block);
+          break;
+        }
+      }
+    }
+
+    return blocks;
+  }
 }
 
 abstract class BlockSyntax {
@@ -145,7 +164,7 @@
   Node parse(BlockParser parser);
 
   List<String> parseChildLines(BlockParser parser) {
-    // Grab all of the lines that form the blockquote, stripping off the ">".
+    // Grab all of the lines that form the block element.
     var childLines = <String>[];
 
     while (!parser.isDone) {
@@ -185,6 +204,7 @@
   const EmptyBlockSyntax();
 
   Node parse(BlockParser parser) {
+    parser.encounteredBlankLine = true;
     parser.advance();
 
     // Don't actually emit anything.
@@ -199,7 +219,14 @@
   bool canParse(BlockParser parser) {
     // Note: matches *next* line, not the current one. We're looking for the
     // underlining after this line.
-    return parser.matchesNext(_setextPattern);
+    return parser.matchesNext(_setextPattern) &&
+        // The current line must look like a paragraph.
+        !(parser.matches(_codePattern) ||
+            parser.matches(_headerPattern) ||
+            parser.matches(_blockquotePattern) ||
+            parser.matches(_hrPattern) ||
+            parser.matches(_ulPattern) ||
+            parser.matches(_olPattern));
   }
 
   Node parse(BlockParser parser) {
@@ -258,6 +285,36 @@
 
   const BlockquoteSyntax();
 
+  List<String> parseChildLines(BlockParser parser) {
+    // Grab all of the lines that form the blockquote, stripping off the ">".
+    var childLines = <String>[];
+
+    while (!parser.isDone) {
+      var match = pattern.firstMatch(parser.current);
+      if (match != null) {
+        childLines.add(match[1]);
+        parser.advance();
+        continue;
+      }
+
+      // A paragraph continuation is OK. This is content that cannot be parsed
+      // as any other syntax except Paragraph, and it doesn't match the bar in
+      // a Setext header.
+      if (parser.blockSyntaxes.firstWhere((s) => s.canParse(parser))
+          is ParagraphSyntax) {
+        var continuedLine = childLines.last + parser.current;
+        childLines
+          ..removeLast()
+          ..add(continuedLine);
+        parser.advance();
+      } else {
+        break;
+      }
+    }
+
+    return childLines;
+  }
+
   Node parse(BlockParser parser) {
     var childLines = parseChildLines(parser);
 
@@ -527,101 +584,67 @@
         // Done with the list.
         break;
       } else {
-        // Anything else is paragraph text or other stuff that can be in a list
-        // item. However, if the previous item is a blank line, this means we're
-        // done with the list and are starting a new top-level paragraph.
-        if ((childLines.length > 0) && (childLines.last == '')) break;
-        childLines.add(parser.current);
+        // If the previous item is a blank line, this means we're done with the
+        // list and are starting a new top-level paragraph.
+        if ((childLines.isNotEmpty) && (childLines.last == '')) break;
+
+        // Anything else is paragraph continuation text.
+        var continuedLine = childLines.last + parser.current;
+        childLines
+          ..removeLast()
+          ..add(continuedLine);
       }
       parser.advance();
     }
 
     endItem();
-    determineBlockItems(items);
     var itemNodes = <Node>[];
 
+    var anyEmptyLines = removeTrailingEmptyLines(items);
+    var anyEmptyLinesBetweenBlocks = false;
+
     for (var item in items) {
-      if (item.forceBlock) {
-        // Block list item.
-        var children = parser.document.parseLines(item.lines);
-        itemNodes.add(new Element('li', children));
-      } else {
-        // Raw list item.
-        var contents = parser.document.parseInline(item.lines[0]);
-        itemNodes.add(new Element('li', contents));
+      var itemParser = new BlockParser(item.lines, parser.document);
+      var children = itemParser.parseLines();
+      itemNodes.add(new Element('li', children));
+      anyEmptyLinesBetweenBlocks =
+          anyEmptyLinesBetweenBlocks || itemParser.encounteredBlankLine;
+    }
+
+    // Must strip paragraph tags if the list is "tight".
+    // http://spec.commonmark.org/0.25/#lists
+    var listIsTight = !anyEmptyLines && !anyEmptyLinesBetweenBlocks;
+
+    if (listIsTight) {
+      // We must post-process the list items, converting any top-level paragraph
+      // elements to just text elements.
+      for (var item in itemNodes) {
+        for (var i = 0; i < item.children.length; i++) {
+          var child = item.children[i];
+          if (child is Element && child.tag == 'p') {
+            item.children.removeAt(i);
+            item.children.insertAll(i, child.children);
+          }
+        }
       }
     }
 
     return new Element(listTag, itemNodes);
   }
 
-  /// Determines whether each item in [items] is a block item.
-  ///
-  /// Also removes any trailing empty lines and notes which items are separated
-  /// by empty lines.
-  void determineBlockItems(List items) {
-    // Markdown, because it hates us, specifies two kinds of list items. If you
-    // have a list like:
-    //
-    // * one
-    // * two
-    //
-    // Then it will insert the contents of the lines directly in the <li>, like:
-    //
-    // <ul>
-    //   <li>one</li>
-    //   <li>two</li>
-    // <ul>
-    //
-    // If, however, there are blank lines between the items, each is wrapped in
-    // paragraphs:
-    //
-    // * one
-    //
-    // * two
-    //
-    // <ul>
-    //   <li><p>one</p></li>
-    //   <li><p>two</p></li>
-    // <ul>
-    //
-    // In other words, sometimes we parse the contents of a list item like a
-    // block, and sometimes line an inline. The rules our parser implements are:
-    //
-    // - If it has more than one line, it's a block.
-    // - If the line matches any block parser (BLOCKQUOTE, HEADER, HR, INDENT,
-    //   UL, OL) it's a block. (This is for cases like "* > quote".)
-    // - If there was a blank line between this item and the previous one, it's
-    //   a block.
-    // - If there was a blank line between this item and the next one, it's a
-    //   block.
-    // - Otherwise, parse it as an inline.
-
-    // Remove any trailing empty lines and note which items are separated by
-    // empty lines. Do this before seeing which items are single-line so that
-    // trailing empty lines on the last item don't force it into being a block.
+  /// Removes any trailing empty lines and notes whether any items are separated
+  /// by such lines.
+  bool removeTrailingEmptyLines(List items) {
+    var anyEmpty = false;
     for (var i = 0; i < items.length; i++) {
-      for (var j = items[i].lines.length - 1; j > 0; j--) {
-        if (!_emptyPattern.hasMatch(items[i].lines[j])) break;
-
-        // Found an empty line. This item and the one after it are blocks.
+      while (_emptyPattern.hasMatch(items[i].lines.last)) {
         if (i < items.length - 1) {
-          items[i].forceBlock = true;
-          items[i + 1].forceBlock = true;
+          anyEmpty = true;
         }
         items[i].lines.removeLast();
       }
-
-      // Items with more than one line are block items.
-      items[i].forceBlock = items[i].forceBlock || items[i].lines.length > 1;
-
-      if (items[i].forceBlock) continue;
-
-      // Items (even one-lined items) that start with a block syntax are block
-      // items.
-      items[i].forceBlock =
-          blocksInList.any((p) => p.hasMatch(items[i].lines[0]));
     }
+    return anyEmpty;
   }
 }
 
diff --git a/pkgs/markdown/lib/src/document.dart b/pkgs/markdown/lib/src/document.dart
index 8720ff0..270fa80 100644
--- a/pkgs/markdown/lib/src/document.dart
+++ b/pkgs/markdown/lib/src/document.dart
@@ -73,22 +73,8 @@
   }
 
   /// Parses the given [lines] of Markdown to a series of AST nodes.
-  List<Node> parseLines(List<String> lines) {
-    var parser = new BlockParser(lines, this);
-
-    var blocks = <Node>[];
-    while (!parser.isDone) {
-      for (var syntax in parser.blockSyntaxes) {
-        if (syntax.canParse(parser)) {
-          var block = syntax.parse(parser);
-          if (block != null) blocks.add(block);
-          break;
-        }
-      }
-    }
-
-    return blocks;
-  }
+  List<Node> parseLines(List<String> lines) =>
+      new BlockParser(lines, this).parseLines();
 
   /// Parses the given inline Markdown [text] to a series of AST nodes.
   List<Node> parseInline(String text) => new InlineParser(text, this).parse();
diff --git a/pkgs/markdown/test/original/block_quotes.unit b/pkgs/markdown/test/original/block_quotes.unit
index 26d8ed7..14107ef 100644
--- a/pkgs/markdown/test/original/block_quotes.unit
+++ b/pkgs/markdown/test/original/block_quotes.unit
@@ -25,3 +25,18 @@
 <p>two</p>
 <blockquote>
 <p>three</p></blockquote></blockquote></blockquote>
+>>> quote turns what might be an h1 into nothing
+> quote
+===
+
+<<<
+<blockquote>
+<p>quote===</p></blockquote>
+>>> quote turns what might be an h2 into an hr
+> quote
+---
+
+<<<
+<blockquote>
+<p>quote</p></blockquote>
+<hr />
diff --git a/pkgs/markdown/test/original/setext_headers.unit b/pkgs/markdown/test/original/setext_headers.unit
index 6e610cf..65038f0 100644
--- a/pkgs/markdown/test/original/setext_headers.unit
+++ b/pkgs/markdown/test/original/setext_headers.unit
@@ -20,27 +20,3 @@
 
 <<<
 <p>-</p>
->>> h1 turns preceding list into text
-- list
-===
-
-<<<
-<h1>- list</h1>
->>> h2 turns preceding list into text
-- list
-===
-
-<<<
-<h1>- list</h1>
->>> h1 turns preceding blockquote into text
-> quote
-===
-
-<<<
-<h1>> quote</h1>
->>> h2 turns preceding blockquote into text
-> quote
-===
-
-<<<
-<h1>> quote</h1>
diff --git a/pkgs/markdown/test/original/unordered_lists.unit b/pkgs/markdown/test/original/unordered_lists.unit
index 90f305a..4729287 100644
--- a/pkgs/markdown/test/original/unordered_lists.unit
+++ b/pkgs/markdown/test/original/unordered_lists.unit
@@ -35,7 +35,8 @@
 *   three
 
 <<<
-<ul><li>one</li><li>
+<ul><li>
+<p>one</p></li><li>
 <p>two</p></li><li>
 <p>three</p></li></ul>
 >>> do not force paragraph if item is already block
@@ -66,9 +67,8 @@
 *   three
 
 <<<
-<ul><li>
-<p>one
-two</p></li><li>three</li></ul>
+<ul><li>one
+two</li><li>three</li></ul>
 >>> can nest lists
 *   one
     * nested one
@@ -80,3 +80,16 @@
 <ul><li>
 <p>one</p><ul><li>nested one</li><li>nested two</li></ul></li><li>
 <p>two</p></li></ul>
+>>> list item turns what might be an h1 into nothing
+- list
+===
+
+<<<
+<ul><li>list===</li></ul>
+>>> list item turns what might be an h2 into nothing
+- list
+---
+
+<<<
+<ul><li>list</li></ul>
+<hr />