Conditionally escape HTML in links; fixes #272
diff --git a/lib/src/ast.dart b/lib/src/ast.dart
index 9765b24..dd672c1 100644
--- a/lib/src/ast.dart
+++ b/lib/src/ast.dart
@@ -52,14 +52,15 @@
}
}
- String get textContent => children == null
- ? ''
- : children.map((Node child) => child.textContent).join('');
+ String get textContent {
+ return (children ?? []).map((Node child) => child.textContent).join('');
+ }
}
/// A plain text element.
class Text implements Node {
final String text;
+
Text(this.text);
void accept(NodeVisitor visitor) => visitor.visitText(this);
@@ -75,6 +76,7 @@
/// definitions.
class UnparsedContent implements Node {
final String textContent;
+
UnparsedContent(this.textContent);
void accept(NodeVisitor visitor) => null;
diff --git a/lib/src/block_parser.dart b/lib/src/block_parser.dart
index eb9980b..56faaab 100644
--- a/lib/src/block_parser.dart
+++ b/lib/src/block_parser.dart
@@ -397,9 +397,10 @@
// The Markdown tests expect a trailing newline.
childLines.add('');
- var content = parser.document.encodeHtml
- ? escapeHtml(childLines.join('\n'))
- : childLines.join('\n');
+ var content = childLines.join('\n');
+ if (parser.document.encodeHtml) {
+ content = escapeHtml(content);
+ }
return Element('pre', [Element.text('code', content)]);
}
@@ -516,7 +517,7 @@
r'figcaption|figure|footer|form|frame|frameset|h1|head|header|hr|html|'
r'iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|'
r'option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|'
- 'title|tr|track|ul)'
+ r'title|tr|track|ul)'
r'(?:\s|>|/>|$)');
/// The [_pattern] regular expression above is very expensive, even on
@@ -792,6 +793,7 @@
/// Parses unordered lists.
class UnorderedListSyntax extends ListSyntax {
RegExp get pattern => _ulPattern;
+
String get listTag => 'ul';
const UnorderedListSyntax();
@@ -800,6 +802,7 @@
/// Parses ordered lists.
class OrderedListSyntax extends ListSyntax {
RegExp get pattern => _olPattern;
+
String get listTag => 'ol';
const OrderedListSyntax();
diff --git a/lib/src/document.dart b/lib/src/document.dart
index f42086b..5f510e0 100644
--- a/lib/src/document.dart
+++ b/lib/src/document.dart
@@ -18,16 +18,17 @@
final _inlineSyntaxes = Set<InlineSyntax>();
Iterable<BlockSyntax> get blockSyntaxes => _blockSyntaxes;
+
Iterable<InlineSyntax> get inlineSyntaxes => _inlineSyntaxes;
- Document(
- {Iterable<BlockSyntax> blockSyntaxes,
- Iterable<InlineSyntax> inlineSyntaxes,
- ExtensionSet extensionSet,
- this.linkResolver,
- this.imageLinkResolver,
- this.encodeHtml = true})
- : this.extensionSet = extensionSet ?? ExtensionSet.commonMark {
+ Document({
+ Iterable<BlockSyntax> blockSyntaxes,
+ Iterable<InlineSyntax> inlineSyntaxes,
+ ExtensionSet extensionSet,
+ this.linkResolver,
+ this.imageLinkResolver,
+ this.encodeHtml = true,
+ }) : this.extensionSet = extensionSet ?? ExtensionSet.commonMark {
this._blockSyntaxes
..addAll(blockSyntaxes ?? [])
..addAll(this.extensionSet.blockSyntaxes);
diff --git a/lib/src/extension_set.dart b/lib/src/extension_set.dart
index c9a4ea9..d740c64 100644
--- a/lib/src/extension_set.dart
+++ b/lib/src/extension_set.dart
@@ -20,8 +20,10 @@
/// The [commonMark] extension set is close to compliance with [CommonMark].
///
/// [CommonMark]: http://commonmark.org/
- static final ExtensionSet commonMark =
- ExtensionSet([const FencedCodeBlockSyntax()], [InlineHtmlSyntax()]);
+ static final ExtensionSet commonMark = ExtensionSet(
+ [const FencedCodeBlockSyntax()],
+ [InlineHtmlSyntax()],
+ );
/// The [gitHubWeb] extension set renders Markdown similarly to GitHub.
///
diff --git a/lib/src/html_renderer.dart b/lib/src/html_renderer.dart
index 25bcd09..2ffc21d 100644
--- a/lib/src/html_renderer.dart
+++ b/lib/src/html_renderer.dart
@@ -12,19 +12,22 @@
import 'inline_parser.dart';
/// Converts the given string of Markdown to HTML.
-String markdownToHtml(String markdown,
- {Iterable<BlockSyntax> blockSyntaxes,
- Iterable<InlineSyntax> inlineSyntaxes,
- ExtensionSet extensionSet,
- Resolver linkResolver,
- Resolver imageLinkResolver,
- bool inlineOnly = false}) {
+String markdownToHtml(
+ String markdown, {
+ Iterable<BlockSyntax> blockSyntaxes,
+ Iterable<InlineSyntax> inlineSyntaxes,
+ ExtensionSet extensionSet,
+ Resolver linkResolver,
+ Resolver imageLinkResolver,
+ bool inlineOnly = false,
+}) {
var document = Document(
- blockSyntaxes: blockSyntaxes,
- inlineSyntaxes: inlineSyntaxes,
- extensionSet: extensionSet,
- linkResolver: linkResolver,
- imageLinkResolver: imageLinkResolver);
+ blockSyntaxes: blockSyntaxes,
+ inlineSyntaxes: inlineSyntaxes,
+ extensionSet: extensionSet,
+ linkResolver: linkResolver,
+ imageLinkResolver: imageLinkResolver,
+ );
if (inlineOnly) return renderToHtml(document.parseInline(markdown));
@@ -78,7 +81,7 @@
var content = text.text;
if (const ['p', 'li'].contains(_lastVisitedTag)) {
var lines = LineSplitter.split(content);
- content = (content.contains('<pre>'))
+ content = content.contains('<pre>')
? lines.join('\n')
: lines.map((line) => line.trimLeft()).join('\n');
if (text.text.endsWith('\n')) {
diff --git a/lib/src/inline_parser.dart b/lib/src/inline_parser.dart
index ed4513f..e17a3ba 100644
--- a/lib/src/inline_parser.dart
+++ b/lib/src/inline_parser.dart
@@ -67,7 +67,7 @@
// User specified syntaxes are the first syntaxes to be evaluated.
syntaxes.addAll(document.inlineSyntaxes);
- var documentHasCustomInlineSyntaxes = document.inlineSyntaxes
+ var hasCustomInlineSyntaxes = document.inlineSyntaxes
.any((s) => !document.extensionSet.inlineSyntaxes.contains(s));
// This first RegExp matches plain text to accelerate parsing. It's written
@@ -75,7 +75,7 @@
// Markdown is plain text, so it's faster to match one RegExp per 'word'
// rather than fail to match all the following RegExps at each non-syntax
// character position.
- if (documentHasCustomInlineSyntaxes) {
+ if (hasCustomInlineSyntaxes) {
// We should be less aggressive in blowing past "words".
syntaxes.add(TextSyntax(r'[A-Za-z0-9]+(?=\s)'));
} else {
@@ -302,7 +302,8 @@
bool onMatch(InlineParser parser, Match match) {
var url = match[1];
- var anchor = Element.text('a', escapeHtml(url));
+ var text = parser.document.encodeHtml ? escapeHtml(url) : url;
+ var anchor = Element.text('a', text);
anchor.attributes['href'] = Uri.encodeFull('mailto:$url');
parser.addNode(anchor);
@@ -316,7 +317,8 @@
bool onMatch(InlineParser parser, Match match) {
var url = match[1];
- var anchor = Element.text('a', escapeHtml(url));
+ var text = parser.document.encodeHtml ? escapeHtml(url) : url;
+ var anchor = Element.text('a', text);
anchor.attributes['href'] = Uri.encodeFull(url);
parser.addNode(anchor);
@@ -331,17 +333,21 @@
// Autolinks can only come at the beginning of a line, after whitespace, or
// any of the delimiting characters *, _, ~, and (.
static const start = r'(?:^|[\s*_~(>])';
+
// An extended url autolink will be recognized when one of the schemes
// http://, https://, or ftp://, followed by a valid domain
static const scheme = r'(?:(?:https?|ftp):\/\/|www\.)';
+
// A valid domain consists of alphanumeric characters, underscores (_),
// hyphens (-) and periods (.). There must be at least one period, and no
// underscores may be present in the last two segments of the domain.
static const domainPart = r'\w\-';
static const domain = '[$domainPart][$domainPart.]+';
+
// A valid domain consists of alphanumeric characters, underscores (_),
// hyphens (-) and periods (.).
static const path = r'[^\s<]*';
+
// Trailing punctuation (specifically, ?, !, ., ,, :, *, _, and ~) will not
// be considered part of the autolink
static const truncatingPunctuationPositive = r'[?!.,:*_~]';
@@ -427,7 +433,8 @@
href = 'http://$href';
}
- final anchor = Element.text('a', escapeHtml(url));
+ final text = parser.document.encodeHtml ? escapeHtml(url) : url;
+ final anchor = Element.text('a', text);
anchor.attributes['href'] = Uri.encodeFull(href);
parser.addNode(anchor);
@@ -478,6 +485,7 @@
r'\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F'
r'\uFF5B\uFF5D\uFF5F-\uFF65'
']');
+
// TODO(srawlins): Unicode whitespace
static final String whitespace = ' \t\r\n';
@@ -488,13 +496,14 @@
final bool isPrecededByPunctuation;
final bool isFollowedByPunctuation;
- _DelimiterRun._(
- {this.char,
- this.length,
- this.isLeftFlanking,
- this.isRightFlanking,
- this.isPrecededByPunctuation,
- this.isFollowedByPunctuation});
+ _DelimiterRun._({
+ this.char,
+ this.length,
+ this.isLeftFlanking,
+ this.isRightFlanking,
+ this.isPrecededByPunctuation,
+ this.isFollowedByPunctuation,
+ });
static _DelimiterRun tryParse(InlineParser parser, int runStart, int runEnd) {
bool leftFlanking,
@@ -542,12 +551,13 @@
}
return _DelimiterRun._(
- char: parser.charAt(runStart),
- length: runEnd - runStart + 1,
- isLeftFlanking: leftFlanking,
- isRightFlanking: rightFlanking,
- isPrecededByPunctuation: precededByPunctuation,
- isFollowedByPunctuation: followedByPunctuation);
+ char: parser.charAt(runStart),
+ length: runEnd - runStart + 1,
+ isLeftFlanking: leftFlanking,
+ isRightFlanking: rightFlanking,
+ isPrecededByPunctuation: precededByPunctuation,
+ isFollowedByPunctuation: followedByPunctuation,
+ );
}
String toString() =>
@@ -769,7 +779,10 @@
///
/// [label] does not need to be normalized.
Node _resolveReferenceLink(
- String label, TagState state, Map<String, LinkReference> linkReferences) {
+ String label,
+ TagState state,
+ Map<String, LinkReference> linkReferences,
+ ) {
var normalizedLabel = label.toLowerCase();
var linkReference = linkReferences[normalizedLabel];
if (linkReference != null) {
@@ -1096,7 +1109,7 @@
Node _createNode(TagState state, String destination, String title) {
var element = Element.empty('img');
- element.attributes['src'] = escapeHtml(destination);
+ element.attributes['src'] = destination;
element.attributes['alt'] = state?.textContent ?? '';
if (title != null && title.isNotEmpty) {
element.attributes['title'] =