0
# MIME Rendering
1
2
Plugin creation utilities for MIME type rendering extensions, enabling support for custom content types and document formats.
3
4
## Capabilities
5
6
### IMimeDocumentTracker Interface
7
8
Widget tracker interface for managing MIME document widgets across the application.
9
10
```typescript { .api }
11
/**
12
* MIME document widget tracker interface extending standard widget tracker
13
*/
14
interface IMimeDocumentTracker extends IWidgetTracker<MimeDocument> {
15
// Inherits all IWidgetTracker methods:
16
// - currentWidget: MimeDocument | null
17
// - widgetAdded: ISignal<this, MimeDocument>
18
// - widgetUpdated: ISignal<this, MimeDocument>
19
// - activeWidget: MimeDocument | null
20
// - size: number
21
// - filter(fn: (widget: MimeDocument) => boolean): MimeDocument[]
22
// - find(fn: (widget: MimeDocument) => boolean): MimeDocument | undefined
23
// - forEach(fn: (widget: MimeDocument) => void): void
24
// - has(widget: MimeDocument): boolean
25
// - inject(widget: MimeDocument): void
26
}
27
28
/**
29
* Service token for MIME document tracker
30
*/
31
const IMimeDocumentTracker: Token<IMimeDocumentTracker>;
32
```
33
34
**Usage Examples:**
35
36
```typescript
37
import { IMimeDocumentTracker } from "@jupyterlab/application";
38
import { JupyterFrontEndPlugin } from "@jupyterlab/application";
39
40
const mimeTrackerPlugin: JupyterFrontEndPlugin<void> = {
41
id: 'mime-tracker-example',
42
autoStart: true,
43
requires: [IMimeDocumentTracker],
44
activate: (app, tracker: IMimeDocumentTracker) => {
45
// Listen for new MIME documents
46
tracker.widgetAdded.connect((sender, widget) => {
47
console.log('New MIME document:', widget.context.path);
48
console.log('MIME type:', widget.content.mimeType);
49
});
50
51
// Access current MIME document
52
if (tracker.currentWidget) {
53
const current = tracker.currentWidget;
54
console.log('Current MIME document path:', current.context.path);
55
console.log('Current MIME type:', current.content.mimeType);
56
}
57
58
// Find specific MIME documents
59
const htmlDocs = tracker.filter(widget =>
60
widget.content.mimeType === 'text/html'
61
);
62
console.log(`Found ${htmlDocs.length} HTML documents`);
63
64
// Find first markdown document
65
const markdownDoc = tracker.find(widget =>
66
widget.content.mimeType === 'text/markdown'
67
);
68
if (markdownDoc) {
69
console.log('Found markdown document:', markdownDoc.title.label);
70
}
71
}
72
};
73
```
74
75
### createRendermimePlugins Function
76
77
Creates multiple plugins for rendering MIME type extensions in the JupyterLab environment.
78
79
```typescript { .api }
80
/**
81
* Creates plugins for multiple rendermime extensions
82
* @param extensions - Array of rendermime extension modules
83
* @returns Array of JupyterFrontEnd plugins
84
*/
85
function createRendermimePlugins(
86
extensions: IRenderMime.IExtensionModule[]
87
): JupyterFrontEndPlugin<void | IMimeDocumentTracker, any, any>[];
88
```
89
90
**Usage Examples:**
91
92
```typescript
93
import { createRendermimePlugins } from "@jupyterlab/application";
94
import { IRenderMime } from "@jupyterlab/rendermime-interfaces";
95
96
// Define custom MIME extensions
97
const customExtensions: IRenderMime.IExtensionModule[] = [
98
{
99
// CSV renderer extension
100
id: 'csv-renderer',
101
rendererFactory: {
102
safe: true,
103
mimeTypes: ['text/csv'],
104
createRenderer: (options) => new CSVRenderer(options)
105
},
106
dataType: 'string',
107
fileTypes: [{
108
name: 'csv',
109
extensions: ['.csv'],
110
mimeTypes: ['text/csv'],
111
icon: 'ui-components:csv'
112
}],
113
documentWidgetFactoryOptions: {
114
name: 'CSV Viewer',
115
primaryFileType: 'csv',
116
fileTypes: ['csv']
117
}
118
},
119
{
120
// JSON renderer extension
121
id: 'json-renderer',
122
rendererFactory: {
123
safe: true,
124
mimeTypes: ['application/json'],
125
createRenderer: (options) => new JSONRenderer(options)
126
},
127
dataType: 'string',
128
fileTypes: [{
129
name: 'json',
130
extensions: ['.json'],
131
mimeTypes: ['application/json'],
132
icon: 'ui-components:json'
133
}],
134
documentWidgetFactoryOptions: {
135
name: 'JSON Viewer',
136
primaryFileType: 'json',
137
fileTypes: ['json']
138
}
139
}
140
];
141
142
// Create plugins for all extensions
143
const mimePlugins = createRendermimePlugins(customExtensions);
144
145
// Register plugins with application
146
mimePlugins.forEach(plugin => {
147
app.registerPlugin(plugin);
148
});
149
150
// Example custom renderer implementation
151
class CSVRenderer implements IRenderMime.IRenderer {
152
constructor(options: IRenderMime.IRendererOptions) {
153
this.mimeType = 'text/csv';
154
}
155
156
readonly mimeType: string;
157
158
render(model: IRenderMime.IMimeModel): Promise<void> {
159
const data = model.data[this.mimeType] as string;
160
const element = document.createElement('div');
161
162
// Simple CSV to table conversion
163
const rows = data.split('\n').map(row => row.split(','));
164
const table = document.createElement('table');
165
166
rows.forEach((row, index) => {
167
const tr = document.createElement('tr');
168
row.forEach(cell => {
169
const td = document.createElement(index === 0 ? 'th' : 'td');
170
td.textContent = cell.trim();
171
tr.appendChild(td);
172
});
173
table.appendChild(tr);
174
});
175
176
element.appendChild(table);
177
return Promise.resolve();
178
}
179
}
180
```
181
182
### createRendermimePlugin Function
183
184
Creates a single plugin for a specific MIME type renderer.
185
186
```typescript { .api }
187
/**
188
* Creates a plugin for a single rendermime extension
189
* @param tracker - Widget tracker for MIME documents
190
* @param item - Single rendermime extension
191
* @returns JupyterFrontEnd plugin
192
*/
193
function createRendermimePlugin(
194
tracker: WidgetTracker<MimeDocument>,
195
item: IRenderMime.IExtension
196
): JupyterFrontEndPlugin<void>;
197
```
198
199
**Usage Examples:**
200
201
```typescript
202
import { createRendermimePlugin } from "@jupyterlab/application";
203
import { WidgetTracker } from "@jupyterlab/apputils";
204
import { MimeDocument } from "@jupyterlab/docregistry";
205
206
// Create a widget tracker for custom MIME documents
207
const customMimeTracker = new WidgetTracker<MimeDocument>({
208
namespace: 'custom-mime-documents'
209
});
210
211
// Define a single MIME extension
212
const xmlExtension: IRenderMime.IExtension = {
213
id: 'xml-renderer',
214
rendererFactory: {
215
safe: true,
216
mimeTypes: ['application/xml', 'text/xml'],
217
createRenderer: (options) => new XMLRenderer(options)
218
},
219
dataType: 'string',
220
fileTypes: [{
221
name: 'xml',
222
extensions: ['.xml', '.xsd', '.xsl'],
223
mimeTypes: ['application/xml', 'text/xml'],
224
icon: 'ui-components:xml'
225
}],
226
documentWidgetFactoryOptions: {
227
name: 'XML Viewer',
228
primaryFileType: 'xml',
229
fileTypes: ['xml'],
230
defaultFor: ['xml']
231
}
232
};
233
234
// Create plugin for the extension
235
const xmlPlugin = createRendermimePlugin(customMimeTracker, xmlExtension);
236
237
// Register with application
238
app.registerPlugin(xmlPlugin);
239
240
// Example XML renderer
241
class XMLRenderer implements IRenderMime.IRenderer {
242
constructor(options: IRenderMime.IRendererOptions) {
243
this.mimeType = 'application/xml';
244
}
245
246
readonly mimeType: string;
247
248
async render(model: IRenderMime.IMimeModel): Promise<void> {
249
const data = model.data[this.mimeType] as string;
250
const element = document.createElement('div');
251
element.className = 'xml-viewer';
252
253
try {
254
// Parse and format XML
255
const parser = new DOMParser();
256
const xmlDoc = parser.parseFromString(data, 'application/xml');
257
258
if (xmlDoc.documentElement.nodeName === 'parsererror') {
259
throw new Error('Invalid XML');
260
}
261
262
// Create formatted display
263
element.innerHTML = this.formatXML(data);
264
} catch (error) {
265
element.textContent = `Error parsing XML: ${error.message}`;
266
element.className += ' error';
267
}
268
269
return Promise.resolve();
270
}
271
272
private formatXML(xml: string): string {
273
// Simple XML formatting implementation
274
const formatted = xml
275
.replace(/></g, '>\n<')
276
.replace(/^\s*\n/gm, '');
277
278
return `<pre><code class="xml">${this.escapeHtml(formatted)}</code></pre>`;
279
}
280
281
private escapeHtml(text: string): string {
282
return text
283
.replace(/&/g, '&')
284
.replace(/</g, '<')
285
.replace(/>/g, '>')
286
.replace(/"/g, '"')
287
.replace(/'/g, ''');
288
}
289
}
290
```
291
292
### MIME Extension Configuration
293
294
Detailed configuration options for creating comprehensive MIME type support.
295
296
```typescript { .api }
297
/**
298
* Complete MIME extension configuration interface
299
*/
300
interface IRenderMime.IExtensionModule {
301
/** Unique identifier for the extension */
302
id: string;
303
304
/** Factory for creating renderers */
305
rendererFactory: IRenderMime.IRendererFactory;
306
307
/** Data type the renderer expects ('string' | 'json') */
308
dataType?: 'string' | 'json';
309
310
/** File types associated with this MIME type */
311
fileTypes?: DocumentRegistry.IFileType[];
312
313
/** Options for document widget factory */
314
documentWidgetFactoryOptions?: DocumentRegistry.IWidgetFactoryOptions;
315
}
316
317
/**
318
* Renderer factory interface
319
*/
320
interface IRenderMime.IRendererFactory {
321
/** Whether the renderer is safe (doesn't execute code) */
322
safe: boolean;
323
324
/** MIME types this renderer handles */
325
mimeTypes: string[];
326
327
/** Function to create renderer instances */
328
createRenderer: (options: IRenderMime.IRendererOptions) => IRenderMime.IRenderer;
329
330
/** Default render timeout in milliseconds */
331
defaultRank?: number;
332
333
/** Whether renderer can render untrusted content */
334
trusted?: boolean;
335
}
336
337
/**
338
* File type configuration for MIME extensions
339
*/
340
interface DocumentRegistry.IFileType {
341
/** Internal name for the file type */
342
name: string;
343
344
/** Display name for the file type */
345
displayName?: string;
346
347
/** File extensions (with dots) */
348
extensions: string[];
349
350
/** MIME types for this file type */
351
mimeTypes: string[];
352
353
/** Icon identifier */
354
icon?: string;
355
356
/** Icon class name */
357
iconClass?: string;
358
359
/** Icon label */
360
iconLabel?: string;
361
362
/** Content type category */
363
contentType?: 'file' | 'directory' | 'notebook';
364
365
/** File format ('text' | 'base64' | 'json') */
366
fileFormat?: 'text' | 'base64' | 'json';
367
}
368
```
369
370
### Advanced MIME Rendering Example
371
372
Complete example showing how to create a sophisticated MIME renderer with multiple features.
373
374
```typescript { .api }
375
// Advanced MIME rendering example
376
interface AdvancedMimeRenderer {
377
/** Support for multiple MIME variants */
378
supportedMimeTypes: string[];
379
380
/** Configurable rendering options */
381
renderingOptions: RenderingOptions;
382
383
/** Error handling and fallbacks */
384
errorHandling: ErrorHandlingOptions;
385
386
/** Performance optimization */
387
optimizations: PerformanceOptions;
388
}
389
390
interface RenderingOptions {
391
theme?: 'light' | 'dark' | 'auto';
392
maxSize?: number;
393
enableSyntaxHighlighting?: boolean;
394
enableLineNumbers?: boolean;
395
}
396
397
interface ErrorHandlingOptions {
398
showErrorDetails?: boolean;
399
fallbackRenderer?: string;
400
retryAttempts?: number;
401
}
402
403
interface PerformanceOptions {
404
lazyLoading?: boolean;
405
virtualScrolling?: boolean;
406
renderTimeout?: number;
407
}
408
```
409
410
**Complete Advanced Example:**
411
412
```typescript
413
import {
414
createRendermimePlugin,
415
IMimeDocumentTracker
416
} from "@jupyterlab/application";
417
import { IRenderMime } from "@jupyterlab/rendermime-interfaces";
418
import { WidgetTracker } from "@jupyterlab/apputils";
419
import { MimeDocument } from "@jupyterlab/docregistry";
420
421
// Advanced GeoJSON renderer with mapping capabilities
422
class GeoJSONRenderer implements IRenderMime.IRenderer {
423
private options: RenderingOptions;
424
425
constructor(rendererOptions: IRenderMime.IRendererOptions) {
426
this.mimeType = 'application/geo+json';
427
this.options = {
428
theme: 'auto',
429
maxSize: 10 * 1024 * 1024, // 10MB
430
enableSyntaxHighlighting: true,
431
enableLineNumbers: false
432
};
433
}
434
435
readonly mimeType = 'application/geo+json';
436
437
async render(model: IRenderMime.IMimeModel): Promise<void> {
438
const data = model.data[this.mimeType] as any;
439
const element = document.createElement('div');
440
element.className = 'geojson-viewer';
441
442
try {
443
// Check size limits
444
const dataSize = JSON.stringify(data).length;
445
if (dataSize > this.options.maxSize!) {
446
throw new Error(`Data too large: ${dataSize} bytes`);
447
}
448
449
// Create map container
450
const mapContainer = document.createElement('div');
451
mapContainer.className = 'geojson-map';
452
mapContainer.style.height = '400px';
453
mapContainer.style.width = '100%';
454
455
// Create data inspector
456
const inspector = document.createElement('div');
457
inspector.className = 'geojson-inspector';
458
459
// Add map (simplified - would use real mapping library)
460
await this.renderMap(mapContainer, data);
461
462
// Add data inspector
463
this.renderInspector(inspector, data);
464
465
// Create tabs for map vs data view
466
const tabs = this.createTabs([
467
{ label: 'Map View', content: mapContainer },
468
{ label: 'Data View', content: inspector }
469
]);
470
471
element.appendChild(tabs);
472
473
} catch (error) {
474
this.renderError(element, error as Error);
475
}
476
477
return Promise.resolve();
478
}
479
480
private async renderMap(container: HTMLElement, data: any): Promise<void> {
481
// Simplified map rendering - in real implementation would use
482
// libraries like Leaflet, Mapbox, or OpenLayers
483
container.innerHTML = `
484
<div class="mock-map">
485
<p>GeoJSON Map View</p>
486
<p>Features: ${data.features?.length || 0}</p>
487
<p>Type: ${data.type}</p>
488
</div>
489
`;
490
}
491
492
private renderInspector(container: HTMLElement, data: any): void {
493
const formatted = JSON.stringify(data, null, 2);
494
const pre = document.createElement('pre');
495
const code = document.createElement('code');
496
code.className = 'language-json';
497
code.textContent = formatted;
498
pre.appendChild(code);
499
container.appendChild(pre);
500
501
// Add syntax highlighting if enabled
502
if (this.options.enableSyntaxHighlighting) {
503
this.applySyntaxHighlighting(code);
504
}
505
}
506
507
private createTabs(tabs: { label: string; content: HTMLElement }[]): HTMLElement {
508
const container = document.createElement('div');
509
container.className = 'tab-container';
510
511
const tabHeaders = document.createElement('div');
512
tabHeaders.className = 'tab-headers';
513
514
const tabContents = document.createElement('div');
515
tabContents.className = 'tab-contents';
516
517
tabs.forEach((tab, index) => {
518
// Create header
519
const header = document.createElement('button');
520
header.textContent = tab.label;
521
header.className = 'tab-header';
522
if (index === 0) header.classList.add('active');
523
524
// Create content wrapper
525
const content = document.createElement('div');
526
content.className = 'tab-content';
527
content.style.display = index === 0 ? 'block' : 'none';
528
content.appendChild(tab.content);
529
530
// Add click handler
531
header.addEventListener('click', () => {
532
// Update headers
533
tabHeaders.querySelectorAll('.tab-header').forEach(h =>
534
h.classList.remove('active')
535
);
536
header.classList.add('active');
537
538
// Update contents
539
tabContents.querySelectorAll('.tab-content').forEach(c =>
540
(c as HTMLElement).style.display = 'none'
541
);
542
content.style.display = 'block';
543
});
544
545
tabHeaders.appendChild(header);
546
tabContents.appendChild(content);
547
});
548
549
container.appendChild(tabHeaders);
550
container.appendChild(tabContents);
551
return container;
552
}
553
554
private applySyntaxHighlighting(element: HTMLElement): void {
555
// Simplified syntax highlighting - would use library like Prism.js
556
element.innerHTML = element.innerHTML
557
.replace(/"([^"]+)":/g, '<span class="key">"$1":</span>')
558
.replace(/: "([^"]+)"/g, ': <span class="string">"$1"</span>')
559
.replace(/: (\d+)/g, ': <span class="number">$1</span>');
560
}
561
562
private renderError(container: HTMLElement, error: Error): void {
563
container.innerHTML = `
564
<div class="error">
565
<h3>Error rendering GeoJSON</h3>
566
<p>${error.message}</p>
567
<details>
568
<summary>Error Details</summary>
569
<pre>${error.stack}</pre>
570
</details>
571
</div>
572
`;
573
}
574
}
575
576
// Create comprehensive GeoJSON extension
577
const geoJSONExtension: IRenderMime.IExtensionModule = {
578
id: 'geojson-renderer',
579
rendererFactory: {
580
safe: true,
581
mimeTypes: ['application/geo+json'],
582
createRenderer: (options) => new GeoJSONRenderer(options),
583
defaultRank: 0,
584
trusted: true
585
},
586
dataType: 'json',
587
fileTypes: [{
588
name: 'geojson',
589
displayName: 'GeoJSON',
590
extensions: ['.geojson', '.json'],
591
mimeTypes: ['application/geo+json'],
592
icon: 'ui-components:json',
593
contentType: 'file',
594
fileFormat: 'text'
595
}],
596
documentWidgetFactoryOptions: {
597
name: 'GeoJSON Viewer',
598
primaryFileType: 'geojson',
599
fileTypes: ['geojson'],
600
defaultFor: ['geojson']
601
}
602
};
603
604
// Create plugin
605
const tracker = new WidgetTracker<MimeDocument>({
606
namespace: 'geojson-documents'
607
});
608
609
const geoJSONPlugin = createRendermimePlugin(tracker, geoJSONExtension);
610
611
// Register with application
612
app.registerPlugin(geoJSONPlugin);
613
```
614
615
## Best Practices
616
617
### Performance Considerations
618
619
```typescript
620
// Performance optimization patterns
621
class PerformantMimeRenderer implements IRenderMime.IRenderer {
622
private renderCache = new Map<string, HTMLElement>();
623
624
readonly mimeType = 'custom/large-data';
625
626
async render(model: IRenderMime.IMimeModel): Promise<void> {
627
const data = model.data[this.mimeType];
628
const cacheKey = this.getCacheKey(data);
629
630
// Check cache first
631
if (this.renderCache.has(cacheKey)) {
632
const cached = this.renderCache.get(cacheKey)!;
633
return Promise.resolve();
634
}
635
636
// Lazy loading for large datasets
637
if (this.isLargeDataset(data)) {
638
return this.renderLazy(data, cacheKey);
639
}
640
641
// Regular rendering for small datasets
642
return this.renderImmediate(data, cacheKey);
643
}
644
645
private isLargeDataset(data: any): boolean {
646
return JSON.stringify(data).length > 100 * 1024; // 100KB
647
}
648
649
private async renderLazy(data: any, cacheKey: string): Promise<void> {
650
// Implement virtual scrolling or pagination
651
// Load data in chunks
652
}
653
654
private async renderImmediate(data: any, cacheKey: string): Promise<void> {
655
// Render all data immediately
656
}
657
658
private getCacheKey(data: any): string {
659
// Create cache key from data hash
660
return btoa(JSON.stringify(data)).substring(0, 32);
661
}
662
}
663
```