Skip to content

Commit 8918e93

Browse files
authored
[flutter_markdown] Pass parent TextStyle down to MarkdownElementBuilder.visitElementAfter (flutter#4393)
The parent `TextStyle` should be passed down to the `MarkdownElementBuilder.visitElementAfter` method to allow custom markdown tags to override only part of the text style, e.g. the color, but keep all the rest of the styles the same. This is especially useful when trying to color markdown headers in a certain color, as the parent font size, font family, etc. all are passed down and can be kept, while only the color is overridden. This will unfortunately lead to a breaking change, due to the nature of how the class is typically used. As all usages of the class are sub-classes any change to the method schema will result in a breaking change! Enables the following flutter#105571 replaces flutter/packages#3281
1 parent cd19bad commit 8918e93

File tree

5 files changed

+119
-4
lines changed

5 files changed

+119
-4
lines changed

packages/flutter_markdown/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.6.17
2+
3+
* Introduces a new `MarkdownElementBuilder.visitElementAfterWithContext()` method passing the widget `BuildContext` and
4+
the parent text's `TextStyle`.
5+
16
## 0.6.16
27

38
* Adds `tableVerticalAlignment` property to allow aligning table cells vertically.

packages/flutter_markdown/lib/src/builder.dart

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ class _InlineElement {
7474

7575
/// A delegate used by [MarkdownBuilder] to control the widgets it creates.
7676
abstract class MarkdownBuilderDelegate {
77+
/// Returns the [BuildContext] of the [MarkdownWidget].
78+
///
79+
/// The context will be passed down to the
80+
/// [MarkdownElementBuilder.visitElementBefore] method and allows elements to
81+
/// get information from the context.
82+
BuildContext get context;
83+
7784
/// Returns a gesture recognizer to use for an `a` element with the given
7885
/// text, `href` attribute, and title.
7986
GestureRecognizer createLink(String text, String? href, String title);
@@ -454,8 +461,12 @@ class MarkdownBuilder implements md.NodeVisitor {
454461
}
455462

456463
if (builders.containsKey(tag)) {
457-
final Widget? child =
458-
builders[tag]!.visitElementAfter(element, styleSheet.styles[tag]);
464+
final Widget? child = builders[tag]!.visitElementAfterWithContext(
465+
delegate.context,
466+
element,
467+
styleSheet.styles[tag],
468+
parent.style,
469+
);
459470
if (child != null) {
460471
if (current.children.isEmpty) {
461472
current.children.add(child);

packages/flutter_markdown/lib/src/widget.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,33 @@ abstract class MarkdownElementBuilder {
7070
/// If you needn't build a widget, return null.
7171
Widget? visitText(md.Text text, TextStyle? preferredStyle) => null;
7272

73+
/// Called when an Element has been reached, after its children have been
74+
/// visited.
75+
///
76+
/// If [MarkdownWidget.styleSheet] has a style with this tag, it will be
77+
/// passed as [preferredStyle].
78+
///
79+
/// If parent element has [TextStyle] set, it will be passed as
80+
/// [parentStyle].
81+
///
82+
/// If a widget build isn't needed, return null.
83+
Widget? visitElementAfterWithContext(
84+
BuildContext context,
85+
md.Element element,
86+
TextStyle? preferredStyle,
87+
TextStyle? parentStyle,
88+
) {
89+
return visitElementAfter(element, preferredStyle);
90+
}
91+
7392
/// Called when an Element has been reached, after its children have been
7493
/// visited.
7594
///
7695
/// If [MarkdownWidget.styleSheet] has a style of this tag, will passing
7796
/// to [preferredStyle].
7897
///
7998
/// If you needn't build a widget, return null.
99+
@Deprecated('Use visitElementAfterWithContext() instead.')
80100
Widget? visitElementAfter(md.Element element, TextStyle? preferredStyle) =>
81101
null;
82102
}

packages/flutter_markdown/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ description: A Markdown renderer for Flutter. Create rich text output,
44
formatted with simple Markdown tags.
55
repository: https://github.com/flutter/packages/tree/main/packages/flutter_markdown
66
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+flutter_markdown%22
7-
version: 0.6.16
7+
version: 0.6.17
88

99
environment:
1010
sdk: ">=3.0.0 <4.0.0"

packages/flutter_markdown/test/custom_syntax_test.dart

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// found in the LICENSE file.
44

55
import 'package:flutter/gestures.dart';
6-
import 'package:flutter/widgets.dart';
6+
import 'package:flutter/material.dart';
77
import 'package:flutter_markdown/flutter_markdown.dart';
88
import 'package:flutter_test/flutter_test.dart';
99
import 'package:markdown/markdown.dart' as md;
@@ -83,6 +83,34 @@ void defineTests() {
8383
expect(widgetSpan.child, isInstanceOf<Container>());
8484
},
8585
);
86+
87+
testWidgets(
88+
'visitElementAfterWithContext is handled correctly',
89+
(WidgetTester tester) async {
90+
await tester.pumpWidget(
91+
boilerplate(
92+
Markdown(
93+
data: r'# This is a header with some \color1{color} in it',
94+
extensionSet: md.ExtensionSet.none,
95+
inlineSyntaxes: <md.InlineSyntax>[InlineTextColorSyntax()],
96+
builders: <String, MarkdownElementBuilder>{
97+
'inlineTextColor': InlineTextColorElementBuilder(),
98+
},
99+
),
100+
),
101+
);
102+
103+
final RichText textWidget = tester.widget(find.byType(RichText));
104+
final TextSpan rootSpan = textWidget.text as TextSpan;
105+
final TextSpan firstSpan = rootSpan.children![0] as TextSpan;
106+
final TextSpan secondSpan = rootSpan.children![1] as TextSpan;
107+
final TextSpan thirdSpan = rootSpan.children![2] as TextSpan;
108+
109+
expect(secondSpan.style!.color, Colors.red);
110+
expect(secondSpan.style!.fontSize, firstSpan.style!.fontSize);
111+
expect(secondSpan.style!.fontSize, thirdSpan.style!.fontSize);
112+
},
113+
);
86114
});
87115

88116
testWidgets(
@@ -250,6 +278,57 @@ class ContainerBuilder2 extends MarkdownElementBuilder {
250278
}
251279
}
252280

281+
// Note: The implementation of inline span is incomplete, it does not handle
282+
// bold, italic, ... text with a colored block.
283+
// This would not work: `\color1{Text with *bold* text}`
284+
class InlineTextColorSyntax extends md.InlineSyntax {
285+
InlineTextColorSyntax() : super(r'\\color([1-9]){(.*?)}');
286+
287+
@override
288+
bool onMatch(md.InlineParser parser, Match match) {
289+
final String colorId = match.group(1)!;
290+
final String textContent = match.group(2)!;
291+
final md.Element node = md.Element.text(
292+
'inlineTextColor',
293+
textContent,
294+
)..attributes['color'] = colorId;
295+
296+
parser.addNode(node);
297+
298+
parser.addNode(
299+
md.Text(''),
300+
);
301+
return true;
302+
}
303+
}
304+
305+
class InlineTextColorElementBuilder extends MarkdownElementBuilder {
306+
@override
307+
Widget visitElementAfterWithContext(
308+
BuildContext context,
309+
md.Element element,
310+
TextStyle? preferredStyle,
311+
TextStyle? parentStyle,
312+
) {
313+
final String innerText = element.textContent;
314+
final String color = element.attributes['color'] ?? '';
315+
316+
final Map<String, Color> contentColors = <String, Color>{
317+
'1': Colors.red,
318+
'2': Colors.green,
319+
'3': Colors.blue,
320+
};
321+
final Color? contentColor = contentColors[color];
322+
323+
return RichText(
324+
text: TextSpan(
325+
text: innerText,
326+
style: parentStyle?.copyWith(color: contentColor),
327+
),
328+
);
329+
}
330+
}
331+
253332
class ImgBuilder extends MarkdownElementBuilder {
254333
@override
255334
Widget? visitElementAfter(md.Element element, TextStyle? preferredStyle) {

0 commit comments

Comments
 (0)