", "eval"),
25 | ${globalsName}
26 | ))
27 | except SyntaxError:
28 | exec(
29 | compile(${JSON.stringify(code.replace(/\r\n/g, "\n") + "\n")}, "", "exec"),
30 | ${globalsName}
31 | )
32 | except Exception as e:
33 | ${printName} (e, file=sys.stderr)
34 | finally:
35 | ${printName} ("${finishSigil}", end="")
36 |
37 | `;
38 |
--------------------------------------------------------------------------------
/src/settings/per-lang/makeGroovySettings.ts:
--------------------------------------------------------------------------------
1 | import { Setting } from "obsidian";
2 | import { SettingsTab } from "../SettingsTab";
3 |
4 | export default (tab: SettingsTab, containerEl: HTMLElement) => {
5 | containerEl.createEl('h3', { text: 'Groovy Settings' });
6 | new Setting(containerEl)
7 | .setName('Groovy path')
8 | .setDesc('The path to your Groovy installation.')
9 | .addText(text => text
10 | .setValue(tab.plugin.settings.groovyPath)
11 | .onChange(async (value) => {
12 | const sanitized = tab.sanitizePath(value);
13 | tab.plugin.settings.groovyPath = sanitized;
14 | console.log('Groovy path set to: ' + sanitized);
15 | await tab.plugin.saveSettings();
16 | }));
17 | new Setting(containerEl)
18 | .setName('Groovy arguments')
19 | .addText(text => text
20 | .setValue(tab.plugin.settings.groovyArgs)
21 | .onChange(async (value) => {
22 | tab.plugin.settings.groovyArgs = value;
23 | console.log('Groovy args set to: ' + value);
24 | await tab.plugin.saveSettings();
25 | }));
26 | tab.makeInjectSetting(containerEl, "groovy");
27 | }
--------------------------------------------------------------------------------
/src/settings/per-lang/makeJavaSettings.ts:
--------------------------------------------------------------------------------
1 | import { Setting } from "obsidian";
2 | import { SettingsTab } from "../SettingsTab";
3 |
4 | export default (tab: SettingsTab, containerEl: HTMLElement) => {
5 | containerEl.createEl('h3', { text: 'Java Settings' });
6 | new Setting(containerEl)
7 | .setName('Java path (Java 11 or higher)')
8 | .setDesc('The path to your Java installation.')
9 | .addText(text => text
10 | .setValue(tab.plugin.settings.javaPath)
11 | .onChange(async (value) => {
12 | const sanitized = tab.sanitizePath(value);
13 | tab.plugin.settings.javaPath = sanitized;
14 | console.log('Java path set to: ' + sanitized);
15 | await tab.plugin.saveSettings();
16 | }));
17 | new Setting(containerEl)
18 | .setName('Java arguments')
19 | .addText(text => text
20 | .setValue(tab.plugin.settings.javaArgs)
21 | .onChange(async (value) => {
22 | tab.plugin.settings.javaArgs = value;
23 | console.log('Java args set to: ' + value);
24 | await tab.plugin.saveSettings();
25 | }));
26 | tab.makeInjectSetting(containerEl, "java");
27 | }
--------------------------------------------------------------------------------
/src/settings/per-lang/makeKotlinSettings.ts:
--------------------------------------------------------------------------------
1 | import { Setting } from "obsidian";
2 | import { SettingsTab } from "../SettingsTab";
3 |
4 | export default (tab: SettingsTab, containerEl: HTMLElement) => {
5 | containerEl.createEl('h3', { text: 'Kotlin Settings' });
6 | new Setting(containerEl)
7 | .setName('Kotlin path')
8 | .setDesc('The path to your Kotlin installation.')
9 | .addText(text => text
10 | .setValue(tab.plugin.settings.kotlinPath)
11 | .onChange(async (value) => {
12 | const sanitized = tab.sanitizePath(value);
13 | tab.plugin.settings.kotlinPath = sanitized;
14 | console.log('Kotlin path set to: ' + sanitized);
15 | await tab.plugin.saveSettings();
16 | }));
17 | new Setting(containerEl)
18 | .setName('Kotlin arguments')
19 | .addText(text => text
20 | .setValue(tab.plugin.settings.kotlinArgs)
21 | .onChange(async (value) => {
22 | tab.plugin.settings.kotlinArgs = value;
23 | console.log('Kotlin args set to: ' + value);
24 | await tab.plugin.saveSettings();
25 | }));
26 | tab.makeInjectSetting(containerEl, "kotlin");
27 | }
--------------------------------------------------------------------------------
/images/figure_minimal_example.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
40 |
--------------------------------------------------------------------------------
/src/settings/per-lang/makeRacketSettings.ts:
--------------------------------------------------------------------------------
1 | import { Setting } from "obsidian";
2 | import { SettingsTab } from "../SettingsTab";
3 |
4 | export default (tab: SettingsTab, containerEl: HTMLElement) => {
5 | containerEl.createEl('h3', { text: 'Racket Settings' });
6 | new Setting(containerEl)
7 | .setName('racket path')
8 | .setDesc("Path to your racket installation")
9 | .addText(text => text
10 | .setValue(tab.plugin.settings.racketPath)
11 | .onChange(async (value) => {
12 | const sanitized = tab.sanitizePath(value);
13 | tab.plugin.settings.racketPath = sanitized;
14 | console.log('racket path set to: ' + sanitized);
15 | await tab.plugin.saveSettings();
16 | }));
17 | new Setting(containerEl)
18 | .setName('Racket arguments')
19 | .addText(text => text
20 | .setValue(tab.plugin.settings.racketArgs)
21 | .onChange(async (value) => {
22 | tab.plugin.settings.racketArgs = value;
23 | console.log('Racket args set to: ' + value);
24 | await tab.plugin.saveSettings();
25 | }));
26 | tab.makeInjectSetting(containerEl, "racket");
27 | }
28 |
--------------------------------------------------------------------------------
/src/executors/AsyncExecutor.ts:
--------------------------------------------------------------------------------
1 | import Executor from "./Executor";
2 |
3 | type PromiseableCallback = (resolve: (result?: any) => void, reject: (reason?: any) => void) => void
4 |
5 | export default abstract class AsyncExecutor extends Executor {
6 | private runningTask: Promise = Promise.resolve();
7 |
8 |
9 | /**
10 | * Add a job to the internal executor queue.
11 | * Callbacks are guaranteed to only be called once, and to be called when there are no other tasks running.
12 | * A callback is interpreted the same as a promise: it must call the `resolve` or `reject` callbacks to complete the job.
13 | * The returned promise resolves when the job has completed.
14 | */
15 | protected async addJobToQueue(promiseCallback: PromiseableCallback): Promise {
16 | const previousJob = this.runningTask;
17 |
18 | this.runningTask = new Promise((resolve, reject) => {
19 | previousJob.finally(async () => {
20 | try {
21 | await new Promise((innerResolve, innerReject) => {
22 | this.once("close", () => innerResolve(undefined));
23 | promiseCallback(innerResolve, innerReject);
24 | });
25 | resolve();
26 | } catch (e) {
27 | reject(e);
28 | }
29 |
30 | })
31 | })
32 |
33 | return this.runningTask;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/versions.json:
--------------------------------------------------------------------------------
1 | {
2 | "0.1,0": "0.12.0",
3 | "0.2.0": "0.12.0",
4 | "0.3.0": "0.12.0",
5 | "0.3.1": "0.12.0",
6 | "0.3.2": "0.12.0",
7 | "0.4.0": "0.12.0",
8 | "0.5.0": "0.12.0",
9 | "0.5.1": "0.12.0",
10 | "0.5.2": "0.12.0",
11 | "0.5.3": "0.12.0",
12 | "0.6.0": "0.12.0",
13 | "0.7.0": "0.12.0",
14 | "0.8.0": "0.12.0",
15 | "0.8.1": "0.12.0",
16 | "0.9.0": "0.12.0",
17 | "0.9.1": "0.12.0",
18 | "0.9.2": "0.12.0",
19 | "0.10.0": "0.12.0",
20 | "0.11.0": "0.12.0",
21 | "0.12.0": "0.12.0",
22 | "0.12.1": "0.12.0",
23 | "0.13.0": "0.12.0",
24 | "0.14.0": "0.12.0",
25 | "0.15.0": "0.12.0",
26 | "0.15.1": "0.12.0",
27 | "0.15.2": "0.12.0",
28 | "0.16.0": "0.12.0",
29 | "0.17.0": "0.12.0",
30 | "0.18.0": "0.12.0",
31 | "1.0.0": "0.12.0",
32 | "1.1.0": "0.12.0",
33 | "1.1.1": "0.12.0",
34 | "1.2.0": "0.12.0",
35 | "1.3.0": "0.12.0",
36 | "1.4.0": "0.12.0",
37 | "1.5.0": "0.12.0",
38 | "1.6.0": "0.12.0",
39 | "1.6.1": "0.12.0",
40 | "1.6.2": "0.12.0",
41 | "1.7.0": "0.12.0",
42 | "1.7.1": "0.12.0",
43 | "1.8.0": "0.12.0",
44 | "1.8.1": "0.12.0",
45 | "1.9.0": "1.2.8",
46 | "1.9.1": "1.2.8",
47 | "1.10.0": "1.2.8",
48 | "1.11.0": "1.2.8",
49 | "1.11.1": "1.2.8",
50 | "1.12.0": "1.2.8",
51 | "2.0.0": "1.7.2",
52 | "2.1.0": "1.7.2",
53 | "2.1.1": "1.7.2",
54 | "undefined": "1.7.2",
55 | "2.1.2": "1.7.2"
56 | }
--------------------------------------------------------------------------------
/src/settings/per-lang/makeApplescriptSettings.ts:
--------------------------------------------------------------------------------
1 | import { Setting } from "obsidian";
2 | import { SettingsTab } from "../SettingsTab";
3 |
4 | export default (tab: SettingsTab, containerEl: HTMLElement) => {
5 | containerEl.createEl('h3', { text: 'Applescript Settings' });
6 | new Setting(containerEl)
7 | .setName('Osascript path')
8 | .setDesc('The path to your osascript installation (only available on MacOS).')
9 | .addText(text => text
10 | .setValue(tab.plugin.settings.applescriptPath)
11 | .onChange(async (value) => {
12 | const sanitized = tab.sanitizePath(value);
13 | tab.plugin.settings.applescriptPath = sanitized;
14 | console.log('Applescript path set to: ' + sanitized);
15 | await tab.plugin.saveSettings();
16 | }));
17 | new Setting(containerEl)
18 | .setName('Applescript arguments')
19 | .addText(text => text
20 | .setValue(tab.plugin.settings.applescriptArgs)
21 | .onChange(async (value) => {
22 | tab.plugin.settings.applescriptArgs = value;
23 | console.log('Applescript args set to: ' + value);
24 | await tab.plugin.saveSettings();
25 | }));
26 | tab.makeInjectSetting(containerEl, "applescript");
27 | }
28 |
--------------------------------------------------------------------------------
/src/settings/per-lang/makeMathematicaSettings.ts:
--------------------------------------------------------------------------------
1 | import { Setting } from "obsidian";
2 | import { SettingsTab } from "../SettingsTab";
3 |
4 | export default (tab: SettingsTab, containerEl: HTMLElement) => {
5 | containerEl.createEl('h3', { text: 'Wolfram Mathematica Settings' });
6 | new Setting(containerEl)
7 | .setName('Mathematica path')
8 | .setDesc('The path to your Mathematica installation.')
9 | .addText(text => text
10 | .setValue(tab.plugin.settings.mathematicaPath)
11 | .onChange(async (value) => {
12 | const sanitized = tab.sanitizePath(value);
13 | tab.plugin.settings.mathematicaPath = sanitized;
14 | console.log('Mathematica path set to: ' + sanitized);
15 | await tab.plugin.saveSettings();
16 | }));
17 | new Setting(containerEl)
18 | .setName('Mathematica arguments')
19 | .addText(text => text
20 | .setValue(tab.plugin.settings.mathematicaArgs)
21 | .onChange(async (value) => {
22 | tab.plugin.settings.mathematicaArgs = value;
23 | console.log('Mathematica args set to: ' + value);
24 | await tab.plugin.saveSettings();
25 | }));
26 | tab.makeInjectSetting(containerEl, "mathematica");
27 | }
--------------------------------------------------------------------------------
/esbuild.config.mjs:
--------------------------------------------------------------------------------
1 | import esbuild from "esbuild";
2 | import process from "process";
3 | import builtins from 'builtin-modules'
4 |
5 | const banner =
6 | `/*
7 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
8 | if you want to view the source, please visit the github repository of this plugin
9 | */
10 | `;
11 |
12 | const prod = (process.argv[2] === 'production');
13 |
14 | esbuild.build({
15 | banner: {
16 | js: banner,
17 | },
18 | entryPoints: ['src/main.ts'],
19 | bundle: true,
20 | external: [
21 | 'obsidian',
22 | 'electron',
23 | '@codemirror/autocomplete',
24 | '@codemirror/closebrackets',
25 | '@codemirror/collab',
26 | '@codemirror/commands',
27 | '@codemirror/comment',
28 | '@codemirror/fold',
29 | '@codemirror/gutter',
30 | '@codemirror/highlight',
31 | '@codemirror/history',
32 | '@codemirror/language',
33 | '@codemirror/lint',
34 | '@codemirror/matchbrackets',
35 | '@codemirror/panel',
36 | '@codemirror/rangeset',
37 | '@codemirror/rectangular-selection',
38 | '@codemirror/search',
39 | '@codemirror/state',
40 | '@codemirror/stream-parser',
41 | '@codemirror/text',
42 | '@codemirror/tooltip',
43 | '@codemirror/view',
44 | ...builtins],
45 | format: 'cjs',
46 | watch: !prod,
47 | target: 'es2018',
48 | logLevel: "info",
49 | sourcemap: prod ? false : 'inline',
50 | treeShaking: true,
51 | outfile: 'main.js',
52 | }).catch(() => process.exit(1));
53 |
--------------------------------------------------------------------------------
/version-bump.mjs:
--------------------------------------------------------------------------------
1 | /**
2 | * This script updates the version in manifest.json, package-lock.json, versions.json and CHANGELOG.md
3 | * with the version specified in the package.json.
4 | */
5 |
6 | import {readFileSync, writeFileSync} from "fs";
7 |
8 | // READ TARGET VERSION FROM NPM package.json
9 | const targetVersion = process.env.npm_package_version;
10 |
11 | // read minAppVersion from manifest.json and bump version to target version
12 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
13 | const {minAppVersion} = manifest;
14 | manifest.version = targetVersion;
15 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
16 |
17 | let package_lock = JSON.parse(readFileSync("package-lock.json", "utf8"));
18 | package_lock.version = targetVersion;
19 | manifest.version = targetVersion;
20 | writeFileSync("package-lock.json", JSON.stringify(package_lock, null, "\t"));
21 |
22 | // update versions.json with target version and minAppVersion from manifest.json
23 | let versions = JSON.parse(readFileSync("versions.json", "utf8"));
24 | versions[targetVersion] = minAppVersion;
25 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
26 |
27 | // Update version in CHANGELOG
28 | const changelog = readFileSync("CHANGELOG.md", "utf8");
29 | const newChangelog = changelog.replace(/^## \[Unreleased\]/m, `## [${targetVersion}]`);
30 | writeFileSync("CHANGELOG.md", newChangelog);
31 |
32 | console.log(`Updated version to ${targetVersion} and minAppVersion to ${minAppVersion} in manifest.json, versions.json and CHANGELOG.md`);
33 |
--------------------------------------------------------------------------------
/src/settings/per-lang/makeJsSettings.ts:
--------------------------------------------------------------------------------
1 | import { Setting } from "obsidian";
2 | import { SettingsTab } from "../SettingsTab";
3 |
4 | export default (tab: SettingsTab, containerEl: HTMLElement) => {
5 | containerEl.createEl('h3', { text: 'JavaScript / Node Settings' });
6 | new Setting(containerEl)
7 | .setName('Node path')
8 | .addText(text => text
9 | .setValue(tab.plugin.settings.nodePath)
10 | .onChange(async (value) => {
11 | const sanitized = tab.sanitizePath(value);
12 | tab.plugin.settings.nodePath = sanitized;
13 | console.log('Node path set to: ' + sanitized);
14 | await tab.plugin.saveSettings();
15 | }));
16 | new Setting(containerEl)
17 | .setName('Node arguments')
18 | .addText(text => text
19 | .setValue(tab.plugin.settings.nodeArgs)
20 | .onChange(async (value) => {
21 | tab.plugin.settings.nodeArgs = value;
22 | console.log('Node args set to: ' + value);
23 | await tab.plugin.saveSettings();
24 | }));
25 | new Setting(containerEl)
26 | .setName("Run Javascript blocks in Notebook Mode")
27 | .addToggle((toggle) => toggle
28 | .setValue(tab.plugin.settings.jsInteractive)
29 | .onChange(async (value) => {
30 | tab.plugin.settings.jsInteractive = value;
31 | await tab.plugin.saveSettings();
32 | })
33 | )
34 | tab.makeInjectSetting(containerEl, "js");
35 | }
--------------------------------------------------------------------------------
/src/settings/per-lang/makeFSharpSettings.ts:
--------------------------------------------------------------------------------
1 | import { Setting } from "obsidian";
2 | import { SettingsTab } from "../SettingsTab";
3 |
4 | export default (tab: SettingsTab, containerEl: HTMLElement) => {
5 | containerEl.createEl('h3', { text: 'F# Settings' });
6 | new Setting(containerEl)
7 | .setName('F# path')
8 | .setDesc('The path to dotnet.')
9 | .addText(text => text
10 | .setValue(tab.plugin.settings.fsharpPath)
11 | .onChange(async (value) => {
12 | const sanitized = tab.sanitizePath(value);
13 | tab.plugin.settings.fsharpPath = sanitized;
14 | console.log('F# path set to: ' + sanitized);
15 | await tab.plugin.saveSettings();
16 | }));
17 | new Setting(containerEl)
18 | .setName('F# arguments')
19 | .addText(text => text
20 | .setValue(tab.plugin.settings.fsharpArgs)
21 | .onChange(async (value) => {
22 | tab.plugin.settings.fsharpArgs = value;
23 | console.log('F# args set to: ' + value);
24 | await tab.plugin.saveSettings();
25 | }));
26 | new Setting(containerEl)
27 | .setName('F# file extension')
28 | .setDesc('Changes the file extension for generated F# scripts.')
29 | .addText(text => text
30 | .setValue(tab.plugin.settings.fsharpFileExtension)
31 | .onChange(async (value) => {
32 | tab.plugin.settings.fsharpFileExtension = value;
33 | console.log('F# file extension set to: ' + value);
34 | await tab.plugin.saveSettings();
35 | }));
36 | tab.makeInjectSetting(containerEl, "fsharp");
37 | }
--------------------------------------------------------------------------------
/src/settings/per-lang/makeBatchSettings.ts:
--------------------------------------------------------------------------------
1 | import { Setting } from "obsidian";
2 | import { SettingsTab } from "../SettingsTab";
3 |
4 | export default (tab: SettingsTab, containerEl: HTMLElement) => {
5 | containerEl.createEl('h3', { text: 'Batch Settings' });
6 | new Setting(containerEl)
7 | .setName('Batch path')
8 | .setDesc('The path to the terminal. Default is command prompt.')
9 | .addText(text => text
10 | .setValue(tab.plugin.settings.batchPath)
11 | .onChange(async (value) => {
12 | const sanitized = tab.sanitizePath(value);
13 | tab.plugin.settings.batchPath = sanitized;
14 | console.log('Batch path set to: ' + sanitized);
15 | await tab.plugin.saveSettings();
16 | }));
17 | new Setting(containerEl)
18 | .setName('Batch arguments')
19 | .addText(text => text
20 | .setValue(tab.plugin.settings.batchArgs)
21 | .onChange(async (value) => {
22 | tab.plugin.settings.batchArgs = value;
23 | console.log('Batch args set to: ' + value);
24 | await tab.plugin.saveSettings();
25 | }));
26 | new Setting(containerEl)
27 | .setName('Batch file extension')
28 | .setDesc('Changes the file extension for generated batch scripts. Default is .bat')
29 | .addText(text => text
30 | .setValue(tab.plugin.settings.batchFileExtension)
31 | .onChange(async (value) => {
32 | tab.plugin.settings.batchFileExtension = value;
33 | console.log('Batch file extension set to: ' + value);
34 | await tab.plugin.saveSettings();
35 | }));
36 | tab.makeInjectSetting(containerEl, "batch");
37 | }
38 |
--------------------------------------------------------------------------------
/src/Vault.ts:
--------------------------------------------------------------------------------
1 | import type {App, FileSystemAdapter} from "obsidian";
2 | import {MarkdownView} from "obsidian";
3 |
4 | /**
5 | * Get the full HTML content of the current MarkdownView
6 | *
7 | * @param view - The MarkdownView to get the HTML from
8 | * @returns The full HTML of the MarkdownView
9 | */
10 | function getFullContentHtml(view: MarkdownView): string {
11 | const codeMirror = view.editor.cm;
12 | codeMirror.viewState.printing = true;
13 | codeMirror.measure();
14 | const html = view.contentEl.innerHTML;
15 | codeMirror.viewState.printing = false;
16 | codeMirror.measure();
17 | return html;
18 | }
19 |
20 | /**
21 | * Tries to get the active view from obsidian and returns a dictionary containing the file name, folder path,
22 | * file path, and vault path of the currently opened / focused note.
23 | *
24 | * @param app The current app handle (this.app from ExecuteCodePlugin)
25 | * @returns { fileName: string; folder: string; filePath: string; vaultPath: string; fileContent: string } A dictionary containing the
26 | * file name, folder path, file path, vault pat, and file content of the currently opened / focused note.
27 | */
28 | export function getVaultVariables(app: App) {
29 | const activeView = app.workspace.getActiveViewOfType(MarkdownView);
30 | if (activeView === null) {
31 | return null;
32 | }
33 |
34 | const adapter = app.vault.adapter as FileSystemAdapter;
35 | const vaultPath = adapter.getBasePath();
36 | const folder = activeView.file.parent.path;
37 | const fileName = activeView.file.name
38 | const filePath = activeView.file.path
39 | const fileContent = getFullContentHtml(activeView);
40 |
41 | const theme = document.body.classList.contains("theme-light") ? "light" : "dark";
42 |
43 | return {
44 | vaultPath: vaultPath,
45 | folder: folder,
46 | fileName: fileName,
47 | filePath: filePath,
48 | theme: theme,
49 | fileContent: fileContent
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/ReleaseNoteModal.ts:
--------------------------------------------------------------------------------
1 | import {App, Component, MarkdownRenderer, Modal} from "obsidian";
2 |
3 | export class ReleaseNoteModel extends Modal {
4 | private component: Component;
5 |
6 | constructor(app: App) {
7 | super(app);
8 | this.component = new Component();
9 | }
10 |
11 | onOpen() {
12 | let text = '# Release Note: Execute Code Plugin v2.1.0\n\n'+
13 | 'Thank you for updating to version 2.1.0! This update includes some bug fixes and improvements and brings two new features:\n' +
14 | '- [LaTeX Support](https://github.com/twibiral/obsidian-execute-code/pull/400): You can now render LaTeX code in your code blocks. Just add the language tag `latex` to your code block.\n' +
15 | '- New Magic command: [@content](https://github.com/twibiral/obsidian-execute-code/pull/390) allows you to load the file content of the open note into your code block.\n' +
16 |
17 | '\n\n\n' +
18 | '[Here you can find a detailed change log.](https://github.com/twibiral/obsidian-execute-code/blob/master/CHANGELOG.md)' +
19 | '\n\n\n' +
20 | 'If you enjoy using the plugin, consider supporting the development via [PayPal](https://www.paypal.com/paypalme/timwibiral) or [Buy Me a Coffee](https://www.buymeacoffee.com/twibiral).' +
21 |
22 | '\n\n\n---\n\n\n[OLD] Release Notes v2.0.0\n\n' +
23 | 'We are happy to announce the release of version 2.0.0. This release brings a special change: You can now make ' +
24 | 'the output of your code blocks persistent.' +
25 | 'If enabled, the output of your code blocks will be saved in the markdown file and will also be exported to PDF.' +
26 | '\n\n\n' +
27 | 'You can enable this in the settings. Be aware that this feature is still experimental and might not work as expected. ' +
28 | 'Check the [github page](https://github.com/twibiral/obsidian-execute-code) for more information.';
29 |
30 |
31 | this.component.load();
32 | MarkdownRenderer.render(this.app, text, this.contentEl, this.app.workspace.getActiveFile().path, this.component);
33 | }
34 | }
--------------------------------------------------------------------------------
/src/executors/RExecutor.ts:
--------------------------------------------------------------------------------
1 | import {ChildProcessWithoutNullStreams, spawn} from "child_process";
2 | import {Outputter} from "src/output/Outputter";
3 | import {ExecutorSettings} from "src/settings/Settings";
4 | import AsyncExecutor from "./AsyncExecutor";
5 | import ReplExecutor from "./ReplExecutor.js";
6 |
7 |
8 | export default class RExecutor extends ReplExecutor {
9 |
10 | process: ChildProcessWithoutNullStreams
11 |
12 | constructor(settings: ExecutorSettings, file: string) {
13 | //use empty array for empty string, instead of [""]
14 | const args = settings.RArgs ? settings.RArgs.split(" ") : [];
15 |
16 | let conArgName = `notebook_connection_${Math.random().toString(16).substring(2)}`;
17 |
18 | // This is the R repl.
19 | // It's coded by itself because Rscript has no REPL, and adding an additional dep on R would be lazy.
20 | //It doesn't handle printing by itself because of the need to print the sigil, so
21 | // it's really more of a REL.
22 | args.unshift(`-e`,
23 | /*R*/
24 | `${conArgName}=file("stdin", "r"); while(1) { eval(parse(text=tail(readLines(con = ${conArgName}, n=1)))) }`
25 | )
26 |
27 |
28 | super(settings, settings.RPath, args, file, "r");
29 | }
30 |
31 | /**
32 | * Writes a single newline to ensure that the stdin is set up correctly.
33 | */
34 | async setup() {
35 | console.log("setup");
36 | //this.process.stdin.write("\n");
37 | }
38 |
39 | wrapCode(code: string, finishSigil: string): string {
40 | return `tryCatch({
41 | cat(sprintf("%s",
42 | eval(parse(text = ${JSON.stringify(code)} ))
43 | ))
44 | },
45 | error = function(e){
46 | cat(sprintf("%s", e), file=stderr())
47 | },
48 | finally = {
49 | cat(${JSON.stringify(finishSigil)});
50 | flush.console()
51 | })`.replace(/\r?\n/g, "") +
52 | "\n";
53 | }
54 |
55 | removePrompts(output: string, source: "stdout" | "stderr"): string {
56 | return output;
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/src/output/RegExpUtilities.ts:
--------------------------------------------------------------------------------
1 | /** Escapes special regex characters in a string to create a RegExp that matches it literally */
2 | export function escape(str: string): RegExp {
3 | return new RegExp(str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); // $& means the whole matched string
4 | }
5 |
6 | /** Converts "/regex/" into RegExp */
7 | export function parse(pattern: string): RegExp | undefined {
8 | try {
9 | const trimmedSlashes: string = pattern.replace(/^\/|\/$/g, '');
10 | return RegExp(trimmedSlashes);
11 | } catch {
12 | return undefined;
13 | }
14 | }
15 |
16 | /** Makes a pattern optional by adding ? quantifier, equivalent to (pattern)? */
17 | export function optional(pattern: RegExp): RegExp {
18 | return new RegExp(group(pattern).source + '?');
19 | }
20 |
21 | /** Creates a named capture group from the pattern, equivalent to (?pattern) */
22 | export function capture(pattern: RegExp, groupName: string): RegExp {
23 | return group(pattern, { name: groupName });
24 | }
25 |
26 | /** Express unit?/scope?/encapsulated?/unbreakable? of inner pattern */
27 | export function group(inner: RegExp, options?: { name?: string }): RegExp {
28 | let identifier = '';
29 | if (options?.name) identifier = `?<${options.name}>`;
30 | return new RegExp('(' + identifier + inner.source + ')');
31 | }
32 |
33 | /** Combines multiple patterns sequentially into a single pattern */
34 | export function concat(...chain: RegExp[]): RegExp {
35 | const combined: string = chain
36 | .filter(Boolean)
37 | .map(pattern => pattern.source)
38 | .join('');
39 | return new RegExp(combined);
40 | }
41 |
42 | /** Creates an alternation (OR) group from multiple patterns, equivalent to (pattern1|pattern2) */
43 | export function alternate(...options: RegExp[]): RegExp {
44 | const alternated: string = options
45 | .filter(Boolean)
46 | .map(pattern => pattern.source)
47 | .join('|');
48 | return group(new RegExp(alternated));
49 | }
--------------------------------------------------------------------------------
/src/settings/per-lang/makeHaskellSettings.ts:
--------------------------------------------------------------------------------
1 | import {Setting} from "obsidian";
2 | import {SettingsTab} from "../SettingsTab";
3 |
4 | export default (tab: SettingsTab, containerEl: HTMLElement) => {
5 | containerEl.createEl('h3', {text: 'Haskell Settings'});
6 | new Setting(containerEl)
7 | .setName('Use Ghci')
8 | .setDesc('Run haskell code with ghci instead of runghc')
9 | .addToggle(toggle => toggle
10 | .setValue(tab.plugin.settings.useGhci)
11 | .onChange(async (value) => {
12 | tab.plugin.settings.useGhci = value;
13 | console.log(value ? 'Now using ghci for haskell' : "Now using runghc for haskell.");
14 | await tab.plugin.saveSettings();
15 | }));
16 | new Setting(containerEl)
17 | .setName('Ghci path')
18 | .setDesc('The path to your ghci installation.')
19 | .addText(text => text
20 | .setValue(tab.plugin.settings.ghciPath)
21 | .onChange(async (value) => {
22 | const sanitized = tab.sanitizePath(value);
23 | tab.plugin.settings.ghciPath = sanitized;
24 | console.log('ghci path set to: ' + sanitized);
25 | await tab.plugin.saveSettings();
26 | }));
27 | new Setting(containerEl)
28 | .setName('Runghc path')
29 | .setDesc('The path to your runghc installation.')
30 | .addText(text => text
31 | .setValue(tab.plugin.settings.runghcPath)
32 | .onChange(async (value) => {
33 | const sanitized = tab.sanitizePath(value);
34 | tab.plugin.settings.runghcPath = sanitized;
35 | console.log('runghc path set to: ' + sanitized);
36 | await tab.plugin.saveSettings();
37 | }));
38 | new Setting(containerEl)
39 | .setName('Ghc path')
40 | .setDesc('The Ghc path your runghc installation will call.')
41 | .addText(text => text
42 | .setValue(tab.plugin.settings.ghcPath)
43 | .onChange(async (value) => {
44 | const sanitized = tab.sanitizePath(value);
45 | tab.plugin.settings.ghcPath = sanitized;
46 | console.log('ghc path set to: ' + sanitized);
47 | await tab.plugin.saveSettings();
48 | }));
49 | tab.makeInjectSetting(containerEl, "haskell");
50 | }
51 |
--------------------------------------------------------------------------------
/src/CodeBlockArgs.ts:
--------------------------------------------------------------------------------
1 | import {Notice} from "obsidian";
2 | import * as JSON5 from "json5";
3 |
4 | export type ExportType = "pre" | "post";
5 |
6 | /**
7 | * Arguments for code blocks, specified next to the language identifier as JSON
8 | * @example ```python {"export": "pre"}
9 | * @example ```cpp {"ignoreExport": ["post"]}
10 | */
11 | export interface CodeBlockArgs {
12 | label?: string;
13 | import?: string | string[];
14 | export?: ExportType | ExportType[];
15 | ignore?: (ExportType | "global")[] | ExportType | "global" | "all";
16 | }
17 |
18 | /**
19 | * Get code block args given the first line of the code block.
20 | *
21 | * @param firstLineOfCode The first line of a code block that contains the language name.
22 | * @returns The arguments from the first line of the code block.
23 | */
24 | export function getArgs(firstLineOfCode: string): CodeBlockArgs {
25 | // No args specified
26 | if (!firstLineOfCode.contains("{") && !firstLineOfCode.contains("}"))
27 | return {};
28 | try {
29 | let args = firstLineOfCode.substring(firstLineOfCode.indexOf("{") + 1).trim();
30 | // Transform custom syntax to JSON5
31 | args = args.replace(/=/g, ":");
32 | // Handle unnamed export arg - pre / post at the beginning of the args without any arg name
33 | const exports: ExportType[] = [];
34 | const handleUnnamedExport = (exportName: ExportType) => {
35 | let i = args.indexOf(exportName);
36 | while (i !== -1) {
37 | const nextChar = args[i + exportName.length];
38 | if (nextChar !== `"` && nextChar !== `'`) {
39 | // Remove from args string
40 | args = args.substring(0, i) + args.substring(i + exportName.length + (nextChar === "}" ? 0 : 1));
41 | exports.push(exportName);
42 | }
43 | i = args.indexOf(exportName, i + 1);
44 | }
45 | };
46 | handleUnnamedExport("pre");
47 | handleUnnamedExport("post");
48 | args = `{export: ['${exports.join("', '")}'], ${args}`;
49 | return JSON5.parse(args);
50 | } catch (err) {
51 | new Notice(`Failed to parse code block arguments from line:\n${firstLineOfCode}\n\nFailed with error:\n${err}`);
52 | return {};
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/settings/per-lang/makePythonSettings.ts:
--------------------------------------------------------------------------------
1 | import { Setting } from "obsidian";
2 | import { SettingsTab } from "../SettingsTab";
3 |
4 | export default (tab: SettingsTab, containerEl: HTMLElement) => {
5 | containerEl.createEl('h3', { text: 'Python Settings' });
6 | new Setting(containerEl)
7 | .setName('Embed Python Plots')
8 | .addToggle(toggle => toggle
9 | .setValue(tab.plugin.settings.pythonEmbedPlots)
10 | .onChange(async (value) => {
11 | tab.plugin.settings.pythonEmbedPlots = value;
12 | console.log(value ? 'Embedding Plots into Notes.' : "Not embedding Plots into Notes.");
13 | await tab.plugin.saveSettings();
14 | }));
15 | new Setting(containerEl)
16 | .setName('Python path')
17 | .setDesc('The path to your Python installation.')
18 | .addText(text => text
19 | .setValue(tab.plugin.settings.pythonPath)
20 | .onChange(async (value) => {
21 | const sanitized = tab.sanitizePath(value);
22 | tab.plugin.settings.pythonPath = sanitized;
23 | console.log('Python path set to: ' + sanitized);
24 | await tab.plugin.saveSettings();
25 | }));
26 | new Setting(containerEl)
27 | .setName('Python arguments')
28 | .addText(text => text
29 | .setValue(tab.plugin.settings.pythonArgs)
30 | .onChange(async (value) => {
31 | tab.plugin.settings.pythonArgs = value;
32 | console.log('Python args set to: ' + value);
33 | await tab.plugin.saveSettings();
34 | }));
35 | new Setting(containerEl)
36 | .setName("Run Python blocks in Notebook Mode")
37 | .addToggle((toggle) => toggle
38 | .setValue(tab.plugin.settings.pythonInteractive)
39 | .onChange(async (value) => {
40 | tab.plugin.settings.pythonInteractive = value;
41 | await tab.plugin.saveSettings();
42 | }));
43 | tab.makeInjectSetting(containerEl, "python");
44 | }
--------------------------------------------------------------------------------
/images/figure_include_attachments.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
61 |
--------------------------------------------------------------------------------
/src/settings/per-lang/makeRSettings.ts:
--------------------------------------------------------------------------------
1 | import { Setting } from "obsidian";
2 | import { SettingsTab } from "../SettingsTab";
3 |
4 | export default (tab: SettingsTab, containerEl: HTMLElement) => {
5 | containerEl.createEl('h3', { text: 'R Settings' });
6 | new Setting(containerEl)
7 | .setName('Embed R Plots created via `plot()` into Notes')
8 | .addToggle(toggle => toggle
9 | .setValue(tab.plugin.settings.REmbedPlots)
10 | .onChange(async (value) => {
11 | tab.plugin.settings.REmbedPlots = value;
12 | console.log(value ? 'Embedding R Plots into Notes.' : "Not embedding R Plots into Notes.");
13 | await tab.plugin.saveSettings();
14 | }));
15 | new Setting(containerEl)
16 | .setName('Rscript path')
17 | .setDesc('The path to your Rscript installation. Ensure you provide the Rscript binary instead of the ordinary R binary.')
18 | .addText(text => text
19 | .setValue(tab.plugin.settings.RPath)
20 | .onChange(async (value) => {
21 | const sanitized = tab.sanitizePath(value);
22 | tab.plugin.settings.RPath = sanitized;
23 | console.log('R path set to: ' + sanitized);
24 | await tab.plugin.saveSettings();
25 | }));
26 | new Setting(containerEl)
27 | .setName('R arguments')
28 | .addText(text => text
29 | .setValue(tab.plugin.settings.RArgs)
30 | .onChange(async (value) => {
31 | tab.plugin.settings.RArgs = value;
32 | console.log('R args set to: ' + value);
33 | await tab.plugin.saveSettings();
34 | }));
35 | new Setting(containerEl)
36 | .setName("Run R blocks in Notebook Mode")
37 | .addToggle((toggle) => toggle
38 | .setValue(tab.plugin.settings.rInteractive)
39 | .onChange(async (value) => {
40 | tab.plugin.settings.rInteractive = value;
41 | await tab.plugin.saveSettings();
42 | }));
43 | tab.makeInjectSetting(containerEl, "r");
44 | }
--------------------------------------------------------------------------------
/src/executors/python/PythonExecutor.ts:
--------------------------------------------------------------------------------
1 | import {ChildProcessWithoutNullStreams, spawn} from "child_process";
2 | import {Outputter} from "src/output/Outputter";
3 | import {ExecutorSettings} from "src/settings/Settings";
4 | import AsyncExecutor from "../AsyncExecutor";
5 | import ReplExecutor from "../ReplExecutor.js";
6 | import wrapPython, {PLT_DEFAULT_BACKEND_PY_VAR} from "./wrapPython";
7 |
8 | export default class PythonExecutor extends ReplExecutor {
9 | removePrompts(output: string, source: "stdout" | "stderr"): string {
10 | if(source == "stderr") {
11 | return output.replace(/(^((\.\.\.|>>>) )+)|(((\.\.\.|>>>) )+$)/g, "");
12 | } else {
13 | return output;
14 | }
15 | }
16 | wrapCode(code: string, finishSigil: string): string {
17 | return wrapPython(code, this.globalsDictionaryName, this.printFunctionName,
18 | finishSigil, this.settings.pythonEmbedPlots);
19 | }
20 |
21 |
22 |
23 | process: ChildProcessWithoutNullStreams
24 |
25 | printFunctionName: string;
26 | globalsDictionaryName: string;
27 |
28 | constructor(settings: ExecutorSettings, file: string) {
29 |
30 | const args = settings.pythonArgs ? settings.pythonArgs.split(" ") : [];
31 |
32 | args.unshift("-i");
33 |
34 | super(settings, settings.pythonPath, args,
35 | file, "python");
36 |
37 | this.printFunctionName = `__print_${Math.random().toString().substring(2)}_${Date.now()}`;
38 | this.globalsDictionaryName = `__globals_${Math.random().toString().substring(2)}_${Date.now()}`;
39 | }
40 |
41 |
42 | /**
43 | * Swallows and does not output the "Welcome to Python v..." message that shows at startup.
44 | * Also sets the printFunctionName up correctly and sets up matplotlib
45 | */
46 | async setup() {
47 | this.addJobToQueue((resolve, reject) => {
48 | this.process.stdin.write(
49 | /*python*/`
50 | ${this.globalsDictionaryName} = {**globals()}
51 | ${this.settings.pythonEmbedPlots ?
52 | /*python*/`
53 | try:
54 | import matplotlib
55 | ${PLT_DEFAULT_BACKEND_PY_VAR} = matplotlib.get_backend()
56 | except:
57 | pass
58 | ` : "" }
59 |
60 | from __future__ import print_function
61 | import sys
62 | ${this.printFunctionName} = print
63 | `.replace(/\r\n/g, "\n"));
64 |
65 | this.process.stderr.once("data", (data) => {
66 | resolve();
67 | });
68 | }).then(() => { /* do nothing */ });
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/settings/per-lang/makeCppSettings.ts:
--------------------------------------------------------------------------------
1 | import { Setting } from "obsidian";
2 | import { SettingsTab } from "../SettingsTab";
3 |
4 | export default (tab: SettingsTab, containerEl: HTMLElement) => {
5 | containerEl.createEl('h3', { text: 'C++ Settings' });
6 | new Setting(containerEl)
7 | .setName('Cling path')
8 | .setDesc('The path to your Cling installation.')
9 | .addText(text => text
10 | .setValue(tab.plugin.settings.clingPath)
11 | .onChange(async (value) => {
12 | const sanitized = tab.sanitizePath(value);
13 | tab.plugin.settings.clingPath = sanitized;
14 | console.log('Cling path set to: ' + sanitized);
15 | await tab.plugin.saveSettings();
16 | }));
17 | new Setting(containerEl)
18 | .setName('Cling arguments for C++')
19 | .addText(text => text
20 | .setValue(tab.plugin.settings.cppArgs)
21 | .onChange(async (value) => {
22 | tab.plugin.settings.cppArgs = value;
23 | console.log('CPP args set to: ' + value);
24 | await tab.plugin.saveSettings();
25 | }));
26 | new Setting(containerEl)
27 | .setName('Cling std')
28 | .addDropdown(dropdown => dropdown
29 | .addOption('c++98', 'C++ 98')
30 | .addOption('c++11', 'C++ 11')
31 | .addOption('c++14', 'C++ 14')
32 | .addOption('c++17', 'C++ 17')
33 | .addOption('c++2a', 'C++ 20')
34 | .setValue(tab.plugin.settings.clingStd)
35 | .onChange(async (value) => {
36 | tab.plugin.settings.clingStd = value;
37 | console.log('Cling std set to: ' + value);
38 | await tab.plugin.saveSettings();
39 | }));
40 | new Setting(containerEl)
41 | .setName('Use main function')
42 | .setDesc('If enabled, will use a main() function as the code block entrypoint.')
43 | .addToggle((toggle) => toggle
44 | .setValue(tab.plugin.settings.cppUseMain)
45 | .onChange(async (value) => {
46 | tab.plugin.settings.cppUseMain = value;
47 | console.log('Cpp use main set to: ' + value);
48 | await tab.plugin.saveSettings();
49 | }));
50 | tab.makeInjectSetting(containerEl, "cpp");
51 | }
52 |
--------------------------------------------------------------------------------
/src/settings/per-lang/makeShellSettings.ts:
--------------------------------------------------------------------------------
1 | import { Setting } from "obsidian";
2 | import { SettingsTab } from "../SettingsTab";
3 |
4 | export default (tab: SettingsTab, containerEl: HTMLElement) => {
5 | containerEl.createEl('h3', { text: 'Shell Settings' });
6 | new Setting(containerEl)
7 | .setName('Shell path')
8 | .setDesc('The path to shell. Default is Bash but you can use any shell you want, e.g. bash, zsh, fish, ...')
9 | .addText(text => text
10 | .setValue(tab.plugin.settings.shellPath)
11 | .onChange(async (value) => {
12 | const sanitized = tab.sanitizePath(value);
13 | tab.plugin.settings.shellPath = sanitized;
14 | console.log('Shell path set to: ' + sanitized);
15 | await tab.plugin.saveSettings();
16 | }));
17 | new Setting(containerEl)
18 | .setName('Shell arguments')
19 | .addText(text => text
20 | .setValue(tab.plugin.settings.shellArgs)
21 | .onChange(async (value) => {
22 | tab.plugin.settings.shellArgs = value;
23 | console.log('Shell args set to: ' + value);
24 | await tab.plugin.saveSettings();
25 | }));
26 | new Setting(containerEl)
27 | .setName('Shell file extension')
28 | .setDesc('Changes the file extension for generated shell scripts. This is useful if you want to use a shell other than bash.')
29 | .addText(text => text
30 | .setValue(tab.plugin.settings.shellFileExtension)
31 | .onChange(async (value) => {
32 | tab.plugin.settings.shellFileExtension = value;
33 | console.log('Shell file extension set to: ' + value);
34 | await tab.plugin.saveSettings();
35 | }));
36 |
37 | new Setting(containerEl)
38 | .setName('Shell WSL mode')
39 | .setDesc('Run the shell script in Windows Subsystem for Linux. This option is used if the global "WSL Mode" is disabled.')
40 | .addToggle((toggle) =>
41 | toggle
42 | .setValue(tab.plugin.settings.shellWSLMode)
43 | .onChange(async (value) => {
44 | tab.plugin.settings.shellWSLMode = value;
45 | await tab.plugin.saveSettings();
46 | })
47 | );
48 | tab.makeInjectSetting(containerEl, "shell");
49 | }
50 |
--------------------------------------------------------------------------------
/src/settings/per-lang/makeCSettings.ts:
--------------------------------------------------------------------------------
1 | import { Setting } from "obsidian";
2 | import { SettingsTab } from "../SettingsTab";
3 |
4 | export default (tab: SettingsTab, containerEl: HTMLElement) => {
5 | containerEl.createEl('h3', { text: 'C Settings' });
6 | new Setting(containerEl)
7 | .setName('gcc / Cling path')
8 | .setDesc('The path to your gcc / Cling installation.')
9 | .addText(text => text
10 | .setValue(tab.plugin.settings.clingPath)
11 | .onChange(async (value) => {
12 | const sanitized = tab.sanitizePath(value);
13 | tab.plugin.settings.clingPath = sanitized;
14 | console.log('gcc / Cling path set to: ' + sanitized);
15 | await tab.plugin.saveSettings();
16 | }));
17 | new Setting(containerEl)
18 | .setName('gcc / Cling arguments for C')
19 | .addText(text => text
20 | .setValue(tab.plugin.settings.cArgs)
21 | .onChange(async (value) => {
22 | tab.plugin.settings.cArgs = value;
23 | console.log('gcc / Cling args set to: ' + value);
24 | await tab.plugin.saveSettings();
25 | }));
26 | new Setting(containerEl)
27 | .setName('Cling std (ignored for gcc)')
28 | .addDropdown(dropdown => dropdown
29 | .addOption('c++98', 'C++ 98')
30 | .addOption('c++11', 'C++ 11')
31 | .addOption('c++14', 'C++ 14')
32 | .addOption('c++17', 'C++ 17')
33 | .addOption('c++2a', 'C++ 20')
34 | .setValue(tab.plugin.settings.clingStd)
35 | .onChange(async (value) => {
36 | tab.plugin.settings.clingStd = value;
37 | console.log('Cling std set to: ' + value);
38 | await tab.plugin.saveSettings();
39 | }));
40 | new Setting(containerEl)
41 | .setName('Use main function (mandatory for gcc)')
42 | .setDesc('If enabled, will use a main() function as the code block entrypoint.')
43 | .addToggle((toggle) => toggle
44 | .setValue(tab.plugin.settings.cUseMain)
45 | .onChange(async (value) => {
46 | tab.plugin.settings.cUseMain = value;
47 | console.log('C use main set to: ' + value);
48 | await tab.plugin.saveSettings();
49 | }));
50 | tab.makeInjectSetting(containerEl, "c");
51 | }
52 |
--------------------------------------------------------------------------------
/src/executors/PowerShellOnWindowsExecutor.ts:
--------------------------------------------------------------------------------
1 | import NonInteractiveCodeExecutor from "./NonInteractiveCodeExecutor";
2 | import {Outputter} from "../output/Outputter";
3 | import * as fs from "fs";
4 | import * as child_process from "child_process";
5 | import windowsPathToWsl from "../transforms/windowsPathToWsl";
6 | import {ExecutorSettings} from "../settings/Settings";
7 | import {LanguageId} from "../main";
8 | import {Notice} from "obsidian";
9 | import Executor from "./Executor";
10 |
11 |
12 | /**
13 | * This class is identical to NoneInteractiveCodeExecutor, except that it uses the PowerShell encoding setting.
14 | * This is necessary because PowerShell still uses windows-1252 as default encoding for legacy reasons.
15 | * In this implementation, we use latin-1 as default encoding, which is basically the same as windows-1252.
16 | * See https://stackoverflow.com/questions/62557890/reading-a-windows-1252-file-in-node-js
17 | * and https://learn.microsoft.com/en-us/powershell/scripting/dev-cross-plat/vscode/understanding-file-encoding?view=powershell-7.3
18 | */
19 | export default class PowerShellOnWindowsExecutor extends NonInteractiveCodeExecutor {
20 | constructor(settings: ExecutorSettings, file: string) {
21 | super(settings, true, file, "powershell");
22 | }
23 |
24 | stop(): Promise {
25 | return Promise.resolve();
26 | }
27 |
28 | run(codeBlockContent: string, outputter: Outputter, cmd: string, cmdArgs: string, ext: string) {
29 | // Resolve any currently running blocks
30 | if (this.resolveRun !== undefined)
31 | this.resolveRun();
32 | this.resolveRun = undefined;
33 |
34 | return new Promise((resolve, reject) => {
35 | const tempFileName = this.getTempFile(ext);
36 |
37 | fs.promises.writeFile(tempFileName, codeBlockContent, this.settings.powershellEncoding).then(() => {
38 | const args = cmdArgs ? cmdArgs.split(" ") : [];
39 |
40 | if (this.settings.wslMode) {
41 | args.unshift("-e", cmd);
42 | cmd = "wsl";
43 | args.push(windowsPathToWsl(tempFileName));
44 | } else {
45 | args.push(tempFileName);
46 | }
47 |
48 | const child = child_process.spawn(cmd, args, {env: process.env, shell: this.usesShell});
49 |
50 | this.handleChildOutput(child, outputter, tempFileName).then(() => {
51 | this.tempFileId = undefined; // Reset the file id to use a new file next time
52 | });
53 |
54 | // We don't resolve the promise here - 'handleChildOutput' registers a listener
55 | // For when the child_process closes, and will resolve the promise there
56 | this.resolveRun = resolve;
57 | }).catch((err) => {
58 | this.notifyError(cmd, cmdArgs, tempFileName, err, outputter);
59 | resolve();
60 | });
61 | });
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/transforms/LatexTransformer.ts:
--------------------------------------------------------------------------------
1 | import { App } from 'obsidian';
2 | import { ExecutorSettings } from 'src/settings/Settings';
3 | import { addFontSpec } from './LatexFontHandler';
4 |
5 | export let appInstance: App;
6 | export let settingsInstance: ExecutorSettings;
7 |
8 | const DOCUMENT_CLASS: RegExp = /^[^%]*(?\\documentclass\s*(\[(?[^\]]*?)\])?\s*{\s*(?[^}]*?)\s*})/;
9 | interface DocumentClass {
10 | src: string,
11 | class: string,
12 | options: string,
13 | }
14 |
15 | export function modifyLatexCode(latexSrc: string, settings: ExecutorSettings): string {
16 | const documentClass: DocumentClass = captureDocumentClass(latexSrc)
17 | const injectSrc = ''
18 | + provideDocumentClass(documentClass?.class, settings.latexDocumentclass)
19 | + addFontSpec(settings)
20 | + disablePageNumberForCropping(settings);
21 | latexSrc = injectSrc + latexSrc;
22 | console.debug(`Injected LaTeX code:`, documentClass, injectSrc);
23 |
24 | latexSrc = moveDocumentClassToBeginning(latexSrc, documentClass);
25 | return latexSrc;
26 | }
27 |
28 | function disablePageNumberForCropping(settings: ExecutorSettings): string {
29 | return (settings.latexDoCrop && settings.latexCropNoPagenum)
30 | ? `\\pagestyle{empty}\n` : '';
31 | }
32 |
33 | function provideDocumentClass(currentClass: string, defaultClass: string): string {
34 | return (currentClass || defaultClass === "") ? ''
35 | : `\\documentclass{${defaultClass}}\n`;
36 | }
37 |
38 | function moveDocumentClassToBeginning(latexSrc: string, documentClass: DocumentClass): string {
39 | return (!documentClass?.src) ? latexSrc
40 | : documentClass.src + '\n' + latexSrc.replace(documentClass.src, '');
41 | }
42 |
43 | function captureDocumentClass(latexSrc: string): DocumentClass | undefined {
44 | const match: RegExpMatchArray = latexSrc.match(DOCUMENT_CLASS);
45 | if (!match) return undefined;
46 | return { src: match.groups?.src, class: match.groups?.class, options: match.groups?.options };
47 | }
48 |
49 | export function isStandaloneClass(latexSrc: string): boolean {
50 | const className = captureDocumentClass(latexSrc)?.class;
51 | return className === "standalone";
52 | }
53 |
54 | export function updateBodyClass(className: string, isActive: boolean) {
55 | if (isActive) {
56 | document.body.classList.add(className);
57 | } else {
58 | document.body.classList.remove(className);
59 | }
60 | }
61 |
62 | export function applyLatexBodyClasses(app: App, settings: ExecutorSettings) {
63 | updateBodyClass('center-latex-figures', settings.latexCenterFigures);
64 | updateBodyClass('invert-latex-figures', settings.latexInvertFigures);
65 | appInstance = app;
66 | settingsInstance = settings;
67 | }
68 |
--------------------------------------------------------------------------------
/src/transforms/TransformCode.ts:
--------------------------------------------------------------------------------
1 | import { expandColorTheme, expandNotePath, expandNoteTitle, expandVaultPath, insertNoteContent } from "./Magic";
2 | import { getVaultVariables } from "src/Vault";
3 | import { canonicalLanguages } from 'src/main';
4 | import type { App } from "obsidian";
5 | import type { LanguageId } from "src/main";
6 |
7 | /**
8 | * Transform a language name, to enable working with multiple language aliases, for example "js" and "javascript".
9 | *
10 | * @param language A language name or shortcut (e.g. 'js', 'python' or 'shell').
11 | * @returns The same language shortcut for every alias of the language.
12 | */
13 | export function getLanguageAlias(language: string | undefined): LanguageId | undefined {
14 | if (language === undefined) return undefined;
15 | switch(language) {
16 | case "javascript": return "js";
17 | case "typescript": return "ts";
18 | case "csharp": return "cs";
19 | case "bash": return "shell";
20 | case "py": return "python";
21 | case "wolfram": return "mathematica";
22 | case "nb": return "mathematica";
23 | case "wl": "mathematica";
24 | case "hs": return "haskell";
25 | }
26 | if ((canonicalLanguages as readonly string[]).includes(language))
27 | return language as LanguageId;
28 | return undefined;
29 | }
30 |
31 | /**
32 | * Perform magic on source code (parse the magic commands) to insert note path, title, vault path, etc.
33 | *
34 | * @param app The current app handle (this.app from ExecuteCodePlugin).
35 | * @param srcCode Code with magic commands.
36 | * @returns The input code with magic commands replaced.
37 | */
38 | export function transformMagicCommands(app: App, srcCode: string) {
39 | let ret = srcCode;
40 | const vars = getVaultVariables(app);
41 | if (vars) {
42 | ret = expandVaultPath(ret, vars.vaultPath);
43 | ret = expandNotePath(ret, vars.filePath);
44 | ret = expandNoteTitle(ret, vars.fileName);
45 | ret = expandColorTheme(ret, vars.theme);
46 | ret = insertNoteContent(ret, vars.fileContent);
47 | } else {
48 | console.warn(`Could not load all Vault variables! ${vars}`)
49 | }
50 | return ret;
51 | }
52 |
53 | /**
54 | * Extract the language from the first line of a code block.
55 | *
56 | * @param firstLineOfCode The first line of a code block that contains the language name.
57 | * @returns The language of the code block.
58 | */
59 | export function getCodeBlockLanguage(firstLineOfCode: string) {
60 | let currentLanguage: string = firstLineOfCode.split("```")[1].trim().split(" ")[0].split("{")[0];
61 | if (isStringNotEmpty(currentLanguage) && currentLanguage.startsWith("run-")) {
62 | currentLanguage = currentLanguage.replace("run-", "");
63 | }
64 | return getLanguageAlias(currentLanguage);
65 | }
66 |
67 | /**
68 | * Check if a string is not empty
69 | *
70 | * @param str Input string
71 | * @returns True when string not empty, False when the string is Empty
72 | */
73 | export function isStringNotEmpty(str: string): boolean {
74 | return !!str && str.trim().length > 0;
75 | }
76 |
--------------------------------------------------------------------------------
/src/executors/ClingExecutor.ts:
--------------------------------------------------------------------------------
1 | import NonInteractiveCodeExecutor from './NonInteractiveCodeExecutor';
2 | import * as child_process from "child_process";
3 | import type {ChildProcessWithoutNullStreams} from "child_process";
4 | import type {Outputter} from "src/output/Outputter";
5 | import type {ExecutorSettings} from "src/settings/Settings";
6 |
7 | export default abstract class ClingExecutor extends NonInteractiveCodeExecutor {
8 |
9 | language: "cpp" | "c"
10 |
11 | constructor(settings: ExecutorSettings, file: string, language: "c" | "cpp") {
12 | super(settings, false, file, language);
13 | }
14 |
15 | override run(codeBlockContent: string, outputter: Outputter, cmd: string, args: string, ext: string) {
16 | // Run code with a main block
17 | if (this.settings[`${this.language}UseMain`]) {
18 | // Generate a new temp file id and don't set to undefined to super.run() uses the same file id
19 | this.getTempFile(ext);
20 | // Cling expects the main function to have the same name as the file / the extension is only c when gcc is used
21 | let code: string;
22 | if (ext != "c") {
23 | code = codeBlockContent.replace(/main\(\)/g, `temp_${this.tempFileId}()`);
24 | } else {
25 | code = codeBlockContent;
26 | }
27 | return super.run(code, outputter, this.settings.clingPath, args, ext);
28 | }
29 |
30 | // Run code without a main block (cling only)
31 | return new Promise((resolve, reject) => {
32 | const childArgs = [...args.split(" "), ...codeBlockContent.split("\n")];
33 | const child = child_process.spawn(this.settings.clingPath, childArgs, {env: process.env, shell: this.usesShell});
34 | // Set resolve callback to resolve the promise in the child_process.on('close', ...) listener from super.handleChildOutput
35 | this.resolveRun = resolve;
36 | this.handleChildOutput(child, outputter, this.tempFileId);
37 | });
38 | }
39 |
40 | /**
41 | * Run parent NonInteractiveCodeExecutor handleChildOutput logic, but replace temporary main function name
42 | * In all outputs from stdout and stderr callbacks, from temp_() to main() to produce understandable output
43 | */
44 | override async handleChildOutput(child: ChildProcessWithoutNullStreams, outputter: Outputter, fileName: string) {
45 | super.handleChildOutput(child, outputter, fileName);
46 | // Remove existing stdout and stderr callbacks
47 | child.stdout.removeListener("data", this.stdoutCb);
48 | child.stderr.removeListener("data", this.stderrCb);
49 | const fileId = this.tempFileId;
50 | // Replace temp_() with main()
51 | const replaceTmpId = (data: string) => {
52 | return data.replace(new RegExp(`temp_${fileId}\\(\\)`, "g"), "main()");
53 | }
54 | // Set new stdout and stderr callbacks, the same as in the parent,
55 | // But replacing temp_() with main()
56 | child.stdout.on("data", (data) => {
57 | this.stdoutCb(replaceTmpId(data.toString()));
58 | });
59 | child.stderr.on("data", (data) => {
60 | this.stderrCb(replaceTmpId(data.toString()));
61 | });
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/executors/Executor.ts:
--------------------------------------------------------------------------------
1 | import {Notice} from "obsidian";
2 | import {Outputter} from "src/output/Outputter";
3 | import * as os from "os";
4 | import * as path from "path";
5 | import {LanguageId} from "src/main";
6 | import {EventEmitter} from "stream";
7 |
8 | export default abstract class Executor extends EventEmitter {
9 | language: LanguageId;
10 | file: string;
11 | tempFileId: string | undefined = undefined;
12 |
13 | constructor(file: string, language: LanguageId) {
14 | super();
15 | this.file = file;
16 | this.language = language;
17 | }
18 |
19 | /**
20 | * Run the given `code` and add all output to the `Outputter`. Resolves the promise once the code is done running.
21 | *
22 | * @param code code to run
23 | * @param outputter outputter to use for showing output to the user
24 | * @param cmd command to run (not used by all executors)
25 | * @param cmdArgs arguments for command to run (not used by all executors)
26 | * @param ext file extension for the programming language (not used by all executors)
27 | */
28 | abstract run(code: string, outputter: Outputter, cmd: string, cmdArgs: string, ext: string): Promise
29 |
30 | /**
31 | * Exit the runtime for the code.
32 | */
33 | abstract stop(): Promise
34 |
35 | /**
36 | * Creates new Notice that is displayed in the top right corner for a few seconds and contains an error message.
37 | * Additionally, the error is logged to the console and showed in the output panel ({@link Outputter}).
38 | *
39 | * @param cmd The command that was executed.
40 | * @param cmdArgs The arguments that were passed to the command.
41 | * @param tempFileName The name of the temporary file that contained the code.
42 | * @param err The error that was thrown.
43 | * @param outputter The outputter that should be used to display the error.
44 | * @param label A high-level, short label to show to the user
45 | * @protected
46 | */
47 | protected notifyError(cmd: string, cmdArgs: string, tempFileName: string, err: any,
48 | outputter: Outputter | undefined, label = "Error while executing code") {
49 | const errorMSG = `Error while executing ${cmd} ${cmdArgs} ${tempFileName}: ${err}`
50 | console.error(errorMSG);
51 | if(outputter) outputter.writeErr(errorMSG);
52 | new Notice(label);
53 | }
54 |
55 | /**
56 | * Creates a new unique file name for the given file extension. The file path is set to the temp path of the os.
57 | * The file name is the current timestamp: '/{temp_dir}/temp_{timestamp}.{file_extension}'
58 | * this.tempFileId will be updated, accessible to other methods
59 | * Once finished using this value, remember to set it to undefined to generate a new file
60 | *
61 | * @param ext The file extension. Should correspond to the language of the code.
62 | * @returns The temporary file path
63 | */
64 | protected getTempFile(ext: string) {
65 | if (this.tempFileId === undefined)
66 | this.tempFileId = Date.now().toString();
67 | return path.join(os.tmpdir(), `temp_${this.tempFileId}.${ext}`);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/settings/per-lang/makePowershellSettings.ts:
--------------------------------------------------------------------------------
1 | import {Notice, Setting} from "obsidian";
2 | import { SettingsTab } from "../SettingsTab";
3 |
4 | export default (tab: SettingsTab, containerEl: HTMLElement) => {
5 | containerEl.createEl('h3', { text: 'Powershell Settings' });
6 | new Setting(containerEl)
7 | .setName('Powershell path')
8 | .setDesc('The path to Powershell.')
9 | .addText(text => text
10 | .setValue(tab.plugin.settings.powershellPath)
11 | .onChange(async (value) => {
12 | const sanitized = tab.sanitizePath(value);
13 | tab.plugin.settings.powershellPath = sanitized;
14 | console.log('Powershell path set to: ' + sanitized);
15 | await tab.plugin.saveSettings();
16 | }));
17 | new Setting(containerEl)
18 | .setName('Powershell arguments')
19 | .addText(text => text
20 | .setValue(tab.plugin.settings.powershellArgs)
21 | .onChange(async (value) => {
22 | tab.plugin.settings.powershellArgs = value;
23 | console.log('Powershell args set to: ' + value);
24 | await tab.plugin.saveSettings();
25 | }));
26 | new Setting(containerEl)
27 | .setName('Powershell file extension')
28 | .setDesc('Changes the file extension for generated shell scripts. This is useful if you don\'t want to use PowerShell.')
29 | .addText(text => text
30 | .setValue(tab.plugin.settings.powershellFileExtension)
31 | .onChange(async (value) => {
32 | tab.plugin.settings.powershellFileExtension = value;
33 | console.log('Powershell file extension set to: ' + value);
34 | await tab.plugin.saveSettings();
35 | }));
36 | new Setting(containerEl)
37 | .setName('PowerShell script encoding')
38 | .setDesc('Windows still uses windows-1252 as default encoding on most systems for legacy reasons. If you change your encodings systemwide' +
39 | ' to UTF-8, you can change this setting to UTF-8 as well. Only use one of the following encodings: ' +
40 | '"ascii", "utf8", "utf-8", "utf16le", "ucs2", "ucs-2", "base64", "latin1", "binary", "hex" (default: "latin1")')
41 | .addText(text => text
42 | .setValue(tab.plugin.settings.powershellEncoding)
43 | .onChange(async (value) => {
44 | value = value.replace(/["'`´]/, "").trim().toLowerCase();
45 | if (["ascii", "utf8", "utf-8", "utf16le", "ucs2", "ucs-2", "base64", "latin1", "binary", "hex"].includes(value)) {
46 | tab.plugin.settings.powershellEncoding = value as BufferEncoding;
47 | console.log('Powershell file extension set to: ' + value);
48 | await tab.plugin.saveSettings();
49 | } else {
50 | console.error("Invalid encoding. " + value + "Please use one of the following encodings: " +
51 | '"ascii", "utf8", "utf-8", "utf16le", "ucs2", "ucs-2", "base64", "latin1", "binary", "hex"');
52 | }
53 | }));
54 | tab.makeInjectSetting(containerEl, "powershell");
55 | }
56 |
--------------------------------------------------------------------------------
/src/transforms/LatexFigureName.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import * as r from 'src/output/RegExpUtilities';
3 | import { ExecutorSettings } from 'src/settings/Settings';
4 |
5 | export const ILLEGAL_FILENAME_CHARS: RegExp = /[<>:"/\\|?*]+/g;
6 | export const WHITESPACE_AND_ILLEGAL_CHARS: RegExp = /[<>:"/\\|?*\s]+/;
7 | export const MAYBE_WHITESPACE_AND_ILLEGAL: RegExp = /[<>:"/\\|?*\s]*/;
8 | export const FIGURE_FILENAME_EXTENSIONS: RegExp = /(.pdf|.svg|.png)/;
9 | export const FILENAME_PREFIX: RegExp = /figure /;
10 | export const UNNAMED_PREFIX: RegExp = /temp /;
11 | export const TEMP_FIGURE_NAME: RegExp = /figure temp \d+/;
12 |
13 | let latexFilenameIndex = 0;
14 |
15 | export async function retrieveFigurePath(codeblockContent: string, titlePattern: string, srcFile: string, settings: ExecutorSettings): Promise {
16 | const vaultAbsolutePath = (this.app.vault.adapter as any).basePath;
17 | const vaultAttachmentPath = await this.app.fileManager.getAvailablePathForAttachment("test", srcFile);
18 | const vaultAttachmentDir = path.dirname(vaultAttachmentPath);
19 | const figureDir = path.join(vaultAbsolutePath, vaultAttachmentDir);
20 | let figureTitle = captureFigureTitle(codeblockContent, titlePattern);
21 | if (!figureTitle) {
22 | const index = nextLatexFilenameIndex(settings.latexMaxFigures);
23 | figureTitle = UNNAMED_PREFIX.source + index;
24 | }
25 | return path.join(figureDir, FILENAME_PREFIX.source + figureTitle);
26 | }
27 |
28 | function captureFigureTitle(codeblockContent: string, titlePattern: string): string | undefined {
29 | const pattern = r.parse(titlePattern);
30 | if (!pattern) return undefined;
31 | const match = codeblockContent.match(pattern);
32 | const title = match?.[1];
33 | if (!title) return undefined;
34 | return sanitizeFilename(title);
35 | }
36 |
37 | function sanitizeFilename(input: string): string {
38 | const trailingFilenames: RegExp = r.concat(FIGURE_FILENAME_EXTENSIONS, /$/);
39 | return input
40 | .replace(ILLEGAL_FILENAME_CHARS, ' ') // Remove illegal filename characters
41 | .replace(/\s+/g, ' ') // Normalize whitespace
42 | .trim()
43 | .replace(r.concat(/^/, FILENAME_PREFIX), '') // Remove prefix
44 | .replace(trailingFilenames, ''); // Remove file extension
45 | }
46 |
47 | export function generalizeFigureTitle(figureName: string): RegExp {
48 | const normalized: string = sanitizeFilename(figureName);
49 | const escaped: RegExp = r.escape(normalized);
50 | const whitespaced = new RegExp(escaped.source
51 | .replace(/\s+/g, WHITESPACE_AND_ILLEGAL_CHARS.source)); // Also allow illegal filename characters in whitespace
52 | return r.concat(
53 | MAYBE_WHITESPACE_AND_ILLEGAL,
54 | r.optional(FILENAME_PREFIX), // Optional prefix
55 | MAYBE_WHITESPACE_AND_ILLEGAL,
56 | whitespaced,
57 | MAYBE_WHITESPACE_AND_ILLEGAL,
58 | r.optional(FIGURE_FILENAME_EXTENSIONS), // Optional file extension
59 | MAYBE_WHITESPACE_AND_ILLEGAL);
60 | }
61 |
62 | function nextLatexFilenameIndex(maxIndex: number): number {
63 | latexFilenameIndex %= maxIndex;
64 | return latexFilenameIndex++;
65 | }
66 |
--------------------------------------------------------------------------------
/src/executors/PrologExecutor.ts:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | import * as prolog from "tau-prolog";
3 | import {Outputter} from "src/output/Outputter";
4 | import Executor from "./Executor";
5 | import {Notice} from "obsidian";
6 | import {ExecutorSettings} from "src/settings/Settings";
7 |
8 | export default class PrologExecutor extends Executor {
9 |
10 | runQueries: boolean;
11 | maxPrologAnswers: number;
12 |
13 | constructor(settings: ExecutorSettings, file: string) {
14 | super(file, "prolog");
15 | this.runQueries = true;
16 | this.maxPrologAnswers = settings.maxPrologAnswers;
17 | }
18 |
19 | async run(code: string, outputter: Outputter, cmd: string, cmdArgs: string, ext: string): Promise {
20 | const prologCode = code.split(/\n+%+\s*query\n+/);
21 |
22 | if (prologCode.length < 2) return; // no query found
23 |
24 | //Prolog does not support input
25 | outputter.closeInput();
26 | outputter.clear();
27 |
28 | this.runPrologCode(prologCode[0], prologCode[1], outputter);
29 | }
30 |
31 | async stop() {
32 | this.runQueries = false;
33 | this.emit("close");
34 | }
35 |
36 | /**
37 | * Executes a string with prolog code using the TauProlog interpreter.
38 | * All queries must be below a line containing only '% queries'.
39 | *
40 | * @param facts Contains the facts.
41 | * @param queries Contains the queries.
42 | * @param out The {@link Outputter} that should be used to display the output of the code.
43 | */
44 | private runPrologCode(facts: string, queries: string, out: Outputter) {
45 | new Notice("Running...");
46 | const session = prolog.create();
47 | session.consult(facts
48 | , {
49 | success: () => {
50 | session.query(queries
51 | , {
52 | success: async (goal: any) => {
53 | console.debug(`Prolog goal: ${goal}`)
54 | let answersLeft = true;
55 | let counter = 0;
56 |
57 | while (answersLeft && counter < this.maxPrologAnswers) {
58 | await session.answer({
59 | success: function (answer: any) {
60 | new Notice("Done!");
61 | console.debug(`Prolog result: ${session.format_answer(answer)}`);
62 | out.write(session.format_answer(answer) + "\n");
63 | out.closeInput();
64 | },
65 | fail: function () {
66 | /* No more answers */
67 | answersLeft = false;
68 | },
69 | error: function (err: any) {
70 | new Notice("Error!");
71 | console.error(err);
72 | answersLeft = false;
73 | out.writeErr(`Error while executing code: ${err}`);
74 | out.closeInput();
75 | },
76 | limit: function () {
77 | answersLeft = false;
78 | }
79 | });
80 | counter++;
81 | }
82 | },
83 | error: (err: any) => {
84 | new Notice("Error!");
85 | out.writeErr("Query failed.\n")
86 | out.writeErr(err.toString());
87 | }
88 | }
89 | )
90 | },
91 | error: (err: any) => {
92 | out.writeErr("Adding facts failed.\n")
93 | out.writeErr(err.toString());
94 | }
95 | }
96 | );
97 | }
98 |
99 | }
100 |
--------------------------------------------------------------------------------
/src/transforms/LatexFontHandler.ts:
--------------------------------------------------------------------------------
1 | import { Platform } from 'obsidian';
2 | import * as path from 'path';
3 | import { ExecutorSettings } from 'src/settings/Settings';
4 |
5 | let validFonts = new Set;
6 | let invalidFonts = new Set;
7 |
8 | interface FontNames {
9 | main: string;
10 | sans: string;
11 | mono: string;
12 | }
13 |
14 | /** Generates LaTeX font configuration based on system or Obsidian fonts. */
15 | export function addFontSpec(settings: ExecutorSettings): string {
16 | const isPdflatex = path.basename(settings.latexCompilerPath).toLowerCase().includes('pdflatex');
17 | if (isPdflatex || settings.latexAdaptFont === '') return '';
18 |
19 | const platformFonts = getPlatformFonts();
20 | const fontSpec = buildFontCommand(settings, platformFonts);
21 | if (!fontSpec) return '';
22 |
23 | const packageSrc = `\\usepackage{fontspec}\n`;
24 | return packageSrc + fontSpec;
25 | }
26 |
27 | /** Retrieves Obsidian's font settings from CSS variables. */
28 | function getObsidianFonts(cssVariable: string): string {
29 | const cssDeclarations = getComputedStyle(document.body);
30 | const fonts = cssDeclarations.getPropertyValue(cssVariable).split(`'??'`)[0];
31 | return sanitizeCommaList(fonts);
32 | }
33 |
34 | /** Constructs LaTeX font commands based on the provided settings and platform-specific fonts. */
35 | function buildFontCommand(settings: ExecutorSettings, fonts: FontNames): string {
36 | if (settings.latexAdaptFont === 'obsidian') {
37 | fonts.main = [getObsidianFonts('--font-text'), fonts.main].join(',');
38 | fonts.sans = [getObsidianFonts('--font-interface'), fonts.sans].join(',');
39 | fonts.mono = [getObsidianFonts('--font-monospace'), fonts.mono].join(',');
40 | }
41 | const mainSrc = buildSetfont('main', fonts.main);
42 | const sansSrc = buildSetfont('sans', fonts.sans);
43 | const monoSrc = buildSetfont('mono', fonts.mono);
44 | return mainSrc + sansSrc + monoSrc;
45 | }
46 |
47 | /** Returns default system fonts based on current platform */
48 | function getPlatformFonts(): FontNames {
49 | if (Platform.isWin) return { main: 'Segoe UI', sans: 'Segoe UI', mono: 'Consolas' };
50 | if (Platform.isMacOS) return { main: 'SF Pro', sans: 'SF Pro', mono: 'SF Mono' };
51 | if (Platform.isLinux) return { main: 'DejaVu Sans', sans: 'DejaVu Sans', mono: 'DejaVu Sans Mono' };
52 | return { main: '', sans: '', mono: '' };
53 | }
54 |
55 | /** Generates LuaLaTeX setfont command for specified font type. */
56 | function buildSetfont(type: 'main' | 'mono' | 'sans', fallbackList: string): string {
57 | const font = firstValidFont(fallbackList);
58 | return (font) ? `\\set${type}font{${font}}\n` : '';
59 | }
60 |
61 | function firstValidFont(fallbackList: string): string {
62 | return sanitizeCommaList(fallbackList)
63 | .split(', ')
64 | .reduce((result, font) => result || (cachedTestFont(font) ? font : undefined), undefined);
65 | }
66 |
67 | /** For performance, do not retest a font during the app's lifetime. */
68 | function cachedTestFont(fontName: string): boolean {
69 | if (validFonts.has(fontName)) return true;
70 | if (invalidFonts.has(fontName)) return false;
71 | if (!testFont(fontName)) {
72 | invalidFonts.add(fontName);
73 | return false;
74 | }
75 | validFonts.add(fontName);
76 | return true;
77 | }
78 |
79 | /** Tests if a font is available by comparing text measurements on canvas. */
80 | function testFont(fontName: string): boolean {
81 | const canvas = document.createElement('canvas');
82 | const context = canvas.getContext('2d');
83 | if (!context) return false;
84 |
85 | const text = 'abcdefghijklmnopqrstuvwxyz';
86 | context.font = `16px monospace`;
87 | const baselineWidth = context.measureText(text).width;
88 |
89 | context.font = `16px "${fontName}", monospace`;
90 | const testWidth = context.measureText(text).width;
91 |
92 | const isFontAvailable = baselineWidth !== testWidth;
93 | console.debug((isFontAvailable) ? `Font ${fontName} accepted.` : `Font ${fontName} ignored.`);
94 | return isFontAvailable;
95 | }
96 |
97 | /** Cleans and normalizes comma-separated font family lists */
98 | function sanitizeCommaList(commaList: string): string {
99 | return commaList
100 | .split(',')
101 | .map(font => font.trim().replace(/^["']|["']$/g, ''))
102 | .filter(Boolean)
103 | .join(', ');
104 | }
105 |
--------------------------------------------------------------------------------
/src/ExecutorContainer.ts:
--------------------------------------------------------------------------------
1 | import {EventEmitter} from "events";
2 | import Executor from "./executors/Executor";
3 | import NodeJSExecutor from "./executors/NodeJSExecutor";
4 | import NonInteractiveCodeExecutor from "./executors/NonInteractiveCodeExecutor";
5 | import PrologExecutor from "./executors/PrologExecutor";
6 | import PythonExecutor from "./executors/python/PythonExecutor";
7 | import CppExecutor from './executors/CppExecutor';
8 | import ExecuteCodePlugin, {LanguageId} from "./main";
9 | import RExecutor from "./executors/RExecutor.js";
10 | import CExecutor from "./executors/CExecutor";
11 | import FSharpExecutor from "./executors/FSharpExecutor";
12 | import LatexExecutor from "./executors/LatexExecutor";
13 |
14 | const interactiveExecutors: Partial> = {
15 | "js": NodeJSExecutor,
16 | "python": PythonExecutor,
17 | "r": RExecutor
18 | };
19 |
20 | const nonInteractiveExecutors: Partial> = {
21 | "prolog": PrologExecutor,
22 | "cpp": CppExecutor,
23 | "c": CExecutor,
24 | "fsharp": FSharpExecutor,
25 | "latex" : LatexExecutor,
26 | };
27 |
28 | export default class ExecutorContainer extends EventEmitter implements Iterable {
29 | executors: { [key in LanguageId]?: { [key: string]: Executor } } = {}
30 | plugin: ExecuteCodePlugin;
31 |
32 | constructor(plugin: ExecuteCodePlugin) {
33 | super();
34 | this.plugin = plugin;
35 |
36 | window.addEventListener("beforeunload", async () => {
37 | for(const executor of this) {
38 | executor.stop();
39 | }
40 | });
41 | }
42 |
43 | /**
44 | * Iterate through all executors
45 | */
46 | * [Symbol.iterator](): Iterator {
47 | for (const language in this.executors) {
48 | for (const file in this.executors[language as LanguageId]) {
49 | yield this.executors[language as LanguageId][file];
50 | }
51 | }
52 | }
53 |
54 | /**
55 | * Gets an executor for the given file and language. If the language in
56 | * question *may* be interactive, then the executor will be cached and re-returned
57 | * the same for subsequent calls with the same arguments.
58 | * If there isn't a cached executor, it will be created.
59 | *
60 | * @param file file to get an executor for
61 | * @param language language to get an executor for.
62 | * @param needsShell whether or not the language requires a shell
63 | */
64 | getExecutorFor(file: string, language: LanguageId, needsShell: boolean) {
65 | if (!this.executors[language]) this.executors[language] = {}
66 | if (!this.executors[language][file]) this.setExecutorInExecutorsObject(file, language, needsShell);
67 |
68 | return this.executors[language][file];
69 | }
70 |
71 | /**
72 | * Create an executor and put it into the `executors` dictionary.
73 | * @param file the file to associate the new executor with
74 | * @param language the language to associate the new executor with
75 | * @param needsShell whether or not the language requires a shell
76 | */
77 | private setExecutorInExecutorsObject(file: string, language: LanguageId, needsShell: boolean) {
78 | const exe = this.createExecutorFor(file, language, needsShell);
79 | if (!(exe instanceof NonInteractiveCodeExecutor)) this.emit("add", exe);
80 | exe.on("close", () => {
81 | delete this.executors[language][file];
82 | });
83 |
84 | this.executors[language][file] = exe;
85 | }
86 |
87 | /**
88 | * Creates an executor
89 | *
90 | * @param file the file to associate the new executor with
91 | * @param language the language to make an executor for
92 | * @param needsShell whether or not the language requires a shell
93 | * @returns a new executor associated with the given language and file
94 | */
95 | private createExecutorFor(file: string, language: LanguageId, needsShell: boolean) {
96 | // Interactive language executor
97 | if (this.plugin.settings[`${language}Interactive`]) {
98 | if (!(language in interactiveExecutors))
99 | throw new Error(`Attempted to use interactive executor for '${language}' but no such executor exists`);
100 | return new interactiveExecutors[language](this.plugin.settings, file);
101 | }
102 | // Custom non-interactive language executor
103 | else if (language in nonInteractiveExecutors)
104 | return new nonInteractiveExecutors[language](this.plugin.settings, file);
105 | // Generic non-interactive language executor
106 | return new NonInteractiveCodeExecutor(this.plugin.settings, needsShell, file, language);
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/ExecutorManagerView.ts:
--------------------------------------------------------------------------------
1 | import {ItemView, setIcon, Workspace, WorkspaceLeaf} from "obsidian";
2 | import {basename} from "path";
3 | import ExecutorContainer from "./ExecutorContainer";
4 | import Executor from "./executors/Executor";
5 |
6 | export const EXECUTOR_MANAGER_VIEW_ID = "code-execute-manage-executors";
7 | export const EXECUTOR_MANAGER_OPEN_VIEW_COMMAND_ID = "code-execute-open-manage-executors";
8 |
9 | export default class ExecutorManagerView extends ItemView {
10 | executors: ExecutorContainer;
11 |
12 | list: HTMLUListElement
13 | emptyStateElement: HTMLDivElement;
14 |
15 | constructor(leaf: WorkspaceLeaf, executors: ExecutorContainer) {
16 | super(leaf);
17 |
18 | this.executors = executors;
19 |
20 | this.executors.on("add", (executor) => {
21 | this.addExecutorElement(executor);
22 | });
23 | }
24 |
25 | /**
26 | * Open the view. Ensure that there may only be one view at a time.
27 | * If there isn't one created, then create a new one.
28 | * @param workspace the workspace of the Obsidian app
29 | */
30 | static async activate(workspace: Workspace) {
31 | workspace.detachLeavesOfType(EXECUTOR_MANAGER_VIEW_ID);
32 |
33 | await workspace.getRightLeaf(false).setViewState({
34 | type: EXECUTOR_MANAGER_VIEW_ID,
35 | active: true,
36 | });
37 |
38 | workspace.revealLeaf(
39 | workspace.getLeavesOfType(EXECUTOR_MANAGER_VIEW_ID)[0]
40 | );
41 | }
42 |
43 | getViewType(): string {
44 | return EXECUTOR_MANAGER_VIEW_ID;
45 | }
46 |
47 | getDisplayText(): string {
48 | return "Execution Runtimes";
49 | }
50 |
51 | getIcon(): string {
52 | return "command-glyph";
53 | }
54 |
55 | /**
56 | * Set up the HTML of the view
57 | */
58 | async onOpen() {
59 | const container = this.contentEl;
60 | container.empty();
61 |
62 | container.classList.add("manage-executors-view");
63 |
64 | const header = document.createElement("h3");
65 | header.textContent = "Runtimes";
66 | container.appendChild(header);
67 |
68 | this.list = document.createElement("ul");
69 | container.appendChild(document.createElement("div")).appendChild(this.list);
70 |
71 | for (const executor of this.executors) {
72 | this.addExecutorElement(executor);
73 | }
74 |
75 | this.addEmptyState();
76 | }
77 |
78 | async onClose() {
79 |
80 | }
81 |
82 | /**
83 | * Add the empty state element to the view. Also update the empty state element
84 | */
85 | private addEmptyState() {
86 | this.emptyStateElement = document.createElement("div");
87 | this.emptyStateElement.classList.add("empty-state");
88 | this.emptyStateElement.textContent = "There are currently no runtimes online. Run some code blocks, and their runtimes will appear here.";
89 |
90 | this.list.parentElement.appendChild(this.emptyStateElement);
91 |
92 | this.updateEmptyState();
93 | }
94 |
95 | /**
96 | * If the list of runtimes is empty, then show the empty-state; otherwise, hide it.
97 | */
98 | private updateEmptyState() {
99 | if (this.list.childElementCount == 0) {
100 | this.emptyStateElement.style.display = "block";
101 | } else {
102 | this.emptyStateElement.style.display = "none";
103 | }
104 | }
105 |
106 | /**
107 | * Creates and adds a manager widget list-item for a given executor.
108 | *
109 | * @param executor an executor to create a manager widget for
110 | */
111 | private addExecutorElement(executor: Executor) {
112 | const li = document.createElement("li");
113 |
114 | const simpleName = basename(executor.file);
115 |
116 | const langElem = document.createElement("small");
117 | langElem.textContent = executor.language;
118 | li.appendChild(langElem);
119 |
120 | li.appendChild(this.createFilenameRowElem(simpleName));
121 |
122 | executor.on("close", () => {
123 | li.remove();
124 | this.updateEmptyState();
125 | });
126 |
127 | const button = document.createElement("button");
128 | button.addEventListener("click", () => executor.stop());
129 | setIcon(button, "trash");
130 | button.setAttribute("aria-label", "Stop Runtime");
131 | li.appendChild(button);
132 |
133 | this.list.appendChild(li);
134 | this.updateEmptyState();
135 | }
136 |
137 | /**
138 | * A helper method to create a file-name label for use in
139 | * runtime management widgets
140 | * @param text text content for the filename label
141 | * @returns the filename label's html element
142 | */
143 | private createFilenameRowElem(text: string) {
144 | const fElem = document.createElement("span");
145 | fElem.textContent = text;
146 | fElem.classList.add("filename");
147 | return fElem;
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/src/executors/ReplExecutor.ts:
--------------------------------------------------------------------------------
1 | import { ChildProcessWithoutNullStreams, spawn } from "child_process";
2 | import { Notice } from "obsidian";
3 | import { LanguageId } from "../main.js";
4 | import { Outputter } from "../output/Outputter.js";
5 | import { ExecutorSettings } from "../settings/Settings.js";
6 | import AsyncExecutor from "./AsyncExecutor.js";
7 | import killWithChildren from "./killWithChildren.js";
8 |
9 | export default abstract class ReplExecutor extends AsyncExecutor {
10 | process: ChildProcessWithoutNullStreams;
11 | settings: ExecutorSettings;
12 |
13 | abstract wrapCode(code: string, finishSigil: string): string;
14 | abstract setup(): Promise;
15 | abstract removePrompts(output: string, source: "stdout" | "stderr"): string;
16 |
17 | protected constructor(settings: ExecutorSettings, path: string, args: string[], file: string, language: LanguageId) {
18 | super(file, language);
19 |
20 | this.settings = settings;
21 |
22 | if (this.settings.wslMode) {
23 | args.unshift("-e", path);
24 | path = "wsl";
25 | }
26 |
27 | // Replace %USERNAME% with actual username (if it exists)
28 | if (path.includes("%USERNAME%") && process?.env?.USERNAME)
29 | path = path.replace("%USERNAME%", process.env.USERNAME);
30 |
31 | // Spawns a new REPL that is used to execute code.
32 | // {env: process.env} is used to ensure that the environment variables are passed to the REPL.
33 | this.process = spawn(path, args, {env: process.env});
34 |
35 | this.process.on("close", () => {
36 | this.emit("close");
37 | new Notice("Runtime exited");
38 | this.process = null;
39 | });
40 | this.process.on("error", (err: any) => {
41 | this.notifyError(settings.pythonPath, args.join(" "), "", err, undefined, "Error launching process: " + err);
42 | this.stop();
43 | });
44 |
45 | this.setup().then(() => { /* Wait for the inheriting class to set up, then do nothing */ });
46 | }
47 |
48 | /**
49 | * Run some code
50 | * @param code code to run
51 | * @param outputter outputter to use
52 | * @param cmd Not used
53 | * @param cmdArgs Not used
54 | * @param ext Not used
55 | * @returns A promise that resolves once the code is done running
56 | */
57 | run(code: string, outputter: Outputter, cmd: string, cmdArgs: string, ext: string): Promise {
58 | outputter.queueBlock();
59 |
60 | return this.addJobToQueue((resolve, _reject) => {
61 | if (this.process === null) return resolve();
62 |
63 | const finishSigil = `SIGIL_BLOCK_DONE_${Math.random()}_${Date.now()}_${code.length}`;
64 |
65 | outputter.startBlock();
66 |
67 | const wrappedCode = this.wrapCode(code, finishSigil);
68 |
69 | this.process.stdin.write(wrappedCode);
70 |
71 | outputter.clear();
72 |
73 | outputter.on("data", (data: string) => {
74 | this.process.stdin.write(data);
75 | });
76 |
77 | const writeToStdout = (data: any) => {
78 | let str = data.toString();
79 |
80 | if (str.endsWith(finishSigil)) {
81 | str = str.substring(0, str.length - finishSigil.length);
82 |
83 | this.process.stdout.removeListener("data", writeToStdout)
84 | this.process.stderr.removeListener("data", writeToStderr);
85 | this.process.removeListener("close", resolve);
86 | outputter.write(str);
87 |
88 | resolve();
89 | } else {
90 | outputter.write(str);
91 | }
92 | };
93 |
94 | const writeToStderr = (data: any) => {
95 | outputter.writeErr(
96 | this.removePrompts(data.toString(), "stderr")
97 | );
98 | }
99 |
100 | this.process.on("close", resolve);
101 |
102 | this.process.stdout.on("data", writeToStdout);
103 | this.process.stderr.on("data", writeToStderr);
104 | });
105 | }
106 |
107 | stop(): Promise {
108 | return new Promise((resolve, _reject) => {
109 | this.process.on("close", () => {
110 | resolve();
111 | });
112 |
113 | killWithChildren(this.process.pid);
114 | this.process = null;
115 | });
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { App, Component, MarkdownRenderer, MarkdownView, Plugin, } from 'obsidian';
2 |
3 | import type { ExecutorSettings } from "./settings/Settings";
4 | import { DEFAULT_SETTINGS } from "./settings/Settings";
5 | import { SettingsTab } from "./settings/SettingsTab";
6 | import { applyLatexBodyClasses } from "./transforms/LatexTransformer"
7 |
8 | import ExecutorContainer from './ExecutorContainer';
9 | import ExecutorManagerView, {
10 | EXECUTOR_MANAGER_OPEN_VIEW_COMMAND_ID,
11 | EXECUTOR_MANAGER_VIEW_ID
12 | } from './ExecutorManagerView';
13 |
14 | import runAllCodeBlocks from './runAllCodeBlocks';
15 | import { ReleaseNoteModel } from "./ReleaseNoteModal";
16 | import * as runButton from './RunButton';
17 |
18 | export const languageAliases = ["javascript", "typescript", "bash", "csharp", "wolfram", "nb", "wl", "hs", "py", "tex"] as const;
19 | export const canonicalLanguages = ["js", "ts", "cs", "latex", "lean", "lua", "python", "cpp", "prolog", "shell", "groovy", "r",
20 | "go", "rust", "java", "powershell", "kotlin", "mathematica", "haskell", "scala", "swift", "racket", "fsharp", "c", "dart",
21 | "ruby", "batch", "sql", "octave", "maxima", "applescript", "zig", "ocaml", "php"] as const;
22 | export const supportedLanguages = [...languageAliases, ...canonicalLanguages] as const;
23 | export type LanguageId = typeof canonicalLanguages[number];
24 |
25 | export interface PluginContext {
26 | app: App;
27 | settings: ExecutorSettings;
28 | executors: ExecutorContainer;
29 | }
30 |
31 | export default class ExecuteCodePlugin extends Plugin {
32 | settings: ExecutorSettings;
33 | executors: ExecutorContainer;
34 |
35 | /**
36 | * Preparations for the plugin (adding buttons, html elements and event listeners).
37 | */
38 | async onload() {
39 | await this.loadSettings();
40 | this.addSettingTab(new SettingsTab(this.app, this));
41 |
42 | this.executors = new ExecutorContainer(this);
43 |
44 | const context: PluginContext = {
45 | app: this.app,
46 | settings: this.settings,
47 | executors: this.executors,
48 | }
49 | runButton.addInOpenFiles(context);
50 | this.registerMarkdownPostProcessor((element, _context) => {
51 | runButton.addToAllCodeBlocks(element, _context.sourcePath, this.app.workspace.getActiveViewOfType(MarkdownView), context);
52 | });
53 |
54 | // live preview renderers
55 | supportedLanguages.forEach(l => {
56 | console.debug(`Registering renderer for ${l}.`)
57 | this.registerMarkdownCodeBlockProcessor(`run-${l}`, async (src, el, _ctx) => {
58 | await MarkdownRenderer.render(this.app, '```' + l + '\n' + src + (src.endsWith('\n') ? '' : '\n') + '```', el, _ctx.sourcePath, new Component());
59 | });
60 | });
61 |
62 | //executor manager
63 |
64 | this.registerView(
65 | EXECUTOR_MANAGER_VIEW_ID, (leaf) => new ExecutorManagerView(leaf, this.executors)
66 | );
67 | this.addCommand({
68 | id: EXECUTOR_MANAGER_OPEN_VIEW_COMMAND_ID,
69 | name: "Open Code Runtime Management",
70 | callback: () => ExecutorManagerView.activate(this.app.workspace)
71 | });
72 |
73 | this.addCommand({
74 | id: "run-all-code-blocks-in-file",
75 | name: "Run all Code Blocks in Current File",
76 | callback: () => runAllCodeBlocks(this.app.workspace)
77 | })
78 |
79 | if (!this.settings.releaseNote2_1_0wasShowed) {
80 | this.app.workspace.onLayoutReady(() => {
81 | new ReleaseNoteModel(this.app).open();
82 | })
83 |
84 | // Set to true to prevent the release note from showing again
85 | this.settings.releaseNote2_1_0wasShowed = true;
86 | this.saveSettings();
87 | }
88 |
89 | applyLatexBodyClasses(this.app, this.settings);
90 | }
91 |
92 | /**
93 | * Remove all generated html elements (run & clear buttons, output elements) when the plugin is disabled.
94 | */
95 | onunload() {
96 | document
97 | .querySelectorAll("pre > code")
98 | .forEach((codeBlock: HTMLElement) => {
99 | const pre = codeBlock.parentElement as HTMLPreElement;
100 | const parent = pre.parentElement as HTMLDivElement;
101 |
102 | if (parent.hasClass(runButton.codeBlockHasButtonClass)) {
103 | parent.removeClass(runButton.codeBlockHasButtonClass);
104 | }
105 | });
106 |
107 | document
108 | .querySelectorAll("." + runButton.buttonClass)
109 | .forEach((button: HTMLButtonElement) => button.remove());
110 |
111 | document
112 | .querySelectorAll("." + runButton.disabledClass)
113 | .forEach((button: HTMLButtonElement) => button.remove());
114 |
115 | document
116 | .querySelectorAll(".clear-button")
117 | .forEach((button: HTMLButtonElement) => button.remove());
118 |
119 | document
120 | .querySelectorAll(".language-output")
121 | .forEach((out: HTMLElement) => out.remove());
122 |
123 | for (const executor of this.executors) {
124 | executor.stop().then(_ => { /* do nothing */
125 | });
126 | }
127 |
128 | console.log("Unloaded plugin: Execute Code");
129 | }
130 |
131 | /**
132 | * Loads the settings for this plugin from the corresponding save file and stores them in {@link settings}.
133 | */
134 | async loadSettings() {
135 | this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
136 | if (process.platform !== "win32") {
137 | this.settings.wslMode = false;
138 | }
139 | }
140 |
141 | /**
142 | * Saves the settings in {@link settings} to the corresponding save file.
143 | */
144 | async saveSettings() {
145 | await this.saveData(this.settings);
146 | }
147 | }
--------------------------------------------------------------------------------
/src/executors/NonInteractiveCodeExecutor.ts:
--------------------------------------------------------------------------------
1 | import {Notice} from "obsidian";
2 | import * as fs from "fs";
3 | import * as child_process from "child_process";
4 | import Executor from "./Executor";
5 | import {Outputter} from "src/output/Outputter";
6 | import {LanguageId} from "src/main";
7 | import { ExecutorSettings } from "../settings/Settings.js";
8 | import windowsPathToWsl from "../transforms/windowsPathToWsl.js";
9 | import { error } from "console";
10 |
11 | export default class NonInteractiveCodeExecutor extends Executor {
12 | usesShell: boolean
13 | stdoutCb: (chunk: any) => void
14 | stderrCb: (chunk: any) => void
15 | resolveRun: (value: void | PromiseLike) => void | undefined = undefined;
16 | settings: ExecutorSettings;
17 |
18 | constructor(settings: ExecutorSettings, usesShell: boolean, file: string, language: LanguageId) {
19 | super(file, language);
20 |
21 | this.settings = settings;
22 | this.usesShell = usesShell;
23 | }
24 |
25 | stop(): Promise {
26 | return Promise.resolve();
27 | }
28 |
29 | run(codeBlockContent: string, outputter: Outputter, cmd: string, cmdArgs: string, ext: string) {
30 | // Resolve any currently running blocks
31 | if (this.resolveRun !== undefined)
32 | this.resolveRun();
33 | this.resolveRun = undefined;
34 |
35 | return new Promise((resolve, reject) => {
36 | const tempFileName = this.getTempFile(ext);
37 |
38 | fs.promises.writeFile(tempFileName, codeBlockContent).then(() => {
39 | const args = cmdArgs ? cmdArgs.split(" ") : [];
40 |
41 | if (this.isWSLEnabled()) {
42 | args.unshift("-e", cmd);
43 | cmd = "wsl";
44 | args.push(windowsPathToWsl(tempFileName));
45 | } else {
46 | args.push(tempFileName);
47 | }
48 |
49 |
50 | let child: child_process.ChildProcessWithoutNullStreams;
51 |
52 | // check if compiled by gcc
53 | if (cmd.endsWith("gcc") || cmd.endsWith("gcc.exe")) {
54 | // remove .c from tempFileName and add .out for the compiled output and add output path to args
55 | const tempFileNameWExe: string = tempFileName.slice(0, -2) + ".out";
56 | args.push("-o", tempFileNameWExe);
57 |
58 | // compile c file with gcc and handle possible output
59 | const childGCC = child_process.spawn(cmd, args, {env: process.env, shell: this.usesShell});
60 | this.handleChildOutput(childGCC, outputter, tempFileName);
61 | childGCC.on('exit', (code) => {
62 | if (code === 0) {
63 | // executing the compiled file
64 | child = child_process.spawn(tempFileNameWExe, { env: process.env, shell: this.usesShell });
65 | this.handleChildOutput(child, outputter, tempFileNameWExe).then(() => {
66 | this.tempFileId = undefined; // Reset the file id to use a new file next time
67 | });
68 | }
69 | });
70 | } else {
71 | child = child_process.spawn(cmd, args, { env: process.env, shell: this.usesShell });
72 | this.handleChildOutput(child, outputter, tempFileName).then(() => {
73 | this.tempFileId = undefined; // Reset the file id to use a new file next time
74 | });
75 | }
76 |
77 | // We don't resolve the promise here - 'handleChildOutput' registers a listener
78 | // For when the child_process closes, and will resolve the promise there
79 | this.resolveRun = resolve;
80 | }).catch((err) => {
81 | this.notifyError(cmd, cmdArgs, tempFileName, err, outputter);
82 | resolve();
83 | });
84 | });
85 | }
86 |
87 | private isWSLEnabled(): boolean {
88 | if (this.settings.wslMode) {
89 | return true;
90 | }
91 |
92 | if (this.language == 'shell' && this.settings.shellWSLMode) {
93 | return true;
94 | }
95 |
96 | return false;
97 | }
98 |
99 | /**
100 | * Handles the output of a child process and redirects stdout and stderr to the given {@link Outputter} element.
101 | * Removes the temporary file after the code execution. Creates a new Notice after the code execution.
102 | *
103 | * @param child The child process to handle.
104 | * @param outputter The {@link Outputter} that should be used to display the output of the code.
105 | * @param fileName The name of the temporary file that was created for the code execution.
106 | * @returns a promise that will resolve when the child proces finishes
107 | */
108 | protected async handleChildOutput(child: child_process.ChildProcessWithoutNullStreams, outputter: Outputter, fileName: string | undefined) {
109 | outputter.clear();
110 |
111 | // Kill process on clear
112 | outputter.killBlock = () => {
113 | // Kill the process
114 | child.kill('SIGINT');
115 | }
116 |
117 | this.stdoutCb = (data) => {
118 | outputter.write(data.toString());
119 | };
120 | this.stderrCb = (data) => {
121 | outputter.writeErr(data.toString());
122 | };
123 |
124 | child.stdout.on('data', this.stdoutCb);
125 | child.stderr.on('data', this.stderrCb);
126 |
127 | outputter.on("data", (data: string) => {
128 | child.stdin.write(data);
129 | });
130 |
131 | child.on('close', (code) => {
132 | if (code !== 0)
133 | new Notice("Error!");
134 |
135 | // Resolve the run promise once finished running the code block
136 | if (this.resolveRun !== undefined)
137 | this.resolveRun();
138 |
139 | outputter.closeInput();
140 |
141 | if (fileName === undefined) return;
142 |
143 | fs.promises.rm(fileName)
144 | .catch((err) => {
145 | console.error("Error in 'Obsidian Execute Code' Plugin while removing file: " + err);
146 | });
147 | });
148 |
149 | child.on('error', (err) => {
150 | new Notice("Error!");
151 | outputter.writeErr(err.toString());
152 | });
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | /* @settings
2 |
3 | name: Execute Code Settings
4 | id: obsidian-execute-code
5 | settings:
6 | -
7 | id: color-section-title
8 | title: Color Settings
9 | type: heading
10 | level: 3
11 | -
12 | id: use-custom-output-color
13 | title: Custom Code Output Color
14 | description: Use a custom color for the output of code blocks
15 | type: class-toggle
16 | default: false
17 | -
18 | id: code-output-text-color
19 | title: Output Text Color
20 | type: variable-color
21 | format: hex
22 | opacity: false
23 | default: '#FFFFFF'
24 | -
25 | id: use-custom-error-color
26 | title: Custom Code Error Color
27 | description: Use a custom color for the error output of code blocks
28 | type: class-toggle
29 | default: false
30 | -
31 | id: code-error-text-color
32 | title: Error Text Color
33 | type: variable-color
34 | format: hex
35 | opacity: false
36 | default: '#FF0000'
37 | */
38 |
39 | button.run-code-button {
40 | display: none;
41 | color: var(--text-muted);
42 | position: absolute;
43 | bottom: 0;
44 | right: 0;
45 | margin: 5px;
46 | padding: 5px 20px 5px 20px;
47 | z-index: 100;
48 | }
49 |
50 | button.clear-button {
51 | display: none;
52 | color: var(--text-muted);
53 | position: absolute;
54 | bottom: 0;
55 | left: 0;
56 | margin: 5px;
57 | padding: 5px 20px 5px 20px;
58 | z-index: 100;
59 | }
60 |
61 | pre:hover .run-code-button,
62 | pre:hover .clear-button {
63 | display: block;
64 | }
65 |
66 | pre:hover .run-button-disabled,
67 | pre:hover .clear-button-disabled {
68 | display: none;
69 | }
70 |
71 | .run-button-disabled,
72 | .clear-button-disabled {
73 | display: none;
74 | }
75 |
76 | pre:hover code.language-output {
77 | margin-bottom: 28px;
78 | }
79 |
80 | :not(.use-custom-output-color) code.language-output span.stdout {
81 | color: var(--text-muted) !important;
82 | }
83 |
84 | .use-custom-output-color code.language-output span.stdout {
85 | color: var(--code-output-text-color) !important;
86 | }
87 |
88 | :not(.use-custom-error-color) code.language-output span.stderr {
89 | color: red !important;
90 | }
91 |
92 | .use-custom-error-color code.language-output span.stderr {
93 | color: var(--code-error-text-color) !important;
94 | }
95 |
96 | code.language-output hr {
97 | margin: 0 0 1em;
98 | }
99 |
100 | .settings-code-input-box textarea,
101 | .settings-code-input-box input {
102 | min-width: 400px;
103 | min-height: 100px;
104 | font-family: monospace;
105 | resize: vertical;
106 | }
107 |
108 | input.interactive-stdin {
109 | font: inherit;
110 | }
111 |
112 | .manage-executors-view h3 {
113 | margin: 1em;
114 | }
115 |
116 | .manage-executors-view ul {
117 | margin: 1em;
118 | padding: 0;
119 | list-style-type: none;
120 | }
121 |
122 | .manage-executors-view ul li {
123 | padding: 0.5em;
124 | background: var(--background-primary-alt);
125 | border-radius: 4px;
126 | display: grid;
127 | flex-direction: column;
128 | margin-bottom: 0.5em;
129 | }
130 |
131 | .manage-executors-view small {
132 | text-transform: uppercase;
133 | font-weight: bold;
134 | letter-spacing: 0.1ch;
135 | grid-row: 1;
136 | }
137 |
138 | .manage-executors-view .filename {
139 | grid-row: 2;
140 | }
141 |
142 | .manage-executors-view li button {
143 | grid-column: 2;
144 | grid-row: 1 / 3;
145 | margin: 0;
146 | padding: 0.25em;
147 | display: flex;
148 | align-items: center;
149 | justify-content: center;
150 | color: var(--text-muted);
151 | background: none;
152 | }
153 |
154 | .manage-executors-view li button:hover {
155 | background: var(--background-tertiary);
156 | color: var(--icon-color-hover);
157 | }
158 |
159 | .manage-executors-view>div {
160 | position: relative;
161 | }
162 |
163 | .manage-executors-view .empty-state {
164 | color: var(--text-muted);
165 | padding: 0.5em;
166 | }
167 |
168 | .has-run-code-button {
169 | position: relative;
170 | }
171 |
172 | .load-state-indicator {
173 | position: absolute;
174 | top: 0.1em;
175 | left: -2em;
176 | width: 2em;
177 | height: 2em;
178 | background: var(--background-primary-alt);
179 | border-top-left-radius: 4px;
180 | border-bottom-left-radius: 4px;
181 | color: var(--tx1);
182 | transform: translateX(2em);
183 | transition: transform 0.25s, opacity 0.25s;
184 | opacity: 0;
185 | pointer-events: none;
186 | cursor: pointer;
187 | }
188 |
189 | .load-state-indicator svg {
190 | width: 1.5em;
191 | height: 1.5em;
192 | margin: 0.25em;
193 | }
194 |
195 | .load-state-indicator.visible {
196 | transform: translateX(0);
197 | opacity: 1;
198 | pointer-events: all;
199 | }
200 |
201 | .load-state-indicator::before {
202 | content: "";
203 | box-shadow: -1em 0 1em -0.75em inset var(--background-modifier-box-shadow);
204 | position: absolute;
205 | display: block;
206 | width: 100%;
207 | height: 100%;
208 | transform: translateX(-2em);
209 | opacity: 0;
210 | transition: transform 0.25s, opacity 0.25s;
211 | pointer-events: none;
212 | }
213 |
214 | .load-state-indicator.visible::before {
215 | transform: translateX(0);
216 | opacity: 1;
217 | }
218 |
219 | /* Hide code blocks with language-output only in markdown view using "markdown-preview-view"*/
220 | .markdown-preview-view pre.language-output {
221 | display: none;
222 | }
223 |
224 | .markdown-rendered pre.language-output {
225 | display: none;
226 | }
227 |
228 | /* Do not hide code block when exporting to PDF */
229 | @media print {
230 | pre.language-output {
231 | display: block;
232 | }
233 |
234 | /* Hide code blocks with language-output only in markdown view using "markdown-preview-view"*/
235 | .markdown-preview-view pre.language-output {
236 | display: block;
237 | }
238 |
239 | .markdown-rendered pre.language-output {
240 | display: block;
241 | }
242 | }
243 |
244 | /* Center LaTeX vector graphics, confine to text width */
245 | .center-latex-figures img[src*="/figure%20"][src$=".svg"],
246 | .center-latex-figures img[src*="/figure%20"][src*=".svg?"],
247 | .center-latex-figures .stdout img[src*=".svg?"] {
248 | display: block;
249 | margin: auto;
250 | max-width: 100%;
251 | }
252 |
253 | /* Invert LaTeX vector graphics in dark mode */
254 | .theme-dark.invert-latex-figures img[src*="/figure%20"][src$=".svg"],
255 | .theme-dark.invert-latex-figures img[src*="/figure%20"][src*=".svg?"],
256 | .theme-dark.invert-latex-figures .stdout img[src*=".svg?"] {
257 | filter: invert(1);
258 | }
259 |
260 | /* Allow descriptions in LaTeX settings to be selected and copied. */
261 | .selectable-description-text {
262 | -moz-user-select: text;
263 | -khtml-user-select: text;
264 | -webkit-user-select: text;
265 | -ms-user-select: text;
266 | user-select: text;
267 | }
268 |
269 | .insert-figure-icon {
270 | margin-left: 0.5em;
271 | }
272 |
273 | /* Try to keep description of cmd arguments in LaTeX settings on the same line. */
274 | code.selectable-description-text {
275 | white-space: nowrap;
276 | }
--------------------------------------------------------------------------------
/src/transforms/CodeInjector.ts:
--------------------------------------------------------------------------------
1 | import type {App} from "obsidian";
2 | import {MarkdownView, Notice} from "obsidian";
3 | import {ExecutorSettings} from "src/settings/Settings";
4 | import {getCodeBlockLanguage, getLanguageAlias, transformMagicCommands} from './TransformCode';
5 | import {getArgs} from "src/CodeBlockArgs";
6 | import type {LanguageId} from "src/main";
7 | import type {CodeBlockArgs} from '../CodeBlockArgs';
8 |
9 | /**
10 | * Inject code and run code transformations on a source code block
11 | */
12 | export class CodeInjector {
13 | private readonly app: App;
14 | private readonly settings: ExecutorSettings;
15 | private readonly language: LanguageId;
16 |
17 | private prependSrcCode = "";
18 | private appendSrcCode = "";
19 | private namedImportSrcCode = "";
20 |
21 | private mainArgs: CodeBlockArgs = {};
22 |
23 | private namedExports: Record = {};
24 |
25 | /**
26 | * @param app The current app handle (this.app from ExecuteCodePlugin).
27 | * @param settings The current app settings.
28 | * @param language The language of the code block e.g. python, js, cpp.
29 | */
30 | constructor(app: App, settings: ExecutorSettings, language: LanguageId) {
31 | this.app = app;
32 | this.settings = settings;
33 | this.language = language;
34 | }
35 |
36 | /**
37 | * Takes the source code of a code block and adds all relevant pre-/post-blocks and global code injections.
38 | *
39 | * @param srcCode The source code of the code block.
40 | * @returns The source code of a code block with all relevant pre/post blocks and global code injections.
41 | */
42 | public async injectCode(srcCode: string) {
43 | const language = getLanguageAlias(this.language);
44 |
45 | // We need to get access to all code blocks on the page so we can grab the pre / post blocks above
46 | // Obsidian unloads code blocks not in view, so instead we load the raw document file and traverse line-by-line
47 | const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
48 | if (activeView === null)
49 | return srcCode;
50 |
51 | // Is await necessary here? Some object variables get changed in this call -> await probably necessary
52 | await this.parseFile(activeView.data, srcCode, language);
53 |
54 | const realLanguage = /[^-]*$/.exec(language)[0];
55 | const globalInject = this.settings[`${realLanguage}Inject` as keyof ExecutorSettings];
56 | let injectedCode = `${this.namedImportSrcCode}\n${srcCode}`;
57 | if (!this.mainArgs.ignore)
58 | injectedCode = `${globalInject}\n${this.prependSrcCode}\n${injectedCode}\n${this.appendSrcCode}`;
59 | else {
60 | // Handle single ignore
61 | if (!Array.isArray(this.mainArgs.ignore) && this.mainArgs.ignore !== "all")
62 | this.mainArgs.ignore = [this.mainArgs.ignore];
63 | if (this.mainArgs.ignore !== "all") {
64 | if (!this.mainArgs.ignore.contains("pre"))
65 | injectedCode = `${this.prependSrcCode}\n${injectedCode}`;
66 | if (!this.mainArgs.ignore.contains("post"))
67 | injectedCode = `${injectedCode}\n${this.appendSrcCode}`;
68 | if (!this.mainArgs.ignore.contains("global"))
69 | injectedCode = `${globalInject}\n${injectedCode}`;
70 | }
71 | }
72 | return transformMagicCommands(this.app, injectedCode);
73 | }
74 |
75 | /**
76 | * Handles adding named imports to code blocks
77 | *
78 | * @param namedImports Populate prependable source code with named imports
79 | * @returns If an error occurred
80 | */
81 | private async handleNamedImports(namedImports: CodeBlockArgs['import']) {
82 | const handleNamedImport = (namedImport: string) => {
83 | // Named export doesn't exist
84 | if (!this.namedExports.hasOwnProperty(namedImport)) {
85 | new Notice(`Named export "${namedImport}" does not exist but was imported`);
86 | return true;
87 | }
88 | this.namedImportSrcCode += `${this.disable_print(this.namedExports[namedImport])}\n`;
89 | return false;
90 | };
91 | // Single import
92 | if (!Array.isArray(namedImports))
93 | return handleNamedImport(namedImports);
94 | // Multiple imports
95 | for (const namedImport of namedImports) {
96 | const err = handleNamedImport(namedImport);
97 | if (err) return true;
98 | }
99 | return false;
100 | }
101 |
102 | /**
103 | * Parse a markdown file
104 | *
105 | * @param fileContents The contents of the file to parse
106 | * @param srcCode The original source code of the code block being run
107 | * @param language The programming language of the code block being run
108 | * @returns
109 | */
110 | private async parseFile(fileContents: string, srcCode: string, language: LanguageId) {
111 | let currentArgs: CodeBlockArgs = {};
112 | let insideCodeBlock = false;
113 | let isLanguageEqual = false;
114 | let currentLanguage = "";
115 | let currentCode = "";
116 | let currentFirstLine = "";
117 |
118 | for (const line of fileContents.split("\n")) {
119 | if (line.startsWith("```")) {
120 | // Reached end of code block
121 | if (insideCodeBlock) {
122 | // Stop traversal once we've reached the code block being run
123 | // Only do this for the original file the user is running
124 | const srcCodeTrimmed = srcCode.trim();
125 | const currentCodeTrimmed = currentCode.trim();
126 | if (isLanguageEqual && srcCodeTrimmed.length === currentCodeTrimmed.length && srcCodeTrimmed === currentCodeTrimmed) {
127 | this.mainArgs = getArgs(currentFirstLine);
128 | // Get named imports
129 | if (this.mainArgs.import) {
130 | const err = this.handleNamedImports(this.mainArgs.import);
131 | if (err) return "";
132 | }
133 | break;
134 | }
135 | // Named export
136 | if (currentArgs.label) {
137 | // Export already exists
138 | if (this.namedExports.hasOwnProperty(currentArgs.label)) {
139 | new Notice(`Error: named export ${currentArgs.label} exported more than once`);
140 | return "";
141 | }
142 | this.namedExports[currentArgs.label] = currentCode;
143 | }
144 | // Pre / post export
145 | if (!Array.isArray(currentArgs.export))
146 | currentArgs.export = [currentArgs.export];
147 | if (currentArgs.export.contains("pre"))
148 | this.prependSrcCode += `${this.disable_print(currentCode)}\n`;
149 | if (currentArgs.export.contains("post"))
150 | this.appendSrcCode += `${this.disable_print(currentCode)}\n`;
151 | currentLanguage = "";
152 | currentCode = "";
153 | insideCodeBlock = false;
154 | currentArgs = {};
155 | }
156 |
157 | // reached start of code block
158 | else {
159 | currentLanguage = getCodeBlockLanguage(line);
160 | // Don't check code blocks from a different language
161 | isLanguageEqual = /[^-]*$/.exec(language)[0] === /[^-]*$/.exec(currentLanguage)[0];
162 | if (isLanguageEqual) {
163 | currentArgs = getArgs(line);
164 | currentFirstLine = line;
165 | }
166 | insideCodeBlock = true;
167 | }
168 | } else if (insideCodeBlock && isLanguageEqual) {
169 | currentCode += `${line}\n`;
170 | }
171 | }
172 | }
173 |
174 | private disable_print(code: String): String {
175 | if (!this.settings.onlyCurrentBlock) {
176 | return code;
177 | }
178 | const pattern: RegExp = /^print\s*(.*)/gm;
179 | // 使用正则表达式替换函数将符合条件的内容注释掉
180 | return code.replace(pattern, ' ');
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/src/output/LatexInserter.ts:
--------------------------------------------------------------------------------
1 | import { App, setIcon, TFile, Vault } from "obsidian";
2 | import { Outputter } from "./Outputter";
3 | import { settingsInstance } from "src/transforms/LatexTransformer";
4 | import { FIGURE_FILENAME_EXTENSIONS, TEMP_FIGURE_NAME } from 'src/transforms/LatexFigureName';
5 | import { generalizeFigureTitle } from 'src/transforms/LatexFigureName';
6 | import * as r from "./RegExpUtilities";
7 | import * as path from "path";
8 |
9 | const LINK_ALIAS = /\|[^\]]*/;
10 | const ANY_WIKILINK_EMBEDDING = r.concat(/!\[\[.*?/, FIGURE_FILENAME_EXTENSIONS, r.optional(LINK_ALIAS), /\]\]/);
11 | const ANY_MARKDOWN_EMBEDDING = r.concat(/!\[.*?\]\(.*?/, FIGURE_FILENAME_EXTENSIONS, /\)/);
12 | const ANY_FIGURE_EMBEDDING: RegExp = r.alternate(ANY_WIKILINK_EMBEDDING, ANY_MARKDOWN_EMBEDDING);
13 |
14 | const SAFE_ANY: RegExp = /([^`]|`[^`]|``[^`])*?/; // Match any text, that does not cross the ``` boundary of code blocks
15 | const EMPTY_LINES: RegExp = /[\s\n]*/;
16 |
17 | interface FigureContext {
18 | app: App;
19 | figureName: string;
20 | link: () => string; // evaluates at button click, to let Obsidian index the file
21 | file: TFile;
22 | }
23 |
24 | /** Forces an image to reload by appending a cache-busting timestamp to its URL */
25 | export function updateImage(image: HTMLImageElement) {
26 | const baseUrl = image.src.split('?')[0];
27 | image.src = `${baseUrl}?cache=${Date.now()}`;
28 | }
29 |
30 | /**
31 | * Adds an obsidian link and clickable insertion icons to the output.
32 | * @param figureName - The name of the figure file with extension that was saved
33 | * @param figurePath - The path where the figure was saved
34 | * @param outputter - The Outputter instance used to write content
35 | */
36 | export async function writeFileLink(figureName: string, figurePath: string, outputter: Outputter): Promise {
37 | await outputter.writeMarkdown(`Saved [[${figureName}]]`);
38 |
39 | const isTempFigure = TEMP_FIGURE_NAME.test(figureName);
40 | if (isTempFigure) return outputter.write('\n');
41 |
42 | const file: TFile | null = outputter.app.vault.getFileByPath(outputter.srcFile);
43 | if (!file) throw new Error(`File not found: ${outputter.srcFile}`);
44 |
45 | const link = () => createObsidianLink(outputter.app, figurePath, outputter.srcFile);
46 | const figure: FigureContext = { app: outputter.app, figureName: figureName, link: link, file: file };
47 | const buttonClass = 'insert-figure-icon';
48 |
49 | const insertAbove: HTMLAnchorElement = outputter.writeIcon('image-up', 'Click to embed file above codeblock.\nCtrl + Click to replace previous embedding.', buttonClass);
50 | insertAbove.addEventListener('click', (event: MouseEvent) => insertEmbedding('above', event.ctrlKey, figure));
51 |
52 | const insertBelow: HTMLAnchorElement = outputter.writeIcon('image-down', 'Click to embed file below codeblock.\nCtrl + Click to replace next embedding.', buttonClass);
53 | insertBelow.addEventListener('click', (event: MouseEvent) => insertEmbedding('below', event.ctrlKey, figure));
54 |
55 | const copyLink: HTMLAnchorElement = outputter.writeIcon('copy', 'Copy the markdown link.', buttonClass);
56 | copyLink.addEventListener('click', () => navigator.clipboard.writeText(link()));
57 |
58 | outputter.write('\n');
59 | }
60 |
61 | /** * Inserts an embedded link to the figure above or below the current code blocks. */
62 | async function insertEmbedding(pastePosition: 'above' | 'below', doReplace: boolean, figure: FigureContext): Promise {
63 | try {
64 | const vault = figure.app.vault;
65 | const content: string = await vault.read(figure.file);
66 |
67 | const identifierSrc: string = settingsInstance.latexFigureTitlePattern
68 | .replace(/\(\?[^)]*\)/, generalizeFigureTitle(figure.figureName).source);
69 | const identifier: RegExp = r.parse(identifierSrc);
70 | if (!identifier) return;
71 |
72 | const codeBlocks: RegExpMatchArray[] = findMatchingCodeBlocks(content, /(la)?tex/, identifier, figure.link(), doReplace);
73 | if (codeBlocks.length === 0) return false;
74 |
75 | codeBlocks.forEach(async (block: RegExpExecArray) => {
76 | await insertAtCodeBlock(block, pastePosition, figure);
77 | });
78 | return true;
79 | } catch (error) {
80 | console.error('Error inserting embedding:', error);
81 | throw error;
82 | }
83 | }
84 |
85 | /** Locates LaTeX code blocks containing the specified figure identifier and their surrounding embeddings */
86 | function findMatchingCodeBlocks(content: string, language: RegExp, identifier: RegExp, link: string, doReplace?: boolean): RegExpMatchArray[] {
87 | const alreadyLinked: RegExp = r.group(r.escape(link));
88 | const codeblock: RegExp = r.concat(
89 | /```(run-)?/, r.group(language), /[\s\n]/,
90 | SAFE_ANY, r.group(identifier), SAFE_ANY,
91 | /```/);
92 |
93 | const previous: RegExp = r.capture(r.concat(ANY_FIGURE_EMBEDDING, EMPTY_LINES), 'replacePrevious');
94 | const above: RegExp = r.capture(r.concat(alreadyLinked, EMPTY_LINES), 'alreadyAbove');
95 |
96 | const below: RegExp = r.capture(r.concat(EMPTY_LINES, alreadyLinked), 'alreadyBelow');
97 | const next: RegExp = r.capture(r.concat(EMPTY_LINES, ANY_FIGURE_EMBEDDING), 'replaceNext');
98 |
99 | const blocksWithEmbeds: RegExp = new RegExp(r.concat(
100 | (doReplace) ? r.optional(previous) : null,
101 | r.optional(above),
102 | r.capture(codeblock, 'codeblock'),
103 | r.optional(below),
104 | (doReplace) ? r.optional(next) : null,
105 | ), 'g');
106 |
107 | const matches: RegExpMatchArray[] = Array.from(content.matchAll(blocksWithEmbeds));
108 | console.debug(`Searching markdown for`, blocksWithEmbeds, `resulted in `, matches.length, `codeblock(s)`, matches.map(match => match.groups));
109 | return matches;
110 | }
111 |
112 | /** Updates markdown source file to insert or replace a figure embedding relative to a code block */
113 | async function insertAtCodeBlock(block: RegExpExecArray, pastePosition: 'above' | 'below', figure: FigureContext): Promise {
114 | const vault = figure.app.vault;
115 | const groups = block.groups;
116 | if (!groups || !groups.codeblock) return;
117 |
118 | const canReplace: Boolean = (pastePosition === 'above')
119 | ? groups.replacePrevious?.length > 0
120 | : groups.replaceNext?.length > 0;
121 |
122 | const isAlreadyEmbedded: boolean = (pastePosition === 'above')
123 | ? groups.alreadyAbove?.length > 0
124 | : groups.alreadyBelow?.length > 0;
125 | if (isAlreadyEmbedded && !canReplace) return;
126 |
127 | const newText: string = (pastePosition === 'above')
128 | ? figure.link() + '\n\n' + groups.codeblock
129 | : groups.codeblock + '\n\n' + figure.link();
130 |
131 | if (!canReplace) {
132 | await vault.process(figure.file, data => data.replace(groups.codeblock, newText));
133 | return;
134 | }
135 |
136 | const oldTexts: string[] = (pastePosition === 'above')
137 | ? [groups.replacePrevious, groups.alreadyAbove, groups.codeblock]
138 | : [groups.codeblock, groups.alreadyBelow, groups.replaceNext];
139 | const oldCombined = oldTexts.filter(Boolean).join('');
140 | await vault.process(figure.file, data => data.replace(oldCombined, newText));
141 | }
142 |
143 | /** Let Obsidian generate a link adhering to preferences */
144 | export function createObsidianLink(app: App, filePath: string, sourcePath: string, subpath?: string, alias?: string): string {
145 | const relative = getPathRelativeToVault(filePath);
146 | try {
147 | const file: TFile | null = app.vault.getFileByPath(relative);
148 | return app.fileManager.generateMarkdownLink(file, sourcePath, subpath, alias);
149 | } catch (error) {
150 | console.error(`File not found: ${relative}`);
151 | return '![[' + path.basename(filePath) + ']]';
152 | }
153 |
154 | }
155 |
156 | function getPathRelativeToVault(absolutePath: string) {
157 | const vaultPath = (this.app.vault.adapter as any).basePath;
158 | absolutePath = path.normalize(absolutePath);
159 |
160 | if (!absolutePath.startsWith(vaultPath)) return absolutePath;
161 | return absolutePath.slice(vaultPath.length)
162 | .replace(/^[\\\/]/, '')
163 | .replace(/\\/g, '/')
164 | .replace(/['"`]/, '')
165 | .trim();
166 | }
--------------------------------------------------------------------------------
/src/output/FileAppender.ts:
--------------------------------------------------------------------------------
1 | import { EditorPosition, EditorRange, MarkdownView } from "obsidian";
2 |
3 | export default class FileAppender {
4 | view: MarkdownView;
5 | codeBlockElement: HTMLPreElement
6 | codeBlockRange: EditorRange
7 | outputPosition: EditorPosition;
8 |
9 | public constructor(view: MarkdownView, blockElem: HTMLPreElement) {
10 | this.view = view;
11 |
12 | this.codeBlockElement = blockElem;
13 |
14 | try {
15 | this.codeBlockRange = this.getRangeOfCodeBlock(blockElem);
16 | } catch (e) {
17 | console.error("Error finding code block range: Probably because of 'run-' prefix");
18 | this.codeBlockRange = null
19 | }
20 | }
21 |
22 | public clearOutput() {
23 | if (this.codeBlockRange && this.outputPosition) {
24 |
25 | const editor = this.view.editor;
26 |
27 | //Offset this.outputPosition by "\n```"
28 | const afterEndOfOutputCodeBlock: EditorPosition = {
29 | line: this.outputPosition.line + 1,
30 | ch: "```".length + 1
31 | };
32 |
33 | editor.replaceRange("", this.codeBlockRange.to, afterEndOfOutputCodeBlock);
34 | this.view.setViewData(editor.getValue(), false);
35 |
36 | this.outputPosition = null;
37 | }
38 | }
39 |
40 | public addOutput(output: string) {
41 | try {
42 | this.findOutputTarget();
43 | } catch (e) {
44 | console.error("Error finding output target: Probably because of 'run-' prefix");
45 | this.view.setViewData(this.view.editor.getValue(), false);
46 | return;
47 | }
48 |
49 | const editor = this.view.editor;
50 |
51 | editor.replaceRange(output, this.outputPosition);
52 |
53 | const lines = output.split("\n");
54 | this.outputPosition = {
55 | line: this.outputPosition.line + (lines.length - 1), //if the addition is only 1 line, don't change current line pos
56 | ch: (lines.length == 1 ? //if the addition is only 1 line, then offset from the existing position.
57 | this.outputPosition.ch : 0 //If it's not, ignore it.
58 | ) + lines[lines.length - 1].length
59 | }
60 |
61 | this.view.setViewData(this.view.editor.getValue(), false);
62 | }
63 |
64 | /**
65 | * Finds where output should be appended to and sets the `outputPosition` property to reflect it.
66 | * @param addIfNotExist Add an `output` code block if one doesn't exist already
67 | */
68 | findOutputTarget(addIfNotExist = true) {
69 | const editor = this.view.editor;
70 |
71 | const EXPECTED_SUFFIX = "\n```output\n";
72 |
73 | const sigilEndIndex = editor.posToOffset(this.codeBlockRange.to) + EXPECTED_SUFFIX.length;
74 |
75 | const outputBlockSigilRange: EditorRange = {
76 | from: this.codeBlockRange.to,
77 | to: {
78 | ch: 0, //since the suffix ends with a newline, it'll be column 0
79 | line: this.codeBlockRange.to.line + 2 // the suffix adds 2 lines
80 | }
81 | }
82 |
83 | const hasOutput = editor.getRange(outputBlockSigilRange.from, outputBlockSigilRange.to) == EXPECTED_SUFFIX;
84 |
85 | if (hasOutput) {
86 | //find the first code block end that occurs after the ```output sigil
87 | const index = editor.getValue().indexOf("\n```\n", sigilEndIndex);
88 |
89 | //bail out if we didn't find an end
90 | if(index == -1) {
91 | this.outputPosition = outputBlockSigilRange.to;
92 | } else {
93 | //subtract 1 so output appears before the newline
94 | this.outputPosition = editor.offsetToPos(index - 1);
95 | }
96 | } else if (addIfNotExist) {
97 | editor.replaceRange(EXPECTED_SUFFIX + "```\n", this.codeBlockRange.to);
98 | this.view.data = this.view.editor.getValue();
99 | //We need to recalculate the outputPosition because the insertion will've changed the lines.
100 | //The expected suffix ends with a newline, so the column will always be 0;
101 | //the row will be the current row + 2: the suffix adds 2 lines
102 | this.outputPosition = {
103 | ch: 0,
104 | line: this.codeBlockRange.to.line + 2
105 | };
106 |
107 | } else {
108 | this.outputPosition = outputBlockSigilRange.to;
109 | }
110 | }
111 |
112 | /**
113 | * With a starting line, ending line, and number of codeblocks in-between those, find the exact EditorRange of a code block.
114 | *
115 | * @param startLine The line to start searching at
116 | * @param endLine The line to end searching AFTER (i.e. it is inclusive)
117 | * @param searchBlockIndex The index of code block, within the startLine-endLine range, to search for
118 | * @returns an EditorRange representing the range occupied by the given block, or null if it couldn't be found
119 | */
120 | findExactCodeBlockRange(startLine: number, endLine: number, searchBlockIndex: number): EditorRange | null {
121 | const editor = this.view.editor;
122 | const textContent = editor.getValue();
123 |
124 | const startIndex = editor.posToOffset({ ch: 0, line: startLine });
125 | const endIndex = editor.posToOffset({ ch: 0, line: endLine + 1 });
126 |
127 | //Start the parsing with a given amount of padding.
128 | //This helps us if the section begins directly with "```".
129 | //At the end, it iterates through the padding again.
130 | const PADDING = "\n\n\n\n\n";
131 |
132 |
133 | /*
134 | escaped: whether we are currently in an escape character
135 | inBlock: whether we are currently inside a code block
136 | last5: a rolling buffer of the last 5 characters.
137 | It could technically work with 4, but it's easier to do 5
138 | and it leaves open future advanced parsing.
139 | blockStart: the start of the last code block we entered
140 |
141 | */
142 | let escaped, inBlock, blockI = 0, last5 = PADDING, blockStart
143 | for (let i = startIndex; i < endIndex + PADDING.length; i++) {
144 | const char = i < endIndex ? textContent[i] : PADDING[0];
145 |
146 | last5 = last5.substring(1) + char;
147 | if (escaped) {
148 | escaped = false;
149 | continue;
150 | }
151 | if (char == "\\") {
152 | escaped = true;
153 | continue;
154 | }
155 | if (last5.substring(0, 4) == "\n```") {
156 | inBlock = !inBlock;
157 | //If we are entering a block, set the block start
158 | if (inBlock) {
159 | blockStart = i - 4;
160 | } else {
161 | //if we're leaving a block, check if its index is the searched index
162 | if (blockI == searchBlockIndex) {
163 | return {
164 | from: this.view.editor.offsetToPos(blockStart),
165 | to: this.view.editor.offsetToPos(i)
166 | }
167 | } else {// if it isn't, just increase the block index
168 | blockI++;
169 | }
170 | }
171 | }
172 | }
173 | return null;
174 | }
175 |
176 | /**
177 | * Uses an undocumented API to find the EditorRange that corresponds to a given codeblock's element.
178 | * Returns null if it wasn't able to find the range.
179 | * @param codeBlock element of the desired code block
180 | * @returns the corresponding EditorRange, or null
181 | */
182 | getRangeOfCodeBlock(codeBlock: HTMLPreElement): EditorRange | null {
183 | const parent = codeBlock.parentElement;
184 | const index = Array.from(parent.children).indexOf(codeBlock);
185 |
186 | //@ts-ignore
187 | const section: null | { lineStart: number, lineEnd: number } = this.view.previewMode.renderer.sections.find(x => x.el == parent);
188 |
189 | if (section) {
190 | return this.findExactCodeBlockRange(section.lineStart, section.lineEnd, index);
191 | } else {
192 | return null;
193 | }
194 | }
195 | }
--------------------------------------------------------------------------------
/src/settings/SettingsTab.ts:
--------------------------------------------------------------------------------
1 | import { App, PluginSettingTab, Setting } from "obsidian";
2 | import ExecuteCodePlugin, { canonicalLanguages, LanguageId } from "src/main";
3 | import { DISPLAY_NAMES } from "./languageDisplayName";
4 | import makeCppSettings from "./per-lang/makeCppSettings";
5 | import makeCSettings from "./per-lang/makeCSettings.js";
6 | import makeCsSettings from "./per-lang/makeCsSettings";
7 | import makeFSharpSettings from "./per-lang/makeFSharpSettings";
8 | import makeGoSettings from "./per-lang/makeGoSettings";
9 | import makeGroovySettings from "./per-lang/makeGroovySettings";
10 | import makeHaskellSettings from "./per-lang/makeHaskellSettings";
11 | import makeJavaSettings from "./per-lang/makeJavaSettings";
12 | import makeJsSettings from "./per-lang/makeJsSettings";
13 | import makeKotlinSettings from "./per-lang/makeKotlinSettings";
14 | import makeLatexSettings from "./per-lang/makeLatexSettings";
15 | import makeLeanSettings from "./per-lang/makeLeanSettings";
16 | import makeLuaSettings from "./per-lang/makeLuaSettings";
17 | import makeDartSettings from "./per-lang/makeDartSettings";
18 | import makeMathematicaSettings from "./per-lang/makeMathematicaSettings";
19 | import makePhpSettings from "./per-lang/makePhpSettings";
20 | import makePowershellSettings from "./per-lang/makePowershellSettings";
21 | import makePrologSettings from "./per-lang/makePrologSettings";
22 | import makePythonSettings from "./per-lang/makePythonSettings";
23 | import makeRSettings from "./per-lang/makeRSettings";
24 | import makeRubySettings from "./per-lang/makeRubySettings";
25 | import makeRustSettings from "./per-lang/makeRustSettings";
26 | import makeScalaSettings from "./per-lang/makeScalaSettings.js";
27 | import makeRacketSettings from "./per-lang/makeRacketSettings.js";
28 | import makeShellSettings from "./per-lang/makeShellSettings";
29 | import makeBatchSettings from "./per-lang/makeBatchSettings";
30 | import makeTsSettings from "./per-lang/makeTsSettings";
31 | import { ExecutorSettings } from "./Settings";
32 | import makeSQLSettings from "./per-lang/makeSQLSettings";
33 | import makeOctaviaSettings from "./per-lang/makeOctaveSettings";
34 | import makeMaximaSettings from "./per-lang/makeMaximaSettings";
35 | import makeApplescriptSettings from "./per-lang/makeApplescriptSettings";
36 | import makeZigSettings from "./per-lang/makeZigSettings";
37 | import makeOCamlSettings from "./per-lang/makeOCamlSettings";
38 | import makeSwiftSettings from "./per-lang/makeSwiftSettings";
39 |
40 |
41 | /**
42 | * This class is responsible for creating a settings tab in the settings menu. The settings tab is showed in the
43 | * regular obsidian settings menu.
44 | *
45 | * The {@link display} functions build the html page that is showed in the settings.
46 | */
47 | export class SettingsTab extends PluginSettingTab {
48 | plugin: ExecuteCodePlugin;
49 |
50 | languageContainers: Partial>;
51 | activeLanguageContainer: HTMLDivElement | undefined;
52 |
53 | constructor(app: App, plugin: ExecuteCodePlugin) {
54 | super(app, plugin);
55 | this.plugin = plugin;
56 |
57 | this.languageContainers = {}
58 | }
59 |
60 | /**
61 | * Builds the html page that is showed in the settings.
62 | */
63 | display() {
64 | const { containerEl } = this;
65 | containerEl.empty();
66 |
67 | containerEl.createEl('h2', { text: 'Settings for the Code Execution Plugin.' });
68 |
69 |
70 | // ========== General ==========
71 | containerEl.createEl('h3', { text: 'General Settings' });
72 | new Setting(containerEl)
73 | .setName('Timeout (in seconds)')
74 | .setDesc('The time after which a program gets shut down automatically. This is to prevent infinite loops. ')
75 | .addText(text => text
76 | .setValue("" + this.plugin.settings.timeout / 1000)
77 | .onChange(async (value) => {
78 | if (Number(value) * 1000) {
79 | console.log('Timeout set to: ' + value);
80 | this.plugin.settings.timeout = Number(value) * 1000;
81 | }
82 | await this.plugin.saveSettings();
83 | }));
84 |
85 | new Setting(containerEl)
86 | .setName('Allow Input')
87 | .setDesc('Whether or not to include a stdin input box when running blocks. In order to apply changes to this, Obsidian must be refreshed. ')
88 | .addToggle(text => text
89 | .setValue(this.plugin.settings.allowInput)
90 | .onChange(async (value) => {
91 | console.log('Allow Input set to: ' + value);
92 | this.plugin.settings.allowInput = value
93 | await this.plugin.saveSettings();
94 | }));
95 |
96 | if (process.platform === "win32") {
97 | new Setting(containerEl)
98 | .setName('WSL Mode')
99 | .setDesc("Whether or not to run code in the Windows Subsystem for Linux. If you don't have WSL installed, don't turn this on!")
100 | .addToggle(text => text
101 | .setValue(this.plugin.settings.wslMode)
102 | .onChange(async (value) => {
103 | console.log('WSL Mode set to: ' + value);
104 | this.plugin.settings.wslMode = value
105 | await this.plugin.saveSettings();
106 | }));
107 | }
108 |
109 | new Setting(containerEl)
110 | .setName('[Experimental] Persistent Output')
111 | .setDesc('If enabled, the output of the code block is written into the markdown file. This feature is ' +
112 | 'experimental and may not work as expected.')
113 | .addToggle(text => text
114 | .setValue(this.plugin.settings.persistentOuput)
115 | .onChange(async (value) => {
116 | console.log('Allow Input set to: ' + value);
117 | this.plugin.settings.persistentOuput = value
118 | await this.plugin.saveSettings();
119 | }));
120 |
121 | // TODO setting per language that requires main function if main function should be implicitly made or not, if not, non-main blocks will not have a run button
122 |
123 | containerEl.createEl("hr");
124 |
125 | new Setting(containerEl)
126 | .setName("Language-Specific Settings")
127 | .setDesc("Pick a language to edit its language-specific settings")
128 | .addDropdown((dropdown) => dropdown
129 | .addOptions(Object.fromEntries(
130 | canonicalLanguages.map(lang => [lang, DISPLAY_NAMES[lang]])
131 | ))
132 | .setValue(this.plugin.settings.lastOpenLanguageTab || canonicalLanguages[0])
133 | .onChange(async (value: LanguageId) => {
134 | this.focusContainer(value);
135 | this.plugin.settings.lastOpenLanguageTab = value;
136 | await this.plugin.saveSettings();
137 | })
138 | )
139 | .settingEl.style.borderTop = "0";
140 |
141 | makeJsSettings(this, this.makeContainerFor("js")); // JavaScript / Node
142 | makeTsSettings(this, this.makeContainerFor("ts")); // TypeScript
143 | makeLeanSettings(this, this.makeContainerFor("lean"));
144 | makeLuaSettings(this, this.makeContainerFor("lua"));
145 | makeDartSettings(this, this.makeContainerFor("dart"));
146 | makeCsSettings(this, this.makeContainerFor("cs")); // CSharp
147 | makeJavaSettings(this, this.makeContainerFor("java"));
148 | makePythonSettings(this, this.makeContainerFor("python"));
149 | makeGoSettings(this, this.makeContainerFor("go")); // Golang
150 | makeRustSettings(this, this.makeContainerFor("rust"));
151 | makeCppSettings(this, this.makeContainerFor("cpp")); // C++
152 | makeCSettings(this, this.makeContainerFor("c"));
153 | makeBatchSettings(this, this.makeContainerFor("batch"));
154 | makeShellSettings(this, this.makeContainerFor("shell"));
155 | makePowershellSettings(this, this.makeContainerFor("powershell"));
156 | makePrologSettings(this, this.makeContainerFor("prolog"));
157 | makeGroovySettings(this, this.makeContainerFor("groovy"));
158 | makeRSettings(this, this.makeContainerFor("r"));
159 | makeKotlinSettings(this, this.makeContainerFor("kotlin"));
160 | makeMathematicaSettings(this, this.makeContainerFor("mathematica"));
161 | makeHaskellSettings(this, this.makeContainerFor("haskell"));
162 | makeScalaSettings(this, this.makeContainerFor("scala"));
163 | makeSwiftSettings(this, this.makeContainerFor("swift"));
164 | makeRacketSettings(this, this.makeContainerFor("racket"));
165 | makeFSharpSettings(this, this.makeContainerFor("fsharp"));
166 | makeRubySettings(this, this.makeContainerFor("ruby"));
167 | makeSQLSettings(this, this.makeContainerFor("sql"));
168 | makeOctaviaSettings(this, this.makeContainerFor("octave"));
169 | makeMaximaSettings(this, this.makeContainerFor("maxima"));
170 | makeApplescriptSettings(this, this.makeContainerFor("applescript"));
171 | makeZigSettings(this, this.makeContainerFor("zig"));
172 | makeOCamlSettings(this, this.makeContainerFor("ocaml"));
173 | makePhpSettings(this, this.makeContainerFor("php"));
174 | makeLatexSettings(this, this.makeContainerFor("latex"));
175 |
176 | this.focusContainer(this.plugin.settings.lastOpenLanguageTab || canonicalLanguages[0]);
177 | }
178 |
179 | private makeContainerFor(language: LanguageId) {
180 | const container = this.containerEl.createDiv();
181 |
182 | container.style.display = "none";
183 |
184 | this.languageContainers[language] = container;
185 |
186 | return container;
187 | }
188 |
189 | private focusContainer(language: LanguageId) {
190 | if (this.activeLanguageContainer)
191 | this.activeLanguageContainer.style.display = "none";
192 |
193 | if (language in this.languageContainers) {
194 | this.activeLanguageContainer = this.languageContainers[language];
195 | this.activeLanguageContainer.style.display = "block";
196 | }
197 | }
198 |
199 | sanitizePath(path: string): string {
200 | path = path.replace(/\\/g, '/');
201 | path = path.replace(/['"`]/, '');
202 | path = path.trim();
203 |
204 | return path
205 | }
206 |
207 | makeInjectSetting(containerEl: HTMLElement, language: LanguageId) {
208 | const languageAlt = DISPLAY_NAMES[language];
209 |
210 | new Setting(containerEl)
211 | .setName(`Inject ${languageAlt} code`)
212 | .setDesc(`Code to add to the top of every ${languageAlt} code block before running.`)
213 | .setClass('settings-code-input-box')
214 | .addTextArea(textarea => {
215 | // @ts-ignore
216 | const val = this.plugin.settings[`${language}Inject` as keyof ExecutorSettings as string]
217 | return textarea
218 | .setValue(val)
219 | .onChange(async (value) => {
220 | (this.plugin.settings[`${language}Inject` as keyof ExecutorSettings] as string) = value;
221 | console.log(`${language} inject set to ${value}`);
222 | await this.plugin.saveSettings();
223 | });
224 | });
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/src/RunButton.ts:
--------------------------------------------------------------------------------
1 | import { App, Workspace, MarkdownView } from 'obsidian';
2 | import ExecutorContainer from './ExecutorContainer';
3 | import { LanguageId, PluginContext, supportedLanguages } from './main';
4 | import { Outputter } from './output/Outputter';
5 | import type { ExecutorSettings } from './settings/Settings';
6 | import { CodeInjector } from './transforms/CodeInjector';
7 | import { retrieveFigurePath } from './transforms/LatexFigureName';
8 | import { modifyLatexCode } from './transforms/LatexTransformer';
9 | import * as macro from './transforms/Magic';
10 | import { getLanguageAlias } from './transforms/TransformCode';
11 |
12 | const buttonText = "Run";
13 |
14 | export const buttonClass: string = "run-code-button";
15 | export const disabledClass: string = "run-button-disabled";
16 | export const codeBlockHasButtonClass: string = "has-run-code-button";
17 |
18 | interface CodeBlockContext {
19 | srcCode: string;
20 | button: HTMLButtonElement;
21 | language: LanguageId;
22 | markdownFile: string;
23 | outputter: Outputter;
24 | executors: ExecutorContainer;
25 | }
26 |
27 | /**
28 | * Handles the execution of code blocks based on the selected programming language.
29 | * Injects any required code, transforms the source if needed, and manages button state.
30 | * @param block Contains context needed for execution including source code, output handler, and UI elements
31 | */
32 | async function handleExecution(block: CodeBlockContext) {
33 | const language: LanguageId = block.language;
34 | const button: HTMLButtonElement = block.button;
35 | const srcCode: string = block.srcCode;
36 | const app: App = block.outputter.app;
37 | const s: ExecutorSettings = block.outputter.settings;
38 |
39 | button.className = disabledClass;
40 | block.srcCode = await new CodeInjector(app, s, language).injectCode(srcCode);
41 |
42 | switch (language) {
43 | case "js": return runCode(s.nodePath, s.nodeArgs, s.jsFileExtension, block, { transform: (code) => macro.expandJS(code) });
44 | case "java": return runCode(s.javaPath, s.javaArgs, s.javaFileExtension, block);
45 | case "python": return runCode(s.pythonPath, s.pythonArgs, s.pythonFileExtension, block, { transform: (code) => macro.expandPython(code, s) });
46 | case "shell": return runCode(s.shellPath, s.shellArgs, s.shellFileExtension, block, { shell: true });
47 | case "batch": return runCode(s.batchPath, s.batchArgs, s.batchFileExtension, block, { shell: true });
48 | case "powershell": return runCode(s.powershellPath, s.powershellArgs, s.powershellFileExtension, block, { shell: true });
49 | case "cpp": return runCode(s.clingPath, `-std=${s.clingStd} ${s.clingArgs}`, s.cppFileExtension, block);
50 | case "prolog":
51 | runCode("", "", "", block);
52 | button.className = buttonClass;
53 | break;
54 | case "groovy": return runCode(s.groovyPath, s.groovyArgs, s.groovyFileExtension, block, { shell: true });
55 | case "rust": return runCode(s.cargoPath, "eval" + s.cargoEvalArgs, s.rustFileExtension, block);
56 | case "r": return runCode(s.RPath, s.RArgs, s.RFileExtension, block, { transform: (code) => macro.expandRPlots(code) });
57 | case "go": return runCode(s.golangPath, s.golangArgs, s.golangFileExtension, block);
58 | case "kotlin": return runCode(s.kotlinPath, s.kotlinArgs, s.kotlinFileExtension, block, { shell: true });
59 | case "ts": return runCode(s.tsPath, s.tsArgs, "ts", block, { shell: true });
60 | case "lua": return runCode(s.luaPath, s.luaArgs, s.luaFileExtension, block, { shell: true });
61 | case "dart": return runCode(s.dartPath, s.dartArgs, s.dartFileExtension, block, { shell: true });
62 | case "cs": return runCode(s.csPath, s.csArgs, s.csFileExtension, block, { shell: true });
63 | case "haskell": return (s.useGhci)
64 | ? runCode(s.ghciPath, "", "hs", block, { shell: true })
65 | : runCode(s.runghcPath, "-f " + s.ghcPath, "hs", block, { shell: true });
66 | case "mathematica": return runCode(s.mathematicaPath, s.mathematicaArgs, s.mathematicaFileExtension, block, { shell: true });
67 | case "scala": return runCode(s.scalaPath, s.scalaArgs, s.scalaFileExtension, block, { shell: true });
68 | case "swift": return runCode(s.swiftPath, s.swiftArgs, s.swiftFileExtension, block, { shell: true });
69 | case "c": return runCode(s.clingPath, s.clingArgs, "c", block, { shell: true });
70 | case "ruby": return runCode(s.rubyPath, s.rubyArgs, s.rubyFileExtension, block, { shell: true });
71 | case "sql": return runCode(s.sqlPath, s.sqlArgs, "sql", block, { shell: true });
72 | case "octave": return runCode(s.octavePath, s.octaveArgs, s.octaveFileExtension, block, { shell: true, transform: (code) => macro.expandOctavePlot(code) });
73 | case "maxima": return runCode(s.maximaPath, s.maximaArgs, s.maximaFileExtension, block, { shell: true, transform: (code) => macro.expandMaximaPlot(code) });
74 | case "racket": return runCode(s.racketPath, s.racketArgs, s.racketFileExtension, block, { shell: true });
75 | case "applescript": return runCode(s.applescriptPath, s.applescriptArgs, s.applescriptFileExtension, block, { shell: true });
76 | case "zig": return runCode(s.zigPath, s.zigArgs, "zig", block, { shell: true });
77 | case "ocaml": return runCode(s.ocamlPath, s.ocamlArgs, "ocaml", block, { shell: true });
78 | case "php": return runCode(s.phpPath, s.phpArgs, s.phpFileExtension, block, { shell: true });
79 | case "latex":
80 | const outputPath: string = await retrieveFigurePath(block.srcCode, s.latexFigureTitlePattern, block.markdownFile, s);
81 | const invokeCompiler: string = [s.latexTexfotArgs, s.latexCompilerPath, s.latexCompilerArgs].join(" ");
82 | return (!s.latexDoFilter)
83 | ? runCode(s.latexCompilerPath, s.latexCompilerArgs, outputPath, block, { transform: (code) => modifyLatexCode(code, s) })
84 | : runCode(s.latexTexfotPath, invokeCompiler, outputPath, block, { transform: (code) => modifyLatexCode(code, s) });
85 | default: break;
86 | }
87 | }
88 |
89 | /**
90 | * Adds run buttons to code blocks in all currently open Markdown files.
91 | * More efficient than scanning entire documents since it only processes visible content.
92 | * @param plugin Contains context needed for execution.
93 | */
94 | export function addInOpenFiles(plugin: PluginContext) {
95 | const workspace: Workspace = plugin.app.workspace;
96 | workspace.iterateRootLeaves(leaf => {
97 | if (leaf.view instanceof MarkdownView) {
98 | addToAllCodeBlocks(leaf.view.contentEl, leaf.view.file.path, leaf.view, plugin);
99 | }
100 | });
101 | }
102 |
103 | /**
104 | * Add a button to each code block that allows the user to run the code. The button is only added if the code block
105 | * utilizes a language that is supported by this plugin.
106 | * @param element The parent element (i.e. the currently showed html page / note).
107 | * @param file An identifier for the currently showed note
108 | * @param view The current markdown view
109 | * @param plugin Contains context needed for execution.
110 | */
111 | export function addToAllCodeBlocks(element: HTMLElement, file: string, view: MarkdownView, plugin: PluginContext) {
112 | Array.from(element.getElementsByTagName("code"))
113 | .forEach((codeBlock: HTMLElement) => addToCodeBlock(codeBlock, file, view, plugin));
114 | }
115 |
116 | /**
117 | * Processes a code block to add execution capabilities. Ensures buttons aren't duplicated on already processed blocks.
118 | * @param codeBlock The code block element to process
119 | * @param file Path to the current markdown file
120 | * @param view The current markdown view
121 | * @param plugin Contains context needed for execution.
122 | */
123 | function addToCodeBlock(codeBlock: HTMLElement, file: string, view: MarkdownView, plugin: PluginContext) {
124 | if (codeBlock.className.match(/^language-\{\w+/i)) {
125 | codeBlock.className = codeBlock.className.replace(/^language-\{(\w+)/i, "language-$1 {");
126 | codeBlock.parentElement.className = codeBlock.className;
127 | }
128 |
129 | const language = codeBlock.className.toLowerCase();
130 |
131 | if (!language || !language.contains("language-"))
132 | return;
133 |
134 | const pre = codeBlock.parentElement as HTMLPreElement;
135 | const parent = pre.parentElement as HTMLDivElement;
136 |
137 | const srcCode = codeBlock.getText();
138 | let sanitizedClassList = sanitizeClassListOfCodeBlock(codeBlock);
139 |
140 | const canonicalLanguage = getLanguageAlias(
141 | supportedLanguages.find(lang => sanitizedClassList.contains(`language-${lang}`))
142 | ) as LanguageId;
143 |
144 | const isLanguageSupported: Boolean = canonicalLanguage !== undefined;
145 | const hasBlockBeenButtonifiedAlready = parent.classList.contains(codeBlockHasButtonClass);
146 | if (!isLanguageSupported || hasBlockBeenButtonifiedAlready) return;
147 |
148 | const outputter = new Outputter(codeBlock, plugin.settings, view, plugin.app, file);
149 | parent.classList.add(codeBlockHasButtonClass);
150 | const button = createButton();
151 | pre.appendChild(button);
152 |
153 | const block: CodeBlockContext = {
154 | srcCode: srcCode,
155 | language: canonicalLanguage,
156 | markdownFile: file,
157 | button: button,
158 | outputter: outputter,
159 | executors: plugin.executors,
160 | };
161 |
162 | button.addEventListener("click", () => handleExecution(block));
163 | }
164 |
165 | /**
166 | * Normalizes language class names to ensure consistent processing.
167 | * @param codeBlock - The code block element whose classes need to be sanitized
168 | * @returns Array of normalized class names
169 | */
170 | function sanitizeClassListOfCodeBlock(codeBlock: HTMLElement) {
171 | let sanitizedClassList = Array.from(codeBlock.classList);
172 | return sanitizedClassList.map(c => c.toLowerCase());
173 | }
174 |
175 | /**
176 | * Creates a new run button and returns it.
177 | */
178 | function createButton(): HTMLButtonElement {
179 | console.debug("Add run button");
180 | const button = document.createElement("button");
181 | button.classList.add(buttonClass);
182 | button.setText(buttonText);
183 | return button;
184 | }
185 |
186 | /**
187 | * Executes the code with the given command and arguments. The code is written to a temporary file and then executed.
188 | * The output of the code is displayed in the output panel ({@link Outputter}).
189 | * If the code execution fails, an error message is displayed and logged.
190 | * After the code execution, the temporary file is deleted and the run button is re-enabled.
191 | * @param cmd The command that should be used to execute the code. (e.g. python, java, ...)
192 | * @param cmdArgs Additional arguments that should be passed to the command.
193 | * @param ext The file extension of the temporary file. Should correspond to the language of the code. (e.g. py, ...)
194 | * @param block Contains context needed for execution including source code, output handler, and UI elements
195 | */
196 | function runCode(cmd: string, cmdArgs: string, ext: string, block: CodeBlockContext, options?: { shell?: boolean; transform?: (code: string) => string; }) {
197 | const useShell: boolean = (options?.shell) ? options.shell : false;
198 | if (options?.transform) block.srcCode = options.transform(block.srcCode);
199 | if (!useShell) block.outputter.startBlock();
200 |
201 | const executor = block.executors.getExecutorFor(block.markdownFile, block.language, useShell);
202 | executor.run(block.srcCode, block.outputter, cmd, cmdArgs, ext).then(() => {
203 | block.button.className = buttonClass;
204 | if (!useShell) {
205 | block.outputter.closeInput();
206 | block.outputter.finishBlock();
207 | }
208 | });
209 | }
210 |
--------------------------------------------------------------------------------
/src/settings/Settings.ts:
--------------------------------------------------------------------------------
1 | import { LanguageId } from "src/main";
2 |
3 | /**
4 | * Interface that contains all the settings for the extension.
5 | */
6 | export interface ExecutorSettings {
7 | lastOpenLanguageTab: LanguageId | undefined;
8 | releaseNote2_1_0wasShowed: boolean;
9 | persistentOuput: boolean;
10 | timeout: number;
11 | allowInput: boolean;
12 | wslMode: boolean;
13 | shellWSLMode: boolean;
14 | onlyCurrentBlock: boolean;
15 | nodePath: string;
16 | nodeArgs: string;
17 | jsInject: string;
18 | jsFileExtension: string;
19 | tsPath: string;
20 | tsArgs: string;
21 | tsInject: string;
22 | latexCompilerPath: string;
23 | latexCompilerArgs: string;
24 | latexDoFilter: boolean;
25 | latexTexfotPath: string;
26 | latexTexfotArgs: string;
27 | latexDocumentclass: string;
28 | latexAdaptFont: '' | 'obsidian' | 'system';
29 | latexKeepLog: boolean;
30 | latexSubprocessesUseShell: boolean;
31 | latexMaxFigures: number;
32 | latexFigureTitlePattern: string;
33 | latexDoCrop: boolean;
34 | latexCropPath: string;
35 | latexCropArgs: string;
36 | latexCropNoStandalone: boolean;
37 | latexCropNoPagenum: boolean;
38 | latexSaveSvg: '' | 'poppler' | 'inkscape';
39 | latexSvgPath: string;
40 | latexSvgArgs: string;
41 | latexInkscapePath: string;
42 | latexInkscapeArgs: string;
43 | latexSavePdf: boolean;
44 | latexSavePng: boolean;
45 | latexPngPath: string;
46 | latexPngArgs: string;
47 | latexOutputEmbeddings: boolean;
48 | latexInvertFigures: boolean;
49 | latexCenterFigures: boolean;
50 |
51 | latexInject: string;
52 | leanPath: string;
53 | leanArgs: string;
54 | leanInject: string;
55 | luaPath: string;
56 | luaArgs: string;
57 | luaFileExtension: string;
58 | luaInject: string;
59 | dartPath: string;
60 | dartArgs: string;
61 | dartFileExtension: string;
62 | dartInject: string;
63 | csPath: string;
64 | csArgs: string;
65 | csFileExtension: string;
66 | csInject: string;
67 | pythonPath: string;
68 | pythonArgs: string;
69 | pythonEmbedPlots: boolean;
70 | pythonFileExtension: string;
71 | pythonInject: string;
72 | shellPath: string;
73 | shellArgs: string;
74 | shellFileExtension: string;
75 | shellInject: string;
76 | batchPath: string;
77 | batchArgs: string;
78 | batchFileExtension: string;
79 | batchInject: string;
80 | groovyPath: string;
81 | groovyArgs: string;
82 | groovyFileExtension: string;
83 | groovyInject: string;
84 | golangPath: string,
85 | golangArgs: string,
86 | golangFileExtension: string,
87 | goInject: string;
88 | javaPath: string,
89 | javaArgs: string,
90 | javaFileExtension: string,
91 | javaInject: string;
92 | maxPrologAnswers: number;
93 | prologInject: string;
94 | powershellPath: string;
95 | powershellArgs: string;
96 | powershellFileExtension: string;
97 | powershellInject: string;
98 | powershellEncoding: BufferEncoding;
99 | octavePath: string;
100 | octaveArgs: string;
101 | octaveFileExtension: string;
102 | octaveInject: string;
103 | maximaPath: string;
104 | maximaArgs: string;
105 | maximaFileExtension: string;
106 | maximaInject: string;
107 | cargoPath: string;
108 | cargoEvalArgs: string;
109 | rustInject: string;
110 | cppRunner: string;
111 | cppFileExtension: string;
112 | cppInject: string;
113 | cppArgs: string;
114 | cppUseMain: boolean;
115 | clingPath: string;
116 | clingArgs: string;
117 | clingStd: string;
118 | rustFileExtension: string,
119 | RPath: string;
120 | RArgs: string;
121 | REmbedPlots: boolean;
122 | RFileExtension: string;
123 | rInject: string;
124 | kotlinPath: string;
125 | kotlinArgs: string;
126 | kotlinFileExtension: string;
127 | kotlinInject: string;
128 | swiftPath: string;
129 | swiftArgs: string;
130 | swiftFileExtension: string;
131 | swiftInject: string;
132 | runghcPath: string;
133 | ghcPath: string;
134 | ghciPath: string;
135 | haskellInject: string;
136 | useGhci: boolean;
137 | mathematicaPath: string;
138 | mathematicaArgs: string;
139 | mathematicaFileExtension: string;
140 | mathematicaInject: string;
141 | phpPath: string;
142 | phpArgs: string;
143 | phpFileExtension: string;
144 | phpInject: string;
145 | scalaPath: string;
146 | scalaArgs: string;
147 | scalaFileExtension: string;
148 | scalaInject: string;
149 | racketPath: string;
150 | racketArgs: string;
151 | racketFileExtension: string;
152 | racketInject: string;
153 | fsharpPath: string;
154 | fsharpArgs: string;
155 | fsharpInject: "";
156 | fsharpFileExtension: string;
157 | cArgs: string;
158 | cUseMain: boolean;
159 | cInject: string;
160 | rubyPath: string;
161 | rubyArgs: string;
162 | rubyFileExtension: string;
163 | rubyInject: string;
164 | sqlPath: string;
165 | sqlArgs: string;
166 | sqlInject: string;
167 | applescriptPath: string;
168 | applescriptArgs: string;
169 | applescriptFileExtension: string;
170 | applescriptInject: string;
171 | zigPath: string;
172 | zigArgs: string;
173 | zigInject: string;
174 | ocamlPath: string;
175 | ocamlArgs: string;
176 | ocamlInject: string;
177 |
178 | jsInteractive: boolean;
179 | tsInteractive: boolean;
180 | csInteractive: boolean;
181 | latexInteractive: boolean;
182 | leanInteractive: boolean;
183 | luaInteractive: boolean;
184 | dartInteractive: boolean;
185 | pythonInteractive: boolean;
186 | cppInteractive: boolean;
187 | prologInteractive: boolean;
188 | shellInteractive: boolean;
189 | batchInteractive: boolean;
190 | bashInteractive: boolean;
191 | groovyInteractive: boolean;
192 | rInteractive: boolean;
193 | goInteractive: boolean;
194 | rustInteractive: boolean;
195 | javaInteractive: boolean;
196 | powershellInteractive: boolean;
197 | kotlinInteractive: boolean;
198 | swiftInteractive: boolean;
199 | mathematicaInteractive: boolean;
200 | haskellInteractive: boolean;
201 | scalaInteractive: boolean;
202 | racketInteractive: boolean;
203 | fsharpInteractive: boolean;
204 | cInteractive: boolean;
205 | rubyInteractive: boolean;
206 | sqlInteractive: boolean;
207 | octaveInteractive: boolean;
208 | maximaInteractive: boolean;
209 | applescriptInteractive: boolean;
210 | zigInteractive: boolean;
211 | ocamlInteractive: boolean;
212 | phpInteractive: boolean;
213 | }
214 |
215 |
216 | /**
217 | * The default settings for the extensions as implementation of the ExecutorSettings interface.
218 | */
219 | export const DEFAULT_SETTINGS: ExecutorSettings = {
220 | lastOpenLanguageTab: undefined,
221 |
222 | releaseNote2_1_0wasShowed: false,
223 | persistentOuput: false,
224 | timeout: 10000,
225 | allowInput: true,
226 | wslMode: false,
227 | shellWSLMode: false,
228 | onlyCurrentBlock: false,
229 | nodePath: "node",
230 | nodeArgs: "",
231 | jsFileExtension: "js",
232 | jsInject: "",
233 | tsPath: "ts-node",
234 | tsArgs: "",
235 | tsInject: "",
236 | latexCompilerPath: "lualatex",
237 | latexCompilerArgs: "-interaction=nonstopmode",
238 | latexDoFilter: true,
239 | latexTexfotPath: "texfot",
240 | latexTexfotArgs: "--quiet",
241 | latexDocumentclass: "article",
242 | latexAdaptFont: "obsidian",
243 | latexKeepLog: false,
244 | latexSubprocessesUseShell: false,
245 | latexMaxFigures: 10,
246 | latexFigureTitlePattern: /[^\n][^%`]*\\title\s*\{(?[^\}]*)\}/.source,
247 | latexDoCrop: false,
248 | latexCropPath: "pdfcrop",
249 | latexCropArgs: "--quiet",
250 | latexCropNoStandalone: true,
251 | latexCropNoPagenum: true,
252 | latexSaveSvg: "poppler",
253 | latexSvgPath: "pdftocairo",
254 | latexSvgArgs: "-svg",
255 | latexInkscapePath: "inkscape",
256 | latexInkscapeArgs: '--pages=all --export-plain-svg',
257 | latexSavePdf: true,
258 | latexSavePng: false,
259 | latexPngPath: "pdftocairo",
260 | latexPngArgs: "-singlefile -png",
261 | latexOutputEmbeddings: true,
262 | latexInvertFigures: true,
263 | latexCenterFigures: true,
264 | latexInject: "",
265 | leanPath: "lean",
266 | leanArgs: "",
267 | leanInject: "",
268 | luaPath: "lua",
269 | luaArgs: "",
270 | luaFileExtension: "lua",
271 | luaInject: "",
272 | dartPath: "dart",
273 | dartArgs: "",
274 | dartFileExtension: "dart",
275 | dartInject: "",
276 | csPath: "dotnet-script",
277 | csArgs: "",
278 | csFileExtension: "csx",
279 | csInject: "",
280 | pythonPath: "python",
281 | pythonArgs: "",
282 | pythonEmbedPlots: true,
283 | pythonFileExtension: "py",
284 | pythonInject: "",
285 | shellPath: "bash",
286 | shellArgs: "",
287 | shellFileExtension: "sh",
288 | shellInject: "",
289 | batchPath: "call",
290 | batchArgs: "",
291 | batchFileExtension: "bat",
292 | batchInject: "",
293 | groovyPath: "groovy",
294 | groovyArgs: "",
295 | groovyFileExtension: "groovy",
296 | groovyInject: "",
297 | golangPath: "go",
298 | golangArgs: "run",
299 | golangFileExtension: "go",
300 | goInject: "",
301 | javaPath: "java",
302 | javaArgs: "-ea",
303 | javaFileExtension: "java",
304 | javaInject: "",
305 | maxPrologAnswers: 15,
306 | prologInject: "",
307 | powershellPath: "powershell",
308 | powershellArgs: "-file",
309 | powershellFileExtension: "ps1",
310 | powershellInject: "$OutputEncoding = [console]::InputEncoding = [console]::OutputEncoding = New-Object System.Text.UTF8Encoding",
311 | powershellEncoding: "latin1",
312 | cargoPath: "cargo",
313 | cargoEvalArgs: "",
314 | rustInject: "",
315 | cppRunner: "cling",
316 | cppFileExtension: "cpp",
317 | cppInject: "",
318 | cppArgs: "",
319 | cppUseMain: false,
320 | clingPath: "cling",
321 | clingArgs: "",
322 | clingStd: "c++17",
323 | rustFileExtension: "rs",
324 | RPath: "Rscript",
325 | RArgs: "",
326 | REmbedPlots: true,
327 | RFileExtension: "R",
328 | rInject: "",
329 | kotlinPath: "kotlinc",
330 | kotlinArgs: "-script",
331 | kotlinFileExtension: "kts",
332 | kotlinInject: "",
333 | swiftPath: "swift",
334 | swiftArgs: "",
335 | swiftFileExtension: "swift",
336 | swiftInject: "",
337 | runghcPath: "runghc",
338 | ghcPath: "ghc",
339 | ghciPath: "ghci",
340 | useGhci: false,
341 | haskellInject: "",
342 | mathematicaPath: "wolframscript",
343 | mathematicaArgs: "-file",
344 | mathematicaFileExtension: "wls",
345 | mathematicaInject: "",
346 | scalaPath: "scala",
347 | scalaArgs: "",
348 | scalaFileExtension: "scala",
349 | scalaInject: "",
350 | racketPath: "racket",
351 | racketArgs: "",
352 | racketFileExtension: "rkt",
353 | racketInject: "#lang racket",
354 | fsharpPath: "dotnet",
355 | fsharpArgs: "fsi",
356 | fsharpInject: "",
357 | fsharpFileExtension: "fsx",
358 | cArgs: "",
359 | cUseMain: true,
360 | cInject: "",
361 | rubyPath: "ruby",
362 | rubyArgs: "",
363 | rubyFileExtension: "rb",
364 | rubyInject: "",
365 | sqlPath: "psql",
366 | sqlArgs: "-d -U -f",
367 | sqlInject: "",
368 | octavePath: "octave",
369 | octaveArgs: "-q",
370 | octaveFileExtension: "m",
371 | octaveInject: "figure('visible','off') # Necessary to embed plots",
372 | maximaPath: "maxima",
373 | maximaArgs: "-qb",
374 | maximaFileExtension: "mx",
375 | maximaInject: "",
376 | applescriptPath: "osascript",
377 | applescriptArgs: "",
378 | applescriptFileExtension: "scpt",
379 | applescriptInject: "",
380 | zigPath: "zig",
381 | zigArgs: "run",
382 | zigInject: "",
383 | ocamlPath: "ocaml",
384 | ocamlArgs: "",
385 | ocamlInject: "",
386 | phpPath: "php",
387 | phpArgs: "",
388 | phpFileExtension: "php",
389 | phpInject: "",
390 | jsInteractive: true,
391 | tsInteractive: false,
392 | csInteractive: false,
393 | latexInteractive: false,
394 | leanInteractive: false,
395 | luaInteractive: false,
396 | dartInteractive: false,
397 | pythonInteractive: true,
398 | cppInteractive: false,
399 | prologInteractive: false,
400 | shellInteractive: false,
401 | batchInteractive: false,
402 | bashInteractive: false,
403 | groovyInteractive: false,
404 | rInteractive: false,
405 | goInteractive: false,
406 | rustInteractive: false,
407 | javaInteractive: false,
408 | powershellInteractive: false,
409 | kotlinInteractive: false,
410 | swiftInteractive: false,
411 | mathematicaInteractive: false,
412 | haskellInteractive: false,
413 | scalaInteractive: false,
414 | fsharpInteractive: false,
415 | cInteractive: false,
416 | racketInteractive: false,
417 | rubyInteractive: false,
418 | sqlInteractive: false,
419 | octaveInteractive: false,
420 | maximaInteractive: false,
421 | applescriptInteractive: false,
422 | zigInteractive: false,
423 | ocamlInteractive: false,
424 | phpInteractive: false,
425 | }
426 |
--------------------------------------------------------------------------------
/src/transforms/Magic.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Adds functions that parse source code for magic commands and transpile them to the target language.
3 | *
4 | * List of Magic Commands:
5 | * - `@show(ImagePath)`: Displays an image at the given path in the note.
6 | * - `@show(ImagePath, Width, Height)`: Displays an image at the given path in the note.
7 | * - `@show(ImagePath, Width, Height, Alignment)`: Displays an image at the given path in the note.
8 | * - `@vault`: Inserts the vault path as string.
9 | * - `@note`: Inserts the note path as string.
10 | * - `@title`: Inserts the note title as string.
11 | * - `@theme`: Inserts the color theme; either `"light"` or `"dark"`. For use with images, inline plots, and `@html()`.
12 | */
13 |
14 | import * as os from "os";
15 | import { Platform } from 'obsidian';
16 | import { TOGGLE_HTML_SIGIL } from "src/output/Outputter";
17 | import { ExecutorSettings } from "src/settings/Settings";
18 |
19 | // Regex for all languages.
20 | const SHOW_REGEX = /@show\(["'](?[^<>?*=!\n#()\[\]{}]+)["'](,\s*(?\d+[\w%]+),?\s*(?\d+[\w%]+))?(,\s*(?left|center|right))?\)/g;
21 | const HTML_REGEX = /@html\((?[^)]+)\)/g;
22 | const VAULT_REGEX = /@vault/g
23 | const VAULT_PATH_REGEX = /@vault_path/g
24 | const VAULT_URL_REGEX = /@vault_url/g
25 | const CURRENT_NOTE_REGEX = /@note/g;
26 | const CURRENT_NOTE_PATH_REGEX = /@note_path/g;
27 | const CURRENT_NOTE_URL_REGEX = /@note_url/g;
28 | const NOTE_TITLE_REGEX = /@title/g;
29 | const NOTE_CONTENT_REGEX = /@content/g;
30 | const COLOR_THEME_REGEX = /@theme/g;
31 |
32 | // Regex that are only used by one language.
33 | const PYTHON_PLOT_REGEX = /^(plt|matplotlib.pyplot|pyplot)\.show\(\)/gm;
34 | const R_PLOT_REGEX = /^plot\(.*\)/gm;
35 | const OCTAVE_PLOT_REGEX = /^plot\s*\(.*\);/gm;
36 | const MAXIMA_PLOT_REGEX = /^plot2d\s*\(.*\[.+\]\)\s*[$;]/gm;
37 |
38 | /**
39 | * Parses the source code for the @vault command and replaces it with the vault path.
40 | *
41 | * @param source The source code to parse.
42 | * @param vaultPath The path of the vault.
43 | * @returns The transformed source code.
44 | */
45 | export function expandVaultPath(source: string, vaultPath: string): string {
46 | // Remove the leading slash (if it is there) and replace all backslashes with forward slashes.
47 | let vaultPathClean = vaultPath.replace(/\\/g, "/").replace(/^\//, "");
48 |
49 | source = source.replace(VAULT_PATH_REGEX, `"${vaultPath.replace(/\\/g, "/")}"`);
50 | source = source.replace(VAULT_URL_REGEX, `"${Platform.resourcePathPrefix + vaultPathClean}"`);
51 | source = source.replace(VAULT_REGEX, `"${Platform.resourcePathPrefix + vaultPathClean}"`);
52 |
53 | return source;
54 | }
55 |
56 |
57 | /**
58 | * Parses the source code for the @note command and replaces it with the note path.
59 | *
60 | * @param source The source code to parse.
61 | * @param notePath The path of the vault.
62 | * @returns The transformed source code.
63 | */
64 | export function expandNotePath(source: string, notePath: string): string {
65 | // Remove the leading slash (if it is there) and replace all backslashes with forward slashes.
66 | let notePathClean = notePath.replace(/\\/g, "/").replace(/^\//, "");
67 |
68 | source = source.replace(CURRENT_NOTE_PATH_REGEX, `"${notePath.replace(/\\/g, "/")}"`);
69 | source = source.replace(CURRENT_NOTE_URL_REGEX, `"${Platform.resourcePathPrefix + notePathClean}"`);
70 | source = source.replace(CURRENT_NOTE_REGEX, `"${Platform.resourcePathPrefix + notePathClean}"`);
71 |
72 | return source;
73 | }
74 |
75 |
76 | /**
77 | * Parses the source code for the @title command and replaces it with the vault path.
78 | *
79 | * @param source The source code to parse.
80 | * @param noteTitle The path of the vault.
81 | * @returns The transformed source code.
82 | */
83 | export function expandNoteTitle(source: string, noteTitle: string): string {
84 | let t = "";
85 | if (noteTitle.contains("."))
86 | t = noteTitle.split(".").slice(0, -1).join(".");
87 |
88 | return source.replace(NOTE_TITLE_REGEX, `"${t}"`);
89 | }
90 |
91 | /**
92 | * Parses the source code and replaces the NOTE_CONTENT_REGEX with the file content.
93 | *
94 | * @param source The source code to parse.
95 | * @param content The content of the note.
96 | * @returns The transformed source code.
97 | */
98 | export function insertNoteContent(source: string, content: string): string {
99 | const escaped_content = JSON.stringify(content)
100 | return source.replace(NOTE_CONTENT_REGEX, `${escaped_content}`)
101 | }
102 |
103 | /**
104 | * Parses the source code for the @theme command and replaces it with the colour theme.
105 | *
106 | * @param source The source code to parse.
107 | * @param noteTitle The current colour theme.
108 | * @returns The transformed source code.
109 | */
110 | export function expandColorTheme(source: string, theme: string): string {
111 | return source.replace(COLOR_THEME_REGEX, `"${theme}"`);
112 | }
113 |
114 | /**
115 | * Add the @show command to python. @show is only supported in python and javascript.
116 | *
117 | * @param source The source code to parse.
118 | * @returns The transformed source code.
119 | */
120 | export function expandPython(source: string, settings: ExecutorSettings): string {
121 | if (settings.pythonEmbedPlots) {
122 | source = expandPythonPlots(source, TOGGLE_HTML_SIGIL);
123 | }
124 | source = expandPythonShowImage(source);
125 | source = expandPythonHtmlMacro(source);
126 | return source;
127 | }
128 |
129 |
130 | /**
131 | * Add the @show command to javascript. @show is only supported in python and javascript.
132 | *
133 | * @param source The source code to parse.
134 | * @returns The transformed source code.
135 | */
136 | export function expandJS(source: string): string {
137 | source = expandJsShowImage(source);
138 | source = expandJsHtmlMacro(source);
139 | return source;
140 | }
141 |
142 |
143 | /**
144 | * Parses some python code and changes it to display plots in the note instead of opening a new window.
145 | * Only supports normal plots generated with the `plt.show(...)` function.
146 | *
147 | * @param source The source code to parse.
148 | * @param toggleHtmlSigil The meta-command to allow and disallow HTML
149 | * @returns The transformed source code.
150 | */
151 | export function expandPythonPlots(source: string, toggleHtmlSigil: string): string {
152 | const showPlot = `import io; import sys; __obsidian_execute_code_temp_pyplot_var__=io.BytesIO(); plt.plot(); plt.savefig(__obsidian_execute_code_temp_pyplot_var__, format='svg'); plt.close(); sys.stdout.write(${JSON.stringify(toggleHtmlSigil)}); sys.stdout.flush(); sys.stdout.buffer.write(__obsidian_execute_code_temp_pyplot_var__.getvalue()); sys.stdout.flush(); sys.stdout.write(${JSON.stringify(toggleHtmlSigil)}); sys.stdout.flush()`;
153 | return source.replace(PYTHON_PLOT_REGEX, showPlot);
154 | }
155 |
156 |
157 | /**
158 | * Parses some R code and changes it to display plots in the note instead of opening a new window.
159 | * Only supports normal plots generated with the `plot(...)` function.
160 | *
161 | * @param source The source code to parse.
162 | * @returns The transformed source code.
163 | */
164 | export function expandRPlots(source: string): string {
165 | const matches = source.matchAll(R_PLOT_REGEX);
166 | for (const match of matches) {
167 | const tempFile = `${os.tmpdir()}/temp_${Date.now()}.png`.replace(/\\/g, "/").replace(/^\//, "");
168 | const substitute = `png("${tempFile}"); ${match[0]}; dev.off(); cat('${TOGGLE_HTML_SIGIL}
${TOGGLE_HTML_SIGIL}')`;
169 |
170 | source = source.replace(match[0], substitute);
171 | }
172 |
173 | return source;
174 | }
175 |
176 |
177 | /**
178 | * Parses the PYTHON code for the @show command and replaces it with the image.
179 | * @param source The source code to parse.
180 | */
181 | function expandPythonShowImage(source: string): string {
182 | const matches = source.matchAll(SHOW_REGEX);
183 | for (const match of matches) {
184 | const imagePath = match.groups.path;
185 | const width = match.groups.width;
186 | const height = match.groups.height;
187 | const alignment = match.groups.align;
188 |
189 | const image = expandShowImage(imagePath.replace(/\\/g, "\\\\"), width, height, alignment);
190 | source = source.replace(match[0], "print(\'" + TOGGLE_HTML_SIGIL + image + TOGGLE_HTML_SIGIL + "\')");
191 | }
192 |
193 | return source;
194 | }
195 |
196 | /**
197 | * Parses the PYTHON code for the @html command and surrounds it with the toggle-escaoe token.
198 | * @param source
199 | */
200 | function expandPythonHtmlMacro(source: string): string {
201 | const matches = source.matchAll(HTML_REGEX);
202 | for (const match of matches) {
203 | const html = match.groups.html;
204 |
205 | const toggle = JSON.stringify(TOGGLE_HTML_SIGIL);
206 |
207 | source = source.replace(match[0], `print(${toggle}); print(${html}); print(${toggle})`)
208 | }
209 | return source;
210 | }
211 |
212 |
213 | /**
214 | * Parses the JAVASCRIPT code for the @show command and replaces it with the image.
215 | * @param source The source code to parse.
216 | */
217 | function expandJsShowImage(source: string): string {
218 | const matches = source.matchAll(SHOW_REGEX);
219 | for (const match of matches) {
220 | const imagePath = match.groups.path;
221 | const width = match.groups.width;
222 | const height = match.groups.height;
223 | const alignment = match.groups.align;
224 |
225 | const image = expandShowImage(imagePath.replace(/\\/g, "\\\\").replace(/^\//, ""), width, height, alignment);
226 |
227 | source = source.replace(match[0], "console.log(\'" + TOGGLE_HTML_SIGIL + image + TOGGLE_HTML_SIGIL + "\')");
228 | console.log(source);
229 | }
230 |
231 | return source;
232 | }
233 |
234 | function expandJsHtmlMacro(source: string): string {
235 | const matches = source.matchAll(HTML_REGEX);
236 | for (const match of matches) {
237 | const html = match.groups.html;
238 |
239 | const toggle = JSON.stringify(TOGGLE_HTML_SIGIL);
240 |
241 | source = source.replace(match[0], `console.log(${toggle}); console.log(${html}); console.log(${toggle})`)
242 | }
243 | return source;
244 | }
245 |
246 |
247 | /**
248 | * Builds the image string that is used to display the image in the note based on the configurations for
249 | * height, width and alignment.
250 | *
251 | * @param imagePath The path to the image.
252 | * @param width The image width.
253 | * @param height The image height.
254 | * @param alignment The image alignment.
255 | */
256 | function expandShowImage(imagePath: string, width: string = "0", height: string = "0", alignment: string = "center"): string {
257 | if (imagePath.contains("+")) {
258 | let splittedPath = imagePath.replace(/['"]/g, "").split("+");
259 | splittedPath = splittedPath.map(element => element.trim())
260 | imagePath = splittedPath.join("");
261 | }
262 |
263 | if (width == "0" || height == "0")
264 | return `
`;
265 |
266 | return `
`;
267 | }
268 |
269 | export function expandOctavePlot(source: string): string {
270 | const matches = source.matchAll(OCTAVE_PLOT_REGEX);
271 | for (const match of matches) {
272 | const tempFile = `${os.tmpdir()}/temp_${Date.now()}.png`.replace(/\\/g, "/").replace(/^\//, "");
273 | const substitute = `${match[0]}; print -dpng ${tempFile}; disp('${TOGGLE_HTML_SIGIL}
${TOGGLE_HTML_SIGIL}');`;
274 |
275 | source = source.replace(match[0], substitute);
276 | }
277 |
278 | return source;
279 | }
280 |
281 | export function expandMaximaPlot(source: string): string {
282 | const matches = source.matchAll(MAXIMA_PLOT_REGEX);
283 | for (const match of matches) {
284 | const tempFile = `${os.tmpdir()}/temp_${Date.now()}.png`.replace(/\\/g, "/").replace(/^\//, "");
285 | const updated_plot_call = match[0].substring(0, match[0].lastIndexOf(')')) + `, [png_file, "${tempFile}"])`;
286 | const substitute = `${updated_plot_call}; print ('${TOGGLE_HTML_SIGIL}
${TOGGLE_HTML_SIGIL}');`;
287 |
288 | source = source.replace(match[0], substitute);
289 | }
290 |
291 | return source;
292 | }
293 |
294 |
--------------------------------------------------------------------------------