Repository: dart-lang/pub-dev (dart)

Issue:
Checkmarks missing from markdown rendering
<!-- File issues with the `pub` client at
https://github.com/dart-lang/pub/issues/new' -->

The current markdown rendering does not include checkmarks like GitHub does, just like this:

- [ ] unfinished
- [x] finished

This causes packages using this syntax to display a simple bullet point list, which can lead to confusion, like in this example:  
![image](https://github.com/user-attachments/assets/550306bd-6e90-4c08-8cfe-d8da37e2502e)
_How it's displayed on GitHub_

![image](https://github.com/user-attachments/assets/2910cdc1-da92-4b2f-9537-5928d4b79b7e)
_How it's displayed on pub.dev_

In this case, a user might assume that a package has a certain feature implemented.

**Edit:**
source package: https://pub.dev/packages/osrm
GitHub link to markdown file: https://github.com/physia/kflutter/blob/main/osrm/README.md

Pull request:
Fixes #8039.

Patch:
diff --git a/app/lib/shared/markdown.dart b/app/lib/shared/markdown.dart
index 1b6d738779..a63ef6c19b 100644
--- a/app/lib/shared/markdown.dart
+++ b/app/lib/shared/markdown.dart
@@ -97,6 +97,8 @@ String _renderSafeHtml(
 }) {
   // Filter unsafe urls on some of the elements.
   nodes.forEach((node) => node.accept(_UnsafeUrlFilter()));
+  // Transform GitHub task lists.
+  nodes.forEach((node) => node.accept(_TaskListRewriteNodeVisitor()));
 
   if (!disableHashIds) {
     // add hash link HTML to header blocks
@@ -277,6 +279,38 @@ class _RelativeUrlRewriter implements m.NodeVisitor {
   }
 }
 
+/// HTML sanitization will remove the rendered `<input type="checkbox">` elements,
+/// we are replacing them with icons.
+class _TaskListRewriteNodeVisitor implements m.NodeVisitor {
+  @override
+  void visitElementAfter(m.Element element) {
+    if (element.tag != 'li') {
+      return;
+    }
+    if (!(element.attributes['class']?.contains('task-list-item') ?? false)) {
+      return;
+    }
+    final children = element.children;
+    if (children == null || children.isEmpty) {
+      return;
+    }
+    final first = children.first;
+    if (first is m.Element &&
+        first.tag == 'input' &&
+        first.attributes['type'] == 'checkbox') {
+      final checked = first.attributes['checked'] == 'true';
+      children.removeAt(0);
+      children.insert(0, m.Text(checked ? '✅ ' : '❌ '));
+    }
+  }
+
+  @override
+  bool visitElementBefore(m.Element element) => true;
+
+  @override
+  void visitText(m.Text text) {}
+}
+
 bool _isAbsolute(String url) => url.contains(':');
 
 String _rewriteAbsoluteUrl(String url) {


Test patch:
diff --git a/app/test/shared/markdown_test.dart b/app/test/shared/markdown_test.dart
index 2d63aaae77..7031d21845 100644
--- a/app/test/shared/markdown_test.dart
+++ b/app/test/shared/markdown_test.dart
@@ -15,6 +15,17 @@ void main() {
       expect(markdownToHtml('# ABC def'),
           '<h1 class="hash-header" id="abc-def">ABC def <a href="#abc-def" class="hash-link">#</a></h1>\n');
     });
+
+    test('task list', () {
+      expect(
+        markdownToHtml('- [ ] a\n- [X] b\n- [ ] c\n'),
+        '<ul>\n'
+        '<li>❌ a</li>\n'
+        '<li>✅ b</li>\n'
+        '<li>❌ c</li>\n'
+        '</ul>\n',
+      );
+    });
   });
 
   group('Valid custom base URL', () {