Skip to content

Commit

Permalink
[flutter_markdown] Footnote support (flutter#5058)
Browse files Browse the repository at this point in the history
  • Loading branch information
dawidope authored Oct 4, 2023
1 parent d654f75 commit dccde60
Show file tree
Hide file tree
Showing 7 changed files with 304 additions and 9 deletions.
4 changes: 4 additions & 0 deletions packages/flutter_markdown/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.6.18

* Adds support for `footnote`.

## 0.6.17+4

* Fixes an issue where a code block would overlap its container decoration.
Expand Down
10 changes: 5 additions & 5 deletions packages/flutter_markdown/example/windows/runner/Runner.rc
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,14 @@ IDI_APP_ICON ICON "resources\\app_icon.ico"
// Version
//

#ifdef FLUTTER_BUILD_NUMBER
#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER
#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
#else
#define VERSION_AS_NUMBER 1,0,0
#define VERSION_AS_NUMBER 1,0,0,0
#endif

#ifdef FLUTTER_BUILD_NAME
#define VERSION_AS_STRING #FLUTTER_BUILD_NAME
#if defined(FLUTTER_VERSION)
#define VERSION_AS_STRING FLUTTER_VERSION
#else
#define VERSION_AS_STRING "1.0.0"
#endif
Expand Down
28 changes: 27 additions & 1 deletion packages/flutter_markdown/lib/src/builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:ui';

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:markdown/markdown.dart' as md;
Expand All @@ -27,7 +29,8 @@ const List<String> _kBlockTags = <String>[
'table',
'thead',
'tbody',
'tr'
'tr',
'section',
];

const List<String> _kListTags = <String>['ul', 'ol'];
Expand Down Expand Up @@ -512,6 +515,29 @@ class MarkdownBuilder implements md.NodeVisitor {
_ambiguate(_tables.single.rows.last.children)!.add(child);
} else if (tag == 'a') {
_linkHandlers.removeLast();
} else if (tag == 'sup') {
final Widget c = current.children.last;
TextSpan? textSpan;
if (c is RichText && c.text is TextSpan) {
textSpan = c.text as TextSpan;
} else if (c is SelectableText && c.textSpan is TextSpan) {
textSpan = c.textSpan;
}
if (textSpan != null) {
final Widget richText = _buildRichText(
TextSpan(
recognizer: textSpan.recognizer,
text: element.textContent,
style: textSpan.style?.copyWith(
fontFeatures: <FontFeature>[
const FontFeature.enable('sups'),
],
),
),
);
current.children.removeLast();
current.children.add(richText);
}
}

if (current.children.isNotEmpty) {
Expand Down
4 changes: 2 additions & 2 deletions 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:/flutter/packages/tree/main/packages/flutter_markdown
issue_tracker: https:/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+flutter_markdown%22
version: 0.6.17+4
version: 0.6.18

environment:
sdk: ">=3.0.0 <4.0.0"
Expand All @@ -13,7 +13,7 @@ environment:
dependencies:
flutter:
sdk: flutter
markdown: ^7.0.0
markdown: ^7.1.1
meta: ^1.3.0
path: ^1.8.0

Expand Down
2 changes: 2 additions & 0 deletions packages/flutter_markdown/test/all.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ library flutter_markdown.all_test;
import 'blockquote_test.dart' as blockquote_test;
import 'custom_syntax_test.dart' as custome_syntax_test;
import 'emphasis_test.dart' as emphasis_test;
import 'footnote_test.dart' as footnote_test;
import 'header_test.dart' as header_test;
import 'horizontal_rule_test.dart' as horizontal_rule_test;
import 'html_test.dart' as html_test;
Expand All @@ -26,6 +27,7 @@ void main() {
blockquote_test.defineTests();
custome_syntax_test.defineTests();
emphasis_test.defineTests();
footnote_test.defineTests();
header_test.defineTests();
horizontal_rule_test.defineTests();
html_test.defineTests();
Expand Down
258 changes: 258 additions & 0 deletions packages/flutter_markdown/test/footnote_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
// 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.

import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_test/flutter_test.dart';

import 'utils.dart';

void main() => defineTests();

void defineTests() {
group(
'structure',
() {
testWidgets(
'footnote is detected and handle correctly',
(WidgetTester tester) async {
const String data = 'Foo[^a]\n[^a]: Bar';
await tester.pumpWidget(
boilerplate(
const MarkdownBody(
data: data,
),
),
);

final Iterable<Widget> widgets = tester.allWidgets;
expectTextStrings(widgets, <String>[
'Foo1',
'1.',
'Bar ↩',
]);
},
);

testWidgets(
'footnote is detected and handle correctly for selectable markdown',
(WidgetTester tester) async {
const String data = 'Foo[^a]\n[^a]: Bar';
await tester.pumpWidget(
boilerplate(
const MarkdownBody(
data: data,
selectable: true,
),
),
);

final Iterable<Widget> widgets = tester.allWidgets;
expectTextStrings(widgets, <String>[
'Foo1',
'1.',
'Bar ↩',
]);
},
);

testWidgets(
'ignore footnotes without description',
(WidgetTester tester) async {
const String data = 'Foo[^1] Bar[^2]\n[^1]: Bar';
await tester.pumpWidget(
boilerplate(
const MarkdownBody(
data: data,
),
),
);

final Iterable<Widget> widgets = tester.allWidgets;
expectTextStrings(widgets, <String>[
'Foo1 Bar[^2]',
'1.',
'Bar ↩',
]);
},
);
testWidgets(
'ignore superscripts and footnotes order',
(WidgetTester tester) async {
const String data = '[^2]: Bar \n [^1]: Foo \n Foo[^f] Bar[^b]';
await tester.pumpWidget(
boilerplate(
const MarkdownBody(
data: data,
),
),
);

final Iterable<Widget> widgets = tester.allWidgets;
expectTextStrings(widgets, <String>[
'Foo1 Bar2',
'1.',
'Foo ↩',
'2.',
'Bar ↩',
]);
},
);

testWidgets(
'handle two digits superscript',
(WidgetTester tester) async {
const String data = '''
1[^1] 2[^2] 3[^3] 4[^4] 5[^5] 6[^6] 7[^7] 8[^8] 9[^9] 10[^10]
[^1]:1
[^2]:2
[^3]:3
[^4]:4
[^5]:5
[^6]:6
[^7]:7
[^8]:8
[^9]:9
[^10]:10
''';
await tester.pumpWidget(
boilerplate(
const MarkdownBody(
data: data,
),
),
);

final Iterable<Widget> widgets = tester.allWidgets;
expectTextStrings(widgets, <String>[
'11 22 33 44 55 66 77 88 99 1010',
'1.',
'1 ↩',
'2.',
'2 ↩',
'3.',
'3 ↩',
'4.',
'4 ↩',
'5.',
'5 ↩',
'6.',
'6 ↩',
'7.',
'7 ↩',
'8.',
'8 ↩',
'9.',
'9 ↩',
'10.',
'10 ↩',
]);
},
);
},
);

group(
'superscript textstyle replacing',
() {
testWidgets(
'superscript has correct fontfeature',
(WidgetTester tester) async {
const String data = 'Foo[^a]\n[^a]: Bar';
await tester.pumpWidget(
boilerplate(
const MarkdownBody(
data: data,
),
),
);

final Iterable<Widget> widgets = tester.allWidgets;
final RichText richText = widgets
.firstWhere((Widget widget) => widget is RichText) as RichText;

final TextSpan span = richText.text as TextSpan;
final List<InlineSpan>? children = span.children;

expect(children, isNotNull);
expect(children!.length, 2);
expect(children[1].style, isNotNull);
expect(children[1].style!.fontFeatures?.length, 1);
expect(children[1].style!.fontFeatures?.first.feature, 'sups');
},
);

testWidgets(
'superscript index has the same font style like text',
(WidgetTester tester) async {
const String data = '# Foo[^a]\n[^a]: Bar';
await tester.pumpWidget(
boilerplate(
const MarkdownBody(
data: data,
),
),
);

final Iterable<Widget> widgets = tester.allWidgets;
final RichText richText = widgets
.firstWhere((Widget widget) => widget is RichText) as RichText;

final TextSpan span = richText.text as TextSpan;
final List<InlineSpan>? children = span.children;

expect(children![0].style, isNotNull);
expect(children[1].style!.fontSize, children[0].style!.fontSize);
expect(children[1].style!.fontFamily, children[0].style!.fontFamily);
expect(children[1].style!.fontStyle, children[0].style!.fontStyle);
expect(children[1].style!.fontSize, children[0].style!.fontSize);
},
);

testWidgets(
'link is correctly copied to new superscript index',
(WidgetTester tester) async {
final List<MarkdownLink> linkTapResults = <MarkdownLink>[];
const String data = 'Foo[^a]\n[^a]: Bar';
await tester.pumpWidget(
boilerplate(
MarkdownBody(
data: data,
onTapLink: (String text, String? href, String title) =>
linkTapResults.add(MarkdownLink(text, href, title)),
),
),
);

final Iterable<Widget> widgets = tester.allWidgets;
final RichText richText = widgets
.firstWhere((Widget widget) => widget is RichText) as RichText;

final TextSpan span = richText.text as TextSpan;

final List<Type> gestureRecognizerTypes = <Type>[];
span.visitChildren((InlineSpan inlineSpan) {
if (inlineSpan is TextSpan) {
final TapGestureRecognizer? recognizer =
inlineSpan.recognizer as TapGestureRecognizer?;
gestureRecognizerTypes.add(recognizer?.runtimeType ?? Null);
if (recognizer != null) {
recognizer.onTap!();
}
}
return true;
});

expect(span.children!.length, 2);
expect(
gestureRecognizerTypes,
orderedEquals(<Type>[Null, TapGestureRecognizer]),
);
expectLinkTap(linkTapResults[0], const MarkdownLink('1', '#fn-a'));
},
);
},
);
}
7 changes: 6 additions & 1 deletion packages/flutter_markdown/test/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,13 @@ void expectWidgetTypes(Iterable<Widget> widgets, List<Type> expected) {
void expectTextStrings(Iterable<Widget> widgets, List<String> strings) {
int currentString = 0;
for (final Widget widget in widgets) {
TextSpan? span;
if (widget is RichText) {
final TextSpan span = widget.text as TextSpan;
span = widget.text as TextSpan;
} else if (widget is SelectableText) {
span = widget.textSpan;
}
if (span != null) {
final String text = _extractTextFromTextSpan(span);
expect(text, equals(strings[currentString]));
currentString += 1;
Expand Down

0 comments on commit dccde60

Please sign in to comment.