Allow custom bullet points through a widget builder (#167)

This commit is contained in:
Ryan Dixon
2021-03-19 21:02:02 +00:00
committed by GitHub
parent 2247f1da86
commit 0191c32003
4 changed files with 96 additions and 111 deletions

View File

@ -1,3 +1,7 @@
## 0.6.1
* Added builder option bulletBuilder
## 0.6.0 ## 0.6.0
* Null safety release * Null safety release

View File

@ -10,25 +10,7 @@ import '_functions_io.dart' if (dart.library.html) '_functions_web.dart';
import 'style_sheet.dart'; import 'style_sheet.dart';
import 'widget.dart'; import 'widget.dart';
const List<String> _kBlockTags = const <String>[ const List<String> _kBlockTags = const <String>['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'blockquote', 'pre', 'ol', 'ul', 'hr', 'table', 'thead', 'tbody', 'tr'];
'p',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'li',
'blockquote',
'pre',
'ol',
'ul',
'hr',
'table',
'thead',
'tbody',
'tr'
];
const List<String> _kListTags = const <String>['ul', 'ol']; const List<String> _kListTags = const <String>['ul', 'ol'];
@ -99,6 +81,7 @@ class MarkdownBuilder implements md.NodeVisitor {
required this.imageDirectory, required this.imageDirectory,
required this.imageBuilder, required this.imageBuilder,
required this.checkboxBuilder, required this.checkboxBuilder,
required this.bulletBuilder,
required this.builders, required this.builders,
required this.listItemCrossAxisAlignment, required this.listItemCrossAxisAlignment,
this.fitContent = false, this.fitContent = false,
@ -125,6 +108,9 @@ class MarkdownBuilder implements md.NodeVisitor {
/// Call when build a checkbox widget. /// Call when build a checkbox widget.
final MarkdownCheckboxBuilder? checkboxBuilder; final MarkdownCheckboxBuilder? checkboxBuilder;
/// Called when building a custom bullet.
final MarkdownBulletBuilder? bulletBuilder;
/// Call when build a custom widget. /// Call when build a custom widget.
final Map<String, MarkdownElementBuilder> builders; final Map<String, MarkdownElementBuilder> builders;
@ -188,16 +174,14 @@ class MarkdownBuilder implements md.NodeVisitor {
_addAnonymousBlockIfNeeded(); _addAnonymousBlockIfNeeded();
if (_isListTag(tag)) { if (_isListTag(tag)) {
_listIndents.add(tag); _listIndents.add(tag);
if (element.attributes["start"] != null) if (element.attributes["start"] != null) start = int.parse(element.attributes["start"]!) - 1;
start = int.parse(element.attributes["start"]!) - 1;
} else if (tag == 'blockquote') { } else if (tag == 'blockquote') {
_isInBlockquote = true; _isInBlockquote = true;
} else if (tag == 'table') { } else if (tag == 'table') {
_tables.add(_TableElement()); _tables.add(_TableElement());
} else if (tag == 'tr') { } else if (tag == 'tr') {
final length = _tables.single.rows.length; final length = _tables.single.rows.length;
BoxDecoration? decoration = BoxDecoration? decoration = styleSheet.tableCellsDecoration as BoxDecoration?;
styleSheet.tableCellsDecoration as BoxDecoration?;
if (length == 0 || length % 2 == 1) decoration = null; if (length == 0 || length % 2 == 1) decoration = null;
_tables.single.rows.add(TableRow( _tables.single.rows.add(TableRow(
decoration: decoration, decoration: decoration,
@ -227,9 +211,7 @@ class MarkdownBuilder implements md.NodeVisitor {
// The Markdown parser passes empty table data tags for blank // The Markdown parser passes empty table data tags for blank
// table cells. Insert a text node with an empty string in this // table cells. Insert a text node with an empty string in this
// case for the table cell to get properly created. // case for the table cell to get properly created.
if (element.tag == 'td' && if (element.tag == 'td' && element.children != null && element.children!.isEmpty) {
element.children != null &&
element.children!.isEmpty) {
element.children!.add(md.Text('')); element.children!.add(md.Text(''));
} }
@ -244,13 +226,7 @@ class MarkdownBuilder implements md.NodeVisitor {
} }
String? extractTextFromElement(element) { String? extractTextFromElement(element) {
return element is md.Element && (element.children?.isNotEmpty ?? false) return element is md.Element && (element.children?.isNotEmpty ?? false) ? element.children!.map((e) => e is md.Text ? e.text : extractTextFromElement(e)).join("") : ((element.attributes?.isNotEmpty ?? false) ? element.attributes["alt"] : "");
? element.children!
.map((e) => e is md.Text ? e.text : extractTextFromElement(e))
.join("")
: ((element.attributes?.isNotEmpty ?? false)
? element.attributes["alt"]
: "");
} }
@override @override
@ -286,8 +262,7 @@ class MarkdownBuilder implements md.NodeVisitor {
Widget? child; Widget? child;
if (_blocks.isNotEmpty && builders.containsKey(_blocks.last.tag)) { if (_blocks.isNotEmpty && builders.containsKey(_blocks.last.tag)) {
child = builders[_blocks.last.tag!]! child = builders[_blocks.last.tag!]!.visitText(text, styleSheet.styles[_blocks.last.tag!]);
.visitText(text, styleSheet.styles[_blocks.last.tag!]);
} else if (_blocks.last.tag == 'pre') { } else if (_blocks.last.tag == 'pre') {
child = Scrollbar( child = Scrollbar(
child: SingleChildScrollView( child: SingleChildScrollView(
@ -299,9 +274,7 @@ class MarkdownBuilder implements md.NodeVisitor {
} else { } else {
child = _buildRichText( child = _buildRichText(
TextSpan( TextSpan(
style: _isInBlockquote style: _isInBlockquote ? styleSheet.blockquote!.merge(_inlines.last.style) : _inlines.last.style,
? styleSheet.blockquote!.merge(_inlines.last.style)
: _inlines.last.style,
text: _isInBlockquote ? text.text : trimText(text.text), text: _isInBlockquote ? text.text : trimText(text.text),
recognizer: _linkHandlers.isNotEmpty ? _linkHandlers.last : null, recognizer: _linkHandlers.isNotEmpty ? _linkHandlers.last : null,
), ),
@ -325,9 +298,7 @@ class MarkdownBuilder implements md.NodeVisitor {
if (current.children.isNotEmpty) { if (current.children.isNotEmpty) {
child = Column( child = Column(
crossAxisAlignment: fitContent crossAxisAlignment: fitContent ? CrossAxisAlignment.start : CrossAxisAlignment.stretch,
? CrossAxisAlignment.start
: CrossAxisAlignment.stretch,
children: current.children, children: current.children,
); );
} else { } else {
@ -351,19 +322,11 @@ class MarkdownBuilder implements md.NodeVisitor {
bullet = _buildBullet(_listIndents.last); bullet = _buildBullet(_listIndents.last);
} }
child = Row( child = Row(
textBaseline: listItemCrossAxisAlignment == textBaseline: listItemCrossAxisAlignment == MarkdownListItemCrossAxisAlignment.start ? null : TextBaseline.alphabetic,
MarkdownListItemCrossAxisAlignment.start crossAxisAlignment: listItemCrossAxisAlignment == MarkdownListItemCrossAxisAlignment.start ? CrossAxisAlignment.start : CrossAxisAlignment.baseline,
? null
: TextBaseline.alphabetic,
crossAxisAlignment: listItemCrossAxisAlignment ==
MarkdownListItemCrossAxisAlignment.start
? CrossAxisAlignment.start
: CrossAxisAlignment.baseline,
children: <Widget>[ children: <Widget>[
SizedBox( SizedBox(
width: styleSheet.listIndent! + width: styleSheet.listIndent! + styleSheet.listBulletPadding!.left + styleSheet.listBulletPadding!.right,
styleSheet.listBulletPadding!.left +
styleSheet.listBulletPadding!.right,
child: bullet, child: bullet,
), ),
Expanded(child: child) Expanded(child: child)
@ -401,8 +364,7 @@ class MarkdownBuilder implements md.NodeVisitor {
final _InlineElement parent = _inlines.last; final _InlineElement parent = _inlines.last;
if (builders.containsKey(tag)) { if (builders.containsKey(tag)) {
final Widget? child = final Widget? child = builders[tag]!.visitElementAfter(element, styleSheet.styles[tag]);
builders[tag]!.visitElementAfter(element, styleSheet.styles[tag]);
if (child != null) current.children[0] = child; if (child != null) current.children[0] = child;
} else if (tag == 'img') { } else if (tag == 'img') {
// create an image widget for this image // create an image widget for this image
@ -474,8 +436,7 @@ class MarkdownBuilder implements md.NodeVisitor {
} }
if (_linkHandlers.isNotEmpty) { if (_linkHandlers.isNotEmpty) {
TapGestureRecognizer recognizer = TapGestureRecognizer recognizer = _linkHandlers.last as TapGestureRecognizer;
_linkHandlers.last as TapGestureRecognizer;
return GestureDetector(child: child, onTap: recognizer.onTap); return GestureDetector(child: child, onTap: recognizer.onTap);
} else { } else {
return child; return child;
@ -497,7 +458,17 @@ class MarkdownBuilder implements md.NodeVisitor {
} }
Widget _buildBullet(String listTag) { Widget _buildBullet(String listTag) {
if (listTag == 'ul') { final int index = _blocks.last.nextListIndex;
final bool isUnordered = listTag == 'ul';
if (bulletBuilder != null) {
return Padding(
padding: styleSheet.listBulletPadding!,
child: bulletBuilder!(index, isUnordered ? BulletStyle.unorderedList : BulletStyle.orderedList),
);
}
if (isUnordered) {
return Padding( return Padding(
padding: styleSheet.listBulletPadding!, padding: styleSheet.listBulletPadding!,
child: Text( child: Text(
@ -508,7 +479,6 @@ class MarkdownBuilder implements md.NodeVisitor {
); );
} }
final int index = _blocks.last.nextListIndex;
return Padding( return Padding(
padding: styleSheet.listBulletPadding!, padding: styleSheet.listBulletPadding!,
child: Text( child: Text(
@ -583,28 +553,20 @@ class MarkdownBuilder implements md.NodeVisitor {
) { ) {
List<Widget> mergedTexts = <Widget>[]; List<Widget> mergedTexts = <Widget>[];
for (Widget child in children) { for (Widget child in children) {
if (mergedTexts.isNotEmpty && if (mergedTexts.isNotEmpty && mergedTexts.last is RichText && child is RichText) {
mergedTexts.last is RichText &&
child is RichText) {
RichText previous = mergedTexts.removeLast() as RichText; RichText previous = mergedTexts.removeLast() as RichText;
TextSpan previousTextSpan = previous.text as TextSpan; TextSpan previousTextSpan = previous.text as TextSpan;
List<TextSpan> children = previousTextSpan.children != null List<TextSpan> children = previousTextSpan.children != null ? List.from(previousTextSpan.children!) : [previousTextSpan];
? List.from(previousTextSpan.children!)
: [previousTextSpan];
children.add(child.text as TextSpan); children.add(child.text as TextSpan);
TextSpan? mergedSpan = _mergeSimilarTextSpans(children); TextSpan? mergedSpan = _mergeSimilarTextSpans(children);
mergedTexts.add(_buildRichText( mergedTexts.add(_buildRichText(
mergedSpan, mergedSpan,
textAlign: textAlign, textAlign: textAlign,
)); ));
} else if (mergedTexts.isNotEmpty && } else if (mergedTexts.isNotEmpty && mergedTexts.last is SelectableText && child is SelectableText) {
mergedTexts.last is SelectableText &&
child is SelectableText) {
SelectableText previous = mergedTexts.removeLast() as SelectableText; SelectableText previous = mergedTexts.removeLast() as SelectableText;
TextSpan previousTextSpan = previous.textSpan!; TextSpan previousTextSpan = previous.textSpan!;
List<TextSpan> children = previousTextSpan.children != null List<TextSpan> children = previousTextSpan.children != null ? List.from(previousTextSpan.children!) : [previousTextSpan];
? List.from(previousTextSpan.children!)
: [previousTextSpan];
if (child.textSpan != null) { if (child.textSpan != null) {
children.add(child.textSpan!); children.add(child.textSpan!);
} }
@ -667,10 +629,7 @@ class MarkdownBuilder implements md.NodeVisitor {
for (int index = 1; index < textSpans.length; index++) { for (int index = 1; index < textSpans.length; index++) {
TextSpan? nextChild = textSpans[index]; TextSpan? nextChild = textSpans[index];
if (nextChild is TextSpan && if (nextChild is TextSpan && nextChild.recognizer == mergedSpans.last.recognizer && nextChild.semanticsLabel == mergedSpans.last.semanticsLabel && nextChild.style == mergedSpans.last.style) {
nextChild.recognizer == mergedSpans.last.recognizer &&
nextChild.semanticsLabel == mergedSpans.last.semanticsLabel &&
nextChild.style == mergedSpans.last.style) {
TextSpan previous = mergedSpans.removeLast(); TextSpan previous = mergedSpans.removeLast();
mergedSpans.add(TextSpan( mergedSpans.add(TextSpan(
text: previous.toPlainText() + nextChild.toPlainText(), text: previous.toPlainText() + nextChild.toPlainText(),
@ -685,9 +644,7 @@ class MarkdownBuilder implements md.NodeVisitor {
// When the mergered spans compress into a single TextSpan return just that // When the mergered spans compress into a single TextSpan return just that
// TextSpan, otherwise bundle the set of TextSpans under a single parent. // TextSpan, otherwise bundle the set of TextSpans under a single parent.
return mergedSpans.length == 1 return mergedSpans.length == 1 ? mergedSpans.first : TextSpan(children: mergedSpans);
? mergedSpans.first
: TextSpan(children: mergedSpans);
} }
Widget _buildRichText(TextSpan? text, {TextAlign? textAlign}) { Widget _buildRichText(TextSpan? text, {TextAlign? textAlign}) {

View File

@ -32,6 +32,19 @@ typedef Widget MarkdownImageBuilder(Uri uri, String? title, String? alt);
/// Used by [MarkdownWidget.checkboxBuilder] /// Used by [MarkdownWidget.checkboxBuilder]
typedef Widget MarkdownCheckboxBuilder(bool value); typedef Widget MarkdownCheckboxBuilder(bool value);
/// Signature for custom bullet widget.
///
/// Used by [MarkdownWidget.bulletBuilder]
typedef Widget MarkdownBulletBuilder(int index, BulletStyle style);
/// Enumeration sent to the user when calling [MarkdownBulletBuilder]
///
/// Use this to differentiate the bullet styling when building your own.
enum BulletStyle {
orderedList,
unorderedList,
}
/// Creates a format [TextSpan] given a string. /// Creates a format [TextSpan] given a string.
/// ///
/// Used by [MarkdownWidget] to highlight the contents of `pre` elements. /// Used by [MarkdownWidget] to highlight the contents of `pre` elements.
@ -61,8 +74,7 @@ abstract class MarkdownElementBuilder {
/// to [preferredStyle]. /// to [preferredStyle].
/// ///
/// If you needn't build a widget, return null. /// If you needn't build a widget, return null.
Widget? visitElementAfter(md.Element element, TextStyle? preferredStyle) => Widget? visitElementAfter(md.Element element, TextStyle? preferredStyle) => null;
null;
} }
/// Enum to specify which theme being used when creating [MarkdownStyleSheet] /// Enum to specify which theme being used when creating [MarkdownStyleSheet]
@ -136,10 +148,10 @@ abstract class MarkdownWidget extends StatefulWidget {
this.extensionSet, this.extensionSet,
this.imageBuilder, this.imageBuilder,
this.checkboxBuilder, this.checkboxBuilder,
this.bulletBuilder,
this.builders = const {}, this.builders = const {},
this.fitContent = false, this.fitContent = false,
this.listItemCrossAxisAlignment = this.listItemCrossAxisAlignment = MarkdownListItemCrossAxisAlignment.baseline,
MarkdownListItemCrossAxisAlignment.baseline,
}) : super(key: key); }) : super(key: key);
/// The Markdown to display. /// The Markdown to display.
@ -191,6 +203,9 @@ abstract class MarkdownWidget extends StatefulWidget {
/// Call when build a checkbox widget. /// Call when build a checkbox widget.
final MarkdownCheckboxBuilder? checkboxBuilder; final MarkdownCheckboxBuilder? checkboxBuilder;
/// Called when building a bullet
final MarkdownBulletBuilder? bulletBuilder;
/// Render certain tags, usually used with [extensionSet] /// Render certain tags, usually used with [extensionSet]
/// ///
/// For example, we will add support for `sub` tag: /// For example, we will add support for `sub` tag:
@ -223,8 +238,7 @@ abstract class MarkdownWidget extends StatefulWidget {
_MarkdownWidgetState createState() => _MarkdownWidgetState(); _MarkdownWidgetState createState() => _MarkdownWidgetState();
} }
class _MarkdownWidgetState extends State<MarkdownWidget> class _MarkdownWidgetState extends State<MarkdownWidget> implements MarkdownBuilderDelegate {
implements MarkdownBuilderDelegate {
List<Widget>? _children; List<Widget>? _children;
final List<GestureRecognizer> _recognizers = <GestureRecognizer>[]; final List<GestureRecognizer> _recognizers = <GestureRecognizer>[];
@ -237,8 +251,7 @@ class _MarkdownWidgetState extends State<MarkdownWidget>
@override @override
void didUpdateWidget(MarkdownWidget oldWidget) { void didUpdateWidget(MarkdownWidget oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (widget.data != oldWidget.data || if (widget.data != oldWidget.data || widget.styleSheet != oldWidget.styleSheet) {
widget.styleSheet != oldWidget.styleSheet) {
_parseMarkdown(); _parseMarkdown();
} }
} }
@ -250,10 +263,8 @@ class _MarkdownWidgetState extends State<MarkdownWidget>
} }
void _parseMarkdown() { void _parseMarkdown() {
final MarkdownStyleSheet fallbackStyleSheet = final MarkdownStyleSheet fallbackStyleSheet = kFallbackStyle(context, widget.styleSheetTheme);
kFallbackStyle(context, widget.styleSheetTheme); final MarkdownStyleSheet styleSheet = fallbackStyleSheet.merge(widget.styleSheet);
final MarkdownStyleSheet styleSheet =
fallbackStyleSheet.merge(widget.styleSheet);
_disposeRecognizers(); _disposeRecognizers();
@ -277,6 +288,7 @@ class _MarkdownWidgetState extends State<MarkdownWidget>
imageDirectory: widget.imageDirectory, imageDirectory: widget.imageDirectory,
imageBuilder: widget.imageBuilder, imageBuilder: widget.imageBuilder,
checkboxBuilder: widget.checkboxBuilder, checkboxBuilder: widget.checkboxBuilder,
bulletBuilder: widget.bulletBuilder,
builders: widget.builders, builders: widget.builders,
fitContent: widget.fitContent, fitContent: widget.fitContent,
listItemCrossAxisAlignment: widget.listItemCrossAxisAlignment, listItemCrossAxisAlignment: widget.listItemCrossAxisAlignment,
@ -288,8 +300,7 @@ class _MarkdownWidgetState extends State<MarkdownWidget>
void _disposeRecognizers() { void _disposeRecognizers() {
if (_recognizers.isEmpty) return; if (_recognizers.isEmpty) return;
final List<GestureRecognizer> localRecognizers = final List<GestureRecognizer> localRecognizers = List<GestureRecognizer>.from(_recognizers);
List<GestureRecognizer>.from(_recognizers);
_recognizers.clear(); _recognizers.clear();
for (GestureRecognizer recognizer in localRecognizers) recognizer.dispose(); for (GestureRecognizer recognizer in localRecognizers) recognizer.dispose();
} }
@ -345,9 +356,9 @@ class MarkdownBody extends MarkdownWidget {
md.ExtensionSet? extensionSet, md.ExtensionSet? extensionSet,
MarkdownImageBuilder? imageBuilder, MarkdownImageBuilder? imageBuilder,
MarkdownCheckboxBuilder? checkboxBuilder, MarkdownCheckboxBuilder? checkboxBuilder,
MarkdownBulletBuilder? bulletBuilder,
Map<String, MarkdownElementBuilder> builders = const {}, Map<String, MarkdownElementBuilder> builders = const {},
MarkdownListItemCrossAxisAlignment listItemCrossAxisAlignment = MarkdownListItemCrossAxisAlignment listItemCrossAxisAlignment = MarkdownListItemCrossAxisAlignment.baseline,
MarkdownListItemCrossAxisAlignment.baseline,
this.shrinkWrap = true, this.shrinkWrap = true,
this.fitContent = true, this.fitContent = true,
}) : super( }) : super(
@ -367,6 +378,7 @@ class MarkdownBody extends MarkdownWidget {
checkboxBuilder: checkboxBuilder, checkboxBuilder: checkboxBuilder,
builders: builders, builders: builders,
listItemCrossAxisAlignment: listItemCrossAxisAlignment, listItemCrossAxisAlignment: listItemCrossAxisAlignment,
bulletBuilder: bulletBuilder,
); );
/// See [ScrollView.shrinkWrap] /// See [ScrollView.shrinkWrap]
@ -380,8 +392,7 @@ class MarkdownBody extends MarkdownWidget {
if (children!.length == 1) return children.single; if (children!.length == 1) return children.single;
return Column( return Column(
mainAxisSize: shrinkWrap ? MainAxisSize.min : MainAxisSize.max, mainAxisSize: shrinkWrap ? MainAxisSize.min : MainAxisSize.max,
crossAxisAlignment: crossAxisAlignment: fitContent ? CrossAxisAlignment.start : CrossAxisAlignment.stretch,
fitContent ? CrossAxisAlignment.start : CrossAxisAlignment.stretch,
children: children, children: children,
); );
} }
@ -413,9 +424,9 @@ class Markdown extends MarkdownWidget {
md.ExtensionSet? extensionSet, md.ExtensionSet? extensionSet,
MarkdownImageBuilder? imageBuilder, MarkdownImageBuilder? imageBuilder,
MarkdownCheckboxBuilder? checkboxBuilder, MarkdownCheckboxBuilder? checkboxBuilder,
MarkdownBulletBuilder? bulletBuilder,
Map<String, MarkdownElementBuilder> builders = const {}, Map<String, MarkdownElementBuilder> builders = const {},
MarkdownListItemCrossAxisAlignment listItemCrossAxisAlignment = MarkdownListItemCrossAxisAlignment listItemCrossAxisAlignment = MarkdownListItemCrossAxisAlignment.baseline,
MarkdownListItemCrossAxisAlignment.baseline,
this.padding = const EdgeInsets.all(16.0), this.padding = const EdgeInsets.all(16.0),
this.controller, this.controller,
this.physics, this.physics,
@ -437,6 +448,7 @@ class Markdown extends MarkdownWidget {
checkboxBuilder: checkboxBuilder, checkboxBuilder: checkboxBuilder,
builders: builders, builders: builders,
listItemCrossAxisAlignment: listItemCrossAxisAlignment, listItemCrossAxisAlignment: listItemCrossAxisAlignment,
bulletBuilder: bulletBuilder,
); );
/// The amount of space by which to inset the children. /// The amount of space by which to inset the children.

View File

@ -69,18 +69,7 @@ void defineTests() {
); );
final Iterable<Widget> widgets = tester.allWidgets; final Iterable<Widget> widgets = tester.allWidgets;
expectTextStrings(widgets, <String>[ expectTextStrings(widgets, <String>['1.', 'Item 1', '2.', 'Item 2', '3.', 'Item 3', '10.', 'Item 10', '11.', 'Item 11']);
'1.',
'Item 1',
'2.',
'Item 2',
'3.',
'Item 3',
'10.',
'Item 10',
'11.',
'Item 11'
]);
}, },
); );
}); });
@ -107,12 +96,35 @@ void defineTests() {
}, },
); );
testWidgets('custom bullet builder', (WidgetTester tester) async {
final String data = '* Item 1\n* Item 2\n1) Item 3\n2) Item 4';
final MarkdownBulletBuilder builder = (int index, BulletStyle style) => Text('$index ${style == BulletStyle.orderedList ? 'ordered' : 'unordered'}');
await tester.pumpWidget(
boilerplate(
Markdown(data: data, bulletBuilder: builder),
),
);
final Iterable<Widget> widgets = tester.allWidgets;
expectTextStrings(widgets, <String>[
'0 unordered',
'Item 1',
'1 unordered',
'Item 2',
'0 ordered',
'Item 3',
'1 ordered',
'Item 4',
]);
});
testWidgets( testWidgets(
'custom checkbox builder', 'custom checkbox builder',
(WidgetTester tester) async { (WidgetTester tester) async {
const String data = '- [x] Item 1\n- [ ] Item 2'; const String data = '- [x] Item 1\n- [ ] Item 2';
final MarkdownCheckboxBuilder builder = final MarkdownCheckboxBuilder builder = (bool checked) => Text('$checked');
(bool checked) => Text('$checked');
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( boilerplate(