Skip to content

Commit a77d1c2

Browse files
authored
Add JS notebook renderer hook (microsoft#160453)
Fixes microsoft#160451
1 parent 11ccbec commit a77d1c2

File tree

1 file changed

+48
-21
lines changed
  • extensions/notebook-renderers/src

1 file changed

+48
-21
lines changed

extensions/notebook-renderers/src/index.ts

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,16 @@ interface HtmlRenderingHook {
1616
*
1717
* @return A new `HTMLElement` or `undefined` to continue using the provided element.
1818
*/
19-
postRender(outputItem: OutputItem, element: HTMLElement): HTMLElement | undefined;
19+
postRender(outputItem: OutputItem, element: HTMLElement, signal: AbortSignal): HTMLElement | undefined | Promise<HTMLElement | undefined>;
20+
}
21+
22+
interface JavaScriptRenderingHook {
23+
/**
24+
* Invoked before the script is evaluated.
25+
*
26+
* @return A new string of JavaScript or `undefined` to continue using the provided string.
27+
*/
28+
preEvaluate(outputItem: OutputItem, element: string, signal: AbortSignal): string | undefined | Promise<string | undefined>;
2029
}
2130

2231
function clearContainer(container: HTMLElement) {
@@ -72,25 +81,37 @@ const domEval = (container: Element) => {
7281
}
7382
};
7483

75-
function renderHTML(outputInfo: OutputItem, container: HTMLElement, hooks: Iterable<HtmlRenderingHook>): void {
84+
async function renderHTML(outputInfo: OutputItem, container: HTMLElement, signal: AbortSignal, hooks: Iterable<HtmlRenderingHook>): Promise<void> {
7685
clearContainer(container);
7786
let element: HTMLElement = document.createElement('div');
7887
const htmlContent = outputInfo.text();
7988
const trustedHtml = ttPolicy?.createHTML(htmlContent) ?? htmlContent;
8089
element.innerHTML = trustedHtml as string;
8190

8291
for (const hook of hooks) {
83-
element = hook.postRender(outputInfo, element) ?? element;
92+
element = (await hook.postRender(outputInfo, element, signal)) ?? element;
93+
if (signal.aborted) {
94+
return;
95+
}
8496
}
8597

8698
container.appendChild(element);
8799
domEval(element);
88100
}
89101

90-
function renderJavascript(outputInfo: OutputItem, container: HTMLElement): void {
102+
async function renderJavascript(outputInfo: OutputItem, container: HTMLElement, signal: AbortSignal, hooks: Iterable<JavaScriptRenderingHook>): Promise<void> {
103+
let scriptText = outputInfo.text();
104+
105+
for (const hook of hooks) {
106+
scriptText = (await hook.preEvaluate(outputInfo, scriptText, signal)) ?? scriptText;
107+
if (signal.aborted) {
108+
return;
109+
}
110+
}
111+
91112
const script = document.createElement('script');
92113
script.type = 'module';
93-
script.textContent = outputInfo.text();
114+
script.textContent = scriptText;
94115

95116
const element = document.createElement('div');
96117
const trustedHtml = ttPolicy?.createHTML(script.outerHTML) ?? script.outerHTML;
@@ -177,12 +198,12 @@ function renderText(outputInfo: OutputItem, container: HTMLElement, ctx: Rendere
177198
const text = outputInfo.text();
178199
truncatedArrayOfString(outputInfo.id, [text], ctx.settings.lineLimit, contentNode);
179200
container.appendChild(contentNode);
180-
181201
}
182202

183203
export const activate: ActivationFunction<void> = (ctx) => {
184204
const disposables = new Map<string, IDisposable>();
185205
const htmlHooks = new Set<HtmlRenderingHook>();
206+
const jsHooks = new Set<JavaScriptRenderingHook>();
186207

187208
const latestContext = ctx as (RendererContext<void> & { readonly settings: { readonly lineLimit: number } });
188209

@@ -229,27 +250,25 @@ export const activate: ActivationFunction<void> = (ctx) => {
229250
document.body.appendChild(style);
230251

231252
return {
232-
renderOutputItem: (outputInfo, element) => {
253+
renderOutputItem: async (outputInfo, element, signal?: AbortSignal) => {
233254
switch (outputInfo.mime) {
234255
case 'text/html':
235-
case 'image/svg+xml':
236-
{
237-
if (!ctx.workspace.isTrusted) {
238-
return;
239-
}
240-
241-
renderHTML(outputInfo, element, htmlHooks);
256+
case 'image/svg+xml': {
257+
if (!ctx.workspace.isTrusted) {
258+
return;
242259
}
243-
break;
244-
case 'application/javascript':
245-
{
246-
if (!ctx.workspace.isTrusted) {
247-
return;
248-
}
249260

250-
renderJavascript(outputInfo, element);
261+
await renderHTML(outputInfo, element, signal!, htmlHooks);
262+
break;
263+
}
264+
case 'application/javascript': {
265+
if (!ctx.workspace.isTrusted) {
266+
return;
251267
}
268+
269+
renderJavascript(outputInfo, element, signal!, jsHooks);
252270
break;
271+
}
253272
case 'image/gif':
254273
case 'image/png':
255274
case 'image/jpeg':
@@ -300,6 +319,14 @@ export const activate: ActivationFunction<void> = (ctx) => {
300319
htmlHooks.delete(hook);
301320
}
302321
};
322+
},
323+
experimental_registerJavaScriptRenderingHook: (hook: JavaScriptRenderingHook): IDisposable => {
324+
jsHooks.add(hook);
325+
return {
326+
dispose: () => {
327+
jsHooks.delete(hook);
328+
}
329+
};
303330
}
304331
};
305332
};

0 commit comments

Comments
 (0)