├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .typstignore ├── CHANGELOG.md ├── Justfile ├── LICENSE ├── README.md ├── docs ├── assets │ ├── example-dark.svg │ ├── example.svg │ ├── example.typ │ ├── finite-logo-dark.svg │ ├── finite-logo.svg │ ├── finite-logo.typ │ └── flaci-export.json ├── manual.pdf └── manual.typ ├── scripts ├── link ├── package ├── setup └── uninstall ├── src ├── cmd.typ ├── draw.typ ├── finite.typ ├── flaci.typ ├── layout.typ └── util.typ ├── tbump.toml ├── tests ├── .gitignore ├── .ignore ├── accepts │ ├── .gitignore │ ├── diff │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ └── 4.png │ ├── out │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ └── 4.png │ ├── ref │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ └── 4.png │ └── test.typ ├── flaci-load │ ├── .gitignore │ ├── DEA-1.json │ ├── DEA-2.json │ ├── DEA-3.json │ ├── NEA-1.json │ ├── NKA-1.json │ ├── diff │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ └── 4.png │ ├── out │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ └── 4.png │ ├── ref │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ └── 4.png │ └── test.typ ├── large-automaton │ ├── diff │ │ └── 1.png │ ├── out │ │ └── 1.png │ ├── ref │ │ └── 1.png │ └── test.typ ├── layout-circular │ ├── .gitignore │ ├── ref │ │ └── 1.png │ └── test.typ ├── layouts │ ├── .gitignore │ ├── diff │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ └── 6.png │ ├── out │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ └── 6.png │ ├── ref │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ └── 6.png │ └── test.typ ├── spec │ ├── .gitignore │ └── test.typ ├── state-anchors │ ├── .gitignore │ ├── diff │ │ ├── 1.png │ │ └── 2.png │ ├── out │ │ ├── 1.png │ │ └── 2.png │ ├── ref │ │ ├── 1.png │ │ └── 2.png │ └── test.typ ├── state-final │ ├── .gitignore │ ├── ref │ │ └── 1.png │ └── test.typ ├── state-initial │ ├── .gitignore │ ├── ref │ │ ├── 1.png │ │ └── 2.png │ └── test.typ ├── state-labels │ ├── diff │ │ ├── 1.png │ │ └── 2.png │ ├── out │ │ ├── 1.png │ │ └── 2.png │ ├── ref │ │ ├── 1.png │ │ └── 2.png │ └── test.typ ├── state │ ├── .gitignore │ ├── ref │ │ ├── 1.png │ │ └── 2.png │ └── test.typ ├── test-utils.typ ├── transition-labels │ ├── .gitignore │ ├── diff │ │ ├── 1.png │ │ └── 2.png │ ├── out │ │ ├── 1.png │ │ └── 2.png │ ├── ref │ │ ├── 1.png │ │ └── 2.png │ └── test.typ ├── transition │ ├── .gitignore │ ├── diff │ │ ├── 1.png │ │ ├── 2.png │ │ └── 3.png │ ├── out │ │ ├── 1.png │ │ ├── 2.png │ │ └── 3.png │ ├── ref │ │ ├── 1.png │ │ ├── 2.png │ │ └── 3.png │ └── test.typ └── ttable │ ├── .gitignore │ ├── ref │ ├── 1.png │ ├── 2.png │ └── 3.png │ └── test.typ └── typst.toml /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | 8 | jobs: 9 | tests: 10 | strategy: 11 | # run tests on all versions, even if one fails 12 | fail-fast: false 13 | matrix: 14 | # add any other Typst versions that your package should support 15 | typst-version: 16 | #- typst: 0.12 17 | # tytanic: 0.1 18 | - typst: 0.13 19 | tytanic: 0.2 20 | # the docs don't need to build with all versions supported by the package; 21 | # the latest one is enough 22 | doc: false 23 | 24 | name: Test for ${{ matrix.typst-version.typst }} (Tytanic ${{ matrix.typst-version.tytanic }}${{ matrix.typst-version.doc && ', with docs' }}) 25 | runs-on: ubuntu-latest 26 | 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | 31 | - name: Install just and tytanic 32 | uses: taiki-e/install-action@v2 33 | with: 34 | tool: just,tytanic@${{ matrix.typst-version.tytanic }} 35 | 36 | - name: Setup typst 37 | id: setup-typst 38 | uses: typst-community/setup-typst@v3 39 | with: 40 | typst-version: ${{ matrix.typst-version.typst }} 41 | 42 | - name: Run test suite 43 | run: just test 44 | 45 | - name: Archive test results 46 | uses: actions/upload-artifact@v4 47 | if: always() 48 | with: 49 | name: typst-${{ steps.setup-typst.outputs.typst-version }}-test-results 50 | path: | 51 | tests/**/diff/*.png 52 | tests/**/out/*.png 53 | tests/**/ref/*.png 54 | retention-days: 5 55 | 56 | - name: Build docs 57 | if: ${{ matrix.typst-version.doc }} 58 | run: just doc 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Thumbs.db 3 | -------------------------------------------------------------------------------- /.typstignore: -------------------------------------------------------------------------------- 1 | # this is not a "standard" ignore file, it's specific to this template's `scripts/package` script 2 | # list any files here that should not be uploaded to Universe when releasing this package 3 | 4 | # if you are used to ignore files, be aware that .typstignore is a bit more limited: 5 | # - only this file is used; .typstignore files in subdirectories are not considered 6 | # - patterns must match file/directory names from the beginning: `x.typ` will not match `src/x.typ` 7 | # - `*` in patterns works, but also matches directory separators: `*.typ` _will_ match `src/x.typ` 8 | # .git and .typstignore are excluded automatically 9 | 10 | .github 11 | scripts 12 | tests 13 | Justfile 14 | # PDF manuals should be included so that they can be linked, but not their sources 15 | docs/* 16 | !docs/*.pdf 17 | !docs/assets 18 | docs/assets/* 19 | !docs/assets/*.png 20 | !docs/assets/*.svg 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### Version 0.5.0 4 | 5 | - :warning: New layout system: 6 | - Layouts are no longer CeTZ groups and only are used as a parameter to `#automaton`. 7 | - :warning: Breaking style changes: 8 | - Changed label color attribute from `color` to `fill` for consistency. 9 | - The default for `state.label.fill` and `transition.label.fill` is now `none` and sets the color of the label to `stroke.paint`. 10 | - The default value for `transition.curve` is now `1.0` for an easier understanding of the influence on the curvature. 11 | - Added `flaci` module to load and display files exported from [FLACI](http://flaci.com): 12 | - `flaci.load` loads a FLACI file as a `finite` automaton specification. 13 | - `flaci.automaton` displays a FLACI file as a `finite` automaton. 14 | 15 | ### Version 0.4.2 16 | 17 | - Fixed label rotation for transitions. 18 | - Bumped dependencies: 19 | - CeTZ 0.3.0 :arrow upper right: 0.3.1 20 | - t4t 0.3.1 :arrow upper right: 0.4.0 21 | 22 | ### Version 0.4.1 23 | 24 | - Fixes error in `#powerset` function (#8). 25 | 26 | ### Version 0.4.0 27 | 28 | :warning: Version 0.4.0 is a major rewrite of finite to make it compatible with Typst 0.12 and CeTZ 0.3.0. This includes some minor breaking changes, mostly in how layouts work, but overall functionality should be the same. 29 | 30 | ### Version 0.3.2 31 | 32 | - Fixed an issue with final states not being recognized properly (#5) 33 | 34 | ### Version 0.3.1 35 | 36 | - Added styling options for initial states: 37 | - `stroke` sets a stroke for the marking. 38 | - `scale` scales the marking by a factor. 39 | - Updated manual. 40 | 41 | ### Version 0.3.0 42 | 43 | - Bumped `tools4typst` to v0.3.2. 44 | - Introducing automaton specs as a data structure. 45 | - Changes to `automaton` command: 46 | - Changed `label-format` argument to `state-format` and `input-format`. 47 | - `layout` can now take a dictionary with (`state`: `coordinate`) pairs to position states. 48 | - Added `#powerset` command, to transform a NFA into a DFA. 49 | - Added `#add-trap` command, to complete a partial DFA. 50 | - Added `#accepts` command, to test a word against an NFA or DFA. 51 | - Added `transpose-table` and `get-inputs` utilities. 52 | - Added "Start" label to the mark for initial states. 53 | - Added option to modify the mark label for initial states. 54 | - Added anchor option for loops, to position the loop at one of the eight default anchors. 55 | - Changed `curve` option to be the height of the arc of the transition. 56 | - This makes styling more consistent over longer distances. 57 | - Added `rest` key to custom layouts. 58 | 59 | ### Version 0.2.0 60 | 61 | - Bumped CeTZ to v0.1.1. 62 | 63 | ### Version 0.1.0 64 | 65 | - Initial release submitted to [typst/packages](https://github.com/typst/packages). 66 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | root := justfile_directory() 2 | package-fork := x'${TYPST_PKG_FORK:-}' 3 | export TYPST_ROOT := root 4 | 5 | [private] 6 | default: 7 | @just --list --unsorted 8 | 9 | # build assets 10 | assets: 11 | typst compile docs/assets/example.typ docs/assets/example.svg 12 | typst compile --input theme=dark docs/assets/example.typ docs/assets/example-dark.svg 13 | typst compile docs/assets/finite-logo.typ docs/assets/finite-logo.svg 14 | typst compile --input theme=dark docs/assets/finite-logo.typ docs/assets/finite-logo-dark.svg 15 | 16 | # generate manual 17 | doc: assets 18 | typst compile docs/manual.typ docs/manual.pdf 19 | 20 | # run test suite 21 | test *args: 22 | tt run {{ args }} 23 | 24 | # update test cases 25 | update *args: 26 | tt update {{ args }} 27 | 28 | # package the library into the specified destination folder 29 | package target: 30 | ./scripts/package "{{ target }}" 31 | 32 | # install the library with the "@local" prefix 33 | install: (package "@local") 34 | 35 | # install the library with the "@preview" prefix (for pre-release testing) 36 | install-preview: (package "@preview") 37 | 38 | [private] 39 | [working-directory(x'${TYPST_PKG_FORK:-.}')] 40 | prepare-fork: 41 | git checkout main 42 | git pull typst main 43 | git push origin --force 44 | 45 | prepare: prepare-fork (package package-fork) 46 | 47 | # create a symbolic link to this library in the target repository 48 | link target="@local": 49 | ./scripts/link "{{ target }}" 50 | 51 | link-preview: (link "@preview") 52 | 53 | [private] 54 | remove target: 55 | ./scripts/uninstall "{{ target }}" 56 | 57 | # uninstalls the library from the "@local" prefix 58 | uninstall: (remove "@local") 59 | 60 | # uninstalls the library from the "@preview" prefix (for pre-release testing) 61 | uninstall-preview: (remove "@preview") 62 | 63 | # unlinks the library from the "@local" prefix 64 | unlink: (remove "@local") 65 | 66 | # unlinks the library from the "@preview" prefix 67 | unlink-preview: (remove "@preview") 68 | 69 | # run ci suite 70 | ci: test doc 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-2025 Jonas Neugebauer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | [![Typst Package](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2Fjneug%2Ftypst-finite%2Frefs%2Fheads%2Fmain%2Ftypst.toml&query=%24.package.version&prefix=v&logo=typst&label=package&color=239DAD)](https://typst.app/universe/package/finite) 11 | [![MIT License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/lilaq-project/lilaq/blob/main/LICENSE) 12 | [![Tests](https://github.com/jneug/typst-finite/actions/workflows/tests.yml/badge.svg)](https://github.com/jneug/typst-finite/actions/workflows/tests.yml) 13 | 14 | 15 | 16 | **finite** is a [Typst](https://github.com/typst/typst) package for rendering finite automata. 17 | 18 | --- 19 | 20 | ## Usage 21 | 22 | Import the package from the Typst preview repository: 23 | 24 | ```typst 25 | #import "@preview/finite:0.5.0": automaton 26 | ``` 27 | 28 | After importing the package, simply call `#automaton()` with a dictionary holding a transition table: 29 | ```typst 30 | #import "@preview/finite:0.5.0": automaton 31 | 32 | #automaton( 33 | ( 34 | q0: (q1: 0, q0: "0,1"), 35 | q1: (q0: (0, 1), q2: "0"), 36 | q2: none, 37 | ), 38 | initial: "q1", 39 | final: ("q0",), 40 | ) 41 | ``` 42 | 43 | The output should look like this: 44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 |
52 | 53 | ## Further documentation 54 | 55 | See [manual.pdf](docs/manual.pdf) for a full manual of the package. 56 | 57 | See the [changelog](CHANGELOG.md) for recent changes. 58 | -------------------------------------------------------------------------------- /docs/assets/example.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | -------------------------------------------------------------------------------- /docs/assets/example.typ: -------------------------------------------------------------------------------- 1 | #import "../../src/finite.typ": automaton 2 | 3 | #set page(width: auto, height: auto, margin: 1cm, fill: none) 4 | 5 | #let theme = ( 6 | text: black, 7 | bg: white, 8 | ) 9 | #if sys.inputs.at("theme", default: "light") == "dark" { 10 | theme.text = rgb("#d6d1cd") 11 | theme.bg = rgb("#343534") 12 | } 13 | 14 | #set text(theme.text) if theme == "dark" 15 | 16 | #automaton( 17 | // @typstyle off 18 | ( 19 | q0: (q1: 0, q0: "0,1"), 20 | q1: (q0: (0, 1), q2: "0"), 21 | q2: none, 22 | ), 23 | initial: "q1", 24 | final: ("q0",), 25 | style: ( 26 | transition: (stroke: theme.text), 27 | state: (fill: theme.bg, stroke: theme.text), 28 | ), 29 | ) 30 | -------------------------------------------------------------------------------- /docs/assets/finite-logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | -------------------------------------------------------------------------------- /docs/assets/finite-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | -------------------------------------------------------------------------------- /docs/assets/finite-logo.typ: -------------------------------------------------------------------------------- 1 | #import "../../src/finite.typ" 2 | 3 | #let theme = ( 4 | text: black, 5 | bg: white, 6 | ) 7 | #if sys.inputs.at("theme", default: "light") == "dark" { 8 | theme.text = rgb("#d6d1cd") 9 | theme.bg = rgb("#343534") 10 | } 11 | 12 | #set page(width: auto, height: auto, margin: 5mm, fill: none) 13 | #set text(font: "Liberation Sans") 14 | 15 | #finite.automaton( 16 | ( 17 | q0: (q1: none), 18 | q1: (q2: none), 19 | q2: (q3: none), 20 | q3: (q4: none), 21 | q4: (q5: none), 22 | q5: none, 23 | ), 24 | labels: range(6).fold( 25 | (:), 26 | (d, i) => { 27 | d.insert("q" + str(i), "FINITE".at(i)) 28 | d 29 | }, 30 | ), 31 | style: ( 32 | state: (label: (size: 20pt), fill: theme.bg, stroke: theme.text), 33 | transition: (curve: .8, stroke: theme.text), 34 | q0: (initial: ""), 35 | ), 36 | layout: finite.layout.linear.with(spacing: .4), 37 | ) 38 | -------------------------------------------------------------------------------- /docs/assets/flaci-export.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TEIL2: Wort mit gerader Anzahl an Nullen", 3 | "description": "", 4 | "type": "DEA", 5 | "automaton": { 6 | "acceptCache": [], 7 | "simulationInput": [], 8 | "Alphabet": [ 9 | "0", 10 | "1", 11 | "2", 12 | "3", 13 | "4", 14 | "5", 15 | "6", 16 | "7", 17 | "8", 18 | "9" 19 | ], 20 | "StackAlphabet": [ 21 | "|" 22 | ], 23 | "States": [ 24 | { 25 | "ID": 1, 26 | "Name": "q0", 27 | "x": 220, 28 | "y": 130, 29 | "Final": false, 30 | "Radius": 30, 31 | "Transitions": [ 32 | { 33 | "Source": 1, 34 | "Target": 1, 35 | "x": 0, 36 | "y": -150, 37 | "Labels": [ 38 | "1", 39 | "2", 40 | "3", 41 | "4", 42 | "5", 43 | "6", 44 | "7", 45 | "8", 46 | "9" 47 | ] 48 | }, 49 | { 50 | "Source": 1, 51 | "Target": 2, 52 | "x": 0, 53 | "y": -80, 54 | "Labels": [ 55 | "0" 56 | ] 57 | } 58 | ], 59 | "Start": true 60 | }, 61 | { 62 | "ID": 2, 63 | "Name": "q1", 64 | "x": 510, 65 | "y": 130, 66 | "Final": true, 67 | "Radius": 30, 68 | "Transitions": [ 69 | { 70 | "Source": 2, 71 | "Target": 2, 72 | "x": 0, 73 | "y": -150, 74 | "Labels": [ 75 | "1", 76 | "2", 77 | "3", 78 | "4", 79 | "5", 80 | "6", 81 | "7", 82 | "8", 83 | "9" 84 | ] 85 | }, 86 | { 87 | "Source": 2, 88 | "Target": 1, 89 | "x": 0, 90 | "y": -50, 91 | "Labels": [ 92 | "0" 93 | ] 94 | } 95 | ], 96 | "Start": false 97 | } 98 | ], 99 | "lastInputs": [] 100 | }, 101 | "GUID": "hiicmv2p1" 102 | } 103 | -------------------------------------------------------------------------------- /docs/manual.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/docs/manual.pdf -------------------------------------------------------------------------------- /docs/manual.typ: -------------------------------------------------------------------------------- 1 | 2 | #import "@preview/mantys:1.0.2": * 3 | 4 | #import "../src/finite.typ" as finite: cetz 5 | 6 | #import "../src/util.typ" 7 | 8 | // Customize the show-module function 9 | #let show-module( 10 | name, 11 | scope: (:), 12 | outlined: false, 13 | ..tidy-args, 14 | ) = tidy-module( 15 | name, 16 | read("../src/" + name + ".typ"), 17 | show-outline: outlined, 18 | // scope: scope, 19 | // include-examples-scope: true, 20 | // extract-headings: 3, 21 | ..tidy-args, 22 | ) 23 | 24 | // Nice display of CeTZ commands 25 | #show "CETZ": _ => package[CeTZ] 26 | #let cetz-cmd = cmd.with(module: "cetz") 27 | #let cetz-cmd- = cmd-.with(module: "cetz") 28 | #let cetz-draw = cmd.with(module: "cetz.draw") 29 | #let cetz-draw- = cmd-.with(module: "cetz.draw") 30 | 31 | #show: mantys( 32 | ..toml("../typst.toml"), 33 | 34 | title: align( 35 | center, 36 | image("assets/finite-logo.svg", width: 100%), 37 | ), 38 | 39 | date: datetime.today(), 40 | abstract: [ 41 | FINITE is a Typst package to draw transition diagrams for finite automata (finite state machines) with the power of CETZ. 42 | 43 | The package provides commands to quickly draw automata from a transition table but also lets you manually create and customize transition diagrams on any CETZ canvas. 44 | ], 45 | 46 | examples-scope: ( 47 | scope: ( 48 | cetz: cetz, 49 | finite: finite, 50 | util: util, 51 | automaton: finite.automaton, 52 | ), 53 | ), 54 | 55 | theme: themes.modern, 56 | 57 | git: git-info(file => read(file)), 58 | 59 | assets: ( 60 | "assets/finite-logo.svg": "assets/finite-logo.typ", 61 | ), 62 | ) 63 | 64 | = Usage 65 | 66 | == Importing the package 67 | 68 | Import the package in your Typst file: 69 | 70 | #show-import(imports: "automaton") 71 | 72 | == Manual installation 73 | 74 | The package can be downloaded and saved into the system dependent local package repository. 75 | 76 | Either download the current release from #github("jneug/typst-finite") and unpack the archive into your system dependent local repository folder#footnote[#std.link("https://github.com/typst/packages#local-packages")] or clone it directly: 77 | 78 | #show-git-clone() 79 | 80 | In either case, make sure the files are placed in a subfolder with the correct version number: #context document.use-value("package", p => raw(block: false, p.name + "/" + str(p.version))) 81 | 82 | After installing the package, just import it inside your `typ` file: 83 | 84 | #show-import(repository: "@local", imports: "automaton") 85 | 86 | == Dependencies 87 | 88 | FINITE loads #link("https://github.com/johannes-wolf/typst-canvas")[CETZ] and the utility package #link("https://github.com/jneug/typst-tools4typst")[#package[t4t]] from the `preview` package repository. The dependencies will be downloaded by Typst automatically on first compilation. 89 | 90 | #custom-type("coordinate", color: silver)Whenever a #dtype("coordinate") type is referenced, a CETZ coordinate can be used. Please refer to the CETZ manual for further information on coordinate systems. 91 | 92 | = Drawing automata 93 | 94 | FINITE helps you draw transition diagrams for finite automata in your Typst documents, using the power of CETZ. 95 | 96 | To draw an automaton, simply import #cmd[automaton] from FINITE and use it like this: 97 | #example[```typ 98 | #automaton(( 99 | q0: (q1:0, q0:"0,1"), 100 | q1: (q0:(0,1), q2:"0"), 101 | q2: none, 102 | )) 103 | ```] 104 | 105 | As you can see, an automaton ist defined by a dictionary of dictionaries. The keys of the top-level dictionary are the names of states to draw. The second-level dictionaries use the names of connected states as keys and transition labels as values. 106 | 107 | In the example above, the states `q0`, `q1` and `q2` are defined. `q0` is connected to `q1` and has a loop to itself. `q1` transitions to `q2` and back to `q0`. #cmd-[automaton] selected the first state in the dictionary (in this case `q0`) to be the initial state and the last (`q2`) to be a final state. 108 | 109 | See @aut-specs for more details on how to specify automata. 110 | 111 | To modify the layout and style of the transition diagram, #cmd-[automaton] accepts a set of options: 112 | #example(breakable: true)[```typ 113 | #automaton( 114 | ( 115 | q0: (q1:0, q0:"0,1"), 116 | q1: (q0:(0,1), q2:"0"), 117 | q2: (), 118 | ), 119 | initial: "q1", 120 | final: ("q0", "q2"), 121 | labels:( 122 | q2: "FIN" 123 | ), 124 | style:( 125 | state: (fill: luma(248), stroke:luma(120)), 126 | transition: (stroke: (dash:"dashed")), 127 | q0-q0: (anchor:top+left), 128 | q1: (initial:top), 129 | q1-q2: (stroke: 2pt + red) 130 | ) 131 | ) 132 | ```] 133 | 134 | For larger automatons, the states can be arranged in different ways: 135 | #example(breakable: true)[```typ 136 | #let aut = (:) 137 | #for i in range(10) { 138 | let name = "q"+str(i) 139 | aut.insert(name, (:)) 140 | if i < 9 { 141 | aut.at(name).insert("q" + str(i + 1), none) 142 | } 143 | } 144 | #automaton( 145 | aut, 146 | layout: finite.layout.circular.with(offset: 45deg), 147 | style: ( 148 | transition: (curve: 0), 149 | q0: (initial: top+left) 150 | ) 151 | ) 152 | ```] 153 | 154 | See @using-layout for more on layouts. 155 | 156 | == Specifing finite automata 157 | 158 | Most of FINITEs commands expect a finite automaton specification ("@type:spec" in short) the first argument. These specifications are dictionaries defining the elements of the automaton. 159 | 160 | If an automaton has only one final state, the spec can simply be a @type:transition-table. In other cases, the specification can explicitly define the various elements. 161 | 162 | #custom-type("transition-table", color: rgb("#57a0be")) 163 | A transition table is a #typ.t.dict with state names as keys and dictionaries as values. The nested dictionaries have state names as keys and the transition inputs / labels as values. 164 | 165 | #codesnippet[```typc 166 | ( 167 | q0: (q1: (0, 1), q2: (0, 1)), 168 | q1: (q1: (0, 1), q0: 0, q2: 1), 169 | q2: (q0: 0, q1: (1, 0)), 170 | ) 171 | ```] 172 | 173 | #custom-type("spec", color: rgb("#76d6ff")) 174 | A specification (@type:spec) is composed of these keys: 175 | ```typc 176 | ( 177 | transitions: (...), 178 | states: (...), 179 | inputs: (...), 180 | initial: "...", 181 | final: (...) 182 | ) 183 | ``` 184 | 185 | - `transitions` is a dictionary of dictionaries in the format: 186 | ```typc 187 | ( 188 | state1: (input1, input2, ...), 189 | state2: (input1, input2, ...), 190 | ... 191 | ) 192 | ``` 193 | - `states` is an optional array with the names of all states. The keys of `transitions` are used by default. 194 | - `inputs` is an optional array with all input values. The inputs found in `transitions` are used by default. 195 | - `initial` is an optional name of the initial state. The first value in `states` is used by default. 196 | - `final` is an optional array of final states. The last value in `states` is used by default. 197 | 198 | The utility function #cmd(module: "util")[to-spec] can be used to create a full spec from a partial dictionary by filling in the missing values with the defaults. 199 | 200 | == Command reference 201 | #show-module("cmd", sort-functions: false) 202 | 203 | == Styling the output 204 | 205 | As common in CETZ, you can pass general styles for states and transitions to the #cetz-cmd-[set-style] function within a call to #cetz-cmd-[canvas]. The elements functions #cmd-[state] and #cmd-[transition] (see below) can take their respective styling options as arguments to style individual elements. 206 | 207 | #cmd[automaton] takes a #arg[style] argument that passes the given style to the above functions. The example below sets a background and stroke color for all states and draws transitions with a dashed style. Additionally, the state `q1` has the arrow indicating an initial state drawn from above instead from the left. The transition from `q1` to `q2` is highlighted in red. 208 | #example(breakable: true)[```typ 209 | #automaton( 210 | ( 211 | q0: (q1:0, q0:"0,1"), 212 | q1: (q0:(0,1), q2:"0"), 213 | q2: (), 214 | ), 215 | initial: "q1", 216 | final: ("q0", "q2"), 217 | style:( 218 | state: (fill: luma(248), stroke:luma(120)), 219 | transition: (stroke: (dash:"dashed")), 220 | q1: (initial:top), 221 | q1-q2: (stroke: 2pt + red) 222 | ) 223 | ) 224 | ```] 225 | 226 | Every state can be accessed by its name and every transition is named with its initial and end state joined with a dash (`-`), for example `q1-q2`. 227 | 228 | The supported styling options (and their defaults) are as follows: 229 | - states: 230 | / #arg(fill: auto): Background fill for states. 231 | / #arg(stroke: auto): Stroke for state borders. 232 | / #arg(radius: .6): Radius of the states circle. 233 | / #arg(extrude: .88): 234 | - `label`: 235 | / #arg(text: auto): State label. 236 | / #arg(size: 1em): Initial text size for the labels (will be modified to fit the label into the states circle). 237 | / #arg(fill: none): Color of the label text. 238 | / #arg(padding: auto): Padding around the label. 239 | - `initial`: 240 | / #arg(anchor: auto): Anchorpoint to point the initial arrow to. 241 | / #arg(label: auto): Text above the arrow. 242 | / #arg(stroke: auto): Stroke for the arrow. 243 | / #arg(scale: auto): Scale of the arrow. 244 | - transitions 245 | / #arg(curve: 1.0): "Curviness" of transitions. Set to #value(0) to get straight lines. 246 | / #arg(stroke: auto): Stroke for transition lines. 247 | - `label`: 248 | / #arg(text: ""): Transition label. 249 | / #arg(size: 1em): Size for label text. 250 | / #arg(fill: auto): Color for label text. 251 | / #arg( 252 | pos: .5, 253 | ): Position on the transition, between #value(0) and #value(1). #value(0) sets the text at the start, #value(1) at the end of the transition. 254 | / #arg(dist: .33): Distance of the label from the transition. 255 | / #arg(angle: auto): Angle of the label text. #value(auto) will set the angle based on the transitions direction. 256 | 257 | == Using #cmd-(module: "cetz")[canvas] 258 | 259 | The above commands use custom CETZ elements to draw states and transitions. For complex automata, the functions in the #module[draw] module can be used inside a call to #cetz-cmd-[canvas]. 260 | #example(breakable: true)[```typ 261 | #cetz.canvas({ 262 | import cetz.draw: set-style 263 | import finite.draw: state, transition 264 | 265 | state((0,0), "q0", initial:true) 266 | state((2,1), "q1") 267 | state((4,-1), "q2", final:true) 268 | state((rel:(0, -3), to:"q1.south"), "trap", label:"TRAP", anchor:"north-west") 269 | 270 | transition("q0", "q1", inputs:(0,1)) 271 | transition("q1", "q2", inputs:(0)) 272 | transition("q1", "trap", inputs:(1), curve:-1) 273 | transition("q2", "trap", inputs:(0,1)) 274 | transition("trap", "trap", inputs:(0,1)) 275 | }) 276 | ```] 277 | 278 | === Element functions 279 | #show-module("draw", sort-functions: false) 280 | 281 | === Anchors 282 | 283 | States and transitions are created in a #cetz-draw[group]. States are drawn with a circle named `state` that can be referenced in the group. Additionally they have a content element named `label` and optionally a line named `initial`. These elements can be referenced inside the group and used as anchors for other CETZ elements. The anchors of `state` are also copied to the state group and are directly accessible. 284 | 285 | #info-alert[ 286 | That means setting #arg(anchor: "west") for a state will anchor the state at the `west` anchor of the states circle, not of the bounding box of the group. 287 | ] 288 | 289 | Transitions have an `arrow` (#cetz-draw[line]) and `label` (#cetz-draw[content]) element. The anchors of `arrow` are copied to the group. 290 | 291 | #example(breakable: true)[```typ 292 | #cetz.canvas({ 293 | import cetz.draw: circle, line, content 294 | import finite.draw: state, transition 295 | 296 | let magenta = rgb("#dc41f1") 297 | 298 | state((0, 0), "q0") 299 | state((4, 0), "q1", final: true, stroke: magenta) 300 | 301 | transition("q0", "q1", label: $epsilon$) 302 | 303 | circle("q0.north-west", radius: .4em, stroke: none, fill: black) 304 | 305 | let magenta-stroke = 2pt + magenta 306 | circle("q0-q1.label", radius: .5em, stroke: magenta-stroke) 307 | line( 308 | name: "q0-arrow", 309 | (rel: (.6, .6), to: "q1.state.north-east"), 310 | (rel: (.1, .1), to: "q1.state.north-east"), 311 | stroke: magenta-stroke, 312 | mark: (end: ">"), 313 | ) 314 | content( 315 | (rel: (0, .25), to: "q0-arrow.start"), 316 | text(fill: magenta, [*very important state*]), 317 | ) 318 | }) 319 | ``` ] 320 | 321 | == Layouts 322 | 323 | #error-alert[ 324 | Layouts changed in FINITE version 0.5 and are no longer compatible with FINITE 0.4 and before. 325 | ] 326 | 327 | Layouts can be passed to @cmd:automaton to position states on the canvas without the need to give specific coordinates for each state. FINITE ships with a bunch of layouts, to accomodate different scenarios. 328 | 329 | === Available layouts 330 | #show-module("layout", sort-functions: false) 331 | 332 | == Utility functions 333 | #show-module("util", outlined: true) 334 | 335 | = Simulating input 336 | 337 | FINITE has a set of functions to simulate, test and view finite automata. 338 | 339 | = FLACI support 340 | 341 | FINITE was heavily inspired by the online app #link("https://flaci.org", "FLACI"). FLACI lets you build automata in a visual online app and export your creations as JSON files. FINITE can import theses files and render the result in your document. 342 | 343 | #warning-alert[FINITE currently only supports DEA and NEA automata.] 344 | 345 | #example[```typ 346 | #finite.flaci.automaton(read("flaci-export.json")) 347 | ```][ 348 | #finite.flaci.automaton(read("assets/flaci-export.json")) 349 | ] 350 | 351 | #warning-alert[ 352 | *Important* \ 353 | Read the FLACI json-file with the #typ.read function, not 354 | the #typ.json function. FLACI exports automatons with a wrong encoding 355 | that prevents Typst from properly loading the file as JSON. 356 | ] 357 | 358 | === FLACI functions 359 | #show-module("flaci", module: "flaci", sort-functions: false) 360 | 361 | // = Working with grammars 362 | 363 | = Doing other stuff with FINITE 364 | 365 | Since transition diagrams are effectively graphs, FINITE could also be used to draw graph structures: 366 | #example[```typ 367 | #cetz.canvas({ 368 | import cetz.draw: set-style 369 | import finite.draw: state, transitions 370 | 371 | state((0,0), "A") 372 | state((3,1), "B") 373 | state((4,-2), "C") 374 | state((1,-3), "D") 375 | state((6,1), "E") 376 | 377 | transitions(( 378 | A: (B: 1.2), 379 | B: (C: .5, E: 2.3), 380 | C: (B: .8, D: 1.4, E: 4.5), 381 | D: (A: 1.8), 382 | E: (:) 383 | ), 384 | C-E: (curve: -1.2)) 385 | }) 386 | ```] 387 | 388 | = Showcase 389 | 390 | #example(breakable: true)[```typ 391 | #scale(80%, automaton(( 392 | q0: (q1: 0, q2: 0), 393 | q2: (q3: 1, q4: 0), 394 | q4: (q2: 0, q5: 0, q6: 0), 395 | q6: (q7: 1), 396 | q1: (q3: 1, q4: 0), 397 | q3: (q1: 1, q5: 1, q6: 1), 398 | q5: (q7: 1), 399 | q7: () 400 | ), 401 | layout: finite.layout.group.with(grouping: ( 402 | ("q0",), 403 | ("q1", "q2", "q3", "q4", "q5", "q6"), 404 | ("q7",) 405 | ), 406 | spacing: 2, 407 | layout: ( 408 | finite.layout.custom.with(positions: (q0: (0, -2))), 409 | finite.layout.grid.with(columns:3, spacing:2.6, position: (2, 1)), 410 | finite.layout.custom.with(positions: (q7: (8, 6))) 411 | ) 412 | ), 413 | style: ( 414 | transition: (curve: 0), 415 | q1-q3: (curve:1), 416 | q3-q1: (curve:1), 417 | q2-q4: (curve:1), 418 | q4-q2: (curve:1), 419 | q1-q4: (label: (pos:.75)), 420 | q2-q3: (label: (pos:.75, dist:-.33)), 421 | q3-q6: (label: (pos:.75)), 422 | q4-q5: (label: (pos:.75, dist:-.33)), 423 | q4-q6: (curve: 1) 424 | ) 425 | )) 426 | ```] 427 | -------------------------------------------------------------------------------- /scripts/link: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | 4 | # licensed under Apache License 2.0 5 | 6 | . "$(dirname "${BASH_SOURCE[0]}")/setup" 7 | 8 | if (( $# < 1 )) || [[ "${1:-}" == "help" ]]; then 9 | echo "link TARGET" 10 | echo "" 11 | echo "Creates a symbolic link from '/' at TARGET " 12 | echo "to this project directory. If TARGET is set to @local or @preview," 13 | echo "the local Typst package directory will be used so that the package" 14 | echo "gets installed for local use." 15 | echo "The name and version are read from 'typst.toml' in the project root." 16 | echo "" 17 | echo "Local package prefix: $DATA_DIR/typst/package/local" 18 | echo "Local preview package prefix: $DATA_DIR/typst/package/preview" 19 | exit 1 20 | fi 21 | 22 | TARGET="$(resolve-target "${1:?Missing target path, @local or @preview}")" 23 | echo "Install dir: $TARGET" 24 | 25 | PKGDIR="${TARGET:?}/${PKG_PREFIX:?}" 26 | TARGET="${PKGDIR}/${VERSION:?}" 27 | if [ ! -e "$TARGET" ]; then 28 | mkdir -p "$PKGDIR" 29 | 30 | echo "Linked from: $TARGET" 31 | ln -s "$PWD" "$TARGET" 32 | else 33 | echo "Version already exists: $TARGET" 34 | fi 35 | -------------------------------------------------------------------------------- /scripts/package: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | 4 | # adapted from https://github.com/johannes-wolf/cetz/blob/35c0868378cea5ad323cc0d9c2f76de8ed9ba5bd/scripts/package 5 | # licensed under Apache License 2.0 6 | 7 | . "$(dirname "${BASH_SOURCE[0]}")/setup" 8 | 9 | if (( $# < 1 )) || [[ "${1:-}" == "help" ]]; then 10 | echo "package TARGET" 11 | echo "" 12 | echo "Packages all relevant files into a directory named '/'" 13 | echo "at TARGET. If TARGET is set to @local or @preview, the local Typst package" 14 | echo "directory will be used so that the package gets installed for local use." 15 | echo "The name and version are read from 'typst.toml' in the project root." 16 | echo "" 17 | echo "Local package prefix: $DATA_DIR/typst/package/local" 18 | echo "Local preview package prefix: $DATA_DIR/typst/package/preview" 19 | exit 1 20 | fi 21 | 22 | TARGET="$(resolve-target "${1:?Missing target path, @local or @preview}")" 23 | echo "Install dir: $TARGET" 24 | 25 | # ignore rules 26 | readarray -t ignores < <(grep -v '^#' .typstignore | grep '[^[:blank:]]') 27 | 28 | # recursively print all files that are not excluded via .typstignore 29 | function enumerate { 30 | local root="$1" 31 | if [[ -f "$root" ]]; then 32 | echo "$root" 33 | else 34 | local files 35 | readarray -t files < <(find "$root" \ 36 | -mindepth 1 -maxdepth 1 \ 37 | -not -name .git \ 38 | -not -name .typstignore) 39 | # declare -p files >&2 40 | 41 | local f 42 | for f in "${files[@]}"; do 43 | local include 44 | include=1 45 | 46 | local ignore 47 | for ignore in "${ignores[@]}"; do 48 | if [[ "$ignore" =~ ^! ]]; then 49 | ignore="${ignore:1}" 50 | if [[ "$f" == ./$ignore ]]; then 51 | # echo "\"$f\" matched \"!$ignore\"" >&2 52 | include=1 53 | fi 54 | elif [[ "$f" == ./$ignore ]]; then 55 | # echo "\"$f\" matched \"$ignore\"" >&2 56 | include=0 57 | fi 58 | done 59 | if [[ "$include" == 1 ]]; then 60 | enumerate "$f" 61 | fi 62 | done 63 | fi 64 | } 65 | 66 | # List of all files that get packaged 67 | readarray -t files < <(enumerate ".") 68 | # declare -p files >&2 69 | 70 | TMP="$(mktemp -d)" 71 | 72 | for f in "${files[@]}"; do 73 | mkdir -p "$TMP/$(dirname "$f")" 2>/dev/null 74 | cp -r "$ROOT/$f" "$TMP/$f" 75 | done 76 | 77 | TARGET="${TARGET:?}/${PKG_PREFIX:?}/${VERSION:?}" 78 | echo "Packaged to: $TARGET" 79 | if rm -r "${TARGET:?}" 2>/dev/null; then 80 | echo "Overwriting existing version." 81 | fi 82 | mkdir -p "$TARGET" 83 | 84 | # include hidden files by setting dotglob 85 | shopt -s dotglob 86 | mv "$TMP"/* "$TARGET" 87 | -------------------------------------------------------------------------------- /scripts/setup: -------------------------------------------------------------------------------- 1 | # source this script to prepare some common environment variables 2 | 3 | # adapted from https://github.com/johannes-wolf/cetz/blob/35c0868378cea5ad323cc0d9c2f76de8ed9ba5bd/scripts/package 4 | # licensed under Apache License 2.0 5 | 6 | # Local package directories per platform 7 | if [[ "$OSTYPE" == "linux"* ]]; then 8 | DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}" 9 | elif [[ "$OSTYPE" == "darwin"* ]]; then 10 | DATA_DIR="$HOME/Library/Application Support" 11 | else 12 | DATA_DIR="${APPDATA}" 13 | fi 14 | 15 | function read-toml() { 16 | local file="$1" 17 | local key="$2" 18 | # Read a key value pair in the format: = "" 19 | # stripping surrounding quotes. 20 | perl -lne "print \"\$1\" if /^${key}\\s*=\\s*\"(.*)\"/" < "$file" 21 | } 22 | 23 | ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd -P)/.." # macOS has no realpath 24 | PKG_PREFIX="$(read-toml "$ROOT/typst.toml" "name")" 25 | VERSION="$(read-toml "$ROOT/typst.toml" "version")" 26 | 27 | function resolve-target() { 28 | local target="$1" 29 | 30 | if [[ "$target" == "@local" ]]; then 31 | echo "${DATA_DIR}/typst/packages/local" 32 | elif [[ "$target" == "@preview" ]]; then 33 | echo "${DATA_DIR}/typst/packages/preview" 34 | else 35 | echo "$target" 36 | fi 37 | } 38 | -------------------------------------------------------------------------------- /scripts/uninstall: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | 4 | # adapted from https://github.com/johannes-wolf/cetz/blob/35c0868378cea5ad323cc0d9c2f76de8ed9ba5bd/scripts/package 5 | # licensed under Apache License 2.0 6 | 7 | . "$(dirname "${BASH_SOURCE[0]}")/setup" 8 | 9 | if (( $# < 1 )) || [[ "${1:-}" == "help" ]]; then 10 | echo "uninstall TARGET" 11 | echo "" 12 | echo "Removes the package installed into a directory named '/'" 13 | echo "at TARGET. If TARGET is set to @local or @preview, the local Typst package" 14 | echo "directory will be used so that the package gets installed for local use." 15 | echo "The name and version are read from 'typst.toml' in the project root." 16 | echo "" 17 | echo "Local package prefix: $DATA_DIR/typst/package/local" 18 | echo "Local preview package prefix: $DATA_DIR/typst/package/preview" 19 | exit 1 20 | fi 21 | 22 | TARGET="$(resolve-target "${1:?Missing target path, @local or @preview}")" 23 | echo "Install dir: $TARGET" 24 | 25 | TARGET="${TARGET:?}/${PKG_PREFIX:?}/${VERSION:?}" 26 | echo "Package to uninstall: $TARGET" 27 | if [[ ! -e "${TARGET:?}" ]]; then 28 | echo "Package was not found." 29 | elif rm -r "${TARGET:?}" 2>/dev/null; then 30 | echo "Successfully removed." 31 | else 32 | echo "Removal failed." 33 | fi 34 | -------------------------------------------------------------------------------- /src/cmd.typ: -------------------------------------------------------------------------------- 1 | #import "./draw.typ" 2 | 3 | #import "./layout.typ" as _layout 4 | #import "./util.typ" as util: cetz 5 | 6 | 7 | /// Creates a full automaton specification (@type:spec) for a finite automaton. 8 | /// The function accepts either a partial specification and 9 | /// adds the missing keys by parsing the available information 10 | /// or takes a @type:transition-table and parses it into a full specification. 11 | /// 12 | /// ```example 13 | /// #finite.create-automaton(( 14 | /// q0: (q1: 0, q0: (0,1)), 15 | /// q1: (q0: (0,1), q2: "0"), 16 | /// q2: none, 17 | /// )) 18 | /// ``` 19 | /// 20 | /// If any of the keyword arguments are set, they will 21 | /// overwrite the information in #arg[spec]. 22 | /// 23 | /// -> automaton 24 | #let create-automaton( 25 | /// Automaton specification. 26 | /// -> spec | transition-table 27 | spec, 28 | /// The list of state names in the automaton. #typ.v.auto uses 29 | /// the keys of #arg[spec]. 30 | /// -> array 31 | states: auto, 32 | /// The name of the initial state. #typ.v.auto uses the first 33 | /// key in #arg[spec]. 34 | /// -> str 35 | initial: auto, 36 | /// The list of final states. #typ.v.auto uses the last 37 | /// key in #arg[spec]. 38 | /// -> array 39 | final: auto, 40 | /// The list of all inputs, the automaton uses. #typ.v.auto 41 | /// uses the inputs provided in #arg[spec]. 42 | inputs: auto, 43 | ) = { 44 | // TODO: (jneug) add asserts to react to malicious specs 45 | util.assert.any-type(dictionary, spec) 46 | 47 | // TODO: (jneug) check for duplicate names 48 | if "transitions" not in spec { 49 | spec = (transitions: spec) 50 | } 51 | 52 | // Make sure transition inputs are string arrays 53 | for (state, trans) in spec.transitions { 54 | if util.is-empty(trans) { 55 | trans = (:) 56 | } 57 | for (s, inputs) in trans { 58 | if inputs == none { 59 | trans.at(s) = () 60 | } else if type(inputs) != array { 61 | trans.at(s) = (str(inputs),) 62 | } else { 63 | trans.at(s) = inputs.map(str) 64 | } 65 | } 66 | spec.transitions.at(state) = trans 67 | } 68 | 69 | // TODO (jneug) validate given states with transitions 70 | if "states" not in spec { 71 | spec.insert( 72 | "states", 73 | util.def.if-auto( 74 | states, 75 | def: spec 76 | .transitions 77 | .pairs() 78 | .fold( 79 | (), 80 | ( 81 | a, 82 | (s, t), 83 | ) => { 84 | a.push(s) 85 | return a + t.keys() 86 | }, 87 | ) 88 | .dedup(), 89 | ), 90 | ) 91 | } 92 | 93 | 94 | if "initial" not in spec { 95 | spec.insert( 96 | "initial", 97 | util.def.if-auto( 98 | initial, 99 | def: spec.transitions.keys().first(), 100 | ), 101 | ) 102 | } 103 | 104 | // Insert final state 105 | if "final" not in spec { 106 | if util.is-auto(final) { 107 | final = (spec.transitions.keys().last(),) 108 | } else if util.is-none(final) { 109 | final = () 110 | } 111 | spec.insert("final", final) 112 | } 113 | 114 | if "inputs" not in spec { 115 | if util.is-auto(inputs) { 116 | inputs = util.get-inputs(spec.transitions) 117 | } 118 | spec.insert("inputs", inputs) 119 | } else { 120 | spec.inputs = spec.inputs.map(str).sorted() 121 | } 122 | 123 | if util.is-dea(spec.transitions) { 124 | spec.insert("type", "DEA") 125 | } else { 126 | spec.insert("type", "NEA") 127 | } 128 | 129 | return spec + (finite-spec: true) 130 | } 131 | 132 | 133 | /// Draw an automaton from a specification. 134 | /// 135 | /// #arg[spec] is a dictionary with a specification for a 136 | /// finite automaton. See above for a description of the 137 | /// specification dictionaries. 138 | /// 139 | /// The following example defines three states `q0`, `q1` and `q2`. 140 | /// For the input `0`, `q0` transitions to `q1` and for the inputs `0` and `1` to `q2`. 141 | /// `q1` transitions to `q0` for `0` and `1` and to `q2` for `0`. `q2` has no transitions. 142 | /// #codesnippet[```typ 143 | /// #automaton(( 144 | /// q0: (q1:0, q0:(0, 1)), 145 | /// q1: (q0:(0, 1), q2:0), 146 | /// q2: none 147 | /// )) 148 | /// ```] 149 | /// 150 | /// #arg[inital] and #arg[final] can be used to customize the initial and final states. 151 | /// #info-alert[ 152 | /// The #arg[inital] and #arg[final] will be removed in future 153 | /// versions in favor of automaton specs. 154 | /// ] 155 | /// 156 | /// -> content 157 | #let automaton( 158 | /// Automaton specification. 159 | /// -> spec 160 | spec, 161 | /// The name of the initial state. For #typ.v.auto, the first state in #arg[spec] is used. 162 | /// -> string | auto | none 163 | initial: auto, 164 | /// A list of final state names. For #typ.v.auto, the last state in #arg[spec] is used. 165 | /// -> string | auto | none 166 | final: auto, 167 | /// A dictionary with custom labels for states and transitions. 168 | /// #example[``` 169 | /// #finite.automaton( 170 | /// (q0: (q1:none), q1: (q2:none), q2: none), 171 | /// labels: ( 172 | /// q0: [START], q1: $lambda$, q2: [END], 173 | /// q0-q1: $delta$ 174 | /// ) 175 | /// ) 176 | /// ```] 177 | /// -> dictionary 178 | labels: (:), 179 | /// A dictionary with styles for states and transitions. 180 | /// -> dictionary 181 | style: (:), 182 | /// A function #lambda("string", ret:"content") to format state labels. 183 | /// The function will get the states name as a string and should return the final label as #dtype("content"). 184 | /// #example[``` 185 | /// #finite.automaton( 186 | /// (q0: (q1:none), q1: none), 187 | /// state-format: (label) => upper(label) 188 | /// ) 189 | /// ```] 190 | /// -> function 191 | state-format: label => { 192 | let m = label.match(regex(`^(\D+)(\d+)$`.text)) 193 | if m != none { 194 | [#m.captures.at(0)#sub(m.captures.at(1))] 195 | } else { 196 | label 197 | } 198 | }, 199 | /// A function #lambda("array", ret:"content") 200 | /// to generate transition labels from input values. The functions will be 201 | /// called with the array of inputs and should return the final label for 202 | /// the transition. This is only necessary, if no label is specified. 203 | /// #example[``` 204 | /// #finite.automaton( 205 | /// (q0: (q1:(3,0,2,1,5)), q1: none), 206 | /// input-format: (inputs) => inputs.sorted().rev().map(str).join("|") 207 | /// ) 208 | /// ```] 209 | /// -> function 210 | input-format: inputs => inputs.map(str).join(","), 211 | /// Either a dictionary with (`state`: `coordinate`) 212 | /// pairs, or a layout function. See below for more information on layouts. 213 | /// #example[``` 214 | /// #finite.automaton( 215 | /// (q0: (q1:none), q1: none), 216 | /// layout: (q0: (0,0), q1: (rel:(-2,1))) 217 | /// ) 218 | /// ```] 219 | /// -> dictionary | function 220 | layout: _layout.linear, 221 | /// Arguments for #cmd-(module:"cetz")[canvas]. 222 | /// -> any 223 | ..canvas-styles, 224 | ) = { 225 | spec = create-automaton(spec, initial: initial, final: final) 226 | 227 | // use a dict with coordinates as custom layout 228 | if util.is-dict(layout) { 229 | layout = _layout.custom.with(positions: layout) 230 | } 231 | 232 | let (coordinates, anchors) = layout(spec, style: style) 233 | 234 | cetz.canvas( 235 | ..canvas-styles, 236 | { 237 | import cetz.draw: set-style 238 | import draw: state, transition 239 | 240 | set-style(..style) 241 | 242 | // Create states 243 | for name in spec.states { 244 | state( 245 | coordinates.at(name, default: ()), 246 | name, 247 | label: labels.at(name, default: state-format(name)), 248 | initial: (name == spec.initial), 249 | final: (name in spec.final), 250 | anchor: anchors.at(name, default: none), 251 | ..style.at(name, default: (:)), 252 | ) 253 | } 254 | 255 | // Create transitions 256 | for (from, transitions) in spec.transitions { 257 | if util.is-dict(transitions) { 258 | for (to, inputs) in transitions { 259 | let name = from + "-" + to 260 | 261 | // prepare inputs (may be a string or int) 262 | if inputs == none { 263 | inputs = () 264 | } else if not util.is-arr(inputs) { 265 | inputs = str(inputs).split(",") 266 | } 267 | 268 | // prepare label 269 | let label = labels.at( 270 | name, 271 | default: input-format(inputs), 272 | ) 273 | if util.is-dict(label) and "text" not in label { 274 | label.text = input-format(inputs) 275 | } 276 | 277 | // create transition 278 | transition( 279 | from, 280 | to, 281 | inputs: inputs, 282 | label: label, 283 | ..style.at(name, default: (:)), 284 | ) 285 | } 286 | } 287 | } 288 | }, 289 | ) 290 | } 291 | 292 | /// Displays a transition table for an automaton. 293 | /// 294 | /// #arg[spec] is a @type:spec for a 295 | /// finite automaton. 296 | /// 297 | /// The table will show states in rows and inputs in columns: 298 | /// #example(``` 299 | /// #finite.transition-table(( 300 | /// q0: (q1: 0, q0: (1,0)), 301 | /// q1: (q0: 1, q2: (1,0)), 302 | /// q2: (q0: 1, q2: 0), 303 | /// )) 304 | /// ```) 305 | /// 306 | /// #info-alert[The #arg[inital] and #arg[final] arguments will be removed 307 | /// in future versions in favor of automaton specs. 308 | /// ] 309 | /// 310 | /// -> content 311 | #let transition-table( 312 | /// Automaton specification. 313 | /// -> spec 314 | spec, 315 | /// The name of the initial state. For #value(auto), the first state in #arg[states] is used. 316 | /// -> str, auto, none 317 | initial: auto, 318 | /// A list of final state names. For #value(auto), the last state in #arg[states] is used. 319 | /// -> array, auto, none 320 | final: auto, 321 | /// A function to format the value in a table cell. The function takes a column and row index and the cell content 322 | /// as a #typ.t.str and generates content: #lambda("int", "int", "str", ret:"content"). 323 | /// #example[``` 324 | /// #finite.transition-table(( 325 | /// q0: (q1: 0, q0: (1,0)), 326 | /// q1: (q0: 1, q2: (1,0)), 327 | /// q2: (q0: 1, q2: 0), 328 | /// ), 329 | /// format: (col, row, value) => if col == 0 and row == 0 { 330 | /// $delta$ 331 | /// } else if col == 1 { 332 | /// strong(value) 333 | /// } else [#value] 334 | /// ) 335 | /// ```] 336 | /// -> function 337 | format: (col, row, v) => raw(str(v)), 338 | /// Formats a list of states for display in a table cell. The function takes an array of state names and generates a string to be passed to @cmd:transition-table.format: 339 | /// #lambda("array", ret:"str") 340 | /// #example[``` 341 | /// #finite.transition-table(( 342 | /// q0: (q1: 0, q0: (1,0)), 343 | /// q1: (q0: 1, q2: (1,0)), 344 | /// q2: (q0: 1, q2: 0), 345 | /// ), format-list: (states) => "[" + states.join(" | ") + "]") 346 | /// ```] 347 | /// -> function 348 | format-list: states => states.join(", "), 349 | /// Arguments for #typ.table. 350 | /// -> any 351 | ..table-style, 352 | ) = { 353 | spec = create-automaton(spec, initial: initial, final: final) 354 | 355 | let table-cnt = ( 356 | format(0, 0, ""), 357 | ) 358 | for (col, input) in spec.inputs.enumerate() { 359 | table-cnt.push(format(col + 1, 0, input)) 360 | } 361 | 362 | for (row, (state, transitions)) in spec.transitions.pairs().enumerate() { 363 | table-cnt.push(format(0, row + 1, state)) 364 | if util.is-dict(transitions) { 365 | for (i, char) in spec.inputs.enumerate() { 366 | let to = () 367 | for (name, label) in transitions { 368 | if util.is-str(label) { 369 | label = label.split(",") 370 | } 371 | label = util.def.as-arr(label).map(str) 372 | 373 | if char in label { 374 | to.push(name) 375 | } 376 | } 377 | table-cnt.push(if to == () { "" } else { format(i + 1, row + 1, format-list(to)) }) 378 | } 379 | } 380 | } 381 | 382 | table( 383 | columns: 1 + spec.inputs.len(), 384 | fill: (c, r) => if r == 0 or c == 0 { 385 | luma(240) 386 | }, 387 | align: center + horizon, 388 | ..table-style, 389 | ..table-cnt 390 | ) 391 | } 392 | 393 | 394 | /// Creates a deterministic finite automaton from a nondeterministic one by using powerset construction. 395 | /// 396 | /// See #link("https://en.wikipedia.org/wiki/Powerset_construction")[the Wikipedia article on powerset construction] for further 397 | /// details on the algorithm. 398 | /// 399 | /// #arg[spec] is an automaton @type:spec. 400 | /// 401 | /// -> spec 402 | #let powerset( 403 | /// Automaton specification. 404 | /// -> spec 405 | spec, 406 | /// The name of the initial state. For #typ.v.auto, the first state in #arg[states] is used. 407 | /// -> string | auto | none 408 | initial: auto, 409 | /// A list of final state names. For #typ.v.auto, the last state in #arg[states] is used. 410 | /// -> string | auto | none 411 | final: auto, 412 | /// A function to generate the new state names from a list of states. 413 | /// The function takes an array of strings and returns a string: #lambda("array", ret:"string"). 414 | /// -> function 415 | state-format: states => "{" + states.sorted().join(",") + "}", 416 | ) = { 417 | spec = create-automaton(spec, initial: initial, final: final) 418 | 419 | let table = util.transpose-table(spec.transitions) 420 | 421 | let (new-initial, new-final) = ( 422 | state-format((spec.initial,)), 423 | (), 424 | ) 425 | 426 | let powerset = (:) 427 | let queue = ((spec.initial,),) 428 | while queue.len() > 0 { 429 | let cur = queue.remove(0) 430 | let key = state-format(cur) 431 | 432 | if key not in powerset { 433 | powerset.insert(key, (:)) 434 | 435 | if cur.any(s => s in spec.final) { 436 | new-final.push(key) 437 | } 438 | 439 | for inp in spec.inputs { 440 | let trans = () 441 | for s in cur { 442 | let s-trans = table.at(s) 443 | if inp in s-trans { 444 | trans += s-trans.at(inp) 445 | } 446 | } 447 | trans = trans.dedup().sorted() 448 | powerset.at(key).insert(inp, trans) 449 | queue.push(trans) 450 | } 451 | } 452 | } 453 | 454 | for (s, t) in powerset { 455 | for (i, states) in t { 456 | powerset.at(s).at(i) = state-format(states) 457 | } 458 | } 459 | 460 | return create-automaton( 461 | util.transpose-table(powerset), 462 | initial: new-initial, 463 | final: new-final, 464 | inputs: spec.inputs, 465 | ) 466 | } 467 | 468 | /// Adds a trap state to a partial DFA and completes it. 469 | /// 470 | /// Deterministic automata need to specify a transition for every 471 | /// possible input. If those inputs don't transition to another 472 | /// state, a trap-state is introduced that is not final 473 | /// and can't be left by any input. To simplify 474 | /// transition diagrams, these trap-states are usually 475 | /// not drawn. This function adds a trap-state to such a 476 | /// partial automaton and thus completes it. 477 | /// 478 | /// #example[``` 479 | /// #finite.transition-table(finite.add-trap(( 480 | /// q0: (q1: 0), 481 | /// q1: (q0: 1) 482 | /// ))) 483 | /// ```] 484 | /// -> spec 485 | #let add-trap( 486 | /// Automaton specification. 487 | /// -> spec 488 | spec, 489 | /// Name for the new trap-state. 490 | /// -> str 491 | trap-name: "TRAP", 492 | ) = { 493 | spec = create-automaton(spec) 494 | 495 | let table = util.transpose-table(spec.transitions) 496 | 497 | let trap-added = false 498 | for (s, values) in table { 499 | for inp in spec.inputs { 500 | if inp not in values { 501 | values.insert(inp, (trap-name,)) 502 | trap-added = true 503 | } 504 | } 505 | table.at(s) = values 506 | } 507 | 508 | if trap-added { 509 | table.insert( 510 | trap-name, 511 | spec.inputs.fold( 512 | (:), 513 | (d, i) => { 514 | d.insert(i, (trap-name,)) 515 | return d 516 | }, 517 | ), 518 | ) 519 | spec.states.push(trap-name) 520 | } 521 | 522 | spec.at("transitions") = util.transpose-table(table) 523 | return spec 524 | } 525 | 526 | 527 | /// Tests if #arg[word] is accepted by a given automaton. 528 | /// 529 | /// The result if either #value(false) or an array of tuples 530 | /// with a state name and the input used to transition to the 531 | /// next state. The array is a possible path to an accepting 532 | /// final state. The last tuple always has #value(none) as 533 | /// an input. 534 | /// #example[``` 535 | /// #let aut = ( 536 | /// q0: (q1: 0), 537 | /// q1: (q0: 1) 538 | /// ) 539 | /// #finite.accepts(aut, "01010") 540 | /// 541 | /// #finite.accepts(aut, "0101") 542 | /// ```] 543 | /// 544 | /// -> content 545 | #let accepts( 546 | /// Automaton specification. 547 | /// -> spec 548 | spec, 549 | /// A word to test. 550 | /// -> str 551 | word, 552 | /// A function to format the result. 553 | /// -> function 554 | format: (spec, states) => states 555 | .map(((s, i)) => if i != none [ 556 | #s #box[#sym.arrow.r#place(top + center, dy: -88%)[#text(.88em, raw(i))]] 557 | ] else [#s]) 558 | .join(), 559 | ) = { 560 | spec = create-automaton(spec) 561 | 562 | let (transitions, initial, final) = ( 563 | spec.at("transitions", default: (:)), 564 | spec.at("initial", default: none), 565 | spec.at("final", default: ()), 566 | ) 567 | transitions = util.transpose-table(transitions) 568 | 569 | util.assert.that(transitions != (:)) 570 | util.assert.that(initial != none) 571 | util.assert.that(final != ()) 572 | 573 | let next-symbol(word, inputs) = { 574 | for sym in inputs { 575 | if word.starts-with(sym) { 576 | return (word.slice(sym.len()), sym) 577 | } 578 | } 579 | return (word, none) 580 | } 581 | let traverse(word, state) = { 582 | if word.len() > 0 { 583 | let (word, symbol) = next-symbol(word, spec.inputs) 584 | if state in transitions { 585 | if symbol != none and symbol in transitions.at(state) { 586 | for next-state in transitions.at(state).at(symbol) { 587 | let states = traverse(word, next-state) 588 | if states != false { 589 | return ((state, symbol),) + states 590 | } 591 | } 592 | } 593 | } 594 | return false 595 | } 596 | // Word accepted? 597 | if state in final { 598 | return ((state, none),) 599 | } else { 600 | return false 601 | } 602 | } 603 | 604 | let result = traverse(word, initial) 605 | if result == false { 606 | return false 607 | } else { 608 | return format(spec, result) 609 | } 610 | } 611 | -------------------------------------------------------------------------------- /src/draw.typ: -------------------------------------------------------------------------------- 1 | 2 | // imports cetz and t4t 3 | #import "./util.typ" 4 | #import util: cetz 5 | 6 | /// Draw a state at the given #arg[position]. 7 | /// 8 | /// #example[``` 9 | /// #cetz.canvas({ 10 | /// import finite.draw: state 11 | /// state((0,0), "q1", label:"S1", initial:true) 12 | /// state("q1.east", "q2", label:"S2", final:true, anchor:"west") 13 | /// }) 14 | /// ```] 15 | /// 16 | /// -> array 17 | #let state( 18 | /// Position of the states center. 19 | /// -> coordinate 20 | position, 21 | /// Name for the state. 22 | /// -> str 23 | name, 24 | /// Label for the state. If set to #value(auto), the #arg[name] is used. 25 | /// -> str | content | auto | none 26 | label: auto, 27 | /// Whether this is an initial state. This can be either 28 | /// - #value(true), 29 | /// - an #dtype("alignment") to specify an anchor for the inital marking, 30 | /// - a #dtype("string") to specify text for the initial marking, 31 | /// - an #dtype("dictionary") with the keys `anchor` and `label` to specifiy both an anchor and a text label for the marking. Additionally, the keys `stroke` and `scale` can be used to style the marking. 32 | /// -> boolean | alignment | dictionary 33 | initial: false, 34 | /// Whether this is a final state. 35 | /// -> boolean 36 | final: false, 37 | /// Anchor to use for drawing. 38 | /// -> str 39 | anchor: none, 40 | /// Styling options. 41 | /// -> any 42 | ..style, 43 | ) = { 44 | // No extra positional arguments from the style sink 45 | util.assert.no-pos(style) 46 | 47 | // Create element function 48 | cetz.draw.group( 49 | name: name, 50 | anchor: anchor, 51 | ctx => { 52 | let style = style.named() 53 | 54 | // Prepare label 55 | if not util.is-dict(label) { 56 | style.insert("label", (text: label)) 57 | } else { 58 | style.insert("label", label) 59 | } 60 | if "text" not in style.label or util.is-auto(style.label.text) { 61 | style.label.insert("text", name) 62 | } 63 | // Prepare initial marking 64 | style.initial = (:) 65 | if util.is-align(initial) { 66 | style.initial.insert("anchor", initial) 67 | } else if util.is-str(initial) { 68 | style.initial.insert("label", (text: initial)) 69 | } else if util.is-dict(initial) { 70 | style.initial = initial 71 | if "label" in initial and util.is-str(initial.label) { 72 | style.initial.label = (text: initial.label) 73 | } 74 | } 75 | 76 | // Prepare padding 77 | if "padding" not in style.label or util.is-auto(style.label.padding) { 78 | style.label.insert("padding", ctx.style.padding) 79 | } 80 | 81 | let style = cetz.styles.resolve( 82 | ctx.style, 83 | merge: style, 84 | base: util.default-style.state, 85 | root: "state", 86 | ) 87 | 88 | // resolve coordinates 89 | let (_, pos) = cetz.coordinate.resolve(ctx, position) 90 | 91 | cetz.draw.circle(pos, name: "state", ..style) 92 | if final { 93 | cetz.draw.circle(pos, stroke: style.stroke, radius: style.radius * style.extrude) 94 | } 95 | if initial != false { 96 | let color = if style.initial.stroke == auto { 97 | stroke(style.stroke).paint 98 | } else { 99 | stroke(style.initial.stroke).paint 100 | } 101 | 102 | let initial-anchor = util.to-anchor(style.initial.anchor) 103 | let align-vec = util.align-to-vec(style.initial.anchor) 104 | let initial-start = ( 105 | rel: cetz.vector.scale( 106 | align-vec, 107 | style.initial.scale, 108 | ), 109 | to: "state." + initial-anchor, 110 | ) 111 | 112 | cetz.draw.line( 113 | name: "initial", 114 | initial-start, 115 | "state." + initial-anchor, 116 | mark: (end: "straight"), 117 | stroke: style.initial.stroke, 118 | ) 119 | if style.initial.label != none { 120 | cetz.draw.content( 121 | name: "initial-label", 122 | ( 123 | rel: util.vector-set-len( 124 | util.vector-rotate( 125 | initial-start.rel, 126 | if util.abs-angle-between(align-vec, (0, 0), -90deg, 90deg) { 127 | -90deg 128 | } else { 129 | 90deg 130 | }, 131 | ), 132 | style.initial.label.dist, 133 | ), 134 | to: initial-start, 135 | ), 136 | anchor: "south", 137 | angle: util.label-angle(align-vec, style.initial.anchor), 138 | text(style.initial.label.size, color, style.initial.label.text), 139 | ) 140 | } 141 | } 142 | if label not in (none, (:)) { 143 | if style.label.fill in (auto, none) { 144 | style.label.fill = stroke(style.stroke).paint 145 | } 146 | if style.label.fill == auto { 147 | style.label.fill = black 148 | } 149 | 150 | cetz.draw.content( 151 | "state.center", 152 | name: "label", 153 | anchor: "center", 154 | padding: style.label.padding, 155 | text( 156 | size: style.label.size, 157 | fill: style.label.fill, 158 | style.label.text, 159 | ), 160 | ) 161 | } 162 | cetz.draw.copy-anchors("state") 163 | cetz.draw.anchor("default", "state.center") 164 | }, 165 | ) 166 | // Update prev coordinate 167 | cetz.draw.set-ctx(ctx => { 168 | let (ctx, _) = cetz.coordinate.resolve(ctx, position) 169 | ctx 170 | }) 171 | } 172 | 173 | /// Draw a transition between two states. 174 | /// 175 | /// The two states #arg[from] and #arg[to] have to be existing names of states. 176 | /// #example[``` 177 | /// #cetz.canvas({ 178 | /// import finite.draw: state, transition 179 | /// state((0,0), "q1") 180 | /// state((2,0), "q2") 181 | /// transition("q1", "q2", label:"a") 182 | /// transition("q2", "q1", label:"b") 183 | /// }) 184 | /// ```] 185 | /// 186 | /// -> array 187 | #let transition( 188 | /// Name of the starting state. 189 | /// -> str 190 | from, 191 | /// Name of the ending state. 192 | /// -> str 193 | to, 194 | /// A list of input symbols for the transition. 195 | /// If provided as a #typ.t.str, it is split at commas to get the list of 196 | /// input symbols. 197 | /// -> str | array | none 198 | inputs: none, 199 | /// A label for the transition. For #typ.v.auto 200 | /// the #arg[input] symbols are joined with commas (`,`). Can be a #typ.t.dictionary with 201 | /// a `text` key and additional styling keys. 202 | /// -> str | content | auto | dictionary 203 | label: auto, 204 | /// Anchor for loops. Has no effect on normal transitions. 205 | /// -> alignment 206 | anchor: top, 207 | ///Styling options. 208 | /// -> any 209 | ..style, 210 | ) = { 211 | // No extra positional arguments from the style sink 212 | util.assert.no-pos(style) 213 | 214 | // TODO: (jneug) allow labels with math or content 215 | 216 | // Name of two states required 217 | util.assert.all-of-type(str, from, to) 218 | let name = from.split(".").last() + "-" + to.split(".").last() 219 | 220 | cetz.draw.group( 221 | name: name, 222 | ctx => { 223 | let style = style.named() 224 | 225 | // Prepare inputs 226 | let inputs = if util.not-empty(inputs) { 227 | if util.is-str(inputs) { 228 | inputs.split(",") 229 | } else if not util.is-arr(inputs) { 230 | (inputs,) 231 | } else { 232 | inputs 233 | } 234 | } else { 235 | none 236 | } 237 | // Prepare label 238 | if util.is-auto(label) { 239 | if util.not-none(inputs) { 240 | style.label = (text: inputs.map(str).join(",")) 241 | } else { 242 | style.label = (text: none) 243 | } 244 | } else if not util.is-dict(label) { 245 | style.label = (text: label) 246 | } else { 247 | style.label = label 248 | } 249 | if not "text" in style.label and util.not-none(inputs) { 250 | // TODO: (jneug) add input-label-format function 251 | style.label.insert("text", inputs.map(str).join(",")) 252 | } 253 | 254 | let style = cetz.styles.resolve( 255 | ctx.style, 256 | merge: style, 257 | base: util.default-style.transition, 258 | root: "transition", 259 | ) 260 | // resolve loop styles on top 261 | if from == to { 262 | style = cetz.styles.resolve( 263 | ctx.style, 264 | merge: ctx.style.at("loop", default: (:)), 265 | base: style, 266 | root: "transition", 267 | ) 268 | } 269 | 270 | let (_, start, f-center, f-right, end, t-center, t-right) = cetz.coordinate.resolve( 271 | ctx, 272 | from + ".state", 273 | from + ".state.center", 274 | from + ".state.east", 275 | to + ".state", 276 | to + ".state.center", 277 | to + ".state.east", 278 | ) 279 | let (start-rad, end-rad) = ( 280 | f-right.at(0) - f-center.at(0), 281 | t-right.at(0) - t-center.at(0), 282 | ) 283 | let (start, end, ctrl1, ctrl2) = util.transition-pts( 284 | start, 285 | end, 286 | start-rad, 287 | end-rad, 288 | curve: style.curve * .75, 289 | anchor: anchor, 290 | ) 291 | cetz.draw.bezier( 292 | name: "arrow", 293 | start, 294 | end, 295 | ctrl1, 296 | ctrl2, 297 | mark: ( 298 | end: ">", 299 | stroke: style.stroke, 300 | fill: stroke(style.stroke).paint, 301 | ), 302 | ..style, 303 | ) 304 | cetz.draw.copy-anchors("arrow") 305 | 306 | if not util.is-empty(style.label.text) { 307 | style.label.size = cetz.util.resolve-number(ctx, style.label.size) * ctx.length 308 | 309 | if style.label.fill in (auto, none) { 310 | style.label.fill = stroke(style.stroke).paint 311 | } 312 | if style.label.fill == auto { 313 | style.label.fill = black 314 | } 315 | 316 | let label-pt = util.label-pt(start, end, ctrl1, ctrl2, style, loop: start == end) 317 | cetz.draw.content( 318 | name: "label", 319 | label-pt, 320 | angle: if util.is-angle(style.label.angle) { 321 | style.label.angle 322 | } else if start == end { 323 | 0deg 324 | } else { 325 | let d = util.cubic-derivative(start, end, ctrl1, ctrl2, style.label.pos) 326 | let a = cetz.vector.angle2((0, 0), d) 327 | if a < 0deg { a += 360deg } 328 | if a > 90deg and a < 270deg { 329 | a = cetz.vector.angle2((0, 0), cetz.vector.scale(d, -1)) 330 | } 331 | a 332 | }, 333 | { 334 | let label-style = (size: style.label.size) 335 | if style.label.fill != none { 336 | label-style.insert("fill", style.label.fill) 337 | } 338 | set text( 339 | size: style.label.size, 340 | fill: style.label.fill, 341 | ) 342 | style.label.text 343 | }, 344 | ) 345 | } 346 | }, 347 | ) 348 | // Update prev coordinate 349 | cetz.draw.set-ctx(ctx => { 350 | let (ctx, _) = cetz.coordinate.resolve(ctx, to) 351 | ctx 352 | }) 353 | } 354 | 355 | 356 | /// Create a transition loop on a state. 357 | /// #example[``` 358 | /// #cetz.canvas({ 359 | /// import finite.draw: state, loop 360 | /// state((0,0), "q1") 361 | /// loop("q1", label:"a") 362 | /// loop("q1", anchor: bottom+right, label:"b") 363 | /// }) 364 | /// ```] 365 | /// 366 | /// This is a shortcut for @cmd:transition that takes only one 367 | /// state name instead of two. 368 | #let loop( 369 | /// Name of the state to draw the loop on. 370 | /// -> str 371 | state, 372 | /// A list of input symbols for the loop. 373 | /// If provided as a #typ.t.str, it is split at commas to get the list of 374 | /// input symbols. 375 | /// -> str | array | none 376 | inputs: none, 377 | /// A label for the loop. For #typ.v.auto 378 | /// the #arg[input] symbols are joined with commas (`,`). Can be a #typ.t.dictionary with 379 | /// a `text` key and additional styling keys. 380 | /// -> str | content | auto | dictionary 381 | label: auto, 382 | /// Anchor for the loop. 383 | /// -> alignment 384 | anchor: top, 385 | ///Styling options. 386 | /// -> any 387 | ..style, 388 | ) = transition( 389 | state, 390 | state, 391 | inputs: inputs, 392 | label: label, 393 | anchor: anchor, 394 | ..style, 395 | ) 396 | 397 | 398 | /// Draws multiple transitions from a transition table with a common style. 399 | /// #example[``` 400 | /// #cetz.canvas({ 401 | /// import finite.draw: state, transitions 402 | /// state((0,0), "q1") 403 | /// state((2,0), "q2") 404 | /// transitions( 405 | /// ( 406 | /// q1: (q2: (0, 1)), 407 | /// q2: (q1: 0, q2: 1) 408 | /// ), 409 | /// transition: (stroke: green) 410 | /// ) 411 | /// }) 412 | /// ```] 413 | /// 414 | /// -> content 415 | #let transitions( 416 | /// A transition table given as a #typ.t.dictionary of dictionaries. 417 | /// -> transition-table 418 | states, 419 | /// Styling options. 420 | /// -> any 421 | ..style, 422 | ) = { 423 | util.assert.no-pos(style) 424 | style = style.named() 425 | 426 | for (from, transitions) in states { 427 | for (to, label) in transitions { 428 | let name = from + "-" + to 429 | 430 | transition( 431 | from, 432 | to, 433 | inputs: label, 434 | // label: label, 435 | ..style.at("transition", default: (:)), 436 | ..style.at(name, default: (:)), 437 | ) 438 | } 439 | } 440 | } 441 | -------------------------------------------------------------------------------- /src/finite.typ: -------------------------------------------------------------------------------- 1 | #import "util.typ": cetz 2 | 3 | #import "./draw.typ" 4 | #import "./layout.typ" 5 | 6 | #import "./cmd.typ": create-automaton, automaton, transition-table, powerset, add-trap, accepts 7 | 8 | #import "./flaci.typ" 9 | -------------------------------------------------------------------------------- /src/flaci.typ: -------------------------------------------------------------------------------- 1 | #import "./cmd.typ" 2 | #import "./util.typ": get, vec-to-align, align-to-vec 3 | 4 | #let _bom = bytes((239, 187, 191)) 5 | // Parse a string with BOM into a JSON dictionary. 6 | #let _load-json(data) = { 7 | data = bytes(data) 8 | if data.slice(0, 3) == _bom { 9 | data = data.slice(3) 10 | } 11 | return json(data) 12 | } 13 | 14 | /// Loads #arg[data] into an automaton @type:spec. 15 | /// #arg[data] needs to be a string and not a JSON dictionary. 16 | /// -> spec 17 | #let load( 18 | /// FLACI data read as a string via #typ.read. 19 | /// -> str 20 | data, 21 | ) = { 22 | data = _load-json(data) 23 | assert( 24 | data.at("type", default: "ERROR") in ("DEA", "NEA"), 25 | message: "Currently only FLACI automata of type DEA or NEA are supported. Got: " + data.at("type", default: "none"), 26 | ) 27 | let automaton = data.at("automaton", default: (:)) 28 | 29 | // Scaling factor for FLACI coordinates to CeTZ conversion 30 | let scaling = .02 31 | 32 | // Some FLACI default styles 33 | let style = ( 34 | transition: ( 35 | curve: 0, 36 | label: (angle: 0deg), 37 | ), 38 | ) 39 | 40 | // Variable declarations 41 | let transitions = (:) 42 | let states = () 43 | let final = () 44 | let initial = none 45 | let inputs = automaton.at("Alphabet", default: ()) 46 | let id-map = (:) 47 | let layout = (:) 48 | 49 | // Parse states 50 | for state in automaton.at("States", default: ()) { 51 | let name = state.Name 52 | 53 | // Prepare state 54 | states.push(name) 55 | transitions.insert(name, (:)) 56 | id-map.insert(str(state.ID), name) 57 | 58 | // Final and initial states 59 | if state.Final { 60 | final.push(name) 61 | } 62 | if state.Start { 63 | initial = name 64 | } 65 | 66 | // Layout 67 | layout.insert( 68 | name, 69 | (state.x * scaling, state.y * -scaling), 70 | ) 71 | 72 | // Style 73 | style.insert( 74 | name, 75 | ( 76 | radius: state.Radius * scaling, 77 | ), 78 | ) 79 | } 80 | 81 | // Second pass to parse transitions 82 | for state in automaton.at("States", default: ()) { 83 | let name = state.Name 84 | let id = state.ID 85 | 86 | let state-transitions = (:) 87 | for transition in state.Transitions { 88 | if transition.Source == id { 89 | state-transitions.insert(id-map.at(str(transition.Target)), transition.Labels) 90 | } else { 91 | state-transitions.insert(id-map.at(str(transition.Source)), transition.Labels) 92 | } 93 | 94 | if transition.Source == transition.Target { 95 | let vec = (transition.x, -transition.y) 96 | let align = vec-to-align(vec) 97 | style.insert(name + "-" + name, (anchor: align)) 98 | } 99 | } 100 | transitions.insert(state.Name, state-transitions) 101 | } 102 | 103 | // Do some cleanup of style (eg curve for double transitions) 104 | for (state, state-transitions) in transitions { 105 | for target-state in state-transitions.keys() { 106 | if state in transitions.at(target-state) { 107 | let name = state + "-" + target-state 108 | let sty = style.at(name, default: (:)) 109 | style.insert(name, get.dict-merge((curve: .6), sty)) 110 | } 111 | } 112 | } 113 | 114 | 115 | return ( 116 | cmd.create-automaton(transitions, states: states, inputs: inputs, initial: initial, final: final), 117 | layout, 118 | style, 119 | ) 120 | } 121 | 122 | 123 | /// Show a FLACI file as an @cmd:automaton. 124 | /// 125 | /// #warning-alert[ 126 | /// Read the FLACI json-file with the #typ.read function, not 127 | /// the #typ.json function. FLACI exports automatons with a wrong encoding 128 | /// that prevents Typst from properly loading the file as JSON. 129 | /// ] 130 | /// 131 | /// #info-alert[ 132 | /// Currently only DEA and NEA automata are supported. 133 | /// ] 134 | /// -> content 135 | #let automaton( 136 | /// FLACI data read as a string via #typ.read. 137 | /// -> str 138 | data, 139 | /// Custom layout for the automaton. Will overwrite state positions from #arg[data]. 140 | /// -> function 141 | layout: auto, 142 | /// Custom state positions to merge with the ones found in #arg[data]. 143 | /// This allows the placement of some states while the 144 | /// rest keeps their positions. 145 | /// -> dictionary 146 | merge-layout: true, 147 | /// Custom styles to overwrite the defaults. 148 | /// -> dictionary 149 | style: auto, 150 | /// Custom styles to merge with the styles from #arg[data]. 151 | merge-style: true, 152 | /// Further arguments for @cmd:automaton. 153 | /// -> any 154 | ..args, 155 | ) = { 156 | let (spec, lay, sty) = load(data) 157 | 158 | if layout == auto { 159 | layout = lay 160 | } else if merge-layout and type(layout) == dictionary { 161 | layout = get.dict-merge(lay, layout) 162 | } 163 | if style == auto { 164 | style = sty 165 | } else if merge-style and type(style) == dictionary { 166 | style = get.dict-merge(sty, style) 167 | } 168 | 169 | cmd.automaton(spec, layout: layout, style: style, ..args) 170 | } 171 | -------------------------------------------------------------------------------- /src/layout.typ: -------------------------------------------------------------------------------- 1 | #import "util.typ" 2 | #import util: default-style, cetz, test, assert-dict, assert-spec 3 | 4 | 5 | /// Helper function to create a layout dictionary by providing 6 | /// #arg[positions] and/or #arg[anchors]. 7 | /// -> array 8 | #let create-layout(positions: (:), anchors: (:)) = { 9 | (positions, anchors) 10 | } 11 | 12 | 13 | /// Create a custom layout from a #typ.t.dictionary with 14 | /// state #dtype("coordinate")s. 15 | /// 16 | /// The result may specify a `rest` key that is used as a default coordinate. This is useful 17 | /// sense in combination with a relative coordinate like `(rel:(2,0))`. 18 | /// 19 | /// #example(breakable:true)[``` 20 | /// #let aut = range(6).fold((:), (d, s) => {d.insert("q"+str(s), none); d}) 21 | /// #finite.automaton( 22 | /// aut, 23 | /// initial: none, final: none, 24 | /// layout:finite.layout.custom.with(positions: ( 25 | /// q0: (0,0), q1: (0,2), rest:(rel: (1.5,-.5)) 26 | /// )) 27 | /// ) 28 | /// ```] 29 | /// 30 | /// -> array 31 | #let custom( 32 | /// Automaton specification. 33 | /// -> spec 34 | spec, 35 | /// A dictionary with #dtype("coordinate")s for each state. 36 | /// 37 | /// The dictionary contains each states name as a 38 | /// key and the new coordinate as a value. 39 | /// 40 | /// -> dictionary 41 | positions: (:), 42 | /// Position of the anchor point. 43 | /// -> coordinate 44 | position: (0, 0), 45 | /// Styling options. 46 | /// -> dictionary 47 | style: (:), 48 | ) = { 49 | assert-spec(spec) 50 | assert-dict(positions) 51 | 52 | let rest = positions.at("rest", default: (rel: (4, 0))) 53 | 54 | for state in spec.states { 55 | positions.insert( 56 | state, 57 | ( 58 | rel: position, 59 | to: positions.at(state, default: util.call-or-get(rest, state)), 60 | ), 61 | ) 62 | } 63 | 64 | // return coordinates 65 | return create-layout(positions: positions) 66 | } 67 | 68 | 69 | /// Arrange states in a line. 70 | /// 71 | /// The direction of the line can be set via #arg[dir] either to an #typ.t.alignment 72 | /// or a direction vector with a x and y shift. Note that the length of the vector is set to #arg[spacing] and only the direction is used. 73 | /// 74 | /// #example(breakable:true)[``` 75 | /// #let aut = range(6).fold((:), (d, s) => {d.insert("q"+str(s), none); d}) 76 | /// #finite.automaton( 77 | /// aut, 78 | /// initial: none, final: none, 79 | /// layout:finite.layout.linear.with(dir: right) 80 | /// ) 81 | /// #finite.automaton( 82 | /// aut, 83 | /// initial: none, final: none, 84 | /// layout:finite.layout.linear.with(spacing: .5, dir:(2,-1)) 85 | /// ) 86 | /// ```] 87 | /// 88 | /// -> array 89 | #let linear( 90 | /// Automaton specification. 91 | /// -> spec 92 | spec, 93 | /// Direction of the line. 94 | /// -> vector | alignment | 2d alignment 95 | dir: right, 96 | /// Spacing between states on the line. 97 | /// -> float 98 | spacing: default-style.state.radius * 2, 99 | /// Position of the anchor point. 100 | /// -> coordinate 101 | position: (0, 0), 102 | /// Styling options. 103 | /// -> dictionary 104 | style: (:), 105 | ) = { 106 | assert-spec(spec) 107 | 108 | let dir = dir 109 | if test.is-type(alignment, dir) { 110 | dir = util.align-to-vec(dir) 111 | } 112 | dir = cetz.vector.norm(dir) 113 | let dir-angle = cetz.vector.angle2((0, 0), dir) 114 | let spacing-vec = cetz.vector.scale(dir, spacing) 115 | 116 | let positions = (:) 117 | let anchors = (:) 118 | 119 | let prev-name = none 120 | for name in spec.states { 121 | positions.insert( 122 | name, 123 | if prev-name == none { 124 | position 125 | } else { 126 | ( 127 | rel: spacing-vec, 128 | to: prev-name + ".state." + repr(dir-angle), 129 | ) 130 | }, 131 | ) 132 | anchors.insert( 133 | name, 134 | if prev-name == none { 135 | "center" 136 | } else { 137 | repr(dir-angle + 180deg) 138 | }, 139 | ) 140 | prev-name = name 141 | } 142 | 143 | // return coordinates 144 | return create-layout(positions: positions, anchors: anchors) 145 | } 146 | 147 | 148 | /// Arrange states in a circle. 149 | /// 150 | /// #example(breakable:true)[``` 151 | /// #let aut = range(6).fold((:), (d, s) => {d.insert("q"+str(s), none); d}) 152 | /// #grid(columns: 2, gutter: 2em, 153 | /// finite.automaton( 154 | /// aut, 155 | /// initial: none, final: none, 156 | /// layout:finite.layout.circular, 157 | /// style: (q0: (fill: yellow.lighten(60%))) 158 | /// ), 159 | /// finite.automaton( 160 | /// aut, 161 | /// initial: none, final: none, 162 | /// layout:finite.layout.circular.with(offset:45deg), 163 | /// style: (q0: (fill: yellow.lighten(60%))) 164 | /// ), 165 | /// finite.automaton( 166 | /// aut, 167 | /// initial: none, final: none, 168 | /// layout:finite.layout.circular.with(dir:left), 169 | /// style: (q0: (fill: yellow.lighten(60%))) 170 | /// ), 171 | /// finite.automaton( 172 | /// aut, 173 | /// initial: none, final: none, 174 | /// layout:finite.layout.circular.with(dir:left, offset:45deg), 175 | /// style: (q0: (fill: yellow.lighten(60%))) 176 | /// ) 177 | /// ) 178 | /// ```] 179 | #let circular( 180 | /// Automaton specification. 181 | /// -> spec 182 | spec, 183 | /// Direction of the circle. Either #value(left) or #value(right). 184 | /// -> alignment 185 | dir: right, 186 | /// Spacing between states on the line. 187 | /// -> float 188 | spacing: default-style.state.radius * 2, 189 | /// Either a fixed radius or #typ.v.auto to calculate a suitable radius. 190 | /// -> float | auto 191 | radius: auto, 192 | /// An offset angle to place the first state at. 193 | /// -> angle 194 | offset: 0deg, 195 | /// Position of the anchor point. 196 | /// -> coordinate 197 | position: (0, 0), 198 | /// Styling options. 199 | /// -> dictionary 200 | style: (:), 201 | ) = { 202 | // TODO: (jneug) fix positioning 203 | let radii = util.get-radii(spec, style: style) 204 | let len = radii.values().fold(0, (s, r) => s + 2 * r + spacing) 205 | 206 | let radius = radius 207 | if util.is-auto(radius) { 208 | radius = len / (2 * calc.pi) 209 | } else { 210 | len = 2 * radius * calc.pi 211 | } 212 | 213 | let positions = (:) 214 | let anchors = (:) 215 | let at = 0.0 216 | for name in spec.states { 217 | let state-radius = radii.at(name) 218 | let ang = 0deg 219 | let ang = ( 220 | offset 221 | + util.math.map( 222 | 0.0, 223 | len, 224 | 0deg, 225 | 360deg, 226 | at + state-radius, 227 | ) 228 | ) 229 | 230 | let pos = ( 231 | rel: ( 232 | -radius * calc.cos(ang), 233 | if dir == right { 234 | radius 235 | } else { 236 | -radius 237 | } 238 | * calc.sin(ang), 239 | ), 240 | to: position, 241 | ) 242 | 243 | positions.insert(name, pos) 244 | 245 | // anchors.insert(name, "state." + repr(-ang)) 246 | 247 | at += 2 * state-radius + spacing 248 | } 249 | return create-layout(positions: positions, anchors: anchors) 250 | } 251 | 252 | 253 | /// Arrange states in rows and columns. 254 | /// 255 | /// #example[``` 256 | /// #let aut = range(6).fold((:), (d, s) => {d.insert("q"+str(s), none); d}) 257 | /// #finite.automaton( 258 | /// aut, 259 | /// initial: none, final: none, 260 | /// layout:finite.layout.grid.with(columns:3) 261 | /// ) 262 | /// ```] 263 | /// 264 | /// -> array 265 | #let grid( 266 | /// Automaton specification. 267 | /// -> spec 268 | spec, 269 | /// Number of columns per row. 270 | /// -> int 271 | columns: 4, 272 | /// Spacing between states on the grid. 273 | /// -> float 274 | spacing: default-style.state.radius * 2, 275 | /// Position of the anchor point. 276 | /// -> coordinate 277 | position: (0, 0), 278 | /// Styling options. 279 | /// -> dictionary 280 | style: (:), 281 | ) = { 282 | let spacing = if not util.is-arr(spacing) { 283 | (x: spacing, y: spacing) 284 | } else { 285 | (x: spacing.first(), y: spacing.last()) 286 | } 287 | 288 | let radii = util.get-radii(spec, style: style) 289 | let max-radius = calc.max(..radii.values()) 290 | 291 | let positions = (:) 292 | for (i, name) in spec.states.enumerate() { 293 | let (row, col) = ( 294 | calc.quo(i, columns), 295 | calc.rem(i, columns), 296 | ) 297 | positions.insert( 298 | name, 299 | ( 300 | rel: ( 301 | col * (2 * max-radius + spacing.x), 302 | row * (2 * max-radius + spacing.y), 303 | ), 304 | to: position, 305 | ), 306 | ) 307 | } 308 | return create-layout(positions: positions) 309 | } 310 | 311 | 312 | /// Arrange states in a grid, but alternate the direction in every even and odd row. 313 | /// 314 | /// #example(breakable:true)[``` 315 | /// #let aut = range(6).fold((:), (d, s) => {d.insert("q"+str(s), none); d}) 316 | /// #finite.automaton( 317 | /// aut, 318 | /// initial: none, final: none, 319 | /// layout:finite.layout.snake.with(columns:3) 320 | /// ) 321 | /// ```] 322 | /// 323 | /// -> array 324 | #let snake( 325 | /// Automaton specification. 326 | /// -> spec 327 | spec, 328 | /// Number of columns per row. 329 | /// -> int 330 | columns: 4, 331 | /// Spacing between states on the line. 332 | /// -> float 333 | spacing: default-style.state.radius * 2, 334 | /// Position of the anchor point. 335 | /// -> coordinate 336 | position: (0, 0), 337 | /// Styling options. 338 | /// -> dictionary 339 | style: (:), 340 | ) = { 341 | let spacing = if not util.is-arr(spacing) { 342 | (x: spacing, y: spacing) 343 | } else { 344 | (x: spacing.first(), y: spacing.last()) 345 | } 346 | 347 | let radii = util.get-radii(spec, style: style) 348 | let max-radius = calc.max(..radii.values()) 349 | 350 | let positions = (:) 351 | for (i, name) in spec.states.enumerate() { 352 | let (row, col) = ( 353 | calc.quo(i, columns), 354 | calc.rem(i, columns), 355 | ) 356 | positions.insert( 357 | name, 358 | if calc.odd(row) { 359 | ( 360 | rel: ( 361 | (columns - col - 1) * (2 * max-radius + spacing.x), 362 | row * (2 * max-radius + spacing.y), 363 | ), 364 | to: position, 365 | ) 366 | } else { 367 | ( 368 | rel: ( 369 | col * (2 * max-radius + spacing.x), 370 | row * (2 * max-radius + spacing.y), 371 | ), 372 | to: position, 373 | ) 374 | }, 375 | ) 376 | } 377 | return create-layout(positions: positions) 378 | } 379 | 380 | 381 | /// Creates a group layout that collects states into groups that are 382 | /// positioned by specific sub-layouts. 383 | /// 384 | /// #example(breakable:true)[``` 385 | /// #let aut = range(6).fold((:), (d, s) => {d.insert("q"+str(s), none); d}) 386 | /// #finite.automaton( 387 | /// aut, 388 | /// initial: none, final: none, 389 | /// layout: finite.layout.group.with( 390 | /// grouping: 3, 391 | /// spacing: 4, 392 | /// layout: ( 393 | /// finite.layout.linear.with(dir: bottom), 394 | /// finite.layout.circular, 395 | /// ) 396 | /// ) 397 | /// ) 398 | /// ```] 399 | /// 400 | /// See @sec:showcase for a more comprehensive example. 401 | /// 402 | /// -> array 403 | #let group( 404 | /// Automaton specification. 405 | /// -> spec 406 | spec, 407 | /// Either an integer to collect states into 408 | /// roughly equal sized groups or an array of arrays that specify which 409 | /// states (by name) are in each group. 410 | /// -> int | array 411 | grouping: auto, 412 | /// Spacing between states on the line. 413 | /// -> float 414 | spacing: default-style.state.radius * 2, 415 | /// An array of layouts to use for each group. The first group of 416 | /// states will be passed to the first layout and so on. 417 | /// -> array 418 | layout: linear.with(dir: bottom), 419 | /// Position of the anchor point. 420 | /// -> coordinate 421 | position: (0, 0), 422 | /// Styling options. 423 | /// -> dictionary 424 | style: (:), 425 | ) = { 426 | assert-spec(spec) 427 | 428 | let groups = () 429 | let rest = () 430 | 431 | let grouping = util.def.if-auto(grouping, def: spec.states.len()) 432 | 433 | // collect state groups 434 | if util.is-int(grouping) { 435 | // by equal size 436 | groups = spec.states.chunks(grouping) 437 | } else if util.is-arr(grouping) { 438 | groups = grouping 439 | // collect remaining states into "rest" group 440 | rest = spec.states.filter(s => not groups.any(g => s in g)) 441 | } 442 | 443 | let positions = (:) 444 | let anchors = (:) 445 | let last-name = none 446 | for (i, group) in groups.enumerate() { 447 | let group-layout 448 | if util.is-arr(layout) { 449 | if layout.len() > i { 450 | group-layout = layout.at(i) 451 | } else { 452 | group-layout = layout.at(-1) 453 | } 454 | } else { 455 | group-layout = layout 456 | } 457 | 458 | let (pos, anc) = group-layout( 459 | spec + (states: group), 460 | position: if i == 0 { 461 | position 462 | } else { 463 | // TODO: (jneug) fix spacing between layouts 464 | (rel: (i * spacing, 0), to: position) 465 | }, 466 | style: style, 467 | ) 468 | 469 | positions += pos 470 | anchors += anc 471 | } 472 | 473 | return create-layout(positions: positions, anchors: anchors) 474 | } 475 | -------------------------------------------------------------------------------- /src/util.typ: -------------------------------------------------------------------------------- 1 | 2 | // Package dependencies 3 | #import "@preview/t4t:0.4.3": * 4 | #import "@preview/cetz:0.3.4" 5 | 6 | // TODO: (jneug) implement scheme validation with valkyrie 7 | // #import "@preview/valkyrie:0.2.1" as t 8 | 9 | // TODO: (jneug) don't import into global scope 10 | #import cetz.util.bezier: cubic-point, cubic-derivative, cubic-through-3points 11 | 12 | 13 | // TODO (jneug) refactor module and cleanup 14 | 15 | // ================================= 16 | // Defaults 17 | // ================================= 18 | 19 | #let default-style = ( 20 | state: ( 21 | fill: white, 22 | stroke: auto, 23 | radius: .6, 24 | extrude: .88, 25 | label: ( 26 | text: auto, 27 | size: 1em, 28 | fill: none, 29 | padding: auto, 30 | ), 31 | initial: ( 32 | anchor: left, 33 | label: ( 34 | text: "Start", 35 | size: .88em, 36 | dist: .1, 37 | ), 38 | stroke: auto, 39 | scale: .8, 40 | ), 41 | ), 42 | transition: ( 43 | curve: 1, 44 | stroke: auto, 45 | label: ( 46 | text: "", 47 | size: 1em, 48 | fill: none, 49 | pos: .5, 50 | dist: .33, 51 | angle: auto, 52 | ), 53 | ), 54 | loop: (:), 55 | ) 56 | 57 | // ================================= 58 | // Helpers 59 | // ================================= 60 | 61 | /// Calls #arg[value] with #sarg[args], if it is a #dtype("function") and returns the result or #arg[value] otherwise. 62 | #let call-or-get(value, ..args) = { 63 | if is-func(value) { 64 | return value(..args) 65 | } else { 66 | return value 67 | } 68 | } 69 | 70 | #let assert-dict = assert.new( 71 | is-dict, 72 | message: v => "dictionary expected. got " + repr(v), 73 | ) 74 | 75 | #let assert-spec = assert.new( 76 | value => is-dict(value) and "finite-spec" in value, 77 | message: v => "automaton specification expected. got " + repr(v), 78 | ) 79 | 80 | #let assert-full-spec = assert.new( 81 | value => is-dict(value) and ("finite-spec", "type", "transitions", "states", "initial", "final").all(k => k in value), 82 | message: v => "full automaton specification expected. got " + repr(v), 83 | ) 84 | 85 | 86 | // ================================= 87 | // Vectors 88 | // ================================= 89 | 90 | /// Set the length of a cetz.vector. 91 | #let vector-set-len(v, len) = if cetz.vector.len(v) == 0 { 92 | return v 93 | } else { 94 | return cetz.vector.scale(cetz.vector.norm(v), len) 95 | } 96 | 97 | /// Compute a normal for a 2d cetz.vector. The normal will be pointing to the right 98 | /// of the original cetz.vector. 99 | #let vector-normal(v) = cetz.vector.norm((-v.at(1), v.at(0), 0)) 100 | 101 | /// Rotates a vector by #arg[angle] degree around the origin. 102 | #let vector-rotate(vec, angle) = { 103 | let (x, y, ..) = vec 104 | return ( 105 | calc.cos(angle) * x - calc.sin(angle) * y, 106 | calc.sin(angle) * x + calc.cos(angle) * y, 107 | ) 108 | } 109 | 110 | /// Returns a vector for an alignment. 111 | #let align-to-vec(a) = { 112 | let v = ( 113 | ("none": 0, "center": 0, "left": -1, "right": 1).at(repr(a.x)), 114 | ("none": 0, "horizon": 0, "top": 1, "bottom": -1).at(repr(a.y)), 115 | ) 116 | 117 | return cetz.vector.norm(v) 118 | } 119 | 120 | #let vec-to-align(vec) = { 121 | let angle = cetz.vector.angle2((0, 0), vec) / 1deg 122 | if angle < 0 { angle = angle + 360 } 123 | let idx = calc.round((angle - 22.5) / 45 + .5) 124 | 125 | return ( 126 | right, 127 | top + right, 128 | top, 129 | top + left, 130 | left, 131 | bottom + left, 132 | bottom, 133 | bottom + right, 134 | ).at(int(idx)) 135 | } 136 | 137 | 138 | // ================================= 139 | // Bezier 140 | // ================================= 141 | 142 | /// Compute a normal vector for a point on a cubic bezier curve. 143 | #let cubic-normal(a, b, c, d, t) = { 144 | let qd = cubic-derivative(a, b, c, d, t) 145 | if cetz.vector.len(qd) == 0 { 146 | return (0, 1, 0) 147 | } else { 148 | return vector-normal(qd) 149 | } 150 | } 151 | 152 | /// Compute the mid point of a quadratic bezier curve. 153 | #let mid-point(a, b, c, d) = cubic-point(a, b, c, d, .5) 154 | 155 | 156 | // ================================= 157 | // Helpers 158 | // ================================= 159 | 160 | /// Calculate the control point for a transition. 161 | #let cubic-pts(a, b, curve: 1) = { 162 | if curve == 0 { 163 | return (a, b, b, a) 164 | } 165 | let ab = cetz.vector.sub(b, a) 166 | let X = cetz.vector.add( 167 | cetz.vector.add( 168 | a, 169 | cetz.vector.scale(ab, .5), 170 | ), 171 | cetz.vector.scale( 172 | vector-normal(ab), 173 | curve, 174 | ), 175 | ) 176 | return cubic-through-3points(a, X, b) 177 | } 178 | 179 | /// Calculate the direction vector for a transition mark (arrowhead) 180 | #let mark-dir(a, b, c, d, scale: 1) = vector-set-len(cubic-derivative(a, b, c, d, 1), scale) 181 | 182 | /// Calculate the location for a transitions label, based 183 | /// on its bezier points. 184 | #let label-pt(a, b, c, d, style, loop: false) = { 185 | let pos = style.label.pos // style.label.at("pos", default: default-style.transition.label.pos) 186 | let dist = style.label.dist // style.label.at("dist", default: default-style.transition.label.dist) 187 | let curve = style.curve // style.at("curve", default: default-style.transition.curve) 188 | 189 | let pt = cubic-point(a, b, c, d, pos) 190 | let n = cubic-normal(a, b, c, d, pos) 191 | 192 | if loop and curve < 0 { 193 | dist *= -1 194 | } 195 | 196 | return cetz.vector.add( 197 | pt, 198 | cetz.vector.scale(n, dist), 199 | ) 200 | } 201 | 202 | 203 | /// Calculate start, end and ctrl points for a transition loop. 204 | /// 205 | /// - start (vector): Center of the state. 206 | /// - start-radius (length): Radius of the state. 207 | /// - curve (float): Curvature of the transition. 208 | /// - anchor (alignment): Anchorpoint on the state 209 | #let loop-pts(start, start-radius, anchor: top, curve: 1) = { 210 | anchor = vector-set-len(align-to-vec(anchor), start-radius) 211 | 212 | let end = cetz.vector.add( 213 | start, 214 | vector-rotate(anchor, -22.5deg), 215 | ) 216 | let start = cetz.vector.add( 217 | start, 218 | vector-rotate(anchor, 22.5deg), 219 | ) 220 | 221 | if curve < 0 { 222 | (start, end) = (end, start) 223 | } else if curve == 0 { 224 | curve = start-radius 225 | } 226 | 227 | let (start, end, c1, c2) = cubic-pts(start, end, curve: curve) 228 | 229 | if curve < 0 { 230 | (c1, c2) = (c2, c1) 231 | } 232 | 233 | let d = cetz.vector.scale(cetz.vector.sub(c2, c1), curve * 4) 234 | c1 = cetz.vector.sub(c1, d) 235 | c2 = cetz.vector.add(c2, d) 236 | 237 | return (start, end, c1, c2) 238 | } 239 | 240 | 241 | /// Calculate start, end and ctrl points for a transition. 242 | /// 243 | /// - start (vector): Center of the start state. 244 | /// - end (vector): Center of the end state. 245 | /// - start-radius (length): Radius of the start state. 246 | /// - end-radius (length): Radius of the end state. 247 | /// - curve (float): Curvature of the transition. 248 | #let transition-pts(start, end, start-radius, end-radius, curve: 1, anchor: top) = { 249 | // Is it a loop? 250 | if start == end { 251 | return loop-pts(start, start-radius, curve: curve, anchor: anchor) 252 | } else { 253 | let (start, end, ctrl1, ctrl2) = cubic-pts(start, end, curve: curve) 254 | 255 | start = cetz.vector.add( 256 | start, 257 | vector-set-len( 258 | cetz.vector.sub( 259 | ctrl1, 260 | start, 261 | ), 262 | start-radius, 263 | ), 264 | ) 265 | end = cetz.vector.add( 266 | end, 267 | vector-set-len( 268 | cetz.vector.sub( 269 | end, 270 | ctrl2, 271 | ), 272 | -end-radius, 273 | ), 274 | ) 275 | return ( 276 | start, 277 | end, 278 | ctrl1, 279 | ctrl2, 280 | ) 281 | } 282 | } 283 | 284 | /// Fits (text) content inside the available space. 285 | /// 286 | /// - ctx (dictionary): The canvas context. 287 | /// - content (string, content): The content to fit. 288 | /// - size (length,auto): The initial text size. 289 | /// - min-size (length): The minimal text size to set. 290 | #let fit-content(ctx, width, height, content, size: auto, min-size: 6pt) = { 291 | let s = def.if-auto(ctx.length, size) 292 | 293 | let m = (width: 2 * width, height: 2 * height) 294 | while (m.height > height or m.width > height) and s > min-size { 295 | s = s * .88 296 | m = cetz.util.measure( 297 | ctx, 298 | { 299 | set text(s) 300 | content 301 | }, 302 | ) 303 | } 304 | s = calc.max(min-size, s) 305 | { 306 | set text(s) 307 | content 308 | } 309 | } 310 | 311 | /// Changes a @type:transition-table from the format (`state`: `inputs`) to (`input`: `states`) or vice versa. 312 | /// -> dict 313 | #let transpose-table( 314 | /// A transition table in any format. 315 | /// -> transition-table 316 | table, 317 | ) = { 318 | let ttable = (:) 319 | for (key, values) in table { 320 | let new-values = (:) 321 | 322 | if not-none(values) { 323 | for (kk, vv) in values { 324 | for i in def.as-arr(vv) { 325 | if not-none(i) { 326 | i = str(i) 327 | if i not in new-values { 328 | new-values.insert(i, (kk,)) 329 | } else { 330 | new-values.at(i).push(kk) 331 | } 332 | } 333 | } 334 | } 335 | } 336 | 337 | ttable.insert(key, new-values) 338 | } 339 | 340 | return ttable 341 | } 342 | 343 | /// Gets a list of all inputs from a transition table. 344 | /// 345 | #let get-inputs( 346 | /// A transition table. 347 | /// -> transition-table 348 | table, 349 | /// If #arg[table] needs to be transposed first. Set this to #typ.v.false if the table already is in the format (`input`: `states`). 350 | /// -> bool 351 | transpose: true, 352 | ) = { 353 | if transpose { 354 | table = transpose-table(table) 355 | } 356 | 357 | let inputs = () 358 | for (_, values) in table { 359 | for (inp, _) in values { 360 | if inp not in inputs { 361 | inputs.push(str(inp)) 362 | } 363 | } 364 | } 365 | 366 | return inputs.sorted() 367 | } 368 | 369 | 370 | /// Checks if a given @type:spec represents 371 | /// a deterministic automaton. 372 | /// 373 | /// ```example 374 | /// #util.is-dea(( 375 | /// q0: (q1: 1, q2: 1), 376 | /// )) 377 | /// #util.is-dea(( 378 | /// q0: (q1: 1, q2: 0), 379 | /// )) 380 | /// ``` 381 | /// 382 | /// -> bool 383 | #let is-dea( 384 | /// A transition table. 385 | /// -> transition-table 386 | table, 387 | ) = { 388 | for (_, transitions) in table { 389 | let inp = () 390 | for (state, inputs) in transitions { 391 | for i in def.as-arr(inputs) { 392 | if i in inp { 393 | return false 394 | } else { 395 | inp.push(i) 396 | } 397 | } 398 | } 399 | } 400 | 401 | return true 402 | } 403 | 404 | 405 | // deprecated!!! 406 | #let to-spec(spec, states: auto, initial: auto, final: auto, inputs: auto, labels: auto) = { 407 | // TODO: (jneug) add asserts to react to malicious specs 408 | // TODO: (jneug) check for duplicate names 409 | if "transitions" not in spec { 410 | spec = (transitions: spec) 411 | } 412 | if "states" not in spec { 413 | if is-auto(states) { 414 | states = spec.transitions.keys() 415 | } 416 | spec.insert("states", states) 417 | } 418 | if "initial" not in spec { 419 | if is-auto(initial) { 420 | initial = spec.states.first() 421 | } 422 | spec.insert("initial", initial) 423 | } 424 | if "final" not in spec { 425 | if is-auto(final) { 426 | final = (spec.states.last(),) 427 | } else if is-none(final) { 428 | final = () 429 | } 430 | spec.insert("final", final) 431 | } 432 | if "inputs" not in spec { 433 | if is-auto(inputs) { 434 | inputs = get-inputs(spec.transitions) 435 | } 436 | spec.insert("inputs", inputs) 437 | } else { 438 | spec.inputs = spec.inputs.map(str).sorted() 439 | } 440 | 441 | return spec + (finite-spec: true, type: "DEA") 442 | } 443 | 444 | 445 | /// Return anchor name for an #dtype(alignment). 446 | #let align-to-anchor(align) = { 447 | let anchor = () 448 | if align.y == top { 449 | anchor.push("north") 450 | } else if align.y == bottom { 451 | anchor.push("south") 452 | } 453 | if align.x == left { 454 | anchor.push("west") 455 | } else if align.x == right { 456 | anchor.push("east") 457 | } 458 | if anchor == () { 459 | return "center" 460 | } else { 461 | return anchor.join("-") 462 | } 463 | } 464 | 465 | #let to-anchor(align) = { 466 | if type(align) == alignment { 467 | return align-to-anchor(align) 468 | } else { 469 | align 470 | } 471 | } 472 | 473 | #let label-angle(vec, a) = if a in (top, top + right, right, bottom + right) { 474 | cetz.vector.angle2((0, 0), vec) 475 | } else { 476 | cetz.vector.angle2(vec, (0, 0)) 477 | } 478 | 479 | #let abs-angle-between(vec1, vec2, lower, upper) = { 480 | let ang = calc.abs(cetz.vector.angle2(vec1, vec2)) 481 | return ang >= lower and ang <= upper 482 | } 483 | 484 | 485 | #let is-state(element) = { 486 | return "finite" in element and element.finite.state 487 | } 488 | 489 | #let is-transition(element) = { 490 | return "finite" in element and element.finite.transition 491 | } 492 | 493 | #let resolve-one(ctx, element) = { 494 | element = (element)(ctx) 495 | ctx = element.ctx 496 | 497 | if "name" in element and element.name != none { 498 | if "nodes" not in ctx { 499 | ctx.insert("nodes", (:)) 500 | } 501 | ctx.nodes.insert(element.name, element) 502 | } 503 | 504 | return (ctx, element) 505 | } 506 | 507 | #let resolve-many(ctx, body) = { 508 | let elements = () 509 | let (_ctx, element) = (ctx, none) 510 | for element in body { 511 | (_ctx, element) = resolve-one(_ctx, element) 512 | elements.push(element) 513 | } 514 | return (_ctx, elements) 515 | } 516 | 517 | #let resolve-zipped(ctx, body) = { 518 | let (_ctx, elements) = resolve-many(ctx, body) 519 | return (_ctx, body.zip(elements)) 520 | } 521 | 522 | #let resolve-states(ctx, body) = { 523 | let (_, elements) = resolve-many(ctx, body) 524 | return elements.filter(is-state) 525 | } 526 | 527 | #let get-radii(elements) = { 528 | return elements 529 | .filter(is-state) 530 | .fold( 531 | (:), 532 | (radii, element) => { 533 | radii.insert(element.name, element.finite.radius) 534 | return radii 535 | }, 536 | ) 537 | } 538 | 539 | // Resolve radii for states by applying styles from other elements. 540 | #let resolve-radii(ctx, body) = { 541 | let (_, elements) = resolve-many(ctx, body) 542 | return get-radii(elements) 543 | } 544 | 545 | #let state-wrapper(group) = { 546 | return ( 547 | ctx => { 548 | let g = (group.first())(ctx) 549 | g.insert( 550 | "finite", 551 | ( 552 | state: true, 553 | transition: false, 554 | radius: (g.anchors)("state.east").at(0) - (g.anchors)("state.center").at(0), 555 | ), 556 | ) 557 | return g 558 | }, 559 | ) 560 | } 561 | 562 | #let get-radii(spec, style: (:)) = spec.states.fold( 563 | (:), 564 | (d, name) => { 565 | let r = style.at(name, default: (:)).at("radius", default: none) 566 | d.insert( 567 | name, 568 | style 569 | .at( 570 | name, 571 | default: style.at( 572 | "state", 573 | default: (:), 574 | ), 575 | ) 576 | .at( 577 | "radius", 578 | default: default-style.state.radius, 579 | ), 580 | ) 581 | d 582 | }, 583 | ) 584 | 585 | #let transition-wrapper(from, to, group) = { 586 | return ( 587 | ctx => { 588 | let g = (group.first())(ctx) 589 | g.insert( 590 | "finite", 591 | ( 592 | state: false, 593 | transition: true, 594 | from: from, 595 | to: to, 596 | ), 597 | ) 598 | return g 599 | }, 600 | ) 601 | } 602 | -------------------------------------------------------------------------------- /tbump.toml: -------------------------------------------------------------------------------- 1 | # Uncomment this if your project is hosted on GitHub: 2 | github_url = "https://github.com/jneug/typst-finite/" 3 | 4 | [version] 5 | current = "0.5.0" 6 | 7 | # Example of a semver regexp. 8 | # Make sure this matches current_version before 9 | # using tbump 10 | regex = ''' 11 | (?P\d+) 12 | \. 13 | (?P\d+) 14 | \. 15 | (?P\d+) 16 | ''' 17 | 18 | [git] 19 | message_template = "Bump to {new_version}" 20 | tag_template = "v{new_version}" 21 | 22 | [[file]] 23 | src = "typst.toml" 24 | search = 'version = "{current_version}"' 25 | 26 | [[file]] 27 | src = "README.md" 28 | search = '\(v{current_version}\)' 29 | [[file]] 30 | src = "README.md" 31 | search = "finite:{current_version}" 32 | 33 | [[before_commit]] 34 | name = "compile manual" 35 | cmd = "just doc" 36 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | # added by typst-test 2 | out/** 3 | diff/** 4 | 5 | -------------------------------------------------------------------------------- /tests/.ignore: -------------------------------------------------------------------------------- 1 | # added by typst-test 2 | **.png 3 | **.svg 4 | **.pdf 5 | -------------------------------------------------------------------------------- /tests/accepts/.gitignore: -------------------------------------------------------------------------------- 1 | # added by typst-test 2 | -------------------------------------------------------------------------------- /tests/accepts/diff/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/accepts/diff/1.png -------------------------------------------------------------------------------- /tests/accepts/diff/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/accepts/diff/2.png -------------------------------------------------------------------------------- /tests/accepts/diff/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/accepts/diff/3.png -------------------------------------------------------------------------------- /tests/accepts/diff/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/accepts/diff/4.png -------------------------------------------------------------------------------- /tests/accepts/out/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/accepts/out/1.png -------------------------------------------------------------------------------- /tests/accepts/out/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/accepts/out/2.png -------------------------------------------------------------------------------- /tests/accepts/out/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/accepts/out/3.png -------------------------------------------------------------------------------- /tests/accepts/out/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/accepts/out/4.png -------------------------------------------------------------------------------- /tests/accepts/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/accepts/ref/1.png -------------------------------------------------------------------------------- /tests/accepts/ref/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/accepts/ref/2.png -------------------------------------------------------------------------------- /tests/accepts/ref/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/accepts/ref/3.png -------------------------------------------------------------------------------- /tests/accepts/ref/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/accepts/ref/4.png -------------------------------------------------------------------------------- /tests/accepts/test.typ: -------------------------------------------------------------------------------- 1 | #import "../../src/finite.typ" 2 | 3 | #set page(width: auto, height: auto, margin: 1cm) 4 | 5 | #let aut = finite.create-automaton(( 6 | q0: (q1: "a"), 7 | q1: (q1: ("a", "b"), q2: "c"), 8 | q2: (q0: "b"), 9 | )) 10 | 11 | #finite.automaton(aut) 12 | 13 | #finite.accepts(aut, "abc") 14 | 15 | #pagebreak() 16 | 17 | #finite.accepts( 18 | aut, 19 | "abbaac", 20 | format: (spec, states) => { 21 | let style = (:) 22 | for i in range(states.len() - 1) { 23 | let key = str(states.at(i).first()) + "-" + str(states.at(i + 1).first()) 24 | style.insert(key, (stroke: red)) 25 | style.insert(states.at(i).first(), (stroke: red)) 26 | style.insert(states.at(i + 1).first(), (stroke: red)) 27 | } 28 | finite.automaton(spec, style: style) 29 | }, 30 | ) 31 | 32 | #pagebreak() 33 | 34 | #let aut = finite.create-automaton(( 35 | q0: (q1: "la"), 36 | q1: (q1: ("la", "le"), q2: "lu"), 37 | q2: (q0: "lu"), 38 | )) 39 | 40 | #finite.automaton(aut) 41 | 42 | #finite.accepts(aut, "lalelu") 43 | 44 | #finite.accepts(aut, "lalalulu") 45 | 46 | #pagebreak() 47 | 48 | #let aut = finite.create-automaton(( 49 | q0: (q1: "la"), 50 | q1: (q1: (1, "x"), q2: "foo"), 51 | q2: (q0: 0), 52 | )) 53 | 54 | #finite.automaton(aut) 55 | 56 | #finite.accepts(aut, "la1xfoo") 57 | -------------------------------------------------------------------------------- /tests/flaci-load/.gitignore: -------------------------------------------------------------------------------- 1 | # added by typst-test 2 | -------------------------------------------------------------------------------- /tests/flaci-load/DEA-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TEIL2: Wort mit gerader Anzahl an Nullen", 3 | "description": "", 4 | "type": "DEA", 5 | "automaton": { 6 | "acceptCache": [], 7 | "simulationInput": [], 8 | "Alphabet": [ 9 | "0", 10 | "1", 11 | "2", 12 | "3", 13 | "4", 14 | "5", 15 | "6", 16 | "7", 17 | "8", 18 | "9" 19 | ], 20 | "StackAlphabet": [ 21 | "|" 22 | ], 23 | "States": [ 24 | { 25 | "ID": 1, 26 | "Name": "q0", 27 | "x": 220, 28 | "y": 130, 29 | "Final": false, 30 | "Radius": 30, 31 | "Transitions": [ 32 | { 33 | "Source": 1, 34 | "Target": 1, 35 | "x": 0, 36 | "y": -150, 37 | "Labels": [ 38 | "1", 39 | "2", 40 | "3", 41 | "4", 42 | "5", 43 | "6", 44 | "7", 45 | "8", 46 | "9" 47 | ] 48 | }, 49 | { 50 | "Source": 1, 51 | "Target": 2, 52 | "x": 0, 53 | "y": -80, 54 | "Labels": [ 55 | "0" 56 | ] 57 | } 58 | ], 59 | "Start": true 60 | }, 61 | { 62 | "ID": 2, 63 | "Name": "q1", 64 | "x": 510, 65 | "y": 130, 66 | "Final": true, 67 | "Radius": 30, 68 | "Transitions": [ 69 | { 70 | "Source": 2, 71 | "Target": 2, 72 | "x": 0, 73 | "y": -150, 74 | "Labels": [ 75 | "1", 76 | "2", 77 | "3", 78 | "4", 79 | "5", 80 | "6", 81 | "7", 82 | "8", 83 | "9" 84 | ] 85 | }, 86 | { 87 | "Source": 2, 88 | "Target": 1, 89 | "x": 0, 90 | "y": -50, 91 | "Labels": [ 92 | "0" 93 | ] 94 | } 95 | ], 96 | "Start": false 97 | } 98 | ], 99 | "lastInputs": [] 100 | }, 101 | "GUID": "hiicmv2p1" 102 | } 103 | -------------------------------------------------------------------------------- /tests/flaci-load/DEA-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Schatzsuche", 3 | "description": "", 4 | "type": "DEA", 5 | "automaton": { 6 | "acceptCache": [], 7 | "simulationInput": [ 8 | "I", 9 | "I", 10 | "I" 11 | ], 12 | "Alphabet": [ 13 | "I", 14 | "V" 15 | ], 16 | "StackAlphabet": [ 17 | "|" 18 | ], 19 | "States": [ 20 | { 21 | "ID": 1, 22 | "Name": "q0", 23 | "x": 220, 24 | "y": 450, 25 | "Final": false, 26 | "Radius": 30, 27 | "Transitions": [ 28 | { 29 | "Source": 1, 30 | "Target": 2, 31 | "x": -20, 32 | "y": 20, 33 | "Labels": [ 34 | "I" 35 | ] 36 | }, 37 | { 38 | "Source": 1, 39 | "Target": 5, 40 | "x": -20, 41 | "y": 30, 42 | "Labels": [ 43 | "V" 44 | ] 45 | } 46 | ], 47 | "Start": true 48 | }, 49 | { 50 | "ID": 2, 51 | "Name": "q1", 52 | "x": 360, 53 | "y": 550, 54 | "Final": true, 55 | "Radius": 30, 56 | "Transitions": [ 57 | { 58 | "Source": 2, 59 | "Target": 3, 60 | "x": -10, 61 | "y": 10, 62 | "Labels": [ 63 | "I" 64 | ] 65 | }, 66 | { 67 | "Source": 2, 68 | "Target": 4, 69 | "x": -20, 70 | "y": 20, 71 | "Labels": [ 72 | "V" 73 | ] 74 | } 75 | ], 76 | "Start": false 77 | }, 78 | { 79 | "ID": 3, 80 | "Name": "q2", 81 | "x": 640, 82 | "y": 510, 83 | "Final": true, 84 | "Radius": 30, 85 | "Transitions": [ 86 | { 87 | "Source": 3, 88 | "Target": 4, 89 | "x": 10, 90 | "y": -10, 91 | "Labels": [ 92 | "I" 93 | ] 94 | }, 95 | { 96 | "Source": 3, 97 | "Target": 9, 98 | "x": 0, 99 | "y": 0, 100 | "Labels": [ 101 | "V" 102 | ] 103 | } 104 | ], 105 | "Start": false 106 | }, 107 | { 108 | "ID": 4, 109 | "Name": "q3", 110 | "x": 770, 111 | "y": 650, 112 | "Final": true, 113 | "Radius": 30, 114 | "Transitions": [ 115 | { 116 | "Source": 4, 117 | "Target": 9, 118 | "x": 0, 119 | "y": 0, 120 | "Labels": [ 121 | "I", 122 | "V" 123 | ] 124 | } 125 | ], 126 | "Start": false 127 | }, 128 | { 129 | "ID": 5, 130 | "Name": "q4", 131 | "x": 360, 132 | "y": 360, 133 | "Final": true, 134 | "Radius": 30, 135 | "Transitions": [ 136 | { 137 | "Source": 5, 138 | "Target": 8, 139 | "x": 0, 140 | "y": 0, 141 | "Labels": [ 142 | "I" 143 | ] 144 | }, 145 | { 146 | "Source": 5, 147 | "Target": 9, 148 | "x": 0, 149 | "y": 0, 150 | "Labels": [ 151 | "V" 152 | ] 153 | } 154 | ], 155 | "Start": false 156 | }, 157 | { 158 | "ID": 8, 159 | "Name": "q7", 160 | "x": 510, 161 | "y": 400, 162 | "Final": true, 163 | "Radius": 30, 164 | "Transitions": [ 165 | { 166 | "Source": 8, 167 | "Target": 3, 168 | "x": 0, 169 | "y": 0, 170 | "Labels": [ 171 | "I" 172 | ] 173 | }, 174 | { 175 | "Source": 8, 176 | "Target": 9, 177 | "x": 0, 178 | "y": 0, 179 | "Labels": [ 180 | "V" 181 | ] 182 | } 183 | ], 184 | "Start": false 185 | }, 186 | { 187 | "ID": 9, 188 | "Name": "TRAP", 189 | "x": 810, 190 | "y": 370, 191 | "Final": false, 192 | "Radius": 30, 193 | "Transitions": [ 194 | { 195 | "Source": 9, 196 | "Target": 9, 197 | "x": 0, 198 | "y": -40, 199 | "Labels": [ 200 | "I", 201 | "V" 202 | ] 203 | } 204 | ], 205 | "Start": false 206 | } 207 | ], 208 | "lastInputs": [ 209 | [ 210 | "I", 211 | "I", 212 | "I" 213 | ] 214 | ] 215 | }, 216 | "GUID": "rz7hb617s" 217 | } 218 | -------------------------------------------------------------------------------- /tests/flaci-load/DEA-3.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Plusterme", 3 | "description": "", 4 | "type": "DEA", 5 | "automaton": { 6 | "acceptCache": [], 7 | "simulationInput": [], 8 | "Alphabet": [ 9 | "0", 10 | "1", 11 | "2", 12 | "3", 13 | "4", 14 | "5", 15 | "6", 16 | "7", 17 | "8", 18 | "9", 19 | "+", 20 | "OPERATOR", 21 | "OPERAND" 22 | ], 23 | "StackAlphabet": [ 24 | "|" 25 | ], 26 | "States": [ 27 | { 28 | "ID": 1, 29 | "Name": "q0", 30 | "x": 170, 31 | "y": 160, 32 | "Final": false, 33 | "Radius": 30, 34 | "Transitions": [ 35 | { 36 | "Source": 1, 37 | "Target": 2, 38 | "x": 0, 39 | "y": 0, 40 | "Labels": [ 41 | "1", 42 | "2", 43 | "3", 44 | "4", 45 | "5", 46 | "6", 47 | "7", 48 | "8", 49 | "9" 50 | ] 51 | }, 52 | { 53 | "Source": 1, 54 | "Target": 3, 55 | "x": 0, 56 | "y": 0, 57 | "Labels": [ 58 | "0" 59 | ] 60 | } 61 | ], 62 | "Start": false 63 | }, 64 | { 65 | "ID": 2, 66 | "Name": "q1", 67 | "x": 390, 68 | "y": 160, 69 | "Final": false, 70 | "Radius": 30, 71 | "Transitions": [ 72 | { 73 | "Source": 2, 74 | "Target": 4, 75 | "x": 0, 76 | "y": 0, 77 | "Labels": [ 78 | "+" 79 | ] 80 | }, 81 | { 82 | "Source": 2, 83 | "Target": 2, 84 | "x": 0, 85 | "y": -150, 86 | "Labels": [ 87 | "0", 88 | "1", 89 | "2", 90 | "3", 91 | "4", 92 | "5", 93 | "6", 94 | "7", 95 | "8", 96 | "9" 97 | ] 98 | } 99 | ], 100 | "Start": false 101 | }, 102 | { 103 | "ID": 3, 104 | "Name": "q2", 105 | "x": 420, 106 | "y": 330, 107 | "Final": false, 108 | "Radius": 30, 109 | "Transitions": [ 110 | { 111 | "Source": 3, 112 | "Target": 4, 113 | "x": 0, 114 | "y": 0, 115 | "Labels": [ 116 | "+" 117 | ] 118 | } 119 | ], 120 | "Start": false 121 | }, 122 | { 123 | "ID": 4, 124 | "Name": "q3", 125 | "x": 630, 126 | "y": 170, 127 | "Final": false, 128 | "Radius": 30, 129 | "Transitions": [ 130 | { 131 | "Source": 4, 132 | "Target": 5, 133 | "x": 0, 134 | "y": 0, 135 | "Labels": [ 136 | "1", 137 | "2", 138 | "3", 139 | "4", 140 | "5", 141 | "6", 142 | "7", 143 | "8", 144 | "9" 145 | ] 146 | }, 147 | { 148 | "Source": 4, 149 | "Target": 6, 150 | "x": 0, 151 | "y": 0, 152 | "Labels": [ 153 | "0" 154 | ] 155 | } 156 | ], 157 | "Start": false 158 | }, 159 | { 160 | "ID": 5, 161 | "Name": "q4", 162 | "x": 860, 163 | "y": 170, 164 | "Final": true, 165 | "Radius": 30, 166 | "Transitions": [ 167 | { 168 | "Source": 5, 169 | "Target": 5, 170 | "x": 0, 171 | "y": -150, 172 | "Labels": [ 173 | "0", 174 | "1", 175 | "2", 176 | "3", 177 | "4", 178 | "5", 179 | "6", 180 | "7", 181 | "8", 182 | "9" 183 | ] 184 | } 185 | ], 186 | "Start": false 187 | }, 188 | { 189 | "ID": 6, 190 | "Name": "q5", 191 | "x": 860, 192 | "y": 310, 193 | "Final": true, 194 | "Radius": 30, 195 | "Transitions": [], 196 | "Start": false 197 | }, 198 | { 199 | "ID": 7, 200 | "Name": "s0", 201 | "x": 230, 202 | "y": 430, 203 | "Final": false, 204 | "Radius": 30, 205 | "Transitions": [ 206 | { 207 | "Source": 7, 208 | "Target": 8, 209 | "x": 0, 210 | "y": 0, 211 | "Labels": [ 212 | "OPERAND" 213 | ] 214 | } 215 | ], 216 | "Start": true 217 | }, 218 | { 219 | "ID": 8, 220 | "Name": "s1", 221 | "x": 400, 222 | "y": 430, 223 | "Final": false, 224 | "Radius": 30, 225 | "Transitions": [ 226 | { 227 | "Source": 8, 228 | "Target": 9, 229 | "x": 0, 230 | "y": 0, 231 | "Labels": [ 232 | "OPERATOR" 233 | ] 234 | } 235 | ], 236 | "Start": false 237 | }, 238 | { 239 | "ID": 9, 240 | "Name": "s2", 241 | "x": 590, 242 | "y": 430, 243 | "Final": false, 244 | "Radius": 30, 245 | "Transitions": [ 246 | { 247 | "Source": 9, 248 | "Target": 10, 249 | "x": 0, 250 | "y": 0, 251 | "Labels": [ 252 | "OPERAND" 253 | ] 254 | } 255 | ], 256 | "Start": false 257 | }, 258 | { 259 | "ID": 10, 260 | "Name": "s3", 261 | "x": 770, 262 | "y": 430, 263 | "Final": true, 264 | "Radius": 30, 265 | "Transitions": [], 266 | "Start": false 267 | } 268 | ], 269 | "lastInputs": [] 270 | }, 271 | "GUID": "f75paviy0" 272 | } 273 | -------------------------------------------------------------------------------- /tests/flaci-load/NEA-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "[NEA] abacc", 3 | "description": "", 4 | "type": "NEA", 5 | "automaton": { 6 | "simulationInput": [], 7 | "Alphabet": [ 8 | "a", 9 | "b", 10 | "c" 11 | ], 12 | "StackAlphabet": [ 13 | "|" 14 | ], 15 | "States": [ 16 | { 17 | "ID": 1, 18 | "Name": "q0", 19 | "x": 150, 20 | "y": 150, 21 | "Final": false, 22 | "Radius": 30, 23 | "Transitions": [ 24 | { 25 | "Source": 1, 26 | "Target": 2, 27 | "x": 0, 28 | "y": 0, 29 | "Labels": [ 30 | "a" 31 | ] 32 | } 33 | ], 34 | "Start": true 35 | }, 36 | { 37 | "ID": 2, 38 | "Name": "q1", 39 | "x": 320, 40 | "y": 150, 41 | "Final": true, 42 | "Radius": 30, 43 | "Transitions": [ 44 | { 45 | "Source": 2, 46 | "Target": 2, 47 | "x": 0, 48 | "y": -150, 49 | "Labels": [ 50 | "a" 51 | ] 52 | }, 53 | { 54 | "Source": 2, 55 | "Target": 3, 56 | "x": 20, 57 | "y": 0, 58 | "Labels": [ 59 | "b", 60 | "c" 61 | ] 62 | }, 63 | { 64 | "Source": 2, 65 | "Target": 4, 66 | "x": 50, 67 | "y": -20, 68 | "Labels": [ 69 | "b", 70 | "c" 71 | ] 72 | } 73 | ], 74 | "Start": false 75 | }, 76 | { 77 | "ID": 3, 78 | "Name": "q2", 79 | "x": 320, 80 | "y": 310, 81 | "Final": false, 82 | "Radius": 30, 83 | "Transitions": [ 84 | { 85 | "Source": 3, 86 | "Target": 2, 87 | "x": 40, 88 | "y": 0, 89 | "Labels": [ 90 | "a" 91 | ] 92 | } 93 | ], 94 | "Start": false 95 | }, 96 | { 97 | "ID": 4, 98 | "Name": "q3", 99 | "x": 500, 100 | "y": 150, 101 | "Final": true, 102 | "Radius": 30, 103 | "Transitions": [ 104 | { 105 | "Source": 4, 106 | "Target": 2, 107 | "x": 50, 108 | "y": -10, 109 | "Labels": [ 110 | "c" 111 | ] 112 | } 113 | ], 114 | "Start": false 115 | } 116 | ], 117 | "lastInputs": [] 118 | }, 119 | "GUID": "ro3ujamz" 120 | } 121 | -------------------------------------------------------------------------------- /tests/flaci-load/NKA-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Vorabi 24", 3 | "description": "", 4 | "type": "NKA", 5 | "automaton": { 6 | "Alphabet": [ 7 | "X", 8 | "$", 9 | "1" 10 | ], 11 | "StackAlphabet": [ 12 | "#", 13 | "A", 14 | "B", 15 | "C", 16 | "X" 17 | ], 18 | "States": [ 19 | { 20 | "ID": 1, 21 | "Name": "q0", 22 | "x": 150, 23 | "y": 150, 24 | "Radius": 30, 25 | "Transitions": [ 26 | { 27 | "Source": 1, 28 | "Target": 2, 29 | "x": 0, 30 | "y": 0, 31 | "Labels": [ 32 | [ 33 | "#", 34 | "", 35 | [ 36 | "A", 37 | "C", 38 | "A", 39 | "B", 40 | "#" 41 | ] 42 | ] 43 | ] 44 | } 45 | ], 46 | "Start": true, 47 | "Final": false 48 | }, 49 | { 50 | "ID": 3, 51 | "Name": "q2", 52 | "x": 590, 53 | "y": 150, 54 | "Radius": 30, 55 | "Transitions": [], 56 | "Start": false, 57 | "Final": true 58 | }, 59 | { 60 | "ID": 2, 61 | "Name": "q1", 62 | "x": 370, 63 | "y": 150, 64 | "Radius": 30, 65 | "Transitions": [ 66 | { 67 | "Source": 2, 68 | "Target": 3, 69 | "x": 0, 70 | "y": 0, 71 | "Labels": [ 72 | [ 73 | "#", 74 | "", 75 | [] 76 | ] 77 | ] 78 | }, 79 | { 80 | "Source": 2, 81 | "Target": 2, 82 | "x": 0, 83 | "y": 40, 84 | "Labels": [ 85 | [ 86 | "X", 87 | "X", 88 | [] 89 | ], 90 | [ 91 | "C", 92 | "1", 93 | [] 94 | ], 95 | [ 96 | "C", 97 | "$", 98 | [] 99 | ], 100 | [ 101 | "A", 102 | "X", 103 | [] 104 | ], 105 | [ 106 | "A", 107 | "X", 108 | [ 109 | "C", 110 | "X" 111 | ] 112 | ], 113 | [ 114 | "B", 115 | "", 116 | [ 117 | "A" 118 | ] 119 | ], 120 | [ 121 | "B", 122 | "", 123 | [ 124 | "A", 125 | "B" 126 | ] 127 | ] 128 | ] 129 | } 130 | ], 131 | "Start": false, 132 | "Final": false 133 | } 134 | ], 135 | "acceptCache": [], 136 | "simulationInput": [ 137 | "X", 138 | "$", 139 | "X", 140 | "$", 141 | "X", 142 | "X", 143 | "1", 144 | "X", 145 | "X", 146 | "X", 147 | "$", 148 | "X" 149 | ], 150 | "lastInputs": [ 151 | [ 152 | "X", 153 | "$", 154 | "X", 155 | "$", 156 | "X", 157 | "X", 158 | "1", 159 | "X", 160 | "X", 161 | "X", 162 | "$", 163 | "X" 164 | ], 165 | [ 166 | "X", 167 | "X", 168 | "$", 169 | "X" 170 | ], 171 | [ 172 | "$", 173 | "X", 174 | "X", 175 | "1", 176 | "X" 177 | ], 178 | [ 179 | "X", 180 | "$", 181 | "X", 182 | "X", 183 | "1", 184 | "X" 185 | ], 186 | [ 187 | "X", 188 | "$", 189 | "X", 190 | "X" 191 | ], 192 | [ 193 | "X", 194 | "X", 195 | "X", 196 | "X" 197 | ] 198 | ] 199 | }, 200 | "GUID": "ibbsud0or" 201 | } 202 | -------------------------------------------------------------------------------- /tests/flaci-load/diff/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/flaci-load/diff/1.png -------------------------------------------------------------------------------- /tests/flaci-load/diff/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/flaci-load/diff/2.png -------------------------------------------------------------------------------- /tests/flaci-load/diff/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/flaci-load/diff/3.png -------------------------------------------------------------------------------- /tests/flaci-load/diff/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/flaci-load/diff/4.png -------------------------------------------------------------------------------- /tests/flaci-load/out/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/flaci-load/out/1.png -------------------------------------------------------------------------------- /tests/flaci-load/out/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/flaci-load/out/2.png -------------------------------------------------------------------------------- /tests/flaci-load/out/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/flaci-load/out/3.png -------------------------------------------------------------------------------- /tests/flaci-load/out/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/flaci-load/out/4.png -------------------------------------------------------------------------------- /tests/flaci-load/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/flaci-load/ref/1.png -------------------------------------------------------------------------------- /tests/flaci-load/ref/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/flaci-load/ref/2.png -------------------------------------------------------------------------------- /tests/flaci-load/ref/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/flaci-load/ref/3.png -------------------------------------------------------------------------------- /tests/flaci-load/ref/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/flaci-load/ref/4.png -------------------------------------------------------------------------------- /tests/flaci-load/test.typ: -------------------------------------------------------------------------------- 1 | #import "../../src/finite.typ": automaton, flaci 2 | 3 | #set page(width: auto, height: auto, margin: 1cm) 4 | 5 | #let automatons = ( 6 | "DEA-1", 7 | "DEA-2", 8 | "DEA-3", // With BOM 9 | "NEA-1", 10 | // "NKA-1", // Not yet supported 11 | ) 12 | 13 | #for file in automatons { 14 | page[ 15 | #let aut = read(file + ".json") 16 | #flaci.automaton(aut, style: (q1: (fill: green))) 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tests/large-automaton/diff/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/large-automaton/diff/1.png -------------------------------------------------------------------------------- /tests/large-automaton/out/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/large-automaton/out/1.png -------------------------------------------------------------------------------- /tests/large-automaton/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/large-automaton/ref/1.png -------------------------------------------------------------------------------- /tests/large-automaton/test.typ: -------------------------------------------------------------------------------- 1 | /// [skip] 2 | 3 | #import "../../src/finite.typ" 4 | 5 | #set page(width: auto, height: auto, margin: 1cm) 6 | 7 | #let spacing = 2 8 | #let splay = 5 9 | #finite.automaton( 10 | ( 11 | q0: (q1: "λ", q5: "λ"), 12 | q1: (q2: 0), 13 | q2: (q3: 0), 14 | q3: (q4: 0), 15 | q4: (q1: "λ", q5: "λ"), 16 | q5: (q6: "λ"), 17 | q6: (q7: "λ", q9: "λ", q13: "λ"), 18 | q7: (q8: "λ"), 19 | q8: (q17: "λ"), 20 | q9: (q10: 0), 21 | q10: (q11: 1), 22 | q11: (q12: 1), 23 | q12: (q17: "λ"), 24 | q13: (q14: 0), 25 | q14: (q15: 0), 26 | q15: (q16: 1), 27 | q16: (q17: "λ"), 28 | q17: (q18: "λ"), 29 | q18: (q19: "λ", q23: "λ"), 30 | q19: (q20: 1), 31 | q20: (q21: 1), 32 | q21: (q22: 1), 33 | q22: (q19: "λ", q23: "λ"), 34 | q23: none, 35 | ), 36 | layout: ( 37 | q0: (0.6, 0), 38 | q1: (spacing, 0), 39 | q2: (2 * spacing, 0), 40 | q3: (3 * spacing, 0), 41 | q4: (4 * spacing, 0), 42 | q5: (5 * spacing, 0), 43 | q6: (5 * spacing, -splay), 44 | q7: (3 * spacing, spacing - splay), 45 | q8: (2 * spacing, spacing - splay), 46 | q9: (4 * spacing, -splay), 47 | q10: (3 * spacing, -splay), 48 | q11: (2 * spacing, -splay), 49 | q12: (2 * spacing, -splay), 50 | q13: (4 * spacing, -spacing - splay), 51 | q14: (3 * spacing, -spacing - splay), 52 | q15: (2 * spacing, -spacing - splay), 53 | q16: (1 * spacing, -spacing - splay), 54 | q17: (0, -splay), 55 | q18: (0, -2 * splay), 56 | q19: (spacing, -2 * splay), 57 | q20: (2 * spacing, -2 * splay), 58 | q21: (3 * spacing, -2 * splay), 59 | q22: (4 * spacing, -2 * splay), 60 | q23: (5 * spacing, -2 * splay), 61 | ), 62 | style: ( 63 | transition: ( 64 | curve: 0, 65 | label: (angle: 0deg), 66 | ), 67 | q0-q5: (curve: 1.5), 68 | q4-q1: (curve: 1.5), 69 | q18-q23: (curve: 1.5), 70 | q22-q19: (curve: 1.5), 71 | ), 72 | ) 73 | 74 | -------------------------------------------------------------------------------- /tests/layout-circular/.gitignore: -------------------------------------------------------------------------------- 1 | # generated by tytanic, do not edit 2 | 3 | diff/** 4 | out/** 5 | -------------------------------------------------------------------------------- /tests/layout-circular/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layout-circular/ref/1.png -------------------------------------------------------------------------------- /tests/layout-circular/test.typ: -------------------------------------------------------------------------------- 1 | #import "../../src/finite.typ" 2 | 3 | #set page(width: auto, height: auto, margin: 1cm) 4 | 5 | #let aut = range(6).fold( 6 | (:), 7 | (d, i) => { 8 | d.insert("q" + str(i), (:)) 9 | d 10 | }, 11 | ) 12 | 13 | #finite.automaton( 14 | aut, 15 | layout: finite.layout.circular.with(offset: 30deg), 16 | style: ( 17 | state: (radius: .8), 18 | q1: (radius: 1), 19 | q4: (radius: .4), 20 | ), 21 | ) 22 | -------------------------------------------------------------------------------- /tests/layouts/.gitignore: -------------------------------------------------------------------------------- 1 | # added by typst-test 2 | -------------------------------------------------------------------------------- /tests/layouts/diff/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/diff/1.png -------------------------------------------------------------------------------- /tests/layouts/diff/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/diff/2.png -------------------------------------------------------------------------------- /tests/layouts/diff/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/diff/3.png -------------------------------------------------------------------------------- /tests/layouts/diff/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/diff/4.png -------------------------------------------------------------------------------- /tests/layouts/diff/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/diff/5.png -------------------------------------------------------------------------------- /tests/layouts/diff/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/diff/6.png -------------------------------------------------------------------------------- /tests/layouts/out/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/out/1.png -------------------------------------------------------------------------------- /tests/layouts/out/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/out/2.png -------------------------------------------------------------------------------- /tests/layouts/out/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/out/3.png -------------------------------------------------------------------------------- /tests/layouts/out/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/out/4.png -------------------------------------------------------------------------------- /tests/layouts/out/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/out/5.png -------------------------------------------------------------------------------- /tests/layouts/out/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/out/6.png -------------------------------------------------------------------------------- /tests/layouts/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/ref/1.png -------------------------------------------------------------------------------- /tests/layouts/ref/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/ref/2.png -------------------------------------------------------------------------------- /tests/layouts/ref/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/ref/3.png -------------------------------------------------------------------------------- /tests/layouts/ref/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/ref/4.png -------------------------------------------------------------------------------- /tests/layouts/ref/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/ref/5.png -------------------------------------------------------------------------------- /tests/layouts/ref/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/ref/6.png -------------------------------------------------------------------------------- /tests/layouts/test.typ: -------------------------------------------------------------------------------- 1 | #import "../../src/finite.typ" 2 | 3 | #set page(width: auto, height: auto, margin: 1cm) 4 | 5 | #let aut = range(6).fold( 6 | (:), 7 | (d, i) => { 8 | d.insert("q" + str(i), ()) 9 | d 10 | }, 11 | ) 12 | #let spec = finite.create-automaton(aut) 13 | 14 | === layout.custom 15 | #finite.automaton( 16 | aut, 17 | layout: ( 18 | q0: (0, 0), 19 | q1: (4, 0), 20 | q2: (rel: (0, -3)), 21 | q3: (rel: (-1, -3), to: "q0"), 22 | rest: (rel: (2, -2)), 23 | ), 24 | ) 25 | 26 | #pagebreak() 27 | 28 | === layout.linear 29 | #finite.automaton( 30 | aut, 31 | layout: finite.layout.linear, 32 | ) 33 | 34 | #pagebreak() 35 | === layout.circular 36 | #finite.automaton( 37 | aut, 38 | layout: finite.layout.circular.with(offset: 30deg), 39 | style: (state: (radius: .8), q1: (radius: 1)), 40 | ) 41 | 42 | #pagebreak() 43 | === layout.grid 44 | #finite.automaton( 45 | aut, 46 | layout: finite.layout.grid, 47 | ) 48 | 49 | #pagebreak() 50 | === layout.snake 51 | #finite.automaton( 52 | aut, 53 | layout: finite.layout.snake, 54 | ) 55 | 56 | #pagebreak() 57 | === layout.group 58 | #finite.automaton( 59 | aut, 60 | layout: finite.layout.group.with( 61 | grouping: 4, 62 | spacing: 2, 63 | layout: ( 64 | finite.layout.circular.with(radius: 8), 65 | finite.layout.linear.with(dir: top), 66 | ), 67 | ), 68 | ) 69 | -------------------------------------------------------------------------------- /tests/spec/.gitignore: -------------------------------------------------------------------------------- 1 | # generated by tytanic, do not edit 2 | 3 | diff/** 4 | out/** 5 | -------------------------------------------------------------------------------- /tests/spec/test.typ: -------------------------------------------------------------------------------- 1 | #import "../../src/finite.typ" 2 | 3 | #let aut = ( 4 | q0: (q1: "λ", q5: "λ"), 5 | q1: (q2: 0), 6 | q2: (q3: 0), 7 | q3: (q4: 0), 8 | q4: (q1: "λ", q5: "λ"), 9 | q5: (q6: "λ"), 10 | q6: (q7: "λ", q9: "λ", q13: "λ"), 11 | q7: (q8: "λ"), 12 | q8: (q17: "λ"), 13 | q9: (q10: 0), 14 | q10: (q11: 1), 15 | q11: (q12: 1), 16 | q12: (q17: "λ"), 17 | q13: (q14: 0), 18 | q14: (q15: 0), 19 | q15: (q16: 1), 20 | q16: (q17: "λ"), 21 | q17: (q18: "λ"), 22 | q18: (q19: "λ", q23: "λ"), 23 | q19: (q20: 1), 24 | q20: (q21: 1), 25 | q21: (q22: 1), 26 | q22: (q19: "λ", q23: "λ"), 27 | q23: none, 28 | ) 29 | 30 | #let spec = finite.create-automaton(aut) 31 | 32 | #assert.eq(spec.type, "NEA") 33 | #assert.eq(spec.initial, "q0") 34 | #assert.eq(spec.final, ("q23",)) 35 | #assert.eq(spec.states.sorted(), aut.keys().sorted()) 36 | #assert.eq(spec.inputs.sorted(), ("0", "1", "λ")) 37 | -------------------------------------------------------------------------------- /tests/state-anchors/.gitignore: -------------------------------------------------------------------------------- 1 | # added by typst-test 2 | -------------------------------------------------------------------------------- /tests/state-anchors/diff/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state-anchors/diff/1.png -------------------------------------------------------------------------------- /tests/state-anchors/diff/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state-anchors/diff/2.png -------------------------------------------------------------------------------- /tests/state-anchors/out/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state-anchors/out/1.png -------------------------------------------------------------------------------- /tests/state-anchors/out/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state-anchors/out/2.png -------------------------------------------------------------------------------- /tests/state-anchors/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state-anchors/ref/1.png -------------------------------------------------------------------------------- /tests/state-anchors/ref/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state-anchors/ref/2.png -------------------------------------------------------------------------------- /tests/state-anchors/test.typ: -------------------------------------------------------------------------------- 1 | #import "../../src/finite.typ" 2 | 3 | #set page(width: auto, height: auto, margin: 1cm) 4 | 5 | #finite.cetz.canvas({ 6 | import finite.cetz.draw: * 7 | import finite.draw: * 8 | import "../test-utils.typ": dot 9 | 10 | let name = "q0" 11 | let anchor = "west" 12 | 13 | state((0, 0), name, initial: true, anchor: anchor) 14 | 15 | for-each-anchor( 16 | name, 17 | exclude: ("initial",), 18 | anchor => { 19 | dot(name + "." + anchor) 20 | }, 21 | ) 22 | dot(name + ".initial.start") 23 | dot(name + ".initial.end") 24 | }) 25 | 26 | #pagebreak() 27 | 28 | #finite.cetz.canvas({ 29 | import finite.cetz.draw: * 30 | import finite.draw: * 31 | import "../test-utils.typ": dot 32 | 33 | let name = "q0" 34 | let anchor = "west" 35 | 36 | state((0, 0), name, final: true, anchor: anchor) 37 | 38 | for-each-anchor( 39 | name, 40 | exclude: ("initial",), 41 | anchor => { 42 | dot(name + "." + anchor) 43 | }, 44 | ) 45 | }) 46 | -------------------------------------------------------------------------------- /tests/state-final/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | out 3 | 4 | diff 5 | -------------------------------------------------------------------------------- /tests/state-final/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state-final/ref/1.png -------------------------------------------------------------------------------- /tests/state-final/test.typ: -------------------------------------------------------------------------------- 1 | #import "../../src/finite.typ" 2 | 3 | #set page(width: auto, height: auto, margin: 1cm) 4 | 5 | #finite.cetz.canvas({ 6 | import finite.draw: state 7 | 8 | state((0, 0), "q0", final: true) 9 | state((2, 2), "q1", final: true, stroke: red) 10 | }) 11 | -------------------------------------------------------------------------------- /tests/state-initial/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | out 3 | 4 | diff 5 | -------------------------------------------------------------------------------- /tests/state-initial/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state-initial/ref/1.png -------------------------------------------------------------------------------- /tests/state-initial/ref/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state-initial/ref/2.png -------------------------------------------------------------------------------- /tests/state-initial/test.typ: -------------------------------------------------------------------------------- 1 | #import "../../src/finite.typ" 2 | 3 | #set page(width: auto, height: auto, margin: 1cm) 4 | 5 | #finite.cetz.canvas({ 6 | import finite.draw: state 7 | 8 | state((0, 0), "q0", initial: true) 9 | state((2, 2), "q1", initial: "Initial") 10 | state((4, 0), "q2", initial: bottom + right) 11 | state((2, -2), "q2", initial: (label: "Foo", anchor: bottom + left)) 12 | }) 13 | 14 | #pagebreak() 15 | 16 | 17 | #finite.cetz.canvas({ 18 | import finite.draw: state 19 | 20 | state( 21 | (0, 0), 22 | "A", 23 | initial: ( 24 | label: ( 25 | text: "Init", 26 | size: 16pt, 27 | ), 28 | anchor: bottom, 29 | ), 30 | stroke: red, 31 | ) 32 | }) 33 | -------------------------------------------------------------------------------- /tests/state-labels/diff/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state-labels/diff/1.png -------------------------------------------------------------------------------- /tests/state-labels/diff/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state-labels/diff/2.png -------------------------------------------------------------------------------- /tests/state-labels/out/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state-labels/out/1.png -------------------------------------------------------------------------------- /tests/state-labels/out/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state-labels/out/2.png -------------------------------------------------------------------------------- /tests/state-labels/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state-labels/ref/1.png -------------------------------------------------------------------------------- /tests/state-labels/ref/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state-labels/ref/2.png -------------------------------------------------------------------------------- /tests/state-labels/test.typ: -------------------------------------------------------------------------------- 1 | #import "../../src/finite.typ" 2 | 3 | #set page(width: auto, height: auto, margin: 1cm) 4 | 5 | #finite.automaton( 6 | (q0: (q1: 1), q1: (q0: 0)), 7 | style: ( 8 | q0-q1: (label: (pos: 0.8)), 9 | q1-q0: (label: (pos: 0.2)), 10 | ), 11 | ) 12 | 13 | #pagebreak() 14 | 15 | #finite.automaton( 16 | (q0: (q1: 1), q1: (q0: 0)), 17 | style: ( 18 | q0-q1: (label: (pos: 0.8)), 19 | transition: (label: (pos: 0.2)), 20 | ), 21 | ) 22 | -------------------------------------------------------------------------------- /tests/state/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | out 3 | 4 | diff 5 | -------------------------------------------------------------------------------- /tests/state/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state/ref/1.png -------------------------------------------------------------------------------- /tests/state/ref/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state/ref/2.png -------------------------------------------------------------------------------- /tests/state/test.typ: -------------------------------------------------------------------------------- 1 | #import "../../src/finite.typ" 2 | 3 | #set page(width: auto, height: auto, margin: 1cm) 4 | 5 | #finite.cetz.canvas({ 6 | import finite.draw: state 7 | 8 | state((0, 0), "q0") 9 | state((2, 2), "q1", stroke: red) 10 | state((4, 0), "q2", stroke: red, fill: red.lighten(80%)) 11 | state((2, -2), "q3", stroke: red, fill: red.lighten(80%), label: "T") 12 | }) 13 | 14 | #pagebreak() 15 | 16 | -------------------------------------------------------------------------------- /tests/test-utils.typ: -------------------------------------------------------------------------------- 1 | #import "../src/util.typ": cetz 2 | 3 | #let dot = cetz.draw.circle.with(fill: red, stroke: none, radius: .1) 4 | -------------------------------------------------------------------------------- /tests/transition-labels/.gitignore: -------------------------------------------------------------------------------- 1 | # added by typst-test 2 | -------------------------------------------------------------------------------- /tests/transition-labels/diff/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/transition-labels/diff/1.png -------------------------------------------------------------------------------- /tests/transition-labels/diff/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/transition-labels/diff/2.png -------------------------------------------------------------------------------- /tests/transition-labels/out/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/transition-labels/out/1.png -------------------------------------------------------------------------------- /tests/transition-labels/out/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/transition-labels/out/2.png -------------------------------------------------------------------------------- /tests/transition-labels/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/transition-labels/ref/1.png -------------------------------------------------------------------------------- /tests/transition-labels/ref/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/transition-labels/ref/2.png -------------------------------------------------------------------------------- /tests/transition-labels/test.typ: -------------------------------------------------------------------------------- 1 | #import "../../src/finite.typ" 2 | 3 | #set page(width: auto, height: auto, margin: 1cm) 4 | 5 | #finite.cetz.canvas({ 6 | import finite.draw: state, transition 7 | 8 | state((0, 0), "q0") 9 | state((4, 0), "q1") 10 | 11 | transition("q0", "q1", inputs: (1, 2, 3, 4)) 12 | transition("q1", "q0", inputs: (1, 2, 3, 4), label: "A") 13 | transition("q1", "q1", label: "1,2,3") 14 | }) 15 | 16 | #pagebreak() 17 | 18 | #finite.cetz.canvas({ 19 | import finite.draw: state, transition 20 | 21 | state((0, 0), "q0") 22 | state((4, 0), "q1") 23 | state((4, -4), "q2") 24 | state((0, -4), "q3") 25 | 26 | transition("q0", "q1", label: "A") 27 | transition("q1", "q2", label: "A", stroke: blue) 28 | transition("q2", "q3", label: (text: "A")) 29 | transition("q3", "q0", label: (text: "A", size: 2em)) 30 | transition("q2", "q1", label: (text: "A", fill: green, pos: .8, dist: .88), stroke: blue) 31 | transition("q0", "q3", label: (text: "A", size: 2em, pos: 0.2, dist: -.33, angle: 0deg), stroke: blue) 32 | }) 33 | -------------------------------------------------------------------------------- /tests/transition/.gitignore: -------------------------------------------------------------------------------- 1 | # added by typst-test 2 | -------------------------------------------------------------------------------- /tests/transition/diff/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/transition/diff/1.png -------------------------------------------------------------------------------- /tests/transition/diff/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/transition/diff/2.png -------------------------------------------------------------------------------- /tests/transition/diff/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/transition/diff/3.png -------------------------------------------------------------------------------- /tests/transition/out/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/transition/out/1.png -------------------------------------------------------------------------------- /tests/transition/out/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/transition/out/2.png -------------------------------------------------------------------------------- /tests/transition/out/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/transition/out/3.png -------------------------------------------------------------------------------- /tests/transition/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/transition/ref/1.png -------------------------------------------------------------------------------- /tests/transition/ref/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/transition/ref/2.png -------------------------------------------------------------------------------- /tests/transition/ref/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/transition/ref/3.png -------------------------------------------------------------------------------- /tests/transition/test.typ: -------------------------------------------------------------------------------- 1 | #import "../../src/finite.typ" 2 | 3 | #set page(width: auto, height: auto, margin: 1cm) 4 | 5 | #finite.cetz.canvas({ 6 | import finite.draw: state, transition 7 | 8 | state((0, 0), "q0") 9 | state((4, 0), "q1") 10 | 11 | transition("q0", "q1") 12 | transition("q1", "q1") 13 | }) 14 | 15 | #pagebreak() 16 | 17 | #finite.cetz.canvas({ 18 | import finite.draw: state, transition 19 | 20 | state((0, 0), "q0") 21 | state((4, 0), "q1") 22 | 23 | transition("q0", "q1", label: "x", curve: -1, stroke: .5pt + green) 24 | transition("q1", "q1", stroke: 2pt + red, mark: (end: "x")) 25 | }) 26 | 27 | #pagebreak() 28 | 29 | #finite.cetz.canvas({ 30 | import finite.draw: state, transition 31 | 32 | state((0, 0), "q0") 33 | state((4, 2), "q1") 34 | 35 | transition("q0", "q1") 36 | transition("q1", "q1", anchor: right) 37 | }) 38 | 39 | -------------------------------------------------------------------------------- /tests/ttable/.gitignore: -------------------------------------------------------------------------------- 1 | # generated by tytanic, do not edit 2 | 3 | diff/** 4 | out/** 5 | -------------------------------------------------------------------------------- /tests/ttable/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/ttable/ref/1.png -------------------------------------------------------------------------------- /tests/ttable/ref/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/ttable/ref/2.png -------------------------------------------------------------------------------- /tests/ttable/ref/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/ttable/ref/3.png -------------------------------------------------------------------------------- /tests/ttable/test.typ: -------------------------------------------------------------------------------- 1 | #import "../../src/finite.typ" 2 | 3 | #set page(width: auto, height: auto, margin: 1cm) 4 | 5 | #let aut = ( 6 | q0: (q1: "a"), 7 | q1: (q1: ("a", "b"), q2: ("c", "b")), 8 | q2: (q0: "b", q2: "c"), 9 | ) 10 | #finite.transition-table(aut) 11 | 12 | #pagebreak() 13 | 14 | #finite.transition-table( 15 | aut, 16 | format: (c, r, v) => if c == 0 and r == 0 [ 17 | $delta$ 18 | ] else if c == 0 [ 19 | #strong(v) 20 | ] else { 21 | raw(str(v)) 22 | }, 23 | ) 24 | 25 | #pagebreak() 26 | 27 | #finite.transition-table( 28 | aut, 29 | fill: (c, r) => (orange, yellow).at(calc.rem(c + r, 2)), 30 | format: (c, r, v) => text((yellow, orange).at(calc.rem(c + r, 2)), weight: "bold", v), 31 | ) 32 | -------------------------------------------------------------------------------- /typst.toml: -------------------------------------------------------------------------------- 1 | 2 | [package] 3 | name = "finite" 4 | version = "0.5.0" 5 | entrypoint = "src/finite.typ" 6 | authors = ["Jonas Neugebauer"] 7 | license = "MIT" 8 | description = "Typst-setting finite automata with CeTZ" 9 | repository = "https://github.com/jneug/typst-finite" 10 | keywords = [] 11 | categories = [] 12 | disciplines = [] 13 | compiler = "0.13.0" 14 | exclude = [ 15 | ".github", 16 | "docs", 17 | "scripts", 18 | "tests", 19 | "assets", 20 | ".typstignore", 21 | "Justfile", 22 | ] 23 | --------------------------------------------------------------------------------