diff --git a/src/sql/services/notebook/localContentManager.ts b/src/sql/services/notebook/localContentManager.ts index e7d83745ce..c70391aa76 100644 --- a/src/sql/services/notebook/localContentManager.ts +++ b/src/sql/services/notebook/localContentManager.ts @@ -13,14 +13,14 @@ import * as json from 'vs/base/common/json'; import * as pfs from 'vs/base/node/pfs'; import URI from 'vs/base/common/uri'; import { localize } from 'vs/nls'; -import { JSONObject } from 'sql/parts/notebook/models/jsonext'; -import ContentManager = nb.ContentManager; +import { JSONObject } from 'sql/parts/notebook/models/jsonext'; import { OutputTypes } from 'sql/parts/notebook/models/contracts'; +import { nbversion } from 'sql/parts/notebook/notebookConstants'; type MimeBundle = { [key: string]: string | string[] | undefined }; -export class LocalContentManager implements ContentManager { +export class LocalContentManager implements nb.ContentManager { public async getNotebookContents(notebookUri: URI): Promise { if (!notebookUri) { return undefined; @@ -71,6 +71,7 @@ namespace v4 { return notebook; } + function readCell(cell: nb.ICellContents): nb.ICellContents { switch (cell.cell_type) { case 'markdown': @@ -79,17 +80,18 @@ namespace v4 { case 'code': return createCodeCell(cell); default: - throw new TypeError(localize('unknownCellType', 'Cell type {0} unknown', cell.cell_type)); - } + throw new TypeError(localize('unknownCellType', 'Cell type {0} unknown', cell.cell_type)); + } } - function createDefaultCell(cell: nb.ICellContents): nb.ICellContents { + export function createDefaultCell(cell: nb.ICellContents): nb.ICellContents { return { cell_type: cell.cell_type, source: demultiline(cell.source), metadata: cell.metadata }; } + function createCodeCell(cell: nb.ICellContents): nb.ICellContents { return { cell_type: cell.cell_type, @@ -107,7 +109,7 @@ namespace v4 { function createOutput(output: nb.Output): nb.ICellOutput { switch (output.output_type) { case OutputTypes.ExecuteResult: - return { + return { output_type: output.output_type, execution_count: output.execution_count, data: createMimeBundle(output.data), @@ -115,19 +117,19 @@ namespace v4 { }; case OutputTypes.DisplayData: case OutputTypes.UpdateDisplayData: - return { + return { output_type: output.output_type, data: createMimeBundle(output.data), metadata: output.metadata }; case 'stream': - return { + return { output_type: output.output_type, name: output.name, text: demultiline(output.text) }; case 'error': - return { + return { output_type: 'error', ename: output.ename, evalue: output.evalue, @@ -138,7 +140,7 @@ namespace v4 { default: // Should never get here throw new TypeError(localize('unrecognizedOutput', 'Output type {0} not recognized', (output).output_type)); - } + } } function createMimeBundle(oldMimeBundle: MimeBundle): MimeBundle { @@ -158,7 +160,7 @@ namespace v4 { * * @returns The cleaned mime data. */ - function cleanMimeData(key: string, data: string | string[] | undefined) { + export function cleanMimeData(key: string, data: string | string[] | undefined) { // See https://github.com/jupyter/nbformat/blob/62d6eb8803616d198eaa2024604d1fe923f2a7b3/nbformat/v4/nbformat.v4.schema.json#L368 if (isJSONKey(key)) { // Data stays as is for JSON types @@ -172,7 +174,7 @@ namespace v4 { throw new TypeError(localize('invalidMimeData', 'Data for {0} is expected to be a string or an Array of strings', key)); } - function demultiline(value: nb.MultilineString): string { + export function demultiline(value: nb.MultilineString): string { return Array.isArray(value) ? value.join('') : value; } @@ -183,8 +185,182 @@ namespace v4 { namespace v3 { - export function readNotebook(contents: nb.INotebookContents): nb.INotebookContents { - // TODO will add v3 support in future update - throw new TypeError(localize('nbNotSupported', 'This notebook format is not supported')); + export function readNotebook(contents: Notebook): nb.INotebookContents { + let notebook: nb.INotebookContents = { + cells: [], + metadata: contents.metadata, + // Note: upgrading to v4 as we're converting to our codebase + nbformat: 4, + nbformat_minor: nbversion.MINOR_VERSION + }; + + if (contents.worksheets) { + for (let worksheet of contents.worksheets) { + if (worksheet.cells) { + notebook.cells.push(...worksheet.cells.map(cell => createCell(cell))); + } + } + } + + return notebook; + } + + function createCell(cell: Cell): nb.ICellContents { + switch (cell.cell_type) { + case 'markdown': + case 'raw': + return v4.createDefaultCell(cell); + case 'code': + return createCodeCell(cell as CodeCell); + case 'heading': + return createHeadingCell(cell); + default: + throw new TypeError(`Cell type ${(cell as any).cell_type} unknown`); + } + } + + + function createMimeBundle(oldMimeBundle: MimeOutput): MimeBundle { + let mimeBundle: MimeBundle = {}; + for (let key of Object.keys(oldMimeBundle)) { + // v3 had non-media types for rich media + if (key in VALID_MIMETYPES) { + let newKey = VALID_MIMETYPES[key as MimeTypeKey]; + mimeBundle[newKey] = v4.cleanMimeData(newKey, oldMimeBundle[key]); + } + } + return mimeBundle; + } + + const createOutput = (output: Output): nb.ICellOutput => { + switch (output.output_type) { + case 'pyout': + return { + output_type: OutputTypes.ExecuteResult, + execution_count: output.prompt_number, + data: createMimeBundle(output), + metadata: output.metadata + }; + case 'display_data': + return { + output_type: OutputTypes.DisplayData, + data: createMimeBundle(output), + metadata: output.metadata + }; + case 'stream': + // Default to stdout in all cases unless it's stderr + const name = output.stream === 'stderr' ? 'stderr' : 'stdout'; + return { + output_type: OutputTypes.Stream, + name: name, + text: v4.demultiline(output.text) + }; + case 'pyerr': + return { + output_type: OutputTypes.Error, + ename: output.ename, + evalue: output.evalue, + traceback: output.traceback + }; + default: + throw new TypeError(localize('unrecognizedOutputType', 'Output type {0} not recognized', output.output_type)); + } + }; + + function createCodeCell(cell: CodeCell): nb.ICellContents { + return { + cell_type: cell.cell_type, + source: v4.demultiline(cell.input), + outputs: cell.outputs.map(createOutput), + execution_count: cell.prompt_number, + metadata: cell.metadata + }; + } + + function createHeadingCell(cell: HeadingCell): nb.ICellContents { + // v3 heading cells are just markdown cells in v4+ + return { + cell_type: 'markdown', + source: Array.isArray(cell.source) + ? v4.demultiline( + cell.source.map(line => + Array(cell.level) + .join('#') + .concat(' ') + .concat(line) + ) + ) + : cell.source, + metadata: cell.metadata + }; + } + + const VALID_MIMETYPES = { + text: 'text/plain', + latex: 'text/latex', + png: 'image/png', + jpeg: 'image/jpeg', + svg: 'image/svg+xml', + html: 'text/html', + javascript: 'application/x-javascript', + json: 'application/javascript', + pdf: 'application/pdf' + }; + type MimeTypeKey = keyof typeof VALID_MIMETYPES; + type MimePayload = { [P in MimeTypeKey]?: nb.MultilineString }; + + interface MimeOutput extends MimePayload { + output_type: T; + prompt_number?: number; + metadata: object; + } + + export interface ExecuteResult extends MimeOutput<'pyout'> { } + export interface DisplayData extends MimeOutput<'display_data'> { } + + export interface StreamOutput { + output_type: 'stream'; + stream: string; + text: nb.MultilineString; + } + + export interface ErrorOutput { + output_type: 'error' | 'pyerr'; + ename: string; + evalue: string; + traceback: string[]; + } + + export type Output = ExecuteResult | DisplayData | StreamOutput | ErrorOutput; + + export interface HeadingCell { + cell_type: 'heading'; + metadata: JSONObject; + source: nb.MultilineString; + level: number; + } + + export interface CodeCell { + cell_type: 'code'; + language: string; + collapsed: boolean; + metadata: JSONObject; + input: nb.MultilineString; + prompt_number: number; + outputs: Array; + } + + export type Cell = nb.ICellContents | HeadingCell | CodeCell; + + export interface Worksheet { + cells: Cell[]; + metadata: object; + } + + export interface Notebook { + worksheets: Worksheet[]; + metadata: nb.INotebookMetadata; + nbformat: 3; + nbformat_minor: number; } }