From 842a619f4575db47b3c38c0fdab688d2b3176c0e Mon Sep 17 00:00:00 2001 From: Viktor Lidholt Date: Thu, 25 Feb 2016 14:47:05 -0800 Subject: [PATCH 001/121] Adds initial version of Markdown renderer --- README.md | 22 ++ example/demo.dart | 36 +++ lib/flutter_markdown.dart | 8 + lib/flutter_markdown_raw.dart | 8 + lib/src/markdown.dart | 84 +++++++ lib/src/markdown_raw.dart | 433 ++++++++++++++++++++++++++++++++ lib/src/markdown_style.dart | 77 ++++++ lib/src/markdown_style_raw.dart | 120 +++++++++ pubspec.yaml | 18 ++ test/flutter_markdown_test.dart | 134 ++++++++++ 10 files changed, 940 insertions(+) create mode 100644 README.md create mode 100644 example/demo.dart create mode 100644 lib/flutter_markdown.dart create mode 100644 lib/flutter_markdown_raw.dart create mode 100644 lib/src/markdown.dart create mode 100644 lib/src/markdown_raw.dart create mode 100644 lib/src/markdown_style.dart create mode 100644 lib/src/markdown_style_raw.dart create mode 100644 pubspec.yaml create mode 100644 test/flutter_markdown_test.dart diff --git a/README.md b/README.md new file mode 100644 index 00000000000..db83e6b73f3 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# Flutter Markdown + +A markdown renderer for Flutter. It supports the +[original format](https://daringfireball.net/projects/markdown/), but no inline +html. + +## Getting Started + +Using the Markdown widget is simple, just pass in the source markdown as a +string: + + new Markdown(data: markdownSource); + +If you do not want the padding or scrolling behavior, use the MarkdownBody +instead: + + new MarkdownBody(data: markdownSource); + +By default, Markdown uses the formatting from the current material design theme, +but it's possible to create your own custom styling. Use the MarkdownStyle class +to pass in your own style. If you don't want to use Markdown outside of material +design, use the MarkdownRaw class. diff --git a/example/demo.dart b/example/demo.dart new file mode 100644 index 00000000000..14bce27408b --- /dev/null +++ b/example/demo.dart @@ -0,0 +1,36 @@ +// Copyright 2016 The Chromium 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/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; + +const String _kMarkdownData = """# Markdown Example +Markdown allows you to easily include formatted text, images, and even formatted Dart code in your app. + +## Styling +Style text as _italic_, __bold__, or `inline code`. + +- Use bulleted lists +- To better clarify +- Your points + +## Code blocks +Formatted Dart code looks really pretty too. This is an example of how to create your own Markdown widget: + + new Markdown(data: "Hello _world_!"); + +Enjoy! +"""; + +void main() { + runApp(new MaterialApp( + title: "Markdown Demo", + routes: { + '/': (RouteArguments args) => new Scaffold( + toolBar: new ToolBar(center: new Text("Markdown Demo")), + body: new Markdown(data: _kMarkdownData) + ) + } + )); +} diff --git a/lib/flutter_markdown.dart b/lib/flutter_markdown.dart new file mode 100644 index 00000000000..736e84f2db1 --- /dev/null +++ b/lib/flutter_markdown.dart @@ -0,0 +1,8 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +library flutter_markdown; + +export 'src/markdown.dart'; +export 'src/markdown_style.dart'; diff --git a/lib/flutter_markdown_raw.dart b/lib/flutter_markdown_raw.dart new file mode 100644 index 00000000000..d7fed2c6661 --- /dev/null +++ b/lib/flutter_markdown_raw.dart @@ -0,0 +1,8 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +library flutter_markdown; + +export 'src/markdown_raw.dart'; +export 'src/markdown_style_raw.dart'; diff --git a/lib/src/markdown.dart b/lib/src/markdown.dart new file mode 100644 index 00000000000..3b437b12418 --- /dev/null +++ b/lib/src/markdown.dart @@ -0,0 +1,84 @@ +// Copyright 2016 The Chromium 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/material.dart'; + +import 'markdown_raw.dart'; +import 'markdown_style.dart'; + +/// A [Widget] that renders markdown formatted text. It supports all standard +/// markdowns from the original markdown specification found here: +/// https://daringfireball.net/projects/markdown/ The rendered markdown is +/// placed in a padded scrolling view port. If you do not want the scrolling +/// behaviour, use the [MarkdownBody] class instead. +class Markdown extends MarkdownRaw { + + /// Creates a new Markdown [Widget] that renders the markdown formatted string + /// passed in as [data]. By default the markdown will be rendered using the + /// styles from the current theme, but you can optionally pass in a custom + /// [markdownStyle] that specifies colors and fonts to use. Code blocks are + /// by default not using syntax highlighting, but it's possible to pass in + /// a custom [syntaxHighlighter]. + /// + /// new Markdown(data: "Hello _world_!"); + Markdown({ + String data, + SyntaxHighlighter syntaxHighlighter, + MarkdownStyle markdownStyle + }) : super( + data: data, + syntaxHighlighter: syntaxHighlighter, + markdownStyle: markdownStyle + ); + + MarkdownBody createMarkdownBody({ + String data, + MarkdownStyle markdownStyle, + SyntaxHighlighter syntaxHighlighter + }) { + return new MarkdownBody( + data: data, + markdownStyle: markdownStyle, + syntaxHighlighter: syntaxHighlighter + ); + } +} + +/// A [Widget] that renders markdown formatted text. It supports all standard +/// markdowns from the original markdown specification found here: +/// https://daringfireball.net/projects/markdown/ This class doesn't implement +/// any scrolling behavior, if you want scrolling either wrap the widget in +/// a [ScrollableViewport] or use the [Markdown] widget. +class MarkdownBody extends MarkdownBodyRaw { + + /// Creates a new Markdown [Widget] that renders the markdown formatted string + /// passed in as [data]. By default the markdown will be rendered using the + /// styles from the current theme, but you can optionally pass in a custom + /// [markdownStyle] that specifies colors and fonts to use. Code blocks are + /// by default not using syntax highlighting, but it's possible to pass in + /// a custom [syntaxHighlighter]. + /// + /// Typically, you may want to wrap the [MarkdownBody] widget in a [Padding] and + /// a [ScrollableViewport], or use the [Markdown] class + /// + /// new ScrollableViewport( + /// child: new Padding( + /// padding: new EdgeDims.all(16.0), + /// child: new Markdown(data: markdownSource) + /// ) + /// ) + MarkdownBody({ + String data, + SyntaxHighlighter syntaxHighlighter, + MarkdownStyle markdownStyle + }) : super( + data: data, + syntaxHighlighter: syntaxHighlighter, + markdownStyle: markdownStyle + ); + + MarkdownStyle createDefaultStyle(BuildContext context) { + return new MarkdownStyle.defaultFromTheme(Theme.of(context)); + } +} diff --git a/lib/src/markdown_raw.dart b/lib/src/markdown_raw.dart new file mode 100644 index 00000000000..043e3e17d32 --- /dev/null +++ b/lib/src/markdown_raw.dart @@ -0,0 +1,433 @@ +// Copyright 2016 The Chromium 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:markdown/markdown.dart' as md; +import 'package:flutter/widgets.dart'; +import 'markdown_style_raw.dart'; + + +/// A [Widget] that renders markdown formatted text. It supports all standard +/// markdowns from the original markdown specification found here: +/// https://daringfireball.net/projects/markdown/ The rendered markdown is +/// placed in a padded scrolling view port. If you do not want the scrolling +/// behaviour, use the [MarkdownBodyRaw] class instead. +class MarkdownRaw extends StatelessComponent { + + /// Creates a new Markdown [Widget] that renders the markdown formatted string + /// passed in as [data]. By default the markdown will be rendered using the + /// styles from the current theme, but you can optionally pass in a custom + /// [markdownStyle] that specifies colors and fonts to use. Code blocks are + /// by default not using syntax highlighting, but it's possible to pass in + /// a custom [syntaxHighlighter]. + /// + /// new MarkdownRaw(data: "Hello _world_!", markdownStyle: myStyle); + MarkdownRaw({ + this.data, + this.markdownStyle, + this.syntaxHighlighter, + this.padding: const EdgeDims.all(16.0) + }); + + /// Markdown styled text + final String data; + + /// Style used for rendering the markdown + final MarkdownStyleRaw markdownStyle; + + /// The syntax highlighter used to color text in code blocks + final SyntaxHighlighter syntaxHighlighter; + + /// Padding used + final EdgeDims padding; + + Widget build(BuildContext context) { + return new ScrollableViewport( + child: new Padding( + padding: padding, + child: createMarkdownBody( + data: data, + markdownStyle: markdownStyle, + syntaxHighlighter: syntaxHighlighter + ) + ) + ); + } + + MarkdownBodyRaw createMarkdownBody({ + String data, + MarkdownStyleRaw markdownStyle, + SyntaxHighlighter syntaxHighlighter + }) { + return new MarkdownBodyRaw( + data: data, + markdownStyle: markdownStyle, + syntaxHighlighter: syntaxHighlighter + ); + } +} + +/// A [Widget] that renders markdown formatted text. It supports all standard +/// markdowns from the original markdown specification found here: +/// https://daringfireball.net/projects/markdown/ This class doesn't implement +/// any scrolling behavior, if you want scrolling either wrap the widget in +/// a [ScrollableViewport] or use the [MarkdownRaw] widget. +class MarkdownBodyRaw extends StatefulComponent { + + /// Creates a new Markdown [Widget] that renders the markdown formatted string + /// passed in as [data]. You need to pass in a [markdownStyle] that defines + /// how the code is rendered. Code blocks are by default not using syntax + /// highlighting, but it's possible to pass in a custom [syntaxHighlighter]. + /// + /// Typically, you may want to wrap the [MarkdownBodyRaw] widget in a + /// [Padding] and a [ScrollableViewport], or use the [Markdown class] + /// + /// new ScrollableViewport( + /// child: new Padding( + /// padding: new EdgeDims.all(16.0), + /// child: new MarkdownBodyRaw( + /// data: markdownSource, + /// markdownStyle: myStyle + /// ) + /// ) + /// ) + MarkdownBodyRaw({ + this.data, + this.markdownStyle, + this.syntaxHighlighter + }); + + /// Markdown styled text + final String data; + + /// Style used for rendering the markdown + final MarkdownStyleRaw markdownStyle; + + /// The syntax highlighter used to color text in code blocks + final SyntaxHighlighter syntaxHighlighter; + + _MarkdownBodyRawState createState() => new _MarkdownBodyRawState(); + + MarkdownStyleRaw createDefaultStyle(BuildContext context) => null; +} + +class _MarkdownBodyRawState extends State { + + void initState() { + super.initState(); + + MarkdownStyleRaw markdownStyle = config.markdownStyle ?? config.createDefaultStyle(context); + SyntaxHighlighter syntaxHighlighter = config.syntaxHighlighter ?? new _DefaultSyntaxHighlighter(markdownStyle.code); + + _cachedBlocks = _blocksFromMarkup(config.data, markdownStyle, syntaxHighlighter); + } + + List<_Block> _cachedBlocks; + + Widget build(BuildContext context) { + List blocks = []; + for (_Block block in _cachedBlocks) { + blocks.add(block.build(context)); + } + + return new Column( + alignItems: FlexAlignItems.stretch, + children: blocks + ); + } +} + +List<_Block> _blocksFromMarkup(String data, MarkdownStyleRaw markdownStyle, SyntaxHighlighter syntaxHighlighter) { + // TODO: This can be optimized by doing the split and removing \r at the same time + List lines = data.replaceAll('\r\n', '\n').split('\n'); + md.Document document = new md.Document(); + + _Renderer renderer = new _Renderer(); + return renderer.render(document.parseLines(lines), markdownStyle, syntaxHighlighter); +} + +class _Renderer implements md.NodeVisitor { + List<_Block> render(List nodes, MarkdownStyleRaw markdownStyle, SyntaxHighlighter syntaxHighlighter) { + assert(markdownStyle != null); + + _blocks = <_Block>[]; + _listIndents = []; + _markdownStyle = markdownStyle; + _syntaxHighlighter = syntaxHighlighter; + + for (final md.Node node in nodes) { + node.accept(this); + } + + return _blocks; + } + + List<_Block> _blocks; + List _listIndents; + MarkdownStyleRaw _markdownStyle; + SyntaxHighlighter _syntaxHighlighter; + + void visitText(md.Text text) { + _MarkdownNodeList topList = _currentBlock.stack.last; + List<_MarkdownNode> top = topList.list; + + if (_currentBlock.tag == 'pre') + top.add(new _MarkdownNodeTextSpan(_syntaxHighlighter.format(text.text))); + else + top.add(new _MarkdownNodeString(text.text)); + } + + bool visitElementBefore(md.Element element) { + if (_isListTag(element.tag)) + _listIndents.add(element.tag); + + if (_isBlockTag(element.tag)) { + List<_Block> blockList; + if (_currentBlock == null) + blockList = _blocks; + else + blockList = _currentBlock.subBlocks; + + _Block newBlock = new _Block(element.tag, element.attributes, _markdownStyle, new List.from(_listIndents), blockList.length); + blockList.add(newBlock); + } else { + TextStyle style = _markdownStyle.styles[element.tag] ?? new TextStyle(); + List<_MarkdownNode> styleElement = <_MarkdownNode>[new _MarkdownNodeTextStyle(style)]; + _currentBlock.stack.add(new _MarkdownNodeList(styleElement)); + } + return true; + } + + void visitElementAfter(md.Element element) { + if (_isListTag(element.tag)) + _listIndents.removeLast(); + + if (_isBlockTag(element.tag)) { + if (_currentBlock.stack.length > 0) { + _MarkdownNodeList stackList = _currentBlock.stack.first; + _currentBlock.stack = stackList.list; + _currentBlock.open = false; + } else { + _currentBlock.stack = <_MarkdownNode>[new _MarkdownNodeString('')]; + } + } else { + if (_currentBlock.stack.length > 1) { + _MarkdownNodeList poppedList = _currentBlock.stack.last; + List<_MarkdownNode> popped = poppedList.list; + _currentBlock.stack.removeLast(); + + _MarkdownNodeList topList = _currentBlock.stack.last; + List<_MarkdownNode> top = topList.list; + top.add(new _MarkdownNodeList(popped)); + } + } + } + + static const List _kBlockTags = const ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'blockquote', 'img', 'pre', 'ol', 'ul']; + static const List _kListTags = const ['ul', 'ol']; + + bool _isBlockTag(String tag) { + return _kBlockTags.contains(tag); + } + + bool _isListTag(String tag) { + return _kListTags.contains(tag); + } + + _Block get _currentBlock => _currentBlockInList(_blocks); + + _Block _currentBlockInList(List<_Block> blocks) { + if (blocks.isEmpty) + return null; + + if (!blocks.last.open) + return null; + + _Block childBlock = _currentBlockInList(blocks.last.subBlocks); + if (childBlock != null) + return childBlock; + + return blocks.last; + } +} + +abstract class _MarkdownNode { +} + +class _MarkdownNodeList extends _MarkdownNode { + _MarkdownNodeList(this.list); + List<_MarkdownNode> list; +} + +class _MarkdownNodeTextStyle extends _MarkdownNode { + _MarkdownNodeTextStyle(this.style); + TextStyle style; +} + +class _MarkdownNodeString extends _MarkdownNode { + _MarkdownNodeString(this.string); + String string; +} + +class _MarkdownNodeTextSpan extends _MarkdownNode { + _MarkdownNodeTextSpan(this.textSpan); + TextSpan textSpan; +} + +class _Block { + _Block(this.tag, this.attributes, this.markdownStyle, this.listIndents, this.blockPosition) { + TextStyle style = markdownStyle.styles[tag]; + if (style == null) + style = new TextStyle(color: const Color(0xffff0000)); + + stack = <_MarkdownNode>[new _MarkdownNodeList(<_MarkdownNode>[new _MarkdownNodeTextStyle(style)])]; + subBlocks = <_Block>[]; + } + + final String tag; + final Map attributes; + final MarkdownStyleRaw markdownStyle; + final List listIndents; + final int blockPosition; + + List<_MarkdownNode> stack; + List<_Block> subBlocks; + + bool get open => _open; + void set open(bool open) { + _open = open; + if (!open && subBlocks.length > 0) + subBlocks.last.isLast = true; + } + + bool _open = true; + bool isLast = false; + + Widget build(BuildContext context) { + + if (tag == 'img') { + return _buildImage(context, attributes['src']); + } + + double spacing = markdownStyle.blockSpacing; + if (isLast) spacing = 0.0; + + Widget contents; + + if (subBlocks.length > 0) { + List subWidgets = []; + for (_Block subBlock in subBlocks) { + subWidgets.add(subBlock.build(context)); + } + + contents = new Column( + alignItems: FlexAlignItems.stretch, + children: subWidgets + ); + } else { + contents = new RichText(text: _stackToTextSpan(new _MarkdownNodeList(stack))); + + if (listIndents.length > 0) { + Widget bullet; + if (listIndents.last == 'ul') { + bullet = new Text( + '•', + style: new TextStyle(textAlign: TextAlign.center) + ); + } + else { + bullet = new Padding( + padding: new EdgeDims.only(right: 5.0), + child: new Text( + "${blockPosition + 1}.", + style: new TextStyle(textAlign: TextAlign.right) + ) + ); + } + + contents = new Row( + alignItems: FlexAlignItems.start, + children: [ + new SizedBox( + width: listIndents.length * markdownStyle.listIndent, + child: bullet + ), + new Flexible(child: contents) + ] + ); + } + } + + BoxDecoration decoration; + EdgeDims padding; + + if (tag == 'blockquote') { + decoration = markdownStyle.blockquoteDecoration; + padding = new EdgeDims.all(markdownStyle.blockquotePadding); + } else if (tag == 'pre') { + decoration = markdownStyle.codeblockDecoration; + padding = new EdgeDims.all(markdownStyle.codeblockPadding); + } + + return new Container( + padding: padding, + margin: new EdgeDims.only(bottom: spacing), + child: contents, + decoration: decoration + ); + } + + TextSpan _stackToTextSpan(_MarkdownNode stack) { + if (stack is _MarkdownNodeTextSpan) + return stack.textSpan; + + if (stack is _MarkdownNodeList) { + List<_MarkdownNode> list = stack.list; + _MarkdownNodeTextStyle styleNode = list[0]; + TextStyle style = styleNode.style; + + List children = []; + for (int i = 1; i < list.length; i++) { + children.add(_stackToTextSpan(list[i])); + } + return new TextSpan(style: style, children: children); + } + + if (stack is _MarkdownNodeString) { + return new TextSpan(text: stack.string); + } + + return null; + } + + Widget _buildImage(BuildContext context, String src) { + List parts = src.split('#'); + if (parts.length == 0) return new Container(); + + String path = parts.first; + double width; + double height; + if (parts.length == 2) { + List dimensions = parts.last.split('x'); + if (dimensions.length == 2) { + width = double.parse(dimensions[0]); + height = double.parse(dimensions[1]); + } + } + + return new NetworkImage(src: path, width: width, height: height); + } +} + +abstract class SyntaxHighlighter { + TextSpan format(String source); +} + +class _DefaultSyntaxHighlighter extends SyntaxHighlighter{ + _DefaultSyntaxHighlighter(this.style); + final TextStyle style; + + TextSpan format(String source) { + return new TextSpan(style: style, children: [new TextSpan(text: source)]); + } +} diff --git a/lib/src/markdown_style.dart b/lib/src/markdown_style.dart new file mode 100644 index 00000000000..d71afce1749 --- /dev/null +++ b/lib/src/markdown_style.dart @@ -0,0 +1,77 @@ +// Copyright 2016 The Chromium 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/material.dart'; +import 'markdown_style_raw.dart'; + +/// Style used for rendering markdown formatted text using the [MarkdownBody] +/// widget. +class MarkdownStyle extends MarkdownStyleRaw{ + + /// Creates a [MarkdownStyle] from the [TextStyle]s in the provided [theme]. + MarkdownStyle.defaultFromTheme(ThemeData theme) : super( + a: new TextStyle(color: Colors.blue[500]), + p: theme.text.body1, + code: new TextStyle( + color: Colors.grey[700], + fontFamily: "monospace", + fontSize: theme.text.body1.fontSize * 0.85 + ), + h1: theme.text.headline, + h2: theme.text.title, + h3: theme.text.subhead, + h4: theme.text.body2, + h5: theme.text.body2, + h6: theme.text.body2, + em: new TextStyle(fontStyle: FontStyle.italic), + strong: new TextStyle(fontWeight: FontWeight.bold), + blockquote: theme.text.body1, + blockSpacing: 8.0, + listIndent: 32.0, + blockquotePadding: 8.0, + blockquoteDecoration: new BoxDecoration( + backgroundColor: Colors.blue[100], + borderRadius: 2.0 + ), + codeblockPadding: 8.0, + codeblockDecoration: new BoxDecoration( + backgroundColor: Colors.grey[100], + borderRadius: 2.0 + ) + ); + + /// Creates a [MarkdownStyle] from the [TextStyle]s in the provided [theme]. + /// This style uses larger fonts for the headings than in + /// [MarkdownStyle.defaultFromTheme]. + MarkdownStyle.largeFromTheme(ThemeData theme) : super ( + a: new TextStyle(color: Colors.blue[500]), + p: theme.text.body1, + code: new TextStyle( + color: Colors.grey[700], + fontFamily: "monospace", + fontSize: theme.text.body1.fontSize * 0.85 + ), + h1: theme.text.display3, + h2: theme.text.display2, + h3: theme.text.display1, + h4: theme.text.headline, + h5: theme.text.title, + h6: theme.text.subhead, + em: new TextStyle(fontStyle: FontStyle.italic), + strong: new TextStyle(fontWeight: FontWeight.bold), + blockquote: theme.text.body1, + blockSpacing: 8.0, + listIndent: 32.0, + blockquotePadding: 8.0, + blockquoteDecoration: new BoxDecoration( + backgroundColor: Colors.blue[100], + borderRadius: 2.0 + ), + codeblockPadding: 8.0, + codeblockDecoration: new BoxDecoration( + backgroundColor: Colors.grey[100], + borderRadius: 2.0 + ) + ); +} diff --git a/lib/src/markdown_style_raw.dart b/lib/src/markdown_style_raw.dart new file mode 100644 index 00000000000..4b694855bd0 --- /dev/null +++ b/lib/src/markdown_style_raw.dart @@ -0,0 +1,120 @@ +// Copyright 2016 The Chromium 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/widgets.dart'; + +/// Style used for rendering markdown formatted text using the [MarkdownBody] +/// widget. +class MarkdownStyleRaw { + + /// Creates a new [MarkdownStyleRaw] + MarkdownStyleRaw({ + this.a, + this.p, + this.code, + this.h1, + this.h2, + this.h3, + this.h4, + this.h5, + this.h6, + this.em, + this.strong, + this.blockquote, + this.blockSpacing, + this.listIndent, + this.blockquotePadding, + this.blockquoteDecoration, + this.codeblockPadding, + this.codeblockDecoration + }) { + _init(); + } + + /// Creates a new [MarkdownStyleRaw] based on the current style, with the + /// provided paramaters overridden. + MarkdownStyleRaw copyWith({ + TextStyle a, + TextStyle p, + TextStyle code, + TextStyle h1, + TextStyle h2, + TextStyle h3, + TextStyle h4, + TextStyle h5, + TextStyle h6, + TextStyle em, + TextStyle strong, + TextStyle blockquote, + double blockSpacing, + double listIndent, + double blockquotePadding, + BoxDecoration blockquoteDecoration, + double codeblockPadding, + BoxDecoration codeblockDecoration + }) { + return new MarkdownStyleRaw( + a: a != null ? a : this.a, + p: p != null ? p : this.p, + code: code != null ? code : this.code, + h1: h1 != null ? h1 : this.h1, + h2: h2 != null ? h2 : this.h2, + h3: h3 != null ? h3 : this.h3, + h4: h4 != null ? h4 : this.h4, + h5: h5 != null ? h5 : this.h5, + h6: h6 != null ? h6 : this.h6, + em: em != null ? em : this.em, + strong: strong != null ? strong : this.strong, + blockquote: blockquote != null ? blockquote : this.blockquote, + blockSpacing: blockSpacing != null ? blockSpacing : this.blockSpacing, + listIndent: listIndent != null ? listIndent : this.listIndent, + blockquotePadding: blockquotePadding != null ? blockquotePadding : this.blockquotePadding, + blockquoteDecoration: blockquoteDecoration != null ? blockquoteDecoration : this.blockquoteDecoration, + codeblockPadding: codeblockPadding != null ? codeblockPadding : this.codeblockPadding, + codeblockDecoration: codeblockDecoration != null ? codeblockDecoration : this.codeblockDecoration + ); + } + + final TextStyle a; + final TextStyle p; + final TextStyle code; + final TextStyle h1; + final TextStyle h2; + final TextStyle h3; + final TextStyle h4; + final TextStyle h5; + final TextStyle h6; + final TextStyle em; + final TextStyle strong; + final TextStyle blockquote; + final double blockSpacing; + final double listIndent; + final double blockquotePadding; + final BoxDecoration blockquoteDecoration; + final double codeblockPadding; + final BoxDecoration codeblockDecoration; + + Map _styles; + + Map get styles => _styles; + + void _init() { + _styles = { + 'a': a, + 'p': p, + 'li': p, + 'code': code, + 'pre': p, + 'h1': h1, + 'h2': h2, + 'h3': h3, + 'h4': h4, + 'h5': h5, + 'h6': h6, + 'em': em, + 'strong': strong, + 'blockquote': blockquote + }; + } +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 00000000000..2a85b010a6a --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,18 @@ +name: flutter_markdown +description: A markdown renderer for Flutter. +version: 0.1.0 +author: Flutter Authors +homepage: http://flutter.io + +dependencies: + flutter: + path: ../flutter + markdown: "0.9.0" + string_scanner: "0.1.4+1" + +dev_dependencies: + flutter_tools: + path: ../flutter_tools + test: any # constrained by the dependency in flutter_tools + flutter_test: + path: ../flutter_test diff --git a/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart new file mode 100644 index 00000000000..f738572fa85 --- /dev/null +++ b/test/flutter_markdown_test.dart @@ -0,0 +1,134 @@ +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/widgets.dart'; +import 'package:test/test.dart'; +import 'package:flutter/material.dart'; + +void main() { + test("Simple string", () { + testWidgets((WidgetTester tester) { + tester.pumpWidget(new MarkdownBody(data: "Hello")); + + Element textElement = tester.findElement((Element element) => element.widget is RichText); + RichText textWidget = textElement.widget; + TextSpan textSpan = textWidget.text; + + List elements = _listElements(tester); + _expectWidgetTypes(elements, [MarkdownBody, Column, Container, Padding, RichText]); + expect(textSpan.children[0].text, equals("Hello")); + }); + }); + + test("Header", () { + testWidgets((WidgetTester tester) { + tester.pumpWidget(new MarkdownBody(data: "# Header")); + + Element textElement = tester.findElement((Element element) => element.widget is RichText); + RichText textWidget = textElement.widget; + TextSpan textSpan = textWidget.text; + + List elements = _listElements(tester); + _expectWidgetTypes(elements, [MarkdownBody, Column, Container, Padding, RichText]); + expect(textSpan.children[0].text, equals("Header")); + }); + }); + + test("Empty string", () { + testWidgets((WidgetTester tester) { + tester.pumpWidget(new MarkdownBody(data: "")); + + List elements = _listElements(tester); + _expectWidgetTypes(elements, [MarkdownBody, Column]); + }); + }); + + test("Ordered list", () { + testWidgets((WidgetTester tester) { + tester.pumpWidget(new MarkdownBody(data: "1. Item 1\n1. Item 2\n2. Item 3")); + + List elements = _listElements(tester); + _expectTextStrings(elements, [ + "1.", + "Item 1", + "2.", + "Item 2", + "3.", + "Item 3"] + ); + }); + }); + + test("Unordered list", () { + testWidgets((WidgetTester tester) { + tester.pumpWidget(new MarkdownBody(data: "- Item 1\n- Item 2\n- Item 3")); + + List elements = _listElements(tester); + _expectTextStrings(elements, [ + "•", + "Item 1", + "•", + "Item 2", + "•", + "Item 3"] + ); + }); + }); + + test("Scrollable wrapping", () { + testWidgets((WidgetTester tester) { + tester.pumpWidget(new Markdown(data: "")); + + List elements = _listElements(tester); + for (Element element in elements) print("e: $element"); + _expectWidgetTypes(elements, [ + Markdown, + ScrollableViewport, + null, null, null, null, null, // ScrollableViewport internals + Padding, + MarkdownBody, + Column + ]); + }); + }); +} + +List _listElements(WidgetTester tester) { + List elements = []; + tester.walkElements((Element element) { + elements.add(element); + }); + return elements; +} + +void _expectWidgetTypes(List elements, List types) { + expect(elements.length, equals(types.length)); + for (int i = 0; i < elements.length; i += 1) { + Element element = elements[i]; + Type type = types[i]; + if (type == null) continue; + expect(element.widget.runtimeType, equals(type)); + } +} + +void _expectTextStrings(List elements, List strings) { + int currentString = 0; + for (Element element in elements) { + Widget widget = element.widget; + if (widget is RichText) { + TextSpan span = widget.text; + String text = _extractTextFromTextSpan(span); + expect(text, equals(strings[currentString])); + currentString += 1; + } + } +} + +String _extractTextFromTextSpan(TextSpan span) { + String text = span.text ?? ""; + if (span.children != null) { + for (TextSpan child in span.children) { + text += _extractTextFromTextSpan(child); + } + } + return text; +} From 051d06c36a4f9435a6050192059b5fbdba960aa5 Mon Sep 17 00:00:00 2001 From: Viktor Lidholt Date: Thu, 10 Mar 2016 13:51:15 -0800 Subject: [PATCH 002/121] Initial support for links in markdown --- example/demo.dart | 3 + lib/src/markdown.dart | 18 +++-- lib/src/markdown_raw.dart | 114 ++++++++++++++++++++++++++------ test/flutter_markdown_test.dart | 26 +++++--- 4 files changed, 124 insertions(+), 37 deletions(-) diff --git a/example/demo.dart b/example/demo.dart index 14bce27408b..64eb2ac8994 100644 --- a/example/demo.dart +++ b/example/demo.dart @@ -15,6 +15,9 @@ Style text as _italic_, __bold__, or `inline code`. - To better clarify - Your points +## Links +You can use [hyperlinks](hyperlink) in markdown + ## Code blocks Formatted Dart code looks really pretty too. This is an example of how to create your own Markdown widget: diff --git a/lib/src/markdown.dart b/lib/src/markdown.dart index 3b437b12418..aa8962729b4 100644 --- a/lib/src/markdown.dart +++ b/lib/src/markdown.dart @@ -25,22 +25,26 @@ class Markdown extends MarkdownRaw { Markdown({ String data, SyntaxHighlighter syntaxHighlighter, - MarkdownStyle markdownStyle + MarkdownStyle markdownStyle, + MarkdownLinkCallback onTapLink }) : super( data: data, syntaxHighlighter: syntaxHighlighter, - markdownStyle: markdownStyle + markdownStyle: markdownStyle, + onTapLink: onTapLink ); MarkdownBody createMarkdownBody({ String data, MarkdownStyle markdownStyle, - SyntaxHighlighter syntaxHighlighter + SyntaxHighlighter syntaxHighlighter, + MarkdownLinkCallback onTapLink }) { return new MarkdownBody( data: data, markdownStyle: markdownStyle, - syntaxHighlighter: syntaxHighlighter + syntaxHighlighter: syntaxHighlighter, + onTapLink: onTapLink ); } } @@ -71,11 +75,13 @@ class MarkdownBody extends MarkdownBodyRaw { MarkdownBody({ String data, SyntaxHighlighter syntaxHighlighter, - MarkdownStyle markdownStyle + MarkdownStyle markdownStyle, + MarkdownLinkCallback onTapLink }) : super( data: data, syntaxHighlighter: syntaxHighlighter, - markdownStyle: markdownStyle + markdownStyle: markdownStyle, + onTapLink: onTapLink ); MarkdownStyle createDefaultStyle(BuildContext context) { diff --git a/lib/src/markdown_raw.dart b/lib/src/markdown_raw.dart index 043e3e17d32..13fc7145462 100644 --- a/lib/src/markdown_raw.dart +++ b/lib/src/markdown_raw.dart @@ -4,8 +4,11 @@ import 'package:markdown/markdown.dart' as md; import 'package:flutter/widgets.dart'; +import 'package:flutter/gestures.dart'; import 'markdown_style_raw.dart'; +typedef void MarkdownLinkCallback(String href); + /// A [Widget] that renders markdown formatted text. It supports all standard /// markdowns from the original markdown specification found here: @@ -26,7 +29,8 @@ class MarkdownRaw extends StatelessComponent { this.data, this.markdownStyle, this.syntaxHighlighter, - this.padding: const EdgeDims.all(16.0) + this.padding: const EdgeDims.all(16.0), + this.onTapLink }); /// Markdown styled text @@ -41,6 +45,9 @@ class MarkdownRaw extends StatelessComponent { /// Padding used final EdgeDims padding; + /// Callback when a link is tapped + final MarkdownLinkCallback onTapLink; + Widget build(BuildContext context) { return new ScrollableViewport( child: new Padding( @@ -48,7 +55,8 @@ class MarkdownRaw extends StatelessComponent { child: createMarkdownBody( data: data, markdownStyle: markdownStyle, - syntaxHighlighter: syntaxHighlighter + syntaxHighlighter: syntaxHighlighter, + onTapLink: onTapLink ) ) ); @@ -57,12 +65,14 @@ class MarkdownRaw extends StatelessComponent { MarkdownBodyRaw createMarkdownBody({ String data, MarkdownStyleRaw markdownStyle, - SyntaxHighlighter syntaxHighlighter + SyntaxHighlighter syntaxHighlighter, + MarkdownLinkCallback onTapLink }) { return new MarkdownBodyRaw( data: data, markdownStyle: markdownStyle, - syntaxHighlighter: syntaxHighlighter + syntaxHighlighter: syntaxHighlighter, + onTapLink: onTapLink ); } } @@ -94,7 +104,8 @@ class MarkdownBodyRaw extends StatefulComponent { MarkdownBodyRaw({ this.data, this.markdownStyle, - this.syntaxHighlighter + this.syntaxHighlighter, + this.onTapLink }); /// Markdown styled text @@ -106,6 +117,9 @@ class MarkdownBodyRaw extends StatefulComponent { /// The syntax highlighter used to color text in code blocks final SyntaxHighlighter syntaxHighlighter; + /// Callback when a link is tapped + final MarkdownLinkCallback onTapLink; + _MarkdownBodyRawState createState() => new _MarkdownBodyRawState(); MarkdownStyleRaw createDefaultStyle(BuildContext context) => null; @@ -119,10 +133,23 @@ class _MarkdownBodyRawState extends State { MarkdownStyleRaw markdownStyle = config.markdownStyle ?? config.createDefaultStyle(context); SyntaxHighlighter syntaxHighlighter = config.syntaxHighlighter ?? new _DefaultSyntaxHighlighter(markdownStyle.code); - _cachedBlocks = _blocksFromMarkup(config.data, markdownStyle, syntaxHighlighter); + _linkHandler = new _LinkHandler(config.onTapLink); + + // TODO: This can be optimized by doing the split and removing \r at the same time + List lines = config.data.replaceAll('\r\n', '\n').split('\n'); + md.Document document = new md.Document(); + + _Renderer renderer = new _Renderer(); + _cachedBlocks = renderer.render(document.parseLines(lines), markdownStyle, syntaxHighlighter, _linkHandler); + } + + void dispose() { + _linkHandler.dispose(); + super.dispose(); } List<_Block> _cachedBlocks; + _LinkHandler _linkHandler; Widget build(BuildContext context) { List blocks = []; @@ -137,23 +164,15 @@ class _MarkdownBodyRawState extends State { } } -List<_Block> _blocksFromMarkup(String data, MarkdownStyleRaw markdownStyle, SyntaxHighlighter syntaxHighlighter) { - // TODO: This can be optimized by doing the split and removing \r at the same time - List lines = data.replaceAll('\r\n', '\n').split('\n'); - md.Document document = new md.Document(); - - _Renderer renderer = new _Renderer(); - return renderer.render(document.parseLines(lines), markdownStyle, syntaxHighlighter); -} - class _Renderer implements md.NodeVisitor { - List<_Block> render(List nodes, MarkdownStyleRaw markdownStyle, SyntaxHighlighter syntaxHighlighter) { + List<_Block> render(List nodes, MarkdownStyleRaw markdownStyle, SyntaxHighlighter syntaxHighlighter, _LinkHandler linkHandler) { assert(markdownStyle != null); _blocks = <_Block>[]; _listIndents = []; _markdownStyle = markdownStyle; _syntaxHighlighter = syntaxHighlighter; + _linkHandler = linkHandler; for (final md.Node node in nodes) { node.accept(this); @@ -166,6 +185,7 @@ class _Renderer implements md.NodeVisitor { List _listIndents; MarkdownStyleRaw _markdownStyle; SyntaxHighlighter _syntaxHighlighter; + _LinkHandler _linkHandler; void visitText(md.Text text) { _MarkdownNodeList topList = _currentBlock.stack.last; @@ -191,8 +211,13 @@ class _Renderer implements md.NodeVisitor { _Block newBlock = new _Block(element.tag, element.attributes, _markdownStyle, new List.from(_listIndents), blockList.length); blockList.add(newBlock); } else { + _LinkInfo linkInfo = null; + if (element.tag == 'a') { + linkInfo = _linkHandler.createLinkInfo(element.attributes['href']); + } + TextStyle style = _markdownStyle.styles[element.tag] ?? new TextStyle(); - List<_MarkdownNode> styleElement = <_MarkdownNode>[new _MarkdownNodeTextStyle(style)]; + List<_MarkdownNode> styleElement = <_MarkdownNode>[new _MarkdownNodeTextStyle(style, linkInfo)]; _currentBlock.stack.add(new _MarkdownNodeList(styleElement)); } return true; @@ -260,8 +285,9 @@ class _MarkdownNodeList extends _MarkdownNode { } class _MarkdownNodeTextStyle extends _MarkdownNode { - _MarkdownNodeTextStyle(this.style); + _MarkdownNodeTextStyle(this.style, [this.linkInfo = null]); TextStyle style; + _LinkInfo linkInfo; } class _MarkdownNodeString extends _MarkdownNode { @@ -325,7 +351,8 @@ class _Block { children: subWidgets ); } else { - contents = new RichText(text: _stackToTextSpan(new _MarkdownNodeList(stack))); + TextSpan span = _stackToTextSpan(new _MarkdownNodeList(stack)); + contents = new RichText(text: span); if (listIndents.length > 0) { Widget bullet; @@ -384,13 +411,23 @@ class _Block { if (stack is _MarkdownNodeList) { List<_MarkdownNode> list = stack.list; _MarkdownNodeTextStyle styleNode = list[0]; + _LinkInfo linkInfo = styleNode.linkInfo; TextStyle style = styleNode.style; List children = []; for (int i = 1; i < list.length; i++) { children.add(_stackToTextSpan(list[i])); } - return new TextSpan(style: style, children: children); + + String text = null; + if (children.length == 1 && _isPlainText(children[0])) { + text = children[0].text; + children = null; + } + + TapGestureRecognizer recognizer = linkInfo?.recognizer; + + return new TextSpan(style: style, children: children, recognizer: recognizer, text: text); } if (stack is _MarkdownNodeString) { @@ -400,6 +437,10 @@ class _Block { return null; } + bool _isPlainText(TextSpan span) { + return (span.text != null && span.style == null && span.recognizer == null && span.children == null); + } + Widget _buildImage(BuildContext context, String src) { List parts = src.split('#'); if (parts.length == 0) return new Container(); @@ -419,6 +460,39 @@ class _Block { } } +class _LinkInfo { + _LinkInfo(this.href, this.recognizer); + + final String href; + final TapGestureRecognizer recognizer; +} + +class _LinkHandler { + _LinkHandler(this.onTapLink); + + List<_LinkInfo> links = <_LinkInfo>[]; + MarkdownLinkCallback onTapLink; + + _LinkInfo createLinkInfo(String href) { + TapGestureRecognizer recognizer = new TapGestureRecognizer(); + recognizer.onTap = () { + if (onTapLink != null) + onTapLink(href); + }; + + _LinkInfo linkInfo = new _LinkInfo(href, recognizer); + links.add(linkInfo); + + return linkInfo; + } + + void dispose() { + for (_LinkInfo linkInfo in links) { + linkInfo.recognizer.dispose(); + } + } +} + abstract class SyntaxHighlighter { TextSpan format(String source); } diff --git a/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart index f738572fa85..72bc37f5a4b 100644 --- a/test/flutter_markdown_test.dart +++ b/test/flutter_markdown_test.dart @@ -1,5 +1,6 @@ import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; import 'package:test/test.dart'; import 'package:flutter/material.dart'; @@ -9,13 +10,9 @@ void main() { testWidgets((WidgetTester tester) { tester.pumpWidget(new MarkdownBody(data: "Hello")); - Element textElement = tester.findElement((Element element) => element.widget is RichText); - RichText textWidget = textElement.widget; - TextSpan textSpan = textWidget.text; - List elements = _listElements(tester); _expectWidgetTypes(elements, [MarkdownBody, Column, Container, Padding, RichText]); - expect(textSpan.children[0].text, equals("Hello")); + _expectTextStrings(elements, ["Hello"]); }); }); @@ -23,13 +20,9 @@ void main() { testWidgets((WidgetTester tester) { tester.pumpWidget(new MarkdownBody(data: "# Header")); - Element textElement = tester.findElement((Element element) => element.widget is RichText); - RichText textWidget = textElement.widget; - TextSpan textSpan = textWidget.text; - List elements = _listElements(tester); _expectWidgetTypes(elements, [MarkdownBody, Column, Container, Padding, RichText]); - expect(textSpan.children[0].text, equals("Header")); + _expectTextStrings(elements, ["Header"]); }); }); @@ -79,7 +72,6 @@ void main() { tester.pumpWidget(new Markdown(data: "")); List elements = _listElements(tester); - for (Element element in elements) print("e: $element"); _expectWidgetTypes(elements, [ Markdown, ScrollableViewport, @@ -90,6 +82,18 @@ void main() { ]); }); }); + + test("Links", () { + testWidgets((WidgetTester tester) { + tester.pumpWidget(new Markdown(data: "[Link Text](href)")); + + Element textElement = tester.findElement((Element element) => element.widget is RichText); + RichText textWidget = textElement.widget; + TextSpan span = textWidget.text; + + expect(span.children[0].recognizer.runtimeType, equals(TapGestureRecognizer)); + }); + }); } List _listElements(WidgetTester tester) { From df470ae615d555cf3cdb1f7cd3db1d58bca5f153 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Thu, 10 Mar 2016 23:15:31 -0800 Subject: [PATCH 003/121] Enable ALL THE LINTS Well, all the easy ones, anyway. For some reason `// ignore:` isn't working for me so I've disabled lints that need that. Also disabled those that require a ton of work (which I'm doing, but not in this PR, to keep it reviewable). This adds: - avoid_init_to_null - library_names - package_api_docs - package_names - package_prefixed_library_names - prefer_is_not_empty - sort_constructors_first - sort_unnamed_constructors_first - unnecessary_getters_setters --- lib/src/markdown_raw.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/src/markdown_raw.dart b/lib/src/markdown_raw.dart index 13fc7145462..ec29536888e 100644 --- a/lib/src/markdown_raw.dart +++ b/lib/src/markdown_raw.dart @@ -211,7 +211,7 @@ class _Renderer implements md.NodeVisitor { _Block newBlock = new _Block(element.tag, element.attributes, _markdownStyle, new List.from(_listIndents), blockList.length); blockList.add(newBlock); } else { - _LinkInfo linkInfo = null; + _LinkInfo linkInfo; if (element.tag == 'a') { linkInfo = _linkHandler.createLinkInfo(element.attributes['href']); } @@ -419,7 +419,7 @@ class _Block { children.add(_stackToTextSpan(list[i])); } - String text = null; + String text; if (children.length == 1 && _isPlainText(children[0])) { text = children[0].text; children = null; @@ -493,12 +493,13 @@ class _LinkHandler { } } -abstract class SyntaxHighlighter { +abstract class SyntaxHighlighter { // ignore: one_member_abstracts TextSpan format(String source); } class _DefaultSyntaxHighlighter extends SyntaxHighlighter{ _DefaultSyntaxHighlighter(this.style); + final TextStyle style; TextSpan format(String source) { From 170c83d3d94be9cca95f9f690e357ab0ea6585b0 Mon Sep 17 00:00:00 2001 From: Viktor Lidholt Date: Thu, 10 Mar 2016 14:48:18 -0800 Subject: [PATCH 004/121] Markdown is now correctly updated when config changes --- lib/src/markdown_raw.dart | 28 +++++++++++++++++++++++----- test/flutter_markdown_test.dart | 31 +++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/lib/src/markdown_raw.dart b/lib/src/markdown_raw.dart index 13fc7145462..d100c397cb3 100644 --- a/lib/src/markdown_raw.dart +++ b/lib/src/markdown_raw.dart @@ -129,10 +129,29 @@ class _MarkdownBodyRawState extends State { void initState() { super.initState(); + _buildMarkdownCache(); + } + + void dispose() { + _linkHandler.dispose(); + super.dispose(); + } + + void didUpdateConfig(MarkdownBodyRaw oldConfig) { + super.didUpdateConfig(oldConfig); + + if (oldConfig.data != config.data || + oldConfig.markdownStyle != config.markdownStyle || + oldConfig.syntaxHighlighter != config.syntaxHighlighter || + oldConfig.onTapLink != config.onTapLink) + _buildMarkdownCache(); + } + void _buildMarkdownCache() { MarkdownStyleRaw markdownStyle = config.markdownStyle ?? config.createDefaultStyle(context); SyntaxHighlighter syntaxHighlighter = config.syntaxHighlighter ?? new _DefaultSyntaxHighlighter(markdownStyle.code); + _linkHandler?.dispose(); _linkHandler = new _LinkHandler(config.onTapLink); // TODO: This can be optimized by doing the split and removing \r at the same time @@ -143,11 +162,6 @@ class _MarkdownBodyRawState extends State { _cachedBlocks = renderer.render(document.parseLines(lines), markdownStyle, syntaxHighlighter, _linkHandler); } - void dispose() { - _linkHandler.dispose(); - super.dispose(); - } - List<_Block> _cachedBlocks; _LinkHandler _linkHandler; @@ -162,6 +176,10 @@ class _MarkdownBodyRawState extends State { children: blocks ); } + + void debugFillDescription(List description) { + description.add('cached blocks identity: ${_cachedBlocks.hashCode}'); + } } class _Renderer implements md.NodeVisitor { diff --git a/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart index 72bc37f5a4b..03c4204e0d4 100644 --- a/test/flutter_markdown_test.dart +++ b/test/flutter_markdown_test.dart @@ -94,6 +94,37 @@ void main() { expect(span.children[0].recognizer.runtimeType, equals(TapGestureRecognizer)); }); }); + + test("Changing config - data", () { + testWidgets((WidgetTester tester) { + tester.pumpWidget(new Markdown(data: "Data1")); + _expectTextStrings(_listElements(tester), ["Data1"]); + + String stateBefore = WidgetFlutterBinding.instance.renderViewElement.toStringDeep(); + tester.pumpWidget(new Markdown(data: "Data1")); + String stateAfter = WidgetFlutterBinding.instance.renderViewElement.toStringDeep(); + expect(stateBefore, equals(stateAfter)); + + tester.pumpWidget(new Markdown(data: "Data2")); + _expectTextStrings(_listElements(tester), ["Data2"]); + }); + }); + + test("Changing config - style", () { + testWidgets((WidgetTester tester) { + ThemeData theme = new ThemeData.light(); + + MarkdownStyle style1 = new MarkdownStyle.defaultFromTheme(theme); + MarkdownStyle style2 = new MarkdownStyle.largeFromTheme(theme); + + tester.pumpWidget(new Markdown(data: "Test", markdownStyle: style1)); + + String stateBefore = WidgetFlutterBinding.instance.renderViewElement.toStringDeep(); + tester.pumpWidget(new Markdown(data: "Test", markdownStyle: style2)); + String stateAfter = WidgetFlutterBinding.instance.renderViewElement.toStringDeep(); + expect(stateBefore, isNot(stateAfter)); + }); + }); } List _listElements(WidgetTester tester) { From ce395bcafcdc6f774eff4fdfb9b800af545d6c66 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Sat, 12 Mar 2016 11:39:27 -0800 Subject: [PATCH 005/121] [rename fixit] ThemeData#text -> textTheme Fixes #1278 --- lib/src/markdown_style.dart | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/src/markdown_style.dart b/lib/src/markdown_style.dart index d71afce1749..b2281b68c0c 100644 --- a/lib/src/markdown_style.dart +++ b/lib/src/markdown_style.dart @@ -12,21 +12,21 @@ class MarkdownStyle extends MarkdownStyleRaw{ /// Creates a [MarkdownStyle] from the [TextStyle]s in the provided [theme]. MarkdownStyle.defaultFromTheme(ThemeData theme) : super( a: new TextStyle(color: Colors.blue[500]), - p: theme.text.body1, + p: theme.textTheme.body1, code: new TextStyle( color: Colors.grey[700], fontFamily: "monospace", - fontSize: theme.text.body1.fontSize * 0.85 + fontSize: theme.textTheme.body1.fontSize * 0.85 ), - h1: theme.text.headline, - h2: theme.text.title, - h3: theme.text.subhead, - h4: theme.text.body2, - h5: theme.text.body2, - h6: theme.text.body2, + h1: theme.textTheme.headline, + h2: theme.textTheme.title, + h3: theme.textTheme.subhead, + h4: theme.textTheme.body2, + h5: theme.textTheme.body2, + h6: theme.textTheme.body2, em: new TextStyle(fontStyle: FontStyle.italic), strong: new TextStyle(fontWeight: FontWeight.bold), - blockquote: theme.text.body1, + blockquote: theme.textTheme.body1, blockSpacing: 8.0, listIndent: 32.0, blockquotePadding: 8.0, @@ -46,21 +46,21 @@ class MarkdownStyle extends MarkdownStyleRaw{ /// [MarkdownStyle.defaultFromTheme]. MarkdownStyle.largeFromTheme(ThemeData theme) : super ( a: new TextStyle(color: Colors.blue[500]), - p: theme.text.body1, + p: theme.textTheme.body1, code: new TextStyle( color: Colors.grey[700], fontFamily: "monospace", - fontSize: theme.text.body1.fontSize * 0.85 + fontSize: theme.textTheme.body1.fontSize * 0.85 ), - h1: theme.text.display3, - h2: theme.text.display2, - h3: theme.text.display1, - h4: theme.text.headline, - h5: theme.text.title, - h6: theme.text.subhead, + h1: theme.textTheme.display3, + h2: theme.textTheme.display2, + h3: theme.textTheme.display1, + h4: theme.textTheme.headline, + h5: theme.textTheme.title, + h6: theme.textTheme.subhead, em: new TextStyle(fontStyle: FontStyle.italic), strong: new TextStyle(fontWeight: FontWeight.bold), - blockquote: theme.text.body1, + blockquote: theme.textTheme.body1, blockSpacing: 8.0, listIndent: 32.0, blockquotePadding: 8.0, From 73751269b8b104f6cad1bfcba01a54993f3121f7 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Sat, 12 Mar 2016 11:43:18 -0800 Subject: [PATCH 006/121] [rename fixit] EdgeDims -> EdgeInsets Fixes #1382 --- lib/src/markdown.dart | 2 +- lib/src/markdown_raw.dart | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/src/markdown.dart b/lib/src/markdown.dart index aa8962729b4..5edfffdef26 100644 --- a/lib/src/markdown.dart +++ b/lib/src/markdown.dart @@ -68,7 +68,7 @@ class MarkdownBody extends MarkdownBodyRaw { /// /// new ScrollableViewport( /// child: new Padding( - /// padding: new EdgeDims.all(16.0), + /// padding: new EdgeInsets.all(16.0), /// child: new Markdown(data: markdownSource) /// ) /// ) diff --git a/lib/src/markdown_raw.dart b/lib/src/markdown_raw.dart index d80555bb6c1..5a3c1e1b5ef 100644 --- a/lib/src/markdown_raw.dart +++ b/lib/src/markdown_raw.dart @@ -29,7 +29,7 @@ class MarkdownRaw extends StatelessComponent { this.data, this.markdownStyle, this.syntaxHighlighter, - this.padding: const EdgeDims.all(16.0), + this.padding: const EdgeInsets.all(16.0), this.onTapLink }); @@ -43,7 +43,7 @@ class MarkdownRaw extends StatelessComponent { final SyntaxHighlighter syntaxHighlighter; /// Padding used - final EdgeDims padding; + final EdgeInsets padding; /// Callback when a link is tapped final MarkdownLinkCallback onTapLink; @@ -94,7 +94,7 @@ class MarkdownBodyRaw extends StatefulComponent { /// /// new ScrollableViewport( /// child: new Padding( - /// padding: new EdgeDims.all(16.0), + /// padding: new EdgeInsets.all(16.0), /// child: new MarkdownBodyRaw( /// data: markdownSource, /// markdownStyle: myStyle @@ -382,7 +382,7 @@ class _Block { } else { bullet = new Padding( - padding: new EdgeDims.only(right: 5.0), + padding: new EdgeInsets.only(right: 5.0), child: new Text( "${blockPosition + 1}.", style: new TextStyle(textAlign: TextAlign.right) @@ -404,19 +404,19 @@ class _Block { } BoxDecoration decoration; - EdgeDims padding; + EdgeInsets padding; if (tag == 'blockquote') { decoration = markdownStyle.blockquoteDecoration; - padding = new EdgeDims.all(markdownStyle.blockquotePadding); + padding = new EdgeInsets.all(markdownStyle.blockquotePadding); } else if (tag == 'pre') { decoration = markdownStyle.codeblockDecoration; - padding = new EdgeDims.all(markdownStyle.codeblockPadding); + padding = new EdgeInsets.all(markdownStyle.codeblockPadding); } return new Container( padding: padding, - margin: new EdgeDims.only(bottom: spacing), + margin: new EdgeInsets.only(bottom: spacing), child: contents, decoration: decoration ); From cb5f2049828e9326d02d3f97101db79108b3feed Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Sat, 12 Mar 2016 12:13:16 -0800 Subject: [PATCH 007/121] [rename fixit] *Component* -> *Widget* This patch renames StatelessComponent to StatelessWidget and StatefulComponent to StatefulWidget. Fixes #2308 --- lib/src/markdown_raw.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/markdown_raw.dart b/lib/src/markdown_raw.dart index 5a3c1e1b5ef..c7554acdd2b 100644 --- a/lib/src/markdown_raw.dart +++ b/lib/src/markdown_raw.dart @@ -15,7 +15,7 @@ typedef void MarkdownLinkCallback(String href); /// https://daringfireball.net/projects/markdown/ The rendered markdown is /// placed in a padded scrolling view port. If you do not want the scrolling /// behaviour, use the [MarkdownBodyRaw] class instead. -class MarkdownRaw extends StatelessComponent { +class MarkdownRaw extends StatelessWidget { /// Creates a new Markdown [Widget] that renders the markdown formatted string /// passed in as [data]. By default the markdown will be rendered using the @@ -82,7 +82,7 @@ class MarkdownRaw extends StatelessComponent { /// https://daringfireball.net/projects/markdown/ This class doesn't implement /// any scrolling behavior, if you want scrolling either wrap the widget in /// a [ScrollableViewport] or use the [MarkdownRaw] widget. -class MarkdownBodyRaw extends StatefulComponent { +class MarkdownBodyRaw extends StatefulWidget { /// Creates a new Markdown [Widget] that renders the markdown formatted string /// passed in as [data]. You need to pass in a [markdownStyle] that defines From 3c1abc788bb037d54a7dd4e58a2f8e78fd66a292 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Sat, 12 Mar 2016 17:01:45 -0800 Subject: [PATCH 008/121] [rename fixit] RouteBuilder -> BuildContext Fixes #2353 --- example/demo.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/demo.dart b/example/demo.dart index 64eb2ac8994..f83b7f1cd8f 100644 --- a/example/demo.dart +++ b/example/demo.dart @@ -29,8 +29,8 @@ Enjoy! void main() { runApp(new MaterialApp( title: "Markdown Demo", - routes: { - '/': (RouteArguments args) => new Scaffold( + routes: { + '/': (BuildContext context) => new Scaffold( toolBar: new ToolBar(center: new Text("Markdown Demo")), body: new Markdown(data: _kMarkdownData) ) From 569f7b4d3fa11c4048363efee3eae282f974bed8 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Sat, 12 Mar 2016 17:22:40 -0800 Subject: [PATCH 009/121] [rename fixit] ToolBar -> AppBar * left -> leading (Removes an LTR bias) * center -> title (Widget was actually centered) * right -> actions (Removes an LTR bias, asymmetric with leading) Fixes #2348 --- example/demo.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/demo.dart b/example/demo.dart index f83b7f1cd8f..7877fe2b0dd 100644 --- a/example/demo.dart +++ b/example/demo.dart @@ -31,7 +31,7 @@ void main() { title: "Markdown Demo", routes: { '/': (BuildContext context) => new Scaffold( - toolBar: new ToolBar(center: new Text("Markdown Demo")), + appBar: new AppBar(title: new Text("Markdown Demo")), body: new Markdown(data: _kMarkdownData) ) } From 6936bea93badcfad1b7bf7167253cb2a353f97b5 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Sat, 12 Mar 2016 18:28:42 -0800 Subject: [PATCH 010/121] [rename fixit] Flex alignments * justifyContent -> mainAxisAlignment * alignItems -> crossAxisAlignment * FlexJustifyContent -> MainAxisAlignment * FlexAlignItems -> CrossAxisAlignment Fixes #231 --- lib/src/markdown_raw.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/markdown_raw.dart b/lib/src/markdown_raw.dart index c7554acdd2b..a7414c9f1ee 100644 --- a/lib/src/markdown_raw.dart +++ b/lib/src/markdown_raw.dart @@ -172,7 +172,7 @@ class _MarkdownBodyRawState extends State { } return new Column( - alignItems: FlexAlignItems.stretch, + crossAxisAlignment: CrossAxisAlignment.stretch, children: blocks ); } @@ -365,7 +365,7 @@ class _Block { } contents = new Column( - alignItems: FlexAlignItems.stretch, + crossAxisAlignment: CrossAxisAlignment.stretch, children: subWidgets ); } else { @@ -391,7 +391,7 @@ class _Block { } contents = new Row( - alignItems: FlexAlignItems.start, + crossAxisAlignment: CrossAxisAlignment.start, children: [ new SizedBox( width: listIndents.length * markdownStyle.listIndent, From a11aa652a32bf217dc4871b4b8ff0d529b10b70c Mon Sep 17 00:00:00 2001 From: Hixie Date: Mon, 14 Mar 2016 13:31:43 -0700 Subject: [PATCH 011/121] Add @override annotations to flutter framework --- lib/src/markdown.dart | 2 ++ lib/src/markdown_raw.dart | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/lib/src/markdown.dart b/lib/src/markdown.dart index 5edfffdef26..3549ae47e08 100644 --- a/lib/src/markdown.dart +++ b/lib/src/markdown.dart @@ -34,6 +34,7 @@ class Markdown extends MarkdownRaw { onTapLink: onTapLink ); + @override MarkdownBody createMarkdownBody({ String data, MarkdownStyle markdownStyle, @@ -84,6 +85,7 @@ class MarkdownBody extends MarkdownBodyRaw { onTapLink: onTapLink ); + @override MarkdownStyle createDefaultStyle(BuildContext context) { return new MarkdownStyle.defaultFromTheme(Theme.of(context)); } diff --git a/lib/src/markdown_raw.dart b/lib/src/markdown_raw.dart index a7414c9f1ee..adb2f722271 100644 --- a/lib/src/markdown_raw.dart +++ b/lib/src/markdown_raw.dart @@ -48,6 +48,7 @@ class MarkdownRaw extends StatelessWidget { /// Callback when a link is tapped final MarkdownLinkCallback onTapLink; + @override Widget build(BuildContext context) { return new ScrollableViewport( child: new Padding( @@ -120,6 +121,7 @@ class MarkdownBodyRaw extends StatefulWidget { /// Callback when a link is tapped final MarkdownLinkCallback onTapLink; + @override _MarkdownBodyRawState createState() => new _MarkdownBodyRawState(); MarkdownStyleRaw createDefaultStyle(BuildContext context) => null; @@ -127,16 +129,19 @@ class MarkdownBodyRaw extends StatefulWidget { class _MarkdownBodyRawState extends State { + @override void initState() { super.initState(); _buildMarkdownCache(); } + @override void dispose() { _linkHandler.dispose(); super.dispose(); } + @override void didUpdateConfig(MarkdownBodyRaw oldConfig) { super.didUpdateConfig(oldConfig); @@ -165,6 +170,7 @@ class _MarkdownBodyRawState extends State { List<_Block> _cachedBlocks; _LinkHandler _linkHandler; + @override Widget build(BuildContext context) { List blocks = []; for (_Block block in _cachedBlocks) { @@ -177,6 +183,7 @@ class _MarkdownBodyRawState extends State { ); } + @override void debugFillDescription(List description) { description.add('cached blocks identity: ${_cachedBlocks.hashCode}'); } @@ -205,6 +212,7 @@ class _Renderer implements md.NodeVisitor { SyntaxHighlighter _syntaxHighlighter; _LinkHandler _linkHandler; + @override void visitText(md.Text text) { _MarkdownNodeList topList = _currentBlock.stack.last; List<_MarkdownNode> top = topList.list; @@ -215,6 +223,7 @@ class _Renderer implements md.NodeVisitor { top.add(new _MarkdownNodeString(text.text)); } + @override bool visitElementBefore(md.Element element) { if (_isListTag(element.tag)) _listIndents.add(element.tag); @@ -241,6 +250,7 @@ class _Renderer implements md.NodeVisitor { return true; } + @override void visitElementAfter(md.Element element) { if (_isListTag(element.tag)) _listIndents.removeLast(); @@ -520,6 +530,7 @@ class _DefaultSyntaxHighlighter extends SyntaxHighlighter{ final TextStyle style; + @override TextSpan format(String source) { return new TextSpan(style: style, children: [new TextSpan(text: source)]); } From f6ac7ff6ab5f1af2ace51c627a18fe1b08f2ab2f Mon Sep 17 00:00:00 2001 From: Devon Carew Date: Tue, 5 Apr 2016 12:33:12 -0700 Subject: [PATCH 012/121] add a dev/dartdoc.dart script to generate docs for the packages/ packages * add a dev/dartdoc.dart script to generate docs for the packages/ packages * remove description * rename readme * change to using --include-external * move docs to dev/docs --- lib/flutter_markdown.dart | 1 + lib/flutter_markdown_raw.dart | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/flutter_markdown.dart b/lib/flutter_markdown.dart index 736e84f2db1..d65c9a5508b 100644 --- a/lib/flutter_markdown.dart +++ b/lib/flutter_markdown.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +/// A library to render markdown formatted text. library flutter_markdown; export 'src/markdown.dart'; diff --git a/lib/flutter_markdown_raw.dart b/lib/flutter_markdown_raw.dart index d7fed2c6661..3d2be6a5f0b 100644 --- a/lib/flutter_markdown_raw.dart +++ b/lib/flutter_markdown_raw.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -library flutter_markdown; +library flutter_markdown_raw; export 'src/markdown_raw.dart'; export 'src/markdown_style_raw.dart'; From 47caf75d6557e24f30bf1dd32ee591537c40205b Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Fri, 8 Apr 2016 10:08:13 -0700 Subject: [PATCH 013/121] Remove pub package version skew (#3212) We need to pin the version of package:analyzer we use to avoid version skew within our project. --- pubspec.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 2a85b010a6a..0eb55b3dbdb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,8 +11,5 @@ dependencies: string_scanner: "0.1.4+1" dev_dependencies: - flutter_tools: - path: ../flutter_tools - test: any # constrained by the dependency in flutter_tools flutter_test: path: ../flutter_test From 35cd2d4696ae4bdea4a7ffd1edb6bd32daed00b1 Mon Sep 17 00:00:00 2001 From: Yegor Date: Wed, 13 Apr 2016 23:40:15 -0700 Subject: [PATCH 014/121] [flutter_test] new WidgetTester API based on finder objects (#3288) --- test/flutter_markdown_test.dart | 62 +++++++++++++-------------------- 1 file changed, 24 insertions(+), 38 deletions(-) diff --git a/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart index 03c4204e0d4..942ab53826c 100644 --- a/test/flutter_markdown_test.dart +++ b/test/flutter_markdown_test.dart @@ -10,9 +10,9 @@ void main() { testWidgets((WidgetTester tester) { tester.pumpWidget(new MarkdownBody(data: "Hello")); - List elements = _listElements(tester); - _expectWidgetTypes(elements, [MarkdownBody, Column, Container, Padding, RichText]); - _expectTextStrings(elements, ["Hello"]); + Iterable widgets = tester.widgets; + _expectWidgetTypes(widgets, [MarkdownBody, Column, Container, Padding, RichText]); + _expectTextStrings(widgets, ["Hello"]); }); }); @@ -20,9 +20,9 @@ void main() { testWidgets((WidgetTester tester) { tester.pumpWidget(new MarkdownBody(data: "# Header")); - List elements = _listElements(tester); - _expectWidgetTypes(elements, [MarkdownBody, Column, Container, Padding, RichText]); - _expectTextStrings(elements, ["Header"]); + Iterable widgets = tester.widgets; + _expectWidgetTypes(widgets, [MarkdownBody, Column, Container, Padding, RichText]); + _expectTextStrings(widgets, ["Header"]); }); }); @@ -30,8 +30,8 @@ void main() { testWidgets((WidgetTester tester) { tester.pumpWidget(new MarkdownBody(data: "")); - List elements = _listElements(tester); - _expectWidgetTypes(elements, [MarkdownBody, Column]); + Iterable widgets = tester.widgets; + _expectWidgetTypes(widgets, [MarkdownBody, Column]); }); }); @@ -39,8 +39,8 @@ void main() { testWidgets((WidgetTester tester) { tester.pumpWidget(new MarkdownBody(data: "1. Item 1\n1. Item 2\n2. Item 3")); - List elements = _listElements(tester); - _expectTextStrings(elements, [ + Iterable widgets = tester.widgets; + _expectTextStrings(widgets, [ "1.", "Item 1", "2.", @@ -55,8 +55,8 @@ void main() { testWidgets((WidgetTester tester) { tester.pumpWidget(new MarkdownBody(data: "- Item 1\n- Item 2\n- Item 3")); - List elements = _listElements(tester); - _expectTextStrings(elements, [ + Iterable widgets = tester.widgets; + _expectTextStrings(widgets, [ "•", "Item 1", "•", @@ -71,11 +71,12 @@ void main() { testWidgets((WidgetTester tester) { tester.pumpWidget(new Markdown(data: "")); - List elements = _listElements(tester); - _expectWidgetTypes(elements, [ + List widgets = tester.widgets.toList(); + _expectWidgetTypes(widgets.take(2), [ Markdown, ScrollableViewport, - null, null, null, null, null, // ScrollableViewport internals + ]); + _expectWidgetTypes(widgets.reversed.take(3).toList().reversed, [ Padding, MarkdownBody, Column @@ -87,8 +88,7 @@ void main() { testWidgets((WidgetTester tester) { tester.pumpWidget(new Markdown(data: "[Link Text](href)")); - Element textElement = tester.findElement((Element element) => element.widget is RichText); - RichText textWidget = textElement.widget; + RichText textWidget = tester.widgets.firstWhere((Widget widget) => widget is RichText); TextSpan span = textWidget.text; expect(span.children[0].recognizer.runtimeType, equals(TapGestureRecognizer)); @@ -98,7 +98,7 @@ void main() { test("Changing config - data", () { testWidgets((WidgetTester tester) { tester.pumpWidget(new Markdown(data: "Data1")); - _expectTextStrings(_listElements(tester), ["Data1"]); + _expectTextStrings(tester.widgets, ["Data1"]); String stateBefore = WidgetFlutterBinding.instance.renderViewElement.toStringDeep(); tester.pumpWidget(new Markdown(data: "Data1")); @@ -106,7 +106,7 @@ void main() { expect(stateBefore, equals(stateAfter)); tester.pumpWidget(new Markdown(data: "Data2")); - _expectTextStrings(_listElements(tester), ["Data2"]); + _expectTextStrings(tester.widgets, ["Data2"]); }); }); @@ -127,28 +127,14 @@ void main() { }); } -List _listElements(WidgetTester tester) { - List elements = []; - tester.walkElements((Element element) { - elements.add(element); - }); - return elements; -} - -void _expectWidgetTypes(List elements, List types) { - expect(elements.length, equals(types.length)); - for (int i = 0; i < elements.length; i += 1) { - Element element = elements[i]; - Type type = types[i]; - if (type == null) continue; - expect(element.widget.runtimeType, equals(type)); - } +void _expectWidgetTypes(Iterable widgets, List expected) { + List actual = widgets.map((Widget w) => w.runtimeType).toList(); + expect(actual, expected); } -void _expectTextStrings(List elements, List strings) { +void _expectTextStrings(Iterable widgets, List strings) { int currentString = 0; - for (Element element in elements) { - Widget widget = element.widget; + for (Widget widget in widgets) { if (widget is RichText) { TextSpan span = widget.text; String text = _extractTextFromTextSpan(span); From 5bf7bac723c80c312cea67e4bf8d0f5cc5ae7517 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Wed, 20 Apr 2016 09:33:28 -0700 Subject: [PATCH 015/121] Hide routes from the API when they're not needed. (#3431) The 'routes' table is a point of confusion with new developers. By providing a 'home' argument that sets the '/' route, we can delay the point at which we teach developers about 'routes' until the point where they want to have a second route. --- example/demo.dart | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/example/demo.dart b/example/demo.dart index 7877fe2b0dd..ceb9e482a60 100644 --- a/example/demo.dart +++ b/example/demo.dart @@ -21,7 +21,7 @@ You can use [hyperlinks](hyperlink) in markdown ## Code blocks Formatted Dart code looks really pretty too. This is an example of how to create your own Markdown widget: - new Markdown(data: "Hello _world_!"); + new Markdown(data: 'Hello _world_!'); Enjoy! """; @@ -29,11 +29,9 @@ Enjoy! void main() { runApp(new MaterialApp( title: "Markdown Demo", - routes: { - '/': (BuildContext context) => new Scaffold( - appBar: new AppBar(title: new Text("Markdown Demo")), - body: new Markdown(data: _kMarkdownData) - ) - } + home: new Scaffold( + appBar: new AppBar(title: new Text('Markdown Demo')), + body: new Markdown(data: _kMarkdownData) + ) )); } From f154c3d477fadb5a0eda877cb5f8e87c5969e55a Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Thu, 21 Apr 2016 16:06:51 -0700 Subject: [PATCH 016/121] Make the widgets binding reusable. (#3479) Previously the widgets layer only provided a concrete binding, which makes it awkward to extend it compared to other bindings. This moves widgets to the same style as the other layers. In a subsequent patch I'll use this to make the tests layer saner. --- test/flutter_markdown_test.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart index 942ab53826c..975a3f5edf9 100644 --- a/test/flutter_markdown_test.dart +++ b/test/flutter_markdown_test.dart @@ -100,9 +100,9 @@ void main() { tester.pumpWidget(new Markdown(data: "Data1")); _expectTextStrings(tester.widgets, ["Data1"]); - String stateBefore = WidgetFlutterBinding.instance.renderViewElement.toStringDeep(); + String stateBefore = Widgeteer.instance.renderViewElement.toStringDeep(); tester.pumpWidget(new Markdown(data: "Data1")); - String stateAfter = WidgetFlutterBinding.instance.renderViewElement.toStringDeep(); + String stateAfter = Widgeteer.instance.renderViewElement.toStringDeep(); expect(stateBefore, equals(stateAfter)); tester.pumpWidget(new Markdown(data: "Data2")); @@ -119,9 +119,9 @@ void main() { tester.pumpWidget(new Markdown(data: "Test", markdownStyle: style1)); - String stateBefore = WidgetFlutterBinding.instance.renderViewElement.toStringDeep(); + String stateBefore = Widgeteer.instance.renderViewElement.toStringDeep(); tester.pumpWidget(new Markdown(data: "Test", markdownStyle: style2)); - String stateAfter = WidgetFlutterBinding.instance.renderViewElement.toStringDeep(); + String stateAfter = Widgeteer.instance.renderViewElement.toStringDeep(); expect(stateBefore, isNot(stateAfter)); }); }); From 9e7c61ac15443857c30ff15b0ea9a11c207afd72 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Thu, 21 Apr 2016 17:18:46 -0700 Subject: [PATCH 017/121] Rename binding abstract classes (#3482) The old names were getting silly and started stepping on valuable namespace. The new names are consistent and clear. --- test/flutter_markdown_test.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart index 975a3f5edf9..6e362a829c4 100644 --- a/test/flutter_markdown_test.dart +++ b/test/flutter_markdown_test.dart @@ -100,9 +100,9 @@ void main() { tester.pumpWidget(new Markdown(data: "Data1")); _expectTextStrings(tester.widgets, ["Data1"]); - String stateBefore = Widgeteer.instance.renderViewElement.toStringDeep(); + String stateBefore = WidgetsBinding.instance.renderViewElement.toStringDeep(); tester.pumpWidget(new Markdown(data: "Data1")); - String stateAfter = Widgeteer.instance.renderViewElement.toStringDeep(); + String stateAfter = WidgetsBinding.instance.renderViewElement.toStringDeep(); expect(stateBefore, equals(stateAfter)); tester.pumpWidget(new Markdown(data: "Data2")); @@ -119,9 +119,9 @@ void main() { tester.pumpWidget(new Markdown(data: "Test", markdownStyle: style1)); - String stateBefore = Widgeteer.instance.renderViewElement.toStringDeep(); + String stateBefore = WidgetsBinding.instance.renderViewElement.toStringDeep(); tester.pumpWidget(new Markdown(data: "Test", markdownStyle: style2)); - String stateAfter = Widgeteer.instance.renderViewElement.toStringDeep(); + String stateAfter = WidgetsBinding.instance.renderViewElement.toStringDeep(); expect(stateBefore, isNot(stateAfter)); }); }); From 11b6c0b6e425e173644776aec174e1cba8dc48f3 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Fri, 29 Apr 2016 13:23:27 -0700 Subject: [PATCH 018/121] Refactor the test framework (#3622) * Refactor widget test framework Instead of: ```dart test("Card Collection smoke test", () { testWidgets((WidgetTester tester) { ``` ...you now say: ```dart testWidgets("Card Collection smoke test", (WidgetTester tester) { ``` Instead of: ```dart expect(tester, hasWidget(find.text('hello'))); ``` ...you now say: ```dart expect(find.text('hello'), findsOneWidget); ``` Instead of the previous API (exists, widgets, widget, stateOf, elementOf, etc), you now have the following comprehensive API. All these are functions that take a Finder, except the all* properties. * `any()` - true if anything matches, c.f. `Iterable.any` * `allWidgets` - all the widgets in the tree * `widget()` - the one and only widget that matches the finder * `firstWidget()` - the first widget that matches the finder * `allElements` - all the elements in the tree * `element()` - the one and only element that matches the finder * `firstElement()` - the first element that matches the finder * `allStates` - all the `State`s in the tree * `state()` - the one and only state that matches the finder * `firstState()` - the first state that matches the finder * `allRenderObjects` - all the render objects in the tree * `renderObject()` - the one and only render object that matches the finder * `firstRenderObject()` - the first render object that matches the finder There's also `layers' which returns the list of current layers. `tap`, `fling`, getCenter, getSize, etc, take Finders, like the APIs above, and expect there to only be one matching widget. The finders are: * `find.text(String text)` * `find.widgetWithText(Type widgetType, String text)` * `find.byKey(Key key)` * `find.byType(Type type)` * `find.byElementType(Type type)` * `find.byConfig(Widget config)` * `find.byWidgetPredicate(WidgetPredicate predicate)` * `find.byElementPredicate(ElementPredicate predicate)` The matchers (for `expect`) are: * `findsNothing` * `findsWidgets` * `findsOneWidget` * `findsNWidgets(n)` * `isOnStage` * `isOffStage` * `isInCard` * `isNotInCard` Benchmarks now use benchmarkWidgets instead of testWidgets. Also, for those of you using mockers, `serviceMocker` now automatically handles the binding initialization. This patch also: * changes how tests are run so that we can more easily swap the logic out for a "real" mode instead of FakeAsync. * introduces CachingIterable. * changes how flutter_driver interacts with the widget tree to use the aforementioned new API rather than ElementTreeTester, which is gone. * removes ElementTreeTester. * changes the semantics of a test for scrollables because we couldn't convince ourselves that the old semantics made sense; it only worked before because flushing the microtasks after every event was broken. * fixes the flushing of microtasks after every event. * Reindent the tests * Fix review comments --- test/flutter_markdown_test.dart | 108 +++++++++++++------------------- 1 file changed, 45 insertions(+), 63 deletions(-) diff --git a/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart index 6e362a829c4..5ad8dbd8ab4 100644 --- a/test/flutter_markdown_test.dart +++ b/test/flutter_markdown_test.dart @@ -6,72 +6,61 @@ import 'package:test/test.dart'; import 'package:flutter/material.dart'; void main() { - test("Simple string", () { - testWidgets((WidgetTester tester) { - tester.pumpWidget(new MarkdownBody(data: "Hello")); + testWidgets('Simple string', (WidgetTester tester) { + tester.pumpWidget(new MarkdownBody(data: 'Hello')); - Iterable widgets = tester.widgets; + Iterable widgets = tester.allWidgets; _expectWidgetTypes(widgets, [MarkdownBody, Column, Container, Padding, RichText]); - _expectTextStrings(widgets, ["Hello"]); - }); + _expectTextStrings(widgets, ['Hello']); }); - test("Header", () { - testWidgets((WidgetTester tester) { - tester.pumpWidget(new MarkdownBody(data: "# Header")); + testWidgets('Header', (WidgetTester tester) { + tester.pumpWidget(new MarkdownBody(data: '# Header')); - Iterable widgets = tester.widgets; + Iterable widgets = tester.allWidgets; _expectWidgetTypes(widgets, [MarkdownBody, Column, Container, Padding, RichText]); - _expectTextStrings(widgets, ["Header"]); - }); + _expectTextStrings(widgets, ['Header']); }); - test("Empty string", () { - testWidgets((WidgetTester tester) { - tester.pumpWidget(new MarkdownBody(data: "")); + testWidgets('Empty string', (WidgetTester tester) { + tester.pumpWidget(new MarkdownBody(data: '')); - Iterable widgets = tester.widgets; + Iterable widgets = tester.allWidgets; _expectWidgetTypes(widgets, [MarkdownBody, Column]); - }); }); - test("Ordered list", () { - testWidgets((WidgetTester tester) { - tester.pumpWidget(new MarkdownBody(data: "1. Item 1\n1. Item 2\n2. Item 3")); + testWidgets('Ordered list', (WidgetTester tester) { + tester.pumpWidget(new MarkdownBody(data: '1. Item 1\n1. Item 2\n2. Item 3')); - Iterable widgets = tester.widgets; + Iterable widgets = tester.allWidgets; _expectTextStrings(widgets, [ - "1.", - "Item 1", - "2.", - "Item 2", - "3.", - "Item 3"] + '1.', + 'Item 1', + '2.', + 'Item 2', + '3.', + 'Item 3'] ); - }); }); - test("Unordered list", () { - testWidgets((WidgetTester tester) { - tester.pumpWidget(new MarkdownBody(data: "- Item 1\n- Item 2\n- Item 3")); + testWidgets('Unordered list', (WidgetTester tester) { + tester.pumpWidget(new MarkdownBody(data: '- Item 1\n- Item 2\n- Item 3')); - Iterable widgets = tester.widgets; + Iterable widgets = tester.allWidgets; _expectTextStrings(widgets, [ - "•", - "Item 1", - "•", - "Item 2", - "•", - "Item 3"] + '•', + 'Item 1', + '•', + 'Item 2', + '•', + 'Item 3'] ); - }); }); - test("Scrollable wrapping", () { - testWidgets((WidgetTester tester) { - tester.pumpWidget(new Markdown(data: "")); + testWidgets('Scrollable wrapping', (WidgetTester tester) { + tester.pumpWidget(new Markdown(data: '')); - List widgets = tester.widgets.toList(); + List widgets = tester.allWidgets.toList(); _expectWidgetTypes(widgets.take(2), [ Markdown, ScrollableViewport, @@ -81,49 +70,42 @@ void main() { MarkdownBody, Column ]); - }); }); - test("Links", () { - testWidgets((WidgetTester tester) { - tester.pumpWidget(new Markdown(data: "[Link Text](href)")); + testWidgets('Links', (WidgetTester tester) { + tester.pumpWidget(new Markdown(data: '[Link Text](href)')); - RichText textWidget = tester.widgets.firstWhere((Widget widget) => widget is RichText); + RichText textWidget = tester.allWidgets.firstWhere((Widget widget) => widget is RichText); TextSpan span = textWidget.text; expect(span.children[0].recognizer.runtimeType, equals(TapGestureRecognizer)); - }); }); - test("Changing config - data", () { - testWidgets((WidgetTester tester) { - tester.pumpWidget(new Markdown(data: "Data1")); - _expectTextStrings(tester.widgets, ["Data1"]); + testWidgets('Changing config - data', (WidgetTester tester) { + tester.pumpWidget(new Markdown(data: 'Data1')); + _expectTextStrings(tester.allWidgets, ['Data1']); String stateBefore = WidgetsBinding.instance.renderViewElement.toStringDeep(); - tester.pumpWidget(new Markdown(data: "Data1")); + tester.pumpWidget(new Markdown(data: 'Data1')); String stateAfter = WidgetsBinding.instance.renderViewElement.toStringDeep(); expect(stateBefore, equals(stateAfter)); - tester.pumpWidget(new Markdown(data: "Data2")); - _expectTextStrings(tester.widgets, ["Data2"]); - }); + tester.pumpWidget(new Markdown(data: 'Data2')); + _expectTextStrings(tester.allWidgets, ['Data2']); }); - test("Changing config - style", () { - testWidgets((WidgetTester tester) { + testWidgets('Changing config - style', (WidgetTester tester) { ThemeData theme = new ThemeData.light(); MarkdownStyle style1 = new MarkdownStyle.defaultFromTheme(theme); MarkdownStyle style2 = new MarkdownStyle.largeFromTheme(theme); - tester.pumpWidget(new Markdown(data: "Test", markdownStyle: style1)); + tester.pumpWidget(new Markdown(data: 'Test', markdownStyle: style1)); String stateBefore = WidgetsBinding.instance.renderViewElement.toStringDeep(); - tester.pumpWidget(new Markdown(data: "Test", markdownStyle: style2)); + tester.pumpWidget(new Markdown(data: 'Test', markdownStyle: style2)); String stateAfter = WidgetsBinding.instance.renderViewElement.toStringDeep(); expect(stateBefore, isNot(stateAfter)); - }); }); } @@ -145,7 +127,7 @@ void _expectTextStrings(Iterable widgets, List strings) { } String _extractTextFromTextSpan(TextSpan span) { - String text = span.text ?? ""; + String text = span.text ?? ''; if (span.children != null) { for (TextSpan child in span.children) { text += _extractTextFromTextSpan(child); From 293b022c9b13f550852ab78a47579c58939fb416 Mon Sep 17 00:00:00 2001 From: pq Date: Wed, 4 May 2016 13:37:23 -0700 Subject: [PATCH 019/121] Last literals get their types. Last fixes to get the repo running clean checking for annotations on list and map literals. --- lib/src/markdown_style_raw.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/markdown_style_raw.dart b/lib/src/markdown_style_raw.dart index 4b694855bd0..2bcac360832 100644 --- a/lib/src/markdown_style_raw.dart +++ b/lib/src/markdown_style_raw.dart @@ -100,7 +100,7 @@ class MarkdownStyleRaw { Map get styles => _styles; void _init() { - _styles = { + _styles = { 'a': a, 'p': p, 'li': p, From 6b77fc7af6526641cd8fdd7d88121ad4fe2cd64e Mon Sep 17 00:00:00 2001 From: Devon Carew Date: Wed, 4 May 2016 13:41:46 -0700 Subject: [PATCH 020/121] checked mode fix; add type annotation (#3737) --- lib/src/markdown_style_raw.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/markdown_style_raw.dart b/lib/src/markdown_style_raw.dart index 4b694855bd0..2bcac360832 100644 --- a/lib/src/markdown_style_raw.dart +++ b/lib/src/markdown_style_raw.dart @@ -100,7 +100,7 @@ class MarkdownStyleRaw { Map get styles => _styles; void _init() { - _styles = { + _styles = { 'a': a, 'p': p, 'li': p, From 3b33c09aeedf56a1d9c7d01233ccd2cf2a3e1c27 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Fri, 6 May 2016 17:33:27 -0700 Subject: [PATCH 021/121] Move TextAlign out of TextStyle (#3789) TextAlign applies to a whole paragraph instead of applying to an individual text span. This patch moves the property out of TextStyle and into a separate property on Text and RichText. --- lib/src/markdown_raw.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/markdown_raw.dart b/lib/src/markdown_raw.dart index adb2f722271..45d16ab1945 100644 --- a/lib/src/markdown_raw.dart +++ b/lib/src/markdown_raw.dart @@ -387,7 +387,7 @@ class _Block { if (listIndents.last == 'ul') { bullet = new Text( '•', - style: new TextStyle(textAlign: TextAlign.center) + textAlign: TextAlign.center ); } else { @@ -395,7 +395,7 @@ class _Block { padding: new EdgeInsets.only(right: 5.0), child: new Text( "${blockPosition + 1}.", - style: new TextStyle(textAlign: TextAlign.right) + textAlign: TextAlign.right ) ); } From 62d04278d4bff4b599d27b0c112d6b9aee0ea577 Mon Sep 17 00:00:00 2001 From: pq Date: Thu, 12 May 2016 11:45:30 -0700 Subject: [PATCH 022/121] Turn on `avoid_return_types_on_setters` and cleanup annotated setters. It's safe to remove the unneeded `void`s from setters since the blocking issues in the `always_declare_return_types` lint have been fixed (https://github.com/dart-lang/linter/). We can also safely flip the bit on `avoid_return_types_on_setters`. --- lib/src/markdown_raw.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/markdown_raw.dart b/lib/src/markdown_raw.dart index 45d16ab1945..80b243d112a 100644 --- a/lib/src/markdown_raw.dart +++ b/lib/src/markdown_raw.dart @@ -348,7 +348,7 @@ class _Block { List<_Block> subBlocks; bool get open => _open; - void set open(bool open) { + set open(bool open) { _open = open; if (!open && subBlocks.length > 0) subBlocks.last.isLast = true; From b7db173818b7b254139c8bfce55f663783d4ccc0 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Mon, 16 May 2016 12:53:13 -0700 Subject: [PATCH 023/121] Make it possible to run tests live on a device (#3936) This makes it possible to substitute 'flutter run' for 'flutter test' and actually watch a test run on a device. For any test that depends on flutter_test: 1. Remove any import of 'package:test/test.dart'. 2. Replace `testWidgets('...', (WidgetTester tester) {` with `testWidgets('...', (WidgetTester tester) async {` 3. Add an "await" in front of calls to any of the following: * tap() * tapAt() * fling() * flingFrom() * scroll() * scrollAt() * pump() * pumpWidget() 4. Replace any calls to `tester.flushMicrotasks()` with calls to `await tester.idle()`. There's a guarding API that you can use, if you have particularly complicated tests, to get better error messages. Search for TestAsyncUtils. --- test/flutter_markdown_test.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart index 5ad8dbd8ab4..c72fcd7ee64 100644 --- a/test/flutter_markdown_test.dart +++ b/test/flutter_markdown_test.dart @@ -1,8 +1,11 @@ +// Copyright 2016 The Chromium 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_markdown/flutter_markdown.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; -import 'package:test/test.dart'; import 'package:flutter/material.dart'; void main() { From cdb6c75af462f28a3ba1b8e4cde53174b6ef9a8f Mon Sep 17 00:00:00 2001 From: pq Date: Thu, 19 May 2016 10:21:38 -0700 Subject: [PATCH 024/121] Cleanup dangling comment references. Quick pass at fixing a few dangling references as revealed by the new `comment_references` lint (https://github.com/dart-lang/linter/issues/240). There's a bunch more to do here before we can turn it on by default (~430 lints as of now). Many of them are a simple matter of adding an import (e.g., `dart:async` for library docs that reference `Future`) but others will require a bit of thought. Probably best done by the folks writing the code. :) --- lib/src/markdown_style.dart | 1 + lib/src/markdown_style_raw.dart | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/src/markdown_style.dart b/lib/src/markdown_style.dart index b2281b68c0c..357fbae8b5f 100644 --- a/lib/src/markdown_style.dart +++ b/lib/src/markdown_style.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:flutter/material.dart'; +import 'markdown.dart'; import 'markdown_style_raw.dart'; /// Style used for rendering markdown formatted text using the [MarkdownBody] diff --git a/lib/src/markdown_style_raw.dart b/lib/src/markdown_style_raw.dart index 2bcac360832..1d25924c74a 100644 --- a/lib/src/markdown_style_raw.dart +++ b/lib/src/markdown_style_raw.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:flutter/widgets.dart'; +import 'markdown.dart'; /// Style used for rendering markdown formatted text using the [MarkdownBody] /// widget. From 7de6979047e3ed83e37c18f5d2621d2cda166710 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Wed, 1 Jun 2016 08:27:19 -0700 Subject: [PATCH 025/121] Use protected and mustCallSuper in more places (#4291) --- lib/src/markdown_raw.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/markdown_raw.dart b/lib/src/markdown_raw.dart index 80b243d112a..10bef55b7f0 100644 --- a/lib/src/markdown_raw.dart +++ b/lib/src/markdown_raw.dart @@ -185,6 +185,7 @@ class _MarkdownBodyRawState extends State { @override void debugFillDescription(List description) { + super.debugFillDescription(description); description.add('cached blocks identity: ${_cachedBlocks.hashCode}'); } } From c70d2081eee9065a4a745c62258d6328ca1848ed Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Mon, 6 Jun 2016 19:22:55 -0700 Subject: [PATCH 026/121] Loosen dependencies on string_scanner (#4412) Now that there's a new string_scanner in town, we're having dependency resolution conflict because of flutter_markdown's tight dependency. This patch loosens the dependency and resolves the conflict. --- pubspec.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 0eb55b3dbdb..dfde8e0ef28 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,8 +7,8 @@ homepage: http://flutter.io dependencies: flutter: path: ../flutter - markdown: "0.9.0" - string_scanner: "0.1.4+1" + markdown: '^0.9.0' + string_scanner: '^0.1.5' dev_dependencies: flutter_test: From 145dcbb4be333209c81c944598783747aa95f3f9 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Thu, 16 Jun 2016 09:49:48 -0700 Subject: [PATCH 027/121] Refactor everything to do with images (#4583) Overview ======== This patch refactors images to achieve the following goals: * it allows references to unresolved assets to be passed around (previously, almost every layer of the system had to know about whether an image came from an asset bundle or the network or elsewhere, and had to manually interact with the image cache). * it allows decorations to use the same API for declaring images as the widget tree. It requires some minor changes to call sites that use images, as discussed below. Widgets ------- Change this: ```dart child: new AssetImage( name: 'my_asset.png', ... ) ``` ...to this: ```dart child: new Image( image: new AssetImage('my_asset.png'), ... ) ``` Decorations ----------- Change this: ```dart child: new DecoratedBox( decoration: new BoxDecoration( backgroundImage: new BackgroundImage( image: DefaultAssetBundle.of(context).loadImage('my_asset.png'), ... ), ... ), child: ... ) ``` ...to this: ```dart child: new DecoratedBox( decoration: new BoxDecoration( backgroundImage: new BackgroundImage( image: new AssetImage('my_asset.png'), ... ), ... ), child: ... ) ``` DETAILED CHANGE LOG =================== The following APIs have been replaced in this patch: * The `AssetImage` and `NetworkImage` widgets have been split in two, with identically-named `ImageProvider` subclasses providing the image-loading logic, and a single `Image` widget providing all the widget tree logic. * `ImageResource` is now `ImageStream`. Rather than configuring it with a `Future`, you complete it with an `ImageStreamCompleter`. * `ImageCache.load` and `ImageCache.loadProvider` are replaced by `ImageCache.putIfAbsent`. The following APIs have changed in this patch: * `ImageCache` works in terms of arbitrary keys and caches `ImageStreamCompleter` objects using those keys. With the new model, you should never need to interact with the cache directly. * `Decoration` can now be `const`. The state has moved to the `BoxPainter` class. Instead of a list of listeners, there's now just a single callback and a `dispose()` method on the painter. The callback is passed in to the `createBoxPainter()` method. When invoked, you should repaint the painter. The following new APIs are introduced: * `AssetBundle.loadStructuredData`. * `SynchronousFuture`, a variant of `Future` that calls the `then` callback synchronously. This enables the asynchronous and synchronous (in-the-cache) code paths to look identical yet for the latter to avoid returning to the event loop mid-paint. * `ExactAssetImage`, a variant of `AssetImage` that doesn't do anything clever. * `ImageConfiguration`, a class that describes parameters that configure the `AssetImage` resolver. The following APIs are entirely removed by this patch: * `AssetBundle.loadImage` is gone. Use an `AssetImage` instead. * `AssetVendor` is gone. `AssetImage` handles everything `AssetVendor` used to handle. * `RawImageResource` and `AsyncImage` are gone. The following code-level changes are performed: * `Image`, which replaces `AsyncImage`, `NetworkImage`, `AssetImage`, and `RawResourceImage`, lives in `image.dart`. * `DecoratedBox` and `Container` live in their own file now, `container.dart` (they reference `image.dart`). DIRECTIONS FOR FUTURE RESEARCH ============================== * The `ImageConfiguration` fields are mostly aspirational. Right now only `devicePixelRatio` and `bundle` are implemented. `locale` isn't even plumbed through, it will require work on the localisation logic. * We should go through and make `BoxDecoration`, `AssetImage`, and `NetworkImage` objects `const` where possible. * This patch makes supporting animated GIFs much easier. * This patch makes it possible to create an abstract concept of an "Icon" that could be either an image or a font-based glyph (using `IconData` or similar). (see https://github.com/flutter/flutter/issues/4494) RELATED ISSUES ============== Fixes https://github.com/flutter/flutter/issues/4500 Fixes https://github.com/flutter/flutter/issues/4495 Obsoletes https://github.com/flutter/flutter/issues/4496 --- lib/src/markdown_raw.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/src/markdown_raw.dart b/lib/src/markdown_raw.dart index 10bef55b7f0..3b9ba3b7710 100644 --- a/lib/src/markdown_raw.dart +++ b/lib/src/markdown_raw.dart @@ -485,7 +485,11 @@ class _Block { } } - return new NetworkImage(src: path, width: width, height: height); + return new Image( + image: new NetworkImage(path), + width: width, + height: height + ); } } From 72c0f516e68683380d8ab472d70a1a2d7a1d76c0 Mon Sep 17 00:00:00 2001 From: pq Date: Wed, 27 Jul 2016 10:02:54 -0700 Subject: [PATCH 028/121] Update Dart (1.19.0-dev.0.0) and analyzer (0.27.4-alpha.19). --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index dfde8e0ef28..6fe9faf26b4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,7 +8,7 @@ dependencies: flutter: path: ../flutter markdown: '^0.9.0' - string_scanner: '^0.1.5' + string_scanner: ^1.0.0 dev_dependencies: flutter_test: From 09a11136b10e65100c7f7c91761d0c6b00e445d5 Mon Sep 17 00:00:00 2001 From: pq Date: Wed, 27 Jul 2016 10:02:54 -0700 Subject: [PATCH 029/121] Update Dart (1.19.0-dev.0.0) and analyzer (0.27.4-alpha.19). --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index dfde8e0ef28..6fe9faf26b4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,7 +8,7 @@ dependencies: flutter: path: ../flutter markdown: '^0.9.0' - string_scanner: '^0.1.5' + string_scanner: ^1.0.0 dev_dependencies: flutter_test: From 32c4fec6083445b9f701d8e7b17d54a587a69e01 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Fri, 29 Jul 2016 08:27:28 -0700 Subject: [PATCH 030/121] Use named Image constructors (#5129) Some folks didn't realize these existed and asked us to add them. By using them in examples, hopefully folks will discover them more easily. --- lib/src/markdown_raw.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/markdown_raw.dart b/lib/src/markdown_raw.dart index 3b9ba3b7710..44c68065a27 100644 --- a/lib/src/markdown_raw.dart +++ b/lib/src/markdown_raw.dart @@ -485,8 +485,8 @@ class _Block { } } - return new Image( - image: new NetworkImage(path), + return new Image.fromNetwork( + src: path, width: width, height: height ); From 0f2bba5a73f766e2620b873ca9e3564da047e1f5 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Fri, 29 Jul 2016 13:28:08 -0700 Subject: [PATCH 031/121] Rename Image.fromNetwork and Image.fromAssetBundle (#5149) These now have sorter names to make the callers less verbose. --- lib/src/markdown_raw.dart | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/src/markdown_raw.dart b/lib/src/markdown_raw.dart index 44c68065a27..42fd4576106 100644 --- a/lib/src/markdown_raw.dart +++ b/lib/src/markdown_raw.dart @@ -485,11 +485,7 @@ class _Block { } } - return new Image.fromNetwork( - src: path, - width: width, - height: height - ); + return new Image.network(path, width: width, height: height); } } From 306fd791b4c47fecbf723c94d42b9372f1584747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Drago=C8=99=20Tiselice?= Date: Fri, 29 Jul 2016 16:17:57 -0700 Subject: [PATCH 032/121] Added BorderRadius. (#5072) * Added custom radii to RRect. This is the first commit towads an implementation of MergeableMaterial. It adds custom radii to RRect. * Renamed RRect constructors and added BorderRadius. BorderRadius is a class similar to EdgeInsets that lets you define all rounded corners of a rounded rectangle easily. --- lib/src/markdown_style.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/markdown_style.dart b/lib/src/markdown_style.dart index 357fbae8b5f..b89443618a3 100644 --- a/lib/src/markdown_style.dart +++ b/lib/src/markdown_style.dart @@ -33,12 +33,12 @@ class MarkdownStyle extends MarkdownStyleRaw{ blockquotePadding: 8.0, blockquoteDecoration: new BoxDecoration( backgroundColor: Colors.blue[100], - borderRadius: 2.0 + borderRadius: new BorderRadius.circular(2.0) ), codeblockPadding: 8.0, codeblockDecoration: new BoxDecoration( backgroundColor: Colors.grey[100], - borderRadius: 2.0 + borderRadius: new BorderRadius.circular(2.0) ) ); @@ -67,12 +67,12 @@ class MarkdownStyle extends MarkdownStyleRaw{ blockquotePadding: 8.0, blockquoteDecoration: new BoxDecoration( backgroundColor: Colors.blue[100], - borderRadius: 2.0 + borderRadius: new BorderRadius.circular(2.0) ), codeblockPadding: 8.0, codeblockDecoration: new BoxDecoration( backgroundColor: Colors.grey[100], - borderRadius: 2.0 + borderRadius: new BorderRadius.circular(2.0) ) ); } From f71fd20a192f41fd14c19b5a604faa75c1fd4543 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Thu, 22 Sep 2016 20:39:35 -0700 Subject: [PATCH 033/121] Use SDK sources to refer to our own packages (#6001) Switch our pubspec.yamls to using SDK sources so that we can have consistent source types when we depend on these packages from external packages using SDK sources. --- pubspec.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 6fe9faf26b4..fb171221188 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,10 +6,10 @@ homepage: http://flutter.io dependencies: flutter: - path: ../flutter + sdk: flutter markdown: '^0.9.0' string_scanner: ^1.0.0 dev_dependencies: flutter_test: - path: ../flutter_test + sdk: flutter From bec620571afa49904198249ab217f1976d521e32 Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Mon, 3 Oct 2016 12:00:01 -0700 Subject: [PATCH 034/121] Fix Gallery example code display initialization (#6176) --- lib/src/markdown_raw.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/markdown_raw.dart b/lib/src/markdown_raw.dart index 42fd4576106..54e46878f90 100644 --- a/lib/src/markdown_raw.dart +++ b/lib/src/markdown_raw.dart @@ -130,9 +130,9 @@ class MarkdownBodyRaw extends StatefulWidget { class _MarkdownBodyRawState extends State { @override - void initState() { - super.initState(); + void dependenciesChanged() { _buildMarkdownCache(); + super.dependenciesChanged(); } @override From 6e1fe0f38c7bc24f600b94bbde291d44c4070857 Mon Sep 17 00:00:00 2001 From: Devon Carew Date: Thu, 6 Oct 2016 17:43:52 -0700 Subject: [PATCH 035/121] commit intellij project metadata (#6232) * commit intellij project metadata * move metadata to the top level * delete dev/intellij --- flutter_markdown.iml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 flutter_markdown.iml diff --git a/flutter_markdown.iml b/flutter_markdown.iml new file mode 100644 index 00000000000..2ae0251c52f --- /dev/null +++ b/flutter_markdown.iml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file From 193757331c0c3ca4fc3c20934d19d5a9f8419385 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Fri, 7 Oct 2016 11:27:54 -0700 Subject: [PATCH 036/121] Deploy `@checked` (#6244) This patch adds `@checked` everywhere is needed to remove the `strong_mode_invalid_method_override` strong mode error. --- lib/src/markdown_raw.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/markdown_raw.dart b/lib/src/markdown_raw.dart index 54e46878f90..2672ce6cf90 100644 --- a/lib/src/markdown_raw.dart +++ b/lib/src/markdown_raw.dart @@ -3,8 +3,10 @@ // found in the LICENSE file. import 'package:markdown/markdown.dart' as md; +import 'package:meta/meta.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter/gestures.dart'; + import 'markdown_style_raw.dart'; typedef void MarkdownLinkCallback(String href); @@ -65,7 +67,7 @@ class MarkdownRaw extends StatelessWidget { MarkdownBodyRaw createMarkdownBody({ String data, - MarkdownStyleRaw markdownStyle, + @checked MarkdownStyleRaw markdownStyle, SyntaxHighlighter syntaxHighlighter, MarkdownLinkCallback onTapLink }) { From 58e456984c38a833fc13ba7a4e8cd7bc10875915 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Wed, 12 Oct 2016 23:33:17 -0700 Subject: [PATCH 037/121] Update iml files (#6300) These changes were generated by IntelliJ. --- flutter_markdown.iml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter_markdown.iml b/flutter_markdown.iml index 2ae0251c52f..162f9c54876 100644 --- a/flutter_markdown.iml +++ b/flutter_markdown.iml @@ -5,7 +5,7 @@ - + From 3b8d3f418227e7031e04cb9dc0c39c83420d5bf0 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Fri, 14 Oct 2016 09:58:48 -0700 Subject: [PATCH 038/121] Update IML files --- flutter_markdown.iml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/flutter_markdown.iml b/flutter_markdown.iml index 162f9c54876..cbcc56cb4bf 100644 --- a/flutter_markdown.iml +++ b/flutter_markdown.iml @@ -3,9 +3,8 @@ - - + From 31011def9c488a1c6290a369f7cc6033cd25da60 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Fri, 14 Oct 2016 21:57:28 -0700 Subject: [PATCH 039/121] More automated edits of iml files --- flutter_markdown.iml | 1 + 1 file changed, 1 insertion(+) diff --git a/flutter_markdown.iml b/flutter_markdown.iml index cbcc56cb4bf..2ae0251c52f 100644 --- a/flutter_markdown.iml +++ b/flutter_markdown.iml @@ -3,6 +3,7 @@ + From 4e46bc3649627e3f6fde536a973f8e1cb306e8f4 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Tue, 25 Oct 2016 10:06:42 -0700 Subject: [PATCH 040/121] Fix common typos in doc comments (#6520) --- lib/src/markdown_style_raw.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/markdown_style_raw.dart b/lib/src/markdown_style_raw.dart index 1d25924c74a..adf8316e58d 100644 --- a/lib/src/markdown_style_raw.dart +++ b/lib/src/markdown_style_raw.dart @@ -34,7 +34,7 @@ class MarkdownStyleRaw { } /// Creates a new [MarkdownStyleRaw] based on the current style, with the - /// provided paramaters overridden. + /// provided parameters overridden. MarkdownStyleRaw copyWith({ TextStyle a, TextStyle p, From b3c1f40d4ed580e1b9e71f25fb5fd33b75c68aad Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Tue, 1 Nov 2016 13:27:06 -0700 Subject: [PATCH 041/121] Mentions `flutter packages get` rather than `pub get` (#6625) Fixes #6417 --- flutter_markdown.iml | 1 - 1 file changed, 1 deletion(-) diff --git a/flutter_markdown.iml b/flutter_markdown.iml index 2ae0251c52f..cbcc56cb4bf 100644 --- a/flutter_markdown.iml +++ b/flutter_markdown.iml @@ -3,7 +3,6 @@ - From 2090963c2648f4b182861faa7171e29417fbc3cb Mon Sep 17 00:00:00 2001 From: Dan Rubel Date: Tue, 8 Nov 2016 17:15:11 -0500 Subject: [PATCH 042/121] revert args to 0.13.6 (#6765) --- pubspec.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pubspec.yaml b/pubspec.yaml index fb171221188..55f609492e2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,6 +5,12 @@ author: Flutter Authors homepage: http://flutter.io dependencies: + + # version 0.13.6+1 does not return the value from the command + # causing flutter tools to always have exit code 0 + # see https://github.com/flutter/flutter/issues/6766 + args: 0.13.6 + flutter: sdk: flutter markdown: '^0.9.0' From eb60a5828520b3bcf376a919528766ee9fbe9b1d Mon Sep 17 00:00:00 2001 From: Dan Rubel Date: Mon, 14 Nov 2016 14:21:30 -0500 Subject: [PATCH 043/121] Refactor flutter command exit code - part 3 of 3 (#6838) * Remove the workaround that pinned args to v0.13.6 This reverts most of the changes in commit 6331b6c8b5d964ec0dbf2cd9bb84c60c650a0878 * throw exception if exit code is not an integer * rework command infrastructure to throw ToolExit when non-zero exitCode * convert commands to return Future * cleanup remaining commands to use throwToolExit for non-zero exit code * remove isUnusual exception message * add type annotations for updated args package --- pubspec.yaml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 55f609492e2..fb171221188 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,12 +5,6 @@ author: Flutter Authors homepage: http://flutter.io dependencies: - - # version 0.13.6+1 does not return the value from the command - # causing flutter tools to always have exit code 0 - # see https://github.com/flutter/flutter/issues/6766 - args: 0.13.6 - flutter: sdk: flutter markdown: '^0.9.0' From b289cfb17689733eb4c96c4cde80275b4a5ad951 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Mon, 21 Nov 2016 23:16:43 -0800 Subject: [PATCH 044/121] Rename Flexible to Expanded and improve docs (#6978) This patch replaces uses of Flexible with Expanded where we're using FlexFit.tight. We still need to think of a better name for the FlexFit.loose variant. Also, improve the docs for Row, Column, Flex, and RenderFlex to be more problem-oriented and to give a complete account of the layout algorithn. Fixes #6960 Fixes #5169 --- lib/src/markdown_raw.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/markdown_raw.dart b/lib/src/markdown_raw.dart index 2672ce6cf90..b26a7eee1db 100644 --- a/lib/src/markdown_raw.dart +++ b/lib/src/markdown_raw.dart @@ -410,7 +410,7 @@ class _Block { width: listIndents.length * markdownStyle.listIndent, child: bullet ), - new Flexible(child: contents) + new Expanded(child: contents) ] ); } From bff22600ef0dcd9ba2709f57274a77f0b23bf0ec Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Sun, 27 Nov 2016 07:46:54 -0800 Subject: [PATCH 045/121] Update iml files (#7025) These now match what 2016.3 expects. --- flutter_markdown.iml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flutter_markdown.iml b/flutter_markdown.iml index cbcc56cb4bf..48dcb90f686 100644 --- a/flutter_markdown.iml +++ b/flutter_markdown.iml @@ -3,6 +3,8 @@ + + From 126dba5db80b0f375aa13c8729d58b255a235de5 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Sun, 5 Feb 2017 21:39:24 -0800 Subject: [PATCH 046/121] Use sliver-based scrolling in more places (#7892) This patch uses sliver-based two more gallery demos, the stocks example, in the date picker, and in markdown. --- lib/src/markdown.dart | 16 ++++++++-------- lib/src/markdown_raw.dart | 40 +++++++++++++++++++-------------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/lib/src/markdown.dart b/lib/src/markdown.dart index 3549ae47e08..40125106240 100644 --- a/lib/src/markdown.dart +++ b/lib/src/markdown.dart @@ -64,15 +64,15 @@ class MarkdownBody extends MarkdownBodyRaw { /// by default not using syntax highlighting, but it's possible to pass in /// a custom [syntaxHighlighter]. /// - /// Typically, you may want to wrap the [MarkdownBody] widget in a [Padding] and - /// a [ScrollableViewport], or use the [Markdown] class + /// Typically, you may want to wrap the [MarkdownBody] widget in a + /// [SingleChildScrollView], or use the [Markdown] class. /// - /// new ScrollableViewport( - /// child: new Padding( - /// padding: new EdgeInsets.all(16.0), - /// child: new Markdown(data: markdownSource) - /// ) - /// ) + /// ```dart + /// new SingleChildScrollView( + /// padding: new EdgeInsets.all(16.0), + /// child: new Markdown(data: markdownSource), + /// ), + /// ``` MarkdownBody({ String data, SyntaxHighlighter syntaxHighlighter, diff --git a/lib/src/markdown_raw.dart b/lib/src/markdown_raw.dart index b26a7eee1db..a00d48a8cb6 100644 --- a/lib/src/markdown_raw.dart +++ b/lib/src/markdown_raw.dart @@ -52,16 +52,16 @@ class MarkdownRaw extends StatelessWidget { @override Widget build(BuildContext context) { - return new ScrollableViewport( - child: new Padding( - padding: padding, - child: createMarkdownBody( - data: data, - markdownStyle: markdownStyle, - syntaxHighlighter: syntaxHighlighter, - onTapLink: onTapLink - ) - ) + // TODO(abarth): We should use a ListView here and lazily build the widgets + // from the markdown. + return new SingleChildScrollView( + padding: padding, + child: createMarkdownBody( + data: data, + markdownStyle: markdownStyle, + syntaxHighlighter: syntaxHighlighter, + onTapLink: onTapLink, + ), ); } @@ -93,17 +93,17 @@ class MarkdownBodyRaw extends StatefulWidget { /// highlighting, but it's possible to pass in a custom [syntaxHighlighter]. /// /// Typically, you may want to wrap the [MarkdownBodyRaw] widget in a - /// [Padding] and a [ScrollableViewport], or use the [Markdown class] + /// a [ScrollableViewport], or use the [Markdown class]. /// - /// new ScrollableViewport( - /// child: new Padding( - /// padding: new EdgeInsets.all(16.0), - /// child: new MarkdownBodyRaw( - /// data: markdownSource, - /// markdownStyle: myStyle - /// ) - /// ) - /// ) + /// ```dart + /// new SingleChildScrollView( + /// padding: new EdgeInsets.all(16.0), + /// child: new MarkdownBodyRaw( + /// data: markdownSource, + /// markdownStyle: myStyle, + /// ), + /// ), + /// ``` MarkdownBodyRaw({ this.data, this.markdownStyle, From 33c25e0281c18972cdce40796bb02b34b9faf71e Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Tue, 14 Feb 2017 08:57:44 -0800 Subject: [PATCH 047/121] Remove mentions of ScrollableViewport from flutter_markdown (#8132) Also, actually run the flutter_markdown tests. --- lib/src/markdown.dart | 13 ++++++---- lib/src/markdown_raw.dart | 15 ++++++----- test/flutter_markdown_test.dart | 44 ++++++++++++++++----------------- 3 files changed, 39 insertions(+), 33 deletions(-) diff --git a/lib/src/markdown.dart b/lib/src/markdown.dart index 40125106240..e9952325803 100644 --- a/lib/src/markdown.dart +++ b/lib/src/markdown.dart @@ -50,11 +50,14 @@ class Markdown extends MarkdownRaw { } } -/// A [Widget] that renders markdown formatted text. It supports all standard -/// markdowns from the original markdown specification found here: -/// https://daringfireball.net/projects/markdown/ This class doesn't implement -/// any scrolling behavior, if you want scrolling either wrap the widget in -/// a [ScrollableViewport] or use the [Markdown] widget. +/// A [Widget] that renders markdown formatted text. +/// +/// It supports all standard markdowns from the original markdown specification +/// found here: +/// +/// This class doesn't implement any scrolling behavior, if you want scrolling +/// either wrap the widget in a [SingleChildScrollView] or use the [Markdown] +/// widget. class MarkdownBody extends MarkdownBodyRaw { /// Creates a new Markdown [Widget] that renders the markdown formatted string diff --git a/lib/src/markdown_raw.dart b/lib/src/markdown_raw.dart index a00d48a8cb6..006714757db 100644 --- a/lib/src/markdown_raw.dart +++ b/lib/src/markdown_raw.dart @@ -80,11 +80,14 @@ class MarkdownRaw extends StatelessWidget { } } -/// A [Widget] that renders markdown formatted text. It supports all standard -/// markdowns from the original markdown specification found here: -/// https://daringfireball.net/projects/markdown/ This class doesn't implement -/// any scrolling behavior, if you want scrolling either wrap the widget in -/// a [ScrollableViewport] or use the [MarkdownRaw] widget. +/// A [Widget] that renders markdown formatted text. +/// +/// It supports all standard markdowns from the original markdown specification +/// found here: . +/// +/// This class doesn't implement any scrolling behavior, if you want scrolling +/// either wrap the widget in a [SingleChildScrollView] or use the [MarkdownRaw] +/// widget. class MarkdownBodyRaw extends StatefulWidget { /// Creates a new Markdown [Widget] that renders the markdown formatted string @@ -93,7 +96,7 @@ class MarkdownBodyRaw extends StatefulWidget { /// highlighting, but it's possible to pass in a custom [syntaxHighlighter]. /// /// Typically, you may want to wrap the [MarkdownBodyRaw] widget in a - /// a [ScrollableViewport], or use the [Markdown class]. + /// a [SingleChildScrollView], or use the [Markdown class]. /// /// ```dart /// new SingleChildScrollView( diff --git a/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart index c72fcd7ee64..0f5be57968e 100644 --- a/test/flutter_markdown_test.dart +++ b/test/flutter_markdown_test.dart @@ -9,31 +9,31 @@ import 'package:flutter/widgets.dart'; import 'package:flutter/material.dart'; void main() { - testWidgets('Simple string', (WidgetTester tester) { - tester.pumpWidget(new MarkdownBody(data: 'Hello')); + testWidgets('Simple string', (WidgetTester tester) async { + await tester.pumpWidget(new MarkdownBody(data: 'Hello')); Iterable widgets = tester.allWidgets; _expectWidgetTypes(widgets, [MarkdownBody, Column, Container, Padding, RichText]); _expectTextStrings(widgets, ['Hello']); }); - testWidgets('Header', (WidgetTester tester) { - tester.pumpWidget(new MarkdownBody(data: '# Header')); + testWidgets('Header', (WidgetTester tester) async { + await tester.pumpWidget(new MarkdownBody(data: '# Header')); Iterable widgets = tester.allWidgets; _expectWidgetTypes(widgets, [MarkdownBody, Column, Container, Padding, RichText]); _expectTextStrings(widgets, ['Header']); }); - testWidgets('Empty string', (WidgetTester tester) { - tester.pumpWidget(new MarkdownBody(data: '')); + testWidgets('Empty string', (WidgetTester tester) async { + await tester.pumpWidget(new MarkdownBody(data: '')); Iterable widgets = tester.allWidgets; _expectWidgetTypes(widgets, [MarkdownBody, Column]); }); - testWidgets('Ordered list', (WidgetTester tester) { - tester.pumpWidget(new MarkdownBody(data: '1. Item 1\n1. Item 2\n2. Item 3')); + testWidgets('Ordered list', (WidgetTester tester) async { + await tester.pumpWidget(new MarkdownBody(data: '1. Item 1\n1. Item 2\n2. Item 3')); Iterable widgets = tester.allWidgets; _expectTextStrings(widgets, [ @@ -46,8 +46,8 @@ void main() { ); }); - testWidgets('Unordered list', (WidgetTester tester) { - tester.pumpWidget(new MarkdownBody(data: '- Item 1\n- Item 2\n- Item 3')); + testWidgets('Unordered list', (WidgetTester tester) async { + await tester.pumpWidget(new MarkdownBody(data: '- Item 1\n- Item 2\n- Item 3')); Iterable widgets = tester.allWidgets; _expectTextStrings(widgets, [ @@ -60,13 +60,13 @@ void main() { ); }); - testWidgets('Scrollable wrapping', (WidgetTester tester) { - tester.pumpWidget(new Markdown(data: '')); + testWidgets('Scrollable wrapping', (WidgetTester tester) async { + await tester.pumpWidget(new Markdown(data: '')); List widgets = tester.allWidgets.toList(); _expectWidgetTypes(widgets.take(2), [ Markdown, - ScrollableViewport, + SingleChildScrollView, ]); _expectWidgetTypes(widgets.reversed.take(3).toList().reversed, [ Padding, @@ -75,8 +75,8 @@ void main() { ]); }); - testWidgets('Links', (WidgetTester tester) { - tester.pumpWidget(new Markdown(data: '[Link Text](href)')); + testWidgets('Links', (WidgetTester tester) async { + await tester.pumpWidget(new Markdown(data: '[Link Text](href)')); RichText textWidget = tester.allWidgets.firstWhere((Widget widget) => widget is RichText); TextSpan span = textWidget.text; @@ -84,29 +84,29 @@ void main() { expect(span.children[0].recognizer.runtimeType, equals(TapGestureRecognizer)); }); - testWidgets('Changing config - data', (WidgetTester tester) { - tester.pumpWidget(new Markdown(data: 'Data1')); + testWidgets('Changing config - data', (WidgetTester tester) async { + await tester.pumpWidget(new Markdown(data: 'Data1')); _expectTextStrings(tester.allWidgets, ['Data1']); String stateBefore = WidgetsBinding.instance.renderViewElement.toStringDeep(); - tester.pumpWidget(new Markdown(data: 'Data1')); + await tester.pumpWidget(new Markdown(data: 'Data1')); String stateAfter = WidgetsBinding.instance.renderViewElement.toStringDeep(); expect(stateBefore, equals(stateAfter)); - tester.pumpWidget(new Markdown(data: 'Data2')); + await tester.pumpWidget(new Markdown(data: 'Data2')); _expectTextStrings(tester.allWidgets, ['Data2']); }); - testWidgets('Changing config - style', (WidgetTester tester) { + testWidgets('Changing config - style', (WidgetTester tester) async { ThemeData theme = new ThemeData.light(); MarkdownStyle style1 = new MarkdownStyle.defaultFromTheme(theme); MarkdownStyle style2 = new MarkdownStyle.largeFromTheme(theme); - tester.pumpWidget(new Markdown(data: 'Test', markdownStyle: style1)); + await tester.pumpWidget(new Markdown(data: 'Test', markdownStyle: style1)); String stateBefore = WidgetsBinding.instance.renderViewElement.toStringDeep(); - tester.pumpWidget(new Markdown(data: 'Test', markdownStyle: style2)); + await tester.pumpWidget(new Markdown(data: 'Test', markdownStyle: style2)); String stateAfter = WidgetsBinding.instance.renderViewElement.toStringDeep(); expect(stateBefore, isNot(stateAfter)); }); From f9bfe359ccd8d0776fe939e431b6837d260fbfbf Mon Sep 17 00:00:00 2001 From: Alexandre Ardhuin Date: Mon, 20 Feb 2017 23:07:16 +0100 Subject: [PATCH 048/121] prefer const constructor (#8292) --- lib/src/markdown_raw.dart | 6 +++--- lib/src/markdown_style.dart | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/src/markdown_raw.dart b/lib/src/markdown_raw.dart index 006714757db..d980fa8dcb6 100644 --- a/lib/src/markdown_raw.dart +++ b/lib/src/markdown_raw.dart @@ -249,7 +249,7 @@ class _Renderer implements md.NodeVisitor { linkInfo = _linkHandler.createLinkInfo(element.attributes['href']); } - TextStyle style = _markdownStyle.styles[element.tag] ?? new TextStyle(); + TextStyle style = _markdownStyle.styles[element.tag] ?? const TextStyle(); List<_MarkdownNode> styleElement = <_MarkdownNode>[new _MarkdownNodeTextStyle(style, linkInfo)]; _currentBlock.stack.add(new _MarkdownNodeList(styleElement)); } @@ -338,7 +338,7 @@ class _Block { _Block(this.tag, this.attributes, this.markdownStyle, this.listIndents, this.blockPosition) { TextStyle style = markdownStyle.styles[tag]; if (style == null) - style = new TextStyle(color: const Color(0xffff0000)); + style = const TextStyle(color: const Color(0xffff0000)); stack = <_MarkdownNode>[new _MarkdownNodeList(<_MarkdownNode>[new _MarkdownNodeTextStyle(style)])]; subBlocks = <_Block>[]; @@ -398,7 +398,7 @@ class _Block { } else { bullet = new Padding( - padding: new EdgeInsets.only(right: 5.0), + padding: const EdgeInsets.only(right: 5.0), child: new Text( "${blockPosition + 1}.", textAlign: TextAlign.right diff --git a/lib/src/markdown_style.dart b/lib/src/markdown_style.dart index b89443618a3..8900ec5a520 100644 --- a/lib/src/markdown_style.dart +++ b/lib/src/markdown_style.dart @@ -25,8 +25,8 @@ class MarkdownStyle extends MarkdownStyleRaw{ h4: theme.textTheme.body2, h5: theme.textTheme.body2, h6: theme.textTheme.body2, - em: new TextStyle(fontStyle: FontStyle.italic), - strong: new TextStyle(fontWeight: FontWeight.bold), + em: const TextStyle(fontStyle: FontStyle.italic), + strong: const TextStyle(fontWeight: FontWeight.bold), blockquote: theme.textTheme.body1, blockSpacing: 8.0, listIndent: 32.0, @@ -59,8 +59,8 @@ class MarkdownStyle extends MarkdownStyleRaw{ h4: theme.textTheme.headline, h5: theme.textTheme.title, h6: theme.textTheme.subhead, - em: new TextStyle(fontStyle: FontStyle.italic), - strong: new TextStyle(fontWeight: FontWeight.bold), + em: const TextStyle(fontStyle: FontStyle.italic), + strong: const TextStyle(fontWeight: FontWeight.bold), blockquote: theme.textTheme.body1, blockSpacing: 8.0, listIndent: 32.0, From 3d5bb10ed3bbba826beec8a46e6ccf551b08c5b7 Mon Sep 17 00:00:00 2001 From: Alexandre Ardhuin Date: Tue, 21 Feb 2017 18:30:22 +0100 Subject: [PATCH 049/121] Replace @checked with covariant (#8300) Fixes #7734 --- lib/src/markdown_raw.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/markdown_raw.dart b/lib/src/markdown_raw.dart index d980fa8dcb6..3a460f0db7b 100644 --- a/lib/src/markdown_raw.dart +++ b/lib/src/markdown_raw.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'package:markdown/markdown.dart' as md; -import 'package:meta/meta.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter/gestures.dart'; @@ -67,7 +66,7 @@ class MarkdownRaw extends StatelessWidget { MarkdownBodyRaw createMarkdownBody({ String data, - @checked MarkdownStyleRaw markdownStyle, + covariant MarkdownStyleRaw markdownStyle, SyntaxHighlighter syntaxHighlighter, MarkdownLinkCallback onTapLink }) { From b382fc7ebf997568c4c388c988e319974de0e354 Mon Sep 17 00:00:00 2001 From: Mike Hoolehan Date: Mon, 27 Feb 2017 22:13:24 +0200 Subject: [PATCH 050/121] Ignore HTML in flutter_markdown content without error (#8420) * Gracefully ignore html content * Test for less than, consolidate table tests * Specify missing type annotation * Add Mike Hoolehan to AUTHORS --- lib/src/markdown_raw.dart | 15 +++++++++------ pubspec.yaml | 2 +- test/flutter_markdown_test.dart | 22 ++++++++++++++++++++++ 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/lib/src/markdown_raw.dart b/lib/src/markdown_raw.dart index 3a460f0db7b..fcdc50ce1a2 100644 --- a/lib/src/markdown_raw.dart +++ b/lib/src/markdown_raw.dart @@ -219,13 +219,16 @@ class _Renderer implements md.NodeVisitor { @override void visitText(md.Text text) { - _MarkdownNodeList topList = _currentBlock.stack.last; - List<_MarkdownNode> top = topList.list; + if (_currentBlock != null) { // ignore if no corresponding block + _MarkdownNodeList topList = _currentBlock.stack.last; + List<_MarkdownNode> top = topList.list; - if (_currentBlock.tag == 'pre') - top.add(new _MarkdownNodeTextSpan(_syntaxHighlighter.format(text.text))); - else - top.add(new _MarkdownNodeString(text.text)); + if (_currentBlock.tag == 'pre') + top.add( + new _MarkdownNodeTextSpan(_syntaxHighlighter.format(text.text))); + else + top.add(new _MarkdownNodeString(text.text)); + } } @override diff --git a/pubspec.yaml b/pubspec.yaml index fb171221188..117d7340236 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,7 +7,7 @@ homepage: http://flutter.io dependencies: flutter: sdk: flutter - markdown: '^0.9.0' + markdown: '^0.11.0' string_scanner: ^1.0.0 dev_dependencies: diff --git a/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart index 0f5be57968e..7552eef6e13 100644 --- a/test/flutter_markdown_test.dart +++ b/test/flutter_markdown_test.dart @@ -84,6 +84,28 @@ void main() { expect(span.children[0].recognizer.runtimeType, equals(TapGestureRecognizer)); }); + testWidgets('HTML tag ignored ', (WidgetTester tester) async { + final List mdData = [ + 'Line 1\n

HTML content

\nLine 2', + 'Line 1\n<\nLine 2' + ]; + + for (String mdLine in mdData) { + await tester.pumpWidget(new MarkdownBody(data: mdLine)); + + Iterable widgets = tester.allWidgets; + _expectTextStrings(widgets, ['Line 1', 'Line 2']); + } + }); + + testWidgets('Less than', (WidgetTester tester) async { + final String mdLine = 'Line 1 <\n\nc < c c\n\n< Line 2'; + await tester.pumpWidget(new MarkdownBody(data: mdLine)); + + Iterable widgets = tester.allWidgets; + _expectTextStrings(widgets, ['Line 1 <','c < c c','< Line 2']); + }); + testWidgets('Changing config - data', (WidgetTester tester) async { await tester.pumpWidget(new Markdown(data: 'Data1')); _expectTextStrings(tester.allWidgets, ['Data1']); From 870a6497e28609d386c28130d91b187438cf8ca1 Mon Sep 17 00:00:00 2001 From: Alexandre Ardhuin Date: Thu, 2 Mar 2017 07:17:30 +0100 Subject: [PATCH 051/121] prefer_is_empty and prefer_is_not_empty (#8474) --- lib/src/markdown_raw.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/src/markdown_raw.dart b/lib/src/markdown_raw.dart index fcdc50ce1a2..2708d301d76 100644 --- a/lib/src/markdown_raw.dart +++ b/lib/src/markdown_raw.dart @@ -264,7 +264,7 @@ class _Renderer implements md.NodeVisitor { _listIndents.removeLast(); if (_isBlockTag(element.tag)) { - if (_currentBlock.stack.length > 0) { + if (_currentBlock.stack.isNotEmpty) { _MarkdownNodeList stackList = _currentBlock.stack.first; _currentBlock.stack = stackList.list; _currentBlock.open = false; @@ -358,7 +358,7 @@ class _Block { bool get open => _open; set open(bool open) { _open = open; - if (!open && subBlocks.length > 0) + if (!open && subBlocks.isNotEmpty) subBlocks.last.isLast = true; } @@ -376,7 +376,7 @@ class _Block { Widget contents; - if (subBlocks.length > 0) { + if (subBlocks.isNotEmpty) { List subWidgets = []; for (_Block subBlock in subBlocks) { subWidgets.add(subBlock.build(context)); @@ -390,7 +390,7 @@ class _Block { TextSpan span = _stackToTextSpan(new _MarkdownNodeList(stack)); contents = new RichText(text: span); - if (listIndents.length > 0) { + if (listIndents.isNotEmpty) { Widget bullet; if (listIndents.last == 'ul') { bullet = new Text( @@ -479,7 +479,7 @@ class _Block { Widget _buildImage(BuildContext context, String src) { List parts = src.split('#'); - if (parts.length == 0) return new Container(); + if (parts.isEmpty) return new Container(); String path = parts.first; double width; From 1bbe4b63c9e1c76b2668980164b16958596a22a5 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Fri, 3 Mar 2017 17:51:10 -0800 Subject: [PATCH 052/121] Declare locals final where not reassigned (flutter_markdown) (#8568) --- lib/src/markdown_raw.dart | 58 ++++++++++++++++----------------- test/flutter_markdown_test.dart | 40 +++++++++++------------ 2 files changed, 49 insertions(+), 49 deletions(-) diff --git a/lib/src/markdown_raw.dart b/lib/src/markdown_raw.dart index 2708d301d76..fa0644d438e 100644 --- a/lib/src/markdown_raw.dart +++ b/lib/src/markdown_raw.dart @@ -157,17 +157,17 @@ class _MarkdownBodyRawState extends State { } void _buildMarkdownCache() { - MarkdownStyleRaw markdownStyle = config.markdownStyle ?? config.createDefaultStyle(context); - SyntaxHighlighter syntaxHighlighter = config.syntaxHighlighter ?? new _DefaultSyntaxHighlighter(markdownStyle.code); + final MarkdownStyleRaw markdownStyle = config.markdownStyle ?? config.createDefaultStyle(context); + final SyntaxHighlighter syntaxHighlighter = config.syntaxHighlighter ?? new _DefaultSyntaxHighlighter(markdownStyle.code); _linkHandler?.dispose(); _linkHandler = new _LinkHandler(config.onTapLink); // TODO: This can be optimized by doing the split and removing \r at the same time - List lines = config.data.replaceAll('\r\n', '\n').split('\n'); - md.Document document = new md.Document(); + final List lines = config.data.replaceAll('\r\n', '\n').split('\n'); + final md.Document document = new md.Document(); - _Renderer renderer = new _Renderer(); + final _Renderer renderer = new _Renderer(); _cachedBlocks = renderer.render(document.parseLines(lines), markdownStyle, syntaxHighlighter, _linkHandler); } @@ -176,7 +176,7 @@ class _MarkdownBodyRawState extends State { @override Widget build(BuildContext context) { - List blocks = []; + final List blocks = []; for (_Block block in _cachedBlocks) { blocks.add(block.build(context)); } @@ -220,8 +220,8 @@ class _Renderer implements md.NodeVisitor { @override void visitText(md.Text text) { if (_currentBlock != null) { // ignore if no corresponding block - _MarkdownNodeList topList = _currentBlock.stack.last; - List<_MarkdownNode> top = topList.list; + final _MarkdownNodeList topList = _currentBlock.stack.last; + final List<_MarkdownNode> top = topList.list; if (_currentBlock.tag == 'pre') top.add( @@ -243,7 +243,7 @@ class _Renderer implements md.NodeVisitor { else blockList = _currentBlock.subBlocks; - _Block newBlock = new _Block(element.tag, element.attributes, _markdownStyle, new List.from(_listIndents), blockList.length); + final _Block newBlock = new _Block(element.tag, element.attributes, _markdownStyle, new List.from(_listIndents), blockList.length); blockList.add(newBlock); } else { _LinkInfo linkInfo; @@ -251,8 +251,8 @@ class _Renderer implements md.NodeVisitor { linkInfo = _linkHandler.createLinkInfo(element.attributes['href']); } - TextStyle style = _markdownStyle.styles[element.tag] ?? const TextStyle(); - List<_MarkdownNode> styleElement = <_MarkdownNode>[new _MarkdownNodeTextStyle(style, linkInfo)]; + final TextStyle style = _markdownStyle.styles[element.tag] ?? const TextStyle(); + final List<_MarkdownNode> styleElement = <_MarkdownNode>[new _MarkdownNodeTextStyle(style, linkInfo)]; _currentBlock.stack.add(new _MarkdownNodeList(styleElement)); } return true; @@ -265,7 +265,7 @@ class _Renderer implements md.NodeVisitor { if (_isBlockTag(element.tag)) { if (_currentBlock.stack.isNotEmpty) { - _MarkdownNodeList stackList = _currentBlock.stack.first; + final _MarkdownNodeList stackList = _currentBlock.stack.first; _currentBlock.stack = stackList.list; _currentBlock.open = false; } else { @@ -273,12 +273,12 @@ class _Renderer implements md.NodeVisitor { } } else { if (_currentBlock.stack.length > 1) { - _MarkdownNodeList poppedList = _currentBlock.stack.last; - List<_MarkdownNode> popped = poppedList.list; + final _MarkdownNodeList poppedList = _currentBlock.stack.last; + final List<_MarkdownNode> popped = poppedList.list; _currentBlock.stack.removeLast(); - _MarkdownNodeList topList = _currentBlock.stack.last; - List<_MarkdownNode> top = topList.list; + final _MarkdownNodeList topList = _currentBlock.stack.last; + final List<_MarkdownNode> top = topList.list; top.add(new _MarkdownNodeList(popped)); } } @@ -304,7 +304,7 @@ class _Renderer implements md.NodeVisitor { if (!blocks.last.open) return null; - _Block childBlock = _currentBlockInList(blocks.last.subBlocks); + final _Block childBlock = _currentBlockInList(blocks.last.subBlocks); if (childBlock != null) return childBlock; @@ -377,7 +377,7 @@ class _Block { Widget contents; if (subBlocks.isNotEmpty) { - List subWidgets = []; + final List subWidgets = []; for (_Block subBlock in subBlocks) { subWidgets.add(subBlock.build(context)); } @@ -387,7 +387,7 @@ class _Block { children: subWidgets ); } else { - TextSpan span = _stackToTextSpan(new _MarkdownNodeList(stack)); + final TextSpan span = _stackToTextSpan(new _MarkdownNodeList(stack)); contents = new RichText(text: span); if (listIndents.isNotEmpty) { @@ -445,10 +445,10 @@ class _Block { return stack.textSpan; if (stack is _MarkdownNodeList) { - List<_MarkdownNode> list = stack.list; - _MarkdownNodeTextStyle styleNode = list[0]; - _LinkInfo linkInfo = styleNode.linkInfo; - TextStyle style = styleNode.style; + final List<_MarkdownNode> list = stack.list; + final _MarkdownNodeTextStyle styleNode = list[0]; + final _LinkInfo linkInfo = styleNode.linkInfo; + final TextStyle style = styleNode.style; List children = []; for (int i = 1; i < list.length; i++) { @@ -461,7 +461,7 @@ class _Block { children = null; } - TapGestureRecognizer recognizer = linkInfo?.recognizer; + final TapGestureRecognizer recognizer = linkInfo?.recognizer; return new TextSpan(style: style, children: children, recognizer: recognizer, text: text); } @@ -478,14 +478,14 @@ class _Block { } Widget _buildImage(BuildContext context, String src) { - List parts = src.split('#'); + final List parts = src.split('#'); if (parts.isEmpty) return new Container(); - String path = parts.first; + final String path = parts.first; double width; double height; if (parts.length == 2) { - List dimensions = parts.last.split('x'); + final List dimensions = parts.last.split('x'); if (dimensions.length == 2) { width = double.parse(dimensions[0]); height = double.parse(dimensions[1]); @@ -510,13 +510,13 @@ class _LinkHandler { MarkdownLinkCallback onTapLink; _LinkInfo createLinkInfo(String href) { - TapGestureRecognizer recognizer = new TapGestureRecognizer(); + final TapGestureRecognizer recognizer = new TapGestureRecognizer(); recognizer.onTap = () { if (onTapLink != null) onTapLink(href); }; - _LinkInfo linkInfo = new _LinkInfo(href, recognizer); + final _LinkInfo linkInfo = new _LinkInfo(href, recognizer); links.add(linkInfo); return linkInfo; diff --git a/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart index 7552eef6e13..af2877ca59d 100644 --- a/test/flutter_markdown_test.dart +++ b/test/flutter_markdown_test.dart @@ -12,7 +12,7 @@ void main() { testWidgets('Simple string', (WidgetTester tester) async { await tester.pumpWidget(new MarkdownBody(data: 'Hello')); - Iterable widgets = tester.allWidgets; + final Iterable widgets = tester.allWidgets; _expectWidgetTypes(widgets, [MarkdownBody, Column, Container, Padding, RichText]); _expectTextStrings(widgets, ['Hello']); }); @@ -20,7 +20,7 @@ void main() { testWidgets('Header', (WidgetTester tester) async { await tester.pumpWidget(new MarkdownBody(data: '# Header')); - Iterable widgets = tester.allWidgets; + final Iterable widgets = tester.allWidgets; _expectWidgetTypes(widgets, [MarkdownBody, Column, Container, Padding, RichText]); _expectTextStrings(widgets, ['Header']); }); @@ -28,14 +28,14 @@ void main() { testWidgets('Empty string', (WidgetTester tester) async { await tester.pumpWidget(new MarkdownBody(data: '')); - Iterable widgets = tester.allWidgets; + final Iterable widgets = tester.allWidgets; _expectWidgetTypes(widgets, [MarkdownBody, Column]); }); testWidgets('Ordered list', (WidgetTester tester) async { await tester.pumpWidget(new MarkdownBody(data: '1. Item 1\n1. Item 2\n2. Item 3')); - Iterable widgets = tester.allWidgets; + final Iterable widgets = tester.allWidgets; _expectTextStrings(widgets, [ '1.', 'Item 1', @@ -49,7 +49,7 @@ void main() { testWidgets('Unordered list', (WidgetTester tester) async { await tester.pumpWidget(new MarkdownBody(data: '- Item 1\n- Item 2\n- Item 3')); - Iterable widgets = tester.allWidgets; + final Iterable widgets = tester.allWidgets; _expectTextStrings(widgets, [ '•', 'Item 1', @@ -63,7 +63,7 @@ void main() { testWidgets('Scrollable wrapping', (WidgetTester tester) async { await tester.pumpWidget(new Markdown(data: '')); - List widgets = tester.allWidgets.toList(); + final List widgets = tester.allWidgets.toList(); _expectWidgetTypes(widgets.take(2), [ Markdown, SingleChildScrollView, @@ -78,8 +78,8 @@ void main() { testWidgets('Links', (WidgetTester tester) async { await tester.pumpWidget(new Markdown(data: '[Link Text](href)')); - RichText textWidget = tester.allWidgets.firstWhere((Widget widget) => widget is RichText); - TextSpan span = textWidget.text; + final RichText textWidget = tester.allWidgets.firstWhere((Widget widget) => widget is RichText); + final TextSpan span = textWidget.text; expect(span.children[0].recognizer.runtimeType, equals(TapGestureRecognizer)); }); @@ -93,7 +93,7 @@ void main() { for (String mdLine in mdData) { await tester.pumpWidget(new MarkdownBody(data: mdLine)); - Iterable widgets = tester.allWidgets; + final Iterable widgets = tester.allWidgets; _expectTextStrings(widgets, ['Line 1', 'Line 2']); } }); @@ -102,7 +102,7 @@ void main() { final String mdLine = 'Line 1 <\n\nc < c c\n\n< Line 2'; await tester.pumpWidget(new MarkdownBody(data: mdLine)); - Iterable widgets = tester.allWidgets; + final Iterable widgets = tester.allWidgets; _expectTextStrings(widgets, ['Line 1 <','c < c c','< Line 2']); }); @@ -110,9 +110,9 @@ void main() { await tester.pumpWidget(new Markdown(data: 'Data1')); _expectTextStrings(tester.allWidgets, ['Data1']); - String stateBefore = WidgetsBinding.instance.renderViewElement.toStringDeep(); + final String stateBefore = WidgetsBinding.instance.renderViewElement.toStringDeep(); await tester.pumpWidget(new Markdown(data: 'Data1')); - String stateAfter = WidgetsBinding.instance.renderViewElement.toStringDeep(); + final String stateAfter = WidgetsBinding.instance.renderViewElement.toStringDeep(); expect(stateBefore, equals(stateAfter)); await tester.pumpWidget(new Markdown(data: 'Data2')); @@ -120,22 +120,22 @@ void main() { }); testWidgets('Changing config - style', (WidgetTester tester) async { - ThemeData theme = new ThemeData.light(); + final ThemeData theme = new ThemeData.light(); - MarkdownStyle style1 = new MarkdownStyle.defaultFromTheme(theme); - MarkdownStyle style2 = new MarkdownStyle.largeFromTheme(theme); + final MarkdownStyle style1 = new MarkdownStyle.defaultFromTheme(theme); + final MarkdownStyle style2 = new MarkdownStyle.largeFromTheme(theme); await tester.pumpWidget(new Markdown(data: 'Test', markdownStyle: style1)); - String stateBefore = WidgetsBinding.instance.renderViewElement.toStringDeep(); + final String stateBefore = WidgetsBinding.instance.renderViewElement.toStringDeep(); await tester.pumpWidget(new Markdown(data: 'Test', markdownStyle: style2)); - String stateAfter = WidgetsBinding.instance.renderViewElement.toStringDeep(); + final String stateAfter = WidgetsBinding.instance.renderViewElement.toStringDeep(); expect(stateBefore, isNot(stateAfter)); }); } void _expectWidgetTypes(Iterable widgets, List expected) { - List actual = widgets.map((Widget w) => w.runtimeType).toList(); + final List actual = widgets.map((Widget w) => w.runtimeType).toList(); expect(actual, expected); } @@ -143,8 +143,8 @@ void _expectTextStrings(Iterable widgets, List strings) { int currentString = 0; for (Widget widget in widgets) { if (widget is RichText) { - TextSpan span = widget.text; - String text = _extractTextFromTextSpan(span); + final TextSpan span = widget.text; + final String text = _extractTextFromTextSpan(span); expect(text, equals(strings[currentString])); currentString += 1; } From 2fe675447ef8ea4c34fdde70608b5f20a4821595 Mon Sep 17 00:00:00 2001 From: Alexandre Ardhuin Date: Fri, 10 Mar 2017 09:00:29 +0100 Subject: [PATCH 053/121] Flutter style guide: Prefer naming the argument to a setter 'value' (#8691) --- lib/src/markdown_raw.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/markdown_raw.dart b/lib/src/markdown_raw.dart index fa0644d438e..efea65ee8aa 100644 --- a/lib/src/markdown_raw.dart +++ b/lib/src/markdown_raw.dart @@ -356,9 +356,9 @@ class _Block { List<_Block> subBlocks; bool get open => _open; - set open(bool open) { - _open = open; - if (!open && subBlocks.isNotEmpty) + set open(bool value) { + _open = value; + if (!value && subBlocks.isNotEmpty) subBlocks.last.isLast = true; } From c45555839774d53eccb56527f15b14ae4d3192e2 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Tue, 14 Mar 2017 11:21:53 -0700 Subject: [PATCH 054/121] Rename dependenciesChanged to didChangeDependencies (#8767) The new name matches the style guide. (The old name was just old and predated the style guide.) Fixes #8000 --- lib/src/markdown_raw.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/markdown_raw.dart b/lib/src/markdown_raw.dart index efea65ee8aa..a3cadd3d2f6 100644 --- a/lib/src/markdown_raw.dart +++ b/lib/src/markdown_raw.dart @@ -134,9 +134,9 @@ class MarkdownBodyRaw extends StatefulWidget { class _MarkdownBodyRawState extends State { @override - void dependenciesChanged() { + void didChangeDependencies() { _buildMarkdownCache(); - super.dependenciesChanged(); + super.didChangeDependencies(); } @override From bff629d03ff51f44b93c9e3d756023468da4cb7a Mon Sep 17 00:00:00 2001 From: Alexandre Ardhuin Date: Tue, 21 Mar 2017 23:14:55 +0100 Subject: [PATCH 055/121] use color.shadeXxx instead of color[Xxx] (#8932) * use color.shadeXxx instead of color[Xxx] * remove calls to .shade500 on MaterialColor * remove calls to .shade200 on MaterialAccentColor * fix test --- lib/src/markdown_style.dart | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/src/markdown_style.dart b/lib/src/markdown_style.dart index 8900ec5a520..ab66fafb4c9 100644 --- a/lib/src/markdown_style.dart +++ b/lib/src/markdown_style.dart @@ -12,10 +12,10 @@ class MarkdownStyle extends MarkdownStyleRaw{ /// Creates a [MarkdownStyle] from the [TextStyle]s in the provided [theme]. MarkdownStyle.defaultFromTheme(ThemeData theme) : super( - a: new TextStyle(color: Colors.blue[500]), + a: new TextStyle(color: Colors.blue), p: theme.textTheme.body1, code: new TextStyle( - color: Colors.grey[700], + color: Colors.grey.shade700, fontFamily: "monospace", fontSize: theme.textTheme.body1.fontSize * 0.85 ), @@ -32,12 +32,12 @@ class MarkdownStyle extends MarkdownStyleRaw{ listIndent: 32.0, blockquotePadding: 8.0, blockquoteDecoration: new BoxDecoration( - backgroundColor: Colors.blue[100], + backgroundColor: Colors.blue.shade100, borderRadius: new BorderRadius.circular(2.0) ), codeblockPadding: 8.0, codeblockDecoration: new BoxDecoration( - backgroundColor: Colors.grey[100], + backgroundColor: Colors.grey.shade100, borderRadius: new BorderRadius.circular(2.0) ) ); @@ -46,10 +46,10 @@ class MarkdownStyle extends MarkdownStyleRaw{ /// This style uses larger fonts for the headings than in /// [MarkdownStyle.defaultFromTheme]. MarkdownStyle.largeFromTheme(ThemeData theme) : super ( - a: new TextStyle(color: Colors.blue[500]), + a: new TextStyle(color: Colors.blue), p: theme.textTheme.body1, code: new TextStyle( - color: Colors.grey[700], + color: Colors.grey.shade700, fontFamily: "monospace", fontSize: theme.textTheme.body1.fontSize * 0.85 ), @@ -66,12 +66,12 @@ class MarkdownStyle extends MarkdownStyleRaw{ listIndent: 32.0, blockquotePadding: 8.0, blockquoteDecoration: new BoxDecoration( - backgroundColor: Colors.blue[100], + backgroundColor: Colors.blue.shade100, borderRadius: new BorderRadius.circular(2.0) ), codeblockPadding: 8.0, codeblockDecoration: new BoxDecoration( - backgroundColor: Colors.grey[100], + backgroundColor: Colors.grey.shade100, borderRadius: new BorderRadius.circular(2.0) ) ); From 56506e878363da644ea3411ca52e4604e8d78bf8 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Mon, 27 Mar 2017 20:47:33 -0700 Subject: [PATCH 056/121] Update and modernize the flutter_markdown package (#9044) We now use modern scrolling machinery and patterns. The API should also be easier to maintain over time. Fixes #6166 Fixes #2591 Fixes #3123 --- lib/flutter_markdown.dart | 5 +- lib/flutter_markdown_raw.dart | 8 - lib/src/builder.dart | 243 ++++++++++++++ lib/src/markdown.dart | 95 ------ lib/src/markdown_raw.dart | 545 -------------------------------- lib/src/markdown_style.dart | 78 ----- lib/src/markdown_style_raw.dart | 121 ------- lib/src/style_sheet.dart | 269 ++++++++++++++++ lib/src/widget.dart | 211 +++++++++++++ test/flutter_markdown_test.dart | 157 ++++----- 10 files changed, 812 insertions(+), 920 deletions(-) delete mode 100644 lib/flutter_markdown_raw.dart create mode 100644 lib/src/builder.dart delete mode 100644 lib/src/markdown.dart delete mode 100644 lib/src/markdown_raw.dart delete mode 100644 lib/src/markdown_style.dart delete mode 100644 lib/src/markdown_style_raw.dart create mode 100644 lib/src/style_sheet.dart create mode 100644 lib/src/widget.dart diff --git a/lib/flutter_markdown.dart b/lib/flutter_markdown.dart index d65c9a5508b..8d7ed6ea082 100644 --- a/lib/flutter_markdown.dart +++ b/lib/flutter_markdown.dart @@ -5,5 +5,6 @@ /// A library to render markdown formatted text. library flutter_markdown; -export 'src/markdown.dart'; -export 'src/markdown_style.dart'; +export 'src/builder.dart'; +export 'src/style_sheet.dart'; +export 'src/widget.dart'; diff --git a/lib/flutter_markdown_raw.dart b/lib/flutter_markdown_raw.dart deleted file mode 100644 index 3d2be6a5f0b..00000000000 --- a/lib/flutter_markdown_raw.dart +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2016 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -library flutter_markdown_raw; - -export 'src/markdown_raw.dart'; -export 'src/markdown_style_raw.dart'; diff --git a/lib/src/builder.dart b/lib/src/builder.dart new file mode 100644 index 00000000000..16219639613 --- /dev/null +++ b/lib/src/builder.dart @@ -0,0 +1,243 @@ +// Copyright 2016 The Chromium 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:markdown/markdown.dart' as md; + +import 'style_sheet.dart'; + +final Set _kBlockTags = new Set.from([ + 'p', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'li', + 'blockquote', + 'img', + 'pre', + 'ol', + 'ul', +]); + +const List _kListTags = const ['ul', 'ol']; + +bool _isBlockTag(String tag) => _kBlockTags.contains(tag); +bool _isListTag(String tag) => _kListTags.contains(tag); + +class _BlockElement { + _BlockElement(this.tag); + + final String tag; + final List children = []; + + int nextListIndex = 0; +} + +class _InlineElement { + final List children = []; +} + +/// A delegate used by [MarkdownBuilder] to control the widgets it creates. +abstract class MarkdownBuilderDelegate { + /// Returns a gesture recognizer to use for an `a` element with the given + /// `href` attribute. + GestureRecognizer createLink(String href); + + /// Returns formatted text to use to display the given contents of a `pre` + /// element. + /// + /// The `styleSheet` is the value of [MarkdownBuilder.styleSheet]. + TextSpan formatText(MarkdownStyleSheet styleSheet, String code); +} + +/// Builds a [Widget] tree from parsed Markdown. +/// +/// See also: +/// +/// * [Markdown], which is a widget that parses and displays Markdown. +class MarkdownBuilder implements md.NodeVisitor { + /// Creates an object that builds a [Widget] tree from parsed Markdown. + MarkdownBuilder({ this.delegate, this.styleSheet }); + + /// A delegate that controls how link and `pre` elements behave. + final MarkdownBuilderDelegate delegate; + + /// Defines which [TextStyle] objects to use for each type of element. + final MarkdownStyleSheet styleSheet; + + final List _listIndents = []; + final List<_BlockElement> _blocks = <_BlockElement>[]; + final List<_InlineElement> _inlines = <_InlineElement>[]; + + /// Returns widgets that display the given Markdown nodes. + /// + /// The returned widgets are typically used as children in a [ListView]. + List build(List nodes) { + _listIndents.clear(); + _blocks.clear(); + _inlines.clear(); + + _blocks.add(new _BlockElement(null)); + _inlines.add(new _InlineElement()); + + for (md.Node node in nodes) { + assert(_blocks.length == 1); + node.accept(this); + } + + assert(_inlines.single.children.isEmpty); + return _blocks.single.children; + } + + @override + 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); + _inlines.last.children.add(span); + } + + @override + bool visitElementBefore(md.Element element) { + final String tag = element.tag; + if (_isBlockTag(tag)) { + _addAnonymousBlockIfNeeded(styleSheet.styles[tag]); + if (_isListTag(tag)) + _listIndents.add(tag); + _blocks.add(new _BlockElement(tag)); + } else { + _inlines.add(new _InlineElement()); + } + return true; + } + + @override + void visitElementAfter(md.Element element) { + final String tag = element.tag; + + if (_isBlockTag(tag)) { + _addAnonymousBlockIfNeeded(styleSheet.styles[tag]); + + final _BlockElement current = _blocks.removeLast(); + Widget child; + if (tag == 'img') { + child = _buildImage(element.attributes['src']); + } else { + if (current.children.isNotEmpty) { + child = new Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: current.children, + ); + } else { + child = const SizedBox(); + } + + if (_isListTag(tag)) { + assert(_listIndents.isNotEmpty); + _listIndents.removeLast(); + } else if (tag == 'li') { + if (_listIndents.isNotEmpty) { + child = new Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + new SizedBox( + width: styleSheet.listIndent, + child: _buildBullet(_listIndents.last), + ), + new Expanded(child: child) + ], + ); + } + } else if (tag == 'blockquote') { + child = new DecoratedBox( + decoration: styleSheet.blockquoteDecoration, + child: new Padding( + padding: new EdgeInsets.all(styleSheet.blockquotePadding), + child: child, + ), + ); + } else if (tag == 'pre') { + child = new DecoratedBox( + decoration: styleSheet.codeblockDecoration, + child: new Padding( + padding: new EdgeInsets.all(styleSheet.codeblockPadding), + child: child, + ), + ); + } + } + + _addBlockChild(child); + } else { + final _InlineElement current = _inlines.removeLast(); + 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, + )); + } + } + } + + Widget _buildImage(String src) { + final List parts = src.split('#'); + if (parts.isEmpty) + return const SizedBox(); + + final String path = parts.first; + double width; + double height; + if (parts.length == 2) { + final List dimensions = parts.last.split('x'); + if (dimensions.length == 2) { + width = double.parse(dimensions[0]); + height = double.parse(dimensions[1]); + } + } + + return new Image.network(path, width: width, height: height); + } + + Widget _buildBullet(String listTag) { + if (listTag == 'ul') + return new Text('•', textAlign: TextAlign.center); + + final int index = _blocks.last.nextListIndex; + return new Padding( + padding: const EdgeInsets.only(right: 5.0), + child: new Text('${index + 1}.', textAlign: TextAlign.right), + ); + } + + void _addBlockChild(Widget child) { + final _BlockElement parent = _blocks.last; + if (parent.children.isNotEmpty) + parent.children.add(new SizedBox(height: styleSheet.blockSpacing)); + parent.children.add(child); + parent.nextListIndex += 1; + } + + void _addAnonymousBlockIfNeeded(TextStyle style) { + final _InlineElement inline = _inlines.single; + if (inline.children.isNotEmpty) { + final TextSpan span = new TextSpan(style: style, children: inline.children); + _addBlockChild(new RichText(text: span)); + _inlines.clear(); + _inlines.add(new _InlineElement()); + } + } +} diff --git a/lib/src/markdown.dart b/lib/src/markdown.dart deleted file mode 100644 index e9952325803..00000000000 --- a/lib/src/markdown.dart +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2016 The Chromium 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/material.dart'; - -import 'markdown_raw.dart'; -import 'markdown_style.dart'; - -/// A [Widget] that renders markdown formatted text. It supports all standard -/// markdowns from the original markdown specification found here: -/// https://daringfireball.net/projects/markdown/ The rendered markdown is -/// placed in a padded scrolling view port. If you do not want the scrolling -/// behaviour, use the [MarkdownBody] class instead. -class Markdown extends MarkdownRaw { - - /// Creates a new Markdown [Widget] that renders the markdown formatted string - /// passed in as [data]. By default the markdown will be rendered using the - /// styles from the current theme, but you can optionally pass in a custom - /// [markdownStyle] that specifies colors and fonts to use. Code blocks are - /// by default not using syntax highlighting, but it's possible to pass in - /// a custom [syntaxHighlighter]. - /// - /// new Markdown(data: "Hello _world_!"); - Markdown({ - String data, - SyntaxHighlighter syntaxHighlighter, - MarkdownStyle markdownStyle, - MarkdownLinkCallback onTapLink - }) : super( - data: data, - syntaxHighlighter: syntaxHighlighter, - markdownStyle: markdownStyle, - onTapLink: onTapLink - ); - - @override - MarkdownBody createMarkdownBody({ - String data, - MarkdownStyle markdownStyle, - SyntaxHighlighter syntaxHighlighter, - MarkdownLinkCallback onTapLink - }) { - return new MarkdownBody( - data: data, - markdownStyle: markdownStyle, - syntaxHighlighter: syntaxHighlighter, - onTapLink: onTapLink - ); - } -} - -/// A [Widget] that renders markdown formatted text. -/// -/// It supports all standard markdowns from the original markdown specification -/// found here: -/// -/// This class doesn't implement any scrolling behavior, if you want scrolling -/// either wrap the widget in a [SingleChildScrollView] or use the [Markdown] -/// widget. -class MarkdownBody extends MarkdownBodyRaw { - - /// Creates a new Markdown [Widget] that renders the markdown formatted string - /// passed in as [data]. By default the markdown will be rendered using the - /// styles from the current theme, but you can optionally pass in a custom - /// [markdownStyle] that specifies colors and fonts to use. Code blocks are - /// by default not using syntax highlighting, but it's possible to pass in - /// a custom [syntaxHighlighter]. - /// - /// Typically, you may want to wrap the [MarkdownBody] widget in a - /// [SingleChildScrollView], or use the [Markdown] class. - /// - /// ```dart - /// new SingleChildScrollView( - /// padding: new EdgeInsets.all(16.0), - /// child: new Markdown(data: markdownSource), - /// ), - /// ``` - MarkdownBody({ - String data, - SyntaxHighlighter syntaxHighlighter, - MarkdownStyle markdownStyle, - MarkdownLinkCallback onTapLink - }) : super( - data: data, - syntaxHighlighter: syntaxHighlighter, - markdownStyle: markdownStyle, - onTapLink: onTapLink - ); - - @override - MarkdownStyle createDefaultStyle(BuildContext context) { - return new MarkdownStyle.defaultFromTheme(Theme.of(context)); - } -} diff --git a/lib/src/markdown_raw.dart b/lib/src/markdown_raw.dart deleted file mode 100644 index a3cadd3d2f6..00000000000 --- a/lib/src/markdown_raw.dart +++ /dev/null @@ -1,545 +0,0 @@ -// Copyright 2016 The Chromium 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:markdown/markdown.dart' as md; -import 'package:flutter/widgets.dart'; -import 'package:flutter/gestures.dart'; - -import 'markdown_style_raw.dart'; - -typedef void MarkdownLinkCallback(String href); - - -/// A [Widget] that renders markdown formatted text. It supports all standard -/// markdowns from the original markdown specification found here: -/// https://daringfireball.net/projects/markdown/ The rendered markdown is -/// placed in a padded scrolling view port. If you do not want the scrolling -/// behaviour, use the [MarkdownBodyRaw] class instead. -class MarkdownRaw extends StatelessWidget { - - /// Creates a new Markdown [Widget] that renders the markdown formatted string - /// passed in as [data]. By default the markdown will be rendered using the - /// styles from the current theme, but you can optionally pass in a custom - /// [markdownStyle] that specifies colors and fonts to use. Code blocks are - /// by default not using syntax highlighting, but it's possible to pass in - /// a custom [syntaxHighlighter]. - /// - /// new MarkdownRaw(data: "Hello _world_!", markdownStyle: myStyle); - MarkdownRaw({ - this.data, - this.markdownStyle, - this.syntaxHighlighter, - this.padding: const EdgeInsets.all(16.0), - this.onTapLink - }); - - /// Markdown styled text - final String data; - - /// Style used for rendering the markdown - final MarkdownStyleRaw markdownStyle; - - /// The syntax highlighter used to color text in code blocks - final SyntaxHighlighter syntaxHighlighter; - - /// Padding used - final EdgeInsets padding; - - /// Callback when a link is tapped - final MarkdownLinkCallback onTapLink; - - @override - Widget build(BuildContext context) { - // TODO(abarth): We should use a ListView here and lazily build the widgets - // from the markdown. - return new SingleChildScrollView( - padding: padding, - child: createMarkdownBody( - data: data, - markdownStyle: markdownStyle, - syntaxHighlighter: syntaxHighlighter, - onTapLink: onTapLink, - ), - ); - } - - MarkdownBodyRaw createMarkdownBody({ - String data, - covariant MarkdownStyleRaw markdownStyle, - SyntaxHighlighter syntaxHighlighter, - MarkdownLinkCallback onTapLink - }) { - return new MarkdownBodyRaw( - data: data, - markdownStyle: markdownStyle, - syntaxHighlighter: syntaxHighlighter, - onTapLink: onTapLink - ); - } -} - -/// A [Widget] that renders markdown formatted text. -/// -/// It supports all standard markdowns from the original markdown specification -/// found here: . -/// -/// This class doesn't implement any scrolling behavior, if you want scrolling -/// either wrap the widget in a [SingleChildScrollView] or use the [MarkdownRaw] -/// widget. -class MarkdownBodyRaw extends StatefulWidget { - - /// Creates a new Markdown [Widget] that renders the markdown formatted string - /// passed in as [data]. You need to pass in a [markdownStyle] that defines - /// how the code is rendered. Code blocks are by default not using syntax - /// highlighting, but it's possible to pass in a custom [syntaxHighlighter]. - /// - /// Typically, you may want to wrap the [MarkdownBodyRaw] widget in a - /// a [SingleChildScrollView], or use the [Markdown class]. - /// - /// ```dart - /// new SingleChildScrollView( - /// padding: new EdgeInsets.all(16.0), - /// child: new MarkdownBodyRaw( - /// data: markdownSource, - /// markdownStyle: myStyle, - /// ), - /// ), - /// ``` - MarkdownBodyRaw({ - this.data, - this.markdownStyle, - this.syntaxHighlighter, - this.onTapLink - }); - - /// Markdown styled text - final String data; - - /// Style used for rendering the markdown - final MarkdownStyleRaw markdownStyle; - - /// The syntax highlighter used to color text in code blocks - final SyntaxHighlighter syntaxHighlighter; - - /// Callback when a link is tapped - final MarkdownLinkCallback onTapLink; - - @override - _MarkdownBodyRawState createState() => new _MarkdownBodyRawState(); - - MarkdownStyleRaw createDefaultStyle(BuildContext context) => null; -} - -class _MarkdownBodyRawState extends State { - - @override - void didChangeDependencies() { - _buildMarkdownCache(); - super.didChangeDependencies(); - } - - @override - void dispose() { - _linkHandler.dispose(); - super.dispose(); - } - - @override - void didUpdateConfig(MarkdownBodyRaw oldConfig) { - super.didUpdateConfig(oldConfig); - - if (oldConfig.data != config.data || - oldConfig.markdownStyle != config.markdownStyle || - oldConfig.syntaxHighlighter != config.syntaxHighlighter || - oldConfig.onTapLink != config.onTapLink) - _buildMarkdownCache(); - } - - void _buildMarkdownCache() { - final MarkdownStyleRaw markdownStyle = config.markdownStyle ?? config.createDefaultStyle(context); - final SyntaxHighlighter syntaxHighlighter = config.syntaxHighlighter ?? new _DefaultSyntaxHighlighter(markdownStyle.code); - - _linkHandler?.dispose(); - _linkHandler = new _LinkHandler(config.onTapLink); - - // TODO: This can be optimized by doing the split and removing \r at the same time - final List lines = config.data.replaceAll('\r\n', '\n').split('\n'); - final md.Document document = new md.Document(); - - final _Renderer renderer = new _Renderer(); - _cachedBlocks = renderer.render(document.parseLines(lines), markdownStyle, syntaxHighlighter, _linkHandler); - } - - List<_Block> _cachedBlocks; - _LinkHandler _linkHandler; - - @override - Widget build(BuildContext context) { - final List blocks = []; - for (_Block block in _cachedBlocks) { - blocks.add(block.build(context)); - } - - return new Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: blocks - ); - } - - @override - void debugFillDescription(List description) { - super.debugFillDescription(description); - description.add('cached blocks identity: ${_cachedBlocks.hashCode}'); - } -} - -class _Renderer implements md.NodeVisitor { - List<_Block> render(List nodes, MarkdownStyleRaw markdownStyle, SyntaxHighlighter syntaxHighlighter, _LinkHandler linkHandler) { - assert(markdownStyle != null); - - _blocks = <_Block>[]; - _listIndents = []; - _markdownStyle = markdownStyle; - _syntaxHighlighter = syntaxHighlighter; - _linkHandler = linkHandler; - - for (final md.Node node in nodes) { - node.accept(this); - } - - return _blocks; - } - - List<_Block> _blocks; - List _listIndents; - MarkdownStyleRaw _markdownStyle; - SyntaxHighlighter _syntaxHighlighter; - _LinkHandler _linkHandler; - - @override - void visitText(md.Text text) { - if (_currentBlock != null) { // ignore if no corresponding block - final _MarkdownNodeList topList = _currentBlock.stack.last; - final List<_MarkdownNode> top = topList.list; - - if (_currentBlock.tag == 'pre') - top.add( - new _MarkdownNodeTextSpan(_syntaxHighlighter.format(text.text))); - else - top.add(new _MarkdownNodeString(text.text)); - } - } - - @override - bool visitElementBefore(md.Element element) { - if (_isListTag(element.tag)) - _listIndents.add(element.tag); - - if (_isBlockTag(element.tag)) { - List<_Block> blockList; - if (_currentBlock == null) - blockList = _blocks; - else - blockList = _currentBlock.subBlocks; - - final _Block newBlock = new _Block(element.tag, element.attributes, _markdownStyle, new List.from(_listIndents), blockList.length); - blockList.add(newBlock); - } else { - _LinkInfo linkInfo; - if (element.tag == 'a') { - linkInfo = _linkHandler.createLinkInfo(element.attributes['href']); - } - - final TextStyle style = _markdownStyle.styles[element.tag] ?? const TextStyle(); - final List<_MarkdownNode> styleElement = <_MarkdownNode>[new _MarkdownNodeTextStyle(style, linkInfo)]; - _currentBlock.stack.add(new _MarkdownNodeList(styleElement)); - } - return true; - } - - @override - void visitElementAfter(md.Element element) { - if (_isListTag(element.tag)) - _listIndents.removeLast(); - - if (_isBlockTag(element.tag)) { - if (_currentBlock.stack.isNotEmpty) { - final _MarkdownNodeList stackList = _currentBlock.stack.first; - _currentBlock.stack = stackList.list; - _currentBlock.open = false; - } else { - _currentBlock.stack = <_MarkdownNode>[new _MarkdownNodeString('')]; - } - } else { - if (_currentBlock.stack.length > 1) { - final _MarkdownNodeList poppedList = _currentBlock.stack.last; - final List<_MarkdownNode> popped = poppedList.list; - _currentBlock.stack.removeLast(); - - final _MarkdownNodeList topList = _currentBlock.stack.last; - final List<_MarkdownNode> top = topList.list; - top.add(new _MarkdownNodeList(popped)); - } - } - } - - static const List _kBlockTags = const ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'blockquote', 'img', 'pre', 'ol', 'ul']; - static const List _kListTags = const ['ul', 'ol']; - - bool _isBlockTag(String tag) { - return _kBlockTags.contains(tag); - } - - bool _isListTag(String tag) { - return _kListTags.contains(tag); - } - - _Block get _currentBlock => _currentBlockInList(_blocks); - - _Block _currentBlockInList(List<_Block> blocks) { - if (blocks.isEmpty) - return null; - - if (!blocks.last.open) - return null; - - final _Block childBlock = _currentBlockInList(blocks.last.subBlocks); - if (childBlock != null) - return childBlock; - - return blocks.last; - } -} - -abstract class _MarkdownNode { -} - -class _MarkdownNodeList extends _MarkdownNode { - _MarkdownNodeList(this.list); - List<_MarkdownNode> list; -} - -class _MarkdownNodeTextStyle extends _MarkdownNode { - _MarkdownNodeTextStyle(this.style, [this.linkInfo = null]); - TextStyle style; - _LinkInfo linkInfo; -} - -class _MarkdownNodeString extends _MarkdownNode { - _MarkdownNodeString(this.string); - String string; -} - -class _MarkdownNodeTextSpan extends _MarkdownNode { - _MarkdownNodeTextSpan(this.textSpan); - TextSpan textSpan; -} - -class _Block { - _Block(this.tag, this.attributes, this.markdownStyle, this.listIndents, this.blockPosition) { - TextStyle style = markdownStyle.styles[tag]; - if (style == null) - style = const TextStyle(color: const Color(0xffff0000)); - - stack = <_MarkdownNode>[new _MarkdownNodeList(<_MarkdownNode>[new _MarkdownNodeTextStyle(style)])]; - subBlocks = <_Block>[]; - } - - final String tag; - final Map attributes; - final MarkdownStyleRaw markdownStyle; - final List listIndents; - final int blockPosition; - - List<_MarkdownNode> stack; - List<_Block> subBlocks; - - bool get open => _open; - set open(bool value) { - _open = value; - if (!value && subBlocks.isNotEmpty) - subBlocks.last.isLast = true; - } - - bool _open = true; - bool isLast = false; - - Widget build(BuildContext context) { - - if (tag == 'img') { - return _buildImage(context, attributes['src']); - } - - double spacing = markdownStyle.blockSpacing; - if (isLast) spacing = 0.0; - - Widget contents; - - if (subBlocks.isNotEmpty) { - final List subWidgets = []; - for (_Block subBlock in subBlocks) { - subWidgets.add(subBlock.build(context)); - } - - contents = new Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: subWidgets - ); - } else { - final TextSpan span = _stackToTextSpan(new _MarkdownNodeList(stack)); - contents = new RichText(text: span); - - if (listIndents.isNotEmpty) { - Widget bullet; - if (listIndents.last == 'ul') { - bullet = new Text( - '•', - textAlign: TextAlign.center - ); - } - else { - bullet = new Padding( - padding: const EdgeInsets.only(right: 5.0), - child: new Text( - "${blockPosition + 1}.", - textAlign: TextAlign.right - ) - ); - } - - contents = new Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - new SizedBox( - width: listIndents.length * markdownStyle.listIndent, - child: bullet - ), - new Expanded(child: contents) - ] - ); - } - } - - BoxDecoration decoration; - EdgeInsets padding; - - if (tag == 'blockquote') { - decoration = markdownStyle.blockquoteDecoration; - padding = new EdgeInsets.all(markdownStyle.blockquotePadding); - } else if (tag == 'pre') { - decoration = markdownStyle.codeblockDecoration; - padding = new EdgeInsets.all(markdownStyle.codeblockPadding); - } - - return new Container( - padding: padding, - margin: new EdgeInsets.only(bottom: spacing), - child: contents, - decoration: decoration - ); - } - - TextSpan _stackToTextSpan(_MarkdownNode stack) { - if (stack is _MarkdownNodeTextSpan) - return stack.textSpan; - - if (stack is _MarkdownNodeList) { - final List<_MarkdownNode> list = stack.list; - final _MarkdownNodeTextStyle styleNode = list[0]; - final _LinkInfo linkInfo = styleNode.linkInfo; - final TextStyle style = styleNode.style; - - List children = []; - for (int i = 1; i < list.length; i++) { - children.add(_stackToTextSpan(list[i])); - } - - String text; - if (children.length == 1 && _isPlainText(children[0])) { - text = children[0].text; - children = null; - } - - final TapGestureRecognizer recognizer = linkInfo?.recognizer; - - return new TextSpan(style: style, children: children, recognizer: recognizer, text: text); - } - - if (stack is _MarkdownNodeString) { - return new TextSpan(text: stack.string); - } - - return null; - } - - bool _isPlainText(TextSpan span) { - return (span.text != null && span.style == null && span.recognizer == null && span.children == null); - } - - Widget _buildImage(BuildContext context, String src) { - final List parts = src.split('#'); - if (parts.isEmpty) return new Container(); - - final String path = parts.first; - double width; - double height; - if (parts.length == 2) { - final List dimensions = parts.last.split('x'); - if (dimensions.length == 2) { - width = double.parse(dimensions[0]); - height = double.parse(dimensions[1]); - } - } - - return new Image.network(path, width: width, height: height); - } -} - -class _LinkInfo { - _LinkInfo(this.href, this.recognizer); - - final String href; - final TapGestureRecognizer recognizer; -} - -class _LinkHandler { - _LinkHandler(this.onTapLink); - - List<_LinkInfo> links = <_LinkInfo>[]; - MarkdownLinkCallback onTapLink; - - _LinkInfo createLinkInfo(String href) { - final TapGestureRecognizer recognizer = new TapGestureRecognizer(); - recognizer.onTap = () { - if (onTapLink != null) - onTapLink(href); - }; - - final _LinkInfo linkInfo = new _LinkInfo(href, recognizer); - links.add(linkInfo); - - return linkInfo; - } - - void dispose() { - for (_LinkInfo linkInfo in links) { - linkInfo.recognizer.dispose(); - } - } -} - -abstract class SyntaxHighlighter { // ignore: one_member_abstracts - TextSpan format(String source); -} - -class _DefaultSyntaxHighlighter extends SyntaxHighlighter{ - _DefaultSyntaxHighlighter(this.style); - - final TextStyle style; - - @override - TextSpan format(String source) { - return new TextSpan(style: style, children: [new TextSpan(text: source)]); - } -} diff --git a/lib/src/markdown_style.dart b/lib/src/markdown_style.dart deleted file mode 100644 index ab66fafb4c9..00000000000 --- a/lib/src/markdown_style.dart +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2016 The Chromium 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/material.dart'; -import 'markdown.dart'; -import 'markdown_style_raw.dart'; - -/// Style used for rendering markdown formatted text using the [MarkdownBody] -/// widget. -class MarkdownStyle extends MarkdownStyleRaw{ - - /// Creates a [MarkdownStyle] from the [TextStyle]s in the provided [theme]. - MarkdownStyle.defaultFromTheme(ThemeData theme) : super( - a: new TextStyle(color: Colors.blue), - p: theme.textTheme.body1, - code: new TextStyle( - color: Colors.grey.shade700, - fontFamily: "monospace", - fontSize: theme.textTheme.body1.fontSize * 0.85 - ), - h1: theme.textTheme.headline, - h2: theme.textTheme.title, - h3: theme.textTheme.subhead, - h4: theme.textTheme.body2, - h5: theme.textTheme.body2, - h6: theme.textTheme.body2, - em: const TextStyle(fontStyle: FontStyle.italic), - strong: const TextStyle(fontWeight: FontWeight.bold), - blockquote: theme.textTheme.body1, - blockSpacing: 8.0, - listIndent: 32.0, - blockquotePadding: 8.0, - blockquoteDecoration: new BoxDecoration( - backgroundColor: Colors.blue.shade100, - borderRadius: new BorderRadius.circular(2.0) - ), - codeblockPadding: 8.0, - codeblockDecoration: new BoxDecoration( - backgroundColor: Colors.grey.shade100, - borderRadius: new BorderRadius.circular(2.0) - ) - ); - - /// Creates a [MarkdownStyle] from the [TextStyle]s in the provided [theme]. - /// This style uses larger fonts for the headings than in - /// [MarkdownStyle.defaultFromTheme]. - MarkdownStyle.largeFromTheme(ThemeData theme) : super ( - a: new TextStyle(color: Colors.blue), - p: theme.textTheme.body1, - code: new TextStyle( - color: Colors.grey.shade700, - fontFamily: "monospace", - fontSize: theme.textTheme.body1.fontSize * 0.85 - ), - h1: theme.textTheme.display3, - h2: theme.textTheme.display2, - h3: theme.textTheme.display1, - h4: theme.textTheme.headline, - h5: theme.textTheme.title, - h6: theme.textTheme.subhead, - em: const TextStyle(fontStyle: FontStyle.italic), - strong: const TextStyle(fontWeight: FontWeight.bold), - blockquote: theme.textTheme.body1, - blockSpacing: 8.0, - listIndent: 32.0, - blockquotePadding: 8.0, - blockquoteDecoration: new BoxDecoration( - backgroundColor: Colors.blue.shade100, - borderRadius: new BorderRadius.circular(2.0) - ), - codeblockPadding: 8.0, - codeblockDecoration: new BoxDecoration( - backgroundColor: Colors.grey.shade100, - borderRadius: new BorderRadius.circular(2.0) - ) - ); -} diff --git a/lib/src/markdown_style_raw.dart b/lib/src/markdown_style_raw.dart deleted file mode 100644 index adf8316e58d..00000000000 --- a/lib/src/markdown_style_raw.dart +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright 2016 The Chromium 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/widgets.dart'; -import 'markdown.dart'; - -/// Style used for rendering markdown formatted text using the [MarkdownBody] -/// widget. -class MarkdownStyleRaw { - - /// Creates a new [MarkdownStyleRaw] - MarkdownStyleRaw({ - this.a, - this.p, - this.code, - this.h1, - this.h2, - this.h3, - this.h4, - this.h5, - this.h6, - this.em, - this.strong, - this.blockquote, - this.blockSpacing, - this.listIndent, - this.blockquotePadding, - this.blockquoteDecoration, - this.codeblockPadding, - this.codeblockDecoration - }) { - _init(); - } - - /// Creates a new [MarkdownStyleRaw] based on the current style, with the - /// provided parameters overridden. - MarkdownStyleRaw copyWith({ - TextStyle a, - TextStyle p, - TextStyle code, - TextStyle h1, - TextStyle h2, - TextStyle h3, - TextStyle h4, - TextStyle h5, - TextStyle h6, - TextStyle em, - TextStyle strong, - TextStyle blockquote, - double blockSpacing, - double listIndent, - double blockquotePadding, - BoxDecoration blockquoteDecoration, - double codeblockPadding, - BoxDecoration codeblockDecoration - }) { - return new MarkdownStyleRaw( - a: a != null ? a : this.a, - p: p != null ? p : this.p, - code: code != null ? code : this.code, - h1: h1 != null ? h1 : this.h1, - h2: h2 != null ? h2 : this.h2, - h3: h3 != null ? h3 : this.h3, - h4: h4 != null ? h4 : this.h4, - h5: h5 != null ? h5 : this.h5, - h6: h6 != null ? h6 : this.h6, - em: em != null ? em : this.em, - strong: strong != null ? strong : this.strong, - blockquote: blockquote != null ? blockquote : this.blockquote, - blockSpacing: blockSpacing != null ? blockSpacing : this.blockSpacing, - listIndent: listIndent != null ? listIndent : this.listIndent, - blockquotePadding: blockquotePadding != null ? blockquotePadding : this.blockquotePadding, - blockquoteDecoration: blockquoteDecoration != null ? blockquoteDecoration : this.blockquoteDecoration, - codeblockPadding: codeblockPadding != null ? codeblockPadding : this.codeblockPadding, - codeblockDecoration: codeblockDecoration != null ? codeblockDecoration : this.codeblockDecoration - ); - } - - final TextStyle a; - final TextStyle p; - final TextStyle code; - final TextStyle h1; - final TextStyle h2; - final TextStyle h3; - final TextStyle h4; - final TextStyle h5; - final TextStyle h6; - final TextStyle em; - final TextStyle strong; - final TextStyle blockquote; - final double blockSpacing; - final double listIndent; - final double blockquotePadding; - final BoxDecoration blockquoteDecoration; - final double codeblockPadding; - final BoxDecoration codeblockDecoration; - - Map _styles; - - Map get styles => _styles; - - void _init() { - _styles = { - 'a': a, - 'p': p, - 'li': p, - 'code': code, - 'pre': p, - 'h1': h1, - 'h2': h2, - 'h3': h3, - 'h4': h4, - 'h5': h5, - 'h6': h6, - 'em': em, - 'strong': strong, - 'blockquote': blockquote - }; - } -} diff --git a/lib/src/style_sheet.dart b/lib/src/style_sheet.dart new file mode 100644 index 00000000000..ed6eecd941d --- /dev/null +++ b/lib/src/style_sheet.dart @@ -0,0 +1,269 @@ +// Copyright 2016 The Chromium 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/material.dart'; + +/// Defines which [TextStyle] objects to use for which Markdown elements. +class MarkdownStyleSheet { + /// Creates an explicit mapping of [TextStyle] objects to Markdown elements. + MarkdownStyleSheet({ + this.a, + this.p, + this.code, + this.h1, + this.h2, + this.h3, + this.h4, + this.h5, + this.h6, + this.em, + this.strong, + this.blockquote, + this.blockSpacing, + this.listIndent, + this.blockquotePadding, + this.blockquoteDecoration, + this.codeblockPadding, + this.codeblockDecoration + }) : _styles = { + 'a': a, + 'p': p, + 'li': p, + 'code': code, + 'pre': p, + 'h1': h1, + 'h2': h2, + 'h3': h3, + 'h4': h4, + 'h5': h5, + 'h6': h6, + 'em': em, + 'strong': strong, + 'blockquote': blockquote + }; + + /// Creates a [MarkdownStyleSheet] from the [TextStyle]s in the provided [ThemeData]. + factory MarkdownStyleSheet.fromTheme(ThemeData theme) { + return new MarkdownStyleSheet( + a: new TextStyle(color: Colors.blue), + p: theme.textTheme.body1, + code: new TextStyle( + color: Colors.grey.shade700, + fontFamily: "monospace", + fontSize: theme.textTheme.body1.fontSize * 0.85 + ), + h1: theme.textTheme.headline, + h2: theme.textTheme.title, + h3: theme.textTheme.subhead, + h4: theme.textTheme.body2, + h5: theme.textTheme.body2, + h6: theme.textTheme.body2, + em: const TextStyle(fontStyle: FontStyle.italic), + strong: const TextStyle(fontWeight: FontWeight.bold), + blockquote: theme.textTheme.body1, + blockSpacing: 8.0, + listIndent: 32.0, + blockquotePadding: 8.0, + blockquoteDecoration: new BoxDecoration( + backgroundColor: Colors.blue.shade100, + borderRadius: new BorderRadius.circular(2.0) + ), + codeblockPadding: 8.0, + codeblockDecoration: new BoxDecoration( + backgroundColor: Colors.grey.shade100, + borderRadius: new BorderRadius.circular(2.0) + ) + ); + } + + /// Creates a [MarkdownStyle] from the [TextStyle]s in the provided [ThemeData]. + /// + /// This constructor uses larger fonts for the headings than in + /// [MarkdownStyle.fromTheme]. + factory MarkdownStyleSheet.largeFromTheme(ThemeData theme) { + return new MarkdownStyleSheet( + a: new TextStyle(color: Colors.blue), + p: theme.textTheme.body1, + code: new TextStyle( + color: Colors.grey.shade700, + fontFamily: "monospace", + fontSize: theme.textTheme.body1.fontSize * 0.85 + ), + h1: theme.textTheme.display3, + h2: theme.textTheme.display2, + h3: theme.textTheme.display1, + h4: theme.textTheme.headline, + h5: theme.textTheme.title, + h6: theme.textTheme.subhead, + em: const TextStyle(fontStyle: FontStyle.italic), + strong: const TextStyle(fontWeight: FontWeight.bold), + blockquote: theme.textTheme.body1, + blockSpacing: 8.0, + listIndent: 32.0, + blockquotePadding: 8.0, + blockquoteDecoration: new BoxDecoration( + backgroundColor: Colors.blue.shade100, + borderRadius: new BorderRadius.circular(2.0) + ), + codeblockPadding: 8.0, + codeblockDecoration: new BoxDecoration( + backgroundColor: Colors.grey.shade100, + borderRadius: new BorderRadius.circular(2.0) + ) + ); + } + + /// Creates a new [MarkdownStyleSheet] based on the current style, with the + /// provided parameters overridden. + MarkdownStyleSheet copyWith({ + TextStyle a, + TextStyle p, + TextStyle code, + TextStyle h1, + TextStyle h2, + TextStyle h3, + TextStyle h4, + TextStyle h5, + TextStyle h6, + TextStyle em, + TextStyle strong, + TextStyle blockquote, + double blockSpacing, + double listIndent, + double blockquotePadding, + Decoration blockquoteDecoration, + double codeblockPadding, + Decoration codeblockDecoration + }) { + return new MarkdownStyleSheet( + a: a != null ? a : this.a, + p: p != null ? p : this.p, + code: code != null ? code : this.code, + h1: h1 != null ? h1 : this.h1, + h2: h2 != null ? h2 : this.h2, + h3: h3 != null ? h3 : this.h3, + h4: h4 != null ? h4 : this.h4, + h5: h5 != null ? h5 : this.h5, + h6: h6 != null ? h6 : this.h6, + em: em != null ? em : this.em, + strong: strong != null ? strong : this.strong, + blockquote: blockquote != null ? blockquote : this.blockquote, + blockSpacing: blockSpacing != null ? blockSpacing : this.blockSpacing, + listIndent: listIndent != null ? listIndent : this.listIndent, + blockquotePadding: blockquotePadding != null ? blockquotePadding : this.blockquotePadding, + blockquoteDecoration: blockquoteDecoration != null ? blockquoteDecoration : this.blockquoteDecoration, + codeblockPadding: codeblockPadding != null ? codeblockPadding : this.codeblockPadding, + codeblockDecoration: codeblockDecoration != null ? codeblockDecoration : this.codeblockDecoration + ); + } + + /// The [TextStyle] to use for `a` elements. + final TextStyle a; + + /// The [TextStyle] to use for `p` elements. + final TextStyle p; + + /// The [TextStyle] to use for `code` elements. + final TextStyle code; + + /// The [TextStyle] to use for `h1` elements. + final TextStyle h1; + + /// The [TextStyle] to use for `h2` elements. + final TextStyle h2; + + /// The [TextStyle] to use for `h3` elements. + final TextStyle h3; + + /// The [TextStyle] to use for `h4` elements. + final TextStyle h4; + + /// The [TextStyle] to use for `h5` elements. + final TextStyle h5; + + /// The [TextStyle] to use for `h6` elements. + final TextStyle h6; + + /// The [TextStyle] to use for `em` elements. + final TextStyle em; + + /// The [TextStyle] to use for `strong` elements. + final TextStyle strong; + + /// The [TextStyle] to use for `blockquote` elements. + final TextStyle blockquote; + + /// The amount of vertical space to use between block-level elements. + final double blockSpacing; + + /// The amount of horizontal space to indent list items. + final double listIndent; + + /// The padding to use for `blockquote` elements. + final double blockquotePadding; + + /// The decoration to use behind `blockquote` elements. + final Decoration blockquoteDecoration; + + /// The padding to use for `pre` elements. + final double codeblockPadding; + + /// The decoration to use behind for `pre` elements. + final Decoration codeblockDecoration; + + /// A [Map] from element name to the cooresponding [TextStyle] object. + Map get styles => _styles; + Map _styles; + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) + return true; + if (other.runtimeType != MarkdownStyleSheet) + return false; + final MarkdownStyleSheet typedOther = other; + return typedOther.a == a + && typedOther.p == p + && typedOther.code == code + && typedOther.h1 == h1 + && typedOther.h2 == h2 + && typedOther.h3 == h3 + && typedOther.h4 == h4 + && typedOther.h5 == h5 + && typedOther.h6 == h6 + && typedOther.em == em + && typedOther.strong == strong + && typedOther.blockquote == blockquote + && typedOther.blockSpacing == blockSpacing + && typedOther.listIndent == listIndent + && typedOther.blockquotePadding == blockquotePadding + && typedOther.blockquoteDecoration == blockquoteDecoration + && typedOther.codeblockPadding == codeblockPadding + && typedOther.codeblockDecoration == codeblockDecoration; + } + + @override + int get hashCode { + return hashValues( + a, + p, + code, + h1, + h2, + h3, + h4, + h5, + h6, + em, + strong, + blockquote, + blockSpacing, + listIndent, + blockquotePadding, + blockquoteDecoration, + codeblockPadding, + codeblockDecoration, + ); + } +} diff --git a/lib/src/widget.dart b/lib/src/widget.dart new file mode 100644 index 00000000000..72931d0a59a --- /dev/null +++ b/lib/src/widget.dart @@ -0,0 +1,211 @@ +// Copyright 2016 The Chromium 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/material.dart'; +import 'package:markdown/markdown.dart' as md; +import 'package:meta/meta.dart'; + +import 'builder.dart'; +import 'style_sheet.dart'; + +/// Signature for callbacks used by [MarkdownWidget] when the user taps a link. +/// +/// Used by [MarkdownWidget.onTapLink]. +typedef void MarkdownTapLinkCallback(String href); + +/// Creates a format [TextSpan] given a string. +/// +/// Used by [MarkdownWidget] to highlight the contents of `pre` elements. +abstract class SyntaxHighlighter { // ignore: one_member_abstracts + /// Returns the formated [TextSpan] for the given string. + TextSpan format(String source); +} + +/// A base class for widgets that parse and display Markdown. +/// +/// Supports all standard Markdown from the original +/// [Markdown specification](https://daringfireball.net/projects/markdown/). +/// +/// See also: +/// +/// * [Markdown], which is a scrolling container of Markdown. +/// * [MarkdownBody], which is a non-scrolling container of Markdown. +/// * +abstract class MarkdownWidget extends StatefulWidget { + /// Creates a widget that parses and displays Markdown. + /// + /// The [data] argument must not be null. + MarkdownWidget({ + Key key, + @required this.data, + this.styleSheet, + this.syntaxHighlighter, + this.onTapLink, + }) : super(key: key) { + assert(data != null); + } + + /// The Markdown to display. + final String data; + + /// The styles to use when displaying the Markdown. + /// + /// If null, the styles are infered from the current [Theme]. + final MarkdownStyleSheet styleSheet; + + /// The syntax highlighter used to color text in `pre` elements. + /// + /// If null, the [MarkdownStyleSheet.code] style is used for `pre` elements. + final SyntaxHighlighter syntaxHighlighter; + + /// Called when the user taps a link. + final MarkdownTapLinkCallback onTapLink; + + /// Subclasses should override this function to display the given children, + /// which are the parsed representation of [data]. + @protected + Widget build(BuildContext context, List children); + + @override + _MarkdownWidgetState createState() => new _MarkdownWidgetState(); +} + +class _MarkdownWidgetState extends State implements MarkdownBuilderDelegate { + List _children; + final List _recognizers = []; + + @override + void didChangeDependencies() { + _parseMarkdown(); + super.didChangeDependencies(); + } + + @override + void didUpdateConfig(MarkdownWidget oldConfig) { + super.didUpdateConfig(oldConfig); + if (config.data != oldConfig.data + || config.styleSheet != oldConfig.styleSheet) + _parseMarkdown(); + } + + @override + void dispose() { + _disposeRecognizers(); + super.dispose(); + } + + void _parseMarkdown() { + final MarkdownStyleSheet styleSheet = config.styleSheet ?? new MarkdownStyleSheet.fromTheme(Theme.of(context)); + + _disposeRecognizers(); + + // TODO: This can be optimized by doing the split and removing \r at the same time + final List lines = config.data.replaceAll('\r\n', '\n').split('\n'); + final md.Document document = new md.Document(); + final MarkdownBuilder builder = new MarkdownBuilder(delegate: this, styleSheet: styleSheet); + _children = builder.build(document.parseLines(lines)); + } + + void _disposeRecognizers() { + if (_recognizers.isEmpty) + return; + final List localRecognizers = new List.from(_recognizers); + _recognizers.clear(); + for (GestureRecognizer recognizer in localRecognizers) + recognizer.dispose(); + } + + @override + GestureRecognizer createLink(String href) { + final TapGestureRecognizer recognizer = new TapGestureRecognizer() + ..onTap = () { + if (config.onTapLink != null) + config.onTapLink(href); + }; + _recognizers.add(recognizer); + return recognizer; + } + + @override + TextSpan formatText(MarkdownStyleSheet styleSheet, String code) { + if (config.syntaxHighlighter != null) + return config.syntaxHighlighter.format(code); + return new TextSpan(style: styleSheet.code, text: code); + } + + @override + Widget build(BuildContext context) => config.build(context, _children); +} + +/// A non-scrolling widget that parses and displays Markdown. +/// +/// Supports all standard Markdown from the original +/// [Markdown specification](https://daringfireball.net/projects/markdown/). +/// +/// See also: +/// +/// * [Markdown], which is a scrolling container of Markdown. +/// * +class MarkdownBody extends MarkdownWidget { + /// Creates a non-scrolling widget that parses and displays Markdown. + MarkdownBody({ + Key key, + String data, + MarkdownStyleSheet styleSheet, + SyntaxHighlighter syntaxHighlighter, + MarkdownTapLinkCallback onTapLink, + }) : super( + key: key, + data: data, + styleSheet: styleSheet, + syntaxHighlighter: syntaxHighlighter, + onTapLink: onTapLink, + ); + + @override + Widget build(BuildContext context, List children) { + if (children.length == 1) + return children.single; + return new Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: children, + ); + } +} + +/// A scrolling widget that parses and displays Markdown. +/// +/// Supports all standard Markdown from the original +/// [Markdown specification](https://daringfireball.net/projects/markdown/). +/// +/// See also: +/// +/// * [MarkdownBody], which is a non-scrolling container of Markdown. +/// * +class Markdown extends MarkdownWidget { + /// Creates a scrolling widget that parses and displays Markdown. + Markdown({ + Key key, + String data, + MarkdownStyleSheet styleSheet, + SyntaxHighlighter syntaxHighlighter, + MarkdownTapLinkCallback onTapLink, + this.padding: const EdgeInsets.all(16.0), + }) : super( + key: key, + data: data, + styleSheet: styleSheet, + syntaxHighlighter: syntaxHighlighter, + onTapLink: onTapLink, + ); + + /// The amount of space by which to inset the children. + final EdgeInsets padding; + + @override + Widget build(BuildContext context, List children) { + return new ListView(padding: padding, children: children); + } +} diff --git a/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart index af2877ca59d..42301e77814 100644 --- a/test/flutter_markdown_test.dart +++ b/test/flutter_markdown_test.dart @@ -10,78 +10,77 @@ import 'package:flutter/material.dart'; void main() { testWidgets('Simple string', (WidgetTester tester) async { - await tester.pumpWidget(new MarkdownBody(data: 'Hello')); + await tester.pumpWidget(new MarkdownBody(data: 'Hello')); - final Iterable widgets = tester.allWidgets; - _expectWidgetTypes(widgets, [MarkdownBody, Column, Container, Padding, RichText]); - _expectTextStrings(widgets, ['Hello']); + final Iterable widgets = tester.allWidgets; + _expectWidgetTypes(widgets, [MarkdownBody, Column, RichText]); + _expectTextStrings(widgets, ['Hello']); }); testWidgets('Header', (WidgetTester tester) async { - await tester.pumpWidget(new MarkdownBody(data: '# Header')); + await tester.pumpWidget(new MarkdownBody(data: '# Header')); - final Iterable widgets = tester.allWidgets; - _expectWidgetTypes(widgets, [MarkdownBody, Column, Container, Padding, RichText]); - _expectTextStrings(widgets, ['Header']); + final Iterable widgets = tester.allWidgets; + _expectWidgetTypes(widgets, [MarkdownBody, Column, RichText]); + _expectTextStrings(widgets, ['Header']); }); testWidgets('Empty string', (WidgetTester tester) async { - await tester.pumpWidget(new MarkdownBody(data: '')); + await tester.pumpWidget(new MarkdownBody(data: '')); - final Iterable widgets = tester.allWidgets; - _expectWidgetTypes(widgets, [MarkdownBody, Column]); + final Iterable widgets = tester.allWidgets; + _expectWidgetTypes(widgets, [MarkdownBody, Column]); }); testWidgets('Ordered list', (WidgetTester tester) async { - await tester.pumpWidget(new MarkdownBody(data: '1. Item 1\n1. Item 2\n2. Item 3')); - - final Iterable widgets = tester.allWidgets; - _expectTextStrings(widgets, [ - '1.', - 'Item 1', - '2.', - 'Item 2', - '3.', - 'Item 3'] - ); + await tester.pumpWidget(new MarkdownBody(data: '1. Item 1\n1. Item 2\n2. Item 3')); + + final Iterable widgets = tester.allWidgets; + _expectTextStrings(widgets, [ + '1.', + 'Item 1', + '2.', + 'Item 2', + '3.', + 'Item 3', + ]); }); testWidgets('Unordered list', (WidgetTester tester) async { - await tester.pumpWidget(new MarkdownBody(data: '- Item 1\n- Item 2\n- Item 3')); - - final Iterable widgets = tester.allWidgets; - _expectTextStrings(widgets, [ - '•', - 'Item 1', - '•', - 'Item 2', - '•', - 'Item 3'] - ); + await tester.pumpWidget(new MarkdownBody(data: '- Item 1\n- Item 2\n- Item 3')); + + final Iterable widgets = tester.allWidgets; + _expectTextStrings(widgets, [ + '•', + 'Item 1', + '•', + 'Item 2', + '•', + 'Item 3', + ]); }); testWidgets('Scrollable wrapping', (WidgetTester tester) async { - await tester.pumpWidget(new Markdown(data: '')); - - final List widgets = tester.allWidgets.toList(); - _expectWidgetTypes(widgets.take(2), [ - Markdown, - SingleChildScrollView, - ]); - _expectWidgetTypes(widgets.reversed.take(3).toList().reversed, [ - Padding, - MarkdownBody, - Column - ]); + await tester.pumpWidget(new Markdown(data: '')); + + final List widgets = tester.allWidgets.toList(); + _expectWidgetTypes(widgets.take(2), [ + Markdown, + ListView, + ]); + _expectWidgetTypes(widgets.reversed.take(2).toList().reversed, [ + SliverPadding, + SliverList, + ]); }); testWidgets('Links', (WidgetTester tester) async { - await tester.pumpWidget(new Markdown(data: '[Link Text](href)')); + await tester.pumpWidget(new Markdown(data: '[Link Text](href)')); - final RichText textWidget = tester.allWidgets.firstWhere((Widget widget) => widget is RichText); - final TextSpan span = textWidget.text; + final RichText textWidget = tester.allWidgets.firstWhere((Widget widget) => widget is RichText); + final TextSpan span = textWidget.text; - expect(span.children[0].recognizer.runtimeType, equals(TapGestureRecognizer)); + expect(span.children[0].recognizer.runtimeType, equals(TapGestureRecognizer)); }); testWidgets('HTML tag ignored ', (WidgetTester tester) async { @@ -93,44 +92,54 @@ void main() { for (String mdLine in mdData) { await tester.pumpWidget(new MarkdownBody(data: mdLine)); - final Iterable widgets = tester.allWidgets; - _expectTextStrings(widgets, ['Line 1', 'Line 2']); + final Iterable widgets = tester.allWidgets; + _expectTextStrings(widgets, ['Line 1', 'Line 2']); } }); testWidgets('Less than', (WidgetTester tester) async { - final String mdLine = 'Line 1 <\n\nc < c c\n\n< Line 2'; - await tester.pumpWidget(new MarkdownBody(data: mdLine)); + final String mdLine = 'Line 1 <\n\nc < c c\n\n< Line 2'; + await tester.pumpWidget(new MarkdownBody(data: mdLine)); - final Iterable widgets = tester.allWidgets; - _expectTextStrings(widgets, ['Line 1 <','c < c c','< Line 2']); + final Iterable widgets = tester.allWidgets; + _expectTextStrings(widgets, ['Line 1 <','c < c c','< Line 2']); }); testWidgets('Changing config - data', (WidgetTester tester) async { - await tester.pumpWidget(new Markdown(data: 'Data1')); - _expectTextStrings(tester.allWidgets, ['Data1']); + await tester.pumpWidget(new Markdown(data: 'Data1')); + _expectTextStrings(tester.allWidgets, ['Data1']); - final String stateBefore = WidgetsBinding.instance.renderViewElement.toStringDeep(); - await tester.pumpWidget(new Markdown(data: 'Data1')); - final String stateAfter = WidgetsBinding.instance.renderViewElement.toStringDeep(); - expect(stateBefore, equals(stateAfter)); + final String stateBefore = _dumpRenderView(); + await tester.pumpWidget(new Markdown(data: 'Data1')); + final String stateAfter = _dumpRenderView(); + expect(stateBefore, equals(stateAfter)); - await tester.pumpWidget(new Markdown(data: 'Data2')); - _expectTextStrings(tester.allWidgets, ['Data2']); + await tester.pumpWidget(new Markdown(data: 'Data2')); + _expectTextStrings(tester.allWidgets, ['Data2']); }); testWidgets('Changing config - style', (WidgetTester tester) async { - final ThemeData theme = new ThemeData.light(); + final ThemeData theme = new ThemeData.light(); - final MarkdownStyle style1 = new MarkdownStyle.defaultFromTheme(theme); - final MarkdownStyle style2 = new MarkdownStyle.largeFromTheme(theme); + final MarkdownStyleSheet style1 = new MarkdownStyleSheet.fromTheme(theme); + final MarkdownStyleSheet style2 = new MarkdownStyleSheet.largeFromTheme(theme); + expect(style1, isNot(style2)); - await tester.pumpWidget(new Markdown(data: 'Test', markdownStyle: style1)); + await tester.pumpWidget(new Markdown(data: '# Test', styleSheet: style1)); + final RichText text1 = tester.widget(find.byType(RichText)); + await tester.pumpWidget(new Markdown(data: '# Test', styleSheet: style2)); + final RichText text2 = tester.widget(find.byType(RichText)); - final String stateBefore = WidgetsBinding.instance.renderViewElement.toStringDeep(); - await tester.pumpWidget(new Markdown(data: 'Test', markdownStyle: style2)); - final String stateAfter = WidgetsBinding.instance.renderViewElement.toStringDeep(); - expect(stateBefore, isNot(stateAfter)); + expect(text1.text, isNot(text2.text)); + }); + + testWidgets('Style equality', (WidgetTester tester) async { + final ThemeData theme = new ThemeData.light(); + + final MarkdownStyleSheet style1 = new MarkdownStyleSheet.fromTheme(theme); + final MarkdownStyleSheet style2 = new MarkdownStyleSheet.fromTheme(theme); + expect(style1, equals(style2)); + expect(style1.hashCode, equals(style2.hashCode)); }); } @@ -160,3 +169,9 @@ String _extractTextFromTextSpan(TextSpan span) { } return text; } + +String _dumpRenderView() { + return WidgetsBinding.instance.renderViewElement.toStringDeep().replaceAll( + new RegExp(r'SliverChildListDelegate#\d+', multiLine: true), 'SliverChildListDelegate' + ); +} From 7898bc3d2a06502ab130bbe0149967ab4f5e1fc7 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Fri, 7 Apr 2017 12:24:32 -0700 Subject: [PATCH 057/121] Constants! Constants everywhere! (#9286) Aggressively apply the const lint. --- example/demo.dart | 2 +- lib/src/builder.dart | 2 +- lib/src/style_sheet.dart | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/example/demo.dart b/example/demo.dart index ceb9e482a60..3a99dd6ce52 100644 --- a/example/demo.dart +++ b/example/demo.dart @@ -30,7 +30,7 @@ void main() { runApp(new MaterialApp( title: "Markdown Demo", home: new Scaffold( - appBar: new AppBar(title: new Text('Markdown Demo')), + appBar: new AppBar(title: const Text('Markdown Demo')), body: new Markdown(data: _kMarkdownData) ) )); diff --git a/lib/src/builder.dart b/lib/src/builder.dart index 16219639613..7b91ba0bf40 100644 --- a/lib/src/builder.dart +++ b/lib/src/builder.dart @@ -214,7 +214,7 @@ class MarkdownBuilder implements md.NodeVisitor { Widget _buildBullet(String listTag) { if (listTag == 'ul') - return new Text('•', textAlign: TextAlign.center); + return const Text('•', textAlign: TextAlign.center); final int index = _blocks.last.nextListIndex; return new Padding( diff --git a/lib/src/style_sheet.dart b/lib/src/style_sheet.dart index ed6eecd941d..f8c0f387333 100644 --- a/lib/src/style_sheet.dart +++ b/lib/src/style_sheet.dart @@ -46,7 +46,7 @@ class MarkdownStyleSheet { /// Creates a [MarkdownStyleSheet] from the [TextStyle]s in the provided [ThemeData]. factory MarkdownStyleSheet.fromTheme(ThemeData theme) { return new MarkdownStyleSheet( - a: new TextStyle(color: Colors.blue), + a: const TextStyle(color: Colors.blue), p: theme.textTheme.body1, code: new TextStyle( color: Colors.grey.shade700, @@ -83,7 +83,7 @@ class MarkdownStyleSheet { /// [MarkdownStyle.fromTheme]. factory MarkdownStyleSheet.largeFromTheme(ThemeData theme) { return new MarkdownStyleSheet( - a: new TextStyle(color: Colors.blue), + a: const TextStyle(color: Colors.blue), p: theme.textTheme.body1, code: new TextStyle( color: Colors.grey.shade700, From 7ae0b5d214ee38e224ced3b759a2eaa2f33bb981 Mon Sep 17 00:00:00 2001 From: xster Date: Mon, 10 Apr 2017 18:32:24 -0700 Subject: [PATCH 058/121] Rename State.config to widget everywhere (#9273) Rename State.config to State.widget Rename State.didUpdateConfig to State.didUpdateWidget Renamed all State subclasses' local variables named config to something else --- lib/src/widget.dart | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/src/widget.dart b/lib/src/widget.dart index 72931d0a59a..52f9cac855a 100644 --- a/lib/src/widget.dart +++ b/lib/src/widget.dart @@ -83,10 +83,10 @@ class _MarkdownWidgetState extends State implements MarkdownBuil } @override - void didUpdateConfig(MarkdownWidget oldConfig) { - super.didUpdateConfig(oldConfig); - if (config.data != oldConfig.data - || config.styleSheet != oldConfig.styleSheet) + void didUpdateWidget(MarkdownWidget oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.data != oldWidget.data + || widget.styleSheet != oldWidget.styleSheet) _parseMarkdown(); } @@ -97,12 +97,12 @@ class _MarkdownWidgetState extends State implements MarkdownBuil } void _parseMarkdown() { - final MarkdownStyleSheet styleSheet = config.styleSheet ?? new MarkdownStyleSheet.fromTheme(Theme.of(context)); + final MarkdownStyleSheet styleSheet = widget.styleSheet ?? new MarkdownStyleSheet.fromTheme(Theme.of(context)); _disposeRecognizers(); // TODO: This can be optimized by doing the split and removing \r at the same time - final List lines = config.data.replaceAll('\r\n', '\n').split('\n'); + final List lines = widget.data.replaceAll('\r\n', '\n').split('\n'); final md.Document document = new md.Document(); final MarkdownBuilder builder = new MarkdownBuilder(delegate: this, styleSheet: styleSheet); _children = builder.build(document.parseLines(lines)); @@ -121,8 +121,8 @@ class _MarkdownWidgetState extends State implements MarkdownBuil GestureRecognizer createLink(String href) { final TapGestureRecognizer recognizer = new TapGestureRecognizer() ..onTap = () { - if (config.onTapLink != null) - config.onTapLink(href); + if (widget.onTapLink != null) + widget.onTapLink(href); }; _recognizers.add(recognizer); return recognizer; @@ -130,13 +130,13 @@ class _MarkdownWidgetState extends State implements MarkdownBuil @override TextSpan formatText(MarkdownStyleSheet styleSheet, String code) { - if (config.syntaxHighlighter != null) - return config.syntaxHighlighter.format(code); + if (widget.syntaxHighlighter != null) + return widget.syntaxHighlighter.format(code); return new TextSpan(style: styleSheet.code, text: code); } @override - Widget build(BuildContext context) => config.build(context, _children); + Widget build(BuildContext context) => widget.build(context, _children); } /// A non-scrolling widget that parses and displays Markdown. From 3d45aeee3671b7ed65810110f65ead207def9202 Mon Sep 17 00:00:00 2001 From: Devon Carew Date: Fri, 14 Apr 2017 08:55:26 -0700 Subject: [PATCH 059/121] update the IntelliJ metadata files (#9388) --- flutter_markdown.iml | 1 + 1 file changed, 1 insertion(+) diff --git a/flutter_markdown.iml b/flutter_markdown.iml index 48dcb90f686..1bf5976da23 100644 --- a/flutter_markdown.iml +++ b/flutter_markdown.iml @@ -12,6 +12,7 @@ +
\ No newline at end of file From 721ee7be53b4e5cf17c34558408d4552be93ae09 Mon Sep 17 00:00:00 2001 From: Phil Quitslund Date: Mon, 17 Apr 2017 13:42:31 -0700 Subject: [PATCH 060/121] IntelliJ metadata cleanup (flutter-intellij#914). (#9427) Follow-up from #9422 --- flutter_markdown.iml | 1 - 1 file changed, 1 deletion(-) diff --git a/flutter_markdown.iml b/flutter_markdown.iml index 1bf5976da23..4f0181093da 100644 --- a/flutter_markdown.iml +++ b/flutter_markdown.iml @@ -11,7 +11,6 @@
-
From 795ce58d9d94521fd62f9f9fa1bc8d73a58683da Mon Sep 17 00:00:00 2001 From: Alexandre Ardhuin Date: Fri, 21 Apr 2017 23:09:42 +0200 Subject: [PATCH 061/121] make @immutable const classes (#9532) * make @immutable const * fix build --- example/demo.dart | 2 +- lib/src/widget.dart | 11 +++++------ test/flutter_markdown_test.dart | 20 ++++++++++---------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/example/demo.dart b/example/demo.dart index 3a99dd6ce52..41336ab84d4 100644 --- a/example/demo.dart +++ b/example/demo.dart @@ -31,7 +31,7 @@ void main() { title: "Markdown Demo", home: new Scaffold( appBar: new AppBar(title: const Text('Markdown Demo')), - body: new Markdown(data: _kMarkdownData) + body: const Markdown(data: _kMarkdownData) ) )); } diff --git a/lib/src/widget.dart b/lib/src/widget.dart index 52f9cac855a..974b2cde36e 100644 --- a/lib/src/widget.dart +++ b/lib/src/widget.dart @@ -37,15 +37,14 @@ abstract class MarkdownWidget extends StatefulWidget { /// Creates a widget that parses and displays Markdown. /// /// The [data] argument must not be null. - MarkdownWidget({ + const MarkdownWidget({ Key key, @required this.data, this.styleSheet, this.syntaxHighlighter, this.onTapLink, - }) : super(key: key) { - assert(data != null); - } + }) : assert(data != null), + super(key: key); /// The Markdown to display. final String data; @@ -150,7 +149,7 @@ class _MarkdownWidgetState extends State implements MarkdownBuil /// * class MarkdownBody extends MarkdownWidget { /// Creates a non-scrolling widget that parses and displays Markdown. - MarkdownBody({ + const MarkdownBody({ Key key, String data, MarkdownStyleSheet styleSheet, @@ -186,7 +185,7 @@ class MarkdownBody extends MarkdownWidget { /// * class Markdown extends MarkdownWidget { /// Creates a scrolling widget that parses and displays Markdown. - Markdown({ + const Markdown({ Key key, String data, MarkdownStyleSheet styleSheet, diff --git a/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart index 42301e77814..73d5ca57bf2 100644 --- a/test/flutter_markdown_test.dart +++ b/test/flutter_markdown_test.dart @@ -10,7 +10,7 @@ import 'package:flutter/material.dart'; void main() { testWidgets('Simple string', (WidgetTester tester) async { - await tester.pumpWidget(new MarkdownBody(data: 'Hello')); + await tester.pumpWidget(const MarkdownBody(data: 'Hello')); final Iterable widgets = tester.allWidgets; _expectWidgetTypes(widgets, [MarkdownBody, Column, RichText]); @@ -18,7 +18,7 @@ void main() { }); testWidgets('Header', (WidgetTester tester) async { - await tester.pumpWidget(new MarkdownBody(data: '# Header')); + await tester.pumpWidget(const MarkdownBody(data: '# Header')); final Iterable widgets = tester.allWidgets; _expectWidgetTypes(widgets, [MarkdownBody, Column, RichText]); @@ -26,14 +26,14 @@ void main() { }); testWidgets('Empty string', (WidgetTester tester) async { - await tester.pumpWidget(new MarkdownBody(data: '')); + await tester.pumpWidget(const MarkdownBody(data: '')); final Iterable widgets = tester.allWidgets; _expectWidgetTypes(widgets, [MarkdownBody, Column]); }); testWidgets('Ordered list', (WidgetTester tester) async { - await tester.pumpWidget(new MarkdownBody(data: '1. Item 1\n1. Item 2\n2. Item 3')); + await tester.pumpWidget(const MarkdownBody(data: '1. Item 1\n1. Item 2\n2. Item 3')); final Iterable widgets = tester.allWidgets; _expectTextStrings(widgets, [ @@ -47,7 +47,7 @@ void main() { }); testWidgets('Unordered list', (WidgetTester tester) async { - await tester.pumpWidget(new MarkdownBody(data: '- Item 1\n- Item 2\n- Item 3')); + await tester.pumpWidget(const MarkdownBody(data: '- Item 1\n- Item 2\n- Item 3')); final Iterable widgets = tester.allWidgets; _expectTextStrings(widgets, [ @@ -61,7 +61,7 @@ void main() { }); testWidgets('Scrollable wrapping', (WidgetTester tester) async { - await tester.pumpWidget(new Markdown(data: '')); + await tester.pumpWidget(const Markdown(data: '')); final List widgets = tester.allWidgets.toList(); _expectWidgetTypes(widgets.take(2), [ @@ -75,7 +75,7 @@ void main() { }); testWidgets('Links', (WidgetTester tester) async { - await tester.pumpWidget(new Markdown(data: '[Link Text](href)')); + await tester.pumpWidget(const Markdown(data: '[Link Text](href)')); final RichText textWidget = tester.allWidgets.firstWhere((Widget widget) => widget is RichText); final TextSpan span = textWidget.text; @@ -106,15 +106,15 @@ void main() { }); testWidgets('Changing config - data', (WidgetTester tester) async { - await tester.pumpWidget(new Markdown(data: 'Data1')); + await tester.pumpWidget(const Markdown(data: 'Data1')); _expectTextStrings(tester.allWidgets, ['Data1']); final String stateBefore = _dumpRenderView(); - await tester.pumpWidget(new Markdown(data: 'Data1')); + await tester.pumpWidget(const Markdown(data: 'Data1')); final String stateAfter = _dumpRenderView(); expect(stateBefore, equals(stateAfter)); - await tester.pumpWidget(new Markdown(data: 'Data2')); + await tester.pumpWidget(const Markdown(data: 'Data2')); _expectTextStrings(tester.allWidgets, ['Data2']); }); From a103c54fc53edd80df1c808ada1d0874c90fd197 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Thu, 27 Apr 2017 14:19:01 -0700 Subject: [PATCH 062/121] BoxDecoration API changes: backgroundColor -> color et al (#9648) backgroundColor -> color backgroundImage -> image BackgroundImage -> DecorationImage --- lib/src/style_sheet.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/style_sheet.dart b/lib/src/style_sheet.dart index f8c0f387333..02851ce4fdc 100644 --- a/lib/src/style_sheet.dart +++ b/lib/src/style_sheet.dart @@ -66,12 +66,12 @@ class MarkdownStyleSheet { listIndent: 32.0, blockquotePadding: 8.0, blockquoteDecoration: new BoxDecoration( - backgroundColor: Colors.blue.shade100, + color: Colors.blue.shade100, borderRadius: new BorderRadius.circular(2.0) ), codeblockPadding: 8.0, codeblockDecoration: new BoxDecoration( - backgroundColor: Colors.grey.shade100, + color: Colors.grey.shade100, borderRadius: new BorderRadius.circular(2.0) ) ); @@ -103,12 +103,12 @@ class MarkdownStyleSheet { listIndent: 32.0, blockquotePadding: 8.0, blockquoteDecoration: new BoxDecoration( - backgroundColor: Colors.blue.shade100, + color: Colors.blue.shade100, borderRadius: new BorderRadius.circular(2.0) ), codeblockPadding: 8.0, codeblockDecoration: new BoxDecoration( - backgroundColor: Colors.grey.shade100, + color: Colors.grey.shade100, borderRadius: new BorderRadius.circular(2.0) ) ); From cf9e4414ba5dd343b774b9fbb6ede6c6c2e30543 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Thu, 4 May 2017 11:36:19 -0700 Subject: [PATCH 063/121] Add .gitignore and update pubspec.yaml --- .gitignore | 2 ++ pubspec.yaml | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..93d2a1eaede --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.packages +pubspec.lock diff --git a/pubspec.yaml b/pubspec.yaml index 117d7340236..428fe96c4d4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,8 +1,8 @@ name: flutter_markdown -description: A markdown renderer for Flutter. -version: 0.1.0 author: Flutter Authors -homepage: http://flutter.io +description: A markdown renderer for Flutter. +homepage: https://github.com/flutter/flutter_markdown +version: 0.0.9 dependencies: flutter: From 7fd68cbd86d017d851778153c861a9a7ef4e4548 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Thu, 4 May 2017 11:39:12 -0700 Subject: [PATCH 064/121] Add package:meta dependency --- pubspec.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 428fe96c4d4..4a285a0ab23 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,7 +7,8 @@ version: 0.0.9 dependencies: flutter: sdk: flutter - markdown: '^0.11.0' + markdown: ^0.11.0 + meta: ^1.0.5 string_scanner: ^1.0.0 dev_dependencies: From 6738a2a2d1268317a4a5f9f9270f4767752a1afb Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Thu, 4 May 2017 11:39:46 -0700 Subject: [PATCH 065/121] Add LICENSE --- LICENSE | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000000..e7892520aaa --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +// Copyright 2017 Google, Inc. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. From 58be3214f3d128db742abca43614624e75ac8710 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Thu, 4 May 2017 11:40:44 -0700 Subject: [PATCH 066/121] Add SDK constraint --- pubspec.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pubspec.yaml b/pubspec.yaml index 4a285a0ab23..826b7caf3dd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,3 +14,6 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + +environment: + sdk: '>=1.19.0 <2.0.0' From 21a4cd0394f72695eaa4be6b1c33f55c234438e6 Mon Sep 17 00:00:00 2001 From: Todd Volkert Date: Fri, 29 Sep 2017 14:27:40 -0700 Subject: [PATCH 067/121] Fix flutter_markdown_test given recent updates to Flutter (#5) --- lib/src/style_sheet.dart | 1 + test/flutter_markdown_test.dart | 78 ++++++++++++++++++++++----------- 2 files changed, 54 insertions(+), 25 deletions(-) diff --git a/lib/src/style_sheet.dart b/lib/src/style_sheet.dart index 02851ce4fdc..bb7be617ec5 100644 --- a/lib/src/style_sheet.dart +++ b/lib/src/style_sheet.dart @@ -45,6 +45,7 @@ class MarkdownStyleSheet { /// Creates a [MarkdownStyleSheet] from the [TextStyle]s in the provided [ThemeData]. factory MarkdownStyleSheet.fromTheme(ThemeData theme) { + assert(theme?.textTheme?.body1?.fontSize != null); return new MarkdownStyleSheet( a: const TextStyle(color: Colors.blue), p: theme.textTheme.body1, diff --git a/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart index 73d5ca57bf2..c64176be3a1 100644 --- a/test/flutter_markdown_test.dart +++ b/test/flutter_markdown_test.dart @@ -9,31 +9,39 @@ import 'package:flutter/widgets.dart'; import 'package:flutter/material.dart'; void main() { + TextTheme textTheme = new Typography(platform: TargetPlatform.android) + .black + .merge(new TextTheme(body1: new TextStyle(fontSize: 12.0))); + testWidgets('Simple string', (WidgetTester tester) async { - await tester.pumpWidget(const MarkdownBody(data: 'Hello')); + await tester.pumpWidget(_boilerplate(const MarkdownBody(data: 'Hello'))); final Iterable widgets = tester.allWidgets; - _expectWidgetTypes(widgets, [MarkdownBody, Column, RichText]); + _expectWidgetTypes( + widgets, [Directionality, MarkdownBody, Column, RichText]); _expectTextStrings(widgets, ['Hello']); }); testWidgets('Header', (WidgetTester tester) async { - await tester.pumpWidget(const MarkdownBody(data: '# Header')); + await tester.pumpWidget(_boilerplate(const MarkdownBody(data: '# Header'))); final Iterable widgets = tester.allWidgets; - _expectWidgetTypes(widgets, [MarkdownBody, Column, RichText]); + _expectWidgetTypes( + widgets, [Directionality, MarkdownBody, Column, RichText]); _expectTextStrings(widgets, ['Header']); }); testWidgets('Empty string', (WidgetTester tester) async { - await tester.pumpWidget(const MarkdownBody(data: '')); + await tester.pumpWidget(_boilerplate(const MarkdownBody(data: ''))); final Iterable widgets = tester.allWidgets; - _expectWidgetTypes(widgets, [MarkdownBody, Column]); + _expectWidgetTypes(widgets, [Directionality, MarkdownBody, Column]); }); testWidgets('Ordered list', (WidgetTester tester) async { - await tester.pumpWidget(const MarkdownBody(data: '1. Item 1\n1. Item 2\n2. Item 3')); + await tester.pumpWidget(_boilerplate( + const MarkdownBody(data: '1. Item 1\n1. Item 2\n2. Item 3'), + )); final Iterable widgets = tester.allWidgets; _expectTextStrings(widgets, [ @@ -47,7 +55,9 @@ void main() { }); testWidgets('Unordered list', (WidgetTester tester) async { - await tester.pumpWidget(const MarkdownBody(data: '- Item 1\n- Item 2\n- Item 3')); + await tester.pumpWidget( + _boilerplate(const MarkdownBody(data: '- Item 1\n- Item 2\n- Item 3')), + ); final Iterable widgets = tester.allWidgets; _expectTextStrings(widgets, [ @@ -61,10 +71,11 @@ void main() { }); testWidgets('Scrollable wrapping', (WidgetTester tester) async { - await tester.pumpWidget(const Markdown(data: '')); + await tester.pumpWidget(_boilerplate(const Markdown(data: ''))); final List widgets = tester.allWidgets.toList(); - _expectWidgetTypes(widgets.take(2), [ + _expectWidgetTypes(widgets.take(3), [ + Directionality, Markdown, ListView, ]); @@ -75,12 +86,15 @@ void main() { }); testWidgets('Links', (WidgetTester tester) async { - await tester.pumpWidget(const Markdown(data: '[Link Text](href)')); + await tester + .pumpWidget(_boilerplate(const Markdown(data: '[Link Text](href)'))); - final RichText textWidget = tester.allWidgets.firstWhere((Widget widget) => widget is RichText); + final RichText textWidget = + tester.allWidgets.firstWhere((Widget widget) => widget is RichText); final TextSpan span = textWidget.text; - expect(span.children[0].recognizer.runtimeType, equals(TapGestureRecognizer)); + expect( + span.children[0].recognizer.runtimeType, equals(TapGestureRecognizer)); }); testWidgets('HTML tag ignored ', (WidgetTester tester) async { @@ -90,7 +104,7 @@ void main() { ]; for (String mdLine in mdData) { - await tester.pumpWidget(new MarkdownBody(data: mdLine)); + await tester.pumpWidget(_boilerplate(new MarkdownBody(data: mdLine))); final Iterable widgets = tester.allWidgets; _expectTextStrings(widgets, ['Line 1', 'Line 2']); @@ -99,42 +113,48 @@ void main() { testWidgets('Less than', (WidgetTester tester) async { final String mdLine = 'Line 1 <\n\nc < c c\n\n< Line 2'; - await tester.pumpWidget(new MarkdownBody(data: mdLine)); + await tester.pumpWidget(_boilerplate(new MarkdownBody(data: mdLine))); final Iterable widgets = tester.allWidgets; - _expectTextStrings(widgets, ['Line 1 <','c < c c','< Line 2']); + _expectTextStrings( + widgets, ['Line 1 <', 'c < c c', '< Line 2']); }); testWidgets('Changing config - data', (WidgetTester tester) async { - await tester.pumpWidget(const Markdown(data: 'Data1')); + await tester.pumpWidget(_boilerplate(const Markdown(data: 'Data1'))); _expectTextStrings(tester.allWidgets, ['Data1']); final String stateBefore = _dumpRenderView(); - await tester.pumpWidget(const Markdown(data: 'Data1')); + await tester.pumpWidget(_boilerplate(const Markdown(data: 'Data1'))); final String stateAfter = _dumpRenderView(); expect(stateBefore, equals(stateAfter)); - await tester.pumpWidget(const Markdown(data: 'Data2')); + await tester.pumpWidget(_boilerplate(const Markdown(data: 'Data2'))); _expectTextStrings(tester.allWidgets, ['Data2']); }); testWidgets('Changing config - style', (WidgetTester tester) async { - final ThemeData theme = new ThemeData.light(); + final ThemeData theme = new ThemeData.light().copyWith(textTheme: textTheme); final MarkdownStyleSheet style1 = new MarkdownStyleSheet.fromTheme(theme); - final MarkdownStyleSheet style2 = new MarkdownStyleSheet.largeFromTheme(theme); + final MarkdownStyleSheet style2 = + new MarkdownStyleSheet.largeFromTheme(theme); expect(style1, isNot(style2)); - await tester.pumpWidget(new Markdown(data: '# Test', styleSheet: style1)); + await tester.pumpWidget( + _boilerplate(new Markdown(data: '# Test', styleSheet: style1)), + ); final RichText text1 = tester.widget(find.byType(RichText)); - await tester.pumpWidget(new Markdown(data: '# Test', styleSheet: style2)); + await tester.pumpWidget( + _boilerplate(new Markdown(data: '# Test', styleSheet: style2)), + ); final RichText text2 = tester.widget(find.byType(RichText)); expect(text1.text, isNot(text2.text)); }); testWidgets('Style equality', (WidgetTester tester) async { - final ThemeData theme = new ThemeData.light(); + final ThemeData theme = new ThemeData.light().copyWith(textTheme: textTheme); final MarkdownStyleSheet style1 = new MarkdownStyleSheet.fromTheme(theme); final MarkdownStyleSheet style2 = new MarkdownStyleSheet.fromTheme(theme); @@ -172,6 +192,14 @@ String _extractTextFromTextSpan(TextSpan span) { String _dumpRenderView() { return WidgetsBinding.instance.renderViewElement.toStringDeep().replaceAll( - new RegExp(r'SliverChildListDelegate#\d+', multiLine: true), 'SliverChildListDelegate' + new RegExp(r'SliverChildListDelegate#\d+', multiLine: true), + 'SliverChildListDelegate'); +} + +/// Wraps a widget with a left-to-right [Directionality] for tests. +Widget _boilerplate(Widget child) { + return new Directionality( + textDirection: TextDirection.ltr, + child: child, ); } From 3344aad7bf364e74fef479991b89270aa6d35115 Mon Sep 17 00:00:00 2001 From: Todd Volkert Date: Fri, 29 Sep 2017 15:47:27 -0700 Subject: [PATCH 068/121] Rev version (#6) --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 826b7caf3dd..7bc47c0153b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: flutter_markdown author: Flutter Authors description: A markdown renderer for Flutter. homepage: https://github.com/flutter/flutter_markdown -version: 0.0.9 +version: 0.0.10 dependencies: flutter: From 958f1ae2d635e3e8aa5a359d61ed15edbb2e134c Mon Sep 17 00:00:00 2001 From: Victor Choueiri Date: Sun, 29 Oct 2017 19:10:18 +0200 Subject: [PATCH 069/121] Add pub badge to README Adds the pub badge with version number/link --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index db83e6b73f3..25e5ef8de9a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Flutter Markdown +[![pub package](https://img.shields.io/pub/v/flutter_markdown.svg)](https://pub.dartlang.org/packages/flutter_markdown) A markdown renderer for Flutter. It supports the [original format](https://daringfireball.net/projects/markdown/), but no inline From f947f3eb1c178958445a9efc2fee7ab630578dcc Mon Sep 17 00:00:00 2001 From: Victor Choueiri Date: Thu, 2 Nov 2017 18:47:47 +0200 Subject: [PATCH 070/121] 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 --- lib/src/builder.dart | 26 +++++++--- test/flutter_markdown_test.dart | 87 +++++++++++++++++++++++++++++---- 2 files changed, 95 insertions(+), 18 deletions(-) diff --git a/lib/src/builder.dart b/lib/src/builder.dart index 7b91ba0bf40..c998e085398 100644 --- a/lib/src/builder.dart +++ b/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/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart index c64176be3a1..33fb3f44453 100644 --- a/test/flutter_markdown_test.dart +++ b/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 { From af15481957e284e20d064bd28325aa7b59ee42d2 Mon Sep 17 00:00:00 2001 From: Michael Thomsen Date: Fri, 3 Nov 2017 10:42:04 +0100 Subject: [PATCH 071/121] Add example and tests for images --- CHANGELOG.md | 8 ++++++++ example/demo.dart | 29 +++++++++++++++++++++++++---- pubspec.yaml | 4 ++-- test/flutter_markdown_test.dart | 14 +++++++++++++- 4 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000000..386dbeab49d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +## 0.1.0 + +* Roll the dependency on `markdown` to 1.0.0 +* Add a test and example for image links + +## 0.0.9 + +* First published version diff --git a/example/demo.dart b/example/demo.dart index 41336ab84d4..033ec1086cf 100644 --- a/example/demo.dart +++ b/example/demo.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; -const String _kMarkdownData = """# Markdown Example +const String _markdownData = """# Markdown Example Markdown allows you to easily include formatted text, images, and even formatted Dart code in your app. ## Styling @@ -18,12 +18,33 @@ Style text as _italic_, __bold__, or `inline code`. ## Links You can use [hyperlinks](hyperlink) in markdown -## Code blocks -Formatted Dart code looks really pretty too. This is an example of how to create your own Markdown widget: +## Images + +You can include images: + +![Flutter logo](https://flutter.io/images/flutter-mark-square-100.png) + +## Markdown widget + +This is an example of how to create your own Markdown widget: new Markdown(data: 'Hello _world_!'); +## Code blocks +Formatted Dart code looks really pretty too: + +``` +void main() { + runApp(new MaterialApp( + home: new Scaffold( + body: new Markdown(data: markdownData) + ) + )); +} + Enjoy! +``` + """; void main() { @@ -31,7 +52,7 @@ void main() { title: "Markdown Demo", home: new Scaffold( appBar: new AppBar(title: const Text('Markdown Demo')), - body: const Markdown(data: _kMarkdownData) + body: const Markdown(data: _markdownData) ) )); } diff --git a/pubspec.yaml b/pubspec.yaml index 7bc47c0153b..0541538388e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,12 +2,12 @@ name: flutter_markdown author: Flutter Authors description: A markdown renderer for Flutter. homepage: https://github.com/flutter/flutter_markdown -version: 0.0.10 +version: 0.1.0 dependencies: flutter: sdk: flutter - markdown: ^0.11.0 + markdown: ^1.0.0 meta: ^1.0.5 string_scanner: ^1.0.0 diff --git a/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart index c64176be3a1..6818f6dbe2b 100644 --- a/test/flutter_markdown_test.dart +++ b/test/flutter_markdown_test.dart @@ -90,13 +90,25 @@ void main() { .pumpWidget(_boilerplate(const Markdown(data: '[Link Text](href)'))); final RichText textWidget = - tester.allWidgets.firstWhere((Widget widget) => widget is RichText); + tester.allWidgets.firstWhere((Widget widget) => widget is RichText); final TextSpan span = textWidget.text; expect( span.children[0].recognizer.runtimeType, equals(TapGestureRecognizer)); }); + testWidgets('Image links', (WidgetTester tester) async { + await tester + .pumpWidget(_boilerplate(const Markdown(data: '![alt](img#50x50)'))); + + final Image image = + tester.allWidgets.firstWhere((Widget widget) => widget is Image); + final NetworkImage networkImage = image.image; + expect(networkImage.url, 'img'); + expect(image.width, 50); + expect(image.height, 50); + }); + testWidgets('HTML tag ignored ', (WidgetTester tester) async { final List mdData = [ 'Line 1\n

HTML content

\nLine 2', From 01962f61f614a6b9619f27b89aee6fb42feb0462 Mon Sep 17 00:00:00 2001 From: Michael Thomsen Date: Fri, 3 Nov 2017 10:49:56 +0100 Subject: [PATCH 072/121] Add onTap fix from yesterday to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 386dbeab49d..023fd9b2b90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ * Roll the dependency on `markdown` to 1.0.0 * Add a test and example for image links +* Fix the `onTap` callback on hyperlinks ## 0.0.9 From 2a230b924adeb6aaa0e729dc5632d5376ff9dbbe Mon Sep 17 00:00:00 2001 From: Michael Thomsen Date: Fri, 3 Nov 2017 14:04:46 +0100 Subject: [PATCH 073/121] Fix bad merge --- test/flutter_markdown_test.dart | 89 +++++++++++++++++++++++++++++---- 1 file changed, 78 insertions(+), 11 deletions(-) diff --git a/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart index 6818f6dbe2b..327e9be741d 100644 --- a/test/flutter_markdown_test.dart +++ b/test/flutter_markdown_test.dart @@ -85,18 +85,85 @@ 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('Image links', (WidgetTester tester) async { await tester .pumpWidget(_boilerplate(const Markdown(data: '![alt](img#50x50)'))); From fc02628204f1015450e025431a9922e3a7844987 Mon Sep 17 00:00:00 2001 From: Victor Choueiri Date: Sat, 4 Nov 2017 04:08:00 +0200 Subject: [PATCH 074/121] Update .gitignore --- .gitignore | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 93d2a1eaede..9f152ab3c07 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ +.DS_Store +.atom/ +.idea .packages -pubspec.lock +.pub/ +packages +pubspec.lock \ No newline at end of file From 66bfe880229108670a36809734d90f269014d521 Mon Sep 17 00:00:00 2001 From: Michael Thomsen Date: Mon, 6 Nov 2017 10:27:35 +0100 Subject: [PATCH 075/121] Review feedback --- example/demo.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/example/demo.dart b/example/demo.dart index 033ec1086cf..8d3be041dfc 100644 --- a/example/demo.dart +++ b/example/demo.dart @@ -22,7 +22,7 @@ You can use [hyperlinks](hyperlink) in markdown You can include images: -![Flutter logo](https://flutter.io/images/flutter-mark-square-100.png) +![Flutter logo](https://flutter.io/images/flutter-mark-square-100.png#100x100) ## Markdown widget @@ -41,10 +41,9 @@ void main() { ) )); } - -Enjoy! ``` +Enjoy! """; void main() { From 03adc52fe7a940b07088aa5ce05d99d1a5ad3185 Mon Sep 17 00:00:00 2001 From: Michael Thomsen Date: Mon, 6 Nov 2017 11:04:11 +0100 Subject: [PATCH 076/121] Update .gitignore Add missing newline --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9f152ab3c07..75e965865ed 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ .packages .pub/ packages -pubspec.lock \ No newline at end of file +pubspec.lock From d262fe312e22f53c9afea8939ee7251dac6854ae Mon Sep 17 00:00:00 2001 From: Michael Thomsen Date: Mon, 6 Nov 2017 19:21:11 +0100 Subject: [PATCH 077/121] Add Travis job for flutter test (#16) * Travis job for flutter test * Add travis badge to readme --- .travis.yml | 7 +++++++ README.md | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000000..191da0afb91 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +os: linux +before_script: + - git clone https://github.com/flutter/flutter.git --depth 1 + - export PATH=`pwd`/flutter/bin:`pwd`/flutter/bin/cache/dart-sdk/bin:$PATH + - flutter doctor +script: + - flutter test diff --git a/README.md b/README.md index 25e5ef8de9a..c5a5ed9e064 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Flutter Markdown -[![pub package](https://img.shields.io/pub/v/flutter_markdown.svg)](https://pub.dartlang.org/packages/flutter_markdown) +[![pub package](https://img.shields.io/pub/v/flutter_markdown.svg)](https://pub.dartlang.org/packages/flutter_markdown) +[![Build Status](https://travis-ci.org/flutter/flutter_markdown.svg?branch=master)](https://travis-ci.org/flutter/flutter_markdown) + A markdown renderer for Flutter. It supports the [original format](https://daringfireball.net/projects/markdown/), but no inline From 8d61e077c15f241b86704d47feeae75105fac2a4 Mon Sep 17 00:00:00 2001 From: Victor Choueiri Date: Mon, 6 Nov 2017 22:30:41 +0200 Subject: [PATCH 078/121] Fix img text style (#12) * Add a TextTheme for text in 'img' tags * Add test for TextStyle --- lib/src/style_sheet.dart | 13 ++++++++++++- test/flutter_markdown_test.dart | 11 +++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/src/style_sheet.dart b/lib/src/style_sheet.dart index bb7be617ec5..5505f7d1eea 100644 --- a/lib/src/style_sheet.dart +++ b/lib/src/style_sheet.dart @@ -20,6 +20,7 @@ class MarkdownStyleSheet { this.em, this.strong, this.blockquote, + this.img, this.blockSpacing, this.listIndent, this.blockquotePadding, @@ -40,7 +41,8 @@ class MarkdownStyleSheet { 'h6': h6, 'em': em, 'strong': strong, - 'blockquote': blockquote + 'blockquote': blockquote, + 'img': img, }; /// Creates a [MarkdownStyleSheet] from the [TextStyle]s in the provided [ThemeData]. @@ -63,6 +65,7 @@ class MarkdownStyleSheet { em: const TextStyle(fontStyle: FontStyle.italic), strong: const TextStyle(fontWeight: FontWeight.bold), blockquote: theme.textTheme.body1, + img: theme.textTheme.body1, blockSpacing: 8.0, listIndent: 32.0, blockquotePadding: 8.0, @@ -100,6 +103,7 @@ class MarkdownStyleSheet { em: const TextStyle(fontStyle: FontStyle.italic), strong: const TextStyle(fontWeight: FontWeight.bold), blockquote: theme.textTheme.body1, + img: theme.textTheme.body1, blockSpacing: 8.0, listIndent: 32.0, blockquotePadding: 8.0, @@ -130,6 +134,7 @@ class MarkdownStyleSheet { TextStyle em, TextStyle strong, TextStyle blockquote, + TextStyle img, double blockSpacing, double listIndent, double blockquotePadding, @@ -150,6 +155,7 @@ class MarkdownStyleSheet { em: em != null ? em : this.em, strong: strong != null ? strong : this.strong, blockquote: blockquote != null ? blockquote : this.blockquote, + img: img != null ? img: this.img, blockSpacing: blockSpacing != null ? blockSpacing : this.blockSpacing, listIndent: listIndent != null ? listIndent : this.listIndent, blockquotePadding: blockquotePadding != null ? blockquotePadding : this.blockquotePadding, @@ -195,6 +201,9 @@ class MarkdownStyleSheet { /// The [TextStyle] to use for `blockquote` elements. final TextStyle blockquote; + /// The [TextStyle] to use for `img` elements. + final TextStyle img; + /// The amount of vertical space to use between block-level elements. final double blockSpacing; @@ -236,6 +245,7 @@ class MarkdownStyleSheet { && typedOther.em == em && typedOther.strong == strong && typedOther.blockquote == blockquote + && typedOther.img == img && typedOther.blockSpacing == blockSpacing && typedOther.listIndent == listIndent && typedOther.blockquotePadding == blockquotePadding @@ -259,6 +269,7 @@ class MarkdownStyleSheet { em, strong, blockquote, + img, blockSpacing, listIndent, blockquotePadding, diff --git a/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart index 327e9be741d..c06ada0df8d 100644 --- a/test/flutter_markdown_test.dart +++ b/test/flutter_markdown_test.dart @@ -176,6 +176,17 @@ void main() { expect(image.height, 50); }); + testWidgets('Image text', (WidgetTester tester) async { + await tester + .pumpWidget(_boilerplate(const Markdown(data: 'Hello ![alt](img#50x50)'))); + + final RichText richText = + tester.allWidgets.firstWhere((Widget widget) => widget is RichText); + TextSpan textSpan = richText.text; + expect(textSpan.children[0].text, 'Hello '); + expect(textSpan.style, isNotNull); + }); + testWidgets('HTML tag ignored ', (WidgetTester tester) async { final List mdData = [ 'Line 1\n

HTML content

\nLine 2', From 02f86379ecd3bc182b5e820b353f426fdf6a8a70 Mon Sep 17 00:00:00 2001 From: Victor Choueiri Date: Fri, 8 Dec 2017 21:28:04 +0200 Subject: [PATCH 079/121] Update .gitignore (#18) Add vscode-related gitignore rules --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 75e965865ed..c3f60f2445e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,7 @@ .pub/ packages pubspec.lock + +# Visual Studio Code related +.vscode/ +.history/ From 5ed7502309127297af089911b78e414d48005413 Mon Sep 17 00:00:00 2001 From: Victor Choueiri Date: Sat, 9 Dec 2017 00:40:42 +0200 Subject: [PATCH 080/121] Add mock httpClient for image tests (#17) * Add mock httpClient for image tests * Rename const _transparentImage * Add group for images tests * Change link test names --- test/flutter_markdown_test.dart | 69 ++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart index c06ada0df8d..268d140390d 100644 --- a/test/flutter_markdown_test.dart +++ b/test/flutter_markdown_test.dart @@ -7,6 +7,9 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show createHttpClient; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart' as http; void main() { TextTheme textTheme = new Typography(platform: TargetPlatform.android) @@ -86,7 +89,7 @@ void main() { }); group('Links', () { - testWidgets('Single link', (WidgetTester tester) async { + testWidgets('should be tappable', (WidgetTester tester) async { String tapResult; await tester.pumpWidget(_boilerplate(new Markdown( data: '[Link Text](href)', @@ -106,7 +109,7 @@ void main() { expect(tapResult, 'href'); }); - testWidgets('Link with nested code', (WidgetTester tester) async { + testWidgets('should work with nested elements', (WidgetTester tester) async { final List tapResults = []; await tester.pumpWidget(_boilerplate(new Markdown( data: '[Link `with nested code` Text](href)', @@ -132,7 +135,7 @@ void main() { expect(tapResults, everyElement('href')); }); - testWidgets('Multiple links', (WidgetTester tester) async { + testWidgets('should work next to other links', (WidgetTester tester) async { final List tapResults = []; await tester.pumpWidget(_boilerplate(new Markdown( @@ -164,27 +167,33 @@ void main() { }); }); - testWidgets('Image links', (WidgetTester tester) async { - await tester - .pumpWidget(_boilerplate(const Markdown(data: '![alt](img#50x50)'))); - - final Image image = - tester.allWidgets.firstWhere((Widget widget) => widget is Image); - final NetworkImage networkImage = image.image; - expect(networkImage.url, 'img'); - expect(image.width, 50); - expect(image.height, 50); - }); + group('Images', () { + setUpAll(() { + createHttpClient = createMockImageHttpClient; + }); + + testWidgets('should work with a link', (WidgetTester tester) async { + await tester + .pumpWidget(_boilerplate(const Markdown(data: '![alt](img#50x50)'))); + + final Image image = + tester.allWidgets.firstWhere((Widget widget) => widget is Image); + final NetworkImage networkImage = image.image; + expect(networkImage.url, 'img'); + expect(image.width, 50); + expect(image.height, 50); + }); - testWidgets('Image text', (WidgetTester tester) async { - await tester - .pumpWidget(_boilerplate(const Markdown(data: 'Hello ![alt](img#50x50)'))); + testWidgets('should show properly next to text', (WidgetTester tester) async { + await tester + .pumpWidget(_boilerplate(const Markdown(data: 'Hello ![alt](img#50x50)'))); - final RichText richText = - tester.allWidgets.firstWhere((Widget widget) => widget is RichText); - TextSpan textSpan = richText.text; - expect(textSpan.children[0].text, 'Hello '); - expect(textSpan.style, isNotNull); + final RichText richText = + tester.allWidgets.firstWhere((Widget widget) => widget is RichText); + TextSpan textSpan = richText.text; + expect(textSpan.children[0].text, 'Hello '); + expect(textSpan.style, isNotNull); + }); }); testWidgets('HTML tag ignored ', (WidgetTester tester) async { @@ -293,3 +302,19 @@ Widget _boilerplate(Widget child) { child: child, ); } + +// Returns a mock HTTP client that responds with an image to all requests. +ValueGetter createMockImageHttpClient = () { + return new http.MockClient((http.BaseRequest request) { + return new Future.value( + new http.Response.bytes(_transparentImage, 200, request: request)); + }); +}; + +const List _transparentImage = const [ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, + 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, + 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, + 0x41, 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, + 0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, +]; From d810567192db9efe6c20d3c105f99a328e7a2536 Mon Sep 17 00:00:00 2001 From: Victor Choueiri Date: Sat, 9 Dec 2017 01:32:04 +0200 Subject: [PATCH 081/121] Inline images (#14) * Add inline image elements - Remove 'img' tag from the list of block tags. - Use a Wrap widget to layout RichText and Image widgets. - Convert _inlineElements list to accept Objects so both TextSpan and Image widgets can be added. * Update tests * Fix Image as child of text node * Modify _InlineElement to inherit styling and merge spans * Update tests * Update tests * Add test for nested inline images * Refactor common _addParentInlineIfNeeded code * Document _InlineElement class * Document _InlineElement TextStyle merging * Use dummy image url in tests --- lib/src/builder.dart | 173 +++++++++++++++++++++----------- test/flutter_markdown_test.dart | 43 +++++--- 2 files changed, 143 insertions(+), 73 deletions(-) diff --git a/lib/src/builder.dart b/lib/src/builder.dart index c998e085398..3d314f4b150 100644 --- a/lib/src/builder.dart +++ b/lib/src/builder.dart @@ -18,7 +18,6 @@ final Set _kBlockTags = new Set.from([ 'h6', 'li', 'blockquote', - 'img', 'pre', 'ol', 'ul', @@ -38,8 +37,27 @@ class _BlockElement { int nextListIndex = 0; } +/// A collection of widgets that should be placed adjacent to (inline with) +/// other inline elements in the same parent block. +/// +/// Inline elements can be textual (a/em/strong) represented by [RichText] +/// widgets or images (img) represented by [Image.network] widgets. +/// +/// Inline elements can be nested within other inline elements, inheriting their +/// parent's style along with the style of the block they are in. +/// +/// When laying out inline widgets, first, any adjacent RichText widgets are +/// merged, then, all inline widgets are enclosed in a parent [Wrap] widget. class _InlineElement { - final List children = []; + _InlineElement(this.tag, {this.style}); + + final String tag; + + /// Created by merging the style defined for this element's [tag] in the + /// delegate's [MarkdownStyleSheet] with the style of its parent. + final TextStyle style; + + final List children = []; } /// A delegate used by [MarkdownBuilder] to control the widgets it creates. @@ -86,14 +104,13 @@ class MarkdownBuilder implements md.NodeVisitor { _linkHandlers.clear(); _blocks.add(new _BlockElement(null)); - _inlines.add(new _InlineElement()); for (md.Node node in nodes) { assert(_blocks.length == 1); node.accept(this); } - assert(_inlines.single.children.isEmpty); + assert(_inlines.isEmpty); return _blocks.single.children; } @@ -101,13 +118,18 @@ class MarkdownBuilder implements md.NodeVisitor { void visitText(md.Text text) { if (_blocks.last.tag == null) // Don't allow text directly under the root. return; + + _addParentInlineIfNeeded(_blocks.last.tag); + final TextSpan span = _blocks.last.tag == 'pre' ? delegate.formatText(styleSheet, text.text) : new TextSpan( + style: _inlines.last.style, text: text.text, recognizer: _linkHandlers.isNotEmpty ? _linkHandlers.last : null, ); - _inlines.last.children.add(span); + + _inlines.last.children.add(new RichText(text: span)); } @override @@ -119,7 +141,13 @@ class MarkdownBuilder implements md.NodeVisitor { _listIndents.add(tag); _blocks.add(new _BlockElement(tag)); } else { - _inlines.add(new _InlineElement()); + _addParentInlineIfNeeded(_blocks.last.tag); + + TextStyle parentStyle = _inlines.last.style; + _inlines.add(new _InlineElement( + tag, + style: parentStyle.merge(styleSheet.styles[tag]), + )); } if (tag == 'a') { @@ -138,51 +166,48 @@ class MarkdownBuilder implements md.NodeVisitor { final _BlockElement current = _blocks.removeLast(); Widget child; - if (tag == 'img') { - child = _buildImage(element.attributes['src']); + + if (current.children.isNotEmpty) { + child = new Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: current.children, + ); } else { - if (current.children.isNotEmpty) { - child = new Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: current.children, - ); - } else { - child = const SizedBox(); - } + child = const SizedBox(); + } - if (_isListTag(tag)) { - assert(_listIndents.isNotEmpty); - _listIndents.removeLast(); - } else if (tag == 'li') { - if (_listIndents.isNotEmpty) { - child = new Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - new SizedBox( - width: styleSheet.listIndent, - child: _buildBullet(_listIndents.last), - ), - new Expanded(child: child) - ], - ); - } - } else if (tag == 'blockquote') { - child = new DecoratedBox( - decoration: styleSheet.blockquoteDecoration, - child: new Padding( - padding: new EdgeInsets.all(styleSheet.blockquotePadding), - child: child, - ), - ); - } else if (tag == 'pre') { - child = new DecoratedBox( - decoration: styleSheet.codeblockDecoration, - child: new Padding( - padding: new EdgeInsets.all(styleSheet.codeblockPadding), - child: child, - ), + if (_isListTag(tag)) { + assert(_listIndents.isNotEmpty); + _listIndents.removeLast(); + } else if (tag == 'li') { + if (_listIndents.isNotEmpty) { + child = new Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + new SizedBox( + width: styleSheet.listIndent, + child: _buildBullet(_listIndents.last), + ), + new Expanded(child: child) + ], ); } + } else if (tag == 'blockquote') { + child = new DecoratedBox( + decoration: styleSheet.blockquoteDecoration, + child: new Padding( + padding: new EdgeInsets.all(styleSheet.blockquotePadding), + child: child, + ), + ); + } else if (tag == 'pre') { + child = new DecoratedBox( + decoration: styleSheet.codeblockDecoration, + child: new Padding( + padding: new EdgeInsets.all(styleSheet.codeblockPadding), + child: child, + ), + ); } _addBlockChild(child); @@ -190,15 +215,15 @@ class MarkdownBuilder implements md.NodeVisitor { final _InlineElement current = _inlines.removeLast(); final _InlineElement parent = _inlines.last; - if (current.children.isNotEmpty) { - parent.children.add(new TextSpan( - style: styleSheet.styles[tag], - children: current.children, - )); + if (tag == 'img') { + // create an image widget for this image + current.children.add(_buildImage(element.attributes['src'])); + } else if (tag == 'a') { + _linkHandlers.removeLast(); + } - if (tag == 'a') { - _linkHandlers.removeLast(); - } + if (current.children.isNotEmpty) { + parent.children.addAll(current.children); } } } @@ -233,6 +258,15 @@ class MarkdownBuilder implements md.NodeVisitor { ); } + void _addParentInlineIfNeeded(String tag) { + if (_inlines.isEmpty) { + _inlines.add(new _InlineElement( + tag, + style: styleSheet.styles[tag], + )); + } + } + void _addBlockChild(Widget child) { final _BlockElement parent = _blocks.last; if (parent.children.isNotEmpty) @@ -242,12 +276,35 @@ class MarkdownBuilder implements md.NodeVisitor { } void _addAnonymousBlockIfNeeded(TextStyle style) { + if (_inlines.isEmpty) { + return; + } + final _InlineElement inline = _inlines.single; if (inline.children.isNotEmpty) { - final TextSpan span = new TextSpan(style: style, children: inline.children); - _addBlockChild(new RichText(text: span)); + List mergedInlines = _mergeInlineChildren(inline); + final Wrap wrap = new Wrap(children: mergedInlines); + _addBlockChild(wrap); _inlines.clear(); - _inlines.add(new _InlineElement()); } } + + /// Merges adjacent [TextSpan] children of the given [_InlineElement] + List _mergeInlineChildren(_InlineElement inline) { + List mergedTexts = []; + for (Widget child in inline.children) { + if (mergedTexts.isNotEmpty && mergedTexts.last is RichText && child is RichText) { + RichText previous = mergedTexts.removeLast(); + List children = previous.text.children != null + ? new List.from(previous.text.children) + : [previous.text]; + children.add(child.text); + TextSpan mergedSpan = new TextSpan(children: children); + mergedTexts.add(new RichText(text: mergedSpan)); + } else { + mergedTexts.add(child); + } + } + return mergedTexts; + } } diff --git a/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart index 268d140390d..3900041228e 100644 --- a/test/flutter_markdown_test.dart +++ b/test/flutter_markdown_test.dart @@ -21,7 +21,7 @@ void main() { final Iterable widgets = tester.allWidgets; _expectWidgetTypes( - widgets, [Directionality, MarkdownBody, Column, RichText]); + widgets, [Directionality, MarkdownBody, Column, Wrap, RichText]); _expectTextStrings(widgets, ['Hello']); }); @@ -30,7 +30,7 @@ void main() { final Iterable widgets = tester.allWidgets; _expectWidgetTypes( - widgets, [Directionality, MarkdownBody, Column, RichText]); + widgets, [Directionality, MarkdownBody, Column, Wrap, RichText]); _expectTextStrings(widgets, ['Header']); }); @@ -100,12 +100,10 @@ void main() { tester.allWidgets.firstWhere((Widget widget) => widget is RichText); final TextSpan span = textWidget.text; - (span.children[0].children[0].recognizer as TapGestureRecognizer).onTap(); + (span.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(span.children, null); + expect(span.recognizer.runtimeType, equals(TapGestureRecognizer)); expect(tapResult, 'href'); }); @@ -128,8 +126,8 @@ void main() { return true; }); - expect(span.children.length, 1); - expect(span.children[0].children.length, 3); + expect(span.children.length, 3); + expect(gestureRecognizerTypes.length, 3); expect(gestureRecognizerTypes, everyElement(TapGestureRecognizer)); expect(tapResults.length, 3); expect(tapResults, everyElement('href')); @@ -155,12 +153,7 @@ void main() { 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'])); @@ -172,6 +165,26 @@ void main() { createHttpClient = createMockImageHttpClient; }); + testWidgets('should not interupt styling', (WidgetTester tester) async { + await tester.pumpWidget(_boilerplate(const Markdown( + data:'_textbefore ![alt](img) textafter_', + ))); + + final RichText firstTextWidget = + tester.allWidgets.firstWhere((Widget widget) => widget is RichText); + final Image image = + tester.allWidgets.firstWhere((Widget widget) => widget is Image); + final NetworkImage networkImage = image.image; + final RichText secondTextWidget = + tester.allWidgets.lastWhere((Widget widget) => widget is RichText); + + expect(firstTextWidget.text.text, 'textbefore '); + expect(firstTextWidget.text.style.fontStyle, FontStyle.italic); + expect(networkImage.url,'img'); + expect(secondTextWidget.text.text, ' textafter'); + expect(secondTextWidget.text.style.fontStyle, FontStyle.italic); + }); + testWidgets('should work with a link', (WidgetTester tester) async { await tester .pumpWidget(_boilerplate(const Markdown(data: '![alt](img#50x50)'))); @@ -191,7 +204,7 @@ void main() { final RichText richText = tester.allWidgets.firstWhere((Widget widget) => widget is RichText); TextSpan textSpan = richText.text; - expect(textSpan.children[0].text, 'Hello '); + expect(textSpan.text, 'Hello '); expect(textSpan.style, isNotNull); }); }); From ffa0157a5da60f8de6e07b0fc83fd655b16ed892 Mon Sep 17 00:00:00 2001 From: Michael Thomsen Date: Mon, 18 Dec 2017 16:21:41 +0100 Subject: [PATCH 082/121] Add support for local image paths in Img tags --- CHANGELOG.md | 6 ++++++ lib/src/builder.dart | 14 ++++++++++++-- lib/src/widget.dart | 16 +++++++++++++++- pubspec.yaml | 2 +- 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 023fd9b2b90..0a3ba9f64f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.1.1 + +* Add support for local file paths in image links. Make sure to set the + `imageDirectory` property to specify the base directory containing the image + files. + ## 0.1.0 * Roll the dependency on `markdown` to 1.0.0 diff --git a/lib/src/builder.dart b/lib/src/builder.dart index 3d314f4b150..3bf890f75a5 100644 --- a/lib/src/builder.dart +++ b/lib/src/builder.dart @@ -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:io'; + import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; import 'package:markdown/markdown.dart' as md; @@ -80,7 +82,7 @@ abstract class MarkdownBuilderDelegate { /// * [Markdown], which is a widget that parses and displays Markdown. class MarkdownBuilder implements md.NodeVisitor { /// Creates an object that builds a [Widget] tree from parsed Markdown. - MarkdownBuilder({ this.delegate, this.styleSheet }); + MarkdownBuilder({ this.delegate, this.styleSheet, this.imageDirectory }); /// A delegate that controls how link and `pre` elements behave. final MarkdownBuilderDelegate delegate; @@ -88,6 +90,9 @@ class MarkdownBuilder implements md.NodeVisitor { /// Defines which [TextStyle] objects to use for each type of element. final MarkdownStyleSheet styleSheet; + /// The base directory holding images referenced by Img tags with local file paths. + final Directory imageDirectory; + final List _listIndents = []; final List<_BlockElement> _blocks = <_BlockElement>[]; final List<_InlineElement> _inlines = <_InlineElement>[]; @@ -244,7 +249,12 @@ class MarkdownBuilder implements md.NodeVisitor { } } - return new Image.network(path, width: width, height: height); + if (path.startsWith('http')) { + return new Image.network(path, width: width, height: height); + } else { + String filePath = (imageDirectory == null ? path : imageDirectory.path + '/' + path); + return new Image.file(new File(filePath), width: width, height: height); + } } Widget _buildBullet(String listTag) { diff --git a/lib/src/widget.dart b/lib/src/widget.dart index 974b2cde36e..fc97cdaee5d 100644 --- a/lib/src/widget.dart +++ b/lib/src/widget.dart @@ -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:io'; + import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:markdown/markdown.dart' as md; @@ -43,6 +45,7 @@ abstract class MarkdownWidget extends StatefulWidget { this.styleSheet, this.syntaxHighlighter, this.onTapLink, + this.imageDirectory, }) : assert(data != null), super(key: key); @@ -62,6 +65,9 @@ abstract class MarkdownWidget extends StatefulWidget { /// Called when the user taps a link. final MarkdownTapLinkCallback onTapLink; + /// The base directory holding images referenced by Img tags with local file paths. + final Directory imageDirectory; + /// Subclasses should override this function to display the given children, /// which are the parsed representation of [data]. @protected @@ -103,7 +109,11 @@ class _MarkdownWidgetState extends State implements MarkdownBuil // TODO: This can be optimized by doing the split and removing \r at the same time final List lines = widget.data.replaceAll('\r\n', '\n').split('\n'); final md.Document document = new md.Document(); - final MarkdownBuilder builder = new MarkdownBuilder(delegate: this, styleSheet: styleSheet); + final MarkdownBuilder builder = new MarkdownBuilder( + delegate: this, + styleSheet: styleSheet, + imageDirectory: widget.imageDirectory, + ); _children = builder.build(document.parseLines(lines)); } @@ -155,12 +165,14 @@ class MarkdownBody extends MarkdownWidget { MarkdownStyleSheet styleSheet, SyntaxHighlighter syntaxHighlighter, MarkdownTapLinkCallback onTapLink, + Directory imageDirectory, }) : super( key: key, data: data, styleSheet: styleSheet, syntaxHighlighter: syntaxHighlighter, onTapLink: onTapLink, + imageDirectory: imageDirectory, ); @override @@ -191,6 +203,7 @@ class Markdown extends MarkdownWidget { MarkdownStyleSheet styleSheet, SyntaxHighlighter syntaxHighlighter, MarkdownTapLinkCallback onTapLink, + Directory imageDirectory, this.padding: const EdgeInsets.all(16.0), }) : super( key: key, @@ -198,6 +211,7 @@ class Markdown extends MarkdownWidget { styleSheet: styleSheet, syntaxHighlighter: syntaxHighlighter, onTapLink: onTapLink, + imageDirectory: imageDirectory, ); /// The amount of space by which to inset the children. diff --git a/pubspec.yaml b/pubspec.yaml index 0541538388e..2df97d34096 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: flutter_markdown author: Flutter Authors description: A markdown renderer for Flutter. homepage: https://github.com/flutter/flutter_markdown -version: 0.1.0 +version: 0.1.1 dependencies: flutter: From 6644f2e0594f973a671c74372090b3f2ad9f2b95 Mon Sep 17 00:00:00 2001 From: Michael Thomsen Date: Mon, 18 Dec 2017 16:26:17 +0100 Subject: [PATCH 083/121] Fix tests --- test/flutter_markdown_test.dart | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart index 3900041228e..c57a04c3302 100644 --- a/test/flutter_markdown_test.dart +++ b/test/flutter_markdown_test.dart @@ -167,7 +167,7 @@ void main() { testWidgets('should not interupt styling', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate(const Markdown( - data:'_textbefore ![alt](img) textafter_', + data:'_textbefore ![alt](http://img) textafter_', ))); final RichText firstTextWidget = @@ -180,19 +180,31 @@ void main() { expect(firstTextWidget.text.text, 'textbefore '); expect(firstTextWidget.text.style.fontStyle, FontStyle.italic); - expect(networkImage.url,'img'); + expect(networkImage.url,'http://img'); expect(secondTextWidget.text.text, ' textafter'); expect(secondTextWidget.text.style.fontStyle, FontStyle.italic); }); testWidgets('should work with a link', (WidgetTester tester) async { await tester - .pumpWidget(_boilerplate(const Markdown(data: '![alt](img#50x50)'))); + .pumpWidget(_boilerplate(const Markdown(data: '![alt](https://img#50x50)'))); final Image image = tester.allWidgets.firstWhere((Widget widget) => widget is Image); final NetworkImage networkImage = image.image; - expect(networkImage.url, 'img'); + expect(networkImage.url, 'https://img'); + expect(image.width, 50); + expect(image.height, 50); + }); + + testWidgets('should work with local image files', (WidgetTester tester) async { + await tester + .pumpWidget(_boilerplate(const Markdown(data: '![alt](img.png#50x50)'))); + + final Image image = + tester.allWidgets.firstWhere((Widget widget) => widget is Image); + final FileImage fileImage = image.image; + expect(fileImage.file.path, 'img.png'); expect(image.width, 50); expect(image.height, 50); }); From e36ddc252b80fe23557eb76ff0c5e514ad151166 Mon Sep 17 00:00:00 2001 From: Michael Thomsen Date: Thu, 21 Dec 2017 09:36:02 +0100 Subject: [PATCH 084/121] Fix http parsing bug --- lib/src/builder.dart | 10 +++++++--- test/flutter_markdown_test.dart | 9 +++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/src/builder.dart b/lib/src/builder.dart index 3bf890f75a5..18bf8b70dc2 100644 --- a/lib/src/builder.dart +++ b/lib/src/builder.dart @@ -7,6 +7,7 @@ import 'dart:io'; import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; import 'package:markdown/markdown.dart' as md; +import 'package:path/path.dart' as p; import 'style_sheet.dart'; @@ -249,10 +250,13 @@ class MarkdownBuilder implements md.NodeVisitor { } } - if (path.startsWith('http')) { - return new Image.network(path, width: width, height: height); + Uri uri = Uri.parse(path); + if (uri.scheme == 'http' || uri.scheme == 'https') { + return new Image.network(uri.toString(), width: width, height: height); } else { - String filePath = (imageDirectory == null ? path : imageDirectory.path + '/' + path); + String filePath = (imageDirectory == null + ? uri.toFilePath() + : p.join(imageDirectory.path, uri.toFilePath())); return new Image.file(new File(filePath), width: width, height: height); } } diff --git a/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart index c57a04c3302..b6ee532db3c 100644 --- a/test/flutter_markdown_test.dart +++ b/test/flutter_markdown_test.dart @@ -197,6 +197,15 @@ void main() { expect(image.height, 50); }); + testWidgets('local files should be files', (WidgetTester tester) async { + await tester + .pumpWidget(_boilerplate(const Markdown(data: '![alt](http.png)'))); + + final Image image = + tester.allWidgets.firstWhere((Widget widget) => widget is Image); + expect(image.image is FileImage, isTrue); + }); + testWidgets('should work with local image files', (WidgetTester tester) async { await tester .pumpWidget(_boilerplate(const Markdown(data: '![alt](img.png#50x50)'))); From 83425302300c2fc6934725ee8cabd8d4935ac5ef Mon Sep 17 00:00:00 2001 From: Michael Thomsen Date: Wed, 3 Jan 2018 14:47:49 +0100 Subject: [PATCH 085/121] Update README.md --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index c5a5ed9e064..5e09e15b602 100644 --- a/README.md +++ b/README.md @@ -23,3 +23,15 @@ By default, Markdown uses the formatting from the current material design theme, but it's possible to create your own custom styling. Use the MarkdownStyle class to pass in your own style. If you don't want to use Markdown outside of material design, use the MarkdownRaw class. + +## Notes + +Please note that `Img` tags only support the following image locations. +They specifically do not support Img tags referring to bundled assets. + +* From the network: Use a Url prefixed by either `http://` or `https://`. + +* From local files on the device: Use an absolute path to the file, for example by + concatenating the file name with the path returned by a known storage location, + such as those provided by the [`path_provider`](https://pub.dartlang.org/packages/path_provider) + plugin. From 90e409518da6418b0975c19f48d3d85fe81c8674 Mon Sep 17 00:00:00 2001 From: Michael Thomsen Date: Thu, 4 Jan 2018 09:22:24 +0100 Subject: [PATCH 086/121] Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5e09e15b602..05bb2662cb6 100644 --- a/README.md +++ b/README.md @@ -24,12 +24,12 @@ but it's possible to create your own custom styling. Use the MarkdownStyle class to pass in your own style. If you don't want to use Markdown outside of material design, use the MarkdownRaw class. -## Notes +## Image support -Please note that `Img` tags only support the following image locations. -They specifically do not support Img tags referring to bundled assets. +The `Img` tag only supports the following image locations. It specifically +does not support image locations referring to bundled assets. -* From the network: Use a Url prefixed by either `http://` or `https://`. +* From the network: Use a URL prefixed by either `http://` or `https://`. * From local files on the device: Use an absolute path to the file, for example by concatenating the file name with the path returned by a known storage location, From 06aa68c80455f0652d79d47bbc8074e67c591a1b Mon Sep 17 00:00:00 2001 From: Victor Choueiri Date: Tue, 9 Jan 2018 20:09:33 +0200 Subject: [PATCH 087/121] Fix the onTap callback for images (#24) * Wrap linked images in a GestureDetector * Add test * Version bump and changelog * Add test for image within a text link * Add test for image links surrounded by different links --- CHANGELOG.md | 4 ++ lib/src/builder.dart | 12 ++++- pubspec.yaml | 2 +- test/flutter_markdown_test.dart | 82 +++++++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a3ba9f64f7..955b8727b91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.2 + +* Fix the `onTap` callback on images nested in hyperlinks + ## 0.1.1 * Add support for local file paths in image links. Make sure to set the diff --git a/lib/src/builder.dart b/lib/src/builder.dart index 18bf8b70dc2..42103d00da6 100644 --- a/lib/src/builder.dart +++ b/lib/src/builder.dart @@ -251,13 +251,21 @@ class MarkdownBuilder implements md.NodeVisitor { } Uri uri = Uri.parse(path); + Widget child; if (uri.scheme == 'http' || uri.scheme == 'https') { - return new Image.network(uri.toString(), width: width, height: height); + child = new Image.network(uri.toString(), width: width, height: height); } else { String filePath = (imageDirectory == null ? uri.toFilePath() : p.join(imageDirectory.path, uri.toFilePath())); - return new Image.file(new File(filePath), width: width, height: height); + child = new Image.file(new File(filePath), width: width, height: height); + } + + if (_linkHandlers.isNotEmpty) { + TapGestureRecognizer recognizer = _linkHandlers.last; + return new GestureDetector(child: child, onTap: recognizer.onTap); + } else { + return child; } } diff --git a/pubspec.yaml b/pubspec.yaml index 2df97d34096..2a92df6d6e0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: flutter_markdown author: Flutter Authors description: A markdown renderer for Flutter. homepage: https://github.com/flutter/flutter_markdown -version: 0.1.1 +version: 0.1.2 dependencies: flutter: diff --git a/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart index b6ee532db3c..7a6b3b60f90 100644 --- a/test/flutter_markdown_test.dart +++ b/test/flutter_markdown_test.dart @@ -228,6 +228,88 @@ void main() { expect(textSpan.text, 'Hello '); expect(textSpan.style, isNotNull); }); + + testWidgets('should work when nested in a link', (WidgetTester tester) async { + final List tapResults = []; + await tester.pumpWidget(_boilerplate(new Markdown( + data: '[![alt](https://img#50x50)](href)', + onTapLink: (value) => tapResults.add(value), + ))); + + final GestureDetector detector = + tester.allWidgets.firstWhere((Widget widget) => widget is GestureDetector); + + detector.onTap(); + + expect(tapResults.length, 1); + expect(tapResults, everyElement('href')); + }); + + testWidgets('should work when nested in a link with text', (WidgetTester tester) async { + final List tapResults = []; + await tester.pumpWidget(_boilerplate(new Markdown( + data: '[Text before ![alt](https://img#50x50) text after](href)', + onTapLink: (value) => tapResults.add(value), + ))); + + final GestureDetector detector = + tester.allWidgets.firstWhere((Widget widget) => widget is GestureDetector); + detector.onTap(); + + final RichText firstTextWidget = + tester.allWidgets.firstWhere((Widget widget) => widget is RichText); + final TextSpan firstSpan = firstTextWidget.text; + (firstSpan.recognizer as TapGestureRecognizer).onTap(); + + final RichText lastTextWidget = + tester.allWidgets.lastWhere((Widget widget) => widget is RichText); + final TextSpan lastSpan = lastTextWidget.text; + (lastSpan.recognizer as TapGestureRecognizer).onTap(); + + expect(firstSpan.children, null); + expect(firstSpan.text, 'Text before '); + expect(firstSpan.recognizer.runtimeType, equals(TapGestureRecognizer)); + + expect(lastSpan.children, null); + expect(lastSpan.text, ' text after'); + expect(lastSpan.recognizer.runtimeType, equals(TapGestureRecognizer)); + + expect(tapResults.length, 3); + expect(tapResults, everyElement('href')); + }); + + testWidgets('should work alongside different links', (WidgetTester tester) async { + final List tapResults = []; + await tester.pumpWidget(_boilerplate(new Markdown( + data: '[Link before](firstHref)[![alt](https://img#50x50)](imageHref)[link after](secondHref)', + onTapLink: (value) => tapResults.add(value), + ))); + + final RichText firstTextWidget = + tester.allWidgets.firstWhere((Widget widget) => widget is RichText); + final TextSpan firstSpan = firstTextWidget.text; + (firstSpan.recognizer as TapGestureRecognizer).onTap(); + + final GestureDetector detector = + tester.allWidgets.firstWhere((Widget widget) => widget is GestureDetector); + detector.onTap(); + + final RichText lastTextWidget = + tester.allWidgets.lastWhere((Widget widget) => widget is RichText); + final TextSpan lastSpan = lastTextWidget.text; + (lastSpan.recognizer as TapGestureRecognizer).onTap(); + + expect(firstSpan.children, null); + expect(firstSpan.text, 'Link before'); + expect(firstSpan.recognizer.runtimeType, equals(TapGestureRecognizer)); + + expect(lastSpan.children, null); + expect(lastSpan.text, 'link after'); + expect(lastSpan.recognizer.runtimeType, equals(TapGestureRecognizer)); + + expect(tapResults.length, 3); + expect(tapResults, ['firstHref', 'imageHref', 'secondHref']); + }); }); testWidgets('HTML tag ignored ', (WidgetTester tester) async { From bed98a71161cfdb3141b971360ad8198c1ea3729 Mon Sep 17 00:00:00 2001 From: Victor Choueiri Date: Thu, 11 Jan 2018 20:36:17 +0200 Subject: [PATCH 088/121] Add support for Horizontal Rules (#25) * Add hr tag to list of block tags * Build Divider widgets for hr elements * Use BoxDecoration to draw a horizontal rule * Update changelog * Add test * Add test cases * Revert "Add test cases" This reverts commit d20cd6f4974ab6d04436ca21f93f168f307c2fd5. --- CHANGELOG.md | 1 + lib/src/builder.dart | 6 ++++++ lib/src/style_sheet.dart | 30 ++++++++++++++++++++++++------ test/flutter_markdown_test.dart | 8 ++++++++ 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 955b8727b91..4d8f1bfed79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 0.1.2 +* Add support for horizontal rules. * Fix the `onTap` callback on images nested in hyperlinks ## 0.1.1 diff --git a/lib/src/builder.dart b/lib/src/builder.dart index 42103d00da6..0c0dffe29a3 100644 --- a/lib/src/builder.dart +++ b/lib/src/builder.dart @@ -24,6 +24,7 @@ final Set _kBlockTags = new Set.from([ 'pre', 'ol', 'ul', + 'hr', ]); const List _kListTags = const ['ul', 'ol']; @@ -214,6 +215,11 @@ class MarkdownBuilder implements md.NodeVisitor { child: child, ), ); + } else if (tag == 'hr') { + child = new DecoratedBox( + decoration: styleSheet.horizontalRuleDecoration, + child: child, + ); } _addBlockChild(child); diff --git a/lib/src/style_sheet.dart b/lib/src/style_sheet.dart index 5505f7d1eea..e0dc79e36a7 100644 --- a/lib/src/style_sheet.dart +++ b/lib/src/style_sheet.dart @@ -26,7 +26,8 @@ class MarkdownStyleSheet { this.blockquotePadding, this.blockquoteDecoration, this.codeblockPadding, - this.codeblockDecoration + this.codeblockDecoration, + this.horizontalRuleDecoration }) : _styles = { 'a': a, 'p': p, @@ -77,7 +78,12 @@ class MarkdownStyleSheet { codeblockDecoration: new BoxDecoration( color: Colors.grey.shade100, borderRadius: new BorderRadius.circular(2.0) - ) + ), + horizontalRuleDecoration: new BoxDecoration( + border: new Border( + top: new BorderSide(width: 5.0, color: Colors.grey.shade300) + ), + ), ); } @@ -115,7 +121,12 @@ class MarkdownStyleSheet { codeblockDecoration: new BoxDecoration( color: Colors.grey.shade100, borderRadius: new BorderRadius.circular(2.0) - ) + ), + horizontalRuleDecoration: new BoxDecoration( + border: new Border( + top: new BorderSide(width: 5.0, color: Colors.grey.shade300) + ), + ), ); } @@ -140,7 +151,8 @@ class MarkdownStyleSheet { double blockquotePadding, Decoration blockquoteDecoration, double codeblockPadding, - Decoration codeblockDecoration + Decoration codeblockDecoration, + Decoration horizontalRuleDecoration }) { return new MarkdownStyleSheet( a: a != null ? a : this.a, @@ -161,7 +173,8 @@ class MarkdownStyleSheet { blockquotePadding: blockquotePadding != null ? blockquotePadding : this.blockquotePadding, blockquoteDecoration: blockquoteDecoration != null ? blockquoteDecoration : this.blockquoteDecoration, codeblockPadding: codeblockPadding != null ? codeblockPadding : this.codeblockPadding, - codeblockDecoration: codeblockDecoration != null ? codeblockDecoration : this.codeblockDecoration + codeblockDecoration: codeblockDecoration != null ? codeblockDecoration : this.codeblockDecoration, + horizontalRuleDecoration: horizontalRuleDecoration != null ? horizontalRuleDecoration : this.horizontalRuleDecoration ); } @@ -222,6 +235,9 @@ class MarkdownStyleSheet { /// The decoration to use behind for `pre` elements. final Decoration codeblockDecoration; + /// The decoration to use for `hr` elements. + final Decoration horizontalRuleDecoration; + /// A [Map] from element name to the cooresponding [TextStyle] object. Map get styles => _styles; Map _styles; @@ -251,7 +267,8 @@ class MarkdownStyleSheet { && typedOther.blockquotePadding == blockquotePadding && typedOther.blockquoteDecoration == blockquoteDecoration && typedOther.codeblockPadding == codeblockPadding - && typedOther.codeblockDecoration == codeblockDecoration; + && typedOther.codeblockDecoration == codeblockDecoration + && typedOther.horizontalRuleDecoration == horizontalRuleDecoration; } @override @@ -276,6 +293,7 @@ class MarkdownStyleSheet { blockquoteDecoration, codeblockPadding, codeblockDecoration, + horizontalRuleDecoration, ); } } diff --git a/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart index 7a6b3b60f90..7a53a3b0fb8 100644 --- a/test/flutter_markdown_test.dart +++ b/test/flutter_markdown_test.dart @@ -73,6 +73,14 @@ void main() { ]); }); + testWidgets('Horizontal Rule', (WidgetTester tester) async { + await tester.pumpWidget(_boilerplate(const MarkdownBody(data: '-----'))); + + final Iterable widgets = tester.allWidgets; + _expectWidgetTypes( + widgets, [Directionality, MarkdownBody, DecoratedBox, SizedBox]); + }); + testWidgets('Scrollable wrapping', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate(const Markdown(data: ''))); From c05091c9221bcdec194e242a7360caf981e56d44 Mon Sep 17 00:00:00 2001 From: Michael Thomsen Date: Tue, 20 Feb 2018 15:38:43 +0100 Subject: [PATCH 089/121] Declare dependencies --- CHANGELOG.md | 4 ++++ pubspec.yaml | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d8f1bfed79..f4555ae89de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.3 + +* Add `path` and `http` as declared dependencies in `pubspec.yaml` + ## 0.1.2 * Add support for horizontal rules. diff --git a/pubspec.yaml b/pubspec.yaml index 2a92df6d6e0..93b58f91eff 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: flutter_markdown author: Flutter Authors description: A markdown renderer for Flutter. homepage: https://github.com/flutter/flutter_markdown -version: 0.1.2 +version: 0.1.3 dependencies: flutter: @@ -10,10 +10,12 @@ dependencies: markdown: ^1.0.0 meta: ^1.0.5 string_scanner: ^1.0.0 + path: ^1.5.1 dev_dependencies: flutter_test: sdk: flutter + http: ^0.11.3+16 environment: sdk: '>=1.19.0 <2.0.0' From 1517de03600e54bf7d12d672e9c533c2ff37bbb7 Mon Sep 17 00:00:00 2001 From: Kevin Gray Date: Tue, 27 Feb 2018 17:39:51 -0500 Subject: [PATCH 090/121] added 'li' style to _buildBullet function --- lib/src/builder.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/builder.dart b/lib/src/builder.dart index 0c0dffe29a3..bc404975fb7 100644 --- a/lib/src/builder.dart +++ b/lib/src/builder.dart @@ -277,12 +277,12 @@ class MarkdownBuilder implements md.NodeVisitor { Widget _buildBullet(String listTag) { if (listTag == 'ul') - return const Text('•', textAlign: TextAlign.center); + return new Text('•', textAlign: TextAlign.center, style: styleSheet.styles['li']); final int index = _blocks.last.nextListIndex; return new Padding( padding: const EdgeInsets.only(right: 5.0), - child: new Text('${index + 1}.', textAlign: TextAlign.right), + child: new Text('${index + 1}.', textAlign: TextAlign.right, style: styleSheet.styles['li']), ); } From f81a88b485f515b73368fd704c174f4ebbffe7a0 Mon Sep 17 00:00:00 2001 From: Kevin Gray Date: Tue, 27 Feb 2018 17:56:38 -0500 Subject: [PATCH 091/121] updated pubspec and changelog --- CHANGELOG.md | 4 ++++ pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4555ae89de..1880a6f5cf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.4 + +* Add `li` style to bullets + ## 0.1.3 * Add `path` and `http` as declared dependencies in `pubspec.yaml` diff --git a/pubspec.yaml b/pubspec.yaml index 93b58f91eff..ec5c8cda3b7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: flutter_markdown author: Flutter Authors description: A markdown renderer for Flutter. homepage: https://github.com/flutter/flutter_markdown -version: 0.1.3 +version: 0.1.4 dependencies: flutter: From 9e780ec779dce09139994decec922873c611f633 Mon Sep 17 00:00:00 2001 From: KevinTheGray Date: Tue, 27 Feb 2018 18:33:08 -0500 Subject: [PATCH 092/121] Adds 'li' style to the _buildBullet function (#34) * added 'li' style to _buildBullet function * updated pubspec and changelog --- CHANGELOG.md | 4 ++++ lib/src/builder.dart | 4 ++-- pubspec.yaml | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4555ae89de..1880a6f5cf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.4 + +* Add `li` style to bullets + ## 0.1.3 * Add `path` and `http` as declared dependencies in `pubspec.yaml` diff --git a/lib/src/builder.dart b/lib/src/builder.dart index 0c0dffe29a3..bc404975fb7 100644 --- a/lib/src/builder.dart +++ b/lib/src/builder.dart @@ -277,12 +277,12 @@ class MarkdownBuilder implements md.NodeVisitor { Widget _buildBullet(String listTag) { if (listTag == 'ul') - return const Text('•', textAlign: TextAlign.center); + return new Text('•', textAlign: TextAlign.center, style: styleSheet.styles['li']); final int index = _blocks.last.nextListIndex; return new Padding( padding: const EdgeInsets.only(right: 5.0), - child: new Text('${index + 1}.', textAlign: TextAlign.right), + child: new Text('${index + 1}.', textAlign: TextAlign.right, style: styleSheet.styles['li']), ); } diff --git a/pubspec.yaml b/pubspec.yaml index 93b58f91eff..ec5c8cda3b7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: flutter_markdown author: Flutter Authors description: A markdown renderer for Flutter. homepage: https://github.com/flutter/flutter_markdown -version: 0.1.3 +version: 0.1.4 dependencies: flutter: From 54a5588fb440449b0c6c0d73038d8a569bcfbed1 Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Thu, 8 Mar 2018 18:57:21 -0500 Subject: [PATCH 093/121] Spelling (#35) * spelling: corresponding * spelling: inferred * spelling: interrupt --- lib/src/style_sheet.dart | 2 +- lib/src/widget.dart | 2 +- test/flutter_markdown_test.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/style_sheet.dart b/lib/src/style_sheet.dart index e0dc79e36a7..2d44c862f70 100644 --- a/lib/src/style_sheet.dart +++ b/lib/src/style_sheet.dart @@ -238,7 +238,7 @@ class MarkdownStyleSheet { /// The decoration to use for `hr` elements. final Decoration horizontalRuleDecoration; - /// A [Map] from element name to the cooresponding [TextStyle] object. + /// A [Map] from element name to the corresponding [TextStyle] object. Map get styles => _styles; Map _styles; diff --git a/lib/src/widget.dart b/lib/src/widget.dart index fc97cdaee5d..55bea0ea8d6 100644 --- a/lib/src/widget.dart +++ b/lib/src/widget.dart @@ -54,7 +54,7 @@ abstract class MarkdownWidget extends StatefulWidget { /// The styles to use when displaying the Markdown. /// - /// If null, the styles are infered from the current [Theme]. + /// If null, the styles are inferred from the current [Theme]. final MarkdownStyleSheet styleSheet; /// The syntax highlighter used to color text in `pre` elements. diff --git a/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart index 7a53a3b0fb8..c8a2cbc9d34 100644 --- a/test/flutter_markdown_test.dart +++ b/test/flutter_markdown_test.dart @@ -173,7 +173,7 @@ void main() { createHttpClient = createMockImageHttpClient; }); - testWidgets('should not interupt styling', (WidgetTester tester) async { + testWidgets('should not interrupt styling', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate(const Markdown( data:'_textbefore ![alt](http://img) textafter_', ))); From 87c378ec8faa6ff52e923167bc03a825a081619b Mon Sep 17 00:00:00 2001 From: fedor Date: Fri, 9 Feb 2018 15:08:57 -0500 Subject: [PATCH 094/121] Cirrus CI --- .cirrus.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .cirrus.yml diff --git a/.cirrus.yml b/.cirrus.yml new file mode 100644 index 00000000000..07b53894ed0 --- /dev/null +++ b/.cirrus.yml @@ -0,0 +1,7 @@ +container: + image: cirrusci/flutter:0.0.24 + +test_task: + pub_cache: + folder: ~/.pub-cache + test_script: flutter test \ No newline at end of file From 8f6e1cad3df48d34ac81d8fc3ba8175a96e55d83 Mon Sep 17 00:00:00 2001 From: fedor Date: Mon, 12 Feb 2018 13:33:38 -0500 Subject: [PATCH 095/121] Use Flutter 0.1.0 --- .cirrus.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cirrus.yml b/.cirrus.yml index 07b53894ed0..fe111386450 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,5 +1,5 @@ container: - image: cirrusci/flutter:0.0.24 + image: cirrusci/flutter:0.1.0 test_task: pub_cache: From 0db3b02c98a364eb5b982c8b7009c5d9e8850623 Mon Sep 17 00:00:00 2001 From: fedor Date: Sat, 10 Mar 2018 09:01:44 -0500 Subject: [PATCH 096/121] Use the latest available Flutter image Plus run `flutter analyze` --- .cirrus.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index fe111386450..0505d8acb42 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,7 +1,12 @@ container: - image: cirrusci/flutter:0.1.0 + image: cirrusci/flutter:latest test_task: pub_cache: folder: ~/.pub-cache - test_script: flutter test \ No newline at end of file + test_script: flutter test + +analyze_task: + pub_cache: + folder: ~/.pub-cache + analyze_script: flutter analyze \ No newline at end of file From 7debeba2c20740181bf93fcb59a44bf0dd2168af Mon Sep 17 00:00:00 2001 From: Fedor Korotkov Date: Wed, 28 Mar 2018 18:53:38 -0400 Subject: [PATCH 097/121] Always upgrade Flutter to the latest master (#39) Always upgrade Flutter to the latest master on Cirrus. --- .cirrus.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.cirrus.yml b/.cirrus.yml index 0505d8acb42..49fe2845d91 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -4,9 +4,15 @@ container: test_task: pub_cache: folder: ~/.pub-cache + upgrade_script: + - flutter channel master + - flutter upgrade test_script: flutter test analyze_task: pub_cache: folder: ~/.pub-cache + upgrade_script: + - flutter channel master + - flutter upgrade analyze_script: flutter analyze \ No newline at end of file From c47aad199ec69cf01d06bd26ed729d9481281406 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Wed, 28 Mar 2018 16:05:49 -0700 Subject: [PATCH 098/121] Replace use of createHttpClient with mocks (#37) package:http was recently removed from Flutter. This replaces the use of createHttpClient, which was previously used to mock HTTP results. This replaces that use with Mockito. See: https://groups.google.com/d/msg/flutter-dev/AnqDqgQ6vus/spjSxiz6BgAJ --- CHANGELOG.md | 5 +++ pubspec.yaml | 4 +-- test/flutter_markdown_test.dart | 55 ++++++++++++++++++++++++++------- 3 files changed, 50 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1880a6f5cf4..42517360505 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.1.5 + +* Add `mockito` as a dev dependency. Eliminate use of `package:http`, which + is no longer part of Flutter. + ## 0.1.4 * Add `li` style to bullets diff --git a/pubspec.yaml b/pubspec.yaml index ec5c8cda3b7..2b7eb77daaf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: flutter_markdown author: Flutter Authors description: A markdown renderer for Flutter. homepage: https://github.com/flutter/flutter_markdown -version: 0.1.4 +version: 0.1.5 dependencies: flutter: @@ -15,7 +15,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - http: ^0.11.3+16 + mockito: ^3.0.0-alpha+2 environment: sdk: '>=1.19.0 <2.0.0' diff --git a/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart index c8a2cbc9d34..9a2a471432b 100644 --- a/test/flutter_markdown_test.dart +++ b/test/flutter_markdown_test.dart @@ -2,14 +2,15 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; +import 'dart:io' as io; + import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart' show createHttpClient; -import 'package:http/http.dart' as http; -import 'package:http/testing.dart' as http; +import 'package:mockito/mockito.dart'; void main() { TextTheme textTheme = new Typography(platform: TargetPlatform.android) @@ -167,10 +168,12 @@ void main() { expect(tapResults, orderedEquals(['firstHref', 'secondHref'])); }); }); - + group('Images', () { - setUpAll(() { - createHttpClient = createMockImageHttpClient; + setUp(() { + // Only needs to be done once since the HttpClient generated by this + // override is cached as a static singleton. + io.HttpOverrides.global = new TestHttpOverrides(); }); testWidgets('should not interrupt styling', (WidgetTester tester) async { @@ -226,7 +229,7 @@ void main() { expect(image.height, 50); }); - testWidgets('should show properly next to text', (WidgetTester tester) async { + testWidgets('should show properly next to text', (WidgetTester tester) async { await tester .pumpWidget(_boilerplate(const Markdown(data: 'Hello ![alt](img#50x50)'))); @@ -427,13 +430,41 @@ Widget _boilerplate(Widget child) { ); } +class MockHttpClient extends Mock implements io.HttpClient {} +class MockHttpClientRequest extends Mock implements io.HttpClientRequest {} +class MockHttpClientResponse extends Mock implements io.HttpClientResponse {} +class MockHttpHeaders extends Mock implements io.HttpHeaders {} + +class TestHttpOverrides extends io.HttpOverrides { + io.HttpClient createHttpClient(io.SecurityContext context) { + return createMockImageHttpClient(context); + } +} + // Returns a mock HTTP client that responds with an image to all requests. -ValueGetter createMockImageHttpClient = () { - return new http.MockClient((http.BaseRequest request) { - return new Future.value( - new http.Response.bytes(_transparentImage, 200, request: request)); +MockHttpClient createMockImageHttpClient(io.SecurityContext _) { + final MockHttpClient client = new MockHttpClient(); + final MockHttpClientRequest request = new MockHttpClientRequest(); + final MockHttpClientResponse response = new MockHttpClientResponse(); + final MockHttpHeaders headers = new MockHttpHeaders(); + + when(client.getUrl(typed(any))).thenAnswer((_) => new Future.value(request)); + when(request.headers).thenReturn(headers); + when(request.close()).thenAnswer((_) => new Future.value(response)); + when(response.contentLength).thenReturn(_transparentImage.length); + when(response.statusCode).thenReturn(io.HttpStatus.OK); + when(response.listen(typed(any))).thenAnswer((Invocation invocation) { + final void Function(List) onData = invocation.positionalArguments[0]; + final void Function() onDone = invocation.namedArguments[#onDone]; + final void Function(Object, [StackTrace]) onError = invocation.namedArguments[#onError]; + final bool cancelOnError = invocation.namedArguments[#cancelOnError]; + + return new Stream>.fromIterable(>[_transparentImage]) + .listen(onData, onDone: onDone, onError: onError, cancelOnError: cancelOnError); }); -}; + + return client; +} const List _transparentImage = const [ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, From 4c56d8d77159c6400bd4e12b2235cb1d2a215beb Mon Sep 17 00:00:00 2001 From: Seth Westphal Date: Wed, 4 Apr 2018 23:16:22 -0500 Subject: [PATCH 099/121] Use coalesce operator. --- lib/src/style_sheet.dart | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/lib/src/style_sheet.dart b/lib/src/style_sheet.dart index 2d44c862f70..53e38db666f 100644 --- a/lib/src/style_sheet.dart +++ b/lib/src/style_sheet.dart @@ -155,26 +155,26 @@ class MarkdownStyleSheet { Decoration horizontalRuleDecoration }) { return new MarkdownStyleSheet( - a: a != null ? a : this.a, - p: p != null ? p : this.p, - code: code != null ? code : this.code, - h1: h1 != null ? h1 : this.h1, - h2: h2 != null ? h2 : this.h2, - h3: h3 != null ? h3 : this.h3, - h4: h4 != null ? h4 : this.h4, - h5: h5 != null ? h5 : this.h5, - h6: h6 != null ? h6 : this.h6, - em: em != null ? em : this.em, - strong: strong != null ? strong : this.strong, - blockquote: blockquote != null ? blockquote : this.blockquote, - img: img != null ? img: this.img, - blockSpacing: blockSpacing != null ? blockSpacing : this.blockSpacing, - listIndent: listIndent != null ? listIndent : this.listIndent, - blockquotePadding: blockquotePadding != null ? blockquotePadding : this.blockquotePadding, - blockquoteDecoration: blockquoteDecoration != null ? blockquoteDecoration : this.blockquoteDecoration, - codeblockPadding: codeblockPadding != null ? codeblockPadding : this.codeblockPadding, - codeblockDecoration: codeblockDecoration != null ? codeblockDecoration : this.codeblockDecoration, - horizontalRuleDecoration: horizontalRuleDecoration != null ? horizontalRuleDecoration : this.horizontalRuleDecoration + a: a ?? this.a, + p: p ?? this.p, + code: code ?? this.code, + h1: h1 ?? this.h1, + h2: h2 ?? this.h2, + h3: h3 ?? this.h3, + h4: h4 ?? this.h4, + h5: h5 ?? this.h5, + h6: h6 ?? this.h6, + em: em ?? this.em, + strong: strong ?? this.strong, + blockquote: blockquote ?? this.blockquote, + img: img ?? this.img, + blockSpacing: blockSpacing ?? this.blockSpacing, + listIndent: listIndent ?? this.listIndent, + blockquotePadding: blockquotePadding ?? this.blockquotePadding, + blockquoteDecoration: blockquoteDecoration ?? this.blockquoteDecoration, + codeblockPadding: codeblockPadding ?? this.codeblockPadding, + codeblockDecoration: codeblockDecoration ?? this.codeblockDecoration, + horizontalRuleDecoration: horizontalRuleDecoration ?? this.horizontalRuleDecoration, ); } From f713a3116bffacff56433620297797b1f280a95a Mon Sep 17 00:00:00 2001 From: Seth Westphal Date: Wed, 4 Apr 2018 23:16:53 -0500 Subject: [PATCH 100/121] Capitalize 'Markdown.' --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 2b7eb77daaf..270b3712674 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_markdown author: Flutter Authors -description: A markdown renderer for Flutter. +description: A Markdown renderer for Flutter. homepage: https://github.com/flutter/flutter_markdown version: 0.1.5 From d981ddf7efb3bc49a733b6633617795de68724be Mon Sep 17 00:00:00 2001 From: Seth Westphal Date: Wed, 4 Apr 2018 23:19:23 -0500 Subject: [PATCH 101/121] Prevent '&' and '<' from being escaped. --- lib/src/widget.dart | 6 +++++- test/flutter_markdown_test.dart | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/src/widget.dart b/lib/src/widget.dart index 55bea0ea8d6..1060485d394 100644 --- a/lib/src/widget.dart +++ b/lib/src/widget.dart @@ -108,7 +108,11 @@ class _MarkdownWidgetState extends State implements MarkdownBuil // TODO: This can be optimized by doing the split and removing \r at the same time final List lines = widget.data.replaceAll('\r\n', '\n').split('\n'); - final md.Document document = new md.Document(); + final List overrides = [ + new md.TextSyntax(r'&', sub: '&'), + new md.TextSyntax(r'<', sub: '<'), + ]; + final md.Document document = new md.Document(inlineSyntaxes: overrides); final MarkdownBuilder builder = new MarkdownBuilder( delegate: this, styleSheet: styleSheet, diff --git a/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart index 9a2a471432b..f28b16a9d71 100644 --- a/test/flutter_markdown_test.dart +++ b/test/flutter_markdown_test.dart @@ -337,13 +337,13 @@ void main() { } }); - testWidgets('Less than', (WidgetTester tester) async { - final String mdLine = 'Line 1 <\n\nc < c c\n\n< Line 2'; + testWidgets('HTML entities', (WidgetTester tester) async { + final String mdLine = 'Line 1 <\n\nc < > c c\n\n< Line & © 2'; await tester.pumpWidget(_boilerplate(new MarkdownBody(data: mdLine))); final Iterable widgets = tester.allWidgets; _expectTextStrings( - widgets, ['Line 1 <', 'c < c c', '< Line 2']); + widgets, ['Line 1 <', 'c < > c c', '< Line & © 2']); }); testWidgets('Changing config - data', (WidgetTester tester) async { From f537134f1b7b0a42bb338734f735cc9ae4251803 Mon Sep 17 00:00:00 2001 From: Seth Westphal Date: Wed, 4 Apr 2018 23:20:12 -0500 Subject: [PATCH 102/121] Ignore generated ios folder. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c3f60f2445e..d3a12988ece 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .idea .packages .pub/ +ios/ packages pubspec.lock From 21cfb2f63cfab133d118280da2601a15a7a8ecda Mon Sep 17 00:00:00 2001 From: Seth Westphal Date: Wed, 4 Apr 2018 23:41:13 -0500 Subject: [PATCH 103/121] Cleanup. --- lib/src/widget.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/widget.dart b/lib/src/widget.dart index 1060485d394..187d246801d 100644 --- a/lib/src/widget.dart +++ b/lib/src/widget.dart @@ -109,8 +109,8 @@ class _MarkdownWidgetState extends State implements MarkdownBuil // TODO: This can be optimized by doing the split and removing \r at the same time final List lines = widget.data.replaceAll('\r\n', '\n').split('\n'); final List overrides = [ - new md.TextSyntax(r'&', sub: '&'), - new md.TextSyntax(r'<', sub: '<'), + new md.TextSyntax('&'), + new md.TextSyntax('<'), ]; final md.Document document = new md.Document(inlineSyntaxes: overrides); final MarkdownBuilder builder = new MarkdownBuilder( From 31a755516f78e915aa357c12b9b67205333b3373 Mon Sep 17 00:00:00 2001 From: Seth Westphal Date: Wed, 9 May 2018 14:56:02 -0500 Subject: [PATCH 104/121] Sync .gitignore with Flutter's template. --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d3a12988ece..6c3b5dcf973 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,9 @@ .idea .packages .pub/ -ios/ +ios/.generated/ +ios/Flutter/Generated.xcconfig +ios/Runner/GeneratedPluginRegistrant.* packages pubspec.lock From 1a7b468d92fd253e072cebd66f15972b3c1fa1f5 Mon Sep 17 00:00:00 2001 From: Seth Westphal Date: Wed, 9 May 2018 14:56:22 -0500 Subject: [PATCH 105/121] Update markdown to 2.0.0. --- lib/src/widget.dart | 6 +----- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/src/widget.dart b/lib/src/widget.dart index 187d246801d..39a15c6b267 100644 --- a/lib/src/widget.dart +++ b/lib/src/widget.dart @@ -108,11 +108,7 @@ class _MarkdownWidgetState extends State implements MarkdownBuil // TODO: This can be optimized by doing the split and removing \r at the same time final List lines = widget.data.replaceAll('\r\n', '\n').split('\n'); - final List overrides = [ - new md.TextSyntax('&'), - new md.TextSyntax('<'), - ]; - final md.Document document = new md.Document(inlineSyntaxes: overrides); + final md.Document document = new md.Document(encodeHtml: false); final MarkdownBuilder builder = new MarkdownBuilder( delegate: this, styleSheet: styleSheet, diff --git a/pubspec.yaml b/pubspec.yaml index 270b3712674..abd09bcac28 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,7 +7,7 @@ version: 0.1.5 dependencies: flutter: sdk: flutter - markdown: ^1.0.0 + markdown: ^2.0.0 meta: ^1.0.5 string_scanner: ^1.0.0 path: ^1.5.1 From 0a04b26bb235fd7974b8b8facec2c9c42f7245b3 Mon Sep 17 00:00:00 2001 From: Seth Westphal Date: Wed, 9 May 2018 15:03:59 -0500 Subject: [PATCH 106/121] Improve unit tests. --- test/flutter_markdown_test.dart | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart index f28b16a9d71..6c34fc695cf 100644 --- a/test/flutter_markdown_test.dart +++ b/test/flutter_markdown_test.dart @@ -337,13 +337,21 @@ void main() { } }); - testWidgets('HTML entities', (WidgetTester tester) async { - final String mdLine = 'Line 1 <\n\nc < > c c\n\n< Line & © 2'; - await tester.pumpWidget(_boilerplate(new MarkdownBody(data: mdLine))); + group('Parser does not convert', () { + testWidgets('& to & when parsing', (WidgetTester tester) async { + await tester.pumpWidget(_boilerplate(const Markdown(data: '&'))); + _expectTextStrings(tester.allWidgets, ['&']); + }); - final Iterable widgets = tester.allWidgets; - _expectTextStrings( - widgets, ['Line 1 <', 'c < > c c', '< Line & © 2']); + testWidgets('< to < when parsing', (WidgetTester tester) async { + await tester.pumpWidget(_boilerplate(const Markdown(data: '<'))); + _expectTextStrings(tester.allWidgets, ['<']); + }); + + testWidgets('existing HTML entities when parsing', (WidgetTester tester) async { + await tester.pumpWidget(_boilerplate(const Markdown(data: '& ©'))); + _expectTextStrings(tester.allWidgets, ['& ©']); + }); }); testWidgets('Changing config - data', (WidgetTester tester) async { From fa1968fa6e340b699dbc155585b555b2875fb824 Mon Sep 17 00:00:00 2001 From: Seth Westphal Date: Wed, 9 May 2018 15:17:45 -0500 Subject: [PATCH 107/121] Test numbered entites. --- test/flutter_markdown_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart index 6c34fc695cf..b6785aec8f6 100644 --- a/test/flutter_markdown_test.dart +++ b/test/flutter_markdown_test.dart @@ -349,8 +349,8 @@ void main() { }); testWidgets('existing HTML entities when parsing', (WidgetTester tester) async { - await tester.pumpWidget(_boilerplate(const Markdown(data: '& ©'))); - _expectTextStrings(tester.allWidgets, ['& ©']); + await tester.pumpWidget(_boilerplate(const Markdown(data: '& © < {'))); + _expectTextStrings(tester.allWidgets, ['& © < {']); }); }); From 8e584d72871d14531b41d2738c3b35825e938c74 Mon Sep 17 00:00:00 2001 From: BC Ko Date: Thu, 24 May 2018 03:25:09 -0700 Subject: [PATCH 108/121] Update .gitignore to new `dart_tool` pub cache dart-lang/sdk#32030 --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c3f60f2445e..c0d73199bdc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .atom/ .idea .packages +.dart_tool/ .pub/ packages pubspec.lock From 8df2e68bb374882c819fa66982555eb729235f18 Mon Sep 17 00:00:00 2001 From: BC Ko Date: Fri, 25 May 2018 17:30:42 -0700 Subject: [PATCH 109/121] Rename demo.dart to example.dart --- example/{demo.dart => example.dart} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename example/{demo.dart => example.dart} (100%) diff --git a/example/demo.dart b/example/example.dart similarity index 100% rename from example/demo.dart rename to example/example.dart From bb71fc7bb9b72a92b357e9b933843ab26f4e580e Mon Sep 17 00:00:00 2001 From: Zachary Anderson Date: Wed, 1 Aug 2018 10:10:58 -0700 Subject: [PATCH 110/121] Update pubspec.yaml version for bump of markdown dependency --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index abd09bcac28..8454a903d77 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: flutter_markdown author: Flutter Authors description: A Markdown renderer for Flutter. homepage: https://github.com/flutter/flutter_markdown -version: 0.1.5 +version: 0.1.6 dependencies: flutter: From 2f94c3c0d708d21f823a8084641ce3d93b98f797 Mon Sep 17 00:00:00 2001 From: Zachary Anderson Date: Wed, 1 Aug 2018 10:16:20 -0700 Subject: [PATCH 111/121] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42517360505..4c37166a7c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.6 + +* Updated `markdown` dependency. + ## 0.1.5 * Add `mockito` as a dev dependency. Eliminate use of `package:http`, which From 24522713ecb2e8712f1d323bcc6641dcd3dab3da Mon Sep 17 00:00:00 2001 From: Kevin Gray Date: Fri, 17 Aug 2018 11:33:25 -0400 Subject: [PATCH 112/121] updated environment sdk dependency to be compatible with most recent version of flutter, update version to 0.1.7 --- CHANGELOG.md | 4 ++++ pubspec.yaml | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c37166a7c3..57d27da40fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.7 + +* Updated environment sdk constraints + ## 0.1.6 * Updated `markdown` dependency. diff --git a/pubspec.yaml b/pubspec.yaml index 8454a903d77..fab9c0d7b48 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: flutter_markdown author: Flutter Authors description: A Markdown renderer for Flutter. homepage: https://github.com/flutter/flutter_markdown -version: 0.1.6 +version: 0.1.7 dependencies: flutter: @@ -18,4 +18,4 @@ dev_dependencies: mockito: ^3.0.0-alpha+2 environment: - sdk: '>=1.19.0 <2.0.0' + sdk: '>=2.0.0-dev.58.0 <3.0.0' From 1893e788e88e23407235bb24a22c9f8fc52dcf49 Mon Sep 17 00:00:00 2001 From: Kevin Gray Date: Fri, 17 Aug 2018 11:45:36 -0400 Subject: [PATCH 113/121] fixed analyzer issues in flutter_markdown_test --- test/flutter_markdown_test.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart index b6785aec8f6..f71902c0fc2 100644 --- a/test/flutter_markdown_test.dart +++ b/test/flutter_markdown_test.dart @@ -456,12 +456,12 @@ MockHttpClient createMockImageHttpClient(io.SecurityContext _) { final MockHttpClientResponse response = new MockHttpClientResponse(); final MockHttpHeaders headers = new MockHttpHeaders(); - when(client.getUrl(typed(any))).thenAnswer((_) => new Future.value(request)); + when(client.getUrl(any)).thenAnswer((_) => new Future.value(request)); when(request.headers).thenReturn(headers); when(request.close()).thenAnswer((_) => new Future.value(response)); when(response.contentLength).thenReturn(_transparentImage.length); - when(response.statusCode).thenReturn(io.HttpStatus.OK); - when(response.listen(typed(any))).thenAnswer((Invocation invocation) { + when(response.statusCode).thenReturn(io.HttpStatus.ok); + when(response.listen(any)).thenAnswer((Invocation invocation) { final void Function(List) onData = invocation.positionalArguments[0]; final void Function() onDone = invocation.namedArguments[#onDone]; final void Function(Object, [StackTrace]) onError = invocation.namedArguments[#onError]; From 0bb9a5472c968ab4eeb4b11fec0e3afa0992b6fa Mon Sep 17 00:00:00 2001 From: Kevin Gray Date: Mon, 20 Aug 2018 10:10:10 -0400 Subject: [PATCH 114/121] Updated changelog description --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57d27da40fd..cf13b1126fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## 0.1.7 -* Updated environment sdk constraints +* Updated environment sdk constraints to make the package + Dart 2. As a result, usage of this version and higher + requires a Dart 2 SDK. ## 0.1.6 From 5387b0fdc2edc938776ab7f8a50add422a5369bb Mon Sep 17 00:00:00 2001 From: Kevin Gray Date: Mon, 20 Aug 2018 10:11:28 -0400 Subject: [PATCH 115/121] changelog fix --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf13b1126fe..a0be2b56d16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ ## 0.1.7 * Updated environment sdk constraints to make the package - Dart 2. As a result, usage of this version and higher + Dart 2 compatible. As a result, usage of this version and higher requires a Dart 2 SDK. ## 0.1.6 From 1d75ee96f417822f9d2074d65f7f6e0f3ea9f44a Mon Sep 17 00:00:00 2001 From: Kevin Gray Date: Mon, 20 Aug 2018 10:21:36 -0400 Subject: [PATCH 116/121] updated mockito to version 3.0.0 final --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index fab9c0d7b48..c679a07afb7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - mockito: ^3.0.0-alpha+2 + mockito: ^3.0.0 environment: sdk: '>=2.0.0-dev.58.0 <3.0.0' From 598dd1293845e36e6daf661a0a8832b2aede35af Mon Sep 17 00:00:00 2001 From: Michael Thomsen Date: Tue, 21 Aug 2018 14:19:06 +0200 Subject: [PATCH 117/121] Update pubspec.yaml Update version no as https://github.com/flutter/flutter_markdown/pull/56 was a breaking change (Dart 2 only) --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index c679a07afb7..d4e656528a1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: flutter_markdown author: Flutter Authors description: A Markdown renderer for Flutter. homepage: https://github.com/flutter/flutter_markdown -version: 0.1.7 +version: 0.2.0 dependencies: flutter: From a76aba222bcec6498aee0591e0ef69cb8dafa6cc Mon Sep 17 00:00:00 2001 From: Michael Thomsen Date: Tue, 21 Aug 2018 14:19:26 +0200 Subject: [PATCH 118/121] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0be2b56d16..195659ea840 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.1.7 +## 0.2.0 * Updated environment sdk constraints to make the package Dart 2 compatible. As a result, usage of this version and higher From 88092c025a5020c87fbf6966a49639b1cd1b3fe2 Mon Sep 17 00:00:00 2001 From: Tam Trieu Date: Wed, 5 Sep 2018 18:28:46 +0700 Subject: [PATCH 119/121] Support Uri data scheme --- lib/src/builder.dart | 26 +++++++++++++++- test/flutter_markdown_test.dart | 55 +++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/lib/src/builder.dart b/lib/src/builder.dart index bc404975fb7..ad8914e8b62 100644 --- a/lib/src/builder.dart +++ b/lib/src/builder.dart @@ -260,7 +260,11 @@ class MarkdownBuilder implements md.NodeVisitor { Widget child; if (uri.scheme == 'http' || uri.scheme == 'https') { child = new Image.network(uri.toString(), width: width, height: height); - } else { + } + else if (uri.scheme == 'data') { + child = _handleDataSchemeUri(uri, width, height); + } + else { String filePath = (imageDirectory == null ? uri.toFilePath() : p.join(imageDirectory.path, uri.toFilePath())); @@ -275,6 +279,26 @@ class MarkdownBuilder implements md.NodeVisitor { } } + Widget _handleDataSchemeUri(Uri uri, final double width, final double height) { + + String mimeType = uri.data.mimeType; + + Widget child; + + if (mimeType.startsWith('image/')) { + child = new Image.memory(uri.data.contentAsBytes(), width: width, height: height); + } + else if (mimeType.startsWith('text/')) { + child = new Text(uri.data.contentAsString()); + } + else { + child = const SizedBox(); + } + + + return child; + } + Widget _buildBullet(String listTag) { if (listTag == 'ul') return new Text('•', textAlign: TextAlign.center, style: styleSheet.styles['li']); diff --git a/test/flutter_markdown_test.dart b/test/flutter_markdown_test.dart index f71902c0fc2..ca341f622d0 100644 --- a/test/flutter_markdown_test.dart +++ b/test/flutter_markdown_test.dart @@ -323,6 +323,61 @@ void main() { }); }); + group('uri data scheme', () { + testWidgets('should work with image in uri data scheme', (WidgetTester tester) async { + const String imageData = '![alt](data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=)'; + await tester + .pumpWidget(_boilerplate(const Markdown(data: imageData))); + + final Image image = + tester.allWidgets.firstWhere((Widget widget) => widget is Image); + expect(image.image.runtimeType, MemoryImage); + }); + + testWidgets('should work with base64 text in uri data scheme', (WidgetTester tester) async { + const String imageData = '![alt](data:text/plan;base64,Rmx1dHRlcg==)'; + await tester + .pumpWidget(_boilerplate(const Markdown(data: imageData))); + + final Text widget = + tester.allWidgets.firstWhere((Widget widget) => widget is Text); + expect(widget.runtimeType, Text); + expect(widget.data, 'Flutter'); + }); + + testWidgets('should work with text in uri data scheme', (WidgetTester tester) async { + const String imageData = '![alt](data:text/plan,Hello%2C%20Flutter)'; + await tester + .pumpWidget(_boilerplate(const Markdown(data: imageData))); + + final Text widget = + tester.allWidgets.firstWhere((Widget widget) => widget is Text); + expect(widget.runtimeType, Text); + expect(widget.data, 'Hello, Flutter'); + }); + + testWidgets('should work with empty uri data scheme', (WidgetTester tester) async { + const String imageData = '![alt](data:,)'; + await tester + .pumpWidget(_boilerplate(const Markdown(data: imageData))); + + final Text widget = + tester.allWidgets.firstWhere((Widget widget) => widget is Text); + expect(widget.runtimeType, Text); + expect(widget.data, ''); + }); + + testWidgets('should work with unsupported mime types of uri data scheme', (WidgetTester tester) async { + const String imageData = '![alt](data:application/javascript,var%20test=1)'; + await tester + .pumpWidget(_boilerplate(const Markdown(data: imageData))); + + final SizedBox widget = + tester.allWidgets.firstWhere((Widget widget) => widget is SizedBox); + expect(widget.runtimeType, SizedBox); + }); + }); + testWidgets('HTML tag ignored ', (WidgetTester tester) async { final List mdData = [ 'Line 1\n

HTML content

\nLine 2', From 0881271dbd4889f55e6b61d0fb612d30d5666245 Mon Sep 17 00:00:00 2001 From: Tam Trieu Date: Wed, 26 Sep 2018 12:01:42 +0700 Subject: [PATCH 120/121] Remove blank lines and reformat else statements --- lib/src/builder.dart | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/lib/src/builder.dart b/lib/src/builder.dart index ad8914e8b62..059bbadbe84 100644 --- a/lib/src/builder.dart +++ b/lib/src/builder.dart @@ -260,11 +260,9 @@ class MarkdownBuilder implements md.NodeVisitor { Widget child; if (uri.scheme == 'http' || uri.scheme == 'https') { child = new Image.network(uri.toString(), width: width, height: height); - } - else if (uri.scheme == 'data') { + } else if (uri.scheme == 'data') { child = _handleDataSchemeUri(uri, width, height); - } - else { + } else { String filePath = (imageDirectory == null ? uri.toFilePath() : p.join(imageDirectory.path, uri.toFilePath())); @@ -280,23 +278,13 @@ class MarkdownBuilder implements md.NodeVisitor { } Widget _handleDataSchemeUri(Uri uri, final double width, final double height) { - - String mimeType = uri.data.mimeType; - - Widget child; - + final String mimeType = uri.data.mimeType; if (mimeType.startsWith('image/')) { - child = new Image.memory(uri.data.contentAsBytes(), width: width, height: height); - } - else if (mimeType.startsWith('text/')) { - child = new Text(uri.data.contentAsString()); - } - else { - child = const SizedBox(); + return new Image.memory(uri.data.contentAsBytes(), width: width, height: height); + } else if (mimeType.startsWith('text/')) { + return new Text(uri.data.contentAsString()); } - - - return child; + return const SizedBox(); } Widget _buildBullet(String listTag) { From d08add76d88471ae0642c15c2a7b3d891759f8c2 Mon Sep 17 00:00:00 2001 From: Ray Rischpater Date: Mon, 10 Dec 2018 14:12:06 -0800 Subject: [PATCH 121/121] Moved flutter_markdown files to packages/flutter_markdown --- .cirrus.yml => packages/flutter_markdown/.cirrus.yml | 0 .gitignore => packages/flutter_markdown/.gitignore | 0 .travis.yml => packages/flutter_markdown/.travis.yml | 0 CHANGELOG.md => packages/flutter_markdown/CHANGELOG.md | 0 LICENSE => packages/flutter_markdown/LICENSE | 0 README.md => packages/flutter_markdown/README.md | 0 {example => packages/flutter_markdown/example}/example.dart | 0 .../flutter_markdown/flutter_markdown.iml | 0 {lib => packages/flutter_markdown/lib}/flutter_markdown.dart | 0 {lib => packages/flutter_markdown/lib}/src/builder.dart | 0 {lib => packages/flutter_markdown/lib}/src/style_sheet.dart | 0 {lib => packages/flutter_markdown/lib}/src/widget.dart | 0 pubspec.yaml => packages/flutter_markdown/pubspec.yaml | 0 .../flutter_markdown/test}/flutter_markdown_test.dart | 0 14 files changed, 0 insertions(+), 0 deletions(-) rename .cirrus.yml => packages/flutter_markdown/.cirrus.yml (100%) rename .gitignore => packages/flutter_markdown/.gitignore (100%) rename .travis.yml => packages/flutter_markdown/.travis.yml (100%) rename CHANGELOG.md => packages/flutter_markdown/CHANGELOG.md (100%) rename LICENSE => packages/flutter_markdown/LICENSE (100%) rename README.md => packages/flutter_markdown/README.md (100%) rename {example => packages/flutter_markdown/example}/example.dart (100%) rename flutter_markdown.iml => packages/flutter_markdown/flutter_markdown.iml (100%) rename {lib => packages/flutter_markdown/lib}/flutter_markdown.dart (100%) rename {lib => packages/flutter_markdown/lib}/src/builder.dart (100%) rename {lib => packages/flutter_markdown/lib}/src/style_sheet.dart (100%) rename {lib => packages/flutter_markdown/lib}/src/widget.dart (100%) rename pubspec.yaml => packages/flutter_markdown/pubspec.yaml (100%) rename {test => packages/flutter_markdown/test}/flutter_markdown_test.dart (100%) diff --git a/.cirrus.yml b/packages/flutter_markdown/.cirrus.yml similarity index 100% rename from .cirrus.yml rename to packages/flutter_markdown/.cirrus.yml diff --git a/.gitignore b/packages/flutter_markdown/.gitignore similarity index 100% rename from .gitignore rename to packages/flutter_markdown/.gitignore diff --git a/.travis.yml b/packages/flutter_markdown/.travis.yml similarity index 100% rename from .travis.yml rename to packages/flutter_markdown/.travis.yml diff --git a/CHANGELOG.md b/packages/flutter_markdown/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to packages/flutter_markdown/CHANGELOG.md diff --git a/LICENSE b/packages/flutter_markdown/LICENSE similarity index 100% rename from LICENSE rename to packages/flutter_markdown/LICENSE diff --git a/README.md b/packages/flutter_markdown/README.md similarity index 100% rename from README.md rename to packages/flutter_markdown/README.md diff --git a/example/example.dart b/packages/flutter_markdown/example/example.dart similarity index 100% rename from example/example.dart rename to packages/flutter_markdown/example/example.dart diff --git a/flutter_markdown.iml b/packages/flutter_markdown/flutter_markdown.iml similarity index 100% rename from flutter_markdown.iml rename to packages/flutter_markdown/flutter_markdown.iml diff --git a/lib/flutter_markdown.dart b/packages/flutter_markdown/lib/flutter_markdown.dart similarity index 100% rename from lib/flutter_markdown.dart rename to packages/flutter_markdown/lib/flutter_markdown.dart diff --git a/lib/src/builder.dart b/packages/flutter_markdown/lib/src/builder.dart similarity index 100% rename from lib/src/builder.dart rename to packages/flutter_markdown/lib/src/builder.dart diff --git a/lib/src/style_sheet.dart b/packages/flutter_markdown/lib/src/style_sheet.dart similarity index 100% rename from lib/src/style_sheet.dart rename to packages/flutter_markdown/lib/src/style_sheet.dart diff --git a/lib/src/widget.dart b/packages/flutter_markdown/lib/src/widget.dart similarity index 100% rename from lib/src/widget.dart rename to packages/flutter_markdown/lib/src/widget.dart diff --git a/pubspec.yaml b/packages/flutter_markdown/pubspec.yaml similarity index 100% rename from pubspec.yaml rename to packages/flutter_markdown/pubspec.yaml diff --git a/test/flutter_markdown_test.dart b/packages/flutter_markdown/test/flutter_markdown_test.dart similarity index 100% rename from test/flutter_markdown_test.dart rename to packages/flutter_markdown/test/flutter_markdown_test.dart