diff --git a/packages/flutter_markdown/lib/src/builder.dart b/packages/flutter_markdown/lib/src/builder.dart index de3a9346ae..b755b5b1d1 100644 --- a/packages/flutter_markdown/lib/src/builder.dart +++ b/packages/flutter_markdown/lib/src/builder.dart @@ -219,6 +219,15 @@ class MarkdownBuilder implements md.NodeVisitor { _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 b242b9a8b2..d3345ca14b 100644 --- a/packages/flutter_markdown/test/table_test.dart +++ b/packages/flutter_markdown/test/table_test.dart @@ -120,5 +120,410 @@ void defineTests() { 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 cellText = find + .byType(RichText) + .evaluate() + .map((e) => e.widget) + .cast() + .map((richText) => richText.text) + .cast() + .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 cellText = find + .byType(RichText) + .evaluate() + .map((e) => e.widget) + .cast() + .map((richText) => richText.text) + .cast() + .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 cellText = find + .byType(RichText) + .evaluate() + .map((e) => e.widget) + .cast() + .map((richText) => richText.text) + .cast() + .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 cellText = find + .byType(RichText) + .evaluate() + .map((e) => e.widget) + .cast() + .map((richText) => richText.text) + .cast() + .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 cellText = find + .byType(RichText) + .evaluate() + .map((e) => e.widget) + .cast() + .map((richText) => richText.text) + .cast() + .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 text = find + .byType(RichText) + .evaluate() + .map((e) => e.widget) + .cast() + .map((richText) => richText.text) + .cast() + .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 text = find + .byType(RichText) + .evaluate() + .map((e) => e.widget) + .cast() + .map((richText) => richText.text) + .cast() + .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 text = find + .byType(RichText) + .evaluate() + .map((e) => e.widget) + .cast() + .map((richText) => richText.text) + .cast() + .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 cellText = find + .byType(RichText) + .evaluate() + .map((e) => e.widget) + .cast() + .map((richText) => richText.text) + .cast() + .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 cellText = find + .byType(RichText) + .evaluate() + .map((e) => e.widget) + .cast() + .map((richText) => richText.text) + .cast() + .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 121939458f..a72ad51537 100644 --- a/packages/flutter_markdown/test/utils.dart +++ b/packages/flutter_markdown/test/utils.dart @@ -135,6 +135,17 @@ void expectInvalidLink(String linkText) { 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: