feat(model): keep the markdown structure in the card's changelog - #45
Merged
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The [changelog] section destroyed the markdown it was handed. stripMarkdown trimmed a leading `*`/`-` off every line, so list markers went; it left `[text](url)` raw, ignored fenced code, and its `<...>` strip swallowed `<https://...>` autolinks. wrapText then rebuilt lines from strings.Fields, losing every indent. markdownToLines replaces it: one line pass over the body with two state flags (inside a fence, inside an HTML comment) returning pre-wrapped lines tagged mdBody or mdHeading. Wrapping lives inside the converter because hanging indents need the block structure, and because styling must land on whole finished lines — wrapLine counts runes and knows nothing about ANSI. renderChangelogBlock is now just the consumer: headings take ui.ChangelogHeadingStyle (bold ColorText, one step above the muted body and no new color to compete with the peach section labels), everything else InfoStyle. Two kinds only, no mdCode. Everything that makes code special (verbatim, no inline processing, no wrap, a 2-space indent) lives in the converter, so a code tag would have no reader. Rules worth naming. CRLF is normalized first: GitHub bodies arrive CRLF from the web form, and a \r-suffixed closing fence would fail to match and swallow the rest of the body as code. Headings and list markers require the space after the marker, so `#123 fixed ...` stays an issue ref and `**Breaking**` stays a paragraph — the old Trim(line, "*_") bug class cannot come back. ---/***/___ collapse to a blank line and are never bullets, since that is the separator above "Full Changelog". Inline order is load-bearing: images, links, autolinks, then the HTML strip that used to eat them. The underscore emphasis pattern requires a non-word rune outside both delimiters or update_cmd would lose its underscore, and it repeats until stable because a match consumes the boundaries it asserts (_a_ _b_ needs two passes). The empty-result fallback now also covers a non-empty body the converter consumed whole: a comment-only or separator-only release note lands on "no release notes available." instead of a blank block. Deliberately not glamour — the card stays muted and the render is synchronous inside Update(). Unchanged: the leading release-URL line, buildCard's clickable-line index, and "a content line is a screen row". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1. The fence pattern enforced CommonMark's 3-space indent limit, where 4+ spaces means an indented code block. This converter does not implement those at all, so the limit only mis-read a fence nested under a list item — "1. " aligns its content at column 4 — and the language tag leaked out as a body line reading "go" while the sample lost its verbatim treatment. The fence now takes any indent. 2. A setext underline survived as a literal row of equals signs: "---" already collapsed as a thematic break, "===" matched nothing. Both are now rule lines that drop to a blank. Full setext support (retagging the paragraph above as a heading) stays out of scope — the text and the layout are right, only the emphasis is lost. 3. A heading whose markup collapsed to nothing (## with only an image) emitted an empty line still tagged mdHeading, bypassing the blank collapse the body path went through. Harmless in today's renderer, which checks text before kind, but it is a line that is not a heading wearing the tag. Heading and paragraph now share mdEmitInline, so the blank can only arrive through emitBlank — and the duplicated guard is gone with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The whole card is rebuilt on every spinner frame — the spinner.TickMsg
handler calls SetContent(renderCard()) about twelve times a second for as
long as a [r] refresh or a [u] update runs — so the release body went
through the converter's regexes twelve times a second to animate one
glyph. Measured on a 49 KB body: 3.3 ms and 1 MB of garbage per pass,
against 15 ns for a cache hit. A typical 1.2 KB body costs 140 µs per
pass, which is invisible on its own; the point is that it repeats on a
timer and the new converter is ~10x the old string-trimming one.
changelogRenderCache is readmeRenderCache's shape, one entry keyed by
(body, width) — the only two inputs markdownToLines has. It hangs off
Model as a pointer because the card renderers are value receivers and
could not fill a plain field, and its method tolerates a nil receiver: a
cache-less but correct mode, which is what the ~50 tests building Model{}
literals get without touching any of them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The
[changelog]section of panel[2]destroyed the markdown it was handed.stripMarkdowntrimmed a leading*/-off every line, so list markers went; it left[text](url)raw, ignored fenced code, and its<...>strip swallowed<https://...>autolinks.wrapTextthen rebuilt lines fromstrings.Fields, losing every indent.markdownToLinesreplaces it — one line pass with two state flags (inside a fence, inside an HTML comment) returning pre-wrapped lines taggedmdBodyormdHeading. Wrapping lives inside the converter because hanging indents need the block structure, and because styling must land on whole finished lines:wrapLinecounts runes and knows nothing about ANSI.renderChangelogBlockis now just the consumer — headings take the newui.ChangelogHeadingStyle(boldColorText, one step above the muted body, no new color to compete with the peach section labels), everything elseInfoStyle.Before / after on keepkit's own v0.1.0 notes:
Rules worth naming
\r-suffixed closing fence would fail to match and swallow the rest of the body as code.#123 fixed ...stays an issue ref and**Breaking**stays a paragraph — the oldTrim(line, "*_")bug class cannot come back.---/***/___collapse to a blank line, never a bullet: that is the separator above "Full Changelog".update_cmdwould lose its underscore, and it repeats until stable because a match consumes the boundaries it asserts (_a_ _b_needs two passes).mdCode: everything that makes code special (verbatim, no inline processing, no wrap, a 2-space indent) lives in the converter, so a code tag would have no reader.The empty-result fallback now also covers a non-empty body the converter consumed whole — a comment-only or separator-only release note lands on
no release notes available.instead of a blank block.Deliberately not glamour: the card stays muted and the render is synchronous inside
Update().Unchanged
The leading release-URL line,
buildCard's clickable-line index and "a content line is a screen row" —TestBriefContentLineIsScreenRow,TestBuildCardLinksandTestMouseBriefLinkClickare untouched and green.Test plan
internal/model/textutil_test.go(new file) plus threerenderChangelogBlocktests inrender_test.go;TestStripMarkdowndeleted with the function.markdownToLines,mdInline,mdEmitListItem,mdEmitWrapped,mdListLevel,mdCutCommentsandrenderChangelogBlockare all at 100% of statements.go build/go vet ./.../go test -race ./.../golangci-lint run(0 issues), plus the cross-compile step.briefW58 and 30.Known limitation, unchanged from the code this replaces
The HTML-tag strip cuts any
<...>outside a fenced block, so--tool <name>in prose or`Vec<String>`in an inline code span loses it. A "looks like a real tag" pattern does not help —<name>is the shape of an HTML tag. Protecting inline code spans from the strip is the natural follow-up.🤖 Generated with Claude Code