diff --git a/app/lib/shared/markdown.dart b/app/lib/shared/markdown.dart index 453a01d142..c635ced07c 100644 --- a/app/lib/shared/markdown.dart +++ b/app/lib/shared/markdown.dart @@ -7,6 +7,7 @@ import 'package:html/dom_parsing.dart' as html_parsing; import 'package:html/parser.dart' as html_parser; import 'package:logging/logging.dart'; import 'package:markdown/markdown.dart' as m; +import 'package:pub_dev/frontend/static_files.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:sanitize_html/sanitize_html.dart'; @@ -46,14 +47,17 @@ String markdownToHtml( final sw = Stopwatch()..start(); try { text = text.replaceAll('\r\n', '\n'); - var nodes = _parseMarkdownSource(text); - nodes = _rewriteRelativeUrls( - nodes, + final nodes = _parseMarkdownSource(text); + final rawHtml = m.renderToHtml(nodes); + final processedHtml = _postProcessHtml( + rawHtml, urlResolverFn: urlResolverFn, relativeFrom: relativeFrom, + isChangelog: isChangelog, + disableHashIds: disableHashIds, ); return _renderSafeHtml( - nodes, + processedHtml, isChangelog: isChangelog, disableHashIds: disableHashIds, ); @@ -82,31 +86,13 @@ List _parseMarkdownSource(String source) { return document.parseLines(lines); } -/// Rewrites relative URLs, re-basing them on [relativeFrom]. -List _rewriteRelativeUrls( - List nodes, { - required UrlResolverFn? urlResolverFn, - required String? relativeFrom, -}) { - final urlRewriter = _RelativeUrlRewriter(urlResolverFn, relativeFrom); - nodes.forEach((node) => node.accept(urlRewriter)); - return nodes; -} - /// Renders sanitized, safe HTML from markdown nodes. /// Adds hash link HTML to header blocks. String _renderSafeHtml( - List nodes, { + String processedHtml, { required bool isChangelog, required bool disableHashIds, }) { - final rawHtml = m.renderToHtml(nodes); - final processedHtml = _postProcessHtml( - rawHtml, - isChangelog: isChangelog, - disableHashIds: disableHashIds, - ); - // Renders the sanitized HTML. final html = sanitizeHtml( processedHtml, @@ -130,11 +116,15 @@ String _renderSafeHtml( String _postProcessHtml( String rawHtml, { + required UrlResolverFn? urlResolverFn, + required String? relativeFrom, required bool isChangelog, required bool disableHashIds, }) { var root = html_parser.parseFragment(rawHtml); + _RelativeUrlRewriter(urlResolverFn, relativeFrom).visit(root); + if (isChangelog) { final oldNodes = [...root.nodes]; root = html.DocumentFragment(); @@ -213,52 +203,63 @@ class _UnsafeUrlFilter extends html_parsing.TreeVisitor { } /// Rewrites relative URLs with the provided [urlResolverFn]. -class _RelativeUrlRewriter implements m.NodeVisitor { +class _RelativeUrlRewriter extends html_parsing.TreeVisitor { final UrlResolverFn? urlResolverFn; final String? relativeFrom; - final _elementsToRemove = {}; + final _elementsToRemove = []; _RelativeUrlRewriter(this.urlResolverFn, this.relativeFrom); @override - void visitText(m.Text text) {} + void visitDocumentFragment(html.DocumentFragment root) { + super.visitDocumentFragment(root); + _removeChildren(root); + } @override - bool visitElementBefore(m.Element element) => true; + void visitElement(html.Element element) { + super.visitElement(element); - @override - void visitElementAfter(m.Element element) { // check current element - if (element.tag == 'a') { + if (element.localName == 'a') { _updateUrlAttributes(element, 'href'); - } else if (element.tag == 'img') { + } else if (element.localName == 'img') { _updateUrlAttributes(element, 'src', raw: true); } - // remove children that are marked to be removed - if (element.children != null && - element.children!.isNotEmpty && - _elementsToRemove.isNotEmpty) { - for (final r in _elementsToRemove.toList()) { - final index = element.children!.indexOf(r); - if (index == -1) continue; - - if (r.children != null && r.children!.isNotEmpty) { - element.children!.insertAll(index, r.children!); - } else if (r.tag == 'img' && r.attributes.containsKey('alt')) { - element.children!.insert(index, m.Text('[${r.attributes['alt']}]')); - } - element.children!.remove(r); - _elementsToRemove.remove(r); + _removeChildren(element); + } + + void _removeChildren(html.Node parent) { + for (var i = _elementsToRemove.length - 1; i >= 0; i--) { + final r = _elementsToRemove[i]; + if (r.parentNode != parent) continue; + _elementsToRemove.removeAt(i); + + if (r.localName == 'img') { + final alt = r.attributes['alt']?.trim(); + final src = r.attributes['src']?.trim(); + final text = alt ?? src ?? r.text.trim(); + r.replaceWith(html.Text('[$text]')); + continue; + } + + final index = parent.nodes.indexOf(r); + parent.nodes.removeAt(index); + + for (var j = r.nodes.length - 1; j >= 0; j--) { + final c = r.nodes.removeLast(); + parent.nodes.insert(index, c); } } } - void _updateUrlAttributes(m.Element element, String attrName, + void _updateUrlAttributes(html.Element element, String attrName, {bool raw = false}) { - final newUrl = _rewriteUrl(element.attributes[attrName], raw: raw); - if (newUrl != null) { - element.attributes[attrName] = newUrl; - } else { + final oldUrl = element.attributes[attrName]; + final newUrl = _rewriteUrl(oldUrl, raw: raw); + if (newUrl == null) { _elementsToRemove.add(element); + } else if (newUrl != oldUrl) { + element.attributes[attrName] = newUrl; } } @@ -271,6 +272,15 @@ class _RelativeUrlRewriter implements m.NodeVisitor { if (url == null || url.startsWith('#')) { return url; } + + // pass-through for score tab + // TODO: consider alternative template generation for score tab + if (url == staticUrls.reportOKIconGreen || + url == staticUrls.reportMissingIconYellow || + url == staticUrls.reportMissingIconRed) { + return url; + } + // reject unparseable URLs final uri = Uri.tryParse(url); if (uri == null) { diff --git a/app/test/shared/markdown_test.dart b/app/test/shared/markdown_test.dart index 8d3aa91c5f..9052860fd8 100644 --- a/app/test/shared/markdown_test.dart +++ b/app/test/shared/markdown_test.dart @@ -112,7 +112,7 @@ void main() { '(https://flutter.dev/docs/development/packages-and-plugins/favorites)', ), '

' - '

\n', + '[../../../assets/flutter-favorite-badge.png]

\n', ); expect( markdownToHtml( @@ -121,7 +121,7 @@ void main() { urlResolverFn: urlResolverFn, ), '

' - '

\n', + '

\n', ); });