diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 145daa9..5d3bc34 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -34,15 +34,11 @@ jobs: run: | yarn make linux - - name: package - run: | - cd build/linux - zip -r vivify-linux.zip * - name: upload artifact uses: actions/upload-artifact@v4 with: name: vivify-linux - path: build/linux/vivify-linux.zip + path: build/linux/* build-macos: name: Build MacOS @@ -55,15 +51,11 @@ jobs: run: | yarn make macos - - name: package - run: | - cd build/macos - zip -r vivify-macos.zip * - name: upload artifact uses: actions/upload-artifact@v4 with: name: vivify-macos - path: build/macos/vivify-macos.zip + path: build/macos/* release: name: Release diff --git a/package.json b/package.json index 02160bc..f951e6f 100644 --- a/package.json +++ b/package.json @@ -3,12 +3,13 @@ "repository": "https://github.com/jannis-baum/vivify.git", "author": "Jannis Baum", "scripts": { - "dev": "VIV_TIMEOUT=0 VIV_PORT=3000 nodemon --exec ts-node ./src/app.ts", + "dev": "VIV_TIMEOUT=0 VIV_PORT=3000 NODE_ENV=development nodemon --exec ts-node ./src/app.ts", "viv": "VIV_PORT=3000 ./viv", "lint": "eslint src static" }, "dependencies": { "@viz-js/viz": "^3.1.0", + "ansi_up": "^6.0.2", "express": "^4.18.2", "highlight.js": "^11.8.0", "katex": "^0.15.6", @@ -26,6 +27,7 @@ "ws": "^8.13.0" }, "devDependencies": { + "@jupyterlab/nbformat": "^4.2.3", "@types/express": "^4.17.17", "@types/markdown-it": "^12.2.3", "@types/node": "^20.4.2", diff --git a/src/parser/ansi.ts b/src/parser/ansi.ts new file mode 100644 index 0000000..b69dd0c --- /dev/null +++ b/src/parser/ansi.ts @@ -0,0 +1,27 @@ +import { Renderer } from './parser'; + +// this entire file is very ugly because ansi_up is only available as module so +// we have to use a dynamic (async) import +let _renderAnsi: Renderer | undefined = undefined; + +// here we asynchronously import ansi_up +(async () => { + const { AnsiUp } = await (async () => { + // this gets even uglier because we can't directly use dynamic imports + // with ts-node https://github.com/TypeStrong/ts-node/discussions/1290 + if (process.env.NODE_ENV === 'development') { + const dynamicImport = new Function('specifier', 'return import(specifier)'); + return await dynamicImport('ansi_up'); + } + // for compilation however we have to directly use dynamic imports + return await import('ansi_up'); + })(); + const ansiup = new AnsiUp(); + _renderAnsi = (content: string): string => ansiup.ansi_to_html(content); +})(); + +// use identity while ansi_up loads +const renderAnsi: Renderer = (content: string): string => + _renderAnsi ? _renderAnsi(content) : content; + +export default renderAnsi; diff --git a/src/parser/ipynb.ts b/src/parser/ipynb.ts new file mode 100644 index 0000000..89945de --- /dev/null +++ b/src/parser/ipynb.ts @@ -0,0 +1,117 @@ +import { + ICell, + ICodeCell, + IDisplayData, + IError, + IMimeBundle, + INotebookContent, + IOutput, + IStream, + MultilineString, + IExecuteResult, +} from '@jupyterlab/nbformat'; +import renderAnsi from './ansi'; +import renderMarkdown from './markdown'; +import { Renderer } from './parser'; + +function joinMultilineString(str: MultilineString): string { + return Array.isArray(str) ? str.join('') : str; +} + +function contain( + content: MultilineString, + classNames: string | string[], + tag: string = 'div', +): string { + function prefix(classNames: string | string[]): string { + if (Array.isArray(classNames)) { + return classNames.map((c) => prefix(c)).join(' '); + } + return `ipynb-${classNames}`; + } + classNames = prefix(classNames); + + return `<${tag} class="${classNames}">${joinMultilineString(content)}`; +} + +function escapeHTML(raw: string): string { + return raw.replace(//g, '>'); +} + +function executionCount(count: ICell['execution_count']): string { + return contain(`[${count?.toString() ?? ' '}]:`, 'execution-count'); +} + +const renderNotebook: Renderer = (content: string): string => { + const nb = JSON.parse(content) as INotebookContent; + const language = nb.metadata.kernelspec?.language ?? nb.metadata.language_info?.name; + + function renderDisplayData(data: IMimeBundle): string { + const identity = (content: string) => content; + const renderMap: [string, (content: string) => string][] = [ + ['image/png', (content) => ``], + ['image/jpeg', (content) => ``], + ['image/svg+xml', identity], + ['text/svg+xml', identity], + ['text/html', identity], + ['text/markdown', renderMarkdown], + ['text/plain', (content) => contain(escapeHTML(content), 'output-plain', 'pre')], + ]; + const result = renderMap.find(([mimeType]) => mimeType in data); + if (!result) return ''; + const [format, render] = result; + return render(joinMultilineString(data[format] as MultilineString)); + } + + function renderOutput(output: IOutput): string { + switch (output.output_type) { + case 'stream': + const text = joinMultilineString((output as IStream).text); + return contain( + renderAnsi(text), + `output-stream-${(output as IStream).name}`, + 'pre', + ); + case 'error': + const traceback = (output as IError).traceback.join('\n'); + return contain(renderAnsi(traceback), 'output-error', 'pre'); + case 'display_data': { + const displayData = (output as IDisplayData).data; + return renderDisplayData(displayData); + } + case 'execute_result': { + const displayData = (output as IExecuteResult).data; + return contain( + [executionCount(output.execution_count), renderDisplayData(displayData)], + 'output-execution-result', + ); + } + default: + return ''; + } + } + + function renderCell(cell: ICell): string { + const source = joinMultilineString(cell.source); + + switch (cell.cell_type) { + case 'code': + const content = [ + executionCount(cell.execution_count), + renderMarkdown(`\`\`\`${language}\n${source}\n\`\`\``), + ]; + if (Array.isArray(cell.outputs)) { + content.push(...(cell as ICodeCell).outputs.map(renderOutput)); + } + return contain(content, 'cell-code'); + case 'markdown': + return contain(renderMarkdown(source), 'cell-markdown'); + default: + // 'raw' cells aren't normally rendered + return ''; + } + } + + return nb.cells.map(renderCell).join('\n\n'); +}; +export default renderNotebook; diff --git a/src/parser/markdown.ts b/src/parser/markdown.ts new file mode 100644 index 0000000..44b0e8c --- /dev/null +++ b/src/parser/markdown.ts @@ -0,0 +1,36 @@ +import MarkdownIt from 'markdown-it'; +import anchor from 'markdown-it-anchor'; +import highlight from './highlight'; +import graphviz from './dot'; +import config from './config'; +import { Renderer } from './parser'; + +const mdit = new MarkdownIt({ + html: true, + highlight: highlight, + linkify: true, +}); + +mdit.use(anchor, { + permalink: anchor.permalink.ariaHidden({ + placement: 'before', + }), +}); +/* eslint-disable @typescript-eslint/no-var-requires */ +mdit.use(require('markdown-it-emoji')); +mdit.use(require('markdown-it-task-lists')); +mdit.use(require('markdown-it-footnote')); +mdit.use(require('markdown-it-inject-linenumbers')); +mdit.use(require('markdown-it-texmath'), { + engine: require('katex'), + delimiters: 'dollars', + katexOptions: config.katexOptions, +}); +mdit.use(require('markdown-it-deflist')); +/* eslint-enable @typescript-eslint/no-var-requires */ +mdit.use(graphviz); + +const renderMarkdown: Renderer = (content: string) => { + return mdit.render(content); +}; +export default renderMarkdown; diff --git a/src/parser/parser.ts b/src/parser/parser.ts index 8712ce2..22e4535 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -1,47 +1,55 @@ +import { Dirent, readdirSync } from 'fs'; import { homedir } from 'os'; - -import MarkdownIt from 'markdown-it'; -import anchor from 'markdown-it-anchor'; -import highlight from './highlight'; -import graphviz from './dot'; +import { join as pjoin } from 'path'; +import { pathToURL } from '../utils/path'; import config from './config'; +import renderNotebook from './ipynb'; +import renderMarkdown from './markdown'; + +export type Renderer = (content: string) => string; + +const pathHeading: Renderer = (path: string) => `# \`${path.replace(homedir(), '~')}\``; +const wrap = (contentType: string, content: string) => + `
${content}
`; + +const mdExtensions = config.mdExtensions ?? ['markdown', 'md', 'mdown', 'mdwn', 'mkd', 'mkdn']; +function textRenderer( + fileEnding: string | undefined, +): { render: Renderer; contentType: string } | undefined { + if (!fileEnding) return undefined; + if (mdExtensions.includes(fileEnding)) { + return { render: renderMarkdown, contentType: 'markdown' }; + } + if (fileEnding === 'ipynb') { + return { render: renderNotebook, contentType: 'ipynb' }; + } +} -const mdit = new MarkdownIt({ - html: true, - highlight: highlight, - linkify: true, -}); - -mdit.use(anchor, { - permalink: anchor.permalink.ariaHidden({ - placement: 'before', - }), -}); -/* eslint-disable @typescript-eslint/no-var-requires */ -mdit.use(require('markdown-it-emoji')); -mdit.use(require('markdown-it-task-lists')); -mdit.use(require('markdown-it-footnote')); -mdit.use(require('markdown-it-inject-linenumbers')); -mdit.use(require('markdown-it-texmath'), { - engine: require('katex'), - delimiters: 'dollars', - katexOptions: config.katexOptions, -}); -mdit.use(require('markdown-it-deflist')); -/* eslint-enable @typescript-eslint/no-var-requires */ -mdit.use(graphviz); - -export const pathHeading = (path: string) => `# \`${path.replace(homedir(), '~')}\``; - -export default function parse(src: string, path?: string) { - let md = src; - - const mdExtensions = config.mdExtensions ?? ['markdown', 'md', 'mdown', 'mdwn', 'mkd', 'mkdn']; - +export function renderTextFile(content: string, path: string): string { const fileEnding = path?.split('.')?.at(-1); - if (fileEnding && !mdExtensions.includes(fileEnding)) { - md = `${pathHeading(path!)}\n\n\`\`\`${fileEnding}\n${src}\n\`\`\``; + const renderInformation = textRenderer(fileEnding); + if (renderInformation === undefined) { + return wrap( + 'txt', + renderMarkdown(`${pathHeading(path!)}\n\n\`\`\`${fileEnding}\n${content}\n\`\`\``), + ); } + const { render, contentType } = renderInformation; + return wrap(contentType, render(content)); +} - return mdit.render(md); +const dirListItem = (item: Dirent, path: string) => + `
  • ${item.name}
  • `; + +export function renderDirectory(path: string): string { + const list = readdirSync(path, { withFileTypes: true }) + .sort((a, b) => +b.isDirectory() - +a.isDirectory()) + .map((item) => dirListItem(item, path)) + .join('\n'); + return wrap( + 'directory', + renderMarkdown(`${pathHeading(path)}\n\n`), + ); } diff --git a/src/routes/viewer.ts b/src/routes/viewer.ts index c1dfcc6..1c84999 100644 --- a/src/routes/viewer.ts +++ b/src/routes/viewer.ts @@ -1,12 +1,12 @@ -import { Dirent, lstatSync, readdirSync, readFileSync } from 'fs'; +import { lstatSync, readFileSync } from 'fs'; import { dirname as pdirname, join as pjoin } from 'path'; import { Request, Response, Router } from 'express'; import { messageClientsAt } from '../app'; import config from '../parser/config'; -import parse, { pathHeading } from '../parser/parser'; import { pathToURL, pcomponents, pmime } from '../utils/path'; +import { renderDirectory, renderTextFile } from '../parser/parser'; export const router = Router(); @@ -22,11 +22,6 @@ const pageTitle = (path: string) => { } else return pjoin(...comps.slice(-2)); }; -const dirListItem = (item: Dirent, path: string) => - `
  • ${item.name}
  • `; - router.get(/.*/, async (req: Request, res: Response) => { const path = res.locals.filepath; @@ -34,21 +29,17 @@ router.get(/.*/, async (req: Request, res: Response) => { if (!body) { try { if (lstatSync(path).isDirectory()) { - const list = readdirSync(path, { withFileTypes: true }) - .sort((a, b) => +b.isDirectory() - +a.isDirectory()) - .map((item) => dirListItem(item, path)) - .join('\n'); - body = parse(`${pathHeading(path)}\n\n`); + body = renderDirectory(path); } else { const data = readFileSync(path); const type = pmime(path); - if (!type.startsWith('text/')) { + if (!(type.startsWith('text/') || type === 'application/json')) { res.setHeader('Content-Type', type).send(data); return; } - body = parse(data.toString(), path); + body = renderTextFile(data.toString(), path); } } catch { res.status(404).send('File not found.'); @@ -70,6 +61,7 @@ router.get(/.*/, async (req: Request, res: Response) => { ${title} + \n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    ab
    0110
    1220
    2330
    \n", + "" + ], + "text/plain": [ + " a b\n", + "0 1 10\n", + "1 2 20\n", + "2 3 30" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import pandas as pd\n", + "df = pd.DataFrame({ 'a': [1, 2, 3], 'b': [10, 20, 30] })\n", + "df" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "f7914d4f-2f2b-4a56-99d3-2b1252380b6a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
    " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "df.plot(figsize=(2, 1))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b334dcb5-3794-4bb1-8d3b-0fac6f5c471f", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/yarn.lock b/yarn.lock index 289ed16..1df50a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -123,6 +123,25 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@jupyterlab/nbformat@^4.2.3": + version "4.2.3" + resolved "https://registry.yarnpkg.com/@jupyterlab/nbformat/-/nbformat-4.2.3.tgz#6b7904a387f04542b8ffe7800d37a2ce41b625a9" + integrity sha512-eNn5F4JGw6Gq8xB1AaJ29hKQFgDvcjv9KZQeSp9joCYvedmc0LHivELCO+GticUjN5Y0+qgS4ZvnOARAqcIyvQ== + dependencies: + "@lumino/coreutils" "^2.1.2" + +"@lumino/algorithm@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@lumino/algorithm/-/algorithm-2.0.2.tgz#d211da98c92be0271afde96b949982e29178ae48" + integrity sha512-cI8yJ2+QK1yM5ZRU3Kuaw9fJ/64JEDZEwWWp7+U0cd/mvcZ44BGdJJ29w+tIet1QXxPAvnsUleWyQ5qm4qUouA== + +"@lumino/coreutils@^2.1.2": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@lumino/coreutils/-/coreutils-2.2.0.tgz#3f9d5c36f2513f067b2563c7ad3b33f43905a4e2" + integrity sha512-x5wnQ/GjWBayJ6vXVaUi6+Q6ETDdcUiH9eSfpRZFbgMQyyM6pi6baKqJBK2CHkCc/YbAEl6ipApTgm3KOJ/I3g== + dependencies: + "@lumino/algorithm" "^2.0.2" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -631,6 +650,11 @@ ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +ansi_up@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/ansi_up/-/ansi_up-6.0.2.tgz#083adb65be5b21ba283fd105d3102e64f3f0b092" + integrity sha512-3G3vKvl1ilEp7J1u6BmULpMA0xVoW/f4Ekqhl8RTrJrhEBkonKn5k3bUc5Xt+qDayA6iDX0jyUh3AbZjB/l0tw== + anymatch@~3.1.2: version "3.1.3" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"