mirror of
https://github.com/ParkerTenBroeck/automata.git
synced 2026-06-07 05:28:45 -04:00
moved frontend to event driven bus, added more simulation stuff
This commit is contained in:
parent
c7309a75d9
commit
8c8bb103b2
17 changed files with 767 additions and 493 deletions
|
|
@ -30,7 +30,7 @@
|
||||||
|
|
||||||
<section class="flexCol gap marginTop">
|
<section class="flexCol gap marginTop">
|
||||||
<div class="flexCenter sidePadding gap">
|
<div class="flexCenter sidePadding gap">
|
||||||
<button id="themeToggle" class="btn btn-grey" title="Toggle light/dark">
|
<button id="themeToggle" class="btn btn-blue" title="Toggle light/dark">
|
||||||
🌙 Dark
|
🌙 Dark
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btm-grey" style="position: relative" id="shareBtn" type="button">
|
<button class="btn btm-grey" style="position: relative" id="shareBtn" type="button">
|
||||||
|
|
@ -46,6 +46,14 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="hSplit styleOnly" title="Drag to resize canvas height"></div>
|
<div class="hSplit styleOnly" title="Drag to resize canvas height"></div>
|
||||||
|
|
||||||
|
<div class="flexCenter sidePadding" style="font-size: calc(16px);font-weight: bold;color: var(--fg-1)">
|
||||||
|
<span style="margin-right: 0.5em;">Status: </span><span style="color: var(--fg-2)" id="simulationStatus">N/A</span>
|
||||||
|
</div>
|
||||||
|
<div class="flexCenter sidePadding">
|
||||||
|
<input id="machineInput" type="text" class="test-input" placeholder="Enter machine input…" />
|
||||||
|
</div>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -53,7 +61,7 @@
|
||||||
|
|
||||||
<div class="flexCol">
|
<div class="flexCol">
|
||||||
<div class="controls" style="background: var(--bg-0)">
|
<div class="controls" style="background: var(--bg-0)">
|
||||||
<button id="togglePhysics" class="btn btn-toggle active" title="Toggle physics layout">
|
<button id="togglePhysics" class="btn btn-red btn-toggle active" title="Toggle physics layout">
|
||||||
Physics: ON
|
Physics: ON
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
@ -63,21 +71,25 @@
|
||||||
|
|
||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
|
|
||||||
<button id="resetSim" class="btn btn-blue" title="Stop and reset simulation">
|
<button id="reloadSim" class="btn btn-blue" title="Stop and Reload simulation">
|
||||||
⟲ Reset
|
⟲ Reload
|
||||||
</button>
|
</button>
|
||||||
<button id="playPause" class="btn btn-green" title="Run / pause simulation">
|
<button id="clearSim" class="btn btn-red" title="Stop and Clear simulation">
|
||||||
▶ Play
|
✖ Clear
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button id="step" class="btn btn-grey" title="Advance one step">
|
|
||||||
Step
|
<button id="stepSim" class="btn btn-yellow" title="Advance one step">
|
||||||
|
⏭ Step
|
||||||
|
</button>
|
||||||
|
<button id="playPauseSim" class="btn btn-green" title="Run / pause simulation">
|
||||||
|
▶ Play
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<label class="speed">
|
<label class="speed">
|
||||||
Speed
|
Speed
|
||||||
<input id="speed" type="range" min="1" max="60" value="10" />
|
<input id="speedSim" type="range" min="1" max="60" value="1" />
|
||||||
<span id="speedLabel">10x</span>
|
<span id="speedSimLabel">10x</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="hSplit styleOnly" title="Drag to resize canvas height"></div>
|
<div class="hSplit styleOnly" title="Drag to resize canvas height"></div>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import { network, updateVisualization } from "./visualizer.ts";
|
|
||||||
|
|
||||||
export type Machine = Fa | Pda | Tm;
|
export type Machine = Fa | Pda | Tm;
|
||||||
|
|
||||||
export function machine_from_json(json: string): Machine {
|
export function parse_machine_from_json(json: string): Machine {
|
||||||
const machine: Machine = JSON.parse(json);
|
const machine: Machine = JSON.parse(json);
|
||||||
machine.states = new Map(Object.entries(machine.states));
|
machine.states = new Map(Object.entries(machine.states));
|
||||||
|
|
||||||
|
|
@ -202,219 +200,3 @@ export type Tm = {
|
||||||
|
|
||||||
edges: Map<string, Edge[]>;
|
edges: Map<string, Edge[]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SimStepResult = "pending" | "accept" | "reject";
|
|
||||||
|
|
||||||
export class FaState {
|
|
||||||
readonly state: State;
|
|
||||||
|
|
||||||
readonly position: number;
|
|
||||||
readonly input: string;
|
|
||||||
readonly accepted: boolean = false;
|
|
||||||
private repr!: string;
|
|
||||||
|
|
||||||
constructor(state: State, position: number, input: string){
|
|
||||||
this.state=state;
|
|
||||||
this.position=position;
|
|
||||||
this.input = input;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString(): string{
|
|
||||||
if(!this.repr) this.repr = this.state + " " + this.position;
|
|
||||||
return this.repr
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export class FaSim {
|
|
||||||
|
|
||||||
current_states: Map<string, FaState[]> = new Map();
|
|
||||||
accepted: FaState[] = []
|
|
||||||
|
|
||||||
step(): SimStepResult {
|
|
||||||
return "pending";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PdaState {
|
|
||||||
readonly state: State;
|
|
||||||
readonly stack: Symbol[];
|
|
||||||
|
|
||||||
readonly position: number;
|
|
||||||
readonly input: string;
|
|
||||||
readonly accepted: boolean = false
|
|
||||||
private repr!: string;
|
|
||||||
|
|
||||||
constructor(state: State, stack: Symbol[], position: number, input: string){
|
|
||||||
this.state=state;
|
|
||||||
this.stack=stack;
|
|
||||||
this.position=position;
|
|
||||||
this.input = input;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString(): string{
|
|
||||||
if(!this.repr) this.repr = this.state + " [" + this.stack + "]" + " " + this.position;
|
|
||||||
return this.repr
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export class PdaSim {
|
|
||||||
machine: Pda;
|
|
||||||
paths: PdaState[];
|
|
||||||
input: string;
|
|
||||||
|
|
||||||
current_states: Map<string, PdaState[]> = new Map();
|
|
||||||
accepted: PdaState[] = []
|
|
||||||
|
|
||||||
constructor(machine: Pda, input: string) {
|
|
||||||
this.machine = machine;
|
|
||||||
this.paths = [new PdaState(machine.initial_state, [machine.initial_stack], 0, input)];
|
|
||||||
this.current_states.set(machine.initial_state, [this.paths[0]])
|
|
||||||
this.input = input;
|
|
||||||
}
|
|
||||||
|
|
||||||
step(): SimStepResult {
|
|
||||||
if (this.paths.length == 0) return "reject";
|
|
||||||
if (this.accepted.length != 0) return "accept";
|
|
||||||
|
|
||||||
const paths: PdaState[] = [];
|
|
||||||
this.current_states.clear();
|
|
||||||
|
|
||||||
const push = (state: PdaState) => {
|
|
||||||
paths.push(state);
|
|
||||||
if (!this.current_states.has(state.state)) this.current_states.set(state.state, []);
|
|
||||||
this.current_states.get(state.state)?.push(state);
|
|
||||||
|
|
||||||
if (
|
|
||||||
state.position == this.input.length && this.machine.final_states &&
|
|
||||||
this.machine.final_states.has(state.state)
|
|
||||||
||
|
|
||||||
state.position == this.input.length && !this.machine.final_states &&
|
|
||||||
state.stack.length == 1 && state.stack[0] == this.machine.initial_stack
|
|
||||||
) {
|
|
||||||
|
|
||||||
// @ts-expect-error sillllyyyy
|
|
||||||
state.accepted = true
|
|
||||||
this.accepted.push(state);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const path of this.paths) {
|
|
||||||
|
|
||||||
const stack = path.stack.pop()!;
|
|
||||||
const letter_map = this.machine.transitions_components.get(path.state)
|
|
||||||
?.get(stack);
|
|
||||||
if (!letter_map) continue;
|
|
||||||
|
|
||||||
for (const to of letter_map.get(null) ?? []) {
|
|
||||||
push(new PdaState(to.state, path.stack.concat(to.stack), path.position, this.input));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (path.position >= this.input.length) continue;
|
|
||||||
|
|
||||||
const char = this.input.charAt(path.position);
|
|
||||||
|
|
||||||
for (const to of letter_map.get(char) ?? []) {
|
|
||||||
push(new PdaState(to.state, path.stack.concat(to.stack), path.position+1, this.input));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.paths = paths;
|
|
||||||
|
|
||||||
|
|
||||||
if (this.paths.length == 0) return "reject";
|
|
||||||
if (this.accepted.length != 0) return "accept";
|
|
||||||
return "pending"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TmState{
|
|
||||||
readonly state: State;
|
|
||||||
readonly tape: Symbol[];
|
|
||||||
|
|
||||||
readonly position: number;
|
|
||||||
readonly input: string;
|
|
||||||
readonly accepted: boolean = false
|
|
||||||
private repr!: string;
|
|
||||||
|
|
||||||
constructor(state: State, tape: Symbol[], position: number, input: string){
|
|
||||||
this.state=state;
|
|
||||||
this.tape = tape;
|
|
||||||
this.position=position;
|
|
||||||
this.input = input;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString(): string{
|
|
||||||
if(!this.repr) this.repr = this.state + " " + this.position;
|
|
||||||
return this.repr
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TmSim {
|
|
||||||
current_states: Map<string, TmState[]> = new Map();
|
|
||||||
accepted: TmState[] = []
|
|
||||||
|
|
||||||
step(): SimStepResult {
|
|
||||||
return "pending"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Sim = FaSim | PdaSim | TmSim | null
|
|
||||||
export let sim: Sim = null;
|
|
||||||
|
|
||||||
export let automaton: Machine = {
|
|
||||||
type: "fa",
|
|
||||||
alphabet: new Map(),
|
|
||||||
final_states: new Map(),
|
|
||||||
initial_state: "",
|
|
||||||
states: new Map(),
|
|
||||||
transitions: new Map(),
|
|
||||||
transitions_components: new Map(),
|
|
||||||
edges: new Map(),
|
|
||||||
};
|
|
||||||
|
|
||||||
export function clearSimulation(){
|
|
||||||
setSimulation(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setSimulation(sim_: Sim){
|
|
||||||
sim = sim_;
|
|
||||||
network.redraw()
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setAutomaton(auto: Machine) {
|
|
||||||
automaton = auto;
|
|
||||||
sim = null;
|
|
||||||
updateVisualization()
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearAutomaton() {
|
|
||||||
setAutomaton({
|
|
||||||
type: "fa",
|
|
||||||
alphabet: new Map(),
|
|
||||||
final_states: new Map(),
|
|
||||||
initial_state: "",
|
|
||||||
states: new Map(),
|
|
||||||
transitions: new Map(),
|
|
||||||
transitions_components: new Map(),
|
|
||||||
edges: new Map(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function stepSimulation(): void {
|
|
||||||
if (sim) {
|
|
||||||
console.log(sim.step());
|
|
||||||
}
|
|
||||||
network.redraw()
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resetSimulation(): void {
|
|
||||||
switch (automaton.type) {
|
|
||||||
case "fa":
|
|
||||||
break;
|
|
||||||
case "pda":
|
|
||||||
setSimulation(new PdaSim(automaton as Pda, "aabbaabbaa"));
|
|
||||||
break;
|
|
||||||
case "tm":
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
87
web/root/src/bus.ts
Normal file
87
web/root/src/bus.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
// deno-lint-ignore-file
|
||||||
|
|
||||||
|
import type { Machine } from "./automata.ts";
|
||||||
|
import type { Sim, SimStepResult } from "./simulation.ts";
|
||||||
|
import type wasm from "./wasm.ts";
|
||||||
|
import type { Text } from "npm:@codemirror/state";
|
||||||
|
|
||||||
|
type Unsubscribe = () => void;
|
||||||
|
|
||||||
|
export class EventBus<Events extends Record<string, any>> {
|
||||||
|
private listeners = new Map<keyof Events, Set<(payload: any) => void>>();
|
||||||
|
|
||||||
|
on<K extends keyof Events>(
|
||||||
|
event: K,
|
||||||
|
handler: (payload: Events[K]) => void,
|
||||||
|
): Unsubscribe {
|
||||||
|
let set = this.listeners.get(event);
|
||||||
|
if (!set) {
|
||||||
|
set = new Set();
|
||||||
|
this.listeners.set(event, set);
|
||||||
|
}
|
||||||
|
set.add(handler as any);
|
||||||
|
|
||||||
|
return () => this.off(event, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
once<K extends keyof Events>(
|
||||||
|
event: K,
|
||||||
|
handler: (payload: Events[K]) => void,
|
||||||
|
): Unsubscribe {
|
||||||
|
const off = this.on(event, (payload) => {
|
||||||
|
off();
|
||||||
|
handler(payload);
|
||||||
|
});
|
||||||
|
return off;
|
||||||
|
}
|
||||||
|
|
||||||
|
off<K extends keyof Events>(event: K, handler: (payload: Events[K]) => void) {
|
||||||
|
const set = this.listeners.get(event);
|
||||||
|
if (!set) return;
|
||||||
|
set.delete(handler as any);
|
||||||
|
if (set.size === 0) this.listeners.delete(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit<K extends keyof Events>(event: K, payload: Events[K]) {
|
||||||
|
const set = this.listeners.get(event);
|
||||||
|
if (!set) return;
|
||||||
|
|
||||||
|
// Copy to avoid issues if handlers subscribe/unsubscribe during emit
|
||||||
|
for (const handler of Array.from(set)) {
|
||||||
|
try {
|
||||||
|
(handler as (p: Events[K]) => void)(payload);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(event?: keyof Events) {
|
||||||
|
if (event) this.listeners.delete(event);
|
||||||
|
else this.listeners.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppEvents = {
|
||||||
|
"begin": void;
|
||||||
|
|
||||||
|
"editor/change": {text: string, doc: Text};
|
||||||
|
"compiled": {log: wasm.CompileLog[], ansi_log: string, machine: string|undefined};
|
||||||
|
|
||||||
|
"automata/sim/update": { simulation: Sim|null };
|
||||||
|
"automata/sim/before_step": { simulation: Sim };
|
||||||
|
"automata/sim/after_step": { simulation: Sim, result: SimStepResult };
|
||||||
|
"automata/update": { automaton: Machine };
|
||||||
|
|
||||||
|
"controls/physics": {enabled: boolean},
|
||||||
|
"controls/reset_network": void,
|
||||||
|
|
||||||
|
|
||||||
|
"controls/step_simulation": void,
|
||||||
|
"controls/reload_simulation": void,
|
||||||
|
"controls/clear_simulation": void,
|
||||||
|
|
||||||
|
"theme/update": void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const bus = new EventBus<AppEvents>();
|
||||||
|
|
@ -1,49 +1,70 @@
|
||||||
import { resetSimulation, stepSimulation } from "./automata.ts";
|
import { bus } from "./bus.ts";
|
||||||
import {nodes, edges, network} from "./visualizer.ts"
|
|
||||||
|
|
||||||
const togglePhysicsBtn = document.getElementById("togglePhysics") as HTMLButtonElement;
|
const togglePhysicsBtn = document.getElementById(
|
||||||
const resetLayoutBtn = document.getElementById("resetLayout") as HTMLButtonElement;
|
"togglePhysics",
|
||||||
const playPauseBtn = document.getElementById("playPause") as HTMLButtonElement;
|
) as HTMLButtonElement;
|
||||||
const stepBtn = document.getElementById("step") as HTMLButtonElement;
|
const resetLayoutBtn = document.getElementById(
|
||||||
const speedSlider = document.getElementById("speed") as HTMLInputElement;
|
"resetLayout",
|
||||||
const speedLabel = document.getElementById("speedLabel") as HTMLSpanElement;
|
) as HTMLButtonElement;
|
||||||
const resetSimBtn = document.getElementById("resetSim") as HTMLButtonElement;
|
const playPauseBtn = document.getElementById(
|
||||||
|
"playPauseSim",
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
const stepBtn = document.getElementById("stepSim") as HTMLButtonElement;
|
||||||
|
const speedSlider = document.getElementById("speedSim") as HTMLInputElement;
|
||||||
|
const speedLabel = document.getElementById("speedSimLabel") as HTMLSpanElement;
|
||||||
|
const reloadSimBtn = document.getElementById("reloadSim") as HTMLButtonElement;
|
||||||
|
const clearSimBtn = document.getElementById("clearSim") as HTMLButtonElement;
|
||||||
|
|
||||||
|
bus.on("controls/physics", ({ enabled }) => {
|
||||||
// ---- Physics toggle (styled label) ----
|
|
||||||
function setPhysicsButtonUI(enabled: boolean) {
|
|
||||||
togglePhysicsBtn.classList.toggle("active", enabled);
|
togglePhysicsBtn.classList.toggle("active", enabled);
|
||||||
togglePhysicsBtn.textContent = enabled ? "Physics: ON" : "Physics: OFF";
|
togglePhysicsBtn.textContent = enabled ? "Physics: ON" : "Physics: OFF";
|
||||||
}
|
});
|
||||||
|
|
||||||
togglePhysicsBtn.onclick = () => {
|
togglePhysicsBtn.onclick = () => {
|
||||||
const enabled = !togglePhysicsBtn.classList.contains("active");
|
const enabled = !togglePhysicsBtn.classList.contains("active");
|
||||||
setPhysicsButtonUI(enabled);
|
bus.emit("controls/physics", { enabled });
|
||||||
network.setOptions({ physics: { enabled } });
|
|
||||||
network.setOptions({edges: {smooth: enabled}});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
setPhysicsButtonUI(togglePhysicsBtn.classList.contains("active"));
|
bus.emit("controls/physics", {
|
||||||
|
enabled: togglePhysicsBtn.classList.contains("active"),
|
||||||
resetLayoutBtn.onclick = () => {
|
|
||||||
try {
|
|
||||||
nodes.forEach((n) => {
|
|
||||||
n.physics = true;
|
|
||||||
n.x = undefined;
|
|
||||||
n.y = undefined;
|
|
||||||
});
|
});
|
||||||
network.setData({ nodes, edges });
|
|
||||||
} catch {
|
resetLayoutBtn.onclick = () => bus.emit("controls/reset_network", undefined);
|
||||||
// Last resort
|
|
||||||
network.setData({ nodes, edges });
|
clearSimBtn.onclick = () => bus.emit("controls/clear_simulation", undefined);
|
||||||
|
|
||||||
|
stepBtn.onclick = () => {
|
||||||
|
bus.emit("controls/step_simulation", undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
reloadSimBtn.onclick = () => bus.emit("controls/reload_simulation", undefined);
|
||||||
|
|
||||||
|
function updateButtons() {
|
||||||
|
stepBtn.disabled = !simulation_active || running;
|
||||||
|
playPauseBtn.disabled = !simulation_active;
|
||||||
|
clearSimBtn.disabled = !simulation_active;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If physics button is OFF, keep it OFF (don’t surprise the user)
|
bus.on("controls/reload_simulation", (_) => {
|
||||||
const physicsEnabled = togglePhysicsBtn.classList.contains("active");
|
if (running) setRunning(false);
|
||||||
network.setOptions({ physics: { enabled: physicsEnabled } });
|
updateButtons();
|
||||||
};
|
});
|
||||||
|
|
||||||
// ---- Play/Pause + Speed ----
|
bus.on("automata/sim/update", ({ simulation }) => {
|
||||||
|
simulation_active = !!simulation;
|
||||||
|
if (!simulation) {
|
||||||
|
if (running) setRunning(false);
|
||||||
|
}
|
||||||
|
updateButtons();
|
||||||
|
});
|
||||||
|
|
||||||
|
bus.on("automata/sim/after_step", ({ result }) => {
|
||||||
|
if (result !== "pending") {
|
||||||
|
if (running) setRunning(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let simulation_active = false;
|
||||||
let running = false;
|
let running = false;
|
||||||
let timer: number | null = null;
|
let timer: number | null = null;
|
||||||
|
|
||||||
|
|
@ -74,8 +95,7 @@ function restartTimer() {
|
||||||
const intervalMs = Math.round(1000 / sps);
|
const intervalMs = Math.round(1000 / sps);
|
||||||
|
|
||||||
timer = globalThis.window.setInterval(() => {
|
timer = globalThis.window.setInterval(() => {
|
||||||
// If your step can throw, keep the interval alive:
|
bus.emit("controls/step_simulation", undefined);
|
||||||
try { stepSimulation(); } catch (e) { console.error(e); }
|
|
||||||
}, intervalMs);
|
}, intervalMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,13 +113,3 @@ function setRunning(on: boolean) {
|
||||||
}
|
}
|
||||||
|
|
||||||
playPauseBtn.onclick = () => setRunning(!running);
|
playPauseBtn.onclick = () => setRunning(!running);
|
||||||
|
|
||||||
stepBtn.onclick = () => {
|
|
||||||
stepSimulation();
|
|
||||||
};
|
|
||||||
|
|
||||||
resetSimBtn.onclick = () => {
|
|
||||||
if (running) setRunning(false);
|
|
||||||
resetSimulation();
|
|
||||||
stepBtn.disabled = false;
|
|
||||||
};
|
|
||||||
|
|
@ -5,7 +5,6 @@ import {
|
||||||
keymap,
|
keymap,
|
||||||
hoverTooltip,
|
hoverTooltip,
|
||||||
Decoration,
|
Decoration,
|
||||||
ViewPlugin,
|
|
||||||
lineNumbers,
|
lineNumbers,
|
||||||
highlightActiveLineGutter,
|
highlightActiveLineGutter,
|
||||||
highlightActiveLine
|
highlightActiveLine
|
||||||
|
|
@ -18,11 +17,10 @@ import { closeBrackets } from "npm:@codemirror/autocomplete";
|
||||||
|
|
||||||
|
|
||||||
import wasm from "./wasm.ts"
|
import wasm from "./wasm.ts"
|
||||||
import { terminalPlugin } from "./terminal.ts";
|
|
||||||
|
|
||||||
import { machine_from_json, setAutomaton } from "./automata.ts";
|
|
||||||
import { sharedText } from "./share.ts";
|
import { sharedText } from "./share.ts";
|
||||||
import { examples } from "./examples.ts";
|
import { examples } from "./examples.ts";
|
||||||
|
import { bus } from "./bus.ts";
|
||||||
|
|
||||||
|
|
||||||
function tokenize(text: string) {
|
function tokenize(text: string) {
|
||||||
|
|
@ -44,31 +42,26 @@ function compile(text: string): wasm.CompileResult {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const analysisField = StateField.define({
|
const eventBusConnection = StateField.define({
|
||||||
create(state) {
|
create(state) {
|
||||||
const text = state.doc.toString();
|
const text = state.doc.toString();
|
||||||
|
bus.emit("editor/change", {text, doc: state.doc});
|
||||||
return buildAnalysis(text, state.doc);
|
return buildAnalysis(text, state.doc);
|
||||||
},
|
},
|
||||||
update(value, tr) {
|
update(value, tr) {
|
||||||
if (!tr.docChanged) return value;
|
if (!tr.docChanged) return value;
|
||||||
const text = tr.state.doc.toString();
|
const text = tr.state.doc.toString();
|
||||||
|
bus.emit("editor/change", {text, doc: state.doc});
|
||||||
return buildAnalysis(text, tr.state.doc);
|
return buildAnalysis(text, tr.state.doc);
|
||||||
},
|
},
|
||||||
provide: (f) => EditorView.decorations.from(f, (v) => v.deco),
|
provide: (f) => EditorView.decorations.from(f, (v) => v.deco),
|
||||||
});
|
});
|
||||||
|
|
||||||
function buildAnalysis(text: string, doc: Text) {
|
function buildAnalysis(text: string, doc: Text) {
|
||||||
save(text);
|
|
||||||
const tokens = tokenize(text);
|
const tokens = tokenize(text);
|
||||||
const { log, log_formatted, graph } = compile(text);
|
const { log, ansi_log, machine } = compile(text);
|
||||||
|
|
||||||
if (graph){
|
bus.emit("compiled", {log, ansi_log, machine})
|
||||||
try{
|
|
||||||
setAutomaton(machine_from_json(graph))
|
|
||||||
}catch(e){
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const marks = [];
|
const marks = [];
|
||||||
const docLen = doc.length;
|
const docLen = doc.length;
|
||||||
|
|
@ -100,7 +93,7 @@ function buildAnalysis(text: string, doc: Text) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const deco = Decoration.set(marks, true);
|
const deco = Decoration.set(marks, true);
|
||||||
return { tokens, log, log_formatted, deco };
|
return { tokens, log, ansi_log, deco };
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokenClass = (t: string) =>
|
const tokenClass = (t: string) =>
|
||||||
|
|
@ -135,7 +128,7 @@ function sevRank(sev: string) {
|
||||||
|
|
||||||
// ===================== Hover tooltip (uses cached diags) =====================
|
// ===================== Hover tooltip (uses cached diags) =====================
|
||||||
const diagHover = hoverTooltip((view, pos) => {
|
const diagHover = hoverTooltip((view, pos) => {
|
||||||
const { log } = view.state.field(analysisField);
|
const { log } = view.state.field(eventBusConnection);
|
||||||
const hits = log.filter((d) => d.start !== undefined && d.end !== undefined && pos >= d.start && pos <= d.end);
|
const hits = log.filter((d) => d.start !== undefined && d.end !== undefined && pos >= d.start && pos <= d.end);
|
||||||
if (hits.length === 0) return null;
|
if (hits.length === 0) return null;
|
||||||
|
|
||||||
|
|
@ -186,7 +179,7 @@ export function getText(): string{
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = EditorState.create({
|
const state = EditorState.create({
|
||||||
doc: sharedText() ?? getSaved() ?? examples[0].machine,
|
doc: "",
|
||||||
extensions: [
|
extensions: [
|
||||||
lineNumbers(),
|
lineNumbers(),
|
||||||
highlightActiveLineGutter(),
|
highlightActiveLineGutter(),
|
||||||
|
|
@ -197,9 +190,8 @@ const state = EditorState.create({
|
||||||
closeBrackets(),
|
closeBrackets(),
|
||||||
keymap.of([...defaultKeymap, ...historyKeymap]),
|
keymap.of([...defaultKeymap, ...historyKeymap]),
|
||||||
|
|
||||||
analysisField,
|
eventBusConnection,
|
||||||
diagHover,
|
diagHover,
|
||||||
terminalPlugin,
|
|
||||||
|
|
||||||
EditorView.lineWrapping,
|
EditorView.lineWrapping,
|
||||||
],
|
],
|
||||||
|
|
@ -209,3 +201,5 @@ const editor = new EditorView({
|
||||||
state,
|
state,
|
||||||
parent: document.getElementById("editor")!,
|
parent: document.getElementById("editor")!,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bus.on("begin", _ => setText(sharedText() ?? getSaved() ?? examples[0].machine))
|
||||||
|
|
@ -10,9 +10,9 @@ export type Category =
|
||||||
| "NTM";
|
| "NTM";
|
||||||
|
|
||||||
export class Example {
|
export class Example {
|
||||||
category: Category;
|
readonly category: Category;
|
||||||
title: string;
|
readonly title: string;
|
||||||
machine: string;
|
readonly machine: string;
|
||||||
|
|
||||||
constructor(category: Category, title: string, machine: string) {
|
constructor(category: Category, title: string, machine: string) {
|
||||||
this.category = category;
|
this.category = category;
|
||||||
|
|
@ -21,7 +21,7 @@ export class Example {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const examples: Example[] = [
|
export const examples: readonly Example[] = [
|
||||||
new Example(
|
new Example(
|
||||||
"Tutorial",
|
"Tutorial",
|
||||||
"DFA",
|
"DFA",
|
||||||
|
|
@ -174,7 +174,7 @@ const CATEGORY_ORDER: Category[] = [
|
||||||
|
|
||||||
function buildExamplesDropdown(
|
function buildExamplesDropdown(
|
||||||
selectEl: HTMLSelectElement,
|
selectEl: HTMLSelectElement,
|
||||||
examples: Example[],
|
examples: readonly Example[],
|
||||||
onPick?: (ex: Example) => void,
|
onPick?: (ex: Example) => void,
|
||||||
) {
|
) {
|
||||||
// Clear everything except the first placeholder option (if present)
|
// Clear everything except the first placeholder option (if present)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
import "./editor.ts"
|
import { bus } from "./bus.ts";
|
||||||
import "./visualizer.ts"
|
|
||||||
import "./splitters.ts"
|
import "./splitters.ts"
|
||||||
import "./controls.ts"
|
import "./controls.ts"
|
||||||
import "./theme.ts"
|
import "./theme.ts"
|
||||||
import "./share.ts"
|
import "./share.ts"
|
||||||
import "./examples.ts"
|
import "./examples.ts"
|
||||||
|
import "./visualizer.ts"
|
||||||
|
import "./editor.ts"
|
||||||
|
import "./simulation.ts"
|
||||||
|
|
||||||
|
bus.emit("begin", undefined);
|
||||||
325
web/root/src/simulation.ts
Normal file
325
web/root/src/simulation.ts
Normal file
|
|
@ -0,0 +1,325 @@
|
||||||
|
import { bus } from "./bus.ts";
|
||||||
|
import {
|
||||||
|
Fa,
|
||||||
|
Machine,
|
||||||
|
parse_machine_from_json,
|
||||||
|
Pda,
|
||||||
|
State,
|
||||||
|
Symbol,
|
||||||
|
Tm,
|
||||||
|
} from "./automata.ts";
|
||||||
|
|
||||||
|
export type SimStepResult = "pending" | "accept" | "reject";
|
||||||
|
export type Sim = FaSim | PdaSim | TmSim;
|
||||||
|
let simulation: Sim | null = null;
|
||||||
|
let automaton: Machine = {
|
||||||
|
type: "fa",
|
||||||
|
alphabet: new Map(),
|
||||||
|
final_states: new Map(),
|
||||||
|
initial_state: "",
|
||||||
|
states: new Map(),
|
||||||
|
transitions: new Map(),
|
||||||
|
transitions_components: new Map(),
|
||||||
|
edges: new Map(),
|
||||||
|
};
|
||||||
|
|
||||||
|
bus.on("compiled", ({ machine }) => {
|
||||||
|
if (machine) {
|
||||||
|
try {
|
||||||
|
bus.emit("controls/clear_simulation", undefined);
|
||||||
|
automaton = parse_machine_from_json(machine);
|
||||||
|
bus.emit("automata/update", { automaton });
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
bus.on("controls/clear_simulation", (_) => {
|
||||||
|
simulation = null;
|
||||||
|
bus.emit("automata/sim/update", { simulation: null });
|
||||||
|
});
|
||||||
|
bus.on("controls/step_simulation", (_) => {
|
||||||
|
if (simulation) {
|
||||||
|
bus.emit("automata/sim/before_step", { simulation });
|
||||||
|
bus.emit("automata/sim/after_step", {
|
||||||
|
result: simulation.step(),
|
||||||
|
simulation: simulation,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const machineInput = document.getElementById("machineInput") as HTMLInputElement;
|
||||||
|
machineInput.addEventListener("input", () => bus.emit("automata/sim/update", {simulation: null}));
|
||||||
|
machineInput.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
bus.emit("controls/reload_simulation", undefined)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
bus.on("controls/reload_simulation", (_) => {
|
||||||
|
const input = machineInput.value;
|
||||||
|
switch (automaton.type) {
|
||||||
|
case "fa":
|
||||||
|
simulation = new FaSim(automaton as Fa, input);
|
||||||
|
break;
|
||||||
|
case "pda":
|
||||||
|
simulation = new PdaSim(automaton as Pda, input);
|
||||||
|
break;
|
||||||
|
case "tm":
|
||||||
|
simulation = new TmSim(automaton as Tm, input);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
bus.emit("automata/sim/update", { simulation });
|
||||||
|
});
|
||||||
|
const simulationStatus = document.getElementById("simulationStatus") as HTMLInputElement;
|
||||||
|
bus.on("automata/sim/update", ({simulation}) => {
|
||||||
|
if (!simulation){
|
||||||
|
simulationStatus.innerText = "N/A"
|
||||||
|
simulationStatus.style.color = "var(--fg-2)";
|
||||||
|
}else{
|
||||||
|
simulationStatus.innerText = "Pending"
|
||||||
|
simulationStatus.style.color = "var(--warning)";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
bus.on("automata/sim/after_step", ({result}) => {
|
||||||
|
if (result === "pending"){
|
||||||
|
simulationStatus.innerText = "Pending"
|
||||||
|
simulationStatus.style.color = "var(--warning)";
|
||||||
|
}else if (result==="accept"){
|
||||||
|
simulationStatus.innerText = "Accepted"
|
||||||
|
simulationStatus.style.color = "var(--success)";
|
||||||
|
}else if (result==="reject"){
|
||||||
|
simulationStatus.innerText = "Rejected"
|
||||||
|
simulationStatus.style.color = "var(--error)";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export class FaState {
|
||||||
|
readonly state: State;
|
||||||
|
|
||||||
|
readonly position: number;
|
||||||
|
readonly input: string;
|
||||||
|
readonly accepted: boolean = false;
|
||||||
|
private repr!: string;
|
||||||
|
|
||||||
|
constructor(state: State, position: number, input: string) {
|
||||||
|
this.state = state;
|
||||||
|
this.position = position;
|
||||||
|
this.input = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
if (!this.repr) {
|
||||||
|
this.repr = this.state + +" >" + this.input.substring(this.position);
|
||||||
|
}
|
||||||
|
return this.repr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FaSim {
|
||||||
|
machine: Fa;
|
||||||
|
paths: FaState[];
|
||||||
|
input: string;
|
||||||
|
|
||||||
|
current_states: Map<string, FaState[]> = new Map();
|
||||||
|
accepted: FaState[] = [];
|
||||||
|
|
||||||
|
constructor(machine: Fa, input: string) {
|
||||||
|
this.machine = machine;
|
||||||
|
this.paths = [new FaState(machine.initial_state, 0, input)];
|
||||||
|
this.current_states.set(machine.initial_state, [this.paths[0]]);
|
||||||
|
this.input = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
step(): SimStepResult {
|
||||||
|
if (this.paths.length == 0) return "reject";
|
||||||
|
if (this.accepted.length != 0) return "accept";
|
||||||
|
|
||||||
|
const paths: FaState[] = [];
|
||||||
|
this.current_states.clear();
|
||||||
|
|
||||||
|
const push = (state: FaState) => {
|
||||||
|
paths.push(state);
|
||||||
|
if (!this.current_states.has(state.state)) {
|
||||||
|
this.current_states.set(state.state, []);
|
||||||
|
}
|
||||||
|
this.current_states.get(state.state)?.push(state);
|
||||||
|
|
||||||
|
if (
|
||||||
|
state.position == this.input.length &&
|
||||||
|
this.machine.final_states.has(state.state)
|
||||||
|
) {
|
||||||
|
// @ts-expect-error sillllyyyy
|
||||||
|
state.accepted = true;
|
||||||
|
this.accepted.push(state);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const path of this.paths) {
|
||||||
|
const letter_map = this.machine.transitions_components.get(path.state)!;
|
||||||
|
if (!letter_map) continue;
|
||||||
|
|
||||||
|
for (const to of letter_map.get(null) ?? []) {
|
||||||
|
push(new FaState(to.state, path.position, this.input));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.position >= this.input.length) continue;
|
||||||
|
|
||||||
|
const char = this.input.charAt(path.position);
|
||||||
|
|
||||||
|
for (const to of letter_map.get(char) ?? []) {
|
||||||
|
push(new FaState(to.state, path.position + 1, this.input));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.paths = paths;
|
||||||
|
|
||||||
|
if (this.paths.length == 0) return "reject";
|
||||||
|
if (this.accepted.length != 0) return "accept";
|
||||||
|
return "pending";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PdaState {
|
||||||
|
readonly state: State;
|
||||||
|
readonly stack: Symbol[];
|
||||||
|
|
||||||
|
readonly position: number;
|
||||||
|
readonly input: string;
|
||||||
|
readonly accepted: boolean = false;
|
||||||
|
private repr!: string;
|
||||||
|
|
||||||
|
constructor(state: State, stack: Symbol[], position: number, input: string) {
|
||||||
|
this.state = state;
|
||||||
|
this.stack = stack;
|
||||||
|
this.position = position;
|
||||||
|
this.input = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
if (!this.repr) {
|
||||||
|
this.repr = this.state + " [" + this.stack + "]" + " >" +
|
||||||
|
this.input.substring(this.position);
|
||||||
|
}
|
||||||
|
return this.repr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PdaSim {
|
||||||
|
machine: Pda;
|
||||||
|
paths: PdaState[];
|
||||||
|
input: string;
|
||||||
|
|
||||||
|
current_states: Map<string, PdaState[]> = new Map();
|
||||||
|
accepted: PdaState[] = [];
|
||||||
|
|
||||||
|
constructor(machine: Pda, input: string) {
|
||||||
|
this.machine = machine;
|
||||||
|
this.paths = [
|
||||||
|
new PdaState(machine.initial_state, [machine.initial_stack], 0, input),
|
||||||
|
];
|
||||||
|
this.current_states.set(machine.initial_state, [this.paths[0]]);
|
||||||
|
this.input = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
step(): SimStepResult {
|
||||||
|
if (this.paths.length == 0) return "reject";
|
||||||
|
if (this.accepted.length != 0) return "accept";
|
||||||
|
|
||||||
|
const paths: PdaState[] = [];
|
||||||
|
this.current_states.clear();
|
||||||
|
|
||||||
|
const push = (state: PdaState) => {
|
||||||
|
paths.push(state);
|
||||||
|
if (!this.current_states.has(state.state)) {
|
||||||
|
this.current_states.set(state.state, []);
|
||||||
|
}
|
||||||
|
this.current_states.get(state.state)?.push(state);
|
||||||
|
|
||||||
|
if (
|
||||||
|
state.position == this.input.length && this.machine.final_states &&
|
||||||
|
this.machine.final_states.has(state.state) ||
|
||||||
|
state.position == this.input.length && !this.machine.final_states &&
|
||||||
|
state.stack.length == 1 &&
|
||||||
|
state.stack[0] == this.machine.initial_stack
|
||||||
|
) {
|
||||||
|
// @ts-expect-error sillllyyyy
|
||||||
|
state.accepted = true;
|
||||||
|
this.accepted.push(state);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const path of this.paths) {
|
||||||
|
const stack = path.stack.pop()!;
|
||||||
|
const letter_map = this.machine.transitions_components.get(path.state)
|
||||||
|
?.get(stack);
|
||||||
|
if (!letter_map) continue;
|
||||||
|
|
||||||
|
for (const to of letter_map.get(null) ?? []) {
|
||||||
|
push(
|
||||||
|
new PdaState(
|
||||||
|
to.state,
|
||||||
|
path.stack.concat(to.stack),
|
||||||
|
path.position,
|
||||||
|
this.input,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.position >= this.input.length) continue;
|
||||||
|
|
||||||
|
const char = this.input.charAt(path.position);
|
||||||
|
|
||||||
|
for (const to of letter_map.get(char) ?? []) {
|
||||||
|
push(
|
||||||
|
new PdaState(
|
||||||
|
to.state,
|
||||||
|
path.stack.concat(to.stack),
|
||||||
|
path.position + 1,
|
||||||
|
this.input,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.paths = paths;
|
||||||
|
|
||||||
|
if (this.paths.length == 0) return "reject";
|
||||||
|
if (this.accepted.length != 0) return "accept";
|
||||||
|
return "pending";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TmState {
|
||||||
|
readonly state: State;
|
||||||
|
readonly tape: Symbol[];
|
||||||
|
|
||||||
|
readonly position: number;
|
||||||
|
readonly input: string;
|
||||||
|
readonly accepted: boolean = false;
|
||||||
|
private repr!: string;
|
||||||
|
|
||||||
|
constructor(state: State, tape: Symbol[], position: number, input: string) {
|
||||||
|
this.state = state;
|
||||||
|
this.tape = tape;
|
||||||
|
this.position = position;
|
||||||
|
this.input = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
if (!this.repr) this.repr = this.state + " " + this.position;
|
||||||
|
return this.repr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TmSim {
|
||||||
|
machine: Tm;
|
||||||
|
input: string;
|
||||||
|
current_states: Map<string, TmState[]> = new Map();
|
||||||
|
accepted: TmState[] = [];
|
||||||
|
|
||||||
|
constructor(machine: Tm, input: string) {
|
||||||
|
this.machine = machine;
|
||||||
|
this.input = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
step(): SimStepResult {
|
||||||
|
return "pending";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -63,7 +63,7 @@ function setFlexFill(pane: HTMLElement) {
|
||||||
pane.style.flex = "1 1 auto";
|
pane.style.flex = "1 1 auto";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function enableFlexSplitters() {
|
function enableFlexSplitters() {
|
||||||
// Horizontal: A | hSplit | B (top/split/bottom)
|
// Horizontal: A | hSplit | B (top/split/bottom)
|
||||||
for (const splitter of document.querySelectorAll<HTMLElement>(".hSplit:not(.styleOnly)")) {
|
for (const splitter of document.querySelectorAll<HTMLElement>(".hSplit:not(.styleOnly)")) {
|
||||||
const parent = splitter.parentElement as HTMLElement | null;
|
const parent = splitter.parentElement as HTMLElement | null;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
// deno-lint-ignore-file
|
import { bus } from "./bus.ts";
|
||||||
|
|
||||||
import {
|
bus.on("compiled", ({log, ansi_log}) => {
|
||||||
ViewPlugin,
|
const term = document.getElementById("terminal");
|
||||||
} from "npm:@codemirror/view";
|
if (!term) return;
|
||||||
|
|
||||||
import { analysisField } from "./editor.ts";
|
let s = "";
|
||||||
|
s += `\x1b[90m[compile]\x1b[0m ${log.length} diagnostics\n`;
|
||||||
|
|
||||||
|
term.innerHTML = ansiToHtml(s + ansi_log);
|
||||||
|
})
|
||||||
|
|
||||||
function escapeHtml(s: string) {
|
function escapeHtml(s: string) {
|
||||||
return s
|
return s
|
||||||
|
|
@ -78,32 +82,3 @@ function ansiToHtml(input: string) {
|
||||||
out += openSpanIfNeeded(input.slice(lastIndex));
|
out += openSpanIfNeeded(input.slice(lastIndex));
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-expect-error bad library
|
|
||||||
function formatTerminal(view) {
|
|
||||||
const term = document.getElementById("terminal");
|
|
||||||
if (!term) return;
|
|
||||||
|
|
||||||
const { log, log_formatted } = view.state.field(analysisField);
|
|
||||||
|
|
||||||
let s = "";
|
|
||||||
s += `\x1b[90m[compile]\x1b[0m ${log.length} diagnostics\n`;
|
|
||||||
|
|
||||||
term.innerHTML = ansiToHtml(s + log_formatted);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const terminalPlugin = ViewPlugin.fromClass(
|
|
||||||
class {
|
|
||||||
|
|
||||||
// @ts-expect-error bad library
|
|
||||||
constructor(view) {
|
|
||||||
// @ts-expect-error bad library
|
|
||||||
this.view = view;
|
|
||||||
formatTerminal(view);
|
|
||||||
}
|
|
||||||
// @ts-expect-error bad library
|
|
||||||
update(update) {
|
|
||||||
if (update.docChanged) formatTerminal(update.view);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { updateGraphTheme } from "./visualizer.ts";
|
import { bus } from "./bus.ts";
|
||||||
|
|
||||||
const themeBtn = document.getElementById("themeToggle") as HTMLButtonElement;
|
const themeBtn = document.getElementById("themeToggle") as HTMLButtonElement;
|
||||||
|
|
||||||
|
|
@ -22,11 +22,11 @@ function setTheme(theme: Theme) {
|
||||||
|
|
||||||
// update button label
|
// update button label
|
||||||
themeBtn.textContent = theme === "dark" ? "🌙 Dark" : "☀️ Light";
|
themeBtn.textContent = theme === "dark" ? "🌙 Dark" : "☀️ Light";
|
||||||
updateGraphTheme();
|
|
||||||
|
bus.emit("theme/update", undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bus.on("begin", _ => setTheme(getPreferredTheme()))
|
||||||
setTheme(getPreferredTheme());
|
|
||||||
|
|
||||||
themeBtn.addEventListener("click", toggleTheme);
|
themeBtn.addEventListener("click", toggleTheme);
|
||||||
function toggleTheme() {
|
function toggleTheme() {
|
||||||
|
|
@ -34,8 +34,6 @@ function toggleTheme() {
|
||||||
setTheme(current === "dark" ? "light" : "dark");
|
setTheme(current === "dark" ? "light" : "dark");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
globalThis.window.matchMedia?.("(prefers-color-scheme: light)")
|
globalThis.window.matchMedia?.("(prefers-color-scheme: light)")
|
||||||
?.addEventListener("change", () => {
|
?.addEventListener("change", () => {
|
||||||
if (localStorage.getItem("theme")) return;
|
if (localStorage.getItem("theme")) return;
|
||||||
|
|
|
||||||
|
|
@ -1,143 +1,44 @@
|
||||||
// deno-lint-ignore-file no-unversioned-import
|
// deno-lint-ignore-file no-unversioned-import
|
||||||
|
|
||||||
// deno-lint-ignore no-import-prefix
|
// deno-lint-ignore no-import-prefix
|
||||||
import * as vis from "npm:vis-network/standalone";
|
import * as vis from "npm:vis-network/standalone";
|
||||||
import { automaton, setAutomaton, sim } from "./automata.ts";
|
|
||||||
|
|
||||||
export const nodes = new vis.DataSet<vis.Node>();
|
import { bus } from "./bus.ts";
|
||||||
export const edges = new vis.DataSet<vis.Edge>();
|
import type { Sim } from "./simulation.ts";
|
||||||
|
import type { Machine } from "./automata.ts";
|
||||||
|
|
||||||
type Color = string;
|
|
||||||
type GraphTheme = {
|
|
||||||
bg_0: Color;
|
|
||||||
bg_1: Color;
|
|
||||||
bg_2: Color;
|
|
||||||
fg_0: Color;
|
|
||||||
fg_1: Color;
|
|
||||||
fg_2: Color;
|
|
||||||
|
|
||||||
node_anchor: Color;
|
bus.on("controls/physics", ({enabled}) => {
|
||||||
node_border: Color;
|
network.setOptions({ physics: { enabled } });
|
||||||
current_node_border: Color;
|
network.setOptions({edges: {smooth: enabled}});
|
||||||
|
});
|
||||||
edge: Color;
|
bus.on("controls/reset_network", _ => {
|
||||||
edge_hover: Color;
|
try {
|
||||||
edge_active: Color;
|
nodes.forEach((n) => {
|
||||||
|
n.physics = true;
|
||||||
font_face: string
|
n.x = undefined;
|
||||||
|
n.y = undefined;
|
||||||
node_font_size: number;
|
});
|
||||||
node_font: string,
|
network.setData({ nodes, edges });
|
||||||
node_font_bold: string,
|
} catch {
|
||||||
|
// Last resort
|
||||||
edge_font_size: number;
|
network.setData({ nodes, edges });
|
||||||
edge_font: string,
|
|
||||||
edge_font_bold: string,
|
|
||||||
};
|
|
||||||
|
|
||||||
let _graphTheme: GraphTheme | null = null;
|
|
||||||
|
|
||||||
function invalidateGraphThemeCache() {
|
|
||||||
_graphTheme = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGraphTheme(): GraphTheme {
|
|
||||||
function cssVar(name: string, fallback = ""): string {
|
|
||||||
return getComputedStyle(document.documentElement)
|
|
||||||
.getPropertyValue(name)
|
|
||||||
.trim() || fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_graphTheme) return _graphTheme;
|
|
||||||
|
|
||||||
_graphTheme = {
|
|
||||||
bg_0: cssVar("--graph-bg-0"),
|
|
||||||
bg_1: cssVar("--graph-bg-1"),
|
|
||||||
bg_2: cssVar("--graph-bg-2"),
|
|
||||||
fg_0: cssVar("--graph-fg-0"),
|
|
||||||
fg_1: cssVar("--graph-fg-1"),
|
|
||||||
fg_2: cssVar("--graph-fg-2"),
|
|
||||||
|
|
||||||
node_anchor: cssVar("--graph-node-anchor"),
|
|
||||||
node_border: cssVar("--graph-node-border"),
|
|
||||||
current_node_border: cssVar("--graph-current-node-border"),
|
|
||||||
|
|
||||||
edge: cssVar("--graph-edge"),
|
|
||||||
edge_hover: cssVar("--graph-edge-hover"),
|
|
||||||
edge_active: cssVar("--graph-edge-active"),
|
|
||||||
|
|
||||||
font_face: cssVar("--graph-font"),
|
|
||||||
|
|
||||||
node_font_size: Number(cssVar("--graph-node-font-size")),
|
|
||||||
node_font: `${cssVar("--graph-node-font-size")}px ${cssVar("--graph-font")}`,
|
|
||||||
node_font_bold: `bold ${cssVar("--graph-node-font-size")}px ${cssVar("--graph-font")}`,
|
|
||||||
|
|
||||||
edge_font_size: Number(cssVar("--graph-edge-font-size")),
|
|
||||||
edge_font: `${Number(cssVar("--graph-edge-font-size"))}px ${cssVar("--graph-font")}`,
|
|
||||||
edge_font_bold: `bold ${Number(cssVar("--graph-edge-font-size"))}px ${cssVar("--graph-font")}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
return _graphTheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateGraphTheme() {
|
|
||||||
invalidateGraphThemeCache();
|
|
||||||
const gt = getGraphTheme();
|
|
||||||
|
|
||||||
network.setOptions({
|
|
||||||
nodes: {
|
|
||||||
labelHighlightBold: false,
|
|
||||||
font: {
|
|
||||||
color: gt.fg_0,
|
|
||||||
bold: {
|
|
||||||
color: gt.fg_1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
edges: {
|
|
||||||
labelHighlightBold: true,
|
|
||||||
font: {
|
|
||||||
align: "top",
|
|
||||||
face: gt.font_face,
|
|
||||||
size: gt.edge_font_size,
|
|
||||||
color: gt.fg_0,
|
|
||||||
strokeColor: gt.bg_0,
|
|
||||||
bold: {
|
|
||||||
color: gt.fg_1,
|
|
||||||
face: gt.font_face,
|
|
||||||
size: gt.edge_font_size,
|
|
||||||
mod: "bold",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
color: {
|
|
||||||
color: gt.edge,
|
|
||||||
hover: gt.edge_hover,
|
|
||||||
highlight: gt.edge_active,
|
|
||||||
},
|
|
||||||
shadow: {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setAutomaton(automaton)
|
bus.on("automata/sim/after_step", _ => {
|
||||||
}
|
network.redraw();
|
||||||
|
});
|
||||||
|
|
||||||
|
let simulation: Sim | null = null;
|
||||||
|
bus.on("automata/sim/update", ({simulation: sim}) => {
|
||||||
|
simulation = sim;
|
||||||
|
network.redraw();
|
||||||
|
});
|
||||||
|
|
||||||
let _measureCanvas: HTMLCanvasElement | null = null;
|
let automaton: Machine
|
||||||
|
|
||||||
export function measureTextWidth(text: string, font: string): number {
|
bus.on("automata/update", ({automaton: auto}) => {
|
||||||
if (!_measureCanvas) {
|
automaton = auto;
|
||||||
_measureCanvas = document.createElement("canvas");
|
|
||||||
}
|
|
||||||
|
|
||||||
const ctx = _measureCanvas.getContext("2d")!;
|
|
||||||
ctx.font = font;
|
|
||||||
|
|
||||||
return ctx.measureText(text).width;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateVisualization() {
|
|
||||||
// Populate nodes
|
// Populate nodes
|
||||||
for (const state of automaton.states.keys()) {
|
for (const state of automaton.states.keys()) {
|
||||||
|
|
||||||
|
|
@ -187,17 +88,154 @@ export function updateVisualization() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// delete old edges
|
||||||
for (const edge_id of edges.getIds()) {
|
for (const edge_id of edges.getIds()) {
|
||||||
if (!automaton.edges.has(edge_id as string)) {
|
if (!automaton.edges.has(edge_id as string)) {
|
||||||
edges.remove(edge_id);
|
edges.remove(edge_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// delete old nodes
|
||||||
for (const node_id of nodes.getIds()) {
|
for (const node_id of nodes.getIds()) {
|
||||||
if (!automaton.states.has(node_id as string)) {
|
if (!automaton.states.has(node_id as string)) {
|
||||||
nodes.remove(node_id);
|
nodes.remove(node_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const nodes = new vis.DataSet<vis.Node>();
|
||||||
|
const edges = new vis.DataSet<vis.Edge>();
|
||||||
|
|
||||||
|
|
||||||
|
let _graphTheme: GraphTheme | null = null;
|
||||||
|
bus.on("theme/update", _ => {
|
||||||
|
_graphTheme = null;
|
||||||
|
updateGraphTheme()
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
type Color = string;
|
||||||
|
type GraphTheme = {
|
||||||
|
bg_0: Color;
|
||||||
|
bg_1: Color;
|
||||||
|
bg_2: Color;
|
||||||
|
fg_0: Color;
|
||||||
|
fg_1: Color;
|
||||||
|
fg_2: Color;
|
||||||
|
|
||||||
|
node_anchor: Color;
|
||||||
|
node_border: Color;
|
||||||
|
current_node_border: Color;
|
||||||
|
|
||||||
|
edge: Color;
|
||||||
|
edge_hover: Color;
|
||||||
|
edge_active: Color;
|
||||||
|
|
||||||
|
font_face: string
|
||||||
|
|
||||||
|
node_font_size: number;
|
||||||
|
node_font: string,
|
||||||
|
node_font_bold: string,
|
||||||
|
|
||||||
|
edge_font_size: number;
|
||||||
|
edge_font: string,
|
||||||
|
edge_font_bold: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
function getGraphTheme(): GraphTheme {
|
||||||
|
function cssVar(name: string, fallback = ""): string {
|
||||||
|
return getComputedStyle(document.documentElement)
|
||||||
|
.getPropertyValue(name)
|
||||||
|
.trim() || fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_graphTheme) return _graphTheme;
|
||||||
|
|
||||||
|
_graphTheme = {
|
||||||
|
bg_0: cssVar("--graph-bg-0"),
|
||||||
|
bg_1: cssVar("--graph-bg-1"),
|
||||||
|
bg_2: cssVar("--graph-bg-2"),
|
||||||
|
fg_0: cssVar("--graph-fg-0"),
|
||||||
|
fg_1: cssVar("--graph-fg-1"),
|
||||||
|
fg_2: cssVar("--graph-fg-2"),
|
||||||
|
|
||||||
|
node_anchor: cssVar("--graph-node-anchor"),
|
||||||
|
node_border: cssVar("--graph-node-border"),
|
||||||
|
current_node_border: cssVar("--graph-current-node-border"),
|
||||||
|
|
||||||
|
edge: cssVar("--graph-edge"),
|
||||||
|
edge_hover: cssVar("--graph-edge-hover"),
|
||||||
|
edge_active: cssVar("--graph-edge-active"),
|
||||||
|
|
||||||
|
font_face: cssVar("--graph-font"),
|
||||||
|
|
||||||
|
node_font_size: Number(cssVar("--graph-node-font-size")),
|
||||||
|
node_font: `${cssVar("--graph-node-font-size")}px ${cssVar("--graph-font")}`,
|
||||||
|
node_font_bold: `bold ${cssVar("--graph-node-font-size")}px ${cssVar("--graph-font")}`,
|
||||||
|
|
||||||
|
edge_font_size: Number(cssVar("--graph-edge-font-size")),
|
||||||
|
edge_font: `${Number(cssVar("--graph-edge-font-size"))}px ${cssVar("--graph-font")}`,
|
||||||
|
edge_font_bold: `bold ${Number(cssVar("--graph-edge-font-size"))}px ${cssVar("--graph-font")}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return _graphTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateGraphTheme() {
|
||||||
|
const gt = getGraphTheme();
|
||||||
|
|
||||||
|
network.setOptions({
|
||||||
|
nodes: {
|
||||||
|
labelHighlightBold: false,
|
||||||
|
font: {
|
||||||
|
color: gt.fg_0,
|
||||||
|
bold: {
|
||||||
|
color: gt.fg_1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
edges: {
|
||||||
|
labelHighlightBold: true,
|
||||||
|
font: {
|
||||||
|
align: "top",
|
||||||
|
face: gt.font_face,
|
||||||
|
size: gt.edge_font_size,
|
||||||
|
color: gt.fg_0,
|
||||||
|
strokeColor: gt.bg_0,
|
||||||
|
bold: {
|
||||||
|
color: gt.fg_1,
|
||||||
|
face: gt.font_face,
|
||||||
|
size: gt.edge_font_size,
|
||||||
|
mod: "bold",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
color: gt.edge,
|
||||||
|
hover: gt.edge_hover,
|
||||||
|
highlight: gt.edge_active,
|
||||||
|
},
|
||||||
|
shadow: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
network.redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let _measureCanvas: HTMLCanvasElement | null = null;
|
||||||
|
|
||||||
|
function measureTextWidth(text: string, font: string): number {
|
||||||
|
if (!_measureCanvas) {
|
||||||
|
_measureCanvas = document.createElement("canvas");
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = _measureCanvas.getContext("2d")!;
|
||||||
|
ctx.font = font;
|
||||||
|
|
||||||
|
return ctx.measureText(text).width;
|
||||||
}
|
}
|
||||||
|
|
||||||
function chosen_edge(
|
function chosen_edge(
|
||||||
|
|
@ -216,7 +254,7 @@ function chosen_node(
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const network: vis.Network = createGraph();
|
const network: vis.Network = createGraph();
|
||||||
|
|
||||||
function createGraph(): vis.Network {
|
function createGraph(): vis.Network {
|
||||||
const container = document.getElementById("graph")!;
|
const container = document.getElementById("graph")!;
|
||||||
|
|
@ -278,6 +316,7 @@ function createGraph(): vis.Network {
|
||||||
return network;
|
return network;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function renderNode({
|
function renderNode({
|
||||||
ctx,
|
ctx,
|
||||||
id,
|
id,
|
||||||
|
|
@ -296,7 +335,7 @@ function renderNode({
|
||||||
const isFinal = automaton.final_states
|
const isFinal = automaton.final_states
|
||||||
? automaton.final_states.has(id)
|
? automaton.final_states.has(id)
|
||||||
: false;
|
: false;
|
||||||
const isActive = sim?sim.current_states.has(id):false;
|
const isActive = simulation?simulation.current_states.has(id):false;
|
||||||
|
|
||||||
const fill = selected ? t.bg_2 : hover ? t.bg_1 : t.bg_0;
|
const fill = selected ? t.bg_2 : hover ? t.bg_1 : t.bg_0;
|
||||||
const stroke = isActive ? t.current_node_border : t.node_border;
|
const stroke = isActive ? t.current_node_border : t.node_border;
|
||||||
|
|
@ -339,7 +378,7 @@ function renderNode({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
const paths = sim?.current_states.get(id)!;
|
const paths = simulation?.current_states.get(id)!;
|
||||||
const padX = 8;
|
const padX = 8;
|
||||||
const padY = 6;
|
const padY = 6;
|
||||||
const lineH = 14;
|
const lineH = 14;
|
||||||
|
|
|
||||||
|
|
@ -52,27 +52,45 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-blue {
|
.btn-yellow {
|
||||||
background: color-mix(in srgb, var(--accent) 14%, transparent);
|
background: color-mix(in srgb, var(--warning) 14%, transparent);
|
||||||
border-color: color-mix(in srgb, var(--accent) 40%, transparent);
|
border-color: color-mix(in srgb, var(--warning) 40%, transparent);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: color-mix(in srgb, var(--accent) 22%, transparent);
|
background: color-mix(in srgb, var(--warning) 22%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-blue {
|
||||||
|
background: color-mix(in srgb, var(--focus) 14%, transparent);
|
||||||
|
border-color: color-mix(in srgb, var(--focus) 40%, transparent);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: color-mix(in srgb, var(--focus) 22%, transparent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-grey {
|
.btn-grey {
|
||||||
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
background: color-mix(in srgb, var(--ansi-fg-90) 12%, transparent);
|
||||||
border-color: color-mix(in srgb, var(--accent) 28%, transparent);
|
border-color: color-mix(in srgb, var(--ansi-fg-90) 28%, transparent);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: color-mix(in srgb, var(--accent) 18%, transparent);
|
background: color-mix(in srgb, var(--ansi-fg-90) 18%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-red {
|
||||||
|
background: color-mix(in srgb, var(--error) 12%, transparent);
|
||||||
|
border-color: color-mix(in srgb, var(--error) 28%, transparent);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: color-mix(in srgb, var(--error) 18%, transparent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-toggle.active {
|
.btn-toggle.active {
|
||||||
background: color-mix(in srgb, var(--warning) 14%, transparent);
|
background: color-mix(in srgb, var(--success) 14%, transparent);
|
||||||
border-color: color-mix(in srgb, var(--warning) 30%, transparent);
|
border-color: color-mix(in srgb, var(--success) 30%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -97,3 +115,33 @@
|
||||||
text-align: right;
|
text-align: right;
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.test-input {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--bg-1);
|
||||||
|
background: var(--bg-2);
|
||||||
|
|
||||||
|
font: var(--font-ui);
|
||||||
|
color: var(--fg-0);
|
||||||
|
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 150ms ease, box-shadow 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-input::placeholder {
|
||||||
|
color: var(--fg-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-input:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 3px var(--accent);
|
||||||
|
}
|
||||||
|
|
@ -31,7 +31,6 @@
|
||||||
.cm-lineNumbers .cm-gutterElement {
|
.cm-lineNumbers .cm-gutterElement {
|
||||||
padding: 0 10px 0 6px;
|
padding: 0 10px 0 6px;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-activeLine {
|
.cm-activeLine {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--fg-0);
|
color: var(--fg-0);
|
||||||
font-family: var(--font-ui);
|
font-family: var(--font-ui);
|
||||||
|
font-size: 14px;
|
||||||
background: #909090;
|
background: #909090;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,6 +104,7 @@ body {
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid var(bg-1);
|
border: 1px solid var(bg-1);
|
||||||
background: var(--bg-2);
|
background: var(--bg-2);
|
||||||
|
font: var(--font-ui)
|
||||||
}
|
}
|
||||||
|
|
||||||
.share-toast {
|
.share-toast {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 12.5px;
|
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
|
|
||||||
|
|
@ -152,8 +152,8 @@ pub struct Graph<'a> {
|
||||||
#[wasm_bindgen(getter_with_clone)]
|
#[wasm_bindgen(getter_with_clone)]
|
||||||
pub struct CompileResult {
|
pub struct CompileResult {
|
||||||
pub log: Vec<CompileLog>,
|
pub log: Vec<CompileLog>,
|
||||||
pub log_formatted: String,
|
pub ansi_log: String,
|
||||||
pub graph: Option<String>,
|
pub machine: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
|
|
@ -161,10 +161,10 @@ pub fn compile(input: &str) -> CompileResult {
|
||||||
let mut ctx = Context::new(input);
|
let mut ctx = Context::new(input);
|
||||||
let result = automata::loader::parse_universal(&mut ctx);
|
let result = automata::loader::parse_universal(&mut ctx);
|
||||||
|
|
||||||
let graph = result.map(|result| serde_json::to_string(&result).unwrap());
|
let machine = result.map(|result| serde_json::to_string(&result).unwrap());
|
||||||
|
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
let log_formatted = ctx.logs_display().fold(String::new(), |mut s, e| {
|
let ansi_log = ctx.logs_display().fold(String::new(), |mut s, e| {
|
||||||
write!(&mut s, "{e}").unwrap();
|
write!(&mut s, "{e}").unwrap();
|
||||||
s
|
s
|
||||||
});
|
});
|
||||||
|
|
@ -190,7 +190,7 @@ pub fn compile(input: &str) -> CompileResult {
|
||||||
|
|
||||||
CompileResult {
|
CompileResult {
|
||||||
log,
|
log,
|
||||||
log_formatted,
|
ansi_log,
|
||||||
graph,
|
machine,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue