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,
+      };
+}