Merge pull request #10 from caffinatedmonkey/images
Images Support
diff --git a/lib/src/inline_parser.dart b/lib/src/inline_parser.dart
index 392ca69..6772c84 100644
--- a/lib/src/inline_parser.dart
+++ b/lib/src/inline_parser.dart
@@ -29,6 +29,7 @@
new AutolinkSyntax(),
new LinkSyntax(),
+ new ImageLinkSyntax(),
// "*" surrounded by spaces is left alone.
new TextSyntax(r' \* '),
// "_" surrounded by spaces is left alone.
@@ -83,8 +84,11 @@
} else {
syntaxes = defaultSyntaxes;
}
- // Custom link resolver goes after the generic text syntax.
- syntaxes.insert(1, new LinkSyntax(linkResolver: document.linkResolver));
+ // Custom link resolvers goes after the generic text syntax.
+ syntaxes.insertAll(1, [
+ new LinkSyntax(linkResolver: document.linkResolver),
+ new ImageLinkSyntax(linkResolver: document.linkResolver)
+ ]);
}
List<Node> parse() {
@@ -214,8 +218,8 @@
bool onMatch(InlineParser parser, Match match) {
final url = match[1];
- final anchor = new Element.text('a', escapeHtml(url));
- anchor.attributes['href'] = url;
+ final anchor = new Element.text('a', escapeHtml(url))
+ ..attributes['href'] = url;
parser.addNode(anchor);
return true;
@@ -267,76 +271,105 @@
// 4: Contains the title, if present, for an inline link.
}
- LinkSyntax({this.linkResolver})
- : super(r'\[', end: linkPattern);
+ LinkSyntax({this.linkResolver, String pattern: r'\['})
+ : super(pattern, end: linkPattern);
- bool onMatchEnd(InlineParser parser, Match match, TagState state) {
- var url;
- var title;
-
+ Node createNode(InlineParser parser, Match match, TagState state) {
// If we didn't match refLink or inlineLink, then it means there was
// nothing after the first square bracket, so it isn't a normal markdown
// link at all. Instead, we allow users of the library to specify a special
// resolver function ([linkResolver]) that may choose to handle
// this. Otherwise, it's just treated as plain text.
- if ((match[1] == null) || (match[1] == '')) {
- if (linkResolver == null) return false;
+ if (isNullOrEmpty(match[1])) {
+ if (linkResolver == null) return null;
// Only allow implicit links if the content is just text.
// TODO(rnystrom): Do we want to relax this?
- if (state.children.any((child) => child is! Text)) return false;
+ if (state.children.any((child) => child is! Text)) return null;
// If there are multiple children, but they are all text, send the
// combined text to linkResolver.
var textToResolve = state.children.fold('',
(oldVal, child) => oldVal + child.text);
+
// See if we have a resolver that will generate a link for us.
- final node = linkResolver(textToResolve);
- if (node == null) return false;
+ return linkResolver(textToResolve);
+ } else {
+ Link link = getLink(parser, match, state);
+ if (link == null) return null;
- parser.addNode(node);
- return true;
+ final Element node = new Element('a', state.children)
+ ..attributes["href"] = escapeHtml(link.url)
+ ..attributes['title'] = escapeHtml(link.title);
+
+ cleanMap(node.attributes);
+ return node;
}
+ }
+ Link getLink(InlineParser parser, Match match, TagState state) {
if ((match[3] != null) && (match[3] != '')) {
// Inline link like [foo](url).
- url = match[3];
- title = match[4];
+ var url = match[3];
+ var title = match[4];
// For whatever reason, markdown allows angle-bracketed URLs here.
if (url.startsWith('<') && url.endsWith('>')) {
url = url.substring(1, url.length - 1);
}
+
+ return new Link(null, url, title);
} else {
+ var id;
// Reference link like [foo] [bar].
- var id = match[2];
- if (id == '') {
+ if (match[2] == '')
// The id is empty ("[]") so infer it from the contents.
id = parser.source.substring(state.startPos + 1, parser.pos);
- }
+ else
+ id = match[2];
// References are case-insensitive.
id = id.toLowerCase();
-
- // Look up the link.
- final link = parser.document.refLinks[id];
- // If it's an unknown link just emit plaintext.
- if (link == null) return false;
-
- url = link.url;
- title = link.title;
+ return parser.document.refLinks[id];
}
+ }
- final anchor = new Element('a', state.children);
- anchor.attributes['href'] = escapeHtml(url);
- if ((title != null) && (title != '')) {
- anchor.attributes['title'] = escapeHtml(title);
- }
-
- parser.addNode(anchor);
+ bool onMatchEnd(InlineParser parser, Match match, TagState state) {
+ Node node = createNode(parser, match, state);
+ if (node == null) return false;
+ parser.addNode(node);
return true;
}
}
+/// Matches images like `![alternate text](url "optional title")` and
+/// `![alternate text][url reference]`.
+class ImageLinkSyntax extends LinkSyntax {
+ Resolver linkResolver;
+ ImageLinkSyntax({this.linkResolver})
+ : super(pattern: r'!\[');
+
+ Node createNode(InlineParser parser, Match match, TagState state) {
+ Node node = super.createNode(parser, match, state);
+ if (node == null) return null;
+
+ final Element imageElement = new Element.withTag("img")
+ ..attributes["src"] = node.attributes["href"]
+ ..attributes["title"] = node.attributes["title"]
+ ..attributes["alt"] = node.children
+ .map((e) => isNullOrEmpty(e) || e is! Text ? '' : e.text)
+ .join(' ');
+
+ cleanMap(imageElement.attributes);
+
+ node.children
+ ..clear()
+ ..add(imageElement);
+
+ return node;
+ }
+}
+
+
/// Matches backtick-enclosed inline code blocks.
class CodeSyntax extends InlineSyntax {
CodeSyntax(String pattern)
diff --git a/lib/src/util.dart b/lib/src/util.dart
index a6d52de..2da80da 100644
--- a/lib/src/util.dart
+++ b/lib/src/util.dart
@@ -2,7 +2,21 @@
/// Replaces `<`, `&`, and `>`, with their HTML entity equivalents.
String escapeHtml(String html) {
+ if (html == '' || html == null) return null;
return html.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>');
}
+
+/// Removes null or empty values from [map].
+void cleanMap(Map map) {
+ map.keys
+ .where((e) => isNullOrEmpty(map[e]))
+ .toList()
+ .forEach(map.remove);
+}
+
+/// Returns true if an object is null or an empty string.
+bool isNullOrEmpty(object) {
+ return object == null || object == '';
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index e5e9f0f..e62748f 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: markdown
-version: 0.5.1
+version: 0.6.0-dev
author: Dart Team <misc@dartlang.org>
description: A library for converting markdown to HTML.
homepage: https://github.com/dpeek/dart-markdown
diff --git a/test/markdown_test.dart b/test/markdown_test.dart
index 3c4564b..e1cc81f 100644
--- a/test/markdown_test.dart
+++ b/test/markdown_test.dart
@@ -862,6 +862,94 @@
''');
});
+ group('Inline Images', () {
+ validate('image','''
+ ![](http://foo.com/foo.png)
+ ''','''
+ <p>
+ <a href="http://foo.com/foo.png">
+ <img src="http://foo.com/foo.png"></img>
+ </a>
+ </p>
+ ''');
+
+ validate('alternate text','''
+ ![alternate text](http://foo.com/foo.png)
+ ''','''
+ <p>
+ <a href="http://foo.com/foo.png">
+ <img alt="alternate text" src="http://foo.com/foo.png"></img>
+ </a>
+ </p>
+ ''');
+
+ validate('title','''
+ ![](http://foo.com/foo.png "optional title")
+ ''','''
+ <p>
+ <a href="http://foo.com/foo.png" title="optional title">
+ <img src="http://foo.com/foo.png" title="optional title"></img>
+ </a>
+ </p>
+ ''');
+ validate('invalid alt text','''
+ ![`alt`](http://foo.com/foo.png)
+ ''','''
+ <p>
+ <a href="http://foo.com/foo.png">
+ <img src="http://foo.com/foo.png"></img>
+ </a>
+ </p>
+ ''');
+ });
+
+ group('Reference Images', () {
+ validate('image','''
+ ![][foo]
+ [foo]: http://foo.com/foo.png
+ ''','''
+ <p>
+ <a href="http://foo.com/foo.png">
+ <img src="http://foo.com/foo.png"></img>
+ </a>
+ </p>
+ ''');
+
+ validate('alternate text','''
+ ![alternate text][foo]
+ [foo]: http://foo.com/foo.png
+ ''','''
+ <p>
+ <a href="http://foo.com/foo.png">
+ <img alt="alternate text" src="http://foo.com/foo.png"></img>
+ </a>
+ </p>
+ ''');
+
+ validate('title','''
+ ![][foo]
+ [foo]: http://foo.com/foo.png "optional title"
+ ''','''
+ <p>
+ <a href="http://foo.com/foo.png" title="optional title">
+ <img src="http://foo.com/foo.png" title="optional title"></img>
+ </a>
+ </p>
+ ''');
+
+ validate('invalid alt text','''
+ ![`alt`][foo]
+ [foo]: http://foo.com/foo.png "optional title"
+ ''','''
+ <p>
+ <a href="http://foo.com/foo.png" title="optional title">
+ <img src="http://foo.com/foo.png" title="optional title"></img>
+ </a>
+ </p>
+ ''');
+
+ });
+
group('Resolver', () {
var nyanResolver = (text) => new Text('~=[,,_${text}_,,]:3');
validate('simple resolver', '''