diff --git a/packages/flutter/lib/src/painting/text_span.dart b/packages/flutter/lib/src/painting/text_span.dart index fa1d18317d..95aa0ed3fa 100644 --- a/packages/flutter/lib/src/painting/text_span.dart +++ b/packages/flutter/lib/src/painting/text_span.dart @@ -185,6 +185,8 @@ class TextSpan { final String indent = '$prefix '; if (style != null) buffer.writeln(style.toString(indent)); + if (recognizer != null) + buffer.writeln('${indent}recognizer: ${recognizer.runtimeType}'); if (text != null) buffer.writeln('$indent"$text"'); if (children != null) { diff --git a/packages/flutter_markdown/lib/flutter_markdown.dart b/packages/flutter_markdown/lib/flutter_markdown.dart index d65c9a5508..8d7ed6ea08 100644 --- a/packages/flutter_markdown/lib/flutter_markdown.dart +++ b/packages/flutter_markdown/lib/flutter_markdown.dart @@ -5,5 +5,6 @@ /// A library to render markdown formatted text. library flutter_markdown; -export 'src/markdown.dart'; -export 'src/markdown_style.dart'; +export 'src/builder.dart'; +export 'src/style_sheet.dart'; +export 'src/widget.dart'; diff --git a/packages/flutter_markdown/lib/flutter_markdown_raw.dart b/packages/flutter_markdown/lib/flutter_markdown_raw.dart deleted file mode 100644 index 3d2be6a5f0..0000000000 --- a/packages/flutter_markdown/lib/flutter_markdown_raw.dart +++ /dev/null @@ -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'; diff --git a/packages/flutter_markdown/lib/src/builder.dart b/packages/flutter_markdown/lib/src/builder.dart new file mode 100644 index 0000000000..1621963961 --- /dev/null +++ b/packages/flutter_markdown/lib/src/builder.dart @@ -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 _kBlockTags = new Set.from([ + 'p', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'li', + 'blockquote', + 'img', + 'pre', + 'ol', + 'ul', +]); + +const List _kListTags = const ['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 children = []; + + int nextListIndex = 0; +} + +class _InlineElement { + final List children = []; +} + +/// 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 _listIndents = []; + 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 build(List 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: [ + 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 parts = src.split('#'); + if (parts.isEmpty) + return const SizedBox(); + + final String path = parts.first; + double width; + double height; + if (parts.length == 2) { + final List 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()); + } + } +} diff --git a/packages/flutter_markdown/lib/src/markdown.dart b/packages/flutter_markdown/lib/src/markdown.dart deleted file mode 100644 index e995232580..0000000000 --- a/packages/flutter_markdown/lib/src/markdown.dart +++ /dev/null @@ -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: -/// -/// 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)); - } -} diff --git a/packages/flutter_markdown/lib/src/markdown_raw.dart b/packages/flutter_markdown/lib/src/markdown_raw.dart deleted file mode 100644 index a3cadd3d2f..0000000000 --- a/packages/flutter_markdown/lib/src/markdown_raw.dart +++ /dev/null @@ -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: . -/// -/// 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 { - - @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 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 blocks = []; - for (_Block block in _cachedBlocks) { - blocks.add(block.build(context)); - } - - return new Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: blocks - ); - } - - @override - void debugFillDescription(List description) { - super.debugFillDescription(description); - description.add('cached blocks identity: ${_cachedBlocks.hashCode}'); - } -} - -class _Renderer implements md.NodeVisitor { - List<_Block> render(List nodes, MarkdownStyleRaw markdownStyle, SyntaxHighlighter syntaxHighlighter, _LinkHandler linkHandler) { - assert(markdownStyle != null); - - _blocks = <_Block>[]; - _listIndents = []; - _markdownStyle = markdownStyle; - _syntaxHighlighter = syntaxHighlighter; - _linkHandler = linkHandler; - - for (final md.Node node in nodes) { - node.accept(this); - } - - return _blocks; - } - - List<_Block> _blocks; - List _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.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 _kBlockTags = const ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'blockquote', 'img', 'pre', 'ol', 'ul']; - static const List _kListTags = const ['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 attributes; - final MarkdownStyleRaw markdownStyle; - final List 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 subWidgets = []; - 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: [ - 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 children = []; - 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 parts = src.split('#'); - if (parts.isEmpty) return new Container(); - - final String path = parts.first; - double width; - double height; - if (parts.length == 2) { - final List 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: [new TextSpan(text: source)]); - } -} diff --git a/packages/flutter_markdown/lib/src/markdown_style.dart b/packages/flutter_markdown/lib/src/markdown_style.dart deleted file mode 100644 index ab66fafb4c..0000000000 --- a/packages/flutter_markdown/lib/src/markdown_style.dart +++ /dev/null @@ -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) - ) - ); -} diff --git a/packages/flutter_markdown/lib/src/markdown_style_raw.dart b/packages/flutter_markdown/lib/src/markdown_style_raw.dart deleted file mode 100644 index adf8316e58..0000000000 --- a/packages/flutter_markdown/lib/src/markdown_style_raw.dart +++ /dev/null @@ -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 _styles; - - Map get styles => _styles; - - void _init() { - _styles = { - 'a': a, - 'p': p, - 'li': p, - 'code': code, - 'pre': p, - 'h1': h1, - 'h2': h2, - 'h3': h3, - 'h4': h4, - 'h5': h5, - 'h6': h6, - 'em': em, - 'strong': strong, - 'blockquote': blockquote - }; - } -} diff --git a/packages/flutter_markdown/lib/src/style_sheet.dart b/packages/flutter_markdown/lib/src/style_sheet.dart new file mode 100644 index 0000000000..ed6eecd941 --- /dev/null +++ b/packages/flutter_markdown/lib/src/style_sheet.dart @@ -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 = { + '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 get styles => _styles; + Map _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, + ); + } +} diff --git a/packages/flutter_markdown/lib/src/widget.dart b/packages/flutter_markdown/lib/src/widget.dart new file mode 100644 index 0000000000..72931d0a59 --- /dev/null +++ b/packages/flutter_markdown/lib/src/widget.dart @@ -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. +/// * +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 children); + + @override + _MarkdownWidgetState createState() => new _MarkdownWidgetState(); +} + +class _MarkdownWidgetState extends State implements MarkdownBuilderDelegate { + List _children; + final List _recognizers = []; + + @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 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 localRecognizers = new List.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. +/// * +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 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. +/// * +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 children) { + return new ListView(padding: padding, children: children); + } +} diff --git a/packages/flutter_markdown/test/flutter_markdown_test.dart b/packages/flutter_markdown/test/flutter_markdown_test.dart index af2877ca59..42301e7781 100644 --- a/packages/flutter_markdown/test/flutter_markdown_test.dart +++ b/packages/flutter_markdown/test/flutter_markdown_test.dart @@ -10,78 +10,77 @@ import 'package:flutter/material.dart'; void main() { testWidgets('Simple string', (WidgetTester tester) async { - await tester.pumpWidget(new MarkdownBody(data: 'Hello')); + await tester.pumpWidget(new MarkdownBody(data: 'Hello')); - final Iterable widgets = tester.allWidgets; - _expectWidgetTypes(widgets, [MarkdownBody, Column, Container, Padding, RichText]); - _expectTextStrings(widgets, ['Hello']); + final Iterable widgets = tester.allWidgets; + _expectWidgetTypes(widgets, [MarkdownBody, Column, RichText]); + _expectTextStrings(widgets, ['Hello']); }); testWidgets('Header', (WidgetTester tester) async { - await tester.pumpWidget(new MarkdownBody(data: '# Header')); + await tester.pumpWidget(new MarkdownBody(data: '# Header')); - final Iterable widgets = tester.allWidgets; - _expectWidgetTypes(widgets, [MarkdownBody, Column, Container, Padding, RichText]); - _expectTextStrings(widgets, ['Header']); + final Iterable widgets = tester.allWidgets; + _expectWidgetTypes(widgets, [MarkdownBody, Column, RichText]); + _expectTextStrings(widgets, ['Header']); }); testWidgets('Empty string', (WidgetTester tester) async { - await tester.pumpWidget(new MarkdownBody(data: '')); + await tester.pumpWidget(new MarkdownBody(data: '')); - final Iterable widgets = tester.allWidgets; - _expectWidgetTypes(widgets, [MarkdownBody, Column]); + final Iterable widgets = tester.allWidgets; + _expectWidgetTypes(widgets, [MarkdownBody, Column]); }); testWidgets('Ordered list', (WidgetTester tester) async { - await tester.pumpWidget(new MarkdownBody(data: '1. Item 1\n1. Item 2\n2. Item 3')); + await tester.pumpWidget(new MarkdownBody(data: '1. Item 1\n1. Item 2\n2. Item 3')); - final Iterable widgets = tester.allWidgets; - _expectTextStrings(widgets, [ - '1.', - 'Item 1', - '2.', - 'Item 2', - '3.', - 'Item 3'] - ); + final Iterable widgets = tester.allWidgets; + _expectTextStrings(widgets, [ + '1.', + 'Item 1', + '2.', + 'Item 2', + '3.', + 'Item 3', + ]); }); testWidgets('Unordered list', (WidgetTester tester) async { - await tester.pumpWidget(new MarkdownBody(data: '- Item 1\n- Item 2\n- Item 3')); + await tester.pumpWidget(new MarkdownBody(data: '- Item 1\n- Item 2\n- Item 3')); - final Iterable widgets = tester.allWidgets; - _expectTextStrings(widgets, [ - '•', - 'Item 1', - '•', - 'Item 2', - '•', - 'Item 3'] - ); + final Iterable widgets = tester.allWidgets; + _expectTextStrings(widgets, [ + '•', + 'Item 1', + '•', + 'Item 2', + '•', + 'Item 3', + ]); }); testWidgets('Scrollable wrapping', (WidgetTester tester) async { - await tester.pumpWidget(new Markdown(data: '')); + await tester.pumpWidget(new Markdown(data: '')); - final List widgets = tester.allWidgets.toList(); - _expectWidgetTypes(widgets.take(2), [ - Markdown, - SingleChildScrollView, - ]); - _expectWidgetTypes(widgets.reversed.take(3).toList().reversed, [ - Padding, - MarkdownBody, - Column - ]); + final List widgets = tester.allWidgets.toList(); + _expectWidgetTypes(widgets.take(2), [ + Markdown, + ListView, + ]); + _expectWidgetTypes(widgets.reversed.take(2).toList().reversed, [ + SliverPadding, + SliverList, + ]); }); testWidgets('Links', (WidgetTester tester) async { - await tester.pumpWidget(new Markdown(data: '[Link Text](href)')); + await tester.pumpWidget(new Markdown(data: '[Link Text](href)')); - final RichText textWidget = tester.allWidgets.firstWhere((Widget widget) => widget is RichText); - final TextSpan span = textWidget.text; + final RichText textWidget = tester.allWidgets.firstWhere((Widget widget) => widget is RichText); + final TextSpan span = textWidget.text; - expect(span.children[0].recognizer.runtimeType, equals(TapGestureRecognizer)); + expect(span.children[0].recognizer.runtimeType, equals(TapGestureRecognizer)); }); testWidgets('HTML tag ignored ', (WidgetTester tester) async { @@ -93,44 +92,54 @@ void main() { for (String mdLine in mdData) { await tester.pumpWidget(new MarkdownBody(data: mdLine)); - final Iterable widgets = tester.allWidgets; - _expectTextStrings(widgets, ['Line 1', 'Line 2']); + final Iterable widgets = tester.allWidgets; + _expectTextStrings(widgets, ['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 String mdLine = 'Line 1 <\n\nc < c c\n\n< Line 2'; + await tester.pumpWidget(new MarkdownBody(data: mdLine)); - final Iterable widgets = tester.allWidgets; - _expectTextStrings(widgets, ['Line 1 <','c < c c','< Line 2']); + final Iterable widgets = tester.allWidgets; + _expectTextStrings(widgets, ['Line 1 <','c < c c','< Line 2']); }); testWidgets('Changing config - data', (WidgetTester tester) async { - await tester.pumpWidget(new Markdown(data: 'Data1')); - _expectTextStrings(tester.allWidgets, ['Data1']); + await tester.pumpWidget(new Markdown(data: 'Data1')); + _expectTextStrings(tester.allWidgets, ['Data1']); - final String stateBefore = WidgetsBinding.instance.renderViewElement.toStringDeep(); - await tester.pumpWidget(new Markdown(data: 'Data1')); - final String stateAfter = WidgetsBinding.instance.renderViewElement.toStringDeep(); - expect(stateBefore, equals(stateAfter)); + final String stateBefore = _dumpRenderView(); + await tester.pumpWidget(new Markdown(data: 'Data1')); + final String stateAfter = _dumpRenderView(); + expect(stateBefore, equals(stateAfter)); - await tester.pumpWidget(new Markdown(data: 'Data2')); - _expectTextStrings(tester.allWidgets, ['Data2']); + await tester.pumpWidget(new Markdown(data: 'Data2')); + _expectTextStrings(tester.allWidgets, ['Data2']); }); 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 MarkdownStyle style2 = new MarkdownStyle.largeFromTheme(theme); + 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', 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(); - await tester.pumpWidget(new Markdown(data: 'Test', markdownStyle: style2)); - final String stateAfter = WidgetsBinding.instance.renderViewElement.toStringDeep(); - expect(stateBefore, isNot(stateAfter)); + 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)); }); } @@ -160,3 +169,9 @@ String _extractTextFromTextSpan(TextSpan span) { } return text; } + +String _dumpRenderView() { + return WidgetsBinding.instance.renderViewElement.toStringDeep().replaceAll( + new RegExp(r'SliverChildListDelegate#\d+', multiLine: true), 'SliverChildListDelegate' + ); +}