Skip to content
5 changes: 5 additions & 0 deletions packages/flutter_markdown/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.6.17

* Introduces a new `MarkdownElementBuilder.visitElementAfterWithContext()` method passing the widget `BuildContext` and
the parent text's `TextStyle`.

## 0.6.16

* Adds `tableVerticalAlignment` property to allow aligning table cells vertically.
Expand Down
15 changes: 13 additions & 2 deletions packages/flutter_markdown/lib/src/builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ class _InlineElement {

/// A delegate used by [MarkdownBuilder] to control the widgets it creates.
abstract class MarkdownBuilderDelegate {
/// Returns the [BuildContext] of the [MarkdownWidget].
///
/// The context will be passed down to the
/// [MarkdownElementBuilder.visitElementBefore] method and allows elements to
/// get information from the context.
BuildContext get context;

/// Returns a gesture recognizer to use for an `a` element with the given
/// text, `href` attribute, and title.
GestureRecognizer createLink(String text, String? href, String title);
Expand Down Expand Up @@ -454,8 +461,12 @@ class MarkdownBuilder implements md.NodeVisitor {
}

if (builders.containsKey(tag)) {
final Widget? child =
builders[tag]!.visitElementAfter(element, styleSheet.styles[tag]);
final Widget? child = builders[tag]!.visitElementAfterWithContext(
delegate.context,
element,
styleSheet.styles[tag],
parent.style,
);
if (child != null) {
if (current.children.isEmpty) {
current.children.add(child);
Expand Down
20 changes: 20 additions & 0 deletions packages/flutter_markdown/lib/src/widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,33 @@ abstract class MarkdownElementBuilder {
/// If you needn't build a widget, return null.
Widget? visitText(md.Text text, TextStyle? preferredStyle) => null;

/// Called when an Element has been reached, after its children have been
/// visited.
///
/// If [MarkdownWidget.styleSheet] has a style with this tag, it will be
/// passed as [preferredStyle].
///
/// If parent element has [TextStyle] set, it will be passed as
/// [parentStyle].
///
/// If a widget build isn't needed, return null.
Widget? visitElementAfterWithContext(
BuildContext context,
md.Element element,
TextStyle? preferredStyle,
TextStyle? parentStyle,
) {
return visitElementAfter(element, preferredStyle);
}

/// Called when an Element has been reached, after its children have been
/// visited.
///
/// If [MarkdownWidget.styleSheet] has a style of this tag, will passing
/// to [preferredStyle].
///
/// If you needn't build a widget, return null.
@Deprecated('Use visitElementAfterWithContext() instead.')
Widget? visitElementAfter(md.Element element, TextStyle? preferredStyle) =>
null;
}
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.6.16
version: 0.6.17

environment:
sdk: ">=3.0.0 <4.0.0"
Expand Down
81 changes: 80 additions & 1 deletion packages/flutter_markdown/test/custom_syntax_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// found in the LICENSE file.

import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:markdown/markdown.dart' as md;
Expand Down Expand Up @@ -83,6 +83,34 @@ void defineTests() {
expect(widgetSpan.child, isInstanceOf<Container>());
},
);

testWidgets(
'visitElementAfterWithContext is handled correctly',
(WidgetTester tester) async {
await tester.pumpWidget(
boilerplate(
Markdown(
data: r'# This is a header with some \color1{color} in it',
extensionSet: md.ExtensionSet.none,
inlineSyntaxes: <md.InlineSyntax>[InlineTextColorSyntax()],
builders: <String, MarkdownElementBuilder>{
'inlineTextColor': InlineTextColorElementBuilder(),
},
),
),
);

final RichText textWidget = tester.widget(find.byType(RichText));
final TextSpan rootSpan = textWidget.text as TextSpan;
final TextSpan firstSpan = rootSpan.children![0] as TextSpan;
final TextSpan secondSpan = rootSpan.children![1] as TextSpan;
final TextSpan thirdSpan = rootSpan.children![2] as TextSpan;

expect(secondSpan.style!.color, Colors.red);
expect(secondSpan.style!.fontSize, firstSpan.style!.fontSize);
expect(secondSpan.style!.fontSize, thirdSpan.style!.fontSize);
},
);
});

testWidgets(
Expand Down Expand Up @@ -250,6 +278,57 @@ class ContainerBuilder2 extends MarkdownElementBuilder {
}
}

// Note: The implementation of inline span is incomplete, it does not handle
// bold, italic, ... text with a colored block.
// This would not work: `\color1{Text with *bold* text}`
class InlineTextColorSyntax extends md.InlineSyntax {
InlineTextColorSyntax() : super(r'\\color([1-9]){(.*?)}');

@override
bool onMatch(md.InlineParser parser, Match match) {
final String colorId = match.group(1)!;
final String textContent = match.group(2)!;
final md.Element node = md.Element.text(
'inlineTextColor',
textContent,
)..attributes['color'] = colorId;

parser.addNode(node);

parser.addNode(
md.Text(''),
);
return true;
}
}

class InlineTextColorElementBuilder extends MarkdownElementBuilder {
@override
Widget visitElementAfterWithContext(
BuildContext context,
md.Element element,
TextStyle? preferredStyle,
TextStyle? parentStyle,
) {
final String innerText = element.textContent;
final String color = element.attributes['color'] ?? '';

final Map<String, Color> contentColors = <String, Color>{
'1': Colors.red,
'2': Colors.green,
'3': Colors.blue,
};
final Color? contentColor = contentColors[color];

return RichText(
text: TextSpan(
text: innerText,
style: parentStyle?.copyWith(color: contentColor),
),
);
}
}

class ImgBuilder extends MarkdownElementBuilder {
@override
Widget? visitElementAfter(md.Element element, TextStyle? preferredStyle) {
Expand Down