mirror of
https://github.com/geeknees/obsidian-ruby-wasm-plugin.git
synced 2026-07-22 10:10:24 +00:00
feat: add REPL-style Ruby code block runner with captured output
- Add Run button for rendered ruby fences and show stdout/stderr/result/error separately - Support running full ruby code block when no selection is made and insert formatted output - Implement version-aware ruby.wasm runtime loading with queued execution and output capture - Document new features in README and remove obsolete manifest/styles assets
This commit is contained in:
parent
c82a236f5a
commit
75a4ebe371
7 changed files with 534 additions and 89 deletions
|
|
@ -16,6 +16,14 @@ Currently supported:
|
|||
- Ruby 3.3
|
||||
- Ruby 3.2
|
||||
|
||||
## Features
|
||||
|
||||
- Run the selected Ruby code from the editor
|
||||
- Run the whole current Ruby code block when nothing is selected
|
||||
- Show stdout, stderr, return value, and errors separately
|
||||
- Add a Run button under rendered `ruby` code blocks for a lightweight REPL flow
|
||||
- Choose the Ruby runtime version from plugin settings
|
||||
|
||||
# How to use
|
||||
|
||||
Select a code block
|
||||
|
|
|
|||
542
main.ts
542
main.ts
|
|
@ -1,10 +1,13 @@
|
|||
/*
|
||||
ABOUTME: Registers Obsidian commands that run selected Ruby code in a modal or inline.
|
||||
ABOUTME: Lazily downloads the ruby.wasm runtime so the release bundle stays sync-friendly.
|
||||
ABOUTME: Runs Ruby code from the editor and rendered markdown using ruby.wasm runtimes.
|
||||
ABOUTME: Adds version-aware runtime loading, block execution, and inline REPL-style output.
|
||||
*/
|
||||
import {
|
||||
App,
|
||||
Editor,
|
||||
EditorPosition,
|
||||
MarkdownPostProcessorContext,
|
||||
MarkdownRenderChild,
|
||||
Modal,
|
||||
Notice,
|
||||
Plugin,
|
||||
|
|
@ -12,10 +15,10 @@ import {
|
|||
Setting,
|
||||
requestUrl,
|
||||
} from "obsidian";
|
||||
import { DefaultRubyVM } from "@ruby/wasm-wasi/dist/browser";
|
||||
import type { RubyVM } from "@ruby/wasm-wasi";
|
||||
import { RubyVM, consolePrinter } from "@ruby/wasm-wasi";
|
||||
|
||||
const RUBY_WASM_PACKAGE_VERSION = "2.9.3-2.9.4";
|
||||
const RUBY_LANGUAGES = new Set(["ruby", "rb"]);
|
||||
|
||||
const RUBY_RUNTIMES = {
|
||||
head: {
|
||||
|
|
@ -46,6 +49,44 @@ interface RubyWasmPluginSettings {
|
|||
rubyVersion: RubyRuntimeId;
|
||||
}
|
||||
|
||||
interface RubyCodeBlock {
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
code: string;
|
||||
}
|
||||
|
||||
interface RubyExecutionError {
|
||||
title: string;
|
||||
details: string;
|
||||
}
|
||||
|
||||
interface RubyExecutionResult {
|
||||
code: string;
|
||||
runtimeLabel: string;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
result: string;
|
||||
hasResult: boolean;
|
||||
error: RubyExecutionError | null;
|
||||
}
|
||||
|
||||
interface RubyRuntimeCapture {
|
||||
stdout: string[];
|
||||
stderr: string[];
|
||||
}
|
||||
|
||||
interface RubyRuntimeSession {
|
||||
vm: RubyVM;
|
||||
setCapture: (capture: RubyRuntimeCapture | null) => void;
|
||||
}
|
||||
|
||||
interface EditorExecutionTarget {
|
||||
code: string;
|
||||
block: RubyCodeBlock | null;
|
||||
from: EditorPosition | null;
|
||||
to: EditorPosition | null;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: RubyWasmPluginSettings = {
|
||||
rubyVersion: "3.3",
|
||||
};
|
||||
|
|
@ -53,99 +94,188 @@ const DEFAULT_SETTINGS: RubyWasmPluginSettings = {
|
|||
const formatError = (error: unknown) =>
|
||||
error instanceof Error ? error.toString() : String(error);
|
||||
|
||||
const formatExecutionError = (error: unknown): RubyExecutionError => {
|
||||
if (error instanceof Error) {
|
||||
return {
|
||||
title: error.name || "Ruby error",
|
||||
details: error.stack ?? error.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: "Ruby error",
|
||||
details: String(error),
|
||||
};
|
||||
};
|
||||
|
||||
const isRubyRuntimeId = (value: string): value is RubyRuntimeId =>
|
||||
Object.prototype.hasOwnProperty.call(RUBY_RUNTIMES, value);
|
||||
|
||||
const compileWebAssemblyModule = (buffer: ArrayBuffer) =>
|
||||
WebAssembly.compile(buffer);
|
||||
|
||||
const normalizeRubyOutput = (chunks: string[]) => chunks.join("");
|
||||
|
||||
const parseFenceLine = (line: string) => {
|
||||
const match = line.trim().match(/^(```+|~~~+)\s*([^\s`~]*)?/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
fence: match[1],
|
||||
language: match[2]?.toLowerCase() ?? "",
|
||||
};
|
||||
};
|
||||
|
||||
const createExecutionSection = (
|
||||
containerEl: HTMLElement,
|
||||
label: string,
|
||||
content: string,
|
||||
className: string
|
||||
) => {
|
||||
const sectionEl = containerEl.createDiv({
|
||||
cls: `ruby-execution-section ${className}`.trim(),
|
||||
});
|
||||
sectionEl.createDiv({ cls: "ruby-execution-label", text: label });
|
||||
sectionEl.createEl("pre", {
|
||||
cls: "ruby-execution-body",
|
||||
text: content.length > 0 ? content : "(empty)",
|
||||
});
|
||||
};
|
||||
|
||||
const renderExecutionResult = (
|
||||
containerEl: HTMLElement,
|
||||
execution: RubyExecutionResult
|
||||
) => {
|
||||
containerEl.empty();
|
||||
containerEl.removeClass("is-empty");
|
||||
containerEl.removeClass("is-loading");
|
||||
containerEl.addClass("ruby-execution");
|
||||
|
||||
containerEl.createDiv({
|
||||
cls: "ruby-execution-runtime",
|
||||
text: execution.runtimeLabel,
|
||||
});
|
||||
|
||||
if (execution.stdout) {
|
||||
createExecutionSection(
|
||||
containerEl,
|
||||
"Stdout",
|
||||
execution.stdout,
|
||||
"is-stdout"
|
||||
);
|
||||
}
|
||||
|
||||
if (execution.stderr) {
|
||||
createExecutionSection(
|
||||
containerEl,
|
||||
"Stderr",
|
||||
execution.stderr,
|
||||
"is-stderr"
|
||||
);
|
||||
}
|
||||
|
||||
if (execution.hasResult) {
|
||||
createExecutionSection(
|
||||
containerEl,
|
||||
"Return value",
|
||||
execution.result,
|
||||
"is-result"
|
||||
);
|
||||
}
|
||||
|
||||
if (execution.error) {
|
||||
createExecutionSection(
|
||||
containerEl,
|
||||
execution.error.title,
|
||||
execution.error.details,
|
||||
"is-error"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renderExecutionLoading = (containerEl: HTMLElement, runtimeLabel: string) => {
|
||||
containerEl.empty();
|
||||
containerEl.removeClass("is-empty");
|
||||
containerEl.addClass("is-loading");
|
||||
containerEl.createDiv({
|
||||
cls: "ruby-execution-runtime",
|
||||
text: runtimeLabel,
|
||||
});
|
||||
containerEl.createDiv({
|
||||
cls: "ruby-execution-status",
|
||||
text: "Running Ruby code...",
|
||||
});
|
||||
};
|
||||
|
||||
const formatExecutionMarkdown = (execution: RubyExecutionResult) => {
|
||||
const sections = [`runtime: ${execution.runtimeLabel}`];
|
||||
|
||||
if (execution.stdout) {
|
||||
sections.push(`stdout:\n${execution.stdout}`);
|
||||
}
|
||||
|
||||
if (execution.stderr) {
|
||||
sections.push(`stderr:\n${execution.stderr}`);
|
||||
}
|
||||
|
||||
if (execution.hasResult) {
|
||||
sections.push(`result:\n${execution.result.length > 0 ? execution.result : "(empty)"}`);
|
||||
}
|
||||
|
||||
if (execution.error) {
|
||||
sections.push(`${execution.error.title}:\n${execution.error.details}`);
|
||||
}
|
||||
|
||||
return `\n\n\`\`\`text\n${sections.join("\n\n")}\n\`\`\``;
|
||||
};
|
||||
|
||||
export default class RubyWasmPlugin extends Plugin {
|
||||
settings: RubyWasmPluginSettings = DEFAULT_SETTINGS;
|
||||
private rubyVmPromise: Promise<RubyVM> | null = null;
|
||||
|
||||
// Function to check if inside code block
|
||||
isInCodeBlock = (editor: Editor, line: number) => {
|
||||
const totalLines = editor.lineCount();
|
||||
let inCodeBlock = true;
|
||||
for (let i = line; i >= 0; i--) {
|
||||
const lineText = editor.getLine(i).trim();
|
||||
if (lineText.startsWith("```")) {
|
||||
inCodeBlock = !inCodeBlock;
|
||||
}
|
||||
}
|
||||
for (let i = line + 1; i < totalLines; i++) {
|
||||
const lineText = editor.getLine(i).trim();
|
||||
if (lineText.startsWith("```")) {
|
||||
inCodeBlock = !inCodeBlock;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return inCodeBlock;
|
||||
};
|
||||
private rubyVmPromise: Promise<RubyRuntimeSession> | null = null;
|
||||
private executionQueue: Promise<void> = Promise.resolve();
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
this.addSettingTab(new RubyWasmSettingTab(this.app, this));
|
||||
|
||||
const showRuntimeLoadError = (error: unknown) => {
|
||||
new Notice(
|
||||
`Failed to load ${this.getSelectedRuntime().label}: ${formatError(
|
||||
error
|
||||
)}`
|
||||
);
|
||||
};
|
||||
|
||||
// This adds a simple command that can be triggered anywhere
|
||||
this.addCommand({
|
||||
id: "run-in-modal",
|
||||
name: "Run in Modal",
|
||||
editorCallback: async (editor: Editor) => {
|
||||
try {
|
||||
const { code, result } = await this.runRuby(editor);
|
||||
new CodeModal(this.app, code, result).open();
|
||||
const target = this.getEditorExecutionTarget(editor);
|
||||
const execution = await this.executeRuby(target.code);
|
||||
new CodeModal(this.app, execution).open();
|
||||
} catch (error) {
|
||||
showRuntimeLoadError(error);
|
||||
this.showRuntimeLoadError(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// This adds an editor command that can perform some operation on the current editor instance
|
||||
this.addCommand({
|
||||
id: "run-in-editor",
|
||||
name: "Run in Editor",
|
||||
editorCallback: async (editor: Editor) => {
|
||||
try {
|
||||
const { code, result } = await this.runRuby(editor);
|
||||
const cursorLine = editor.getCursor().line;
|
||||
const insideCode = this.isInCodeBlock(editor, cursorLine);
|
||||
|
||||
if (insideCode) {
|
||||
editor.replaceSelection(`${code}\n# => ${result}`);
|
||||
} else {
|
||||
editor.replaceSelection(
|
||||
`${code}\n\`\`\`\n${result}\n\`\`\``
|
||||
);
|
||||
}
|
||||
const target = this.getEditorExecutionTarget(editor);
|
||||
const execution = await this.executeRuby(target.code);
|
||||
this.insertExecutionResult(editor, target, execution);
|
||||
} catch (error) {
|
||||
showRuntimeLoadError(error);
|
||||
this.showRuntimeLoadError(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
|
||||
// Using this function will automatically remove the event listener when this plugin is disabled.
|
||||
// this.registerDomEvent(document, "click", (evt: MouseEvent) => {
|
||||
// console.log("click", evt);
|
||||
// });
|
||||
|
||||
// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
|
||||
// this.registerInterval(
|
||||
// window.setInterval(() => console.log("setInterval"), 5 * 60 * 1000)
|
||||
// );
|
||||
this.registerMarkdownPostProcessor((el, ctx) => {
|
||||
this.enhanceRubyCodeBlocks(el, ctx);
|
||||
});
|
||||
}
|
||||
|
||||
onunload() {
|
||||
this.rubyVmPromise = null;
|
||||
this.executionQueue = Promise.resolve();
|
||||
}
|
||||
|
||||
async updateRubyVersion(rubyVersion: RubyRuntimeId) {
|
||||
|
|
@ -162,18 +292,182 @@ export default class RubyWasmPlugin extends Plugin {
|
|||
new Notice(`Using ${this.getSelectedRuntime().label}.`);
|
||||
}
|
||||
|
||||
private async runRuby(editor: Editor) {
|
||||
const code = editor.getSelection();
|
||||
const vm = await this.getRubyVM();
|
||||
getSelectedRuntimeLabel() {
|
||||
return this.getSelectedRuntime().label;
|
||||
}
|
||||
|
||||
try {
|
||||
return { code, result: vm.eval(code).toString() };
|
||||
} catch (error) {
|
||||
return { code, result: formatError(error) };
|
||||
async executeRuby(code: string): Promise<RubyExecutionResult> {
|
||||
const runtimeLabel = this.getSelectedRuntime().label;
|
||||
const run = async () => {
|
||||
const runtime = await this.getRubyVM();
|
||||
const capture: RubyRuntimeCapture = {
|
||||
stdout: [],
|
||||
stderr: [],
|
||||
};
|
||||
|
||||
runtime.setCapture(capture);
|
||||
|
||||
try {
|
||||
const value = runtime.vm.eval(code);
|
||||
return {
|
||||
code,
|
||||
runtimeLabel,
|
||||
stdout: normalizeRubyOutput(capture.stdout),
|
||||
stderr: normalizeRubyOutput(capture.stderr),
|
||||
result: value.toString(),
|
||||
hasResult: true,
|
||||
error: null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
code,
|
||||
runtimeLabel,
|
||||
stdout: normalizeRubyOutput(capture.stdout),
|
||||
stderr: normalizeRubyOutput(capture.stderr),
|
||||
result: "",
|
||||
hasResult: false,
|
||||
error: formatExecutionError(error),
|
||||
};
|
||||
} finally {
|
||||
runtime.setCapture(null);
|
||||
}
|
||||
};
|
||||
|
||||
const resultPromise = this.executionQueue.then(run, run);
|
||||
this.executionQueue = resultPromise.then(
|
||||
() => undefined,
|
||||
() => undefined
|
||||
);
|
||||
return resultPromise;
|
||||
}
|
||||
|
||||
private showRuntimeLoadError(error: unknown) {
|
||||
new Notice(
|
||||
`Failed to load ${this.getSelectedRuntime().label}: ${formatError(error)}`
|
||||
);
|
||||
}
|
||||
|
||||
private getEditorExecutionTarget(editor: Editor): EditorExecutionTarget {
|
||||
if (editor.somethingSelected()) {
|
||||
return {
|
||||
code: editor.getSelection(),
|
||||
block: this.getRubyCodeBlock(editor, editor.getCursor("from").line),
|
||||
from: editor.getCursor("from"),
|
||||
to: editor.getCursor("to"),
|
||||
};
|
||||
}
|
||||
|
||||
const block = this.getRubyCodeBlock(editor, editor.getCursor().line);
|
||||
if (block) {
|
||||
return {
|
||||
code: block.code,
|
||||
block,
|
||||
from: null,
|
||||
to: null,
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"Select Ruby code or place the cursor inside a ruby code block."
|
||||
);
|
||||
}
|
||||
|
||||
private getRubyCodeBlock(editor: Editor, line: number): RubyCodeBlock | null {
|
||||
for (let current = line; current >= 0; current--) {
|
||||
const fence = parseFenceLine(editor.getLine(current));
|
||||
if (!fence) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!RUBY_LANGUAGES.has(fence.language)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (let endLine = current + 1; endLine < editor.lineCount(); endLine++) {
|
||||
const endFence = parseFenceLine(editor.getLine(endLine));
|
||||
if (!endFence || endFence.fence !== fence.fence) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line > endLine) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const code = editor.getRange(
|
||||
{ line: current + 1, ch: 0 },
|
||||
{ line: endLine, ch: 0 }
|
||||
);
|
||||
|
||||
return {
|
||||
startLine: current,
|
||||
endLine,
|
||||
code,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private insertExecutionResult(
|
||||
editor: Editor,
|
||||
target: EditorExecutionTarget,
|
||||
execution: RubyExecutionResult
|
||||
) {
|
||||
const output = formatExecutionMarkdown(execution);
|
||||
|
||||
if (target.block) {
|
||||
const closingLine = editor.getLine(target.block.endLine);
|
||||
editor.replaceRange(output, {
|
||||
line: target.block.endLine,
|
||||
ch: closingLine.length,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.to) {
|
||||
editor.replaceRange(output, target.to);
|
||||
return;
|
||||
}
|
||||
|
||||
editor.replaceSelection(output);
|
||||
}
|
||||
|
||||
private enhanceRubyCodeBlocks(
|
||||
el: HTMLElement,
|
||||
ctx: MarkdownPostProcessorContext
|
||||
) {
|
||||
const codeBlocks = Array.from(
|
||||
el.querySelectorAll("pre > code.language-ruby")
|
||||
);
|
||||
|
||||
for (const codeEl of codeBlocks) {
|
||||
const preEl = codeEl.parentElement;
|
||||
if (!(preEl instanceof HTMLElement)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preEl.parentElement?.classList.contains("ruby-repl-block")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const wrapperEl = createDiv({ cls: "ruby-repl-block" });
|
||||
preEl.parentElement?.insertBefore(wrapperEl, preEl);
|
||||
wrapperEl.appendChild(preEl);
|
||||
|
||||
ctx.addChild(
|
||||
new RubyCodeBlockRenderChild(
|
||||
wrapperEl,
|
||||
this,
|
||||
codeEl.textContent ?? ""
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async getRubyVM(): Promise<RubyVM> {
|
||||
private async getRubyVM(): Promise<RubyRuntimeSession> {
|
||||
if (!this.rubyVmPromise) {
|
||||
this.rubyVmPromise = this.createRubyVM();
|
||||
}
|
||||
|
|
@ -181,11 +475,15 @@ export default class RubyWasmPlugin extends Plugin {
|
|||
return this.rubyVmPromise;
|
||||
}
|
||||
|
||||
private async createRubyVM(): Promise<RubyVM> {
|
||||
private async createRubyVM(): Promise<RubyRuntimeSession> {
|
||||
const runtime = this.getSelectedRuntime();
|
||||
const loadingNotice = new Notice(`Loading ${runtime.label}...`, 0);
|
||||
let capture: RubyRuntimeCapture | null = null;
|
||||
|
||||
try {
|
||||
const { File, OpenFile, PreopenDirectory, WASI } = await import(
|
||||
"@bjorn3/browser_wasi_shim"
|
||||
);
|
||||
const response = await requestUrl({
|
||||
url: runtime.url,
|
||||
method: "GET",
|
||||
|
|
@ -198,8 +496,38 @@ export default class RubyWasmPlugin extends Plugin {
|
|||
}
|
||||
|
||||
const module = await compileWebAssemblyModule(response.arrayBuffer);
|
||||
const { vm } = await DefaultRubyVM(module);
|
||||
return vm;
|
||||
const wasi = new WASI(
|
||||
[],
|
||||
[],
|
||||
[
|
||||
new OpenFile(new File([])),
|
||||
new OpenFile(new File([])),
|
||||
new OpenFile(new File([])),
|
||||
new PreopenDirectory("/", new Map()),
|
||||
],
|
||||
{ debug: false }
|
||||
);
|
||||
const printer = consolePrinter({
|
||||
stdout: (text) => capture?.stdout.push(text),
|
||||
stderr: (text) => capture?.stderr.push(text),
|
||||
});
|
||||
const { vm } = await RubyVM.instantiateModule({
|
||||
module,
|
||||
wasip1: wasi,
|
||||
addToImports: (imports) => {
|
||||
printer.addToImports(imports);
|
||||
},
|
||||
setMemory: (memory) => {
|
||||
printer.setMemory(memory);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
vm,
|
||||
setCapture: (nextCapture) => {
|
||||
capture = nextCapture;
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
this.rubyVmPromise = null;
|
||||
throw error;
|
||||
|
|
@ -231,30 +559,28 @@ export default class RubyWasmPlugin extends Plugin {
|
|||
}
|
||||
|
||||
class CodeModal extends Modal {
|
||||
code: string;
|
||||
result: string;
|
||||
constructor(app: App, code: string, result: string) {
|
||||
execution: RubyExecutionResult;
|
||||
|
||||
constructor(app: App, execution: RubyExecutionResult) {
|
||||
super(app);
|
||||
this.code = code;
|
||||
this.result = result;
|
||||
this.execution = execution;
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
const { contentEl } = this;
|
||||
|
||||
// contentEl.createEl("h1", { text: "Code" });
|
||||
const codeBlock = contentEl.createEl("pre", {
|
||||
cls: "language-ruby",
|
||||
});
|
||||
const codeElement = codeBlock.createEl("code", {
|
||||
cls: "language-ruby",
|
||||
});
|
||||
codeElement.textContent = this.code || "code";
|
||||
codeElement.textContent = this.execution.code || "code";
|
||||
|
||||
const resultElement = contentEl.createDiv({
|
||||
cls: "ruby-output",
|
||||
});
|
||||
resultElement.textContent = this.result || "result";
|
||||
renderExecutionResult(resultElement, this.execution);
|
||||
|
||||
const closeButton = contentEl.createEl("button", {
|
||||
cls: "modal-button",
|
||||
|
|
@ -272,6 +598,64 @@ class CodeModal extends Modal {
|
|||
}
|
||||
}
|
||||
|
||||
class RubyCodeBlockRenderChild extends MarkdownRenderChild {
|
||||
plugin: RubyWasmPlugin;
|
||||
source: string;
|
||||
runButtonEl: HTMLButtonElement;
|
||||
outputEl: HTMLDivElement;
|
||||
runtimeEl: HTMLSpanElement;
|
||||
|
||||
constructor(containerEl: HTMLElement, plugin: RubyWasmPlugin, source: string) {
|
||||
super(containerEl);
|
||||
this.plugin = plugin;
|
||||
this.source = source;
|
||||
|
||||
const toolbarEl = containerEl.createDiv({ cls: "ruby-repl-toolbar" });
|
||||
this.runButtonEl = toolbarEl.createEl("button", {
|
||||
cls: "clickable-icon ruby-repl-run-button",
|
||||
attr: { type: "button", "aria-label": "Run Ruby code block" },
|
||||
text: "Run",
|
||||
});
|
||||
this.runtimeEl = toolbarEl.createEl("span", {
|
||||
cls: "ruby-repl-runtime",
|
||||
text: plugin.getSelectedRuntimeLabel(),
|
||||
});
|
||||
this.outputEl = containerEl.createDiv({
|
||||
cls: "ruby-output ruby-repl-panel is-empty",
|
||||
});
|
||||
}
|
||||
|
||||
onload() {
|
||||
this.registerDomEvent(this.runButtonEl, "click", () => {
|
||||
void this.runCodeBlock();
|
||||
});
|
||||
}
|
||||
|
||||
private async runCodeBlock() {
|
||||
this.runButtonEl.disabled = true;
|
||||
renderExecutionLoading(this.outputEl, this.plugin.getSelectedRuntimeLabel());
|
||||
|
||||
try {
|
||||
const execution = await this.plugin.executeRuby(this.source);
|
||||
this.runtimeEl.textContent = execution.runtimeLabel;
|
||||
renderExecutionResult(this.outputEl, execution);
|
||||
} catch (error) {
|
||||
this.runtimeEl.textContent = this.plugin.getSelectedRuntimeLabel();
|
||||
renderExecutionResult(this.outputEl, {
|
||||
code: this.source,
|
||||
runtimeLabel: this.plugin.getSelectedRuntimeLabel(),
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
result: "",
|
||||
hasResult: false,
|
||||
error: formatExecutionError(error),
|
||||
});
|
||||
} finally {
|
||||
this.runButtonEl.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RubyWasmSettingTab extends PluginSettingTab {
|
||||
plugin: RubyWasmPlugin;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"id": "ruby-wasm",
|
||||
"name": "ruby.wasm",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"minAppVersion": "1.5.12",
|
||||
"description": "Run ruby code in your notes using WebAssembly.",
|
||||
"author": "geeknees",
|
||||
"authorUrl": "https://github.com/geeknees/",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
}
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "obsidian-ruby.wasm-plugin",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-ruby.wasm-plugin",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@bjorn3/browser_wasi_shim": "^0.3.0",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "obsidian-ruby.wasm-plugin",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"description": "This is a ruby.wasm plugin for Obsidian (https://obsidian.md).",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
58
styles.css
58
styles.css
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
ABOUTME: Styles modal output for the Obsidian ruby.wasm plugin.
|
||||
ABOUTME: Keeps runtime output readable without inline styles in the TypeScript code.
|
||||
ABOUTME: Styles modal and inline REPL output for the Obsidian ruby.wasm plugin.
|
||||
ABOUTME: Keeps runtime output, play buttons, and execution panels readable in preview.
|
||||
|
||||
This CSS file will be included with your plugin, and
|
||||
available in the app when your plugin is enabled.
|
||||
|
|
@ -14,5 +14,57 @@ button.modal-button {
|
|||
}
|
||||
|
||||
.ruby-output {
|
||||
white-space: pre-wrap;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.ruby-repl-block {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.ruby-repl-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.ruby-repl-run-button {
|
||||
padding: 0.25rem 0.75rem;
|
||||
}
|
||||
|
||||
.ruby-repl-runtime,
|
||||
.ruby-execution-runtime {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-ui-small);
|
||||
}
|
||||
|
||||
.ruby-repl-panel.is-empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ruby-repl-panel.is-loading,
|
||||
.ruby-execution {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ruby-execution-status,
|
||||
.ruby-execution-label {
|
||||
font-size: var(--font-ui-small);
|
||||
font-weight: var(--font-medium);
|
||||
}
|
||||
|
||||
.ruby-execution-section {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.ruby-execution-body {
|
||||
white-space: pre-wrap;
|
||||
margin: 0.25rem 0 0;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--background-secondary);
|
||||
}
|
||||
|
||||
.ruby-execution-section.is-error .ruby-execution-body {
|
||||
color: var(--text-error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
{
|
||||
"0.0.1": "0.15.0"
|
||||
}
|
||||
"0.0.1": "1.5.12",
|
||||
"0.0.2": "1.5.12"
|
||||
}
|
||||
Loading…
Reference in a new issue