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;
58import 'package:logging/logging.dart' ;
69import 'package:markdown/markdown.dart' as m;
710import '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.
133144class _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
0 commit comments