diff --git a/packages/flutter_markdown/CHANGELOG.md b/packages/flutter_markdown/CHANGELOG.md index dba9cb95878..002d6d150ab 100644 --- a/packages/flutter_markdown/CHANGELOG.md +++ b/packages/flutter_markdown/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.7.4+1 + +* Makes it so that custom blocks are not limited to being a Column or + SizedBox. + ## 0.7.4 * Makes paragraphs in blockquotes soft-wrap like a normal `
` instead of hard-wrapping like a `
` block.
diff --git a/packages/flutter_markdown/lib/src/builder.dart b/packages/flutter_markdown/lib/src/builder.dart
index 3a0f4b15e99..ea9af16acfb 100644
--- a/packages/flutter_markdown/lib/src/builder.dart
+++ b/packages/flutter_markdown/lib/src/builder.dart
@@ -383,20 +383,29 @@ class MarkdownBuilder implements md.NodeVisitor {
       _addAnonymousBlockIfNeeded();
 
       final _BlockElement current = _blocks.removeLast();
-      Widget child;
 
-      if (current.children.isNotEmpty) {
-        child = Column(
-          mainAxisSize: MainAxisSize.min,
-          crossAxisAlignment: fitContent
-              ? CrossAxisAlignment.start
-              : CrossAxisAlignment.stretch,
-          children: current.children,
-        );
-      } else {
-        child = const SizedBox();
+      Widget defaultChild() {
+        if (current.children.isNotEmpty) {
+          return Column(
+            mainAxisSize: MainAxisSize.min,
+            crossAxisAlignment: fitContent
+                ? CrossAxisAlignment.start
+                : CrossAxisAlignment.stretch,
+            children: current.children,
+          );
+        } else {
+          return const SizedBox();
+        }
       }
 
+      Widget child = builders[tag]?.visitElementAfterWithContext(
+            delegate.context,
+            element,
+            styleSheet.styles[tag],
+            _inlines.isNotEmpty ? _inlines.last.style : null,
+          ) ??
+          defaultChild();
+
       if (_isListTag(tag)) {
         assert(_listIndents.isNotEmpty);
         _listIndents.removeLast();
diff --git a/packages/flutter_markdown/pubspec.yaml b/packages/flutter_markdown/pubspec.yaml
index f0ac1fd8353..5f743a8b032 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.4
+version: 0.7.4+1
 
 environment:
   sdk: ^3.3.0
diff --git a/packages/flutter_markdown/test/custom_syntax_test.dart b/packages/flutter_markdown/test/custom_syntax_test.dart
index 28d55cdfd0b..8dc0c806e51 100644
--- a/packages/flutter_markdown/test/custom_syntax_test.dart
+++ b/packages/flutter_markdown/test/custom_syntax_test.dart
@@ -59,6 +59,35 @@ void defineTests() {
       },
     );
 
+    testWidgets(
+      'Block with custom tag',
+      (WidgetTester tester) async {
+        const String textBefore = 'Before ';
+        const String textAfter = ' After';
+        const String blockContent = 'Custom content rendered in a ColoredBox';
+
+        await tester.pumpWidget(
+          boilerplate(
+            Markdown(
+              data:
+                  '$textBefore\n{{custom}}\n$blockContent\n{{/custom}}\n$textAfter',
+              extensionSet: md.ExtensionSet.none,
+              blockSyntaxes: [CustomTagBlockSyntax()],
+              builders: {
+                'custom': CustomTagBlockBuilder(),
+              },
+            ),
+          ),
+        );
+
+        final ColoredBox container =
+            tester.widgetList(find.byType(ColoredBox)).first as ColoredBox;
+        expect(container.color, Colors.red);
+        expect(container.child, isInstanceOf());
+        expect((container.child! as Text).data, blockContent);
+      },
+    );
+
     testWidgets(
       'link for wikistyle',
       (WidgetTester tester) async {
@@ -380,3 +409,54 @@ class NoteSyntax extends md.BlockSyntax {
   @override
   RegExp get pattern => RegExp(r'^\[!NOTE] ');
 }
+
+class CustomTagBlockBuilder extends MarkdownElementBuilder {
+  @override
+  bool isBlockElement() => true;
+
+  @override
+  Widget visitElementAfterWithContext(
+    BuildContext context,
+    md.Element element,
+    TextStyle? preferredStyle,
+    TextStyle? parentStyle,
+  ) {
+    if (element.tag == 'custom') {
+      final String content = element.attributes['content']!;
+      return ColoredBox(
+          color: Colors.red, child: Text(content, style: preferredStyle));
+    }
+    return const SizedBox.shrink();
+  }
+}
+
+class CustomTagBlockSyntax extends md.BlockSyntax {
+  @override
+  bool canParse(md.BlockParser parser) {
+    return parser.current.content.startsWith('{{custom}}');
+  }
+
+  @override
+  RegExp get pattern => RegExp(r'\{\{custom\}\}([\s\S]*?)\{\{/custom\}\}');
+
+  @override
+  md.Node parse(md.BlockParser parser) {
+    parser.advance();
+
+    final StringBuffer buffer = StringBuffer();
+    while (
+        !parser.current.content.startsWith('{{/custom}}') && !parser.isDone) {
+      buffer.writeln(parser.current.content);
+      parser.advance();
+    }
+
+    if (!parser.isDone) {
+      parser.advance();
+    }
+
+    final String content = buffer.toString().trim();
+    final md.Element element = md.Element.empty('custom');
+    element.attributes['content'] = content;
+    return element;
+  }
+}