diff --git a/README.md b/README.md index c798680..6f0565b 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,12 @@ With JupyMD you can: ### Visualising fractals with `matplotlib` ![mandelbrot-set](assets/mandelbrot-set.png) + ## Features -- **Notebook Conversion** – Convert existing notes in Obsidian to `.ipynb` files via Jupytext +- **Notebook Conversion** + - Convert existing notes in Obsidian to `.ipynb` files via Jupytext + - Convert existing Jupyter notebooks (`.ipynb`) to Markdown notes (`.md`) via Jupytext - **Bidirectional Updates** – Changes in Obsidian or Jupyter automatically sync between `.md` and `.ipynb` files - **Execute Code** – Run code blocks in Obsidian with output captured below each block, regardless of viewing mode - **Persistent Execution Environment** – Variables and imports defined in one code block are remembered by the following code blocks @@ -43,15 +46,23 @@ With JupyMD you can: - [Jupytext](https://github.com/mwouts/jupytext) `pip install jupytext` + ## Getting started Download the plugin through the [Obsidian community plugin browser](obsidian://show-plugin?id=jupymd) and enable it. If not yet installed, install Jupyter notebook and Jupytext through pip. -Simply execute the following command on a note you want Jupyter notebook capability on: +### To convert a note to a Jupyter notebook +Execute the following command on a note you want Jupyter notebook capability on: > `JupyMD: Create Jupyter notebook from note` This will create an `.ipynb` file with the same file name as the current note on the file directory, and will transform your Python code blocks into interactive code blocks. Your note will now behave like a Jupyter notebook, and sync its contents automatically to the `.ipynb` file. You may choose to ignore the created `.ipynb` file completely, as its functionality will be mirrored in Obsidian. +### To convert a Jupyter notebook to a note +Open a `.ipynb` file in Obsidian and execute: +> `JupyMD: Create note from Jupyter notebook` + +This will create a Markdown note (`.md`) with the same file name as the notebook, and set up bidirectional sync between the notebook and the note. + ## Contributing This project was originally built to solve a personal problem, and it's still in an early stage. Feedback, bug reports, and pull requests are all welcome and appreciated! diff --git a/src/commands.ts b/src/commands.ts index 1458e43..578dae5 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -1,8 +1,8 @@ import JupyMDPlugin from "./main"; export function registerCommands(plugin: JupyMDPlugin) { - const {fileSync} = plugin; - const {executor} = plugin; + const { fileSync } = plugin; + const { executor } = plugin; plugin.addCommand({ id: "create-jupyter-notebook", @@ -10,6 +10,12 @@ export function registerCommands(plugin: JupyMDPlugin) { callback: () => fileSync.createNotebook(), }); + plugin.addCommand({ + id: "create-note-from-jupyter-notebook", + name: "Create note from Jupyter notebook", + callback: () => fileSync.convertNotebookToNote(), + }); + plugin.addCommand({ id: "open-jupyter-notebook-editor", name: "Open Jupyter notebook in editor", diff --git a/src/components/FileSync.ts b/src/components/FileSync.ts index d1d33f5..755c705 100644 --- a/src/components/FileSync.ts +++ b/src/components/FileSync.ts @@ -1,7 +1,7 @@ -import {App, Notice, TFile, MarkdownView} from "obsidian"; -import {exec} from "child_process"; -import {getAbsolutePath, isNotebookPaired} from "../utils/helpers"; -import {getPackageExecutablePath} from "../utils/pythonPathUtils"; +import { App, Notice, TFile, MarkdownView } from "obsidian"; +import { exec } from "child_process"; +import { getAbsolutePath, isNotebookPaired } from "../utils/helpers"; +import { getPackageExecutablePath } from "../utils/pythonPathUtils"; export class FileSync { private readonly pythonPath: string; @@ -10,6 +10,93 @@ export class FileSync { this.pythonPath = pythonPath; } + async convertNotebookToNote() { + // List all .ipynb files in the vault + const files = this.app.vault.getFiles().filter(f => f.path.endsWith('.ipynb')); + if (files.length === 0) { + new Notice("No Jupyter notebook (.ipynb) files found in your vault."); + return; + } + + // Prompt user to pick a file + const fileNames = files.map(f => f.path); + const selected = await new Promise((resolve) => { + const modal = document.createElement('div'); + modal.style.position = 'fixed'; + modal.style.top = '30%'; + modal.style.left = '50%'; + modal.style.transform = 'translate(-50%, -50%)'; + modal.style.background = 'var(--background-primary)'; + modal.style.padding = '2em'; + modal.style.borderRadius = '8px'; + modal.style.zIndex = '9999'; + modal.style.boxShadow = '0 2px 16px rgba(0,0,0,0.2)'; + + const label = document.createElement('div'); + label.textContent = 'Select a Jupyter notebook to convert:'; + label.style.marginBottom = '1em'; + modal.appendChild(label); + + const select = document.createElement('select'); + select.style.width = '100%'; + for (const name of fileNames) { + const option = document.createElement('option'); + option.value = name; + option.textContent = name; + select.appendChild(option); + } + modal.appendChild(select); + + const btn = document.createElement('button'); + btn.textContent = 'Convert'; + btn.style.marginTop = '1em'; + btn.onclick = () => { + document.body.removeChild(modal); + resolve(select.value); + }; + modal.appendChild(btn); + + const cancel = document.createElement('button'); + cancel.textContent = 'Cancel'; + cancel.style.marginLeft = '1em'; + cancel.onclick = () => { + document.body.removeChild(modal); + resolve(null); + }; + modal.appendChild(cancel); + + document.body.appendChild(modal); + }); + + if (!selected) return; + const file = files.find(f => f.path === selected); + if (!file) return; + + const absPath = getAbsolutePath.call(this, file); + const mdPath = absPath.replace(/\.ipynb$/, ".md"); + const jupytextCmd = getPackageExecutablePath("jupytext", this.pythonPath); + + exec(`${jupytextCmd} --to markdown "${absPath}"`, (error) => { + if (error) { + new Notice(`Failed to convert notebook: ${error.message}`); + return; + } + + exec(`${jupytextCmd} --set-formats ipynb,md "${absPath}"`, (pairError) => { + if (pairError) { + new Notice(`Failed to pair notebook and note: ${pairError.message}`); + return; + } + new Notice(`Note created and paired: ${mdPath}`); + // Open the new note in Obsidian + const mdRelative = this.app.vault.getFiles().find(f => getAbsolutePath.call(this, f) === mdPath); + if (mdRelative) { + this.app.workspace.openLinkText(mdRelative.path, '', true); + } + }); + }); + } + async createNotebook() { const activeFile = this.app.workspace.getActiveFile(); if (!activeFile) {