feat: add Create Note from Jupyter Notebook command

This commit is contained in:
137 2025-07-18 15:30:43 +08:00
parent e9e6242556
commit a88e55a4cc
3 changed files with 112 additions and 8 deletions

View file

@ -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!

View file

@ -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",

View file

@ -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<string | null>((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) {