Update and modernize the flutter_markdown package (#9044)
We now use modern scrolling machinery and patterns. The API should also be easier to maintain over time. Fixes #6166 Fixes #2591 Fixes #3123
This commit is contained in:
parent
e2b49d64d6
commit
bbac5dcb45
@ -185,6 +185,8 @@ class TextSpan {
|
|||||||
final String indent = '$prefix ';
|
final String indent = '$prefix ';
|
||||||
if (style != null)
|
if (style != null)
|
||||||
buffer.writeln(style.toString(indent));
|
buffer.writeln(style.toString(indent));
|
||||||
|
if (recognizer != null)
|
||||||
|
buffer.writeln('${indent}recognizer: ${recognizer.runtimeType}');
|
||||||
if (text != null)
|
if (text != null)
|
||||||
buffer.writeln('$indent"$text"');
|
buffer.writeln('$indent"$text"');
|
||||||
if (children != null) {
|
if (children != null) {
|
||||||
|
@ -5,5 +5,6 @@
|
|||||||
/// A library to render markdown formatted text.
|
/// A library to render markdown formatted text.
|
||||||
library flutter_markdown;
|
library flutter_markdown;
|
||||||
|
|
||||||
export 'src/markdown.dart';
|
export 'src/builder.dart';
|
||||||
export 'src/markdown_style.dart';
|
export 'src/style_sheet.dart';
|
||||||
|
export 'src/widget.dart';
|
||||||
|
@ -1,8 +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.
|
|
||||||
|
|
||||||
library flutter_markdown_raw;
|
|
||||||
|
|
||||||
export 'src/markdown_raw.dart';
|
|
||||||
export 'src/markdown_style_raw.dart';
|
|
243
packages/flutter_markdown/lib/src/builder.dart
Normal file
243
packages/flutter_markdown/lib/src/builder.dart
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
|
||||||
|
import 'style_sheet.dart';
|
||||||
|
|
||||||
|
final Set<String> _kBlockTags = new Set<String>.from(<String>[
|
||||||
|
'p',
|
||||||
|
'h1',
|
||||||
|
'h2',
|
||||||
|
'h3',
|
||||||
|
'h4',
|
||||||
|
'h5',
|
||||||
|
'h6',
|
||||||
|
'li',
|
||||||
|
'blockquote',
|
||||||
|
'img',
|
||||||
|
'pre',
|
||||||
|
'ol',
|
||||||
|
'ul',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const List<String> _kListTags = const <String>['ul', 'ol'];
|
||||||
|
|
||||||
|
bool _isBlockTag(String tag) => _kBlockTags.contains(tag);
|
||||||
|
bool _isListTag(String tag) => _kListTags.contains(tag);
|
||||||
|
|
||||||
|
class _BlockElement {
|
||||||
|
_BlockElement(this.tag);
|
||||||
|
|
||||||
|
final String tag;
|
||||||
|
final List<Widget> children = <Widget>[];
|
||||||
|
|
||||||
|
int nextListIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _InlineElement {
|
||||||
|
final List<TextSpan> children = <TextSpan>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A delegate used by [MarkdownBuilder] to control the widgets it creates.
|
||||||
|
abstract class MarkdownBuilderDelegate {
|
||||||
|
/// Returns a gesture recognizer to use for an `a` element with the given
|
||||||
|
/// `href` attribute.
|
||||||
|
GestureRecognizer createLink(String href);
|
||||||
|
|
||||||
|
/// Returns formatted text to use to display the given contents of a `pre`
|
||||||
|
/// element.
|
||||||
|
///
|
||||||
|
/// The `styleSheet` is the value of [MarkdownBuilder.styleSheet].
|
||||||
|
TextSpan formatText(MarkdownStyleSheet styleSheet, String code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a [Widget] tree from parsed Markdown.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [Markdown], which is a widget that parses and displays Markdown.
|
||||||
|
class MarkdownBuilder implements md.NodeVisitor {
|
||||||
|
/// Creates an object that builds a [Widget] tree from parsed Markdown.
|
||||||
|
MarkdownBuilder({ this.delegate, this.styleSheet });
|
||||||
|
|
||||||
|
/// A delegate that controls how link and `pre` elements behave.
|
||||||
|
final MarkdownBuilderDelegate delegate;
|
||||||
|
|
||||||
|
/// Defines which [TextStyle] objects to use for each type of element.
|
||||||
|
final MarkdownStyleSheet styleSheet;
|
||||||
|
|
||||||
|
final List<String> _listIndents = <String>[];
|
||||||
|
final List<_BlockElement> _blocks = <_BlockElement>[];
|
||||||
|
final List<_InlineElement> _inlines = <_InlineElement>[];
|
||||||
|
|
||||||
|
/// Returns widgets that display the given Markdown nodes.
|
||||||
|
///
|
||||||
|
/// The returned widgets are typically used as children in a [ListView].
|
||||||
|
List<Widget> build(List<md.Node> nodes) {
|
||||||
|
_listIndents.clear();
|
||||||
|
_blocks.clear();
|
||||||
|
_inlines.clear();
|
||||||
|
|
||||||
|
_blocks.add(new _BlockElement(null));
|
||||||
|
_inlines.add(new _InlineElement());
|
||||||
|
|
||||||
|
for (md.Node node in nodes) {
|
||||||
|
assert(_blocks.length == 1);
|
||||||
|
node.accept(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(_inlines.single.children.isEmpty);
|
||||||
|
return _blocks.single.children;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void visitText(md.Text text) {
|
||||||
|
if (_blocks.last.tag == null) // Don't allow text directly under the root.
|
||||||
|
return;
|
||||||
|
final TextSpan span = _blocks.last.tag == 'pre' ?
|
||||||
|
delegate.formatText(styleSheet, text.text) : new TextSpan(text: text.text);
|
||||||
|
_inlines.last.children.add(span);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool visitElementBefore(md.Element element) {
|
||||||
|
final String tag = element.tag;
|
||||||
|
if (_isBlockTag(tag)) {
|
||||||
|
_addAnonymousBlockIfNeeded(styleSheet.styles[tag]);
|
||||||
|
if (_isListTag(tag))
|
||||||
|
_listIndents.add(tag);
|
||||||
|
_blocks.add(new _BlockElement(tag));
|
||||||
|
} else {
|
||||||
|
_inlines.add(new _InlineElement());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void visitElementAfter(md.Element element) {
|
||||||
|
final String tag = element.tag;
|
||||||
|
|
||||||
|
if (_isBlockTag(tag)) {
|
||||||
|
_addAnonymousBlockIfNeeded(styleSheet.styles[tag]);
|
||||||
|
|
||||||
|
final _BlockElement current = _blocks.removeLast();
|
||||||
|
Widget child;
|
||||||
|
if (tag == 'img') {
|
||||||
|
child = _buildImage(element.attributes['src']);
|
||||||
|
} else {
|
||||||
|
if (current.children.isNotEmpty) {
|
||||||
|
child = new Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: current.children,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
child = const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_isListTag(tag)) {
|
||||||
|
assert(_listIndents.isNotEmpty);
|
||||||
|
_listIndents.removeLast();
|
||||||
|
} else if (tag == 'li') {
|
||||||
|
if (_listIndents.isNotEmpty) {
|
||||||
|
child = new Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
new SizedBox(
|
||||||
|
width: styleSheet.listIndent,
|
||||||
|
child: _buildBullet(_listIndents.last),
|
||||||
|
),
|
||||||
|
new Expanded(child: child)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (tag == 'blockquote') {
|
||||||
|
child = new DecoratedBox(
|
||||||
|
decoration: styleSheet.blockquoteDecoration,
|
||||||
|
child: new Padding(
|
||||||
|
padding: new EdgeInsets.all(styleSheet.blockquotePadding),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (tag == 'pre') {
|
||||||
|
child = new DecoratedBox(
|
||||||
|
decoration: styleSheet.codeblockDecoration,
|
||||||
|
child: new Padding(
|
||||||
|
padding: new EdgeInsets.all(styleSheet.codeblockPadding),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_addBlockChild(child);
|
||||||
|
} else {
|
||||||
|
final _InlineElement current = _inlines.removeLast();
|
||||||
|
final _InlineElement parent = _inlines.last;
|
||||||
|
|
||||||
|
if (current.children.isNotEmpty) {
|
||||||
|
GestureRecognizer recognizer;
|
||||||
|
|
||||||
|
if (tag == 'a')
|
||||||
|
recognizer = delegate.createLink(element.attributes['href']);
|
||||||
|
|
||||||
|
parent.children.add(new TextSpan(
|
||||||
|
style: styleSheet.styles[tag],
|
||||||
|
recognizer: recognizer,
|
||||||
|
children: current.children,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildImage(String src) {
|
||||||
|
final List<String> parts = src.split('#');
|
||||||
|
if (parts.isEmpty)
|
||||||
|
return const SizedBox();
|
||||||
|
|
||||||
|
final String path = parts.first;
|
||||||
|
double width;
|
||||||
|
double height;
|
||||||
|
if (parts.length == 2) {
|
||||||
|
final List<String> dimensions = parts.last.split('x');
|
||||||
|
if (dimensions.length == 2) {
|
||||||
|
width = double.parse(dimensions[0]);
|
||||||
|
height = double.parse(dimensions[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Image.network(path, width: width, height: height);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBullet(String listTag) {
|
||||||
|
if (listTag == 'ul')
|
||||||
|
return new Text('•', textAlign: TextAlign.center);
|
||||||
|
|
||||||
|
final int index = _blocks.last.nextListIndex;
|
||||||
|
return new Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 5.0),
|
||||||
|
child: new Text('${index + 1}.', textAlign: TextAlign.right),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _addBlockChild(Widget child) {
|
||||||
|
final _BlockElement parent = _blocks.last;
|
||||||
|
if (parent.children.isNotEmpty)
|
||||||
|
parent.children.add(new SizedBox(height: styleSheet.blockSpacing));
|
||||||
|
parent.children.add(child);
|
||||||
|
parent.nextListIndex += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _addAnonymousBlockIfNeeded(TextStyle style) {
|
||||||
|
final _InlineElement inline = _inlines.single;
|
||||||
|
if (inline.children.isNotEmpty) {
|
||||||
|
final TextSpan span = new TextSpan(style: style, children: inline.children);
|
||||||
|
_addBlockChild(new RichText(text: span));
|
||||||
|
_inlines.clear();
|
||||||
|
_inlines.add(new _InlineElement());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,95 +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 'markdown_raw.dart';
|
|
||||||
import 'markdown_style.dart';
|
|
||||||
|
|
||||||
/// A [Widget] that renders markdown formatted text. It supports all standard
|
|
||||||
/// markdowns from the original markdown specification found here:
|
|
||||||
/// https://daringfireball.net/projects/markdown/ The rendered markdown is
|
|
||||||
/// placed in a padded scrolling view port. If you do not want the scrolling
|
|
||||||
/// behaviour, use the [MarkdownBody] class instead.
|
|
||||||
class Markdown extends MarkdownRaw {
|
|
||||||
|
|
||||||
/// Creates a new Markdown [Widget] that renders the markdown formatted string
|
|
||||||
/// passed in as [data]. By default the markdown will be rendered using the
|
|
||||||
/// styles from the current theme, but you can optionally pass in a custom
|
|
||||||
/// [markdownStyle] that specifies colors and fonts to use. Code blocks are
|
|
||||||
/// by default not using syntax highlighting, but it's possible to pass in
|
|
||||||
/// a custom [syntaxHighlighter].
|
|
||||||
///
|
|
||||||
/// new Markdown(data: "Hello _world_!");
|
|
||||||
Markdown({
|
|
||||||
String data,
|
|
||||||
SyntaxHighlighter syntaxHighlighter,
|
|
||||||
MarkdownStyle markdownStyle,
|
|
||||||
MarkdownLinkCallback onTapLink
|
|
||||||
}) : super(
|
|
||||||
data: data,
|
|
||||||
syntaxHighlighter: syntaxHighlighter,
|
|
||||||
markdownStyle: markdownStyle,
|
|
||||||
onTapLink: onTapLink
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
|
||||||
MarkdownBody createMarkdownBody({
|
|
||||||
String data,
|
|
||||||
MarkdownStyle markdownStyle,
|
|
||||||
SyntaxHighlighter syntaxHighlighter,
|
|
||||||
MarkdownLinkCallback onTapLink
|
|
||||||
}) {
|
|
||||||
return new MarkdownBody(
|
|
||||||
data: data,
|
|
||||||
markdownStyle: markdownStyle,
|
|
||||||
syntaxHighlighter: syntaxHighlighter,
|
|
||||||
onTapLink: onTapLink
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A [Widget] that renders markdown formatted text.
|
|
||||||
///
|
|
||||||
/// It supports all standard markdowns from the original markdown specification
|
|
||||||
/// found here: <https://daringfireball.net/projects/markdown/>
|
|
||||||
///
|
|
||||||
/// This class doesn't implement any scrolling behavior, if you want scrolling
|
|
||||||
/// either wrap the widget in a [SingleChildScrollView] or use the [Markdown]
|
|
||||||
/// widget.
|
|
||||||
class MarkdownBody extends MarkdownBodyRaw {
|
|
||||||
|
|
||||||
/// Creates a new Markdown [Widget] that renders the markdown formatted string
|
|
||||||
/// passed in as [data]. By default the markdown will be rendered using the
|
|
||||||
/// styles from the current theme, but you can optionally pass in a custom
|
|
||||||
/// [markdownStyle] that specifies colors and fonts to use. Code blocks are
|
|
||||||
/// by default not using syntax highlighting, but it's possible to pass in
|
|
||||||
/// a custom [syntaxHighlighter].
|
|
||||||
///
|
|
||||||
/// Typically, you may want to wrap the [MarkdownBody] widget in a
|
|
||||||
/// [SingleChildScrollView], or use the [Markdown] class.
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// new SingleChildScrollView(
|
|
||||||
/// padding: new EdgeInsets.all(16.0),
|
|
||||||
/// child: new Markdown(data: markdownSource),
|
|
||||||
/// ),
|
|
||||||
/// ```
|
|
||||||
MarkdownBody({
|
|
||||||
String data,
|
|
||||||
SyntaxHighlighter syntaxHighlighter,
|
|
||||||
MarkdownStyle markdownStyle,
|
|
||||||
MarkdownLinkCallback onTapLink
|
|
||||||
}) : super(
|
|
||||||
data: data,
|
|
||||||
syntaxHighlighter: syntaxHighlighter,
|
|
||||||
markdownStyle: markdownStyle,
|
|
||||||
onTapLink: onTapLink
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
|
||||||
MarkdownStyle createDefaultStyle(BuildContext context) {
|
|
||||||
return new MarkdownStyle.defaultFromTheme(Theme.of(context));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,545 +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:markdown/markdown.dart' as md;
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:flutter/gestures.dart';
|
|
||||||
|
|
||||||
import 'markdown_style_raw.dart';
|
|
||||||
|
|
||||||
typedef void MarkdownLinkCallback(String href);
|
|
||||||
|
|
||||||
|
|
||||||
/// 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 StatelessWidget {
|
|
||||||
|
|
||||||
/// 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 EdgeInsets.all(16.0),
|
|
||||||
this.onTapLink
|
|
||||||
});
|
|
||||||
|
|
||||||
/// 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 EdgeInsets padding;
|
|
||||||
|
|
||||||
/// Callback when a link is tapped
|
|
||||||
final MarkdownLinkCallback onTapLink;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
// TODO(abarth): We should use a ListView here and lazily build the widgets
|
|
||||||
// from the markdown.
|
|
||||||
return new SingleChildScrollView(
|
|
||||||
padding: padding,
|
|
||||||
child: createMarkdownBody(
|
|
||||||
data: data,
|
|
||||||
markdownStyle: markdownStyle,
|
|
||||||
syntaxHighlighter: syntaxHighlighter,
|
|
||||||
onTapLink: onTapLink,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MarkdownBodyRaw createMarkdownBody({
|
|
||||||
String data,
|
|
||||||
covariant MarkdownStyleRaw markdownStyle,
|
|
||||||
SyntaxHighlighter syntaxHighlighter,
|
|
||||||
MarkdownLinkCallback onTapLink
|
|
||||||
}) {
|
|
||||||
return new MarkdownBodyRaw(
|
|
||||||
data: data,
|
|
||||||
markdownStyle: markdownStyle,
|
|
||||||
syntaxHighlighter: syntaxHighlighter,
|
|
||||||
onTapLink: onTapLink
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A [Widget] that renders markdown formatted text.
|
|
||||||
///
|
|
||||||
/// It supports all standard markdowns from the original markdown specification
|
|
||||||
/// found here: <https://daringfireball.net/projects/markdown/>.
|
|
||||||
///
|
|
||||||
/// This class doesn't implement any scrolling behavior, if you want scrolling
|
|
||||||
/// either wrap the widget in a [SingleChildScrollView] or use the [MarkdownRaw]
|
|
||||||
/// widget.
|
|
||||||
class MarkdownBodyRaw extends StatefulWidget {
|
|
||||||
|
|
||||||
/// 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
|
|
||||||
/// a [SingleChildScrollView], or use the [Markdown class].
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// new SingleChildScrollView(
|
|
||||||
/// padding: new EdgeInsets.all(16.0),
|
|
||||||
/// child: new MarkdownBodyRaw(
|
|
||||||
/// data: markdownSource,
|
|
||||||
/// markdownStyle: myStyle,
|
|
||||||
/// ),
|
|
||||||
/// ),
|
|
||||||
/// ```
|
|
||||||
MarkdownBodyRaw({
|
|
||||||
this.data,
|
|
||||||
this.markdownStyle,
|
|
||||||
this.syntaxHighlighter,
|
|
||||||
this.onTapLink
|
|
||||||
});
|
|
||||||
|
|
||||||
/// 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;
|
|
||||||
|
|
||||||
/// Callback when a link is tapped
|
|
||||||
final MarkdownLinkCallback onTapLink;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_MarkdownBodyRawState createState() => new _MarkdownBodyRawState();
|
|
||||||
|
|
||||||
MarkdownStyleRaw createDefaultStyle(BuildContext context) => null;
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MarkdownBodyRawState extends State<MarkdownBodyRaw> {
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeDependencies() {
|
|
||||||
_buildMarkdownCache();
|
|
||||||
super.didChangeDependencies();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_linkHandler.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateConfig(MarkdownBodyRaw oldConfig) {
|
|
||||||
super.didUpdateConfig(oldConfig);
|
|
||||||
|
|
||||||
if (oldConfig.data != config.data ||
|
|
||||||
oldConfig.markdownStyle != config.markdownStyle ||
|
|
||||||
oldConfig.syntaxHighlighter != config.syntaxHighlighter ||
|
|
||||||
oldConfig.onTapLink != config.onTapLink)
|
|
||||||
_buildMarkdownCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _buildMarkdownCache() {
|
|
||||||
final MarkdownStyleRaw markdownStyle = config.markdownStyle ?? config.createDefaultStyle(context);
|
|
||||||
final SyntaxHighlighter syntaxHighlighter = config.syntaxHighlighter ?? new _DefaultSyntaxHighlighter(markdownStyle.code);
|
|
||||||
|
|
||||||
_linkHandler?.dispose();
|
|
||||||
_linkHandler = new _LinkHandler(config.onTapLink);
|
|
||||||
|
|
||||||
// TODO: This can be optimized by doing the split and removing \r at the same time
|
|
||||||
final List<String> lines = config.data.replaceAll('\r\n', '\n').split('\n');
|
|
||||||
final md.Document document = new md.Document();
|
|
||||||
|
|
||||||
final _Renderer renderer = new _Renderer();
|
|
||||||
_cachedBlocks = renderer.render(document.parseLines(lines), markdownStyle, syntaxHighlighter, _linkHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<_Block> _cachedBlocks;
|
|
||||||
_LinkHandler _linkHandler;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final List<Widget> blocks = <Widget>[];
|
|
||||||
for (_Block block in _cachedBlocks) {
|
|
||||||
blocks.add(block.build(context));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: blocks
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void debugFillDescription(List<String> description) {
|
|
||||||
super.debugFillDescription(description);
|
|
||||||
description.add('cached blocks identity: ${_cachedBlocks.hashCode}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _Renderer implements md.NodeVisitor {
|
|
||||||
List<_Block> render(List<md.Node> nodes, MarkdownStyleRaw markdownStyle, SyntaxHighlighter syntaxHighlighter, _LinkHandler linkHandler) {
|
|
||||||
assert(markdownStyle != null);
|
|
||||||
|
|
||||||
_blocks = <_Block>[];
|
|
||||||
_listIndents = <String>[];
|
|
||||||
_markdownStyle = markdownStyle;
|
|
||||||
_syntaxHighlighter = syntaxHighlighter;
|
|
||||||
_linkHandler = linkHandler;
|
|
||||||
|
|
||||||
for (final md.Node node in nodes) {
|
|
||||||
node.accept(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _blocks;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<_Block> _blocks;
|
|
||||||
List<String> _listIndents;
|
|
||||||
MarkdownStyleRaw _markdownStyle;
|
|
||||||
SyntaxHighlighter _syntaxHighlighter;
|
|
||||||
_LinkHandler _linkHandler;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void visitText(md.Text text) {
|
|
||||||
if (_currentBlock != null) { // ignore if no corresponding block
|
|
||||||
final _MarkdownNodeList topList = _currentBlock.stack.last;
|
|
||||||
final List<_MarkdownNode> top = topList.list;
|
|
||||||
|
|
||||||
if (_currentBlock.tag == 'pre')
|
|
||||||
top.add(
|
|
||||||
new _MarkdownNodeTextSpan(_syntaxHighlighter.format(text.text)));
|
|
||||||
else
|
|
||||||
top.add(new _MarkdownNodeString(text.text));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
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;
|
|
||||||
|
|
||||||
final _Block newBlock = new _Block(element.tag, element.attributes, _markdownStyle, new List<String>.from(_listIndents), blockList.length);
|
|
||||||
blockList.add(newBlock);
|
|
||||||
} else {
|
|
||||||
_LinkInfo linkInfo;
|
|
||||||
if (element.tag == 'a') {
|
|
||||||
linkInfo = _linkHandler.createLinkInfo(element.attributes['href']);
|
|
||||||
}
|
|
||||||
|
|
||||||
final TextStyle style = _markdownStyle.styles[element.tag] ?? const TextStyle();
|
|
||||||
final List<_MarkdownNode> styleElement = <_MarkdownNode>[new _MarkdownNodeTextStyle(style, linkInfo)];
|
|
||||||
_currentBlock.stack.add(new _MarkdownNodeList(styleElement));
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void visitElementAfter(md.Element element) {
|
|
||||||
if (_isListTag(element.tag))
|
|
||||||
_listIndents.removeLast();
|
|
||||||
|
|
||||||
if (_isBlockTag(element.tag)) {
|
|
||||||
if (_currentBlock.stack.isNotEmpty) {
|
|
||||||
final _MarkdownNodeList stackList = _currentBlock.stack.first;
|
|
||||||
_currentBlock.stack = stackList.list;
|
|
||||||
_currentBlock.open = false;
|
|
||||||
} else {
|
|
||||||
_currentBlock.stack = <_MarkdownNode>[new _MarkdownNodeString('')];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (_currentBlock.stack.length > 1) {
|
|
||||||
final _MarkdownNodeList poppedList = _currentBlock.stack.last;
|
|
||||||
final List<_MarkdownNode> popped = poppedList.list;
|
|
||||||
_currentBlock.stack.removeLast();
|
|
||||||
|
|
||||||
final _MarkdownNodeList topList = _currentBlock.stack.last;
|
|
||||||
final 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;
|
|
||||||
|
|
||||||
final _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, [this.linkInfo = null]);
|
|
||||||
TextStyle style;
|
|
||||||
_LinkInfo linkInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = const 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;
|
|
||||||
set open(bool value) {
|
|
||||||
_open = value;
|
|
||||||
if (!value && subBlocks.isNotEmpty)
|
|
||||||
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.isNotEmpty) {
|
|
||||||
final List<Widget> subWidgets = <Widget>[];
|
|
||||||
for (_Block subBlock in subBlocks) {
|
|
||||||
subWidgets.add(subBlock.build(context));
|
|
||||||
}
|
|
||||||
|
|
||||||
contents = new Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: subWidgets
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
final TextSpan span = _stackToTextSpan(new _MarkdownNodeList(stack));
|
|
||||||
contents = new RichText(text: span);
|
|
||||||
|
|
||||||
if (listIndents.isNotEmpty) {
|
|
||||||
Widget bullet;
|
|
||||||
if (listIndents.last == 'ul') {
|
|
||||||
bullet = new Text(
|
|
||||||
'•',
|
|
||||||
textAlign: TextAlign.center
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
bullet = new Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 5.0),
|
|
||||||
child: new Text(
|
|
||||||
"${blockPosition + 1}.",
|
|
||||||
textAlign: TextAlign.right
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
contents = new Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
new SizedBox(
|
|
||||||
width: listIndents.length * markdownStyle.listIndent,
|
|
||||||
child: bullet
|
|
||||||
),
|
|
||||||
new Expanded(child: contents)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BoxDecoration decoration;
|
|
||||||
EdgeInsets padding;
|
|
||||||
|
|
||||||
if (tag == 'blockquote') {
|
|
||||||
decoration = markdownStyle.blockquoteDecoration;
|
|
||||||
padding = new EdgeInsets.all(markdownStyle.blockquotePadding);
|
|
||||||
} else if (tag == 'pre') {
|
|
||||||
decoration = markdownStyle.codeblockDecoration;
|
|
||||||
padding = new EdgeInsets.all(markdownStyle.codeblockPadding);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Container(
|
|
||||||
padding: padding,
|
|
||||||
margin: new EdgeInsets.only(bottom: spacing),
|
|
||||||
child: contents,
|
|
||||||
decoration: decoration
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
TextSpan _stackToTextSpan(_MarkdownNode stack) {
|
|
||||||
if (stack is _MarkdownNodeTextSpan)
|
|
||||||
return stack.textSpan;
|
|
||||||
|
|
||||||
if (stack is _MarkdownNodeList) {
|
|
||||||
final List<_MarkdownNode> list = stack.list;
|
|
||||||
final _MarkdownNodeTextStyle styleNode = list[0];
|
|
||||||
final _LinkInfo linkInfo = styleNode.linkInfo;
|
|
||||||
final TextStyle style = styleNode.style;
|
|
||||||
|
|
||||||
List<TextSpan> children = <TextSpan>[];
|
|
||||||
for (int i = 1; i < list.length; i++) {
|
|
||||||
children.add(_stackToTextSpan(list[i]));
|
|
||||||
}
|
|
||||||
|
|
||||||
String text;
|
|
||||||
if (children.length == 1 && _isPlainText(children[0])) {
|
|
||||||
text = children[0].text;
|
|
||||||
children = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final TapGestureRecognizer recognizer = linkInfo?.recognizer;
|
|
||||||
|
|
||||||
return new TextSpan(style: style, children: children, recognizer: recognizer, text: text);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stack is _MarkdownNodeString) {
|
|
||||||
return new TextSpan(text: stack.string);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _isPlainText(TextSpan span) {
|
|
||||||
return (span.text != null && span.style == null && span.recognizer == null && span.children == null);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildImage(BuildContext context, String src) {
|
|
||||||
final List<String> parts = src.split('#');
|
|
||||||
if (parts.isEmpty) return new Container();
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LinkInfo {
|
|
||||||
_LinkInfo(this.href, this.recognizer);
|
|
||||||
|
|
||||||
final String href;
|
|
||||||
final TapGestureRecognizer recognizer;
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LinkHandler {
|
|
||||||
_LinkHandler(this.onTapLink);
|
|
||||||
|
|
||||||
List<_LinkInfo> links = <_LinkInfo>[];
|
|
||||||
MarkdownLinkCallback onTapLink;
|
|
||||||
|
|
||||||
_LinkInfo createLinkInfo(String href) {
|
|
||||||
final TapGestureRecognizer recognizer = new TapGestureRecognizer();
|
|
||||||
recognizer.onTap = () {
|
|
||||||
if (onTapLink != null)
|
|
||||||
onTapLink(href);
|
|
||||||
};
|
|
||||||
|
|
||||||
final _LinkInfo linkInfo = new _LinkInfo(href, recognizer);
|
|
||||||
links.add(linkInfo);
|
|
||||||
|
|
||||||
return linkInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
void dispose() {
|
|
||||||
for (_LinkInfo linkInfo in links) {
|
|
||||||
linkInfo.recognizer.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class SyntaxHighlighter { // ignore: one_member_abstracts
|
|
||||||
TextSpan format(String source);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DefaultSyntaxHighlighter extends SyntaxHighlighter{
|
|
||||||
_DefaultSyntaxHighlighter(this.style);
|
|
||||||
|
|
||||||
final TextStyle style;
|
|
||||||
|
|
||||||
@override
|
|
||||||
TextSpan format(String source) {
|
|
||||||
return new TextSpan(style: style, children: <TextSpan>[new TextSpan(text: source)]);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,78 +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 'markdown.dart';
|
|
||||||
import 'markdown_style_raw.dart';
|
|
||||||
|
|
||||||
/// Style used for rendering markdown formatted text using the [MarkdownBody]
|
|
||||||
/// widget.
|
|
||||||
class MarkdownStyle extends MarkdownStyleRaw{
|
|
||||||
|
|
||||||
/// Creates a [MarkdownStyle] from the [TextStyle]s in the provided [theme].
|
|
||||||
MarkdownStyle.defaultFromTheme(ThemeData theme) : super(
|
|
||||||
a: new TextStyle(color: Colors.blue),
|
|
||||||
p: theme.textTheme.body1,
|
|
||||||
code: new TextStyle(
|
|
||||||
color: Colors.grey.shade700,
|
|
||||||
fontFamily: "monospace",
|
|
||||||
fontSize: theme.textTheme.body1.fontSize * 0.85
|
|
||||||
),
|
|
||||||
h1: theme.textTheme.headline,
|
|
||||||
h2: theme.textTheme.title,
|
|
||||||
h3: theme.textTheme.subhead,
|
|
||||||
h4: theme.textTheme.body2,
|
|
||||||
h5: theme.textTheme.body2,
|
|
||||||
h6: theme.textTheme.body2,
|
|
||||||
em: const TextStyle(fontStyle: FontStyle.italic),
|
|
||||||
strong: const TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
blockquote: theme.textTheme.body1,
|
|
||||||
blockSpacing: 8.0,
|
|
||||||
listIndent: 32.0,
|
|
||||||
blockquotePadding: 8.0,
|
|
||||||
blockquoteDecoration: new BoxDecoration(
|
|
||||||
backgroundColor: Colors.blue.shade100,
|
|
||||||
borderRadius: new BorderRadius.circular(2.0)
|
|
||||||
),
|
|
||||||
codeblockPadding: 8.0,
|
|
||||||
codeblockDecoration: new BoxDecoration(
|
|
||||||
backgroundColor: Colors.grey.shade100,
|
|
||||||
borderRadius: new BorderRadius.circular(2.0)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Creates a [MarkdownStyle] from the [TextStyle]s in the provided [theme].
|
|
||||||
/// This style uses larger fonts for the headings than in
|
|
||||||
/// [MarkdownStyle.defaultFromTheme].
|
|
||||||
MarkdownStyle.largeFromTheme(ThemeData theme) : super (
|
|
||||||
a: new TextStyle(color: Colors.blue),
|
|
||||||
p: theme.textTheme.body1,
|
|
||||||
code: new TextStyle(
|
|
||||||
color: Colors.grey.shade700,
|
|
||||||
fontFamily: "monospace",
|
|
||||||
fontSize: theme.textTheme.body1.fontSize * 0.85
|
|
||||||
),
|
|
||||||
h1: theme.textTheme.display3,
|
|
||||||
h2: theme.textTheme.display2,
|
|
||||||
h3: theme.textTheme.display1,
|
|
||||||
h4: theme.textTheme.headline,
|
|
||||||
h5: theme.textTheme.title,
|
|
||||||
h6: theme.textTheme.subhead,
|
|
||||||
em: const TextStyle(fontStyle: FontStyle.italic),
|
|
||||||
strong: const TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
blockquote: theme.textTheme.body1,
|
|
||||||
blockSpacing: 8.0,
|
|
||||||
listIndent: 32.0,
|
|
||||||
blockquotePadding: 8.0,
|
|
||||||
blockquoteDecoration: new BoxDecoration(
|
|
||||||
backgroundColor: Colors.blue.shade100,
|
|
||||||
borderRadius: new BorderRadius.circular(2.0)
|
|
||||||
),
|
|
||||||
codeblockPadding: 8.0,
|
|
||||||
codeblockDecoration: new BoxDecoration(
|
|
||||||
backgroundColor: Colors.grey.shade100,
|
|
||||||
borderRadius: new BorderRadius.circular(2.0)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,121 +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/widgets.dart';
|
|
||||||
import 'markdown.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 parameters 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 = <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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
269
packages/flutter_markdown/lib/src/style_sheet.dart
Normal file
269
packages/flutter_markdown/lib/src/style_sheet.dart
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
// 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: new TextStyle(color: Colors.blue),
|
||||||
|
p: theme.textTheme.body1,
|
||||||
|
code: new TextStyle(
|
||||||
|
color: Colors.grey.shade700,
|
||||||
|
fontFamily: "monospace",
|
||||||
|
fontSize: theme.textTheme.body1.fontSize * 0.85
|
||||||
|
),
|
||||||
|
h1: theme.textTheme.headline,
|
||||||
|
h2: theme.textTheme.title,
|
||||||
|
h3: theme.textTheme.subhead,
|
||||||
|
h4: theme.textTheme.body2,
|
||||||
|
h5: theme.textTheme.body2,
|
||||||
|
h6: theme.textTheme.body2,
|
||||||
|
em: const TextStyle(fontStyle: FontStyle.italic),
|
||||||
|
strong: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
blockquote: theme.textTheme.body1,
|
||||||
|
blockSpacing: 8.0,
|
||||||
|
listIndent: 32.0,
|
||||||
|
blockquotePadding: 8.0,
|
||||||
|
blockquoteDecoration: new BoxDecoration(
|
||||||
|
backgroundColor: Colors.blue.shade100,
|
||||||
|
borderRadius: new BorderRadius.circular(2.0)
|
||||||
|
),
|
||||||
|
codeblockPadding: 8.0,
|
||||||
|
codeblockDecoration: new BoxDecoration(
|
||||||
|
backgroundColor: Colors.grey.shade100,
|
||||||
|
borderRadius: new BorderRadius.circular(2.0)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [MarkdownStyle] from the [TextStyle]s in the provided [ThemeData].
|
||||||
|
///
|
||||||
|
/// This constructor uses larger fonts for the headings than in
|
||||||
|
/// [MarkdownStyle.fromTheme].
|
||||||
|
factory MarkdownStyleSheet.largeFromTheme(ThemeData theme) {
|
||||||
|
return new MarkdownStyleSheet(
|
||||||
|
a: new TextStyle(color: Colors.blue),
|
||||||
|
p: theme.textTheme.body1,
|
||||||
|
code: new TextStyle(
|
||||||
|
color: Colors.grey.shade700,
|
||||||
|
fontFamily: "monospace",
|
||||||
|
fontSize: theme.textTheme.body1.fontSize * 0.85
|
||||||
|
),
|
||||||
|
h1: theme.textTheme.display3,
|
||||||
|
h2: theme.textTheme.display2,
|
||||||
|
h3: theme.textTheme.display1,
|
||||||
|
h4: theme.textTheme.headline,
|
||||||
|
h5: theme.textTheme.title,
|
||||||
|
h6: theme.textTheme.subhead,
|
||||||
|
em: const TextStyle(fontStyle: FontStyle.italic),
|
||||||
|
strong: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
blockquote: theme.textTheme.body1,
|
||||||
|
blockSpacing: 8.0,
|
||||||
|
listIndent: 32.0,
|
||||||
|
blockquotePadding: 8.0,
|
||||||
|
blockquoteDecoration: new BoxDecoration(
|
||||||
|
backgroundColor: Colors.blue.shade100,
|
||||||
|
borderRadius: new BorderRadius.circular(2.0)
|
||||||
|
),
|
||||||
|
codeblockPadding: 8.0,
|
||||||
|
codeblockDecoration: new BoxDecoration(
|
||||||
|
backgroundColor: Colors.grey.shade100,
|
||||||
|
borderRadius: new BorderRadius.circular(2.0)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new [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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
211
packages/flutter_markdown/lib/src/widget.dart
Normal file
211
packages/flutter_markdown/lib/src/widget.dart
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
import 'builder.dart';
|
||||||
|
import 'style_sheet.dart';
|
||||||
|
|
||||||
|
/// Signature for callbacks used by [MarkdownWidget] when the user taps a link.
|
||||||
|
///
|
||||||
|
/// Used by [MarkdownWidget.onTapLink].
|
||||||
|
typedef void MarkdownTapLinkCallback(String href);
|
||||||
|
|
||||||
|
/// Creates a format [TextSpan] given a string.
|
||||||
|
///
|
||||||
|
/// Used by [MarkdownWidget] to highlight the contents of `pre` elements.
|
||||||
|
abstract class SyntaxHighlighter { // ignore: one_member_abstracts
|
||||||
|
/// Returns the formated [TextSpan] for the given string.
|
||||||
|
TextSpan format(String source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A base class for widgets that parse and display Markdown.
|
||||||
|
///
|
||||||
|
/// Supports all standard Markdown from the original
|
||||||
|
/// [Markdown specification](https://daringfireball.net/projects/markdown/).
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [Markdown], which is a scrolling container of Markdown.
|
||||||
|
/// * [MarkdownBody], which is a non-scrolling container of Markdown.
|
||||||
|
/// * <https://daringfireball.net/projects/markdown/>
|
||||||
|
abstract class MarkdownWidget extends StatefulWidget {
|
||||||
|
/// Creates a widget that parses and displays Markdown.
|
||||||
|
///
|
||||||
|
/// The [data] argument must not be null.
|
||||||
|
MarkdownWidget({
|
||||||
|
Key key,
|
||||||
|
@required this.data,
|
||||||
|
this.styleSheet,
|
||||||
|
this.syntaxHighlighter,
|
||||||
|
this.onTapLink,
|
||||||
|
}) : super(key: key) {
|
||||||
|
assert(data != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Markdown to display.
|
||||||
|
final String data;
|
||||||
|
|
||||||
|
/// The styles to use when displaying the Markdown.
|
||||||
|
///
|
||||||
|
/// If null, the styles are infered from the current [Theme].
|
||||||
|
final MarkdownStyleSheet styleSheet;
|
||||||
|
|
||||||
|
/// The syntax highlighter used to color text in `pre` elements.
|
||||||
|
///
|
||||||
|
/// If null, the [MarkdownStyleSheet.code] style is used for `pre` elements.
|
||||||
|
final SyntaxHighlighter syntaxHighlighter;
|
||||||
|
|
||||||
|
/// Called when the user taps a link.
|
||||||
|
final MarkdownTapLinkCallback onTapLink;
|
||||||
|
|
||||||
|
/// Subclasses should override this function to display the given children,
|
||||||
|
/// which are the parsed representation of [data].
|
||||||
|
@protected
|
||||||
|
Widget build(BuildContext context, List<Widget> children);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_MarkdownWidgetState createState() => new _MarkdownWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MarkdownWidgetState extends State<MarkdownWidget> implements MarkdownBuilderDelegate {
|
||||||
|
List<Widget> _children;
|
||||||
|
final List<GestureRecognizer> _recognizers = <GestureRecognizer>[];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
_parseMarkdown();
|
||||||
|
super.didChangeDependencies();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateConfig(MarkdownWidget oldConfig) {
|
||||||
|
super.didUpdateConfig(oldConfig);
|
||||||
|
if (config.data != oldConfig.data
|
||||||
|
|| config.styleSheet != oldConfig.styleSheet)
|
||||||
|
_parseMarkdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_disposeRecognizers();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _parseMarkdown() {
|
||||||
|
final MarkdownStyleSheet styleSheet = config.styleSheet ?? new MarkdownStyleSheet.fromTheme(Theme.of(context));
|
||||||
|
|
||||||
|
_disposeRecognizers();
|
||||||
|
|
||||||
|
// TODO: This can be optimized by doing the split and removing \r at the same time
|
||||||
|
final List<String> lines = config.data.replaceAll('\r\n', '\n').split('\n');
|
||||||
|
final md.Document document = new md.Document();
|
||||||
|
final MarkdownBuilder builder = new MarkdownBuilder(delegate: this, styleSheet: styleSheet);
|
||||||
|
_children = builder.build(document.parseLines(lines));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _disposeRecognizers() {
|
||||||
|
if (_recognizers.isEmpty)
|
||||||
|
return;
|
||||||
|
final List<GestureRecognizer> localRecognizers = new List<GestureRecognizer>.from(_recognizers);
|
||||||
|
_recognizers.clear();
|
||||||
|
for (GestureRecognizer recognizer in localRecognizers)
|
||||||
|
recognizer.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
GestureRecognizer createLink(String href) {
|
||||||
|
final TapGestureRecognizer recognizer = new TapGestureRecognizer()
|
||||||
|
..onTap = () {
|
||||||
|
if (config.onTapLink != null)
|
||||||
|
config.onTapLink(href);
|
||||||
|
};
|
||||||
|
_recognizers.add(recognizer);
|
||||||
|
return recognizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
TextSpan formatText(MarkdownStyleSheet styleSheet, String code) {
|
||||||
|
if (config.syntaxHighlighter != null)
|
||||||
|
return config.syntaxHighlighter.format(code);
|
||||||
|
return new TextSpan(style: styleSheet.code, text: code);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => config.build(context, _children);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A non-scrolling widget that parses and displays Markdown.
|
||||||
|
///
|
||||||
|
/// Supports all standard Markdown from the original
|
||||||
|
/// [Markdown specification](https://daringfireball.net/projects/markdown/).
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [Markdown], which is a scrolling container of Markdown.
|
||||||
|
/// * <https://daringfireball.net/projects/markdown/>
|
||||||
|
class MarkdownBody extends MarkdownWidget {
|
||||||
|
/// Creates a non-scrolling widget that parses and displays Markdown.
|
||||||
|
MarkdownBody({
|
||||||
|
Key key,
|
||||||
|
String data,
|
||||||
|
MarkdownStyleSheet styleSheet,
|
||||||
|
SyntaxHighlighter syntaxHighlighter,
|
||||||
|
MarkdownTapLinkCallback onTapLink,
|
||||||
|
}) : super(
|
||||||
|
key: key,
|
||||||
|
data: data,
|
||||||
|
styleSheet: styleSheet,
|
||||||
|
syntaxHighlighter: syntaxHighlighter,
|
||||||
|
onTapLink: onTapLink,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, List<Widget> children) {
|
||||||
|
if (children.length == 1)
|
||||||
|
return children.single;
|
||||||
|
return new Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A scrolling widget that parses and displays Markdown.
|
||||||
|
///
|
||||||
|
/// Supports all standard Markdown from the original
|
||||||
|
/// [Markdown specification](https://daringfireball.net/projects/markdown/).
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [MarkdownBody], which is a non-scrolling container of Markdown.
|
||||||
|
/// * <https://daringfireball.net/projects/markdown/>
|
||||||
|
class Markdown extends MarkdownWidget {
|
||||||
|
/// Creates a scrolling widget that parses and displays Markdown.
|
||||||
|
Markdown({
|
||||||
|
Key key,
|
||||||
|
String data,
|
||||||
|
MarkdownStyleSheet styleSheet,
|
||||||
|
SyntaxHighlighter syntaxHighlighter,
|
||||||
|
MarkdownTapLinkCallback onTapLink,
|
||||||
|
this.padding: const EdgeInsets.all(16.0),
|
||||||
|
}) : super(
|
||||||
|
key: key,
|
||||||
|
data: data,
|
||||||
|
styleSheet: styleSheet,
|
||||||
|
syntaxHighlighter: syntaxHighlighter,
|
||||||
|
onTapLink: onTapLink,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// The amount of space by which to inset the children.
|
||||||
|
final EdgeInsets padding;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, List<Widget> children) {
|
||||||
|
return new ListView(padding: padding, children: children);
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,7 @@ void main() {
|
|||||||
await tester.pumpWidget(new MarkdownBody(data: 'Hello'));
|
await tester.pumpWidget(new MarkdownBody(data: 'Hello'));
|
||||||
|
|
||||||
final Iterable<Widget> widgets = tester.allWidgets;
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
_expectWidgetTypes(widgets, <Type>[MarkdownBody, Column, Container, Padding, RichText]);
|
_expectWidgetTypes(widgets, <Type>[MarkdownBody, Column, RichText]);
|
||||||
_expectTextStrings(widgets, <String>['Hello']);
|
_expectTextStrings(widgets, <String>['Hello']);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ void main() {
|
|||||||
await tester.pumpWidget(new MarkdownBody(data: '# Header'));
|
await tester.pumpWidget(new MarkdownBody(data: '# Header'));
|
||||||
|
|
||||||
final Iterable<Widget> widgets = tester.allWidgets;
|
final Iterable<Widget> widgets = tester.allWidgets;
|
||||||
_expectWidgetTypes(widgets, <Type>[MarkdownBody, Column, Container, Padding, RichText]);
|
_expectWidgetTypes(widgets, <Type>[MarkdownBody, Column, RichText]);
|
||||||
_expectTextStrings(widgets, <String>['Header']);
|
_expectTextStrings(widgets, <String>['Header']);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -42,8 +42,8 @@ void main() {
|
|||||||
'2.',
|
'2.',
|
||||||
'Item 2',
|
'Item 2',
|
||||||
'3.',
|
'3.',
|
||||||
'Item 3']
|
'Item 3',
|
||||||
);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Unordered list', (WidgetTester tester) async {
|
testWidgets('Unordered list', (WidgetTester tester) async {
|
||||||
@ -56,8 +56,8 @@ void main() {
|
|||||||
'•',
|
'•',
|
||||||
'Item 2',
|
'Item 2',
|
||||||
'•',
|
'•',
|
||||||
'Item 3']
|
'Item 3',
|
||||||
);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Scrollable wrapping', (WidgetTester tester) async {
|
testWidgets('Scrollable wrapping', (WidgetTester tester) async {
|
||||||
@ -66,12 +66,11 @@ void main() {
|
|||||||
final List<Widget> widgets = tester.allWidgets.toList();
|
final List<Widget> widgets = tester.allWidgets.toList();
|
||||||
_expectWidgetTypes(widgets.take(2), <Type>[
|
_expectWidgetTypes(widgets.take(2), <Type>[
|
||||||
Markdown,
|
Markdown,
|
||||||
SingleChildScrollView,
|
ListView,
|
||||||
]);
|
]);
|
||||||
_expectWidgetTypes(widgets.reversed.take(3).toList().reversed, <Type>[
|
_expectWidgetTypes(widgets.reversed.take(2).toList().reversed, <Type>[
|
||||||
Padding,
|
SliverPadding,
|
||||||
MarkdownBody,
|
SliverList,
|
||||||
Column
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -110,9 +109,9 @@ void main() {
|
|||||||
await tester.pumpWidget(new Markdown(data: 'Data1'));
|
await tester.pumpWidget(new Markdown(data: 'Data1'));
|
||||||
_expectTextStrings(tester.allWidgets, <String>['Data1']);
|
_expectTextStrings(tester.allWidgets, <String>['Data1']);
|
||||||
|
|
||||||
final String stateBefore = WidgetsBinding.instance.renderViewElement.toStringDeep();
|
final String stateBefore = _dumpRenderView();
|
||||||
await tester.pumpWidget(new Markdown(data: 'Data1'));
|
await tester.pumpWidget(new Markdown(data: 'Data1'));
|
||||||
final String stateAfter = WidgetsBinding.instance.renderViewElement.toStringDeep();
|
final String stateAfter = _dumpRenderView();
|
||||||
expect(stateBefore, equals(stateAfter));
|
expect(stateBefore, equals(stateAfter));
|
||||||
|
|
||||||
await tester.pumpWidget(new Markdown(data: 'Data2'));
|
await tester.pumpWidget(new Markdown(data: 'Data2'));
|
||||||
@ -122,15 +121,25 @@ void main() {
|
|||||||
testWidgets('Changing config - style', (WidgetTester tester) async {
|
testWidgets('Changing config - style', (WidgetTester tester) async {
|
||||||
final ThemeData theme = new ThemeData.light();
|
final ThemeData theme = new ThemeData.light();
|
||||||
|
|
||||||
final MarkdownStyle style1 = new MarkdownStyle.defaultFromTheme(theme);
|
final MarkdownStyleSheet style1 = new MarkdownStyleSheet.fromTheme(theme);
|
||||||
final MarkdownStyle style2 = new MarkdownStyle.largeFromTheme(theme);
|
final MarkdownStyleSheet style2 = new MarkdownStyleSheet.largeFromTheme(theme);
|
||||||
|
expect(style1, isNot(style2));
|
||||||
|
|
||||||
await tester.pumpWidget(new Markdown(data: 'Test', markdownStyle: style1));
|
await tester.pumpWidget(new Markdown(data: '# Test', styleSheet: style1));
|
||||||
|
final RichText text1 = tester.widget(find.byType(RichText));
|
||||||
|
await tester.pumpWidget(new Markdown(data: '# Test', styleSheet: style2));
|
||||||
|
final RichText text2 = tester.widget(find.byType(RichText));
|
||||||
|
|
||||||
final String stateBefore = WidgetsBinding.instance.renderViewElement.toStringDeep();
|
expect(text1.text, isNot(text2.text));
|
||||||
await tester.pumpWidget(new Markdown(data: 'Test', markdownStyle: style2));
|
});
|
||||||
final String stateAfter = WidgetsBinding.instance.renderViewElement.toStringDeep();
|
|
||||||
expect(stateBefore, isNot(stateAfter));
|
testWidgets('Style equality', (WidgetTester tester) async {
|
||||||
|
final ThemeData theme = new ThemeData.light();
|
||||||
|
|
||||||
|
final MarkdownStyleSheet style1 = new MarkdownStyleSheet.fromTheme(theme);
|
||||||
|
final MarkdownStyleSheet style2 = new MarkdownStyleSheet.fromTheme(theme);
|
||||||
|
expect(style1, equals(style2));
|
||||||
|
expect(style1.hashCode, equals(style2.hashCode));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,3 +169,9 @@ String _extractTextFromTextSpan(TextSpan span) {
|
|||||||
}
|
}
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _dumpRenderView() {
|
||||||
|
return WidgetsBinding.instance.renderViewElement.toStringDeep().replaceAll(
|
||||||
|
new RegExp(r'SliverChildListDelegate#\d+', multiLine: true), 'SliverChildListDelegate'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user