Skip to content

Commit 1052c25

Browse files
committed
refactor outputs
1 parent 323e76d commit 1052c25

File tree

7 files changed

+608
-303
lines changed

7 files changed

+608
-303
lines changed
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { NotebookCellOutputItem } from 'vscode';
2+
import { parseJsonSafely, convertBase64ToUint8Array } from './dataConversionUtils';
3+
4+
export interface MimeProcessor {
5+
canHandle(mimeType: string): boolean;
6+
processForDeepnote(content: unknown, mimeType: string): unknown;
7+
processForVSCode(content: unknown, mimeType: string): NotebookCellOutputItem | null;
8+
}
9+
10+
/**
11+
* Handles text-based MIME types
12+
*/
13+
export class TextMimeProcessor implements MimeProcessor {
14+
private readonly supportedTypes = ['text/plain', 'text/html'];
15+
16+
canHandle(mimeType: string): boolean {
17+
return this.supportedTypes.includes(mimeType);
18+
}
19+
20+
processForDeepnote(content: unknown): unknown {
21+
return typeof content === 'string' ? content : String(content);
22+
}
23+
24+
processForVSCode(content: unknown, mimeType: string): NotebookCellOutputItem | null {
25+
if (mimeType === 'text/plain') {
26+
return NotebookCellOutputItem.text(content as string);
27+
}
28+
if (mimeType === 'text/html') {
29+
return NotebookCellOutputItem.text(content as string, 'text/html');
30+
}
31+
return null;
32+
}
33+
}
34+
35+
/**
36+
* Handles image MIME types
37+
*/
38+
export class ImageMimeProcessor implements MimeProcessor {
39+
canHandle(mimeType: string): boolean {
40+
return mimeType.startsWith('image/');
41+
}
42+
43+
processForDeepnote(content: unknown, mimeType: string): unknown {
44+
if (content instanceof Uint8Array) {
45+
const base64String = btoa(String.fromCharCode(...content));
46+
return `data:${mimeType};base64,${base64String}`;
47+
}
48+
return content;
49+
}
50+
51+
processForVSCode(content: unknown, mimeType: string): NotebookCellOutputItem | null {
52+
try {
53+
let uint8Array: Uint8Array;
54+
55+
if (typeof content === 'string') {
56+
uint8Array = convertBase64ToUint8Array(content);
57+
} else if (content instanceof ArrayBuffer) {
58+
uint8Array = new Uint8Array(content);
59+
} else if (content instanceof Uint8Array) {
60+
uint8Array = content;
61+
} else {
62+
return null;
63+
}
64+
65+
return NotebookCellOutputItem.binary(uint8Array, mimeType);
66+
} catch {
67+
return NotebookCellOutputItem.text(String(content), mimeType);
68+
}
69+
}
70+
}
71+
72+
/**
73+
* Handles JSON MIME types
74+
*/
75+
export class JsonMimeProcessor implements MimeProcessor {
76+
canHandle(mimeType: string): boolean {
77+
return mimeType === 'application/json';
78+
}
79+
80+
processForDeepnote(content: unknown): unknown {
81+
if (typeof content === 'string') {
82+
return parseJsonSafely(content);
83+
}
84+
return content;
85+
}
86+
87+
processForVSCode(content: unknown, mimeType: string): NotebookCellOutputItem | null {
88+
try {
89+
let jsonObject: unknown;
90+
91+
if (typeof content === 'string') {
92+
jsonObject = JSON.parse(content);
93+
} else if (typeof content === 'object' && content !== null) {
94+
jsonObject = content;
95+
} else {
96+
return NotebookCellOutputItem.text(String(content), mimeType);
97+
}
98+
99+
return NotebookCellOutputItem.text(JSON.stringify(jsonObject, null, 2), mimeType);
100+
} catch {
101+
return NotebookCellOutputItem.text(String(content), mimeType);
102+
}
103+
}
104+
}
105+
106+
/**
107+
* Handles other application MIME types
108+
*/
109+
export class ApplicationMimeProcessor implements MimeProcessor {
110+
canHandle(mimeType: string): boolean {
111+
return mimeType.startsWith('application/') && mimeType !== 'application/json';
112+
}
113+
114+
processForDeepnote(content: unknown): unknown {
115+
if (typeof content === 'string') {
116+
return parseJsonSafely(content);
117+
}
118+
return content;
119+
}
120+
121+
processForVSCode(content: unknown, mimeType: string): NotebookCellOutputItem | null {
122+
const textContent = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
123+
return NotebookCellOutputItem.text(textContent, mimeType);
124+
}
125+
}
126+
127+
/**
128+
* Generic fallback processor
129+
*/
130+
export class GenericMimeProcessor implements MimeProcessor {
131+
canHandle(): boolean {
132+
return true; // Always can handle as fallback
133+
}
134+
135+
processForDeepnote(content: unknown): unknown {
136+
return content;
137+
}
138+
139+
processForVSCode(content: unknown, mimeType: string): NotebookCellOutputItem | null {
140+
return NotebookCellOutputItem.text(String(content), mimeType);
141+
}
142+
}
143+
144+
/**
145+
* Registry for MIME type processors
146+
*/
147+
export class MimeTypeProcessorRegistry {
148+
private readonly processors: MimeProcessor[] = [
149+
new TextMimeProcessor(),
150+
new ImageMimeProcessor(),
151+
new JsonMimeProcessor(),
152+
new ApplicationMimeProcessor(),
153+
new GenericMimeProcessor() // Must be last as fallback
154+
];
155+
156+
getProcessor(mimeType: string): MimeProcessor {
157+
return this.processors.find(processor => processor.canHandle(mimeType)) || new GenericMimeProcessor();
158+
}
159+
160+
processForDeepnote(content: unknown, mimeType: string): unknown {
161+
const processor = this.getProcessor(mimeType);
162+
return processor.processForDeepnote(content, mimeType);
163+
}
164+
165+
processForVSCode(content: unknown, mimeType: string): NotebookCellOutputItem | null {
166+
const processor = this.getProcessor(mimeType);
167+
return processor.processForVSCode(content, mimeType);
168+
}
169+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { NotebookCellOutput } from 'vscode';
2+
3+
export type DetectedOutputType = 'error' | 'stream' | 'rich';
4+
5+
export interface OutputTypeResult {
6+
type: DetectedOutputType;
7+
streamMimes?: string[];
8+
errorItem?: { mime: string; data: Uint8Array };
9+
}
10+
11+
/**
12+
* Detects the appropriate output type from VS Code NotebookCellOutput items
13+
*/
14+
export class OutputTypeDetector {
15+
private readonly streamMimes = [
16+
'text/plain',
17+
'application/vnd.code.notebook.stdout',
18+
'application/vnd.code.notebook.stderr'
19+
];
20+
21+
detect(output: NotebookCellOutput): OutputTypeResult {
22+
if (output.items.length === 0) {
23+
return { type: 'rich' };
24+
}
25+
26+
// Check for error output first
27+
const errorItem = output.items.find(item => item.mime === 'application/vnd.code.notebook.error');
28+
if (errorItem) {
29+
return {
30+
type: 'error',
31+
errorItem: { mime: errorItem.mime, data: errorItem.data }
32+
};
33+
}
34+
35+
// Check for stream outputs
36+
const streamItems = output.items.filter(item => this.streamMimes.includes(item.mime));
37+
if (streamItems.length > 0) {
38+
return {
39+
type: 'stream',
40+
streamMimes: streamItems.map(item => item.mime)
41+
};
42+
}
43+
44+
// Default to rich output
45+
return { type: 'rich' };
46+
}
47+
48+
isStreamMime(mimeType: string): boolean {
49+
return this.streamMimes.includes(mimeType);
50+
}
51+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/**
2+
* Utility functions for data transformation in Deepnote conversion
3+
*/
4+
5+
/**
6+
* Safely decode content using TextDecoder
7+
*/
8+
export function decodeContent(data: Uint8Array): string {
9+
return new TextDecoder().decode(data);
10+
}
11+
12+
/**
13+
* Safely parse JSON with fallback to original content
14+
*/
15+
export function parseJsonSafely(content: string): unknown {
16+
try {
17+
return JSON.parse(content);
18+
} catch {
19+
return content;
20+
}
21+
}
22+
23+
/**
24+
* Convert base64 string to Uint8Array
25+
*/
26+
export function convertBase64ToUint8Array(base64Content: string): Uint8Array {
27+
const base64Data = base64Content.includes(',') ? base64Content.split(',')[1] : base64Content;
28+
const binaryString = atob(base64Data);
29+
const uint8Array = new Uint8Array(binaryString.length);
30+
for (let i = 0; i < binaryString.length; i++) {
31+
uint8Array[i] = binaryString.charCodeAt(i);
32+
}
33+
return uint8Array;
34+
}
35+
36+
/**
37+
* Convert Uint8Array to base64 data URL
38+
*/
39+
export function convertUint8ArrayToBase64DataUrl(data: Uint8Array, mimeType: string): string {
40+
const base64String = btoa(String.fromCharCode(...data));
41+
return `data:${mimeType};base64,${base64String}`;
42+
}
43+
44+
/**
45+
* Merge metadata objects, filtering out undefined values
46+
*/
47+
export function mergeMetadata(...metadataObjects: (Record<string, unknown> | undefined)[]): Record<string, unknown> {
48+
const result: Record<string, unknown> = {};
49+
50+
for (const metadata of metadataObjects) {
51+
if (metadata) {
52+
Object.entries(metadata).forEach(([key, value]) => {
53+
if (value !== undefined) {
54+
result[key] = value;
55+
}
56+
});
57+
}
58+
}
59+
60+
return result;
61+
}
62+
63+
/**
64+
* Check if metadata object has any content
65+
*/
66+
export function hasMetadataContent(metadata: Record<string, unknown>): boolean {
67+
return Object.keys(metadata).length > 0;
68+
}
69+
70+
/**
71+
* Generate a random hex ID for blocks
72+
*/
73+
export function generateBlockId(): string {
74+
const chars = '0123456789abcdef';
75+
let id = '';
76+
for (let i = 0; i < 32; i++) {
77+
id += chars[Math.floor(Math.random() * chars.length)];
78+
}
79+
return id;
80+
}
81+
82+
/**
83+
* Generate sorting key based on index
84+
*/
85+
export function generateSortingKey(index: number): string {
86+
const alphabet = 'abcdefghijklmnopqrstuvwxyz';
87+
const letterIndex = Math.floor(index / 100);
88+
const letter = letterIndex < alphabet.length ? alphabet[letterIndex] : 'z';
89+
const number = index % 100;
90+
return `${letter}${number}`;
91+
}

0 commit comments

Comments
 (0)