Repository navigation
feat(notebook): export Markdown and SQL cell source as a Markdown document - #1530
shu-chen-dot-ca wants to merge 4 commits into
Conversation
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Incremental review of changes since Files Reviewed (10 files)
Previous Review Summary (commit ddac9b8)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit ddac9b8)Status: No Issues Found | Recommendation: Merge Files Reviewed (21 files)
Reviewed by claude-sonnet-5.5 · Input: 248 · Output: 1K · Cached: 176.6K Review guidance: REVIEW.md from base branch |
Xnadir
left a comment
There was a problem hiding this comment.
Thanks @shu-chen-dot-ca, this reads well. The serializer is small and pure, and picking the fence from the longest backtick run is the right way to keep pasted SQL from closing its block.
What I checked on ddac9b8:
notebookMarkdownExport,notebookTransfer,NotebookToolbar,NotebookViewandnotebookSqlExporttests: 90 passed.- The two new keys exist in all 13 locales.
- Sections are separated by a blank line, so a Markdown cell ending in a list, table or HTML block can't run into the next section.
One edge case, non-blocking: a Markdown cell with an unclosed code fence swallows the cells after it. In the notebook each Markdown cell renders on its own, so an open fence ends at the end of the cell. In the exported file it stays open, and CommonMark only closes it at the next bare fence line. I rendered this export with the micromark that react-markdown uses here:
exportNotebookToMarkdown([
{type: 'markdown', content: 'Notes\n\n```sql\nSELECT 1'},
{type: 'markdown', content: '## Next section'},
{type: 'sql', content: 'SELECT 2'},
])<p>Notes</p>
<pre><code class="language-sql">SELECT 1
## Next section
```sql
SELECT 2
</code></pre>The heading and the whole SQL cell end up inside the first cell's code block. The ```sql opener of the SQL cell can't close it, because a closing fence has no info string. So the closing fence of the SQL cell is what ends it.
A possible fix: when serializing a Markdown cell, track an open fence (a line of 3+ ` or ~, indented at most 3 spaces) and append a matching closing fence if the cell ends inside one. Then the export renders like the notebook. Happy for this to be a follow-up if you'd rather keep the PR as it is.
Reviewed with help from Claude Code.
debba
left a comment
There was a problem hiding this comment.
Thanks for making the Markdown export use the current cell source, including unsaved edits and collapsed cells. I also appreciate the fence handling for SQL that contains backticks and the explicit exclusion of results and parameter values. All 68 tests in the four changed test files passed, along with typecheck. I haven't found a blocking issue in the diff. I'll run e2e checks in the app before merging.
debba
left a comment
There was a problem hiding this comment.
I tried the export in the running Web app with SQLite and found that it can miss the latest edit.
Steps I reproduced, including with real keystrokes:
- Open a notebook with
SELECT id, customer, amount FROM orders ORDER BY id LIMIT 3in a SQL cell. - Change it to
SELECT id, customer, amount FROM orders WHERE amount > 20 ORDER BY id LIMIT 3. - Immediately choose Export Notebook, then Export Markdown, before the editor's 300 ms debounce has elapsed.
The editor shows the WHERE clause, but the downloaded Markdown contains the previous query without it. Waiting 400 ms before exporting includes the edit correctly. There were no console errors during this reproduction.
handleExportMarkdown reads cellsRef.current, while SqlEditorWrapper only forwards the latest buffer to the notebook after 300 ms and does not flush on blur. Please flush pending editor changes, or take a current buffer snapshot, before building the Markdown. A regression using the real editor wrapper would catch this; the existing mocked-editor test does not exercise that delay. I am leaving this open until the downloaded source matches what is visible in the editor.
…cell in the Markdown export
|
@debba thanks, reproduced with your steps in the Web app (SQLite, a scripted browser so the export lands inside the 300 ms debounce). The same race affected all four notebook exports, not only Markdown: Export Notebook, HTML, SQL source and Markdown all wrote the previous query, and all were correct after 400 ms.
@Xnadir: a Markdown cell that ends inside a fenced code block now gets a matching closing fence, so it no longer swallows the cells after it. A line scan was too eager: it also closed fence-like lines inside HTML comments, Checks on this commit, all passing: the CI steps for a lockfile change, i.e. |
Fixes #1491.
Adds Export Markdown to the notebook export menu, after Export SQL source. It writes a source-only
.mdfile: Markdown cells as they are, and SQL cells in fencedsqlcode blocks, in notebook order, separated by a blank line.utils/notebookMarkdownExport.ts: new pure serializerexportNotebookToMarkdown(cells). It reads cell content only, so collapsed cells are included and placeholders (@name,${name}) and{{cell_N}}references are copied verbatim. Results, charts, errors, execution time, history, per-cell database, cell names and parameter values are left out. Empty and whitespace-only cells are skipped, and it returnsnullwhen nothing remains.chooseCodeFenceuses a backtick fence one longer than the longest backtick run in the SQL (at least three), so a pasted fence or MySQL identifiers cannot close the block early. A trailing line break is added only when the source has none. A Markdown cell that ends inside a fenced block gets a matching closing fence (same fence and indentation as the opener), so the block ends with its cell as it does in the notebook instead of swallowing the cells after it.findUnclosedFencedecides this by parsing the cell plus a probe paragraph withmdast-util-from-markdown, the parser react-markdown already uses, now a direct dependency at the locked 2.0.3. Fence-like lines inside HTML blocks, closed blocks and ended list items are left alone.utils/notebookTransfer.ts:downloadNotebookMarkdowngoes throughplatform.downloadFile(desktop save dialog and browser download), as<title>.mdwithtext/markdown;charset=utf-8and a Markdown filter. It returnsdownloaded,cancelledorempty, like the SQL export; the empty case opens no dialog.NotebookView.tsx:handleExportMarkdownexportscellsRef.current(current in-memory source). Empty shows the new localizedexportMarkdownEmptymessage, a cancelled dialog shows nothing, and a failure shows the existing export error. It executes nothing and does not touch notebook or save state. All four export handlers first callflushPendingEditorChanges(), so an edit still inside the SQL editor's 300 ms debounce is included.utils/pendingEditorChanges.ts(new) andSqlEditorWrapper.tsx: mounted SQL editors register a flush that emits their held-back change at once and cancels the timer, so it fires once.NotebookToolbar.tsx: the new menu item (Markdown cell icon) andonExportMarkdownprop.exportNotebookToSqlis unchanged.editor.notebook.exportMarkdownandexportMarkdownEmptyadded to all 13 locales.Tests:
tests/utils/notebookMarkdownExport.test.ts(new): mixed, Markdown-only and SQL-only notebooks, cell order, verbatim parameters and references, Unicode and multiline text, existing trailing line breaks (LF and CRLF), embedded backticks (triple, longer runs, MySQL identifiers, indented fence) checked by parsing the fenced blocks back, collapsed cells, excluded runtime data, skipped empty cells,nullwhen nothing remains,chooseCodeFencelengths, and closing of fences left open in Markdown cells (backtick and tilde, indented, shorter inner fences, CR line endings), with fence-like lines in HTML comments,<pre>blocks, ended list items, block quotes and 4-space indentation left alone.tests/utils/notebookTransfer.test.ts: file name, MIME type, filter and contents through the download adapter; cancelled dialog; no dialog when empty; a write failure reaches the caller.tests/components/notebook/NotebookToolbar.test.tsx: the item follows Export SQL source and calls only its own handler.tests/components/notebook/NotebookView.test.tsx: exporting after an unsaved edit writes the edited source of a collapsed cell with results, chart, error and history, without running a query or saving the notebook; the empty message with no save dialog; cancellation does nothing; a write failure shows the export error.tests/components/notebook/NotebookViewExport.test.tsx(new): with the realSqlEditorWrapper(only Monaco is a textarea), each of the four exports includes an edit typed just before, inside the debounce. All four fail without the flush.tests/components/ui/SqlEditorWrapper.test.tsxandtests/utils/pendingEditorChanges.test.ts(new): the flush emits once with the latest text, and does nothing when nothing is pending, after an external change or after unmount.Checks run on the latest commit, all passing: the CI steps for a lockfile change, i.e.
pnpm install --frozen-lockfile, plugin guest build with no diff, typecheck, lint,format:check,web:boundaries,web:packaging,web:docs,web:manual-parity,test:contrast,eslint ., theme tokens, build,test:web-contract,test:web-resilience,cargo test(2619 passed, 19 ignored), vitest with coverage (569 files, 9164 tests),cargo check --locked --no-default-features --bin tabularis, the Tauri boundary grep, and the plugin-api, explain and create-plugin package scripts. No backend changes.GitNexus: upstream impact was LOW for
NotebookToolbar(4 impacted),NotebookView(3 impacted),SqlEditorInternal(10 impacted),exportNotebookToMarkdownand the export handlers.detect_changesreports low risk and 0 affected processes.Manual check in the web build (
tabularis webwith this frontend, SQLite): with a notebook that has a SQL cell (run, results on screen) and a Markdown cell, Export Markdown downloadedNotebook.mdwith the SQL block followed by the Markdown, and no result rows. A scripted browser that types a WHERE clause and exports at once gets the edited query in all four exports. The desktop save dialog path is covered by the tests above, not by a manual run.