Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/flutter_markdown/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## NEXT
## 0.7.4+4

* Makes table column custom alignment work even when text wraps.
* Updates minimum supported SDK version to Flutter 3.22/Dart 3.4.

## 0.7.4+3
Expand Down
120 changes: 47 additions & 73 deletions packages/flutter_markdown/lib/src/builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -766,7 +766,7 @@ class MarkdownBuilder implements md.NodeVisitor {
}

/// Extracts all spans from an inline element and merges them into a single list
Iterable<InlineSpan> _getInlineSpans(InlineSpan span) {
Iterable<InlineSpan> _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 <InlineSpan>[span];
Expand All @@ -790,95 +790,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<Widget> _mergeInlineChildren(
List<Widget> children,
TextAlign? textAlign,
) {
// List of merged text spans and widgets
final List<Widget> mergedTexts = <Widget>[];
// List of text widgets (merged) and non-text widgets (non-merged)
final List<Widget> mergedWidgets = <Widget>[];

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<InlineSpan> spans = <InlineSpan>[];

// 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(<Widget>[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(<Widget>[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) {
Expand Down Expand Up @@ -992,12 +966,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
Expand All @@ -1009,7 +983,7 @@ class MarkdownBuilder implements md.NodeVisitor {
);
} else {
return Text.rich(
text!,
text,
textScaler: styleSheet.textScaler,
textAlign: textAlign ?? TextAlign.start,
key: k,
Expand Down
2 changes: 1 addition & 1 deletion packages/flutter_markdown/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.4+3
version: 0.7.4+4

environment:
sdk: ^3.4.0
Expand Down
24 changes: 9 additions & 15 deletions packages/flutter_markdown/test/table_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -58,27 +58,21 @@ void defineTests() {
final Iterable<DefaultTextStyle> 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<Wrap> 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<Text> 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);
},
);

Expand Down
Loading