Commit b0542344 authored by Viktor Lidholt's avatar Viktor Lidholt

Merge pull request #2585 from vlidholt/master

Initial support for links in markdown
parents 83caeb35 800e2558
......@@ -15,6 +15,9 @@ Style text as _italic_, __bold__, or `inline code`.
- To better clarify
- Your points
## Links
You can use [hyperlinks](hyperlink) in markdown
## Code blocks
Formatted Dart code looks really pretty too. This is an example of how to create your own Markdown widget:
......
......@@ -25,22 +25,26 @@ class Markdown extends MarkdownRaw {
Markdown({
String data,
SyntaxHighlighter syntaxHighlighter,
MarkdownStyle markdownStyle
MarkdownStyle markdownStyle,
MarkdownLinkCallback onTapLink
}) : super(
data: data,
syntaxHighlighter: syntaxHighlighter,
markdownStyle: markdownStyle
markdownStyle: markdownStyle,
onTapLink: onTapLink
);
MarkdownBody createMarkdownBody({
String data,
MarkdownStyle markdownStyle,
SyntaxHighlighter syntaxHighlighter
SyntaxHighlighter syntaxHighlighter,
MarkdownLinkCallback onTapLink
}) {
return new MarkdownBody(
data: data,
markdownStyle: markdownStyle,
syntaxHighlighter: syntaxHighlighter
syntaxHighlighter: syntaxHighlighter,
onTapLink: onTapLink
);
}
}
......@@ -71,11 +75,13 @@ class MarkdownBody extends MarkdownBodyRaw {
MarkdownBody({
String data,
SyntaxHighlighter syntaxHighlighter,
MarkdownStyle markdownStyle
MarkdownStyle markdownStyle,
MarkdownLinkCallback onTapLink
}) : super(
data: data,
syntaxHighlighter: syntaxHighlighter,
markdownStyle: markdownStyle
markdownStyle: markdownStyle,
onTapLink: onTapLink
);
MarkdownStyle createDefaultStyle(BuildContext context) {
......
......@@ -4,8 +4,11 @@
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:
......@@ -26,7 +29,8 @@ class MarkdownRaw extends StatelessComponent {
this.data,
this.markdownStyle,
this.syntaxHighlighter,
this.padding: const EdgeDims.all(16.0)
this.padding: const EdgeDims.all(16.0),
this.onTapLink
});
/// Markdown styled text
......@@ -41,6 +45,9 @@ class MarkdownRaw extends StatelessComponent {
/// Padding used
final EdgeDims padding;
/// Callback when a link is tapped
final MarkdownLinkCallback onTapLink;
Widget build(BuildContext context) {
return new ScrollableViewport(
child: new Padding(
......@@ -48,7 +55,8 @@ class MarkdownRaw extends StatelessComponent {
child: createMarkdownBody(
data: data,
markdownStyle: markdownStyle,
syntaxHighlighter: syntaxHighlighter
syntaxHighlighter: syntaxHighlighter,
onTapLink: onTapLink
)
)
);
......@@ -57,12 +65,14 @@ class MarkdownRaw extends StatelessComponent {
MarkdownBodyRaw createMarkdownBody({
String data,
MarkdownStyleRaw markdownStyle,
SyntaxHighlighter syntaxHighlighter
SyntaxHighlighter syntaxHighlighter,
MarkdownLinkCallback onTapLink
}) {
return new MarkdownBodyRaw(
data: data,
markdownStyle: markdownStyle,
syntaxHighlighter: syntaxHighlighter
syntaxHighlighter: syntaxHighlighter,
onTapLink: onTapLink
);
}
}
......@@ -94,7 +104,8 @@ class MarkdownBodyRaw extends StatefulComponent {
MarkdownBodyRaw({
this.data,
this.markdownStyle,
this.syntaxHighlighter
this.syntaxHighlighter,
this.onTapLink
});
/// Markdown styled text
......@@ -106,6 +117,9 @@ class MarkdownBodyRaw extends StatefulComponent {
/// The syntax highlighter used to color text in code blocks
final SyntaxHighlighter syntaxHighlighter;
/// Callback when a link is tapped
final MarkdownLinkCallback onTapLink;
_MarkdownBodyRawState createState() => new _MarkdownBodyRawState();
MarkdownStyleRaw createDefaultStyle(BuildContext context) => null;
......@@ -119,10 +133,23 @@ class _MarkdownBodyRawState extends State<MarkdownBodyRaw> {
MarkdownStyleRaw markdownStyle = config.markdownStyle ?? config.createDefaultStyle(context);
SyntaxHighlighter syntaxHighlighter = config.syntaxHighlighter ?? new _DefaultSyntaxHighlighter(markdownStyle.code);
_cachedBlocks = _blocksFromMarkup(config.data, markdownStyle, syntaxHighlighter);
_linkHandler = new _LinkHandler(config.onTapLink);
// TODO: This can be optimized by doing the split and removing \r at the same time
List<String> lines = config.data.replaceAll('\r\n', '\n').split('\n');
md.Document document = new md.Document();
_Renderer renderer = new _Renderer();
_cachedBlocks = renderer.render(document.parseLines(lines), markdownStyle, syntaxHighlighter, _linkHandler);
}
void dispose() {
_linkHandler.dispose();
super.dispose();
}
List<_Block> _cachedBlocks;
_LinkHandler _linkHandler;
Widget build(BuildContext context) {
List<Widget> blocks = <Widget>[];
......@@ -137,23 +164,15 @@ class _MarkdownBodyRawState extends State<MarkdownBodyRaw> {
}
}
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<String> 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<md.Node> nodes, MarkdownStyleRaw markdownStyle, SyntaxHighlighter syntaxHighlighter) {
List<_Block> render(List<md.Node> nodes, MarkdownStyleRaw markdownStyle, SyntaxHighlighter syntaxHighlighter, _LinkHandler linkHandler) {
assert(markdownStyle != null);
_blocks = <_Block>[];
_listIndents = <String>[];
_markdownStyle = markdownStyle;
_syntaxHighlighter = syntaxHighlighter;
_linkHandler = linkHandler;
for (final md.Node node in nodes) {
node.accept(this);
......@@ -166,6 +185,7 @@ class _Renderer implements md.NodeVisitor {
List<String> _listIndents;
MarkdownStyleRaw _markdownStyle;
SyntaxHighlighter _syntaxHighlighter;
_LinkHandler _linkHandler;
void visitText(md.Text text) {
_MarkdownNodeList topList = _currentBlock.stack.last;
......@@ -191,8 +211,13 @@ class _Renderer implements md.NodeVisitor {
_Block newBlock = new _Block(element.tag, element.attributes, _markdownStyle, new List<String>.from(_listIndents), blockList.length);
blockList.add(newBlock);
} else {
_LinkInfo linkInfo = null;
if (element.tag == 'a') {
linkInfo = _linkHandler.createLinkInfo(element.attributes['href']);
}
TextStyle style = _markdownStyle.styles[element.tag] ?? new TextStyle();
List<_MarkdownNode> styleElement = <_MarkdownNode>[new _MarkdownNodeTextStyle(style)];
List<_MarkdownNode> styleElement = <_MarkdownNode>[new _MarkdownNodeTextStyle(style, linkInfo)];
_currentBlock.stack.add(new _MarkdownNodeList(styleElement));
}
return true;
......@@ -260,8 +285,9 @@ class _MarkdownNodeList extends _MarkdownNode {
}
class _MarkdownNodeTextStyle extends _MarkdownNode {
_MarkdownNodeTextStyle(this.style);
_MarkdownNodeTextStyle(this.style, [this.linkInfo = null]);
TextStyle style;
_LinkInfo linkInfo;
}
class _MarkdownNodeString extends _MarkdownNode {
......@@ -325,7 +351,8 @@ class _Block {
children: subWidgets
);
} else {
contents = new RichText(text: _stackToTextSpan(new _MarkdownNodeList(stack)));
TextSpan span = _stackToTextSpan(new _MarkdownNodeList(stack));
contents = new RichText(text: span);
if (listIndents.length > 0) {
Widget bullet;
......@@ -384,13 +411,23 @@ class _Block {
if (stack is _MarkdownNodeList) {
List<_MarkdownNode> list = stack.list;
_MarkdownNodeTextStyle styleNode = list[0];
_LinkInfo linkInfo = styleNode.linkInfo;
TextStyle style = styleNode.style;
List<TextSpan> children = <TextSpan>[];
for (int i = 1; i < list.length; i++) {
children.add(_stackToTextSpan(list[i]));
}
return new TextSpan(style: style, children: children);
String text = null;
if (children.length == 1 && _isPlainText(children[0])) {
text = children[0].text;
children = null;
}
TapGestureRecognizer recognizer = linkInfo?.recognizer;
return new TextSpan(style: style, children: children, recognizer: recognizer, text: text);
}
if (stack is _MarkdownNodeString) {
......@@ -400,6 +437,10 @@ class _Block {
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) {
List<String> parts = src.split('#');
if (parts.length == 0) return new Container();
......@@ -419,6 +460,39 @@ class _Block {
}
}
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) {
TapGestureRecognizer recognizer = new TapGestureRecognizer();
recognizer.onTap = () {
if (onTapLink != null)
onTapLink(href);
};
_LinkInfo linkInfo = new _LinkInfo(href, recognizer);
links.add(linkInfo);
return linkInfo;
}
void dispose() {
for (_LinkInfo linkInfo in links) {
linkInfo.recognizer.dispose();
}
}
}
abstract class SyntaxHighlighter {
TextSpan format(String source);
}
......
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';
import 'package:test/test.dart';
import 'package:flutter/material.dart';
......@@ -9,13 +10,9 @@ void main() {
testWidgets((WidgetTester tester) {
tester.pumpWidget(new MarkdownBody(data: "Hello"));
Element textElement = tester.findElement((Element element) => element.widget is RichText);
RichText textWidget = textElement.widget;
TextSpan textSpan = textWidget.text;
List<Element> elements = _listElements(tester);
_expectWidgetTypes(elements, <Type>[MarkdownBody, Column, Container, Padding, RichText]);
expect(textSpan.children[0].text, equals("Hello"));
_expectTextStrings(elements, <String>["Hello"]);
});
});
......@@ -23,13 +20,9 @@ void main() {
testWidgets((WidgetTester tester) {
tester.pumpWidget(new MarkdownBody(data: "# Header"));
Element textElement = tester.findElement((Element element) => element.widget is RichText);
RichText textWidget = textElement.widget;
TextSpan textSpan = textWidget.text;
List<Element> elements = _listElements(tester);
_expectWidgetTypes(elements, <Type>[MarkdownBody, Column, Container, Padding, RichText]);
expect(textSpan.children[0].text, equals("Header"));
_expectTextStrings(elements, <String>["Header"]);
});
});
......@@ -79,7 +72,6 @@ void main() {
tester.pumpWidget(new Markdown(data: ""));
List<Element> elements = _listElements(tester);
for (Element element in elements) print("e: $element");
_expectWidgetTypes(elements, <Type>[
Markdown,
ScrollableViewport,
......@@ -90,6 +82,18 @@ void main() {
]);
});
});
test("Links", () {
testWidgets((WidgetTester tester) {
tester.pumpWidget(new Markdown(data: "[Link Text](href)"));
Element textElement = tester.findElement((Element element) => element.widget is RichText);
RichText textWidget = textElement.widget;
TextSpan span = textWidget.text;
expect(span.children[0].recognizer.runtimeType, equals(TapGestureRecognizer));
});
});
}
List<Element> _listElements(WidgetTester tester) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment