diff --git a/lib/components/flutter_markdown/LICENSE b/lib/components/flutter_markdown/LICENSE new file mode 100644 index 00000000..e7892520 --- /dev/null +++ b/lib/components/flutter_markdown/LICENSE @@ -0,0 +1,27 @@ +// Copyright 2017 Google, Inc. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/lib/components/flutter_markdown/README.md b/lib/components/flutter_markdown/README.md new file mode 100644 index 00000000..35c4a267 --- /dev/null +++ b/lib/components/flutter_markdown/README.md @@ -0,0 +1,39 @@ +# Flutter Markdown +[![pub package](https://img.shields.io/pub/v/flutter_markdown.svg)](https://pub.dartlang.org/packages/flutter_markdown) +[![Build Status](https://travis-ci.org/flutter/flutter_markdown.svg?branch=master)](https://travis-ci.org/flutter/flutter_markdown) + + +A markdown renderer for Flutter. It supports the +[original format](https://daringfireball.net/projects/markdown/), but no inline +html. + +## Getting Started + +Using the Markdown widget is simple, just pass in the source markdown as a +string: + + new Markdown(data: markdownSource); + +If you do not want the padding or scrolling behavior, use the MarkdownBody +instead: + + new MarkdownBody(data: markdownSource); + +By default, Markdown uses the formatting from the current material design theme, +but it's possible to create your own custom styling. Use the MarkdownStyle class +to pass in your own style. If you don't want to use Markdown outside of material +design, use the MarkdownRaw class. + +## Image support + +The `Img` tag only supports the following image locations: + +* From the network: Use a URL prefixed by either `http://` or `https://`. + +* From local files on the device: Use an absolute path to the file, for example by + concatenating the file name with the path returned by a known storage location, + such as those provided by the [`path_provider`](https://pub.dartlang.org/packages/path_provider) + plugin. + +* From image locations referring to bundled assets: Use an asset name prefixed by `resource:`. + like `resource:assets/image.png`. diff --git a/lib/components/flutter_markdown/lib/flutter_markdown.dart b/lib/components/flutter_markdown/lib/flutter_markdown.dart new file mode 100644 index 00000000..8d7ed6ea --- /dev/null +++ b/lib/components/flutter_markdown/lib/flutter_markdown.dart @@ -0,0 +1,10 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// A library to render markdown formatted text. +library flutter_markdown; + +export 'src/builder.dart'; +export 'src/style_sheet.dart'; +export 'src/widget.dart'; diff --git a/lib/components/flutter_markdown/lib/src/builder.dart b/lib/components/flutter_markdown/lib/src/builder.dart new file mode 100644 index 00000000..089e83c3 --- /dev/null +++ b/lib/components/flutter_markdown/lib/src/builder.dart @@ -0,0 +1,376 @@ +// 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 'dart:io'; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; +import 'package:markdown/markdown.dart' as md; +import 'package:path/path.dart' as p; +import 'style_sheet.dart'; + +typedef Widget DemoBuilder(Map attrs); + +final Set _kBlockTags = new Set.from([ + 'p', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'li', + 'blockquote', + 'pre', + 'ol', + 'ul', + 'hr', +]); + +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; +} + +/// A collection of widgets that should be placed adjacent to (inline with) +/// other inline elements in the same parent block. +/// +/// Inline elements can be textual (a/em/strong) represented by [RichText] +/// widgets or images (img) represented by [Image.network] widgets. +/// +/// Inline elements can be nested within other inline elements, inheriting their +/// parent's style along with the style of the block they are in. +/// +/// When laying out inline widgets, first, any adjacent RichText widgets are +/// merged, then, all inline widgets are enclosed in a parent [Wrap] widget. +class _InlineElement { + _InlineElement(this.tag, {this.style}); + + final String tag; + + /// Created by merging the style defined for this element's [tag] in the + /// delegate's [MarkdownStyleSheet] with the style of its parent. + final TextStyle style; + + 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, + this.imageDirectory, + this.demoParser + }); + + /// 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 DemoBuilder demoParser; + /// The base directory holding images referenced by Img tags with local file paths. + final Directory imageDirectory; + + final List _listIndents = []; + final List<_BlockElement> _blocks = <_BlockElement>[]; + final List<_InlineElement> _inlines = <_InlineElement>[]; + final List _linkHandlers = []; + + + /// 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(); + _linkHandlers.clear(); + + _blocks.add(new _BlockElement(null)); + + for (md.Node node in nodes) { + assert(_blocks.length == 1); + node.accept(this); + } + + assert(_inlines.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; + + _addParentInlineIfNeeded(_blocks.last.tag); + + final TextSpan span = _blocks.last.tag == 'pre' + ? delegate.formatText(styleSheet, text.text) + : new TextSpan( + style: _inlines.last.style, + text: text.text, + recognizer: _linkHandlers.isNotEmpty ? _linkHandlers.last : null, + ); + + _inlines.last.children.add(new RichText( + textScaleFactor: styleSheet.textScaleFactor, + text: span, + )); + } + + @override + bool visitElementBefore(md.Element element) { +// print("visitElementBefore ${element.tag}"); + final String tag = element.tag; + if (_isBlockTag(tag)) { + _addAnonymousBlockIfNeeded(styleSheet.styles[tag]); + if (_isListTag(tag)) + _listIndents.add(tag); + _blocks.add(new _BlockElement(tag)); + } else { + _addParentInlineIfNeeded(_blocks.last.tag); + + TextStyle parentStyle = _inlines.last.style; + _inlines.add(new _InlineElement( + tag, + style: parentStyle.merge(styleSheet.styles[tag]), + )); + } + + if (tag == 'a') { + _linkHandlers.add(delegate.createLink(element.attributes['href'])); + } + 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 (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, + ), + ); + } else if (tag == 'hr') { + child = new DecoratedBox( + decoration: styleSheet.horizontalRuleDecoration, + child: child, + ); + } + _addBlockChild(child); + } else { + + final _InlineElement current = _inlines.removeLast(); + final _InlineElement parent = _inlines.last; + + if (tag == 'img') { + // create an image widget for this image + current.children.add(_buildImage(element.attributes['src'])); + } else if (tag == 'a') { + _linkHandlers.removeLast(); + } else if (tag == 'demo') { + current.children.add(_buildGoDemos(element.attributes)); + } + + + if (current.children.isNotEmpty) { + parent.children.addAll(current.children); + } + } + } + Widget _buildGoDemos(Map attrs) { + Widget targetGoDemos; + + if (demoParser != null) { + targetGoDemos = demoParser(attrs); + } + + return targetGoDemos ?? new Text('demo not exits'); + } + + 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]); + } + } + + Uri uri = Uri.parse(path); + Widget child; + if (uri.scheme == 'http' || uri.scheme == 'https') { + child = new Image.network(uri.toString(), width: width, height: height); + } else if (uri.scheme == 'data') { + child = _handleDataSchemeUri(uri, width, height); + } else if (uri.scheme == "resource") { + child = new Image.asset(path.substring(9), width: width, height: height); + } else { + String filePath = (imageDirectory == null + ? uri.toFilePath() + : p.join(imageDirectory.path, uri.toFilePath())); + child = new Image.file(new File(filePath), width: width, height: height); + } + + if (_linkHandlers.isNotEmpty) { + TapGestureRecognizer recognizer = _linkHandlers.last; + return new GestureDetector(child: child, onTap: recognizer.onTap); + } else { + return child; + } + } + + Widget _handleDataSchemeUri(Uri uri, final double width, final double height) { + final String mimeType = uri.data.mimeType; + if (mimeType.startsWith('image/')) { + return new Image.memory(uri.data.contentAsBytes(), width: width, height: height); + } else if (mimeType.startsWith('text/')) { + return new Text(uri.data.contentAsString()); + } + return const SizedBox(); + } + + Widget _buildBullet(String listTag) { + if (listTag == 'ul') + return new Text('•', textAlign: TextAlign.center, style: styleSheet.styles['li']); + + final int index = _blocks.last.nextListIndex; + return new Padding( + padding: const EdgeInsets.only(right: 5.0), + child: new Text('${index + 1}.', textAlign: TextAlign.right, style: styleSheet.styles['li']), + ); + } + + void _addParentInlineIfNeeded(String tag) { + if (_inlines.isEmpty) { + _inlines.add(new _InlineElement( + tag, + style: styleSheet.styles[tag], + )); + } + } + + 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) { + if (_inlines.isEmpty) { + return; + } + + final _InlineElement inline = _inlines.single; + if (inline.children.isNotEmpty) { + List mergedInlines = _mergeInlineChildren(inline); + final Wrap wrap = new Wrap(children: mergedInlines); + _addBlockChild(wrap); + _inlines.clear(); + } + } + + /// Merges adjacent [TextSpan] children of the given [_InlineElement] + List _mergeInlineChildren(_InlineElement inline) { + List mergedTexts = []; + for (Widget child in inline.children) { + if (mergedTexts.isNotEmpty && mergedTexts.last is RichText && child is RichText) { + RichText previous = mergedTexts.removeLast(); + List children = previous.text.children != null + ? new List.from(previous.text.children) + : [previous.text]; + children.add(child.text); + TextSpan mergedSpan = new TextSpan(children: children); + mergedTexts.add(new RichText( + textScaleFactor: styleSheet.textScaleFactor, + text: mergedSpan, + )); + } else { + mergedTexts.add(child); + } + } + return mergedTexts; + } +} diff --git a/lib/components/flutter_markdown/lib/src/style_sheet.dart b/lib/components/flutter_markdown/lib/src/style_sheet.dart new file mode 100644 index 00000000..6c44b773 --- /dev/null +++ b/lib/components/flutter_markdown/lib/src/style_sheet.dart @@ -0,0 +1,307 @@ +// 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.img, + this.blockSpacing, + this.listIndent, + this.blockquotePadding, + this.blockquoteDecoration, + this.codeblockPadding, + this.codeblockDecoration, + this.horizontalRuleDecoration, + this.textScaleFactor = 1.0 + }) : _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, + 'img': img, + }; + + /// Creates a [MarkdownStyleSheet] from the [TextStyle]s in the provided [ThemeData]. + factory MarkdownStyleSheet.fromTheme(ThemeData theme) { + assert(theme?.textTheme?.body1?.fontSize != null); + return new MarkdownStyleSheet( + a: const TextStyle(color: Colors.blue), + p: theme.textTheme.body1, + code: new TextStyle( + color: Colors.grey.shade700, + fontFamily: "monospace", + fontSize: theme.textTheme.body1.fontSize * 0.85 + ), + h1: theme.textTheme.headline, + h2: theme.textTheme.title, + h3: theme.textTheme.subhead, + h4: theme.textTheme.body2, + h5: theme.textTheme.body2, + h6: theme.textTheme.body2, + em: const TextStyle(fontStyle: FontStyle.italic), + strong: const TextStyle(fontWeight: FontWeight.bold), + blockquote: theme.textTheme.body1, + img: theme.textTheme.body1, + blockSpacing: 8.0, + listIndent: 32.0, + blockquotePadding: 8.0, + blockquoteDecoration: new BoxDecoration( + color: Colors.blue.shade100, + borderRadius: new BorderRadius.circular(2.0) + ), + codeblockPadding: 8.0, + codeblockDecoration: new BoxDecoration( + color: Colors.grey.shade100, + borderRadius: new BorderRadius.circular(2.0) + ), + horizontalRuleDecoration: new BoxDecoration( + border: new Border( + top: new BorderSide(width: 5.0, color: Colors.grey.shade300) + ), + ), + ); + } + + /// 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: const TextStyle(color: Colors.blue), + p: theme.textTheme.body1, + code: new TextStyle( + color: Colors.grey.shade700, + fontFamily: "monospace", + fontSize: theme.textTheme.body1.fontSize * 0.85 + ), + h1: theme.textTheme.display3, + h2: theme.textTheme.display2, + h3: theme.textTheme.display1, + h4: theme.textTheme.headline, + h5: theme.textTheme.title, + h6: theme.textTheme.subhead, + em: const TextStyle(fontStyle: FontStyle.italic), + strong: const TextStyle(fontWeight: FontWeight.bold), + blockquote: theme.textTheme.body1, + img: theme.textTheme.body1, + blockSpacing: 8.0, + listIndent: 32.0, + blockquotePadding: 8.0, + blockquoteDecoration: new BoxDecoration( + color: Colors.blue.shade100, + borderRadius: new BorderRadius.circular(2.0) + ), + codeblockPadding: 8.0, + codeblockDecoration: new BoxDecoration( + color: Colors.grey.shade100, + borderRadius: new BorderRadius.circular(2.0) + ), + horizontalRuleDecoration: new BoxDecoration( + border: new Border( + top: new BorderSide(width: 5.0, color: Colors.grey.shade300) + ), + ), + ); + } + + /// 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, + TextStyle img, + double blockSpacing, + double listIndent, + double blockquotePadding, + Decoration blockquoteDecoration, + double codeblockPadding, + Decoration codeblockDecoration, + Decoration horizontalRuleDecoration, + double textScaleFactor, + }) { + return new MarkdownStyleSheet( + a: a ?? this.a, + p: p ?? this.p, + code: code ?? this.code, + h1: h1 ?? this.h1, + h2: h2 ?? this.h2, + h3: h3 ?? this.h3, + h4: h4 ?? this.h4, + h5: h5 ?? this.h5, + h6: h6 ?? this.h6, + em: em ?? this.em, + strong: strong ?? this.strong, + blockquote: blockquote ?? this.blockquote, + img: img ?? this.img, + blockSpacing: blockSpacing ?? this.blockSpacing, + listIndent: listIndent ?? this.listIndent, + blockquotePadding: blockquotePadding ?? this.blockquotePadding, + blockquoteDecoration: blockquoteDecoration ?? this.blockquoteDecoration, + codeblockPadding: codeblockPadding ?? this.codeblockPadding, + codeblockDecoration: codeblockDecoration ?? this.codeblockDecoration, + horizontalRuleDecoration: horizontalRuleDecoration ?? this.horizontalRuleDecoration, + textScaleFactor : textScaleFactor ?? this.textScaleFactor, + ); + } + + /// 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 [TextStyle] to use for `img` elements. + final TextStyle img; + + /// 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; + + /// The decoration to use for `hr` elements. + final Decoration horizontalRuleDecoration; + + // The text scale factor to use in textual elements + final double textScaleFactor; + + /// A [Map] from element name to the corresponding [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.img == img + && typedOther.blockSpacing == blockSpacing + && typedOther.listIndent == listIndent + && typedOther.blockquotePadding == blockquotePadding + && typedOther.blockquoteDecoration == blockquoteDecoration + && typedOther.codeblockPadding == codeblockPadding + && typedOther.codeblockDecoration == codeblockDecoration + && typedOther.horizontalRuleDecoration == horizontalRuleDecoration + && typedOther.textScaleFactor == textScaleFactor; + } + + @override + int get hashCode { + return hashList([ + a, + p, + code, + h1, + h2, + h3, + h4, + h5, + h6, + em, + strong, + blockquote, + img, + blockSpacing, + listIndent, + blockquotePadding, + blockquoteDecoration, + codeblockPadding, + codeblockDecoration, + horizontalRuleDecoration, + textScaleFactor, + ]); + } +} diff --git a/lib/components/flutter_markdown/lib/src/widget.dart b/lib/components/flutter_markdown/lib/src/widget.dart new file mode 100644 index 00000000..c9d5cbaf --- /dev/null +++ b/lib/components/flutter_markdown/lib/src/widget.dart @@ -0,0 +1,247 @@ +// 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 'dart:io'; + +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'; +// +typedef Widget ItemDemoBuilder(Map attrs); + +/// 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. + const MarkdownWidget({ + Key key, + @required this.data, + this.styleSheet, + this.syntaxHighlighter, + this.onTapLink, + this.imageDirectory, + this.demoBuilder, + }) : assert(data != null), + super(key: key); + + /// The Markdown to display. + final String data; + + /// The styles to use when displaying the Markdown. + /// + /// If null, the styles are inferred 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; + + /// The base directory holding images referenced by Img tags with local file paths. + final Directory imageDirectory; + + final ItemDemoBuilder demoBuilder; + /// 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 DemosSyntax extends md.InlineSyntax { + DemosSyntax() : super('\\[demo:([a-z0-9_+-]+)\\]'); + bool onMatch(parser, match) { + var anchor = new md.Element.empty('demo'); + anchor.attributes['id'] = match[1]; + parser.addNode(anchor); + return true; + } +} + +class _MarkdownWidgetState extends State implements MarkdownBuilderDelegate { + List _children; + final List _recognizers = []; + + @override + void didChangeDependencies() { + _parseMarkdown(); + super.didChangeDependencies(); + } + + @override + void didUpdateWidget(MarkdownWidget oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.data != oldWidget.data + || widget.styleSheet != oldWidget.styleSheet) + _parseMarkdown(); + } + + @override + void dispose() { + _disposeRecognizers(); + super.dispose(); + } + + void _parseMarkdown() { + final MarkdownStyleSheet styleSheet = widget.styleSheet ?? new MarkdownStyleSheet.fromTheme(Theme.of(context)); + + _disposeRecognizers(); + + // TODO: This can be optimized by doing the split and removing \r at the same time + final List lines = widget.data.replaceAll('\r\n', '\n').split('\n'); + final md.ExtensionSet extens = new md.ExtensionSet([ + md.FencedCodeBlockSyntax() + ], [ + new DemosSyntax(), + new md.InlineHtmlSyntax(), + ]); + final md.Document document = new md.Document(encodeHtml: false, extensionSet: extens); + final MarkdownBuilder builder = new MarkdownBuilder( + delegate: this, + styleSheet: styleSheet, + imageDirectory: widget.imageDirectory, + demoParser: widget.demoBuilder + ); + _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 (widget.onTapLink != null) + widget.onTapLink(href); + }; + _recognizers.add(recognizer); + return recognizer; + } + + @override + TextSpan formatText(MarkdownStyleSheet styleSheet, String code) { + if (widget.syntaxHighlighter != null) + return widget.syntaxHighlighter.format(code); + return new TextSpan(style: styleSheet.code, text: code); + } + + @override + Widget build(BuildContext context) => widget.build(context, _children); +} + +/// A non-scrolling widget that parses and displays Markdown. +/// +/// Supports all standard Markdown from the original +/// [Markdown specification](https://daringfireball.net/projects/markdown/). +/// +/// See also: +/// +/// * [Markdown], which is a scrolling container of Markdown. +/// * +class MarkdownBody extends MarkdownWidget { + /// Creates a non-scrolling widget that parses and displays Markdown. + const MarkdownBody({ + Key key, + String data, + MarkdownStyleSheet styleSheet, + SyntaxHighlighter syntaxHighlighter, + MarkdownTapLinkCallback onTapLink, + Directory imageDirectory, + ItemDemoBuilder demoBuilder, + }) : super( + key: key, + data: data, + styleSheet: styleSheet, + syntaxHighlighter: syntaxHighlighter, + onTapLink: onTapLink, + imageDirectory: imageDirectory, + demoBuilder: demoBuilder + ); + + @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. + const Markdown({ + Key key, + String data, + MarkdownStyleSheet styleSheet, + SyntaxHighlighter syntaxHighlighter, + MarkdownTapLinkCallback onTapLink, + Directory imageDirectory, + this.padding: const EdgeInsets.all(16.0), + }) : super( + key: key, + data: data, + styleSheet: styleSheet, + syntaxHighlighter: syntaxHighlighter, + onTapLink: onTapLink, + imageDirectory: imageDirectory, + ); + + /// 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/lib/standard_pages/page_snfan_facca78e_32ae_4241_9c8a_5c9e1f92b096/.page.json b/lib/standard_pages/page_snfan_facca78e_32ae_4241_9c8a_5c9e1f92b096/.page.json deleted file mode 100644 index d8f0f7c9..00000000 --- a/lib/standard_pages/page_snfan_facca78e_32ae_4241_9c8a_5c9e1f92b096/.page.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "page", - "screenShot": "", - "author":"snfan", - "email": "hanxu317@qq.com", - "desc": "desc", - "id": "facca78e_32ae_4241_9c8a_5c9e1f92b096" -} - \ No newline at end of file diff --git a/lib/standard_pages/page_snfan_facca78e_32ae_4241_9c8a_5c9e1f92b096/index.dart b/lib/standard_pages/page_snfan_facca78e_32ae_4241_9c8a_5c9e1f92b096/index.dart deleted file mode 100644 index d8487663..00000000 --- a/lib/standard_pages/page_snfan_facca78e_32ae_4241_9c8a_5c9e1f92b096/index.dart +++ /dev/null @@ -1,18 +0,0 @@ -String getMd() { - return """ - # page123 - -this is page markdown for test 123 - -you can load demo like this 123an load demo like this 123an load demo like this 123 - -123123 -``` -[demo: xxxid] -``` - -"""; - - -} - \ No newline at end of file diff --git a/lib/standard_pages/page_snfan_facca78e_32ae_4241_9c8a_5c9e1f92b096/index.md b/lib/standard_pages/page_snfan_facca78e_32ae_4241_9c8a_5c9e1f92b096/index.md deleted file mode 100644 index 33a8f96c..00000000 --- a/lib/standard_pages/page_snfan_facca78e_32ae_4241_9c8a_5c9e1f92b096/index.md +++ /dev/null @@ -1,11 +0,0 @@ -# page123 - -this is page markdown for test 123 - -you can load demo like this 123an load demo like this 123an load demo like this 123 - -123123 -``` -[demo: xxxid] -``` -