diff --git a/extensions/notebook-renderers/src/index.ts b/extensions/notebook-renderers/src/index.ts index efd9a81bfac3c..10fd6c99eb281 100644 --- a/extensions/notebook-renderers/src/index.ts +++ b/extensions/notebook-renderers/src/index.ts @@ -33,6 +33,10 @@ function renderImage(outputInfo: OutputItem, element: HTMLElement): IDisposable const image = document.createElement('img'); image.src = src; + const alt = getAltText(outputInfo); + if (alt) { + image.alt = alt; + } const display = document.createElement('div'); display.classList.add('display'); display.appendChild(image); @@ -64,12 +68,33 @@ const domEval = (container: Element) => { } }; +function getAltText(outputInfo: OutputItem) { + const metadata = outputInfo.metadata; + if (typeof metadata === 'object' && metadata && 'vscode_altText' in metadata && typeof metadata.vscode_altText === 'string') { + return metadata.vscode_altText; + } + return undefined; +} + +function injectTitleForSvg(outputInfo: OutputItem, element: HTMLElement) { + if (outputInfo.mime.indexOf('svg') > -1) { + const svgElement = element.querySelector('svg'); + const altText = getAltText(outputInfo); + if (svgElement && altText) { + const title = document.createElement('title'); + title.innerText = altText; + svgElement.prepend(title); + } + } +} + async function renderHTML(outputInfo: OutputItem, container: HTMLElement, signal: AbortSignal, hooks: Iterable): Promise { clearContainer(container); let element: HTMLElement = document.createElement('div'); const htmlContent = outputInfo.text(); const trustedHtml = ttPolicy?.createHTML(htmlContent) ?? htmlContent; element.innerHTML = trustedHtml as string; + injectTitleForSvg(outputInfo, element); for (const hook of hooks) { element = (await hook.postRender(outputInfo, element, signal)) ?? element; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index d2712dea5f82b..590d2f00c4288 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -38,7 +38,7 @@ import { NOTEBOOK_WEBVIEW_BOUNDARY } from 'vs/workbench/contrib/notebook/browser import { preloadsScriptStr } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads'; import { transformWebviewThemeVars } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewThemeMapping'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; -import { CellUri, INotebookRendererInfo, RendererMessagingSpec } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellUri, ICellOutput, INotebookRendererInfo, RendererMessagingSpec } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookKernel } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { IScopedRendererMessaging } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; @@ -1422,6 +1422,17 @@ export class BackLayerWebView extends Themable { createOutput(); } + private createMetadata(output: ICellOutput, mimeType: string) { + if (mimeType.startsWith('image')) { + const buffer = output.outputs.find(out => out.mime === 'text/plain')?.data.buffer; + if (buffer?.length && buffer?.length > 0) { + const altText = new TextDecoder().decode(buffer); + return { ...output.metadata, vscode_altText: altText }; + } + } + return output.metadata; + } + private _createOutputCreationMessage(cellInfo: T, content: IInsetRenderOutput, cellTop: number, offset: number, createOnIdle: boolean, initiallyHidden: boolean): { readonly message: ICreationRequestMessage; readonly renderer: INotebookRendererInfo | undefined; transfer: readonly ArrayBuffer[] } { const messageBase = { type: 'html', @@ -1442,7 +1453,7 @@ export class BackLayerWebView extends Themable { const output = content.source.model; renderer = content.renderer; const first = output.outputs.find(op => op.mime === content.mimeType)!; - + const metadata = this.createMetadata(output, content.mimeType); const valueBytes = copyBufferIfNeeded(first.data.buffer, transfer); message = { ...messageBase, @@ -1451,7 +1462,7 @@ export class BackLayerWebView extends Themable { content: { type: RenderOutputType.Extension, outputId: output.outputId, - metadata: output.metadata, + metadata: metadata, output: { mime: first.mime, valueBytes,