Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
}