Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/ipynb/src/deserializers.ts
3291 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import type * as nbformat from '@jupyterlab/nbformat';
7
import { extensions, NotebookCellData, NotebookCellExecutionSummary, NotebookCellKind, NotebookCellOutput, NotebookCellOutputItem, NotebookData } from 'vscode';
8
import { CellMetadata, CellOutputMetadata } from './common';
9
import { textMimeTypes } from './constants';
10
11
const jupyterLanguageToMonacoLanguageMapping = new Map([
12
['c#', 'csharp'],
13
['f#', 'fsharp'],
14
['q#', 'qsharp'],
15
['c++11', 'c++'],
16
['c++12', 'c++'],
17
['c++14', 'c++']
18
]);
19
20
export function getPreferredLanguage(metadata?: nbformat.INotebookMetadata) {
21
const jupyterLanguage =
22
metadata?.language_info?.name ||
23
(metadata?.kernelspec as any)?.language;
24
25
// Default to python language only if the Python extension is installed.
26
const defaultLanguage =
27
extensions.getExtension('ms-python.python')
28
? 'python'
29
: (extensions.getExtension('ms-dotnettools.dotnet-interactive-vscode') ? 'csharp' : 'python');
30
31
// Note, whatever language is returned here, when the user selects a kernel, the cells (of blank documents) get updated based on that kernel selection.
32
return translateKernelLanguageToMonaco(jupyterLanguage || defaultLanguage);
33
}
34
35
function translateKernelLanguageToMonaco(language: string): string {
36
language = language.toLowerCase();
37
if (language.length === 2 && language.endsWith('#')) {
38
return `${language.substring(0, 1)}sharp`;
39
}
40
return jupyterLanguageToMonacoLanguageMapping.get(language) || language;
41
}
42
43
const orderOfMimeTypes = [
44
'application/vnd.*',
45
'application/vdom.*',
46
'application/geo+json',
47
'application/x-nteract-model-debug+json',
48
'text/html',
49
'application/javascript',
50
'image/gif',
51
'text/latex',
52
'text/markdown',
53
'image/png',
54
'image/svg+xml',
55
'image/jpeg',
56
'application/json',
57
'text/plain'
58
];
59
60
function isEmptyVendoredMimeType(outputItem: NotebookCellOutputItem) {
61
if (outputItem.mime.startsWith('application/vnd.')) {
62
try {
63
return outputItem.data.byteLength === 0 || Buffer.from(outputItem.data).toString().length === 0;
64
} catch { }
65
}
66
return false;
67
}
68
function isMimeTypeMatch(value: string, compareWith: string) {
69
if (value.endsWith('.*')) {
70
value = value.substr(0, value.indexOf('.*'));
71
}
72
return compareWith.startsWith(value);
73
}
74
75
function sortOutputItemsBasedOnDisplayOrder(outputItems: NotebookCellOutputItem[]): NotebookCellOutputItem[] {
76
return outputItems
77
.map(item => {
78
let index = orderOfMimeTypes.findIndex((mime) => isMimeTypeMatch(mime, item.mime));
79
// Sometimes we can have mime types with empty data, e.g. when using holoview we can have `application/vnd.holoviews_load.v0+json` with empty value.
80
// & in these cases we have HTML/JS and those take precedence.
81
// https://github.com/microsoft/vscode-jupyter/issues/6109
82
if (isEmptyVendoredMimeType(item)) {
83
index = -1;
84
}
85
index = index === -1 ? 100 : index;
86
return {
87
item, index
88
};
89
})
90
.sort((outputItemA, outputItemB) => outputItemA.index - outputItemB.index).map(item => item.item);
91
}
92
93
/**
94
* Concatenates a multiline string or an array of strings into a single string.
95
* Also normalizes line endings to use LF (`\n`) instead of CRLF (`\r\n`).
96
* Same is done in serializer as well.
97
*/
98
function concatMultilineCellSource(source: string | string[]): string {
99
return concatMultilineString(source).replace(/\r\n/g, '\n');
100
}
101
102
function concatMultilineString(str: string | string[]): string {
103
if (Array.isArray(str)) {
104
let result = '';
105
for (let i = 0; i < str.length; i += 1) {
106
const s = str[i];
107
if (i < str.length - 1 && !s.endsWith('\n')) {
108
result = result.concat(`${s}\n`);
109
} else {
110
result = result.concat(s);
111
}
112
}
113
114
return result;
115
}
116
return str.toString();
117
}
118
119
function convertJupyterOutputToBuffer(mime: string, value: unknown): NotebookCellOutputItem {
120
if (!value) {
121
return NotebookCellOutputItem.text('', mime);
122
}
123
try {
124
if (
125
(mime.startsWith('text/') || textMimeTypes.includes(mime)) &&
126
(Array.isArray(value) || typeof value === 'string')
127
) {
128
const stringValue = Array.isArray(value) ? concatMultilineString(value) : value;
129
return NotebookCellOutputItem.text(stringValue, mime);
130
} else if (mime.startsWith('image/') && typeof value === 'string' && mime !== 'image/svg+xml') {
131
// Images in Jupyter are stored in base64 encoded format.
132
// VS Code expects bytes when rendering images.
133
if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') {
134
return new NotebookCellOutputItem(Buffer.from(value, 'base64'), mime);
135
} else {
136
const data = Uint8Array.from(atob(value), c => c.charCodeAt(0));
137
return new NotebookCellOutputItem(data, mime);
138
}
139
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
140
return NotebookCellOutputItem.text(JSON.stringify(value), mime);
141
} else if (mime === 'application/json') {
142
return NotebookCellOutputItem.json(value, mime);
143
} else {
144
// For everything else, treat the data as strings (or multi-line strings).
145
value = Array.isArray(value) ? concatMultilineString(value) : value;
146
return NotebookCellOutputItem.text(value as string, mime);
147
}
148
} catch (ex) {
149
return NotebookCellOutputItem.error(ex);
150
}
151
}
152
153
function getNotebookCellMetadata(cell: nbformat.IBaseCell): {
154
[key: string]: any;
155
} {
156
// We put this only for VSC to display in diff view.
157
// Else we don't use this.
158
const cellMetadata: CellMetadata = {};
159
if (cell.cell_type === 'code') {
160
if (typeof cell['execution_count'] === 'number') {
161
cellMetadata.execution_count = cell['execution_count'];
162
} else {
163
cellMetadata.execution_count = null;
164
}
165
}
166
167
if (cell['metadata']) {
168
cellMetadata['metadata'] = JSON.parse(JSON.stringify(cell['metadata']));
169
}
170
171
if ('id' in cell && typeof cell.id === 'string') {
172
cellMetadata.id = cell.id;
173
}
174
175
if (cell['attachments']) {
176
cellMetadata.attachments = JSON.parse(JSON.stringify(cell['attachments']));
177
}
178
return cellMetadata;
179
}
180
181
function getOutputMetadata(output: nbformat.IOutput): CellOutputMetadata {
182
// Add on transient data if we have any. This should be removed by our save functions elsewhere.
183
const metadata: CellOutputMetadata = {
184
outputType: output.output_type
185
};
186
if (output.transient) {
187
metadata.transient = output.transient;
188
}
189
190
switch (output.output_type as nbformat.OutputType) {
191
case 'display_data':
192
case 'execute_result':
193
case 'update_display_data': {
194
metadata.executionCount = output.execution_count;
195
metadata.metadata = output.metadata ? JSON.parse(JSON.stringify(output.metadata)) : {};
196
break;
197
}
198
default:
199
break;
200
}
201
202
return metadata;
203
}
204
205
206
function translateDisplayDataOutput(
207
output: nbformat.IDisplayData | nbformat.IDisplayUpdate | nbformat.IExecuteResult
208
): NotebookCellOutput {
209
// Metadata could be as follows:
210
// We'll have metadata specific to each mime type as well as generic metadata.
211
/*
212
IDisplayData = {
213
output_type: 'display_data',
214
data: {
215
'image/jpg': '/////'
216
'image/png': '/////'
217
'text/plain': '/////'
218
},
219
metadata: {
220
'image/png': '/////',
221
'background': true,
222
'xyz': '///
223
}
224
}
225
*/
226
const metadata = getOutputMetadata(output);
227
const items: NotebookCellOutputItem[] = [];
228
if (output.data) {
229
for (const key in output.data) {
230
items.push(convertJupyterOutputToBuffer(key, output.data[key]));
231
}
232
}
233
234
return new NotebookCellOutput(sortOutputItemsBasedOnDisplayOrder(items), metadata);
235
}
236
237
function translateErrorOutput(output?: nbformat.IError): NotebookCellOutput {
238
output = output || { output_type: 'error', ename: '', evalue: '', traceback: [] };
239
return new NotebookCellOutput(
240
[
241
NotebookCellOutputItem.error({
242
name: output?.ename || '',
243
message: output?.evalue || '',
244
stack: (output?.traceback || []).join('\n')
245
})
246
],
247
{ ...getOutputMetadata(output), originalError: output }
248
);
249
}
250
251
function translateStreamOutput(output: nbformat.IStream): NotebookCellOutput {
252
const value = concatMultilineString(output.text);
253
const item = output.name === 'stderr' ? NotebookCellOutputItem.stderr(value) : NotebookCellOutputItem.stdout(value);
254
return new NotebookCellOutput([item], getOutputMetadata(output));
255
}
256
257
const cellOutputMappers = new Map<nbformat.OutputType, (output: any) => NotebookCellOutput>();
258
cellOutputMappers.set('display_data', translateDisplayDataOutput);
259
cellOutputMappers.set('execute_result', translateDisplayDataOutput);
260
cellOutputMappers.set('update_display_data', translateDisplayDataOutput);
261
cellOutputMappers.set('error', translateErrorOutput);
262
cellOutputMappers.set('stream', translateStreamOutput);
263
264
export function jupyterCellOutputToCellOutput(output: nbformat.IOutput): NotebookCellOutput {
265
/**
266
* Stream, `application/x.notebook.stream`
267
* Error, `application/x.notebook.error-traceback`
268
* Rich, { mime: value }
269
*
270
* outputs: [
271
new vscode.NotebookCellOutput([
272
new vscode.NotebookCellOutputItem('application/x.notebook.stream', 2),
273
new vscode.NotebookCellOutputItem('application/x.notebook.stream', 3),
274
]),
275
new vscode.NotebookCellOutput([
276
new vscode.NotebookCellOutputItem('text/markdown', '## header 2'),
277
new vscode.NotebookCellOutputItem('image/svg+xml', [
278
"<svg baseProfile=\"full\" height=\"200\" version=\"1.1\" width=\"300\" xmlns=\"http://www.w3.org/2000/svg\">\n",
279
" <rect fill=\"blue\" height=\"100%\" width=\"100%\"/>\n",
280
" <circle cx=\"150\" cy=\"100\" fill=\"green\" r=\"80\"/>\n",
281
" <text fill=\"white\" font-size=\"60\" text-anchor=\"middle\" x=\"150\" y=\"125\">SVG</text>\n",
282
"</svg>"
283
]),
284
]),
285
]
286
*
287
*/
288
const fn = cellOutputMappers.get(output.output_type as nbformat.OutputType);
289
let result: NotebookCellOutput;
290
if (fn) {
291
result = fn(output);
292
} else {
293
result = translateDisplayDataOutput(output as any);
294
}
295
return result;
296
}
297
298
function createNotebookCellDataFromRawCell(cell: nbformat.IRawCell): NotebookCellData {
299
const cellData = new NotebookCellData(NotebookCellKind.Code, concatMultilineCellSource(cell.source), 'raw');
300
cellData.outputs = [];
301
cellData.metadata = getNotebookCellMetadata(cell);
302
return cellData;
303
}
304
function createNotebookCellDataFromMarkdownCell(cell: nbformat.IMarkdownCell): NotebookCellData {
305
const cellData = new NotebookCellData(
306
NotebookCellKind.Markup,
307
concatMultilineCellSource(cell.source),
308
'markdown'
309
);
310
cellData.outputs = [];
311
cellData.metadata = getNotebookCellMetadata(cell);
312
return cellData;
313
}
314
function createNotebookCellDataFromCodeCell(cell: nbformat.ICodeCell, cellLanguage: string): NotebookCellData {
315
const cellOutputs = Array.isArray(cell.outputs) ? cell.outputs : [];
316
const outputs = cellOutputs.map(jupyterCellOutputToCellOutput);
317
const hasExecutionCount = typeof cell.execution_count === 'number' && cell.execution_count > 0;
318
319
const source = concatMultilineCellSource(cell.source);
320
321
const executionSummary: NotebookCellExecutionSummary = hasExecutionCount
322
? { executionOrder: cell.execution_count as number }
323
: {};
324
325
const vscodeCustomMetadata = cell.metadata['vscode'] as { [key: string]: any } | undefined;
326
const cellLanguageId = vscodeCustomMetadata && vscodeCustomMetadata.languageId && typeof vscodeCustomMetadata.languageId === 'string' ? vscodeCustomMetadata.languageId : cellLanguage;
327
const cellData = new NotebookCellData(NotebookCellKind.Code, source, cellLanguageId);
328
329
cellData.outputs = outputs;
330
cellData.metadata = getNotebookCellMetadata(cell);
331
cellData.executionSummary = executionSummary;
332
return cellData;
333
}
334
335
function createNotebookCellDataFromJupyterCell(
336
cellLanguage: string,
337
cell: nbformat.IBaseCell
338
): NotebookCellData | undefined {
339
switch (cell.cell_type) {
340
case 'raw': {
341
return createNotebookCellDataFromRawCell(cell as nbformat.IRawCell);
342
}
343
case 'markdown': {
344
return createNotebookCellDataFromMarkdownCell(cell as nbformat.IMarkdownCell);
345
}
346
case 'code': {
347
return createNotebookCellDataFromCodeCell(cell as nbformat.ICodeCell, cellLanguage);
348
}
349
}
350
351
return;
352
}
353
354
/**
355
* Converts a NotebookModel into VS Code format.
356
*/
357
export function jupyterNotebookModelToNotebookData(
358
notebookContent: Partial<nbformat.INotebookContent>,
359
preferredLanguage: string
360
): NotebookData {
361
const notebookContentWithoutCells = { ...notebookContent, cells: [] };
362
if (!Array.isArray(notebookContent.cells)) {
363
throw new Error('Notebook content is missing cells');
364
}
365
366
const cells = notebookContent.cells
367
.map(cell => createNotebookCellDataFromJupyterCell(preferredLanguage, cell))
368
.filter((item): item is NotebookCellData => !!item);
369
370
const notebookData = new NotebookData(cells);
371
notebookData.metadata = notebookContentWithoutCells;
372
return notebookData;
373
}
374
375