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
This commit is contained in:
Adam Barth 2017-03-27 20:47:33 -07:00 committed by GitHub
parent e2b49d64d6
commit bbac5dcb45
11 changed files with 811 additions and 917 deletions

View File

@ -185,6 +185,8 @@ class TextSpan {
final String indent = '$prefix ';
if (style != null)
buffer.writeln(style.toString(indent));
if (recognizer != null)
buffer.writeln('${indent}recognizer: ${recognizer.runtimeType}');
if (text != null)
buffer.writeln('$indent"$text"');
if (children != null) {

View File

@ -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';

View File

@ -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';

View File

@ -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<String> _kBlockTags = new Set<String>.from(<String>[
'p',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'li',
'blockquote',
'img',
'pre',
'ol',
'ul',
]);
const List<String> _kListTags = const <String>['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<Widget> children = <Widget>[];
int nextListIndex = 0;
}
class _InlineElement {
final List<TextSpan> children = <TextSpan>[];
}
/// 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<String> _listIndents = <String>[];
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<Widget> build(List<md.Node> 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: <Widget>[
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<String> parts = src.split('#');
if (parts.isEmpty)
return const SizedBox();
final String path = parts.first;
double width;
double height;
if (parts.length == 2) {
final List<String> 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());
}
}
}

View File

@ -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: <https://daringfireball.net/projects/markdown/>
///
/// 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));
}
}

View File

@ -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: <https://daringfireball.net/projects/markdown/>.
///
/// 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<MarkdownBodyRaw> {
@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<String> 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<Widget> blocks = <Widget>[];
for (_Block block in _cachedBlocks) {
blocks.add(block.build(context));
}
return new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: blocks
);
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('cached blocks identity: ${_cachedBlocks.hashCode}');
}
}
class _Renderer implements md.NodeVisitor {
List<_Block> render(List<md.Node> nodes, MarkdownStyleRaw markdownStyle, SyntaxHighlighter syntaxHighlighter, _LinkHandler linkHandler) {
assert(markdownStyle != null);
_blocks = <_Block>[];
_listIndents = <String>[];
_markdownStyle = markdownStyle;
_syntaxHighlighter = syntaxHighlighter;
_linkHandler = linkHandler;
for (final md.Node node in nodes) {
node.accept(this);
}
return _blocks;
}
List<_Block> _blocks;
List<String> _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<String>.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<String> _kBlockTags = const <String>['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'blockquote', 'img', 'pre', 'ol', 'ul'];
static const List<String> _kListTags = const <String>['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<String, String> attributes;
final MarkdownStyleRaw markdownStyle;
final List<String> 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<Widget> subWidgets = <Widget>[];
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: <Widget>[
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<TextSpan> children = <TextSpan>[];
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<String> parts = src.split('#');
if (parts.isEmpty) return new Container();
final String path = parts.first;
double width;
double height;
if (parts.length == 2) {
final List<String> 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: <TextSpan>[new TextSpan(text: source)]);
}
}

View File

@ -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)
)
);
}

View File

@ -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<String, TextStyle> _styles;
Map<String, TextStyle> get styles => _styles;
void _init() {
_styles = <String, TextStyle>{
'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
};
}
}

View File

@ -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 = <String, TextStyle>{
'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<String, TextStyle> get styles => _styles;
Map<String, TextStyle> _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,
);
}
}

View File

@ -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.
/// * <https://daringfireball.net/projects/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<Widget> children);
@override
_MarkdownWidgetState createState() => new _MarkdownWidgetState();
}
class _MarkdownWidgetState extends State<MarkdownWidget> implements MarkdownBuilderDelegate {
List<Widget> _children;
final List<GestureRecognizer> _recognizers = <GestureRecognizer>[];
@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<String> 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<GestureRecognizer> localRecognizers = new List<GestureRecognizer>.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.
/// * <https://daringfireball.net/projects/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<Widget> 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.
/// * <https://daringfireball.net/projects/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<Widget> children) {
return new ListView(padding: padding, children: children);
}
}

View File

@ -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<Widget> widgets = tester.allWidgets;
_expectWidgetTypes(widgets, <Type>[MarkdownBody, Column, Container, Padding, RichText]);
_expectTextStrings(widgets, <String>['Hello']);
final Iterable<Widget> widgets = tester.allWidgets;
_expectWidgetTypes(widgets, <Type>[MarkdownBody, Column, RichText]);
_expectTextStrings(widgets, <String>['Hello']);
});
testWidgets('Header', (WidgetTester tester) async {
await tester.pumpWidget(new MarkdownBody(data: '# Header'));
await tester.pumpWidget(new MarkdownBody(data: '# Header'));
final Iterable<Widget> widgets = tester.allWidgets;
_expectWidgetTypes(widgets, <Type>[MarkdownBody, Column, Container, Padding, RichText]);
_expectTextStrings(widgets, <String>['Header']);
final Iterable<Widget> widgets = tester.allWidgets;
_expectWidgetTypes(widgets, <Type>[MarkdownBody, Column, RichText]);
_expectTextStrings(widgets, <String>['Header']);
});
testWidgets('Empty string', (WidgetTester tester) async {
await tester.pumpWidget(new MarkdownBody(data: ''));
await tester.pumpWidget(new MarkdownBody(data: ''));
final Iterable<Widget> widgets = tester.allWidgets;
_expectWidgetTypes(widgets, <Type>[MarkdownBody, Column]);
final Iterable<Widget> widgets = tester.allWidgets;
_expectWidgetTypes(widgets, <Type>[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(new MarkdownBody(data: '1. Item 1\n1. Item 2\n2. Item 3'));
final Iterable<Widget> widgets = tester.allWidgets;
_expectTextStrings(widgets, <String>[
'1.',
'Item 1',
'2.',
'Item 2',
'3.',
'Item 3']
);
final Iterable<Widget> widgets = tester.allWidgets;
_expectTextStrings(widgets, <String>[
'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'));
await tester.pumpWidget(new MarkdownBody(data: '- Item 1\n- Item 2\n- Item 3'));
final Iterable<Widget> widgets = tester.allWidgets;
_expectTextStrings(widgets, <String>[
'',
'Item 1',
'',
'Item 2',
'',
'Item 3']
);
final Iterable<Widget> widgets = tester.allWidgets;
_expectTextStrings(widgets, <String>[
'',
'Item 1',
'',
'Item 2',
'',
'Item 3',
]);
});
testWidgets('Scrollable wrapping', (WidgetTester tester) async {
await tester.pumpWidget(new Markdown(data: ''));
await tester.pumpWidget(new Markdown(data: ''));
final List<Widget> widgets = tester.allWidgets.toList();
_expectWidgetTypes(widgets.take(2), <Type>[
Markdown,
SingleChildScrollView,
]);
_expectWidgetTypes(widgets.reversed.take(3).toList().reversed, <Type>[
Padding,
MarkdownBody,
Column
]);
final List<Widget> widgets = tester.allWidgets.toList();
_expectWidgetTypes(widgets.take(2), <Type>[
Markdown,
ListView,
]);
_expectWidgetTypes(widgets.reversed.take(2).toList().reversed, <Type>[
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<Widget> widgets = tester.allWidgets;
_expectTextStrings(widgets, <String>['Line 1', 'Line 2']);
final Iterable<Widget> widgets = tester.allWidgets;
_expectTextStrings(widgets, <String>['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<Widget> widgets = tester.allWidgets;
_expectTextStrings(widgets, <String>['Line 1 &lt;','c &lt; c c','&lt; Line 2']);
final Iterable<Widget> widgets = tester.allWidgets;
_expectTextStrings(widgets, <String>['Line 1 &lt;','c &lt; c c','&lt; Line 2']);
});
testWidgets('Changing config - data', (WidgetTester tester) async {
await tester.pumpWidget(new Markdown(data: 'Data1'));
_expectTextStrings(tester.allWidgets, <String>['Data1']);
await tester.pumpWidget(new Markdown(data: 'Data1'));
_expectTextStrings(tester.allWidgets, <String>['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, <String>['Data2']);
await tester.pumpWidget(new Markdown(data: 'Data2'));
_expectTextStrings(tester.allWidgets, <String>['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'
);
}