├── .npmrc ├── .eslintignore ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug-report.md ├── images ├── batch_settings.png ├── magic_example.png ├── plotting_example.png ├── execute_code_example.gif ├── path_location_shell.png ├── path_location_settings.png ├── figure_time_of_day.svg ├── figure_minimal_example.svg └── figure_include_attachments.svg ├── .editorconfig ├── src ├── svgs │ ├── parseHTML.ts │ ├── loadSpinner.ts │ └── loadEllipses.ts ├── transforms │ ├── windowsPathToWsl.ts │ ├── LatexTransformer.ts │ ├── TransformCode.ts │ ├── LatexFigureName.ts │ ├── LatexFontHandler.ts │ ├── CodeInjector.ts │ └── Magic.ts ├── runAllCodeBlocks.ts ├── executors │ ├── killWithChildren.ts │ ├── CppExecutor.ts │ ├── FSharpExecutor.ts │ ├── CExecutor.ts │ ├── NodeJSExecutor.ts │ ├── python │ │ ├── wrapPython.ts │ │ └── PythonExecutor.ts │ ├── AsyncExecutor.ts │ ├── RExecutor.ts │ ├── PowerShellOnWindowsExecutor.ts │ ├── ClingExecutor.ts │ ├── Executor.ts │ ├── PrologExecutor.ts │ ├── ReplExecutor.ts │ └── NonInteractiveCodeExecutor.ts ├── settings │ ├── per-lang │ │ ├── makeGoSettings.ts │ │ ├── makeRustSettings.ts │ │ ├── makePrologSettings.ts │ │ ├── makeDartSettings.ts │ │ ├── makeLuaSettings.ts │ │ ├── makeCsSettings.ts │ │ ├── makeSQLSettings.ts │ │ ├── makeLeanSettings.ts │ │ ├── makeTsSettings.ts │ │ ├── makePhpSettings.ts │ │ ├── makeZigSettings.ts │ │ ├── makeRubySettings.ts │ │ ├── makeOCamlSettings.ts │ │ ├── makeSwiftSettings.ts │ │ ├── makeMaximaSettings.ts │ │ ├── makeOctaveSettings.ts │ │ ├── makeScalaSettings.ts │ │ ├── makeGroovySettings.ts │ │ ├── makeJavaSettings.ts │ │ ├── makeKotlinSettings.ts │ │ ├── makeRacketSettings.ts │ │ ├── makeApplescriptSettings.ts │ │ ├── makeMathematicaSettings.ts │ │ ├── makeJsSettings.ts │ │ ├── makeFSharpSettings.ts │ │ ├── makeBatchSettings.ts │ │ ├── makeHaskellSettings.ts │ │ ├── makePythonSettings.ts │ │ ├── makeRSettings.ts │ │ ├── makeCppSettings.ts │ │ ├── makeShellSettings.ts │ │ ├── makeCSettings.ts │ │ └── makePowershellSettings.ts │ ├── languageDisplayName.ts │ ├── SettingsTab.ts │ └── Settings.ts ├── Vault.ts ├── ReleaseNoteModal.ts ├── output │ ├── RegExpUtilities.ts │ ├── LatexInserter.ts │ └── FileAppender.ts ├── CodeBlockArgs.ts ├── ExecutorContainer.ts ├── ExecutorManagerView.ts ├── main.ts ├── styles.css └── RunButton.ts ├── manifest.json ├── .gitignore ├── tsconfig.json ├── .eslintrc ├── package.json ├── LICENSE ├── versions.json ├── esbuild.config.mjs └── version-bump.mjs /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | npm node_modules 2 | build -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [twibiral] 2 | buy_me_a_coffee: timwibiral 3 | -------------------------------------------------------------------------------- /images/batch_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twibiral/obsidian-execute-code/HEAD/images/batch_settings.png -------------------------------------------------------------------------------- /images/magic_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twibiral/obsidian-execute-code/HEAD/images/magic_example.png -------------------------------------------------------------------------------- /images/plotting_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twibiral/obsidian-execute-code/HEAD/images/plotting_example.png -------------------------------------------------------------------------------- /images/execute_code_example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twibiral/obsidian-execute-code/HEAD/images/execute_code_example.gif -------------------------------------------------------------------------------- /images/path_location_shell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twibiral/obsidian-execute-code/HEAD/images/path_location_shell.png -------------------------------------------------------------------------------- /images/path_location_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twibiral/obsidian-execute-code/HEAD/images/path_location_settings.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | insert_final_newline = true 7 | indent_style = tab 8 | indent_size = 4 9 | tab_width = 4 10 | -------------------------------------------------------------------------------- /src/svgs/parseHTML.ts: -------------------------------------------------------------------------------- 1 | export default (html: string) => { 2 | let container = document.createElement("div"); 3 | container.innerHTML = html; 4 | return container.firstElementChild; 5 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "execute-code", 3 | "name": "Execute Code", 4 | "version": "2.1.2", 5 | "minAppVersion": "1.7.2", 6 | "description": "Allows you to execute code snippets within a note. Support C, C++, Python, R, JavaScript, TypeScript, LaTeX, SQL, and many more.", 7 | "author": "twibiral", 8 | "authorUrl": "https://www.github.com/twibiral", 9 | "isDesktopOnly": true 10 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # Don't include the compiled main.js file in the repo. 12 | # They should be uploaded to GitHub releases instead. 13 | main.js 14 | 15 | # Exclude sourcemaps 16 | *.map 17 | 18 | # obsidian 19 | data.json 20 | 21 | # Exclude macOS Finder (System Explorer) View States 22 | .DS_Store 23 | *.log 24 | -------------------------------------------------------------------------------- /src/transforms/windowsPathToWsl.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path/posix"; 2 | import { sep } from "path"; 3 | 4 | export default (windowsPath: string) => { 5 | const driveLetter = windowsPath[0].toLowerCase(); 6 | const posixyPath = windowsPath.replace(/^[^:]*:/, "") //remove drive letter 7 | .split(sep).join("/"); //force / as separator 8 | 9 | return join("/mnt/", driveLetter, posixyPath); 10 | } -------------------------------------------------------------------------------- /src/runAllCodeBlocks.ts: -------------------------------------------------------------------------------- 1 | import { TextFileView, Workspace } from "obsidian"; 2 | import { buttonClass } from './RunButton'; 3 | 4 | export default function runAllCodeBlocks(workspace: Workspace) { 5 | const lastActiveView = workspace.getMostRecentLeaf().view; 6 | 7 | if (lastActiveView instanceof TextFileView) { 8 | lastActiveView.containerEl.querySelectorAll("button." + buttonClass).forEach((button: HTMLButtonElement) => { 9 | button.click(); 10 | }); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/svgs/loadSpinner.ts: -------------------------------------------------------------------------------- 1 | import parseHTML from "./parseHTML"; 2 | 3 | const svg = parseHTML(` 4 | 5 | 6 | `); 7 | 8 | export default () => { 9 | return svg.cloneNode(true); 10 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "ES2018", 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "isolatedModules": true, 13 | "lib": [ 14 | "DOM", 15 | "ES5", 16 | "ES6", 17 | "ES7", 18 | "ES2018" 19 | ], 20 | "types": ["obsidian-typings"] 21 | }, 22 | "include": [ 23 | "**/*.ts", 24 | "src/*ts" 25 | ] 26 | } -------------------------------------------------------------------------------- /src/executors/killWithChildren.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "child_process" 2 | 3 | export default (pid: number) => { 4 | if(process.platform === "win32") { 5 | execSync(`taskkill /pid ${pid} /T /F`) 6 | } else { 7 | try { 8 | execSync(`pkill -P ${pid}`) 9 | } catch(err) { 10 | // An error code of 1 signifies that no children were found to kill 11 | // In this case, ignore the error 12 | // Otherwise, re-throw it. 13 | if (err.status !== 1) throw err; 14 | } 15 | process.kill(pid); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/executors/CppExecutor.ts: -------------------------------------------------------------------------------- 1 | import type { Outputter } from "src/output/Outputter"; 2 | import type { ExecutorSettings } from "src/settings/Settings"; 3 | import ClingExecutor from './ClingExecutor'; 4 | 5 | export default class CppExecutor extends ClingExecutor { 6 | 7 | constructor(settings: ExecutorSettings, file: string) { 8 | super(settings, file, "cpp"); 9 | } 10 | 11 | override run(codeBlockContent: string, outputter: Outputter, cmd: string, cmdArgs: string, ext: string) { 12 | return super.run(codeBlockContent, outputter, cmd, `-std=${this.settings.clingStd} ${cmdArgs}`, "cpp"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/executors/FSharpExecutor.ts: -------------------------------------------------------------------------------- 1 | import NonInteractiveCodeExecutor from './NonInteractiveCodeExecutor'; 2 | import type {Outputter} from "src/output/Outputter"; 3 | import type {ExecutorSettings} from "src/settings/Settings"; 4 | 5 | export default class FSharpExecutor extends NonInteractiveCodeExecutor { 6 | constructor(settings: ExecutorSettings, file: string) { 7 | super(settings, false, file, "fsharp"); 8 | } 9 | 10 | override run(codeBlockContent: string, outputter: Outputter, cmd: string, args: string, ext: string) { 11 | return super.run(codeBlockContent, outputter, cmd, args, ext); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { "node": true }, 5 | "plugins": [ 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-unused-vars": "off", 18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 19 | "@typescript-eslint/ban-ts-comment": "off", 20 | "no-prototype-builtins": "off", 21 | "@typescript-eslint/no-empty-function": "off" 22 | } 23 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: features request 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/executors/CExecutor.ts: -------------------------------------------------------------------------------- 1 | import type {Outputter} from "src/output/Outputter"; 2 | import type {ExecutorSettings} from "src/settings/Settings"; 3 | import ClingExecutor from './ClingExecutor'; 4 | 5 | export default class CExecutor extends ClingExecutor { 6 | 7 | constructor(settings: ExecutorSettings, file: string) { 8 | super(settings, file, "c"); 9 | } 10 | 11 | override run(codeBlockContent: string, outputter: Outputter, cmd: string, cmdArgs: string, ext: string) { 12 | const install_path = this.settings[`clingPath`]; 13 | if (install_path.endsWith("cling") || install_path.endsWith("cling.exe")) { 14 | return super.run(codeBlockContent, outputter, cmd, this.settings[`cArgs`], "cpp"); 15 | } else { 16 | return super.run(codeBlockContent, outputter, cmd, this.settings[`cArgs`], "c"); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/settings/per-lang/makeGoSettings.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from "obsidian"; 2 | import { SettingsTab } from "../SettingsTab"; 3 | 4 | export default (tab: SettingsTab, containerEl: HTMLElement) => { 5 | containerEl.createEl('h3', { text: 'Golang Settings' }); 6 | new Setting(containerEl) 7 | .setName('Golang Path') 8 | .setDesc('The path to your Golang installation.') 9 | .addText(text => text 10 | .setValue(tab.plugin.settings.golangPath) 11 | .onChange(async (value) => { 12 | const sanitized = tab.sanitizePath(value); 13 | tab.plugin.settings.golangPath = sanitized; 14 | console.log('Golang path set to: ' + sanitized); 15 | await tab.plugin.saveSettings(); 16 | })); 17 | tab.makeInjectSetting(containerEl, "go"); 18 | } -------------------------------------------------------------------------------- /src/settings/per-lang/makeRustSettings.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from "obsidian"; 2 | import { SettingsTab } from "../SettingsTab"; 3 | 4 | export default (tab: SettingsTab, containerEl: HTMLElement) => { 5 | containerEl.createEl('h3', { text: 'Rust Settings' }); 6 | new Setting(containerEl) 7 | .setName('Cargo Path') 8 | .setDesc('The path to your Cargo installation.') 9 | .addText(text => text 10 | .setValue(tab.plugin.settings.cargoPath) 11 | .onChange(async (value) => { 12 | const sanitized = tab.sanitizePath(value); 13 | tab.plugin.settings.cargoPath = sanitized; 14 | console.log('Cargo path set to: ' + sanitized); 15 | await tab.plugin.saveSettings(); 16 | })); 17 | tab.makeInjectSetting(containerEl, "rust"); 18 | } -------------------------------------------------------------------------------- /src/svgs/loadEllipses.ts: -------------------------------------------------------------------------------- 1 | import parseHTML from "./parseHTML"; 2 | 3 | const svg = parseHTML(` 4 | 11 | 12 | 13 | 14 | `); 15 | 16 | export default () => { 17 | return svg.cloneNode(true); 18 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve the plugin 4 | title: "[BUG] Short Description" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Software Version** 14 | Specify your *OS*, *Plugin Version*, and *Obsidian Version*! Issues without this will be removed without further comment, since the problem can't be reproduced without this information. 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behavior: 18 | 1. Go to '...' 19 | 2. Click on '....' 20 | 3. Scroll down to '....' 21 | 4. See error 22 | 23 | **Expected behavior** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Screenshots** 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /images/figure_time_of_day.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 14 | 16 | Thetimeis13:27:48. 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/settings/languageDisplayName.ts: -------------------------------------------------------------------------------- 1 | import {LanguageId} from "src/main"; 2 | 3 | export const DISPLAY_NAMES: Record = { 4 | cpp: "C++", 5 | cs: "C#", 6 | go: "Golang", 7 | groovy: "Groovy", 8 | haskell: "Haskell", 9 | java: "Java", 10 | js: "Javascript", 11 | kotlin: "Kotlin", 12 | latex: "LaTeX", 13 | lua: "Lua", 14 | mathematica: "Mathematica", 15 | php: "PHP", 16 | powershell: "Powershell", 17 | prolog: "Prolog", 18 | python: "Python", 19 | r: "R", 20 | rust: "Rust", 21 | shell: "Shell", 22 | batch: "Batch", 23 | ts: "Typescript", 24 | scala: "Scala", 25 | swift: "Swift", 26 | racket: "Racket", 27 | c: "C", 28 | fsharp: "F#", 29 | ruby: "Ruby", 30 | dart: "Dart", 31 | lean: "Lean", 32 | sql: "SQL", 33 | octave: "Octave", 34 | maxima: "Maxima", 35 | applescript: "Applescript", 36 | zig: "Zig", 37 | ocaml: "OCaml", 38 | } as const; 39 | -------------------------------------------------------------------------------- /src/settings/per-lang/makePrologSettings.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from "obsidian"; 2 | import { SettingsTab } from "../SettingsTab"; 3 | 4 | export default (tab: SettingsTab, containerEl: HTMLElement) => { 5 | containerEl.createEl('h3', { text: 'Prolog Settings' }); 6 | new Setting(containerEl) 7 | .setName('Prolog Answer Limit') 8 | .setDesc('Maximal number of answers to be returned by the Prolog engine. tab is to prevent creating too huge texts in the notebook.') 9 | .addText(text => text 10 | .setValue("" + tab.plugin.settings.maxPrologAnswers) 11 | .onChange(async (value) => { 12 | if (Number(value) * 1000) { 13 | console.log('Prolog answer limit set to: ' + value); 14 | tab.plugin.settings.maxPrologAnswers = Number(value); 15 | } 16 | await tab.plugin.saveSettings(); 17 | })); 18 | tab.makeInjectSetting(containerEl, "prolog"); 19 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "execute-code", 3 | "version": "2.1.2", 4 | "description": "Allows you to execute code snippets within a note. Support C, C++, Python, R, JavaScript, TypeScript, LaTeX, SQL, and many more.", 5 | "main": "src/main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "version": "node version-bump.mjs && git add manifest.json versions.json" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@types/node": "^18.7.18", 16 | "@typescript-eslint/eslint-plugin": "^5.38.0", 17 | "@typescript-eslint/parser": "^5.38.0", 18 | "builtin-modules": "^3.3.0", 19 | "esbuild": "0.15.8", 20 | "obsidian": "latest", 21 | "obsidian-typings": "^2.34.0", 22 | "parallelshell": "^3.0.1", 23 | "tslib": "2.4.0", 24 | "typescript": "^4.8.3" 25 | }, 26 | "dependencies": { 27 | "g": "^2.0.1", 28 | "json5": "^2.2.2", 29 | "moment": ">=2.29.4", 30 | "original-fs": "^1.2.0", 31 | "tau-prolog": "^0.3.4" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Tim Wibiral 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/settings/per-lang/makeDartSettings.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from "obsidian"; 2 | import { SettingsTab } from "../SettingsTab"; 3 | 4 | export default (tab: SettingsTab, containerEl: HTMLElement) => { 5 | containerEl.createEl('h3', { text: 'Dart Settings' }); 6 | new Setting(containerEl) 7 | .setName('dart path') 8 | .addText(text => text 9 | .setValue(tab.plugin.settings.dartPath) 10 | .onChange(async (value) => { 11 | const sanitized = tab.sanitizePath(value); 12 | tab.plugin.settings.dartPath = sanitized; 13 | console.log('dart path set to: ' + sanitized); 14 | await tab.plugin.saveSettings(); 15 | })); 16 | new Setting(containerEl) 17 | .setName('Dart arguments') 18 | .addText(text => text 19 | .setValue(tab.plugin.settings.dartArgs) 20 | .onChange(async (value) => { 21 | tab.plugin.settings.dartArgs = value; 22 | console.log('Dart args set to: ' + value); 23 | await tab.plugin.saveSettings(); 24 | })); 25 | tab.makeInjectSetting(containerEl, "dart"); 26 | } 27 | -------------------------------------------------------------------------------- /src/settings/per-lang/makeLuaSettings.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from "obsidian"; 2 | import { SettingsTab } from "../SettingsTab"; 3 | 4 | export default (tab: SettingsTab, containerEl: HTMLElement) => { 5 | containerEl.createEl('h3', { text: 'Lua Settings' }); 6 | new Setting(containerEl) 7 | .setName('lua path') 8 | .addText(text => text 9 | .setValue(tab.plugin.settings.luaPath) 10 | .onChange(async (value) => { 11 | const sanitized = tab.sanitizePath(value); 12 | tab.plugin.settings.luaPath = sanitized; 13 | console.log('lua path set to: ' + sanitized); 14 | await tab.plugin.saveSettings(); 15 | })); 16 | new Setting(containerEl) 17 | .setName('Lua arguments') 18 | .addText(text => text 19 | .setValue(tab.plugin.settings.luaArgs) 20 | .onChange(async (value) => { 21 | tab.plugin.settings.luaArgs = value; 22 | console.log('Lua args set to: ' + value); 23 | await tab.plugin.saveSettings(); 24 | })); 25 | tab.makeInjectSetting(containerEl, "lua"); 26 | } -------------------------------------------------------------------------------- /src/settings/per-lang/makeCsSettings.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from "obsidian"; 2 | import { SettingsTab } from "../SettingsTab"; 3 | 4 | export default (tab: SettingsTab, containerEl: HTMLElement) => { 5 | containerEl.createEl('h3', { text: 'CSharp Settings' }); 6 | new Setting(containerEl) 7 | .setName('dotnet path') 8 | .addText(text => text 9 | .setValue(tab.plugin.settings.csPath) 10 | .onChange(async (value) => { 11 | const sanitized = tab.sanitizePath(value); 12 | tab.plugin.settings.csPath = sanitized; 13 | console.log('dotnet path set to: ' + sanitized); 14 | await tab.plugin.saveSettings(); 15 | })); 16 | new Setting(containerEl) 17 | .setName('CSharp arguments') 18 | .addText(text => text 19 | .setValue(tab.plugin.settings.csArgs) 20 | .onChange(async (value) => { 21 | tab.plugin.settings.csArgs = value; 22 | console.log('CSharp args set to: ' + value); 23 | await tab.plugin.saveSettings(); 24 | })); 25 | tab.makeInjectSetting(containerEl, "cs"); 26 | } -------------------------------------------------------------------------------- /src/settings/per-lang/makeSQLSettings.ts: -------------------------------------------------------------------------------- 1 | import {Setting} from "obsidian"; 2 | import {SettingsTab} from "../SettingsTab"; 3 | 4 | export default (tab: SettingsTab, containerEl: HTMLElement) => { 5 | containerEl.createEl('h3', {text: 'SQL Settings'}); 6 | new Setting(containerEl) 7 | .setName('SQL path') 8 | .setDesc("Path to your SQL installation. You can select the SQL dialect you prefer but you need to set the right arguments by yourself.") 9 | .addText(text => text 10 | .setValue(tab.plugin.settings.sqlPath) 11 | .onChange(async (value) => { 12 | const sanitized = tab.sanitizePath(value); 13 | tab.plugin.settings.sqlPath = sanitized; 14 | console.log('ruby path set to: ' + sanitized); 15 | await tab.plugin.saveSettings(); 16 | })); 17 | new Setting(containerEl) 18 | .setName('SQL arguments') 19 | .setDesc('Set the right arguments for your database.') 20 | .addText(text => text 21 | .setValue(tab.plugin.settings.sqlArgs) 22 | .onChange(async (value) => { 23 | tab.plugin.settings.sqlArgs = value; 24 | console.log('SQL args set to: ' + value); 25 | await tab.plugin.saveSettings(); 26 | })); 27 | tab.makeInjectSetting(containerEl, "sql"); 28 | } 29 | -------------------------------------------------------------------------------- /src/settings/per-lang/makeLeanSettings.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from "obsidian"; 2 | import { SettingsTab } from "../SettingsTab"; 3 | 4 | export default (tab: SettingsTab, containerEl: HTMLElement) => { 5 | containerEl.createEl('h3', { text: 'Lean Settings' }); 6 | new Setting(containerEl) 7 | .setName('lean path') 8 | .addText(text => text 9 | .setValue(tab.plugin.settings.leanPath) 10 | .onChange(async (value) => { 11 | const sanitized = tab.sanitizePath(value); 12 | tab.plugin.settings.leanPath = sanitized; 13 | console.log('lean path set to: ' + sanitized); 14 | await tab.plugin.saveSettings(); 15 | })); 16 | new Setting(containerEl) 17 | .setName('Lean arguments') 18 | .addText(text => text 19 | .setValue(tab.plugin.settings.leanArgs) 20 | .onChange(async (value) => { 21 | tab.plugin.settings.leanArgs = value; 22 | console.log('Lean args set to: ' + value); 23 | await tab.plugin.saveSettings(); 24 | })); 25 | tab.makeInjectSetting(containerEl, "lean"); 26 | } 27 | -------------------------------------------------------------------------------- /src/settings/per-lang/makeTsSettings.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from "obsidian"; 2 | import { SettingsTab } from "../SettingsTab"; 3 | 4 | export default (tab: SettingsTab, containerEl: HTMLElement) => { 5 | containerEl.createEl('h3', { text: 'TypeScript Settings' }); 6 | new Setting(containerEl) 7 | .setName('ts-node path') 8 | .addText(text => text 9 | .setValue(tab.plugin.settings.tsPath) 10 | .onChange(async (value) => { 11 | const sanitized = tab.sanitizePath(value); 12 | tab.plugin.settings.tsPath = sanitized; 13 | console.log('ts-node path set to: ' + sanitized); 14 | await tab.plugin.saveSettings(); 15 | })); 16 | new Setting(containerEl) 17 | .setName('TypeScript arguments') 18 | .addText(text => text 19 | .setValue(tab.plugin.settings.tsArgs) 20 | .onChange(async (value) => { 21 | tab.plugin.settings.tsArgs = value; 22 | console.log('TypeScript args set to: ' + value); 23 | await tab.plugin.saveSettings(); 24 | })); 25 | tab.makeInjectSetting(containerEl, "ts"); 26 | } -------------------------------------------------------------------------------- /src/settings/per-lang/makePhpSettings.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from "obsidian"; 2 | import { SettingsTab } from "../SettingsTab"; 3 | 4 | export default (tab: SettingsTab, containerEl: HTMLElement) => { 5 | containerEl.createEl('h3', { text: 'PHP Settings' }); 6 | new Setting(containerEl) 7 | .setName('php path') 8 | .setDesc("Path to your php installation") 9 | .addText(text => text 10 | .setValue(tab.plugin.settings.phpPath) 11 | .onChange(async (value) => { 12 | const sanitized = tab.sanitizePath(value); 13 | tab.plugin.settings.phpPath = sanitized; 14 | console.log('php path set to: ' + sanitized); 15 | await tab.plugin.saveSettings(); 16 | })); 17 | new Setting(containerEl) 18 | .setName('php arguments') 19 | .addText(text => text 20 | .setValue(tab.plugin.settings.phpArgs) 21 | .onChange(async (value) => { 22 | tab.plugin.settings.phpArgs = value; 23 | console.log('php args set to: ' + value); 24 | await tab.plugin.saveSettings(); 25 | })); 26 | tab.makeInjectSetting(containerEl, "php"); 27 | } 28 | -------------------------------------------------------------------------------- /src/settings/per-lang/makeZigSettings.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from "obsidian"; 2 | import { SettingsTab } from "../SettingsTab"; 3 | 4 | export default (tab: SettingsTab, containerEl: HTMLElement) => { 5 | containerEl.createEl('h3', { text: 'Zig Settings' }); 6 | new Setting(containerEl) 7 | .setName('zig path') 8 | .setDesc("Path to your zig installation") 9 | .addText(text => text 10 | .setValue(tab.plugin.settings.zigPath) 11 | .onChange(async (value) => { 12 | const sanitized = tab.sanitizePath(value); 13 | tab.plugin.settings.zigPath = sanitized; 14 | console.log('zig path set to: ' + sanitized); 15 | await tab.plugin.saveSettings(); 16 | })); 17 | new Setting(containerEl) 18 | .setName('zig arguments') 19 | .addText(text => text 20 | .setValue(tab.plugin.settings.zigArgs) 21 | .onChange(async (value) => { 22 | tab.plugin.settings.zigArgs = value; 23 | console.log('zig args set to: ' + value); 24 | await tab.plugin.saveSettings(); 25 | })); 26 | tab.makeInjectSetting(containerEl, "zig"); 27 | } 28 | -------------------------------------------------------------------------------- /src/settings/per-lang/makeRubySettings.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from "obsidian"; 2 | import { SettingsTab } from "../SettingsTab"; 3 | 4 | export default (tab: SettingsTab, containerEl: HTMLElement) => { 5 | containerEl.createEl('h3', { text: 'Ruby Settings' }); 6 | new Setting(containerEl) 7 | .setName('ruby path') 8 | .setDesc("Path to your ruby installation") 9 | .addText(text => text 10 | .setValue(tab.plugin.settings.rubyPath) 11 | .onChange(async (value) => { 12 | const sanitized = tab.sanitizePath(value); 13 | tab.plugin.settings.rubyPath = sanitized; 14 | console.log('ruby path set to: ' + sanitized); 15 | await tab.plugin.saveSettings(); 16 | })); 17 | new Setting(containerEl) 18 | .setName('ruby arguments') 19 | .addText(text => text 20 | .setValue(tab.plugin.settings.rubyArgs) 21 | .onChange(async (value) => { 22 | tab.plugin.settings.rubyArgs = value; 23 | console.log('ruby args set to: ' + value); 24 | await tab.plugin.saveSettings(); 25 | })); 26 | tab.makeInjectSetting(containerEl, "ruby"); 27 | } 28 | -------------------------------------------------------------------------------- /src/settings/per-lang/makeOCamlSettings.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from "obsidian"; 2 | import { SettingsTab } from "../SettingsTab"; 3 | 4 | export default (tab: SettingsTab, containerEl: HTMLElement) => { 5 | containerEl.createEl('h3', { text: 'OCaml Settings' }); 6 | new Setting(containerEl) 7 | .setName('ocaml path') 8 | .setDesc("Path to your ocaml installation") 9 | .addText(text => text 10 | .setValue(tab.plugin.settings.ocamlPath) 11 | .onChange(async (value) => { 12 | const sanitized = tab.sanitizePath(value); 13 | tab.plugin.settings.ocamlPath = sanitized; 14 | console.log('ocaml path set to: ' + sanitized); 15 | await tab.plugin.saveSettings(); 16 | })); 17 | new Setting(containerEl) 18 | .setName('ocaml arguments') 19 | .addText(text => text 20 | .setValue(tab.plugin.settings.ocamlArgs) 21 | .onChange(async (value) => { 22 | tab.plugin.settings.ocamlArgs = value; 23 | console.log('ocaml args set to: ' + value); 24 | await tab.plugin.saveSettings(); 25 | })); 26 | tab.makeInjectSetting(containerEl, "ocaml"); 27 | } 28 | -------------------------------------------------------------------------------- /src/settings/per-lang/makeSwiftSettings.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from "obsidian"; 2 | import { SettingsTab } from "../SettingsTab"; 3 | 4 | export default (tab: SettingsTab, containerEl: HTMLElement) => { 5 | containerEl.createEl('h3', { text: 'Swift Settings' }); 6 | new Setting(containerEl) 7 | .setName('Swift path') 8 | .setDesc('The path to your Swift installation.') 9 | .addText(text => text 10 | .setValue(tab.plugin.settings.swiftPath) 11 | .onChange(async (value) => { 12 | const sanitized = tab.sanitizePath(value); 13 | tab.plugin.settings.swiftPath = sanitized; 14 | console.log('Swift path set to: ' + sanitized); 15 | await tab.plugin.saveSettings(); 16 | })); 17 | new Setting(containerEl) 18 | .setName('Swift arguments') 19 | .addText(text => text 20 | .setValue(tab.plugin.settings.swiftArgs) 21 | .onChange(async (value) => { 22 | tab.plugin.settings.swiftArgs = value; 23 | console.log('Swift args set to: ' + value); 24 | await tab.plugin.saveSettings(); 25 | })); 26 | tab.makeInjectSetting(containerEl, "swift"); 27 | } -------------------------------------------------------------------------------- /src/executors/NodeJSExecutor.ts: -------------------------------------------------------------------------------- 1 | import {ChildProcessWithoutNullStreams} from "child_process"; 2 | import {ExecutorSettings} from "src/settings/Settings"; 3 | import ReplExecutor from "./ReplExecutor.js"; 4 | 5 | 6 | export default class NodeJSExecutor extends ReplExecutor { 7 | 8 | process: ChildProcessWithoutNullStreams 9 | 10 | constructor(settings: ExecutorSettings, file: string) { 11 | const args = settings.nodeArgs ? settings.nodeArgs.split(" ") : []; 12 | 13 | args.unshift(`-e`, `require("repl").start({prompt: "", preview: false, ignoreUndefined: true}).on("exit", ()=>process.exit())`); 14 | 15 | super(settings, settings.nodePath, args, file, "js"); 16 | } 17 | 18 | /** 19 | * Writes a single newline to ensure that the stdin is set up correctly. 20 | */ 21 | async setup() { 22 | this.process.stdin.write("\n"); 23 | } 24 | 25 | wrapCode(code: string, finishSigil: string): string { 26 | return `try { eval(${JSON.stringify(code)}); }` + 27 | `catch(e) { console.error(e); }` + 28 | `finally { process.stdout.write(${JSON.stringify(finishSigil)}); }` + 29 | "\n"; 30 | } 31 | 32 | removePrompts(output: string, source: "stdout" | "stderr"): string { 33 | return output; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/settings/per-lang/makeMaximaSettings.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from "obsidian"; 2 | import { SettingsTab } from "../SettingsTab"; 3 | 4 | export default (tab: SettingsTab, containerEl: HTMLElement) => { 5 | containerEl.createEl('h3', { text: 'Maxima Settings' }); 6 | new Setting(containerEl) 7 | .setName('Maxima path') 8 | .setDesc('The path to your Maxima installation.') 9 | .addText(text => text 10 | .setValue(tab.plugin.settings.maximaPath) 11 | .onChange(async (value) => { 12 | const sanitized = tab.sanitizePath(value); 13 | tab.plugin.settings.maximaPath = sanitized; 14 | console.log('Maxima path set to: ' + sanitized); 15 | await tab.plugin.saveSettings(); 16 | })); 17 | new Setting(containerEl) 18 | .setName('Maxima arguments') 19 | .addText(text => text 20 | .setValue(tab.plugin.settings.maximaArgs) 21 | .onChange(async (value) => { 22 | tab.plugin.settings.maximaArgs = value; 23 | console.log('Maxima args set to: ' + value); 24 | await tab.plugin.saveSettings(); 25 | })); 26 | tab.makeInjectSetting(containerEl, "maxima"); 27 | } 28 | -------------------------------------------------------------------------------- /src/settings/per-lang/makeOctaveSettings.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from "obsidian"; 2 | import { SettingsTab } from "../SettingsTab"; 3 | 4 | export default (tab: SettingsTab, containerEl: HTMLElement) => { 5 | containerEl.createEl('h3', { text: 'Octave Settings' }); 6 | new Setting(containerEl) 7 | .setName('Octave path') 8 | .setDesc('The path to your Octave installation.') 9 | .addText(text => text 10 | .setValue(tab.plugin.settings.octavePath) 11 | .onChange(async (value) => { 12 | const sanitized = tab.sanitizePath(value); 13 | tab.plugin.settings.octavePath = sanitized; 14 | console.log('Octave path set to: ' + sanitized); 15 | await tab.plugin.saveSettings(); 16 | })); 17 | new Setting(containerEl) 18 | .setName('Octave arguments') 19 | .addText(text => text 20 | .setValue(tab.plugin.settings.octaveArgs) 21 | .onChange(async (value) => { 22 | tab.plugin.settings.octaveArgs = value; 23 | console.log('Octave args set to: ' + value); 24 | await tab.plugin.saveSettings(); 25 | })); 26 | tab.makeInjectSetting(containerEl, "octave"); 27 | } 28 | -------------------------------------------------------------------------------- /src/settings/per-lang/makeScalaSettings.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from "obsidian"; 2 | import { SettingsTab } from "../SettingsTab"; 3 | 4 | export default (tab: SettingsTab, containerEl: HTMLElement) => { 5 | containerEl.createEl('h3', { text: 'Scala Settings' }); 6 | new Setting(containerEl) 7 | .setName('scala path') 8 | .setDesc("Path to your scala installation") 9 | .addText(text => text 10 | .setValue(tab.plugin.settings.scalaPath) 11 | .onChange(async (value) => { 12 | const sanitized = tab.sanitizePath(value); 13 | tab.plugin.settings.scalaPath = sanitized; 14 | console.log('scala path set to: ' + sanitized); 15 | await tab.plugin.saveSettings(); 16 | })); 17 | new Setting(containerEl) 18 | .setName('Scala arguments') 19 | .addText(text => text 20 | .setValue(tab.plugin.settings.scalaArgs) 21 | .onChange(async (value) => { 22 | tab.plugin.settings.scalaArgs = value; 23 | console.log('Scala args set to: ' + value); 24 | await tab.plugin.saveSettings(); 25 | })); 26 | tab.makeInjectSetting(containerEl, "scala"); 27 | } 28 | -------------------------------------------------------------------------------- /src/executors/python/wrapPython.ts: -------------------------------------------------------------------------------- 1 | export const PLT_DEFAULT_BACKEND_PY_VAR = "OBSIDIAN_EXECUTE_CODE_MATPLOTLIB_DEFAULT_BACKEND"; 2 | 3 | export default (code: string, globalsName: string, printName: string, finishSigil: string, embedPlots: boolean) => 4 | /*python*/` 5 | ${embedPlots ? 6 | // Use 'agg' raster non-interactive renderer to prevent freezing the runtime on plt.show() 7 | /*python*/` 8 | try: 9 | matplotlib.use('agg') 10 | except: 11 | pass 12 | ` : 13 | // Use the default renderer stored in the python variable from 'async setup()' in 'PythonExecutor' when not embedding 14 | /*python*/` 15 | try: 16 | matplotlib.use(${PLT_DEFAULT_BACKEND_PY_VAR}) 17 | except: 18 | pass 19 | `} 20 | 21 | try: 22 | try: 23 | ${printName}(eval( 24 | compile(${JSON.stringify(code.replace(/\r\n/g, "\n") + "\n")}, "", "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 | 12 | 14 | 17 | 21 | 22 | 23 | 25 | 27 | HelloWorld! 37 | 38 | 39 | 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 | 12 | 14 | 17 | 21 | 22 | 25 | 29 | 30 | 31 | 33 | 35 | Thetimeis13:27:48. 45 | 46 | 48 | Thetimeis13:27:48. 58 | 59 | 60 | 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 `Image found at path ${imagePath}.`;
265 | 
266 | 	return `Image found at path ${imagePath}.`;
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 | 


--------------------------------------------------------------------------------