import { GraphEditor } from './editor/GraphEditor.ts'; import { createGraph, createStateList } from './editor/mapper.ts'; const IS_VALID = 'is-valid'; const IS_INVALID = 'is-invalid'; const currentWord = /** @type {HTMLInputElement} */ (document.getElementById('current-word')); const wordInput = /** @type {HTMLInputElement} */ (document.getElementById('word-input')); const buttons = /** @type {HTMLDivElement} */ (document.getElementById('input-buttons')); const clearButton = /** @type {HTMLButtonElement} */ (document.getElementById('clear-button')); const light = /** @type {HTMLDivElement} */ (document.getElementById('light')); const container = /** @type {HTMLDivElement} */ (document.getElementById('state-graph')); const displayGraphButton = /** @type {HTMLButtonElement} */ (document.getElementById('display-graph')); const controls = /** @type {HTMLElement} */ (document.getElementById('controls')); const sidebarToggle = /** @type {HTMLElement} */ (document.getElementById('sidebar-toggle')); /** * @param {string} name * @param {import('./examples.js').State[]} states * @param {boolean} [editable] */ export function openAutomaton(name, states, editable = false) { let state = findStart(states); let builder = ''; const viewer = new GraphEditor(); viewer.readonly = !editable; if (editable) { sidebarToggle.removeAttribute('hidden'); container.classList.remove('blurred'); displayGraphButton.setAttribute('hidden', 'hidden'); } else { sidebarToggle.setAttribute('hidden', 'hidden'); container.classList.add('blurred'); displayGraphButton.removeAttribute('hidden'); } const graph = createGraph(states); viewer.addEventListener('change', () => { try { states = createStateList(graph); type(); createLetters(); } catch (e) { console.error(e); } }); while (container.firstChild) { container.removeChild(container.firstChild); } container.appendChild(viewer); viewer.graph = graph; currentWord.innerText = name; /** * Updates the UI to reflect the current state. */ function updateUIState() { wordInput.value = builder; if (typeof Array.from(state).find((state) => states[state]?.accepting) === 'undefined') { light.classList.remove(IS_VALID); light.classList.add(IS_INVALID); } else { light.classList.remove(IS_INVALID); light.classList.add(IS_VALID); } graph.forEach((node) => node.active = false); for (const s of state) { if (s in graph.nodes) { graph.nodes[s].active = true; } } viewer.restart(); } function type() { const value = wordInput.value; builder = ''; state = findStart(states); for (const letter of value) { step(letter); } if (!value.length) { updateUIState(); } } /** * Steps the FSM with the given letter. * * @param {string} letter */ function step(letter) { /** @type {number[]} */ const newStates = []; builder += letter; for (const s of state) { if (s in states) { const transitions = states[s].transitions[letter]; if (transitions) { newStates.push(...transitions); } } } state = newStates; updateUIState(); } /** * Dynamically create buttons for each letter in the alphabet. */ function createLetters() { /** * @type {string[]} */ const alphabet = Array.from(states.reduce((acc, current) => { Object.keys(current.transitions).forEach(current => acc.add(current)); return acc; }, new Set())).sort(); while (buttons.firstChild) { buttons.removeChild(buttons.firstChild); } for (const letter of alphabet) { const button = document.createElement('button'); button.innerText = letter; button.addEventListener('click', () => step(letter)); buttons.appendChild(button); } } createLetters(); // Reacts to input in the text box wordInput.addEventListener('input', () => type()); clearButton.addEventListener('click', () => { wordInput.value = ''; builder = ''; state = findStart(states); updateUIState(); }); updateUIState(); } displayGraphButton.addEventListener('click', () => { container.classList.remove('blurred'); displayGraphButton.setAttribute('hidden', 'hidden'); }); sidebarToggle.addEventListener('click', () => { console.log('toggle'); controls.classList.toggle('has-sidebar'); }); /** * @param {import('./examples.js').State[]} states * @returns {number[]} */ function findStart(states) { const starts = []; for (let i = 0; i < states.length; i++) { if (states[i].start) { starts.push(i); } } if (starts.length === 0) { starts.push(0); } return starts; }