From 860e51e571715a8a55ced74999a2f3f2e377e394 Mon Sep 17 00:00:00 2001
From: Piotr Zalewa
Date: Sun, 8 Feb 2026 22:05:32 +0100
Subject: [PATCH 1/2] Add GFM task list checkbox rendering
Co-Authored-By: Claude Opus 4.6
---
MarkdownViewer/Markdown/MarkdownRenderer.swift | 10 ++++++++--
MarkdownViewer/Resources/markdown.css | 11 +++++++++++
2 files changed, 19 insertions(+), 2 deletions(-)
diff --git a/MarkdownViewer/Markdown/MarkdownRenderer.swift b/MarkdownViewer/Markdown/MarkdownRenderer.swift
index efd7a09..2456bba 100644
--- a/MarkdownViewer/Markdown/MarkdownRenderer.swift
+++ b/MarkdownViewer/Markdown/MarkdownRenderer.swift
@@ -108,7 +108,8 @@ struct MarkdownRenderer: MarkupWalker {
let alt = image.plainText
result += ""
case let list as UnorderedList:
- result += "
\n"
+ let hasCheckboxes = list.children.contains { ($0 as? ListItem)?.checkbox != nil }
+ result += hasCheckboxes ? "
\n" : "
\n"
for child in list.children { visit(child) }
result += "
\n"
case let list as OrderedList:
@@ -116,7 +117,12 @@ struct MarkdownRenderer: MarkupWalker {
for child in list.children { visit(child) }
result += "\n"
case let item as ListItem:
- result += "
"
+ if let checkbox = item.checkbox {
+ let checked = checkbox == .checked ? " checked" : ""
+ result += "
"
+ } else {
+ result += "
"
+ }
for child in item.children { visit(child) }
result += "
\n"
case let quote as BlockQuote:
diff --git a/MarkdownViewer/Resources/markdown.css b/MarkdownViewer/Resources/markdown.css
index bc2300b..625873a 100644
--- a/MarkdownViewer/Resources/markdown.css
+++ b/MarkdownViewer/Resources/markdown.css
@@ -171,6 +171,17 @@ em { font-style: italic; }
color: var(--color-fg-default);
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
}
+ul.task-list {
+ list-style: none;
+ padding-left: 1.5em;
+}
+.task-list-item input[type="checkbox"] {
+ margin: 0 0.35em 0 -1.3em;
+ vertical-align: middle;
+}
+.task-list-item p {
+ display: inline;
+}
.mermaid {
margin-top: 0;
margin-bottom: 16px;
From 1a49a4d45512727f8bc7b5a0250b51b48bed7a80 Mon Sep 17 00:00:00 2001
From: Piotr Zalewa
Date: Tue, 3 Mar 2026 19:58:43 +0100
Subject: [PATCH 2/2] list checkboxes
---
.../Markdown/MarkdownRenderer.swift | 35 +++++++++++++++++--
MarkdownViewer/Resources/markdown.css | 4 +++
2 files changed, 37 insertions(+), 2 deletions(-)
diff --git a/MarkdownViewer/Markdown/MarkdownRenderer.swift b/MarkdownViewer/Markdown/MarkdownRenderer.swift
index 2456bba..4f59ab2 100644
--- a/MarkdownViewer/Markdown/MarkdownRenderer.swift
+++ b/MarkdownViewer/Markdown/MarkdownRenderer.swift
@@ -59,6 +59,7 @@ struct MarkdownRenderer: MarkupWalker {
var result = ""
var outline: [OutlineItem] = []
var slugger = HeadingSlugger()
+ private var stripCancelledPrefix = false
mutating func render(_ document: Document) -> RenderedMarkdown {
result = ""
@@ -86,7 +87,19 @@ struct MarkdownRenderer: MarkupWalker {
for child in paragraph.children { visit(child) }
result += "
\n"
case let text as Markdown.Text:
- result += escapeHTML(text.string)
+ if stripCancelledPrefix {
+ stripCancelledPrefix = false
+ var str = text.string
+ for prefix in ["[-] ", "[~] "] {
+ if str.hasPrefix(prefix) {
+ str = String(str.dropFirst(prefix.count))
+ break
+ }
+ }
+ result += escapeHTML(str)
+ } else {
+ result += escapeHTML(text.string)
+ }
case let emphasis as Emphasis:
result += ""
for child in emphasis.children { visit(child) }
@@ -108,7 +121,9 @@ struct MarkdownRenderer: MarkupWalker {
let alt = image.plainText
result += ""
case let list as UnorderedList:
- let hasCheckboxes = list.children.contains { ($0 as? ListItem)?.checkbox != nil }
+ let hasCheckboxes = list.children.contains {
+ ($0 as? ListItem)?.checkbox != nil || isCancelledCheckbox($0 as? ListItem)
+ }
result += hasCheckboxes ? "
\n" : "
\n"
for child in list.children { visit(child) }
result += "
\n"
@@ -120,6 +135,9 @@ struct MarkdownRenderer: MarkupWalker {
if let checkbox = item.checkbox {
let checked = checkbox == .checked ? " checked" : ""
result += "
"
+ } else if isCancelledCheckbox(item) {
+ result += "