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
|
|
@ -96,7 +96,7 @@ fn begin_ident(c: char) -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn continue_ident(c: char) -> bool {
|
fn continue_ident(c: char) -> bool {
|
||||||
c.is_alphanumeric() || c == '_' || (!c.is_ascii() && !c.is_control() && !c.is_whitespace())
|
c.is_alphanumeric() || c == '_' || c=='\'' || (!c.is_ascii() && !c.is_control() && !c.is_whitespace())
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> std::iter::Iterator for Lexer<'a> {
|
impl<'a> std::iter::Iterator for Lexer<'a> {
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,24 @@
|
||||||
|
|
||||||
<div class="vSplit" style="--split-default: 20%" title="Drag to resize canvas width"></div>
|
<div class="vSplit" style="--split-default: 20%" title="Drag to resize canvas width"></div>
|
||||||
|
|
||||||
<section>
|
<section class="flexCol gap marginTop">
|
||||||
<button id="themeToggle" class="btn btn-grey" title="Toggle light/dark">
|
<div class="flexCenter sidePadding gap">
|
||||||
🌙 Dark
|
<button id="themeToggle" class="btn btn-grey" title="Toggle light/dark">
|
||||||
</button>
|
🌙 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>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,19 @@ export function machine_from_json(json: string): Machine {
|
||||||
}
|
}
|
||||||
machine.edges = new Map();
|
machine.edges = new Map();
|
||||||
|
|
||||||
|
machine.transitions_components = new Map();
|
||||||
switch (machine.type) {
|
switch (machine.type) {
|
||||||
case "fa":
|
case "fa":
|
||||||
{
|
{
|
||||||
for (const [from, tos] of machine.transitions) {
|
for (const [from, tos] of machine.transitions) {
|
||||||
for (const to of tos) {
|
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;
|
const edge = from.state + "#" + to.state;
|
||||||
if (!machine.edges.has(edge)) machine.edges.set(edge, []);
|
if (!machine.edges.has(edge)) machine.edges.set(edge, []);
|
||||||
machine.edges.get(edge)?.push({
|
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));
|
machine.symbols = new Map(Object.entries(machine.symbols));
|
||||||
for (const [from, tos] of machine.transitions) {
|
for (const [from, tos] of machine.transitions) {
|
||||||
for (const to of tos) {
|
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;
|
const edge = from.state + "#" + to.state;
|
||||||
if (!machine.edges.has(edge)) machine.edges.set(edge, []);
|
if (!machine.edges.has(edge)) machine.edges.set(edge, []);
|
||||||
machine.edges.get(edge)?.push({
|
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));
|
machine.symbols = new Map(Object.entries(machine.symbols));
|
||||||
for (const [from, tos] of machine.transitions) {
|
for (const [from, tos] of machine.transitions) {
|
||||||
for (const to of tos) {
|
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;
|
const edge = from.state + "#" + to.state;
|
||||||
if (!machine.edges.has(edge)) machine.edges.set(edge, []);
|
if (!machine.edges.has(edge)) machine.edges.set(edge, []);
|
||||||
machine.edges.get(edge)?.push({
|
machine.edges.get(edge)?.push({
|
||||||
|
|
@ -108,6 +132,7 @@ export type Fa = {
|
||||||
final_states: Map<State, StateInfo>;
|
final_states: Map<State, StateInfo>;
|
||||||
|
|
||||||
transitions: Map<FaTransFrom, FaTransTo[]>;
|
transitions: Map<FaTransFrom, FaTransTo[]>;
|
||||||
|
transitions_components: Map<State, Map<Letter|null, FaTransTo[]>>;
|
||||||
|
|
||||||
edges: Map<string, Edge[]>;
|
edges: Map<string, Edge[]>;
|
||||||
};
|
};
|
||||||
|
|
@ -137,6 +162,7 @@ export type Pda = {
|
||||||
final_states: Map<State, StateInfo> | null;
|
final_states: Map<State, StateInfo> | null;
|
||||||
|
|
||||||
transitions: Map<PdaTransFrom, PdaTransTo[]>;
|
transitions: Map<PdaTransFrom, PdaTransTo[]>;
|
||||||
|
transitions_components: Map<State, Map<Symbol, Map<Letter|null, PdaTransTo[]>>>;
|
||||||
|
|
||||||
edges: Map<string, Edge[]>;
|
edges: Map<string, Edge[]>;
|
||||||
};
|
};
|
||||||
|
|
@ -166,6 +192,8 @@ export type Tm = {
|
||||||
final_states: Map<State, StateInfo>;
|
final_states: Map<State, StateInfo>;
|
||||||
|
|
||||||
transitions: Map<TmTransFrom, TmTransTo[]>;
|
transitions: Map<TmTransFrom, TmTransTo[]>;
|
||||||
|
transitions_components: Map<State, Map<Symbol, TmTransTo[]>>;
|
||||||
|
|
||||||
edges: Map<string, Edge[]>;
|
edges: Map<string, Edge[]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ import { terminalPlugin } from "./terminal.ts";
|
||||||
|
|
||||||
import { setAutomaton } from "./visualizer.ts";
|
import { setAutomaton } from "./visualizer.ts";
|
||||||
import { machine_from_json } from "./automata.ts";
|
import { machine_from_json } from "./automata.ts";
|
||||||
|
import { sharedText } from "./share.ts";
|
||||||
|
import { examples } from "./examples.ts";
|
||||||
|
|
||||||
|
|
||||||
function tokenize(text: string) {
|
function tokenize(text: string) {
|
||||||
|
|
@ -43,38 +45,21 @@ function compile(text: string): wasm.CompileResult {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokenClass = (t: string) =>
|
export const analysisField = StateField.define({
|
||||||
({
|
create(state) {
|
||||||
comment: "tok-comment",
|
const text = state.doc.toString();
|
||||||
keyword: "tok-keyword",
|
return buildAnalysis(text, state.doc);
|
||||||
error: "tok-error",
|
},
|
||||||
ident: "tok-ident",
|
update(value, tr) {
|
||||||
punc: "tok-punc",
|
if (!tr.docChanged) return value;
|
||||||
string: "tok-string",
|
const text = tr.state.doc.toString();
|
||||||
lpar: "rb-",
|
return buildAnalysis(text, tr.state.doc);
|
||||||
lbrace: "rb-",
|
},
|
||||||
lbracket: "rb-",
|
provide: (f) => EditorView.decorations.from(f, (v) => v.deco),
|
||||||
|
});
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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, log_formatted, graph } = compile(text);
|
||||||
|
|
||||||
|
|
@ -86,7 +71,6 @@ function buildAnalysis(text: string, doc: Text) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build ONE Decoration set: syntax + diagnostics
|
|
||||||
const marks = [];
|
const marks = [];
|
||||||
const docLen = doc.length;
|
const docLen = doc.length;
|
||||||
|
|
||||||
|
|
@ -120,18 +104,35 @@ function buildAnalysis(text: string, doc: Text) {
|
||||||
return { tokens, log, log_formatted, deco };
|
return { tokens, log, log_formatted, deco };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const analysisField = StateField.define({
|
const tokenClass = (t: string) =>
|
||||||
create(state) {
|
({
|
||||||
const text = state.doc.toString();
|
comment: "tok-comment",
|
||||||
return buildAnalysis(text, state.doc);
|
keyword: "tok-keyword",
|
||||||
},
|
error: "tok-error",
|
||||||
update(value, tr) {
|
ident: "tok-ident",
|
||||||
if (!tr.docChanged) return value;
|
punc: "tok-punc",
|
||||||
const text = tr.state.doc.toString();
|
string: "tok-string",
|
||||||
return buildAnalysis(text, tr.state.doc);
|
lpar: "rb-",
|
||||||
},
|
lbrace: "rb-",
|
||||||
provide: (f) => EditorView.decorations.from(f, (v) => v.deco),
|
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) =====================
|
// ===================== Hover tooltip (uses cached diags) =====================
|
||||||
const diagHover = hoverTooltip((view, pos) => {
|
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
|
function getSaved(): string | undefined{
|
||||||
Q = {q0, q1} // states
|
return globalThis.localStorage.save;
|
||||||
E = {a, b} // alphabet
|
}
|
||||||
T = {z0, A, B} // stack
|
|
||||||
q0 = q0
|
|
||||||
z0 = z0
|
|
||||||
|
|
||||||
// construct all possible permutations of A's and B's
|
export function setText(text: string){
|
||||||
d(q0, epsilon, z0) = { (q0, [A z0]), (q0, [B z0]) }
|
editor.dispatch({ changes: { from: 0, to: editor.state.doc.length, insert: text } });
|
||||||
d(q0, epsilon, A) = { (q0, [A A]), (q0, [B A]) }
|
}
|
||||||
|
|
||||||
d(q0, epsilon, B) = { (q0, [A B]), (q0, [B B]) }
|
export function getText(): string{
|
||||||
|
return editor.state.doc.toString()
|
||||||
// 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 state = EditorState.create({
|
const state = EditorState.create({
|
||||||
doc: initialText,
|
doc: sharedText() ?? getSaved() ?? examples[0].machine,
|
||||||
extensions: [
|
extensions: [
|
||||||
lineNumbers(),
|
lineNumbers(),
|
||||||
highlightActiveLineGutter(),
|
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 "./visualizer.ts"
|
||||||
import "./splitters.ts"
|
import "./splitters.ts"
|
||||||
import "./controls.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;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flexCenter{
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap {
|
||||||
|
gap: 1em
|
||||||
|
}
|
||||||
|
|
||||||
|
.marginTop {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidePadding {
|
||||||
|
padding: 0px 1em;
|
||||||
|
}
|
||||||
|
|
||||||
.hSplit {
|
.hSplit {
|
||||||
:not( .styleOnly){
|
:not( .styleOnly){
|
||||||
cursor: col-resize;
|
cursor: col-resize;
|
||||||
|
|
@ -72,3 +89,56 @@ body {
|
||||||
.vSplit:hover:not(.styleOnly) {
|
.vSplit:hover:not(.styleOnly) {
|
||||||
background: var(--separator-hover);
|
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