Commit 48c7a04f authored by Viktor Lidholt's avatar Viktor Lidholt

Adds initial version of Markdown renderer

parent b193854b
# 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.
// 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: <String, RouteBuilder>{
'/': (RouteArguments args) => new Scaffold(
toolBar: new ToolBar(center: new Text("Markdown Demo")),
body: new Markdown(data: _kMarkdownData)
)
}
));
}
// 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';
// 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';
// 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));
}
}
// 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<MarkdownBodyRaw> {
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<Widget> blocks = <Widget>[];
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<String> 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<md.Node> nodes, MarkdownStyleRaw markdownStyle, SyntaxHighlighter syntaxHighlighter) {
assert(markdownStyle != null);
_blocks = <_Block>[];
_listIndents = <String>[];
_markdownStyle = markdownStyle;
_syntaxHighlighter = syntaxHighlighter;
for (final md.Node node in nodes) {
node.accept(this);
}
return _blocks;
}
List<_Block> _blocks;
List<String> _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<String>.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<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;
_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<String, String> attributes;
final MarkdownStyleRaw markdownStyle;
final List<String> 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<Widget> subWidgets = <Widget>[];
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: <Widget>[
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<TextSpan> children = <TextSpan>[];
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<String> parts = src.split('#');
if (parts.length == 0) return new Container();
String path = parts.first;
double width;
double height;
if (parts.length == 2) {
List<String> 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: <TextSpan>[new TextSpan(text: source)]);
}
}
// 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
)
);
}
// 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<String, TextStyle> _styles;
Map<String, TextStyle> 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
};
}
}
name: flutter_markdown
description: A markdown renderer for Flutter.
version: 0.1.0
author: Flutter Authors <flutter-dev@googlegroups.com>
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
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<Element> elements = _listElements(tester);
_expectWidgetTypes(elements, <Type>[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<Element> elements = _listElements(tester);
_expectWidgetTypes(elements, <Type>[MarkdownBody, Column, Container, Padding, RichText]);
expect(textSpan.children[0].text, equals("Header"));
});
});
test("Empty string", () {
testWidgets((WidgetTester tester) {
tester.pumpWidget(new MarkdownBody(data: ""));
List<Element> elements = _listElements(tester);
_expectWidgetTypes(elements, <Type>[MarkdownBody, Column]);
});
});
test("Ordered list", () {
testWidgets((WidgetTester tester) {
tester.pumpWidget(new MarkdownBody(data: "1. Item 1\n1. Item 2\n2. Item 3"));
List<Element> elements = _listElements(tester);
_expectTextStrings(elements, <String>[
"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<Element> elements = _listElements(tester);
_expectTextStrings(elements, <String>[
"•",
"Item 1",
"•",
"Item 2",
"•",
"Item 3"]
);
});
});
test("Scrollable wrapping", () {
testWidgets((WidgetTester tester) {
tester.pumpWidget(new Markdown(data: ""));
List<Element> elements = _listElements(tester);
for (Element element in elements) print("e: $element");
_expectWidgetTypes(elements, <Type>[
Markdown,
ScrollableViewport,
null, null, null, null, null, // ScrollableViewport internals
Padding,
MarkdownBody,
Column
]);
});
});
}
List<Element> _listElements(WidgetTester tester) {
List<Element> elements = <Element>[];
tester.walkElements((Element element) {
elements.add(element);
});
return elements;
}
void _expectWidgetTypes(List<Element> elements, List<Type> 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<Element> elements, List<String> 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;
}
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