introduce a Line class (dart-lang/markdown#494)
* introduce a Line class
* Update CHANGELOG
* maintain backwards compatibility
* Update CHANGELOG
* Improve CHANGELOG.md
* Improve CHANGELOG.md
* Add more comment for tabRemaining
* Remove toMap from Line
* set `isBlankLine` of `Line` as final
* remove extension name LineX
diff --git a/pkgs/markdown/CHANGELOG.md b/pkgs/markdown/CHANGELOG.md
index 38d261b..fead92f 100644
--- a/pkgs/markdown/CHANGELOG.md
+++ b/pkgs/markdown/CHANGELOG.md
@@ -6,6 +6,9 @@
`indicatorForCheckedCheckBox`, and `indicatorForUncheckedCheckBox`.
* **Breaking change**: Removed `BlockHtmlSyntax`, `BlockTagBlockHtmlSyntax`,
`LongBlockHtmlSyntax`, and `OtherTagBlockHtmlSyntax`.
+* **Breaking change**: Change the `line` properties of type `String` to `Line`.
+* **Breaking change**: Change the `lines` properties of type `List<String>` to
+ `List<Line>`.
* Add a new syntax `HtmlBlockSyntax` to parse HTML blocks.
* Add a new syntax `DecodeHtmlSyntax` to decode HTML entity and numeric
character references.
@@ -13,6 +16,9 @@
line ending.
* Add a new syntax `EscapeHtmlSyntax` to encode (`"`), (`<`), (`>`) and (`&`).
* Add an option `caseSensitive` to `TextSyntax`.
+* Add a new public method `parse(String text)` for `Document`.
+* Add a new public method `parseLineList(List<Line> text)` for `Document`.
+* Add a new type: `Line`.
## 6.0.1
diff --git a/pkgs/markdown/lib/markdown.dart b/pkgs/markdown/lib/markdown.dart
index 3e435af..0db8af7 100644
--- a/pkgs/markdown/lib/markdown.dart
+++ b/pkgs/markdown/lib/markdown.dart
@@ -82,5 +82,6 @@
export 'src/inline_syntaxes/soft_line_break_syntax.dart';
export 'src/inline_syntaxes/strikethrough_syntax.dart';
export 'src/inline_syntaxes/text_syntax.dart';
+export 'src/line.dart';
const version = packageVersion;
diff --git a/pkgs/markdown/lib/src/block_parser.dart b/pkgs/markdown/lib/src/block_parser.dart
index 9289ae1..5d930fe 100644
--- a/pkgs/markdown/lib/src/block_parser.dart
+++ b/pkgs/markdown/lib/src/block_parser.dart
@@ -16,11 +16,12 @@
import 'block_syntaxes/setext_header_syntax.dart';
import 'block_syntaxes/unordered_list_syntax.dart';
import 'document.dart';
+import 'line.dart';
/// Maintains the internal state needed to parse a series of lines into blocks
/// of Markdown suitable for further inline parsing.
class BlockParser {
- final List<String> lines;
+ final List<Line> lines;
/// The Markdown document this parser is parsing.
final Document document;
@@ -63,10 +64,10 @@
}
/// Gets the current line.
- String get current => lines[_pos];
+ Line get current => lines[_pos];
/// Gets the line after the current one or `null` if there is none.
- String? get next {
+ Line? get next {
// Don't read past the end.
if (_pos >= lines.length - 1) return null;
return lines[_pos + 1];
@@ -78,7 +79,7 @@
/// `peek(0)` is equivalent to [current].
///
/// `peek(1)` is equivalent to [next].
- String? peek(int linesAhead) {
+ Line? peek(int linesAhead) {
if (linesAhead < 0) {
throw ArgumentError('Invalid linesAhead: $linesAhead; must be >= 0.');
}
@@ -100,13 +101,13 @@
/// Gets whether or not the current line matches the given pattern.
bool matches(RegExp regex) {
if (isDone) return false;
- return regex.hasMatch(current);
+ return regex.hasMatch(current.content);
}
/// Gets whether or not the next line matches the given pattern.
bool matchesNext(RegExp regex) {
if (next == null) return false;
- return regex.hasMatch(next!);
+ return regex.hasMatch(next!.content);
}
List<Node> parseLines() {
diff --git a/pkgs/markdown/lib/src/block_syntaxes/block_syntax.dart b/pkgs/markdown/lib/src/block_syntaxes/block_syntax.dart
index f33195d..298dc05 100644
--- a/pkgs/markdown/lib/src/block_syntaxes/block_syntax.dart
+++ b/pkgs/markdown/lib/src/block_syntaxes/block_syntax.dart
@@ -4,6 +4,7 @@
import '../ast.dart';
import '../block_parser.dart';
+import '../line.dart';
abstract class BlockSyntax {
const BlockSyntax();
@@ -14,19 +15,19 @@
bool canEndBlock(BlockParser parser) => true;
bool canParse(BlockParser parser) {
- return pattern.hasMatch(parser.current);
+ return pattern.hasMatch(parser.current.content);
}
Node? parse(BlockParser parser);
- List<String?> parseChildLines(BlockParser parser) {
+ List<Line?> parseChildLines(BlockParser parser) {
// Grab all of the lines that form the block element.
- final childLines = <String?>[];
+ final childLines = <Line?>[];
while (!parser.isDone) {
- final match = pattern.firstMatch(parser.current);
+ final match = pattern.firstMatch(parser.current.content);
if (match == null) break;
- childLines.add(match[1]);
+ childLines.add(parser.current);
parser.advance();
}
diff --git a/pkgs/markdown/lib/src/block_syntaxes/blockquote_syntax.dart b/pkgs/markdown/lib/src/block_syntaxes/blockquote_syntax.dart
index 62f1903..f57346f 100644
--- a/pkgs/markdown/lib/src/block_syntaxes/blockquote_syntax.dart
+++ b/pkgs/markdown/lib/src/block_syntaxes/blockquote_syntax.dart
@@ -5,6 +5,7 @@
import '../ast.dart';
import '../block_parser.dart';
import '../charcode.dart';
+import '../line.dart';
import '../patterns.dart';
import '../util.dart';
import 'block_syntax.dart';
@@ -19,32 +20,32 @@
const BlockquoteSyntax();
@override
- List<String> parseChildLines(BlockParser parser) {
+ List<Line> parseChildLines(BlockParser parser) {
// Grab all of the lines that form the blockquote, stripping off the ">".
- final childLines = <String>[];
+ final childLines = <Line>[];
while (!parser.isDone) {
final currentLine = parser.current;
- final match = pattern.firstMatch(parser.current);
+ final match = pattern.firstMatch(parser.current.content);
if (match != null) {
// A block quote marker consists of a `>` together with an optional
// following space of indentation, see
// https://spec.commonmark.org/0.30/#block-quote-marker.
final markerStart = match.match.indexOf('>');
int markerEnd;
- if (currentLine.length > 1) {
+ if (currentLine.content.length > 1) {
var hasSpace = false;
// Check if there is a following space if the marker is not at the end
// of this line.
- if (markerStart < currentLine.length - 1) {
- final nextChar = currentLine.codeUnitAt(markerStart + 1);
+ if (markerStart < currentLine.content.length - 1) {
+ final nextChar = currentLine.content.codeUnitAt(markerStart + 1);
hasSpace = nextChar == $tab || nextChar == $space;
}
markerEnd = markerStart + (hasSpace ? 2 : 1);
} else {
markerEnd = markerStart + 1;
}
- childLines.add(currentLine.substring(markerEnd));
+ childLines.add(Line(currentLine.content.substring(markerEnd)));
parser.advance();
continue;
}
@@ -59,10 +60,10 @@
final otherMatched =
parser.blockSyntaxes.firstWhere((s) => s.canParse(parser));
if ((otherMatched is ParagraphSyntax &&
- lastLine.isNotEmpty &&
- !codeFencePattern.hasMatch(lastLine)) ||
+ !lastLine.isBlankLine &&
+ !codeFencePattern.hasMatch(lastLine.content)) ||
(otherMatched is CodeBlockSyntax &&
- !indentPattern.hasMatch(lastLine))) {
+ !indentPattern.hasMatch(lastLine.content))) {
childLines.add(parser.current);
parser.advance();
} else {
diff --git a/pkgs/markdown/lib/src/block_syntaxes/code_block_syntax.dart b/pkgs/markdown/lib/src/block_syntaxes/code_block_syntax.dart
index 326e0b2..f9e88d2 100644
--- a/pkgs/markdown/lib/src/block_syntaxes/code_block_syntax.dart
+++ b/pkgs/markdown/lib/src/block_syntaxes/code_block_syntax.dart
@@ -4,6 +4,7 @@
import '../ast.dart';
import '../block_parser.dart';
+import '../line.dart';
import '../patterns.dart';
import '../util.dart';
import 'block_syntax.dart';
@@ -19,21 +20,21 @@
const CodeBlockSyntax();
@override
- List<String?> parseChildLines(BlockParser parser) {
- final childLines = <String?>[];
+ List<Line> parseChildLines(BlockParser parser) {
+ final childLines = <Line>[];
while (!parser.isDone) {
- final isBlankLine = parser.current.isBlank;
+ final isBlankLine = parser.current.isBlankLine;
if (isBlankLine && _shouldEnd(parser)) {
break;
}
if (!isBlankLine &&
childLines.isNotEmpty &&
- pattern.hasMatch(parser.current) != true) {
+ pattern.hasMatch(parser.current.content) != true) {
break;
}
- childLines.add(parser.current.dedent().text);
+ childLines.add(Line(parser.current.content.dedent().text));
parser.advance();
}
@@ -46,9 +47,9 @@
final childLines = parseChildLines(parser);
// The Markdown tests expect a trailing newline.
- childLines.add('');
+ childLines.add(Line(''));
- var content = childLines.join('\n');
+ var content = childLines.map((e) => e.content).join('\n');
if (parser.document.encodeHtml) {
content = escapeHtml(content);
}
@@ -67,12 +68,12 @@
// It does not matter how many blank lines between chunks:
// https://spec.commonmark.org/0.30/#example-111
- if (nextLine.isBlank) {
+ if (nextLine.isBlankLine) {
i++;
continue;
}
- return pattern.hasMatch(nextLine) == false;
+ return pattern.hasMatch(nextLine.content) == false;
}
}
}
diff --git a/pkgs/markdown/lib/src/block_syntaxes/dummy_block_syntax.dart b/pkgs/markdown/lib/src/block_syntaxes/dummy_block_syntax.dart
index 46ac98e..c08c8a8 100644
--- a/pkgs/markdown/lib/src/block_syntaxes/dummy_block_syntax.dart
+++ b/pkgs/markdown/lib/src/block_syntaxes/dummy_block_syntax.dart
@@ -27,7 +27,7 @@
final childLines = <String>[];
while (!BlockSyntax.isAtBlockEnd(parser)) {
- childLines.add(parser.current);
+ childLines.add(parser.current.content);
parser.advance();
}
diff --git a/pkgs/markdown/lib/src/block_syntaxes/fenced_blockquote_syntax.dart b/pkgs/markdown/lib/src/block_syntaxes/fenced_blockquote_syntax.dart
index d4a3592..9e18d42 100644
--- a/pkgs/markdown/lib/src/block_syntaxes/fenced_blockquote_syntax.dart
+++ b/pkgs/markdown/lib/src/block_syntaxes/fenced_blockquote_syntax.dart
@@ -4,6 +4,7 @@
import '../ast.dart';
import '../block_parser.dart';
+import '../line.dart';
import '../patterns.dart';
import 'block_syntax.dart';
@@ -15,12 +16,12 @@
RegExp get pattern => blockquoteFencePattern;
@override
- List<String> parseChildLines(BlockParser parser) {
- final childLines = <String>[];
+ List<Line> parseChildLines(BlockParser parser) {
+ final childLines = <Line>[];
parser.advance();
while (!parser.isDone) {
- final match = pattern.hasMatch(parser.current);
+ final match = pattern.hasMatch(parser.current.content);
if (!match) {
childLines.add(parser.current);
parser.advance();
diff --git a/pkgs/markdown/lib/src/block_syntaxes/fenced_code_block_syntax.dart b/pkgs/markdown/lib/src/block_syntaxes/fenced_code_block_syntax.dart
index 931fb88..3677ca5 100644
--- a/pkgs/markdown/lib/src/block_syntaxes/fenced_code_block_syntax.dart
+++ b/pkgs/markdown/lib/src/block_syntaxes/fenced_code_block_syntax.dart
@@ -4,6 +4,7 @@
import '../ast.dart';
import '../block_parser.dart';
+import '../line.dart';
import '../patterns.dart';
import '../util.dart';
import 'block_syntax.dart';
@@ -20,14 +21,14 @@
@override
Node parse(BlockParser parser) {
final openingFence = _FenceMatch.fromMatch(pattern.firstMatch(
- escapePunctuation(parser.current),
+ escapePunctuation(parser.current.content),
)!);
var text = parseChildLines(
parser,
openingFence.marker,
openingFence.indent,
- ).join('\n');
+ ).map((e) => e.content).join('\n');
if (parser.document.encodeHtml) {
text = escapeHtml(text);
@@ -54,18 +55,18 @@
}
@override
- List<String> parseChildLines(
+ List<Line> parseChildLines(
BlockParser parser, [
String openingMarker = '',
int indent = 0,
]) {
- final childLines = <String>[];
+ final childLines = <Line>[];
parser.advance();
_FenceMatch? closingFence;
while (!parser.isDone) {
- final match = pattern.firstMatch(parser.current);
+ final match = pattern.firstMatch(parser.current.content);
closingFence = match == null ? null : _FenceMatch.fromMatch(match);
// Closing code fences cannot have info strings:
@@ -73,7 +74,9 @@
if (closingFence == null ||
!closingFence.marker.startsWith(openingMarker) ||
closingFence.hasInfo) {
- childLines.add(_removeIndentation(parser.current, indent));
+ childLines.add(
+ Line(_removeIndentation(parser.current.content, indent)),
+ );
parser.advance();
} else {
parser.advance();
@@ -85,7 +88,7 @@
// https://spec.commonmark.org/0.30/#example-128
if (closingFence == null &&
childLines.isNotEmpty &&
- childLines.last.trim().isEmpty) {
+ childLines.last.isBlankLine) {
childLines.removeLast();
}
diff --git a/pkgs/markdown/lib/src/block_syntaxes/header_syntax.dart b/pkgs/markdown/lib/src/block_syntaxes/header_syntax.dart
index b68268d..197b417 100644
--- a/pkgs/markdown/lib/src/block_syntaxes/header_syntax.dart
+++ b/pkgs/markdown/lib/src/block_syntaxes/header_syntax.dart
@@ -16,7 +16,7 @@
@override
Node parse(BlockParser parser) {
- final match = pattern.firstMatch(parser.current)!;
+ final match = pattern.firstMatch(parser.current.content)!;
final matchedText = match[0]!;
final openMarker = match[1]!;
final closeMarker = match[2];
@@ -26,10 +26,13 @@
String? content;
if (closeMarker == null) {
- content = parser.current.substring(openMarkerEnd);
+ content = parser.current.content.substring(openMarkerEnd);
} else {
final closeMarkerStart = matchedText.lastIndexOf(closeMarker);
- content = parser.current.substring(openMarkerEnd, closeMarkerStart);
+ content = parser.current.content.substring(
+ openMarkerEnd,
+ closeMarkerStart,
+ );
}
content = content.trim();
diff --git a/pkgs/markdown/lib/src/block_syntaxes/html_block_syntax.dart b/pkgs/markdown/lib/src/block_syntaxes/html_block_syntax.dart
index a25da4b..642481f 100644
--- a/pkgs/markdown/lib/src/block_syntaxes/html_block_syntax.dart
+++ b/pkgs/markdown/lib/src/block_syntaxes/html_block_syntax.dart
@@ -4,6 +4,7 @@
import '../ast.dart';
import '../block_parser.dart';
+import '../line.dart';
import '../patterns.dart';
import 'block_syntax.dart';
@@ -21,7 +22,8 @@
// more detail.
@override
bool canEndBlock(BlockParser parser) =>
- pattern.firstMatch(parser.current)!.namedGroup('condition_7') == null;
+ pattern.firstMatch(parser.current.content)!.namedGroup('condition_7') ==
+ null;
static final _endConditions = [
// For condition 1, it does not need to match the start tag, see
@@ -38,10 +40,10 @@
const HtmlBlockSyntax();
@override
- List<String> parseChildLines(BlockParser parser) {
- final lines = <String>[];
+ List<Line> parseChildLines(BlockParser parser) {
+ final lines = <Line>[];
- final match = pattern.firstMatch(parser.current);
+ final match = pattern.firstMatch(parser.current.content);
var matchedCondition = 0;
for (var i = 0; i < match!.groupCount; i++) {
if (match.group(i + 1) != null) {
@@ -55,14 +57,14 @@
lines.add(parser.current);
parser.advance();
- while (!parser.isDone && !endCondition.hasMatch(parser.current)) {
+ while (!parser.isDone && !endCondition.hasMatch(parser.current.content)) {
lines.add(parser.current);
parser.advance();
}
} else {
while (!parser.isDone) {
lines.add(parser.current);
- if (endCondition.hasMatch(parser.current)) {
+ if (endCondition.hasMatch(parser.current.content)) {
break;
}
parser.advance();
@@ -74,7 +76,7 @@
// current HTML block.
if (!parser.isDone &&
parser.next != null &&
- pattern.hasMatch(parser.next!)) {
+ pattern.hasMatch(parser.next!.content)) {
parser.advance();
lines.addAll(parseChildLines(parser));
}
@@ -85,6 +87,6 @@
@override
Node parse(BlockParser parser) {
final childLines = parseChildLines(parser);
- return Text(childLines.join('\n').trimRight());
+ return Text(childLines.map((e) => e.content).join('\n').trimRight());
}
}
diff --git a/pkgs/markdown/lib/src/block_syntaxes/list_syntax.dart b/pkgs/markdown/lib/src/block_syntaxes/list_syntax.dart
index 4105ec0..1acb080 100644
--- a/pkgs/markdown/lib/src/block_syntaxes/list_syntax.dart
+++ b/pkgs/markdown/lib/src/block_syntaxes/list_syntax.dart
@@ -4,6 +4,7 @@
import '../ast.dart';
import '../block_parser.dart';
+import '../line.dart';
import '../patterns.dart';
import 'block_syntax.dart';
import 'ordered_list_with_checkbox_syntax.dart';
@@ -15,7 +16,7 @@
this.taskListItemState,
});
- final List<String> lines;
+ final List<Line> lines;
final TaskListItemState? taskListItemState;
}
@@ -30,7 +31,7 @@
// Ideally, [BlockSyntax.canEndBlock] should be changed to be a method
// which accepts a [BlockParser], but this would be a breaking change,
// so we're going with this temporarily.
- final match = pattern.firstMatch(parser.current)!;
+ final match = pattern.firstMatch(parser.current.content)!;
// The seventh group, in both [olPattern] and [ulPattern] is the text
// after the delimiter.
return match[7]?.isNotEmpty ?? false;
@@ -57,13 +58,13 @@
final taskListParserEnabled = this is UnorderedListWithCheckboxSyntax ||
this is OrderedListWithCheckboxSyntax;
final items = <ListItem>[];
- var childLines = <String>[];
+ var childLines = <Line>[];
TaskListItemState? taskListItemState;
void endItem() {
if (childLines.isNotEmpty) {
items.add(ListItem(childLines, taskListItemState: taskListItemState));
- childLines = <String>[];
+ childLines = <Line>[];
}
}
@@ -86,7 +87,7 @@
late Match? possibleMatch;
bool tryMatch(RegExp pattern) {
- possibleMatch = pattern.firstMatch(parser.current);
+ possibleMatch = pattern.firstMatch(parser.current.content);
return possibleMatch != null;
}
@@ -98,21 +99,21 @@
while (!parser.isDone) {
final leadingSpace =
- _whitespaceRe.matchAsPrefix(parser.current)!.group(0)!;
+ _whitespaceRe.matchAsPrefix(parser.current.content)!.group(0)!;
final leadingExpandedTabLength = _expandedTabLength(leadingSpace);
- if (emptyPattern.hasMatch(parser.current)) {
- if (emptyPattern.hasMatch(parser.next ?? '')) {
+ if (parser.current.isBlankLine) {
+ if (parser.next?.isBlankLine ?? true) {
// Two blank lines ends a list.
break;
}
// Add a blank line to the current list item.
- childLines.add('');
+ childLines.add(Line(''));
} else if (indent != null && indent.length <= leadingExpandedTabLength) {
// Strip off indent and add to current item.
- final line = parser.current
+ final line = parser.current.content
.replaceFirst(leadingSpace, ' ' * leadingExpandedTabLength)
.replaceFirst(indent, '');
- childLines.add(parseTaskListItem(line));
+ childLines.add(Line(parseTaskListItem(line)));
} else if (tryMatch(hrPattern)) {
// Horizontal rule takes precedence to a new list item.
break;
@@ -156,14 +157,14 @@
}
// End the current list item and start a new one.
endItem();
- childLines.add(parseTaskListItem('$restWhitespace$content'));
+ childLines.add(Line(parseTaskListItem('$restWhitespace$content')));
} else if (BlockSyntax.isAtBlockEnd(parser)) {
// Done with the list.
break;
} else {
// 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 == '')) {
+ if (childLines.isNotEmpty && childLines.last.isBlankLine) {
parser.encounteredBlankLine = true;
break;
}
@@ -238,7 +239,7 @@
}
void _removeLeadingEmptyLine(ListItem item) {
- if (item.lines.isNotEmpty && emptyPattern.hasMatch(item.lines.first)) {
+ if (item.lines.isNotEmpty && item.lines.first.isBlankLine) {
item.lines.removeAt(0);
}
}
@@ -249,8 +250,7 @@
var anyEmpty = false;
for (var i = 0; i < items.length; i++) {
if (items[i].lines.length == 1) continue;
- while (items[i].lines.isNotEmpty &&
- emptyPattern.hasMatch(items[i].lines.last)) {
+ while (items[i].lines.isNotEmpty && items[i].lines.last.isBlankLine) {
if (i < items.length - 1) {
anyEmpty = true;
}
diff --git a/pkgs/markdown/lib/src/block_syntaxes/paragraph_syntax.dart b/pkgs/markdown/lib/src/block_syntaxes/paragraph_syntax.dart
index aaf4de7..83ae08c 100644
--- a/pkgs/markdown/lib/src/block_syntaxes/paragraph_syntax.dart
+++ b/pkgs/markdown/lib/src/block_syntaxes/paragraph_syntax.dart
@@ -32,7 +32,7 @@
// Eat until we hit something that ends a paragraph.
while (!BlockSyntax.isAtBlockEnd(parser)) {
- childLines.add(parser.current);
+ childLines.add(parser.current.content);
parser.advance();
}
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 552e983..c9aa4ec 100644
--- a/pkgs/markdown/lib/src/block_syntaxes/setext_header_syntax.dart
+++ b/pkgs/markdown/lib/src/block_syntaxes/setext_header_syntax.dart
@@ -16,7 +16,7 @@
@override
bool canParse(BlockParser parser) {
- if (!_interperableAsParagraph(parser.current)) return false;
+ if (!_interperableAsParagraph(parser.current.content)) return false;
var i = 1;
while (true) {
final nextLine = parser.peek(i);
@@ -24,11 +24,11 @@
// We never reached an underline.
return false;
}
- if (setextPattern.hasMatch(nextLine)) {
+ if (setextPattern.hasMatch(nextLine.content)) {
return true;
}
// Ensure that we're still in something like paragraph text.
- if (!_interperableAsParagraph(nextLine)) {
+ if (!_interperableAsParagraph(nextLine.content)) {
return false;
}
i++;
@@ -40,10 +40,10 @@
final lines = <String>[];
String? tag;
while (!parser.isDone) {
- final match = setextPattern.firstMatch(parser.current);
+ final match = setextPattern.firstMatch(parser.current.content);
if (match == null) {
// More text.
- lines.add(parser.current);
+ lines.add(parser.current.content);
parser.advance();
continue;
} else {
diff --git a/pkgs/markdown/lib/src/block_syntaxes/table_syntax.dart b/pkgs/markdown/lib/src/block_syntaxes/table_syntax.dart
index cfc70e1..669306b 100644
--- a/pkgs/markdown/lib/src/block_syntaxes/table_syntax.dart
+++ b/pkgs/markdown/lib/src/block_syntaxes/table_syntax.dart
@@ -32,7 +32,7 @@
/// * many body rows of body cells (`<td>` cells)
@override
Node? parse(BlockParser parser) {
- final alignments = _parseAlignments(parser.next!);
+ final alignments = _parseAlignments(parser.next!.content);
final columnCount = alignments.length;
final headRow = _parseRow(parser, alignments, 'th');
if (headRow.children!.length != columnCount) {
@@ -122,19 +122,19 @@
) {
final line = parser.current;
final cells = <String>[];
- var index = _walkPastOpeningPipe(line);
+ var index = _walkPastOpeningPipe(line.content);
final cellBuffer = StringBuffer();
while (true) {
- if (index >= line.length) {
+ if (index >= line.content.length) {
// This row ended without a trailing pipe, which is fine.
cells.add(cellBuffer.toString().trimRight());
cellBuffer.clear();
break;
}
- final ch = line.codeUnitAt(index);
+ final ch = line.content.codeUnitAt(index);
if (ch == $backslash) {
- if (index == line.length - 1) {
+ if (index == line.content.length - 1) {
// A table row ending in a backslash is not well-specified, but it
// looks like GitHub just allows the character as part of the text of
// the last cell.
@@ -143,7 +143,7 @@
cellBuffer.clear();
break;
}
- final escaped = line.codeUnitAt(index + 1);
+ final escaped = line.content.codeUnitAt(index + 1);
if (escaped == $pipe) {
// GitHub Flavored Markdown has a strange bit here; the pipe is to be
// escaped before any other inline processing. One consequence, for
@@ -163,8 +163,8 @@
cellBuffer.clear();
// Walk forward past any whitespace which leads the next cell.
index++;
- index = _walkPastWhitespace(line, index);
- if (index >= line.length) {
+ index = _walkPastWhitespace(line.content, index);
+ if (index >= line.content.length) {
// This row ended with a trailing pipe.
break;
}
diff --git a/pkgs/markdown/lib/src/block_syntaxes/unordered_list_syntax.dart b/pkgs/markdown/lib/src/block_syntaxes/unordered_list_syntax.dart
index 66bdf0f..357e029 100644
--- a/pkgs/markdown/lib/src/block_syntaxes/unordered_list_syntax.dart
+++ b/pkgs/markdown/lib/src/block_syntaxes/unordered_list_syntax.dart
@@ -19,11 +19,11 @@
// ```
// * * *
// ```
- if (hrPattern.hasMatch(parser.current)) {
+ if (hrPattern.hasMatch(parser.current.content)) {
return false;
}
- return pattern.hasMatch(parser.current);
+ return pattern.hasMatch(parser.current.content);
}
@override
diff --git a/pkgs/markdown/lib/src/document.dart b/pkgs/markdown/lib/src/document.dart
index a42823e..ff6bc0f 100644
--- a/pkgs/markdown/lib/src/document.dart
+++ b/pkgs/markdown/lib/src/document.dart
@@ -8,6 +8,8 @@
import 'extension_set.dart';
import 'inline_parser.dart';
import 'inline_syntaxes/inline_syntax.dart';
+import 'line.dart';
+import 'util.dart';
/// Maintains the context needed to parse a Markdown document.
class Document {
@@ -66,7 +68,14 @@
}
/// Parses the given [lines] of Markdown to a series of AST nodes.
- List<Node> parseLines(List<String> lines) {
+ List<Node> parseLines(List<String> lines) =>
+ parseLineList(lines.map(Line.new).toList());
+
+ /// Parses the given [text] to a series of AST nodes.
+ List<Node> parse(String text) => parseLineList(text.toLines());
+
+ /// Parses the given [lines] of [Line] to a series of AST nodes.
+ List<Node> parseLineList(List<Line> lines) {
final nodes = BlockParser(lines, this).parseLines();
_parseInlineContent(nodes);
return nodes;
diff --git a/pkgs/markdown/lib/src/html_renderer.dart b/pkgs/markdown/lib/src/html_renderer.dart
index 8b7dedc..6ff803d 100644
--- a/pkgs/markdown/lib/src/html_renderer.dart
+++ b/pkgs/markdown/lib/src/html_renderer.dart
@@ -36,10 +36,7 @@
if (inlineOnly) return renderToHtml(document.parseInline(markdown));
- // Replace windows line endings with unix line endings, and split.
- final lines = markdown.replaceAll('\r\n', '\n').split('\n');
-
- final nodes = document.parseLines(lines);
+ final nodes = document.parse(markdown);
return '${renderToHtml(nodes)}\n';
}
diff --git a/pkgs/markdown/lib/src/line.dart b/pkgs/markdown/lib/src/line.dart
new file mode 100644
index 0000000..1ccd5da
--- /dev/null
+++ b/pkgs/markdown/lib/src/line.dart
@@ -0,0 +1,47 @@
+// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'patterns.dart';
+
+/// A [Line] is a sequence of zero or more characters other than line feed
+/// (`U+000A`) or carriage return (`U+000D`), followed by a line ending or by
+/// the end of file.
+// See https://spec.commonmark.org/0.30/#line.
+class Line {
+ /// A sequence of zero or more characters other than the line ending.
+ final String content;
+
+ /// How many spaces of a tab that remains after part of it has been consumed.
+ // See: https://spec.commonmark.org/0.30/#example-6
+ // We cannot simply expand the `tabRemaining` to spaces, for example
+ //
+ // `>\t\tfoo`
+ //
+ // If we expand the 2 space width `tabRemaining` from blockquote block into 2
+ // spaces, so the string segment for the indented code block is:
+ //
+ // ` \tfoo`,
+ //
+ // then the output will be:
+ // ```html
+ // <pre><code>foo
+ // </code></pre>
+ // ```
+ // instead of the expected:
+ // ```html
+ // <pre><code> foo
+ // </code></pre>
+ // ```
+ final int? tabRemaining;
+
+ // A line containing no characters, or a line containing only spaces
+ // (`U+0020`) or tabs (`U+0009`), is called a blank line.
+ // https://spec.commonmark.org/0.30/#blank-line
+ final bool isBlankLine;
+
+ Line(
+ this.content, {
+ this.tabRemaining,
+ }) : isBlankLine = emptyPattern.hasMatch(content);
+}
diff --git a/pkgs/markdown/lib/src/util.dart b/pkgs/markdown/lib/src/util.dart
index b482c1c..e83e519 100644
--- a/pkgs/markdown/lib/src/util.dart
+++ b/pkgs/markdown/lib/src/util.dart
@@ -7,6 +7,7 @@
import 'assets/case_folding.dart';
import 'assets/html_entities.dart';
import 'charcode.dart';
+import 'line.dart';
import 'patterns.dart';
/// One or more whitespace, for compressing.
@@ -171,6 +172,9 @@
/// Whether this string contains only whitespaces.
bool get isBlank => trim().isEmpty;
+
+ /// Converts this string to a list of [Line].
+ List<Line> toLines() => LineSplitter.split(this).map(Line.new).toList();
}
/// A class that describes a dedented text.
diff --git a/pkgs/markdown/test/document_test.dart b/pkgs/markdown/test/document_test.dart
index a9a8be7..adfa009 100644
--- a/pkgs/markdown/test/document_test.dart
+++ b/pkgs/markdown/test/document_test.dart
@@ -2,9 +2,8 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
-import 'dart:convert';
-
import 'package:markdown/markdown.dart';
+import 'package:markdown/src/util.dart';
import 'package:test/test.dart';
void main() {
@@ -33,8 +32,8 @@
});
test('encodes HTML in a fenced code block', () {
- final lines = '```\n<p>Hello <em>Markdown</em></p>\n```\n'.split('\n');
- final result = document.parseLines(lines);
+ final lines = '```\n<p>Hello <em>Markdown</em></p>\n```\n'.toLines();
+ final result = document.parseLineList(lines);
final codeBlock = result.single as Element;
expect(
codeBlock.textContent,
@@ -43,8 +42,8 @@
});
test('encodes HTML in an indented code block', () {
- final lines = ' <p>Hello <em>Markdown</em></p>\n'.split('\n');
- final result = document.parseLines(lines);
+ final lines = ' <p>Hello <em>Markdown</em></p>\n'.toLines();
+ final result = document.parseLineList(lines);
final codeBlock = result.single as Element;
expect(
codeBlock.textContent,
@@ -56,8 +55,7 @@
// Example to get a <p> tag rendered before a text node.
const contents = 'Sample\n\n<pre>\n A\n B\n</pre>';
final document = Document();
- final lines = LineSplitter.split(contents).toList();
- final nodes = BlockParser(lines, document).parseLines();
+ final nodes = BlockParser(contents.toLines(), document).parseLines();
final result = HtmlRenderer().render(nodes);
expect(result, '<p>\n</p><pre>\n A\n B\n</pre>');
});
@@ -93,8 +91,8 @@
});
test('leaves HTML alone, in a fenced code block', () {
- final lines = '```\n<p>Hello <em>Markdown</em></p>\n```\n'.split('\n');
- final result = document.parseLines(lines);
+ final lines = '```\n<p>Hello <em>Markdown</em></p>\n```\n'.toLines();
+ final result = document.parseLineList(lines);
final codeBlock = result.single as Element;
expect(
codeBlock.textContent,
@@ -103,8 +101,8 @@
});
test('leaves HTML alone, in an indented code block', () {
- final lines = ' <p>Hello <em>Markdown</em></p>\n'.split('\n');
- final result = document.parseLines(lines);
+ final lines = ' <p>Hello <em>Markdown</em></p>\n'.toLines();
+ final result = document.parseLineList(lines);
final codeBlock = result.single as Element;
expect(
codeBlock.textContent,
diff --git a/pkgs/markdown/test/util_test.dart b/pkgs/markdown/test/util_test.dart
new file mode 100644
index 0000000..50a9453
--- /dev/null
+++ b/pkgs/markdown/test/util_test.dart
@@ -0,0 +1,63 @@
+// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:markdown/markdown.dart';
+import 'package:markdown/src/util.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('String.toLines()', () {
+ test('a single line without a line ending', () {
+ const text = 'Foo';
+ final lines = text.toLines();
+
+ expect(lines.map((e) => e.toMap()), [
+ {
+ 'content': 'Foo',
+ 'isBlankLine': false,
+ }
+ ]);
+ });
+
+ test('a single line with a line ending', () {
+ const text = 'Foo\n';
+ final lines = text.toLines();
+
+ expect(lines.map((e) => e.toMap()), [
+ {
+ 'content': 'Foo',
+ 'isBlankLine': false,
+ },
+ ]);
+ });
+
+ test('multiple lines with a blank line in between', () {
+ const text = 'Foo\r\n\nBar';
+ final lines = text.toLines();
+
+ expect(lines.map((e) => e.toMap()), [
+ {
+ 'content': 'Foo',
+ 'isBlankLine': false,
+ },
+ {
+ 'content': '',
+ 'isBlankLine': true,
+ },
+ {
+ 'content': 'Bar',
+ 'isBlankLine': false,
+ }
+ ]);
+ });
+ });
+}
+
+extension on Line {
+ Map<String, dynamic> toMap() => {
+ 'content': content,
+ 'isBlankLine': isBlankLine,
+ if (tabRemaining != null) 'tabRemaining': tabRemaining,
+ };
+}