diff --git a/packages/flutter_markdown/CHANGELOG.md b/packages/flutter_markdown/CHANGELOG.md index bbf8fa279e7f..c710c57d401b 100644 --- a/packages/flutter_markdown/CHANGELOG.md +++ b/packages/flutter_markdown/CHANGELOG.md @@ -1,10 +1,14 @@ -## 0.7.6 +## 0.7.6+1 * Adds horizontal scrolling for table when using `tableColumnWidth: IntrinsicColumnWidth()`. + +## 0.7.6 + * Adds styleSheet option `tableScrollbarThumbVisibility` for setting the `thumbVisibility` on tables' `ScrollBar`. ## 0.7.5 +* Makes table column custom alignment work even when text wraps. * Updates minimum supported SDK version to Flutter 3.22/Dart 3.4. * Fixes some memory leaks. diff --git a/packages/flutter_markdown/lib/src/builder.dart b/packages/flutter_markdown/lib/src/builder.dart index d35c8e7da8f3..de27657609da 100644 --- a/packages/flutter_markdown/lib/src/builder.dart +++ b/packages/flutter_markdown/lib/src/builder.dart @@ -777,7 +777,7 @@ class MarkdownBuilder implements md.NodeVisitor { } /// Extracts all spans from an inline element and merges them into a single list - Iterable _getInlineSpans(InlineSpan span) { + Iterable _getInlineSpansFromSpan(InlineSpan span) { // If the span is not a TextSpan or it has no children, return the span if (span is! TextSpan || span.children == null) { return [span]; @@ -801,95 +801,69 @@ class MarkdownBuilder implements md.NodeVisitor { return spans; } - /// Merges adjacent [TextSpan] children + // Accesses the TextSpan property correctly depending on the widget type. + // Returns null if not a valid (text) widget. + InlineSpan? _getInlineSpanFromText(Widget widget) => switch (widget) { + SelectableText() => widget.textSpan, + Text() => widget.textSpan, + RichText() => widget.text, + _ => null + }; + + /// Merges adjacent [TextSpan] children. + /// Also forces a specific [TextAlign] regardless of merging. + /// This is essential for table column alignment, since desired column alignment + /// is discovered after the text widgets have been created. This function is the + /// last chance to enforce the desired column alignment in the texts. List _mergeInlineChildren( List children, TextAlign? textAlign, ) { - // List of merged text spans and widgets - final List mergedTexts = []; + // List of text widgets (merged) and non-text widgets (non-merged) + final List mergedWidgets = []; + bool lastIsText = false; for (final Widget child in children) { - // If the list is empty, add the current widget to the list - if (mergedTexts.isEmpty) { - mergedTexts.add(child); + final InlineSpan? currentSpan = _getInlineSpanFromText(child); + final bool currentIsText = currentSpan != null; + + if (!currentIsText) { + // There is no merging to do, so just add and continue + mergedWidgets.add(child); + lastIsText = false; continue; } - // Remove last widget from the list to merge it with the current widget - final Widget last = mergedTexts.removeLast(); - // Extracted spans from the last and the current widget List spans = []; - // Extract the text spans from the last widget - if (last is SelectableText) { - final TextSpan span = last.textSpan!; - spans.addAll(_getInlineSpans(span)); - } else if (last is Text) { - final InlineSpan span = last.textSpan!; - spans.addAll(_getInlineSpans(span)); - } else if (last is RichText) { - final InlineSpan span = last.text; - spans.addAll(_getInlineSpans(span)); - } else { - // If the last widget is not a text widget, - // add both the last and the current widget to the list - mergedTexts.addAll([last, child]); - continue; + if (lastIsText) { + // Removes last widget from the list for merging and extracts its spans + spans.addAll(_getInlineSpansFromSpan( + _getInlineSpanFromText(mergedWidgets.removeLast())!)); } - // Extract the text spans from the current widget - if (child is Text) { - final InlineSpan span = child.textSpan!; - spans.addAll(_getInlineSpans(span)); - } else if (child is SelectableText) { - final TextSpan span = child.textSpan!; - spans.addAll(_getInlineSpans(span)); - } else if (child is RichText) { - final InlineSpan span = child.text; - spans.addAll(_getInlineSpans(span)); - } else { - // If the current widget is not a text widget, - // add both the last and the current widget to the list - mergedTexts.addAll([last, child]); - continue; - } + spans.addAll(_getInlineSpansFromSpan(currentSpan)); + spans = _mergeSimilarTextSpans(spans); - if (spans.isNotEmpty) { - // Merge similar text spans - spans = _mergeSimilarTextSpans(spans); + final Widget mergedWidget; - // Create a new text widget with the merged text spans - InlineSpan child; - if (spans.length == 1) { - child = spans.first; - } else { - child = TextSpan(children: spans); - } - - // Add the new text widget to the list - if (selectable) { - mergedTexts.add(SelectableText.rich( - TextSpan(children: spans), - textScaler: styleSheet.textScaler, - textAlign: textAlign ?? TextAlign.start, - onTap: onTapText, - )); - } else { - mergedTexts.add(Text.rich( - child, - textScaler: styleSheet.textScaler, - textAlign: textAlign ?? TextAlign.start, - )); - } + if (spans.isEmpty) { + // no spans found, just insert the current widget + mergedWidget = child; } else { - // If no text spans were found, add the current widget to the list - mergedTexts.add(child); + final InlineSpan first = spans.first; + final TextSpan textSpan = (spans.length == 1 && first is TextSpan) + ? first + : TextSpan(children: spans); + mergedWidget = _buildRichText(textSpan, textAlign: textAlign); } + + mergedWidgets.add(mergedWidget); + lastIsText = true; } - return mergedTexts; + return mergedWidgets; } TextAlign _textAlignForBlockTag(String? blockTag) { @@ -1003,12 +977,12 @@ class MarkdownBuilder implements md.NodeVisitor { return mergedSpans; } - Widget _buildRichText(TextSpan? text, {TextAlign? textAlign, String? key}) { + Widget _buildRichText(TextSpan text, {TextAlign? textAlign, String? key}) { //Adding a unique key prevents the problem of using the same link handler for text spans with the same text final Key k = key == null ? UniqueKey() : Key(key); if (selectable) { return SelectableText.rich( - text!, + text, textScaler: styleSheet.textScaler, textAlign: textAlign ?? TextAlign.start, onSelectionChanged: onSelectionChanged != null @@ -1020,7 +994,7 @@ class MarkdownBuilder implements md.NodeVisitor { ); } else { return Text.rich( - text!, + text, textScaler: styleSheet.textScaler, textAlign: textAlign ?? TextAlign.start, key: k, diff --git a/packages/flutter_markdown/pubspec.yaml b/packages/flutter_markdown/pubspec.yaml index d82c42f2bcaf..5ee9cdd10af5 100644 --- a/packages/flutter_markdown/pubspec.yaml +++ b/packages/flutter_markdown/pubspec.yaml @@ -4,7 +4,7 @@ description: A Markdown renderer for Flutter. Create rich text output, formatted with simple Markdown tags. repository: https://github.com/flutter/packages/tree/main/packages/flutter_markdown issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+flutter_markdown%22 -version: 0.7.6 +version: 0.7.6+1 environment: sdk: ^3.4.0 diff --git a/packages/flutter_markdown/test/table_test.dart b/packages/flutter_markdown/test/table_test.dart index 93341ee625af..6d0f678d34f3 100644 --- a/packages/flutter_markdown/test/table_test.dart +++ b/packages/flutter_markdown/test/table_test.dart @@ -48,7 +48,7 @@ void defineTests() { 'should work with alignments', (WidgetTester tester) async { const String data = - '|Header 1|Header 2|\n|:----:|----:|\n|Col 1|Col 2|'; + '|Header 1|Header 2|Header 3|\n|:----|:----:|----:|\n|Col 1|Col 2|Col 3|'; await tester.pumpWidget( boilerplate( const MarkdownBody(data: data), @@ -58,27 +58,21 @@ void defineTests() { final Iterable styles = tester.widgetList(find.byType(DefaultTextStyle)); - expect(styles.first.textAlign, TextAlign.center); + expect(styles.first.textAlign, TextAlign.left); + expect(styles.elementAt(1).textAlign, TextAlign.center); expect(styles.last.textAlign, TextAlign.right); - }, - ); - - testWidgets( - 'should work with table alignments', - (WidgetTester tester) async { - const String data = - '|Header 1|Header 2|Header 3|\n|:----|:----:|----:|\n|Col 1|Col 2|Col 3|'; - await tester.pumpWidget( - boilerplate( - const MarkdownBody(data: data), - ), - ); final Iterable wraps = tester.widgetList(find.byType(Wrap)); expect(wraps.first.alignment, WrapAlignment.start); expect(wraps.elementAt(1).alignment, WrapAlignment.center); expect(wraps.last.alignment, WrapAlignment.end); + + final Iterable texts = tester.widgetList(find.byType(Text)); + + expect(texts.first.textAlign, TextAlign.left); + expect(texts.elementAt(1).textAlign, TextAlign.center); + expect(texts.last.textAlign, TextAlign.right); }, );