Skip to content

A document read as a document, and boxes you can tick - #21

Merged
necessarylion merged 2 commits into
mainfrom
markdown-block-tree
Jul 30, 2026
Merged

necessarylion merged 2 commits into
mainfrom
markdown-block-tree

Conversation

@necessarylion

@necessarylion necessarylion commented Jul 30, 2026 •

Copy link
Copy Markdown
Owner

Every gap left in the Markdown preview was the same gap. The parser was
hand-rolled and line-by-line — each source line classified on its own — so a
paragraph hard-wrapped at 80 columns became one block per line and was drawn as
a ladder, a continuation line under a bullet lost its bullet, Title over
===== came out as a paragraph and a rule, and a fence inside a list item was
hoisted out of it. Sixteen lines in TODO.md, one missing thing: a block tree.

So the parser is replaced and the renderer is kept.
swift-markdown is a parser and
nothing else — an immutable tree over swift-cmark, the same cmark-gfm both hosts
render a comment with — so the app now reads a document the way the host that
served it does. Support/MarkdownParser.swift is the whole of the walk from its
nodes to ours; Docs/Markdown.md records why a library that renders too was the
wrong trade.

The tree answers, for free: hard-wrapped paragraphs, multi-line list items,
nested and ordered checklists, setext headings, every spelling of a horizontal
rule (___, ----, - - -), indented code blocks, reference-style links,
table alignment, and a fence or a sub-list inside a list item.

Three things it does not answer, and each has its place:

Bare URLs, :tada:, #123 and @name are not Markdown at all. They are a
scan over the Text nodes, which makes them one piece of work rather than four
— Support/MarkdownInline.swift. It does the escaping in the same pass, and
that is why the two live together: the blocks carry Markdown strings, which
the views read back with AttributedString(markdown:), so a * the author
escaped has to be written back escaped or it comes out as emphasis the second
time round.

HTML arrives raw and flat. cmark splits <details> into three siblings — the
open tag, the inner Markdown parsed properly, the close tag — so there is
nothing to recurse into. MarkdownHTMLText reads one raw block into events and
ContainerStack keeps the stack. Loose HTML is handed back as Markdown and
re-read as a document of its own, which is what finally makes a README's
<p align="center"><img …></p> a picture. A <table> arrives whole in one
block, so the plain shape is read into a real table.

Footnotes are worse than missing. A definition whose note is one word is a
valid link reference definition, so cmark eats the line and turns [^1] into a
link pointing at the note's words. Both halves are caught: a definition left as
a paragraph is lifted to the end of the document, and a mangled reference is
drawn as the marker it was.

Comments are dropped from the tree, not from the text. strippingComments
cut whole lines out before parsing, which moved every line number after them —
and a source range is the one thing a tickable checkbox depends on. A tick is a
write at the item's own line: through OpenDocument for a .md file, where it
lands as an unsaved edit like any other, and through the comment and description
edit paths for a pull request, from what the host holds rather than from what is
drawn.

The rest of the list with it: table alignment now reaches the cell, a fenced
block has a copy button, a picture opens at full size instead of being squinted
at under its 420pt cap, and a long document has an outline.

Scrolling

A pull request draws its comments in a LazyVStack, so a comment scrolled out
of sight is thrown away and built again on the way back — which is why the cost
of one comment shows up as a stutter rather than as load time. Measured, not
guessed:

Every paragraph was laid out twice. CodeChipBackdrop puts a second copy of
the text behind the first so a code span can have a rounded fill, and it went
behind every line in the document. Most prose has no span at all, and that half
of the work was never visible. Now it is attached only where there is a chip to
draw. This one predates the branch.

A name on every block. The outline needs a heading to answer to a name; it
was putting one on all of them, in every MarkdownText on the page — the same
handful of names over and over. An explicit .id is structural identity, so
SwiftUI tore views down and built them again as the indices moved.

A closure rebuilt every pass. A closure is never equal to another closure, so
an action written into the environment on each draw read as a change to
everything beneath it, and defeated the check that lets an unchanged comment be
skipped whole. MarkdownTaskToggle is equal by what it writes to, and is not
set at all for a comment nobody here may edit — which is most of them.

Hover on a fence rebuilt the fence. Scrolling drags the document under a
pointer that never moved, so every block it crosses reports a hover. The state
now sits in the overlay, where it rebuilds one button.

Plus: an item of one paragraph is drawn as the line it is rather than as a
nested MarkdownText, and the caches are sized for a page rather than for a
view — 2500 lines and 500 documents, against 600 and 200.

Also here

Three changes that were already in the tree and are not part of the above:
marksman joins the language server catalogue; EditorThemeBridge gains a note
on why a .md file paints flat in the editor and what fixing it would cost; and
a diff now folds away the files it deletes, which is the one file nobody reads.

Built and run. The parser was checked against the whole of Docs/, README.md
and TODO.md, and against the shapes a review bot writes — nested
<details>/<blockquote>, <kbd> chips, HTML tables, badge links, Bitbucket's
{: width=140 }.

TODO.md is rewritten in the same pass, and not only because those sixteen
lines are done: its items were written [ ] thing with no -, which is not a
list in Markdown at all. The old parser made it look like one; cmark reads it
the way GitHub does, as one paragraph. They are real list items now, so the file
renders as a checklist here and on the host, and its boxes tick.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BAgEj6aqwdPgmZEq5BapSu

Summary by CodeRabbit

  • New Features
    • Markdown previews now support richer rendering for nested lists, aligned tables, footnotes, alerts, diagrams, links, and embedded images.
    • Added a heading outline for quicker navigation within long previews.
    • Checkboxes can be toggled directly in Markdown previews, pull request descriptions, and comments.
    • Images can be opened in a full-size zoom view with enhanced actions.
    • Added Markdown language-server support with completion, navigation, and diagnostics.
  • Bug Fixes
    • Deleted files remain consistently folded when diffs refresh.
  • Documentation
    • Updated Markdown architecture, capabilities, and development guidance.

**Every gap left in the Markdown preview was the same gap.** The parser was
hand-rolled and line-by-line — each source line classified on its own — so a
paragraph hard-wrapped at 80 columns became one block per line and was drawn as
a ladder, a continuation line under a bullet lost its bullet, `Title` over
`=====` came out as a paragraph and a rule, and a fence inside a list item was
hoisted out of it. Sixteen lines in `TODO.md`, one missing thing: a block tree.

So the parser is replaced and the renderer is kept.
[swift-markdown](https://github.com/swiftlang/swift-markdown) is a parser and
nothing else — an immutable tree over swift-cmark, the same cmark-gfm both hosts
render a comment with — so the app now reads a document the way the host that
served it does. `Support/MarkdownParser.swift` is the whole of the walk from its
nodes to ours; `Docs/Markdown.md` records why a library that renders too was the
wrong trade.

The tree answers, for free: hard-wrapped paragraphs, multi-line list items,
nested and ordered checklists, setext headings, every spelling of a horizontal
rule (`___`, `----`, `- - -`), indented code blocks, reference-style links,
table alignment, and a fence or a sub-list inside a list item.

**Three things it does not answer**, and each has its place:

*Bare URLs, `:tada:`, `#123` and `@name` are not Markdown at all.* They are a
scan over the `Text` nodes, which makes them one piece of work rather than four
— `Support/MarkdownInline.swift`. It does the escaping in the same pass, and
that is why the two live together: the blocks carry Markdown *strings*, which
the views read back with `AttributedString(markdown:)`, so a `*` the author
escaped has to be written back escaped or it comes out as emphasis the second
time round.

*HTML arrives raw and flat.* cmark splits `<details>` into three siblings — the
open tag, the inner Markdown parsed properly, the close tag — so there is
nothing to recurse into. `MarkdownHTMLText` reads one raw block into events and
`ContainerStack` keeps the stack. Loose HTML is handed back as Markdown and
re-read as a document of its own, which is what finally makes a README's
`<p align="center"><img …></p>` a picture. A `<table>` arrives whole in one
block, so the plain shape is read into a real table.

*Footnotes are worse than missing.* A definition whose note is one word is a
valid link reference definition, so cmark eats the line and turns `[^1]` into a
link pointing at the note's words. Both halves are caught: a definition left as
a paragraph is lifted to the end of the document, and a mangled reference is
drawn as the marker it was.

**Comments are dropped from the tree, not from the text.** `strippingComments`
cut whole lines out before parsing, which moved every line number after them —
and a source range is the one thing a tickable checkbox depends on. A tick is a
write at the item's own line: through `OpenDocument` for a `.md` file, where it
lands as an unsaved edit like any other, and through the comment and description
edit paths for a pull request, from what the host holds rather than from what is
drawn.

The rest of the list with it: table alignment now reaches the cell, a fenced
block has a copy button, a picture opens at full size instead of being squinted
at under its 420pt cap, and a long document has an outline.

## Scrolling

A pull request draws its comments in a `LazyVStack`, so a comment scrolled out
of sight is thrown away and built again on the way back — which is why the cost
of one comment shows up as a stutter rather than as load time. Measured, not
guessed:

**Every paragraph was laid out twice.** `CodeChipBackdrop` puts a second copy of
the text behind the first so a `code` span can have a rounded fill, and it went
behind every line in the document. Most prose has no span at all, and that half
of the work was never visible. Now it is attached only where there is a chip to
draw. This one predates the branch.

**A name on every block.** The outline needs a heading to answer to a name; it
was putting one on all of them, in every `MarkdownText` on the page — the same
handful of names over and over. An explicit `.id` is structural identity, so
SwiftUI tore views down and built them again as the indices moved.

**A closure rebuilt every pass.** A closure is never equal to another closure, so
an action written into the environment on each draw read as a change to
everything beneath it, and defeated the check that lets an unchanged comment be
skipped whole. `MarkdownTaskToggle` is equal by what it writes to, and is not
set at all for a comment nobody here may edit — which is most of them.

**Hover on a fence rebuilt the fence.** Scrolling drags the document under a
pointer that never moved, so every block it crosses reports a hover. The state
now sits in the overlay, where it rebuilds one button.

Plus: an item of one paragraph is drawn as the line it is rather than as a
nested `MarkdownText`, and the caches are sized for a page rather than for a
view — 2500 lines and 500 documents, against 600 and 200.

## Also here

Three changes that were already in the tree and are not part of the above:
`marksman` joins the language server catalogue; `EditorThemeBridge` gains a note
on why a `.md` file paints flat in the editor and what fixing it would cost; and
a diff now folds away the files it deletes, which is the one file nobody reads.

Built and run. The parser was checked against the whole of `Docs/`, `README.md`
and `TODO.md`, and against the shapes a review bot writes — nested
`<details>`/`<blockquote>`, `<kbd>` chips, HTML tables, badge links, Bitbucket's
`{: width=140 }`.

`TODO.md` is rewritten in the same pass, and not only because those sixteen
lines are done: its items were written `[ ] thing` with no `-`, which is not a
list in Markdown at all. The old parser made it *look* like one; cmark reads it
the way GitHub does, as one paragraph. They are real list items now, so the file
renders as a checklist here and on the host, and its boxes tick.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAgEj6aqwdPgmZEq5BapSu
@coderabbitai

coderabbitai Bot commented Jul 30, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@necessarylion, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 42 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4bbf2bbe-7a38-4b90-b432-1d3c23118486

📥 Commits

Reviewing files that changed from the base of the PR and between 7768f49 and 34a2634.

📒 Files selected for processing (10)
  • CLAUDE.md
  • Docs/Markdown.md
  • Sources/Workspace/Editor/EditorThemeBridge.swift
  • Sources/Workspace/Support/MarkdownHTMLText.swift
  • Sources/Workspace/Support/MarkdownInline.swift
  • Sources/Workspace/Support/MarkdownParser.swift
  • Sources/Workspace/Support/RemoteImage.swift
  • Sources/Workspace/Views/MarkdownPreview.swift
  • Sources/Workspace/Views/PullRequestDetailView.swift
  • Sources/Workspace/Views/ViewerView.swift
📝 Walkthrough

Walkthrough

The change migrates Markdown parsing to swift-markdown, adds nested list and aligned table rendering, introduces inline Markdown rewriting, supports checklist persistence and heading outlines, improves image interaction, registers Marksman, updates documentation, and preserves folded deleted files across diff reloads.

Changes

Markdown migration

Layer / File(s) Summary
Parser and HTML foundation
Package.swift, Sources/Workspace/Support/MarkdownParser.swift, Sources/Workspace/Support/MarkdownHTMLText.swift
Adds swift-markdown parsing, block conversion, HTML event handling, footnotes, checkbox source editing, and nested container reconstruction.
Inline Markdown re-emission
Sources/Workspace/Support/MarkdownInline.swift
Escapes Markdown syntax and rewrites URLs, footnotes, pull-request references, mentions, and emoji shortcodes.
Nested preview rendering and HTML output
Sources/Workspace/Views/MarkdownPreview.swift, Sources/Workspace/Support/MarkdownHTML.swift
Adds nested list blocks, aligned tables, heading anchors, recursive image and diagram handling, code-chip optimization, and matching HTML rendering.
Markdown links, task editing, and image interaction
Sources/Workspace/Views/{ViewerView,PullRequestDetailView,MarkdownPreview.swift}, Sources/Workspace/Support/RemoteImage.swift, Sources/Workspace/Models/LanguageServerCatalog.swift
Threads repository links and task toggles through previews and PR content, adds a zoomable image sheet, and registers Marksman.
Markdown migration documentation
CLAUDE.md, Docs/Markdown.md, Docs/Development.md, README.md, TODO.md, Sources/Workspace/Editor/EditorThemeBridge.swift
Documents the parser migration, supported behavior, language-server setup, editor highlighting, and remaining Markdown limitations.

Diff deletion folding

Layer / File(s) Summary
Persist deleted-file folding
Sources/Workspace/Views/DiffView.swift
Tracks deleted files folded during diff reloads and applies the tracked state during rebuilds.

Estimated code review effort: 5 (Critical) | ~90 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ViewerView
  participant MarkdownPreview
  participant MarkdownTask
  participant DocumentStore
  ViewerView->>MarkdownPreview: provide task toggle callback
  MarkdownPreview->>MarkdownTask: toggle checkbox on source line
  MarkdownTask-->>ViewerView: return updated Markdown
  ViewerView->>DocumentStore: persist updated text
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title hints at Markdown parsing and checkboxes, but it is poetic and not specific enough to scan as a clear change summary. Use a concise, specific title that names the main change, such as replacing the Markdown parser and adding checkbox/task toggling.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch markdown-block-tree

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (2)
Package.swift (1)

44-54: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Declare a product for direct swift-cmark use.

swift-cmark 0.8.0 exists, but it exposes cmark-gfm and cmark-gfm-extensions products rather than an implicit package product. This target already imports Markdown instead, so the direct package is not unused; to keep the grammar-version pin without relying on a future upstream product change, drop the raw declaration and add a local MarkdownSwiftCmark product/package wrapper that exposes the needed cmark-gfm* product.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Package.swift` around lines 44 - 54, Update the Package.swift dependency
declarations around swift-markdown and swift-cmark to remove the raw direct
swift-cmark package entry and add the local MarkdownSwiftCmark product/package
wrapper exposing the cmark-gfm and cmark-gfm-extensions products. Preserve the
exact 0.8.0 grammar-version pin and keep the existing Markdown dependency usage
unchanged.
Sources/Workspace/Views/DiffView.swift (1)

120-120: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid rebuilding the flattened diff twice.

foldDeletions() mutates collapsedFiles, while the revision handler immediately calls rebuild(collapsed:) and the separate collapsedFiles observer calls rebuild() again. On large diffs with new deletions, this can perform two full flattening passes. Coalesce these updates or defer the state write until after the rebuild.

Also applies to: 233-234

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Workspace/Views/DiffView.swift` at line 120, Update the change
handlers in DiffView around foldDeletions(), the diff.revision observer, and the
collapsedFiles observer so a revision update that changes collapsedFiles
triggers only one rebuild() pass. Coalesce the state mutation and rebuild, or
defer the collapsedFiles write until after the explicit rebuild, while
preserving initial rebuilding and deletion-folding behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CLAUDE.md`:
- Line 105: Update the wording in the table description so “read only” becomes
the hyphenated “read-only” when modifying “shape”; leave the rest of the
statement unchanged.

In `@Docs/Markdown.md`:
- Line 45: Update the table example in Markdown.md so each pipe character in the
columnAlignments value is escaped, preserving the row as exactly two cells and
preventing markdownlint MD056.

In `@Sources/Workspace/Editor/EditorThemeBridge.swift`:
- Around line 24-31: Revise the Markdown explanation in EditorThemeBridge.swift
to state that normal Markdown preview coloring comes through MarkdownText and
the parser/AttributedString path, while TreeSitterHighlighter applies only to
fenced code snippets. Remove the claim that TreeSitterHighlighter processes the
same full .md file, preserving the existing explanation of the unrecognised
token list.

In `@Sources/Workspace/Support/MarkdownHTMLText.swift`:
- Around line 163-202: Update table(in:) to track the header row by value rather
than its pre-filter index. When processing the first header cell, retain the
corresponding row value or otherwise identify it after empty rows are removed,
then remove that header row safely while preserving the default first-row
behavior when no header exists.

In `@Sources/Workspace/Support/MarkdownInline.swift`:
- Around line 35-43: Update the www. autolink construction in the inline parsing
branch to pass its generated destination through destination(_:) before
interpolation, while preserving the existing escaping and https:// prefix
behavior.

In `@Sources/Workspace/Support/MarkdownParser.swift`:
- Around line 242-247: Thread an isNested (or equivalent) flag through the
blocks(of:) builder and its nested HTML re-parse path, setting it for
Document(parsing: text) content. In list(_:isOrdered:start:), omit the source
line when building Markdown.List.Item while this flag is set, while preserving
line ranges for top-level document lists.

In `@Sources/Workspace/Support/RemoteImage.swift`:
- Around line 164-195: The zoom interaction in the image view’s onTapGesture is
pointer-only and lacks button semantics. Add an accessibility action that sets
isZoomed to true and mark the image with the button accessibility trait,
preserving the existing click behavior and sheet presentation.

In `@Sources/Workspace/Views/MarkdownPreview.swift`:
- Around line 857-862: Document on MarkdownTaskToggle that perform must read
mutable source data when invoked because equality compares only target and
ignores captured values. In Sources/Workspace/Views/PullRequestDetailView.swift
lines 2387-2397, update the action construction to read or re-fetch the comment
body inside the perform closure rather than capturing comment.editableBody when
constructing the action.

In `@Sources/Workspace/Views/ViewerView.swift`:
- Around line 382-399: Update the MarkdownPreview link configuration to resolve
the project containing document.url via store.project(containing:) instead of
store.selectedProject, and pass that project’s remote to MarkdownLinks. Keep the
existing preview and task-toggle behavior unchanged.

---

Nitpick comments:
In `@Package.swift`:
- Around line 44-54: Update the Package.swift dependency declarations around
swift-markdown and swift-cmark to remove the raw direct swift-cmark package
entry and add the local MarkdownSwiftCmark product/package wrapper exposing the
cmark-gfm and cmark-gfm-extensions products. Preserve the exact 0.8.0
grammar-version pin and keep the existing Markdown dependency usage unchanged.

In `@Sources/Workspace/Views/DiffView.swift`:
- Line 120: Update the change handlers in DiffView around foldDeletions(), the
diff.revision observer, and the collapsedFiles observer so a revision update
that changes collapsedFiles triggers only one rebuild() pass. Coalesce the state
mutation and rebuild, or defer the collapsedFiles write until after the explicit
rebuild, while preserving initial rebuilding and deletion-folding behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 076d4eeb-ca4c-4a1d-9d0d-2e6b95a3f851

📥 Commits

Reviewing files that changed from the base of the PR and between e29ad6e and 7768f49.

⛔ Files ignored due to path filters (1)
  • Package.resolved is excluded by !**/Package.resolved
📒 Files selected for processing (17)
  • CLAUDE.md
  • Docs/Development.md
  • Docs/Markdown.md
  • Package.swift
  • README.md
  • Sources/Workspace/Editor/EditorThemeBridge.swift
  • Sources/Workspace/Models/LanguageServerCatalog.swift
  • Sources/Workspace/Support/MarkdownHTML.swift
  • Sources/Workspace/Support/MarkdownHTMLText.swift
  • Sources/Workspace/Support/MarkdownInline.swift
  • Sources/Workspace/Support/MarkdownParser.swift
  • Sources/Workspace/Support/RemoteImage.swift
  • Sources/Workspace/Views/DiffView.swift
  • Sources/Workspace/Views/MarkdownPreview.swift
  • Sources/Workspace/Views/PullRequestDetailView.swift
  • Sources/Workspace/Views/ViewerView.swift
  • TODO.md

Comment thread CLAUDE.md Outdated
`Support/MarkdownHTMLText.swift` reads one raw block into events and
`ContainerStack` in the parser keeps the stack. It is not an HTML parser: an
unknown tag is dropped and its text kept, `<!-- … -->` goes nowhere, and
`<table>` is read only in its plain shape.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Hyphenate “read-only”.

Line 105 should use “read-only” when modifying “shape”; this is also reported by the grammar checker.

Suggested wording
-  `<table>` is read only in its plain shape.
+  `<table>` is read-only in its plain shape.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
`<table>` is read only in its plain shape.
`<table>` is read-only in its plain shape.
🧰 Tools
🪛 LanguageTool

[grammar] ~105-~105: Use a hyphen to join words.
Context: ... goes nowhere, and <table> is read only in its plain shape. - Comments ar...

(QB_NEW_EN_HYPHEN)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLAUDE.md` at line 105, Update the wording in the table description so “read
only” becomes the hyphenated “read-only” when modifying “shape”; leave the rest
of the statement unchanged.

Source: Linters/SAST tools

Comment thread Docs/Markdown.md Outdated
Comment thread Sources/Workspace/Editor/EditorThemeBridge.swift Outdated
Comment thread Sources/Workspace/Support/MarkdownHTMLText.swift
Comment thread Sources/Workspace/Support/MarkdownInline.swift
Comment thread Sources/Workspace/Support/MarkdownParser.swift
Comment thread Sources/Workspace/Support/RemoteImage.swift
Comment thread Sources/Workspace/Views/MarkdownPreview.swift
Comment thread Sources/Workspace/Views/ViewerView.swift
**An HTML table with an empty row took the app down.** `table(in:)` recorded the
header as an *index* while scanning and then dropped the empty rows out from
under it, so every row after an empty `<tr></tr>` was renumbered — and
`<tr></tr><tr><th>A</th></tr>` left one row with the header pointing at the
second. `Index out of range`, reproduced before fixing and after. The header's
place is now corrected in the same breath as the rows are dropped.

**A tick inside HTML wrote to the wrong line.** Loose HTML is re-read as a
document of its own, and cmark numbers that document from *its* first line — so
a checkbox in there named a line in the snippet and a tick would have been
written over whatever sat on that line of the real document. `depth` already
counted the re-read; the item's line is now only recorded at the top level.
A box in there is drawn and not offered, which is the right answer.

**A second tick could discard the first.** `MarkdownTaskToggle` compares on
`target` so that a rebuilt action reads as unchanged — but a comment's action
captures its body when the closure is made, so the kept closure would compute
the second tick against the words from before the first. The body is part of
equality now, which retires the stale closure and costs nothing in the ordinary
case: the same string, so `==` answers on the pointer.

Smaller, all of them real:

- A `.md` file takes its `#123` from the repository it is *in*
  (`project(containing:)`), not from whichever is selected in the sidebar.
- A bare `www.` address goes through `destination(_:)` like every other, so one
  with a bracket in it cannot break out of the link.
- The picture says it is a button and answers an accessibility action, rather
  than being openable by pointer alone.
- `Docs/Markdown.md` escapes the pipes inside `columnAlignments: \|l\|c\|r\|`,
  which were splitting that row into six cells.
- `EditorThemeBridge` no longer claims the preview colours a `.md` file through
  `TreeSitterHighlighter`; that reaches only as far as the snippet in a fence.
- `CLAUDE.md` says "only the plain shape of a `<table>` is read" rather than
  "`<table>` is read only in its plain shape" — the linter wanted "read-only",
  which means the opposite of what the sentence does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAgEj6aqwdPgmZEq5BapSu
@necessarylion

Copy link
Copy Markdown
Owner Author

All nine addressed in 34a2634. Each was checked against the code first; three were more than cosmetic.

Confirmed as a crash — MarkdownHTMLText.table(in:). Reproduced before fixing: <table><tr></tr><tr><th>A</th></tr></table> → Index out of range. The header was held as an index and the empty rows were dropped out from under it. Fixed by correcting the header's place in the same breath as the rows are dropped, rather than matching the row back by value — two identical rows would have picked the wrong one. Verified against an empty row before / in the middle of / after the header, a table with no rows at all, and the ordinary <thead> shape.

Confirmed, and it could have corrupted a document — the nested re-parse. depth already counted the loose-HTML re-read, so the item's line is now recorded only at the top level. Verified both ways: a box inside an HTML block comes back line: nil (drawn, not offered), and a real sibling list keeps its true line.

Confirmed — the target-only equality. The body is part of equality now. Worth noting it is free in the ordinary case: it is the same String instance, so == answers on the pointer without a compare.

Applied as suggested: project(containing:), destination(_:) on the www. autolink, .isButton plus an accessibility action on the picture, and the escaped pipes in Docs/Markdown.md.

One taken differently. The read only → read-only hyphen is the linter reading the sentence backwards: it means "only the plain shape is read", not "read-only" as in not writable. Rephrased to only the plain shape of a <table> is read, which says the intended thing and stops the false positive.

On the two failing pre-merge checks: the docstring coverage number counts the parser's small private helpers, most of which sit under a doc comment that explains the whole method — I would rather not add noise to reach a percentage. The title is deliberate; it follows the house style of the last several PRs here.

@necessarylion
necessarylion merged commit 8350dbf into main Jul 30, 2026
2 checks passed
necessarylion added a commit that referenced this pull request Jul 30, 2026
Main arrived as the squash of #21, whose content this branch was already
built on, so the only conflict was in the bubble: the same lines this
branch moved out of `body` and into `folding`, which is what the merge
keeps — plus the `taskToggle` the merge had written down twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant