@deepnote/convert 1.4.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1441 @@
1
+ import fs from "node:fs/promises";
2
+ import { basename, dirname, extname, join } from "node:path";
3
+ import { createMarkdown, createPythonCode, deserializeDeepnoteFile, environmentSchema, executionSchema } from "@deepnote/blocks";
4
+ import { v4 } from "uuid";
5
+ import { parse, stringify } from "yaml";
6
+
7
+ //#region src/utils.ts
8
+ /**
9
+ * Creates a sorting key for Deepnote blocks.
10
+ * Uses a base-36 encoding to generate compact, sortable keys.
11
+ *
12
+ * @param index - The zero-based index of the block
13
+ * @returns A sortable string key
14
+ */
15
+ function createSortingKey(index) {
16
+ const maxLength = 6;
17
+ const chars = "0123456789abcdefghijklmnopqrstuvwxyz";
18
+ const base = 36;
19
+ if (index < 0) throw new Error("Index must be non-negative");
20
+ let result = "";
21
+ let num = index + 1;
22
+ let iterations = 0;
23
+ while (num > 0 && iterations < maxLength) {
24
+ num--;
25
+ result = chars[num % base] + result;
26
+ num = Math.floor(num / base);
27
+ iterations++;
28
+ }
29
+ if (num > 0) throw new Error(`Index ${index} exceeds maximum key length of ${maxLength}`);
30
+ return result;
31
+ }
32
+ /**
33
+ * Sanitizes a filename by removing invalid characters and replacing spaces.
34
+ *
35
+ * @param name - The original filename
36
+ * @returns A sanitized filename safe for all platforms
37
+ */
38
+ function sanitizeFileName(name) {
39
+ return name.replace(/[<>:"/\\|?*]/g, "_").replace(/\s+/g, "_");
40
+ }
41
+ /**
42
+ * Deepnote block types that should be converted to code cells.
43
+ * Unknown types default to markdown (less lossy).
44
+ */
45
+ const CODE_BLOCK_TYPES = [
46
+ "big-number",
47
+ "button",
48
+ "code",
49
+ "notebook-function",
50
+ "sql",
51
+ "visualization"
52
+ ];
53
+ /**
54
+ * Checks if a Deepnote block type should be converted to a markdown cell.
55
+ * Uses a whitelist of code types and defaults unknown types to markdown (less lossy).
56
+ *
57
+ * @param blockType - The type of the Deepnote block
58
+ * @returns true if the block should be treated as markdown
59
+ */
60
+ function isMarkdownBlockType(blockType) {
61
+ if (blockType.startsWith("input-")) return false;
62
+ if (CODE_BLOCK_TYPES.includes(blockType)) return false;
63
+ return true;
64
+ }
65
+
66
+ //#endregion
67
+ //#region src/deepnote-to-jupyter.ts
68
+ /**
69
+ * Converts an array of Deepnote blocks into a single Jupyter Notebook.
70
+ * This is the lowest-level conversion function, suitable for use in Deepnote Cloud.
71
+ *
72
+ * @param blocks - Array of DeepnoteBlock objects to convert
73
+ * @param options - Notebook metadata options
74
+ * @returns A JupyterNotebook object
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * import { convertBlocksToJupyterNotebook } from '@deepnote/convert'
79
+ *
80
+ * const notebook = convertBlocksToJupyterNotebook(blocks, {
81
+ * notebookId: 'abc123',
82
+ * notebookName: 'My Notebook',
83
+ * executionMode: 'block'
84
+ * })
85
+ * ```
86
+ */
87
+ function convertBlocksToJupyterNotebook(blocks, options) {
88
+ return {
89
+ cells: blocks.map((block) => convertBlockToCell$3(block)),
90
+ metadata: {
91
+ deepnote_notebook_id: options.notebookId,
92
+ deepnote_notebook_name: options.notebookName,
93
+ deepnote_execution_mode: options.executionMode,
94
+ deepnote_is_module: options.isModule,
95
+ deepnote_working_directory: options.workingDirectory,
96
+ deepnote_environment: options.environment,
97
+ deepnote_execution: options.execution
98
+ },
99
+ nbformat: 4,
100
+ nbformat_minor: 0
101
+ };
102
+ }
103
+ /**
104
+ * Converts a Deepnote project into Jupyter Notebook objects.
105
+ * This is a pure conversion function that doesn't perform any file I/O.
106
+ * Each notebook in the Deepnote project is converted to a separate Jupyter notebook.
107
+ *
108
+ * @param deepnoteFile - The deserialized Deepnote project file
109
+ * @returns Array of objects containing filename and corresponding Jupyter notebook
110
+ *
111
+ * @example
112
+ * ```typescript
113
+ * import { deserializeDeepnoteFile } from '@deepnote/blocks'
114
+ * import { convertDeepnoteToJupyterNotebooks } from '@deepnote/convert'
115
+ *
116
+ * const yamlContent = await fs.readFile('project.deepnote', 'utf-8')
117
+ * const deepnoteFile = deserializeDeepnoteFile(yamlContent)
118
+ * const notebooks = convertDeepnoteToJupyterNotebooks(deepnoteFile)
119
+ *
120
+ * for (const { filename, notebook } of notebooks) {
121
+ * console.log(`${filename}: ${notebook.cells.length} cells`)
122
+ * }
123
+ * ```
124
+ */
125
+ function convertDeepnoteToJupyterNotebooks(deepnoteFile) {
126
+ return deepnoteFile.project.notebooks.map((notebook) => {
127
+ const jupyterNotebook = convertNotebookToJupyter(deepnoteFile, notebook);
128
+ return {
129
+ filename: `${sanitizeFileName(notebook.name)}.ipynb`,
130
+ notebook: jupyterNotebook
131
+ };
132
+ });
133
+ }
134
+ /**
135
+ * Converts a Deepnote project file into separate Jupyter Notebook (.ipynb) files.
136
+ * Each notebook in the Deepnote project becomes a separate .ipynb file.
137
+ */
138
+ async function convertDeepnoteFileToJupyterFiles(deepnoteFilePath, options) {
139
+ const notebooks = convertDeepnoteToJupyterNotebooks(deserializeDeepnoteFile(await fs.readFile(deepnoteFilePath, "utf-8")));
140
+ await fs.mkdir(options.outputDir, { recursive: true });
141
+ for (const { filename, notebook } of notebooks) {
142
+ const filePath = join(options.outputDir, filename);
143
+ await fs.writeFile(filePath, JSON.stringify(notebook, null, 2), "utf-8");
144
+ }
145
+ }
146
+ function convertBlockToCell$3(block) {
147
+ const content = block.content || "";
148
+ const jupyterCellType = convertBlockTypeToJupyter(block.type);
149
+ const metadata = {
150
+ cell_id: block.id,
151
+ deepnote_block_group: block.blockGroup,
152
+ deepnote_cell_type: block.type,
153
+ deepnote_sorting_key: block.sortingKey,
154
+ deepnote_content_hash: block.contentHash,
155
+ deepnote_execution_started_at: block.executionStartedAt,
156
+ deepnote_execution_finished_at: block.executionFinishedAt,
157
+ ...block.metadata || {}
158
+ };
159
+ metadata.deepnote_source = content;
160
+ return {
161
+ block_group: block.blockGroup,
162
+ cell_type: jupyterCellType,
163
+ execution_count: block.executionCount ?? null,
164
+ metadata,
165
+ outputs: block.outputs,
166
+ source: getSourceForBlock(block, jupyterCellType, content)
167
+ };
168
+ }
169
+ function getSourceForBlock(block, jupyterCellType, content) {
170
+ if (jupyterCellType === "markdown") return createMarkdown(block);
171
+ if (block.type === "code") return content;
172
+ return createPythonCode(block);
173
+ }
174
+ function convertBlockTypeToJupyter(blockType) {
175
+ return isMarkdownBlockType(blockType) ? "markdown" : "code";
176
+ }
177
+ function convertNotebookToJupyter(deepnoteFile, notebook) {
178
+ return convertBlocksToJupyterNotebook(notebook.blocks, {
179
+ notebookId: notebook.id,
180
+ notebookName: notebook.name,
181
+ executionMode: notebook.executionMode,
182
+ isModule: notebook.isModule,
183
+ workingDirectory: notebook.workingDirectory,
184
+ environment: deepnoteFile.environment,
185
+ execution: deepnoteFile.execution
186
+ });
187
+ }
188
+
189
+ //#endregion
190
+ //#region src/deepnote-to-marimo.ts
191
+ /** Current Marimo version for generated files */
192
+ const MARIMO_VERSION = "0.10.0";
193
+ /**
194
+ * Converts an array of Deepnote blocks into a Marimo app.
195
+ * This is the lowest-level conversion function.
196
+ *
197
+ * @param blocks - Array of DeepnoteBlock objects to convert
198
+ * @param notebookName - Name of the notebook (used for app title)
199
+ * @returns A MarimoApp object
200
+ */
201
+ function convertBlocksToMarimoApp(blocks, notebookName) {
202
+ return {
203
+ generatedWith: MARIMO_VERSION,
204
+ width: "medium",
205
+ title: notebookName,
206
+ cells: blocks.map((block) => convertBlockToCell$2(block))
207
+ };
208
+ }
209
+ /**
210
+ * Converts a Deepnote project into Marimo app objects.
211
+ * This is a pure conversion function that doesn't perform any file I/O.
212
+ * Each notebook in the Deepnote project is converted to a separate Marimo app.
213
+ *
214
+ * @param deepnoteFile - The deserialized Deepnote project file
215
+ * @returns Array of objects containing filename and corresponding Marimo app
216
+ */
217
+ function convertDeepnoteToMarimoApps(deepnoteFile) {
218
+ return deepnoteFile.project.notebooks.map((notebook) => {
219
+ const app = convertBlocksToMarimoApp(notebook.blocks, notebook.name);
220
+ return {
221
+ filename: `${sanitizeFileName(notebook.name)}.py`,
222
+ app
223
+ };
224
+ });
225
+ }
226
+ /**
227
+ * Serializes a Marimo app to a Python file string.
228
+ *
229
+ * @param app - The Marimo app to serialize
230
+ * @returns The serialized Python code string
231
+ */
232
+ function serializeMarimoFormat(app) {
233
+ const lines = [];
234
+ const hasMarkdownOrSqlCells = app.cells.some((cell) => cell.cellType === "markdown" || cell.cellType === "sql");
235
+ if (hasMarkdownOrSqlCells) lines.push("import marimo as mo");
236
+ else lines.push("import marimo");
237
+ lines.push("");
238
+ lines.push(`__generated_with = "${app.generatedWith || MARIMO_VERSION}"`);
239
+ const appOptions = [];
240
+ if (app.width) appOptions.push(`width="${app.width}"`);
241
+ if (app.title) appOptions.push(`title="${escapeString(app.title)}"`);
242
+ const marimoRef = hasMarkdownOrSqlCells ? "mo" : "marimo";
243
+ const optionsStr = appOptions.length > 0 ? appOptions.join(", ") : "";
244
+ lines.push(`app = ${marimoRef}.App(${optionsStr})`);
245
+ lines.push("");
246
+ lines.push("");
247
+ for (const cell of app.cells) {
248
+ const decoratorOptions = [];
249
+ if (cell.hidden) decoratorOptions.push("hide_code=True");
250
+ if (cell.disabled) decoratorOptions.push("disabled=True");
251
+ const decoratorStr = decoratorOptions.length > 0 ? `@app.cell(${decoratorOptions.join(", ")})` : "@app.cell";
252
+ lines.push(decoratorStr);
253
+ const funcName = cell.functionName || "__";
254
+ const params = cell.dependencies?.join(", ") || "";
255
+ lines.push(`def ${funcName}(${params}):`);
256
+ if (cell.cellType === "markdown") {
257
+ const escaped = escapeTripleQuote(cell.content);
258
+ lines.push(` mo.md(r"""`);
259
+ for (const contentLine of escaped.split("\n")) if (contentLine.trim() === "") lines.push("");
260
+ else lines.push(` ${contentLine}`);
261
+ lines.push(` """)`);
262
+ lines.push(" return");
263
+ } else if (cell.cellType === "sql") {
264
+ const escaped = escapeTripleQuote(cell.content);
265
+ const varName = cell.exports && cell.exports.length > 0 ? cell.exports[0] : "df";
266
+ const engineParam = cell.dependencies?.includes("engine") ? ", engine=engine" : "";
267
+ lines.push(` ${varName} = mo.sql(`);
268
+ lines.push(` f"""`);
269
+ for (const contentLine of escaped.split("\n")) if (contentLine === "" || contentLine.trim() === "") lines.push("");
270
+ else lines.push(` ${contentLine}`);
271
+ lines.push(` """${engineParam}`);
272
+ lines.push(` )`);
273
+ if (cell.exports && cell.exports.length > 0) lines.push(` return ${cell.exports.join(", ")},`);
274
+ else lines.push(" return");
275
+ } else {
276
+ const contentLines = cell.content.split("\n");
277
+ for (const contentLine of contentLines) if (contentLine.trim() === "") lines.push("");
278
+ else lines.push(` ${contentLine}`);
279
+ if (cell.exports && cell.exports.length > 0) lines.push(` return ${cell.exports.join(", ")},`);
280
+ else lines.push(" return");
281
+ }
282
+ lines.push("");
283
+ lines.push("");
284
+ }
285
+ lines.push("if __name__ == \"__main__\":");
286
+ lines.push(" app.run()");
287
+ lines.push("");
288
+ return lines.join("\n");
289
+ }
290
+ /**
291
+ * Converts a Deepnote project file into separate Marimo (.py) files.
292
+ * Each notebook in the Deepnote project becomes a separate .py file.
293
+ */
294
+ async function convertDeepnoteFileToMarimoFiles(deepnoteFilePath, options) {
295
+ const apps = convertDeepnoteToMarimoApps(deserializeDeepnoteFile(await fs.readFile(deepnoteFilePath, "utf-8")));
296
+ await fs.mkdir(options.outputDir, { recursive: true });
297
+ for (const { filename, app } of apps) {
298
+ const filePath = join(options.outputDir, filename);
299
+ try {
300
+ const content = serializeMarimoFormat(app);
301
+ await fs.writeFile(filePath, content, "utf-8");
302
+ } catch (err) {
303
+ const errorMessage = err instanceof Error ? err.message : String(err);
304
+ throw new Error(`Failed to write ${filename}: ${errorMessage}`, { cause: err });
305
+ }
306
+ }
307
+ }
308
+ function convertBlockToCell$2(block) {
309
+ const isMarkdown = isMarkdownBlockType(block.type);
310
+ const isSql = block.type === "sql";
311
+ const metadata = block.metadata || {};
312
+ let content;
313
+ let cellType;
314
+ if (isMarkdown) {
315
+ cellType = "markdown";
316
+ try {
317
+ content = createMarkdown(block);
318
+ } catch {
319
+ content = block.content || "";
320
+ }
321
+ } else if (isSql) {
322
+ cellType = "sql";
323
+ content = block.content || "";
324
+ } else if (block.type === "code") {
325
+ cellType = "code";
326
+ content = block.content || "";
327
+ } else {
328
+ cellType = "code";
329
+ try {
330
+ content = createPythonCode(block);
331
+ } catch {
332
+ content = block.content || "";
333
+ }
334
+ }
335
+ const dependencies = metadata.marimo_dependencies;
336
+ const exports = metadata.marimo_exports;
337
+ const hidden = metadata.is_code_hidden;
338
+ const disabled = metadata.marimo_disabled;
339
+ const functionName = metadata.marimo_function_name;
340
+ return {
341
+ cellType,
342
+ content,
343
+ ...functionName ? { functionName } : {},
344
+ ...dependencies && dependencies.length > 0 ? { dependencies } : {},
345
+ ...exports && exports.length > 0 ? { exports } : {},
346
+ ...hidden ? { hidden } : {},
347
+ ...disabled ? { disabled } : {}
348
+ };
349
+ }
350
+ /**
351
+ * Escapes a string for use in a Python double-quoted string literal.
352
+ * Handles backslashes, quotes, control characters, and non-printable characters.
353
+ */
354
+ function escapeString(str) {
355
+ return str.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(/[\x08]/g, "\\b").replace(/[\x0C]/g, "\\f").replace(/[\x00-\x07\x0B\x0E-\x1F\x7F-\x9F]/g, (char) => {
356
+ return `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`;
357
+ });
358
+ }
359
+ /**
360
+ * Escapes content for use in a Python raw triple-quoted string (r""").
361
+ * Raw strings don't interpret escape sequences, so we can't use backslashes to escape.
362
+ * When we encounter triple quotes, we break the raw string and concatenate with a regular string.
363
+ */
364
+ function escapeTripleQuote(str) {
365
+ return str.replace(/"""/g, "\"\"\"+'\"'+r\"\"\"");
366
+ }
367
+
368
+ //#endregion
369
+ //#region src/deepnote-to-percent.ts
370
+ /**
371
+ * Converts an array of Deepnote blocks into a percent format notebook.
372
+ * This is the lowest-level conversion function.
373
+ *
374
+ * @param blocks - Array of DeepnoteBlock objects to convert
375
+ * @returns A PercentNotebook object
376
+ */
377
+ function convertBlocksToPercentNotebook(blocks) {
378
+ return { cells: blocks.map((block) => convertBlockToCell$1(block)) };
379
+ }
380
+ /**
381
+ * Converts a Deepnote project into percent format notebook objects.
382
+ * This is a pure conversion function that doesn't perform any file I/O.
383
+ * Each notebook in the Deepnote project is converted to a separate percent notebook.
384
+ *
385
+ * @param deepnoteFile - The deserialized Deepnote project file
386
+ * @returns Array of objects containing filename and corresponding percent notebook
387
+ */
388
+ function convertDeepnoteToPercentNotebooks(deepnoteFile) {
389
+ return deepnoteFile.project.notebooks.map((notebook) => {
390
+ const percentNotebook = convertBlocksToPercentNotebook(notebook.blocks);
391
+ return {
392
+ filename: `${sanitizeFileName(notebook.name)}.py`,
393
+ notebook: percentNotebook
394
+ };
395
+ });
396
+ }
397
+ /**
398
+ * Serializes a percent format notebook to a string.
399
+ *
400
+ * @param notebook - The percent notebook to serialize
401
+ * @returns The serialized percent format string
402
+ */
403
+ function serializePercentFormat(notebook) {
404
+ const lines = [];
405
+ for (const cell of notebook.cells) {
406
+ let marker = "# %%";
407
+ if (cell.cellType === "markdown") marker += " [markdown]";
408
+ else if (cell.cellType === "raw") marker += " [raw]";
409
+ if (cell.title) marker += ` ${cell.title}`;
410
+ if (cell.tags && cell.tags.length > 0) {
411
+ const tagsStr = cell.tags.map((t) => JSON.stringify(t)).join(", ");
412
+ marker += ` tags=[${tagsStr}]`;
413
+ }
414
+ lines.push(marker);
415
+ if (cell.cellType === "markdown") {
416
+ const contentLines = cell.content.split("\n");
417
+ for (const contentLine of contentLines) if (contentLine === "") lines.push("#");
418
+ else lines.push(`# ${contentLine}`);
419
+ } else lines.push(cell.content);
420
+ lines.push("");
421
+ }
422
+ while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
423
+ return `${lines.join("\n")}\n`;
424
+ }
425
+ /**
426
+ * Converts a Deepnote project file into separate percent format (.py) files.
427
+ * Each notebook in the Deepnote project becomes a separate .py file.
428
+ */
429
+ async function convertDeepnoteFileToPercentFiles(deepnoteFilePath, options) {
430
+ const notebooks = convertDeepnoteToPercentNotebooks(deserializeDeepnoteFile(await fs.readFile(deepnoteFilePath, "utf-8")));
431
+ await fs.mkdir(options.outputDir, { recursive: true });
432
+ for (const { filename, notebook } of notebooks) {
433
+ const filePath = join(options.outputDir, filename);
434
+ const content = serializePercentFormat(notebook);
435
+ await fs.writeFile(filePath, content, "utf-8");
436
+ }
437
+ }
438
+ function convertBlockToCell$1(block) {
439
+ const isMarkdown = isMarkdownBlockType(block.type);
440
+ let content;
441
+ if (isMarkdown) try {
442
+ content = createMarkdown(block);
443
+ } catch {
444
+ content = block.content || "";
445
+ }
446
+ else if (block.type === "code") content = block.content || "";
447
+ else try {
448
+ content = createPythonCode(block);
449
+ } catch {
450
+ content = block.content || "";
451
+ }
452
+ const title = block.metadata?.title;
453
+ const tags = block.metadata?.tags;
454
+ return {
455
+ cellType: block.metadata?.percent_cell_type === "raw" ? "raw" : isMarkdown ? "markdown" : "code",
456
+ content,
457
+ ...title ? { title } : {},
458
+ ...tags && tags.length > 0 ? { tags } : {}
459
+ };
460
+ }
461
+
462
+ //#endregion
463
+ //#region src/deepnote-to-quarto.ts
464
+ /**
465
+ * Converts an array of Deepnote blocks into a Quarto document.
466
+ * This is the lowest-level conversion function.
467
+ *
468
+ * @param blocks - Array of DeepnoteBlock objects to convert
469
+ * @param notebookName - Name of the notebook (used for document title)
470
+ * @returns A QuartoDocument object
471
+ */
472
+ function convertBlocksToQuartoDocument(blocks, notebookName) {
473
+ const cells = blocks.map((block) => convertBlockToCell(block));
474
+ return {
475
+ frontmatter: {
476
+ title: notebookName,
477
+ jupyter: "python3"
478
+ },
479
+ cells
480
+ };
481
+ }
482
+ /**
483
+ * Converts a Deepnote project into Quarto document objects.
484
+ * This is a pure conversion function that doesn't perform any file I/O.
485
+ * Each notebook in the Deepnote project is converted to a separate Quarto document.
486
+ *
487
+ * @param deepnoteFile - The deserialized Deepnote project file
488
+ * @returns Array of objects containing filename and corresponding Quarto document
489
+ */
490
+ function convertDeepnoteToQuartoDocuments(deepnoteFile) {
491
+ return deepnoteFile.project.notebooks.map((notebook) => {
492
+ const document = convertBlocksToQuartoDocument(notebook.blocks, notebook.name);
493
+ return {
494
+ filename: `${sanitizeFileName(notebook.name)}.qmd`,
495
+ document
496
+ };
497
+ });
498
+ }
499
+ /**
500
+ * Serializes a Quarto document to a string.
501
+ *
502
+ * @param document - The Quarto document to serialize
503
+ * @returns The serialized Quarto format string
504
+ */
505
+ function serializeQuartoFormat(document) {
506
+ const lines = [];
507
+ if (document.frontmatter && Object.keys(document.frontmatter).length > 0) {
508
+ lines.push("---");
509
+ for (const [key, value] of Object.entries(document.frontmatter)) if (typeof value === "string") if (value.includes(":") || value.includes("#") || value.includes("'") || value.includes("\"")) lines.push(`${key}: "${value.replace(/"/g, "\\\"")}"`);
510
+ else lines.push(`${key}: ${value}`);
511
+ else if (typeof value === "boolean") lines.push(`${key}: ${value}`);
512
+ else if (value !== void 0) lines.push(`${key}: ${JSON.stringify(value)}`);
513
+ lines.push("---");
514
+ lines.push("");
515
+ }
516
+ let previousCellType = null;
517
+ for (const cell of document.cells) {
518
+ if (cell.cellType === "markdown") {
519
+ if (previousCellType === "markdown") {
520
+ lines.push("<!-- cell -->");
521
+ lines.push("");
522
+ }
523
+ lines.push(cell.content);
524
+ lines.push("");
525
+ } else {
526
+ const language = cell.language || "python";
527
+ lines.push(`\`\`\`{${language}}`);
528
+ if (cell.options) {
529
+ const optionLines = serializeQuartoCellOptions(cell.options);
530
+ lines.push(...optionLines);
531
+ }
532
+ lines.push(cell.content);
533
+ lines.push("```");
534
+ lines.push("");
535
+ }
536
+ previousCellType = cell.cellType;
537
+ }
538
+ while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
539
+ return `${lines.join("\n")}\n`;
540
+ }
541
+ /**
542
+ * Serializes Quarto cell options to #| format lines.
543
+ */
544
+ function serializeQuartoCellOptions(options) {
545
+ const lines = [];
546
+ if (options.label) lines.push(`#| label: ${options.label}`);
547
+ if (options.echo !== void 0) lines.push(`#| echo: ${options.echo}`);
548
+ if (options.eval !== void 0) lines.push(`#| eval: ${options.eval}`);
549
+ if (options.output !== void 0) lines.push(`#| output: ${options.output}`);
550
+ if (options.figCap) lines.push(`#| fig-cap: "${options.figCap}"`);
551
+ if (options.figWidth !== void 0) lines.push(`#| fig-width: ${options.figWidth}`);
552
+ if (options.figHeight !== void 0) lines.push(`#| fig-height: ${options.figHeight}`);
553
+ if (options.tblCap) lines.push(`#| tbl-cap: "${options.tblCap}"`);
554
+ if (options.warning !== void 0) lines.push(`#| warning: ${options.warning}`);
555
+ if (options.message !== void 0) lines.push(`#| message: ${options.message}`);
556
+ if (options.raw) for (const [key, value] of Object.entries(options.raw)) if (typeof value === "string") lines.push(`#| ${key}: "${value}"`);
557
+ else lines.push(`#| ${key}: ${value}`);
558
+ return lines;
559
+ }
560
+ /**
561
+ * Converts a Deepnote project file into separate Quarto (.qmd) files.
562
+ * Each notebook in the Deepnote project becomes a separate .qmd file.
563
+ */
564
+ async function convertDeepnoteFileToQuartoFiles(deepnoteFilePath, options) {
565
+ const documents = convertDeepnoteToQuartoDocuments(deserializeDeepnoteFile(await fs.readFile(deepnoteFilePath, "utf-8")));
566
+ await fs.mkdir(options.outputDir, { recursive: true });
567
+ for (const { filename, document } of documents) {
568
+ const filePath = join(options.outputDir, filename);
569
+ const content = serializeQuartoFormat(document);
570
+ await fs.writeFile(filePath, content, "utf-8");
571
+ }
572
+ }
573
+ function convertBlockToCell(block) {
574
+ const isMarkdown = isMarkdownBlockType(block.type);
575
+ let content;
576
+ if (isMarkdown) try {
577
+ content = createMarkdown(block);
578
+ } catch {
579
+ content = block.content || "";
580
+ }
581
+ else if (block.type === "code") content = block.content || "";
582
+ else try {
583
+ content = createPythonCode(block);
584
+ } catch {
585
+ content = block.content || "";
586
+ }
587
+ let options;
588
+ const metadata = block.metadata || {};
589
+ if (metadata.quarto_label || metadata.is_code_hidden || metadata.quarto_fig_cap || metadata.quarto_tbl_cap) {
590
+ options = {};
591
+ if (metadata.quarto_label) options.label = metadata.quarto_label;
592
+ if (metadata.is_code_hidden) options.echo = false;
593
+ if (metadata.quarto_fig_cap) options.figCap = metadata.quarto_fig_cap;
594
+ if (metadata.quarto_tbl_cap) options.tblCap = metadata.quarto_tbl_cap;
595
+ if (metadata.quarto_options) options.raw = metadata.quarto_options;
596
+ }
597
+ const cell = {
598
+ cellType: isMarkdown ? "markdown" : "code",
599
+ content,
600
+ ...options ? { options } : {}
601
+ };
602
+ if (metadata.language && metadata.language !== "python") cell.language = metadata.language;
603
+ return cell;
604
+ }
605
+
606
+ //#endregion
607
+ //#region src/jupyter-to-deepnote.ts
608
+ /**
609
+ * Converts a single Jupyter Notebook into an array of Deepnote blocks.
610
+ * This is the lowest-level conversion function, suitable for use in Deepnote Cloud.
611
+ *
612
+ * @param notebook - The Jupyter notebook object to convert
613
+ * @param options - Optional conversion options including custom ID generator
614
+ * @returns Array of DeepnoteBlock objects
615
+ *
616
+ * @example
617
+ * ```typescript
618
+ * import { convertJupyterNotebookToBlocks } from '@deepnote/convert'
619
+ *
620
+ * const notebook = JSON.parse(ipynbContent)
621
+ * const blocks = convertJupyterNotebookToBlocks(notebook, {
622
+ * idGenerator: () => myCustomIdGenerator()
623
+ * })
624
+ * ```
625
+ */
626
+ function convertJupyterNotebookToBlocks(notebook, options) {
627
+ const idGenerator = options?.idGenerator ?? v4;
628
+ return notebook.cells.map((cell, index) => convertCellToBlock$3(cell, index, idGenerator));
629
+ }
630
+ /**
631
+ * Converts Jupyter Notebook objects into a Deepnote project file.
632
+ * This is a pure conversion function that doesn't perform any file I/O.
633
+ *
634
+ * @param notebooks - Array of Jupyter notebooks with filenames
635
+ * @param options - Conversion options including project name
636
+ * @returns A DeepnoteFile object
637
+ */
638
+ function convertJupyterNotebooksToDeepnote(notebooks, options) {
639
+ let environment;
640
+ let execution;
641
+ for (const { notebook } of notebooks) {
642
+ if (!environment && notebook.metadata?.deepnote_environment) {
643
+ const parsed = environmentSchema.safeParse(notebook.metadata.deepnote_environment);
644
+ if (parsed.success) environment = parsed.data;
645
+ }
646
+ if (!execution && notebook.metadata?.deepnote_execution) {
647
+ const parsed = executionSchema.safeParse(notebook.metadata.deepnote_execution);
648
+ if (parsed.success) execution = parsed.data;
649
+ }
650
+ }
651
+ const firstNotebookId = notebooks.length > 0 ? notebooks[0].notebook.metadata?.deepnote_notebook_id ?? v4() : void 0;
652
+ const deepnoteFile = {
653
+ environment,
654
+ execution,
655
+ metadata: { createdAt: (/* @__PURE__ */ new Date()).toISOString() },
656
+ project: {
657
+ id: v4(),
658
+ initNotebookId: firstNotebookId,
659
+ integrations: [],
660
+ name: options.projectName,
661
+ notebooks: [],
662
+ settings: {}
663
+ },
664
+ version: "1.0.0"
665
+ };
666
+ for (let i = 0; i < notebooks.length; i++) {
667
+ const { filename, notebook } = notebooks[i];
668
+ const filenameWithoutExt = basename(filename, extname(filename)) || "Untitled notebook";
669
+ const blocks = convertJupyterNotebookToBlocks(notebook);
670
+ const notebookId = notebook.metadata?.deepnote_notebook_id;
671
+ const notebookName = notebook.metadata?.deepnote_notebook_name;
672
+ const executionMode = notebook.metadata?.deepnote_execution_mode;
673
+ const isModule = notebook.metadata?.deepnote_is_module;
674
+ const workingDirectory = notebook.metadata?.deepnote_working_directory;
675
+ const resolvedNotebookId = i === 0 && firstNotebookId ? firstNotebookId : notebookId ?? v4();
676
+ deepnoteFile.project.notebooks.push({
677
+ blocks,
678
+ executionMode: executionMode ?? "block",
679
+ id: resolvedNotebookId,
680
+ isModule: isModule ?? false,
681
+ name: notebookName ?? filenameWithoutExt,
682
+ workingDirectory
683
+ });
684
+ }
685
+ return deepnoteFile;
686
+ }
687
+ /**
688
+ * Converts multiple Jupyter Notebook (.ipynb) files into a single Deepnote project file.
689
+ */
690
+ async function convertIpynbFilesToDeepnoteFile(inputFilePaths, options) {
691
+ const notebooks = [];
692
+ for (const filePath of inputFilePaths) {
693
+ const notebook = await parseIpynbFile(filePath);
694
+ notebooks.push({
695
+ filename: basename(filePath),
696
+ notebook
697
+ });
698
+ }
699
+ const yamlContent = stringify(convertJupyterNotebooksToDeepnote(notebooks, { projectName: options.projectName }));
700
+ const parentDir = dirname(options.outputPath);
701
+ await fs.mkdir(parentDir, { recursive: true });
702
+ await fs.writeFile(options.outputPath, yamlContent, "utf-8");
703
+ }
704
+ async function parseIpynbFile(filePath) {
705
+ let ipynbJson;
706
+ try {
707
+ ipynbJson = await fs.readFile(filePath, "utf-8");
708
+ } catch (error) {
709
+ const message = error instanceof Error ? error.message : String(error);
710
+ throw new Error(`Failed to read ${filePath}: ${message}`);
711
+ }
712
+ try {
713
+ return JSON.parse(ipynbJson);
714
+ } catch (error) {
715
+ const message = error instanceof Error ? error.message : String(error);
716
+ throw new Error(`Failed to parse ${filePath}: invalid JSON - ${message}`);
717
+ }
718
+ }
719
+ function convertCellToBlock$3(cell, index, idGenerator) {
720
+ let source = Array.isArray(cell.source) ? cell.source.join("") : cell.source;
721
+ const cellId = cell.metadata?.cell_id;
722
+ const deepnoteCellType = cell.metadata?.deepnote_cell_type;
723
+ const sortingKey = cell.metadata?.deepnote_sorting_key;
724
+ const contentHash = cell.metadata?.deepnote_content_hash;
725
+ const executionStartedAt = cell.metadata?.deepnote_execution_started_at;
726
+ const executionFinishedAt = cell.metadata?.deepnote_execution_finished_at;
727
+ const blockGroup = cell.metadata?.deepnote_block_group ?? cell.block_group ?? idGenerator();
728
+ const deepnoteSource = cell.metadata?.deepnote_source;
729
+ if (deepnoteSource !== void 0) source = deepnoteSource;
730
+ const blockType = deepnoteCellType ?? (cell.cell_type === "code" ? "code" : "markdown");
731
+ const originalMetadata = { ...cell.metadata };
732
+ delete originalMetadata.cell_id;
733
+ delete originalMetadata.deepnote_cell_type;
734
+ delete originalMetadata.deepnote_block_group;
735
+ delete originalMetadata.deepnote_sorting_key;
736
+ delete originalMetadata.deepnote_source;
737
+ delete originalMetadata.deepnote_content_hash;
738
+ delete originalMetadata.deepnote_execution_started_at;
739
+ delete originalMetadata.deepnote_execution_finished_at;
740
+ delete cell.block_group;
741
+ const executionCount = cell.execution_count ?? void 0;
742
+ const hasExecutionCount = executionCount !== void 0;
743
+ const hasOutputs = cell.cell_type === "code" && cell.outputs !== void 0;
744
+ return {
745
+ blockGroup,
746
+ content: source,
747
+ ...contentHash ? { contentHash } : {},
748
+ ...hasExecutionCount ? { executionCount } : {},
749
+ ...executionFinishedAt ? { executionFinishedAt } : {},
750
+ ...executionStartedAt ? { executionStartedAt } : {},
751
+ id: cellId ?? idGenerator(),
752
+ metadata: originalMetadata,
753
+ ...hasOutputs ? { outputs: cell.outputs } : {},
754
+ sortingKey: sortingKey ?? createSortingKey(index),
755
+ type: blockType
756
+ };
757
+ }
758
+
759
+ //#endregion
760
+ //#region src/marimo-to-deepnote.ts
761
+ /**
762
+ * Splits a string on commas that are at the top level (not inside parentheses, brackets, braces, or string literals).
763
+ * This handles cases like "func(a, b), other" and 'return "a,b", x' correctly.
764
+ * Supports single quotes, double quotes, and backticks, with proper escape handling.
765
+ */
766
+ function splitOnTopLevelCommas(str) {
767
+ const results = [];
768
+ let current = "";
769
+ let depth = 0;
770
+ let inString = null;
771
+ let escaped = false;
772
+ for (const char of str) {
773
+ if (escaped) {
774
+ current += char;
775
+ escaped = false;
776
+ continue;
777
+ }
778
+ if (char === "\\") {
779
+ current += char;
780
+ escaped = true;
781
+ continue;
782
+ }
783
+ if (char === "\"" || char === "'" || char === "`") {
784
+ if (inString === null) inString = char;
785
+ else if (inString === char) inString = null;
786
+ current += char;
787
+ continue;
788
+ }
789
+ if (inString !== null) {
790
+ current += char;
791
+ continue;
792
+ }
793
+ if (char === "(" || char === "[" || char === "{") {
794
+ depth++;
795
+ current += char;
796
+ } else if (char === ")" || char === "]" || char === "}") {
797
+ depth--;
798
+ current += char;
799
+ } else if (char === "," && depth === 0) {
800
+ results.push(current);
801
+ current = "";
802
+ } else current += char;
803
+ }
804
+ if (current) results.push(current);
805
+ return results;
806
+ }
807
+ /**
808
+ * Parses a Marimo Python file into a MarimoApp structure.
809
+ *
810
+ * @param content - The raw content of the .py file
811
+ * @returns A MarimoApp object
812
+ *
813
+ * @example
814
+ * ```typescript
815
+ * const content = `import marimo
816
+ *
817
+ * app = marimo.App()
818
+ *
819
+ * @app.cell
820
+ * def __():
821
+ * print("hello")
822
+ * return
823
+ * `
824
+ * const app = parseMarimoFormat(content)
825
+ * ```
826
+ */
827
+ function parseMarimoFormat(content) {
828
+ const cells = [];
829
+ const generatedWith = /__generated_with\s*=\s*["']([^"']+)["']/.exec(content)?.[1];
830
+ const width = /(?:marimo|mo)\.App\([^)]*width\s*=\s*["']([^"']+)["']/.exec(content)?.[1];
831
+ const title = /(?:marimo|mo)\.App\([^)]*title\s*=\s*["']([^"']+)["']/.exec(content)?.[1];
832
+ const cellRegex = /@app\.cell(?:\(([^)]*)\))?\s*\n\s*def\s+(\w+)\s*\(([^)]*)\)\s*(?:->.*?)?\s*:\s*\n([\s\S]*?)(?=@app\.cell|if\s+__name__|$)/g;
833
+ let match = cellRegex.exec(content);
834
+ while (match !== null) {
835
+ const decoratorArgs = match[1] || "";
836
+ const functionName = match[2];
837
+ const params = match[3].trim();
838
+ let body = match[4];
839
+ const dependencies = params ? params.split(",").map((p) => p.trim()).filter((p) => p.length > 0) : void 0;
840
+ const hidden = /hide_code\s*=\s*True/.test(decoratorArgs);
841
+ const disabled = /disabled\s*=\s*True/.test(decoratorArgs);
842
+ const lines = body.split("\n");
843
+ const firstNonEmptyLine = lines.find((l) => l.trim().length > 0);
844
+ const baseIndent = (firstNonEmptyLine ? /^(\s*)/.exec(firstNonEmptyLine) : null)?.[1] || "";
845
+ let exports;
846
+ for (let i = lines.length - 1; i >= 0; i--) {
847
+ const line = lines[i];
848
+ if (line.startsWith(baseIndent) && !line.startsWith(`${baseIndent} `) && !line.startsWith(`${baseIndent}\t`)) {
849
+ const lineContent = line.slice(baseIndent.length);
850
+ const returnMatch = /^return\s*([^\n]*?)(?:,\s*)?$/.exec(lineContent);
851
+ if (returnMatch) {
852
+ const returnVal = returnMatch[1].trim();
853
+ if (returnVal && returnVal !== "None" && returnVal !== "") {
854
+ exports = splitOnTopLevelCommas(returnVal.replace(/^\(|\)$/g, "").replace(/,\s*$/, "")).map((e) => e.trim()).filter((e) => e.length > 0 && e !== "None");
855
+ if (exports.length === 0) exports = void 0;
856
+ }
857
+ break;
858
+ }
859
+ }
860
+ }
861
+ const filteredLines = lines.filter((line) => {
862
+ const normalizedLine = line.replace(/\t/g, " ");
863
+ const normalizedBaseIndent = baseIndent.replace(/\t/g, " ");
864
+ if (normalizedLine.startsWith(normalizedBaseIndent) && !normalizedLine.startsWith(`${normalizedBaseIndent} `) && !normalizedLine.startsWith(`${normalizedBaseIndent}\t`)) {
865
+ const lineContent = normalizedLine.slice(normalizedBaseIndent.length);
866
+ if (/^return\s*(?:[^\n]*)?(?:,\s*)?$/.test(lineContent)) return false;
867
+ }
868
+ return true;
869
+ });
870
+ let processedBody;
871
+ if (baseIndent.length > 0) processedBody = filteredLines.map((l) => l.startsWith(baseIndent) ? l.slice(baseIndent.length) : l).join("\n");
872
+ else processedBody = filteredLines.join("\n");
873
+ body = processedBody.trim();
874
+ const isMarkdown = /^\s*mo\.md\s*\(/.test(body) || /^\s*marimo\.md\s*\(/.test(body);
875
+ const isSql = /^\s*(?:\w+\s*=\s*)?(?:mo|marimo)\.sql\s*\(/.test(body);
876
+ if (isMarkdown) {
877
+ const mdMatch = /(?:mo|marimo)\.md\s*\(\s*(?:(?:r|f|rf|fr)?(?:"""([\s\S]*?)"""|'''([\s\S]*?)'''|"([^"]*)"|'([^']*)'))\s*\)/.exec(body);
878
+ if (mdMatch) {
879
+ const mdContent = mdMatch[1] || mdMatch[2] || mdMatch[3] || mdMatch[4] || "";
880
+ cells.push({
881
+ cellType: "markdown",
882
+ content: mdContent.trim(),
883
+ functionName,
884
+ ...dependencies && dependencies.length > 0 ? { dependencies } : {},
885
+ ...hidden ? { hidden } : {},
886
+ ...disabled ? { disabled } : {}
887
+ });
888
+ }
889
+ } else if (isSql) {
890
+ const sqlMatch = /(?:mo|marimo)\.sql\s*\(\s*(?:f)?(?:"""([\s\S]*?)"""|'''([\s\S]*?)'''|"([^"]*)"|'([^']*)')\s*(?:,[\s\S]*)?\)/.exec(body);
891
+ if (sqlMatch) {
892
+ const sqlQuery = sqlMatch[1] || sqlMatch[2] || sqlMatch[3] || sqlMatch[4] || "";
893
+ cells.push({
894
+ cellType: "sql",
895
+ content: sqlQuery.trim(),
896
+ functionName,
897
+ ...dependencies && dependencies.length > 0 ? { dependencies } : {},
898
+ ...exports && exports.length > 0 ? { exports } : {},
899
+ ...hidden ? { hidden } : {},
900
+ ...disabled ? { disabled } : {}
901
+ });
902
+ }
903
+ } else cells.push({
904
+ cellType: "code",
905
+ content: body,
906
+ functionName,
907
+ ...dependencies && dependencies.length > 0 ? { dependencies } : {},
908
+ ...exports && exports.length > 0 ? { exports } : {},
909
+ ...hidden ? { hidden } : {},
910
+ ...disabled ? { disabled } : {}
911
+ });
912
+ match = cellRegex.exec(content);
913
+ }
914
+ return {
915
+ ...generatedWith ? { generatedWith } : {},
916
+ ...width ? { width } : {},
917
+ ...title ? { title } : {},
918
+ cells
919
+ };
920
+ }
921
+ /**
922
+ * Converts a single Marimo app into an array of Deepnote blocks.
923
+ * This is the lowest-level conversion function.
924
+ *
925
+ * @param app - The Marimo app object to convert
926
+ * @param options - Optional conversion options including custom ID generator
927
+ * @returns Array of DeepnoteBlock objects
928
+ */
929
+ function convertMarimoAppToBlocks(app, options) {
930
+ const idGenerator = options?.idGenerator ?? v4;
931
+ return app.cells.map((cell, index) => convertCellToBlock$2(cell, index, idGenerator));
932
+ }
933
+ /**
934
+ * Converts Marimo app objects into a Deepnote project file.
935
+ * This is a pure conversion function that doesn't perform any file I/O.
936
+ *
937
+ * @param apps - Array of Marimo apps with filenames
938
+ * @param options - Conversion options including project name and optional ID generator
939
+ * @returns A DeepnoteFile object
940
+ */
941
+ function convertMarimoAppsToDeepnote(apps, options) {
942
+ const idGenerator = options.idGenerator ?? v4;
943
+ const firstNotebookId = apps.length > 0 ? idGenerator() : void 0;
944
+ const deepnoteFile = {
945
+ metadata: { createdAt: (/* @__PURE__ */ new Date()).toISOString() },
946
+ project: {
947
+ id: idGenerator(),
948
+ initNotebookId: firstNotebookId,
949
+ integrations: [],
950
+ name: options.projectName,
951
+ notebooks: [],
952
+ settings: {}
953
+ },
954
+ version: "1.0.0"
955
+ };
956
+ for (let i = 0; i < apps.length; i++) {
957
+ const { filename, app } = apps[i];
958
+ const filenameWithoutExt = basename(filename, extname(filename)) || "Untitled notebook";
959
+ const notebookName = app.title || filenameWithoutExt;
960
+ const blocks = convertMarimoAppToBlocks(app, { idGenerator });
961
+ const notebookId = i === 0 && firstNotebookId ? firstNotebookId : idGenerator();
962
+ deepnoteFile.project.notebooks.push({
963
+ blocks,
964
+ executionMode: "block",
965
+ id: notebookId,
966
+ isModule: false,
967
+ name: notebookName
968
+ });
969
+ }
970
+ return deepnoteFile;
971
+ }
972
+ /**
973
+ * Converts multiple Marimo (.py) files into a single Deepnote project file.
974
+ */
975
+ async function convertMarimoFilesToDeepnoteFile(inputFilePaths, options) {
976
+ const apps = [];
977
+ for (const filePath of inputFilePaths) try {
978
+ const app = parseMarimoFormat(await fs.readFile(filePath, "utf-8"));
979
+ apps.push({
980
+ filename: basename(filePath),
981
+ app
982
+ });
983
+ } catch (err) {
984
+ const errorMessage = err instanceof Error ? err.message : String(err);
985
+ const errorStack = err instanceof Error ? err.stack : void 0;
986
+ throw new Error(`Failed to read or parse file ${basename(filePath)}: ${errorMessage}`, { cause: errorStack ? {
987
+ originalError: err,
988
+ stack: errorStack
989
+ } : err });
990
+ }
991
+ const yamlContent = stringify(convertMarimoAppsToDeepnote(apps, { projectName: options.projectName }));
992
+ const parentDir = dirname(options.outputPath);
993
+ await fs.mkdir(parentDir, { recursive: true });
994
+ await fs.writeFile(options.outputPath, yamlContent, "utf-8");
995
+ }
996
+ function convertCellToBlock$2(cell, index, idGenerator) {
997
+ let blockType;
998
+ if (cell.cellType === "markdown") blockType = "markdown";
999
+ else if (cell.cellType === "sql") blockType = "sql";
1000
+ else blockType = "code";
1001
+ const metadata = {};
1002
+ if (cell.dependencies && cell.dependencies.length > 0) metadata.marimo_dependencies = cell.dependencies;
1003
+ if (cell.exports && cell.exports.length > 0) metadata.marimo_exports = cell.exports;
1004
+ if (cell.hidden) metadata.is_code_hidden = true;
1005
+ if (cell.disabled) metadata.marimo_disabled = true;
1006
+ if (cell.functionName && cell.functionName !== "__") metadata.marimo_function_name = cell.functionName;
1007
+ if (cell.cellType === "sql" && cell.exports && cell.exports.length > 0) metadata.deepnote_variable_name = cell.exports[0];
1008
+ return {
1009
+ blockGroup: idGenerator(),
1010
+ content: cell.content,
1011
+ id: idGenerator(),
1012
+ metadata: Object.keys(metadata).length > 0 ? metadata : {},
1013
+ sortingKey: createSortingKey(index),
1014
+ type: blockType
1015
+ };
1016
+ }
1017
+
1018
+ //#endregion
1019
+ //#region src/percent-to-deepnote.ts
1020
+ /**
1021
+ * Parses a percent format Python file into a PercentNotebook structure.
1022
+ *
1023
+ * @param content - The raw content of the .py file
1024
+ * @returns A PercentNotebook object
1025
+ *
1026
+ * @example
1027
+ * ```typescript
1028
+ * const content = `# %% [markdown]
1029
+ * # # My Title
1030
+ *
1031
+ * # %%
1032
+ * print("hello")
1033
+ * `
1034
+ * const notebook = parsePercentFormat(content)
1035
+ * ```
1036
+ */
1037
+ function parsePercentFormat(content) {
1038
+ const cells = [];
1039
+ const lines = content.split("\n");
1040
+ if (lines.length === 0 || lines.length === 1 && lines[0] === "") return { cells: [] };
1041
+ const cellMarkerRegex = /^# %%\s*(?:\[(\w+)\])?\s*(.*)$/;
1042
+ let currentCell = null;
1043
+ let currentContent = [];
1044
+ function finalizeCell() {
1045
+ if (currentCell) {
1046
+ if (currentCell.cellType === "markdown") currentCell.content = currentContent.map((line) => {
1047
+ if (line.startsWith("# ")) return line.slice(2);
1048
+ if (line === "#") return "";
1049
+ return line;
1050
+ }).join("\n").trim();
1051
+ else currentCell.content = currentContent.join("\n").trim();
1052
+ cells.push(currentCell);
1053
+ }
1054
+ currentContent = [];
1055
+ }
1056
+ for (const line of lines) {
1057
+ const match = cellMarkerRegex.exec(line);
1058
+ if (match) {
1059
+ finalizeCell();
1060
+ const cellTypeStr = match[1]?.toLowerCase() || "code";
1061
+ const rest = match[2]?.trim() || "";
1062
+ let cellType = "code";
1063
+ if (cellTypeStr === "markdown" || cellTypeStr === "md") cellType = "markdown";
1064
+ else if (cellTypeStr === "raw") cellType = "raw";
1065
+ let title;
1066
+ let tags;
1067
+ const tagsMatch = /tags\s*=\s*\[([^\]]*)\]/.exec(rest);
1068
+ if (tagsMatch) {
1069
+ tags = tagsMatch[1].split(",").map((t) => t.trim().replace(/^["']|["']$/g, "")).filter((t) => t.length > 0);
1070
+ const titlePart = rest.replace(/tags\s*=\s*\[[^\]]*\]/, "").trim();
1071
+ if (titlePart) title = titlePart;
1072
+ } else if (rest) title = rest;
1073
+ currentCell = {
1074
+ cellType,
1075
+ content: "",
1076
+ ...title ? { title } : {},
1077
+ ...tags && tags.length > 0 ? { tags } : {}
1078
+ };
1079
+ } else if (currentCell) currentContent.push(line);
1080
+ else if (line.trim() !== "" || currentContent.length > 0) {
1081
+ currentCell = {
1082
+ cellType: "code",
1083
+ content: ""
1084
+ };
1085
+ currentContent.push(line);
1086
+ }
1087
+ }
1088
+ finalizeCell();
1089
+ return { cells };
1090
+ }
1091
+ /**
1092
+ * Converts a single percent format notebook into an array of Deepnote blocks.
1093
+ * This is the lowest-level conversion function.
1094
+ *
1095
+ * @param notebook - The percent notebook object to convert
1096
+ * @param options - Optional conversion options including custom ID generator
1097
+ * @returns Array of DeepnoteBlock objects
1098
+ */
1099
+ function convertPercentNotebookToBlocks(notebook, options) {
1100
+ const idGenerator = options?.idGenerator ?? v4;
1101
+ return notebook.cells.map((cell, index) => convertCellToBlock$1(cell, index, idGenerator));
1102
+ }
1103
+ /**
1104
+ * Converts percent format notebook objects into a Deepnote project file.
1105
+ * This is a pure conversion function that doesn't perform any file I/O.
1106
+ *
1107
+ * @param notebooks - Array of percent notebooks with filenames
1108
+ * @param options - Conversion options including project name
1109
+ * @returns A DeepnoteFile object
1110
+ */
1111
+ function convertPercentNotebooksToDeepnote(notebooks, options) {
1112
+ const firstNotebookId = notebooks.length > 0 ? v4() : void 0;
1113
+ const deepnoteFile = {
1114
+ metadata: { createdAt: (/* @__PURE__ */ new Date()).toISOString() },
1115
+ project: {
1116
+ id: v4(),
1117
+ initNotebookId: firstNotebookId,
1118
+ integrations: [],
1119
+ name: options.projectName,
1120
+ notebooks: [],
1121
+ settings: {}
1122
+ },
1123
+ version: "1.0.0"
1124
+ };
1125
+ for (let i = 0; i < notebooks.length; i++) {
1126
+ const { filename, notebook } = notebooks[i];
1127
+ const filenameWithoutExt = basename(filename, extname(filename)) || "Untitled notebook";
1128
+ const blocks = convertPercentNotebookToBlocks(notebook);
1129
+ const notebookId = i === 0 && firstNotebookId ? firstNotebookId : v4();
1130
+ deepnoteFile.project.notebooks.push({
1131
+ blocks,
1132
+ executionMode: "block",
1133
+ id: notebookId,
1134
+ isModule: false,
1135
+ name: filenameWithoutExt
1136
+ });
1137
+ }
1138
+ return deepnoteFile;
1139
+ }
1140
+ /**
1141
+ * Converts multiple percent format (.py) files into a single Deepnote project file.
1142
+ */
1143
+ async function convertPercentFilesToDeepnoteFile(inputFilePaths, options) {
1144
+ const notebooks = [];
1145
+ for (const filePath of inputFilePaths) {
1146
+ const notebook = parsePercentFormat(await fs.readFile(filePath, "utf-8"));
1147
+ notebooks.push({
1148
+ filename: basename(filePath),
1149
+ notebook
1150
+ });
1151
+ }
1152
+ const yamlContent = stringify(convertPercentNotebooksToDeepnote(notebooks, { projectName: options.projectName }));
1153
+ const parentDir = dirname(options.outputPath);
1154
+ await fs.mkdir(parentDir, { recursive: true });
1155
+ await fs.writeFile(options.outputPath, yamlContent, "utf-8");
1156
+ }
1157
+ function convertCellToBlock$1(cell, index, idGenerator) {
1158
+ const blockType = cell.cellType === "markdown" || cell.cellType === "raw" ? "markdown" : "code";
1159
+ const metadata = {};
1160
+ if (cell.cellType === "raw") metadata.percent_cell_type = "raw";
1161
+ if (cell.title) metadata.title = cell.title;
1162
+ if (cell.tags && cell.tags.length > 0) metadata.tags = cell.tags;
1163
+ return {
1164
+ blockGroup: idGenerator(),
1165
+ content: cell.content,
1166
+ id: idGenerator(),
1167
+ metadata: Object.keys(metadata).length > 0 ? metadata : {},
1168
+ sortingKey: createSortingKey(index),
1169
+ type: blockType
1170
+ };
1171
+ }
1172
+
1173
+ //#endregion
1174
+ //#region src/quarto-to-deepnote.ts
1175
+ /**
1176
+ * Parses a Quarto (.qmd) file into a QuartoDocument structure.
1177
+ *
1178
+ * @param content - The raw content of the .qmd file
1179
+ * @returns A QuartoDocument object
1180
+ *
1181
+ * @example
1182
+ * ```typescript
1183
+ * const content = `---
1184
+ * title: "My Document"
1185
+ * ---
1186
+ *
1187
+ * # Introduction
1188
+ *
1189
+ * \`\`\`{python}
1190
+ * print("hello")
1191
+ * \`\`\`
1192
+ * `
1193
+ * const doc = parseQuartoFormat(content)
1194
+ * ```
1195
+ */
1196
+ function parseQuartoFormat(content) {
1197
+ const cells = [];
1198
+ let frontmatter;
1199
+ let mainContent = content;
1200
+ const frontmatterMatch = /^---\r?\n([\s\S]*?)---\r?\n?/.exec(content);
1201
+ if (frontmatterMatch) {
1202
+ frontmatter = parseYamlFrontmatter(frontmatterMatch[1]);
1203
+ mainContent = content.slice(frontmatterMatch[0].length);
1204
+ }
1205
+ const codeChunkRegex = /```\{([\w-]+)\}\r?\n([\s\S]*?)```/g;
1206
+ let lastIndex = 0;
1207
+ let match = codeChunkRegex.exec(mainContent);
1208
+ while (match !== null) {
1209
+ const markdownBefore = mainContent.slice(lastIndex, match.index).trim();
1210
+ if (markdownBefore) addMarkdownCells(cells, markdownBefore);
1211
+ const language = match[1];
1212
+ let codeContent = match[2];
1213
+ const options = parseQuartoCellOptions(codeContent);
1214
+ if (options) codeContent = codeContent.split("\n").filter((line) => !line.trimStart().startsWith("#|")).join("\n").trim();
1215
+ else codeContent = codeContent.trim();
1216
+ cells.push({
1217
+ cellType: "code",
1218
+ content: codeContent,
1219
+ language,
1220
+ ...options ? { options } : {}
1221
+ });
1222
+ lastIndex = match.index + match[0].length;
1223
+ match = codeChunkRegex.exec(mainContent);
1224
+ }
1225
+ const markdownAfter = mainContent.slice(lastIndex).trim();
1226
+ if (markdownAfter) addMarkdownCells(cells, markdownAfter);
1227
+ return {
1228
+ ...frontmatter ? { frontmatter } : {},
1229
+ cells
1230
+ };
1231
+ }
1232
+ /**
1233
+ * Splits markdown content at cell delimiter comments and adds each part as a separate cell.
1234
+ * The delimiter `<!-- cell -->` is used to preserve cell boundaries during roundtrip conversion.
1235
+ */
1236
+ function addMarkdownCells(cells, content) {
1237
+ const parts = content.split(/\s*<!--\s*cell\s*-->\s*/);
1238
+ for (const part of parts) {
1239
+ const trimmed = part.trim();
1240
+ if (trimmed) cells.push({
1241
+ cellType: "markdown",
1242
+ content: trimmed
1243
+ });
1244
+ }
1245
+ }
1246
+ /**
1247
+ * Parses YAML frontmatter string into a QuartoFrontmatter object.
1248
+ * Uses the yaml package to properly handle nested objects, arrays, and all YAML features.
1249
+ */
1250
+ function parseYamlFrontmatter(yamlString) {
1251
+ if (!yamlString || yamlString.trim() === "") return {};
1252
+ try {
1253
+ const parsed = parse(yamlString);
1254
+ if (parsed === null || parsed === void 0) return {};
1255
+ if (typeof parsed !== "object" || Array.isArray(parsed)) return {};
1256
+ return parsed;
1257
+ } catch {
1258
+ return {};
1259
+ }
1260
+ }
1261
+ /**
1262
+ * Parses Quarto cell options from #| lines.
1263
+ */
1264
+ function parseQuartoCellOptions(content) {
1265
+ const optionLines = content.split("\n").filter((line) => line.trimStart().startsWith("#|"));
1266
+ if (optionLines.length === 0) return;
1267
+ const options = {};
1268
+ const raw = {};
1269
+ for (const line of optionLines) {
1270
+ const match = /^#\|\s*(\S+):\s*(.*)$/.exec(line.trimStart());
1271
+ if (match) {
1272
+ const key = match[1];
1273
+ let value = match[2].trim();
1274
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
1275
+ else if (value === "true") value = true;
1276
+ else if (value === "false") value = false;
1277
+ else {
1278
+ const num = Number(value);
1279
+ if (!Number.isNaN(num) && Number.isFinite(num)) value = num;
1280
+ }
1281
+ switch (key) {
1282
+ case "label":
1283
+ if (typeof value === "string") options.label = value;
1284
+ else raw[key] = value;
1285
+ break;
1286
+ case "echo":
1287
+ if (typeof value === "boolean") options.echo = value;
1288
+ else if (value === "true" || value === "false") options.echo = value === "true";
1289
+ else raw[key] = value;
1290
+ break;
1291
+ case "eval":
1292
+ if (typeof value === "boolean") options.eval = value;
1293
+ else if (value === "true" || value === "false") options.eval = value === "true";
1294
+ else raw[key] = value;
1295
+ break;
1296
+ case "output":
1297
+ if (typeof value === "boolean") options.output = value;
1298
+ else if (value === "true" || value === "false") options.output = value === "true";
1299
+ else raw[key] = value;
1300
+ break;
1301
+ case "fig-cap":
1302
+ if (typeof value === "string") options.figCap = value;
1303
+ else raw[key] = value;
1304
+ break;
1305
+ case "fig-width":
1306
+ if (typeof value === "number" && Number.isFinite(value)) options.figWidth = value;
1307
+ else if (typeof value === "string") {
1308
+ const num = Number(value);
1309
+ if (!Number.isNaN(num) && Number.isFinite(num)) options.figWidth = num;
1310
+ else raw[key] = value;
1311
+ } else raw[key] = value;
1312
+ break;
1313
+ case "fig-height":
1314
+ if (typeof value === "number" && Number.isFinite(value)) options.figHeight = value;
1315
+ else if (typeof value === "string") {
1316
+ const num = Number(value);
1317
+ if (!Number.isNaN(num) && Number.isFinite(num)) options.figHeight = num;
1318
+ else raw[key] = value;
1319
+ } else raw[key] = value;
1320
+ break;
1321
+ case "tbl-cap":
1322
+ if (typeof value === "string") options.tblCap = value;
1323
+ else raw[key] = value;
1324
+ break;
1325
+ case "warning":
1326
+ if (typeof value === "boolean") options.warning = value;
1327
+ else if (value === "true" || value === "false") options.warning = value === "true";
1328
+ else raw[key] = value;
1329
+ break;
1330
+ case "message":
1331
+ if (typeof value === "boolean") options.message = value;
1332
+ else if (value === "true" || value === "false") options.message = value === "true";
1333
+ else raw[key] = value;
1334
+ break;
1335
+ default: raw[key] = value;
1336
+ }
1337
+ }
1338
+ }
1339
+ if (Object.keys(raw).length > 0) options.raw = raw;
1340
+ return Object.keys(options).length > 0 ? options : void 0;
1341
+ }
1342
+ /**
1343
+ * Converts a single Quarto document into an array of Deepnote blocks.
1344
+ * This is the lowest-level conversion function.
1345
+ *
1346
+ * @param document - The Quarto document object to convert
1347
+ * @param options - Optional conversion options including custom ID generator
1348
+ * @returns Array of DeepnoteBlock objects
1349
+ */
1350
+ function convertQuartoDocumentToBlocks(document, options) {
1351
+ const idGenerator = options?.idGenerator ?? v4;
1352
+ const blocks = [];
1353
+ if (document.frontmatter?.title) blocks.push({
1354
+ blockGroup: idGenerator(),
1355
+ content: `# ${document.frontmatter.title}`,
1356
+ id: idGenerator(),
1357
+ metadata: {},
1358
+ sortingKey: createSortingKey(blocks.length),
1359
+ type: "markdown"
1360
+ });
1361
+ for (const cell of document.cells) blocks.push(convertCellToBlock(cell, blocks.length, idGenerator));
1362
+ return blocks;
1363
+ }
1364
+ /**
1365
+ * Converts Quarto document objects into a Deepnote project file.
1366
+ * This is a pure conversion function that doesn't perform any file I/O.
1367
+ *
1368
+ * @param documents - Array of Quarto documents with filenames
1369
+ * @param options - Conversion options including project name
1370
+ * @returns A DeepnoteFile object
1371
+ */
1372
+ function convertQuartoDocumentsToDeepnote(documents, options) {
1373
+ const firstNotebookId = documents.length > 0 ? v4() : void 0;
1374
+ const deepnoteFile = {
1375
+ metadata: { createdAt: (/* @__PURE__ */ new Date()).toISOString() },
1376
+ project: {
1377
+ id: v4(),
1378
+ initNotebookId: firstNotebookId,
1379
+ integrations: [],
1380
+ name: options.projectName,
1381
+ notebooks: [],
1382
+ settings: {}
1383
+ },
1384
+ version: "1.0.0"
1385
+ };
1386
+ for (let i = 0; i < documents.length; i++) {
1387
+ const { filename, document } = documents[i];
1388
+ const filenameWithoutExt = basename(filename, extname(filename)) || "Untitled notebook";
1389
+ const notebookName = document.frontmatter?.title || filenameWithoutExt;
1390
+ const blocks = convertQuartoDocumentToBlocks(document);
1391
+ const notebookId = i === 0 && firstNotebookId ? firstNotebookId : v4();
1392
+ deepnoteFile.project.notebooks.push({
1393
+ blocks,
1394
+ executionMode: "block",
1395
+ id: notebookId,
1396
+ isModule: false,
1397
+ name: typeof notebookName === "string" ? notebookName : filenameWithoutExt
1398
+ });
1399
+ }
1400
+ return deepnoteFile;
1401
+ }
1402
+ /**
1403
+ * Converts multiple Quarto (.qmd) files into a single Deepnote project file.
1404
+ */
1405
+ async function convertQuartoFilesToDeepnoteFile(inputFilePaths, options) {
1406
+ const documents = [];
1407
+ for (const filePath of inputFilePaths) {
1408
+ const document = parseQuartoFormat(await fs.readFile(filePath, "utf-8"));
1409
+ documents.push({
1410
+ filename: basename(filePath),
1411
+ document
1412
+ });
1413
+ }
1414
+ const yamlContent = stringify(convertQuartoDocumentsToDeepnote(documents, { projectName: options.projectName }));
1415
+ const parentDir = dirname(options.outputPath);
1416
+ await fs.mkdir(parentDir, { recursive: true });
1417
+ await fs.writeFile(options.outputPath, yamlContent, "utf-8");
1418
+ }
1419
+ function convertCellToBlock(cell, index, idGenerator) {
1420
+ const blockType = cell.cellType === "markdown" ? "markdown" : "code";
1421
+ const metadata = {};
1422
+ if (cell.options) {
1423
+ if (cell.options.label) metadata.quarto_label = cell.options.label;
1424
+ if (cell.options.echo === false) metadata.is_code_hidden = true;
1425
+ if (cell.options.figCap) metadata.quarto_fig_cap = cell.options.figCap;
1426
+ if (cell.options.tblCap) metadata.quarto_tbl_cap = cell.options.tblCap;
1427
+ if (cell.options.raw) metadata.quarto_options = cell.options.raw;
1428
+ }
1429
+ if (cell.language && cell.language !== "python") metadata.language = cell.language;
1430
+ return {
1431
+ blockGroup: idGenerator(),
1432
+ content: cell.content,
1433
+ id: idGenerator(),
1434
+ metadata: Object.keys(metadata).length > 0 ? metadata : {},
1435
+ sortingKey: createSortingKey(index),
1436
+ type: blockType
1437
+ };
1438
+ }
1439
+
1440
+ //#endregion
1441
+ export { convertDeepnoteToJupyterNotebooks as A, serializePercentFormat as C, serializeMarimoFormat as D, convertDeepnoteToMarimoApps as E, convertBlocksToJupyterNotebook as O, convertDeepnoteToPercentNotebooks as S, convertDeepnoteFileToMarimoFiles as T, convertDeepnoteFileToQuartoFiles as _, convertPercentFilesToDeepnoteFile as a, convertBlocksToPercentNotebook as b, parsePercentFormat as c, convertMarimoFilesToDeepnoteFile as d, parseMarimoFormat as f, convertBlocksToQuartoDocument as g, convertJupyterNotebooksToDeepnote as h, parseQuartoFormat as i, convertDeepnoteFileToJupyterFiles as k, convertMarimoAppToBlocks as l, convertJupyterNotebookToBlocks as m, convertQuartoDocumentsToDeepnote as n, convertPercentNotebookToBlocks as o, convertIpynbFilesToDeepnoteFile as p, convertQuartoFilesToDeepnoteFile as r, convertPercentNotebooksToDeepnote as s, convertQuartoDocumentToBlocks as t, convertMarimoAppsToDeepnote as u, convertDeepnoteToQuartoDocuments as v, convertBlocksToMarimoApp as w, convertDeepnoteFileToPercentFiles as x, serializeQuartoFormat as y };