Conditionally escape HTML in links; fixes dart-lang/markdown#272
diff --git a/pkgs/markdown/lib/src/ast.dart b/pkgs/markdown/lib/src/ast.dart index 9765b24..dd672c1 100644 --- a/pkgs/markdown/lib/src/ast.dart +++ b/pkgs/markdown/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/pkgs/markdown/lib/src/block_parser.dart b/pkgs/markdown/lib/src/block_parser.dart index eb9980b..56faaab 100644 --- a/pkgs/markdown/lib/src/block_parser.dart +++ b/pkgs/markdown/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/pkgs/markdown/lib/src/document.dart b/pkgs/markdown/lib/src/document.dart index f42086b..5f510e0 100644 --- a/pkgs/markdown/lib/src/document.dart +++ b/pkgs/markdown/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/pkgs/markdown/lib/src/extension_set.dart b/pkgs/markdown/lib/src/extension_set.dart index c9a4ea9..d740c64 100644 --- a/pkgs/markdown/lib/src/extension_set.dart +++ b/pkgs/markdown/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/pkgs/markdown/lib/src/html_renderer.dart b/pkgs/markdown/lib/src/html_renderer.dart index 25bcd09..2ffc21d 100644 --- a/pkgs/markdown/lib/src/html_renderer.dart +++ b/pkgs/markdown/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/pkgs/markdown/lib/src/inline_parser.dart b/pkgs/markdown/lib/src/inline_parser.dart index ed4513f..e17a3ba 100644 --- a/pkgs/markdown/lib/src/inline_parser.dart +++ b/pkgs/markdown/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'] =