Skip to content

Commit

Permalink
Allow custom blocks to be something other than Column or SizedBox (
Browse files Browse the repository at this point in the history
…flutter#7859)

# Description

This adds support for allowing block tags recognized by the Markdown processor to insert something other than just a `Column` or a `SizedBox` (the defaults for blocks with children, and without). Without this ability, custom builders can't insert their own widgets to, say, make it be a colored container instead.

This addresses a customer request.

Fixes flutter/flutter#135848
  • Loading branch information
gspencergoog authored Oct 21, 2024
1 parent 2c1b4a7 commit aad3fd0
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 12 deletions.
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.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 `<blockquote>` instead of hard-wrapping like a `<pre>` block.
Expand Down
31 changes: 20 additions & 11 deletions packages/flutter_markdown/lib/src/builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
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.7.4
version: 0.7.4+1

environment:
sdk: ^3.3.0
Expand Down
80 changes: 80 additions & 0 deletions packages/flutter_markdown/test/custom_syntax_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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: <md.BlockSyntax>[CustomTagBlockSyntax()],
builders: <String, MarkdownElementBuilder>{
'custom': CustomTagBlockBuilder(),
},
),
),
);

final ColoredBox container =
tester.widgetList(find.byType(ColoredBox)).first as ColoredBox;
expect(container.color, Colors.red);
expect(container.child, isInstanceOf<Text>());
expect((container.child! as Text).data, blockContent);
},
);

testWidgets(
'link for wikistyle',
(WidgetTester tester) async {
Expand Down Expand Up @@ -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;
}
}

0 comments on commit aad3fd0

Please sign in to comment.