Skip to content

markdown: the markdown, export, and TUI renderers measure and clip text through the Unicode 17 library - #34

Merged
tawago merged 2 commits into
mainfrom
markdown-render-model
Sep 14, 2026
Merged

tawago merged 2 commits into
mainfrom
markdown-render-model

Conversation

@tawago

@tawago tawago commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Re-opens #33 against main (#33 merged into unicode-17-authority after that branch had already landed as #32). Rebased onto main; same two commits.

Second slice of #31, stacked on #32. The markdown, export, and TUI renderers, the theme files, and the legacy Mermaid canvas now measure and clip text through the Unicode 17 library. One grapheme iterator replaces the per-file width loops. src/core/mermaid_v2 is untouched here; #31 is now based on this branch and retargets automatically once it merges.

What moves

  • The markdown render model lives in render/line.zig; render/geometry.zig is the strict width wrapper the table, front matter, inline, code, and rule paths use.
  • Code blocks and rules leave blocks.zig for render/code.zig and render/rules.zig.
  • Export layout, plain, and PNG walk graphemes through the library iterator. TUI selection, copy preview, toast, and metadata use PreparedLine.
  • The legacy Mermaid renderers get their own test root, zig build test-mermaid-legacy, folded into zig build test.
  • build.zig adds the text and unicode modules to the executable and test roots. src/cli/input.zig switches to the text module import for the same reason.

User-visible behavior changes (all of these are how #31 behaves today; they are listed so they get reviewed on their own)

  • Legacy Mermaid canvas (sequence, class, ER, state) draws a label only when every grapheme is a single scalar. A label with an emoji plus variation selector, a flag, a ZWJ sequence, a combining mark, or a tab is dropped silently. The previous canvas drew it.
  • Markdown documents containing a Grapheme_Cluster_Break=Control code point (soft hyphen, zero-width space, LRM/RLM, word joiner) are rejected whole with error: DisallowedControl, in every format and in the TUI. They rendered before.
  • Malformed UTF-8 anywhere in a document is rejected with error: InvalidUtf8. Before, it was tolerated byte-wise. This supersedes the one-byte loop floors unicode: Unicode 17.0.0 is the width and grapheme authority #32 added; those loops no longer exist.
  • Table cells are re-tokenized on spaces and re-joined with single spaces, so runs of spaces inside a cell collapse, including inside inline code.
  • Link and image widths count columns of the rendered text rather than bytes, so tables with multibyte URLs come out narrower.
  • Tabs in front matter expand to four-column stops in every style.

Verification

  • zig build, zig build test (1680 passed, 3 skipped), zig fmt --check green on this branch alone.
  • Rendering of Mermaid diagrams under the current renderer is unchanged by this slice.
  • Malformed front matter exits with error: InvalidUtf8 immediately in every style.

🤖 Generated with Claude Code

https://claude.ai/code/session_012r9nF92bG3LRf1DC3r933M

tawago and others added 2 commits September 14, 2026 09:48
…xt through the Unicode 17 library — one grapheme iterator replaces the per-file width loops

The markdown render model moves into render/line.zig (SpanStyle, Span, Line,
Rendered) with render/types.zig re-exporting it. render/geometry.zig is the
strict width wrapper the table, front matter, inline, code, and rule paths use;
line.zig, wrap.zig, and builder.zig call the library directly. Every measure
returns error.InvalidUtf8 or error.DisallowedControl instead of guessing a byte
width, and the builder validates each finished line as a whole, so malformed
input is rejected once at render time rather than tolerated by each wrap,
table, and front matter loop.
Code blocks and rules leave blocks.zig for render/code.zig and render/rules.zig.
Export layout, plain, and PNG walk unicode.Iterator graphemes; the glyph sheet
measures with rawDisplayWidth. TUI selection, copy preview, toast, and metadata
use PreparedLine and the same iterator. The legacy Mermaid canvas draws text by
authority width and refuses non-scalar or tab text, and its renderers get their
own test root (src/core/mermaid/legacy_test.zig, `zig build test-mermaid-legacy`).
src/export/font.zig adds six required glyphs (U+25B3, U+25B7, U+25BD, U+25C1,
U+25CB, U+2715). src/core/theme.zig and src/core/theme/ (comment stripping and
literal reflow only), src/export/png_encode.zig, src/export/surface.zig, and the
legacy mermaid parser.zig, types.zig (besides its `text` module import), and
state/render.zig change comments only.
build.zig adds the `text` and `unicode` modules to the executable and unit test
roots and wires the legacy Mermaid test root into `zig build test`.

User-visible behavior changes in this slice:
- The legacy Mermaid canvas (sequence, class, ER, state) draws a label only
  when every grapheme is a single scalar; a label containing a multi-scalar
  grapheme (emoji with VS16, flags, ZWJ sequences, combining marks) or a tab
  is dropped silently, where the previous canvas drew it.
- Markdown documents containing a Grapheme_Cluster_Break=Control code point
  (soft hyphen U+00AD, zero-width space U+200B, LRM/RLM, word joiner) are
  rejected whole with `error: DisallowedControl`, in every format and in the
  TUI; previously they rendered.
- Malformed UTF-8 anywhere in a document is rejected with
  `error: InvalidUtf8`; previously it was tolerated byte-wise.
- Table cells are re-tokenized on spaces and re-joined with single spaces, so
  runs of spaces inside a cell (including inside inline code) collapse.
- Link and image widths are measured from rendered text, so multibyte URLs
  count columns, not bytes; tables with such links come out narrower.
- Tabs in front matter expand to four-column stops in every style, including
  raw on the terminal.
- Options.for_export is now inert for markdown; sanitizeFrontMatter,
  sanitizeText, decor.slotPtr, inline.inlinesDisplayWidth, and
  wrap.isWhitespace are deleted.

Extracted from the rail-renderer branch as the second slice; src/core/mermaid_v2
is untouched here. src/cli/input.zig and src/cli/input_test.zig are pulled in
whole because they must import lib/text.zig by module name once the `text`
module exists, or the file belongs to two modules and the build fails.
The one-byte floors at the nextGlyph callers in tui/app.zig, tui/selection.zig,
and markdown/render/frontmatter.zig are gone with those loops; malformed UTF-8
now exits with error.InvalidUtf8 from the builder and geometry path, verified on
every front matter style in well under a second. The three floor regression
tests were dropped with the loops they guarded and are replaced by tests that
expect error.InvalidUtf8 in selection.zig, metadata.zig, and builder.zig.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012r9nF92bG3LRf1DC3r933M
…, one prepare pass per line, one status-message helper

Cleanup pass over the Unicode 17 render-model change; no output changes.

- Line.joinedText replaces five hand-written span concatenation loops
  (selection, plain export, PNG layout, line preparation, a test).
- Line.prepareOwned measures a tab-free line in place instead of building
  a prepared copy, a byte map, and re-duplicating every span; the
  blockquote paths no longer prepare a line before finish prepares it again.
- Selection: one overlap scan serves both highlight and copy; rows outside
  the selection return before allocating; Line replaces anytype and the
  test doubles.
- Metadata overlay prepares each entry once per frame and reuses the theme's
  panel style type instead of mirroring it.
- Table alignment measures a cell once; the offset search remains only for
  cells containing a tab. Natural widths fill the widths array directly.
- Rules, code frames, and the mermaid debug dump call Builder.appendRepeated
  and one debug-line helper instead of private repeat and format loops.
- TUI: one reportMeasureError replaces five copies of the same switch.
- Removed: the unused Options.for_export, a stale export error arm, the
  PNG exporter's duplicate nextGrapheme, three validation-only width calls,
  and empty branches in the paragraph and state-diagram code.
- Export tests share one ChildEnv helper for the temporary config home.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrTcgx7ZfWmNowJ1RXAwfv
@tawago
tawago merged commit 5706931 into main Sep 14, 2026
2 checks passed
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