Adds initial version of Markdown renderer
This commit is contained in:
parent
b193854bfd
commit
48c7a04f1f
22
packages/flutter_markdown/README.md
Normal file
22
packages/flutter_markdown/README.md
Normal file
@ -0,0 +1,22 @@
|
||||
# Flutter Markdown
|
||||
|
||||
A markdown renderer for Flutter. It supports the
|
||||
[original format](https://daringfireball.net/projects/markdown/), but no inline
|
||||
html.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Using the Markdown widget is simple, just pass in the source markdown as a
|
||||
string:
|
||||
|
||||
new Markdown(data: markdownSource);
|
||||
|
||||
If you do not want the padding or scrolling behavior, use the MarkdownBody
|
||||
instead:
|
||||
|
||||
new MarkdownBody(data: markdownSource);
|
||||
|
||||
By default, Markdown uses the formatting from the current material design theme,
|
||||
but it's possible to create your own custom styling. Use the MarkdownStyle class
|
||||
to pass in your own style. If you don't want to use Markdown outside of material
|
||||
design, use the MarkdownRaw class.
|
36
packages/flutter_markdown/example/demo.dart
Normal file
36
packages/flutter_markdown/example/demo.dart
Normal file
@ -0,0 +1,36 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
|
||||
const String _kMarkdownData = """# Markdown Example
|
||||
Markdown allows you to easily include formatted text, images, and even formatted Dart code in your app.
|
||||
|
||||
## Styling
|
||||
Style text as _italic_, __bold__, or `inline code`.
|
||||
|
||||
- Use bulleted lists
|
||||
- To better clarify
|
||||
- Your points
|
||||
|
||||
## Code blocks
|
||||
Formatted Dart code looks really pretty too. This is an example of how to create your own Markdown widget:
|
||||
|
||||
new Markdown(data: "Hello _world_!");
|
||||
|
||||
Enjoy!
|
||||
""";
|
||||
|
||||
void main() {
|
||||
runApp(new MaterialApp(
|
||||
title: "Markdown Demo",
|
||||
routes: <String, RouteBuilder>{
|
||||
'/': (RouteArguments args) => new Scaffold(
|
||||
toolBar: new ToolBar(center: new Text("Markdown Demo")),
|
||||
body: new Markdown(data: _kMarkdownData)
|
||||
)
|
||||
}
|
||||
));
|
||||
}
|
8
packages/flutter_markdown/lib/flutter_markdown.dart
Normal file
8
packages/flutter_markdown/lib/flutter_markdown.dart
Normal file
@ -0,0 +1,8 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
library flutter_markdown;
|
||||
|
||||
export 'src/markdown.dart';
|
||||
export 'src/markdown_style.dart';
|
8
packages/flutter_markdown/lib/flutter_markdown_raw.dart
Normal file
8
packages/flutter_markdown/lib/flutter_markdown_raw.dart
Normal file
@ -0,0 +1,8 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
library flutter_markdown;
|
||||
|
||||
export 'src/markdown_raw.dart';
|
||||
export 'src/markdown_style_raw.dart';
|
84
packages/flutter_markdown/lib/src/markdown.dart
Normal file
84
packages/flutter_markdown/lib/src/markdown.dart
Normal file
@ -0,0 +1,84 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'markdown_raw.dart';
|
||||
import 'markdown_style.dart';
|
||||
|
||||
/// A [Widget] that renders markdown formatted text. It supports all standard
|
||||
/// markdowns from the original markdown specification found here:
|
||||
/// https://daringfireball.net/projects/markdown/ The rendered markdown is
|
||||
/// placed in a padded scrolling view port. If you do not want the scrolling
|
||||
/// behaviour, use the [MarkdownBody] class instead.
|
||||
class Markdown extends MarkdownRaw {
|
||||
|
||||
/// Creates a new Markdown [Widget] that renders the markdown formatted string
|
||||
/// passed in as [data]. By default the markdown will be rendered using the
|
||||
/// styles from the current theme, but you can optionally pass in a custom
|
||||
/// [markdownStyle] that specifies colors and fonts to use. Code blocks are
|
||||
/// by default not using syntax highlighting, but it's possible to pass in
|
||||
/// a custom [syntaxHighlighter].
|
||||
///
|
||||
/// new Markdown(data: "Hello _world_!");
|
||||
Markdown({
|
||||
String data,
|
||||
SyntaxHighlighter syntaxHighlighter,
|
||||
MarkdownStyle markdownStyle
|
||||
}) : super(
|
||||
data: data,
|
||||
syntaxHighlighter: syntaxHighlighter,
|
||||
markdownStyle: markdownStyle
|
||||
);
|
||||
|
||||
MarkdownBody createMarkdownBody({
|
||||
String data,
|
||||
MarkdownStyle markdownStyle,
|
||||
SyntaxHighlighter syntaxHighlighter
|
||||
}) {
|
||||
return new MarkdownBody(
|
||||
data: data,
|
||||
markdownStyle: markdownStyle,
|
||||
syntaxHighlighter: syntaxHighlighter
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A [Widget] that renders markdown formatted text. It supports all standard
|
||||
/// markdowns from the original markdown specification found here:
|
||||
/// https://daringfireball.net/projects/markdown/ This class doesn't implement
|
||||
/// any scrolling behavior, if you want scrolling either wrap the widget in
|
||||
/// a [ScrollableViewport] or use the [Markdown] widget.
|
||||
class MarkdownBody extends MarkdownBodyRaw {
|
||||
|
||||
/// Creates a new Markdown [Widget] that renders the markdown formatted string
|
||||
/// passed in as [data]. By default the markdown will be rendered using the
|
||||
/// styles from the current theme, but you can optionally pass in a custom
|
||||
/// [markdownStyle] that specifies colors and fonts to use. Code blocks are
|
||||
/// by default not using syntax highlighting, but it's possible to pass in
|
||||
/// a custom [syntaxHighlighter].
|
||||
///
|
||||
/// Typically, you may want to wrap the [MarkdownBody] widget in a [Padding] and
|
||||
/// a [ScrollableViewport], or use the [Markdown] class
|
||||
///
|
||||
/// new ScrollableViewport(
|
||||
/// child: new Padding(
|
||||
/// padding: new EdgeDims.all(16.0),
|
||||
/// child: new Markdown(data: markdownSource)
|
||||
/// )
|
||||
/// )
|
||||
MarkdownBody({
|
||||
String data,
|
||||
SyntaxHighlighter syntaxHighlighter,
|
||||
MarkdownStyle markdownStyle
|
||||
}) : super(
|
||||
data: data,
|
||||
syntaxHighlighter: syntaxHighlighter,
|
||||
markdownStyle: markdownStyle
|
||||
);
|
||||
|
||||
MarkdownStyle createDefaultStyle(BuildContext context) {
|
||||
return new MarkdownStyle.defaultFromTheme(Theme.of(context));
|
||||
}
|
||||
}
|
433
packages/flutter_markdown/lib/src/markdown_raw.dart
Normal file
433
packages/flutter_markdown/lib/src/markdown_raw.dart
Normal file
@ -0,0 +1,433 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:markdown/markdown.dart' as md;
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'markdown_style_raw.dart';
|
||||
|
||||
|
||||
/// A [Widget] that renders markdown formatted text. It supports all standard
|
||||
/// markdowns from the original markdown specification found here:
|
||||
/// https://daringfireball.net/projects/markdown/ The rendered markdown is
|
||||
/// placed in a padded scrolling view port. If you do not want the scrolling
|
||||
/// behaviour, use the [MarkdownBodyRaw] class instead.
|
||||
class MarkdownRaw extends StatelessComponent {
|
||||
|
||||
/// Creates a new Markdown [Widget] that renders the markdown formatted string
|
||||
/// passed in as [data]. By default the markdown will be rendered using the
|
||||
/// styles from the current theme, but you can optionally pass in a custom
|
||||
/// [markdownStyle] that specifies colors and fonts to use. Code blocks are
|
||||
/// by default not using syntax highlighting, but it's possible to pass in
|
||||
/// a custom [syntaxHighlighter].
|
||||
///
|
||||
/// new MarkdownRaw(data: "Hello _world_!", markdownStyle: myStyle);
|
||||
MarkdownRaw({
|
||||
this.data,
|
||||
this.markdownStyle,
|
||||
this.syntaxHighlighter,
|
||||
this.padding: const EdgeDims.all(16.0)
|
||||
});
|
||||
|
||||
/// Markdown styled text
|
||||
final String data;
|
||||
|
||||
/// Style used for rendering the markdown
|
||||
final MarkdownStyleRaw markdownStyle;
|
||||
|
||||
/// The syntax highlighter used to color text in code blocks
|
||||
final SyntaxHighlighter syntaxHighlighter;
|
||||
|
||||
/// Padding used
|
||||
final EdgeDims padding;
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
return new ScrollableViewport(
|
||||
child: new Padding(
|
||||
padding: padding,
|
||||
child: createMarkdownBody(
|
||||
data: data,
|
||||
markdownStyle: markdownStyle,
|
||||
syntaxHighlighter: syntaxHighlighter
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
MarkdownBodyRaw createMarkdownBody({
|
||||
String data,
|
||||
MarkdownStyleRaw markdownStyle,
|
||||
SyntaxHighlighter syntaxHighlighter
|
||||
}) {
|
||||
return new MarkdownBodyRaw(
|
||||
data: data,
|
||||
markdownStyle: markdownStyle,
|
||||
syntaxHighlighter: syntaxHighlighter
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A [Widget] that renders markdown formatted text. It supports all standard
|
||||
/// markdowns from the original markdown specification found here:
|
||||
/// https://daringfireball.net/projects/markdown/ This class doesn't implement
|
||||
/// any scrolling behavior, if you want scrolling either wrap the widget in
|
||||
/// a [ScrollableViewport] or use the [MarkdownRaw] widget.
|
||||
class MarkdownBodyRaw extends StatefulComponent {
|
||||
|
||||
/// Creates a new Markdown [Widget] that renders the markdown formatted string
|
||||
/// passed in as [data]. You need to pass in a [markdownStyle] that defines
|
||||
/// how the code is rendered. Code blocks are by default not using syntax
|
||||
/// highlighting, but it's possible to pass in a custom [syntaxHighlighter].
|
||||
///
|
||||
/// Typically, you may want to wrap the [MarkdownBodyRaw] widget in a
|
||||
/// [Padding] and a [ScrollableViewport], or use the [Markdown class]
|
||||
///
|
||||
/// new ScrollableViewport(
|
||||
/// child: new Padding(
|
||||
/// padding: new EdgeDims.all(16.0),
|
||||
/// child: new MarkdownBodyRaw(
|
||||
/// data: markdownSource,
|
||||
/// markdownStyle: myStyle
|
||||
/// )
|
||||
/// )
|
||||
/// )
|
||||
MarkdownBodyRaw({
|
||||
this.data,
|
||||
this.markdownStyle,
|
||||
this.syntaxHighlighter
|
||||
});
|
||||
|
||||
/// Markdown styled text
|
||||
final String data;
|
||||
|
||||
/// Style used for rendering the markdown
|
||||
final MarkdownStyleRaw markdownStyle;
|
||||
|
||||
/// The syntax highlighter used to color text in code blocks
|
||||
final SyntaxHighlighter syntaxHighlighter;
|
||||
|
||||
_MarkdownBodyRawState createState() => new _MarkdownBodyRawState();
|
||||
|
||||
MarkdownStyleRaw createDefaultStyle(BuildContext context) => null;
|
||||
}
|
||||
|
||||
class _MarkdownBodyRawState extends State<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)]);
|
||||
}
|
||||
}
|
77
packages/flutter_markdown/lib/src/markdown_style.dart
Normal file
77
packages/flutter_markdown/lib/src/markdown_style.dart
Normal file
@ -0,0 +1,77 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'markdown_style_raw.dart';
|
||||
|
||||
/// Style used for rendering markdown formatted text using the [MarkdownBody]
|
||||
/// widget.
|
||||
class MarkdownStyle extends MarkdownStyleRaw{
|
||||
|
||||
/// Creates a [MarkdownStyle] from the [TextStyle]s in the provided [theme].
|
||||
MarkdownStyle.defaultFromTheme(ThemeData theme) : super(
|
||||
a: new TextStyle(color: Colors.blue[500]),
|
||||
p: theme.text.body1,
|
||||
code: new TextStyle(
|
||||
color: Colors.grey[700],
|
||||
fontFamily: "monospace",
|
||||
fontSize: theme.text.body1.fontSize * 0.85
|
||||
),
|
||||
h1: theme.text.headline,
|
||||
h2: theme.text.title,
|
||||
h3: theme.text.subhead,
|
||||
h4: theme.text.body2,
|
||||
h5: theme.text.body2,
|
||||
h6: theme.text.body2,
|
||||
em: new TextStyle(fontStyle: FontStyle.italic),
|
||||
strong: new TextStyle(fontWeight: FontWeight.bold),
|
||||
blockquote: theme.text.body1,
|
||||
blockSpacing: 8.0,
|
||||
listIndent: 32.0,
|
||||
blockquotePadding: 8.0,
|
||||
blockquoteDecoration: new BoxDecoration(
|
||||
backgroundColor: Colors.blue[100],
|
||||
borderRadius: 2.0
|
||||
),
|
||||
codeblockPadding: 8.0,
|
||||
codeblockDecoration: new BoxDecoration(
|
||||
backgroundColor: Colors.grey[100],
|
||||
borderRadius: 2.0
|
||||
)
|
||||
);
|
||||
|
||||
/// Creates a [MarkdownStyle] from the [TextStyle]s in the provided [theme].
|
||||
/// This style uses larger fonts for the headings than in
|
||||
/// [MarkdownStyle.defaultFromTheme].
|
||||
MarkdownStyle.largeFromTheme(ThemeData theme) : super (
|
||||
a: new TextStyle(color: Colors.blue[500]),
|
||||
p: theme.text.body1,
|
||||
code: new TextStyle(
|
||||
color: Colors.grey[700],
|
||||
fontFamily: "monospace",
|
||||
fontSize: theme.text.body1.fontSize * 0.85
|
||||
),
|
||||
h1: theme.text.display3,
|
||||
h2: theme.text.display2,
|
||||
h3: theme.text.display1,
|
||||
h4: theme.text.headline,
|
||||
h5: theme.text.title,
|
||||
h6: theme.text.subhead,
|
||||
em: new TextStyle(fontStyle: FontStyle.italic),
|
||||
strong: new TextStyle(fontWeight: FontWeight.bold),
|
||||
blockquote: theme.text.body1,
|
||||
blockSpacing: 8.0,
|
||||
listIndent: 32.0,
|
||||
blockquotePadding: 8.0,
|
||||
blockquoteDecoration: new BoxDecoration(
|
||||
backgroundColor: Colors.blue[100],
|
||||
borderRadius: 2.0
|
||||
),
|
||||
codeblockPadding: 8.0,
|
||||
codeblockDecoration: new BoxDecoration(
|
||||
backgroundColor: Colors.grey[100],
|
||||
borderRadius: 2.0
|
||||
)
|
||||
);
|
||||
}
|
120
packages/flutter_markdown/lib/src/markdown_style_raw.dart
Normal file
120
packages/flutter_markdown/lib/src/markdown_style_raw.dart
Normal file
@ -0,0 +1,120 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Style used for rendering markdown formatted text using the [MarkdownBody]
|
||||
/// widget.
|
||||
class MarkdownStyleRaw {
|
||||
|
||||
/// Creates a new [MarkdownStyleRaw]
|
||||
MarkdownStyleRaw({
|
||||
this.a,
|
||||
this.p,
|
||||
this.code,
|
||||
this.h1,
|
||||
this.h2,
|
||||
this.h3,
|
||||
this.h4,
|
||||
this.h5,
|
||||
this.h6,
|
||||
this.em,
|
||||
this.strong,
|
||||
this.blockquote,
|
||||
this.blockSpacing,
|
||||
this.listIndent,
|
||||
this.blockquotePadding,
|
||||
this.blockquoteDecoration,
|
||||
this.codeblockPadding,
|
||||
this.codeblockDecoration
|
||||
}) {
|
||||
_init();
|
||||
}
|
||||
|
||||
/// Creates a new [MarkdownStyleRaw] based on the current style, with the
|
||||
/// provided paramaters overridden.
|
||||
MarkdownStyleRaw copyWith({
|
||||
TextStyle a,
|
||||
TextStyle p,
|
||||
TextStyle code,
|
||||
TextStyle h1,
|
||||
TextStyle h2,
|
||||
TextStyle h3,
|
||||
TextStyle h4,
|
||||
TextStyle h5,
|
||||
TextStyle h6,
|
||||
TextStyle em,
|
||||
TextStyle strong,
|
||||
TextStyle blockquote,
|
||||
double blockSpacing,
|
||||
double listIndent,
|
||||
double blockquotePadding,
|
||||
BoxDecoration blockquoteDecoration,
|
||||
double codeblockPadding,
|
||||
BoxDecoration codeblockDecoration
|
||||
}) {
|
||||
return new MarkdownStyleRaw(
|
||||
a: a != null ? a : this.a,
|
||||
p: p != null ? p : this.p,
|
||||
code: code != null ? code : this.code,
|
||||
h1: h1 != null ? h1 : this.h1,
|
||||
h2: h2 != null ? h2 : this.h2,
|
||||
h3: h3 != null ? h3 : this.h3,
|
||||
h4: h4 != null ? h4 : this.h4,
|
||||
h5: h5 != null ? h5 : this.h5,
|
||||
h6: h6 != null ? h6 : this.h6,
|
||||
em: em != null ? em : this.em,
|
||||
strong: strong != null ? strong : this.strong,
|
||||
blockquote: blockquote != null ? blockquote : this.blockquote,
|
||||
blockSpacing: blockSpacing != null ? blockSpacing : this.blockSpacing,
|
||||
listIndent: listIndent != null ? listIndent : this.listIndent,
|
||||
blockquotePadding: blockquotePadding != null ? blockquotePadding : this.blockquotePadding,
|
||||
blockquoteDecoration: blockquoteDecoration != null ? blockquoteDecoration : this.blockquoteDecoration,
|
||||
codeblockPadding: codeblockPadding != null ? codeblockPadding : this.codeblockPadding,
|
||||
codeblockDecoration: codeblockDecoration != null ? codeblockDecoration : this.codeblockDecoration
|
||||
);
|
||||
}
|
||||
|
||||
final TextStyle a;
|
||||
final TextStyle p;
|
||||
final TextStyle code;
|
||||
final TextStyle h1;
|
||||
final TextStyle h2;
|
||||
final TextStyle h3;
|
||||
final TextStyle h4;
|
||||
final TextStyle h5;
|
||||
final TextStyle h6;
|
||||
final TextStyle em;
|
||||
final TextStyle strong;
|
||||
final TextStyle blockquote;
|
||||
final double blockSpacing;
|
||||
final double listIndent;
|
||||
final double blockquotePadding;
|
||||
final BoxDecoration blockquoteDecoration;
|
||||
final double codeblockPadding;
|
||||
final BoxDecoration codeblockDecoration;
|
||||
|
||||
Map<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
|
||||
};
|
||||
}
|
||||
}
|
18
packages/flutter_markdown/pubspec.yaml
Normal file
18
packages/flutter_markdown/pubspec.yaml
Normal file
@ -0,0 +1,18 @@
|
||||
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
|
134
packages/flutter_markdown/test/flutter_markdown_test.dart
Normal file
134
packages/flutter_markdown/test/flutter_markdown_test.dart
Normal file
@ -0,0 +1,134 @@
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() {
|
||||
test("Simple string", () {
|
||||
testWidgets((WidgetTester tester) {
|
||||
tester.pumpWidget(new MarkdownBody(data: "Hello"));
|
||||
|
||||
Element textElement = tester.findElement((Element element) => element.widget is RichText);
|
||||
RichText textWidget = textElement.widget;
|
||||
TextSpan textSpan = textWidget.text;
|
||||
|
||||
List<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;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user