Skip to content

[flutter_markdown] Ensure customize nested bullet list style. #6384

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 19, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// TODO(goderbauer): Restructure the examples to avoid this ignore, https://github.com/flutter/flutter/issues/110208.
// ignore_for_file: avoid_implementing_value_types

import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import '../shared/markdown_demo_widget.dart';

// ignore_for_file: public_member_api_docs

const String _markdownData = '''
# Custom Ordered List Demo

## Unordered List

- first
- second
- first
- first
- second
- first
- second

## Ordered List

1. first
2. second
1. first
1. first
2. second
1. first
1. second
''';

const String _notes = '''
# Custom Bullet List Demo
---

## Overview

This is the custom bullet list demo. This demo shows how to customize the bullet list style.
This demo example is being preserved for reference purposes.
''';

class CustomBulletListDemo extends StatelessWidget
implements MarkdownDemoWidget {
const CustomBulletListDemo({super.key});

static const String _title = 'Custom Bullet List Demo';

@override
String get title => CustomBulletListDemo._title;

@override
String get description => 'Shows how to customize the bullet list style.';

@override
Future<String> get data => Future<String>.value(_markdownData);

@override
Future<String> get notes => Future<String>.value(_notes);

@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Markdown(
data: _markdownData,
bulletBuilder: (int index, BulletStyle style, int nestLevel) =>
FittedBox(
fit: BoxFit.scaleDown,
child: switch (style) {
BulletStyle.unorderedList => const Text('・'),
BulletStyle.orderedList => Text('$nestLevel-${index + 1}.'),
},
),
),
),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import 'package:flutter/material.dart';
import '../demos/basic_markdown_demo.dart';
import '../demos/centered_header_demo.dart';
import '../demos/custom_bullet_list_demo.dart';
import '../demos/extended_emoji_demo.dart';
import '../demos/markdown_body_shrink_wrap_demo.dart';
import '../demos/minimal_markdown_demo.dart';
Expand All @@ -30,6 +31,7 @@ class HomeScreen extends StatelessWidget {
OriginalMarkdownDemo(),
const CenteredHeaderDemo(),
const MarkdownBodyShrinkWrapDemo(),
const CustomBulletListDemo(),
];

@override
Expand Down
7 changes: 5 additions & 2 deletions packages/flutter_markdown/lib/src/builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -611,8 +611,11 @@ class MarkdownBuilder implements md.NodeVisitor {
if (bulletBuilder != null) {
return Padding(
padding: styleSheet.listBulletPadding!,
child: bulletBuilder!(index,
isUnordered ? BulletStyle.unorderedList : BulletStyle.orderedList),
child: bulletBuilder!(
index,
isUnordered ? BulletStyle.unorderedList : BulletStyle.orderedList,
_listIndents.length - 1,
),
);
}

Expand Down
6 changes: 5 additions & 1 deletion packages/flutter_markdown/lib/src/widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ typedef MarkdownCheckboxBuilder = Widget Function(bool value);
/// Signature for custom bullet widget.
///
/// Used by [MarkdownWidget.bulletBuilder]
typedef MarkdownBulletBuilder = Widget Function(int index, BulletStyle style);
typedef MarkdownBulletBuilder = Widget Function(
int index,
BulletStyle style,
int nestLevel,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't done a full review here, but I assume the breaking change is that we're adding a parameter to a method that package clients need to implement?

If so, it would be better to make the breaking change replace it with a parameter object, so that next time it doesn't need a breaking change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree.
Which is better to use class or record, as a parameter object here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a field to a record later would be breaking (since it changes the type), so it needs to be a class to get the benefit.

);

/// Enumeration sent to the user when calling [MarkdownBulletBuilder]
///
Expand Down
60 changes: 59 additions & 1 deletion packages/flutter_markdown/test/list_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,35 @@ void defineTests() {
]);
},
);

testWidgets('custom bullet builder', (WidgetTester tester) async {
const String data =
'* Item 1\n * Item 2\n * Item 3\n * Item 4\n* Item 5';
Widget builder(int index, BulletStyle style, int nestLevel) => Text(
'$index ${style == BulletStyle.orderedList ? 'ordered' : 'unordered'} $nestLevel',
);

await tester.pumpWidget(
boilerplate(
Markdown(data: data, bulletBuilder: builder),
),
);

final Iterable<Widget> widgets = tester.allWidgets;

expectTextStrings(widgets, <String>[
'0 unordered 0',
'Item 1',
'0 unordered 1',
'Item 2',
'0 unordered 2',
'Item 3',
'1 unordered 1',
'Item 4',
'1 unordered 0',
'Item 5',
]);
});
});

group('Ordered List', () {
Expand Down Expand Up @@ -135,6 +164,35 @@ void defineTests() {
final Iterable<Widget> widgets = tester.allWidgets;
expectTextStrings(widgets, <String>['1.', 'one', 'two']);
});

testWidgets('custom bullet builder', (WidgetTester tester) async {
const String data =
'1. Item 1\n 1. Item 2\n 1. Item 3\n 1. Item 4\n1. Item 5';
Widget builder(int index, BulletStyle style, int nestLevel) => Text(
'$index ${style == BulletStyle.orderedList ? 'ordered' : 'unordered'} $nestLevel',
);

await tester.pumpWidget(
boilerplate(
Markdown(data: data, bulletBuilder: builder),
),
);

final Iterable<Widget> widgets = tester.allWidgets;

expectTextStrings(widgets, <String>[
'0 ordered 0',
'Item 1',
'0 ordered 1',
'Item 2',
'0 ordered 2',
'Item 3',
'1 ordered 1',
'Item 4',
'1 ordered 0',
'Item 5',
]);
});
});

group('Task List', () {
Expand All @@ -161,7 +219,7 @@ void defineTests() {

testWidgets('custom bullet builder', (WidgetTester tester) async {
const String data = '* Item 1\n* Item 2\n1) Item 3\n2) Item 4';
Widget builder(int index, BulletStyle style) => Text(
Widget builder(int index, BulletStyle style, _) => Text(
'$index ${style == BulletStyle.orderedList ? 'ordered' : 'unordered'}');

await tester.pumpWidget(
Expand Down