diff --git a/package-lock.json b/package-lock.json index 430b39ddfba..f4eef21e5bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@eagleoutice/tree-sitter-r": "^1.1.2", + "@jupyterlab/nbformat": "^4.5.0", "@xmldom/xmldom": "^0.9.7", "clipboardy": "^4.0.0", "command-line-args": "^6.0.1", @@ -1833,6 +1834,30 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jupyterlab/nbformat": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/nbformat/-/nbformat-4.5.0.tgz", + "integrity": "sha512-yNG4EGawtyM398VEyfXlDvJP5mRJMUs06oVBhigUDsKdyhc7xO++z2zMSj9sJOUfV27qh6B/4RppFzIl8vvWBA==", + "license": "BSD-3-Clause", + "dependencies": { + "@lumino/coreutils": "^2.2.2" + } + }, + "node_modules/@lumino/algorithm": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@lumino/algorithm/-/algorithm-2.0.4.tgz", + "integrity": "sha512-gddBhESPqu25KWLeAK9Kz8tS9Ph7P45i0CNG7Ia4XMhK9PHLtTsBdJTC9jP+MqhbzC8zDT/4ekvYRV9ojRPj7Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@lumino/coreutils": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@lumino/coreutils/-/coreutils-2.2.2.tgz", + "integrity": "sha512-zaKJaK7rawPATn2BGHkbMrR6oK3s9PxNe9KreLwWF2dB4ZBHDiEmNLRyHRorfJ7XqVOEXAsAAj0jFn+qJPC/4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@lumino/algorithm": "^2.0.4" + } + }, "node_modules/@microsoft/tsdoc": { "version": "0.15.1", "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", diff --git a/package.json b/package.json index 378d0e738dc..58b8432ed56 100644 --- a/package.json +++ b/package.json @@ -198,6 +198,7 @@ }, "dependencies": { "@eagleoutice/tree-sitter-r": "^1.1.2", + "@jupyterlab/nbformat": "^4.5.0", "@xmldom/xmldom": "^0.9.7", "clipboardy": "^4.0.0", "command-line-args": "^6.0.1", diff --git a/src/project/plugins/file-plugins/notebooks/flowr-analyzer-jupyter-file-plugin.ts b/src/project/plugins/file-plugins/notebooks/flowr-analyzer-jupyter-file-plugin.ts new file mode 100644 index 00000000000..fbc411301c5 --- /dev/null +++ b/src/project/plugins/file-plugins/notebooks/flowr-analyzer-jupyter-file-plugin.ts @@ -0,0 +1,36 @@ +import type { PathLike } from 'fs'; +import { SemVer } from 'semver'; +import type { FlowrAnalyzerContext } from '../../../context/flowr-analyzer-context'; +import type { FlowrFileProvider } from '../../../context/flowr-file'; +import { FlowrAnalyzerFilePlugin } from '../flowr-analyzer-file-plugin'; +import { platformBasename } from '../../../../dataflow/internal/process/functions/call/built-in/built-in-source'; +import { FlowrJupyterFile } from './flowr-jupyter-file'; + +const IpynbPattern = /\.ipynb$/i; + +/** + * The plugin provides support for Jupyter (`.ipynb`) files + */ +export class FlowrAnalyzerJupyterFilePlugin extends FlowrAnalyzerFilePlugin { + public readonly name = 'ipynb-file-plugin'; + public readonly description = 'Parses Jupyter files'; + public readonly version = new SemVer('0.1.0'); + private readonly pattern: RegExp; + + /** + * Creates a new instance of the Jupyter file plugin. + * @param filePattern - The pattern to identify Jupyter files, see {@link IpynbPattern} for the default pattern. + */ + constructor(filePattern: RegExp = IpynbPattern) { + super(); + this.pattern = filePattern; + } + + public applies(file: PathLike): boolean { + return this.pattern.test(platformBasename(file.toString())); + } + + protected process(_ctx: FlowrAnalyzerContext, arg: FlowrFileProvider): FlowrJupyterFile { + return FlowrJupyterFile.from(arg); + } +} diff --git a/src/project/plugins/file-plugins/notebooks/flowr-analyzer-qmd-file-plugin.ts b/src/project/plugins/file-plugins/notebooks/flowr-analyzer-qmd-file-plugin.ts index 7c40571760a..9c695510585 100644 --- a/src/project/plugins/file-plugins/notebooks/flowr-analyzer-qmd-file-plugin.ts +++ b/src/project/plugins/file-plugins/notebooks/flowr-analyzer-qmd-file-plugin.ts @@ -31,6 +31,6 @@ export class FlowrAnalyzerQmdFilePlugin extends FlowrAnalyzerFilePlugin { } protected process(_ctx: FlowrAnalyzerContext, arg: FlowrFileProvider): FlowrRMarkdownFile { - return new FlowrRMarkdownFile(arg); + return FlowrRMarkdownFile.from(arg); } -} \ No newline at end of file +} diff --git a/src/project/plugins/file-plugins/notebooks/flowr-jupyter-file.ts b/src/project/plugins/file-plugins/notebooks/flowr-jupyter-file.ts new file mode 100644 index 00000000000..339b2f3c62a --- /dev/null +++ b/src/project/plugins/file-plugins/notebooks/flowr-jupyter-file.ts @@ -0,0 +1,45 @@ +import type { INotebookContent } from '@jupyterlab/nbformat'; +import type { FlowrFileProvider } from '../../../context/flowr-file'; +import { FileRole, FlowrFile } from '../../../context/flowr-file'; + + +/** + * This decorates a text file and parses its contents as a Jupyter file. + * Finnaly, it provides access to the single cells, and all cells fused together as one R file. + */ +export class FlowrJupyterFile extends FlowrFile { + private readonly wrapped: FlowrFileProvider; + + /** + * Prefer the static {@link FlowrRMarkdownFile.from} method + * @param file - the file to load as R Markdown + */ + constructor(file: FlowrFileProvider) { + super(file.path(), FileRole.Source); + this.wrapped = file; + } + + /** + * Loads and parses the content of the wrapped file. + * @returns RmdInfo + */ + protected loadContent(): string { + return loadJupyter(this.wrapped.content()); + } + + public static from(file: FlowrFileProvider | FlowrJupyterFile): FlowrJupyterFile { + return file instanceof FlowrJupyterFile ? file : new FlowrJupyterFile(file); + } +} + +function loadJupyter(content: string): string { + const nb = JSON.parse(content) as INotebookContent; + + return nb.cells.map(cell => { + if(cell.cell_type === 'code') { + return typeof cell.source === 'object' ? cell.source.join('') : cell.source; + } else { + return typeof cell.source === 'object' ? cell.source.map(s => `# ${s}`).join('') : `# ${cell.source}`; + } + }).join('\n'); +} diff --git a/src/project/plugins/file-plugins/notebooks/notebook.ts b/src/project/plugins/file-plugins/notebooks/notebook.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/project/plugins/flowr-analyzer-plugin-defaults.ts b/src/project/plugins/flowr-analyzer-plugin-defaults.ts index c3bdfc33053..3214ad78453 100644 --- a/src/project/plugins/flowr-analyzer-plugin-defaults.ts +++ b/src/project/plugins/flowr-analyzer-plugin-defaults.ts @@ -8,6 +8,7 @@ import { } from './loading-order-plugins/flowr-analyzer-loading-order-description-file-plugin'; import { FlowrAnalyzerRmdFilePlugin } from './file-plugins/notebooks/flowr-analyzer-rmd-file-plugin'; import { FlowrAnalyzerQmdFilePlugin } from './file-plugins/notebooks/flowr-analyzer-qmd-file-plugin'; +import { FlowrAnalyzerJupyterFilePlugin } from './file-plugins/notebooks/flowr-analyzer-jupyter-file-plugin'; /** * Provides the default set of Flowr Analyzer plugins. @@ -18,6 +19,7 @@ export function FlowrAnalyzerPluginDefaults(): FlowrAnalyzerPlugin[] { new FlowrAnalyzerPackageVersionsDescriptionFilePlugin(), new FlowrAnalyzerLoadingOrderDescriptionFilePlugin(), new FlowrAnalyzerRmdFilePlugin(), - new FlowrAnalyzerQmdFilePlugin() + new FlowrAnalyzerQmdFilePlugin(), + new FlowrAnalyzerJupyterFilePlugin(), ]; -} \ No newline at end of file +} diff --git a/src/project/plugins/plugin-registry.ts b/src/project/plugins/plugin-registry.ts index 9195a659f77..141ef02c938 100644 --- a/src/project/plugins/plugin-registry.ts +++ b/src/project/plugins/plugin-registry.ts @@ -9,6 +9,7 @@ import { import { FlowrAnalyzerRmdFilePlugin } from './file-plugins/notebooks/flowr-analyzer-rmd-file-plugin'; import { FlowrAnalyzerQmdFilePlugin } from './file-plugins/notebooks/flowr-analyzer-qmd-file-plugin'; import { guard } from '../../util/assert'; +import { FlowrAnalyzerJupyterFilePlugin } from './file-plugins/notebooks/flowr-analyzer-jupyter-file-plugin'; /** * The built-in Flowr Analyzer plugins that are always available. @@ -18,7 +19,8 @@ export const BuiltInPlugins = [ ['versions:description', FlowrAnalyzerPackageVersionsDescriptionFilePlugin], ['loading-order:description', FlowrAnalyzerLoadingOrderDescriptionFilePlugin], ['file:rmd', FlowrAnalyzerRmdFilePlugin], - ['file:qmd', FlowrAnalyzerQmdFilePlugin] + ['file:qmd', FlowrAnalyzerQmdFilePlugin], + ['file:ipynb', FlowrAnalyzerJupyterFilePlugin], ] as const satisfies [string, PluginProducer][]; export type BuiltInFlowrPluginName = typeof BuiltInPlugins[number][0]; @@ -79,4 +81,4 @@ export function makePlugin(toRegister const plugin = getPlugin(toRegister, []); guard(plugin !== undefined, () => `Unknown Flowr Analyzer plugin: ${toRegister.toString()}`); return plugin; -} \ No newline at end of file +} diff --git a/test/functionality/project/plugin/jupyter-file.test.ts b/test/functionality/project/plugin/jupyter-file.test.ts new file mode 100644 index 00000000000..dd6f7ffd7c1 --- /dev/null +++ b/test/functionality/project/plugin/jupyter-file.test.ts @@ -0,0 +1,14 @@ +import { describe } from 'vitest'; +import { testFileLoadPlugin } from './plugin-test-helper'; +import { FlowrAnalyzerJupyterFilePlugin } from '../../../../src/project/plugins/file-plugins/notebooks/flowr-analyzer-jupyter-file-plugin'; +import { FlowrJupyterFile } from '../../../../src/project/plugins/file-plugins/notebooks/flowr-jupyter-file'; + +describe('Jupyter-file', async() => { + await testFileLoadPlugin(FlowrAnalyzerJupyterFilePlugin, FlowrJupyterFile, 'test/testfiles/notebook/example.ipynb', `x <- 5 +cat(x) +# # Hello +# This is a cool test +y <- c(1,2,3) +plot(y) +# Hi`); +}); diff --git a/test/functionality/project/plugin/plugin-test-helper.ts b/test/functionality/project/plugin/plugin-test-helper.ts index df85368fc87..be63c2db747 100644 --- a/test/functionality/project/plugin/plugin-test-helper.ts +++ b/test/functionality/project/plugin/plugin-test-helper.ts @@ -43,8 +43,8 @@ export async function testFileLoadPlugin, P ext await analyzer.parse(); const files = analyzer.inspectContext().files.getFilesByRole(FileRole.Source); - assert(files.length === 1); + assert.strictEqual(files.length, 1); assert(files[0] instanceof pluginFileType); - assert(files[0].content() === expectedContent); + assert.strictEqual(files[0].content(), expectedContent); }); } diff --git a/test/testfiles/notebook/example.ipynb b/test/testfiles/notebook/example.ipynb new file mode 100644 index 00000000000..c5913a58dca --- /dev/null +++ b/test/testfiles/notebook/example.ipynb @@ -0,0 +1,96 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "eefb24a3", + "metadata": { + "vscode": { + "languageId": "r" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "5" + ] + } + ], + "source": [ + "x <- 5\n", + "cat(x)" + ] + }, + { + "cell_type": "markdown", + "id": "fe6f7093", + "metadata": {}, + "source": [ + "# Hello\n", + "This is a cool test" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "221198cd", + "metadata": { + "vscode": { + "languageId": "r" + } + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "plot without title" + ] + }, + "metadata": { + "image/png": { + "height": 420, + "width": 420 + } + }, + "output_type": "display_data" + } + ], + "source": [ + "y <- c(1,2,3)\n", + "plot(y)" + ] + }, + { + "cell_type": "raw", + "id": "3514be9a", + "metadata": { + "vscode": { + "languageId": "raw" + } + }, + "source": [ + "Hi" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "R", + "language": "R", + "name": "ir" + }, + "language_info": { + "codemirror_mode": "r", + "file_extension": ".r", + "mimetype": "text/x-r-source", + "name": "R", + "pygments_lexer": "r", + "version": "4.5.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}