Fixed table last row empty bug (#296)

diff --git a/packages/flutter_markdown/lib/src/builder.dart b/packages/flutter_markdown/lib/src/builder.dart
index de3a934..b755b5b 100644
--- a/packages/flutter_markdown/lib/src/builder.dart
+++ b/packages/flutter_markdown/lib/src/builder.dart
@@ -219,6 +219,15 @@
 
       _addParentInlineIfNeeded(_blocks.last.tag);
 
+      // The Markdown parser passes empty table data tags for blank
+      // table cells. Insert a text node with an empty string in this
+      // case for the table cell to get properly created.
+      if (element.tag == 'td' &&
+          element.children != null &&
+          element.children.isEmpty) {
+        element.children.add(md.Text(''));
+      }
+
       TextStyle parentStyle = _inlines.last.style;
       _inlines.add(_InlineElement(
         tag,
diff --git a/packages/flutter_markdown/test/table_test.dart b/packages/flutter_markdown/test/table_test.dart
index b242b9a..d3345ca 100644
--- a/packages/flutter_markdown/test/table_test.dart
+++ b/packages/flutter_markdown/test/table_test.dart
@@ -120,5 +120,410 @@
         expect(table.defaultColumnWidth, columnWidth);
       },
     );
+
+    testWidgets(
+      'table with last row of empty table cells',
+      (WidgetTester tester) async {
+        final ThemeData theme =
+            ThemeData.light().copyWith(textTheme: textTheme);
+
+        const String data = '|Header 1|Header 2|\n|----|----|\n| | |';
+        const FixedColumnWidth columnWidth = FixedColumnWidth(100);
+        final MarkdownStyleSheet style =
+            MarkdownStyleSheet.fromTheme(theme).copyWith(
+          tableColumnWidth: columnWidth,
+        );
+
+        await tester.pumpWidget(
+            boilerplate(MarkdownBody(data: data, styleSheet: style)));
+
+        final Table table = tester.widget(find.byType(Table));
+
+        expectTableSize(2, 2);
+
+        expect(find.byType(RichText), findsNWidgets(4));
+        List<String> cellText = find
+            .byType(RichText)
+            .evaluate()
+            .map((e) => e.widget)
+            .cast<RichText>()
+            .map((richText) => richText.text)
+            .cast<TextSpan>()
+            .map((e) => e.text)
+            .toList();
+        expect(cellText[0], 'Header 1');
+        expect(cellText[1], 'Header 2');
+        expect(cellText[2], '');
+        expect(cellText[3], '');
+
+        expect(table.defaultColumnWidth, columnWidth);
+      },
+    );
+
+    testWidgets(
+      'table with an empty row an last row has an empty table cell',
+      (WidgetTester tester) async {
+        final ThemeData theme =
+            ThemeData.light().copyWith(textTheme: textTheme);
+
+        const String data =
+            '|Header 1|Header 2|\n|----|----|\n| | |\n| bar | |';
+        const FixedColumnWidth columnWidth = FixedColumnWidth(100);
+        final MarkdownStyleSheet style =
+            MarkdownStyleSheet.fromTheme(theme).copyWith(
+          tableColumnWidth: columnWidth,
+        );
+
+        await tester.pumpWidget(
+            boilerplate(MarkdownBody(data: data, styleSheet: style)));
+
+        final Table table = tester.widget(find.byType(Table));
+
+        expectTableSize(3, 2);
+
+        expect(find.byType(RichText), findsNWidgets(6));
+        List<String> cellText = find
+            .byType(RichText)
+            .evaluate()
+            .map((e) => e.widget)
+            .cast<RichText>()
+            .map((richText) => richText.text)
+            .cast<TextSpan>()
+            .map((e) => e.text)
+            .toList();
+        expect(cellText[0], 'Header 1');
+        expect(cellText[1], 'Header 2');
+        expect(cellText[2], '');
+        expect(cellText[3], '');
+        expect(cellText[4], 'bar');
+        expect(cellText[5], '');
+
+        expect(table.defaultColumnWidth, columnWidth);
+      },
+    );
+
+    group('GFM Examples', () {
+      testWidgets(
+        // Example 198 from GFM.
+        'simple table',
+        (WidgetTester tester) async {
+          final ThemeData theme =
+              ThemeData.light().copyWith(textTheme: textTheme);
+
+          const String data = '| foo | bar |\n| --- | --- |\n| baz | bim |';
+          const FixedColumnWidth columnWidth = FixedColumnWidth(100);
+          final MarkdownStyleSheet style =
+              MarkdownStyleSheet.fromTheme(theme).copyWith(
+            tableColumnWidth: columnWidth,
+          );
+
+          await tester.pumpWidget(
+              boilerplate(MarkdownBody(data: data, styleSheet: style)));
+
+          final Table table = tester.widget(find.byType(Table));
+
+          expectTableSize(2, 2);
+
+          expect(find.byType(RichText), findsNWidgets(4));
+          List<String> cellText = find
+              .byType(RichText)
+              .evaluate()
+              .map((e) => e.widget)
+              .cast<RichText>()
+              .map((richText) => richText.text)
+              .cast<TextSpan>()
+              .map((e) => e.text)
+              .toList();
+          expect(cellText[0], 'foo');
+          expect(cellText[1], 'bar');
+          expect(cellText[2], 'baz');
+          expect(cellText[3], 'bim');
+          expect(table.defaultColumnWidth, columnWidth);
+        },
+      );
+
+      testWidgets(
+        // Example 199 from GFM.
+        'input table cell data does not need to match column length',
+        (WidgetTester tester) async {
+          final ThemeData theme =
+              ThemeData.light().copyWith(textTheme: textTheme);
+
+          const String data = '| abc | defghi |\n:-: | -----------:\nbar | baz';
+          const FixedColumnWidth columnWidth = FixedColumnWidth(100);
+          final MarkdownStyleSheet style =
+              MarkdownStyleSheet.fromTheme(theme).copyWith(
+            tableColumnWidth: columnWidth,
+          );
+
+          await tester.pumpWidget(
+              boilerplate(MarkdownBody(data: data, styleSheet: style)));
+
+          final Table table = tester.widget(find.byType(Table));
+
+          expectTableSize(2, 2);
+
+          expect(find.byType(RichText), findsNWidgets(4));
+          List<String> cellText = find
+              .byType(RichText)
+              .evaluate()
+              .map((e) => e.widget)
+              .cast<RichText>()
+              .map((richText) => richText.text)
+              .cast<TextSpan>()
+              .map((e) => e.text)
+              .toList();
+          expect(cellText[0], 'abc');
+          expect(cellText[1], 'defghi');
+          expect(cellText[2], 'bar');
+          expect(cellText[3], 'baz');
+          expect(table.defaultColumnWidth, columnWidth);
+        },
+      );
+
+      testWidgets(
+        // Example 200 from GFM.
+        'include a pipe in table cell data by escaping the pipe',
+        (WidgetTester tester) async {
+          final ThemeData theme =
+              ThemeData.light().copyWith(textTheme: textTheme);
+
+          const String data =
+              '| f\|oo  |\n| ------ |\n| b `\|` az |\n| b **\|** im |';
+          const FixedColumnWidth columnWidth = FixedColumnWidth(100);
+          final MarkdownStyleSheet style =
+              MarkdownStyleSheet.fromTheme(theme).copyWith(
+            tableColumnWidth: columnWidth,
+          );
+
+          await tester.pumpWidget(
+              boilerplate(MarkdownBody(data: data, styleSheet: style)));
+
+          final Table table = tester.widget(find.byType(Table));
+
+          expectTableSize(1, 3);
+
+          expect(find.byType(RichText), findsNWidgets(4));
+          List<String> cellText = find
+              .byType(RichText)
+              .evaluate()
+              .map((e) => e.widget)
+              .cast<RichText>()
+              .map((richText) => richText.text)
+              .cast<TextSpan>()
+              .map((e) => e.text)
+              .toList();
+          expect(cellText[0], 'f|oo');
+          expect(cellText[1], 'defghi');
+          expect(cellText[2], 'b | az');
+          expect(cellText[3], 'b | im');
+          expect(table.defaultColumnWidth, columnWidth);
+        },
+        // TODO(mjordan56) Remove skip once the issue #340 in the markdown package
+        // is fixed and released. https://github.com/dart-lang/markdown/issues/340
+        // This test will need adjusting once issue #340 is fixed.
+        skip: true,
+      );
+
+      testWidgets(
+        // Example 201 from GFM.
+        'table definition is complete at beginning of new block',
+        (WidgetTester tester) async {
+          final ThemeData theme =
+              ThemeData.light().copyWith(textTheme: textTheme);
+
+          const String data =
+              '| abc | def |\n| --- | --- |\n| bar | baz |\n> bar';
+          const FixedColumnWidth columnWidth = FixedColumnWidth(100);
+          final MarkdownStyleSheet style =
+              MarkdownStyleSheet.fromTheme(theme).copyWith(
+            tableColumnWidth: columnWidth,
+          );
+
+          await tester.pumpWidget(
+              boilerplate(MarkdownBody(data: data, styleSheet: style)));
+
+          final Table table = tester.widget(find.byType(Table));
+
+          expectTableSize(2, 2);
+
+          expect(find.byType(RichText), findsNWidgets(5));
+          List<String> text = find
+              .byType(RichText)
+              .evaluate()
+              .map((e) => e.widget)
+              .cast<RichText>()
+              .map((richText) => richText.text)
+              .cast<TextSpan>()
+              .map((e) => e.text)
+              .toList();
+          expect(text[0], 'abc');
+          expect(text[1], 'def');
+          expect(text[2], 'bar');
+          expect(text[3], 'baz');
+          expect(table.defaultColumnWidth, columnWidth);
+
+          // Blockquote
+          expect(find.byType(DecoratedBox), findsOneWidget);
+          expect(text[4], 'bar');
+        },
+      );
+
+      testWidgets(
+        // Example 202 from GFM.
+        'table definition is complete at first empty line',
+        (WidgetTester tester) async {
+          final ThemeData theme =
+              ThemeData.light().copyWith(textTheme: textTheme);
+
+          const String data =
+              '| abc | def |\n| --- | --- |\n| bar | baz |\nbar\n\nbar';
+          const FixedColumnWidth columnWidth = FixedColumnWidth(100);
+          final MarkdownStyleSheet style =
+              MarkdownStyleSheet.fromTheme(theme).copyWith(
+            tableColumnWidth: columnWidth,
+          );
+
+          await tester.pumpWidget(
+              boilerplate(MarkdownBody(data: data, styleSheet: style)));
+
+          final Table table = tester.widget(find.byType(Table));
+
+          expectTableSize(3, 2);
+
+          expect(find.byType(RichText), findsNWidgets(6));
+          List<String> text = find
+              .byType(RichText)
+              .evaluate()
+              .map((e) => e.widget)
+              .cast<RichText>()
+              .map((richText) => richText.text)
+              .cast<TextSpan>()
+              .map((e) => e.text)
+              .toList();
+          expect(text[0], 'abc');
+          expect(text[1], 'def');
+          expect(text[2], 'bar');
+          expect(text[3], 'baz');
+          expect(text[4], 'bar');
+          expect(table.defaultColumnWidth, columnWidth);
+
+          // Paragraph text
+          expect(text[5], 'bar');
+        },
+      );
+
+      testWidgets(
+        // Example 203 from GFM.
+        'table header row must match the delimiter row in number of cells',
+        (WidgetTester tester) async {
+          final ThemeData theme =
+              ThemeData.light().copyWith(textTheme: textTheme);
+
+          const String data = '| abc | def |\n| --- |\n| bar |';
+          const FixedColumnWidth columnWidth = FixedColumnWidth(100);
+          final MarkdownStyleSheet style =
+              MarkdownStyleSheet.fromTheme(theme).copyWith(
+            tableColumnWidth: columnWidth,
+          );
+
+          await tester.pumpWidget(
+              boilerplate(MarkdownBody(data: data, styleSheet: style)));
+
+          expect(find.byType(Table), findsNothing);
+          List<String> text = find
+              .byType(RichText)
+              .evaluate()
+              .map((e) => e.widget)
+              .cast<RichText>()
+              .map((richText) => richText.text)
+              .cast<TextSpan>()
+              .map((e) => e.text)
+              .toList();
+          expect(text[0], '| abc | def | | --- | | bar |');
+        },
+        // TODO(mjordan56) Remove skip once the issue #341 in the markdown package
+        // is fixed and released. https://github.com/dart-lang/markdown/issues/341
+        skip: true,
+      );
+
+      testWidgets(
+        // Example 204 from GFM.
+        'remainder of table cells may vary, excess cells are ignored',
+        (WidgetTester tester) async {
+          final ThemeData theme =
+              ThemeData.light().copyWith(textTheme: textTheme);
+
+          const String data =
+              '| abc | def |\n| --- | --- |\n| bar |\n| bar | baz | boo |';
+          const FixedColumnWidth columnWidth = FixedColumnWidth(100);
+          final MarkdownStyleSheet style =
+              MarkdownStyleSheet.fromTheme(theme).copyWith(
+            tableColumnWidth: columnWidth,
+          );
+
+          await tester.pumpWidget(
+              boilerplate(MarkdownBody(data: data, styleSheet: style)));
+
+          final Table table = tester.widget(find.byType(Table));
+
+          expectTableSize(3, 2);
+
+          expect(find.byType(RichText), findsNWidgets(5));
+          List<String> cellText = find
+              .byType(RichText)
+              .evaluate()
+              .map((e) => e.widget)
+              .cast<RichText>()
+              .map((richText) => richText.text)
+              .cast<TextSpan>()
+              .map((e) => e.text)
+              .toList();
+          expect(cellText[0], 'abc');
+          expect(cellText[1], 'def');
+          expect(cellText[2], 'bar');
+          expect(cellText[3], 'bar');
+          expect(cellText[4], 'baz');
+          expect(table.defaultColumnWidth, columnWidth);
+        },
+      );
+
+      testWidgets(
+        // Example 205 from GFM.
+        'no table body is created when no rows are defined',
+        (WidgetTester tester) async {
+          final ThemeData theme =
+              ThemeData.light().copyWith(textTheme: textTheme);
+
+          const String data = '| abc | def |\n| --- | --- |';
+          const FixedColumnWidth columnWidth = FixedColumnWidth(100);
+          final MarkdownStyleSheet style =
+              MarkdownStyleSheet.fromTheme(theme).copyWith(
+            tableColumnWidth: columnWidth,
+          );
+
+          await tester.pumpWidget(
+              boilerplate(MarkdownBody(data: data, styleSheet: style)));
+
+          final Table table = tester.widget(find.byType(Table));
+
+          expectTableSize(1, 2);
+
+          expect(find.byType(RichText), findsNWidgets(2));
+          List<String> cellText = find
+              .byType(RichText)
+              .evaluate()
+              .map((e) => e.widget)
+              .cast<RichText>()
+              .map((richText) => richText.text)
+              .cast<TextSpan>()
+              .map((e) => e.text)
+              .toList();
+          expect(cellText[0], 'abc');
+          expect(cellText[1], 'def');
+          expect(table.defaultColumnWidth, columnWidth);
+        },
+      );
+    });
   });
 }
diff --git a/packages/flutter_markdown/test/utils.dart b/packages/flutter_markdown/test/utils.dart
index 1219394..a72ad51 100644
--- a/packages/flutter_markdown/test/utils.dart
+++ b/packages/flutter_markdown/test/utils.dart
@@ -135,6 +135,17 @@
   expect(textSpan.recognizer, isNull);
 }
 
+void expectTableSize(int rows, int columns) {
+  final tableFinder = find.byType(Table);
+  expect(tableFinder, findsOneWidget);
+  final table = tableFinder.evaluate().first.widget as Table;
+
+  expect(table.children.length, rows);
+  for (int index = 0; index < rows; index++) {
+    expect(table.children[index].children.length, columns);
+  }
+}
+
 void expectLinkTap(MarkdownLink actual, MarkdownLink expected) {
   expect(actual, equals(expected),
       reason: