Skip to content

Commit aad3fd0

Browse files
authored
Allow custom blocks to be something other than Column or SizedBox (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#135848
1 parent 2c1b4a7 commit aad3fd0

File tree

4 files changed

+106
-12
lines changed

4 files changed

+106
-12
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.7.4+1
2+
3+
* Makes it so that custom blocks are not limited to being a Column or
4+
SizedBox.
5+
16
## 0.7.4
27

38
* Makes paragraphs in blockquotes soft-wrap like a normal `<blockquote>` instead of hard-wrapping like a `<pre>` block.

packages/flutter_markdown/lib/src/builder.dart

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -383,20 +383,29 @@ class MarkdownBuilder implements md.NodeVisitor {
383383
_addAnonymousBlockIfNeeded();
384384

385385
final _BlockElement current = _blocks.removeLast();
386-
Widget child;
387386

388-
if (current.children.isNotEmpty) {
389-
child = Column(
390-
mainAxisSize: MainAxisSize.min,
391-
crossAxisAlignment: fitContent
392-
? CrossAxisAlignment.start
393-
: CrossAxisAlignment.stretch,
394-
children: current.children,
395-
);
396-
} else {
397-
child = const SizedBox();
387+
Widget defaultChild() {
388+
if (current.children.isNotEmpty) {
389+
return Column(
390+
mainAxisSize: MainAxisSize.min,
391+
crossAxisAlignment: fitContent
392+
? CrossAxisAlignment.start
393+
: CrossAxisAlignment.stretch,
394+
children: current.children,
395+
);
396+
} else {
397+
return const SizedBox();
398+
}
398399
}
399400

401+
Widget child = builders[tag]?.visitElementAfterWithContext(
402+
delegate.context,
403+
element,
404+
styleSheet.styles[tag],
405+
_inlines.isNotEmpty ? _inlines.last.style : null,
406+
) ??
407+
defaultChild();
408+
400409
if (_isListTag(tag)) {
401410
assert(_listIndents.isNotEmpty);
402411
_listIndents.removeLast();

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.7.4
7+
version: 0.7.4+1
88

99
environment:
1010
sdk: ^3.3.0

packages/flutter_markdown/test/custom_syntax_test.dart

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,35 @@ void defineTests() {
5959
},
6060
);
6161

62+
testWidgets(
63+
'Block with custom tag',
64+
(WidgetTester tester) async {
65+
const String textBefore = 'Before ';
66+
const String textAfter = ' After';
67+
const String blockContent = 'Custom content rendered in a ColoredBox';
68+
69+
await tester.pumpWidget(
70+
boilerplate(
71+
Markdown(
72+
data:
73+
'$textBefore\n{{custom}}\n$blockContent\n{{/custom}}\n$textAfter',
74+
extensionSet: md.ExtensionSet.none,
75+
blockSyntaxes: <md.BlockSyntax>[CustomTagBlockSyntax()],
76+
builders: <String, MarkdownElementBuilder>{
77+
'custom': CustomTagBlockBuilder(),
78+
},
79+
),
80+
),
81+
);
82+
83+
final ColoredBox container =
84+
tester.widgetList(find.byType(ColoredBox)).first as ColoredBox;
85+
expect(container.color, Colors.red);
86+
expect(container.child, isInstanceOf<Text>());
87+
expect((container.child! as Text).data, blockContent);
88+
},
89+
);
90+
6291
testWidgets(
6392
'link for wikistyle',
6493
(WidgetTester tester) async {
@@ -380,3 +409,54 @@ class NoteSyntax extends md.BlockSyntax {
380409
@override
381410
RegExp get pattern => RegExp(r'^\[!NOTE] ');
382411
}
412+
413+
class CustomTagBlockBuilder extends MarkdownElementBuilder {
414+
@override
415+
bool isBlockElement() => true;
416+
417+
@override
418+
Widget visitElementAfterWithContext(
419+
BuildContext context,
420+
md.Element element,
421+
TextStyle? preferredStyle,
422+
TextStyle? parentStyle,
423+
) {
424+
if (element.tag == 'custom') {
425+
final String content = element.attributes['content']!;
426+
return ColoredBox(
427+
color: Colors.red, child: Text(content, style: preferredStyle));
428+
}
429+
return const SizedBox.shrink();
430+
}
431+
}
432+
433+
class CustomTagBlockSyntax extends md.BlockSyntax {
434+
@override
435+
bool canParse(md.BlockParser parser) {
436+
return parser.current.content.startsWith('{{custom}}');
437+
}
438+
439+
@override
440+
RegExp get pattern => RegExp(r'\{\{custom\}\}([\s\S]*?)\{\{/custom\}\}');
441+
442+
@override
443+
md.Node parse(md.BlockParser parser) {
444+
parser.advance();
445+
446+
final StringBuffer buffer = StringBuffer();
447+
while (
448+
!parser.current.content.startsWith('{{/custom}}') && !parser.isDone) {
449+
buffer.writeln(parser.current.content);
450+
parser.advance();
451+
}
452+
453+
if (!parser.isDone) {
454+
parser.advance();
455+
}
456+
457+
final String content = buffer.toString().trim();
458+
final md.Element element = md.Element.empty('custom');
459+
element.attributes['content'] = content;
460+
return element;
461+
}
462+
}

0 commit comments

Comments
 (0)