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:
Adam Barth 2017-05-09 15:32:38 -07:00 committed by GitHub
parent 9cb12086bb
commit 3bd2ecb2c4
11 changed files with 1 additions and 1003 deletions

View file

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

View file

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

View file

@ -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.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 &lt;','c &lt; c c','&lt; 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'
);
}