Skip to content

Fix nested blockquote parsing, drawn quote bars, and phantom list-item spacing#7

Merged
ronaldmannak merged 9 commits into
mainfrom
claude/code-block-syntax-highlighting-g6l056
Jul 7, 2026
Merged

Fix nested blockquote parsing, drawn quote bars, and phantom list-item spacing#7
ronaldmannak merged 9 commits into
mainfrom
claude/code-block-syntax-highlighting-g6l056

Conversation

@ronaldmannak

Copy link
Copy Markdown
Contributor

Summary

Fixes three rendering inconsistencies reported against the demo app (comparing markdown-it.md and TEST.md fixtures with GitHub's rendering):

  1. Nested blockquotes rendered flat. The tokenizer consumed >, >>, and > > > as one flat prefix and appended everything to a single quote block. Now detectBlockquote counts markers and deeper lines push child blockquote blocks (the same open-stack pattern nested list items use), giving the assembler real parentID/depth. Shallower-marked lines lazily continue the deepest open quote per CommonMark.

  2. Marker-only > lines were merged into paragraphs. A > line with no content is a blank line inside the quote: it now ends the open paragraph, so > p1 / > / > p2 renders as two paragraphs, and > Back to the first level. after a nested quote returns to level 1 (the classic Markdown.pl sample in TEST.md) instead of continuing the nested quote.

  3. Phantom blank line after list items ending in styled spans. A finalized list line stores a trailing "\n" run; when the item ends in inline code/bold/link the run can't coalesce into plain text, survived sanitization, and combined with the item's own terminator into an empty paragraph. renderListItem now trims trailing newlines.

  4. Quote bars are now drawn, not typed. The old "│ " glyphs read as pipe characters, broke across wrapped lines, and were included in selection/copy. renderBlockquote now marks ranges with .picoBlockquoteLevel/.picoBlockquoteBarColor and reserves an indent gutter; the text views draw one continuous rounded bar per nesting level:

    • BlockquoteBarLayoutManager (NSLayoutManager.drawBackground override) for the iOS TextKit 1 view and both macOS views
    • BlockquoteBarTextLayoutFragment + NSTextLayoutManagerDelegate for the real TextKit 2 path on iOS 16+

    Because the attributes span block-trailing newlines, adjacent quote blocks merge into one uninterrupted bar, matching GitHub.

Tests

  • Golden tokenizer tests: nested quote depths (>>, > > >), lazy continuation, marker-only paragraph splits, outer-level return
  • Renderer tests: level attributes/indents per depth, quote block order for the Markdown.pl sample, tight styled-ending list items
  • A round-trip test guarding that the custom bar attributes survive the pipeline's AttributedStringNSAttributedString conversions

Known follow-up (out of scope here)

Block elements inside blockquotes (> ## heading, > 1. list, quoted code blocks) still render as plain quote text — that requires making blockquotes container blocks in the tokenizer and is planned as a separate PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NRUDZwowe1T836zcYLwQoo


Generated by Claude Code

claude added 3 commits July 6, 2026 18:01
…yled list items

Nested blockquotes: the tokenizer treated '>', '>>', and '> > >'
identically — it consumed the whole marker prefix as one flat count and
kept appending to the same open blockquote, so every nesting level
collapsed into a single flat quote. Now detectBlockquote counts markers
and deeper lines push child blockquote contexts (the same
open-stack pattern nested list items use), giving the assembler real
parentID/depth. Shallower-marked lines lazily continue the deepest open
quote per CommonMark; blank lines close the whole quote stack. The
renderer draws one '│ ' bar per depth level and collapses the
inter-paragraph gap between adjacent quote blocks.

List spacing: a finalized list line stores a trailing standalone
newline run. It normally coalesces into the plain text and is sanitized
away, but when an item ends in a styled span (inline code, bold, link)
the runs cannot merge, and the surviving newline plus the item's own
terminator rendered as an empty paragraph — a full blank line after
bullets like 'ending with `code`'. renderListItem now trims trailing
newlines from the body before appending its terminator.

Golden tokenizer tests for both nesting behaviors and renderer
regression tests for depth bars and tight styled-ending items.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRUDZwowe1T836zcYLwQoo
A '>' line with no content is a blank line inside a blockquote: it ends
the open paragraph. Previously it just appended a newline that was
later sanitized into spaces, so '> p1\n>\n> p2' merged into one line,
and after '> > nested' a '> back to first' line lazily continued the
nested quote instead of returning to level one (the classic Markdown.pl
sample in TEST.md).

Now finalizeLine marks the deepest quote context when it sees a
marker-only line, and the next quote line closes the open quote stack
and reopens fresh blocks at its own marker depth. Rendered result for
the TEST.md sample matches GitHub: level 1, nested level 2 (with a
level-1 connector bar line), back to level 1. Adjacent quote blocks
already collapse their spacing, so paragraph splits inside one quote
read as a single quote with paragraph gaps.

Golden tokenizer tests and a renderer shape test cover both the
paragraph split and the outer-level return.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRUDZwowe1T836zcYLwQoo
… text

The quote bar was a literal '│ ' string prepended to every quoted line:
it read as a pipe character, broke across wrapped lines, and was
included in text selection and copy.

Now renderBlockquote marks the block's whole range (including the
trailing newline) with .picoBlockquoteLevel/.picoBlockquoteBarColor and
indents the text past a leading gutter; the text views draw one rounded
vertical bar per nesting level there:

- BlockquoteBarLayoutManager (NSLayoutManager.drawBackground override)
  covers the iOS TextKit 1 view and both macOS views (the macOS
  'TextKit 2' view already runs on NSLayoutManager).
- BlockquoteBarTextLayoutFragment + NSTextLayoutManagerDelegate covers
  the real TextKit 2 path on iOS 16+.

Because the attribute spans block-trailing newlines, adjacent quote
blocks (paragraph splits, nested levels) merge into one continuous
bar — matching GitHub's rendering — and copied quote text is now clean.
Bar color stays theme-driven (blockquoteColor) and resolves dynamically
for light/dark at draw time.

Includes a round-trip test guarding that the custom attributes survive
the AttributedString conversion the pipeline performs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRUDZwowe1T836zcYLwQoo

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request adds support for nested blockquotes in PicoMarkdownView by parsing marker depths and rendering vertical bars dynamically via custom TextKit 1 and TextKit 2 layout hooks, rather than baking characters into the text. It also fixes an issue where list items ending in styled spans rendered with extra blank lines. The review feedback focuses on improving robustness and performance, including dynamically retrieving the correct text container in multi-container layouts, avoiding unnecessary rendering bounds expansion for non-blockquote paragraphs, optimizing trailing newline trimming to prevent string allocations, and simplifying boolean assignment logic.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread Sources/PicoMarkdownView/Views/BlockquoteBarDecoration.swift Outdated
Comment thread Sources/PicoMarkdownView/Views/BlockquoteBarDecoration.swift
Comment thread Sources/PicoMarkdownView/Renderer/MarkdownAttributeBuilder.swift
Comment thread Sources/PicoMarkdownView/Renderer/MarkdownAttributeBuilder.swift Outdated
claude added 2 commits July 6, 2026 22:22
CI confirmed the round-trip test failure: AttributedString's plain
conversion initializers silently drop custom NSAttributedString.Keys,
so .picoBlockquoteLevel/.picoBlockquoteBarColor never reached
NSTextStorage and no bars were drawn (the paragraph indent survived,
being a standard attribute — exactly the symptom seen on macOS).

Adds AttributeScopes.PicoMarkdownAttributes with typed keys for the two
attributes (nesting the platform + Foundation scopes so standard
attributes convert too) and pico-scoped conversion helpers, used at the
pipeline seams: renderBlockquote's NSAttributedString→AttributedString
conversion and the backend's three AttributedString→NSAttributedString
sites.

Also applies review feedback: resolve the text container per glyph
range instead of textContainers.first, skip rendering-surface expansion
for non-quote fragments, trim trailing newlines without per-iteration
string allocation, and simplify the follows-blockquote check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRUDZwowe1T836zcYLwQoo
The streaming replacement engine holds back a trailing '.' (potential
'...' → '…' replacement) and flushes it as a separate run, so
'First para.' tokenizes as [plain("First para"), plain("."), …] and
the event-shape assertions mismatched. Every other quote golden already
uses period-free text; align these two.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRUDZwowe1T836zcYLwQoo

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: adc1f07560

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/PicoMarkdownView/Tokenizer/BlockStateMachine.swift
Comment thread Sources/PicoMarkdownView/Tokenizer/BlockStateMachine.swift Outdated
Comment thread Sources/PicoMarkdownView/Renderer/MarkdownAttributeBuilder.swift
…parators

- Mark the NSTextLayoutManagerDelegate fragment method nonisolated: the
  protocol is not main-actor bound (TextKit 2 may lay out off-main) and
  the method only inspects the text element. Fixes the iOS build error.
- Close the quote stack immediately when a marker-only '>' line
  finalizes, instead of deferring via a context flag. Simpler, and it
  fixes a real bug the deferred approach had: an *unquoted* line after
  the separator lazily continued the old quote instead of starting a
  normal paragraph. Event sequences for quoted continuations are
  unchanged; new golden covers the unquoted case.
- Container-only quote parents (e.g. the implicit level-1 block that
  '>> nested' opens) now render nothing: children draw the bars for all
  enclosing levels, so the parent's newline showed as a stray blank
  quote line.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRUDZwowe1T836zcYLwQoo
@ronaldmannak

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cee78e0095

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/PicoMarkdownView/Tokenizer/BlockStateMachine.swift
Comment thread Sources/PicoMarkdownView/Renderer/MarkdownAttributeBuilder.swift Outdated
Comment thread Sources/PicoMarkdownView/Renderer/MarkdownAttributeBuilder.swift
…ndings

Bar continuity (user-reported): each quote block's bars were drawn from
its own bounding rect, so the shared level-1 bar broke at every block
boundary (nested quotes, paragraph splits). The TextKit 1 layout manager
now collects adjacent bar segments (scanning one attribute run past the
drawn region) and extends bars shared with a text-adjacent neighbor down
to the neighbor's top — one uninterrupted bar per level, GitHub-style.
Bars are plain rects now so joined segments meet seamlessly.

Codex review findings:
- Image-only quote parents (empty alt text) were suppressed as
  container-only, dropping the attachment: atomic payloads (image, math)
  now count as content.
- The container-only suppression went stale when a nested marker split
  across chunks: inserting a child block now refreshes its parent's
  cached render.
- Whitespace emitted by marker-only separator lines (and trailing
  newline runs surviving styled spans) rendered as blank quoted lines:
  the quote body now trims trailing whitespace before its terminator.

Regression tests for all three plus the styled-span quote ending.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRUDZwowe1T836zcYLwQoo

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7ce527f4b6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/PicoMarkdownView/Renderer/MarkdownRenderer.swift
Codex follow-up on the parent-refresh fix: the renderer's cache was
refreshed, but the emitted diff only mentions the new child, and the
backend updates storage strictly by diff IDs — so the parent's stale
blank quoted line survived in the view until the quote closed. The
backend's blockStarted handling now also syncs the inserted block's
parent record (no-op when content is unchanged). Backend regression
test replays the split-marker streaming scenario against NSTextStorage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRUDZwowe1T836zcYLwQoo
@ronaldmannak

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a1681f6dcd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/PicoMarkdownView/Tokenizer/BlockStateMachine.swift Outdated
Comment thread Sources/PicoMarkdownView/Renderer/MarkdownAttributeBuilder.swift Outdated
Comment thread Sources/PicoMarkdownView/Tokenizer/BlockStateMachine.swift
…t-safe trim

Third Codex review round, all three accepted:

- detectBlockquote now allows the marker's optional space plus up to
  three spaces of indentation before a nested '>' (CommonMark), so
  '>  > nested' deepens instead of rendering a literal '> nested'.
  Five or more spaces keep the inner marker as content.
- Marker-only quote lines ('>', '>  ') no longer leak their leftover
  whitespace or a spurious hard-break newline into the event stream:
  appendDeltaIfNeeded defers emission while a quote line is marker-only
  (deferral, not swallowing — '>   continuation' content is unchanged).
- With separators clean at the source, renderBlockquote's trailing trim
  is newline-only again, so meaningful trailing spaces inside quoted
  code spans survive rendering and copy.

Goldens for the padded-nesting and clean-separator event shapes, and a
renderer test pinning the code-span trailing space.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRUDZwowe1T836zcYLwQoo
@ronaldmannak ronaldmannak merged commit 383c270 into main Jul 7, 2026
2 checks passed
@ronaldmannak ronaldmannak deleted the claude/code-block-syntax-highlighting-g6l056 branch July 7, 2026 01:05
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.

2 participants