fix(export): gate the export menu on the buffer, not the render cache - #441
Merged
Conversation
`Tab` carries three same-shaped strings. `rawContent` is the Markdown, and
it is what the editor edits, what reaches disk, and what the exports read.
`content` is the rendered preview HTML, injected via `{@html}`. Two places
read the wrong one.
1. TitleBar.svelte gated Export as HTML / Export as PDF on
`tabManager.activeTab.content`, while `exportAsHtml` gates on
`ctx.rawContent`. `content` is only refreshed while the preview is on
screen — `tab.isSplit || (isEditing && settings.showToc)` — so for an
untitled buffer being edited with the TOC closed it is still `''` while
`rawContent` holds the user's text. `showToc` and `newFileDefaultMode`
default such that Ctrl+T then typing is exactly that state, and the menu
hid two commands that would have produced a file. Reachable since #421
let the preview work without saving first.
2. `addTab(path, content = '')` assigned its argument to `content`,
`rawContent` and `originalContent` alike — coherent when the three were
one field, but it let Markdown into the field that is injected as HTML.
Both callers in the app pass `''`, so nothing shipped broken; the value
is sanitized at the sink either way. The parameter is now `rawContent`
and `content` starts empty, as at every other Tab construction site.
scripts/renderedHtmlField.test.ts evaluates the real gate, the real
refresh condition and `exportAsHtml`'s real precondition against the real
TabManager. Four of its nine tests are red on master.
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.
Two reads of the wrong one of
Tab's three same-shaped string fields. #437 documents the distinction and reports both of these as out of scope; this is the fix. Expect a small rebase against it — it touches the field comments, this touchesaddTaband the menu.rawContentsave_file_contentoriginalContentisDirtycontent{@html sanitizedHtml}Verified independently before relying on it:
grep -rn '\.content\b' srcreturns exactly two reads ofactiveTab.content—htmlContentatMarkdownViewer.svelte:239, which is correct, and the export gate below. That grep is also the answer to "does any other menu item gate on the wrong field": no.1. The export menu gated on the render cache
{#if currentFile !== '' || (tabManager.activeTab && tabManager.activeTab.content)}exportAsHtmlgates onctx.rawContent(export.ts:376).contentis a cache of a render, refreshed only by the effect atMarkdownViewer.svelte:2363:So in plain edit mode with the TOC closed, nothing re-renders, and for a buffer that has never been rendered
contentis''. AddcurrentFile === ''and both halves of the gate are false whilerawContentholds the document.settings.showTocdefaults tofalseandsettings.newFileDefaultModedefaults totrue, so Ctrl+T, then type is exactly that state. Reachable since #421 let the preview work without saving first; before that an untitled tab had nothing to export either way.Reproduced in the running app
Vite dev server, Tauri IPC stubbed by a temporary shim (not committed), same tab and same buffer in both, only the gate expression changed between the two:
…activeTab.content(master)…activeTab.rawContent(this PR)Tab:
无标题 1, dirty,# Hello Markpadin the editor, TOC closed, not split.The state matrix
scripts/renderedHtmlField.test.tsevaluates the shipping expressions rather than matching their text: the{#if}is lifted from the parsed TitleBar AST, the refresh condition from the parsed MarkdownViewer AST, andexportAsHtml's early return from the TypeScript AST — all three run against the realTabManager. Rewrite any of them however you like and the tests follow.contentis brought to what the running app would hold (refreshed only when the shipping condition says so) rather than being set by hand, so the repro is derived, not staged.Five states, asking "is the export possible but hidden?":
Only the first row changes. The menu is allowed to be looser than the export (a tab with a path whose file has not been read yet still lists the entries, and the export then no-ops — pinned by its own test); being stricter is the defect.
Red on master: 4 of 9 tests. Mutation check on the fixed gate:
…activeTab.content(master)…activeTab.originalContenttruefalseactiveTab && activeTab.rawContent(drops the path half)2.
addTabseeded the HTML field from a Markdown-shaped argumentaddTab(path, content = '')assigned its argument tocontent,rawContentandoriginalContentalike (tabs.svelte.ts:342). That was coherent when the three were one field.Caller survey, done first:
''—documentSession.svelte.ts:254explicitly,MarkdownViewer.svelte:1434by default. Nothing shipped broken.scripts/, ~25 callers pass real Markdown (tabPathIdentity,reopenDirtyDocument,foldStatePerDocument,externalChangeReload) and depend on it landing inrawContent/originalContent. That is what says the argument is the buffer, not the render.windowSession/documentSession/ tab-transfer path constructs through it: window restore goes throughrestoreState, cross-window arrival throughinsertTransferredTab→buildTransferredTab.Chosen: rename the parameter to
rawContentand stop seedingcontent. Rename alone leaves the wrong assignment; dropping the seed alone leaves a call site that reads as ifcontentwere the thing being passed. A new shape for the two buffers is a bigger change than this defect justifies, and renamingcontent→renderedHtmlacross the codebase is deliberately not attempted here.Nothing depends on the seeded value:
createFileHistory(path, _content)ignores it, the only reader ishtmlContent→sanitizedHtml→{@html}, andaddNewTab,restoreState,addHomeTabandbuildTransferredTaball already startcontentat''and let the first render fill it. This makesaddTabthe fifth, not the exception — asserted directly byevery Tab construction site starts the rendered-HTML field empty.On the security framing, plainly:
contentreaches the DOM throughMarkdownViewer.svelte:250,sanitizedHtml = $derived(sanitizeMarkdownHtml(htmlContent)), and that is what{@html}at:3450injects. The sanitizer is between this field and the sink in every case, and both live callers pass''. This is a correctness fix — the signature permitted a value the field does not mean — not the closing of a hole.Mutation check on
addTab, whole suite:content: rawContent(the old seed)rawContent: ''originalContent: ''Verification
npm test537/537 (528 before, +9).npm run check632 files / 0 errors / 0 warnings.npm run buildclean; the new template comment is stripped from the production bundle.Not covered
syncPreviewForPrint(MarkdownViewer.svelte:1957) re-renders before printing, which is why Export PDF is correct to be enabled in the newly-reachable state too. Not exercised here — the tests establish which branch the menu takes, not what the PDF contains.syncPreviewForPrint's counterpart for HTML.exportAsHtmlrenders fromrawContentdirectly, so it needs none, but that was read rather than exercised.contentstill means rendered HTML andrawContentstill means the document. That rename touches many files and belongs in its own discussion.contentline becomes more true, and itsaddTabline numbers shift.🤖 Generated with Claude Code