0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-05-11 11:25:42 +02:00
This commit is contained in:
karthik.bhandary 2026-04-29 11:18:34 +00:00
parent ab43de0836
commit 85634015d1
2 changed files with 54 additions and 85 deletions

View File

@ -9,7 +9,6 @@
/* Cell containers */
.jupyter-notebook .cell {
margin-bottom: 20px;
overflow: hidden;
}
/* Markdown cells */

View File

@ -2,48 +2,23 @@ import type {FrontendRenderFunc} from '../plugin.ts';
import {marked} from 'marked';
import '../../../css/features/jupyter.css';
// Sanitize HTML by removing dangerous attributes and elements
function sanitizeHtml(element: HTMLElement) {
const dangerousAttrs = ['onerror', 'onload', 'onclick', 'onmouseover', 'onmouseout', 'onmousemove',
'onmouseenter', 'onmouseleave', 'onfocus', 'onblur', 'onchange', 'onsubmit', 'onkeydown',
'onkeyup', 'onkeypress', 'onanimationstart', 'onanimationend', 'onbegin', 'onend', 'onrepeat'];
const walker = document.createTreeWalker(element, NodeFilter.SHOW_ELEMENT);
const nodes: Element[] = [];
let node: Node | null;
while ((node = walker.nextNode())) {
nodes.push(node as Element);
}
for (const el of nodes) {
// Remove all on* event handlers
for (const attr of dangerousAttrs) {
el.removeAttribute(attr);
}
// Remove javascript: and data: URLs from href and src
const urlPattern = /^(javascript|data):/;
const href = el.getAttribute('href');
if (href && urlPattern.test(href.toLowerCase().trim())) {
el.removeAttribute('href');
}
const src = el.getAttribute('src');
if (src && urlPattern.test(src.toLowerCase().trim())) {
el.removeAttribute('src');
}
// Remove <script>, <iframe>, and <foreignObject> elements (SVG can embed HTML via foreignObject)
if (el.tagName === 'SCRIPT' || el.tagName === 'IFRAME' || el.tagName === 'foreignObject') {
el.remove();
}
}
// Helper to create elements with properties
function createElement<K extends keyof HTMLElementTagNameMap>(
tag: K,
props?: {className?: string; textContent?: string; innerHTML?: string},
): HTMLElementTagNameMap[K] {
const el = document.createElement(tag);
if (props?.className) el.className = props.className;
if (props?.textContent) el.textContent = props.textContent;
if (props?.innerHTML) el.innerHTML = props.innerHTML;
return el;
}
// Render markdown using marked library
function renderMarkdown(markdown: string): HTMLElement {
const container = document.createElement('div');
container.className = 'markup';
container.innerHTML = marked.parse(markdown) as string;
sanitizeHtml(container);
return container;
}
@ -60,38 +35,33 @@ export const frontendRender: FrontendRenderFunc = async (opts) => {
notebook.metadata?.kernelspec?.language ||
'text';
const container = document.createElement('div');
container.className = 'jupyter-notebook';
const container = createElement('div', {className: 'jupyter-notebook'});
let executionCount = 1;
for (const cell of notebook.cells) {
if (!cell.cell_type) continue;
const cellDiv = document.createElement('div');
cellDiv.className = `cell ${cell.cell_type}`;
const cellDiv = createElement('div', {className: `cell ${cell.cell_type}`});
if (cell.cell_type === 'markdown') {
const inputDiv = document.createElement('div');
inputDiv.className = 'input markup';
const inputDiv = createElement('div', {className: 'input markup'});
const source = Array.isArray(cell.source) ? cell.source.join('') : (cell.source || '');
inputDiv.append(renderMarkdown(source));
cellDiv.append(inputDiv);
} else if (cell.cell_type === 'code') {
const inputWrapper = document.createElement('div');
inputWrapper.className = 'input-wrapper';
const inputWrapper = createElement('div', {className: 'input-wrapper'});
const prompt = document.createElement('div');
prompt.className = 'prompt input-prompt';
prompt.textContent = `In [${cell.execution_count ?? executionCount}]:`;
const prompt = createElement('div', {
className: 'prompt input-prompt',
textContent: `In [${cell.execution_count ?? executionCount}]:`,
});
inputWrapper.append(prompt);
const inputDiv = document.createElement('div');
inputDiv.className = 'input';
const inputDiv = createElement('div', {className: 'input'});
const pre = document.createElement('pre');
const code = document.createElement('code');
code.className = `language-${language}`;
const code = createElement('code', {className: `language-${language}`});
const source = Array.isArray(cell.source) ? cell.source.join('') : (cell.source || '');
code.textContent = source;
pre.append(code);
@ -100,20 +70,17 @@ export const frontendRender: FrontendRenderFunc = async (opts) => {
cellDiv.append(inputWrapper);
if (cell.outputs && Array.isArray(cell.outputs) && cell.outputs.length > 0) {
const outputWrapper = document.createElement('div');
outputWrapper.className = 'output-wrapper';
const outputWrapper = createElement('div', {className: 'output-wrapper'});
const hasExecutionResult = cell.outputs.some((o: any) => o.output_type === 'execute_result');
const outPrompt = document.createElement('div');
outPrompt.className = 'prompt output-prompt';
const outPrompt = createElement('div', {className: 'prompt output-prompt'});
if (hasExecutionResult) {
outPrompt.textContent = `Out[${cell.execution_count ?? executionCount}]:`;
}
outputWrapper.append(outPrompt);
const outputDiv = document.createElement('div');
outputDiv.className = 'output';
const outputDiv = createElement('div', {className: 'output'});
for (const output of cell.outputs) {
try {
@ -137,7 +104,6 @@ export const frontendRender: FrontendRenderFunc = async (opts) => {
const svgData = Array.isArray(output.data['image/svg+xml']) ?
output.data['image/svg+xml'].join('') : output.data['image/svg+xml'];
svgDiv.innerHTML = svgData;
sanitizeHtml(svgDiv);
outputDiv.append(svgDiv);
} else if (output.data['text/html']) {
const wrapperDiv = document.createElement('div');
@ -147,7 +113,6 @@ export const frontendRender: FrontendRenderFunc = async (opts) => {
const htmlData = Array.isArray(output.data['text/html']) ?
output.data['text/html'].join('') : output.data['text/html'];
htmlDiv.innerHTML = htmlData;
sanitizeHtml(htmlDiv);
// Ensure images inside HTML outputs are constrained
for (const img of htmlDiv.querySelectorAll('img')) {
img.style.maxWidth = '100%';
@ -156,23 +121,26 @@ export const frontendRender: FrontendRenderFunc = async (opts) => {
wrapperDiv.append(htmlDiv);
outputDiv.append(wrapperDiv);
} else if (output.data['application/javascript']) {
const jsDiv = document.createElement('div');
jsDiv.className = 'js-output-warning';
jsDiv.textContent = '[JavaScript output - execution disabled for security]';
const jsDiv = createElement('div', {
className: 'js-output-warning',
textContent: '[JavaScript output - execution disabled for security]',
});
jsDiv.style.color = 'var(--color-text-light-2)';
jsDiv.style.fontStyle = 'italic';
outputDiv.append(jsDiv);
} else if (output.data['application/vnd.plotly.v1+json']) {
const plotlyDiv = document.createElement('div');
plotlyDiv.className = 'plotly-output-warning';
plotlyDiv.textContent = '[Plotly output - interactive plots not supported]';
const plotlyDiv = createElement('div', {
className: 'plotly-output-warning',
textContent: '[Plotly output - interactive plots not supported]',
});
plotlyDiv.style.color = 'var(--color-text-light-2)';
plotlyDiv.style.fontStyle = 'italic';
outputDiv.append(plotlyDiv);
} else if (output.data['application/vnd.jupyter.widget-view+json']) {
const widgetDiv = document.createElement('div');
widgetDiv.className = 'widget-output-warning';
widgetDiv.textContent = '[Jupyter widget - interactive widgets not supported]';
const widgetDiv = createElement('div', {
className: 'widget-output-warning',
textContent: '[Jupyter widget - interactive widgets not supported]',
});
widgetDiv.style.color = 'var(--color-text-light-2)';
widgetDiv.style.fontStyle = 'italic';
outputDiv.append(widgetDiv);
@ -180,42 +148,44 @@ export const frontendRender: FrontendRenderFunc = async (opts) => {
const latex = Array.isArray(output.data['text/latex']) ?
output.data['text/latex'].join('') : output.data['text/latex'];
const pre = document.createElement('pre');
const mathCode = document.createElement('code');
mathCode.className = 'language-math display';
mathCode.textContent = latex.replace(/^\$\$|\$\$$/g, '');
const mathCode = createElement('code', {
className: 'language-math display',
textContent: latex.replace(/^\$\$|\$\$$/g, ''),
});
pre.append(mathCode);
outputDiv.append(pre);
} else if (output.data['text/plain']) {
const textPre = document.createElement('pre');
const plainText = Array.isArray(output.data['text/plain']) ?
output.data['text/plain'].join('') : output.data['text/plain'];
textPre.textContent = plainText;
const textPre = createElement('pre', {textContent: plainText});
outputDiv.append(textPre);
}
} else if (output.output_type === 'stream' && output.name) {
const streamPre = document.createElement('pre');
streamPre.className = `stream-${output.name}`;
const streamText = Array.isArray(output.text) ? output.text.join('') : (output.text || '');
streamPre.textContent = streamText;
const streamPre = createElement('pre', {
className: `stream-${output.name}`,
textContent: streamText,
});
outputDiv.append(streamPre);
} else if (output.output_type === 'error') {
const errorPre = document.createElement('pre');
errorPre.className = 'error-output';
errorPre.style.color = 'var(--color-red)';
const traceback = Array.isArray(output.traceback) ? output.traceback.join('\n') :
(output.ename && output.evalue ? `${output.ename}: ${output.evalue}` : 'Error');
errorPre.textContent = traceback;
const errorPre = createElement('pre', {
className: 'error-output',
textContent: traceback,
});
errorPre.style.color = 'var(--color-red)';
outputDiv.append(errorPre);
} else if (output.text) {
const textPre = document.createElement('pre');
const text = Array.isArray(output.text) ? output.text.join('') : output.text;
textPre.textContent = text;
const textPre = createElement('pre', {textContent: text});
outputDiv.append(textPre);
}
} catch (outputError) {
console.warn('Failed to render output:', outputError);
const errorDiv = document.createElement('div');
errorDiv.textContent = '[Output rendering failed]';
const errorDiv = createElement('div', {
textContent: '[Output rendering failed]',
});
errorDiv.style.color = 'var(--color-text-light-2)';
errorDiv.style.fontStyle = 'italic';
outputDiv.append(errorDiv);