Rewrite SetextHeaderSyntax (dart-lang/markdown#500)
* Rewrite SetextHeaderSyntax
* Update CHANGELOG.md
* Update stats
* Fix some comments
diff --git a/pkgs/markdown/CHANGELOG.md b/pkgs/markdown/CHANGELOG.md
index 91da8b7..47cb1a5 100644
--- a/pkgs/markdown/CHANGELOG.md
+++ b/pkgs/markdown/CHANGELOG.md
@@ -21,6 +21,8 @@
* Add a new type: `Line`.
* Add a new optional parameter `parentSyntax` for `parseLines()` of
`BlockParser`, which can be used when parsing nested blocks.
+* Add a new optional parameter `disabledSetextHeading` for `parseLines()` of
+ `BlockParser`, which is used to disable the `SetextHeaderSyntax`.
## 6.0.1
diff --git a/pkgs/markdown/lib/src/block_parser.dart b/pkgs/markdown/lib/src/block_parser.dart
index 023e9cb..2564759 100644
--- a/pkgs/markdown/lib/src/block_parser.dart
+++ b/pkgs/markdown/lib/src/block_parser.dart
@@ -35,6 +35,18 @@
/// Index of the current line.
int _pos = 0;
+ /// Starting line of the last unconsumed content.
+ int _start = 0;
+
+ /// The lines from [_start] to [_pos] (inclusive), it works as a buffer for
+ /// some blocks, for example:
+ /// When the [ParagraphSyntax] parsing process is interrupted by the
+ /// [SetextHeaderSyntax], so this structure is not a paragraph but a setext
+ /// heading, then the [ParagraphSyntax.parse] does not have to retreat the
+ /// reading position, it only needs to return `null`, the [SetextHeaderSyntax]
+ /// will pick up the lines in [linesToConsume].
+ List<Line> get linesToConsume => lines.getRange(_start, _pos + 1).toList();
+
/// Whether the parser has encountered a blank line between two block-level
/// elements.
bool encounteredBlankLine = false;
@@ -114,10 +126,22 @@
BlockSyntax? get parentSyntax => _parentSyntax;
BlockSyntax? _parentSyntax;
+ /// Whether the [SetextHeadingSyntax] is disabled temporarily.
+ bool get setextHeadingDisabled => _setextHeadingDisabled;
+ bool _setextHeadingDisabled = false;
+
+ /// The [BlockSyntax] which is running now.
+ /// The value is `null` until we found the first matched [BlockSyntax].
+ BlockSyntax? get currentSyntax => _currentSyntax;
+ BlockSyntax? _currentSyntax;
+
List<Node> parseLines({
BlockSyntax? parentSyntax,
+ bool disabledSetextHeading = false,
}) {
_parentSyntax = parentSyntax;
+ _setextHeadingDisabled = disabledSetextHeading;
+
final blocks = <Node>[];
// If the `_pos` does not change before and after `parse()`, never try to
@@ -134,6 +158,7 @@
}
if (syntax.canParse(this)) {
+ _currentSyntax = syntax;
final positionBefore = _pos;
final block = syntax.parse(this);
if (block != null) {
@@ -141,6 +166,10 @@
}
neverMatch = _pos != positionBefore ? null : syntax;
+ if (block != null || syntax is EmptyBlockSyntax) {
+ _start = _pos;
+ }
+
break;
}
}
diff --git a/pkgs/markdown/lib/src/block_syntaxes/block_syntax.dart b/pkgs/markdown/lib/src/block_syntaxes/block_syntax.dart
index 298dc05..947824a 100644
--- a/pkgs/markdown/lib/src/block_syntaxes/block_syntax.dart
+++ b/pkgs/markdown/lib/src/block_syntaxes/block_syntax.dart
@@ -34,6 +34,19 @@
return childLines;
}
+ /// Returns the block which interrupts current syntax parsing if there is one,
+ /// otherwise returns `null`.
+ ///
+ /// Make sure to check if [parser] `isDone` is `false` first.
+ BlockSyntax? interruptedBy(BlockParser parser) {
+ for (final syntax in parser.blockSyntaxes) {
+ if (syntax.canParse(parser) && syntax.canEndBlock(parser)) {
+ return syntax;
+ }
+ }
+ return null;
+ }
+
/// Gets whether or not [parser]'s current line should end the previous block.
static bool isAtBlockEnd(BlockParser parser) {
if (parser.isDone) return true;
diff --git a/pkgs/markdown/lib/src/block_syntaxes/blockquote_syntax.dart b/pkgs/markdown/lib/src/block_syntaxes/blockquote_syntax.dart
index f57346f..439af43 100644
--- a/pkgs/markdown/lib/src/block_syntaxes/blockquote_syntax.dart
+++ b/pkgs/markdown/lib/src/block_syntaxes/blockquote_syntax.dart
@@ -19,10 +19,15 @@
const BlockquoteSyntax();
+ /// Whether this blockquote ends with a lazy continuation line.
+ // The definition of lazy continuation lines:
+ // https://spec.commonmark.org/0.30/#lazy-continuation-line
+ static var _lazyContinuation = false;
@override
List<Line> parseChildLines(BlockParser parser) {
// Grab all of the lines that form the blockquote, stripping off the ">".
final childLines = <Line>[];
+ _lazyContinuation = false;
while (!parser.isDone) {
final currentLine = parser.current;
@@ -47,6 +52,7 @@
}
childLines.add(Line(currentLine.content.substring(markerEnd)));
parser.advance();
+ _lazyContinuation = false;
continue;
}
@@ -65,6 +71,7 @@
(otherMatched is CodeBlockSyntax &&
!indentPattern.hasMatch(lastLine.content))) {
childLines.add(parser.current);
+ _lazyContinuation = true;
parser.advance();
} else {
break;
@@ -79,7 +86,12 @@
final childLines = parseChildLines(parser);
// Recursively parse the contents of the blockquote.
- final children = BlockParser(childLines, parser.document).parseLines();
+ final children = BlockParser(childLines, parser.document).parseLines(
+ // The setext heading underline cannot be a lazy continuation line in a
+ // block quote.
+ // https://spec.commonmark.org/0.30/#example-93
+ disabledSetextHeading: _lazyContinuation,
+ );
return Element('blockquote', children);
}
diff --git a/pkgs/markdown/lib/src/block_syntaxes/paragraph_syntax.dart b/pkgs/markdown/lib/src/block_syntaxes/paragraph_syntax.dart
index 83ae08c..affdefd 100644
--- a/pkgs/markdown/lib/src/block_syntaxes/paragraph_syntax.dart
+++ b/pkgs/markdown/lib/src/block_syntaxes/paragraph_syntax.dart
@@ -8,6 +8,7 @@
import '../patterns.dart';
import '../util.dart';
import 'block_syntax.dart';
+import 'setext_header_syntax.dart';
/// Parses paragraphs of regular text.
class ParagraphSyntax extends BlockSyntax {
@@ -27,15 +28,27 @@
bool canParse(BlockParser parser) => true;
@override
- Node parse(BlockParser parser) {
- final childLines = <String>[];
+ Node? parse(BlockParser parser) {
+ final childLines = <String>[parser.current.content];
+ parser.advance();
+ var interruptedBySetextHeading = false;
// Eat until we hit something that ends a paragraph.
- while (!BlockSyntax.isAtBlockEnd(parser)) {
+ while (!parser.isDone) {
+ final syntax = interruptedBy(parser);
+ if (syntax != null) {
+ interruptedBySetextHeading = syntax is SetextHeaderSyntax;
+ break;
+ }
childLines.add(parser.current.content);
parser.advance();
}
+ // It is not a paragraph, but a setext heading.
+ if (interruptedBySetextHeading) {
+ return null;
+ }
+
final paragraphLines = _extractReflinkDefinitions(parser, childLines);
if (paragraphLines == null) {
// Paragraph consisted solely of reference link definitions.
diff --git a/pkgs/markdown/lib/src/block_syntaxes/setext_header_syntax.dart b/pkgs/markdown/lib/src/block_syntaxes/setext_header_syntax.dart
index d4777a5..49f4eda 100644
--- a/pkgs/markdown/lib/src/block_syntaxes/setext_header_syntax.dart
+++ b/pkgs/markdown/lib/src/block_syntaxes/setext_header_syntax.dart
@@ -6,65 +6,39 @@
import '../block_parser.dart';
import '../patterns.dart';
import 'block_syntax.dart';
+import 'paragraph_syntax.dart';
/// Parses setext-style headers.
class SetextHeaderSyntax extends BlockSyntax {
@override
- RegExp get pattern => dummyPattern;
+ RegExp get pattern => setextPattern;
const SetextHeaderSyntax();
@override
bool canParse(BlockParser parser) {
- if (!_interperableAsParagraph(parser.current.content)) return false;
- var i = 1;
- while (true) {
- final nextLine = parser.peek(i);
- if (nextLine == null) {
- // We never reached an underline.
- return false;
- }
- if (setextPattern.hasMatch(nextLine.content)) {
- return true;
- }
- // Ensure that we're still in something like paragraph text.
- if (!_interperableAsParagraph(nextLine.content)) {
- return false;
- }
- i++;
+ final lastSyntax = parser.currentSyntax;
+ if (parser.setextHeadingDisabled || lastSyntax is! ParagraphSyntax) {
+ return false;
}
+ return pattern.hasMatch(parser.current.content);
}
@override
- Node parse(BlockParser parser) {
- final lines = <String>[];
- String? tag;
- while (!parser.isDone) {
- final match = setextPattern.firstMatch(parser.current.content);
- if (match == null) {
- // More text.
- lines.add(parser.current.content);
- parser.advance();
- continue;
- } else {
- // The underline.
- tag = (match[1]![0] == '=') ? 'h1' : 'h2';
- parser.advance();
- break;
- }
+ Node? parse(BlockParser parser) {
+ final lines = parser.linesToConsume;
+ if (lines.length < 2) {
+ return null;
}
- final contents = UnparsedContent(lines.join('\n').trimRight());
+ // Remove the last line which is a marker.
+ lines.removeLast();
- return Element(tag!, [contents]);
+ final marker = parser.current.content.trim();
+ final level = (marker[0] == '=') ? '1' : '2';
+ final content = lines.map((e) => e.content).join('\n').trimRight();
+
+ parser.advance();
+ return Element('h$level', [UnparsedContent(content)]);
}
-
- bool _interperableAsParagraph(String line) =>
- !(indentPattern.hasMatch(line) ||
- codeFencePattern.hasMatch(line) ||
- headerPattern.hasMatch(line) ||
- blockquotePattern.hasMatch(line) ||
- hrPattern.hasMatch(line) ||
- listPattern.hasMatch(line) ||
- emptyPattern.hasMatch(line));
}
diff --git a/pkgs/markdown/test/common_mark/setext_headings.unit b/pkgs/markdown/test/common_mark/setext_headings.unit
index dca5f93..702c72c 100644
--- a/pkgs/markdown/test/common_mark/setext_headings.unit
+++ b/pkgs/markdown/test/common_mark/setext_headings.unit
@@ -115,9 +115,10 @@
===
<<<
<blockquote>
-<p>foo</p>
+<p>foo
+bar
+===</p>
</blockquote>
-<h1>bar</h1>
>>> Setext headings - 94
- Foo
---
diff --git a/pkgs/markdown/test/gfm/setext_headings.unit b/pkgs/markdown/test/gfm/setext_headings.unit
index 4306bf4..725a175 100644
--- a/pkgs/markdown/test/gfm/setext_headings.unit
+++ b/pkgs/markdown/test/gfm/setext_headings.unit
@@ -115,9 +115,10 @@
===
<<<
<blockquote>
-<p>foo</p>
+<p>foo
+bar
+===</p>
</blockquote>
-<h1>bar</h1>
>>> Setext headings - 64
- Foo
---
diff --git a/pkgs/markdown/tool/common_mark_stats.json b/pkgs/markdown/tool/common_mark_stats.json
index 12a63e8..d009416 100644
--- a/pkgs/markdown/tool/common_mark_stats.json
+++ b/pkgs/markdown/tool/common_mark_stats.json
@@ -645,7 +645,7 @@
"90": "strict",
"91": "strict",
"92": "strict",
- "93": "fail",
+ "93": "strict",
"94": "strict",
"95": "strict",
"96": "strict",
diff --git a/pkgs/markdown/tool/common_mark_stats.txt b/pkgs/markdown/tool/common_mark_stats.txt
index eb0ac59..4bd7bda 100644
--- a/pkgs/markdown/tool/common_mark_stats.txt
+++ b/pkgs/markdown/tool/common_mark_stats.txt
@@ -19,10 +19,10 @@
8 of 8 – 100.0% Paragraphs
1 of 1 – 100.0% Precedence
21 of 21 – 100.0% Raw HTML
- 26 of 27 – 96.3% Setext headings
+ 27 of 27 – 100.0% Setext headings
2 of 2 – 100.0% Soft line breaks
11 of 11 – 100.0% Tabs
3 of 3 – 100.0% Textual content
19 of 19 – 100.0% Thematic breaks
- 644 of 652 – 98.8% TOTAL
- 626 of 644 – 97.2% TOTAL Strict
+ 645 of 652 – 98.9% TOTAL
+ 627 of 645 – 97.2% TOTAL Strict
diff --git a/pkgs/markdown/tool/gfm_stats.json b/pkgs/markdown/tool/gfm_stats.json
index 43cd122..879abf6 100644
--- a/pkgs/markdown/tool/gfm_stats.json
+++ b/pkgs/markdown/tool/gfm_stats.json
@@ -658,7 +658,7 @@
"60": "strict",
"61": "strict",
"62": "strict",
- "63": "fail",
+ "63": "strict",
"64": "strict",
"65": "strict",
"66": "strict",
diff --git a/pkgs/markdown/tool/gfm_stats.txt b/pkgs/markdown/tool/gfm_stats.txt
index c14547e..4e82a21 100644
--- a/pkgs/markdown/tool/gfm_stats.txt
+++ b/pkgs/markdown/tool/gfm_stats.txt
@@ -21,12 +21,12 @@
8 of 8 – 100.0% Paragraphs
1 of 1 – 100.0% Precedence
21 of 21 – 100.0% Raw HTML
- 26 of 27 – 96.3% Setext headings
+ 27 of 27 – 100.0% Setext headings
2 of 2 – 100.0% Soft line breaks
2 of 2 – 100.0% Strikethrough (extension)
8 of 8 – 100.0% Tables (extension)
11 of 11 – 100.0% Tabs
3 of 3 – 100.0% Textual content
19 of 19 – 100.0% Thematic breaks
- 662 of 671 – 98.7% TOTAL
- 643 of 662 – 97.1% TOTAL Strict
+ 663 of 671 – 98.8% TOTAL
+ 644 of 663 – 97.1% TOTAL Strict