mirror of
https://github.com/ParkerTenBroeck/automata.git
synced 2026-06-06 21:24:06 -04:00
added example selector, added share button, added saving to local storage, layout change
This commit is contained in:
parent
9dcef68d82
commit
2edc7aca3c
8 changed files with 427 additions and 70 deletions
|
|
@ -28,10 +28,24 @@
|
|||
|
||||
<div class="vSplit" style="--split-default: 20%" title="Drag to resize canvas width"></div>
|
||||
|
||||
<section>
|
||||
<button id="themeToggle" class="btn btn-grey" title="Toggle light/dark">
|
||||
🌙 Dark
|
||||
</button>
|
||||
<section class="flexCol gap marginTop">
|
||||
<div class="flexCenter sidePadding gap">
|
||||
<button id="themeToggle" class="btn btn-grey" title="Toggle light/dark">
|
||||
🌙 Dark
|
||||
</button>
|
||||
<button class="btn btm-grey" style="position: relative" id="shareBtn" type="button">
|
||||
Share
|
||||
<span class="share-toast" id="shareToast">
|
||||
Copied to clipboard
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flexCenter sidePadding">
|
||||
<select id="exampleSelect" class="ex-select">
|
||||
<option value="" selected disabled>Choose an example…</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="hSplit styleOnly" title="Drag to resize canvas height"></div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
|
|
|
|||
|
|
@ -19,11 +19,19 @@ export function machine_from_json(json: string): Machine {
|
|||
}
|
||||
machine.edges = new Map();
|
||||
|
||||
machine.transitions_components = new Map();
|
||||
switch (machine.type) {
|
||||
case "fa":
|
||||
{
|
||||
for (const [from, tos] of machine.transitions) {
|
||||
for (const to of tos) {
|
||||
const layer_0 = machine.transitions_components;
|
||||
if(!layer_0.has(from.state)) layer_0.set(from.state, new Map());
|
||||
const layer_1 = machine.transitions_components.get(from.state)!;
|
||||
if(!layer_1.has(from.letter)) layer_1.set(from.letter, []);
|
||||
const layer_2 = layer_1.get(from.letter)!;
|
||||
layer_2.push(to);
|
||||
|
||||
const edge = from.state + "#" + to.state;
|
||||
if (!machine.edges.has(edge)) machine.edges.set(edge, []);
|
||||
machine.edges.get(edge)?.push({
|
||||
|
|
@ -40,6 +48,15 @@ export function machine_from_json(json: string): Machine {
|
|||
machine.symbols = new Map(Object.entries(machine.symbols));
|
||||
for (const [from, tos] of machine.transitions) {
|
||||
for (const to of tos) {
|
||||
const layer_0 = machine.transitions_components;
|
||||
if(!layer_0.has(from.state)) layer_0.set(from.state, new Map());
|
||||
const layer_1 = machine.transitions_components.get(from.state)!;
|
||||
if(!layer_1.has(from.symbol)) layer_1.set(from.symbol, new Map());
|
||||
const layer_2 = layer_1.get(from.symbol)!;
|
||||
if(!layer_2.has(from.letter)) layer_2.set(from.letter, []);
|
||||
const layer_3 = layer_2.get(from.letter)!;
|
||||
layer_3.push(to);
|
||||
|
||||
const edge = from.state + "#" + to.state;
|
||||
if (!machine.edges.has(edge)) machine.edges.set(edge, []);
|
||||
machine.edges.get(edge)?.push({
|
||||
|
|
@ -56,6 +73,13 @@ export function machine_from_json(json: string): Machine {
|
|||
machine.symbols = new Map(Object.entries(machine.symbols));
|
||||
for (const [from, tos] of machine.transitions) {
|
||||
for (const to of tos) {
|
||||
const layer_0 = machine.transitions_components;
|
||||
if(!layer_0.has(from.state)) layer_0.set(from.state, new Map());
|
||||
const layer_1 = machine.transitions_components.get(from.state)!;
|
||||
if(!layer_1.has(from.symbol)) layer_1.set(from.symbol, []);
|
||||
const layer_2 = layer_1.get(from.symbol)!;
|
||||
layer_2.push(to);
|
||||
|
||||
const edge = from.state + "#" + to.state;
|
||||
if (!machine.edges.has(edge)) machine.edges.set(edge, []);
|
||||
machine.edges.get(edge)?.push({
|
||||
|
|
@ -108,6 +132,7 @@ export type Fa = {
|
|||
final_states: Map<State, StateInfo>;
|
||||
|
||||
transitions: Map<FaTransFrom, FaTransTo[]>;
|
||||
transitions_components: Map<State, Map<Letter|null, FaTransTo[]>>;
|
||||
|
||||
edges: Map<string, Edge[]>;
|
||||
};
|
||||
|
|
@ -137,6 +162,7 @@ export type Pda = {
|
|||
final_states: Map<State, StateInfo> | null;
|
||||
|
||||
transitions: Map<PdaTransFrom, PdaTransTo[]>;
|
||||
transitions_components: Map<State, Map<Symbol, Map<Letter|null, PdaTransTo[]>>>;
|
||||
|
||||
edges: Map<string, Edge[]>;
|
||||
};
|
||||
|
|
@ -166,6 +192,8 @@ export type Tm = {
|
|||
final_states: Map<State, StateInfo>;
|
||||
|
||||
transitions: Map<TmTransFrom, TmTransTo[]>;
|
||||
transitions_components: Map<State, Map<Symbol, TmTransTo[]>>;
|
||||
|
||||
edges: Map<string, Edge[]>;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ import { terminalPlugin } from "./terminal.ts";
|
|||
|
||||
import { setAutomaton } from "./visualizer.ts";
|
||||
import { machine_from_json } from "./automata.ts";
|
||||
import { sharedText } from "./share.ts";
|
||||
import { examples } from "./examples.ts";
|
||||
|
||||
|
||||
function tokenize(text: string) {
|
||||
|
|
@ -43,38 +45,21 @@ function compile(text: string): wasm.CompileResult {
|
|||
}
|
||||
}
|
||||
|
||||
const tokenClass = (t: string) =>
|
||||
({
|
||||
comment: "tok-comment",
|
||||
keyword: "tok-keyword",
|
||||
error: "tok-error",
|
||||
ident: "tok-ident",
|
||||
punc: "tok-punc",
|
||||
string: "tok-string",
|
||||
lpar: "rb-",
|
||||
lbrace: "rb-",
|
||||
lbracket: "rb-",
|
||||
|
||||
rpar: "rb-",
|
||||
rbrace: "rb-",
|
||||
rbracket: "rb-",
|
||||
}[t] || "tok-ident");
|
||||
|
||||
|
||||
function severityClass(sev: string) {
|
||||
const s = (sev || "error").toLowerCase();
|
||||
if (s === "warning") return "cm-diag-warning";
|
||||
if (s === "info") return "cm-diag-info";
|
||||
return "cm-diag-error";
|
||||
}
|
||||
function sevRank(sev: string) {
|
||||
if (sev === "error") return 3;
|
||||
if (sev === "warning") return 2;
|
||||
return 1;
|
||||
}
|
||||
|
||||
export const analysisField = StateField.define({
|
||||
create(state) {
|
||||
const text = state.doc.toString();
|
||||
return buildAnalysis(text, state.doc);
|
||||
},
|
||||
update(value, tr) {
|
||||
if (!tr.docChanged) return value;
|
||||
const text = tr.state.doc.toString();
|
||||
return buildAnalysis(text, tr.state.doc);
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f, (v) => v.deco),
|
||||
});
|
||||
|
||||
function buildAnalysis(text: string, doc: Text) {
|
||||
save(text);
|
||||
const tokens = tokenize(text);
|
||||
const { log, log_formatted, graph } = compile(text);
|
||||
|
||||
|
|
@ -86,7 +71,6 @@ function buildAnalysis(text: string, doc: Text) {
|
|||
}
|
||||
}
|
||||
|
||||
// Build ONE Decoration set: syntax + diagnostics
|
||||
const marks = [];
|
||||
const docLen = doc.length;
|
||||
|
||||
|
|
@ -120,18 +104,35 @@ function buildAnalysis(text: string, doc: Text) {
|
|||
return { tokens, log, log_formatted, deco };
|
||||
}
|
||||
|
||||
export const analysisField = StateField.define({
|
||||
create(state) {
|
||||
const text = state.doc.toString();
|
||||
return buildAnalysis(text, state.doc);
|
||||
},
|
||||
update(value, tr) {
|
||||
if (!tr.docChanged) return value;
|
||||
const text = tr.state.doc.toString();
|
||||
return buildAnalysis(text, tr.state.doc);
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f, (v) => v.deco),
|
||||
});
|
||||
const tokenClass = (t: string) =>
|
||||
({
|
||||
comment: "tok-comment",
|
||||
keyword: "tok-keyword",
|
||||
error: "tok-error",
|
||||
ident: "tok-ident",
|
||||
punc: "tok-punc",
|
||||
string: "tok-string",
|
||||
lpar: "rb-",
|
||||
lbrace: "rb-",
|
||||
lbracket: "rb-",
|
||||
|
||||
rpar: "rb-",
|
||||
rbrace: "rb-",
|
||||
rbracket: "rb-",
|
||||
}[t] || "tok-ident");
|
||||
|
||||
|
||||
function severityClass(sev: string) {
|
||||
const s = (sev || "error").toLowerCase();
|
||||
if (s === "warning") return "cm-diag-warning";
|
||||
if (s === "info") return "cm-diag-info";
|
||||
return "cm-diag-error";
|
||||
}
|
||||
function sevRank(sev: string) {
|
||||
if (sev === "error") return 3;
|
||||
if (sev === "warning") return 2;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ===================== Hover tooltip (uses cached diags) =====================
|
||||
const diagHover = hoverTooltip((view, pos) => {
|
||||
|
|
@ -169,32 +170,24 @@ const diagHover = hoverTooltip((view, pos) => {
|
|||
};
|
||||
});
|
||||
|
||||
function save(text: string){
|
||||
globalThis.localStorage.save = text;
|
||||
}
|
||||
|
||||
const initialText = `type=NPDA
|
||||
Q = {q0, q1} // states
|
||||
E = {a, b} // alphabet
|
||||
T = {z0, A, B} // stack
|
||||
q0 = q0
|
||||
z0 = z0
|
||||
function getSaved(): string | undefined{
|
||||
return globalThis.localStorage.save;
|
||||
}
|
||||
|
||||
// construct all possible permutations of A's and B's
|
||||
d(q0, epsilon, z0) = { (q0, [A z0]), (q0, [B z0]) }
|
||||
d(q0, epsilon, A) = { (q0, [A A]), (q0, [B A]) }
|
||||
export function setText(text: string){
|
||||
editor.dispatch({ changes: { from: 0, to: editor.state.doc.length, insert: text } });
|
||||
}
|
||||
|
||||
d(q0, epsilon, B) = { (q0, [A B]), (q0, [B B]) }
|
||||
|
||||
// transition to q1
|
||||
d(q0, epsilon, z0) = { (q1, z0) }
|
||||
d(q0, epsilon, A) = { (q1, A) }
|
||||
d(q0, epsilon, B) = { (q1, B) }
|
||||
|
||||
// consume stack until empty
|
||||
d(q1, a, A) = { (q1, epsilon) }
|
||||
d(q1, b, B) = { (q1, epsilon) }
|
||||
`;
|
||||
export function getText(): string{
|
||||
return editor.state.doc.toString()
|
||||
}
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: initialText,
|
||||
doc: sharedText() ?? getSaved() ?? examples[0].machine,
|
||||
extensions: [
|
||||
lineNumbers(),
|
||||
highlightActiveLineGutter(),
|
||||
|
|
|
|||
215
web/root/src/examples.ts
Normal file
215
web/root/src/examples.ts
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
import { setText } from "./editor.ts";
|
||||
|
||||
export type Category =
|
||||
| "Tutorial"
|
||||
| "DFA"
|
||||
| "NFA"
|
||||
| "DPDA"
|
||||
| "NPDA"
|
||||
| "TM"
|
||||
| "NTM";
|
||||
|
||||
export class Example {
|
||||
category: Category;
|
||||
title: string;
|
||||
machine: string;
|
||||
|
||||
constructor(category: Category, title: string, machine: string) {
|
||||
this.category = category;
|
||||
this.title = title;
|
||||
this.machine = machine;
|
||||
}
|
||||
}
|
||||
|
||||
export const examples: Example[] = [
|
||||
new Example(
|
||||
"Tutorial",
|
||||
"DFA",
|
||||
`// strings over a,b which start and end with different letters
|
||||
|
||||
type = DFA // type of machine DFA, NFA, DPDA, NPDA, DTM, NTM
|
||||
Q = {q0, qa, qa', qb, qb'} // set of states
|
||||
E = {a, b} // alphabet
|
||||
F = {qa', qb'} // set of final states
|
||||
q0 = q0 // initial state
|
||||
|
||||
// transition function (state, letter) -> state
|
||||
d(q0, a) = qa
|
||||
d(q0, b) = qb
|
||||
|
||||
d(qa, a) = qa
|
||||
d(qa, b) = qa'
|
||||
|
||||
d(qa', a) = qa
|
||||
d(qa', b) = qa'
|
||||
|
||||
d(qb, a) = qb'
|
||||
d(qb, b) = qb
|
||||
|
||||
d(qb', a) = qb'
|
||||
d(qb', b) = qb`,
|
||||
),
|
||||
|
||||
new Example(
|
||||
"DFA",
|
||||
"modulo",
|
||||
`type=DFA
|
||||
E={1,2,3}
|
||||
Q={q0, q1, q2, q3, q4}
|
||||
F = {q0}
|
||||
q0=q0
|
||||
|
||||
d(q0, 1) = q1
|
||||
d(q1, 1) = q2
|
||||
d(q2, 1) = q3
|
||||
d(q3, 1) = q4
|
||||
d(q4, 1) = q0
|
||||
|
||||
d(q0, 2) = q2
|
||||
d(q1, 2) = q3
|
||||
d(q2, 2) = q4
|
||||
d(q3, 2) = q0
|
||||
d(q4, 2) = q1
|
||||
|
||||
d(q0, 3) = q3
|
||||
d(q1, 3) = q4
|
||||
d(q2, 3) = q0
|
||||
d(q3, 3) = q1
|
||||
d(q4, 3) = q2`,
|
||||
),
|
||||
|
||||
new Example(
|
||||
"DPDA",
|
||||
"unequal",
|
||||
`type=DPDA
|
||||
Q = {q0, qas, qeq, qmb, qlb} // states
|
||||
E = {a, b} // alphabet
|
||||
T = {z0, A} // stack
|
||||
F = {qmb, qlb} // final states
|
||||
q0 = q0
|
||||
z0 = z0
|
||||
|
||||
d(q0, a, z0) = (qas, z0)
|
||||
|
||||
d(qas, a, z0) = (qas, [A z0])
|
||||
d(qas, b, z0) = (qeq, z0)
|
||||
d(qas, a, A) = (qas, [A A])
|
||||
d(qas, b, A) = (qlb, ~)
|
||||
|
||||
d(qlb, b, A) = (qeq, ~)
|
||||
d(qlb, b, z0) = (qeq, z0)
|
||||
|
||||
d(qeq, b, z0) = (qmb, z0)
|
||||
|
||||
d(qmb, b, z0) = (qmb, z0)`,
|
||||
),
|
||||
|
||||
new Example(
|
||||
"NPDA",
|
||||
"unequal",
|
||||
`type=NPDA
|
||||
Q = {q0, q1} // states
|
||||
E = {a, b} // alphabet
|
||||
T = {z0, A, B} // stack
|
||||
q0 = q0
|
||||
z0 = z0
|
||||
|
||||
// construct all possible permutations of A's and B's
|
||||
d(q0, epsilon, z0) = { (q0, [A z0]), (q0, [B z0]) }
|
||||
d(q0, epsilon, A) = { (q0, [A A]), (q0, [B A]) }
|
||||
|
||||
d(q0, epsilon, B) = { (q0, [A B]), (q0, [B B]) }
|
||||
|
||||
// transition to q1
|
||||
d(q0, epsilon, z0) = { (q1, z0) }
|
||||
d(q0, epsilon, A) = { (q1, A) }
|
||||
d(q0, epsilon, B) = { (q1, B) }
|
||||
|
||||
// consume stack until empty
|
||||
d(q1, a, A) = { (q1, epsilon) }
|
||||
d(q1, b, B) = { (q1, epsilon) }`,
|
||||
),
|
||||
];
|
||||
|
||||
const CATEGORY_ORDER: Category[] = [
|
||||
"Tutorial",
|
||||
"DFA",
|
||||
"NFA",
|
||||
"DPDA",
|
||||
"NPDA",
|
||||
"TM",
|
||||
"NTM",
|
||||
];
|
||||
|
||||
function buildExamplesDropdown(
|
||||
selectEl: HTMLSelectElement,
|
||||
examples: Example[],
|
||||
onPick?: (ex: Example) => void,
|
||||
) {
|
||||
// Clear everything except the first placeholder option (if present)
|
||||
const keepFirstPlaceholder = selectEl.options.length > 0 &&
|
||||
selectEl.options[0].disabled && selectEl.options[0].value === "";
|
||||
|
||||
selectEl.innerHTML = "";
|
||||
if (keepFirstPlaceholder) {
|
||||
const placeholder = document.createElement("option");
|
||||
placeholder.value = "";
|
||||
placeholder.disabled = true;
|
||||
placeholder.selected = true;
|
||||
placeholder.textContent = "Choose an example…";
|
||||
selectEl.appendChild(placeholder);
|
||||
}
|
||||
|
||||
// Group examples by category
|
||||
const grouped = new Map<Category, Example[]>();
|
||||
for (const ex of examples) {
|
||||
if (!grouped.has(ex.category)) grouped.set(ex.category, []);
|
||||
grouped.get(ex.category)!.push(ex);
|
||||
}
|
||||
|
||||
// Optional: sort titles within each group
|
||||
for (const [cat, list] of grouped) {
|
||||
list.sort((a, b) => a.title.localeCompare(b.title));
|
||||
grouped.set(cat, list);
|
||||
}
|
||||
|
||||
// Create optgroups in your preferred order (and then any extras)
|
||||
const categoriesToRender: Category[] = [
|
||||
...CATEGORY_ORDER.filter((c) => grouped.has(c)),
|
||||
...Array.from(grouped.keys()).filter((c) => !CATEGORY_ORDER.includes(c))
|
||||
.sort(),
|
||||
];
|
||||
|
||||
// We'll store a stable reference via an index into the examples array
|
||||
// (simplest + avoids encoding large machine strings into <option value>)
|
||||
const indexByIdentity = new Map<Example, number>();
|
||||
examples.forEach((ex, i) => indexByIdentity.set(ex, i));
|
||||
|
||||
for (const category of categoriesToRender) {
|
||||
const optgroup = document.createElement("optgroup");
|
||||
optgroup.label = category;
|
||||
|
||||
for (const ex of grouped.get(category)!) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = String(indexByIdentity.get(ex)!); // index
|
||||
opt.textContent = ex.title;
|
||||
optgroup.appendChild(opt);
|
||||
}
|
||||
|
||||
selectEl.appendChild(optgroup);
|
||||
}
|
||||
|
||||
// Change handler
|
||||
selectEl.onchange = () => {
|
||||
const v = selectEl.value;
|
||||
if (!v) return;
|
||||
const picked = examples[Number(v)];
|
||||
if (picked && onPick) onPick(picked);
|
||||
selectEl.value = "";
|
||||
};
|
||||
}
|
||||
|
||||
const selectEl = document.getElementById("exampleSelect") as HTMLSelectElement;
|
||||
buildExamplesDropdown(selectEl, examples, (picked) => {
|
||||
setText(picked.machine);
|
||||
});
|
||||
|
|
@ -2,4 +2,6 @@ import "./editor.ts"
|
|||
import "./visualizer.ts"
|
||||
import "./splitters.ts"
|
||||
import "./controls.ts"
|
||||
import "./theme.ts"
|
||||
import "./theme.ts"
|
||||
import "./share.ts"
|
||||
import "./examples.ts"
|
||||
35
web/root/src/share.ts
Normal file
35
web/root/src/share.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { getText } from "./editor.ts";
|
||||
|
||||
const btn = document.getElementById("shareBtn")!;
|
||||
const toast = document.getElementById("shareToast")!;
|
||||
|
||||
function generateShareLink() {
|
||||
return `${globalThis.window.location.href}?share=${encodeURIComponent(getText())}`;
|
||||
}
|
||||
|
||||
async function copy(text: string) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
}
|
||||
|
||||
btn.addEventListener("click", async () => {
|
||||
await copy(generateShareLink());
|
||||
|
||||
toast.classList.remove("show");
|
||||
void toast.offsetWidth;
|
||||
toast.classList.add("show");
|
||||
});
|
||||
|
||||
|
||||
export function sharedText(): string|null {
|
||||
const url = new URL(globalThis.window.location.href);
|
||||
const text: string | null = url.searchParams.get("share");
|
||||
if (text !== null) {
|
||||
url.searchParams.delete("share");
|
||||
globalThis.window.history.replaceState(
|
||||
{},
|
||||
document.title,
|
||||
url.pathname + url.search + url.hash
|
||||
);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
|
@ -42,6 +42,23 @@ body {
|
|||
flex-direction: row;
|
||||
}
|
||||
|
||||
.flexCenter{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.gap {
|
||||
gap: 1em
|
||||
}
|
||||
|
||||
.marginTop {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.sidePadding {
|
||||
padding: 0px 1em;
|
||||
}
|
||||
|
||||
.hSplit {
|
||||
:not( .styleOnly){
|
||||
cursor: col-resize;
|
||||
|
|
@ -72,3 +89,56 @@ body {
|
|||
.vSplit:hover:not(.styleOnly) {
|
||||
background: var(--separator-hover);
|
||||
}
|
||||
|
||||
|
||||
.ex-label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.ex-select {
|
||||
width: 320px;
|
||||
max-width: 100%;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(bg-1);
|
||||
background: var(--bg-2);
|
||||
}
|
||||
|
||||
.share-toast {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: calc(100% + 10px);
|
||||
transform: translateX(-50%);
|
||||
|
||||
padding: 0.45rem 0.65rem;
|
||||
border-radius: 10px;
|
||||
background: var(--bg-2);
|
||||
color: var(--fg-0);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.share-toast.show {
|
||||
animation: toastFade 1.4s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes toastFade {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(-4px);
|
||||
}
|
||||
15% {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
80% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue