Skip to content

Commit 6862056

Browse files
Support markdown in travel body text fields (#272)
1 parent e3a4012 commit 6862056

File tree

9 files changed

+208
-74
lines changed

9 files changed

+208
-74
lines changed

examples/travel_app/lib/src/catalog/information_card.dart

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
import 'package:dart_schema_builder/dart_schema_builder.dart';
66
import 'package:flutter/material.dart';
77
import 'package:flutter_genui/flutter_genui.dart';
8+
import 'package:flutter_markdown/flutter_markdown.dart';
9+
10+
import '../utils.dart';
811

912
final _schema = S.object(
1013
properties: {
@@ -16,7 +19,9 @@ final _schema = S.object(
1619
),
1720
'title': S.string(description: 'The title of the card.'),
1821
'subtitle': S.string(description: 'The subtitle of the card.'),
19-
'body': S.string(description: 'The body text of the card.'),
22+
'body': S.string(
23+
description: 'The body text of the card. This supports markdown.',
24+
),
2025
},
2126
required: ['title', 'body'],
2227
);
@@ -86,9 +91,9 @@ final informationCard = CatalogItem(
8691
style: Theme.of(context).textTheme.titleMedium,
8792
),
8893
const SizedBox(height: 8.0),
89-
Text(
90-
cardData.body,
91-
style: Theme.of(context).textTheme.bodyMedium,
94+
MarkdownBody(
95+
data: cardData.body,
96+
styleSheet: getMarkdownStyleSheet(context),
9297
),
9398
],
9499
),

examples/travel_app/lib/src/catalog/itinerary_day.dart

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ library;
88
import 'package:dart_schema_builder/dart_schema_builder.dart';
99
import 'package:flutter/material.dart';
1010
import 'package:flutter_genui/flutter_genui.dart';
11+
import 'package:flutter_markdown/flutter_markdown.dart';
12+
13+
import '../utils.dart';
1114

1215
final _schema = S.object(
1316
description:
@@ -20,7 +23,8 @@ final _schema = S.object(
2023
description: 'The subtitle for the day, e.g., "Arrival in Tokyo".',
2124
),
2225
'description': S.string(
23-
description: 'A short description of the day\'s plan.',
26+
description:
27+
'A short description of the day\'s plan. This supports markdown.',
2428
),
2529
'imageChildId': S.string(
2630
description:
@@ -133,7 +137,10 @@ class _ItineraryDay extends StatelessWidget {
133137
],
134138
),
135139
const SizedBox(height: 8.0),
136-
Text(description, style: theme.textTheme.bodyMedium),
140+
MarkdownBody(
141+
data: description,
142+
styleSheet: getMarkdownStyleSheet(context),
143+
),
137144
const SizedBox(height: 8.0),
138145
const Divider(),
139146
...children,

examples/travel_app/lib/src/catalog/itinerary_entry.dart

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import 'package:dart_schema_builder/dart_schema_builder.dart';
66
import 'package:flutter/material.dart';
77
import 'package:flutter_genui/flutter_genui.dart';
8+
import 'package:flutter_markdown/flutter_markdown.dart';
89

10+
import '../utils.dart';
911
import '../widgets/dismiss_notification.dart';
1012

1113
enum ItineraryEntryType { accommodation, transport, activity }
@@ -19,7 +21,9 @@ final _schema = S.object(
1921
properties: {
2022
'title': S.string(description: 'The title of the itinerary entry.'),
2123
'subtitle': S.string(description: 'The subtitle of the itinerary entry.'),
22-
'bodyText': S.string(description: 'The body text for the entry.'),
24+
'bodyText': S.string(
25+
description: 'The body text for the entry. This supports markdown.',
26+
),
2327
'address': S.string(description: 'The address for the entry.'),
2428
'time': S.string(description: 'The time for the entry (formatted string).'),
2529
'totalCost': S.string(description: 'The total cost for the entry.'),
@@ -217,7 +221,10 @@ class _ItineraryEntry extends StatelessWidget {
217221
),
218222
],
219223
const SizedBox(height: 8.0),
220-
Text(bodyText, style: theme.textTheme.bodyMedium),
224+
MarkdownBody(
225+
data: bodyText,
226+
styleSheet: getMarkdownStyleSheet(context),
227+
),
221228
],
222229
),
223230
),

examples/travel_app/lib/src/catalog/padded_body_text.dart

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
import 'package:dart_schema_builder/dart_schema_builder.dart';
66
import 'package:flutter/material.dart';
77
import 'package:flutter_genui/flutter_genui.dart';
8+
import 'package:flutter_markdown/flutter_markdown.dart';
9+
10+
import '../utils.dart';
811

912
extension type _PaddedBodyTextData.fromMap(Map<String, Object?> _json) {
1013
factory _PaddedBodyTextData({required String text}) =>
@@ -18,7 +21,7 @@ final paddedBodyText = CatalogItem(
1821
dataSchema: S.object(
1922
properties: {
2023
'text': S.string(
21-
description: 'The text to display. This does *not* support markdown.',
24+
description: 'The text to display. This supports markdown.',
2225
),
2326
},
2427
required: ['text'],
@@ -37,9 +40,9 @@ final paddedBodyText = CatalogItem(
3740
);
3841
return Padding(
3942
padding: const EdgeInsets.symmetric(horizontal: 16.0),
40-
child: Text(
41-
textData.text,
42-
style: Theme.of(context).textTheme.bodyMedium,
43+
child: MarkdownBody(
44+
data: textData.text,
45+
styleSheet: getMarkdownStyleSheet(context),
4346
),
4447
);
4548
},
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright 2025 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/material.dart';
6+
import 'package:flutter_markdown/flutter_markdown.dart';
7+
8+
MarkdownStyleSheet getMarkdownStyleSheet(BuildContext context) {
9+
final theme = Theme.of(context);
10+
return MarkdownStyleSheet.fromTheme(
11+
theme,
12+
).copyWith(p: theme.textTheme.bodyMedium);
13+
}

examples/travel_app/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ dependencies:
2424
sdk: flutter
2525
flutter_genui:
2626
path: ../../packages/flutter_genui
27+
flutter_markdown: ^0.7.1
2728
json_rpc_2: ^4.0.0
2829
logging: ^1.3.0
2930
platform: ^3.1.6

examples/travel_app/test/itinerary_day_test.dart

Lines changed: 83 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,95 @@
33
// found in the LICENSE file.
44

55
import 'package:flutter/material.dart';
6+
import 'package:flutter_markdown/flutter_markdown.dart';
67
import 'package:flutter_test/flutter_test.dart';
8+
import 'package:network_image_mock/network_image_mock.dart';
79
import 'package:travel_app/src/catalog/itinerary_day.dart';
810

911
void main() {
10-
testWidgets('ItineraryDay golden test', (WidgetTester tester) async {
11-
await tester.pumpWidget(
12-
MaterialApp(
13-
home: Scaffold(
14-
body: Builder(
15-
builder: (context) {
16-
return Center(
17-
child: itineraryDay.widgetBuilder(
18-
data: {
19-
'title': 'Day 1',
20-
'subtitle': 'Arrival in Tokyo',
21-
'description': 'A day of exploring the city.',
22-
'imageChildId': 'tokyo_image',
23-
'children': <String>[],
24-
},
25-
id: 'test',
26-
buildChild: (_) => const Placeholder(),
27-
dispatchEvent: (_) {},
28-
context: context,
29-
values: {},
30-
),
31-
);
32-
},
12+
group('ItineraryDay', () {
13+
testWidgets('renders correctly', (WidgetTester tester) async {
14+
await tester.pumpWidget(
15+
MaterialApp(
16+
home: Scaffold(
17+
body: Builder(
18+
builder: (context) {
19+
return Center(
20+
child: itineraryDay.widgetBuilder(
21+
data: {
22+
'title': 'Day 1',
23+
'subtitle': 'Arrival in Tokyo',
24+
'description': 'A day of exploring the city.',
25+
'imageChildId': 'tokyo_image',
26+
'children': <String>[],
27+
},
28+
id: 'test',
29+
buildChild: (_) => const Placeholder(),
30+
dispatchEvent: (_) {},
31+
context: context,
32+
values: {},
33+
),
34+
);
35+
},
36+
),
3337
),
3438
),
35-
),
36-
);
39+
);
3740

38-
expect(find.text('Day 1'), findsOneWidget);
39-
expect(find.text('Arrival in Tokyo'), findsOneWidget);
40-
expect(find.text('A day of exploring the city.'), findsOneWidget);
41+
expect(find.text('Day 1'), findsOneWidget);
42+
expect(find.text('Arrival in Tokyo'), findsOneWidget);
43+
expect(find.text('A day of exploring the city.'), findsOneWidget);
44+
});
45+
46+
testWidgets('renders correctly with markdown', (WidgetTester tester) async {
47+
await mockNetworkImagesFor(() async {
48+
const testTitle = 'Test Title';
49+
const testSubtitle = 'Test Subtitle';
50+
const testDescription = 'Test **Description**';
51+
52+
await tester.pumpWidget(
53+
MaterialApp(
54+
home: Scaffold(
55+
body: Builder(
56+
builder: (context) {
57+
return itineraryDay.widgetBuilder(
58+
data: {
59+
'title': testTitle,
60+
'subtitle': testSubtitle,
61+
'description': testDescription,
62+
'imageChildId': 'image_child_id',
63+
'children': <String>[],
64+
},
65+
id: 'test_id',
66+
buildChild: (id) {
67+
if (id == 'image_child_id') {
68+
return Image.network(
69+
'https://example.com/thumbnail.jpg',
70+
);
71+
}
72+
return const SizedBox.shrink();
73+
},
74+
dispatchEvent: (event) {},
75+
context: context,
76+
values: {},
77+
);
78+
},
79+
),
80+
),
81+
),
82+
);
83+
84+
expect(find.text(testTitle), findsOneWidget);
85+
expect(find.text(testSubtitle), findsOneWidget);
86+
expect(
87+
find.descendant(
88+
of: find.byType(MarkdownBody),
89+
matching: find.byType(RichText),
90+
),
91+
findsOneWidget,
92+
);
93+
expect(find.byType(Image), findsOneWidget);
94+
});
95+
});
4196
});
4297
}

0 commit comments

Comments
 (0)