├── .github └── workflows │ ├── general.yml │ ├── gh-pages.yml │ └── release.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── crates ├── flou │ ├── Cargo.toml │ ├── LICENSE-APACHE │ ├── LICENSE-MIT │ ├── README.md │ └── src │ │ ├── css │ │ └── default.css │ │ ├── lib.rs │ │ ├── parse │ │ ├── ast.rs │ │ ├── combinators.rs │ │ ├── constants.rs │ │ ├── mod.rs │ │ ├── parts.rs │ │ └── types.rs │ │ ├── parts │ │ ├── error.rs │ │ ├── flou.rs │ │ ├── grid.rs │ │ └── mod.rs │ │ ├── pos.rs │ │ ├── render_svg │ │ ├── mod.rs │ │ ├── node.rs │ │ ├── path.rs │ │ ├── renderer.rs │ │ └── viewport.rs │ │ ├── svg │ │ ├── arrowhead.rs │ │ ├── element.rs │ │ ├── mod.rs │ │ ├── path.rs │ │ └── text.rs │ │ └── test.rs └── flou_cli │ ├── Cargo.toml │ ├── LICENSE-APACHE │ ├── LICENSE-MIT │ ├── README.md │ └── src │ ├── lib.rs │ └── main.rs └── docs ├── .gitignore ├── book.toml └── src ├── README.md ├── SUMMARY.md ├── cli.md ├── install.md ├── styling_flowchart.md ├── styling_flowchart ├── example1.svg └── example2.svg └── syntax ├── README.md ├── define_block.md ├── define_block ├── example1.svg ├── example2.svg ├── example3.svg └── example4.svg ├── hello_world.md ├── hello_world ├── example1.svg └── example2.svg ├── list_of_attributes.md ├── making_connections.md └── making_connections ├── example1.svg ├── example2.svg ├── example3.svg └── example4.svg /.github/workflows/general.yml: -------------------------------------------------------------------------------- 1 | name: General 2 | on: [push, pull_request] 3 | 4 | env: 5 | CARGO_TERM_COLOR: always 6 | 7 | jobs: 8 | check: 9 | name: Check code 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout sources 13 | uses: actions/checkout@v2 14 | 15 | - name: Install stable toolchain 16 | uses: actions-rs/toolchain@v1 17 | with: 18 | profile: minimal 19 | toolchain: stable 20 | override: true 21 | 22 | - uses: Swatinem/rust-cache@v1 23 | 24 | - name: Run cargo check 25 | uses: actions-rs/cargo@v1 26 | with: 27 | command: check 28 | 29 | test: 30 | name: Run tests 31 | strategy: 32 | matrix: 33 | os: [ubuntu-latest, macos-latest, windows-latest] 34 | rust: [stable] 35 | runs-on: ${{ matrix.os }} 36 | steps: 37 | - name: Checkout sources 38 | uses: actions/checkout@v2 39 | 40 | - name: Install stable toolchain 41 | uses: actions-rs/toolchain@v1 42 | with: 43 | profile: minimal 44 | toolchain: ${{ matrix.rust }} 45 | override: true 46 | 47 | - uses: Swatinem/rust-cache@v1 48 | 49 | - name: Run cargo test 50 | uses: actions-rs/cargo@v1 51 | with: 52 | command: test 53 | 54 | 55 | lints: 56 | name: Lint code 57 | runs-on: ubuntu-latest 58 | steps: 59 | - name: Checkout sources 60 | uses: actions/checkout@v2 61 | with: 62 | submodules: true 63 | 64 | - name: Install stable toolchain 65 | uses: actions-rs/toolchain@v1 66 | with: 67 | profile: minimal 68 | toolchain: stable 69 | override: true 70 | components: rustfmt, clippy 71 | 72 | - uses: Swatinem/rust-cache@v1 73 | 74 | - name: Run cargo fmt 75 | uses: actions-rs/cargo@v1 76 | with: 77 | command: fmt 78 | args: --all -- --check 79 | 80 | - name: Run cargo clippy 81 | uses: actions-rs/cargo@v1 82 | with: 83 | command: clippy 84 | args: -- -D warnings -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Github Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-20.04 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | 18 | - name: Setup mdBook 19 | uses: peaceiris/actions-mdbook@v1 20 | with: 21 | mdbook-version: 'latest' 22 | 23 | - name: Build website 24 | run: mdbook build docs 25 | 26 | - name: Deploy 27 | uses: peaceiris/actions-gh-pages@v3 28 | if: ${{ github.ref == 'refs/heads/master' }} 29 | with: 30 | github_token: ${{ secrets.GITHUB_TOKEN }} 31 | publish_dir: ./docs/book -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - 'v[0-9]+.[0-9]+.[0-9]+' 6 | 7 | env: 8 | BIN_NAME: flou 9 | PROJECT_NAME: flou 10 | REPO_NAME: asha20/flou 11 | 12 | jobs: 13 | dist: 14 | name: Dist 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | build: [x86_64-linux, aarch64-linux, x86_64-macos, x86_64-windows] 20 | include: 21 | - build: x86_64-linux 22 | os: ubuntu-20.04 23 | rust: stable 24 | target: x86_64-unknown-linux-gnu 25 | cross: false 26 | - build: aarch64-linux 27 | os: ubuntu-20.04 28 | rust: stable 29 | target: aarch64-unknown-linux-gnu 30 | cross: true 31 | - build: x86_64-macos 32 | os: macos-latest 33 | rust: stable 34 | target: x86_64-apple-darwin 35 | cross: false 36 | - build: x86_64-windows 37 | os: windows-2019 38 | rust: stable 39 | target: x86_64-pc-windows-msvc 40 | cross: false 41 | 42 | steps: 43 | - name: Checkout sources 44 | uses: actions/checkout@v2 45 | with: 46 | submodules: true 47 | 48 | - name: Install ${{ matrix.rust }} toolchain 49 | uses: actions-rs/toolchain@v1 50 | with: 51 | profile: minimal 52 | toolchain: ${{ matrix.rust }} 53 | target: ${{ matrix.target }} 54 | override: true 55 | 56 | - name: Run cargo test 57 | uses: actions-rs/cargo@v1 58 | with: 59 | use-cross: ${{ matrix.cross }} 60 | command: test 61 | args: --release --locked --target ${{ matrix.target }} 62 | 63 | - name: Build release binary 64 | uses: actions-rs/cargo@v1 65 | with: 66 | use-cross: ${{ matrix.cross }} 67 | command: build 68 | args: --release --locked --target ${{ matrix.target }} 69 | 70 | - name: Strip release binary (linux and macos) 71 | if: matrix.build == 'x86_64-linux' || matrix.build == 'x86_64-macos' 72 | run: strip "target/${{ matrix.target }}/release/$BIN_NAME" 73 | 74 | - name: Strip release binary (arm) 75 | if: matrix.build == 'aarch64-linux' 76 | run: | 77 | docker run --rm -v \ 78 | "$PWD/target:/target:Z" \ 79 | rustembedded/cross:${{ matrix.target }} \ 80 | aarch64-linux-gnu-strip \ 81 | /target/${{ matrix.target }}/release/$BIN_NAME 82 | 83 | - name: Build archive 84 | shell: bash 85 | run: | 86 | mkdir dist 87 | if [ "${{ matrix.os }}" = "windows-2019" ]; then 88 | cp "target/${{ matrix.target }}/release/$BIN_NAME.exe" "dist/" 89 | else 90 | cp "target/${{ matrix.target }}/release/$BIN_NAME" "dist/" 91 | fi 92 | 93 | - uses: actions/upload-artifact@v2.2.4 94 | with: 95 | name: bins-${{ matrix.build }} 96 | path: dist 97 | 98 | publish: 99 | name: Publish 100 | needs: [dist] 101 | runs-on: ubuntu-latest 102 | steps: 103 | - name: Checkout sources 104 | uses: actions/checkout@v2 105 | with: 106 | submodules: false 107 | 108 | - uses: actions/download-artifact@v2 109 | 110 | - run: ls -al bins-* 111 | 112 | - name: Calculate tag name 113 | run: | 114 | name=dev 115 | if [[ $GITHUB_REF == refs/tags/v* ]]; then 116 | name=${GITHUB_REF:10} 117 | fi 118 | echo ::set-output name=val::$name 119 | echo TAG=$name >> $GITHUB_ENV 120 | id: tagname 121 | 122 | - name: Build archive 123 | shell: bash 124 | run: | 125 | set -ex 126 | 127 | rm -rf tmp 128 | mkdir tmp 129 | mkdir dist 130 | 131 | for dir in bins-* ; do 132 | platform=${dir#"bins-"} 133 | if [[ $platform =~ "windows" ]]; then 134 | exe=".exe" 135 | fi 136 | pkgname=$PROJECT_NAME-$TAG-$platform 137 | mkdir tmp/$pkgname 138 | # cp LICENSE README.md tmp/$pkgname 139 | mv bins-$platform/$BIN_NAME$exe tmp/$pkgname 140 | chmod +x tmp/$pkgname/$BIN_NAME$exe 141 | 142 | if [ "$exe" = "" ]; then 143 | tar cJf dist/$pkgname.tar.xz -C tmp $pkgname 144 | else 145 | (cd tmp && 7z a -r ../dist/$pkgname.zip $pkgname) 146 | fi 147 | done 148 | 149 | - name: Upload binaries to release 150 | uses: svenstaro/upload-release-action@v2 151 | with: 152 | repo_token: ${{ secrets.GITHUB_TOKEN }} 153 | file: dist/* 154 | file_glob: true 155 | tag: ${{ steps.tagname.outputs.val }} 156 | overwrite: true 157 | 158 | - name: Extract version 159 | id: extract-version 160 | run: | 161 | printf "::set-output name=%s::%s\n" tag-name "${GITHUB_REF#refs/tags/}" 162 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .vscode/ 3 | .project/ -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ansi_term" 7 | version = "0.11.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 10 | dependencies = [ 11 | "winapi", 12 | ] 13 | 14 | [[package]] 15 | name = "ansi_term" 16 | version = "0.12.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 19 | dependencies = [ 20 | "winapi", 21 | ] 22 | 23 | [[package]] 24 | name = "arrayvec" 25 | version = "0.7.1" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "be4dc07131ffa69b8072d35f5007352af944213cde02545e2103680baed38fcd" 28 | 29 | [[package]] 30 | name = "atty" 31 | version = "0.2.14" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 34 | dependencies = [ 35 | "hermit-abi", 36 | "libc", 37 | "winapi", 38 | ] 39 | 40 | [[package]] 41 | name = "autocfg" 42 | version = "1.0.1" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 45 | 46 | [[package]] 47 | name = "bitflags" 48 | version = "1.3.2" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 51 | 52 | [[package]] 53 | name = "brownstone" 54 | version = "1.0.1" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "83b83224877479161e925498306710f448b13a7f6fdcf69022a60dad1fe3d1bd" 57 | dependencies = [ 58 | "arrayvec", 59 | ] 60 | 61 | [[package]] 62 | name = "clap" 63 | version = "2.33.3" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" 66 | dependencies = [ 67 | "ansi_term 0.11.0", 68 | "atty", 69 | "bitflags", 70 | "strsim", 71 | "textwrap", 72 | "unicode-width", 73 | "vec_map", 74 | ] 75 | 76 | [[package]] 77 | name = "ctor" 78 | version = "0.1.21" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "ccc0a48a9b826acdf4028595adc9db92caea352f7af011a3034acd172a52a0aa" 81 | dependencies = [ 82 | "quote", 83 | "syn", 84 | ] 85 | 86 | [[package]] 87 | name = "diff" 88 | version = "0.1.12" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" 91 | 92 | [[package]] 93 | name = "flou" 94 | version = "0.1.0" 95 | dependencies = [ 96 | "nom", 97 | "nom-supreme", 98 | "num-traits", 99 | "pretty_assertions", 100 | ] 101 | 102 | [[package]] 103 | name = "flou_cli" 104 | version = "0.1.0" 105 | dependencies = [ 106 | "flou", 107 | "structopt", 108 | ] 109 | 110 | [[package]] 111 | name = "heck" 112 | version = "0.3.3" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" 115 | dependencies = [ 116 | "unicode-segmentation", 117 | ] 118 | 119 | [[package]] 120 | name = "hermit-abi" 121 | version = "0.1.19" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 124 | dependencies = [ 125 | "libc", 126 | ] 127 | 128 | [[package]] 129 | name = "indent_write" 130 | version = "2.2.0" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "0cfe9645a18782869361d9c8732246be7b410ad4e919d3609ebabdac00ba12c3" 133 | 134 | [[package]] 135 | name = "joinery" 136 | version = "2.1.0" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "72167d68f5fce3b8655487b8038691a3c9984ee769590f93f2a631f4ad64e4f5" 139 | 140 | [[package]] 141 | name = "lazy_static" 142 | version = "1.4.0" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 145 | 146 | [[package]] 147 | name = "libc" 148 | version = "0.2.106" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "a60553f9a9e039a333b4e9b20573b9e9b9c0bb3a11e201ccc48ef4283456d673" 151 | 152 | [[package]] 153 | name = "memchr" 154 | version = "2.4.1" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 157 | 158 | [[package]] 159 | name = "minimal-lexical" 160 | version = "0.1.4" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "9c64630dcdd71f1a64c435f54885086a0de5d6a12d104d69b165fb7d5286d677" 163 | 164 | [[package]] 165 | name = "nom" 166 | version = "7.0.0" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "7ffd9d26838a953b4af82cbeb9f1592c6798916983959be223a7124e992742c1" 169 | dependencies = [ 170 | "memchr", 171 | "minimal-lexical", 172 | "version_check", 173 | ] 174 | 175 | [[package]] 176 | name = "nom-supreme" 177 | version = "0.6.0" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "aadc66631948f6b65da03be4c4cd8bd104d481697ecbb9bbd65719b1ec60bc9f" 180 | dependencies = [ 181 | "brownstone", 182 | "indent_write", 183 | "joinery", 184 | "memchr", 185 | "nom", 186 | ] 187 | 188 | [[package]] 189 | name = "num-traits" 190 | version = "0.2.14" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 193 | dependencies = [ 194 | "autocfg", 195 | ] 196 | 197 | [[package]] 198 | name = "output_vt100" 199 | version = "0.1.2" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9" 202 | dependencies = [ 203 | "winapi", 204 | ] 205 | 206 | [[package]] 207 | name = "pretty_assertions" 208 | version = "1.0.0" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "ec0cfe1b2403f172ba0f234e500906ee0a3e493fb81092dac23ebefe129301cc" 211 | dependencies = [ 212 | "ansi_term 0.12.1", 213 | "ctor", 214 | "diff", 215 | "output_vt100", 216 | ] 217 | 218 | [[package]] 219 | name = "proc-macro-error" 220 | version = "1.0.4" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 223 | dependencies = [ 224 | "proc-macro-error-attr", 225 | "proc-macro2", 226 | "quote", 227 | "syn", 228 | "version_check", 229 | ] 230 | 231 | [[package]] 232 | name = "proc-macro-error-attr" 233 | version = "1.0.4" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 236 | dependencies = [ 237 | "proc-macro2", 238 | "quote", 239 | "version_check", 240 | ] 241 | 242 | [[package]] 243 | name = "proc-macro2" 244 | version = "1.0.30" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70" 247 | dependencies = [ 248 | "unicode-xid", 249 | ] 250 | 251 | [[package]] 252 | name = "quote" 253 | version = "1.0.10" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" 256 | dependencies = [ 257 | "proc-macro2", 258 | ] 259 | 260 | [[package]] 261 | name = "strsim" 262 | version = "0.8.0" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 265 | 266 | [[package]] 267 | name = "structopt" 268 | version = "0.3.25" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "40b9788f4202aa75c240ecc9c15c65185e6a39ccdeb0fd5d008b98825464c87c" 271 | dependencies = [ 272 | "clap", 273 | "lazy_static", 274 | "structopt-derive", 275 | ] 276 | 277 | [[package]] 278 | name = "structopt-derive" 279 | version = "0.4.18" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" 282 | dependencies = [ 283 | "heck", 284 | "proc-macro-error", 285 | "proc-macro2", 286 | "quote", 287 | "syn", 288 | ] 289 | 290 | [[package]] 291 | name = "syn" 292 | version = "1.0.80" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194" 295 | dependencies = [ 296 | "proc-macro2", 297 | "quote", 298 | "unicode-xid", 299 | ] 300 | 301 | [[package]] 302 | name = "textwrap" 303 | version = "0.11.0" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 306 | dependencies = [ 307 | "unicode-width", 308 | ] 309 | 310 | [[package]] 311 | name = "unicode-segmentation" 312 | version = "1.8.0" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" 315 | 316 | [[package]] 317 | name = "unicode-width" 318 | version = "0.1.9" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" 321 | 322 | [[package]] 323 | name = "unicode-xid" 324 | version = "0.2.2" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 327 | 328 | [[package]] 329 | name = "vec_map" 330 | version = "0.8.2" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 333 | 334 | [[package]] 335 | name = "version_check" 336 | version = "0.9.3" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" 339 | 340 | [[package]] 341 | name = "winapi" 342 | version = "0.3.9" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 345 | dependencies = [ 346 | "winapi-i686-pc-windows-gnu", 347 | "winapi-x86_64-pc-windows-gnu", 348 | ] 349 | 350 | [[package]] 351 | name = "winapi-i686-pc-windows-gnu" 352 | version = "0.4.0" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 355 | 356 | [[package]] 357 | name = "winapi-x86_64-pc-windows-gnu" 358 | version = "0.4.0" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 361 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "crates/flou", 5 | "crates/flou_cli", 6 | ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flou 2 | 3 | Flou is a [domain-specific language (DSL)](https://en.wikipedia.org/wiki/Domain-specific_language) for describing flowcharts. It is also a CLI of the same name that renders the previously mentioned flowchart description into an SVG file. 4 | 5 | Flou's goal is to offer a textual representation of flowcharts. Here's one example of the kind of flowcharts Flou can create: 6 | 7 | ![Example](docs/src/syntax/define_block/example1.svg) 8 | 9 | Here's the corresponding Flou definition: 10 | 11 | ```js 12 | grid { 13 | block("Think about going outside", connect: s:n@s); 14 | condition("Is it raining?"), block#stay("Stay inside"); 15 | condition("Is it cold?"); 16 | condition("Is it night?"); 17 | block("Go outside"); 18 | } 19 | 20 | define { 21 | block(class: "pink"); 22 | condition(shape: diamond, class: "yellow", connect: { 23 | s:n@s("No"); 24 | e:w#stay("Yes"); 25 | }); 26 | } 27 | ``` 28 | 29 | ## Documentation 30 | 31 | Flou's documentation and user guide can be found [here](https://asha20.github.io/flou). 32 | 33 | ## Installation 34 | 35 | You can grab a prebuilt binary for your operating system [here](https://github.com/Asha20/flou/releases). Alternatively, if you have Cargo installed, you can run: 36 | 37 | $ cargo install flou_cli 38 | 39 | Which will install the `flou` binary for you to use. 40 | 41 | ## Reasons to use Flou? 42 | 43 | - If you need to generate a flowchart automatically, you can write a program that generates Flou DSL and then use the CLI tool to compile the DSL into an image. 44 | - Textual representation avoids easy-to-miss slight design inconsistencies that might occur when creating a flowchart with a visual design software. 45 | - Flou makes modifying shared flowchart parts straightforward and painless. 46 | - A textual flowchart representation is more suited for version control. 47 | 48 | ## Reasons NOT to use Flou? 49 | 50 | - It's still in beta. This means some features might be unpolished. 51 | - Connections that happen to have overlapping segments can bring visual ambiguity since Flou CLI won't render them side by side and will overlap them instead. However, this issue can be offset by the user since they can pick and choose the connection sides. -------------------------------------------------------------------------------- /crates/flou/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flou" 3 | description = "Parser for Flou, a flowchart description language." 4 | homepage = "https://asha20.github.io/flou" 5 | repository = "https://github.com/Asha20/flou" 6 | readme = "../../README.md" 7 | license = "MIT OR Apache-2.0" 8 | keywords = ["flowchart"] 9 | 10 | version = "0.1.0" 11 | edition = "2018" 12 | 13 | [dependencies] 14 | nom = "7.0.0" 15 | nom-supreme = "0.6.0" 16 | num-traits = "0.2.14" 17 | 18 | [dev-dependencies] 19 | pretty_assertions = "1.0.0" 20 | -------------------------------------------------------------------------------- /crates/flou/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Vukašin Stepanović 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /crates/flou/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Vukašin Stepanović 4 | 5 | Permission is hereby granted, free of charge, to any 6 | person obtaining a copy of this software and associated 7 | documentation files (the "Software"), to deal in the 8 | Software without restriction, including without 9 | limitation the rights to use, copy, modify, merge, 10 | publish, distribute, sublicense, and/or sell copies of 11 | the Software, and to permit persons to whom the Software 12 | is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice 16 | shall be included in all copies or substantial portions 17 | of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 20 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 21 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 22 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 23 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 24 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 25 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 26 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 27 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /crates/flou/README.md: -------------------------------------------------------------------------------- 1 | # flou 2 | 3 | This library crate parses Flou's DSL. -------------------------------------------------------------------------------- /crates/flou/src/css/default.css: -------------------------------------------------------------------------------- 1 | .background { 2 | fill: #eee; 3 | } 4 | 5 | .node { 6 | fill: #fff; 7 | stroke: #1e1e1e; 8 | } 9 | 10 | text, 11 | tspan { 12 | fill: #1e1e1e; 13 | text-anchor: middle; 14 | dominant-baseline: middle; 15 | } 16 | 17 | .connection-text { 18 | dominant-baseline: auto; 19 | } 20 | 21 | .path { 22 | fill: none; 23 | stroke: #1e1e1e; 24 | stroke-width: 1; 25 | shape-rendering: crispEdges; 26 | } 27 | 28 | .connection .arrowhead { 29 | stroke: #1e1e1e; 30 | fill: #1e1e1e; 31 | } 32 | -------------------------------------------------------------------------------- /crates/flou/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod test; 3 | 4 | mod parse; 5 | pub mod parts; 6 | mod pos; 7 | 8 | mod render_svg; 9 | mod svg; 10 | 11 | pub use parts::{Flou, FlouError, LogicError, RenderConfig, Renderer, ResolutionError}; 12 | pub use render_svg::SvgRenderer; 13 | -------------------------------------------------------------------------------- /crates/flou/src/parse/ast.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::fmt; 4 | 5 | use nom::{ 6 | branch::{alt, permutation}, 7 | bytes::complete::take_while, 8 | character::{ 9 | complete::{anychar, char}, 10 | is_alphabetic, is_alphanumeric, 11 | }, 12 | combinator::{map, opt, recognize, value, verify}, 13 | multi::many1, 14 | sequence::{pair, preceded, separated_pair, terminated, tuple}, 15 | Parser, 16 | }; 17 | use nom_supreme::{final_parser::final_parser, tag::complete::tag, ParserExt}; 18 | 19 | use crate::{ 20 | parse::combinators::enclosed_list0, 21 | pos::{pos, IndexPos}, 22 | }; 23 | 24 | use super::{ 25 | combinators::{attribute, block, list1, space, ws}, 26 | constants::*, 27 | parts::quoted_string, 28 | types::{Input, Result}, 29 | Error, 30 | }; 31 | 32 | #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] 33 | pub struct Identifier<'i>(pub(crate) &'i str); 34 | 35 | impl<'i> Identifier<'i> { 36 | pub(crate) fn parse(i: Input<'i>) -> Result { 37 | let wchar = take_while(|x: char| x == '_' || is_alphanumeric(x as u8)); 38 | map( 39 | recognize(pair( 40 | verify(anychar, |&c| c == '_' || is_alphabetic(c as u8)), 41 | wchar, 42 | )), 43 | Self, 44 | )(i) 45 | } 46 | } 47 | 48 | impl fmt::Display for Identifier<'_> { 49 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 50 | self.0.fmt(f) 51 | } 52 | } 53 | 54 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 55 | pub(crate) enum NodeShape { 56 | Rectangle, 57 | Square, 58 | Ellipse, 59 | Circle, 60 | Diamond, 61 | AngledSquare, 62 | } 63 | 64 | impl NodeShape { 65 | pub(crate) fn parse(i: Input) -> Result { 66 | alt(( 67 | value(Self::Rectangle, tag("rect")), 68 | value(Self::Square, tag("square")), 69 | value(Self::Ellipse, tag("ellipse")), 70 | value(Self::Circle, tag("circle")), 71 | value(Self::Diamond, tag("diamond")), 72 | value(Self::AngledSquare, tag("angled_square")), 73 | ))(i) 74 | } 75 | } 76 | 77 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 78 | pub enum Direction { 79 | North, 80 | South, 81 | West, 82 | East, 83 | } 84 | 85 | impl Direction { 86 | pub(crate) fn parse(i: Input) -> Result { 87 | alt(( 88 | value(Self::North, tag("n")), 89 | value(Self::South, tag("s")), 90 | value(Self::West, tag("w")), 91 | value(Self::East, tag("e")), 92 | ))(i) 93 | } 94 | } 95 | 96 | impl fmt::Display for Direction { 97 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 98 | let s = match self { 99 | Direction::North => "North", 100 | Direction::South => "South", 101 | Direction::West => "West", 102 | Direction::East => "East", 103 | }; 104 | 105 | f.write_str(s) 106 | } 107 | } 108 | 109 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 110 | pub(crate) enum Destination<'i> { 111 | Itself, 112 | Relative(Direction), 113 | Label(Identifier<'i>), 114 | } 115 | 116 | impl<'i> Destination<'i> { 117 | pub(crate) fn parse(i: Input<'i>) -> Result { 118 | alt(( 119 | preceded( 120 | char(RELATIVE_SIGIL), 121 | map(opt(Direction::parse), |dir| match dir { 122 | Some(dir) => Self::Relative(dir), 123 | None => Self::Itself, 124 | }), 125 | ), 126 | map(preceded(char(LABEL_SIGIL), Identifier::parse), Self::Label), 127 | ))(i) 128 | } 129 | } 130 | 131 | #[derive(Debug, PartialEq, Eq, Clone)] 132 | pub(crate) enum NodeAttribute<'i> { 133 | Text(String), 134 | Class(String), 135 | Shape(NodeShape), 136 | Connect(Vec>), 137 | } 138 | 139 | impl<'i> NodeAttribute<'i> { 140 | fn parse(i: Input<'i>) -> Result { 141 | let connection_descriptors = alt(( 142 | map(ConnectionDescriptor::parse, |x| vec![x]), 143 | enclosed_list0(BLOCK_DELIMITERS, ConnectionDescriptor::parse, TERMINATOR), 144 | )); 145 | 146 | alt(( 147 | map(attribute("text", quoted_string), Self::Text), 148 | map(attribute("class", quoted_string), Self::Class), 149 | map(attribute("shape", NodeShape::parse), Self::Shape), 150 | map(attribute("connect", connection_descriptors), Self::Connect), 151 | ))(i) 152 | } 153 | 154 | fn parse_vec(i: Input<'i>) -> Result> { 155 | alt(( 156 | map(quoted_string.terminated(opt(char(LIST_SEPARATOR))), |x| { 157 | vec![Self::Text(x)] 158 | }) 159 | .terminated(char(LIST_DELIMITERS.1)), 160 | map( 161 | pair( 162 | quoted_string.terminated(ws(char(LIST_SEPARATOR))), 163 | list1(Self::parse, LIST_SEPARATOR, LIST_DELIMITERS.1), 164 | ), 165 | |(text_shorthand, mut tail)| { 166 | tail.insert(0, Self::Text(text_shorthand)); 167 | tail 168 | }, 169 | ), 170 | list1(Self::parse, LIST_SEPARATOR, LIST_DELIMITERS.1), 171 | )) 172 | .preceded_by(char(LIST_DELIMITERS.0)) 173 | .parse(i) 174 | } 175 | 176 | pub(crate) fn as_key(&self) -> &'static str { 177 | match self { 178 | NodeAttribute::Text(_) => "text", 179 | NodeAttribute::Class(_) => "class", 180 | NodeAttribute::Shape(_) => "shape", 181 | NodeAttribute::Connect(_) => "connect", 182 | } 183 | } 184 | } 185 | 186 | #[derive(Debug, PartialEq, Eq, Clone)] 187 | pub(crate) struct ConnectionDescriptor<'i> { 188 | pub(crate) to: Destination<'i>, 189 | pub(crate) sides: (Direction, Direction), 190 | pub(crate) attrs: Vec, 191 | } 192 | 193 | impl<'i> ConnectionDescriptor<'i> { 194 | pub(crate) fn parse(i: Input<'i>) -> Result { 195 | let sides = separated_pair(Direction::parse, char(SIDES_SIGIL), Direction::parse); 196 | 197 | map( 198 | tuple(( 199 | sides, 200 | Destination::parse, 201 | opt(ConnectionAttribute::parse_vec), 202 | )), 203 | |(sides, to, attrs)| Self { 204 | to, 205 | sides, 206 | attrs: attrs.unwrap_or_default(), 207 | }, 208 | )(i) 209 | } 210 | } 211 | 212 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 213 | pub(crate) enum ArrowheadType { 214 | None, 215 | Start, 216 | End, 217 | Both, 218 | } 219 | 220 | impl Default for ArrowheadType { 221 | fn default() -> Self { 222 | Self::End 223 | } 224 | } 225 | 226 | #[derive(Debug, PartialEq, Eq, Clone)] 227 | pub(crate) enum ConnectionAttribute { 228 | Text(String), 229 | Class(String), 230 | Arrowheads(ArrowheadType), 231 | } 232 | 233 | impl ConnectionAttribute { 234 | pub(crate) fn parse(i: Input) -> Result { 235 | let arrowheads = alt(( 236 | value(ArrowheadType::None, tag("none")), 237 | value(ArrowheadType::Start, tag("start")), 238 | value(ArrowheadType::End, tag("end")), 239 | value(ArrowheadType::Both, tag("both")), 240 | )); 241 | 242 | alt(( 243 | map(attribute("text", quoted_string), Self::Text), 244 | map(attribute("class", quoted_string), Self::Class), 245 | map(attribute("arrowheads", arrowheads), Self::Arrowheads), 246 | ))(i) 247 | } 248 | 249 | pub(crate) fn parse_vec(i: Input) -> Result> { 250 | alt(( 251 | map(quoted_string, |x| vec![Self::Text(x)]).terminated(char(LIST_DELIMITERS.1)), 252 | map( 253 | pair( 254 | quoted_string.terminated(ws(char(LIST_SEPARATOR))), 255 | list1(Self::parse, LIST_SEPARATOR, LIST_DELIMITERS.1), 256 | ), 257 | |(text_shorthand, mut tail)| { 258 | tail.insert(0, Self::Text(text_shorthand)); 259 | tail 260 | }, 261 | ), 262 | list1(Self::parse, LIST_SEPARATOR, LIST_DELIMITERS.1), 263 | )) 264 | .preceded_by(char(LIST_DELIMITERS.0)) 265 | .parse(i) 266 | } 267 | 268 | pub(crate) fn as_key(&self) -> &'static str { 269 | match self { 270 | Self::Text(_) => "text", 271 | Self::Class(_) => "class", 272 | Self::Arrowheads(_) => "arrowheads", 273 | } 274 | } 275 | } 276 | 277 | #[derive(Debug, PartialEq, Eq)] 278 | pub(crate) struct Node<'i> { 279 | pub(crate) id: Identifier<'i>, 280 | pub(crate) label: Option>, 281 | pub(crate) attrs: Vec>, 282 | } 283 | 284 | impl<'i> Node<'i> { 285 | pub(crate) fn parse(i: Input<'i>) -> Result { 286 | map( 287 | tuple(( 288 | Identifier::parse, 289 | opt(preceded(char(LABEL_SIGIL), Identifier::parse)), 290 | opt(NodeAttribute::parse_vec), 291 | )), 292 | |(id, label, attrs)| Self { 293 | id, 294 | label, 295 | attrs: attrs.unwrap_or_default(), 296 | }, 297 | )(i) 298 | } 299 | } 300 | 301 | #[derive(Debug, PartialEq, Eq)] 302 | pub(crate) struct Grid<'i>(Vec>>>); 303 | 304 | impl<'i> Grid<'i> { 305 | pub(crate) fn parse(i: Input<'i>) -> Result { 306 | let empty = tag(EMPTY); 307 | let opt_node = alt((map(empty, |_| None), map(Node::parse, Some))); 308 | let row = list1(opt_node, LIST_SEPARATOR, TERMINATOR); 309 | let grid = map(many1(ws(row)), Self); 310 | 311 | preceded(terminated(tag("grid"), space), block(grid))(i) 312 | } 313 | 314 | pub(crate) fn nodes(&self) -> impl Iterator)> { 315 | self.0.iter().enumerate().flat_map(|(y, row)| { 316 | row.iter() 317 | .enumerate() 318 | .filter_map(move |(x, node)| node.as_ref().map(|node| (pos(x, y).into(), node))) 319 | }) 320 | } 321 | 322 | pub(crate) fn size(&self) -> IndexPos { 323 | let height = self.0.len(); 324 | let width = self.0.iter().map(|v| v.len()).max().unwrap_or_default(); 325 | 326 | pos(width, height).into() 327 | } 328 | } 329 | 330 | pub(crate) type Definitions<'i> = Vec<(Identifier<'i>, Vec>)>; 331 | 332 | pub(crate) fn parse_definitions(i: Input) -> Result { 333 | let definition = pair(Identifier::parse, NodeAttribute::parse_vec).terminated(char(TERMINATOR)); 334 | let definitions = many1(ws(definition)); 335 | 336 | preceded(terminated(tag("define"), space), block(definitions))(i) 337 | } 338 | 339 | #[derive(Debug, PartialEq, Eq)] 340 | pub(crate) struct Document<'i> { 341 | pub(crate) grid: Grid<'i>, 342 | pub(crate) definitions: Definitions<'i>, 343 | } 344 | 345 | impl<'i> Document<'i> { 346 | pub(crate) fn parse(i: Input<'i>) -> std::result::Result> { 347 | let document = map( 348 | permutation((ws(Grid::parse), opt(ws(parse_definitions)))), 349 | |(grid, definitions)| Self { 350 | grid, 351 | definitions: definitions.unwrap_or_default(), 352 | }, 353 | ); 354 | 355 | final_parser(document)(i) 356 | } 357 | } 358 | 359 | #[cfg(test)] 360 | mod tests { 361 | use nom::combinator::all_consuming; 362 | 363 | use super::*; 364 | use crate::test::{assert_not_parsed, assert_parsed_eq}; 365 | 366 | #[test] 367 | fn valid_identifier() { 368 | assert_parsed_eq(Identifier::parse, "foo", Identifier("foo")); 369 | assert_parsed_eq(Identifier::parse, "bar21foo", Identifier("bar21foo")); 370 | assert_parsed_eq(Identifier::parse, "_example", Identifier("_example")); 371 | assert_parsed_eq(Identifier::parse, "text_14", Identifier("text_14")); 372 | } 373 | 374 | #[test] 375 | fn invalid_identifier() { 376 | assert_not_parsed(Identifier::parse, ""); 377 | assert_not_parsed(Identifier::parse, "12number_first"); 378 | } 379 | 380 | #[test] 381 | fn valid_node_shape() { 382 | assert_parsed_eq(NodeShape::parse, "rect", NodeShape::Rectangle); 383 | assert_parsed_eq(NodeShape::parse, "square", NodeShape::Square); 384 | assert_parsed_eq(NodeShape::parse, "ellipse", NodeShape::Ellipse); 385 | assert_parsed_eq(NodeShape::parse, "circle", NodeShape::Circle); 386 | assert_parsed_eq(NodeShape::parse, "diamond", NodeShape::Diamond); 387 | assert_parsed_eq(NodeShape::parse, "angled_square", NodeShape::AngledSquare); 388 | } 389 | 390 | #[test] 391 | fn valid_direction() { 392 | assert_parsed_eq(Direction::parse, "n", Direction::North); 393 | assert_parsed_eq(Direction::parse, "s", Direction::South); 394 | assert_parsed_eq(Direction::parse, "w", Direction::West); 395 | assert_parsed_eq(Direction::parse, "e", Direction::East); 396 | } 397 | 398 | #[test] 399 | fn valid_destination() { 400 | const NORTH: Destination = Destination::Relative(Direction::North); 401 | const SOUTH: Destination = Destination::Relative(Direction::South); 402 | const WEST: Destination = Destination::Relative(Direction::West); 403 | const EAST: Destination = Destination::Relative(Direction::East); 404 | 405 | assert_parsed_eq(Destination::parse, "@n", NORTH); 406 | assert_parsed_eq(Destination::parse, "@s", SOUTH); 407 | assert_parsed_eq(Destination::parse, "@w", WEST); 408 | assert_parsed_eq(Destination::parse, "@e", EAST); 409 | 410 | assert_parsed_eq( 411 | Destination::parse, 412 | "#foo", 413 | Destination::Label(Identifier("foo")), 414 | ); 415 | 416 | assert_parsed_eq(Destination::parse, "@", Destination::Itself); 417 | } 418 | 419 | #[test] 420 | fn valid_node_attribute() { 421 | assert_parsed_eq( 422 | NodeAttribute::parse, 423 | r#"text: "foo""#, 424 | NodeAttribute::Text(String::from("foo")), 425 | ); 426 | 427 | assert_parsed_eq( 428 | NodeAttribute::parse, 429 | r#"class: "class name here""#, 430 | NodeAttribute::Class(String::from("class name here")), 431 | ); 432 | 433 | assert_parsed_eq( 434 | NodeAttribute::parse, 435 | r#"shape: diamond"#, 436 | NodeAttribute::Shape(NodeShape::Diamond), 437 | ); 438 | } 439 | 440 | #[test] 441 | fn valid_connection_attribute() { 442 | assert_parsed_eq( 443 | ConnectionAttribute::parse, 444 | r#"text: "foo""#, 445 | ConnectionAttribute::Text(String::from("foo")), 446 | ); 447 | 448 | assert_parsed_eq( 449 | ConnectionAttribute::parse, 450 | r#"class: "class name here""#, 451 | ConnectionAttribute::Class(String::from("class name here")), 452 | ); 453 | 454 | assert_parsed_eq( 455 | ConnectionAttribute::parse, 456 | "arrowheads: none", 457 | ConnectionAttribute::Arrowheads(ArrowheadType::None), 458 | ); 459 | } 460 | 461 | #[test] 462 | fn valid_connection_descriptor() { 463 | assert_parsed_eq( 464 | ConnectionDescriptor::parse, 465 | r#"n:s@s("foo", class: "bar")"#, 466 | ConnectionDescriptor { 467 | to: Destination::Relative(Direction::South), 468 | sides: (Direction::North, Direction::South), 469 | attrs: vec![ 470 | ConnectionAttribute::Text(String::from("foo")), 471 | ConnectionAttribute::Class(String::from("bar")), 472 | ], 473 | }, 474 | ); 475 | 476 | assert_parsed_eq( 477 | ConnectionDescriptor::parse, 478 | "w:e@s", 479 | ConnectionDescriptor { 480 | to: Destination::Relative(Direction::South), 481 | sides: (Direction::West, Direction::East), 482 | attrs: vec![], 483 | }, 484 | ); 485 | 486 | assert_parsed_eq( 487 | ConnectionDescriptor::parse, 488 | "n:e@s", 489 | ConnectionDescriptor { 490 | to: Destination::Relative(Direction::South), 491 | sides: (Direction::North, Direction::East), 492 | attrs: vec![], 493 | }, 494 | ); 495 | } 496 | 497 | #[test] 498 | fn valid_node_connect_attribute() { 499 | assert_parsed_eq( 500 | NodeAttribute::parse, 501 | "connect: n:e@n", 502 | NodeAttribute::Connect(vec![ConnectionDescriptor { 503 | to: Destination::Relative(Direction::North), 504 | sides: (Direction::North, Direction::East), 505 | attrs: vec![], 506 | }]), 507 | ); 508 | 509 | assert_parsed_eq( 510 | NodeAttribute::parse, 511 | "connect: {n:n@e; n:n#foo}", 512 | NodeAttribute::Connect(vec![ 513 | ConnectionDescriptor { 514 | to: Destination::Relative(Direction::East), 515 | sides: (Direction::North, Direction::North), 516 | attrs: vec![], 517 | }, 518 | ConnectionDescriptor { 519 | to: Destination::Label(Identifier("foo")), 520 | sides: (Direction::North, Direction::North), 521 | attrs: vec![], 522 | }, 523 | ]), 524 | ) 525 | } 526 | 527 | #[test] 528 | fn valid_node() { 529 | assert_parsed_eq( 530 | Node::parse, 531 | "foo", 532 | Node { 533 | id: Identifier("foo"), 534 | label: None, 535 | attrs: vec![], 536 | }, 537 | ); 538 | 539 | assert_parsed_eq( 540 | Node::parse, 541 | "foo#bar(shape: rect)", 542 | Node { 543 | id: Identifier("foo"), 544 | label: Some(Identifier("bar")), 545 | attrs: vec![NodeAttribute::Shape(NodeShape::Rectangle)], 546 | }, 547 | ); 548 | 549 | assert_parsed_eq( 550 | Node::parse, 551 | r#"foo("hello",)"#, 552 | Node { 553 | id: Identifier("foo"), 554 | label: None, 555 | attrs: vec![NodeAttribute::Text(String::from("hello"))], 556 | }, 557 | ); 558 | 559 | assert_parsed_eq( 560 | Node::parse, 561 | r#"foo("hey", shape: diamond,)"#, 562 | Node { 563 | id: Identifier("foo"), 564 | label: None, 565 | attrs: vec![ 566 | NodeAttribute::Text(String::from("hey")), 567 | NodeAttribute::Shape(NodeShape::Diamond), 568 | ], 569 | }, 570 | ); 571 | 572 | assert_parsed_eq( 573 | Node::parse, 574 | "foo#bar(shape: rect)", 575 | Node { 576 | id: Identifier("foo"), 577 | label: Some(Identifier("bar")), 578 | attrs: vec![NodeAttribute::Shape(NodeShape::Rectangle)], 579 | }, 580 | ); 581 | } 582 | 583 | #[test] 584 | fn invalid_node() { 585 | assert_not_parsed(Node::parse, ""); 586 | assert_not_parsed(Node::parse, "(shape: rect)"); 587 | assert_not_parsed(Node::parse, "#bar"); 588 | assert_not_parsed(Node::parse, "#bar(shape: rect)"); 589 | // Without all_consuming the parser just stops once it reaches "()". 590 | assert_not_parsed(all_consuming(Node::parse), "foo()"); 591 | } 592 | 593 | #[test] 594 | fn valid_grid() { 595 | let input = r#" 596 | grid { 597 | foo#main, bar; 598 | baz, _; 599 | _; 600 | } 601 | "# 602 | .trim(); 603 | 604 | let foo_node = Node { 605 | id: Identifier("foo"), 606 | label: Some(Identifier("main")), 607 | attrs: vec![], 608 | }; 609 | let bar_node = Node { 610 | id: Identifier("bar"), 611 | label: None, 612 | attrs: vec![], 613 | }; 614 | let baz_node = Node { 615 | id: Identifier("baz"), 616 | label: None, 617 | attrs: vec![], 618 | }; 619 | 620 | assert_parsed_eq( 621 | Grid::parse, 622 | input, 623 | Grid(vec![ 624 | vec![Some(foo_node), Some(bar_node)], 625 | vec![Some(baz_node), None], 626 | vec![None], 627 | ]), 628 | ); 629 | } 630 | 631 | #[test] 632 | fn invalid_grid() { 633 | assert_not_parsed(Grid::parse, "grid {}"); 634 | assert_not_parsed(Grid::parse, "grid { missing_terminator }"); 635 | assert_not_parsed(Grid::parse, "grid { missing separator; }"); 636 | assert_not_parsed(Grid::parse, "grid { foo; ; }"); 637 | } 638 | 639 | #[test] 640 | fn valid_definitions() { 641 | let input = r#" 642 | define { 643 | foo(shape: rect); 644 | bar(text: "hello"); 645 | } 646 | "# 647 | .trim(); 648 | 649 | assert_parsed_eq( 650 | parse_definitions, 651 | input, 652 | vec![ 653 | ( 654 | Identifier("foo"), 655 | vec![NodeAttribute::Shape(NodeShape::Rectangle)], 656 | ), 657 | ( 658 | Identifier("bar"), 659 | vec![NodeAttribute::Text(String::from("hello"))], 660 | ), 661 | ], 662 | ) 663 | } 664 | 665 | #[test] 666 | fn invalid_definitions() { 667 | assert_not_parsed(parse_definitions, "define {}"); 668 | assert_not_parsed(parse_definitions, "define { no_attrs; }"); 669 | assert_not_parsed(parse_definitions, "define { no_terminator(shape: rect) }"); 670 | assert_not_parsed(parse_definitions, "define { ; }"); 671 | } 672 | } 673 | -------------------------------------------------------------------------------- /crates/flou/src/parse/combinators.rs: -------------------------------------------------------------------------------- 1 | use nom::{ 2 | branch::alt, 3 | character::complete::{char, line_ending, multispace0, not_line_ending}, 4 | combinator::{cut, map, opt, recognize, value}, 5 | sequence::{delimited, pair, preceded}, 6 | }; 7 | use nom_supreme::{multi::collect_separated_terminated, tag::complete::tag, ParserExt}; 8 | 9 | use super::{constants::BLOCK_DELIMITERS, Input, Parser, Result}; 10 | 11 | fn comment(i: Input) -> Result { 12 | delimited(tag("//"), not_line_ending, line_ending)(i) 13 | } 14 | 15 | pub(super) fn space(i: Input) -> Result<()> { 16 | delimited(multispace0, value((), opt(comment)), multispace0)(i) 17 | } 18 | 19 | /// Parses an item surrounded by space and optional comments. 20 | pub(super) fn ws<'i, O, P: Parser<'i, O>>(item: P) -> impl Parser<'i, O> { 21 | delimited(space, item, space) 22 | } 23 | 24 | pub(super) fn attribute<'i, O, V: Parser<'i, O>>( 25 | key: &'static str, 26 | value: V, 27 | ) -> impl Parser<'i, O> { 28 | preceded(pair(tag(key), cut(ws(char(':')))), cut(value)) 29 | } 30 | 31 | pub(super) fn enclosed_list0<'i, Item>( 32 | delimiters: (char, char), 33 | item: impl Parser<'i, Item>, 34 | separator: char, 35 | ) -> impl Parser<'i, Vec> { 36 | preceded( 37 | char(delimiters.0).terminated(space), 38 | alt(( 39 | map(char(delimiters.1).preceded_by(space), |_| Vec::default()), 40 | list1(item, separator, delimiters.1), 41 | )), 42 | ) 43 | } 44 | 45 | pub(super) fn list1<'i, Item>( 46 | item: impl Parser<'i, Item>, 47 | separator: char, 48 | terminator: char, 49 | ) -> impl Parser<'i, Vec> { 50 | collect_separated_terminated( 51 | item, 52 | ws(char(separator)), 53 | alt(( 54 | recognize(pair(char(separator), char(terminator).preceded_by(space))), 55 | recognize(char(terminator)), 56 | )) 57 | .preceded_by(space), 58 | ) 59 | } 60 | 61 | pub(super) fn block<'i, O, P: Parser<'i, O>>(item: P) -> impl Parser<'i, O> { 62 | delimited(char(BLOCK_DELIMITERS.0), ws(item), char(BLOCK_DELIMITERS.1)) 63 | } 64 | -------------------------------------------------------------------------------- /crates/flou/src/parse/constants.rs: -------------------------------------------------------------------------------- 1 | pub(super) const RELATIVE_SIGIL: char = '@'; 2 | pub(super) const LABEL_SIGIL: char = '#'; 3 | pub(super) const SIDES_SIGIL: char = ':'; 4 | 5 | pub(super) const LIST_SEPARATOR: char = ','; 6 | pub(super) const TERMINATOR: char = ';'; 7 | pub(super) const LIST_DELIMITERS: (char, char) = ('(', ')'); 8 | pub(super) const BLOCK_DELIMITERS: (char, char) = ('{', '}'); 9 | 10 | pub(super) const EMPTY: &str = "_"; 11 | -------------------------------------------------------------------------------- /crates/flou/src/parse/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod ast; 2 | mod combinators; 3 | mod constants; 4 | mod parts; 5 | mod types; 6 | 7 | pub(crate) use types::*; 8 | -------------------------------------------------------------------------------- /crates/flou/src/parse/parts.rs: -------------------------------------------------------------------------------- 1 | use nom::{ 2 | branch::alt, 3 | bytes::complete::escaped_transform, 4 | character::complete::{char, none_of}, 5 | combinator::{cut, map, opt, value}, 6 | sequence::delimited, 7 | }; 8 | use nom_supreme::tag::complete::tag; 9 | 10 | use super::{Input, Result}; 11 | 12 | pub(super) fn quoted_string(i: Input) -> Result { 13 | let esc = escaped_transform( 14 | none_of("\\\""), 15 | '\\', 16 | alt(( 17 | value("\\", tag("\\")), 18 | value("\"", tag("\"")), 19 | value("\n", tag("n")), 20 | )), 21 | ); 22 | 23 | map( 24 | delimited(char('"'), cut(opt(esc)), cut(char('"'))), 25 | Option::unwrap_or_default, 26 | )(i) 27 | } 28 | 29 | #[cfg(test)] 30 | mod tests { 31 | use super::*; 32 | use crate::test::{assert_not_parsed, assert_parsed_eq}; 33 | 34 | #[test] 35 | fn valid_quoted_string() { 36 | assert_parsed_eq(quoted_string, r#""example""#, "example".into()); 37 | } 38 | 39 | #[test] 40 | fn quoted_string_escapes_characters() { 41 | assert_parsed_eq( 42 | quoted_string, 43 | r#""I said \"hello\".\n""#, 44 | "I said \"hello\".\n".into(), 45 | ); 46 | } 47 | 48 | #[test] 49 | fn invalid_quoted_string() { 50 | assert_not_parsed(quoted_string, r#""missing end quote"#); 51 | assert_not_parsed(quoted_string, r#"missing start quote""#); 52 | assert_not_parsed(quoted_string, r#"missing both quotes"#); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /crates/flou/src/parse/types.rs: -------------------------------------------------------------------------------- 1 | use nom_supreme::error::ErrorTree; 2 | 3 | pub(crate) type Input<'i> = &'i str; 4 | pub(crate) type Error<'i> = ErrorTree>; 5 | 6 | pub(crate) type Result<'i, O> = nom::IResult, O, Error<'i>>; 7 | 8 | pub(crate) trait Parser<'i, O>: nom::Parser, O, Error<'i>> {} 9 | impl<'i, O, N> Parser<'i, O> for N where N: nom::Parser, O, Error<'i>> {} 10 | -------------------------------------------------------------------------------- /crates/flou/src/parts/error.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | 3 | use crate::{parse::ast::Identifier, pos::IndexPos}; 4 | 5 | use super::grid::ResolutionError; 6 | 7 | type MapPos = HashMap; 8 | type MapId<'i, T> = HashMap, T>; 9 | 10 | #[derive(Debug, PartialEq, Eq)] 11 | pub enum LogicError<'i> { 12 | /// A label was used more than once. 13 | DuplicateLabels(MapId<'i, HashSet>), 14 | 15 | /// There is more than one definition for one identifier. 16 | DuplicateDefinitions(HashSet>), 17 | 18 | /// Some definitions contain duplicate node attributes. 19 | DuplicateNodeAttributesInDefinitions(MapId<'i, HashSet<&'static str>>), 20 | 21 | /// Some nodes inside the grid have duplicate node attributes. 22 | DuplicateNodeAttributesInGrid(MapPos>), 23 | 24 | /// Some connections inside the `define` block contain duplicate attributes. 25 | DuplicateConnectionAttributesInDefinitions(MapId<'i, HashMap>>), 26 | 27 | /// Some connections inside the `grid` block contain duplicate attributes. 28 | DuplicateConnectionAttributesInGrid(MapPos>>), 29 | 30 | /// One or more connections have destinations that couldn't be resolved. 31 | InvalidDestination(MapPos>>), 32 | } 33 | -------------------------------------------------------------------------------- /crates/flou/src/parts/flou.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{hash_map, HashMap, HashSet}, 3 | convert::TryFrom, 4 | fmt::Display, 5 | }; 6 | 7 | use crate::{ 8 | parse::ast::{ 9 | ArrowheadType, ConnectionAttribute, ConnectionDescriptor, Destination, Direction, Document, 10 | Grid as ASTGrid, Identifier, NodeAttribute, NodeShape, 11 | }, 12 | parse::Error as AstError, 13 | pos::{pos, IndexPos, PixelPos}, 14 | }; 15 | 16 | use super::{ 17 | error::LogicError, 18 | grid::{Grid, ResolutionError}, 19 | }; 20 | 21 | type MapPos = HashMap; 22 | type MapId<'i, T> = HashMap, T>; 23 | type TwoMapId<'i, T1, T2> = (MapId<'i, T1>, MapId<'i, T2>); 24 | type TwoMapPos = (MapPos, MapPos); 25 | 26 | #[derive(Debug, Default, Clone)] 27 | pub(crate) struct NodeAttributes { 28 | pub(crate) text: Option, 29 | pub(crate) class: Option, 30 | pub(crate) shape: Option, 31 | } 32 | 33 | #[derive(Debug, Default, Clone)] 34 | pub(crate) struct ConnectionAttributes { 35 | pub(crate) text: Option, 36 | pub(crate) class: Option, 37 | pub(crate) arrowheads: Option, 38 | } 39 | 40 | #[derive(Debug)] 41 | pub(crate) struct Connection { 42 | pub(crate) from: (IndexPos, Direction), 43 | pub(crate) to: (IndexPos, Direction), 44 | pub(crate) attrs: ConnectionAttributes, 45 | } 46 | 47 | #[derive(Debug)] 48 | pub struct Flou<'i> { 49 | pub(crate) grid: Grid<'i>, 50 | pub(crate) connections: Vec, 51 | pub(crate) node_attributes: MapPos, 52 | } 53 | 54 | #[derive(Debug)] 55 | pub enum FlouError<'i> { 56 | Parse(AstError<'i>), 57 | Logic(LogicError<'i>), 58 | } 59 | 60 | impl<'i> TryFrom<&'i str> for Flou<'i> { 61 | type Error = FlouError<'i>; 62 | 63 | fn try_from(i: &'i str) -> Result { 64 | let document = Document::parse(i).map_err(FlouError::Parse)?; 65 | let flou = Flou::try_from(document).map_err(FlouError::Logic)?; 66 | Ok(flou) 67 | } 68 | } 69 | 70 | pub struct RenderConfig { 71 | pub default_css: bool, 72 | pub css: Vec, 73 | 74 | // Element sizes 75 | pub arrowhead: PixelPos, 76 | pub node: PixelPos, 77 | pub grid_gap: PixelPos, 78 | } 79 | 80 | impl Default for RenderConfig { 81 | fn default() -> Self { 82 | Self { 83 | default_css: true, 84 | css: Vec::new(), 85 | arrowhead: pos(10, 10), 86 | node: pos(200, 100), 87 | grid_gap: pos(50, 50), 88 | } 89 | } 90 | } 91 | 92 | pub trait Renderer { 93 | fn render<'i>(flou: &'i Flou<'i>, config: &'i RenderConfig) -> Box; 94 | } 95 | 96 | impl<'i> TryFrom> for Flou<'i> { 97 | type Error = LogicError<'i>; 98 | 99 | fn try_from(document: Document<'i>) -> Result { 100 | let grid = Grid::from(&document.grid); 101 | 102 | let definitions = ensure_definitions_are_unique(document.definitions) 103 | .map_err(LogicError::DuplicateDefinitions)?; 104 | 105 | // TODO: Warn if a definition doesn't map to any nodes in the grid. 106 | 107 | let (def_attrs, def_connections) = { 108 | let (def_attrs, def_connection_desc_map) = get_attributes_from_definitions(definitions) 109 | .map_err(LogicError::DuplicateNodeAttributesInDefinitions)?; 110 | 111 | let def_connections = parse_connection_desc_map(def_connection_desc_map) 112 | .map_err(LogicError::DuplicateConnectionAttributesInDefinitions)?; 113 | 114 | ( 115 | resolve_id_map(&grid, def_attrs), 116 | resolve_id_map(&grid, def_connections), 117 | ) 118 | }; 119 | 120 | let (grid_attrs, grid_connections) = { 121 | let (grid_attrs, grid_conn_desc_map) = get_attributes_from_grid(&document.grid) 122 | .map_err(LogicError::DuplicateNodeAttributesInGrid)?; 123 | 124 | let grid_connections = parse_connection_desc_map(grid_conn_desc_map) 125 | .map_err(LogicError::DuplicateConnectionAttributesInGrid)?; 126 | 127 | (grid_attrs, grid_connections) 128 | }; 129 | 130 | let node_attributes = Overwrite::overwrite(def_attrs, grid_attrs); 131 | let connections = Overwrite::overwrite(def_connections, grid_connections); 132 | 133 | let labels = try_into_label_map(&document.grid).map_err(LogicError::DuplicateLabels)?; 134 | 135 | let connections = resolve_connections_map(&grid, &labels, connections) 136 | .map_err(LogicError::InvalidDestination)?; 137 | 138 | Ok(Self { 139 | grid, 140 | connections, 141 | node_attributes, 142 | }) 143 | } 144 | } 145 | 146 | /// Tries to assemble `NodeAttributes` from the vector of individual attributes. 147 | /// The `connect` attribute is separated from the rest so that an independent 148 | /// vector of connections can be created later down the line. 149 | fn parse_node_attributes<'i>( 150 | attributes: Vec>, 151 | ) -> Result<(NodeAttributes, Option>>), HashSet<&'static str>> { 152 | let mut res = NodeAttributes::default(); 153 | let mut duplicates = HashSet::new(); 154 | let mut conn_descriptors = None; 155 | 156 | for attribute in attributes { 157 | match attribute { 158 | NodeAttribute::Text(text) if res.text.is_none() => res.text = Some(text), 159 | NodeAttribute::Class(class) if res.class.is_none() => res.class = Some(class), 160 | NodeAttribute::Shape(shape) if res.shape.is_none() => res.shape = Some(shape), 161 | NodeAttribute::Connect(descriptors) if conn_descriptors.is_none() => { 162 | conn_descriptors = Some(descriptors) 163 | } 164 | _ => { 165 | duplicates.insert(attribute.as_key()); 166 | } 167 | } 168 | } 169 | 170 | if duplicates.is_empty() { 171 | Ok((res, conn_descriptors)) 172 | } else { 173 | Err(duplicates) 174 | } 175 | } 176 | 177 | impl TryFrom> for ConnectionAttributes { 178 | type Error = HashSet<&'static str>; 179 | 180 | fn try_from(attributes: Vec) -> Result { 181 | let mut res = Self::default(); 182 | let mut duplicates = HashSet::new(); 183 | 184 | for attribute in attributes { 185 | match attribute { 186 | ConnectionAttribute::Text(text) if res.text.is_none() => res.text = Some(text), 187 | ConnectionAttribute::Class(class) if res.class.is_none() => res.class = Some(class), 188 | ConnectionAttribute::Arrowheads(arrowheads) if res.arrowheads.is_none() => { 189 | res.arrowheads = Some(arrowheads) 190 | } 191 | _ => { 192 | duplicates.insert(attribute.as_key()); 193 | } 194 | } 195 | } 196 | 197 | if duplicates.is_empty() { 198 | Ok(res) 199 | } else { 200 | Err(duplicates) 201 | } 202 | } 203 | } 204 | 205 | /// Tries to map a label to the position of the node it's attached to. 206 | /// Labels are supposed to be unique, so encountering duplicates is an error. 207 | fn try_into_label_map<'i>( 208 | grid: &ASTGrid<'i>, 209 | ) -> Result, MapId<'i, HashSet>> { 210 | let mut positions: HashMap> = HashMap::new(); 211 | 212 | for (pos, node) in grid.nodes() { 213 | if let Some(label) = node.label { 214 | positions.entry(label).or_default().insert(pos); 215 | } 216 | } 217 | 218 | let mut labels = HashMap::new(); 219 | let mut unique_labels = HashSet::new(); 220 | 221 | for (&label, positions) in &positions { 222 | if positions.len() == 1 { 223 | labels.insert(label, *positions.iter().next().unwrap()); 224 | unique_labels.insert(label); 225 | } 226 | } 227 | 228 | if unique_labels.len() < positions.len() { 229 | for label in unique_labels { 230 | positions.remove(&label); 231 | } 232 | Err(positions) 233 | } else { 234 | Ok(labels) 235 | } 236 | } 237 | 238 | fn get_attributes_from_definitions<'i>( 239 | definitions: MapId<'i, Vec>>, 240 | ) -> Result< 241 | TwoMapId<'i, NodeAttributes, Vec>>, 242 | MapId<'i, HashSet<&'static str>>, 243 | > { 244 | let mut errors = HashMap::new(); 245 | let mut map_node_attrs = HashMap::new(); 246 | let mut map_connection_descriptors = HashMap::new(); 247 | 248 | for (id, attrs) in definitions { 249 | match parse_node_attributes(attrs) { 250 | Ok((node_attrs, connection_descriptors)) => { 251 | map_node_attrs.insert(id, node_attrs); 252 | if let Some(descriptors) = connection_descriptors { 253 | map_connection_descriptors.insert(id, descriptors); 254 | } 255 | } 256 | Err(duplicate_attrs) => { 257 | errors.insert(id, duplicate_attrs); 258 | } 259 | } 260 | } 261 | 262 | if errors.is_empty() { 263 | Ok((map_node_attrs, map_connection_descriptors)) 264 | } else { 265 | Err(errors) 266 | } 267 | } 268 | 269 | fn get_attributes_from_grid<'i>( 270 | grid: &ASTGrid<'i>, 271 | ) -> Result>>, MapPos>> 272 | { 273 | let mut errors = HashMap::new(); 274 | let mut map_node_attrs = HashMap::new(); 275 | let mut map_connection_descriptors = HashMap::new(); 276 | 277 | for (pos, node) in grid.nodes() { 278 | match parse_node_attributes(node.attrs.clone()) { 279 | Ok((node_attrs, connection_descriptors)) => { 280 | map_node_attrs.insert(pos, node_attrs); 281 | if let Some(descriptors) = connection_descriptors { 282 | map_connection_descriptors.insert(pos, descriptors); 283 | } 284 | } 285 | Err(duplicate_attrs) => { 286 | errors.insert(pos, duplicate_attrs); 287 | } 288 | } 289 | } 290 | 291 | if errors.is_empty() { 292 | Ok((map_node_attrs, map_connection_descriptors)) 293 | } else { 294 | Err(errors) 295 | } 296 | } 297 | 298 | fn ensure_definitions_are_unique<'i>( 299 | definitions: Vec<(Identifier<'i>, Vec>)>, 300 | ) -> Result>>, HashSet>> { 301 | let mut duplicates = HashSet::new(); 302 | let mut res = HashMap::new(); 303 | 304 | for (id, attrs) in definitions { 305 | if let hash_map::Entry::Vacant(e) = res.entry(id) { 306 | e.insert(attrs); 307 | } else { 308 | duplicates.insert(id); 309 | } 310 | } 311 | 312 | if duplicates.is_empty() { 313 | Ok(res) 314 | } else { 315 | Err(duplicates) 316 | } 317 | } 318 | 319 | #[derive(Debug, Clone)] 320 | struct UnresolvedConnection<'i> { 321 | to: Destination<'i>, 322 | sides: (Direction, Direction), 323 | attrs: ConnectionAttributes, 324 | } 325 | 326 | type MapToUnresolvedConnection<'i, T> = HashMap>>; 327 | type MapToDuplicateAttrs<'i, T> = HashMap>>; 328 | 329 | fn parse_connection_desc_map( 330 | def_connection_desc_map: HashMap>, 331 | ) -> Result, MapToDuplicateAttrs> { 332 | let mut errors = HashMap::new(); 333 | let mut res = HashMap::new(); 334 | 335 | for (id, descriptors) in def_connection_desc_map { 336 | let mut value = Vec::new(); 337 | for (i, descriptor) in descriptors.into_iter().enumerate() { 338 | match ConnectionAttributes::try_from(descriptor.attrs) { 339 | Ok(attrs) => { 340 | value.push(UnresolvedConnection { 341 | to: descriptor.to, 342 | sides: descriptor.sides, 343 | attrs, 344 | }); 345 | } 346 | Err(duplicate_attrs) => { 347 | errors 348 | .entry(id) 349 | .or_insert_with(HashMap::new) 350 | .insert(i, duplicate_attrs); 351 | } 352 | }; 353 | } 354 | 355 | res.insert(id, value); 356 | } 357 | 358 | if errors.is_empty() { 359 | Ok(res) 360 | } else { 361 | Err(errors) 362 | } 363 | } 364 | 365 | fn resolve_connections_map<'i>( 366 | grid: &Grid<'i>, 367 | labels: &MapId<'i, IndexPos>, 368 | connections_map: MapPos>>, 369 | ) -> Result, MapPos>>> { 370 | let mut errors: MapPos> = HashMap::new(); 371 | let mut res = Vec::new(); 372 | 373 | for (from, connections) in connections_map { 374 | for (i, unresolved) in connections.into_iter().enumerate() { 375 | match grid.normalize_destination(from, unresolved.to, labels) { 376 | Ok(to) => res.push(Connection { 377 | from: (from, unresolved.sides.0), 378 | to: (to, unresolved.sides.1), 379 | attrs: unresolved.attrs, 380 | }), 381 | Err(resolution_error) => { 382 | errors.entry(from).or_default().insert(i, resolution_error); 383 | } 384 | } 385 | } 386 | } 387 | 388 | if errors.is_empty() { 389 | Ok(res) 390 | } else { 391 | Err(errors) 392 | } 393 | } 394 | 395 | fn resolve_id_map<'i, T: Clone>(grid: &Grid<'i>, map_id: MapId) -> MapPos { 396 | let mut res = HashMap::new(); 397 | 398 | for (id, value) in map_id { 399 | if let Some(positions) = grid.get_positions(&id) { 400 | for &pos in positions { 401 | res.insert(pos, value.clone()); 402 | } 403 | } 404 | } 405 | 406 | res 407 | } 408 | 409 | trait Overwrite { 410 | fn overwrite(old: Self, new: Self) -> Self; 411 | } 412 | 413 | impl Overwrite for NodeAttributes { 414 | fn overwrite(old: Self, new: Self) -> Self { 415 | Self { 416 | text: new.text.or(old.text), 417 | class: new.class.or(old.class), 418 | shape: new.shape.or(old.shape), 419 | } 420 | } 421 | } 422 | 423 | impl Overwrite for ConnectionAttributes { 424 | fn overwrite(old: Self, new: Self) -> Self { 425 | Self { 426 | text: new.text.or(old.text), 427 | class: new.class.or(old.class), 428 | arrowheads: new.arrowheads.or(old.arrowheads), 429 | } 430 | } 431 | } 432 | 433 | impl Overwrite for Vec { 434 | fn overwrite(_old: Self, new: Self) -> Self { 435 | new 436 | } 437 | } 438 | 439 | impl Overwrite for Option { 440 | fn overwrite(old: Self, new: Self) -> Self { 441 | match (old, new) { 442 | (None, None) => None, 443 | (Some(x), None) => Some(x), 444 | (None, Some(x)) => Some(x), 445 | (Some(old), Some(new)) => Some(Overwrite::overwrite(old, new)), 446 | } 447 | } 448 | } 449 | 450 | impl Overwrite for HashMap { 451 | fn overwrite(old: Self, new: Self) -> Self { 452 | let mut res = old; 453 | 454 | for (key, new_val) in new { 455 | let old_val = res.remove(&key); 456 | let result = Overwrite::overwrite(old_val, Some(new_val)).unwrap(); 457 | res.insert(key, result); 458 | } 459 | 460 | res 461 | } 462 | } 463 | 464 | #[cfg(test)] 465 | mod tests { 466 | use std::convert::TryFrom; 467 | 468 | use crate::{ 469 | parse::ast::{Direction, Document}, 470 | pos::pos, 471 | test::{assert_eq, id, map, set}, 472 | }; 473 | 474 | use super::{ 475 | super::grid::ResolutionError, 476 | {Flou, LogicError}, 477 | }; 478 | 479 | macro_rules! parse_flou { 480 | (grid: $grid:literal $(,)?) => {{ 481 | let input = concat!("grid { ", $grid, " }"); 482 | let document = Document::parse(input).unwrap(); 483 | Flou::try_from(document) 484 | }}; 485 | 486 | (grid: $grid:literal, define: $define:literal $(,)?) => {{ 487 | let input = concat!("grid { ", $grid, " } ", "define { ", $define, " }"); 488 | let document = Document::parse(input).unwrap(); 489 | Flou::try_from(document) 490 | }}; 491 | } 492 | 493 | #[test] 494 | fn duplicate_labels() { 495 | let flou = parse_flou! { 496 | grid: "block#foo; question#bar; block#foo; name#cat, block#bar;", 497 | }; 498 | 499 | assert_eq!( 500 | flou.unwrap_err(), 501 | LogicError::DuplicateLabels(map([ 502 | (id("foo"), set([pos(0, 0), pos(0, 2)])), 503 | (id("bar"), set([pos(0, 1), pos(1, 3)])), 504 | ])) 505 | ); 506 | } 507 | 508 | #[test] 509 | fn duplicate_node_attributes_in_definitions() { 510 | let flou = parse_flou! { 511 | grid: "foo; bar;", 512 | define: r#" 513 | foo(shape: rect, shape: diamond); 514 | bar(shape: rect, text: "hello", shape: diamond, connect: n:n@s, text: "hey"); 515 | "#, 516 | }; 517 | 518 | assert_eq!( 519 | flou.unwrap_err(), 520 | LogicError::DuplicateNodeAttributesInDefinitions(map([ 521 | (id("foo"), set(["shape"])), 522 | (id("bar"), set(["shape", "text"])), 523 | ])) 524 | ); 525 | } 526 | 527 | #[test] 528 | fn duplicate_connection_attributes_in_definitions() { 529 | let flou = parse_flou! { 530 | grid: "foo; bar;", 531 | define: r#" 532 | foo(connect: n:n@s(text: "hi", text: "hello")); 533 | bar(connect: {n:n@n(text: "hey", class: "hello"); n:n@e(class: "hi", class: "hello")}); 534 | "#, 535 | }; 536 | 537 | assert_eq!( 538 | flou.unwrap_err(), 539 | LogicError::DuplicateConnectionAttributesInDefinitions(map([ 540 | (id("foo"), map([(0, set(["text"]))])), 541 | (id("bar"), map([(1, set(["class"]))])), 542 | ])) 543 | ); 544 | } 545 | 546 | #[test] 547 | fn duplicate_node_attributes_in_grid() { 548 | let flou = parse_flou! { 549 | grid: r#" 550 | foo(shape: rect, text: "hi", shape: diamond); 551 | bar(connect: n:n@s, shape: circle, text: "hello", shape: rect, connect: n:n#end); 552 | "#, 553 | }; 554 | 555 | assert_eq!( 556 | flou.unwrap_err(), 557 | LogicError::DuplicateNodeAttributesInGrid(map([ 558 | (pos(0, 0), set(["shape"])), 559 | (pos(0, 1), set(["connect", "shape"])), 560 | ])) 561 | ); 562 | } 563 | 564 | #[test] 565 | fn duplicate_connection_attributes_in_grid() { 566 | let flou = parse_flou! { 567 | grid: r#" 568 | foo(connect: n:n@s(text: "hi", text: "hello")); 569 | _, bar(connect: {n:n@n(text: "hey", class: "hello"); n:n@e(class: "hi", class: "hello")}); 570 | "#, 571 | }; 572 | 573 | assert_eq!( 574 | flou.unwrap_err(), 575 | LogicError::DuplicateConnectionAttributesInGrid(map([ 576 | (pos(0, 0), map([(0, set(["text"]))])), 577 | (pos(1, 1), map([(1, set(["class"]))])), 578 | ])) 579 | ); 580 | } 581 | 582 | #[test] 583 | fn invalid_destination() { 584 | let flou = parse_flou! { 585 | grid: r#" 586 | start(connect: n:n@n); 587 | middle; 588 | end(connect: {n:n#foo; n:n@e}); 589 | "#, 590 | define: "middle(connect: {n:n@n; n:n@s});", 591 | }; 592 | 593 | assert_eq!( 594 | flou.unwrap_err(), 595 | LogicError::InvalidDestination(map([ 596 | ( 597 | pos(0, 0), 598 | map([(0, ResolutionError::InvalidDirection(Direction::North))]) 599 | ), 600 | ( 601 | pos(0, 2), 602 | map([ 603 | (0, ResolutionError::UnknownLabel(id("foo"))), 604 | (1, ResolutionError::InvalidDirection(Direction::East)), 605 | ]) 606 | ) 607 | ])) 608 | ) 609 | } 610 | } 611 | -------------------------------------------------------------------------------- /crates/flou/src/parts/grid.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::{ 4 | parse::ast::{Destination, Direction, Grid as ASTGrid, Identifier}, 5 | pos::IndexPos, 6 | }; 7 | 8 | #[derive(Debug, PartialEq, Eq)] 9 | pub enum ResolutionError<'i> { 10 | InvalidDirection(Direction), 11 | UnknownLabel(Identifier<'i>), 12 | } 13 | 14 | #[derive(Debug)] 15 | pub(crate) struct Grid<'i> { 16 | pub(crate) size: IndexPos, 17 | pub(crate) position_to_id: HashMap>, 18 | id_to_positions: HashMap, Vec>, 19 | } 20 | 21 | impl<'i> Grid<'i> { 22 | pub(crate) fn normalize_destination( 23 | &self, 24 | from: IndexPos, 25 | to: Destination<'i>, 26 | labels: &HashMap, IndexPos>, 27 | ) -> Result> { 28 | match to { 29 | Destination::Itself => Ok(from), 30 | Destination::Relative(dir) => { 31 | let step = IndexPos::from(dir); 32 | self.walk(from, None, step) 33 | .ok_or(ResolutionError::InvalidDirection(dir)) 34 | } 35 | Destination::Label(label) => labels 36 | .get(&label) 37 | .copied() 38 | .ok_or(ResolutionError::UnknownLabel(label)), 39 | } 40 | } 41 | 42 | pub(crate) fn get_positions(&self, id: &Identifier<'i>) -> Option<&Vec> { 43 | self.id_to_positions.get(id) 44 | } 45 | 46 | pub(crate) fn walk( 47 | &self, 48 | start: IndexPos, 49 | end: Option, 50 | step: IndexPos, 51 | ) -> Option { 52 | let mut current = start; 53 | loop { 54 | current += step; 55 | 56 | if let Some(end) = end { 57 | if current == end { 58 | return match self.get_id(current) { 59 | None => None, // Out of bounds 60 | Some(Some(_)) => Some(current), // Ran into something 61 | Some(None) => None, // Empty space 62 | }; 63 | } 64 | } 65 | 66 | break match self.get_id(current) { 67 | None => None, // Out of bounds 68 | Some(Some(_)) => Some(current), // Ran into something; stop immediately 69 | Some(None) => continue, // Empty space; keep moving 70 | }; 71 | } 72 | } 73 | 74 | pub(crate) fn get_id(&self, pos: IndexPos) -> Option> { 75 | pos.in_bounds(self.size) 76 | .then(|| self.position_to_id.get(&pos)) 77 | } 78 | } 79 | 80 | impl<'i> From<&ASTGrid<'i>> for Grid<'i> { 81 | fn from(grid: &ASTGrid<'i>) -> Self { 82 | let mut position_to_id = HashMap::new(); 83 | let mut id_to_positions: HashMap> = HashMap::new(); 84 | 85 | for (pos, node) in grid.nodes() { 86 | position_to_id.insert(pos, node.id); 87 | id_to_positions.entry(node.id).or_default().push(pos); 88 | } 89 | 90 | Self { 91 | size: grid.size(), 92 | position_to_id, 93 | id_to_positions, 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /crates/flou/src/parts/mod.rs: -------------------------------------------------------------------------------- 1 | mod error; 2 | mod flou; 3 | mod grid; 4 | 5 | pub(crate) use self::flou::*; 6 | pub(crate) use self::grid::*; 7 | 8 | pub use self::error::LogicError; 9 | pub use self::flou::{Flou, FlouError, RenderConfig, Renderer}; 10 | pub use self::grid::ResolutionError; 11 | -------------------------------------------------------------------------------- /crates/flou/src/pos.rs: -------------------------------------------------------------------------------- 1 | use num_traits::{Num, Signed}; 2 | 3 | use std::{fmt, marker::PhantomData, ops}; 4 | 5 | #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] 6 | pub struct IndexSpace; 7 | pub type IndexPos = Position2D; 8 | 9 | #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] 10 | pub struct PixelSpace; 11 | pub type PixelPos = Position2D; 12 | 13 | impl_pos_from!(Position2D, IndexPos, isize); 14 | impl_pos_from!(PixelPos, IndexPos, isize); 15 | impl_pos_from!(IndexPos, PixelPos, i32); 16 | 17 | #[derive(PartialEq, Eq, std::hash::Hash)] 18 | pub struct Position2D { 19 | pub x: T, 20 | pub y: T, 21 | #[doc(hidden)] 22 | _unit: PhantomData, 23 | } 24 | 25 | pub(crate) fn pos(x: T, y: T) -> Position2D { 26 | Position2D::new(x, y) 27 | } 28 | 29 | impl fmt::Display for Position2D { 30 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 31 | write!(f, "({}, {})", self.x, self.y) 32 | } 33 | } 34 | 35 | impl Copy for Position2D {} 36 | 37 | impl Clone for Position2D { 38 | fn clone(&self) -> Self { 39 | Self { 40 | x: self.x.clone(), 41 | y: self.y.clone(), 42 | _unit: PhantomData, 43 | } 44 | } 45 | } 46 | 47 | impl fmt::Debug for Position2D { 48 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 49 | write!(f, "Pos({:?}, {:?})", self.x, self.y) 50 | } 51 | } 52 | 53 | impl Position2D { 54 | pub(crate) fn new(x: T, y: T) -> Self { 55 | Self { 56 | x, 57 | y, 58 | _unit: PhantomData, 59 | } 60 | } 61 | } 62 | 63 | impl ops::Neg for Position2D { 64 | type Output = Self; 65 | 66 | fn neg(self) -> Self::Output { 67 | Self::new(-self.x, -self.y) 68 | } 69 | } 70 | 71 | impl From for Position2D { 72 | fn from(val: T) -> Self { 73 | Self::new(val, val) 74 | } 75 | } 76 | 77 | impl> ops::Mul for Position2D { 78 | type Output = Self; 79 | 80 | fn mul(self, rhs: I) -> Self::Output { 81 | let rhs = rhs.into(); 82 | Self::new(self.x * rhs.x, self.y * rhs.y) 83 | } 84 | } 85 | 86 | impl> ops::Div for Position2D { 87 | type Output = Self; 88 | 89 | fn div(self, rhs: I) -> Self::Output { 90 | let rhs = rhs.into(); 91 | Self::new(self.x / rhs.x, self.y / rhs.y) 92 | } 93 | } 94 | 95 | impl> ops::AddAssign for Position2D { 96 | fn add_assign(&mut self, rhs: I) { 97 | let rhs = rhs.into(); 98 | self.x += rhs.x; 99 | self.y += rhs.y; 100 | } 101 | } 102 | 103 | impl> ops::SubAssign for Position2D { 104 | fn sub_assign(&mut self, rhs: I) { 105 | let rhs = rhs.into(); 106 | self.x -= rhs.x; 107 | self.y -= rhs.y; 108 | } 109 | } 110 | 111 | impl> ops::Add for Position2D { 112 | type Output = Self; 113 | 114 | fn add(self, rhs: I) -> Self::Output { 115 | let rhs = rhs.into(); 116 | Self::new(self.x + rhs.x, self.y + rhs.y) 117 | } 118 | } 119 | 120 | impl> ops::Sub for Position2D { 121 | type Output = Self; 122 | 123 | fn sub(self, rhs: I) -> Self::Output { 124 | let rhs = rhs.into(); 125 | Self::new(self.x - rhs.x, self.y - rhs.y) 126 | } 127 | } 128 | 129 | impl Position2D { 130 | pub(crate) fn in_bounds(&self, bounds: Self) -> bool { 131 | self.x >= T::zero() && self.x < bounds.x && self.y >= T::zero() && self.y < bounds.y 132 | } 133 | } 134 | 135 | macro_rules! impl_pos_from { 136 | ($from:ty, $to:ty) => { 137 | impl From<$from> for $to { 138 | fn from(other: $from) -> Self { 139 | Self::new(other.x, other.y) 140 | } 141 | } 142 | }; 143 | 144 | ($from:ty, $to:ty, $cast:ty) => { 145 | impl From<$from> for $to { 146 | fn from(other: $from) -> Self { 147 | Self::new(other.x as $cast, other.y as $cast) 148 | } 149 | } 150 | }; 151 | } 152 | 153 | pub(crate) use impl_pos_from; 154 | 155 | use crate::parse::ast::Direction; 156 | 157 | impl From for Position2D { 158 | fn from(dir: Direction) -> Self { 159 | let one = T::one(); 160 | let zero = T::zero(); 161 | match dir { 162 | Direction::North => Self::new(zero, -one), 163 | Direction::South => Self::new(zero, one), 164 | Direction::West => Self::new(-one, zero), 165 | Direction::East => Self::new(one, zero), 166 | } 167 | } 168 | } 169 | 170 | impl From<(T, T)> for Position2D { 171 | fn from((x, y): (T, T)) -> Self { 172 | Self::new(x, y) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /crates/flou/src/render_svg/mod.rs: -------------------------------------------------------------------------------- 1 | mod node; 2 | mod path; 3 | mod renderer; 4 | mod viewport; 5 | 6 | pub use renderer::SvgRenderer; 7 | pub(crate) use viewport::Viewport; 8 | -------------------------------------------------------------------------------- /crates/flou/src/render_svg/node.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | parse::ast::{Direction, NodeShape}, 3 | parts::NodeAttributes, 4 | pos::{pos, PixelPos}, 5 | svg::{SVGElement, SVGPath, SVGText}, 6 | }; 7 | 8 | use super::viewport::{Midpoints, Viewport}; 9 | 10 | impl Default for NodeShape { 11 | fn default() -> Self { 12 | Self::Rectangle 13 | } 14 | } 15 | 16 | impl NodeShape { 17 | pub(crate) fn render(&self, viewport: Viewport) -> SVGElement<'static> { 18 | match &self { 19 | Self::Rectangle => SVGElement::new("rect") 20 | .class("rect") 21 | .pos(viewport.origin) 22 | .size(viewport.size), 23 | 24 | Self::Square => { 25 | let size = std::cmp::min(viewport.size.x, viewport.size.y); 26 | let origin = viewport.origin + (viewport.size - size) / 2; 27 | 28 | SVGElement::new("rect") 29 | .class("square") 30 | .pos(origin) 31 | .size(size.into()) 32 | } 33 | 34 | Self::Diamond => { 35 | let midpoints = viewport.midpoints(); 36 | 37 | SVGPath::new() 38 | .line_to(midpoints.top) 39 | .line_to(midpoints.left) 40 | .line_to(midpoints.bottom) 41 | .line_to(midpoints.right) 42 | .end() 43 | .render() 44 | .class("diamond") 45 | } 46 | 47 | Self::AngledSquare => { 48 | let size = std::cmp::min(viewport.size.x, viewport.size.y); 49 | let origin = viewport.origin + (viewport.size - size) / 2; 50 | let viewport = Viewport::new(origin, size.into()); 51 | let midpoints = viewport.midpoints(); 52 | 53 | SVGPath::new() 54 | .line_to(midpoints.top) 55 | .line_to(midpoints.left) 56 | .line_to(midpoints.bottom) 57 | .line_to(midpoints.right) 58 | .end() 59 | .render() 60 | .class("angled_square") 61 | } 62 | 63 | Self::Ellipse => { 64 | let size = viewport.size / 2; 65 | 66 | SVGElement::new("ellipse") 67 | .class("ellipse") 68 | .cpos(viewport.center()) 69 | .attr("rx", size.x.to_string()) 70 | .attr("ry", size.y.to_string()) 71 | } 72 | 73 | Self::Circle => { 74 | let diameter = std::cmp::min(viewport.size.x, viewport.size.y); 75 | let radius = diameter / 2; 76 | 77 | SVGElement::new("circle") 78 | .class("circle") 79 | .cpos(viewport.center()) 80 | .attr("r", radius.to_string()) 81 | } 82 | } 83 | } 84 | } 85 | 86 | impl NodeAttributes { 87 | fn wrapper() -> SVGElement<'static> { 88 | SVGElement::new("g").class("node-wrapper") 89 | } 90 | 91 | pub(crate) fn render_default(viewport: Viewport) -> SVGElement<'static> { 92 | let shape = NodeShape::default().render(viewport); 93 | Self::wrapper().child(shape.class("node")) 94 | } 95 | 96 | pub(crate) fn render(&self, viewport: Viewport) -> SVGElement { 97 | let shape = self.shape.unwrap_or_default().render(viewport); 98 | 99 | let text = self 100 | .text 101 | .as_ref() 102 | .map(|text| SVGText::new(viewport.center()).render(text)); 103 | 104 | Self::wrapper() 105 | .class_opt(self.class.as_ref()) 106 | .child(shape.class("node")) 107 | .child_opt(text) 108 | } 109 | 110 | pub(crate) fn link_point(&self, viewport: Viewport, dir: Direction) -> PixelPos { 111 | match &self.shape.unwrap_or_default() { 112 | NodeShape::Circle | NodeShape::Square | NodeShape::AngledSquare => { 113 | let radius = std::cmp::min(viewport.size.x, viewport.size.y) / 2; 114 | let center = viewport.center(); 115 | 116 | // Calculate the midpoints as offsets from the center of the 117 | // viewport because it's simpler, but then subtract the origin 118 | // (top-left corner) because the offsets need to be relative to *it*. 119 | let midpoints = Midpoints { 120 | top: center + pos(0, -radius) - viewport.origin, 121 | bottom: center + pos(0, radius) - viewport.origin, 122 | left: center + pos(-radius, 0) - viewport.origin, 123 | right: center + pos(radius, 0) - viewport.origin, 124 | }; 125 | 126 | midpoints.get_from_direction(dir) 127 | } 128 | _ => viewport.midpoints_relative().get_from_direction(dir), 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /crates/flou/src/render_svg/path.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::{cmp::Ordering, ops::Sub}; 4 | 5 | use num_traits::Num; 6 | 7 | use crate::{ 8 | parse::ast::{Direction, Identifier}, 9 | parts::Grid, 10 | pos::{pos, IndexPos, Position2D}, 11 | render_svg::renderer::PaddedPos, 12 | }; 13 | 14 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] 15 | enum Axis { 16 | X, 17 | Y, 18 | } 19 | 20 | impl Axis { 21 | fn from_dir(dir: Direction) -> Self { 22 | match dir { 23 | Direction::North | Direction::South => Self::Y, 24 | Direction::West | Direction::East => Self::X, 25 | } 26 | } 27 | } 28 | 29 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] 30 | enum FreeAxisCount { 31 | Zero, 32 | One(Axis), 33 | Two, 34 | } 35 | 36 | impl FreeAxisCount { 37 | fn from_pos(pos: PaddedPos) -> Self { 38 | let x_aligned = pos.x & 1 == 1; 39 | let y_aligned = pos.y & 1 == 1; 40 | match (x_aligned, y_aligned) { 41 | (false, false) => Self::Two, 42 | (false, true) => Self::One(Axis::Y), 43 | (true, false) => Self::One(Axis::X), 44 | (true, true) => Self::Zero, 45 | } 46 | } 47 | } 48 | 49 | impl Position2D 50 | where 51 | Self: Sub, 52 | { 53 | fn x_direction(from: Self, to: Self) -> Option { 54 | match from.x.cmp(&to.x) { 55 | Ordering::Less => Some(Direction::East), 56 | Ordering::Greater => Some(Direction::West), 57 | Ordering::Equal => None, 58 | } 59 | } 60 | 61 | fn y_direction(from: Self, to: Self) -> Option { 62 | match from.y.cmp(&to.y) { 63 | Ordering::Less => Some(Direction::South), 64 | Ordering::Greater => Some(Direction::North), 65 | Ordering::Equal => None, 66 | } 67 | } 68 | 69 | pub(crate) fn straight_line(from: Self, to: Self) -> Option { 70 | let distance = to - from; 71 | 72 | match (distance.x == T::zero(), distance.y == T::zero()) { 73 | (false, false) | (true, true) => None, 74 | (false, true) => Self::x_direction(from, to), 75 | (true, false) => Self::y_direction(from, to), 76 | } 77 | } 78 | 79 | fn taxicab(from: Self, to: Self) -> T { 80 | Self::x_distance(from, to) + Self::y_distance(from, to) 81 | } 82 | 83 | fn x_distance(from: Self, to: Self) -> T { 84 | match from.x > to.x { 85 | true => from.x - to.x, 86 | false => to.x - from.x, 87 | } 88 | } 89 | 90 | fn y_distance(from: Self, to: Self) -> T { 91 | match from.y > to.y { 92 | true => from.y - to.y, 93 | false => to.y - from.y, 94 | } 95 | } 96 | } 97 | 98 | impl PaddedPos { 99 | pub(crate) fn snap_to_grid(&self) -> Self { 100 | let x = self.x - ((self.x & 1) ^ 1); 101 | let y = self.y - ((self.y & 1) ^ 1); 102 | Self::new(x, y) 103 | } 104 | 105 | pub(crate) fn grid_aligned(&self) -> bool { 106 | self.grid_x_aligned() && self.grid_y_aligned() 107 | } 108 | 109 | pub(crate) fn grid_x_aligned(&self) -> bool { 110 | self.x & 1 == 1 111 | } 112 | 113 | pub(crate) fn grid_y_aligned(&self) -> bool { 114 | self.y & 1 == 1 115 | } 116 | } 117 | 118 | #[derive(Debug, Clone, Copy)] 119 | struct PosSide { 120 | origin: IndexPos, 121 | side: Direction, 122 | } 123 | 124 | impl PosSide { 125 | fn new(origin: IndexPos, side: Direction) -> Self { 126 | Self { origin, side } 127 | } 128 | } 129 | 130 | impl From for PaddedPos { 131 | fn from(x: PosSide) -> Self { 132 | Self::from(x.origin) + Self::from(x.side) 133 | } 134 | } 135 | 136 | impl Grid<'_> { 137 | fn padded_get_id(&self, pos: PaddedPos) -> Option> { 138 | pos.in_bounds(self.size.into()) 139 | .then(|| match pos.grid_aligned() { 140 | true => self.position_to_id.get(&pos.into()), 141 | false => None, 142 | }) 143 | } 144 | 145 | fn padded_walk( 146 | &self, 147 | start: PaddedPos, 148 | end: Option, 149 | step: PaddedPos, 150 | ) -> Option { 151 | let mut current = start; 152 | loop { 153 | current += step; 154 | 155 | if let Some(end) = end { 156 | if current == end { 157 | return match self.padded_get_id(current) { 158 | None => None, // Out of bounds 159 | Some(Some(_)) => Some(current), // Ran into something 160 | Some(None) => None, // Empty space 161 | }; 162 | } 163 | } 164 | 165 | break match self.padded_get_id(current) { 166 | None => None, // Out of bounds 167 | Some(Some(_)) => Some(current), // Ran into something; stop immediately 168 | Some(None) => continue, // Empty space; keep moving 169 | }; 170 | } 171 | } 172 | } 173 | 174 | fn get_best_corner(grid: &Grid, a: PosSide, b: PosSide) -> (PaddedPos, FreeAxisCount) { 175 | let a_pos = PaddedPos::from(a); 176 | let b_pos = PaddedPos::from(b); 177 | 178 | if PaddedPos::straight_line(a_pos, b_pos).is_some() { 179 | let corner = a_pos + PaddedPos::from(a.side.rotate_clockwise()); 180 | return (corner, FreeAxisCount::from_pos(corner)); 181 | } 182 | 183 | let corners = [pos(a_pos.x, b_pos.y), pos(b_pos.x, a_pos.y)]; 184 | let mut corners = vec![ 185 | (corners[0], FreeAxisCount::from_pos(corners[0])), 186 | (corners[1], FreeAxisCount::from_pos(corners[1])), 187 | ]; 188 | 189 | corners.sort_unstable_by(|(_, a_lane_count), (_, b_lane_count)| a_lane_count.cmp(b_lane_count)); 190 | 191 | let (smaller, larger) = (corners[0], corners[1]); 192 | 193 | let can_make_direct_connection = |from: PosSide, to: PosSide, corner: PaddedPos| -> bool { 194 | line_does_not_overlap_nodes(grid, from, corner) 195 | && line_does_not_overlap_nodes(grid, to, corner) 196 | }; 197 | 198 | match smaller.1 { 199 | FreeAxisCount::Zero => { 200 | if can_make_direct_connection(a, b, smaller.0) { 201 | (smaller.0, FreeAxisCount::Two) 202 | } else { 203 | larger 204 | } 205 | } 206 | FreeAxisCount::One(_) => { 207 | if can_make_direct_connection(a, b, smaller.0) { 208 | (smaller.0, FreeAxisCount::Two) 209 | } else if can_make_direct_connection(a, b, larger.0) { 210 | (larger.0, FreeAxisCount::Two) 211 | } else { 212 | larger 213 | } 214 | } 215 | FreeAxisCount::Two => unreachable!(), 216 | } 217 | } 218 | 219 | fn line_does_not_overlap_nodes( 220 | grid: &Grid, 221 | from: impl Into, 222 | to: impl Into, 223 | ) -> bool { 224 | fn inner(grid: &Grid, from: PaddedPos, to: PaddedPos) -> bool { 225 | if let Some(dir) = PaddedPos::straight_line(from, to) { 226 | if let FreeAxisCount::One(free) = FreeAxisCount::from_pos(from) { 227 | if Axis::from_dir(dir) == free { 228 | return true; 229 | } else { 230 | return match grid.padded_walk(from, Some(to), dir.into()) { 231 | Some(dest) => dest == to, 232 | None => true, 233 | }; 234 | } 235 | } 236 | } 237 | 238 | false 239 | } 240 | 241 | inner(grid, from.into(), to.into()) 242 | } 243 | 244 | pub(crate) fn get_path( 245 | grid: &Grid, 246 | from: (IndexPos, Direction), 247 | to: (IndexPos, Direction), 248 | ) -> Vec { 249 | if PaddedPos::PADDING != 1 { 250 | panic!("Algorithm is designed to work with a padding of 1"); 251 | } 252 | 253 | let from = PosSide::new(from.0, from.1); 254 | let to = PosSide::new(to.0, to.1); 255 | 256 | let s_from: PaddedPos = from.into(); 257 | let s_to: PaddedPos = to.into(); 258 | 259 | if s_from == s_to { 260 | return vec![from.origin.into(), to.origin.into()]; 261 | } 262 | 263 | // TODO: Don't insert s_from and s_to if they lie on the line 264 | // that the first and last point make. 265 | let make_connection = |mid: Vec| { 266 | let mut res = vec![from.origin.into(), s_from]; 267 | res.extend(mid); 268 | res.push(s_to); 269 | res.push(to.origin.into()); 270 | res 271 | }; 272 | 273 | if let Some(dir) = PaddedPos::straight_line(s_from, s_to) { 274 | if line_does_not_overlap_nodes(grid, from, to) { 275 | return make_connection(vec![]); 276 | } 277 | 278 | let offset = PaddedPos::from(dir.rotate_clockwise()); 279 | 280 | return make_connection(vec![s_from + offset, s_to + offset]); 281 | } 282 | 283 | let (corner, lane_count) = get_best_corner(grid, from, to); 284 | 285 | match lane_count { 286 | FreeAxisCount::Two => make_connection(vec![corner]), 287 | FreeAxisCount::One(free_axis) => { 288 | let dirs = match free_axis { 289 | Axis::X => (Direction::West, Direction::East), 290 | Axis::Y => (Direction::North, Direction::South), 291 | }; 292 | let corner_candidates = ( 293 | (dirs.0, corner + PaddedPos::from(dirs.0)), 294 | (dirs.1, corner + PaddedPos::from(dirs.1)), 295 | ); 296 | let (dir, corner) = std::cmp::min_by_key(corner_candidates.0, corner_candidates.1, |&(_, c)| { 297 | PaddedPos::taxicab(s_from, c) + PaddedPos::taxicab(s_to, c) 298 | }); 299 | 300 | let mut res = vec![]; 301 | if PaddedPos::straight_line(s_from, corner).is_none() { 302 | res.push(s_from + PaddedPos::from(dir)); 303 | } 304 | res.push(corner); 305 | if PaddedPos::straight_line(s_to, corner).is_none() { 306 | res.push(s_to + PaddedPos::from(dir)); 307 | } 308 | 309 | make_connection(res) 310 | }, 311 | FreeAxisCount::Zero => unreachable!("The FreeAxisCount of the two calculated corners can either be (0, 2) or (1, 1), so by taking the max we should never end up here."), 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /crates/flou/src/render_svg/renderer.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, cmp::Ordering, convert::TryFrom, fmt::Display}; 2 | 3 | use crate::{ 4 | parse::ast::{ArrowheadType, Direction}, 5 | parts::{Connection, Flou, NodeAttributes, RenderConfig, Renderer}, 6 | pos::{impl_pos_from, pos, IndexPos, PixelPos, Position2D}, 7 | svg::{ArrowHead, SVGElement, SVGPath, SVGText}, 8 | }; 9 | 10 | use super::{path::get_path, viewport::Viewport}; 11 | 12 | const ARROWHEAD_WIDTH: i32 = 10; 13 | const ARROWHEAD_HEIGHT: i32 = 10; 14 | const CONNECTION_TEXT_OFFSET: i32 = 20; 15 | 16 | #[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)] 17 | pub(crate) struct PaddedSpace; 18 | pub(crate) type PaddedPos = Position2D; 19 | 20 | impl_pos_from!(PaddedPos, PixelPos, i32); 21 | 22 | impl PaddedPos { 23 | pub(crate) const PADDING: isize = 1; 24 | 25 | fn max(self, val: isize) -> Self { 26 | Self::new(std::cmp::max(self.x, val), std::cmp::max(self.y, val)) 27 | } 28 | 29 | fn normalize(self) -> Self { 30 | let x = match self.x.cmp(&0) { 31 | Ordering::Less => -1, 32 | Ordering::Equal => 0, 33 | Ordering::Greater => 1, 34 | }; 35 | 36 | let y = match self.y.cmp(&0) { 37 | Ordering::Less => -1, 38 | Ordering::Equal => 0, 39 | Ordering::Greater => 1, 40 | }; 41 | 42 | Self::new(x, y) 43 | } 44 | } 45 | 46 | impl From for PaddedPos { 47 | fn from(other: IndexPos) -> Self { 48 | let res = other * (PaddedPos::PADDING + 1) + PaddedPos::PADDING; 49 | Self::new(res.x, res.y) 50 | } 51 | } 52 | 53 | impl From for IndexPos { 54 | fn from(other: PaddedPos) -> Self { 55 | let res = (other - PaddedPos::PADDING) / (PaddedPos::PADDING + 1); 56 | Self::new(res.x, res.y) 57 | } 58 | } 59 | 60 | impl PixelPos { 61 | fn middle(a: Self, b: Self) -> Self { 62 | Self::new((a.x + b.x) / 2, (a.y + b.y) / 2) 63 | } 64 | } 65 | 66 | impl Direction { 67 | pub(crate) fn reverse(&self) -> Self { 68 | match self { 69 | Direction::North => Direction::South, 70 | Direction::South => Direction::North, 71 | Direction::West => Direction::East, 72 | Direction::East => Direction::West, 73 | } 74 | } 75 | 76 | pub(crate) fn rotate_clockwise(&self) -> Self { 77 | match self { 78 | Direction::North => Direction::East, 79 | Direction::South => Direction::West, 80 | Direction::West => Direction::North, 81 | Direction::East => Direction::South, 82 | } 83 | } 84 | 85 | pub(crate) fn rotate_counter_clockwise(&self) -> Self { 86 | match self { 87 | Direction::North => Direction::West, 88 | Direction::South => Direction::East, 89 | Direction::West => Direction::South, 90 | Direction::East => Direction::North, 91 | } 92 | } 93 | } 94 | 95 | pub struct SvgRenderer; 96 | 97 | impl Renderer for SvgRenderer { 98 | fn render<'i>(flou: &'i Flou<'i>, config: &'i RenderConfig) -> Box { 99 | let mut styles: Vec> = Vec::new(); 100 | if config.default_css { 101 | styles.push(include_str!("../css/default.css").into()); 102 | } 103 | 104 | styles.extend(config.css.iter().map(Into::into)); 105 | 106 | let styles = styles 107 | .into_iter() 108 | .map(|css| SVGElement::new("style").text(css)); 109 | 110 | let size = Self::calculate_svg_size(config, flou.grid.size); 111 | 112 | let svg = SVGElement::new("svg") 113 | .attr("xmlns", "http://www.w3.org/2000/svg") 114 | .size(size) 115 | .children(styles); 116 | 117 | let nodes = SVGElement::new("g") 118 | .class("nodes") 119 | .children(Self::render_nodes(config, flou)); 120 | 121 | let connections = SVGElement::new("g") 122 | .class("connections") 123 | .children(Self::render_connections(config, flou)); 124 | 125 | let background = SVGElement::new("rect") 126 | .class("background") 127 | .pos(pos(0, 0)) 128 | .size(size); 129 | 130 | let result = svg.child(background).child(nodes).child(connections); 131 | 132 | Box::new(result) 133 | } 134 | } 135 | 136 | impl SvgRenderer { 137 | fn calculate_node_origin(config: &RenderConfig, pos: IndexPos) -> PixelPos { 138 | let node_offset: PixelPos = pos.into(); 139 | let num_grid_gaps = (node_offset + 1) * PaddedPos::PADDING as i32; 140 | 141 | node_offset * config.node + num_grid_gaps * config.grid_gap 142 | } 143 | 144 | fn calculate_origin(config: &RenderConfig, pos: PaddedPos) -> PixelPos { 145 | let aligned_pos = pos.snap_to_grid(); 146 | let grid_distance = pos - aligned_pos; 147 | 148 | // Pos of the origin of the nearest node, which is grid-aligned. 149 | let node_offset = Self::calculate_node_origin(config, aligned_pos.into()); 150 | 151 | // If the connection point isn't grid-aligned, then it's past the nearest node. 152 | let norm_distance = PixelPos::from(grid_distance.normalize()) * config.node; 153 | 154 | // Include the distance to the nearest node if the position isn't grid-aligned. 155 | let grid_distance = (grid_distance - 1).max(0); 156 | let grid_offset = PixelPos::from(grid_distance) * config.grid_gap; 157 | 158 | node_offset + norm_distance + grid_offset 159 | } 160 | 161 | fn calculate_svg_size(config: &RenderConfig, grid_size: IndexPos) -> PixelPos { 162 | Self::calculate_origin(config, grid_size.into()) 163 | } 164 | 165 | fn render_nodes<'i>(config: &RenderConfig, flou: &'i Flou<'i>) -> Vec> { 166 | let mut positions = flou 167 | .grid 168 | .position_to_id 169 | .iter() 170 | .map(|(&pos, _)| pos) 171 | .collect::>(); 172 | 173 | positions.sort_unstable_by(|a, b| a.y.cmp(&b.y).then(a.x.cmp(&b.x))); 174 | 175 | positions 176 | .into_iter() 177 | .map(|pos| { 178 | let origin = Self::calculate_node_origin(config, pos); 179 | let viewport = Viewport::new(origin, config.node); 180 | 181 | match flou.node_attributes.get(&pos) { 182 | Some(node_attrs) => node_attrs.render(viewport), 183 | None => NodeAttributes::render_default(viewport), 184 | } 185 | }) 186 | .collect() 187 | } 188 | 189 | fn render_connections<'i>(config: &RenderConfig, flou: &'i Flou<'i>) -> Vec> { 190 | let mut connections = flou.connections.iter().collect::>(); 191 | 192 | connections.sort_unstable_by(|a, b| { 193 | let a_pos = a.from.0; 194 | let b_pos = b.from.0; 195 | 196 | a_pos.y.cmp(&b_pos.y).then(a_pos.x.cmp(&b_pos.x)) 197 | }); 198 | 199 | connections 200 | .into_iter() 201 | .map(|c| Self::render_connection(config, flou, c)) 202 | .collect() 203 | } 204 | 205 | fn render_connection<'i>( 206 | config: &RenderConfig, 207 | flou: &Flou<'i>, 208 | connection: &'i Connection, 209 | ) -> SVGElement<'i> { 210 | let path = get_path(&flou.grid, connection.from, connection.to); 211 | 212 | // It is assumed that path always has at least 2 points. 213 | let first_pair: &[PaddedPos] = &[path[1], path[0]]; 214 | 215 | let link_points: Vec<_> = std::iter::once(first_pair) 216 | .chain(path.windows(2)) 217 | .flat_map(<&[_; 2]>::try_from) 218 | .map(|&[from, to]| { 219 | let dir = PaddedPos::straight_line(to, from).unwrap(); 220 | let link_point_offset = Self::get_link_point_offset(config, flou, to, dir); 221 | let point = Self::calculate_origin(config, to) + link_point_offset; 222 | (point, dir) 223 | }) 224 | .collect(); 225 | 226 | let mut path_svg = SVGPath::new(); 227 | for (point, _) in &link_points { 228 | path_svg = path_svg.line_to(*point); 229 | } 230 | 231 | let svg_text = connection.attrs.text.as_ref().map(|text| { 232 | let text_origin = match &link_points[..2] { 233 | [from, to] => { 234 | PixelPos::middle(from.0, to.0) 235 | + PixelPos::from(from.1.rotate_clockwise()) * CONNECTION_TEXT_OFFSET 236 | } 237 | // Again fine since it is assumed that path always has at least 2 points. 238 | _ => unreachable!(), 239 | }; 240 | 241 | SVGText::new(text_origin) 242 | .render(text) 243 | .class("connection-text") 244 | }); 245 | 246 | let path = path_svg.render().class("path"); 247 | 248 | let mut result = SVGElement::new("g") 249 | .class("connection") 250 | .class_opt(connection.attrs.class.as_ref()) 251 | .child(path) 252 | .child_opt(svg_text); 253 | 254 | fn create_arrowhead((link_point, dir): (PixelPos, Direction)) -> SVGElement<'static> { 255 | let arrowhead_viewport = 256 | Viewport::new(link_point, pos(ARROWHEAD_WIDTH, ARROWHEAD_HEIGHT)); 257 | ArrowHead::render(arrowhead_viewport, dir.reverse()).class("arrowhead") 258 | } 259 | 260 | let arrowheads = connection.attrs.arrowheads.unwrap_or_default(); 261 | 262 | if arrowheads == ArrowheadType::Start || arrowheads == ArrowheadType::Both { 263 | result = result 264 | .child(create_arrowhead(link_points.first().cloned().unwrap()).class("start")); 265 | } 266 | 267 | if arrowheads == ArrowheadType::End || arrowheads == ArrowheadType::Both { 268 | result = 269 | result.child(create_arrowhead(link_points.last().cloned().unwrap()).class("end")); 270 | } 271 | 272 | result 273 | } 274 | 275 | fn get_link_point_offset<'i>( 276 | config: &RenderConfig, 277 | flou: &Flou<'i>, 278 | point: PaddedPos, 279 | dir: Direction, 280 | ) -> PixelPos { 281 | let empty_offset = { 282 | let x = if point.grid_x_aligned() { 283 | config.node.x / 2 284 | } else { 285 | config.grid_gap.x / 2 286 | }; 287 | let y = if point.grid_y_aligned() { 288 | config.node.y / 2 289 | } else { 290 | config.grid_gap.y / 2 291 | }; 292 | 293 | pos(x, y) 294 | }; 295 | 296 | if !point.grid_aligned() || matches!(flou.grid.get_id(point.into()), Some(None)) { 297 | return empty_offset; 298 | } 299 | 300 | let origin = Self::calculate_node_origin(config, point.into()); 301 | let viewport = Viewport::new(origin, config.node); 302 | 303 | match flou.node_attributes.get(&IndexPos::from(point)) { 304 | Some(attrs) => attrs.link_point(viewport, dir), 305 | None => NodeAttributes::default().link_point(viewport, dir), 306 | } 307 | } 308 | } 309 | 310 | #[cfg(test)] 311 | mod tests { 312 | use crate::{parts::RenderConfig, pos::pos, test::assert_eq}; 313 | 314 | use super::SvgRenderer; 315 | 316 | #[test] 317 | fn calculates_origin_without_grid_gap() { 318 | let config = &RenderConfig { 319 | node: pos(50, 100), 320 | grid_gap: pos(0, 0), 321 | ..Default::default() 322 | }; 323 | 324 | let actual = SvgRenderer::calculate_node_origin(config, pos(0, 0)); 325 | assert_eq!(actual, pos(0, 0)); 326 | 327 | let actual = SvgRenderer::calculate_node_origin(config, pos(2, 0)); 328 | assert_eq!(actual, pos(100, 0)); 329 | 330 | let actual = SvgRenderer::calculate_node_origin(config, pos(1, 3)); 331 | assert_eq!(actual, pos(50, 300)); 332 | } 333 | 334 | #[test] 335 | fn calculates_origin_with_grid_gap() { 336 | let config = &RenderConfig { 337 | node: pos(50, 100), 338 | grid_gap: pos(10, 20), 339 | ..Default::default() 340 | }; 341 | 342 | let actual = SvgRenderer::calculate_node_origin(config, pos(0, 0)); 343 | assert_eq!(actual, pos(10, 20)); 344 | 345 | let actual = SvgRenderer::calculate_node_origin(config, pos(2, 0)); 346 | assert_eq!(actual, pos(130, 20)); 347 | 348 | let actual = SvgRenderer::calculate_node_origin(config, pos(1, 3)); 349 | assert_eq!(actual, pos(70, 380)); 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /crates/flou/src/render_svg/viewport.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | parse::ast::Direction, 3 | pos::{pos, PixelPos}, 4 | }; 5 | 6 | #[derive(Debug, Clone, Copy)] 7 | pub(crate) struct Viewport { 8 | pub(crate) origin: PixelPos, 9 | pub(crate) size: PixelPos, 10 | } 11 | 12 | pub(crate) struct Midpoints { 13 | pub(crate) top: PixelPos, 14 | pub(crate) bottom: PixelPos, 15 | pub(crate) left: PixelPos, 16 | pub(crate) right: PixelPos, 17 | } 18 | 19 | impl Midpoints { 20 | pub(crate) fn get_from_direction(&self, dir: Direction) -> PixelPos { 21 | match dir { 22 | Direction::North => self.top, 23 | Direction::East => self.right, 24 | Direction::West => self.left, 25 | Direction::South => self.bottom, 26 | } 27 | } 28 | } 29 | 30 | impl Viewport { 31 | pub(crate) fn new(origin: PixelPos, size: PixelPos) -> Self { 32 | Self { origin, size } 33 | } 34 | 35 | pub(crate) fn center(&self) -> PixelPos { 36 | self.origin + self.size / 2 37 | } 38 | 39 | pub(crate) fn midpoints(&self) -> Midpoints { 40 | let rel = self.midpoints_relative(); 41 | 42 | Midpoints { 43 | top: self.origin + rel.top, 44 | bottom: self.origin + rel.bottom, 45 | left: self.origin + rel.left, 46 | right: self.origin + rel.right, 47 | } 48 | } 49 | 50 | pub(crate) fn midpoints_relative(&self) -> Midpoints { 51 | let half = self.size / 2; 52 | 53 | Midpoints { 54 | top: pos(half.x, 0), 55 | bottom: pos(half.x, self.size.y), 56 | left: pos(0, half.y), 57 | right: pos(self.size.x, half.y), 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /crates/flou/src/svg/arrowhead.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | parse::ast::Direction, 3 | pos::{pos, PixelPos}, 4 | render_svg::Viewport, 5 | svg::{SVGElement, SVGPath}, 6 | }; 7 | 8 | #[derive(Debug, PartialEq)] 9 | struct ArrowheadPoints { 10 | tip: PixelPos, 11 | center: PixelPos, 12 | left_corner: PixelPos, 13 | right_corner: PixelPos, 14 | } 15 | 16 | impl ArrowheadPoints { 17 | pub(crate) fn render(self) -> SVGElement<'static> { 18 | SVGPath::new() 19 | .line_to(self.tip) 20 | .line_to(self.left_corner) 21 | .line_to(self.center) 22 | .line_to(self.right_corner) 23 | .line_to(self.tip) 24 | .render() 25 | } 26 | } 27 | 28 | pub(crate) struct ArrowHead; 29 | 30 | impl ArrowHead { 31 | /// `viewport.origin` is the tip of the arrowhead. 32 | /// `viewport.size.x` is the wingspan of the arrowhead. 33 | /// `viewport.size.y` is the length of the arrowhead. 34 | /// `dir` is the direction the arrowhead is facing. 35 | pub(crate) fn render(viewport: Viewport, dir: Direction) -> SVGElement<'static> { 36 | Self::get_points(viewport, dir).render() 37 | } 38 | 39 | fn get_points(viewport: Viewport, dir: Direction) -> ArrowheadPoints { 40 | let dir = dir.reverse(); 41 | 42 | let size = match dir { 43 | Direction::East | Direction::West => pos(viewport.size.y, viewport.size.x), 44 | _ => viewport.size, 45 | }; 46 | 47 | let center = viewport.origin + PixelPos::from(dir) * size / 2; 48 | 49 | let left_corner = PixelPos::from(dir) + PixelPos::from(dir.rotate_clockwise()); 50 | let left_corner = center + left_corner * size / 2; 51 | 52 | let right_corner = PixelPos::from(dir) + PixelPos::from(dir.rotate_counter_clockwise()); 53 | let right_corner = center + right_corner * size / 2; 54 | 55 | ArrowheadPoints { 56 | tip: viewport.origin, 57 | center, 58 | left_corner, 59 | right_corner, 60 | } 61 | } 62 | } 63 | 64 | #[cfg(test)] 65 | mod tests { 66 | use crate::{parse::ast::Direction, pos::pos, render_svg::Viewport, test::assert_eq}; 67 | 68 | use super::{ArrowHead, ArrowheadPoints}; 69 | 70 | #[test] 71 | fn points_are_correct() { 72 | let viewport = Viewport::new(pos(100, 100), pos(20, 40)); 73 | let actual = ArrowHead::get_points(viewport, Direction::North); 74 | 75 | assert_eq!( 76 | actual, 77 | ArrowheadPoints { 78 | tip: pos(100, 100), 79 | center: pos(100, 120), 80 | left_corner: pos(90, 140), 81 | right_corner: pos(110, 140) 82 | } 83 | ); 84 | 85 | let viewport = Viewport::new(pos(200, 200), pos(20, 40)); 86 | let actual = ArrowHead::get_points(viewport, Direction::East); 87 | 88 | assert_eq!( 89 | actual, 90 | ArrowheadPoints { 91 | tip: pos(200, 200), 92 | center: pos(180, 200), 93 | left_corner: pos(160, 190), 94 | right_corner: pos(160, 210), 95 | } 96 | ) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /crates/flou/src/svg/element.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, 3 | fmt::{self}, 4 | }; 5 | 6 | use crate::pos::PixelPos; 7 | 8 | fn escape(input: &str) -> Cow { 9 | fn should_escape(c: char) -> bool { 10 | c == '<' || c == '>' || c == '&' || c == '"' || c == '\'' 11 | } 12 | 13 | if input.contains(should_escape) { 14 | let mut output = String::with_capacity(input.len()); 15 | for c in input.chars() { 16 | match c { 17 | '\'' => output.push_str("'"), 18 | '"' => output.push_str("""), 19 | '<' => output.push_str("<"), 20 | '>' => output.push_str(">"), 21 | '&' => output.push_str("&"), 22 | _ => output.push(c), 23 | } 24 | } 25 | Cow::Owned(output) 26 | } else { 27 | Cow::Borrowed(input) 28 | } 29 | } 30 | 31 | // This is a hacky workaround for lifetime issues in SVGElement::text(). 32 | // There's probably a better way of resolving them without duplicating code. 33 | fn escape_cow(input: Cow) -> Cow { 34 | fn should_escape(c: char) -> bool { 35 | c == '<' || c == '>' || c == '&' || c == '"' || c == '\'' 36 | } 37 | 38 | if input.contains(should_escape) { 39 | let mut output = String::with_capacity(input.len()); 40 | for c in input.chars() { 41 | match c { 42 | '\'' => output.push_str("'"), 43 | '"' => output.push_str("""), 44 | '<' => output.push_str("<"), 45 | '>' => output.push_str(">"), 46 | '&' => output.push_str("&"), 47 | _ => output.push(c), 48 | } 49 | } 50 | Cow::Owned(output) 51 | } else { 52 | input 53 | } 54 | } 55 | 56 | fn indent(depth: usize) -> String { 57 | const SIZE: usize = 2; 58 | " ".repeat(SIZE * depth) 59 | } 60 | 61 | #[derive(Debug)] 62 | enum Node<'a> { 63 | Text(Cow<'a, str>), 64 | Element(SVGElement<'a>), 65 | } 66 | 67 | impl Node<'_> { 68 | fn print(&self, depth: usize, f: &mut fmt::Formatter) -> fmt::Result { 69 | match self { 70 | Node::Text(text) => { 71 | for (i, line) in text.lines().enumerate() { 72 | if i != 0 { 73 | writeln!(f)?; 74 | } 75 | f.write_str(&indent(depth))?; 76 | f.write_str(line)?; 77 | } 78 | } 79 | Node::Element(el) => el.print(depth, f)?, 80 | }; 81 | 82 | Ok(()) 83 | } 84 | } 85 | 86 | #[derive(Debug)] 87 | pub(crate) struct SVGElement<'a> { 88 | tag: Cow<'a, str>, 89 | attributes: Vec<(Cow<'a, str>, Cow<'a, str>)>, 90 | classes: Vec>, 91 | children: Vec>, 92 | } 93 | 94 | impl<'a> SVGElement<'a> { 95 | pub(crate) fn new>>(tag: I) -> Self { 96 | Self { 97 | tag: tag.into(), 98 | attributes: Vec::new(), 99 | classes: Vec::new(), 100 | children: Vec::new(), 101 | } 102 | } 103 | 104 | pub(crate) fn pos(self, pos: PixelPos) -> Self { 105 | self.attr("x", pos.x.to_string()) 106 | .attr("y", pos.y.to_string()) 107 | } 108 | 109 | pub(crate) fn cpos(self, pos: PixelPos) -> Self { 110 | self.attr("cx", pos.x.to_string()) 111 | .attr("cy", pos.y.to_string()) 112 | } 113 | 114 | pub(crate) fn size(self, size: PixelPos) -> Self { 115 | self.attr("width", size.x.to_string()) 116 | .attr("height", size.y.to_string()) 117 | } 118 | 119 | pub(crate) fn class>>(mut self, s: I) -> Self { 120 | self.classes.push(s.into()); 121 | self 122 | } 123 | 124 | pub(crate) fn class_opt>>(self, s: Option) -> Self { 125 | match s { 126 | Some(s) => self.class(s), 127 | None => self, 128 | } 129 | } 130 | 131 | pub(crate) fn attr(mut self, key: K, value: V) -> Self 132 | where 133 | K: Into>, 134 | V: Into>, 135 | { 136 | let key = key.into(); 137 | if key == "class" { 138 | panic!("Use .class() instead."); 139 | } 140 | 141 | self.attributes.push((key, value.into())); 142 | self 143 | } 144 | 145 | pub(crate) fn child(mut self, child: SVGElement<'a>) -> Self { 146 | self.children.push(Node::Element(child)); 147 | self 148 | } 149 | 150 | pub(crate) fn child_opt(self, child: Option>) -> Self { 151 | match child { 152 | Some(child) => self.child(child), 153 | None => self, 154 | } 155 | } 156 | 157 | pub(crate) fn text>>(mut self, text: I) -> Self { 158 | let text = text.into(); 159 | let text = escape_cow(text); 160 | self.children.push(Node::Text(text)); 161 | self 162 | } 163 | 164 | pub(crate) fn children(mut self, children: T) -> Self 165 | where 166 | T: IntoIterator>, 167 | { 168 | self.children 169 | .extend(children.into_iter().map(Node::Element)); 170 | self 171 | } 172 | 173 | fn print(&self, depth: usize, f: &mut fmt::Formatter) -> fmt::Result { 174 | let attributes = self 175 | .attributes 176 | .iter() 177 | .map(|(key, value)| (key, escape(value))) 178 | .collect::>(); 179 | 180 | let classes = self.classes.iter().map(|x| escape(x)).collect::>(); 181 | 182 | f.write_str(&indent(depth))?; 183 | f.write_str("<")?; 184 | f.write_str(&self.tag)?; 185 | 186 | if !classes.is_empty() { 187 | write!(f, " class=\"{}\"", classes.join(" "))?; 188 | } 189 | 190 | let attributes = attributes 191 | .iter() 192 | .map(|(k, v)| format!(" {}=\"{}\"", k, v)) 193 | .collect::>() 194 | .join(""); 195 | f.write_str(&attributes)?; 196 | 197 | if self.children.is_empty() { 198 | f.write_str(" />")?; 199 | return Ok(()); 200 | } 201 | 202 | f.write_str(">")?; 203 | 204 | match self.children.first() { 205 | Some(child @ Node::Text(_)) if self.children.len() == 1 => { 206 | child.print(0, f)?; 207 | } 208 | _ => { 209 | for child in &self.children { 210 | writeln!(f)?; 211 | child.print(depth + 1, f)?; 212 | } 213 | writeln!(f)?; 214 | f.write_str(&indent(depth))?; 215 | } 216 | } 217 | 218 | write!(f, "", self.tag)?; 219 | Ok(()) 220 | } 221 | } 222 | 223 | impl fmt::Display for SVGElement<'_> { 224 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 225 | self.print(0, f) 226 | } 227 | } 228 | 229 | #[cfg(test)] 230 | mod tests { 231 | use super::{escape, SVGElement}; 232 | 233 | use crate::test::assert_eq; 234 | 235 | #[test] 236 | fn tag_only() { 237 | assert_eq!(SVGElement::new("a").to_string(), ""); 238 | } 239 | 240 | #[test] 241 | fn with_attributes() { 242 | assert_eq!( 243 | SVGElement::new("a").attr("foo", "bar").to_string(), 244 | r#""#, 245 | ); 246 | 247 | assert_eq!( 248 | SVGElement::new("a") 249 | .attr("foo", "bar") 250 | .attr("bar", "baz") 251 | .to_string(), 252 | r#""#, 253 | ); 254 | } 255 | 256 | #[test] 257 | fn with_child() { 258 | assert_eq!( 259 | SVGElement::new("div") 260 | .child(SVGElement::new("foo")) 261 | .to_string(), 262 | r#" 263 |
264 | 265 |
266 | "# 267 | .trim(), 268 | ); 269 | } 270 | 271 | #[test] 272 | fn escape_attributes() { 273 | assert_eq!(escape("\""), """); 274 | assert_eq!(escape("'"), "'"); 275 | assert_eq!(escape("<"), "<"); 276 | assert_eq!(escape(">"), ">"); 277 | assert_eq!(escape("&"), "&"); 278 | } 279 | 280 | #[test] 281 | fn with_escaped_attribute() { 282 | assert_eq!( 283 | SVGElement::new("div").class("'Hi'").to_string(), 284 | r#"
"#, 285 | ) 286 | } 287 | 288 | #[test] 289 | fn complex_example() { 290 | assert_eq!( 291 | SVGElement::new("p") 292 | .class("block") 293 | .child( 294 | SVGElement::new("a") 295 | .attr("href", "example.com") 296 | .child(SVGElement::new("span").text("Hi")) 297 | .text("there") 298 | ) 299 | .child(SVGElement::new("button").text("Press me")) 300 | .to_string(), 301 | r#" 302 |

303 | 304 | Hi 305 | there 306 | 307 | 308 |

309 | "# 310 | .trim(), 311 | ); 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /crates/flou/src/svg/mod.rs: -------------------------------------------------------------------------------- 1 | mod arrowhead; 2 | mod element; 3 | mod path; 4 | mod text; 5 | 6 | pub(crate) use arrowhead::*; 7 | pub(crate) use element::*; 8 | pub(crate) use path::*; 9 | pub(crate) use text::*; 10 | -------------------------------------------------------------------------------- /crates/flou/src/svg/path.rs: -------------------------------------------------------------------------------- 1 | use crate::{pos::PixelPos, svg::SVGElement}; 2 | 3 | pub(crate) enum PathD { 4 | MoveTo(PixelPos), 5 | LineTo(PixelPos), 6 | End, 7 | } 8 | 9 | impl ToString for PathD { 10 | fn to_string(&self) -> String { 11 | match self { 12 | PathD::MoveTo(pos) => format!("M {} {}", pos.x, pos.y), 13 | PathD::LineTo(pos) => format!("L {} {}", pos.x, pos.y), 14 | PathD::End => "Z".into(), 15 | } 16 | } 17 | } 18 | 19 | pub(crate) struct SVGPath { 20 | d: Vec, 21 | } 22 | 23 | impl SVGPath { 24 | pub(crate) fn new() -> Self { 25 | Self { d: Vec::new() } 26 | } 27 | 28 | pub(crate) fn line_to(mut self, pos: PixelPos) -> Self { 29 | let cmd = if self.d.is_empty() { 30 | PathD::MoveTo(pos) 31 | } else { 32 | PathD::LineTo(pos) 33 | }; 34 | self.d.push(cmd); 35 | self 36 | } 37 | 38 | pub(crate) fn end(mut self) -> Self { 39 | self.d.push(PathD::End); 40 | self 41 | } 42 | 43 | pub(crate) fn render(self) -> SVGElement<'static> { 44 | SVGElement::new("path").attr("d", self.get_d()) 45 | } 46 | 47 | fn get_d(&self) -> String { 48 | self.d 49 | .iter() 50 | .map(ToString::to_string) 51 | .collect::>() 52 | .join(" ") 53 | } 54 | } 55 | 56 | #[cfg(test)] 57 | mod tests { 58 | use crate::{pos::pos, svg::SVGPath}; 59 | 60 | use crate::test::assert_eq; 61 | 62 | #[test] 63 | fn create_path() { 64 | let mut path = SVGPath::new().line_to(pos(10, 20)); 65 | assert_eq!(path.get_d(), "M 10 20"); 66 | 67 | path = path.line_to(pos(30, 40)); 68 | assert_eq!(path.get_d(), "M 10 20 L 30 40"); 69 | 70 | path = path.end(); 71 | assert_eq!(path.get_d(), "M 10 20 L 30 40 Z"); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /crates/flou/src/svg/text.rs: -------------------------------------------------------------------------------- 1 | use crate::{pos::PixelPos, svg::SVGElement}; 2 | 3 | pub(crate) struct SVGText { 4 | pos: PixelPos, 5 | } 6 | 7 | impl SVGText { 8 | pub(crate) fn new(pos: PixelPos) -> Self { 9 | Self { pos } 10 | } 11 | 12 | pub(crate) fn render(self, s: &str) -> SVGElement { 13 | let text = SVGElement::new("text").pos(self.pos); 14 | let line_count = s.lines().count(); 15 | 16 | if line_count == 1 { 17 | return text.text(s); 18 | } 19 | 20 | let children = s.lines().enumerate().map(|(i, line)| { 21 | let offset = Self::calculate_offset(i, line_count); 22 | 23 | SVGElement::new("tspan") 24 | .attr("x", self.pos.x.to_string()) 25 | .attr("dy", format!("{}em", offset)) 26 | .text(line) 27 | }); 28 | 29 | text.children(children) 30 | } 31 | 32 | fn calculate_offset(line_number: usize, line_count: usize) -> f32 { 33 | if line_number == 0 { 34 | -((line_count - 1) as f32) / 2.0 35 | } else { 36 | 1.0 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /crates/flou/src/test.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{HashMap, HashSet}, 3 | iter::FromIterator, 4 | }; 5 | 6 | use crate::parse::{ast, Input, Parser}; 7 | 8 | pub(crate) use pretty_assertions::assert_eq; 9 | 10 | pub(crate) fn assert_parsed_eq<'i, P: Parser<'i, O>, O: std::fmt::Debug + PartialEq>( 11 | mut parser: P, 12 | input: Input<'i>, 13 | expected: O, 14 | ) { 15 | let actual = parser.parse(input); 16 | assert!(actual.is_ok(), "Unexpected error: {}", actual.unwrap_err()); 17 | 18 | let actual = actual.unwrap(); 19 | assert_eq!( 20 | actual.1, expected, 21 | "Parsed value does not match expected value" 22 | ); 23 | assert_eq!(actual.0, "", "Unexpected input: {:?}", actual.0); 24 | } 25 | 26 | pub(crate) fn assert_not_parsed<'i, P: Parser<'i, O>, O: std::fmt::Debug + PartialEq>( 27 | mut parser: P, 28 | input: Input<'i>, 29 | ) { 30 | let actual = parser.parse(input); 31 | assert!(actual.is_err(), "Unexpected success: {:?}", actual.unwrap()); 32 | } 33 | 34 | pub(crate) fn id(s: &str) -> ast::Identifier { 35 | ast::Identifier(s) 36 | } 37 | 38 | pub(crate) fn map>( 39 | xs: I, 40 | ) -> HashMap { 41 | HashMap::from_iter(xs) 42 | } 43 | 44 | pub(crate) fn set>(xs: I) -> HashSet { 45 | HashSet::from_iter(xs) 46 | } 47 | -------------------------------------------------------------------------------- /crates/flou_cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flou_cli" 3 | description = "CLI for Flou, a flowchart description language." 4 | homepage = "https://asha20.github.io/flou" 5 | repository = "https://github.com/Asha20/flou" 6 | readme = "../../README.md" 7 | license = "MIT OR Apache-2.0" 8 | keywords = ["flowchart"] 9 | categories = ["command-line-utilities"] 10 | 11 | version = "0.1.0" 12 | edition = "2018" 13 | 14 | [dependencies] 15 | flou = { path = "../flou", version = "0.1.0" } 16 | structopt = "0.3.25" 17 | 18 | [[bin]] 19 | name = "flou" 20 | path = "src/main.rs" -------------------------------------------------------------------------------- /crates/flou_cli/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Vukašin Stepanović 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /crates/flou_cli/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Vukašin Stepanović 4 | 5 | Permission is hereby granted, free of charge, to any 6 | person obtaining a copy of this software and associated 7 | documentation files (the "Software"), to deal in the 8 | Software without restriction, including without 9 | limitation the rights to use, copy, modify, merge, 10 | publish, distribute, sublicense, and/or sell copies of 11 | the Software, and to permit persons to whom the Software 12 | is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice 16 | shall be included in all copies or substantial portions 17 | of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 20 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 21 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 22 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 23 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 24 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 25 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 26 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 27 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /crates/flou_cli/README.md: -------------------------------------------------------------------------------- 1 | # flou_cli 2 | 3 | This binary crate creates an SVG file from a Flou flowchart description. -------------------------------------------------------------------------------- /crates/flou_cli/src/lib.rs: -------------------------------------------------------------------------------- 1 | use flou::{Flou, FlouError, LogicError, RenderConfig, Renderer, ResolutionError, SvgRenderer}; 2 | use std::convert::TryFrom; 3 | use std::fmt; 4 | use std::io::{BufWriter, Write}; 5 | use std::{ 6 | fs, 7 | io::{self, BufRead, BufReader}, 8 | path::PathBuf, 9 | }; 10 | use structopt::StructOpt; 11 | 12 | #[derive(Debug, StructOpt)] 13 | pub struct Opt { 14 | /// Input file; use "-" to read input from stdin. 15 | #[structopt(parse(from_os_str))] 16 | input: PathBuf, 17 | 18 | /// Output file; outputs to stdout if omitted. 19 | #[structopt(short = "o", long = "output", parse(from_os_str))] 20 | output: Option, 21 | 22 | /// Specifies the width and height of nodes in the grid (format: x,y). 23 | #[structopt(short = "n", long = "node", parse(try_from_str = parse_size))] 24 | node: Option<(i32, i32)>, 25 | 26 | /// Specifies the width and height of the grid gaps (format: x,y). 27 | #[structopt(short = "g", long = "gap", parse(try_from_str = parse_size))] 28 | gap: Option<(i32, i32)>, 29 | 30 | /// Injects the given CSS files into the generated SVG. 31 | #[structopt(long = "css", parse(from_os_str))] 32 | css: Option>, 33 | 34 | /// Don't inject the default CSS file. 35 | #[structopt(long = "no-default-css")] 36 | no_default_css: bool, 37 | } 38 | 39 | fn parse_size(src: &str) -> Result<(i32, i32), &'static str> { 40 | let tokens = src.split(',').collect::>(); 41 | if tokens.len() != 2 { 42 | return Err("Size should have format: \"x,y\""); 43 | } 44 | 45 | let x = tokens[0] 46 | .parse::() 47 | .map_err(|_| "Could not parse X coordinate")?; 48 | let y = tokens[1] 49 | .parse::() 50 | .map_err(|_| "Could not parse Y coordinate")?; 51 | 52 | if x < 0 || y < 0 { 53 | return Err("X and Y cannot be negative."); 54 | } 55 | 56 | Ok((x, y)) 57 | } 58 | 59 | pub enum Error { 60 | InputOpen(io::Error), 61 | InputRead(io::Error), 62 | OutputOpen(io::Error), 63 | OutputWrite(io::Error), 64 | CssRead(PathBuf, io::Error), 65 | Parse(String), 66 | } 67 | 68 | pub fn run(opt: Opt) -> Result<(), Error> { 69 | let mut reader: Box = if opt.input != PathBuf::from("-") { 70 | fs::File::open(opt.input) 71 | .map(|x| -> Box { Box::new(BufReader::new(x)) }) 72 | .map_err(Error::InputOpen)? 73 | } else { 74 | Box::new(BufReader::new(io::stdin())) 75 | }; 76 | 77 | let mut writer: Box = if let Some(filename) = opt.output { 78 | fs::OpenOptions::new() 79 | .create(true) 80 | .write(true) 81 | .truncate(true) 82 | .open(filename) 83 | .map(|x| -> Box { Box::new(BufWriter::new(x)) }) 84 | .map_err(Error::OutputOpen)? 85 | } else { 86 | Box::new(BufWriter::new(io::stdout())) 87 | }; 88 | 89 | let mut input = String::new(); 90 | reader 91 | .read_to_string(&mut input) 92 | .map_err(Error::InputRead)?; 93 | 94 | let css = opt 95 | .css 96 | .unwrap_or_default() 97 | .into_iter() 98 | .map(|filename| fs::read_to_string(&filename).map_err(|e| Error::CssRead(filename, e))) 99 | .collect::, _>>()?; 100 | 101 | let flou = Flou::try_from(input.as_str()).map_err(|x| Error::Parse(flou_error_to_string(x)))?; 102 | 103 | let mut config = RenderConfig { 104 | css, 105 | default_css: !opt.no_default_css, 106 | ..Default::default() 107 | }; 108 | 109 | if let Some(node) = opt.node { 110 | config.node = node.into(); 111 | } 112 | 113 | if let Some(gap) = opt.gap { 114 | config.grid_gap = gap.into(); 115 | } 116 | 117 | let output = SvgRenderer::render(&flou, &config); 118 | 119 | write!(writer, "{}", output).map_err(Error::OutputWrite)?; 120 | 121 | Ok(()) 122 | } 123 | 124 | fn flou_error_to_string(e: FlouError) -> String { 125 | match e { 126 | FlouError::Parse(e) => { 127 | format!("Error parsing Flou:\n\n{}", e) 128 | } 129 | FlouError::Logic(e) => { 130 | format!("Error in Flou logic:\n{}", logic_error_to_string(e)) 131 | } 132 | } 133 | } 134 | 135 | fn logic_error_to_string(e: LogicError) -> String { 136 | match e { 137 | LogicError::DuplicateLabels(labels) => { 138 | let labels = print_map(labels, "\n", |label, locations| { 139 | let locations = print_sequence(locations, ", ", |x| x.to_string()); 140 | format!(" - \"{}\" at: {}", label, locations) 141 | }); 142 | 143 | format!("Some labels are used more than once:\n\n{}", labels) 144 | } 145 | LogicError::DuplicateDefinitions(ids) => { 146 | let ids = print_sequence(ids, "\n", |id| format!(" - \"{}\"", id)); 147 | format!("Some identifiers have multiple definitions:\n\n{}", ids) 148 | } 149 | LogicError::DuplicateNodeAttributesInDefinitions(attrs) => { 150 | let attrs = print_map(attrs, "\n", |id, attrs| { 151 | let attrs = print_sequence(attrs, ", ", quote); 152 | format!(" - \"{}\" has duplicate(s): {}", id, attrs) 153 | }); 154 | 155 | format!( 156 | "Some node definitions have duplicate attributes:\n\n{}", 157 | attrs 158 | ) 159 | } 160 | LogicError::DuplicateNodeAttributesInGrid(attrs) => { 161 | let attrs = print_map(attrs, "\n", |id, attrs| { 162 | let attrs = print_sequence(attrs, ", ", quote); 163 | format!(" - Node at {} has duplicate(s): {}", id, attrs) 164 | }); 165 | 166 | format!( 167 | "Some nodes declared in the grid have duplicate attributes:\n\n{}", 168 | attrs 169 | ) 170 | } 171 | LogicError::DuplicateConnectionAttributesInDefinitions(attrs) => { 172 | let attrs = print_map(attrs, "\n", |id, index_map| { 173 | let indexes = print_map(index_map, "\n", |index, attrs| { 174 | format!( 175 | " - At index {}: {}", 176 | index, 177 | print_sequence(attrs, ", ", quote) 178 | ) 179 | }); 180 | 181 | format!(" - At definition \"{}\":\n{}", id, indexes) 182 | }); 183 | 184 | format!( 185 | "Some connections in node definitions have duplicate attributes:\n\n{}", 186 | attrs 187 | ) 188 | } 189 | LogicError::DuplicateConnectionAttributesInGrid(attrs) => { 190 | let attrs = print_map(attrs, "\n", |pos, index_map| { 191 | let index_map = print_map(index_map, "\n", |index, attrs| { 192 | format!( 193 | " - For connection at index {}: {}", 194 | index, 195 | print_sequence(attrs, ", ", quote) 196 | ) 197 | }); 198 | 199 | format!(" - At grid position {}:\n{}", pos, index_map) 200 | }); 201 | 202 | format!( 203 | "Some connections declared in the grid have duplicate attributes:\n\n{}", 204 | attrs 205 | ) 206 | } 207 | LogicError::InvalidDestination(errors) => { 208 | let errors = print_map(errors, "\n", |pos, index_map| { 209 | let index_map = print_map(index_map, "\n", |index, error| { 210 | format!( 211 | " - For connection at index {}: {}", 212 | index, 213 | print_resolution_error(error) 214 | ) 215 | }); 216 | 217 | format!(" - For node at grid position {}:\n{}", pos, index_map) 218 | }); 219 | 220 | format!( 221 | "Could not resolve destination for some node's connections:\n\n{}", 222 | errors 223 | ) 224 | } 225 | } 226 | } 227 | 228 | fn quote(item: T) -> String { 229 | format!("\"{}\"", item) 230 | } 231 | 232 | fn print_sequence>( 233 | seq: I, 234 | delimiter: &str, 235 | print: impl Fn(T) -> String, 236 | ) -> String { 237 | seq.into_iter() 238 | .map(print) 239 | .collect::>() 240 | .join(delimiter) 241 | } 242 | 243 | fn print_map>( 244 | map: I, 245 | delimiter: &str, 246 | print: impl Fn(K, V) -> String, 247 | ) -> String { 248 | map.into_iter() 249 | .map(|(k, v)| print(k, v)) 250 | .collect::>() 251 | .join(delimiter) 252 | } 253 | 254 | fn print_resolution_error(e: ResolutionError) -> String { 255 | match e { 256 | ResolutionError::InvalidDirection(dir) => { 257 | format!("No destination found in direction: {}", dir) 258 | } 259 | ResolutionError::UnknownLabel(label) => format!("No destination with label: \"{}\"", label), 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /crates/flou_cli/src/main.rs: -------------------------------------------------------------------------------- 1 | use flou_cli::{run, Error, Opt}; 2 | use structopt::StructOpt; 3 | 4 | fn main() { 5 | let opt = Opt::from_args(); 6 | run(opt).unwrap_or_else(|e| { 7 | match e { 8 | Error::InputOpen(e) => eprintln!("Could not open input file: {}", e), 9 | Error::InputRead(e) => eprintln!("Could not read input: {}", e), 10 | Error::OutputOpen(e) => eprintln!("Could not open output file: {}", e), 11 | Error::OutputWrite(e) => eprintln!("Could not write output: {}", e), 12 | Error::CssRead(filename, e) => { 13 | eprintln!( 14 | "Could not read CSS file \"{}\": {}", 15 | filename.to_string_lossy(), 16 | e 17 | ) 18 | } 19 | Error::Parse(e) => eprintln!("{}", e), 20 | }; 21 | 22 | std::process::exit(1); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | book/ 2 | -------------------------------------------------------------------------------- /docs/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Vukašin Stepanović"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "flou Documentation" 7 | -------------------------------------------------------------------------------- /docs/src/README.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | ### What is Flou? 4 | 5 | Flou is a [domain-specific language (DSL)](https://en.wikipedia.org/wiki/Domain-specific_language) for describing flowcharts. It is also a CLI of the same name that renders the previously mentioned flowchart description into an SVG file. 6 | 7 | Flou's goal is to offer a textual representation of flowcharts. 8 | 9 | ### Reasons to use Flou? 10 | 11 | - If you need to generate a flowchart automatically, you can write a program that generates Flou DSL and then use the CLI tool to compile the DSL into an image. 12 | - Textual representation avoids easy-to-miss slight design inconsistencies that might occur when creating a flowchart with a visual design software. 13 | - Flou makes modifying shared flowchart parts straightforward and painless. 14 | - A textual flowchart representation is more suited for version control. 15 | 16 | ### Reasons NOT to use Flou? 17 | 18 | - It's still in beta. This means some features might be unpolished. 19 | - Connections that happen to have overlapping segments can bring visual ambiguity since Flou CLI won't render them side by side and will overlap them instead. However, this issue can be offset by the user since they can pick and choose the connection sides. -------------------------------------------------------------------------------- /docs/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | [Introduction](README.md) 4 | [Installation](install.md) 5 | 6 | - [Syntax](syntax/README.md) 7 | - [Hello World!](syntax/hello_world.md) 8 | - [Making connections](syntax/making_connections.md) 9 | - [Using a define block](syntax/define_block.md) 10 | - [List of attributes](syntax/list_of_attributes.md) 11 | - [CLI](cli.md) 12 | - [Styling your flowchart](styling_flowchart.md) -------------------------------------------------------------------------------- /docs/src/cli.md: -------------------------------------------------------------------------------- 1 | ## CLI 2 | 3 | Usage: 4 | 5 | $ flou [FLAGS] [OPTIONS] 6 | 7 | Flags: 8 | 9 | - `-h, --help` — Prints help information. 10 | - `-V, --version` — Prints version information. 11 | - `--no-default-css` — If present, the default CSS file won't be embedded. Read more [here](styling_flowchart.md). 12 | 13 | Options: 14 | 15 | - `--css ...` — Injects one or more CSS files into the generated SVG. Read more [here](styling_flowchart.md). 16 | - `-g, --gap ` — Specifies the size of the grid gaps. Defaults to (50, 50). 17 | - `-n, --node ` — Specifies the size of nodes in the grid. Defaults to (200, 100). 18 | - `-o, --output ` — Specifies the output SVG file. Outputs to stdout if no output file is provided. 19 | 20 | Args: 21 | - `` — The input file, written in Flou DSL. Use `-` to read from standard input instead. -------------------------------------------------------------------------------- /docs/src/install.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | You can grab a prebuilt binary from [here](https://github.com/Asha20/flou/releases). Alternatively, if you have Cargo, you can use: 4 | 5 | $ cargo install flou_cli 6 | 7 | Which will install the `flou` binary for you to use. -------------------------------------------------------------------------------- /docs/src/styling_flowchart.md: -------------------------------------------------------------------------------- 1 | ## Styling your flowchart 2 | 3 | ### SVG output structure 4 | 5 | Here's a simple flowchart: 6 | 7 | ```js 8 | grid { 9 | src("Source"); 10 | dest("Destination", class: "my-destination"); 11 | } 12 | 13 | define { 14 | src(connect: { 15 | s:n@s(class: "my-south-connection", arrowheads: both); 16 | w:w@s(class: "my-west-connection"); 17 | }); 18 | } 19 | ``` 20 | 21 | ![Example 1](styling_flowchart/example1.svg) 22 | 23 | Let's look at the generated SVG: 24 | 25 | ```xml 26 | 27 | 30 | 31 | 32 | 33 | 34 | 35 | Source 36 | 37 | 38 | 39 | 40 | Destination 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | ``` 58 | 59 | Various parts of the flowchart are tagged with CSS classes. By default, the flowchart is rendered with some default CSS that targets these classes (that's the ` 33 | 34 | 35 | 36 | 37 | Source 38 | 39 | 40 | 41 | Destination 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /docs/src/styling_flowchart/example2.svg: -------------------------------------------------------------------------------- 1 | 2 | 33 | 52 | 53 | 54 | 55 | 56 | Source 57 | 58 | 59 | 60 | Destination 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /docs/src/syntax/README.md: -------------------------------------------------------------------------------- 1 | ## Syntax 2 | 3 | This section talks about Flou's syntax. You'll learn: 4 | 5 | - How to define flowcharts using `grid`. 6 | - How to reuse common parts using `define`. 7 | - All about the basic building blocks of flowcharts: nodes and connections. -------------------------------------------------------------------------------- /docs/src/syntax/define_block.md: -------------------------------------------------------------------------------- 1 | ## Using a define block 2 | 3 | Let's take a look at the following flowchart: 4 | 5 | ```js 6 | grid { 7 | block("Think about going outside", class: "pink", connect: s:n@s); 8 | condition("Is it raining?", shape: diamond, class: "yellow", connect: { s:n@s("No"); e:w#stay("Yes") }), block#stay("Stay inside", class: "pink"); 9 | condition("Is it cold?", shape: diamond, class: "yellow", connect: { s:n@s("No"); e:w#stay("Yes") }); 10 | condition("Is it night?", shape: diamond, class: "yellow", connect: { s:n@s("No"); e:w#stay("Yes") }); 11 | block("Go outside", class: "pink"); 12 | } 13 | ``` 14 | 15 | ![Example 1](define_block/example1.svg) 16 | 17 | This flowchart has lots of repetition going on, which means: 18 | 19 | - It's harder to read 20 | - If you want to change something, you need to change it in multiple places 21 | - Long lines make it hard to figure out how many columns there are in a row 22 | 23 | We can rewrite the example like so: 24 | 25 | ```js 26 | grid { 27 | block("Think about going outside", connect: s:n@s); 28 | condition("Is it raining?"), block#stay("Stay inside"); 29 | condition("Is it cold?"); 30 | condition("Is it night?"); 31 | block("Go outside"); 32 | } 33 | 34 | define { 35 | block(class: "pink"); 36 | condition(shape: diamond, class: "yellow", connect: { 37 | s:n@s("No"); 38 | e:w#stay("Yes"); 39 | }); 40 | } 41 | ``` 42 | 43 | *Definitions* are a mechanism for reusing common attributes. They belong in the optional `define` block and are separated by semicolons. They are specified in the same way as nodes in `grid`, with one exception being that labels can't be attached to identifiers inside `define`. Attributes specified for an identifier in `define` will apply to all nodes with the same identifier in `grid`. 44 | 45 | Apart from using the `define` block to put shared attributes in one place, a valid use-case could also be putting a node's long list of attributes in `define` so as to reduce clutter in `grid`, making it more readable. 46 | 47 | It can also be convenient to put the `connect` attribute in `define` if there are multiple connections so that they can be spread across multiple lines for improved readability like in the example above. The same could be done in `grid`, though it's generally not a good idea since it makes it harder to tell how many rows the grid has. 48 | 49 | ### Overwriting 50 | 51 | If a node has the same attribute defined in `grid` and in `define`, the value in `grid` will overwrite the one in `define`: 52 | 53 | ```js 54 | grid { 55 | src("Source", connect: e:w@e), dest; 56 | dest("Custom destination", shape: rect); 57 | } 58 | 59 | define { 60 | src(shape: square, connect: {s:n@s, e:w@e}); 61 | dest("Destination", shape: circle); 62 | } 63 | ``` 64 | 65 | ![Example 3](define_block/example3.svg) 66 | 67 | As an example of where this can come in handy, say we wanted all `block` nodes connect to the node below them except for the last one. This can be achieved by overriding its `connect` to an empty list: 68 | 69 | ```js 70 | grid { 71 | block("Step one"); 72 | block("Step two"); 73 | block("Step three"); 74 | block("Step four", connect: {}); 75 | } 76 | 77 | define { 78 | block(shape: ellipse, class: "pink", connect: s:n@s); 79 | } 80 | ``` 81 | 82 | ![Example 4](define_block/example4.svg) 83 | 84 | -------------------------------------------------------------------------------- /docs/src/syntax/define_block/example1.svg: -------------------------------------------------------------------------------- 1 | 2 | 41 | 42 | 43 | 44 | 45 | Think about going outside 46 | 47 | 48 | 49 | Is it raining? 50 | 51 | 52 | 53 | Stay inside 54 | 55 | 56 | 57 | Is it cold? 58 | 59 | 60 | 61 | Is it night? 62 | 63 | 64 | 65 | Go outside 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | No 76 | 77 | 78 | 79 | 80 | Yes 81 | 82 | 83 | 84 | 85 | No 86 | 87 | 88 | 89 | 90 | Yes 91 | 92 | 93 | 94 | 95 | No 96 | 97 | 98 | 99 | 100 | Yes 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /docs/src/syntax/define_block/example2.svg: -------------------------------------------------------------------------------- 1 | 2 | 33 | 34 | 35 | 36 | 37 | Think about going outside 38 | 39 | 40 | 41 | Is it raining? 42 | 43 | 44 | 45 | Stay inside 46 | 47 | 48 | 49 | Is it cold? 50 | 51 | 52 | 53 | Is it night? 54 | 55 | 56 | 57 | Go outside 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | No 68 | 69 | 70 | 71 | 72 | Yes 73 | 74 | 75 | 76 | 77 | No 78 | 79 | 80 | 81 | 82 | Yes 83 | 84 | 85 | 86 | 87 | No 88 | 89 | 90 | 91 | 92 | Yes 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /docs/src/syntax/define_block/example3.svg: -------------------------------------------------------------------------------- 1 | 2 | 33 | 34 | 35 | 36 | 37 | Source 38 | 39 | 40 | 41 | Destination 42 | 43 | 44 | 45 | Custom destination 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /docs/src/syntax/define_block/example4.svg: -------------------------------------------------------------------------------- 1 | 2 | 37 | 38 | 39 | 40 | 41 | Step one 42 | 43 | 44 | 45 | Step two 46 | 47 | 48 | 49 | Step three 50 | 51 | 52 | 53 | Step four 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /docs/src/syntax/hello_world.md: -------------------------------------------------------------------------------- 1 | ## Hello World! 2 | 3 | Let's create our first flowchart: 4 | 5 | ```js 6 | grid { 7 | // This is a comment 8 | block(text: "Hello World!", shape: rect); 9 | } 10 | ``` 11 | 12 | ![Example 1](hello_world/example1.svg) 13 | 14 | In Flou, all flowcharts are represented with a *grid*. The basic building blocks of grids are *nodes*. Our Hello World flowchart has a single node. `block` is that node's *identifier*. `block` is completely arbitrary and can be replaced with anything else. We'll talk more about identifiers and why they're useful later. Other than the identifier, we've included some basic *attributes* on our node. Also, line-comments can be defined using `//`. 15 | 16 | ### Shorthands 17 | 18 | Since `text` is the most commonly used attribute, it has a convenient shorthand. We could use the following form and get the same result: 19 | 20 | ```js 21 | grid { 22 | block("Hello World!", shape: rect); 23 | } 24 | ``` 25 | 26 | When using the text shorthand, the text string needs to be the first element of the attribute list. For example, this is invalid: 27 | 28 | ```js 29 | grid { 30 | block(shape: rect, "Hello World!"); 31 | } 32 | ``` 33 | 34 | Rectangle is the default node shape, so we could also omit the `shape` attribute: 35 | 36 | ```js 37 | grid { 38 | block("Hello World!"); 39 | } 40 | ``` 41 | 42 | ### More nodes 43 | 44 | A flowchart with a single node isn't that interesting to look at, so let's add some more nodes. Grid rows are separated with a semicolon, while grid columns are separated with a comma. If you want to leave some space intentionally empty like in the third row, use the special `_` node. 45 | 46 | ```js 47 | grid { 48 | block("One"), block("Two"); 49 | block("Three"); 50 | _, block("Four"); 51 | } 52 | ``` 53 | 54 | ![Example 2](hello_world/example2.svg) 55 | 56 | -------------------------------------------------------------------------------- /docs/src/syntax/hello_world/example1.svg: -------------------------------------------------------------------------------- 1 | 2 | 33 | 34 | 35 | 36 | 37 | Hello World! 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /docs/src/syntax/hello_world/example2.svg: -------------------------------------------------------------------------------- 1 | 2 | 33 | 34 | 35 | 36 | 37 | One 38 | 39 | 40 | 41 | Two 42 | 43 | 44 | 45 | Three 46 | 47 | 48 | 49 | Four 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /docs/src/syntax/list_of_attributes.md: -------------------------------------------------------------------------------- 1 | ## List of attributes 2 | 3 | ### Node attributes 4 | 5 | These are the attributes that can be defined on a node: 6 | 7 | - `text` — The text to render inside the node. 8 | - `class` — One or more CSS classes that will get appended to this node's SVG representation; read more [here](../styling_flowchart.md). 9 | - `shape` — Determines the node's shape. Can be one of the following: 10 | - `rect` — Rectangle (default). 11 | - `square` — Square. 12 | - `ellipse` — Ellipse. 13 | - `circle` — Circle. 14 | - `diamond` — Diamond. 15 | - `angled_square` — Square at a 45° angle. 16 | - `connect` — Defines one or more connections this node has to other nodes. Consists of two parts: 17 | - Connection sides. Has the format `x:y` meaning "connect the **x** side of the source node to the **y** side of the destination node. `x` and `y` can be one of the following: 18 | - `n` — North. 19 | - `s` — South. 20 | - `w` — West. 21 | - `e` — East. 22 | - Destination. Can be one of the following: 23 | - `#dest` — Connect to the node with the label `dest`. 24 | - `@n` — Connect to the node directly **north** of source node. (similar for other cardinal directions). 25 | - `@` — Connect source node to itself. 26 | 27 | ### Connection attributes 28 | 29 | These are the attributes that can be defined on a connection: 30 | 31 | - `text` — The text that appears next to the connection's beginning. 32 | - `class` — One or more CSS classes that will get appended to this connection's SVG representation; read more [here](../styling_flowchart.md). 33 | - `arrowheads` — Determines which arrowheads the connection will have. Can be one of the following: 34 | - `none` — No arrowheads. 35 | - `start` — Arrowhead on the source node only. 36 | - `end` — Arrowhead on the destination node only (default). 37 | - `both` — Arrowheads on both the source and destination nodes. -------------------------------------------------------------------------------- /docs/src/syntax/making_connections.md: -------------------------------------------------------------------------------- 1 | ## Making connections 2 | 3 | 4 | ### Absolute connections 5 | 6 | Here's a flowchart with two connected nodes: 7 | 8 | ```js 9 | grid { 10 | block("Source", connect: s:n#dest); 11 | block#dest("Destination"); 12 | } 13 | ``` 14 | 15 | ![Example 1](making_connections/example1.svg) 16 | 17 | We've attached `#dest` to the destination node's identifier. This is called a *label*. Two nodes cannot have the same label. The connection itself is represented with a `connect` attribute on the source node. The connection declaration above can be split into two parts: `s:n` and `#dest`. 18 | 19 | - `s:n` means that we'd like to connect the **south** side of the source node to the **north** side of the destination node. 20 | - `#dest` is the destination. Here it means that the destination node is the one with the label `dest`. 21 | 22 | 23 | ### Relative connections 24 | 25 | Keeping track of multiple labels is tedious and error-prone. We could simplify the previous example like so: 26 | 27 | ```js 28 | grid { 29 | block("Source", connect: s:n@s); 30 | block("Destination"); 31 | } 32 | ``` 33 | 34 | Instead of a label on the destination node we used `@s` as the connection destination, meaning "the node directly **south** of me". Relative connections are easier to understand and don't require labels, but can't represent all possible connections like absolute ones can. 35 | 36 | 37 | ### Self connection 38 | 39 | A node can be connected to itself by omitting the direction and using just `@`, like so: 40 | 41 | ```js 42 | grid { 43 | block("Source and destination", connect: s:n@); 44 | } 45 | ``` 46 | 47 | ![Example 2](making_connections/example2.svg) 48 | 49 | 50 | ### Connection attributes 51 | 52 | Certain attributes can be set on connections just like on nodes. Here's an example: 53 | 54 | ```js 55 | grid { 56 | block("Source", connect: e:w@e("Yes")), block("Destination"); 57 | } 58 | ``` 59 | 60 | ![Example 3](making_connections/example3.svg) 61 | 62 | ### Multiple connections 63 | 64 | A node may also define multiple connections like so: 65 | 66 | ```js 67 | grid { 68 | block("Destination 1"), block("Source", connect: { w:e@w("1"); s:n@s("2") }); 69 | _, block("Destination 2"); 70 | } 71 | ``` 72 | 73 | ![Example 4](making_connections/example4.svg) -------------------------------------------------------------------------------- /docs/src/syntax/making_connections/example1.svg: -------------------------------------------------------------------------------- 1 | 2 | 33 | 34 | 35 | 36 | 37 | Source 38 | 39 | 40 | 41 | Destination 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /docs/src/syntax/making_connections/example2.svg: -------------------------------------------------------------------------------- 1 | 2 | 33 | 34 | 35 | 36 | 37 | Source and destination 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /docs/src/syntax/making_connections/example3.svg: -------------------------------------------------------------------------------- 1 | 2 | 33 | 34 | 35 | 36 | 37 | Source 38 | 39 | 40 | 41 | Destination 42 | 43 | 44 | 45 | 46 | 47 | Yes 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /docs/src/syntax/making_connections/example4.svg: -------------------------------------------------------------------------------- 1 | 2 | 33 | 34 | 35 | 36 | 37 | Destination 1 38 | 39 | 40 | 41 | Source 42 | 43 | 44 | 45 | Destination 2 46 | 47 | 48 | 49 | 50 | 51 | 1 52 | 53 | 54 | 55 | 56 | 2 57 | 58 | 59 | 60 | 61 | --------------------------------------------------------------------------------