Skip to content

feat(notebook): export Markdown and SQL cell source as a Markdown document - #1530

Open
shu-chen-dot-ca wants to merge 4 commits into
TabularisDB:mainfrom
shu-chen-dot-ca:feat-1491-notebook-markdown-export
Open

shu-chen-dot-ca wants to merge 4 commits into
TabularisDB:mainfrom
shu-chen-dot-ca:feat-1491-notebook-markdown-export

Conversation

@shu-chen-dot-ca

@shu-chen-dot-ca shu-chen-dot-ca commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1491.

Adds Export Markdown to the notebook export menu, after Export SQL source. It writes a source-only .md file: Markdown cells as they are, and SQL cells in fenced sql code blocks, in notebook order, separated by a blank line.

  • utils/notebookMarkdownExport.ts: new pure serializer exportNotebookToMarkdown(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 returns null when nothing remains. chooseCodeFence uses 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. findUnclosedFence decides this by parsing the cell plus a probe paragraph with mdast-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: downloadNotebookMarkdown goes through platform.downloadFile (desktop save dialog and browser download), as <title>.md with text/markdown;charset=utf-8 and a Markdown filter. It returns downloaded, cancelled or empty, like the SQL export; the empty case opens no dialog.
  • NotebookView.tsx: handleExportMarkdown exports cellsRef.current (current in-memory source). Empty shows the new localized exportMarkdownEmpty message, 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 call flushPendingEditorChanges(), so an edit still inside the SQL editor's 300 ms debounce is included.
  • utils/pendingEditorChanges.ts (new) and SqlEditorWrapper.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) and onExportMarkdown prop. exportNotebookToSql is unchanged.
  • editor.notebook.exportMarkdown and exportMarkdownEmpty added 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, null when nothing remains, chooseCodeFence lengths, 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 real SqlEditorWrapper (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.tsx and tests/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), exportNotebookToMarkdown and the export handlers. detect_changes reports low risk and 0 affected processes.

Manual check in the web build (tabularis web with this frontend, SQLite): with a notebook that has a SQL cell (run, results on screen) and a Markdown cell, Export Markdown downloaded Notebook.md with 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.

Before After
before after

@kilo-code-bot

kilo-code-bot Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Incremental review of changes since ddac9b81: pending-editor flush before export, unclosed-fence handling in Markdown export, and the new mdast-util-from-markdown dependency. cellsRef is updated synchronously on change, so the flush is visible to the export handlers. No defects found.

Files Reviewed (10 files)
  • package.json
  • pnpm-lock.yaml
  • NotebookView.tsx
  • SqlEditorWrapper.tsx
  • notebookMarkdownExport.ts
  • pendingEditorChanges.ts
  • 4 test 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)
  • NotebookToolbar.tsx
  • NotebookView.tsx
  • 13 locale JSON files
  • notebookMarkdownExport.ts
  • notebookTransfer.ts
  • 4 test files (toolbar, view, markdown export, transfer)

Reviewed by claude-sonnet-5.5 · Input: 248 · Output: 1K · Cached: 176.6K

Review guidance: REVIEW.md from base branch main

@Xnadir Xnadir left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, NotebookView and notebookSqlExport tests: 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 debba left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 debba left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Open a notebook with SELECT id, customer, amount FROM orders ORDER BY id LIMIT 3 in a SQL cell.
  2. Change it to SELECT id, customer, amount FROM orders WHERE amount > 20 ORDER BY id LIMIT 3.
  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.

@shu-chen-dot-ca

Copy link
Copy Markdown
Contributor Author

@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.

  • utils/pendingEditorChanges.ts (new): mounted SQL editors register a flush; flushPendingEditorChanges() hands every held-back change to its owner at once.
  • SqlEditorWrapper: the debounced emit is kept in a ref and registered for its lifetime. A flush emits it immediately and cancels the timer, so it fires once, and the echo is still recognized. An external value change or unmount drops it, as before.
  • NotebookView: the four export handlers flush first, so cellsRef.current holds the visible text.
  • tests/components/notebook/NotebookViewExport.test.tsx renders NotebookView with the real SqlEditorWrapper (only Monaco is a textarea), types, and exports immediately with each of the four actions. All four fail without the flush. Unit tests cover the flush in the wrapper (once only, latest text, nothing pending, external change, unmount) and the helper. After the change the same browser run writes the WHERE clause for all four exports, both immediately and 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, <pre> blocks and list items that had already ended. So the check parses the cell plus a probe paragraph with mdast-util-from-markdown, the CommonMark parser react-markdown already uses. It only closes a fenced block that takes in the probe, with that block's own fence and indentation. This adds it as a direct dependency at the version already in the lockfile (2.0.3). I checked 15 cases rendered with micromark (your example, ~~~, list items, block quotes, HTML comment and <pre>, 4-space indentation, shorter inner fences, CR line endings); they are now regression tests.

Checks on this 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 including both smokes. GitNexus impact was LOW for SqlEditorInternal (10 impacted) and the export handlers; detect_changes reports low risk with 0 affected processes.

This branch has not been deployed

No deployments
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.

[Feat]: Notebooks: export Markdown and SQL cell source as a Markdown document

3 participants