[flutter_markdown] Pass parent TextStyle down to MarkdownElementBuilder.visitElementAfter (#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 https://github.com/flutter/flutter/issues/105571

replaces https://github.com/flutter/packages/pull/3281
diff --git a/packages/flutter_markdown/CHANGELOG.md b/packages/flutter_markdown/CHANGELOG.md
index 82d0964..fa273db 100644
--- a/packages/flutter_markdown/CHANGELOG.md
+++ b/packages/flutter_markdown/CHANGELOG.md
@@ -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.
diff --git a/packages/flutter_markdown/lib/src/builder.dart b/packages/flutter_markdown/lib/src/builder.dart
index f438524..fbdcb31 100644
--- a/packages/flutter_markdown/lib/src/builder.dart
+++ b/packages/flutter_markdown/lib/src/builder.dart
@@ -74,6 +74,13 @@
 
 /// 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);
@@ -454,8 +461,12 @@
       }
 
       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);
diff --git a/packages/flutter_markdown/lib/src/widget.dart b/packages/flutter_markdown/lib/src/widget.dart
index 690f5b8..65dc983 100644
--- a/packages/flutter_markdown/lib/src/widget.dart
+++ b/packages/flutter_markdown/lib/src/widget.dart
@@ -73,10 +73,30 @@
   /// 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;
 }
diff --git a/packages/flutter_markdown/pubspec.yaml b/packages/flutter_markdown/pubspec.yaml
index 8ff74e8..e7485b0 100644
--- a/packages/flutter_markdown/pubspec.yaml
+++ b/packages/flutter_markdown/pubspec.yaml
@@ -4,7 +4,7 @@
   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"
diff --git a/packages/flutter_markdown/test/custom_syntax_test.dart b/packages/flutter_markdown/test/custom_syntax_test.dart
index 46ed3ed..fd81ba7 100644
--- a/packages/flutter_markdown/test/custom_syntax_test.dart
+++ b/packages/flutter_markdown/test/custom_syntax_test.dart
@@ -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;
@@ -83,6 +83,34 @@
         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(
@@ -250,6 +278,57 @@
   }
 }
 
+// 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) {