├── .github └── FUNDING.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── src ├── index.js ├── main.rs └── style.css └── tests ├── cli.rs └── resources ├── indices.txt └── ranges.txt /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [fasterthanlime] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | output.html 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstyle" 16 | version = "1.0.10" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 19 | 20 | [[package]] 21 | name = "argh" 22 | version = "0.1.13" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "34ff18325c8a36b82f992e533ece1ec9f9a9db446bd1c14d4f936bac88fcd240" 25 | dependencies = [ 26 | "argh_derive", 27 | "argh_shared", 28 | "rust-fuzzy-search", 29 | ] 30 | 31 | [[package]] 32 | name = "argh_derive" 33 | version = "0.1.13" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "adb7b2b83a50d329d5d8ccc620f5c7064028828538bdf5646acd60dc1f767803" 36 | dependencies = [ 37 | "argh_shared", 38 | "proc-macro2", 39 | "quote", 40 | "syn", 41 | ] 42 | 43 | [[package]] 44 | name = "argh_shared" 45 | version = "0.1.13" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "a464143cc82dedcdc3928737445362466b7674b5db4e2eb8e869846d6d84f4f6" 48 | dependencies = [ 49 | "serde", 50 | ] 51 | 52 | [[package]] 53 | name = "assert_cmd" 54 | version = "2.0.16" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" 57 | dependencies = [ 58 | "anstyle", 59 | "bstr", 60 | "doc-comment", 61 | "libc", 62 | "predicates", 63 | "predicates-core", 64 | "predicates-tree", 65 | "wait-timeout", 66 | ] 67 | 68 | [[package]] 69 | name = "autocfg" 70 | version = "1.4.0" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 73 | 74 | [[package]] 75 | name = "bitflags" 76 | version = "2.8.0" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" 79 | 80 | [[package]] 81 | name = "bstr" 82 | version = "1.11.3" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" 85 | dependencies = [ 86 | "memchr", 87 | "regex-automata", 88 | "serde", 89 | ] 90 | 91 | [[package]] 92 | name = "cfg-if" 93 | version = "1.0.0" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 96 | 97 | [[package]] 98 | name = "difflib" 99 | version = "0.4.0" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" 102 | 103 | [[package]] 104 | name = "doc-comment" 105 | version = "0.3.3" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 108 | 109 | [[package]] 110 | name = "errno" 111 | version = "0.3.10" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 114 | dependencies = [ 115 | "libc", 116 | "windows-sys", 117 | ] 118 | 119 | [[package]] 120 | name = "fastrand" 121 | version = "2.3.0" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 124 | 125 | [[package]] 126 | name = "float-cmp" 127 | version = "0.10.0" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" 130 | dependencies = [ 131 | "num-traits", 132 | ] 133 | 134 | [[package]] 135 | name = "getrandom" 136 | version = "0.2.15" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 139 | dependencies = [ 140 | "cfg-if", 141 | "libc", 142 | "wasi", 143 | ] 144 | 145 | [[package]] 146 | name = "libc" 147 | version = "0.2.169" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 150 | 151 | [[package]] 152 | name = "linux-raw-sys" 153 | version = "0.4.15" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 156 | 157 | [[package]] 158 | name = "memchr" 159 | version = "2.7.4" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 162 | 163 | [[package]] 164 | name = "normalize-line-endings" 165 | version = "0.3.0" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" 168 | 169 | [[package]] 170 | name = "num-traits" 171 | version = "0.2.19" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 174 | dependencies = [ 175 | "autocfg", 176 | ] 177 | 178 | [[package]] 179 | name = "once_cell" 180 | version = "1.20.2" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 183 | 184 | [[package]] 185 | name = "peg" 186 | version = "0.8.4" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "295283b02df346d1ef66052a757869b2876ac29a6bb0ac3f5f7cd44aebe40e8f" 189 | dependencies = [ 190 | "peg-macros", 191 | "peg-runtime", 192 | ] 193 | 194 | [[package]] 195 | name = "peg-macros" 196 | version = "0.8.4" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "bdad6a1d9cf116a059582ce415d5f5566aabcd4008646779dab7fdc2a9a9d426" 199 | dependencies = [ 200 | "peg-runtime", 201 | "proc-macro2", 202 | "quote", 203 | ] 204 | 205 | [[package]] 206 | name = "peg-runtime" 207 | version = "0.8.3" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "e3aeb8f54c078314c2065ee649a7241f46b9d8e418e1a9581ba0546657d7aa3a" 210 | 211 | [[package]] 212 | name = "pegviz" 213 | version = "0.1.0" 214 | dependencies = [ 215 | "argh", 216 | "assert_cmd", 217 | "peg", 218 | "predicates", 219 | "tempfile", 220 | ] 221 | 222 | [[package]] 223 | name = "predicates" 224 | version = "3.1.3" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" 227 | dependencies = [ 228 | "anstyle", 229 | "difflib", 230 | "float-cmp", 231 | "normalize-line-endings", 232 | "predicates-core", 233 | "regex", 234 | ] 235 | 236 | [[package]] 237 | name = "predicates-core" 238 | version = "1.0.9" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" 241 | 242 | [[package]] 243 | name = "predicates-tree" 244 | version = "1.0.12" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" 247 | dependencies = [ 248 | "predicates-core", 249 | "termtree", 250 | ] 251 | 252 | [[package]] 253 | name = "proc-macro2" 254 | version = "1.0.93" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 257 | dependencies = [ 258 | "unicode-ident", 259 | ] 260 | 261 | [[package]] 262 | name = "quote" 263 | version = "1.0.38" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 266 | dependencies = [ 267 | "proc-macro2", 268 | ] 269 | 270 | [[package]] 271 | name = "regex" 272 | version = "1.11.1" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 275 | dependencies = [ 276 | "aho-corasick", 277 | "memchr", 278 | "regex-automata", 279 | "regex-syntax", 280 | ] 281 | 282 | [[package]] 283 | name = "regex-automata" 284 | version = "0.4.9" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 287 | dependencies = [ 288 | "aho-corasick", 289 | "memchr", 290 | "regex-syntax", 291 | ] 292 | 293 | [[package]] 294 | name = "regex-syntax" 295 | version = "0.8.5" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 298 | 299 | [[package]] 300 | name = "rust-fuzzy-search" 301 | version = "0.1.1" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "a157657054ffe556d8858504af8a672a054a6e0bd9e8ee531059100c0fa11bb2" 304 | 305 | [[package]] 306 | name = "rustix" 307 | version = "0.38.43" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" 310 | dependencies = [ 311 | "bitflags", 312 | "errno", 313 | "libc", 314 | "linux-raw-sys", 315 | "windows-sys", 316 | ] 317 | 318 | [[package]] 319 | name = "serde" 320 | version = "1.0.217" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" 323 | dependencies = [ 324 | "serde_derive", 325 | ] 326 | 327 | [[package]] 328 | name = "serde_derive" 329 | version = "1.0.217" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" 332 | dependencies = [ 333 | "proc-macro2", 334 | "quote", 335 | "syn", 336 | ] 337 | 338 | [[package]] 339 | name = "syn" 340 | version = "2.0.96" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" 343 | dependencies = [ 344 | "proc-macro2", 345 | "quote", 346 | "unicode-ident", 347 | ] 348 | 349 | [[package]] 350 | name = "tempfile" 351 | version = "3.15.0" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" 354 | dependencies = [ 355 | "cfg-if", 356 | "fastrand", 357 | "getrandom", 358 | "once_cell", 359 | "rustix", 360 | "windows-sys", 361 | ] 362 | 363 | [[package]] 364 | name = "termtree" 365 | version = "0.5.1" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" 368 | 369 | [[package]] 370 | name = "unicode-ident" 371 | version = "1.0.14" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 374 | 375 | [[package]] 376 | name = "wait-timeout" 377 | version = "0.2.0" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 380 | dependencies = [ 381 | "libc", 382 | ] 383 | 384 | [[package]] 385 | name = "wasi" 386 | version = "0.11.0+wasi-snapshot-preview1" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 389 | 390 | [[package]] 391 | name = "windows-sys" 392 | version = "0.59.0" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 395 | dependencies = [ 396 | "windows-targets", 397 | ] 398 | 399 | [[package]] 400 | name = "windows-targets" 401 | version = "0.52.6" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 404 | dependencies = [ 405 | "windows_aarch64_gnullvm", 406 | "windows_aarch64_msvc", 407 | "windows_i686_gnu", 408 | "windows_i686_gnullvm", 409 | "windows_i686_msvc", 410 | "windows_x86_64_gnu", 411 | "windows_x86_64_gnullvm", 412 | "windows_x86_64_msvc", 413 | ] 414 | 415 | [[package]] 416 | name = "windows_aarch64_gnullvm" 417 | version = "0.52.6" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 420 | 421 | [[package]] 422 | name = "windows_aarch64_msvc" 423 | version = "0.52.6" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 426 | 427 | [[package]] 428 | name = "windows_i686_gnu" 429 | version = "0.52.6" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 432 | 433 | [[package]] 434 | name = "windows_i686_gnullvm" 435 | version = "0.52.6" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 438 | 439 | [[package]] 440 | name = "windows_i686_msvc" 441 | version = "0.52.6" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 444 | 445 | [[package]] 446 | name = "windows_x86_64_gnu" 447 | version = "0.52.6" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 450 | 451 | [[package]] 452 | name = "windows_x86_64_gnullvm" 453 | version = "0.52.6" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 456 | 457 | [[package]] 458 | name = "windows_x86_64_msvc" 459 | version = "0.52.6" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 462 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pegviz" 3 | version = "0.1.0" 4 | authors = ["Amos Wenger "] 5 | edition = "2018" 6 | license = "MIT OR Apache-2.0" 7 | 8 | [features] 9 | debug-backfill = [] 10 | 11 | [dependencies] 12 | peg = "0.8.4" 13 | argh = "0.1.13" 14 | 15 | [dev-dependencies] 16 | assert_cmd = { version = "2.0" } 17 | predicates = { version = "3.0" } 18 | tempfile = "3.15.0" 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT LICENSE 2 | 3 | Copyright (c) 2020 Amos Wenger, https://fasterthanli.me 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pegviz 2 | 3 | ![Quick hack status: Yes!](https://img.shields.io/badge/quick%20hack%3F-yes!-green) 4 | ![Maintenance status: Not really](https://img.shields.io/badge/maintained%3F-not%20really-red) 5 | [![license: MIT/Apache-2.0](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](LICENSE-MIT) 6 | 7 | Visualizer for https://crates.io/crates/peg parsers. 8 | 9 | ## Screenshot 10 | 11 | `pegviz` reads peg's tracing markers and generates a collapsible HTML tree. 12 | 13 | ![](https://user-images.githubusercontent.com/7998310/80628077-1fe05100-8a51-11ea-87aa-4b8362adf56c.png) 14 | 15 | Left side: 16 | 17 | * Green: matched rule 18 | * Yellow: partial match (see below) 19 | * Red: failed rule 20 | 21 | Right side: 22 | 23 | * Gray: previous input, for context 24 | * Blue background: input matched by this rule 25 | * White text: rest of input after matching 26 | 27 | 28 | ## Partial Matches 29 | 30 | A partial match is a (special kind of) match failure. 31 | It happens, if a rule consists of multiple sub-rules and some of them do match, but they do not all match. 32 | 33 | Consider for example the grammar 34 | 35 | ```rust 36 | pub rule traits() -> (Vec, Vec) 37 | = awesome_traits:(awesome() ++ ". ") "."? " "? 38 | boring_traits:(boring() ** ". ") "."? 39 | { 40 | (awesome_traits, boring_traits) 41 | } 42 | 43 | rule awesome() -> String 44 | = name() " is awesome due to " reason:$(['a'..='z' | ' ']+) { reason.to_string() } 45 | 46 | rule boring() -> String 47 | = name() " is boring because of " reason:$(['a'..='z' | ' ']+) { reason.to_string() } 48 | 49 | rule name() -> String = s:$(['A'..='Z']['a'..='z']+) { s.to_string() } 50 | ``` 51 | 52 | Here both `awesome` and `boring` start with `name()`. 53 | When parsing a string like 54 | 55 | "Paul is awesome due to his kindness. Ludwig is boring because of his cat." 56 | 57 | the first sentence will match to the `awesome` rule, the second does not, but it _partially_ matches, because `Ludwig` also matches to `name()`. 58 | It will, though, match to the `boring` rule. 59 | 60 | ![partial match](https://github.com/user-attachments/assets/99fe050d-2ba6-44a7-9a76-a3d96956d788) 61 | 62 | 63 | ## Format 64 | 65 | `pegviz` expects input in the following format: 66 | 67 | ``` 68 | [PEG_INPUT_START] 69 | int a = 12 + 45; 70 | [PEG_TRACE_START] 71 | [PEG_TRACE] Attempting to match rule `translation_unit0` at 1:1 72 | [PEG_TRACE] Attempting to match rule `list0` at 1:1 73 | [PEG_TRACE] Attempting to match rule `node` at 1:1 74 | [PEG_TRACE] Attempting to match rule `external_declaration` at 1:1 75 | [PEG_TRACE] Attempting to match rule `declaration` at 1:1 76 | [PEG_TRACE] Attempting to match rule `node` at 1:1 77 | [PEG_TRACE] Attempting to match rule `declaration0` at 1:1 78 | [PEG_TRACE] Attempting to match rule `gnu` at 1:1 79 | [PEG_TRACE] Attempting to match rule `gnu_guard` at 1:1 80 | [PEG_TRACE] Failed to match rule `gnu_guard` at 1:1 81 | [PEG_TRACE] Failed to match rule `gnu` at 1:1 82 | [PEG_TRACE] Attempting to match rule `_` at 1:1 83 | [PEG_TRACE] Matched rule `_` at 1:1 to 1:1 84 | ... ... 85 | [PEG_TRACE_STOP] 86 | ``` 87 | 88 | The `_START` and `_STOP` marker are pegviz-specific, you'll need to add 89 | them to your program. See the **Integration** section for more information. 90 | 91 | Multiple traces may be processed, they'll all show up in the output file. 92 | Output that occurs *between* traces is ignored. 93 | 94 | ## Compatibility 95 | 96 | pegviz has been used with: 97 | 98 | * peg 0.5.7 99 | * peg 0.6.2 100 | * peg 0.8.4 101 | 102 | There are no tests. It's quickly thrown together. 103 | 104 | ## Integration 105 | 106 | In your crate, re-export the `trace` feature: 107 | 108 | ``` 109 | # in Cargo.toml 110 | 111 | [features] 112 | trace = ["peg/trace"] 113 | ``` 114 | 115 | Then, in your parser, add a `tracing` rule that captures all the input 116 | and outputs the markers `pegviz` is looking for: 117 | 118 | ```rust 119 | peg::parser! { pub grammar example() for str { 120 | 121 | rule traced(e: rule) -> T = 122 | &(input:$([_]*) { 123 | #[cfg(feature = "trace")] 124 | println!("[PEG_INPUT_START]\n{}\n[PEG_TRACE_START]", input); 125 | }) 126 | e:e()? {? 127 | #[cfg(feature = "trace")] 128 | println!("[PEG_TRACE_STOP]"); 129 | e.ok_or("") 130 | } 131 | 132 | pub rule toplevel() -> Toplevel = traced() 133 | 134 | }} 135 | ``` 136 | 137 | If your parser uses slices (such as `&[u8]`, `&[T]`), then each character or token must be on a new line. 138 | 139 | ```rust 140 | peg::parser! { pub grammar example() for str { 141 | 142 | rule traced(e: rule) -> T = 143 | &(input:$([_]*) { 144 | #[cfg(feature = "trace")] 145 | println!( 146 | "[PEG_INPUT_START]\n{}\n[PEG_TRACE_START]", 147 | input.iter().fold( 148 | String::new(), 149 | |s1, s2| s1 + "\n" + s2.to_string().as_str() 150 | ).trim_start().to_string() 151 | ); 152 | }) 153 | e:e()? {? 154 | #[cfg(feature = "trace")] 155 | println!("[PEG_TRACE_STOP]"); 156 | e.ok_or("") 157 | } 158 | 159 | pub rule toplevel() -> Toplevel = traced() 160 | 161 | }} 162 | ``` 163 | 164 | The above is the recommended way *if you're maintaining the grammar* and want 165 | to be able to turn on pegviz support anytime. 166 | 167 | If you're debugging someone else's parser, you may want to print the start/stop 168 | markers and the source yourself, around the parser invocation, like so: 169 | 170 | ```rust 171 | let source = std::fs::read_to_string(&source).unwrap(); 172 | println!("[PEG_INPUT_START]\n{}\n[PEG_TRACE_START]", source); 173 | let res = lang_c::driver::parse_preprocessed(&config, source); 174 | println!("[PEG_TRACE_STOP]"); 175 | ``` 176 | 177 | Make sure you've installed `pegviz` into your `$PATH`: 178 | 179 | ```shell 180 | cd pegviz/ 181 | cargo install --force --path . 182 | ``` 183 | 184 | > While installing it, you may notice `pegviz` depends on `peg`. 185 | > That's right! It's using a PEG to analyze PEG traces. 186 | 187 | Then, simply run your program with the `trace` Cargo feature enabled, and 188 | pipe its standard output to `pegviz`. 189 | 190 | ```shell 191 | cd example/ 192 | cargo run --features trace | pegviz --output ./pegviz.html 193 | ``` 194 | 195 | Note that the `--output` argument is mandatory. 196 | 197 | The last step is to open the resulting HTML file in a browser and click around! 198 | 199 | ## License 200 | 201 | pegviz is released under the MIT License. See the LICENSE file for details. 202 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("click", (ev) => { 2 | if (ev.ctrlKey && ev.target.classList.contains("rule")) { 3 | ev.preventDefault(); 4 | let text = ev.target.innerText; 5 | navigator.clipboard.writeText(text); 6 | 7 | let notifs = document.getElementById("notifications"); 8 | let child = document.createElement("div"); 9 | child.classList.add("notification"); 10 | child.innerHTML = `Copied "${text}" to clipboard!`; 11 | notifs.appendChild(child); 12 | 13 | setTimeout(() => { 14 | child.classList.add("dead"); 15 | setTimeout(() => { 16 | child.remove(); 17 | }, 1000); 18 | }, 1000); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use argh::FromArgs; 2 | use std::{ 3 | cmp::Ordering, 4 | error::Error, 5 | fmt, 6 | fs::File, 7 | io::{BufRead, BufReader, Write}, 8 | path::PathBuf, 9 | }; 10 | 11 | #[derive(Debug)] 12 | enum State { 13 | Success, 14 | Failure, 15 | Unknown, 16 | } 17 | 18 | // Location is the position of a statement in the source 19 | // text or slice. 20 | #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] 21 | enum Location { 22 | CharLocation(CharLocation), 23 | TokenIndex(TokenIndex), 24 | } 25 | 26 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 27 | struct CharLocation { 28 | line: usize, 29 | column: usize, 30 | } 31 | 32 | impl fmt::Display for CharLocation { 33 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 34 | write!(f, "{}:{}", self.line, self.column) 35 | } 36 | } 37 | 38 | impl Ord for CharLocation { 39 | fn cmp(&self, other: &Self) -> Ordering { 40 | match self.line.cmp(&other.line) { 41 | Ordering::Equal => self.column.cmp(&other.column), 42 | x => x, 43 | } 44 | } 45 | } 46 | 47 | impl PartialOrd for CharLocation { 48 | fn partial_cmp(&self, other: &Self) -> Option { 49 | Some(self.cmp(other)) 50 | } 51 | } 52 | 53 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 54 | struct TokenIndex { 55 | index: usize, 56 | } 57 | 58 | impl fmt::Display for TokenIndex { 59 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 60 | write!(f, "{}", self.index) 61 | } 62 | } 63 | 64 | #[derive(Debug)] 65 | struct Node { 66 | rule: Rule, 67 | partial_match: bool, 68 | state: State, 69 | children: Vec, 70 | } 71 | 72 | #[derive(Debug)] 73 | struct Rule { 74 | name: String, 75 | loc: Location, 76 | next_loc: Option, 77 | } 78 | 79 | impl Rule { 80 | #[allow(dead_code)] 81 | fn is_zero_len(&self) -> bool { 82 | if let Some(next_loc) = self.next_loc { 83 | if next_loc > self.loc { 84 | return false; 85 | } 86 | } 87 | true 88 | } 89 | } 90 | 91 | #[derive(Debug)] 92 | enum Line { 93 | Attempt(Rule), 94 | Failure(Rule), 95 | Success(Rule), 96 | Cache, 97 | EnterLevel, 98 | LeaveLevel, 99 | } 100 | 101 | peg::parser! { 102 | grammar tracer() for str { 103 | pub(crate) rule line() -> Line 104 | = "[PEG_TRACE] " l:line0() { l } 105 | 106 | rule line0() -> Line 107 | = r:attempt() { Line::Attempt(r) } 108 | / r:fail() { Line::Failure(r) } 109 | / r:succ() { Line::Success(r) } 110 | / cach() { Line::Cache } 111 | / enter() { Line::EnterLevel } 112 | / leave() { Line::LeaveLevel } 113 | 114 | rule attempt() -> Rule 115 | = "Attempting to match rule " r:rule0() { r } 116 | 117 | rule fail() -> Rule 118 | = "Failed to match rule " r:rule0() { r } 119 | 120 | rule succ() -> Rule 121 | = "Matched rule " r:rule0() { r } 122 | 123 | rule cach() 124 | = "Cached " ("match" / "fail") " of rule " [_]* 125 | 126 | rule enter() 127 | = "Entering level " [_]* 128 | 129 | rule leave() 130 | = "Leaving level " [_]* 131 | 132 | rule rule0() -> Rule 133 | = rule1(, ) 134 | / rule1()>, ) 135 | 136 | rule rule1(name: rule<&'input str>, at: rule<(Location, Option)>) -> Rule 137 | = name:name() at:at() { 138 | Rule { 139 | name: name.into(), 140 | loc: at.0, 141 | next_loc: at.1, 142 | } 143 | } 144 | 145 | rule at5() -> (Location, Option) 146 | = " at " at:location() " (pos " int() ")" { (at, None) } 147 | 148 | rule at6() -> (Location, Option) 149 | = " at " at:location() to:(" to " to:location() { to })? { (at, to) } 150 | 151 | rule backquoted(e: rule) -> T 152 | = "`" e:e() "`" { e } 153 | 154 | rule identifier() -> &'input str 155 | = $(['A'..='Z' | 'a'..='z' | '0'..='9' | '_']*) 156 | 157 | rule location() -> Location 158 | = range_location() / index_location() 159 | 160 | rule range_location() -> Location 161 | = line:int() ":" column:int() { Location::CharLocation(CharLocation { line, column } ) } 162 | 163 | rule index_location() -> Location 164 | = index:int() { Location::TokenIndex(TokenIndex { index } ) } 165 | 166 | rule int() -> usize 167 | = digits:$(['0'..='9']+) { digits.parse().unwrap() } 168 | } 169 | } 170 | 171 | #[derive(FromArgs)] 172 | /// Creates an HTML visualization for a trace generated from https://crates.io/crates/peg 173 | struct Args { 174 | #[argh(positional)] 175 | input: Option, 176 | 177 | #[argh(option, short = 'o')] 178 | /// output path, "./trace.html" for example 179 | output: PathBuf, 180 | 181 | #[argh(option, short = 'f')] 182 | /// name of rules to flatten - if they have only a single child, 183 | /// then only the child will appear in the tree 184 | flatten: Vec, 185 | 186 | #[argh(option, short = 'h')] 187 | /// name of rules to hide altogether 188 | hide: Vec, 189 | } 190 | 191 | impl Args { 192 | fn should_flatten(&self, node: &Node) -> bool { 193 | self.flatten.iter().any(|x| x == &node.rule.name) && node.children.len() == 1 194 | } 195 | 196 | fn should_hide(&self, node: &Node) -> bool { 197 | self.hide.iter().any(|x| x == &node.rule.name) 198 | } 199 | } 200 | 201 | fn main() -> Result<(), Box> { 202 | let args: Args = argh::from_env(); 203 | 204 | enum ParseState { 205 | WaitingForInputStart, 206 | ReadingInput, 207 | ReadingTrace, 208 | } 209 | let mut state = ParseState::WaitingForInputStart; 210 | let mut traces: Vec<(Node, String)> = Default::default(); 211 | let mut stack: Vec = vec![]; 212 | let mut input = String::new(); 213 | let mut trace_number = 1; 214 | 215 | let stdin = std::io::stdin(); 216 | let stream = match &args.input { 217 | Some(input) => Box::new(BufReader::new(File::open(input)?)) as Box, 218 | None => Box::new(stdin.lock()) as Box, 219 | }; 220 | 221 | for line in stream.lines() { 222 | let line = line?; 223 | 224 | match state { 225 | ParseState::WaitingForInputStart => { 226 | if line == "[PEG_INPUT_START]" { 227 | println!("= pegviz input start"); 228 | state = ParseState::ReadingInput; 229 | continue; 230 | } 231 | } 232 | ParseState::ReadingInput => { 233 | if line == "[PEG_TRACE_START]" { 234 | println!("= pegviz trace start"); 235 | state = ParseState::ReadingTrace; 236 | stack.push(Node { 237 | rule: Rule { 238 | name: format!("Trace #{}", trace_number), 239 | loc: Location::CharLocation(CharLocation { column: 0, line: 0 }), 240 | next_loc: None, 241 | }, 242 | partial_match: false, 243 | state: State::Success, 244 | children: vec![], 245 | }); 246 | trace_number += 1; 247 | continue; 248 | } 249 | 250 | use std::fmt::Write; 251 | writeln!(&mut input, "{}", line)?; 252 | } 253 | ParseState::ReadingTrace => { 254 | if line == "[PEG_TRACE_STOP]" { 255 | println!("= pegviz trace stop"); 256 | assert_eq!(stack.len(), 1); 257 | let root = stack.pop().unwrap(); 258 | traces.push((root, input.clone())); 259 | input.clear(); 260 | state = ParseState::WaitingForInputStart; 261 | continue; 262 | } 263 | 264 | let t = match tracer::line(&line) { 265 | Ok(t) => t, 266 | Err(e) => { 267 | println!("= pegviz error:\nfor line\n| {}\n{:#?}", line, e); 268 | return Ok(()); 269 | } 270 | }; 271 | 272 | match t { 273 | Line::Attempt(rule) => { 274 | let node = Node { 275 | rule, 276 | state: State::Unknown, 277 | children: vec![], 278 | partial_match: false, 279 | }; 280 | stack.push(node); 281 | } 282 | Line::Success(rule) => { 283 | let mut node = stack.pop().unwrap(); 284 | if rule.name != node.rule.name { 285 | panic!( 286 | "pegviz: expected rule {:?} to finish, but got {:?}", 287 | rule.name, node.rule.name 288 | ); 289 | } 290 | node.state = State::Success; 291 | node.rule.next_loc = rule.next_loc; 292 | stack.last_mut().unwrap().children.push(node); 293 | } 294 | Line::Failure(rule) => { 295 | let mut node = stack.pop().unwrap(); 296 | if rule.name != node.rule.name { 297 | panic!( 298 | "pegviz: expected rule {:?} to finish, but got {:?}", 299 | rule.name, node.rule.name 300 | ); 301 | } 302 | node.state = State::Failure; 303 | stack.last_mut().unwrap().children.push(node); 304 | } 305 | Line::Cache => {} 306 | Line::EnterLevel => {} 307 | Line::LeaveLevel => {} 308 | } 309 | } 310 | } 311 | } 312 | 313 | println!("======================================="); 314 | println!("= pegviz input stop"); 315 | println!("======================================="); 316 | 317 | if traces.is_empty() { 318 | println!("pegviz: no trace, exiting"); 319 | return Ok(()); 320 | } 321 | 322 | let mut out = File::create(&args.output)?; 323 | 324 | writeln!( 325 | &mut out, 326 | r#" 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 |
336 | "#, 337 | style = include_str!("style.css"), 338 | script = include_str!("index.js") 339 | )?; 340 | 341 | for trace in &mut traces { 342 | backfill_next_loc(&mut trace.0, None); 343 | mark_partial_matches(&mut trace.0); 344 | } 345 | 346 | for trace in &traces { 347 | let (root, input) = &trace; 348 | visit(&mut out, &args, root, input)?; 349 | } 350 | writeln!( 351 | &mut out, 352 | r#" 353 | 354 | 355 | "# 356 | )?; 357 | 358 | println!("= pegviz generated to {}", args.output.display()); 359 | 360 | Ok(()) 361 | } 362 | 363 | #[allow(unused)] 364 | fn print_backfilled(node: &Node, state: &str) { 365 | #[cfg(feature = "debug-backfill")] 366 | { 367 | if node.rule.is_zero_len() { 368 | return; 369 | } 370 | 371 | if let Some(next_loc) = node.rule.next_loc { 372 | println!( 373 | "{name:?} {state}: {from}-{to}", 374 | name = node.rule.name, 375 | state = state, 376 | from = node.rule.loc, 377 | to = next_loc, 378 | ); 379 | } 380 | } 381 | } 382 | 383 | fn mark_partial_matches(node: &mut Node) -> bool { 384 | for c in &mut node.children { 385 | mark_partial_matches(c); 386 | } 387 | 388 | let ret = (matches!(node.state, State::Success) && !node.rule.is_zero_len()) 389 | || node.children.iter().any(|c| c.partial_match); 390 | node.partial_match = ret; 391 | ret 392 | } 393 | 394 | fn backfill_next_loc(node: &mut Node, next: Option<&Node>) { 395 | for i in 1..node.children.len() { 396 | if let ([prev], [next]) = &mut node.children[i - 1..i + 1].split_at_mut(1) { 397 | if prev.rule.next_loc.is_none() { 398 | prev.rule.next_loc = Some(next.rule.loc); 399 | print_backfilled(prev, "backfilled"); 400 | } else { 401 | print_backfilled(prev, "parsed"); 402 | } 403 | backfill_next_loc(prev, Some(next)); 404 | } 405 | } 406 | 407 | if let Some(last) = node.children.last_mut() { 408 | if let Some(next) = next { 409 | if last.rule.next_loc.is_none() { 410 | last.rule.next_loc = Some(next.rule.loc); 411 | print_backfilled(last, "backfilled"); 412 | } else { 413 | print_backfilled(last, "parsed"); 414 | } 415 | } 416 | backfill_next_loc(last, next) 417 | } 418 | } 419 | 420 | impl Location { 421 | fn pos(&self, input: &str) -> usize { 422 | match self { 423 | Location::CharLocation(char_loc) => char_loc.pos(input), 424 | Location::TokenIndex(tok_idx) => tok_idx.pos(input), 425 | } 426 | } 427 | } 428 | 429 | impl CharLocation { 430 | // returns the index of the character at the specified line and column 431 | fn pos(&self, input: &str) -> usize { 432 | let mut line = 1; 433 | let mut column = 1; 434 | 435 | for (i, c) in input.chars().enumerate() { 436 | if line == self.line && column == self.column { 437 | return i; 438 | } 439 | 440 | match c { 441 | '\n' => { 442 | line += 1; 443 | column = 1; 444 | } 445 | _ => { 446 | column += 1; 447 | } 448 | } 449 | } 450 | 0 451 | } 452 | } 453 | 454 | impl TokenIndex { 455 | // returns the index of the character at the specified line and column 456 | fn pos(&self, input: &str) -> usize { 457 | let mut line = 0; 458 | 459 | for (i, c) in input.chars().enumerate() { 460 | if line == self.index { 461 | return i; 462 | } 463 | if c == '\n' { 464 | line += 1; 465 | } 466 | } 467 | 0 468 | } 469 | } 470 | 471 | fn visit(f: &mut dyn Write, args: &Args, node: &Node, input: &str) -> Result<(), Box> { 472 | if args.should_flatten(node) { 473 | return visit(f, args, &node.children[0], input); 474 | } 475 | 476 | let rule = &node.rule; 477 | 478 | write!( 479 | f, 480 | r#" 481 |
482 | 483 | {name} 484 | "#, 485 | class = match node.state { 486 | State::Success => "success", 487 | State::Failure => "failure", 488 | State::Unknown => "unknown", 489 | }, 490 | class2 = if node.partial_match { 491 | "partial-match" 492 | } else { 493 | "" 494 | }, 495 | name = rule.name 496 | )?; 497 | 498 | // Print up to 10 characters before and 25 characters after 499 | let before = 10; 500 | let after = 25; 501 | write!( 502 | f, 503 | r#"{}"#, 504 | &input[if rule.loc.pos(input) < before { 505 | 0 506 | } else { 507 | rule.loc.pos(input) - before 508 | }..rule.loc.pos(input)] 509 | )?; 510 | let rulepos = rule.loc.pos(input); 511 | if let Some(next_loc) = rule.next_loc.as_ref() { 512 | let nextpos = next_loc.pos(input); 513 | match nextpos.cmp(&rulepos) { 514 | Ordering::Greater => { 515 | write!(f, r#"{}"#, &input[rulepos..nextpos])?; 516 | } 517 | Ordering::Less => { 518 | write!(f, r#"↩"#)?; 519 | } 520 | Ordering::Equal => {} 521 | } 522 | write!( 523 | f, 524 | r#"{}{}"#, 525 | &input[nextpos..std::cmp::min(nextpos + after, input.len())], 526 | if input.len() > nextpos + after { 527 | "…" 528 | } else { 529 | "" 530 | } 531 | )?; 532 | } else { 533 | write!( 534 | f, 535 | r#"{}{}"#, 536 | &input[rulepos..std::cmp::min(rulepos + after, input.len())], 537 | if input.len() > rulepos + after { 538 | "…" 539 | } else { 540 | "" 541 | } 542 | )?; 543 | } 544 | 545 | writeln!(f, "")?; 546 | for child in &node.children { 547 | if args.should_hide(child) { 548 | continue; 549 | } 550 | visit(f, args, child, input)?; 551 | } 552 | writeln!(f, "
")?; 553 | 554 | Ok(()) 555 | } 556 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Open+Sans&family=Source+Code+Pro&display=swap'); 2 | 3 | * { 4 | box-sizing: border-box; 5 | } 6 | 7 | #notifications { 8 | position: fixed; 9 | top: 0; 10 | right: 0; 11 | } 12 | 13 | .notification { 14 | transition: position 0.4s; 15 | 16 | padding: 8px; 17 | margin: 8px; 18 | background: #333; 19 | border: #666; 20 | border-radius: 2px; 21 | animation: fadein .4s; 22 | } 23 | 24 | @keyframes fadein { 25 | 0% { 26 | transform: translateY(-20%); 27 | opacity: 0; 28 | } 29 | 100% { 30 | transform: translateY(0%); 31 | opacity: 1; 32 | } 33 | } 34 | 35 | body { 36 | font-family: 'Open Sans', sans-serif; 37 | line-height: 1.6; 38 | } 39 | 40 | code { 41 | color: #fefefe; 42 | font-family: 'Source Code Pro', monospace; 43 | } 44 | 45 | code em, code strong, code span { 46 | background: #333; 47 | border-radius: 2px; 48 | padding-top: 4px; 49 | padding-bottom: 4px; 50 | } 51 | 52 | code em { 53 | font-style: initial; 54 | color: #676767; 55 | } 56 | 57 | code strong { 58 | font-weight: normal; 59 | background: #3a5d9c; 60 | color: #fefefe; 61 | } 62 | 63 | body { 64 | background: #111; 65 | color: white; 66 | } 67 | 68 | details { 69 | cursor: pointer; 70 | user-select: none; 71 | padding-left: 30px; 72 | margin-top: 2px; 73 | } 74 | 75 | summary { 76 | padding-top: 2px; 77 | padding-bottom: 2px; 78 | } 79 | 80 | *:focus { 81 | outline: none; 82 | } 83 | 84 | span.rule { 85 | margin: 2px; 86 | padding: 2px; 87 | font-family: 'Source Code Pro', monospace; 88 | background: #333; 89 | border: 2px solid #333; 90 | border-radius: 2px; 91 | margin-right: 1em; 92 | } 93 | 94 | span.success { 95 | border-color: #27966d; 96 | } 97 | span.failure { 98 | border-color: #942c2c; 99 | text-decoration: line-through; 100 | } 101 | span.failure.partial-match { 102 | border-color: #ba8925; 103 | text-decoration: initial; 104 | } -------------------------------------------------------------------------------- /tests/cli.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::prelude::*; 2 | use predicates::prelude::*; 3 | use std::{path::PathBuf, process::Command}; 4 | 5 | pub fn path_to_test_resource(name: &'static str) -> PathBuf { 6 | let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 7 | path.push("tests"); 8 | path.push("resources"); 9 | path.push(name); 10 | path 11 | } 12 | 13 | use tempfile::NamedTempFile; 14 | 15 | fn run_pegviz(input_file: &'static str) -> Result<(), Box> { 16 | let temp_file = NamedTempFile::new()?; 17 | let temp_path = temp_file.path().to_str().unwrap(); 18 | 19 | let mut cmd = Command::cargo_bin("pegviz")?; 20 | 21 | cmd.arg(path_to_test_resource(input_file)) 22 | .arg("--output") 23 | .arg(temp_path); 24 | 25 | cmd.assert() 26 | .success() 27 | .stdout(predicate::str::contains("pegviz generated to")); 28 | 29 | Ok(()) 30 | } 31 | 32 | #[test] 33 | fn main_when_valid_character_ranges_then_ok() -> Result<(), Box> { 34 | run_pegviz("ranges.txt") 35 | } 36 | 37 | #[test] 38 | fn main_when_valid_token_indices_then_ok() -> Result<(), Box> { 39 | run_pegviz("indices.txt") 40 | } 41 | -------------------------------------------------------------------------------- /tests/resources/indices.txt: -------------------------------------------------------------------------------- 1 | [PEG_INPUT_START] 2 | Token 0 3 | Token 1 4 | Token 2 5 | Token 3 6 | Token 4 7 | Token 5 8 | [PEG_TRACE_START] 9 | [PEG_TRACE] Attempting to match rule `list` at 0 10 | [PEG_TRACE] Matched rule `list` at 0 to 5 11 | [PEG_TRACE_STOP] -------------------------------------------------------------------------------- /tests/resources/ranges.txt: -------------------------------------------------------------------------------- 1 | [PEG_INPUT_START] 2 | 3729 3 | [PEG_TRACE_START] 4 | [PEG_TRACE] Attempting to match rule `nums` at 1:1 5 | [PEG_TRACE] Matched rule `nums` at 1:1 to 1:5 6 | [PEG_TRACE_STOP] 7 | --------------------------------------------------------------------------------