Overhaul link reference definitions. The new process is similar to CommonMark.js, where reference link definitions are parsed with block nodes, and inline nodes are parsed after all block nodes have been parsed.
diff --git a/pkgs/markdown/lib/src/ast.dart b/pkgs/markdown/lib/src/ast.dart index fa623be..26cd4a7 100644 --- a/pkgs/markdown/lib/src/ast.dart +++ b/pkgs/markdown/lib/src/ast.dart
@@ -65,6 +65,19 @@ String get textContent => text; } +/// Inline content that has not been parsed into inline nodes (strong, links, +/// etc). +/// +/// These placeholder nodes should only remain in place while the block nodes +/// of a document are still being parsed, in order to gather all reference link +/// definitions. +class UnparsedContent implements Node { + final String content; + UnparsedContent(this.content); + + void accept(NodeVisitor visitor) => null; +} + /// Visitor pattern for the AST. /// /// Renderers or other AST transformers should implement this.
diff --git a/pkgs/markdown/lib/src/block_parser.dart b/pkgs/markdown/lib/src/block_parser.dart index 100cc44..13340f4 100644 --- a/pkgs/markdown/lib/src/block_parser.dart +++ b/pkgs/markdown/lib/src/block_parser.dart
@@ -190,7 +190,7 @@ /// Generates a valid HTML anchor from the inner text of [element]. static String generateAnchorHash(Element element) => - _concatenatedText(element) + element.children.first.content .toLowerCase() .trim() .replaceFirst(new RegExp(r'^[^a-z]+'), '') @@ -238,11 +238,11 @@ var match = _setextPattern.firstMatch(parser.next); var tag = (match[1][0] == '=') ? 'h1' : 'h2'; - var contents = parser.document.parseInline(parser.current); + var contents = new UnparsedContent(parser.current); parser.advance(); parser.advance(); - return new Element(tag, contents); + return new Element(tag, [contents]); } } @@ -268,8 +268,8 @@ var match = pattern.firstMatch(parser.current); parser.advance(); var level = match[1].length; - var contents = parser.document.parseInline(match[2].trim()); - return new Element('h$level', contents); + var contents = new UnparsedContent(match[2].trim()); + return new Element('h$level', [contents]); } } @@ -713,6 +713,10 @@ /// Parses paragraphs of regular text. class ParagraphSyntax extends BlockSyntax { + static final _reflinkDefinitionStart = new RegExp(r'[ ]{0,3}\['); + + static final _whitespacePattern = new RegExp(r'^\s*$'); + bool get canEndBlock => false; const ParagraphSyntax(); @@ -728,7 +732,142 @@ parser.advance(); } - var contents = parser.document.parseInline(childLines.join('\n')); - return new Element('p', contents); + var paragraphLines = _extractReflinkDefinitions(parser, childLines); + if (paragraphLines == null) { + // Paragraph consisted solely of reference link definitions. + return new Text(''); + } else { + var contents = new UnparsedContent(paragraphLines.join('\n')); + return new Element('p', [contents]); + } + } + + /// Extract reference link definitions from the front of the paragraph, and + /// return the remaining paragraph lines. + List<String> _extractReflinkDefinitions( + BlockParser parser, List<String> lines) { + bool lineStartsReflinkDefinition(int i) => + lines[i].startsWith(new RegExp(r'[ ]{0,3}\[')); + + int i = 0; + loopOverDefinitions: while (true) { + // Check for reflink definitions. + if (!lineStartsReflinkDefinition(i)) { + // It's paragraph content from here on out. + break; + } + var contents = lines[i]; + var j = i + 1; + while (j < lines.length) { + // Check to see if the _next_ line might start a new reflink definition. + // Even if it turns out not to be, but it started with a '[', then it + // is not a part of _this_ possible reflink definition. + if (lineStartsReflinkDefinition(j)) { + // Try to parse [contents] as a reflink definition. + if (_parseReflinkDefinition(parser, contents)) { + // Loop again, starting at the next possible reflink definition. + i = j; + continue loopOverDefinitions; + } else { + // Could not parse [contents] as a reflink definition. + break; + } + } else { + contents = contents + '\n' + lines[j]; + j++; + } + } + // End of the block. + if (_parseReflinkDefinition(parser, contents)) { + i = j; + break; + } + + // It may be that there is a reflink definition starting at [i], but it + // does not extend all the way to [j], such as: + // + // [link]: url // line i + // "title" + // garbage + // [link2]: url // line j + // + // In this case, [i, i+1] is a reflink definition, and the rest is + // paragraph content. + while (j >= i) { + // This isn't the most efficient loop, what with this big ole' + // Iterable allocation (`getRange`) followed by a big 'ole String + // allocation, but we + // must walk backwards, checking each range. + contents = lines.getRange(i, j).join('\n'); + if (_parseReflinkDefinition(parser, contents)) { + // That is the last reflink definition. The rest is paragraph + // content. + i = j; + break; + } + j--; + } + // The ending was not a reflink definition at all. Just paragraph + // content. + + break; + } + + if (i == lines.length) { + // No paragraph content. + return null; + } else { + // Ends with paragraph content. + return lines.sublist(i); + } + } + + // Parse [contents] as a reference link definition. + // + // Also adds the reference link definition to the document. + // + // Returns whether [contents] could be parsed as a reference link definition. + bool _parseReflinkDefinition(BlockParser parser, String contents) { + var pattern = new RegExp( + // Leading indentation. + r'''^[ ]{0,3}''' + // Reference id in brackets, and URL. + r'''\[([^\]]+)\]:\s+(?:<(\S+)>|(\S+))\s*''' + // Title in double or single quotes, or parens. + r'''("[^"]+"|'[^']+'|\([^)]+\)|)\s*$''', + multiLine: true); + var match = pattern.firstMatch(contents); + if (match == null) { + // Not a reference link definition. + return false; + } + if (match[0].length < contents.length) { + // Trailing text. No good. + return false; + } + + var label = match[1]; + var destination = match[2] ?? match[3]; + var title = match[4]; + + // The label must contain at least one non-whitespace character. + if (_whitespacePattern.hasMatch(label)) { + return false; + } + + if (title == '') { + // No title. + title = null; + } else { + // Remove "", '', or (). + title = title.substring(1, title.length - 1); + } + + // References are case-insensitive. + label = label.toLowerCase().trim(); + + parser.document.refLinks + .putIfAbsent(label, () => new Link(label, destination, title)); + return true; } }
diff --git a/pkgs/markdown/lib/src/document.dart b/pkgs/markdown/lib/src/document.dart index 270fa80..f804d12 100644 --- a/pkgs/markdown/lib/src/document.dart +++ b/pkgs/markdown/lib/src/document.dart
@@ -29,55 +29,29 @@ ..addAll(this.extensionSet.inlineSyntaxes); } - /// Parses [lines] for reference links, adding them to [refLinks] and - /// replaces their source lines with blank lines. - parseRefLinks(List<String> lines) { - // This is a hideous regex. It matches: - // [id]: http:foo.com "some title" - // Where there may whitespace in there, and where the title may be in - // single quotes, double quotes, or parentheses. - var indent = r'^[ ]{0,3}'; // Leading indentation. - var id = r'\[([^\]]+)\]'; // Reference id in [brackets]. - var quote = r'"[^"]+"'; // Title in "double quotes". - var apos = r"'[^']+'"; // Title in 'single quotes'. - var paren = r"\([^)]+\)"; // Title in (parentheses). - var pattern = - new RegExp('$indent$id:\\s+(\\S+)\\s*($quote|$apos|$paren|)\\s*\$'); - - for (var i = 0; i < lines.length; i++) { - var match = pattern.firstMatch(lines[i]); - if (match != null) { - // Parse the link. - var id = match[1]; - var url = match[2]; - var title = match[3]; - - if (title == '') { - // No title. - title = null; - } else { - // Remove "", '', or (). - title = title.substring(1, title.length - 1); - } - - // References are case-insensitive. - id = id.toLowerCase(); - - refLinks[id] = new Link(id, url, title); - - // Remove it from the output. We replace it with a blank line which will - // get consumed by later processing. - lines[i] = ''; - } - } - } - /// Parses the given [lines] of Markdown to a series of AST nodes. - List<Node> parseLines(List<String> lines) => - new BlockParser(lines, this).parseLines(); + List<Node> parseLines(List<String> lines) { + List<Node> nodes = new BlockParser(lines, this).parseLines(); + _parseInlineContent(nodes); + return nodes; + } /// Parses the given inline Markdown [text] to a series of AST nodes. List<Node> parseInline(String text) => new InlineParser(text, this).parse(); + + void _parseInlineContent(List<Node> nodes) { + for (int i = 0; i < nodes.length; i++) { + var node = nodes[i]; + if (node is UnparsedContent) { + List<Node> inlineNodes = parseInline(node.content); + nodes.removeAt(i); + nodes.insertAll(i, inlineNodes); + i += inlineNodes.length - 1; + } else if (node is Element && node.children != null) { + _parseInlineContent(node.children); + } + } + } } class Link {
diff --git a/pkgs/markdown/lib/src/html_renderer.dart b/pkgs/markdown/lib/src/html_renderer.dart index 03977ca..95ce7c1 100644 --- a/pkgs/markdown/lib/src/html_renderer.dart +++ b/pkgs/markdown/lib/src/html_renderer.dart
@@ -31,7 +31,6 @@ // Replace windows line endings with unix line endings, and split. var lines = markdown.replaceAll('\r\n', '\n').split('\n'); - document.parseRefLinks(lines); return renderToHtml(document.parseLines(lines)) + '\n'; }
diff --git a/pkgs/markdown/test/original/reference_images.unit b/pkgs/markdown/test/original/reference_images.unit index 24dac30..2444a11 100644 --- a/pkgs/markdown/test/original/reference_images.unit +++ b/pkgs/markdown/test/original/reference_images.unit
@@ -1,23 +1,27 @@ >>> image ![][foo] + [foo]: http://foo.com/foo.png <<< <p><img alt="" src="http://foo.com/foo.png" /></p> >>> alternate text ![alternate text][foo] + [foo]: http://foo.com/foo.png <<< <p><img alt="alternate text" src="http://foo.com/foo.png" /></p> >>> title ![][foo] + [foo]: http://foo.com/foo.png "optional title" <<< <p><img alt="" src="http://foo.com/foo.png" title="optional title" /></p> >>> invalid alt text ![`alt`][foo] + [foo]: http://foo.com/foo.png "optional title" <<<
diff --git a/pkgs/markdown/test/original/reference_links.unit b/pkgs/markdown/test/original/reference_links.unit index 0449da7..02a1c56 100644 --- a/pkgs/markdown/test/original/reference_links.unit +++ b/pkgs/markdown/test/original/reference_links.unit
@@ -63,3 +63,22 @@ [are]: http://foo.com <<< <p>links <a href="http://foo.com">are</a> awesome</p> +>>> reference definitions can span lines +links [are] [awesome] + +[are]: +http://foo.com +[awesome]: +http://bar.com +"Long +Title" +<<< +<p>links <a href="http://foo.com">are</a> <a href="http://bar.com" title="Long +Title">awesome</a></p> +>>> references can be defined in blocks +> links [are] awesome +> +> [are]: http://foo.com +<<< +<blockquote> +<p>links <a href="http://foo.com">are</a> awesome</p></blockquote>
diff --git a/pkgs/markdown/tool/common_mark_stats.json b/pkgs/markdown/tool/common_mark_stats.json index d40d501..a020258 100644 --- a/pkgs/markdown/tool/common_mark_stats.json +++ b/pkgs/markdown/tool/common_mark_stats.json
@@ -380,16 +380,16 @@ }, "Link reference definitions": { "157": true, - "158": false, + "158": true, "159": false, - "160": false, - "161": false, + "160": true, + "161": true, "162": true, - "163": false, + "163": true, "164": true, "165": false, "166": true, - "167": false, + "167": true, "168": true, "169": false, "170": true, @@ -397,11 +397,11 @@ "172": true, "173": true, "174": true, - "175": false, - "176": false, + "175": true, + "176": true, "177": true, - "178": false, - "179": false + "178": true, + "179": true }, "Links": { "457": true, @@ -460,7 +460,7 @@ "510": false, "511": true, "512": true, - "513": false, + "513": true, "514": true, "515": false, "516": true, @@ -552,7 +552,7 @@ "274": true, "275": true, "276": true, - "277": false, + "277": true, "278": false, "279": true, "280": true,
diff --git a/pkgs/markdown/tool/common_mark_stats.txt b/pkgs/markdown/tool/common_mark_stats.txt index 480873e..23c9e12 100644 --- a/pkgs/markdown/tool/common_mark_stats.txt +++ b/pkgs/markdown/tool/common_mark_stats.txt
@@ -12,10 +12,10 @@ 21 of 22 – 95.5% Images 10 of 12 – 83.3% Indented code blocks 1 of 1 – 100.0% Inlines - 11 of 23 – 47.8% Link reference definitions - 52 of 81 – 64.2% Links + 20 of 23 – 87.0% Link reference definitions + 53 of 81 – 65.4% Links 30 of 48 – 62.5% List items - 16 of 25 – 64.0% Lists + 17 of 25 – 68.0% Lists 8 of 8 – 100.0% Paragraphs 1 of 1 – 100.0% Precedence 15 of 21 – 71.4% Raw HTML @@ -24,4 +24,4 @@ 6 of 11 – 54.5% Tabs 3 of 3 – 100.0% Textual content 17 of 19 – 89.5% Thematic breaks - 453 of 619 – 73.2% TOTAL + 464 of 619 – 75.0% TOTAL