Skip to content

Commit 03794c4

Browse files
committed
Migrate URL rewrite markdown post-processing to HTML-based nodes.
1 parent e372213 commit 03794c4

File tree

2 files changed

+63
-53
lines changed

2 files changed

+63
-53
lines changed

app/lib/shared/markdown.dart

Lines changed: 61 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:html/dom_parsing.dart' as html_parsing;
77
import 'package:html/parser.dart' as html_parser;
88
import 'package:logging/logging.dart';
99
import 'package:markdown/markdown.dart' as m;
10+
import 'package:pub_dev/frontend/static_files.dart';
1011
import 'package:pub_semver/pub_semver.dart';
1112
import '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.
9891
String _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

131117
String _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) {

app/test/shared/markdown_test.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ void main() {
112112
'(https://flutter.dev/docs/development/packages-and-plugins/favorites)',
113113
),
114114
'<p><a href="https://flutter.dev/docs/development/packages-and-plugins/favorites">'
115-
'<img src="../../../assets/flutter-favorite-badge.png" width="100"></a></p>\n',
115+
'[../../../assets/flutter-favorite-badge.png]</a></p>\n',
116116
);
117117
expect(
118118
markdownToHtml(
@@ -121,7 +121,7 @@ void main() {
121121
urlResolverFn: urlResolverFn,
122122
),
123123
'<p><a href="https://flutter.dev/docs/development/packages-and-plugins/favorites">'
124-
'<img src="../../../assets/flutter-favorite-badge.png" width="100"></a></p>\n',
124+
'<img src="https://github.com/example/project/raw/master/assets/flutter-favorite-badge.png" width="100"></a></p>\n',
125125
);
126126
});
127127

0 commit comments

Comments
 (0)