├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature_request.md ├── .gitignore ├── .npmrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── esbuild.config.mjs ├── images ├── batch_settings.png ├── execute_code_example.gif ├── figure_include_attachments.svg ├── figure_minimal_example.svg ├── figure_sidenotes_comparison.svg ├── figure_sum_of_two_poisson_distributions.svg ├── figure_time_of_day.svg ├── magic_example.png ├── path_location_settings.png ├── path_location_shell.png └── plotting_example.png ├── manifest.json ├── package-lock.json ├── package.json ├── src ├── CodeBlockArgs.ts ├── ExecutorContainer.ts ├── ExecutorManagerView.ts ├── ReleaseNoteModal.ts ├── RunButton.ts ├── Vault.ts ├── executors │ ├── AsyncExecutor.ts │ ├── CExecutor.ts │ ├── ClingExecutor.ts │ ├── CppExecutor.ts │ ├── Executor.ts │ ├── FSharpExecutor.ts │ ├── LatexExecutor.ts │ ├── NodeJSExecutor.ts │ ├── NonInteractiveCodeExecutor.ts │ ├── PowerShellOnWindowsExecutor.ts │ ├── PrologExecutor.ts │ ├── RExecutor.ts │ ├── ReplExecutor.ts │ ├── killWithChildren.ts │ └── python │ │ ├── PythonExecutor.ts │ │ └── wrapPython.ts ├── main.ts ├── output │ ├── FileAppender.ts │ ├── LatexInserter.ts │ ├── Outputter.ts │ └── RegExpUtilities.ts ├── runAllCodeBlocks.ts ├── settings │ ├── Settings.ts │ ├── SettingsTab.ts │ ├── languageDisplayName.ts │ └── per-lang │ │ ├── makeApplescriptSettings.ts │ │ ├── makeBatchSettings.ts │ │ ├── makeCSettings.ts │ │ ├── makeCppSettings.ts │ │ ├── makeCsSettings.ts │ │ ├── makeDartSettings.ts │ │ ├── makeFSharpSettings.ts │ │ ├── makeGoSettings.ts │ │ ├── makeGroovySettings.ts │ │ ├── makeHaskellSettings.ts │ │ ├── makeJavaSettings.ts │ │ ├── makeJsSettings.ts │ │ ├── makeKotlinSettings.ts │ │ ├── makeLatexSettings.ts │ │ ├── makeLeanSettings.ts │ │ ├── makeLuaSettings.ts │ │ ├── makeMathematicaSettings.ts │ │ ├── makeMaximaSettings.ts │ │ ├── makeOCamlSettings.ts │ │ ├── makeOctaveSettings.ts │ │ ├── makePhpSettings.ts │ │ ├── makePowershellSettings.ts │ │ ├── makePrologSettings.ts │ │ ├── makePythonSettings.ts │ │ ├── makeRSettings.ts │ │ ├── makeRacketSettings.ts │ │ ├── makeRubySettings.ts │ │ ├── makeRustSettings.ts │ │ ├── makeSQLSettings.ts │ │ ├── makeScalaSettings.ts │ │ ├── makeShellSettings.ts │ │ ├── makeSwiftSettings.ts │ │ ├── makeTsSettings.ts │ │ └── makeZigSettings.ts ├── styles.css ├── svgs │ ├── loadEllipses.ts │ ├── loadSpinner.ts │ └── parseHTML.ts └── transforms │ ├── CodeInjector.ts │ ├── LatexFigureName.ts │ ├── LatexFontHandler.ts │ ├── LatexTransformer.ts │ ├── Magic.ts │ ├── TransformCode.ts │ └── windowsPathToWsl.ts ├── tsconfig.json ├── version-bump.mjs └── versions.json /.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 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | npm node_modules 2 | build -------------------------------------------------------------------------------- /.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/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [twibiral] 2 | buy_me_a_coffee: timwibiral 3 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /images/batch_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twibiral/obsidian-execute-code/3331392f006d2fc1f65e0e68e74b771f78481295/images/batch_settings.png -------------------------------------------------------------------------------- /images/execute_code_example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twibiral/obsidian-execute-code/3331392f006d2fc1f65e0e68e74b771f78481295/images/execute_code_example.gif -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /images/figure_minimal_example.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 14 | 17 | 21 | 22 | 23 | 25 | 27 | HelloWorld! 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /images/figure_time_of_day.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 14 | 16 | Thetimeis13:27:48. 25 | 26 | 27 | -------------------------------------------------------------------------------- /images/magic_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twibiral/obsidian-execute-code/3331392f006d2fc1f65e0e68e74b771f78481295/images/magic_example.png -------------------------------------------------------------------------------- /images/path_location_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twibiral/obsidian-execute-code/3331392f006d2fc1f65e0e68e74b771f78481295/images/path_location_settings.png -------------------------------------------------------------------------------- /images/path_location_shell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twibiral/obsidian-execute-code/3331392f006d2fc1f65e0e68e74b771f78481295/images/path_location_shell.png -------------------------------------------------------------------------------- /images/plotting_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twibiral/obsidian-execute-code/3331392f006d2fc1f65e0e68e74b771f78481295/images/plotting_example.png -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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 | }


--------------------------------------------------------------------------------
/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/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/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 | }


--------------------------------------------------------------------------------
/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/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/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/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/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/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 | }


--------------------------------------------------------------------------------
/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 | }


--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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/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/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 | }


--------------------------------------------------------------------------------
/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 | }


--------------------------------------------------------------------------------
/src/svgs/parseHTML.ts:
--------------------------------------------------------------------------------
1 | export default (html: string) => {
2 |     let container = document.createElement("div");
3 |     container.innerHTML = html;
4 |     return container.firstElementChild;
5 | }


--------------------------------------------------------------------------------
/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/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/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/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/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 | 


--------------------------------------------------------------------------------
/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/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 | }


--------------------------------------------------------------------------------
/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 | }


--------------------------------------------------------------------------------
/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 | 


--------------------------------------------------------------------------------
/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 | }


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