Commit bbac5dcb authored by Adam Barth's avatar Adam Barth Committed by GitHub

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
parent e2b49d64
......@@ -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) {
......
......@@ -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';
// 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';
// 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());
}
}
}
// 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));
}
}
This diff is collapsed.
// 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)
)
);
}
......@@ -2,15 +2,12 @@
// 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';
import 'package:flutter/material.dart';
/// Style used for rendering markdown formatted text using the [MarkdownBody]
/// widget.
class MarkdownStyleRaw {
/// Creates a new [MarkdownStyleRaw]
MarkdownStyleRaw({
/// 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,
......@@ -29,13 +26,97 @@ class MarkdownStyleRaw {
this.blockquoteDecoration,
this.codeblockPadding,
this.codeblockDecoration
}) {
_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
};
/// 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 [MarkdownStyleRaw] based on the current style, with the
/// Creates a new [MarkdownStyleSheet] based on the current style, with the
/// provided parameters overridden.
MarkdownStyleRaw copyWith({
MarkdownStyleSheet copyWith({
TextStyle a,
TextStyle p,
TextStyle code,
......@@ -51,11 +132,11 @@ class MarkdownStyleRaw {
double blockSpacing,
double listIndent,
double blockquotePadding,
BoxDecoration blockquoteDecoration,
Decoration blockquoteDecoration,
double codeblockPadding,
BoxDecoration codeblockDecoration
Decoration codeblockDecoration
}) {
return new MarkdownStyleRaw(
return new MarkdownStyleSheet(
a: a != null ? a : this.a,
p: p != null ? p : this.p,
code: code != null ? code : this.code,
......@@ -77,45 +158,112 @@ class MarkdownStyleRaw {
);
}
/// 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;
final BoxDecoration blockquoteDecoration;
/// The decoration to use behind `blockquote` elements.
final Decoration blockquoteDecoration;
/// The padding to use for `pre` elements.
final double codeblockPadding;
final BoxDecoration codeblockDecoration;
Map<String, TextStyle> _styles;
/// 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;
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
};
@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,
);
}
}
// 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);
}
}
......@@ -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'));
final Iterable<Widget> widgets = tester.allWidgets;
_expectTextStrings(widgets, <String>[
'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<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'));
final Iterable<Widget> widgets = tester.allWidgets;
_expectTextStrings(widgets, <String>[
'•',
'Item 1',
'•',
'Item 2',
'•',
'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',
]);
});
testWidgets('Scrollable wrapping', (WidgetTester tester) async {
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
]);
await tester.pumpWidget(new Markdown(data: ''));
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'
);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment