From e3e7045fe03a3357be8ced2c105b34561e2ae013 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Fri, 11 Oct 2024 16:41:05 -0700 Subject: [PATCH 1/6] Add support for custom tag blocks --- .../flutter_markdown/lib/src/builder.dart | 23 +++++- packages/flutter_markdown/lib/src/widget.dart | 7 ++ .../test/custom_syntax_test.dart | 79 +++++++++++++++++++ 3 files changed, 107 insertions(+), 2 deletions(-) diff --git a/packages/flutter_markdown/lib/src/builder.dart b/packages/flutter_markdown/lib/src/builder.dart index 3a0f4b15e99..f4bdc11a74f 100644 --- a/packages/flutter_markdown/lib/src/builder.dart +++ b/packages/flutter_markdown/lib/src/builder.dart @@ -33,8 +33,6 @@ final List _kBlockTags = [ const List _kListTags = ['ul', 'ol']; -bool _isBlockTag(String? tag) => _kBlockTags.contains(tag); - bool _isListTag(String tag) => _kListTags.contains(tag); class _BlockElement { @@ -111,6 +109,7 @@ class MarkdownBuilder implements md.NodeVisitor { required this.builders, required this.paddingBuilders, required this.listItemCrossAxisAlignment, + required this.customBlockTags, this.fitContent = false, this.onSelectionChanged, this.onTapText, @@ -156,6 +155,9 @@ class MarkdownBuilder implements md.NodeVisitor { /// does not allow for intrinsic height measurements. final MarkdownListItemCrossAxisAlignment listItemCrossAxisAlignment; + /// Collection of custom block tags to be used building block widgets. + final List? customBlockTags; + /// Called when the user changes selection when [selectable] is set to true. final MarkdownOnSelectionChangedCallback? onSelectionChanged; @@ -178,6 +180,10 @@ class MarkdownBuilder implements md.NodeVisitor { String? _lastVisitedTag; bool _isInBlockquote = false; + bool _isBlockTag(String? tag) => + _kBlockTags.contains(tag) || + (customBlockTags ?? []).contains(tag); + /// Returns widgets that display the given Markdown nodes. /// /// The returned widgets are typically used as children in a [ListView]. @@ -397,6 +403,19 @@ class MarkdownBuilder implements md.NodeVisitor { child = const SizedBox(); } + if (builders.containsKey(tag)) { + final Widget? builderChild = + builders[tag]!.visitElementAfterWithContext( + delegate.context, + element, + styleSheet.styles[tag], + _inlines.isNotEmpty ? _inlines.last.style : null, + ); + if (builderChild != null) { + child = builderChild; + } + } + if (_isListTag(tag)) { assert(_listIndents.isNotEmpty); _listIndents.removeLast(); diff --git a/packages/flutter_markdown/lib/src/widget.dart b/packages/flutter_markdown/lib/src/widget.dart index be7f9d7047c..cbd874c44ab 100644 --- a/packages/flutter_markdown/lib/src/widget.dart +++ b/packages/flutter_markdown/lib/src/widget.dart @@ -217,6 +217,7 @@ abstract class MarkdownWidget extends StatefulWidget { this.onTapLink, this.onTapText, this.imageDirectory, + this.customBlockTags, this.blockSyntaxes, this.inlineSyntaxes, this.extensionSet, @@ -269,6 +270,9 @@ abstract class MarkdownWidget extends StatefulWidget { /// Collection of custom block syntax types to be used parsing the Markdown data. final List? blockSyntaxes; + /// Collection of custom block tags to be used building block widgets. + final List? customBlockTags; + /// Collection of custom inline syntax types to be used parsing the Markdown data. final List? inlineSyntaxes; @@ -393,6 +397,7 @@ class _MarkdownWidgetState extends State imageBuilder: widget.imageBuilder, checkboxBuilder: widget.checkboxBuilder, bulletBuilder: widget.bulletBuilder, + customBlockTags: widget.customBlockTags, builders: widget.builders, paddingBuilders: widget.paddingBuilders, fitContent: widget.fitContent, @@ -464,6 +469,7 @@ class MarkdownBody extends MarkdownWidget { super.onTapLink, super.onTapText, super.imageDirectory, + super.customBlockTags, super.blockSyntaxes, super.inlineSyntaxes, super.extensionSet, @@ -519,6 +525,7 @@ class Markdown extends MarkdownWidget { super.onTapLink, super.onTapText, super.imageDirectory, + super.customBlockTags, super.blockSyntaxes, super.inlineSyntaxes, super.extensionSet, diff --git a/packages/flutter_markdown/test/custom_syntax_test.dart b/packages/flutter_markdown/test/custom_syntax_test.dart index 28d55cdfd0b..2a0d4dfe32a 100644 --- a/packages/flutter_markdown/test/custom_syntax_test.dart +++ b/packages/flutter_markdown/test/custom_syntax_test.dart @@ -48,6 +48,7 @@ void defineTests() { builders: { 'note': NoteBuilder(), }, + customBlockTags: const ['note'], ), ), ); @@ -59,6 +60,36 @@ 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(), + }, + customBlockTags: const ['custom'], + ), + ), + ); + + 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 +411,51 @@ class NoteSyntax extends md.BlockSyntax { @override RegExp get pattern => RegExp(r'^\[!NOTE] '); } + +class CustomTagBlockBuilder extends MarkdownElementBuilder { + @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; + } +} From 21b8275d6e84b6647961abe1a675255dc3b23451 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Fri, 11 Oct 2024 17:14:42 -0700 Subject: [PATCH 2/6] Bump version --- packages/flutter_markdown/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/flutter_markdown/CHANGELOG.md b/packages/flutter_markdown/CHANGELOG.md index dba9cb95878..66df28d3e26 100644 --- a/packages/flutter_markdown/CHANGELOG.md +++ b/packages/flutter_markdown/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.7.5 + +* Added the ability to extend the list of allowed customizable block tags with the + `customBlockTags` attribute on `MarkdownBuilder`. + ## 0.7.4 * Makes paragraphs in blockquotes soft-wrap like a normal `
` instead of hard-wrapping like a `
` block.

From 214421523232ed4fcdb61a4e39e42e756e2ebd1b Mon Sep 17 00:00:00 2001
From: Greg Spencer 
Date: Mon, 14 Oct 2024 11:00:10 -0700
Subject: [PATCH 3/6] Review Changes

---
 packages/flutter_markdown/lib/src/builder.dart | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/flutter_markdown/lib/src/builder.dart b/packages/flutter_markdown/lib/src/builder.dart
index f4bdc11a74f..d1a9a6060aa 100644
--- a/packages/flutter_markdown/lib/src/builder.dart
+++ b/packages/flutter_markdown/lib/src/builder.dart
@@ -156,7 +156,7 @@ class MarkdownBuilder implements md.NodeVisitor {
   final MarkdownListItemCrossAxisAlignment listItemCrossAxisAlignment;
 
   /// Collection of custom block tags to be used building block widgets.
-  final List? customBlockTags;
+  final List customBlockTags;
 
   /// Called when the user changes selection when [selectable] is set to true.
   final MarkdownOnSelectionChangedCallback? onSelectionChanged;
@@ -182,7 +182,7 @@ class MarkdownBuilder implements md.NodeVisitor {
 
   bool _isBlockTag(String? tag) =>
       _kBlockTags.contains(tag) ||
-      (customBlockTags ?? []).contains(tag);
+      customBlockTags.contains(tag);
 
   /// Returns widgets that display the given Markdown nodes.
   ///

From 391f89f5a2ac722f38814f5a33482666c62858ac Mon Sep 17 00:00:00 2001
From: Greg Spencer 
Date: Mon, 14 Oct 2024 11:20:26 -0700
Subject: [PATCH 4/6] Remove new API, update test

---
 packages/flutter_markdown/CHANGELOG.md        |  4 +-
 .../flutter_markdown/lib/src/builder.dart     | 38 +++++++++----------
 packages/flutter_markdown/lib/src/widget.dart |  7 ----
 .../test/custom_syntax_test.dart              |  7 +++-
 4 files changed, 24 insertions(+), 32 deletions(-)

diff --git a/packages/flutter_markdown/CHANGELOG.md b/packages/flutter_markdown/CHANGELOG.md
index 66df28d3e26..3dc35af2687 100644
--- a/packages/flutter_markdown/CHANGELOG.md
+++ b/packages/flutter_markdown/CHANGELOG.md
@@ -1,7 +1,7 @@
 ## 0.7.5
 
-* Added the ability to extend the list of allowed customizable block tags with the
-  `customBlockTags` attribute on `MarkdownBuilder`.
+* Makes it so that custom blocks are not limited to being a Column or
+  SizedBox.
 
 ## 0.7.4
 
diff --git a/packages/flutter_markdown/lib/src/builder.dart b/packages/flutter_markdown/lib/src/builder.dart
index d1a9a6060aa..ab003f67926 100644
--- a/packages/flutter_markdown/lib/src/builder.dart
+++ b/packages/flutter_markdown/lib/src/builder.dart
@@ -33,6 +33,8 @@ final List _kBlockTags = [
 
 const List _kListTags = ['ul', 'ol'];
 
+bool _isBlockTag(String? tag) => _kBlockTags.contains(tag);
+
 bool _isListTag(String tag) => _kListTags.contains(tag);
 
 class _BlockElement {
@@ -109,7 +111,6 @@ class MarkdownBuilder implements md.NodeVisitor {
     required this.builders,
     required this.paddingBuilders,
     required this.listItemCrossAxisAlignment,
-    required this.customBlockTags,
     this.fitContent = false,
     this.onSelectionChanged,
     this.onTapText,
@@ -155,9 +156,6 @@ class MarkdownBuilder implements md.NodeVisitor {
   /// does not allow for intrinsic height measurements.
   final MarkdownListItemCrossAxisAlignment listItemCrossAxisAlignment;
 
-  /// Collection of custom block tags to be used building block widgets.
-  final List customBlockTags;
-
   /// Called when the user changes selection when [selectable] is set to true.
   final MarkdownOnSelectionChangedCallback? onSelectionChanged;
 
@@ -180,10 +178,6 @@ class MarkdownBuilder implements md.NodeVisitor {
   String? _lastVisitedTag;
   bool _isInBlockquote = false;
 
-  bool _isBlockTag(String? tag) =>
-      _kBlockTags.contains(tag) ||
-      customBlockTags.contains(tag);
-
   /// Returns widgets that display the given Markdown nodes.
   ///
   /// The returned widgets are typically used as children in a [ListView].
@@ -391,16 +385,18 @@ class MarkdownBuilder implements md.NodeVisitor {
       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();
+        }
       }
 
       if (builders.containsKey(tag)) {
@@ -411,9 +407,9 @@ class MarkdownBuilder implements md.NodeVisitor {
           styleSheet.styles[tag],
           _inlines.isNotEmpty ? _inlines.last.style : null,
         );
-        if (builderChild != null) {
-          child = builderChild;
-        }
+        child = builderChild ?? defaultChild();
+      } else {
+        child = defaultChild();
       }
 
       if (_isListTag(tag)) {
diff --git a/packages/flutter_markdown/lib/src/widget.dart b/packages/flutter_markdown/lib/src/widget.dart
index cbd874c44ab..be7f9d7047c 100644
--- a/packages/flutter_markdown/lib/src/widget.dart
+++ b/packages/flutter_markdown/lib/src/widget.dart
@@ -217,7 +217,6 @@ abstract class MarkdownWidget extends StatefulWidget {
     this.onTapLink,
     this.onTapText,
     this.imageDirectory,
-    this.customBlockTags,
     this.blockSyntaxes,
     this.inlineSyntaxes,
     this.extensionSet,
@@ -270,9 +269,6 @@ abstract class MarkdownWidget extends StatefulWidget {
   /// Collection of custom block syntax types to be used parsing the Markdown data.
   final List? blockSyntaxes;
 
-  /// Collection of custom block tags to be used building block widgets.
-  final List? customBlockTags;
-
   /// Collection of custom inline syntax types to be used parsing the Markdown data.
   final List? inlineSyntaxes;
 
@@ -397,7 +393,6 @@ class _MarkdownWidgetState extends State
       imageBuilder: widget.imageBuilder,
       checkboxBuilder: widget.checkboxBuilder,
       bulletBuilder: widget.bulletBuilder,
-      customBlockTags: widget.customBlockTags,
       builders: widget.builders,
       paddingBuilders: widget.paddingBuilders,
       fitContent: widget.fitContent,
@@ -469,7 +464,6 @@ class MarkdownBody extends MarkdownWidget {
     super.onTapLink,
     super.onTapText,
     super.imageDirectory,
-    super.customBlockTags,
     super.blockSyntaxes,
     super.inlineSyntaxes,
     super.extensionSet,
@@ -525,7 +519,6 @@ class Markdown extends MarkdownWidget {
     super.onTapLink,
     super.onTapText,
     super.imageDirectory,
-    super.customBlockTags,
     super.blockSyntaxes,
     super.inlineSyntaxes,
     super.extensionSet,
diff --git a/packages/flutter_markdown/test/custom_syntax_test.dart b/packages/flutter_markdown/test/custom_syntax_test.dart
index 2a0d4dfe32a..ed24f2c9a17 100644
--- a/packages/flutter_markdown/test/custom_syntax_test.dart
+++ b/packages/flutter_markdown/test/custom_syntax_test.dart
@@ -48,7 +48,6 @@ void defineTests() {
               builders: {
                 'note': NoteBuilder(),
               },
-              customBlockTags: const ['note'],
             ),
           ),
         );
@@ -77,7 +76,6 @@ void defineTests() {
               builders: {
                 'custom': CustomTagBlockBuilder(),
               },
-              customBlockTags: const ['custom'],
             ),
           ),
         );
@@ -413,6 +411,11 @@ class NoteSyntax extends md.BlockSyntax {
 }
 
 class CustomTagBlockBuilder extends MarkdownElementBuilder {
+  @override
+  bool isBlockElement() {
+    return true;
+  }
+
   @override
   Widget visitElementAfterWithContext(
     BuildContext context,

From 869aa9a3fc0aaaf7c5fe5b229447a62f6194a94e Mon Sep 17 00:00:00 2001
From: Greg Spencer 
Date: Mon, 14 Oct 2024 12:36:42 -0700
Subject: [PATCH 5/6] Fix version bump

---
 packages/flutter_markdown/CHANGELOG.md                 | 2 +-
 packages/flutter_markdown/pubspec.yaml                 | 2 +-
 packages/flutter_markdown/test/custom_syntax_test.dart | 4 +---
 3 files changed, 3 insertions(+), 5 deletions(-)

diff --git a/packages/flutter_markdown/CHANGELOG.md b/packages/flutter_markdown/CHANGELOG.md
index 3dc35af2687..002d6d150ab 100644
--- a/packages/flutter_markdown/CHANGELOG.md
+++ b/packages/flutter_markdown/CHANGELOG.md
@@ -1,4 +1,4 @@
-## 0.7.5
+## 0.7.4+1
 
 * Makes it so that custom blocks are not limited to being a Column or
   SizedBox.
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 ed24f2c9a17..8dc0c806e51 100644
--- a/packages/flutter_markdown/test/custom_syntax_test.dart
+++ b/packages/flutter_markdown/test/custom_syntax_test.dart
@@ -412,9 +412,7 @@ class NoteSyntax extends md.BlockSyntax {
 
 class CustomTagBlockBuilder extends MarkdownElementBuilder {
   @override
-  bool isBlockElement() {
-    return true;
-  }
+  bool isBlockElement() => true;
 
   @override
   Widget visitElementAfterWithContext(

From e6ed41310b8c52bf2045621fd89a2423bc40344f Mon Sep 17 00:00:00 2001
From: Greg Spencer 
Date: Mon, 21 Oct 2024 13:01:51 -0700
Subject: [PATCH 6/6] Address review comments

---
 .../flutter_markdown/lib/src/builder.dart     | 20 +++++++------------
 1 file changed, 7 insertions(+), 13 deletions(-)

diff --git a/packages/flutter_markdown/lib/src/builder.dart b/packages/flutter_markdown/lib/src/builder.dart
index ab003f67926..ea9af16acfb 100644
--- a/packages/flutter_markdown/lib/src/builder.dart
+++ b/packages/flutter_markdown/lib/src/builder.dart
@@ -383,7 +383,6 @@ class MarkdownBuilder implements md.NodeVisitor {
       _addAnonymousBlockIfNeeded();
 
       final _BlockElement current = _blocks.removeLast();
-      Widget child;
 
       Widget defaultChild() {
         if (current.children.isNotEmpty) {
@@ -399,18 +398,13 @@ class MarkdownBuilder implements md.NodeVisitor {
         }
       }
 
-      if (builders.containsKey(tag)) {
-        final Widget? builderChild =
-            builders[tag]!.visitElementAfterWithContext(
-          delegate.context,
-          element,
-          styleSheet.styles[tag],
-          _inlines.isNotEmpty ? _inlines.last.style : null,
-        );
-        child = builderChild ?? defaultChild();
-      } else {
-        child = defaultChild();
-      }
+      Widget child = builders[tag]?.visitElementAfterWithContext(
+            delegate.context,
+            element,
+            styleSheet.styles[tag],
+            _inlines.isNotEmpty ? _inlines.last.style : null,
+          ) ??
+          defaultChild();
 
       if (_isListTag(tag)) {
         assert(_listIndents.isNotEmpty);