@deepnote/convert 1.3.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.
- package/README.md +154 -71
- package/dist/bin.js +154 -45
- package/dist/index.d.ts +1097 -3
- package/dist/index.js +2 -2
- package/dist/src-CUESP0m8.js +1441 -0
- package/package.json +2 -2
- package/dist/src-j4HyYJfD.js +0 -279
|
@@ -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 };
|