// Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:markdown/markdown.dart' as md; import 'package:flutter/widgets.dart'; import 'markdown_style_raw.dart'; /// A [Widget] that renders markdown formatted text. It supports all standard /// markdowns from the original markdown specification found here: /// https://daringfireball.net/projects/markdown/ The rendered markdown is /// placed in a padded scrolling view port. If you do not want the scrolling /// behaviour, use the [MarkdownBodyRaw] class instead. class MarkdownRaw extends StatelessComponent { /// Creates a new Markdown [Widget] that renders the markdown formatted string /// passed in as [data]. By default the markdown will be rendered using the /// styles from the current theme, but you can optionally pass in a custom /// [markdownStyle] that specifies colors and fonts to use. Code blocks are /// by default not using syntax highlighting, but it's possible to pass in /// a custom [syntaxHighlighter]. /// /// new MarkdownRaw(data: "Hello _world_!", markdownStyle: myStyle); MarkdownRaw({ this.data, this.markdownStyle, this.syntaxHighlighter, this.padding: const EdgeDims.all(16.0) }); /// Markdown styled text final String data; /// Style used for rendering the markdown final MarkdownStyleRaw markdownStyle; /// The syntax highlighter used to color text in code blocks final SyntaxHighlighter syntaxHighlighter; /// Padding used final EdgeDims padding; Widget build(BuildContext context) { return new ScrollableViewport( child: new Padding( padding: padding, child: createMarkdownBody( data: data, markdownStyle: markdownStyle, syntaxHighlighter: syntaxHighlighter ) ) ); } MarkdownBodyRaw createMarkdownBody({ String data, MarkdownStyleRaw markdownStyle, SyntaxHighlighter syntaxHighlighter }) { return new MarkdownBodyRaw( data: data, markdownStyle: markdownStyle, syntaxHighlighter: syntaxHighlighter ); } } /// A [Widget] that renders markdown formatted text. It supports all standard /// markdowns from the original markdown specification found here: /// https://daringfireball.net/projects/markdown/ This class doesn't implement /// any scrolling behavior, if you want scrolling either wrap the widget in /// a [ScrollableViewport] or use the [MarkdownRaw] widget. class MarkdownBodyRaw extends StatefulComponent { /// Creates a new Markdown [Widget] that renders the markdown formatted string /// passed in as [data]. You need to pass in a [markdownStyle] that defines /// how the code is rendered. Code blocks are by default not using syntax /// highlighting, but it's possible to pass in a custom [syntaxHighlighter]. /// /// Typically, you may want to wrap the [MarkdownBodyRaw] widget in a /// [Padding] and a [ScrollableViewport], or use the [Markdown class] /// /// new ScrollableViewport( /// child: new Padding( /// padding: new EdgeDims.all(16.0), /// child: new MarkdownBodyRaw( /// data: markdownSource, /// markdownStyle: myStyle /// ) /// ) /// ) MarkdownBodyRaw({ this.data, this.markdownStyle, this.syntaxHighlighter }); /// Markdown styled text final String data; /// Style used for rendering the markdown final MarkdownStyleRaw markdownStyle; /// The syntax highlighter used to color text in code blocks final SyntaxHighlighter syntaxHighlighter; _MarkdownBodyRawState createState() => new _MarkdownBodyRawState(); MarkdownStyleRaw createDefaultStyle(BuildContext context) => null; } class _MarkdownBodyRawState extends State { void initState() { super.initState(); MarkdownStyleRaw markdownStyle = config.markdownStyle ?? config.createDefaultStyle(context); SyntaxHighlighter syntaxHighlighter = config.syntaxHighlighter ?? new _DefaultSyntaxHighlighter(markdownStyle.code); _cachedBlocks = _blocksFromMarkup(config.data, markdownStyle, syntaxHighlighter); } List<_Block> _cachedBlocks; Widget build(BuildContext context) { List blocks = []; for (_Block block in _cachedBlocks) { blocks.add(block.build(context)); } return new Column( alignItems: FlexAlignItems.stretch, children: blocks ); } } List<_Block> _blocksFromMarkup(String data, MarkdownStyleRaw markdownStyle, SyntaxHighlighter syntaxHighlighter) { // TODO: This can be optimized by doing the split and removing \r at the same time List lines = data.replaceAll('\r\n', '\n').split('\n'); md.Document document = new md.Document(); _Renderer renderer = new _Renderer(); return renderer.render(document.parseLines(lines), markdownStyle, syntaxHighlighter); } class _Renderer implements md.NodeVisitor { List<_Block> render(List nodes, MarkdownStyleRaw markdownStyle, SyntaxHighlighter syntaxHighlighter) { assert(markdownStyle != null); _blocks = <_Block>[]; _listIndents = []; _markdownStyle = markdownStyle; _syntaxHighlighter = syntaxHighlighter; for (final md.Node node in nodes) { node.accept(this); } return _blocks; } List<_Block> _blocks; List _listIndents; MarkdownStyleRaw _markdownStyle; SyntaxHighlighter _syntaxHighlighter; void visitText(md.Text text) { _MarkdownNodeList topList = _currentBlock.stack.last; List<_MarkdownNode> top = topList.list; if (_currentBlock.tag == 'pre') top.add(new _MarkdownNodeTextSpan(_syntaxHighlighter.format(text.text))); else top.add(new _MarkdownNodeString(text.text)); } bool visitElementBefore(md.Element element) { if (_isListTag(element.tag)) _listIndents.add(element.tag); if (_isBlockTag(element.tag)) { List<_Block> blockList; if (_currentBlock == null) blockList = _blocks; else blockList = _currentBlock.subBlocks; _Block newBlock = new _Block(element.tag, element.attributes, _markdownStyle, new List.from(_listIndents), blockList.length); blockList.add(newBlock); } else { TextStyle style = _markdownStyle.styles[element.tag] ?? new TextStyle(); List<_MarkdownNode> styleElement = <_MarkdownNode>[new _MarkdownNodeTextStyle(style)]; _currentBlock.stack.add(new _MarkdownNodeList(styleElement)); } return true; } void visitElementAfter(md.Element element) { if (_isListTag(element.tag)) _listIndents.removeLast(); if (_isBlockTag(element.tag)) { if (_currentBlock.stack.length > 0) { _MarkdownNodeList stackList = _currentBlock.stack.first; _currentBlock.stack = stackList.list; _currentBlock.open = false; } else { _currentBlock.stack = <_MarkdownNode>[new _MarkdownNodeString('')]; } } else { if (_currentBlock.stack.length > 1) { _MarkdownNodeList poppedList = _currentBlock.stack.last; List<_MarkdownNode> popped = poppedList.list; _currentBlock.stack.removeLast(); _MarkdownNodeList topList = _currentBlock.stack.last; List<_MarkdownNode> top = topList.list; top.add(new _MarkdownNodeList(popped)); } } } static const List _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; _Block childBlock = _currentBlockInList(blocks.last.subBlocks); if (childBlock != null) return childBlock; return blocks.last; } } abstract class _MarkdownNode { } class _MarkdownNodeList extends _MarkdownNode { _MarkdownNodeList(this.list); List<_MarkdownNode> list; } class _MarkdownNodeTextStyle extends _MarkdownNode { _MarkdownNodeTextStyle(this.style); TextStyle style; } class _MarkdownNodeString extends _MarkdownNode { _MarkdownNodeString(this.string); String string; } class _MarkdownNodeTextSpan extends _MarkdownNode { _MarkdownNodeTextSpan(this.textSpan); TextSpan textSpan; } class _Block { _Block(this.tag, this.attributes, this.markdownStyle, this.listIndents, this.blockPosition) { TextStyle style = markdownStyle.styles[tag]; if (style == null) style = new TextStyle(color: const Color(0xffff0000)); stack = <_MarkdownNode>[new _MarkdownNodeList(<_MarkdownNode>[new _MarkdownNodeTextStyle(style)])]; subBlocks = <_Block>[]; } final String tag; final Map attributes; final MarkdownStyleRaw markdownStyle; final List listIndents; final int blockPosition; List<_MarkdownNode> stack; List<_Block> subBlocks; bool get open => _open; void set open(bool open) { _open = open; if (!open && subBlocks.length > 0) subBlocks.last.isLast = true; } bool _open = true; bool isLast = false; Widget build(BuildContext context) { if (tag == 'img') { return _buildImage(context, attributes['src']); } double spacing = markdownStyle.blockSpacing; if (isLast) spacing = 0.0; Widget contents; if (subBlocks.length > 0) { List subWidgets = []; for (_Block subBlock in subBlocks) { subWidgets.add(subBlock.build(context)); } contents = new Column( alignItems: FlexAlignItems.stretch, children: subWidgets ); } else { contents = new RichText(text: _stackToTextSpan(new _MarkdownNodeList(stack))); if (listIndents.length > 0) { Widget bullet; if (listIndents.last == 'ul') { bullet = new Text( '•', style: new TextStyle(textAlign: TextAlign.center) ); } else { bullet = new Padding( padding: new EdgeDims.only(right: 5.0), child: new Text( "${blockPosition + 1}.", style: new TextStyle(textAlign: TextAlign.right) ) ); } contents = new Row( alignItems: FlexAlignItems.start, children: [ new SizedBox( width: listIndents.length * markdownStyle.listIndent, child: bullet ), new Flexible(child: contents) ] ); } } BoxDecoration decoration; EdgeDims padding; if (tag == 'blockquote') { decoration = markdownStyle.blockquoteDecoration; padding = new EdgeDims.all(markdownStyle.blockquotePadding); } else if (tag == 'pre') { decoration = markdownStyle.codeblockDecoration; padding = new EdgeDims.all(markdownStyle.codeblockPadding); } return new Container( padding: padding, margin: new EdgeDims.only(bottom: spacing), child: contents, decoration: decoration ); } TextSpan _stackToTextSpan(_MarkdownNode stack) { if (stack is _MarkdownNodeTextSpan) return stack.textSpan; if (stack is _MarkdownNodeList) { List<_MarkdownNode> list = stack.list; _MarkdownNodeTextStyle styleNode = list[0]; TextStyle style = styleNode.style; List children = []; for (int i = 1; i < list.length; i++) { children.add(_stackToTextSpan(list[i])); } return new TextSpan(style: style, children: children); } if (stack is _MarkdownNodeString) { return new TextSpan(text: stack.string); } return null; } Widget _buildImage(BuildContext context, String src) { List parts = src.split('#'); if (parts.length == 0) return new Container(); String path = parts.first; double width; double height; if (parts.length == 2) { List dimensions = parts.last.split('x'); if (dimensions.length == 2) { width = double.parse(dimensions[0]); height = double.parse(dimensions[1]); } } return new NetworkImage(src: path, width: width, height: height); } } abstract class SyntaxHighlighter { TextSpan format(String source); } class _DefaultSyntaxHighlighter extends SyntaxHighlighter{ _DefaultSyntaxHighlighter(this.style); final TextStyle style; TextSpan format(String source) { return new TextSpan(style: style, children: [new TextSpan(text: source)]); } }