Skip to content

Commit 487cf60

Browse files
authored
Migrate some markdown-based post-processing into a separate HTML-based method. (#8593)
1 parent 73d06a9 commit 487cf60

File tree

2 files changed

+56
-33
lines changed

2 files changed

+56
-33
lines changed

app/lib/shared/markdown.dart

Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'package:html/dom.dart' as html;
6+
import 'package:html/dom_parsing.dart' as html_parsing;
7+
import 'package:html/parser.dart' as html_parser;
58
import 'package:logging/logging.dart';
69
import 'package:markdown/markdown.dart' as m;
710
import 'package:pub_semver/pub_semver.dart';
@@ -95,22 +98,18 @@ String _renderSafeHtml(
9598
List<m.Node> nodes, {
9699
required bool disableHashIds,
97100
}) {
98-
// Filter unsafe urls on some of the elements.
99-
nodes.forEach((node) => node.accept(_UnsafeUrlFilter()));
100-
// Transform GitHub task lists.
101-
nodes.forEach((node) => node.accept(_TaskListRewriteNodeVisitor()));
102-
103101
if (!disableHashIds) {
104102
// add hash link HTML to header blocks
105103
final hashLink = _HashLink();
106104
nodes.forEach((node) => node.accept(hashLink));
107105
}
108106

109107
final rawHtml = m.renderToHtml(nodes);
108+
final processedHtml = _postProcessHtml(rawHtml);
110109

111110
// Renders the sanitized HTML.
112111
final html = sanitizeHtml(
113-
rawHtml,
112+
processedHtml,
114113
allowElementId: (String id) =>
115114
!disableHashIds, // TODO: Use a denylist for ids used by pub site
116115
allowClassName: (String cn) {
@@ -129,6 +128,18 @@ String _renderSafeHtml(
129128
return '$html\n';
130129
}
131130

131+
String _postProcessHtml(String rawHtml) {
132+
final root = html_parser.parseFragment(rawHtml);
133+
134+
// Filter unsafe urls on some of the elements.
135+
_UnsafeUrlFilter().visit(root);
136+
137+
// Transform GitHub task lists.
138+
_TaskListRewriteTreeVisitor().visit(root);
139+
140+
return root.outerHtml;
141+
}
142+
132143
/// Adds an extra <a href="#hash">#</a> element to h1, h2 and h3 elements.
133144
class _HashLink implements m.NodeVisitor {
134145
@override
@@ -159,24 +170,20 @@ class _HashLink implements m.NodeVisitor {
159170
}
160171

161172
/// Filters unsafe URLs from the generated HTML.
162-
class _UnsafeUrlFilter implements m.NodeVisitor {
173+
class _UnsafeUrlFilter extends html_parsing.TreeVisitor {
163174
@override
164-
void visitText(m.Text text) {}
175+
void visitElement(html.Element element) {
176+
super.visitElement(element);
165177

166-
@override
167-
bool visitElementBefore(m.Element element) {
168178
final isUnsafe =
169179
_isUnsafe(element, 'a', 'href') || _isUnsafe(element, 'img', 'src');
170-
return !isUnsafe;
171-
}
172-
173-
@override
174-
void visitElementAfter(m.Element element) {
175-
// no-op
180+
if (isUnsafe) {
181+
element.replaceWith(html.Text(element.text));
182+
}
176183
}
177184

178-
bool _isUnsafe(m.Element element, String tag, String attr) {
179-
if (element.tag != tag) {
185+
bool _isUnsafe(html.Element element, String tag, String attr) {
186+
if (element.localName != tag) {
180187
return false;
181188
}
182189
final url = element.attributes[attr];
@@ -282,34 +289,30 @@ class _RelativeUrlRewriter implements m.NodeVisitor {
282289

283290
/// HTML sanitization will remove the rendered `<input type="checkbox">` elements,
284291
/// we are replacing them with icons.
285-
class _TaskListRewriteNodeVisitor implements m.NodeVisitor {
292+
class _TaskListRewriteTreeVisitor extends html_parsing.TreeVisitor {
286293
@override
287-
void visitElementAfter(m.Element element) {
288-
if (element.tag != 'li') {
294+
void visitElement(html.Element element) {
295+
super.visitElement(element);
296+
297+
if (element.localName != 'li') {
289298
return;
290299
}
291300
if (!(element.attributes['class']?.contains('task-list-item') ?? false)) {
292301
return;
293302
}
294-
final children = element.children;
295-
if (children == null || children.isEmpty) {
303+
final children = element.nodes;
304+
if (children.isEmpty) {
296305
return;
297306
}
298307
final first = children.first;
299-
if (first is m.Element &&
300-
first.tag == 'input' &&
308+
if (first is html.Element &&
309+
first.localName == 'input' &&
301310
first.attributes['type'] == 'checkbox') {
302311
final checked = first.attributes['checked'] == 'true';
303312
children.removeAt(0);
304-
children.insert(0, m.Text(checked ? '✅ ' : '❌ '));
313+
children.insert(0, html.Text(checked ? '✅ ' : '❌ '));
305314
}
306315
}
307-
308-
@override
309-
bool visitElementBefore(m.Element element) => true;
310-
311-
@override
312-
void visitText(m.Text text) {}
313316
}
314317

315318
/// Group corresponding changelog nodes together, if it matches the following

app/test/shared/markdown_test.dart

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,26 @@ void main() {
105105
'<p><img src="https://github.com/example/project/raw/master/example/image.png" alt="text"></p>\n');
106106
});
107107

108+
test('relative image using html tag', () {
109+
expect(
110+
markdownToHtml(
111+
'[<img src="../../../assets/flutter-favorite-badge.png" width="100" />]'
112+
'(https://flutter.dev/docs/development/packages-and-plugins/favorites)',
113+
),
114+
'<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',
116+
);
117+
expect(
118+
markdownToHtml(
119+
'[<img src="../../../assets/flutter-favorite-badge.png" width="100" />]'
120+
'(https://flutter.dev/docs/development/packages-and-plugins/favorites)',
121+
urlResolverFn: urlResolverFn,
122+
),
123+
'<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',
125+
);
126+
});
127+
108128
test('root link within site', () {
109129
expect(markdownToHtml('[text](/README.md)'), '<p>text</p>\n');
110130
expect(
@@ -133,7 +153,7 @@ void main() {
133153

134154
group('Unsafe markdown', () {
135155
test('javascript link', () {
136-
expect(markdownToHtml('[a](javascript:alert("x"))'), '<p><a>a</a></p>\n');
156+
expect(markdownToHtml('[a](javascript:alert("x"))'), '<p>a</p>\n');
137157
});
138158
});
139159

0 commit comments

Comments
 (0)