From 4ead1ed87475307980eed81844c473eb366684c3 Mon Sep 17 00:00:00 2001 From: Victor Choueiri Date: Thu, 2 Nov 2017 18:47:47 +0200 Subject: [PATCH] Add GestureRecognizer to children of 'a' Tag (#9) * Add GestureRecognizer to children of 'a' Tag * Add support links with nested items * Add test for nested elements in links * Add onTap tests --- .../flutter_markdown/lib/src/builder.dart | 26 ++++-- .../test/flutter_markdown_test.dart | 87 ++++++++++++++++--- 2 files changed, 95 insertions(+), 18 deletions(-) diff --git a/packages/flutter_markdown/lib/src/builder.dart b/packages/flutter_markdown/lib/src/builder.dart index 7b91ba0bf405..c998e0853983 100644 --- a/packages/flutter_markdown/lib/src/builder.dart +++ b/packages/flutter_markdown/lib/src/builder.dart @@ -73,6 +73,8 @@ class MarkdownBuilder implements md.NodeVisitor { final List _listIndents = []; final List<_BlockElement> _blocks = <_BlockElement>[]; final List<_InlineElement> _inlines = <_InlineElement>[]; + final List _linkHandlers = []; + /// Returns widgets that display the given Markdown nodes. /// @@ -81,6 +83,7 @@ class MarkdownBuilder implements md.NodeVisitor { _listIndents.clear(); _blocks.clear(); _inlines.clear(); + _linkHandlers.clear(); _blocks.add(new _BlockElement(null)); _inlines.add(new _InlineElement()); @@ -98,8 +101,12 @@ class MarkdownBuilder implements md.NodeVisitor { void visitText(md.Text text) { if (_blocks.last.tag == null) // Don't allow text directly under the root. return; - final TextSpan span = _blocks.last.tag == 'pre' ? - delegate.formatText(styleSheet, text.text) : new TextSpan(text: text.text); + final TextSpan span = _blocks.last.tag == 'pre' + ? delegate.formatText(styleSheet, text.text) + : new TextSpan( + text: text.text, + recognizer: _linkHandlers.isNotEmpty ? _linkHandlers.last : null, + ); _inlines.last.children.add(span); } @@ -114,6 +121,11 @@ class MarkdownBuilder implements md.NodeVisitor { } else { _inlines.add(new _InlineElement()); } + + if (tag == 'a') { + _linkHandlers.add(delegate.createLink(element.attributes['href'])); + } + return true; } @@ -179,16 +191,14 @@ class MarkdownBuilder implements md.NodeVisitor { final _InlineElement parent = _inlines.last; if (current.children.isNotEmpty) { - GestureRecognizer recognizer; - - if (tag == 'a') - recognizer = delegate.createLink(element.attributes['href']); - parent.children.add(new TextSpan( style: styleSheet.styles[tag], - recognizer: recognizer, children: current.children, )); + + if (tag == 'a') { + _linkHandlers.removeLast(); + } } } } diff --git a/packages/flutter_markdown/test/flutter_markdown_test.dart b/packages/flutter_markdown/test/flutter_markdown_test.dart index c64176be3a12..33fb3f444537 100644 --- a/packages/flutter_markdown/test/flutter_markdown_test.dart +++ b/packages/flutter_markdown/test/flutter_markdown_test.dart @@ -85,16 +85,83 @@ void main() { ]); }); - testWidgets('Links', (WidgetTester tester) async { - await tester - .pumpWidget(_boilerplate(const Markdown(data: '[Link Text](href)'))); - - final RichText textWidget = - tester.allWidgets.firstWhere((Widget widget) => widget is RichText); - final TextSpan span = textWidget.text; - - expect( - span.children[0].recognizer.runtimeType, equals(TapGestureRecognizer)); + group('Links', () { + testWidgets('Single link', (WidgetTester tester) async { + String tapResult; + await tester.pumpWidget(_boilerplate(new Markdown( + data: '[Link Text](href)', + onTapLink: (value) => tapResult = value, + ))); + + final RichText textWidget = + tester.allWidgets.firstWhere((Widget widget) => widget is RichText); + final TextSpan span = textWidget.text; + + (span.children[0].children[0].recognizer as TapGestureRecognizer).onTap(); + + expect(span.children.length, 1); + expect(span.children[0].children.length, 1); + expect(span.children[0].children[0].recognizer.runtimeType, + equals(TapGestureRecognizer)); + expect(tapResult, 'href'); + }); + + testWidgets('Link with nested code', (WidgetTester tester) async { + final List tapResults = []; + await tester.pumpWidget(_boilerplate(new Markdown( + data: '[Link `with nested code` Text](href)', + onTapLink: (value) => tapResults.add(value), + ))); + + final RichText textWidget = + tester.allWidgets.firstWhere((Widget widget) => widget is RichText); + final TextSpan span = textWidget.text; + + final List gestureRecognizerTypes = []; + span.visitTextSpan((TextSpan textSpan) { + TapGestureRecognizer recognizer = textSpan.recognizer; + gestureRecognizerTypes.add(recognizer.runtimeType); + recognizer.onTap(); + return true; + }); + + expect(span.children.length, 1); + expect(span.children[0].children.length, 3); + expect(gestureRecognizerTypes, everyElement(TapGestureRecognizer)); + expect(tapResults.length, 3); + expect(tapResults, everyElement('href')); + }); + + testWidgets('Multiple links', (WidgetTester tester) async { + final List tapResults = []; + + await tester.pumpWidget(_boilerplate(new Markdown( + data: '[First Link](firstHref) and [Second Link](secondHref)', + onTapLink: (value) => tapResults.add(value), + ))); + + final RichText textWidget = + tester.allWidgets.firstWhere((Widget widget) => widget is RichText); + final TextSpan span = textWidget.text; + + final List gestureRecognizerTypes = []; + span.visitTextSpan((TextSpan textSpan) { + TapGestureRecognizer recognizer = textSpan.recognizer; + gestureRecognizerTypes.add(recognizer.runtimeType); + recognizer?.onTap(); + return true; + }); + + + expect(span.children.length, 3); + expect(span.children[0].children.length, 1); + expect(span.children[1].children, null); + expect(span.children[2].children.length, 1); + + expect(gestureRecognizerTypes, + orderedEquals([TapGestureRecognizer, Null, TapGestureRecognizer])); + expect(tapResults, orderedEquals(['firstHref', 'secondHref'])); + }); }); testWidgets('HTML tag ignored ', (WidgetTester tester) async {