mirror of
https://github.com/ParkerTenBroeck/automata.git
synced 2026-06-07 05:28:45 -04:00
Merge branch 'main' into gh-pages
This commit is contained in:
commit
31b554e776
11 changed files with 745 additions and 239 deletions
63
Cargo.lock
generated
63
Cargo.lock
generated
|
|
@ -12,6 +12,8 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"automata",
|
"automata",
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
@ -38,6 +40,12 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.83"
|
version = "0.3.83"
|
||||||
|
|
@ -48,6 +56,12 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.3"
|
version = "1.21.3"
|
||||||
|
|
@ -78,6 +92,49 @@ version = "1.0.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.149"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.111"
|
version = "2.0.111"
|
||||||
|
|
@ -149,3 +206,9 @@ dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8"
|
||||||
|
|
|
||||||
|
|
@ -72,9 +72,21 @@ pub struct StateMap<T>(Vec<T>);
|
||||||
|
|
||||||
index!(StateMap, self, self.0, index.0 as usize, index = State);
|
index!(StateMap, self, self.0, index.0 as usize, index = State);
|
||||||
|
|
||||||
|
impl<T> StateMap<T>{
|
||||||
|
pub fn entries(&self) -> impl Iterator<Item = (State, &T)>{
|
||||||
|
self.0.iter().enumerate().map(|(i, v)|(State(i as u16), v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct SymbolMap<T>(Vec<T>);
|
pub struct SymbolMap<T>(Vec<T>);
|
||||||
|
|
||||||
|
impl<T> SymbolMap<T>{
|
||||||
|
pub fn entries(&self) -> impl Iterator<Item = (Symbol, &T)>{
|
||||||
|
self.0.iter().enumerate().map(|(i, v)|(Symbol(i as u16), v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
index!(SymbolMap, self, self.0, index.0 as usize, index = Symbol);
|
index!(SymbolMap, self, self.0, index.0 as usize, index = Symbol);
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
|
|
@ -83,6 +95,16 @@ pub struct StateSymbolMap<T> {
|
||||||
max_state: u16,
|
max_state: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> StateSymbolMap<T>{
|
||||||
|
pub fn entries(&self) -> impl Iterator<Item = ((State, Symbol), &T)>{
|
||||||
|
self.map.iter().enumerate().map(|(i, v)|{
|
||||||
|
let state = State((i % self.max_state as usize) as u16);
|
||||||
|
let symbol = Symbol((i / self.max_state as usize) as u16);
|
||||||
|
((state, symbol), v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
index!(
|
index!(
|
||||||
StateSymbolMap,
|
StateSymbolMap,
|
||||||
self,
|
self,
|
||||||
|
|
@ -101,6 +123,12 @@ index!(
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct CharMap<T>(HashMap<char, T>);
|
pub struct CharMap<T>(HashMap<char, T>);
|
||||||
|
|
||||||
|
impl<T> CharMap<T>{
|
||||||
|
pub fn entries(&self) -> impl Iterator<Item = (char, &T)>{
|
||||||
|
self.0.iter().map(|(k, v)|(*k, v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
index!(
|
index!(
|
||||||
CharMap,
|
CharMap,
|
||||||
self,
|
self,
|
||||||
|
|
@ -113,6 +141,12 @@ index!(
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct CharEpsilonMap<T>(HashMap<Option<char>, T>);
|
pub struct CharEpsilonMap<T>(HashMap<Option<char>, T>);
|
||||||
|
|
||||||
|
impl<T> CharEpsilonMap<T>{
|
||||||
|
pub fn entries(&self) -> impl Iterator<Item = (Option<char>, &T)>{
|
||||||
|
self.0.iter().map(|(k, v)|(*k, v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
index!(
|
index!(
|
||||||
CharEpsilonMap,
|
CharEpsilonMap,
|
||||||
self,
|
self,
|
||||||
|
|
@ -122,3 +156,4 @@ index!(
|
||||||
self.0.entry(Some(char)).or_default()
|
self.0.entry(Some(char)).or_default()
|
||||||
);
|
);
|
||||||
index!(CharEpsilonMap, self, self.0, &char, char = Option<char>, self.0.entry(char).or_default());
|
index!(CharEpsilonMap, self, self.0, &char, char = Option<char>, self.0.entry(char).or_default());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,17 @@ use std::collections::HashSet;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
struct To(State, Vec<Symbol>);
|
pub struct To(State, Vec<Symbol>);
|
||||||
|
|
||||||
|
impl To{
|
||||||
|
pub fn state(&self) -> State{
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stack(&self) -> &[Symbol]{
|
||||||
|
&self.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
|
|
@ -18,6 +28,46 @@ pub struct Npda {
|
||||||
transitions: StateSymbolMap<CharEpsilonMap<Vec<To>>>,
|
transitions: StateSymbolMap<CharEpsilonMap<Vec<To>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct StateTransition<T> {
|
||||||
|
pub from: T,
|
||||||
|
pub to: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Npda {
|
||||||
|
pub fn get_state_name(&self, state: State) -> Option<&str>{
|
||||||
|
self.state_names.get(state).map(String::as_str)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_symbol_name(&self, symbol: Symbol) -> Option<&str>{
|
||||||
|
self.symbol_names.get(symbol).map(String::as_str)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initial_state(&self) -> State{
|
||||||
|
self.initial_state
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initial_stack(&self) -> Symbol{
|
||||||
|
self.initial_stack
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn final_states(&self) -> Option<impl Iterator<Item = State>>{
|
||||||
|
Some(self.final_states.as_ref()?.entries().filter(|&(_, f)| *f).map(|(s, _)| s))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn states(&self) -> impl Iterator<Item = (State, &str)>{
|
||||||
|
self.state_names.entries().map(|s|(s.0, s.1.as_str()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn symbols(&self) -> impl Iterator<Item = (Symbol, &str)>{
|
||||||
|
self.symbol_names.entries().map(|s|(s.0, s.1.as_str()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn transitions(&self) -> &StateSymbolMap<CharEpsilonMap<Vec<To>>>{
|
||||||
|
&self.transitions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
pub struct NpdaState {
|
pub struct NpdaState {
|
||||||
pub state: State,
|
pub state: State,
|
||||||
|
|
@ -120,13 +170,14 @@ impl Simulator {
|
||||||
// ------ parser/semantics
|
// ------ parser/semantics
|
||||||
|
|
||||||
use crate::loader::{
|
use crate::loader::{
|
||||||
Context, DELTA_LOWER, GAMMA_UPPER, SIGMA_UPPER, Spanned, ast::{self, Symbol as Sym}
|
Context, DELTA_LOWER, GAMMA_UPPER, SIGMA_UPPER, Spanned,
|
||||||
|
ast::{self, Symbol as Sym},
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Npda {
|
impl Npda {
|
||||||
pub fn load_from_ast<'a>(
|
pub fn load_from_ast<'a>(
|
||||||
items: impl Iterator<Item = Spanned<ast::TopLevel<'a>>>,
|
items: impl Iterator<Item = Spanned<ast::TopLevel<'a>>>,
|
||||||
ctx: &mut Context<'a>
|
ctx: &mut Context<'a>,
|
||||||
) -> Option<Npda> {
|
) -> Option<Npda> {
|
||||||
let mut initial_state = None;
|
let mut initial_state = None;
|
||||||
let mut initial_stack = None;
|
let mut initial_stack = None;
|
||||||
|
|
@ -275,10 +326,7 @@ impl Npda {
|
||||||
ctx.emit_error(format!("unknown item {name:?}, expected 'Q' | 'E' | '{SIGMA_UPPER}' | 'sigma' | 'F' | 'T' | '{GAMMA_UPPER}' | 'gamma' | 'I' | 'q0' | 'S' | 'z0'"), dest_s);
|
ctx.emit_error(format!("unknown item {name:?}, expected 'Q' | 'E' | '{SIGMA_UPPER}' | 'sigma' | 'F' | 'T' | '{GAMMA_UPPER}' | 'gamma' | 'I' | 'q0' | 'S' | 'z0'"), dest_s);
|
||||||
}
|
}
|
||||||
|
|
||||||
TL::TransitionFunc(
|
TL::TransitionFunc(S((S("d" | DELTA_LOWER | "delta", _), tuple), _), list) => {
|
||||||
S((S("d" | DELTA_LOWER | "delta", _), tuple), _),
|
|
||||||
list,
|
|
||||||
) => {
|
|
||||||
let list = list.set_weak();
|
let list = list.set_weak();
|
||||||
let Some((state, letter, stack_symbol)) =
|
let Some((state, letter, stack_symbol)) =
|
||||||
tuple.as_ref().expect_npda_transition_function(ctx)
|
tuple.as_ref().expect_npda_transition_function(ctx)
|
||||||
|
|
@ -343,10 +391,7 @@ impl Npda {
|
||||||
let ident = symbol.expect_ident(ctx)?;
|
let ident = symbol.expect_ident(ctx)?;
|
||||||
|
|
||||||
let Some(symbol) = stack_symbols.get(ident).copied() else {
|
let Some(symbol) = stack_symbols.get(ident).copied() else {
|
||||||
ctx.emit_error(
|
ctx.emit_error("transition stack symbol not defined", symbol.1);
|
||||||
"transition stack symbol not defined",
|
|
||||||
symbol.1,
|
|
||||||
);
|
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
Some(symbol)
|
Some(symbol)
|
||||||
|
|
@ -364,7 +409,9 @@ impl Npda {
|
||||||
}
|
}
|
||||||
TL::TransitionFunc(S((S(name, _), _), dest_s), _) => {
|
TL::TransitionFunc(S((S(name, _), _), dest_s), _) => {
|
||||||
ctx.emit_error(
|
ctx.emit_error(
|
||||||
format!("unknown function {name:?}, expected 'd' | 'delta' | '{DELTA_LOWER}'"),
|
format!(
|
||||||
|
"unknown function {name:?}, expected 'd' | 'delta' | '{DELTA_LOWER}'"
|
||||||
|
),
|
||||||
dest_s,
|
dest_s,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -449,7 +496,7 @@ impl Npda {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.contains_errors(){
|
if ctx.contains_errors() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ edition = "2024"
|
||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
serde_json = "1.0"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
|
||||||
automata = {path=".."}
|
automata = {path=".."}
|
||||||
console_error_panic_hook = "0.1.7"
|
console_error_panic_hook = "0.1.7"
|
||||||
wasm-bindgen = "*"
|
wasm-bindgen = "*"
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,9 @@ import { closeBrackets } from "npm:@codemirror/autocomplete";
|
||||||
|
|
||||||
|
|
||||||
import wasm from "./wasm.ts"
|
import wasm from "./wasm.ts"
|
||||||
|
import { terminalPlugin } from "./terminal.ts";
|
||||||
|
|
||||||
|
import { setAutomaton } from "./visualizer.ts";
|
||||||
|
|
||||||
|
|
||||||
function tokenize(text: string) {
|
function tokenize(text: string) {
|
||||||
|
|
@ -35,7 +38,7 @@ function compile(text: string): wasm.CompileResult {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
// @ts-expect-error wasm defines extra cleanup
|
// @ts-expect-error wasm defines extra cleanup
|
||||||
return {log: [], log_formatted: ""};
|
return {log: [], log_formatted: "", graph: ""};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,7 +75,11 @@ function sevRank(sev: string) {
|
||||||
|
|
||||||
function buildAnalysis(text: string, doc: Text) {
|
function buildAnalysis(text: string, doc: Text) {
|
||||||
const tokens = tokenize(text);
|
const tokens = tokenize(text);
|
||||||
const { log, log_formatted } = compile(text);
|
const { log, log_formatted, graph } = compile(text);
|
||||||
|
|
||||||
|
if (graph){
|
||||||
|
setAutomaton(JSON.parse(graph))
|
||||||
|
}
|
||||||
|
|
||||||
// Build ONE Decoration set: syntax + diagnostics
|
// Build ONE Decoration set: syntax + diagnostics
|
||||||
const marks = [];
|
const marks = [];
|
||||||
|
|
@ -108,7 +115,7 @@ function buildAnalysis(text: string, doc: Text) {
|
||||||
return { tokens, log, log_formatted, deco };
|
return { tokens, log, log_formatted, deco };
|
||||||
}
|
}
|
||||||
|
|
||||||
const analysisField = StateField.define({
|
export const analysisField = StateField.define({
|
||||||
create(state) {
|
create(state) {
|
||||||
const text = state.doc.toString();
|
const text = state.doc.toString();
|
||||||
return buildAnalysis(text, state.doc);
|
return buildAnalysis(text, state.doc);
|
||||||
|
|
@ -158,109 +165,6 @@ const diagHover = hoverTooltip((view, pos) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
function escapeHtml(s: string) {
|
|
||||||
return s
|
|
||||||
.replace(/&/g, "&")
|
|
||||||
.replace(/</g, "<")
|
|
||||||
.replace(/>/g, ">");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function ansiToHtml(input: string) {
|
|
||||||
// deno-lint-ignore no-control-regex
|
|
||||||
const ESC_RE = /\x1b\[([0-9;]*)m/g;
|
|
||||||
|
|
||||||
let out = "";
|
|
||||||
let lastIndex = 0;
|
|
||||||
|
|
||||||
// current style state
|
|
||||||
let fg: number|null = null; // e.g. 31, 92
|
|
||||||
let bg: number|null = null; // e.g. 41
|
|
||||||
let bold = false;
|
|
||||||
let dim = false;
|
|
||||||
|
|
||||||
function openSpanIfNeeded(text: string) {
|
|
||||||
if (text.length === 0) return "";
|
|
||||||
const classes = [];
|
|
||||||
if (bold) classes.push("ansi-bold");
|
|
||||||
if (dim) classes.push("ansi-dim");
|
|
||||||
if (fg != null) classes.push(`ansi-fg-${fg}`);
|
|
||||||
if (bg != null) classes.push(`ansi-bg-${bg}`);
|
|
||||||
if (classes.length === 0) return escapeHtml(text);
|
|
||||||
return `<span class="${classes.join(" ")}">${escapeHtml(text)}</span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyCodes(codes: string[]) {
|
|
||||||
if (codes.length === 0) codes = ["0"];
|
|
||||||
for (const c of codes) {
|
|
||||||
const code = Number(c);
|
|
||||||
if (Number.isNaN(code)) continue;
|
|
||||||
|
|
||||||
if (code === 0) {
|
|
||||||
fg = null; bg = null; bold = false; dim = false;
|
|
||||||
} else if (code === 1) {
|
|
||||||
bold = true;
|
|
||||||
} else if (code === 2) {
|
|
||||||
dim = true;
|
|
||||||
} else if (code === 22) {
|
|
||||||
bold = false; dim = false;
|
|
||||||
} else if (code === 39) {
|
|
||||||
fg = null;
|
|
||||||
} else if (code === 49) {
|
|
||||||
bg = null;
|
|
||||||
} else if ((code >= 30 && code <= 37) || (code >= 90 && code <= 97)) {
|
|
||||||
fg = code;
|
|
||||||
} else if ((code >= 40 && code <= 47) || (code >= 100 && code <= 107)) {
|
|
||||||
bg = code;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let m;
|
|
||||||
while ((m = ESC_RE.exec(input)) !== null) {
|
|
||||||
const chunk = input.slice(lastIndex, m.index);
|
|
||||||
out += openSpanIfNeeded(chunk);
|
|
||||||
|
|
||||||
const codes = m[1] ? m[1].split(";") : [];
|
|
||||||
applyCodes(codes);
|
|
||||||
|
|
||||||
lastIndex = ESC_RE.lastIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
out += openSpanIfNeeded(input.slice(lastIndex));
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
const initialText = `type=NPDA
|
const initialText = `type=NPDA
|
||||||
Q = {q0, q1} // states
|
Q = {q0, q1} // states
|
||||||
E = {a, b} // alphabet
|
E = {a, b} // alphabet
|
||||||
|
|
|
||||||
109
web/root/src/terminal.ts
Normal file
109
web/root/src/terminal.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
// deno-lint-ignore-file
|
||||||
|
|
||||||
|
import {
|
||||||
|
ViewPlugin,
|
||||||
|
} from "npm:@codemirror/view";
|
||||||
|
|
||||||
|
import { analysisField } from "./editor.ts";
|
||||||
|
|
||||||
|
function escapeHtml(s: string) {
|
||||||
|
return s
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function ansiToHtml(input: string) {
|
||||||
|
// deno-lint-ignore no-control-regex
|
||||||
|
const ESC_RE = /\x1b\[([0-9;]*)m/g;
|
||||||
|
|
||||||
|
let out = "";
|
||||||
|
let lastIndex = 0;
|
||||||
|
|
||||||
|
// current style state
|
||||||
|
let fg: number|null = null; // e.g. 31, 92
|
||||||
|
let bg: number|null = null; // e.g. 41
|
||||||
|
let bold = false;
|
||||||
|
let dim = false;
|
||||||
|
|
||||||
|
function openSpanIfNeeded(text: string) {
|
||||||
|
if (text.length === 0) return "";
|
||||||
|
const classes = [];
|
||||||
|
if (bold) classes.push("ansi-bold");
|
||||||
|
if (dim) classes.push("ansi-dim");
|
||||||
|
if (fg != null) classes.push(`ansi-fg-${fg}`);
|
||||||
|
if (bg != null) classes.push(`ansi-bg-${bg}`);
|
||||||
|
if (classes.length === 0) return escapeHtml(text);
|
||||||
|
return `<span class="${classes.join(" ")}">${escapeHtml(text)}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCodes(codes: string[]) {
|
||||||
|
if (codes.length === 0) codes = ["0"];
|
||||||
|
for (const c of codes) {
|
||||||
|
const code = Number(c);
|
||||||
|
if (Number.isNaN(code)) continue;
|
||||||
|
|
||||||
|
if (code === 0) {
|
||||||
|
fg = null; bg = null; bold = false; dim = false;
|
||||||
|
} else if (code === 1) {
|
||||||
|
bold = true;
|
||||||
|
} else if (code === 2) {
|
||||||
|
dim = true;
|
||||||
|
} else if (code === 22) {
|
||||||
|
bold = false; dim = false;
|
||||||
|
} else if (code === 39) {
|
||||||
|
fg = null;
|
||||||
|
} else if (code === 49) {
|
||||||
|
bg = null;
|
||||||
|
} else if ((code >= 30 && code <= 37) || (code >= 90 && code <= 97)) {
|
||||||
|
fg = code;
|
||||||
|
} else if ((code >= 40 && code <= 47) || (code >= 100 && code <= 107)) {
|
||||||
|
bg = code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let m;
|
||||||
|
while ((m = ESC_RE.exec(input)) !== null) {
|
||||||
|
const chunk = input.slice(lastIndex, m.index);
|
||||||
|
out += openSpanIfNeeded(chunk);
|
||||||
|
|
||||||
|
const codes = m[1] ? m[1].split(";") : [];
|
||||||
|
applyCodes(codes);
|
||||||
|
|
||||||
|
lastIndex = ESC_RE.lastIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
out += openSpanIfNeeded(input.slice(lastIndex));
|
||||||
|
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 { network } from "./visualizer.ts";
|
import { invalidateGraphThemeCache, network } from "./visualizer.ts";
|
||||||
|
|
||||||
function cssVar(name: string, fallback = ""): string {
|
function cssVar(name: string, fallback = ""): string {
|
||||||
return getComputedStyle(document.documentElement)
|
return getComputedStyle(document.documentElement)
|
||||||
|
|
@ -49,27 +49,39 @@ globalThis.window.matchMedia?.("(prefers-color-scheme: light)")
|
||||||
setTheme(getPreferredTheme());
|
setTheme(getPreferredTheme());
|
||||||
});
|
});
|
||||||
|
|
||||||
export function applyGraphTheme() {
|
function applyGraphTheme() {
|
||||||
|
invalidateGraphThemeCache();
|
||||||
|
|
||||||
network.setOptions({
|
network.setOptions({
|
||||||
nodes: {
|
nodes: {
|
||||||
color: {
|
|
||||||
background: cssVar("--graph-node-bg"),
|
|
||||||
border: cssVar("--graph-node-border"),
|
|
||||||
highlight: {
|
|
||||||
background: cssVar("--graph-node-active-bg"),
|
|
||||||
border: cssVar("--graph-node-active-border"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
font: {
|
font: {
|
||||||
color: cssVar("--graph-node-text"),
|
color: cssVar("--graph-node-text"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
edges: {
|
edges: {
|
||||||
|
labelHighlightBold: true,
|
||||||
|
font: {
|
||||||
|
align: "middle",
|
||||||
|
color: cssVar("--fg-0"),
|
||||||
|
strokeColor: cssVar("--bg-0"),
|
||||||
|
bold: {
|
||||||
|
color: cssVar("--fg-1"),
|
||||||
|
mod: ''
|
||||||
|
},
|
||||||
|
},
|
||||||
color: {
|
color: {
|
||||||
color: cssVar("--graph-edge"),
|
color: cssVar("--graph-edge"),
|
||||||
highlight: cssVar("--graph-edge-active"),
|
highlight: cssVar("--graph-edge-active"),
|
||||||
hover: cssVar("--graph-edge-hover"),
|
hover: cssVar("--graph-edge-hover"),
|
||||||
},
|
},
|
||||||
|
shadow: {
|
||||||
|
enabled: true,
|
||||||
|
color: cssVar("--bg-2")
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,101 +6,79 @@ import * as vis from "npm:vis-network/standalone";
|
||||||
export const nodes = new vis.DataSet<vis.Node>();
|
export const nodes = new vis.DataSet<vis.Node>();
|
||||||
export const edges = new vis.DataSet<vis.Edge>();
|
export const edges = new vis.DataSet<vis.Edge>();
|
||||||
|
|
||||||
const automaton = {
|
type StateId = string;
|
||||||
states: ["q0", "q1"],
|
type GraphDef = {
|
||||||
initialState: "q0",
|
initial: StateId;
|
||||||
acceptStates: ["q1"],
|
final: StateId[];
|
||||||
|
states: StateId[];
|
||||||
transitions: [
|
transitions: Record<string, string>;
|
||||||
{
|
|
||||||
from: "q0",
|
|
||||||
to: "q0",
|
|
||||||
label: "ε, z0 → A z0\n",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
from: "q0",
|
|
||||||
to: "q0",
|
|
||||||
label: "ε, z0 → B z0",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
from: "q0",
|
|
||||||
to: "q1",
|
|
||||||
label: "ε, z0 → z0",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
from: "q1",
|
|
||||||
to: "q1",
|
|
||||||
label: "a, A → ε",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
from: "q1",
|
|
||||||
to: "q1",
|
|
||||||
label: "b, B → ε",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function renderNode({
|
let automaton: GraphDef = {
|
||||||
ctx,
|
initial: "",
|
||||||
id,
|
final: [],
|
||||||
x,
|
states: [],
|
||||||
y,
|
transitions: {},
|
||||||
state: { selected, hover },
|
};
|
||||||
style,
|
|
||||||
label,
|
|
||||||
}: any) {
|
|
||||||
return {
|
|
||||||
drawNode() {
|
|
||||||
ctx.save();
|
|
||||||
const r = style.size;
|
|
||||||
|
|
||||||
ctx.beginPath();
|
export function clearAutomaton() {
|
||||||
ctx.arc(x, y, r, 0, 2 * Math.PI);
|
setAutomaton({
|
||||||
ctx.fillStyle = "red";
|
initial: "",
|
||||||
ctx.fill();
|
final: [],
|
||||||
ctx.lineWidth = 4;
|
states: [],
|
||||||
ctx.strokeStyle = "blue";
|
transitions: {},
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
ctx.fillStyle = "black";
|
|
||||||
ctx.textAlign = "center";
|
|
||||||
ctx.fillText(label, x, y, r);
|
|
||||||
|
|
||||||
ctx.textAlign = "center";
|
|
||||||
ctx.strokeStyle = "white";
|
|
||||||
ctx.fillStyle = "black";
|
|
||||||
let cy = y - (r + 10);
|
|
||||||
for (const part of "meow[]\nbeeep".split("\n").reverse()) {
|
|
||||||
const metrics = ctx.measureText(part);
|
|
||||||
cy -= metrics.actualBoundingBoxAscent +
|
|
||||||
metrics.actualBoundingBoxDescent;
|
|
||||||
ctx.strokeText(part, x, cy);
|
|
||||||
ctx.fillText(part, x, cy);
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.restore();
|
|
||||||
},
|
|
||||||
nodeDimensions: { width: 20, height: 20 },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate nodes
|
|
||||||
for (const state of automaton.states) {
|
|
||||||
nodes.add({
|
|
||||||
id: state,
|
|
||||||
label: state,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate edges
|
export function setAutomaton(auto: GraphDef) {
|
||||||
automaton.transitions.forEach((t, i) => {
|
automaton = auto;
|
||||||
edges.add({
|
// Populate nodes
|
||||||
id: `e${i}`,
|
for (const state of automaton.states) {
|
||||||
from: t.from,
|
if (nodes.get(state)) {
|
||||||
to: t.to,
|
nodes.update({
|
||||||
label: t.label,
|
id: state,
|
||||||
});
|
label: state,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
nodes.add({
|
||||||
|
id: state,
|
||||||
|
label: state,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate edges
|
||||||
|
for (const [k, v] of Object.entries(automaton.transitions)) {
|
||||||
|
const to_from = k.split("#");
|
||||||
|
if (edges.get(k)) {
|
||||||
|
edges.update({
|
||||||
|
id: k,
|
||||||
|
from: to_from[0],
|
||||||
|
to: to_from[1],
|
||||||
|
label: v,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
edges.add({
|
||||||
|
id: k,
|
||||||
|
from: to_from[0],
|
||||||
|
to: to_from[1],
|
||||||
|
label: v,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const edge_id of edges.getIds()){
|
||||||
|
if (auto.transitions[edge_id as string] === undefined){
|
||||||
|
edges.remove(edge_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const node_id of nodes.getIds()){
|
||||||
|
if (!auto.states.includes(node_id as string)){
|
||||||
|
nodes.remove(node_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function chosen_edge(
|
function chosen_edge(
|
||||||
_: vis.ChosenNodeValues,
|
_: vis.ChosenNodeValues,
|
||||||
|
|
@ -108,7 +86,6 @@ function chosen_edge(
|
||||||
selected: boolean,
|
selected: boolean,
|
||||||
hovered: boolean,
|
hovered: boolean,
|
||||||
) {
|
) {
|
||||||
console.log("edge", id, selected, hovered);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function chosen_node(
|
function chosen_node(
|
||||||
|
|
@ -117,7 +94,6 @@ function chosen_node(
|
||||||
selected: boolean,
|
selected: boolean,
|
||||||
hovered: boolean,
|
hovered: boolean,
|
||||||
) {
|
) {
|
||||||
console.log("node", id, selected, hovered);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const network: vis.Network = createGraph();
|
export const network: vis.Network = createGraph();
|
||||||
|
|
@ -154,18 +130,19 @@ function createGraph(): vis.Network {
|
||||||
border: "#79c0ff",
|
border: "#79c0ff",
|
||||||
highlight: { background: "#388bfd", border: "#a5d6ff" },
|
highlight: { background: "#388bfd", border: "#a5d6ff" },
|
||||||
},
|
},
|
||||||
// @ts-expect-error bad library
|
// // @ts-expect-error bad library
|
||||||
chosen: {
|
// chosen: {
|
||||||
node: chosen_node,
|
// node: chosen_node,
|
||||||
},
|
// },
|
||||||
shape: "custom",
|
shape: "custom",
|
||||||
|
// @ts-expect-error bad library
|
||||||
ctxRenderer: renderNode,
|
ctxRenderer: renderNode,
|
||||||
size: 18,
|
size: 18,
|
||||||
},
|
},
|
||||||
edges: {
|
edges: {
|
||||||
chosen: {
|
chosen: {
|
||||||
// @ts-expect-error bad library
|
// // @ts-expect-error bad library
|
||||||
edge: chosen_edge,
|
// edge: chosen_edge,
|
||||||
},
|
},
|
||||||
arrowStrikethrough: false,
|
arrowStrikethrough: false,
|
||||||
font: { align: "middle", color: "#000000ff" },
|
font: { align: "middle", color: "#000000ff" },
|
||||||
|
|
@ -189,3 +166,294 @@ function createGraph(): vis.Network {
|
||||||
|
|
||||||
return network;
|
return network;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type GraphTheme = {
|
||||||
|
bg_0: string;
|
||||||
|
bg_1: string;
|
||||||
|
bg_2: string;
|
||||||
|
fg_0: string;
|
||||||
|
|
||||||
|
anchor: string;
|
||||||
|
selected: string;
|
||||||
|
node: string;
|
||||||
|
current: string;
|
||||||
|
edge: string;
|
||||||
|
glow: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let _graphTheme: GraphTheme | null = null;
|
||||||
|
|
||||||
|
export 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("--bg-0"),
|
||||||
|
bg_1: cssVar("--bg-1"),
|
||||||
|
bg_2: cssVar("--bg-2"),
|
||||||
|
fg_0: cssVar("--fg-0"),
|
||||||
|
|
||||||
|
selected: cssVar("--bg-2"),
|
||||||
|
|
||||||
|
node: cssVar("--focus"),
|
||||||
|
current: cssVar("--success"),
|
||||||
|
|
||||||
|
anchor: cssVar("--warning"),
|
||||||
|
|
||||||
|
edge: cssVar("--graph-edge", "rgba(201,209,217,0.55)"),
|
||||||
|
|
||||||
|
glow: cssVar("--accent", "#79c0ff"),
|
||||||
|
};
|
||||||
|
|
||||||
|
return _graphTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNode({
|
||||||
|
ctx,
|
||||||
|
id,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
state: { selected, hover },
|
||||||
|
style,
|
||||||
|
label,
|
||||||
|
}: any) {
|
||||||
|
return {
|
||||||
|
drawNode() {
|
||||||
|
// @ts-expect-error bad library
|
||||||
|
const node: vis.Node = nodes.get(id)!;
|
||||||
|
|
||||||
|
const t = getGraphTheme();
|
||||||
|
const r = Math.max(14, style?.size ?? 18);
|
||||||
|
|
||||||
|
const isInitial = id === "q0";
|
||||||
|
const isFinal = id === "q1"; // <-- change if your schema differs
|
||||||
|
const isActive = id === "q0"; // <-- change if your schema differs
|
||||||
|
|
||||||
|
const fill = selected ? t.glow : hover ? t.bg_1 : t.bg_0;
|
||||||
|
const stroke = isActive ? t.current : t.node;
|
||||||
|
|
||||||
|
const emphasis = (selected ? 1 : 0) + (hover ? 0.6 : 0);
|
||||||
|
|
||||||
|
const outerW = isFinal ? 3.5 : 3;
|
||||||
|
const innerW = 2;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.textBaseline = "middle";
|
||||||
|
|
||||||
|
ctx.lineWidth = outerW + emphasis;
|
||||||
|
ctx.strokeStyle = stroke;
|
||||||
|
ctx.fillStyle = fill;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, y, r - ctx.lineWidth * 0.5, 0, Math.PI * 2);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
if (isFinal) {
|
||||||
|
ctx.lineWidth = innerW;
|
||||||
|
ctx.strokeStyle = stroke;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, y, r - 7, 0, Math.PI * 2);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.fillStyle = t.fg_0;
|
||||||
|
ctx.strokeStyle = t.bg_0;
|
||||||
|
ctx.strokeText(label, x, y);
|
||||||
|
ctx.fillText(label, x, y);
|
||||||
|
|
||||||
|
if (isInitial) {
|
||||||
|
drawInitialArrow(ctx, x, y, r, t.edge);
|
||||||
|
}
|
||||||
|
|
||||||
|
// const badgeText = "bleh\npee";
|
||||||
|
// if (badgeText) {
|
||||||
|
// const lines = badgeText.split("\n").slice(0, 3);
|
||||||
|
// const padX = 8;
|
||||||
|
// const padY = 6;
|
||||||
|
// const lineH = 14;
|
||||||
|
|
||||||
|
// let w = 0;
|
||||||
|
// for (const ln of lines) w = Math.max(w, ctx.measureText(ln).width);
|
||||||
|
// const boxW = w + padX * 2;
|
||||||
|
// const boxH = lines.length * lineH + padY * 2;
|
||||||
|
|
||||||
|
// const bx = x - boxW / 2;
|
||||||
|
// const by = y - r - 12 - boxH;
|
||||||
|
|
||||||
|
// ctx.fillStyle = t.bg_1;
|
||||||
|
// ctx.strokeStyle = t.bg_2;
|
||||||
|
// ctx.lineWidth = 1;
|
||||||
|
// roundRect(ctx, bx, by, boxW, boxH, 8);
|
||||||
|
// ctx.fill();
|
||||||
|
// ctx.stroke();
|
||||||
|
|
||||||
|
// ctx.fillStyle = t.fg_0;
|
||||||
|
// ctx.textBaseline = "top";
|
||||||
|
// for (let i = 0; i < lines.length; i++) {
|
||||||
|
// ctx.fillText(lines[i], x, by + padY + i * lineH);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
const physicsOff = node.physics === false;
|
||||||
|
if (physicsOff) {
|
||||||
|
drawPinIndicator(ctx, x, y, r, t.anchor);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawInitialArrow(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
r: number,
|
||||||
|
color: string,
|
||||||
|
) {
|
||||||
|
const len = Math.max(14, r * 0.95); // arrow length
|
||||||
|
const head = Math.max(7, r * 0.32); // arrow head size
|
||||||
|
const lineW = Math.max(2, r * 0.12); // stroke width
|
||||||
|
const gap = 4; // distance from node edge
|
||||||
|
|
||||||
|
// Direction: from top-left → center (45° down-right)
|
||||||
|
const dx = Math.SQRT1_2;
|
||||||
|
const dy = Math.SQRT1_2;
|
||||||
|
|
||||||
|
// Tip position (just outside node)
|
||||||
|
const tipX = x - dx * (r + gap);
|
||||||
|
const tipY = y - dy * (r + gap);
|
||||||
|
|
||||||
|
// Tail start
|
||||||
|
const tailX = tipX - dx * len;
|
||||||
|
const tailY = tipY - dy * len;
|
||||||
|
|
||||||
|
// Perpendicular for arrow head
|
||||||
|
const px = -dy;
|
||||||
|
const py = dx;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
|
||||||
|
ctx.lineCap = "round";
|
||||||
|
ctx.lineJoin = "round";
|
||||||
|
ctx.lineWidth = lineW;
|
||||||
|
ctx.strokeStyle = color;
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
|
||||||
|
// Shaft
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(tailX, tailY);
|
||||||
|
ctx.lineTo(
|
||||||
|
tipX - dx * head * 0.6,
|
||||||
|
tipY - dy * head * 0.6,
|
||||||
|
);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Head
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(tipX, tipY);
|
||||||
|
ctx.lineTo(
|
||||||
|
tipX - dx * head + px * head * 0.7,
|
||||||
|
tipY - dy * head + py * head * 0.7,
|
||||||
|
);
|
||||||
|
ctx.lineTo(
|
||||||
|
tipX - dx * head - px * head * 0.7,
|
||||||
|
tipY - dy * head - py * head * 0.7,
|
||||||
|
);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawPinIndicator(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
r: number,
|
||||||
|
color: string,
|
||||||
|
) {
|
||||||
|
const size = Math.max(7, Math.round(r * 0.28));
|
||||||
|
const ox = x + r - size * 0.55;
|
||||||
|
const oy = y + r - size * 0.55;
|
||||||
|
|
||||||
|
const stroke = color;
|
||||||
|
const fill = "rgba(0,0,0,0)";
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
|
||||||
|
ctx.shadowColor = "rgba(0,0,0,0)";
|
||||||
|
ctx.shadowBlur = 6;
|
||||||
|
ctx.shadowOffsetX = 0;
|
||||||
|
ctx.shadowOffsetY = 2;
|
||||||
|
|
||||||
|
// Pin head (circle)
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(ox, oy, size * 0.55, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = fill;
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Pin stem (triangle-ish)
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(ox, oy + size * 0.25);
|
||||||
|
ctx.lineTo(ox - size * 0.35, oy + size * 0.95);
|
||||||
|
ctx.lineTo(ox + size * 0.35, oy + size * 0.95);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fillStyle = fill;
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Outline
|
||||||
|
ctx.shadowBlur = 0;
|
||||||
|
ctx.lineWidth = Math.max(1.25, Math.round(r * 0.06));
|
||||||
|
ctx.strokeStyle = stroke;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(ox, oy, size * 0.55, 0, Math.PI * 2);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(ox, oy + size * 0.25);
|
||||||
|
ctx.lineTo(ox - size * 0.35, oy + size * 0.95);
|
||||||
|
ctx.lineTo(ox + size * 0.35, oy + size * 0.95);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Inner dot
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(ox, oy, size * 0.18, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = stroke;
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small helper for rounded rectangles
|
||||||
|
function roundRect(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
w: number,
|
||||||
|
h: number,
|
||||||
|
r: number,
|
||||||
|
) {
|
||||||
|
const rr = Math.min(r, w / 2, h / 2);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x + rr, y);
|
||||||
|
ctx.arcTo(x + w, y, x + w, y + h, rr);
|
||||||
|
ctx.arcTo(x + w, y + h, x, y + h, rr);
|
||||||
|
ctx.arcTo(x, y + h, x, y, rr);
|
||||||
|
ctx.arcTo(x, y, x + w, y, rr);
|
||||||
|
ctx.closePath();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,14 +56,14 @@
|
||||||
|
|
||||||
--graph-node-bg: #1f6feb;
|
--graph-node-bg: #1f6feb;
|
||||||
--graph-node-border: #388bfd;
|
--graph-node-border: #388bfd;
|
||||||
--graph-node-text: #e6edf3;
|
--graph-node-text: var(--fg-0);
|
||||||
|
|
||||||
--graph-node-active-bg: #79c0ff;
|
--graph-node-active-bg: #79c0ff;
|
||||||
--graph-node-active-border: #a5d6ff;
|
--graph-node-active-border: #ff0000;
|
||||||
|
|
||||||
--graph-edge: rgba(201, 209, 217, 0.55);
|
--graph-edge: rgba(201, 209, 217, 0.55);
|
||||||
--graph-edge-hover: #79c0ff;
|
--graph-edge-hover: rgba(201, 209, 217, 0.864);
|
||||||
--graph-edge-active: #a5d6ff;
|
--graph-edge-active: var(--accent);
|
||||||
|
|
||||||
|
|
||||||
--ansi-fg-30: #0b0f14; /* black */
|
--ansi-fg-30: #0b0f14; /* black */
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
use automata::{
|
use std::collections::HashMap;
|
||||||
loader::{self, Context, Span, Spanned, lexer::Lexer},
|
|
||||||
};
|
|
||||||
|
|
||||||
|
use automata::loader::{self, Context, Span, Spanned, lexer::Lexer};
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
use wasm_bindgen::prelude::wasm_bindgen;
|
use wasm_bindgen::prelude::wasm_bindgen;
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
|
|
@ -148,16 +149,75 @@ pub struct CompileLog {
|
||||||
pub end: Option<usize>,
|
pub end: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
pub struct Graph<'a> {
|
||||||
|
initial: &'a str,
|
||||||
|
final_states: Vec<&'a str>,
|
||||||
|
states: Vec<&'a str>,
|
||||||
|
transitions: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[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 log_formatted: String,
|
||||||
|
pub graph: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn compile(input: &str) -> CompileResult {
|
pub fn compile(input: &str) -> CompileResult {
|
||||||
let mut ctx = Context::new(input);
|
let mut ctx = Context::new(input);
|
||||||
_ = automata::loader::parse_universal(&mut ctx);
|
let result = automata::loader::parse_universal(&mut ctx);
|
||||||
|
|
||||||
|
let graph = if let Some(result) = result {
|
||||||
|
match result {
|
||||||
|
loader::Machine::Npda(npda) => {
|
||||||
|
let mut transitions = HashMap::new();
|
||||||
|
for ((from, symbol), to_transitions) in npda.transitions().entries(){
|
||||||
|
let from = npda.get_state_name(from).unwrap_or("<INVALID>");
|
||||||
|
let symbol = npda.get_symbol_name(symbol).unwrap_or("<INVALID>");
|
||||||
|
for (char, to) in to_transitions.entries(){
|
||||||
|
for to in to{
|
||||||
|
let to_state = npda.get_state_name(to.state()).unwrap_or("<INVALID>");
|
||||||
|
let string: &mut String = transitions.entry(format!("{from}#{to_state}")).or_default();
|
||||||
|
if !string.is_empty(){
|
||||||
|
string.push('\n');
|
||||||
|
}
|
||||||
|
let char = char.unwrap_or('ε');
|
||||||
|
let stack = to.stack().iter().map(|s|npda.get_symbol_name(*s).unwrap_or("<INVALID>")).fold(String::new(), |mut s, b|{
|
||||||
|
if !s.is_empty(){
|
||||||
|
s.push_str(", ");
|
||||||
|
}
|
||||||
|
s.push_str(b);
|
||||||
|
s
|
||||||
|
});
|
||||||
|
write!(string, "{char}, {symbol} -> [{stack}]").unwrap();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let graph = Graph {
|
||||||
|
states: npda.states().map(|(_, n)| n).collect(),
|
||||||
|
initial: npda
|
||||||
|
.get_state_name(npda.initial_state())
|
||||||
|
.unwrap_or("<INVALID>"),
|
||||||
|
final_states: npda
|
||||||
|
.final_states()
|
||||||
|
.map(|i| {
|
||||||
|
i.map(|s| npda.get_state_name(s).unwrap_or("<INVALID>"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.unwrap_or_default(),
|
||||||
|
transitions
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(serde_json::to_string(&graph).unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
let log_formatted = ctx.logs_display().fold(String::new(), |mut s, e| {
|
let log_formatted = ctx.logs_display().fold(String::new(), |mut s, e| {
|
||||||
|
|
@ -165,7 +225,8 @@ pub fn compile(input: &str) -> CompileResult {
|
||||||
s
|
s
|
||||||
});
|
});
|
||||||
|
|
||||||
let log = ctx.into_logs()
|
let log = ctx
|
||||||
|
.into_logs()
|
||||||
.into_entries()
|
.into_entries()
|
||||||
.map(|e| CompileLog {
|
.map(|e| CompileLog {
|
||||||
level: match e.level {
|
level: match e.level {
|
||||||
|
|
@ -183,5 +244,9 @@ pub fn compile(input: &str) -> CompileResult {
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
CompileResult { log, log_formatted }
|
CompileResult {
|
||||||
|
log,
|
||||||
|
log_formatted,
|
||||||
|
graph,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ await startServer();
|
||||||
|
|
||||||
console.log("👀 watching for changes…");
|
console.log("👀 watching for changes…");
|
||||||
|
|
||||||
const watcher = Deno.watchFs(["root", "../src"]);
|
const watcher = Deno.watchFs(["root", "src"]);
|
||||||
for await (const event of watcher) {
|
for await (const event of watcher) {
|
||||||
if (
|
if (
|
||||||
event.kind === "modify" ||
|
event.kind === "modify" ||
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue