TypeScript interfaces for implementing MIME renderer extensions in JupyterLab
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Supporting interfaces for HTML sanitization, URL resolution, LaTeX typesetting, and Markdown parsing.
Handles HTML sanitization to ensure safe rendering of untrusted content.
/**
* An object that handles html sanitization
*/
interface ISanitizer {
/** Whether to replace URLs by HTML anchors */
getAutolink?(): boolean;
/**
* Sanitize an HTML string
* @param dirty - The dirty text
* @param options - The optional sanitization options
* @returns The sanitized string
*/
sanitize(dirty: string, options?: ISanitizerOptions): string;
/** Whether to allow name and id properties */
readonly allowNamedProperties?: boolean;
}Configuration options for HTML sanitization.
/**
* The options used to sanitize
*/
interface ISanitizerOptions {
/** The allowed tags */
allowedTags?: string[];
/** The allowed attributes for a given tag */
allowedAttributes?: { [key: string]: string[] };
/** The allowed style values for a given tag */
allowedStyles?: { [key: string]: { [key: string]: RegExp[] } };
}Usage Example:
import { IRenderMime } from "@jupyterlab/rendermime-interfaces";
class CustomRenderer implements IRenderMime.IRenderer {
constructor(private options: IRenderMime.IRendererOptions) {}
async renderModel(model: IRenderMime.IMimeModel): Promise<void> {
const htmlContent = model.data['text/html'] as string;
// Custom sanitization options for this renderer
const sanitizeOptions: IRenderMime.ISanitizerOptions = {
allowedTags: ['div', 'span', 'p', 'strong', 'em', 'a', 'img'],
allowedAttributes: {
'a': ['href', 'title'],
'img': ['src', 'alt', 'width', 'height'],
'div': ['class', 'style']
},
allowedStyles: {
'div': {
'color': [/^#[0-9a-f]{6}$/i],
'background-color': [/^#[0-9a-f]{6}$/i],
'font-size': [/^\d+px$/]
}
}
};
// Sanitize the HTML
const sanitizedHtml = this.options.sanitizer.sanitize(htmlContent, sanitizeOptions);
// Check if autolink is enabled
const hasAutolink = this.options.sanitizer.getAutolink?.() ?? false;
if (hasAutolink) {
console.log('URLs will be automatically converted to links');
}
this.node.innerHTML = sanitizedHtml;
}
}Resolves relative URLs and handles file path resolution within JupyterLab.
/**
* An object that resolves relative URLs
*/
interface IResolver {
/** Resolve a relative url to an absolute url path */
resolveUrl(url: string): Promise<string>;
/**
* Get the download url for a given absolute url path.
* This URL may include a query parameter.
*/
getDownloadUrl(url: string): Promise<string>;
/**
* Whether the URL should be handled by the resolver or not.
* This is similar to the `isLocal` check in `URLExt`,
* but can also perform additional checks on whether the
* resolver should handle a given URL.
* @param allowRoot - Whether the paths starting at Unix-style filesystem root (`/`) are permitted
*/
isLocal?(url: string, allowRoot?: boolean): boolean;
/**
* Resolve a path from Jupyter kernel to a path:
* - relative to `root_dir` (preferably) this is in jupyter-server scope,
* - path understood and known by kernel (if such a path exists).
* Returns `null` if there is no file matching provided path in neither
* kernel nor jupyter-server contents manager.
*/
resolvePath?(path: string): Promise<IResolvedLocation | null>;
}Represents a resolved file location with scope information.
interface IResolvedLocation {
/** Location scope */
scope: 'kernel' | 'server';
/** Resolved path */
path: string;
}Usage Example:
import { IRenderMime } from "@jupyterlab/rendermime-interfaces";
class ImageRenderer implements IRenderMime.IRenderer {
constructor(private options: IRenderMime.IRendererOptions) {}
async renderModel(model: IRenderMime.IMimeModel): Promise<void> {
const imagePath = model.data['image/path'] as string;
const resolver = this.options.resolver;
if (resolver) {
try {
// Check if this is a local path we should handle
const isLocal = resolver.isLocal?.(imagePath) ?? true;
if (isLocal) {
// Resolve relative path to absolute
const absoluteUrl = await resolver.resolveUrl(imagePath);
// Get download URL for the image
const downloadUrl = await resolver.getDownloadUrl(absoluteUrl);
// Resolve kernel path if needed
const resolved = await resolver.resolvePath?.(imagePath);
if (resolved) {
console.log(`Image resolved to ${resolved.scope}: ${resolved.path}`);
}
// Create image element with resolved URL
const img = document.createElement('img');
img.src = downloadUrl;
img.alt = 'Resolved image';
this.node.appendChild(img);
}
} catch (error) {
console.error('Failed to resolve image path:', error);
this.node.textContent = `Failed to load image: ${imagePath}`;
}
}
}
}Handles click events on links within rendered content.
/**
* An object that handles links on a node
*/
interface ILinkHandler {
/**
* Add the link handler to the node
* @param node the anchor node for which to handle the link
* @param path the path to open when the link is clicked
* @param id an optional element id to scroll to when the path is opened
*/
handleLink(node: HTMLElement, path: string, id?: string): void;
/**
* Add the path handler to the node
* @param node the anchor node for which to handle the link
* @param path the path to open when the link is clicked
* @param scope the scope to which the path is bound
* @param id an optional element id to scroll to when the path is opened
*/
handlePath?(node: HTMLElement, path: string, scope: 'kernel' | 'server', id?: string): void;
}Usage Example:
import { IRenderMime } from "@jupyterlab/rendermime-interfaces";
class LinkEnabledRenderer implements IRenderMime.IRenderer {
constructor(private options: IRenderMime.IRendererOptions) {}
async renderModel(model: IRenderMime.IMimeModel): Promise<void> {
const htmlContent = model.data['text/html'] as string;
const sanitized = this.options.sanitizer.sanitize(htmlContent);
this.node.innerHTML = sanitized;
// Setup link handling
if (this.options.linkHandler) {
this.setupLinkHandling();
}
}
private setupLinkHandling(): void {
const links = this.node.querySelectorAll('a[href]');
links.forEach(link => {
const href = link.getAttribute('href')!;
const linkElement = link as HTMLElement;
// Handle different types of links
if (href.startsWith('#')) {
// Fragment link - scroll to element
const elementId = href.substring(1);
this.options.linkHandler!.handleLink(linkElement, '', elementId);
} else if (href.startsWith('/')) {
// Absolute path - specify scope
this.options.linkHandler!.handlePath?.(linkElement, href, 'server');
} else if (!href.startsWith('http')) {
// Relative path
this.options.linkHandler!.handleLink(linkElement, href);
}
// External links (http/https) are handled by default browser behavior
});
}
}Handles LaTeX mathematical expression rendering.
/**
* The interface for a LaTeX typesetter
*/
interface ILatexTypesetter {
/**
* Typeset a DOM element.
* The typesetting may happen synchronously or asynchronously.
* @param element - the DOM element to typeset
*/
typeset(element: HTMLElement): void;
}Usage Example:
import { IRenderMime } from "@jupyterlab/rendermime-interfaces";
class MathRenderer implements IRenderMime.IRenderer {
constructor(private options: IRenderMime.IRendererOptions) {}
async renderModel(model: IRenderMime.IMimeModel): Promise<void> {
const mathContent = model.data['text/latex'] as string;
// Create container for math content
const mathContainer = document.createElement('div');
mathContainer.className = 'math-content';
mathContainer.textContent = mathContent;
this.node.appendChild(mathContainer);
// Typeset LaTeX if typesetter is available
if (this.options.latexTypesetter) {
this.options.latexTypesetter.typeset(this.node);
}
}
}Converts Markdown text to HTML.
/**
* The interface for a Markdown parser
*/
interface IMarkdownParser {
/**
* Render a markdown source into unsanitized HTML
* @param source - The string to render
* @returns A promise of the string containing HTML which may require sanitization
*/
render(source: string): Promise<string>;
}Usage Example:
import { IRenderMime } from "@jupyterlab/rendermime-interfaces";
class MarkdownRenderer implements IRenderMime.IRenderer {
constructor(private options: IRenderMime.IRendererOptions) {}
async renderModel(model: IRenderMime.IMimeModel): Promise<void> {
const markdownSource = model.data['text/markdown'] as string;
if (this.options.markdownParser) {
try {
// Parse markdown to HTML
const rawHtml = await this.options.markdownParser.render(markdownSource);
// Sanitize the resulting HTML
const sanitizedHtml = this.options.sanitizer.sanitize(rawHtml);
this.node.innerHTML = sanitizedHtml;
// Apply LaTeX typesetting if available
if (this.options.latexTypesetter) {
this.options.latexTypesetter.typeset(this.node);
}
// Setup link handling if available
if (this.options.linkHandler) {
this.setupLinks();
}
} catch (error) {
console.error('Failed to render markdown:', error);
this.node.textContent = 'Failed to render markdown content';
}
} else {
// Fallback: display raw markdown
const pre = document.createElement('pre');
pre.textContent = markdownSource;
this.node.appendChild(pre);
}
}
private setupLinks(): void {
const links = this.node.querySelectorAll('a[href]');
links.forEach(link => {
const href = link.getAttribute('href')!;
this.options.linkHandler!.handleLink(link as HTMLElement, href);
});
}
}