mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-10 09:41:52 +02:00
Merge 3ea826abf15491444482afa39643f1cc30d38f58 into ce089f498bce32305b2d9e8c6adfd8cb7c82f88f
This commit is contained in:
commit
d78490724b
5
modules/markup/external/external.go
vendored
5
modules/markup/external/external.go
vendored
@ -48,6 +48,11 @@ func RegisterRenderers() {
|
||||
},
|
||||
})
|
||||
|
||||
markup.RegisterRenderer(&frontendRenderer{
|
||||
name: "jupyter-notebook",
|
||||
patterns: []string{"*.ipynb"},
|
||||
})
|
||||
|
||||
for _, renderer := range setting.ExternalMarkupRenderers {
|
||||
markup.RegisterRenderer(&Renderer{renderer})
|
||||
}
|
||||
|
||||
@ -52,6 +52,7 @@
|
||||
"jquery": "4.0.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"katex": "0.16.45",
|
||||
"marked": "18.0.2",
|
||||
"mermaid": "11.14.0",
|
||||
"online-3d-viewer": "0.18.0",
|
||||
"pdfobject": "2.3.1",
|
||||
|
||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@ -168,6 +168,9 @@ importers:
|
||||
katex:
|
||||
specifier: 0.16.45
|
||||
version: 0.16.45
|
||||
marked:
|
||||
specifier: 18.0.2
|
||||
version: 18.0.2
|
||||
mermaid:
|
||||
specifier: 11.14.0
|
||||
version: 11.14.0
|
||||
@ -3297,6 +3300,11 @@ packages:
|
||||
engines: {node: '>= 20'}
|
||||
hasBin: true
|
||||
|
||||
marked@18.0.2:
|
||||
resolution: {integrity: sha512-NsmlUYBS/Zg57rgDWMYdnre6OTj4e+qq/JS2ot3KrYLSoHLw+sDu0Nm1ZGpRgYAq6c+b1ekaY5NzVchMCQnzcg==}
|
||||
engines: {node: '>= 20'}
|
||||
hasBin: true
|
||||
|
||||
marked@4.3.0:
|
||||
resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==}
|
||||
engines: {node: '>= 12'}
|
||||
@ -7603,6 +7611,8 @@ snapshots:
|
||||
|
||||
marked@16.4.2: {}
|
||||
|
||||
marked@18.0.2: {}
|
||||
|
||||
marked@4.3.0: {}
|
||||
|
||||
material-icon-theme@5.34.0:
|
||||
|
||||
62
tests/e2e/jupyter-render.test.ts
Normal file
62
tests/e2e/jupyter-render.test.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import {env} from 'node:process';
|
||||
import {expect, test} from '@playwright/test';
|
||||
import {login, apiCreateRepo, apiCreateFile, assertNoJsError, randomString} from './utils.ts';
|
||||
|
||||
test.describe('jupyter notebook rendering', () => {
|
||||
let repoName: string;
|
||||
let owner: string;
|
||||
|
||||
test.beforeAll(async ({request}) => {
|
||||
repoName = `e2e-jupyter-${randomString(8)}`;
|
||||
owner = env.GITEA_TEST_E2E_USER;
|
||||
|
||||
await apiCreateRepo(request, {name: repoName});
|
||||
|
||||
// Single comprehensive test notebook
|
||||
const notebook = JSON.stringify({
|
||||
cells: [
|
||||
{cell_type: 'markdown', source: ['# Header 1\n', '## Header 2\n', '**bold** *italic* `code`\n', '- List item 1\n', '- List item 2\n', '[link](https://example.com)\n', '| Col1 | Col2 |\n', '|------|------|\n', '| A | B |\n', '```python\ncode block\n```\n', '> blockquote\n', '~~strikethrough~~']},
|
||||
{cell_type: 'code', execution_count: 1, source: ['print("Hello")'], outputs: [{output_type: 'stream', name: 'stdout', text: ['Hello\n']}]},
|
||||
{cell_type: 'code', execution_count: 2, source: ['x'], outputs: [{output_type: 'execute_result', data: {'image/png': 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='}}]},
|
||||
{cell_type: 'code', source: ['# No output'], outputs: []},
|
||||
{cell_type: 'code', source: ['err'], outputs: [{output_type: 'error', ename: 'ValueError', evalue: 'Test', traceback: ['ValueError: Test']}]},
|
||||
{cell_type: 'code', source: ['mixed'], outputs: [{output_type: 'stream', name: 'stdout', text: ['text\n']}, {output_type: 'execute_result', data: {'text/html': ['<b>HTML</b>']}}]},
|
||||
],
|
||||
metadata: {}, nbformat: 4, nbformat_minor: 5,
|
||||
});
|
||||
|
||||
await apiCreateFile(request, owner, repoName, 'test.ipynb', notebook);
|
||||
});
|
||||
|
||||
test('renders markdown cells', async ({page}) => {
|
||||
await login(page);
|
||||
await page.goto(`/${owner}/${repoName}/src/branch/main/test.ipynb`);
|
||||
await assertNoJsError(page);
|
||||
const frame = page.frameLocator('iframe.external-render-iframe');
|
||||
await expect(frame.locator('.cell.markdown h1')).toBeVisible();
|
||||
await expect(frame.locator('.cell.markdown strong')).toBeVisible();
|
||||
await expect(frame.locator('.cell.markdown ul li').first()).toBeVisible();
|
||||
await expect(frame.locator('.cell.markdown table')).toBeVisible();
|
||||
});
|
||||
|
||||
test('renders code cells with outputs', async ({page}) => {
|
||||
await login(page);
|
||||
await page.goto(`/${owner}/${repoName}/src/branch/main/test.ipynb`);
|
||||
await assertNoJsError(page);
|
||||
await expect(page.frameLocator('iframe.external-render-iframe').locator('.cell.code .output pre').first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('renders image outputs', async ({page}) => {
|
||||
await login(page);
|
||||
await page.goto(`/${owner}/${repoName}/src/branch/main/test.ipynb`);
|
||||
await assertNoJsError(page);
|
||||
await expect(page.frameLocator('iframe.external-render-iframe').locator('.cell.code .output img')).toBeVisible();
|
||||
});
|
||||
|
||||
test('renders error outputs', async ({page}) => {
|
||||
await login(page);
|
||||
await page.goto(`/${owner}/${repoName}/src/branch/main/test.ipynb`);
|
||||
await assertNoJsError(page);
|
||||
await expect(page.frameLocator('iframe.external-render-iframe').locator('.error-output')).toBeVisible();
|
||||
});
|
||||
});
|
||||
192
web_src/css/features/jupyter.css
Normal file
192
web_src/css/features/jupyter.css
Normal file
@ -0,0 +1,192 @@
|
||||
/* Gitea styles for Jupyter notebook content */
|
||||
.jupyter-notebook {
|
||||
padding: 20px;
|
||||
background: var(--color-body);
|
||||
color: var(--color-text);
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
/* Cell containers */
|
||||
.jupyter-notebook .cell {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Markdown cells */
|
||||
.jupyter-notebook .cell.markdown {
|
||||
background: var(--color-body);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.jupyter-notebook .cell.markdown .input {
|
||||
padding: 6px 12px;
|
||||
line-height: 1.6;
|
||||
background: var(--color-body);
|
||||
color: var(--color-text);
|
||||
margin-left: 110px;
|
||||
}
|
||||
|
||||
.jupyter-notebook .cell.markdown h1,
|
||||
.jupyter-notebook .cell.markdown h2,
|
||||
.jupyter-notebook .cell.markdown h3 {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 16px;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
line-height: 1.25;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.jupyter-notebook .cell.markdown h1 {
|
||||
font-size: 1.875em;
|
||||
border-bottom: 1px solid var(--color-secondary-alpha-20);
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
.jupyter-notebook .cell.markdown h2 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.jupyter-notebook .cell.markdown h3 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
.jupyter-notebook .cell.markdown p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.jupyter-notebook .cell.markdown code {
|
||||
padding: 0.2em 0.4em;
|
||||
margin: 0;
|
||||
font-size: 85%;
|
||||
background-color: var(--color-secondary-alpha-20);
|
||||
border-radius: 3px;
|
||||
font-family: var(--fonts-monospace);
|
||||
}
|
||||
|
||||
.jupyter-notebook .cell.markdown table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 16px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.jupyter-notebook .cell.markdown table th,
|
||||
.jupyter-notebook .cell.markdown table td {
|
||||
border: 1px solid var(--color-secondary);
|
||||
padding: 6px 13px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.jupyter-notebook .cell.markdown table th {
|
||||
background: var(--color-secondary-alpha-20);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.jupyter-notebook .cell.markdown table tr:nth-child(even) {
|
||||
background: var(--color-secondary-alpha-10);
|
||||
}
|
||||
|
||||
/* Code cells */
|
||||
.jupyter-notebook .cell.code {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.jupyter-notebook .cell.code .input-wrapper,
|
||||
.jupyter-notebook .cell.code .output-wrapper {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.jupyter-notebook .cell.code .prompt {
|
||||
padding: 10px 10px 10px 0;
|
||||
color: var(--color-text-light-2);
|
||||
font-family: var(--fonts-monospace);
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
text-align: right;
|
||||
width: 100px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.jupyter-notebook .cell.code .input {
|
||||
flex: 1;
|
||||
background-color: var(--color-code-bg, #f6f8fa);
|
||||
border: 1px solid var(--color-secondary-alpha-20, #d0d7de);
|
||||
border-radius: 4px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.jupyter-notebook .cell.code .input pre {
|
||||
margin: 0;
|
||||
padding: 10px 16px;
|
||||
font-family: var(--fonts-monospace, monospace);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
overflow-x: auto;
|
||||
color: var(--color-text);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.jupyter-notebook .cell.code .input code {
|
||||
display: block;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
/* Code outputs */
|
||||
.jupyter-notebook .cell.code .output {
|
||||
flex: 1;
|
||||
background: var(--color-body);
|
||||
color: var(--color-text);
|
||||
overflow-x: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.jupyter-notebook .cell.code .output pre {
|
||||
margin: 0;
|
||||
padding: 10px 16px;
|
||||
font-family: var(--fonts-monospace);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
overflow-x: auto;
|
||||
color: var(--color-text);
|
||||
background: var(--color-body);
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.jupyter-notebook .cell.code .output table {
|
||||
border-collapse: collapse;
|
||||
margin: 10px 16px;
|
||||
font-size: 13px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.jupyter-notebook .cell.code .output table th,
|
||||
.jupyter-notebook .cell.code .output table td {
|
||||
border: 1px solid var(--color-secondary);
|
||||
padding: 6px 13px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.jupyter-notebook .cell.code .output table th {
|
||||
background: var(--color-secondary-alpha-20);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.jupyter-notebook .cell.code .output table tr:nth-child(even) {
|
||||
background: var(--color-secondary-alpha-10);
|
||||
}
|
||||
|
||||
.jupyter-notebook .cell.code .output img {
|
||||
max-width: 90%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.jupyter-notebook .cell.code .output table img {
|
||||
margin: 0;
|
||||
width: auto;
|
||||
}
|
||||
@ -8,6 +8,7 @@ type LazyLoadFunc = () => Promise<{frontendRender: FrontendRenderFunc}>;
|
||||
const frontendPlugins: Record<string, LazyLoadFunc> = {
|
||||
'viewer-3d': () => import('./render/plugins/frontend-viewer-3d.ts'),
|
||||
'openapi-swagger': () => import('./render/plugins/frontend-openapi-swagger.ts'),
|
||||
'jupyter-notebook': () => import('./render/plugins/frontend-jupyter-notebook.ts'),
|
||||
};
|
||||
|
||||
class Options implements FrontendRenderOptions {
|
||||
|
||||
227
web_src/js/render/plugins/frontend-jupyter-notebook.ts
Normal file
227
web_src/js/render/plugins/frontend-jupyter-notebook.ts
Normal file
@ -0,0 +1,227 @@
|
||||
import type {FrontendRenderFunc} from '../plugin.ts';
|
||||
import {marked} from 'marked';
|
||||
import '../../../css/features/jupyter.css';
|
||||
|
||||
// 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;
|
||||
return container;
|
||||
}
|
||||
|
||||
export const frontendRender: FrontendRenderFunc = async (opts) => {
|
||||
try {
|
||||
const notebook = JSON.parse(opts.contentString());
|
||||
|
||||
if (!notebook.cells || !Array.isArray(notebook.cells)) {
|
||||
throw new Error('Invalid notebook format: missing or invalid cells array');
|
||||
}
|
||||
|
||||
// Detect language from notebook metadata
|
||||
const language = notebook.metadata?.language_info?.name ||
|
||||
notebook.metadata?.kernelspec?.language ||
|
||||
'text';
|
||||
|
||||
const container = createElement('div', {className: 'jupyter-notebook'});
|
||||
|
||||
let executionCount = 1;
|
||||
|
||||
for (const cell of notebook.cells) {
|
||||
if (!cell.cell_type) continue;
|
||||
|
||||
const cellDiv = createElement('div', {className: `cell ${cell.cell_type}`});
|
||||
|
||||
if (cell.cell_type === 'markdown') {
|
||||
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 = createElement('div', {className: 'input-wrapper'});
|
||||
|
||||
const prompt = createElement('div', {
|
||||
className: 'prompt input-prompt',
|
||||
textContent: `In [${cell.execution_count ?? executionCount}]:`,
|
||||
});
|
||||
inputWrapper.append(prompt);
|
||||
|
||||
const inputDiv = createElement('div', {className: 'input'});
|
||||
|
||||
const pre = document.createElement('pre');
|
||||
const code = createElement('code', {className: `language-${language}`});
|
||||
const source = Array.isArray(cell.source) ? cell.source.join('') : (cell.source || '');
|
||||
code.textContent = source;
|
||||
pre.append(code);
|
||||
inputDiv.append(pre);
|
||||
inputWrapper.append(inputDiv);
|
||||
cellDiv.append(inputWrapper);
|
||||
|
||||
if (cell.outputs && Array.isArray(cell.outputs) && cell.outputs.length > 0) {
|
||||
const outputWrapper = createElement('div', {className: 'output-wrapper'});
|
||||
|
||||
const hasExecutionResult = cell.outputs.some((o: any) => o.output_type === 'execute_result');
|
||||
|
||||
const outPrompt = createElement('div', {className: 'prompt output-prompt'});
|
||||
if (hasExecutionResult) {
|
||||
outPrompt.textContent = `Out[${cell.execution_count ?? executionCount}]:`;
|
||||
}
|
||||
outputWrapper.append(outPrompt);
|
||||
|
||||
const outputDiv = createElement('div', {className: 'output'});
|
||||
|
||||
for (const output of cell.outputs) {
|
||||
try {
|
||||
if (output.data) {
|
||||
if (output.data['image/png']) {
|
||||
const img = document.createElement('img');
|
||||
const imgData = Array.isArray(output.data['image/png']) ?
|
||||
output.data['image/png'].join('') : output.data['image/png'];
|
||||
img.src = `data:image/png;base64,${imgData}`;
|
||||
img.style.maxWidth = '100%';
|
||||
outputDiv.append(img);
|
||||
} else if (output.data['image/jpeg']) {
|
||||
const img = document.createElement('img');
|
||||
const imgData = Array.isArray(output.data['image/jpeg']) ?
|
||||
output.data['image/jpeg'].join('') : output.data['image/jpeg'];
|
||||
img.src = `data:image/jpeg;base64,${imgData}`;
|
||||
img.style.maxWidth = '100%';
|
||||
outputDiv.append(img);
|
||||
} else if (output.data['image/svg+xml']) {
|
||||
const svgDiv = document.createElement('div');
|
||||
const svgData = Array.isArray(output.data['image/svg+xml']) ?
|
||||
output.data['image/svg+xml'].join('') : output.data['image/svg+xml'];
|
||||
svgDiv.innerHTML = svgData;
|
||||
outputDiv.append(svgDiv);
|
||||
} else if (output.data['text/html']) {
|
||||
const wrapperDiv = document.createElement('div');
|
||||
wrapperDiv.style.overflowX = 'auto';
|
||||
wrapperDiv.style.maxWidth = '100%';
|
||||
const htmlDiv = document.createElement('div');
|
||||
const htmlData = Array.isArray(output.data['text/html']) ?
|
||||
output.data['text/html'].join('') : output.data['text/html'];
|
||||
htmlDiv.innerHTML = htmlData;
|
||||
// Ensure images inside HTML outputs are constrained
|
||||
for (const img of htmlDiv.querySelectorAll('img')) {
|
||||
img.style.maxWidth = '100%';
|
||||
img.style.height = 'auto';
|
||||
}
|
||||
wrapperDiv.append(htmlDiv);
|
||||
outputDiv.append(wrapperDiv);
|
||||
} else if (output.data['application/javascript']) {
|
||||
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 = 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 = 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);
|
||||
} else if (output.data['text/latex']) {
|
||||
const latex = Array.isArray(output.data['text/latex']) ?
|
||||
output.data['text/latex'].join('') : output.data['text/latex'];
|
||||
const pre = document.createElement('pre');
|
||||
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 plainText = Array.isArray(output.data['text/plain']) ?
|
||||
output.data['text/plain'].join('') : output.data['text/plain'];
|
||||
const textPre = createElement('pre', {textContent: plainText});
|
||||
outputDiv.append(textPre);
|
||||
}
|
||||
} else if (output.output_type === 'stream' && output.name) {
|
||||
const streamText = Array.isArray(output.text) ? output.text.join('') : (output.text || '');
|
||||
const streamPre = createElement('pre', {
|
||||
className: `stream-${output.name}`,
|
||||
textContent: streamText,
|
||||
});
|
||||
outputDiv.append(streamPre);
|
||||
} else if (output.output_type === 'error') {
|
||||
const traceback = Array.isArray(output.traceback) ? output.traceback.join('\n') :
|
||||
(output.ename && output.evalue ? `${output.ename}: ${output.evalue}` : 'Error');
|
||||
const errorPre = createElement('pre', {
|
||||
className: 'error-output',
|
||||
textContent: traceback,
|
||||
});
|
||||
errorPre.style.color = 'var(--color-red)';
|
||||
outputDiv.append(errorPre);
|
||||
} else if (output.text) {
|
||||
const text = Array.isArray(output.text) ? output.text.join('') : output.text;
|
||||
const textPre = createElement('pre', {textContent: text});
|
||||
outputDiv.append(textPre);
|
||||
}
|
||||
} catch (outputError) {
|
||||
console.warn('Failed to render output:', outputError);
|
||||
const errorDiv = createElement('div', {
|
||||
textContent: '[Output rendering failed]',
|
||||
});
|
||||
errorDiv.style.color = 'var(--color-text-light-2)';
|
||||
errorDiv.style.fontStyle = 'italic';
|
||||
outputDiv.append(errorDiv);
|
||||
}
|
||||
}
|
||||
|
||||
if (outputDiv.children.length > 0) {
|
||||
outputWrapper.append(outputDiv);
|
||||
cellDiv.append(outputWrapper);
|
||||
}
|
||||
}
|
||||
|
||||
executionCount++;
|
||||
}
|
||||
|
||||
container.append(cellDiv);
|
||||
}
|
||||
|
||||
opts.container.append(container);
|
||||
|
||||
const {initMarkupCodeMath} = await import('../../markup/math.ts');
|
||||
await initMarkupCodeMath(container);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Jupyter notebook rendering failed:', error);
|
||||
const errorDiv = document.createElement('div');
|
||||
errorDiv.style.padding = '20px';
|
||||
errorDiv.style.color = 'var(--color-red)';
|
||||
const errorTitle = document.createElement('strong');
|
||||
errorTitle.textContent = 'Failed to render notebook:';
|
||||
errorDiv.append(errorTitle);
|
||||
errorDiv.append(document.createElement('br'));
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
errorDiv.append(document.createTextNode(errorMessage));
|
||||
opts.container.append(errorDiv);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user