mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-01 09:35:41 -05:00
282 lines
8.2 KiB
TypeScript
282 lines
8.2 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import type { ActivationFunction, OutputItem, RendererContext } from 'vscode-notebook-renderer';
|
|
import { truncatedArrayOfString } from './textHelper';
|
|
|
|
interface IDisposable {
|
|
dispose(): void;
|
|
}
|
|
|
|
function clearContainer(container: HTMLElement) {
|
|
while (container.firstChild) {
|
|
container.removeChild(container.firstChild);
|
|
}
|
|
}
|
|
|
|
|
|
function renderImage(outputInfo: OutputItem, element: HTMLElement): IDisposable {
|
|
const blob = new Blob([outputInfo.data()], { type: outputInfo.mime });
|
|
const src = URL.createObjectURL(blob);
|
|
const disposable = {
|
|
dispose: () => {
|
|
URL.revokeObjectURL(src);
|
|
}
|
|
};
|
|
|
|
const image = document.createElement('img');
|
|
image.src = src;
|
|
const display = document.createElement('div');
|
|
display.classList.add('display');
|
|
display.appendChild(image);
|
|
element.appendChild(display);
|
|
|
|
return disposable;
|
|
}
|
|
|
|
const ttPolicy = window.trustedTypes?.createPolicy('notebookRenderer', {
|
|
createHTML: value => value,
|
|
createScript: value => value,
|
|
});
|
|
|
|
const preservedScriptAttributes: (keyof HTMLScriptElement)[] = [
|
|
'type', 'src', 'nonce', 'noModule', 'async',
|
|
];
|
|
|
|
const domEval = (container: Element) => {
|
|
const arr = Array.from(container.getElementsByTagName('script'));
|
|
for (let n = 0; n < arr.length; n++) {
|
|
const node = arr[n];
|
|
const scriptTag = document.createElement('script');
|
|
const trustedScript = ttPolicy?.createScript(node.innerText) ?? node.innerText;
|
|
scriptTag.text = trustedScript as string;
|
|
for (const key of preservedScriptAttributes) {
|
|
const val = node[key] || node.getAttribute && node.getAttribute(key);
|
|
if (val) {
|
|
scriptTag.setAttribute(key, val as any);
|
|
}
|
|
}
|
|
|
|
// TODO@connor4312: should script with src not be removed?
|
|
container.appendChild(scriptTag).parentNode!.removeChild(scriptTag);
|
|
}
|
|
};
|
|
|
|
function renderHTML(outputInfo: OutputItem, container: HTMLElement): void {
|
|
clearContainer(container);
|
|
const htmlContent = outputInfo.text();
|
|
const element = document.createElement('div');
|
|
const trustedHtml = ttPolicy?.createHTML(htmlContent) ?? htmlContent;
|
|
element.innerHTML = trustedHtml as string;
|
|
container.appendChild(element);
|
|
domEval(element);
|
|
}
|
|
|
|
function renderJavascript(outputInfo: OutputItem, container: HTMLElement): void {
|
|
const str = outputInfo.text();
|
|
const scriptVal = `<script type="application/javascript">${str}</script>`;
|
|
const element = document.createElement('div');
|
|
const trustedHtml = ttPolicy?.createHTML(scriptVal) ?? scriptVal;
|
|
element.innerHTML = trustedHtml as string;
|
|
container.appendChild(element);
|
|
domEval(element);
|
|
}
|
|
|
|
function renderError(outputInfo: OutputItem, container: HTMLElement, ctx: RendererContext<void> & { readonly settings: { readonly lineLimit: number } }): void {
|
|
const element = document.createElement('div');
|
|
container.appendChild(element);
|
|
type ErrorLike = Partial<Error>;
|
|
|
|
let err: ErrorLike;
|
|
try {
|
|
err = <ErrorLike>JSON.parse(outputInfo.text());
|
|
} catch (e) {
|
|
console.log(e);
|
|
return;
|
|
}
|
|
|
|
if (err.stack) {
|
|
const stack = document.createElement('pre');
|
|
stack.classList.add('traceback');
|
|
stack.style.margin = '8px 0';
|
|
const element = document.createElement('span');
|
|
truncatedArrayOfString(outputInfo.id, [err.stack ?? ''], ctx.settings.lineLimit, element);
|
|
stack.appendChild(element);
|
|
container.appendChild(stack);
|
|
} else {
|
|
const header = document.createElement('div');
|
|
const headerMessage = err.name && err.message ? `${err.name}: ${err.message}` : err.name || err.message;
|
|
if (headerMessage) {
|
|
header.innerText = headerMessage;
|
|
container.appendChild(header);
|
|
}
|
|
}
|
|
|
|
container.classList.add('error');
|
|
}
|
|
|
|
function renderStream(outputInfo: OutputItem, container: HTMLElement, error: boolean, ctx: RendererContext<void> & { readonly settings: { readonly lineLimit: number } }): void {
|
|
const outputContainer = container.parentElement;
|
|
if (!outputContainer) {
|
|
// should never happen
|
|
return;
|
|
}
|
|
|
|
const prev = outputContainer.previousSibling;
|
|
if (prev) {
|
|
// OutputItem in the same cell
|
|
// check if the previous item is a stream
|
|
const outputElement = (prev.firstChild as HTMLElement | null);
|
|
if (outputElement && outputElement.getAttribute('output-mime-type') === outputInfo.mime) {
|
|
// same stream
|
|
const text = outputInfo.text();
|
|
|
|
const element = document.createElement('span');
|
|
truncatedArrayOfString(outputInfo.id, [text], ctx.settings.lineLimit, element);
|
|
outputElement.appendChild(element);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const element = document.createElement('span');
|
|
element.classList.add('output-stream');
|
|
|
|
const text = outputInfo.text();
|
|
truncatedArrayOfString(outputInfo.id, [text], ctx.settings.lineLimit, element);
|
|
while (container.firstChild) {
|
|
container.removeChild(container.firstChild);
|
|
}
|
|
container.appendChild(element);
|
|
container.setAttribute('output-mime-type', outputInfo.mime);
|
|
if (error) {
|
|
container.classList.add('error');
|
|
}
|
|
}
|
|
|
|
function renderText(outputInfo: OutputItem, container: HTMLElement, ctx: RendererContext<void> & { readonly settings: { readonly lineLimit: number } }): void {
|
|
clearContainer(container);
|
|
const contentNode = document.createElement('div');
|
|
contentNode.classList.add('output-plaintext');
|
|
const text = outputInfo.text();
|
|
truncatedArrayOfString(outputInfo.id, [text], ctx.settings.lineLimit, contentNode);
|
|
container.appendChild(contentNode);
|
|
|
|
}
|
|
|
|
export const activate: ActivationFunction<void> = (ctx) => {
|
|
const disposables = new Map<string, IDisposable>();
|
|
const latestContext = ctx as (RendererContext<void> & { readonly settings: { readonly lineLimit: number } });
|
|
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
.output-plaintext,
|
|
.output-stream,
|
|
.traceback {
|
|
line-height: var(--notebook-cell-output-line-height);
|
|
font-family: var(--notebook-cell-output-font-family);
|
|
white-space: pre-wrap;
|
|
word-wrap: break-word;
|
|
|
|
font-size: var(--notebook-cell-output-font-size);
|
|
user-select: text;
|
|
-webkit-user-select: text;
|
|
-ms-user-select: text;
|
|
cursor: auto;
|
|
}
|
|
span.output-stream {
|
|
display: inline-block;
|
|
}
|
|
.output-plaintext .code-bold,
|
|
.output-stream .code-bold,
|
|
.traceback .code-bold {
|
|
font-weight: bold;
|
|
}
|
|
.output-plaintext .code-italic,
|
|
.output-stream .code-italic,
|
|
.traceback .code-italic {
|
|
font-style: italic;
|
|
}
|
|
.output-plaintext .code-strike-through,
|
|
.output-stream .code-strike-through,
|
|
.traceback .code-strike-through {
|
|
text-decoration: line-through;
|
|
}
|
|
.output-plaintext .code-underline,
|
|
.output-stream .code-underline,
|
|
.traceback .code-underline {
|
|
text-decoration: underline;
|
|
}
|
|
`;
|
|
document.body.appendChild(style);
|
|
return {
|
|
renderOutputItem: (outputInfo, element) => {
|
|
switch (outputInfo.mime) {
|
|
case 'text/html':
|
|
case 'image/svg+xml':
|
|
{
|
|
if (!ctx.workspace.isTrusted) {
|
|
return;
|
|
}
|
|
|
|
renderHTML(outputInfo, element);
|
|
}
|
|
break;
|
|
case 'application/javascript':
|
|
{
|
|
if (!ctx.workspace.isTrusted) {
|
|
return;
|
|
}
|
|
|
|
renderJavascript(outputInfo, element);
|
|
}
|
|
break;
|
|
case 'image/gif':
|
|
case 'image/png':
|
|
case 'image/jpeg':
|
|
case 'image/git':
|
|
{
|
|
const disposable = renderImage(outputInfo, element);
|
|
disposables.set(outputInfo.id, disposable);
|
|
}
|
|
break;
|
|
case 'application/vnd.code.notebook.error':
|
|
{
|
|
renderError(outputInfo, element, latestContext);
|
|
}
|
|
break;
|
|
case 'application/vnd.code.notebook.stdout':
|
|
case 'application/x.notebook.stdout':
|
|
case 'application/x.notebook.stream':
|
|
{
|
|
renderStream(outputInfo, element, false, latestContext);
|
|
}
|
|
break;
|
|
case 'application/vnd.code.notebook.stderr':
|
|
case 'application/x.notebook.stderr':
|
|
{
|
|
renderStream(outputInfo, element, true, latestContext);
|
|
}
|
|
break;
|
|
case 'text/plain':
|
|
{
|
|
renderText(outputInfo, element, latestContext);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
|
|
},
|
|
disposeOutputItem: (id: string | undefined) => {
|
|
if (id) {
|
|
disposables.get(id)?.dispose();
|
|
} else {
|
|
disposables.forEach(d => d.dispose());
|
|
}
|
|
}
|
|
};
|
|
};
|