commit f80161ee2257e1cd4b7aca7858732414d5ceae54 Author: clfreville2 Date: Thu Dec 21 21:42:10 2023 +0100 Initial commit diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..fc3f00e --- /dev/null +++ b/.drone.yml @@ -0,0 +1,10 @@ +kind: pipeline +name: default +type: docker + +steps: + - name: build + image: node:20-alpine + commands: + - yarn install + - yarn build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d239ce5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Locks +package-lock.json +yarn.lock +pnpm-lock.yaml +bun.lockb diff --git a/README.md b/README.md new file mode 100644 index 0000000..00e278d --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +finite-state-automaton +====================== + +Use +--- + +### No build-tool + +While this project is written with the Node.Js tooling (Vite+MermaidJS), a simple transformation can be done to make it work in a browser. + +```sh +sed -i "s|from 'mermaid'|from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs'|" src/fsm.js +``` + +### Build-tool + +Run your favorite package manager (npm, yarn, pnpm, bun, etc.) to install the dependencies and build the Vite application: + +```sh +npm install +npm run dev # Run the Vite development server +npm run build # Build the application in the dist/ directory +``` diff --git a/dprint.json b/dprint.json new file mode 100644 index 0000000..694f12e --- /dev/null +++ b/dprint.json @@ -0,0 +1,12 @@ +{ + "typescript": { + "quoteStyle": "preferSingle" + }, + "includes": ["src/**/*.{ts,tsx,js,jsx,cjs,mjs,json}", "*.ts"], + "excludes": [ + "**/node_modules" + ], + "plugins": [ + "https://plugins.dprint.dev/typescript-0.88.7.wasm" + ] +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..1e27320 --- /dev/null +++ b/index.html @@ -0,0 +1,22 @@ + + + + + + Finite-state automaton + + + + +
+ +
+
+
+
+

+      

+    
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..35fa534 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "finite-state-automaton", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "devDependencies": { + "typescript": "^5.3.3", + "vite": "^5.0.8" + }, + "dependencies": { + "mermaid": "^10.6.1" + } +} diff --git a/src/examples.js b/src/examples.js new file mode 100644 index 0000000..f69e68d --- /dev/null +++ b/src/examples.js @@ -0,0 +1,14 @@ +/** + * @typedef State + * @property {Object.} transitions + * @property {boolean} [accepting] + */ + +/** + * @type {State[]} + */ +export const ENDS_WITH_TWO_B = [ + { transitions: { a: 0, b: 1 } }, + { transitions: { a: 0, b: 2 } }, + { transitions: { a: 0, b: 2 }, accepting: true }, +]; diff --git a/src/fsm.js b/src/fsm.js new file mode 100644 index 0000000..f0b7681 --- /dev/null +++ b/src/fsm.js @@ -0,0 +1,94 @@ +import mermaid from 'mermaid'; +import { ENDS_WITH_TWO_B } from './examples.js'; + +const IS_VALID = 'is-valid'; +const IS_INVALID = 'is-invalid'; + +const wordInput = /** @type {HTMLInputElement} */ (document.getElementById('word-input')); +const buttons = /** @type {HTMLDivElement} */ (document.getElementById('input-buttons')); +const light = /** @type {HTMLDivElement} */ (document.getElementById('light')); + +mermaid.initialize({ + theme: window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'default', + startOnLoad: false, +}); + +const states = ENDS_WITH_TWO_B; +let state = 0; +let builder = ''; + +// Build the mermaid graph definition +let graphDefinition = 'stateDiagram-v2'; +for (let i = 0; i < states.length; ++i) { + graphDefinition += `\n s${i} : ${i}`; + for (const [transition, destination] of Object.entries(states[i].transitions)) { + graphDefinition += `\n s${i} --> s${destination}: ${transition}`; + } +} +const graph = /** @type {HTMLDivElement} */ (document.getElementById('pen')); +const { svg } = await mermaid.render('state-graph', graphDefinition); +graph.innerHTML = svg; +const nodes = graph.querySelectorAll('.label-container'); + +/** + * Updates the UI to reflect the current state. + */ +function updateUIState() { + wordInput.value = builder; + if (state === -1 || !states[state].accepting) { + light.classList.remove(IS_VALID); + light.classList.add(IS_INVALID); + } else { + light.classList.remove(IS_INVALID); + light.classList.add(IS_VALID); + } + nodes.forEach((node) => node.classList.remove('current-node')); + if (state in nodes) { + nodes[state].classList.add('current-node'); + } +} + +/** + * Steps the FSM with the given letter. + * + * @param {string} letter + */ +function step(letter) { + if (state === -1) { + return; + } + builder += letter; + state = states[state].transitions[letter] ?? -1; + updateUIState(); +} + +// Dynamically create buttons for each letter in the alphabet +/** + * @type {string[]} + */ +const alphabet = Array.from(states.reduce((acc, current) => { + Object.keys(current.transitions).forEach(current => acc.add(current)); + return acc; +}, new Set())).sort(); +for (const letter of alphabet) { + const button = document.createElement('button'); + button.innerText = letter; + button.addEventListener('click', () => step(letter)); + buttons.appendChild(button); +} + +// Reacts to input in the text box +wordInput.addEventListener('input', () => { + const value = wordInput.value; + console.log(value); + builder = ''; + state = 0; + for (const letter of value) { + step(letter); + } + if (!value.length) { + updateUIState(); + } +}); + +updateUIState(); diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..9a6105c --- /dev/null +++ b/src/style.css @@ -0,0 +1,93 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.input { + display: flex; + align-items: center; +} +input { + padding: 0.3em 0.5em; + margin-bottom: 0.5em; + font-size: 1.1em; + width: 6em; + background-color: transparent; + transition: border-color 0.25s; +} +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +#light { + width: 2.5em; + height: 2.5em; + border-radius: 50%; + background-color: #1a1a1a; + margin-left: .75em; +} +#light.is-valid { + background-color: #46fd46; +} +#light.is-invalid { + background-color: #fd3838; +} + +.current-node { + fill: red !important; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..591aec8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "types": ["vite/client"], + "skipLibCheck": true, + "allowJs": true, + "checkJs": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..20d0cf7 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + base: '', + build: { + target: 'esnext', + modulePreload: false, + rollupOptions: { + external: ['mermaid'], + output: { + paths: { + mermaid: 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs', + }, + }, + }, + }, +});