@@ -7,6 +7,7 @@ import 'package:html/dom_parsing.dart' as html_parsing;
77import 'package:html/parser.dart' as html_parser;
88import 'package:logging/logging.dart' ;
99import 'package:markdown/markdown.dart' as m;
10+ import 'package:pub_dev/frontend/static_files.dart' ;
1011import 'package:pub_semver/pub_semver.dart' ;
1112import 'package:sanitize_html/sanitize_html.dart' ;
1213
@@ -46,14 +47,17 @@ String markdownToHtml(
4647 final sw = Stopwatch ()..start ();
4748 try {
4849 text = text.replaceAll ('\r\n ' , '\n ' );
49- var nodes = _parseMarkdownSource (text);
50- nodes = _rewriteRelativeUrls (
51- nodes,
50+ final nodes = _parseMarkdownSource (text);
51+ final rawHtml = m.renderToHtml (nodes);
52+ final processedHtml = _postProcessHtml (
53+ rawHtml,
5254 urlResolverFn: urlResolverFn,
5355 relativeFrom: relativeFrom,
56+ isChangelog: isChangelog,
57+ disableHashIds: disableHashIds,
5458 );
5559 return _renderSafeHtml (
56- nodes ,
60+ processedHtml ,
5761 isChangelog: isChangelog,
5862 disableHashIds: disableHashIds,
5963 );
@@ -82,31 +86,13 @@ List<m.Node> _parseMarkdownSource(String source) {
8286 return document.parseLines (lines);
8387}
8488
85- /// Rewrites relative URLs, re-basing them on [relativeFrom] .
86- List <m.Node > _rewriteRelativeUrls (
87- List <m.Node > nodes, {
88- required UrlResolverFn ? urlResolverFn,
89- required String ? relativeFrom,
90- }) {
91- final urlRewriter = _RelativeUrlRewriter (urlResolverFn, relativeFrom);
92- nodes.forEach ((node) => node.accept (urlRewriter));
93- return nodes;
94- }
95-
9689/// Renders sanitized, safe HTML from markdown nodes.
9790/// Adds hash link HTML to header blocks.
9891String _renderSafeHtml (
99- List <m. Node > nodes , {
92+ String processedHtml , {
10093 required bool isChangelog,
10194 required bool disableHashIds,
10295}) {
103- final rawHtml = m.renderToHtml (nodes);
104- final processedHtml = _postProcessHtml (
105- rawHtml,
106- isChangelog: isChangelog,
107- disableHashIds: disableHashIds,
108- );
109-
11096 // Renders the sanitized HTML.
11197 final html = sanitizeHtml (
11298 processedHtml,
@@ -130,11 +116,15 @@ String _renderSafeHtml(
130116
131117String _postProcessHtml (
132118 String rawHtml, {
119+ required UrlResolverFn ? urlResolverFn,
120+ required String ? relativeFrom,
133121 required bool isChangelog,
134122 required bool disableHashIds,
135123}) {
136124 var root = html_parser.parseFragment (rawHtml);
137125
126+ _RelativeUrlRewriter (urlResolverFn, relativeFrom).visit (root);
127+
138128 if (isChangelog) {
139129 final oldNodes = [...root.nodes];
140130 root = html.DocumentFragment ();
@@ -213,52 +203,63 @@ class _UnsafeUrlFilter extends html_parsing.TreeVisitor {
213203}
214204
215205/// Rewrites relative URLs with the provided [urlResolverFn] .
216- class _RelativeUrlRewriter implements m. NodeVisitor {
206+ class _RelativeUrlRewriter extends html_parsing. TreeVisitor {
217207 final UrlResolverFn ? urlResolverFn;
218208 final String ? relativeFrom;
219- final _elementsToRemove = < m .Element > {} ;
209+ final _elementsToRemove = < html .Element > [] ;
220210 _RelativeUrlRewriter (this .urlResolverFn, this .relativeFrom);
221211
222212 @override
223- void visitText (m.Text text) {}
213+ void visitDocumentFragment (html.DocumentFragment root) {
214+ super .visitDocumentFragment (root);
215+ _removeChildren (root);
216+ }
224217
225218 @override
226- bool visitElementBefore (m.Element element) => true ;
219+ void visitElement (html.Element element) {
220+ super .visitElement (element);
227221
228- @override
229- void visitElementAfter (m.Element element) {
230222 // check current element
231- if (element.tag == 'a' ) {
223+ if (element.localName == 'a' ) {
232224 _updateUrlAttributes (element, 'href' );
233- } else if (element.tag == 'img' ) {
225+ } else if (element.localName == 'img' ) {
234226 _updateUrlAttributes (element, 'src' , raw: true );
235227 }
236- // remove children that are marked to be removed
237- if (element.children != null &&
238- element.children! .isNotEmpty &&
239- _elementsToRemove.isNotEmpty) {
240- for (final r in _elementsToRemove.toList ()) {
241- final index = element.children! .indexOf (r);
242- if (index == - 1 ) continue ;
243-
244- if (r.children != null && r.children! .isNotEmpty) {
245- element.children! .insertAll (index, r.children! );
246- } else if (r.tag == 'img' && r.attributes.containsKey ('alt' )) {
247- element.children! .insert (index, m.Text ('[${r .attributes ['alt' ]}]' ));
248- }
249- element.children! .remove (r);
250- _elementsToRemove.remove (r);
228+ _removeChildren (element);
229+ }
230+
231+ void _removeChildren (html.Node parent) {
232+ for (var i = _elementsToRemove.length - 1 ; i >= 0 ; i-- ) {
233+ final r = _elementsToRemove[i];
234+ if (r.parentNode != parent) continue ;
235+ _elementsToRemove.removeAt (i);
236+
237+ if (r.localName == 'img' ) {
238+ final alt = r.attributes['alt' ]? .trim ();
239+ final src = r.attributes['src' ]? .trim ();
240+ final text = alt ?? src ?? r.text.trim ();
241+ r.replaceWith (html.Text ('[$text ]' ));
242+ continue ;
243+ }
244+
245+ final index = parent.nodes.indexOf (r);
246+ parent.nodes.removeAt (index);
247+
248+ for (var j = r.nodes.length - 1 ; j >= 0 ; j-- ) {
249+ final c = r.nodes.removeLast ();
250+ parent.nodes.insert (index, c);
251251 }
252252 }
253253 }
254254
255- void _updateUrlAttributes (m .Element element, String attrName,
255+ void _updateUrlAttributes (html .Element element, String attrName,
256256 {bool raw = false }) {
257- final newUrl = _rewriteUrl (element.attributes[attrName], raw: raw);
258- if (newUrl != null ) {
259- element.attributes[attrName] = newUrl;
260- } else {
257+ final oldUrl = element.attributes[attrName];
258+ final newUrl = _rewriteUrl (oldUrl, raw: raw);
259+ if (newUrl == null ) {
261260 _elementsToRemove.add (element);
261+ } else if (newUrl != oldUrl) {
262+ element.attributes[attrName] = newUrl;
262263 }
263264 }
264265
@@ -271,6 +272,15 @@ class _RelativeUrlRewriter implements m.NodeVisitor {
271272 if (url == null || url.startsWith ('#' )) {
272273 return url;
273274 }
275+
276+ // pass-through for score tab
277+ // TODO: consider alternative template generation for score tab
278+ if (url == staticUrls.reportOKIconGreen ||
279+ url == staticUrls.reportMissingIconYellow ||
280+ url == staticUrls.reportMissingIconRed) {
281+ return url;
282+ }
283+
274284 // reject unparseable URLs
275285 final uri = Uri .tryParse (url);
276286 if (uri == null ) {
0 commit comments