mirror of
https://github.com/flutter/flutter
synced 2024-10-12 19:23:02 +00:00
Move flutter_markdown to https://github.com/flutter/flutter_markdown (#9807)
Now that we can host packages on pub that depend on Flutter, we can host flutter_markdown in its own git repository.
This commit is contained in:
parent
9cb12086bb
commit
3bd2ecb2c4
|
@ -7,10 +7,9 @@
|
|||
<module fileurl="file://$PROJECT_DIR$/packages/flutter/flutter.iml" filepath="$PROJECT_DIR$/packages/flutter/flutter.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/packages/flutter_driver/flutter_driver.iml" filepath="$PROJECT_DIR$/packages/flutter_driver/flutter_driver.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/examples/flutter_gallery/flutter_gallery.iml" filepath="$PROJECT_DIR$/examples/flutter_gallery/flutter_gallery.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/packages/flutter_markdown/flutter_markdown.iml" filepath="$PROJECT_DIR$/packages/flutter_markdown/flutter_markdown.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/packages/flutter_test/flutter_test.iml" filepath="$PROJECT_DIR$/packages/flutter_test/flutter_test.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/packages/flutter_tools/flutter_tools.iml" filepath="$PROJECT_DIR$/packages/flutter_tools/flutter_tools.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/examples/hello_world/hello_world.iml" filepath="$PROJECT_DIR$/examples/hello_world/hello_world.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
</project>
|
||||
|
|
|
@ -100,7 +100,6 @@ Future<Null> main() async {
|
|||
);
|
||||
await _runFlutterTest(path.join(flutterRoot, 'packages', 'flutter_driver'));
|
||||
await _runFlutterTest(path.join(flutterRoot, 'packages', 'flutter_test'));
|
||||
await _runFlutterTest(path.join(flutterRoot, 'packages', 'flutter_markdown'));
|
||||
await _pubRunTest(path.join(flutterRoot, 'packages', 'flutter_tools'));
|
||||
|
||||
await _runAllDartTests(path.join(flutterRoot, 'dev', 'devicelab'));
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
# 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.
|
|
@ -1,37 +0,0 @@
|
|||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '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
|
||||
|
||||
## Links
|
||||
You can use [hyperlinks](hyperlink) in markdown
|
||||
|
||||
## Code blocks
|
||||
Formatted Dart code looks really pretty too. This is an example of how to create your own Markdown widget:
|
||||
|
||||
new Markdown(data: 'Hello _world_!');
|
||||
|
||||
Enjoy!
|
||||
""";
|
||||
|
||||
void main() {
|
||||
runApp(new MaterialApp(
|
||||
title: "Markdown Demo",
|
||||
home: new Scaffold(
|
||||
appBar: new AppBar(title: const Text('Markdown Demo')),
|
||||
body: const Markdown(data: _kMarkdownData)
|
||||
)
|
||||
));
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/example/packages" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/test/packages" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="Dart SDK" level="project" />
|
||||
<orderEntry type="library" name="Dart Packages" level="project" />
|
||||
</component>
|
||||
</module>
|
|
@ -1,10 +0,0 @@
|
|||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
/// A library to render markdown formatted text.
|
||||
library flutter_markdown;
|
||||
|
||||
export 'src/builder.dart';
|
||||
export 'src/style_sheet.dart';
|
||||
export 'src/widget.dart';
|
|
@ -1,243 +0,0 @@
|
|||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/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 const 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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,269 +0,0 @@
|
|||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Defines which [TextStyle] objects to use for which Markdown elements.
|
||||
class MarkdownStyleSheet {
|
||||
/// Creates an explicit mapping of [TextStyle] objects to Markdown elements.
|
||||
MarkdownStyleSheet({
|
||||
this.a,
|
||||
this.p,
|
||||
this.code,
|
||||
this.h1,
|
||||
this.h2,
|
||||
this.h3,
|
||||
this.h4,
|
||||
this.h5,
|
||||
this.h6,
|
||||
this.em,
|
||||
this.strong,
|
||||
this.blockquote,
|
||||
this.blockSpacing,
|
||||
this.listIndent,
|
||||
this.blockquotePadding,
|
||||
this.blockquoteDecoration,
|
||||
this.codeblockPadding,
|
||||
this.codeblockDecoration
|
||||
}) : _styles = <String, TextStyle>{
|
||||
'a': a,
|
||||
'p': p,
|
||||
'li': p,
|
||||
'code': code,
|
||||
'pre': p,
|
||||
'h1': h1,
|
||||
'h2': h2,
|
||||
'h3': h3,
|
||||
'h4': h4,
|
||||
'h5': h5,
|
||||
'h6': h6,
|
||||
'em': em,
|
||||
'strong': strong,
|
||||
'blockquote': blockquote
|
||||
};
|
||||
|
||||
/// Creates a [MarkdownStyleSheet] from the [TextStyle]s in the provided [ThemeData].
|
||||
factory MarkdownStyleSheet.fromTheme(ThemeData theme) {
|
||||
return new MarkdownStyleSheet(
|
||||
a: const 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(
|
||||
color: Colors.blue.shade100,
|
||||
borderRadius: new BorderRadius.circular(2.0)
|
||||
),
|
||||
codeblockPadding: 8.0,
|
||||
codeblockDecoration: new BoxDecoration(
|
||||
color: 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
|
||||
/// [new MarkdownStyle.fromTheme].
|
||||
factory MarkdownStyleSheet.largeFromTheme(ThemeData theme) {
|
||||
return new MarkdownStyleSheet(
|
||||
a: const 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(
|
||||
color: Colors.blue.shade100,
|
||||
borderRadius: new BorderRadius.circular(2.0)
|
||||
),
|
||||
codeblockPadding: 8.0,
|
||||
codeblockDecoration: new BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: new BorderRadius.circular(2.0)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates a new [MarkdownStyleSheet] based on the current style, with the
|
||||
/// provided parameters overridden.
|
||||
MarkdownStyleSheet copyWith({
|
||||
TextStyle a,
|
||||
TextStyle p,
|
||||
TextStyle code,
|
||||
TextStyle h1,
|
||||
TextStyle h2,
|
||||
TextStyle h3,
|
||||
TextStyle h4,
|
||||
TextStyle h5,
|
||||
TextStyle h6,
|
||||
TextStyle em,
|
||||
TextStyle strong,
|
||||
TextStyle blockquote,
|
||||
double blockSpacing,
|
||||
double listIndent,
|
||||
double blockquotePadding,
|
||||
Decoration blockquoteDecoration,
|
||||
double codeblockPadding,
|
||||
Decoration codeblockDecoration
|
||||
}) {
|
||||
return new MarkdownStyleSheet(
|
||||
a: a != null ? a : this.a,
|
||||
p: p != null ? p : this.p,
|
||||
code: code != null ? code : this.code,
|
||||
h1: h1 != null ? h1 : this.h1,
|
||||
h2: h2 != null ? h2 : this.h2,
|
||||
h3: h3 != null ? h3 : this.h3,
|
||||
h4: h4 != null ? h4 : this.h4,
|
||||
h5: h5 != null ? h5 : this.h5,
|
||||
h6: h6 != null ? h6 : this.h6,
|
||||
em: em != null ? em : this.em,
|
||||
strong: strong != null ? strong : this.strong,
|
||||
blockquote: blockquote != null ? blockquote : this.blockquote,
|
||||
blockSpacing: blockSpacing != null ? blockSpacing : this.blockSpacing,
|
||||
listIndent: listIndent != null ? listIndent : this.listIndent,
|
||||
blockquotePadding: blockquotePadding != null ? blockquotePadding : this.blockquotePadding,
|
||||
blockquoteDecoration: blockquoteDecoration != null ? blockquoteDecoration : this.blockquoteDecoration,
|
||||
codeblockPadding: codeblockPadding != null ? codeblockPadding : this.codeblockPadding,
|
||||
codeblockDecoration: codeblockDecoration != null ? codeblockDecoration : this.codeblockDecoration
|
||||
);
|
||||
}
|
||||
|
||||
/// The [TextStyle] to use for `a` elements.
|
||||
final TextStyle a;
|
||||
|
||||
/// The [TextStyle] to use for `p` elements.
|
||||
final TextStyle p;
|
||||
|
||||
/// The [TextStyle] to use for `code` elements.
|
||||
final TextStyle code;
|
||||
|
||||
/// The [TextStyle] to use for `h1` elements.
|
||||
final TextStyle h1;
|
||||
|
||||
/// The [TextStyle] to use for `h2` elements.
|
||||
final TextStyle h2;
|
||||
|
||||
/// The [TextStyle] to use for `h3` elements.
|
||||
final TextStyle h3;
|
||||
|
||||
/// The [TextStyle] to use for `h4` elements.
|
||||
final TextStyle h4;
|
||||
|
||||
/// The [TextStyle] to use for `h5` elements.
|
||||
final TextStyle h5;
|
||||
|
||||
/// The [TextStyle] to use for `h6` elements.
|
||||
final TextStyle h6;
|
||||
|
||||
/// The [TextStyle] to use for `em` elements.
|
||||
final TextStyle em;
|
||||
|
||||
/// The [TextStyle] to use for `strong` elements.
|
||||
final TextStyle strong;
|
||||
|
||||
/// The [TextStyle] to use for `blockquote` elements.
|
||||
final TextStyle blockquote;
|
||||
|
||||
/// The amount of vertical space to use between block-level elements.
|
||||
final double blockSpacing;
|
||||
|
||||
/// The amount of horizontal space to indent list items.
|
||||
final double listIndent;
|
||||
|
||||
/// The padding to use for `blockquote` elements.
|
||||
final double blockquotePadding;
|
||||
|
||||
/// The decoration to use behind `blockquote` elements.
|
||||
final Decoration blockquoteDecoration;
|
||||
|
||||
/// The padding to use for `pre` elements.
|
||||
final double codeblockPadding;
|
||||
|
||||
/// The decoration to use behind for `pre` elements.
|
||||
final Decoration codeblockDecoration;
|
||||
|
||||
/// A [Map] from element name to the cooresponding [TextStyle] object.
|
||||
Map<String, TextStyle> get styles => _styles;
|
||||
Map<String, TextStyle> _styles;
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
if (identical(this, other))
|
||||
return true;
|
||||
if (other.runtimeType != MarkdownStyleSheet)
|
||||
return false;
|
||||
final MarkdownStyleSheet typedOther = other;
|
||||
return typedOther.a == a
|
||||
&& typedOther.p == p
|
||||
&& typedOther.code == code
|
||||
&& typedOther.h1 == h1
|
||||
&& typedOther.h2 == h2
|
||||
&& typedOther.h3 == h3
|
||||
&& typedOther.h4 == h4
|
||||
&& typedOther.h5 == h5
|
||||
&& typedOther.h6 == h6
|
||||
&& typedOther.em == em
|
||||
&& typedOther.strong == strong
|
||||
&& typedOther.blockquote == blockquote
|
||||
&& typedOther.blockSpacing == blockSpacing
|
||||
&& typedOther.listIndent == listIndent
|
||||
&& typedOther.blockquotePadding == blockquotePadding
|
||||
&& typedOther.blockquoteDecoration == blockquoteDecoration
|
||||
&& typedOther.codeblockPadding == codeblockPadding
|
||||
&& typedOther.codeblockDecoration == codeblockDecoration;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return hashValues(
|
||||
a,
|
||||
p,
|
||||
code,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
em,
|
||||
strong,
|
||||
blockquote,
|
||||
blockSpacing,
|
||||
listIndent,
|
||||
blockquotePadding,
|
||||
blockquoteDecoration,
|
||||
codeblockPadding,
|
||||
codeblockDecoration,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,210 +0,0 @@
|
|||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/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.
|
||||
const MarkdownWidget({
|
||||
Key key,
|
||||
@required this.data,
|
||||
this.styleSheet,
|
||||
this.syntaxHighlighter,
|
||||
this.onTapLink,
|
||||
}) : assert(data != null),
|
||||
super(key: key);
|
||||
|
||||
/// 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 didUpdateWidget(MarkdownWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.data != oldWidget.data
|
||||
|| widget.styleSheet != oldWidget.styleSheet)
|
||||
_parseMarkdown();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_disposeRecognizers();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _parseMarkdown() {
|
||||
final MarkdownStyleSheet styleSheet = widget.styleSheet ?? new MarkdownStyleSheet.fromTheme(Theme.of(context));
|
||||
|
||||
_disposeRecognizers();
|
||||
|
||||
// TODO: This can be optimized by doing the split and removing \r at the same time
|
||||
final List<String> lines = widget.data.replaceAll('\r\n', '\n').split('\n');
|
||||
final md.Document document = new md.Document();
|
||||
final MarkdownBuilder builder = new MarkdownBuilder(delegate: this, styleSheet: styleSheet);
|
||||
_children = builder.build(document.parseLines(lines));
|
||||
}
|
||||
|
||||
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 (widget.onTapLink != null)
|
||||
widget.onTapLink(href);
|
||||
};
|
||||
_recognizers.add(recognizer);
|
||||
return recognizer;
|
||||
}
|
||||
|
||||
@override
|
||||
TextSpan formatText(MarkdownStyleSheet styleSheet, String code) {
|
||||
if (widget.syntaxHighlighter != null)
|
||||
return widget.syntaxHighlighter.format(code);
|
||||
return new TextSpan(style: styleSheet.code, text: code);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.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.
|
||||
const 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.
|
||||
const 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);
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
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:
|
||||
sdk: flutter
|
||||
markdown: '^0.11.0'
|
||||
string_scanner: ^1.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
|
@ -1,177 +0,0 @@
|
|||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Simple string', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const MarkdownBody(data: '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(const MarkdownBody(data: '# 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(const MarkdownBody(data: ''));
|
||||
|
||||
final Iterable<Widget> widgets = tester.allWidgets;
|
||||
_expectWidgetTypes(widgets, <Type>[MarkdownBody, Column]);
|
||||
});
|
||||
|
||||
testWidgets('Ordered list', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const 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(const 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(const 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(const Markdown(data: '[Link Text](href)'));
|
||||
|
||||
final RichText textWidget = tester.allWidgets.firstWhere((Widget widget) => widget is RichText);
|
||||
final TextSpan span = textWidget.text;
|
||||
|
||||
expect(span.children[0].recognizer.runtimeType, equals(TapGestureRecognizer));
|
||||
});
|
||||
|
||||
testWidgets('HTML tag ignored ', (WidgetTester tester) async {
|
||||
final List<String> mdData = <String>[
|
||||
'Line 1\n<p>HTML content</p>\nLine 2',
|
||||
'Line 1\n<!-- HTML\n comment\n ignored --><\nLine 2'
|
||||
];
|
||||
|
||||
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']);
|
||||
}
|
||||
});
|
||||
|
||||
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 Iterable<Widget> widgets = tester.allWidgets;
|
||||
_expectTextStrings(widgets, <String>['Line 1 <','c < c c','< Line 2']);
|
||||
});
|
||||
|
||||
testWidgets('Changing config - data', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const Markdown(data: 'Data1'));
|
||||
_expectTextStrings(tester.allWidgets, <String>['Data1']);
|
||||
|
||||
final String stateBefore = _dumpRenderView();
|
||||
await tester.pumpWidget(const Markdown(data: 'Data1'));
|
||||
final String stateAfter = _dumpRenderView();
|
||||
expect(stateBefore, equals(stateAfter));
|
||||
|
||||
await tester.pumpWidget(const Markdown(data: 'Data2'));
|
||||
_expectTextStrings(tester.allWidgets, <String>['Data2']);
|
||||
});
|
||||
|
||||
testWidgets('Changing config - style', (WidgetTester tester) async {
|
||||
final ThemeData theme = new ThemeData.light();
|
||||
|
||||
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', 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));
|
||||
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
||||
void _expectWidgetTypes(Iterable<Widget> widgets, List<Type> expected) {
|
||||
final List<Type> actual = widgets.map((Widget w) => w.runtimeType).toList();
|
||||
expect(actual, expected);
|
||||
}
|
||||
|
||||
void _expectTextStrings(Iterable<Widget> widgets, List<String> strings) {
|
||||
int currentString = 0;
|
||||
for (Widget widget in widgets) {
|
||||
if (widget is RichText) {
|
||||
final TextSpan span = widget.text;
|
||||
final 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;
|
||||
}
|
||||
|
||||
String _dumpRenderView() {
|
||||
return WidgetsBinding.instance.renderViewElement.toStringDeep().replaceAll(
|
||||
new RegExp(r'SliverChildListDelegate#\d+', multiLine: true), 'SliverChildListDelegate'
|
||||
);
|
||||
}
|
Loading…
Reference in a new issue