From a0efd197b378d6712e3bbc3e35f6acfa0b4f5528 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Wed, 6 Mar 2024 23:15:47 +0200 Subject: [PATCH 01/98] Long-due initial commit. Current state is "broken" --- Cargo.lock | 755 ++++++++++ Cargo.toml | 21 + README.md | 3 + src/data.rs | 164 +++ src/gen/ayano.rs | 2565 ++++++++++++++++++++++++++++++++++ src/gen/ayano/python.rs | 134 ++ src/gen/ayano/test_code.py | 5 + src/gen/ayano/test_table.csv | 9 + src/gen/lab/context.rs | 6 + src/gen/lab/lab_preamble.tex | 112 ++ src/gen/lab/meta.rs | 127 ++ src/gen/lab/mod.rs | 631 +++++++++ src/gen/mod.rs | 51 + src/lexer.rs | 413 ++++++ src/lib.rs | 39 + src/path_engine.rs | 193 +++ src/util.rs | 369 +++++ 17 files changed, 5597 insertions(+) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/data.rs create mode 100644 src/gen/ayano.rs create mode 100644 src/gen/ayano/python.rs create mode 100644 src/gen/ayano/test_code.py create mode 100644 src/gen/ayano/test_table.csv create mode 100644 src/gen/lab/context.rs create mode 100644 src/gen/lab/lab_preamble.tex create mode 100644 src/gen/lab/meta.rs create mode 100644 src/gen/lab/mod.rs create mode 100644 src/gen/mod.rs create mode 100644 src/lexer.rs create mode 100644 src/lib.rs create mode 100644 src/path_engine.rs create mode 100644 src/util.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..cc3b565 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,755 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ahash" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42cd52102d3df161c77a887b608d7a4897d7cc112886a9537b738a887a03aaff" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cached" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "355face540df58778b96814c48abb3c2ed67c4878a8087ab1819c1fedeec505f" +dependencies = [ + "ahash", + "cached_proc_macro", + "cached_proc_macro_types", + "hashbrown", + "instant", + "once_cell", + "thiserror", +] + +[[package]] +name = "cached_proc_macro" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d52f526f7cbc875b296856ca8c964a9f6290556922c303a8a3883e3c676e6a1" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "cached_proc_macro_types" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indoc" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi", + "winapi-build", +] + +[[package]] +name = "kyomato" +version = "0.1.0" +dependencies = [ + "cached", + "derive_more", + "itertools", + "nom", + "once_cell", + "pyo3", + "rand", + "regex", + "text-diff", + "thiserror", + "url", +] + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a89dc7a5850d0e983be1ec2a463a171d20990487c3cfcd68b5363f1ee3d6fe0" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "parking_lot", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07426f0d8fe5a601f26293f300afd1a7b1ed5e78b2a705870c5f30893c5163be" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb7dec17e17766b46bca4f1a4215a85006b4c2ecde122076c562dd058da6cf1" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f738b4e40d50b5711957f142878cfa0f28e054aa0ebdfc3fd137a843f74ed3" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc910d4851847827daf9d6cdd4a823fbdaab5b8818325c5e97a86da79e8881f" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" + +[[package]] +name = "smallvec" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.12.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae" + +[[package]] +name = "term" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2077e54d38055cf1ca0fd7933a2e00cd3ec8f6fed352b2a377f06dcdaaf3281" +dependencies = [ + "kernel32-sys", + "winapi", +] + +[[package]] +name = "text-diff" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "309238dd66f8bf11a20d015b727b926f294a13fcb8d56770bb984e7a22c43897" +dependencies = [ + "getopts", + "term", +] + +[[package]] +name = "thiserror" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "unindent" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c40f5c0 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "kyomato" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +cached = "0.48.1" +derive_more = "0.99.17" +itertools = "0.12.1" +nom = "7.1.3" +once_cell = "1.19.0" +pyo3 = { version = "0.20.2", features = ["auto-initialize"] } +rand = "0.8.5" +regex = "1.10.3" +thiserror = "1.0.56" +url = "2.5.0" + +[dev-dependencies] +text-diff = "0.4.0" diff --git a/README.md b/README.md new file mode 100644 index 0000000..ee8ddcd --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +*Kyoko's being a tomato. +Why? I don't know :idk:* + diff --git a/src/data.rs b/src/data.rs new file mode 100644 index 0000000..be7745d --- /dev/null +++ b/src/data.rs @@ -0,0 +1,164 @@ +use std::{ + borrow::Cow, + ops::Deref, + path::{Path, PathBuf}, +}; + +use url::Url; + +// For whatever reason, std library Cow requires ToOwned for it's contained value to be cloned +// That makes NO SENSE in my case. +// I do not need a functionality to obtain owned value here, +// but rather be able to refer to the same data from multiple instances. + +// Also, vectors of parsed tokens are obviously not intended to live longer than the source. +// In fact, for any possible appliance they can be assumed to life for as long as source does. +// Hope is for lifetime variance to help with lifetime casting, or th +#[derive(Debug, PartialEq)] +pub enum Tokens<'source> { + Owned(Box<[Token<'source>]>), + Borrowed(&'source [Token<'source>]), +} + +impl<'source> Deref for Tokens<'source> { + type Target = [Token<'source>]; + + fn deref(&self) -> &Self::Target { + match &self { + Tokens::Owned(b) => &b[..], + Tokens::Borrowed(s) => s, + } + } +} + +impl<'r, 'source: 'r> IntoIterator for &'r Tokens<'source> { + type Item = &'r Token<'source>; + + type IntoIter = std::slice::Iter<'r, Token<'source>>; + + fn into_iter(self) -> Self::IntoIter { + self.get_slice().into_iter() + } +} + +impl<'source> Tokens<'source> { + pub fn new(value: impl Into]>>) -> Self { + Self::Owned(value.into()) + } + + pub fn get_slice<'r>(&'r self) -> &'r [Token<'source>] + where + 'source: 'r, + { + match &self { + Tokens::Owned(b) => &b[..], + Tokens::Borrowed(s) => s, + } + } + + pub fn borrowed(&self) -> Tokens<'_> { + Tokens::Borrowed(self.get_slice()) + } +} + +#[derive(Debug, Clone, Hash, PartialEq)] +pub struct AyanoBlock<'code> { + pub is_display: bool, + pub is_static: bool, + pub code: Tx<'code>, + pub insert_path: Option>, +} + +// an abandoned idea. this would result in weird behavior, if underlying data happened to be moved. +// WARN! note this semantics: AyanoBlocks are deemed the same, if they are literally referencing the same data, +/* +impl PartialEq for AyanoBlock<'_> { + fn eq(&self, other: &Self) -> bool { + self.is_display == other.is_display + && self.is_static == other.is_static + && self.code.as_ptr() == other.code.as_ptr() + && self.code.len() == other.code.len() + && self.insert_path == other.insert_path + } +} +*/ + +#[derive(Debug, Clone, PartialEq)] +pub enum ListType { + Bullet, + Num, + Latin, + Cyrillic, + Roman, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Formatting { + Bold, + Italic, + StrikeThrough, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Font { + Normal, + Caption, +} + +type Tx<'source> = Cow<'source, str>; +type Pth<'source> = Cow<'source, Path>; + +#[derive(Debug, PartialEq)] +pub enum Token<'source> { + PageDiv, + Header { + // 0..6 + order: usize, + content: Tx<'source>, + }, + Equation { + content: Tx<'source>, + ident: Option>, + }, + Table { + header: Tokens<'source>, + cells: Tokens<'source>, + caption: Option>, + ident: Option>, + }, + Figure { + src_name: Pth<'source>, + caption: Option>, + ident: Option>, + }, + Href { + url: Url, + display: Tx<'source>, + }, + Ayano(AyanoBlock<'source>), + List { + list_type: ListType, + content: Tokens<'source>, + }, + Formatted(Formatting, Tokens<'source>), + Text(Tokens<'source>), + Paragraph(Font, Tx<'source>), + InlineMathmode(Tx<'source>), + Reference(Tx<'source>), + FootNoteReference(Tx<'source>), + FootNoteContent { + content: Tokens<'source>, + ident: Tx<'source>, + }, + CodeBlock { + code: Tx<'source>, + language: Option>, + }, + Error(String), +} + +impl<'source> Token<'source> { + pub fn text(text: impl Into>) -> Self { + Self::Paragraph(Font::Normal, text.into()) + } +} diff --git a/src/gen/ayano.rs b/src/gen/ayano.rs new file mode 100644 index 0000000..24272e0 --- /dev/null +++ b/src/gen/ayano.rs @@ -0,0 +1,2565 @@ +use cached::proc_macro::cached; +use itertools::{repeat_n, Itertools}; +use once_cell::sync::Lazy; +use pyo3::{ + prelude::PyErr, + types::{PyList, PyTuple}, + PyAny, Python, +}; +use regex::Regex; +use std::{ + borrow::Cow, + collections::{hash_map::DefaultHasher, HashMap}, + fmt::{Arguments, Debug, Write}, + fs::File, + hash::{Hash, Hasher}, + io::{BufRead, BufReader}, + num::{ParseFloatError, ParseIntError}, + path::PathBuf, + str::FromStr, +}; + +use crate::{ + data::{AyanoBlock, Font, Token, Tokens}, + util::{ + Equivalent, GenericRange, GenericRangeParseError, Immutable, InlinedStack, + InlinedStackAccessor, + }, +}; + +use self::python::{init, InitToken}; + +mod python; + +#[derive(Debug)] +pub struct DisplayInfo { + pub code: String, + pub name: String, +} + +#[derive(Debug, PartialEq)] +struct CodelessBlockInfo { + ident: u64, + function_name: Option, + display_name: Option, + insert_path: Option, +} + +#[derive(Debug)] +struct BlockInfo { + ident: u64, + function_name: Option, // none means it's a static block + display_info: Option, // none means it's not meant to be displayed + insert_path: Option, // none means there's no script to insert additionally +} + +#[derive(Debug)] +pub struct AyanoBuilder { + code: String, + blocks: HashMap, +} + +impl Default for AyanoBuilder { + fn default() -> Self { + Self::new() + } +} + +impl AyanoBuilder { + pub fn new() -> Self { + Self { + code: String::new(), + blocks: HashMap::new(), + } + } + + pub fn add_block(&mut self, block: &AyanoBlock<'_>) -> Result<(), SyntaxError> { + let (transformed_code, info) = apply_ayano(block)?; + let ident = info.ident; + + // Now, append the code to python handler + if let Some(function_name) = info.function_name.as_ref() { + python::append_function(&mut self.code, &function_name, &transformed_code); + } else { + python::append_static(&mut self.code, &transformed_code); + } + self.blocks.insert( + ident, + BlockInfo { + ident, + function_name: info.function_name, + display_info: info.display_name.map(|name| DisplayInfo { + code: transformed_code, + name, + }), + insert_path: info.insert_path, + }, + ); + Ok(()) + } + + pub fn initialize(self) -> Result { + let Self { code, blocks } = self; + let token = init(&code.as_str())?; + Ok(AyanoExecutor { + token, + blocks: blocks.into(), + }) + } +} + +#[derive(Debug)] +pub struct AyanoExecutor { + token: InitToken, + blocks: Immutable>, +} + +#[derive(Debug, derive_more::From, PartialEq)] +pub enum AyanoError<'b> { + Python(Equivalent), + NoFunction(AyanoBlock<'b>), + Applying(SyntaxError), + Parsing(ParsingError), +} + +impl From for AyanoError<'_> { + fn from(value: PyErr) -> Self { + Equivalent(value).into() + } +} + +type AyanoResult<'b, T = ()> = Result>; + +impl AyanoExecutor { + // this could easily be part of `display_token`, but I separated it for debugging purposes + fn call_block<'token, 'source: 'token, Out, Transform>( + &self, + token: &'token AyanoBlock<'source>, + transform: Transform, + ) -> AyanoResult<'source, Out> + where + for<'py> Transform: FnOnce(&'py PyAny, Python<'py>) -> AyanoResult<'source, Out>, + { + let hash = calculate_hash(token); + let Some(info) = self.blocks.get(&hash) else { + // this block was not passed here before + return Err(AyanoError::NoFunction(token.clone())); + }; + let Some(function_name) = info.function_name.as_ref() else { + // this block was passed here before, but it's not a function + return Err(AyanoError::NoFunction(token.clone())); + }; + + python::call_function(&self.token, function_name, transform) + } + + pub fn display_token<'token, 'source: 'token>( + &self, + token: &'token AyanoBlock<'source>, + ) -> AyanoResult<'source, Token<'static>> { + self.call_block(token, |res, _| Ok(parse_ayano(res)?)) + } + + pub fn display_blocks<'s>(&'s self) -> impl IntoIterator { + self.blocks + .values() + .filter_map(|info| info.display_info.as_ref()) + } +} + +#[cfg(test)] +mod ayano_tests { + use super::*; + macro_rules! function { + {$name:ident, $code:literal, $output:literal} => { + #[test] + fn $name() { + // arrange + let block = AyanoBlock { + is_display: false, + is_static: false, + code: $code.into(), + insert_path: None, + }; + let mut builder = AyanoBuilder::new(); + + // act + builder.add_block(&block).expect("Should be able to add block"); + // dbg!(&handler); + let executor = builder.initialize().expect("Should be able to initialize"); + let output = executor.call_block(&block, |res, _| Ok::<_, super::AyanoError<'_>>(res.str()?.to_string())).expect("Should be able to call this function"); + + // assert + assert_eq!(output, $output); + } + }; + } + + function! {number1, "1", "1"} + function! {number2, r" +x = 5 +y = 2 + x +x, y", + "(5, 7)"} + function! {value_err1, r" +x = 5 +y = x / 100 +@dev: x, y +","('err', 5, 0.05)"} + function! {csv_table1, "@csv_table: src=\"src/gen/ayano/test_table.csv\", columns=[(\"a\", \"a_err\")]", + "('tab', ['a'], [[('err', '1.0', '0.001')], [('err', '1.0', '0.001')], [('err', '1.0', '0.001')], [('err', '1.0', '0.001')], [('err', '1.0', '0.001')], [('err', '1.0', '0.001')], [('err', '1.0', '0.001')], [('err', '1.0', '0.001')]], None, None)"} + + function! {csv_table2, "@csv_table: src=\"src/gen/ayano/test_table.csv\", rows = 3..5, columns=[(\"a\", \"a_err\"), \"b\", (\"d\", \"d_err\")]", + "('tab', ['a', 'b', 'd'], [[('err', '1.0', '0.001'), '3', ('err', '-5', '0.001')]], None, None)"} + + function! {gen_table1, "@gen_table: lambda r,c: r + c; rows=4, columns=2", + "('tab', [[0, 1], [1, 2], [2, 3], [3, 4]], None, None)"} + + macro_rules! static_function { + {$name:ident, $static_code:literal, $function_code:literal, $expected_output:literal} => { + #[test] + fn $name() { + // arrange + let mut builder = AyanoBuilder::new(); + let static_block = AyanoBlock { + code: $static_code.into(), + insert_path: None, + is_display: false, + is_static: true, + }; + let function_block = AyanoBlock { + code: $function_code.into(), + insert_path: None, + is_display: false, + is_static: false, + }; + builder.add_block(&static_block).expect("Should be able to add static block"); + builder.add_block(&function_block).expect("Should be able to add function block"); + // dbg!(&builder); + let executor = builder.initialize().expect("Should be able to initialize"); + + // act + let static_output = executor.call_block(&static_block, |res, _| Ok(res.str()?.to_string())); + let function_output = executor.call_block(&function_block, |res, _| Ok(res.str()?.to_string())); + + // assert + assert_eq!(static_output, Err(AyanoError::NoFunction(static_block.clone())), "Static block shouldn't be callable!"); + assert_eq!(function_output, Ok($expected_output.to_string()), "Function should give expected result"); + } + }; + } + + static_function! {function_return, "x=1", "x", "1"} + static_function! {function_mutate, "x=1", +r"y = x + 1 +y", "2"} + static_function! {function_static_err, "x, x_err = 1.0, 0.025", "@dev: x, x_err", "('err', 1.0, 0.025)"} + static_function! {function_static_gen_tab1, "table = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]", "@gen_table: lambda r,c: table[r][c]; rows = 2, columns = 3", + "('tab', [[1, 2, 3], [5, 6, 7]], None, None)"} + + macro_rules! static_function_function { + {$name:ident, $static_code:literal, $function1_code:literal, $function2_code:literal, $expected_output1:literal, $expected_output2:literal} => { + #[test] + fn $name() { + // arrange + let mut builder = AyanoBuilder::new(); + let static_block = AyanoBlock { + code: $static_code.into(), + insert_path: None, + is_display: false, + is_static: true, + }; + let function1_block = AyanoBlock { + code: $function1_code.into(), + insert_path: None, + is_display: false, + is_static: false, + }; + let function2_block = AyanoBlock { + code: $function2_code.into(), + insert_path: None, + is_display: false, + is_static: false, + }; + builder.add_block(&static_block).expect("Should be able to add static block"); + builder.add_block(&function1_block).expect("Should be able to add function block"); + builder.add_block(&function2_block).expect("Should be able to add function block"); + // dbg!(&builder); + let executor = builder.initialize().expect("Should be able to initialize"); + + // act + let static_output = executor.call_block(&static_block, |res, _| Ok(res.str()?.to_string())); + let function1_output = executor.call_block(&function1_block, |res, _| Ok(res.str()?.to_string())); + let function2_output = executor.call_block(&function1_block, |res, _| Ok(res.str()?.to_string())); + + // assert + assert_eq!(static_output, Err(AyanoError::NoFunction(static_block.clone())), "Static block shouldn't be callable!"); + assert_eq!(function1_output, Ok($expected_output1.to_string()), "Function should give expected result"); + assert_eq!(function2_output, Ok($expected_output2.to_string()), "Function should give expected result"); + } + }; + } + + static_function_function! {function_mutate_changes, "x=0", "global x\nx += 1\nx", "global x\nx += 1\nx", "1", "2"} + + // TODO add function for display block tests +} + +fn function_name(ident: u64) -> String { + format!("f_{ident}") +} + +fn block_name(ident: u64) -> String { + format!("ayano_block__{ident}") +} + +fn calculate_hash(t: &T) -> u64 { + let mut s = DefaultHasher::new(); + t.hash(&mut s); + s.finish() +} + +#[derive( + Debug, + derive_more::From, + derive_more::Into, + derive_more::Deref, + derive_more::DerefMut, + PartialEq, +)] +pub struct UnknownArgument(String); + +#[derive( + Debug, + derive_more::From, + derive_more::Into, + derive_more::Deref, + derive_more::DerefMut, + PartialEq, +)] +pub struct CsvColumnsFormat(String); + +#[derive(Debug, PartialEq)] +enum CsvTableColumn<'l> { + Single(&'l str), + ValueError(&'l str, &'l str), +} + +impl CsvTableColumn<'_> { + fn write_to(&self, dest: &mut CodeWriter<'_, W>) -> std::fmt::Result { + dest.write_str("\"")?; + dest.write_str(match self { + CsvTableColumn::Single(column) => column, + CsvTableColumn::ValueError(value, _) => value, + })?; + dest.write_str("\"")?; + Ok(()) + } +} + +fn apply_ayano<'token, 'source: 'token>( + block: &'token AyanoBlock<'source>, +) -> Result<(String, CodelessBlockInfo), SyntaxError> { + let ident = calculate_hash(block); + // FIRST OF ALL, + // read the code from insert-file (if present) + let insert_path = block.insert_path.as_ref().map(|path| path.to_path_buf()); + let mut res = if let Some(insert_path) = insert_path.as_ref() { + let mut res = String::new(); + for line in BufReader::new(File::open(insert_path).map_err(Equivalent::from)?).lines() { + let line = line.map_err(Equivalent::from)?; + if line.trim().starts_with("# TOSHINO KYOKO!") { + break; + } + res.push_str(&line); + res.push('\n'); + } + res + } else { + String::new() + }; + let mut code_lines = block.code.lines() + // I used to have this filter here, but I don't think that's a good thing anymore + // There's no harm in preserving basic formatting for better debugging + // TODO probably and #[cfg(test)] switch, that would simplify the code is it's added to Python module itself (i.e. this is to be done in `python` module) + /*.filter(|line| { + let line = line.trim(); + !line.is_empty() && !line.starts_with('#') // remove comments + }) + */ + ; + + // create display name + let display_name = if block.is_display { + Some(block_name(ident)) + } else { + None + }; + + // return out static block here + if block.is_static { + res.extend(itertools::intersperse(code_lines, "\n")); + return Ok(( + res, + CodelessBlockInfo { + ident, + function_name: None, + display_name, + insert_path, + }, + )); + } + // block is a function-block at this point + let function_name = Some(function_name(ident)); + + // take out last line, as it may contain ayano syntax + let Some(last_line) = code_lines.next_back() else { + // there are no lines at all, it's an empty block, or th + // return inserted code anyway, to allow static empty blocks that just insert the code. + // no Ayano syntax is expected there, and that code should not be altered + return Ok(( + res, + CodelessBlockInfo { + ident, + function_name, + display_name, + insert_path, + }, + )); + }; + + // push all other lines + for line in code_lines { + res.push_str(line); // append the line + res.push('\n'); // and a newline character + } + + // extract syntax from the last line, + let syntax = SyntaxData::try_from(last_line)?; + // and write code corresponding to it + syntax + .write_to(&mut res) + .expect("Should always be able to write into an owned String"); + res.push('\n'); // newline for a good measure + + Ok(( + res, + CodelessBlockInfo { + ident, + function_name, + insert_path, + display_name, + }, + )) +} + +#[cfg(test)] +mod apply_ayano_tests { + use super::*; + + macro_rules! test { + {$name:ident, $input:literal, l = $output:literal} => { + test!{$name, $input, l = $output, true} + }; + {$name:ident, $input:literal, l = $output:literal, $is_function:literal} => { + test!{$name, $input, Ok($output.to_string()), $is_function} + }; + {$name:ident, $input:literal, $output:expr} => { + test!{$name, $input, $output, true} + }; + {$name:ident, $input:literal, $output:expr, $is_function:literal} => { + #[test] + fn $name() { + // arrange + let block = AyanoBlock { + code: $input.into(), + insert_path: None, + is_display: false, + is_static: false, + }; + + // act + let actual = apply_ayano(&block).map(|(s, _)| s); + + // assert + assert_eq!(actual, $output, "Should yield expected result"); + } + }; + } + + test! {empty, "", l = ""} + test! {empty2, r" + + + ", l = r" + + + return +"} + test! {comment1, r" +# TOSHINO KYOKO! (this is not actually supposed to trigger any sort of syntax, it's just a test) +# Some other comments bla-bla-bla", l = r" +# TOSHINO KYOKO! (this is not actually supposed to trigger any sort of syntax, it's just a test) +return # Some other comments bla-bla-bla +"} + test! {static_code, r" +x = 4 +y = x + 2 +# Some comment + ", l = +r" +x = 4 +y = x + 2 +# Some comment + return +", false} + test! {function_code, r" +x = 4 +y = x + 2 +# Some comment +y +", l = +r" +x = 4 +y = x + 2 +# Some comment +return y +", true} + test! {function_return, r" +x = 4 +# Even more comments + +y = x + 2 +# Some comment +return y +", l = +r" +x = 4 +# Even more comments + +y = x + 2 +# Some comment +return y +", true} + test! {indent1, r" +x = 4 +# Even more comments + +y = x + 2 +# Some comment + y +", l = +r" +x = 4 +# Even more comments + +y = x + 2 +# Some comment + return y +", true} + test! {bad_syntax, r"@dev: 5,", Err(SyntaxError::BadSyntax), true} + + // TODO I don't feel like testing all of that again here. maybe later? + + #[test] + fn insert_test() { + // arrange + let insert_path: PathBuf = "src/gen/ayano/test_code.py" + .parse() + .expect("Should be a valid path"); + + let code = "@dev: x,y"; + let block = AyanoBlock { + is_display: false, + code: code.into(), + insert_path: Some(insert_path.into()), + is_static: false, + }; + + // act + let transformed_code = + apply_ayano(&block).expect("Should be able to apply ayano transform"); + + // assert + assert_eq!( + transformed_code.0, + r#"x = 5 +y = x ** 10 +return "err", x, y +"# + ); + } +} + +#[derive(Debug, PartialEq)] +struct SyntaxData<'l> { + indent: usize, + syntax: Syntax<'l>, +} + +#[derive(Debug, PartialEq)] +enum Syntax<'source> { + Nothing(&'source str), + ValueError { + value: &'source str, + error: &'source str, + }, + Figure { + src: &'source str, + ident: Option<&'source str>, + caption: Option<&'source str>, + }, + CsvTable { + src: &'source str, + rows: GenericRange, + columns: Option>>, + ident: Option<&'source str>, + caption: Option<&'source str>, + }, + GenTable { + generator: &'source str, + rows: usize, + columns: usize, + ident: Option<&'source str>, + caption: Option<&'source str>, + }, +} + +#[derive(Debug, derive_more::From, PartialEq)] +pub enum SyntaxError { + MissingArgument(&'static str), + UnknownArgument(String), + BadSyntax, + RangeParse(GenericRangeParseError), + IntParse(ParseIntError), + InsertIo(Equivalent), +} + +macro_rules! or_bs { + ($input:expr) => { + ($input).ok_or(SyntaxError::BadSyntax)? + }; +} + +// CHIPPI CHIPPI +// CHAPPA CHAPPA +macro_rules! chip_literal { + ($s:ident, $word:literal, ?) => {{ + chip_literal!($s, $word).unwrap_or($s) + }}; + ($s:ident, $word:literal) => {{ + static REGEX: Lazy = Lazy::new(|| { + Regex::new(concat!("^", $word, r"\s+(.+?)$")).expect("Should be a valid regex") + }); + REGEX + .captures($s) + .and_then(|c| c.get(1)) + .map(|c| c.as_str()) + }}; +} + +#[cfg(test)] +mod chip_literal_tests { + use super::*; + + macro_rules! test { + {$name:ident, $input:literal, $literal:literal} => { + test!{$name, $input, $literal, None} + }; + {$name:ident, $input:literal, $literal:literal, +, $output:literal} => { + test!{$name, $input, $literal, Some($output)} + }; + {$name:ident, $input:literal, $literal:literal, $output:expr} => { + #[test] + fn $name() { + // arrange + let s = $input; + + // act + let output = chip_literal!(s, $literal); + + // assert + assert_eq!(output, $output); + } + }; + } + + test! {none1, "abcd abc", "a"} + test! {some1, "abcd abc", "abcd", +, "abc"} + test! {some1_simple, "a b", "a", +, "b"} + test! {return1, "return x", "return", +, "x"} + + macro_rules! test_optional { + {$name:ident, $input:literal, $literal:literal, $output:literal} => { + #[test] + fn $name() { + // arrange + let mut s = $input; + + // act + s = chip_literal!(s, $literal, ?); + + // assert + assert_eq!(s, $output); + } + }; + } + test_optional! {edge1, r#"@gen_table: lambda row, col: 1; rows = 10, columns = 10 caption = "Some awesome caption""#, "return", + r#"@gen_table: lambda row, col: 1; rows = 10, columns = 10 caption = "Some awesome caption""# + } + + #[test] + fn return2() { + // arrange + let mut s = "return x"; + + // act + s = chip_literal!(s, "return", ?); + + // assert + assert_eq!(s, "x"); + } +} + +macro_rules! regex { + ($regex:literal) => {{ + const STR: &'static str = $regex; + static REGEX: Lazy = Lazy::new(|| Regex::new(STR).expect("Should be a valid regex")); + &*REGEX + }}; + ($regex:literal, $s:ident) => { + or_bs!(regex!($regex).captures($s)) + }; +} + +macro_rules! _parse_arg { + (continue, $_:expr) => { + continue + }; + ($var:ident, $val:expr) => { + $var = Some($val) + }; +} + +macro_rules! parse_args { + ($s:ident, $($var:tt, $name:literal), *) => { + for (kw, val) in find_args($s) { + match kw { + $($name => _parse_arg!($var, val),)* + _ => return Err(SyntaxError::UnknownArgument(kw.to_string())), + }; + } + }; +} + +macro_rules! needed { + ($var:ident, $name:literal) => { + let Some($var) = $var else { + return Err(SyntaxError::MissingArgument($name)); + }; + }; +} + +// Can't use FromStr here, as if requires parsing from &str with any lifetime +impl<'l> TryFrom<&'l str> for SyntaxData<'l> { + type Error = SyntaxError; + + fn try_from(mut s: &'l str) -> Result { + // First, cut off empty space (but record number of them) + let indent = s + .find(|ch: char| !ch.is_whitespace()) + .unwrap_or(s.chars().count()); // TODO POSSIBLE BUG: `str::len` is a length of underlying byte array, not string itself + s = &s[indent..]; + s = s.trim_end(); // also remove whitespace at the end + + // Then, chip off return statement (will be added regardless) + s = chip_literal!(s, "return", ?); + + // Check for @dev syntax + if let Some(s) = chip_literal!(s, "@dev:") { + let m = regex!(r"^\s*(.+?)\s*,\s*(.+?)$", s); + return Ok(Self { + indent, + syntax: Syntax::ValueError { + value: or_bs!(m.get(1)).as_str(), + error: or_bs!(m.get(2)).as_str(), + }, + }); + } + + if let Some(s) = chip_literal!(s, "@fig:") { + let mut src = None; + let mut ident = None; + let mut caption = None; + parse_args!(s, src, "src", ident, "ident", caption, "caption"); + needed!(src, "src"); + return Ok(Self { + indent, + syntax: Syntax::Figure { + src, + caption, + ident, + }, + }); + } + + if let Some(s) = chip_literal!(s, "@csv_table:") { + let mut src = None; + let mut rows = None; + let mut columns = None; + let mut ident = None; + let mut caption = None; + parse_args!( + s, src, "src", rows, "rows", columns, "columns", ident, "ident", caption, "caption" + ); + needed!(src, "src"); + let rows = rows + .map(|val| val.parse()) + .unwrap_or(Ok(GenericRange::Full))?; + + let columns = if let Some(val) = columns.as_ref() { + Some(parse_columns(&val)) + } else { + None + }; + + return Ok(Self { + indent, + syntax: Syntax::CsvTable { + src, + rows, + columns, + ident, + caption, + }, + }); + } + + if let Some(s) = chip_literal!(s, "@gen_table:") { + let Some((generator, s)) = s.split_once(';') else { + return Err(SyntaxError::BadSyntax); + }; + let mut rows = None; + let mut columns = None; + let mut ident = None; + let mut caption = None; + parse_args!(s, rows, "rows", columns, "columns", ident, "ident", caption, "caption"); + needed!(rows, "rows"); + needed!(columns, "columns"); + return Ok(Self { + indent, + syntax: Syntax::GenTable { + generator, + rows: rows.parse()?, + columns: columns.parse()?, + ident, + caption, + }, + }); + } + + // If executing reaches here, this is a no-syntax-thing + Ok(Self { + indent, + syntax: Syntax::Nothing(s), + }) + } +} + +#[cfg(test)] +mod syntax_data_tests { + use super::*; + macro_rules! test { + ($name:ident, $input:literal, $output:expr) => { + #[test] + fn $name() { + // arrange + + // act + let output = SyntaxData::try_from($input).expect("Should be a valid syntax"); + + // assert + assert_eq!(output, $output, "Should produce expected result"); + } + }; + } + + macro_rules! c { + ($col:literal) => { + CsvTableColumn::Single($col) + }; + ($value:literal, $error:literal) => { + CsvTableColumn::ValueError($value, $error) + }; + } + + test! {nothing1, "a", SyntaxData {indent: 0, syntax: Syntax::Nothing("a")}} + test! {nothing2, " a", SyntaxData {indent: 4, syntax: Syntax::Nothing("a")}} + test! {nothing3, " a, lala, c ", SyntaxData {indent: 4, syntax: Syntax::Nothing("a, lala, c")}} + test! {return1, " return x", SyntaxData {indent: 4, syntax: Syntax::Nothing("x")}} + test! {value_error1, "@dev: 1, 2", SyntaxData {indent: 0, syntax: Syntax::ValueError { value: "1", error: "2" }}} + test! {value_error2, "@dev: 1, 2 ", SyntaxData {indent: 0, syntax: Syntax::ValueError { value: "1", error: "2" }}} + test! {value_error3, "@dev: x + 1, (2 + 3) / 10 ", SyntaxData {indent: 0, syntax: Syntax::ValueError { value: "x + 1", error: "(2 + 3) / 10" }}} + test! {fig1, r#" @fig: src = "path.png" , caption = "Wow, such caption", ident="path" "#, SyntaxData {indent: 4, syntax: Syntax::Figure{ src:"\"path.png\"", caption: Some("\"Wow, such caption\""), ident: Some("\"path\"")}}} + test! {fig2, r#" @fig: caption = "Wow, such caption", ident="path" ,src = "path.png" "#, SyntaxData {indent: 4, syntax: Syntax::Figure{ src: "\"path.png\"", caption: Some("\"Wow, such caption\""), ident: Some("\"path\"")}}} + test! {csv_table1, r#" @csv_table: src = "path/to/table.csv", caption = "such caption, wow""#, SyntaxData {indent: 3, syntax: Syntax::CsvTable { src: "\"path/to/table.csv\"", rows: GenericRange::Full, columns: None, ident: None, caption: Some("\"such caption, wow\"") }}} + test! {csv_table2, r#" @csv_table: src = "path/to/table.csv", rows = 1..1000"#, SyntaxData {indent: 3, syntax: Syntax::CsvTable { src: "\"path/to/table.csv\"", rows: GenericRange::DoubleBounded(1..1000), columns: None, ident: None, caption: None, }}} + test! {csv_table3, r#" @csv_table: src = "path/to/table.csv", rows = 2.."#, SyntaxData {indent: 3, syntax: Syntax::CsvTable { src: "\"path/to/table.csv\"", rows: GenericRange::From(2..), columns: None, ident: None, caption: None }}} + test! {csv_table4, r#" @csv_table: src = "path/to/table.csv", rows = 2.., columns = ["a", ("b", "b_err")]"#, SyntaxData {indent: 3, syntax: Syntax::CsvTable { src: "\"path/to/table.csv\"", rows: GenericRange::From(2..), columns: Some(vec![c!("a"),c!("b","b_err")]), caption: None, ident: None }}} + test! {csv_table5, r#" @csv_table: src = "path/to/table.csv", rows = 2.., columns = ["a", ("b", "b_err")], caption = "Lol, there's a bunch of random stuff going on, this should not be included into a syntax data""#, + SyntaxData {indent: 4, syntax: Syntax::CsvTable { src: "\"path/to/table.csv\"", rows: GenericRange::From(2..), columns: Some(vec![c!("a"),c!("b","b_err")]), + caption: Some("\"Lol, there's a bunch of random stuff going on, this should not be included into a syntax data\""), // ha-ha, get spec-changed, lol + ident: None, }}} + test! {get_table1, r#" @gen_table: lambda row, col: 1; rows = 10, columns = 10, caption = "Some awesome caption""#, + SyntaxData {indent: 4, syntax: Syntax::GenTable { generator: "lambda row, col: 1", rows: 10, columns: 10, caption: Some("\"Some awesome caption\""), ident: None, }}} +} + +struct CodeWriter<'w, W: ?Sized> { + inner: &'w mut W, + indent: &'static str, +} + +#[cached] +fn get_indent(indent: usize) -> &'static str { + itertools::repeat_n(' ', indent).collect::().leak() +} + +impl<'w, W: Write + ?Sized> CodeWriter<'w, W> { + fn new(inner: &'w mut W, indent: usize) -> Result { + let indent = get_indent(indent); + inner.write_str(indent)?; + Ok(Self { inner, indent }) + } + + fn breakline(&mut self) -> std::fmt::Result { + self.inner.write_str("\n")?; + self.inner.write_str(self.indent)?; // start off by writing out indentation + Ok(()) + } + + fn write_str(&mut self, s: &str) -> std::fmt::Result { + self.inner.write_str(s); + Ok(()) + } + + fn write_fmt(&mut self, fmt: Arguments<'_>) -> std::fmt::Result { + self.inner.write_fmt(fmt)?; + Ok(()) + } + + fn write_line(&mut self, s: &str) -> std::fmt::Result { + self.write_str(s)?; + self.breakline()?; + Ok(()) + } +} + +impl SyntaxData<'_> { + fn write_to<'w, W: Write + ?Sized>(&self, dest: &'w mut W) -> std::fmt::Result { + let mut writer: CodeWriter<'w, W> = CodeWriter::new(dest, self.indent)?; + self.syntax.write_code(&mut writer)?; + Ok(()) + } +} + +impl Syntax<'_> { + fn write_code(&self, writer: &mut CodeWriter<'_, W>) -> std::fmt::Result { + match self { + Syntax::Nothing(s) => { + writer.write_str("return ")?; + writer.write_str(s)?; + } + Syntax::ValueError { value, error } => { + writer.write_str("return \"err\", ")?; + writer.write_str(value)?; + writer.write_str(", ")?; + writer.write_str(error)?; + } + Syntax::Figure { + src, + ident, + caption, + } => { + writer.write_str("return \"fig\", ")?; + writer.write_str(src)?; + writer.write_str(", ")?; + if let Some(ident) = ident { + writer.write_str(ident)?; + } else { + writer.write_str("None")?; + } + writer.write_str(", ")?; + if let Some(caption) = caption { + writer.write_str(caption)?; + } else { + writer.write_str("None")?; + } + } + Syntax::CsvTable { + src, + rows, + columns, + ident, + caption, + } => { + writer.write_line("import csv")?; + writer.write_str("with open(")?; + writer.write_str(src)?; + writer.write_line(") as ___csv_file___:")?; + writer.write_line(" ___reader___ = csv.DictReader(___csv_file___, delimiter=',', skipinitialspace=True, strict=True)")?; + // if there is a lower bound, skip some lines + if let Some(mut from) = rows.from() { + // if it's 0 or 1, there's effectively no lines to skip + if from > 1 { + // line 1 refers to a first line of file + from -= 1; + write!(writer, " for i in range({from}):")?; // it's a number here, so it can't really be more optimized than that + writer.breakline()?; + writer.write_line(" ___reader___.__next__()")?; + } + } + + let headers = |writer: &mut CodeWriter<'_, W>| { + if let Some(columns) = columns.as_ref() { + writer.write_str("[")?; + for i in 0..columns.len() { + columns[i].write_to(writer)?; + writer.write_str(", ")?; + } + writer.write_str("]")?; + } else { + writer.write_str("list(___reader___.fieldnames)")?; + }; + Ok::<_, std::fmt::Error>(()) + }; + + let line_syntax = |writer: &mut CodeWriter<'_, W>| { + if let Some(columns) = columns.as_ref() { + writer.write_str("[")?; + for i in 0..columns.len() { + match columns[i] { + CsvTableColumn::Single(col) => { + writer.write_str("line[\"")?; + writer.write_str(col)?; + writer.write_str("\"]")?; + } + CsvTableColumn::ValueError(value, error) => { + writer.write_str("(\"err\", line[\"")?; + writer.write_str(value)?; + writer.write_str("\"], line[\"")?; + writer.write_str(error)?; + writer.write_str("\"])")?; + } + } + writer.write_str(", ")?; + } + writer.write_str("]")?; + } else { + writer.write_str("list([line[f] for f in line])")?; + }; + Ok::<_, std::fmt::Error>(()) + }; + + // If a dedicated line generator is necessary, write out one now + if let Some(rows_to) = rows.to() { + let line_iterations = rows_to - rows.from().unwrap_or_default(); + writer.write_line(" def ___line_generator___():")?; + write!(writer, " for i in range({line_iterations}):")?; // it's again numbers, can't do much better + writer.breakline()?; + writer.write_line(" line = ___reader___.__next__()")?; + writer.write_str(" yield ")?; + line_syntax(writer)?; + writer.breakline()?; + } + + let line_generator = |writer: &mut CodeWriter<'_, W>| { + if rows.to().is_some() { + writer.write_str("___line_generator___()")?; + } else { + writer.write_str("[")?; + line_syntax(writer)?; + writer.write_str(" for line in ___reader___]")? + }; + Ok::<_, std::fmt::Error>(()) + }; + + writer.write_str(" return \"tab\", ")?; + headers(writer)?; + writer.write_str(", list(")?; + line_generator(writer)?; + writer.write_str("), ")?; + if let Some(ident) = ident { + writer.write_str(ident)?; + } else { + writer.write_str("None")?; + } + writer.write_str(", ")?; + if let Some(caption) = caption { + writer.write_str(caption)?; + } else { + writer.write_str("None")?; + } + } + &Syntax::GenTable { + generator, + rows, + columns, + ident, + caption, + } => { + writer.write_str("___generator___ = ")?; + writer.write_line(generator)?; + writer.write_str("return \"tab\", [")?; + for row_index in 0..rows { + writer.write_str("[")?; + for column_index in 0..columns { + write!(writer, "___generator___({row_index}, {column_index}), ")?; + // numbers. can't really do much + } + writer.write_str("], ")?; + } + writer.write_str("], ")?; + if let Some(ident) = ident { + writer.write_str(ident)?; + } else { + writer.write_str("None")?; + } + writer.write_str(", ")?; + if let Some(caption) = caption { + writer.write_str(caption)?; + } else { + writer.write_str("None")?; + } + } + }; + Ok(()) + } +} + +#[cfg(test)] +mod syntax_display_tests { + use super::*; + macro_rules! test { + {$name:ident, $input:expr, $output:literal} => { + #[test] + fn $name() { + // arrange + let expected_output = $output.to_string(); + let mut actual_output = String::new(); + + // act + $input.write_to(&mut actual_output).expect("Should be able to write into string"); + + // assert + assert_eq!(actual_output, expected_output, "Should produce expected result"); + } + }; + } + + test! {empty1, SyntaxData {indent: 0, syntax: Syntax::Nothing("")}, "return "} + test! {indent1, SyntaxData {indent: 4, syntax: Syntax::Nothing("")}, " return "} + test! {value_error1, SyntaxData {indent: 4, syntax: Syntax::ValueError{value:"x", error:"y"}}, " return \"err\", x, y"} + test! {value_error2, SyntaxData {indent: 0, syntax: Syntax::ValueError{value:"x + 2", error:"sqrt(x/2 +y)"}}, "return \"err\", x + 2, sqrt(x/2 +y)"} + test! {figure1, SyntaxData {indent: 0, syntax: Syntax::Figure{src:"output_path", caption: Some("\"some caption text\""), ident: Some("\"fig1\"")}}, "return \"fig\", output_path, \"fig1\", \"some caption text\""} + test! {csv_table1, SyntaxData { + indent: 0, + syntax: Syntax::CsvTable { + src: "file_path", + rows: GenericRange::ToInclusive(..=10), + columns: Some(vec![CsvTableColumn::Single("a")]), + ident: Some("\"table1\""), + caption: None + }}, +r#"import csv +with open(file_path) as ___csv_file___: + ___reader___ = csv.DictReader(___csv_file___, delimiter=',', skipinitialspace=True, strict=True) + def ___line_generator___(): + for i in range(10): + line = ___reader___.__next__() + yield [line["a"], ] + return "tab", ["a", ], list(___line_generator___()), "table1", None"# + } + test! {gen_table1, SyntaxData {indent: 0, syntax: Syntax::GenTable { generator: "lambda r,c:1", rows: 2, columns: 3, ident: None, caption: Some("caption_variable") }}, +r#"___generator___ = lambda r,c:1 +return "tab", [[___generator___(0, 0), ___generator___(0, 1), ___generator___(0, 2), ], [___generator___(1, 0), ___generator___(1, 1), ___generator___(1, 2), ], ], None, caption_variable"# + } + test! {gen_table2, SyntaxData {indent: 4, syntax: Syntax::GenTable { generator: "lambda r,c:1", rows: 2, columns: 3, ident: Some("\"table2\""), caption: None }}, +r#" ___generator___ = lambda r,c:1 + return "tab", [[___generator___(0, 0), ___generator___(0, 1), ___generator___(0, 2), ], [___generator___(1, 0), ___generator___(1, 1), ___generator___(1, 2), ], ], "table2", None"# + } +} + +#[cfg(test)] +mod last_line_tests { + use super::*; + + macro_rules! test { + {$name:ident, $input:literal, -, $output:expr} => { + test!{$name, $input, Err($output)} + }; + {$name:ident, $input:literal, +, $output:expr} => { + test!{$name, $input, Ok($output)} + }; + {$name:ident, $input:literal, $output:literal} => { + test!{$name, $input, +, $output.trim().to_string()} + }; + {$name:ident, $input:literal, $output:expr} => { + #[test] + fn $name() { + // arrange + + // act + let actual_output = SyntaxData::try_from($input).map(move |data| { + let mut res = String::new(); + data.write_to(&mut res).expect("Should be able to write"); + res + }); + + // assert + assert_eq!( + $output, + actual_output, + "Should produce expected result" + ); + } + }; + } + + test! {nothing1, "1, 2", "return 1, 2"} + test! {nothing2, "\"resulting string!\"", "return \"resulting string!\""} + test! {dev1, "@dev: 2.0, -0.34E4", "return \"err\", 2.0, -0.34E4"} + + test! {fig1, "@fig: src=\"./picture.jpg\"", "return \"fig\", \"./picture.jpg\", None, None"} + test! {fig2, "@fig: caption=\"Some very cool picture\", src=\"./picture.jpg\"", + "return \"fig\", \"./picture.jpg\", None, \"Some very cool picture\""} + test! {fig3, "@fig: ident=\"cool\", caption=\"Some very cool picture\", src=\"./picture.jpg\"", + "return \"fig\", \"./picture.jpg\", \"cool\", \"Some very cool picture\""} + test! {fig4, "@fig: caption=\"Some very cool picture\", src=\"./picture.jpg\", ident=\"cool\"", + "return \"fig\", \"./picture.jpg\", \"cool\", \"Some very cool picture\""} + test! {fig5, "@fig: caption=\"Some very cool picture\", ident=\"cool\"", -, SyntaxError::MissingArgument("src".into())} + test! {fig6, "@fig: caption=\"Some very cool picture\", ident=\"cool\", coolness=1000", -, SyntaxError::UnknownArgument("coolness".to_string().into())} + test! {fig7, "@fig: caption=\"Some very cool picture\", ident=\"cool\", coolness=1000, src=\"path.png\"", -, SyntaxError::UnknownArgument("coolness".to_string().into())} + + test! {csv_tab1e_simple, "@csv_table: src=\"some/path/idk.csv\"", r#" +import csv +with open("some/path/idk.csv") as ___csv_file___: + ___reader___ = csv.DictReader(___csv_file___, delimiter=',', skipinitialspace=True, strict=True) + return "tab", list(___reader___.fieldnames), list([list([line[f] for f in line]) for line in ___reader___]), None, None + "#} + + test! {csv_table_no_src_should_err, "@csv_table: caption=\"nice caption, bro!\"", -, SyntaxError::MissingArgument("src")} + test! {csv_tab1e_rows1, "@csv_table: src=\"some/path/idk.csv\", rows=..", r#" +import csv +with open("some/path/idk.csv") as ___csv_file___: + ___reader___ = csv.DictReader(___csv_file___, delimiter=',', skipinitialspace=True, strict=True) + return "tab", list(___reader___.fieldnames), list([list([line[f] for f in line]) for line in ___reader___]), None, None + "#} + test! {csv_tab1e_rows2, "@csv_table: src=\"some/path/idk.csv\", rows=1..", r#" +import csv +with open("some/path/idk.csv") as ___csv_file___: + ___reader___ = csv.DictReader(___csv_file___, delimiter=',', skipinitialspace=True, strict=True) + return "tab", list(___reader___.fieldnames), list([list([line[f] for f in line]) for line in ___reader___]), None, None + "#} + test! {csv_tab1e_rows3, "@csv_table: src=\"some/path/idk.csv\", rows=10..", r#" +import csv +with open("some/path/idk.csv") as ___csv_file___: + ___reader___ = csv.DictReader(___csv_file___, delimiter=',', skipinitialspace=True, strict=True) + for i in range(9): + ___reader___.__next__() + return "tab", list(___reader___.fieldnames), list([list([line[f] for f in line]) for line in ___reader___]), None, None + "#} + test! {csv_tab1e_rows4, "@csv_table: src=\"some/path/idk.csv\", rows=..10", r#" +import csv +with open("some/path/idk.csv") as ___csv_file___: + ___reader___ = csv.DictReader(___csv_file___, delimiter=',', skipinitialspace=True, strict=True) + def ___line_generator___(): + for i in range(9): + line = ___reader___.__next__() + yield list([line[f] for f in line]) + return "tab", list(___reader___.fieldnames), list(___line_generator___()), None, None + "#} + test! {csv_tab1e_rows5, "@csv_table: src=\"some/path/idk.csv\", rows=5..=100", r#" +import csv +with open("some/path/idk.csv") as ___csv_file___: + ___reader___ = csv.DictReader(___csv_file___, delimiter=',', skipinitialspace=True, strict=True) + for i in range(4): + ___reader___.__next__() + def ___line_generator___(): + for i in range(95): + line = ___reader___.__next__() + yield list([line[f] for f in line]) + return "tab", list(___reader___.fieldnames), list(___line_generator___()), None, None + "#} + test! {csv_tab1e_rows6, "@csv_table: src=\"some/path/idk.csv\", rows=1..100", r#" +import csv +with open("some/path/idk.csv") as ___csv_file___: + ___reader___ = csv.DictReader(___csv_file___, delimiter=',', skipinitialspace=True, strict=True) + def ___line_generator___(): + for i in range(98): + line = ___reader___.__next__() + yield list([line[f] for f in line]) + return "tab", list(___reader___.fieldnames), list(___line_generator___()), None, None + "#} + test! {csv_tab1e_bad_rows, "@csv_table: src=\"some/path/idk.csv\", rows=10=..", -, + SyntaxError::RangeParse(GenericRangeParseError::Format)} + test! {csv_tab1e_columns1, r#"@csv_table: src="some/path/idk.csv", columns = []"#, r#" +import csv +with open("some/path/idk.csv") as ___csv_file___: + ___reader___ = csv.DictReader(___csv_file___, delimiter=',', skipinitialspace=True, strict=True) + return "tab", [], list([[] for line in ___reader___]), None, None + "#} + test! {csv_tab1e_columns2, r#"@csv_table: src="some/path/idk.csv", columns = ["a"]"#, r#" +import csv +with open("some/path/idk.csv") as ___csv_file___: + ___reader___ = csv.DictReader(___csv_file___, delimiter=',', skipinitialspace=True, strict=True) + return "tab", ["a", ], list([[line["a"], ] for line in ___reader___]), None, None + "#} + test! {csv_tab1e_columns3, r#"@csv_table: src="some/path/idk.csv", columns = ["a", ("b", "b_err")]"#, r#" +import csv +with open("some/path/idk.csv") as ___csv_file___: + ___reader___ = csv.DictReader(___csv_file___, delimiter=',', skipinitialspace=True, strict=True) + return "tab", ["a", "b", ], list([[line["a"], ("err", line["b"], line["b_err"]), ] for line in ___reader___]), None, None + "#} + test! {csv_table_combined1, r#"@csv_table: src="some/path/idk.csv", columns = ["a", ("b", "b_err")], rows =..10"#, r#" +import csv +with open("some/path/idk.csv") as ___csv_file___: + ___reader___ = csv.DictReader(___csv_file___, delimiter=',', skipinitialspace=True, strict=True) + def ___line_generator___(): + for i in range(9): + line = ___reader___.__next__() + yield [line["a"], ("err", line["b"], line["b_err"]), ] + return "tab", ["a", "b", ], list(___line_generator___()), None, None + "#} + test! {csv_table_combined2, r#"@csv_table: src="some/path/idk.csv", columns = ["a", ("b", "b_err")], rows =10..=100"#, r#" +import csv +with open("some/path/idk.csv") as ___csv_file___: + ___reader___ = csv.DictReader(___csv_file___, delimiter=',', skipinitialspace=True, strict=True) + for i in range(9): + ___reader___.__next__() + def ___line_generator___(): + for i in range(90): + line = ___reader___.__next__() + yield [line["a"], ("err", line["b"], line["b_err"]), ] + return "tab", ["a", "b", ], list(___line_generator___()), None, None + "#} + test! {csv_table_combined3, r#"@csv_table: src="some/path/idk.csv", columns = ["a", ("b", "b_err")], rows =10..100"#, r#" +import csv +with open("some/path/idk.csv") as ___csv_file___: + ___reader___ = csv.DictReader(___csv_file___, delimiter=',', skipinitialspace=True, strict=True) + for i in range(9): + ___reader___.__next__() + def ___line_generator___(): + for i in range(89): + line = ___reader___.__next__() + yield [line["a"], ("err", line["b"], line["b_err"]), ] + return "tab", ["a", "b", ], list(___line_generator___()), None, None + "#} + + test! {gen_table1, r#"@gen_table: lambda row, col: 1; rows= 5, columns =2"#, +r#"___generator___ = lambda row, col: 1 +return "tab", [[___generator___(0, 0), ___generator___(0, 1), ], [___generator___(1, 0), ___generator___(1, 1), ], [___generator___(2, 0), ___generator___(2, 1), ], [___generator___(3, 0), ___generator___(3, 1), ], [___generator___(4, 0), ___generator___(4, 1), ], ], None, None"#} + test! {gen_table2, r#"@gen_table: lambda row, col: -1;columns =2, rows= 5"#, +r#"___generator___ = lambda row, col: -1 +return "tab", [[___generator___(0, 0), ___generator___(0, 1), ], [___generator___(1, 0), ___generator___(1, 1), ], [___generator___(2, 0), ___generator___(2, 1), ], [___generator___(3, 0), ___generator___(3, 1), ], [___generator___(4, 0), ___generator___(4, 1), ], ], None, None"#} +} + +#[derive(Debug, PartialEq)] +pub enum ParsingError { + Float(ParseFloatError), + UnexpectedSyntax(String, Equivalent), + TableNoHeader, +} + +impl From for ParsingError { + fn from(value: ParseFloatError) -> Self { + Self::Float(value) + } +} + +fn parse_ayano(python_output: &PyAny) -> Result, ParsingError> { + // it was quite a journey! but I believe to be close to it's end. + if let Ok(tuple) = python_output.downcast::() { + if let Ok(discriminator) = tuple + .get_item(0) + .and_then(|e| e.str()) + .and_then(|s| s.to_str()) + { + macro_rules! throw_syntax { + ($input:expr) => { + $input.map_err(|err| { + ParsingError::UnexpectedSyntax( + python_output + .str() + .map(|s| s.to_string()) + .unwrap_or(String::from("")), + err.into(), + ) + }) + }; + } + + macro_rules! throw_downcast { + ($input:expr) => { + $input.map_err(|err| { + ParsingError::UnexpectedSyntax( + python_output + .str() + .map(|s| s.to_string()) + .unwrap_or(String::from("")), + PyErr::from(err).into(), + ) + }) + }; + } + match discriminator { + "err" => { + // this is a value-error syntax. + // any inconsistency will be treated as syntax error from now on. + // expected syntax: ('err', , ) + let value = throw_syntax!(tuple + .get_item(1) + .and_then(|item| item.str()) + .and_then(|s| s.to_str()))?; + let error = throw_syntax!(tuple + .get_item(2) + .and_then(|item| item.str()) + .and_then(|s| s.to_str()))?; + return Ok(value_error_format(value, error)?); + } + "fig" => { + // this is a figure syntax. + // any inconsistency will be treated as syntax error from now on. + // expected syntax: ('fig', , , ) + let src: &str = throw_syntax!(tuple + .get_item(1) + .and_then(|item| item.str()) + .and_then(|s| s.to_str()))?; + let ident = throw_syntax!(tuple.get_item(2))?; + let ident = (!ident.is_none()) + .then_some(throw_syntax!(ident.str().and_then(|s| s.to_str()))?); + let caption = throw_syntax!(tuple.get_item(3))?; + let caption = (!caption.is_none()) + .then_some(throw_syntax!(caption.str().and_then(|s| s.to_str()))?); + return Ok(Token::Figure { + src_name: Cow::Owned(PathBuf::from(src)), + caption: None, // TODO MUST USE LEXER HERE + ident: ident.map(|i| Cow::Owned(i.to_string())), + }); + } + "tab" => { + // this is a table syntax. + // any inconsistency will be treated as syntax error from now on. + // expected syntax: ('tab', [$([$(), +]), +], 'ident'|None, 'caption'|None) + + let list = + throw_downcast!(throw_syntax!(tuple.get_item(1))?.downcast::())?; + let mut rows = list.into_iter(); + let header: Vec<_> = throw_downcast!(rows + .next() + .ok_or(ParsingError::TableNoHeader)? + .downcast::())? + .into_iter() + .map(|h: &PyAny| parse_ayano(h)) + .try_collect()?; + let cells: Vec<_> = rows + .map(|row| { + Ok(throw_downcast!(row.downcast::())? + .into_iter() + .map(|c: &PyAny| parse_ayano(c))) + }) + .try_fold(Vec::new(), |acc, next: Result<_, ParsingError>| { + // OLD: there might be a better way with iterators directly, but I kinda can't come up with it + // UPD: this looks... BAD + // but I guess that's iterators all the way + next?.try_fold(acc, |mut acc, next| { + acc.push(next?); + Ok::<_, ParsingError>(acc) + }) + })?; + + let ident = throw_syntax!(tuple.get_item(2))?; + let ident = (!ident.is_none()) + .then_some(throw_syntax!(ident.str().and_then(|s| s.to_str()))?); + let caption = throw_syntax!(tuple.get_item(3))?; + let caption = (!caption.is_none()) + .then_some(throw_syntax!(caption.str().and_then(|s| s.to_str()))?); + return Ok(Token::Table { + header: Tokens::new(header), + cells: Tokens::new(cells), + ident: ident.map(|s| Cow::Owned(s.to_string())), + caption: None, // TODO MUST USE LEXER HERE + }); + } + _ => {} + } + } + } + // any sort of parsing had failed - return regular text + Ok(Token::Paragraph( + Font::Normal, + Cow::Owned(python_output.to_string()), + )) +} + +#[cfg(test)] +fn value_error_token(value: f64, error: f64) -> Token<'static> { + Token::Text(Tokens::new([ + Token::text(value.to_string()), + Token::InlineMathmode(r"\pm".into()), + Token::text(error.to_string()), + ])) +} + +#[cfg(test)] +mod parsing_tests { + + use super::*; + use pyo3::{ + exceptions::PyTypeError, + types::{PyFloat, PyList, PyString, PyTuple}, + IntoPy, PyErr, + }; + + use crate::data::{Token, Tokens}; + + macro_rules! test { + ($name:ident, $input:expr, $output:expr) => { + #[test] + fn $name() { + super::Python::with_gil(|py| { + // arrange + let python_output = $input(py); + + // act + let result = super::parse_ayano(python_output); + + // assert + assert_eq!(result, $output, "Parsing should produce expected tokens"); + }); + } + }; + } + + macro_rules! py_err { + ($py:ident) => { + PyErr::new::("Error message").into() + }; + } + + test! {text1, |py| PyString::new(py, "Some text".into()), Ok(Token::text("Some text"))} + test! {text2, |py| PyFloat::new(py, 0.0025), Ok(Token::text("0.0025"))} + test! {text3, |py| PyList::new(py, [1, 2, 3]), Ok(Token::text("[1, 2, 3]"))} + test! {text4, |py| PyTuple::new(py, ["lol", "some_info"]), Ok(Token::text("('lol', 'some_info')"))} + // TODO add tests with lexer + test! {err1, |py| PyTuple::new(py, ["err".into_py(py), ["yet another not a number"].into_py(py)]), + Err(crate::gen::ayano::ParsingError::UnexpectedSyntax("('err', ['yet another not a number'])".to_string(), py_err!(py)))} + test! {value_error1, |py| PyTuple::new(py, ["err".into_py(py), 1.0.into_py(py), 0.5.into_py(py)]), + Ok(Token::Text(Tokens::new([Token::text("1.0"), Token::InlineMathmode(r"\pm".into()), Token::text("0.5")])))} + test! {value_error2, |py| PyTuple::new(py, ["err".into_py(py), "1.0".into_py(py), 0.5.into_py(py)]), + Ok(Token::Text(Tokens::new([Token::text("1.0"), Token::InlineMathmode(r"\pm".into()), Token::text("0.5")])))} + test! {value_error_format1, |py| PyTuple::new(py, ["err".into_py(py), "1.987654321".into_py(py), 0.00000123456.into_py(py)]), + Ok(value_error_token(1.9876543, 0.0000012))} + // TODO probably should add some more tests for value-error formatting + + test! {figure_nocaption1, |py| PyTuple::new(py, ["fig".into_py(py), "path/to/file.png".into_py(py), "circle".into_py(py), None::<()>.into_py(py)]), + Ok(Token::Figure { + src_name: Cow::Owned(PathBuf::from("path/to/file.png")), + caption: None, + ident: Some("circle".into()) })} + // TODO add tests for figure WITH CAPTION + + test! {table_nocaption, |py| { + PyTuple::new(py, [ + "tab".into_py(py), + PyList::new(py, [ + PyList::new(py, ["header1".into_py(py), 1.0.into_py(py)]), + PyList::new(py, ["cell_11".into_py(py), 0.25.into_py(py)]), + PyList::new(py, ["cell_21".into_py(py), (-2).into_py(py)]), + ]).into_py(py), + "table1".into_py(py), + None::<()>.into_py(py), + ])}, + Ok(Token::Table { + header: Tokens::new([Token::text("header1"), Token::text("1.0")]), + cells: Tokens::new([ + Token::text("cell_11"), Token::text("0.25"), + Token::text("cell_21"), Token::text("-2"), + ]), + caption: None, + ident: Some("table1".into()) + }) + } + test! {table_value_error, |py| { + PyTuple::new(py, [ + "tab".into_py(py), + PyList::new(py, [ + PyList::new(py, ["header1".into_py(py), 1.0.into_py(py)]), + PyList::new(py, [PyTuple::new(py, [ + "err".into_py(py), + 1.012345.into_py(py), + "0.25".into_py(py) + ]).into_py(py), 0.25.into_py(py)]), + PyList::new(py, ["cell_21".into_py(py), (-2).into_py(py)]), + ]).into_py(py), + "table1".into_py(py), + None::<()>.into_py(py), + ])}, + Ok(Token::Table { + header: Tokens::new([Token::text("header1"), Token::text("1.0")]), + cells: Tokens::new([ + value_error_token(1.01, 0.25), Token::text("0.25"), + Token::text("cell_21"), Token::text("-2"), + ]), + caption: None, + ident: Some("table1".into()) + }) + } + // TODO add tests for table WITH CAPTION +} + +struct PrimitiveDecimal { + digits: Vec, + zero: isize, + sign: bool, +} + +impl FromStr for PrimitiveDecimal { + type Err = ParseFloatError; + + fn from_str(mut s: &str) -> Result { + let _: f64 = s.parse()?; + let sign = if s.starts_with('+') { + false + } else if s.starts_with('-') { + true + } else { + false + }; + s = s.trim_start_matches(['+', '-']); + let (s, exponent) = if let Some(split) = s.split_once(['e', 'E']) { + split + } else { + (s, "0") + }; + let (integer, fraction) = if let Some(split) = s.split_once('.') { + split + } else { + (s, "") + }; + let zero = (isize::try_from(integer.len()).expect("Why.") - 1).try_into().expect("How in the world did you get 2B digits into the integer part? Things are certainly getting out of hand"); + let digits = integer.chars().chain(fraction.chars()).collect_vec(); + let mut res = Self { zero, digits, sign }; + + // Account for exponent + let exponent: isize = exponent + .parse() + .expect("Should be able to parse exponent, as initial input is correct"); // TODO ensure there are tests to check that in detail + res.apply_exponent(exponent); + Ok(res) + } +} + +impl ToString for PrimitiveDecimal { + fn to_string(&self) -> String { + let len = self.digits.len(); + let (integer_part, fraction_part) = if self.zero < 0 { + let additinal: usize = (-self.zero - 1).try_into().expect("Why.?"); + // All the digits are in the fraction part :) + ( + String::new(), + repeat_n(&'0', additinal) + .chain(self.digits.iter()) + .collect(), + ) + } else { + let zero = self.zero as usize; + if zero + 1 >= len { + // All the digits are in the integer part :) + ( + self.digits + .iter() + .chain(repeat_n(&'0', zero + 1 - len)) + .collect(), + String::new(), + ) + } else { + let (integer, fraction) = self.digits.split_at(zero + 1); + ( + integer.into_iter().collect(), + fraction.into_iter().collect(), + ) + } + }; + let sign = if self.sign { "-" } else { "" }; + format!( + "{sign}{}", + match (integer_part.is_empty(), fraction_part.is_empty()) { + (true, true) => String::from("0"), + (true, false) => format!("0.{}", fraction_part), + (false, true) => integer_part, + (false, false) => format!("{}.{}", integer_part, fraction_part), + } + ) + } +} + +impl PrimitiveDecimal { + pub fn first_digit(&self) -> isize { + self.digits + .iter() + .position(|ch| ch != &'0') + .map(|pos| self.zero.wrapping_sub_unsigned(pos)) + .unwrap_or(isize::MIN) + } + + pub fn get_digit(&self, digit: isize) -> char { + let index = self.zero.wrapping_sub(digit); + (index >= 0) + .then_some(self.digits.iter().nth(index as usize).cloned()) + .flatten() + .unwrap_or('0') + } + + fn set_digit(&mut self, digit: isize, value: char) { + #[cfg(test)] + assert!( + matches!(value, '0'..='9'), + "New digit's value should be a valid decimal digit" + ); + + let index = self.zero.saturating_sub(digit); + let len = self.digits.len(); + if index < 0 { + let fillers: usize = (-index - 1).try_into().expect("Why.?"); + for _ in 0..fillers { + self.digits.insert(0, '0'); + } + self.zero -= index; + self.digits.insert(0, value); + } else { + let index = index as usize; + if index >= len { + self.digits + .extend(repeat_n('0', index - len).chain(std::iter::once(value))); + } else { + // It's ok to just set the digit, lol + self.digits[index] = value; + } + } + } + + pub fn truncate_zeroes(&mut self) { + if let Some(first_nonzero) = self.digits.iter().position(|ch| ch != &'0') { + // Move zero pointer + self.zero = self.zero.wrapping_sub_unsigned(first_nonzero); + // Remove leading zeroes + let _ = self.digits.drain(..first_nonzero); + // Remove trailing zeroes + let last_nonzero = self + .digits + .iter() + .rposition(|ch| ch != &'0') + .expect("There are non-zero digits not, for sure"); + if last_nonzero != self.digits.len() - 1 { + let _ = self.digits.drain((last_nonzero + 1)..); + } + } else { + self.digits.clear(); + self.zero = 0; + } + } + + pub fn apply_exponent(&mut self, exponent: isize) { + self.zero += exponent; + } + + pub fn round_to_digit(&mut self, digit: isize) { + self.apply_exponent(-digit); + + // Round the way fractional part turns into zero + if matches!(self.get_digit(-1), '5'..='9') { + // Must perform rounding, I guess + // While current digit is 9, set it to 0, and go up even more + let mut tgt = 0; + while self.get_digit(tgt) == '9' { + self.set_digit(tgt, '0'); + tgt += 1; + } + // Once digit is not 9, go up by one + self.set_digit( + tgt, + match self.get_digit(tgt) { + '0' => '1', + '1' => '2', + '2' => '3', + '3' => '4', + '4' => '5', + '5' => '6', + '6' => '7', + '7' => '8', + '8' => '9', + _ => unreachable!(), + }, + ) + } + + // Lastly, discard the fractional part + if self.zero > -2 { + // fractional part is not the entire value + if self.zero + 2 <= self.digits.len().try_into().expect("Wha...?") { + // ... and is actually present in the chars array + let _ = self.digits.drain(((self.zero + 1) as usize)..); + } + } else { + // fractional part is the only thing left, discard the entire value + self.digits.clear(); + self.zero = 0; + } + + // .. and extend chars to the last rounding digit, if rounding is performed up to a fractional part digit + if digit < 0 { + self.set_digit(0, self.get_digit(0)); // yeah, this does the trick + } + + self.apply_exponent(digit); + } + + pub fn unsign(&mut self) { + self.sign = false; + } +} + +#[cfg(test)] +mod primitive_decimal_tests { + mod parsing { + use super::super::*; + + macro_rules! test { + {$name:ident, $input:literal, $digits:literal, $zero:literal} => { + test!{$name, $input, $digits, $zero, +} + }; + {$name:ident, $input:literal, $digits:literal, $zero:literal, +} => { + test!{$name, $input, $digits, $zero, false} + }; + {$name:ident, $input:literal, $digits:literal, $zero:literal, -} => { + test!{$name, $input, $digits, $zero, true} + }; + {$name:ident, $input:literal, $digits:literal, $zero:literal, $sign:literal} => { + #[test] + fn $name() { + // arrange + let expected_digits = $digits.chars().collect_vec(); + + // act + let decimal: PrimitiveDecimal = $input.parse().expect("Should be able to parse valid float"); + + // assert + let PrimitiveDecimal { digits, zero, sign } = decimal; + assert_eq!(digits, expected_digits, "Digit vectors should be the same"); + assert_eq!(zero, $zero, "Zero positions should be the same"); + assert_eq!(sign, $sign, "Signs should be the same"); + } + }; + } + + test! {zero_pos1, "0.0", "00", 0} + test! {zero_pos2, "0.", "0", 0} + test! {zero_pos3, ".000000", "000000", -1} + test! {zero_pos4, "000000000", "000000000", 8} + test! {zero_pos5, ".0", "0", -1} + test! {zero_pos6, ".0", "0", -1} + test! {zero_pos7, ".0", "0", -1} + test! {zero_pos8, ".0", "0", -1} + test! {sign1, "+1.0000", "10000", 0, +} + test! {sign2, "-000.000", "000000", 2, -} + test! {digits1, ".123456789", "123456789", -1} + test! {digits2, "1.23456789", "123456789", 0} + test! {digits3, "123456.789", "123456789", 5} + test! {digits4, "123456789000", "123456789000", 11} + test! {exponent1, "0e0", "0", 0} + test! {exponent2, "0E0", "0", 0} + test! {exponent3, "1E1", "1", 1} + test! {exponent4, "1E-1", "1", -1} + test! {exponent5, "1E-3", "1", -3} + test! {exponent6, "-0001.001E-1", "0001001", 2, -} + test! {exponent7, "-0001.001E10", "0001001", 13, -} + } + + mod to_string { + use super::super::*; + + macro_rules! test_noop { + {$name:ident, $input:literal, $output:literal} => { + #[test] + fn $name() { + // arrange + let decimal: PrimitiveDecimal = $input.parse().expect("Should be able to parse"); + + // act + let output = decimal.to_string(); + + // assert + assert_eq!(output, $output, "Should produce expected result"); + } + }; + } + + test_noop! {sign1, "0.0", "0.0"} + test_noop! {sign2, "-0.0", "-0.0"} + test_noop! {missing_decimal1, ".0", "0.0"} + test_noop! {missing_decimal2, ".1234", "0.1234"} + test_noop! {missing_decimal3, "1E-3", "0.001"} + test_noop! {missing_fraction1, "12345", "12345"} + test_noop! {missing_fraction2, "-0123", "-0123"} + test_noop! {missing_fraction3, "0.1E2", "010"} + test_noop! {missing_fraction4, "0.1E5", "010000"} + + macro_rules! test_truncate { + {$name:ident, $input:literal, $output:literal} => { + #[test] + fn $name() { + // arrange + let mut decimal: PrimitiveDecimal = $input.parse().expect("Should be able to parse"); + decimal.truncate_zeroes(); + + // act + let output = decimal.to_string(); + + // assert + assert_eq!(output, $output, "Should produce expected result"); + } + }; + } + + test_truncate! {zero1, "0", "0"} + test_truncate! {zero2, "0.0", "0"} + test_truncate! {zero3, ".0", "0"} + test_truncate! {zero4, "0.0e-100", "0"} + test_truncate! {zero5, ".00000000000E50", "0"} + test_truncate! {fraction1, "1.00000000", "1"} + test_truncate! {fraction2, "123.1234000", "123.1234"} + test_truncate! {fraction3, "-12.3400000000", "-12.34"} + test_truncate! {fraction4, "-1.0", "-1"} + test_truncate! {fraction5, "2.30", "2.3"} + test_truncate! {fraction6, "3.0E1", "30"} + test_truncate! {fraction7, "123.450000E2", "12345"} + test_truncate! {fraction8, "-0.3400E1", "-3.4"} + test_truncate! {integer1, "000012", "12"} + test_truncate! {integer2, "012", "12"} + test_truncate! {integer3, "-00123.45E3", "-123450"} + test_truncate! {integer4, "001234.5E-2", "12.345"} + test_truncate! {trim1, "01234.5670000", "1234.567"} + test_truncate! {trim2, "-0001.0000", "-1"} + test_truncate! {trim3, "-000.000", "-0"} + } + + mod first_digit { + use super::super::*; + + macro_rules! test { + {$name:ident, $input:literal, $first_digit:expr} => { + #[test] + fn $name() { + // arrange + let mut decimal: PrimitiveDecimal = $input.parse().expect("Should be able to parse"); + decimal.truncate_zeroes(); + + // act + let output = decimal.first_digit(); + + // assert + assert_eq!(output, $first_digit, "Should produce expected result"); + } + }; + } + + test! {zero1, "0", isize::MIN} + test! {zero2, "-0.00", isize::MIN} + test! {zero3, "000.0000", isize::MIN} + test! {one1, "-00001.00000", 0} + test! {one2, "00010.00000E-1", 0} + test! {one3, "-0.0001e4", 0} + test! {test1, "123.000", 2} + test! {test2, "0000123.30953", 2} + test! {test3, "0000123.30953E4", 6} + test! {test4, "0000123.30953E-10", -8} + test! {test5, "0.1", -1} + test! {test6, "0000000.0002e-0", -4} + } + + mod get_digit { + use super::super::*; + + macro_rules! test { + {$name:ident, $input:literal, $range:expr, $output:literal} => { + #[test] + fn $name() { + // arrange + let decimal: PrimitiveDecimal = $input.parse().expect("Should be able to parse"); + + // act + let res = ($range).rev().into_iter().map(|i| decimal.get_digit(i)); + + // assert + assert_eq!(res.collect_vec(), $output.chars().collect_vec(), "Should produce expected output") + } + }; + } + + test! {test1, "1.01", -3..=1, "01010"} + test! {test2, "-00012345.0001100", -7..=7, "000123450001100"} + test! {test3, "-00012345.0001100e2", -7..=7, "012345000110000"} + } + + mod set_digit { + use super::super::*; + + macro_rules! test { + {$name:ident, $input:literal, $digit:literal, $value:literal, $output:literal} => { + #[test] + fn $name() { + // arrange + let mut decimal: PrimitiveDecimal = $input.parse().expect("Should be able to parse"); + + // act + decimal.set_digit($digit, $value); + let output = decimal.to_string(); + + // assert + assert_eq!(output, $output, "Should produce expected output") + } + }; + } + + test! {test1, "0.0", 0, '1', "1.0"} + test! {test2, "0.0", 10, '1', "10000000000.0"} + test! {test3, "-100", 2, '2', "-200"} + test! {test4, "1", -1, '1', "1.1"} + test! {test5, "1e4", -10, '1', "10000.0000000001"} + test! {test6, "1e-5", 4, '2', "20000.00001"} + test! {test7, "-6543210.023456", -1, '1', "-6543210.123456"} + } + + mod round { + use std::ops::RangeInclusive; + + use rand::{random, seq::IteratorRandom}; + + use super::super::*; + + macro_rules! test { + {$name:ident, $input:literal, $digit:literal, $output:literal} => { + #[test] + fn $name() { + // arrange + let mut decimal: PrimitiveDecimal = $input.parse().expect("Should be able to parse"); + + // act + decimal.round_to_digit($digit); + let output = decimal.to_string(); + + // assert + assert_eq!(output, $output, "Should produce expected output") + } + }; + } + + test! {zero1, "0", 0, "0"} + test! {zero2, "-0", 0, "-0"} + test! {zero3, "00.0000", 0, "00"} + test! {zero4, "0", 1, "0"} + test! {zero5, "0e-5", 0, "0"} + test! {test1, "1.1", 0, "1"} + test! {test2, "1.5", 0, "2"} + test! {test3, "9.5", 0, "10"} + test! {test4, "9999.5", 0, "10000"} + test! {test5, "8999.5", 0, "9000"} + test! {test6, "1.23", -2, "1.23"} + test! {test7, "1.23", -3, "1.230"} + test! {test8, "-1.23", -5, "-1.23000"} + test! {test9, "1.23999", -4, "1.2400"} + + #[test] + fn fuzz() { + const TOTAL_TESTS: usize = 1_000_000; + const MAX_ABS: f64 = 1E6f64; + const EXP_PROB: f64 = 0.2f64; + const DIGIT_RANGE: RangeInclusive = -10..=10; + let mut dev_sum = 0.0; + for _ in 0..TOTAL_TESTS { + // arrange + let number: f64 = (random::() - 0.5f64) * 2.0f64 * MAX_ABS; + let string_repr = if random::() < EXP_PROB { + format!("{number:e}") + } else { + number.to_string() + }; + let mut decimal: PrimitiveDecimal = string_repr.parse().expect( + "Should be able to parse, as this string was just generated by Rust, lol", + ); + let digit = DIGIT_RANGE + .choose(&mut rand::thread_rng()) + .expect("Range is not empty"); + + // act + decimal.round_to_digit(digit); + + // assert + let rounded: f64 = decimal + .to_string() + .parse() + .expect("Should be able to parse, as decimal value generates correct strings"); + // after correct rounding, result must differ by no more than value of a corresponding digit + let diff = (number - rounded).abs(); + let ok_diff = 10.0f64.powi(digit.try_into().expect("Huh.")); + assert!(diff <= ok_diff, "Difference is larger than expected for number = {number} rounding to digit = {digit}. Rounding result: {rounded}, ok difference: {ok_diff}, actual difference: {diff}"); + dev_sum += diff / ok_diff; + } + println!("Average dev: {:.4}%", dev_sum / (TOTAL_TESTS as f64)); + } + } +} + +pub fn value_error_format<'v, 'e>( + value: impl Into>, + error: impl Into>, +) -> Result, ParsingError> { + let (value, error) = value_error_format_inner(value.into().as_ref(), error.into().as_ref())?; + // Result is a combination of text tokens with mathmode +- in between + Ok(Token::Text(Tokens::new([ + Token::Paragraph(crate::data::Font::Normal, value.into()), + Token::InlineMathmode(r"\pm".into()), + Token::Paragraph(crate::data::Font::Normal, error.into()), + ]))) +} + +fn value_error_format_inner<'v, 'e>( + value: &'v str, + error: &'e str, +) -> Result<(String, String), ParsingError> { + // first, parse values into primitive decimals, for easier manipulation + let mut value: PrimitiveDecimal = value.parse()?; + let mut error: PrimitiveDecimal = error.parse()?; + + // Trim zeroes (just in case) + value.truncate_zeroes(); + error.truncate_zeroes(); + + // Get error's first non-zero digit + let mut round_digit = error.first_digit(); + if matches!(error.get_digit(round_digit), '1'..='2') { + // If first digit's value is 1-3, round to one more digit behind it + round_digit -= 1; + } + + // Lastly, round value and error to the same digit + value.round_to_digit(round_digit); + error.round_to_digit(round_digit); + + Ok((value.to_string(), error.to_string())) +} + +#[cfg(test)] +mod value_error_tests { + use super::*; + macro_rules! test { + {$name:ident, [$value:literal, $error:literal] => [$output_value:literal, $output_error:literal]} => { + #[test] + fn $name() { + // act + let (output_value, output_error) = value_error_format_inner($value, $error).expect("Should be able to parse"); + + // assert + assert_eq!(output_value, $output_value, "Values should be equal"); + assert_eq!(output_error, $output_error, "Errors should be equal"); + } + }; + } + + // These are examples from docs + test! {doc1, ["5", "0.1"] => ["5.00", "0.10"]} + test! {doc2, ["12252", "54"] => ["12250", "50"]} + test! {doc3, ["1232", "12"] => ["1232", "12"]} + + // These are examples from place I actually get to know about this formatting (these are hidden in the code anyway, so I guess credit is optional here) + test! {src1, ["77.567439", "4.8561133"] => ["78", "5"]} + test! {src2, ["1.3354792", "0.02417797"] => ["1.335", "0.024"]} + test! {src3, ["157863.44", "377.36711"] => ["157900", "400"]} + test! {src4, ["0.1437765", "0.019734"] => ["0.144", "0.020"]} +} + +fn find_args<'l>(input: &'l str) -> impl IntoIterator { + // I sincerely tried to use regex here, but apparently, I'm too dumb to actually figure this out + // my final guess: r#"\s*([0-9A-Za-z_]+)\s*=\s*((["'].*[^\\]["'])|([^["=,\s\[\]]][^[",\s\[\]]]*)|(\[.*\]))"# + // Here is a state machine instead: + // States: + // - (1) Normal (start state, final state) + // - (2) Inside name + // - (3) Before equals + // - (4) After equals + // - (5) Inside value(quotes) + // - (6) Before comma (final state) + // Transitions: + // (1) + ' ' -> (1) + // (1) + [a-zA-Z] -> (2) + // (2) + [a-zA-z0-9] -> (2) + // (2) + ' ' -> (3) + // (2) + '=' -> (4) + // (3) + ' ' -> (3) + // (3) + '=' -> (4) + // (4) + ' ' -> (4) + // (4) + ^' ' -> (5)() + // (5)(q) + "\"" -> (5)(q) + // (5)(q) + "\'" -> (5)(q) + // (5)() + ["'] -> (5)(["']) + // (5)(["']) + ["'] -> (5)() + // (5)(["']) + ['"] -> (5)(["']) + // (5)() + ' '|',' -> (6) + // (6) + ' ' -> (6) + // (6) + ',' -> (1) + enum State<'l> { + Initial, // (1) + InsideName, // (2) + BeforeEquals { + name: &'l str, + }, // (3) + AfterEquals { + name: &'l str, + }, // (4) + InsideValue { + name: &'l str, + value_rest: &'l str, + len: usize, + quote: Option, + }, // (5) + BeforeComa, // (6) + } + #[derive(Debug, PartialEq)] + enum Quote { + Single, + Double, + } + fn inner<'l>( + state: State<'l>, + rest: &'l str, + mut result: InlinedStackAccessor<(&'l str, &'l str)>, + ) -> bool { + match state { + State::Initial => { + // (1) -> fin + let Some(first_char) = rest.find(|c| c != ' ') else { + // fin + return true; + }; + // (1) + ' ' -> (1) is done through finding first non-empty char + let inner_rest = &rest[first_char..]; + // (1) + [a-zA-z] -> (2) + if inner_rest.starts_with(|c| matches!(c, 'A'..='Z' | 'a'..='z')) { + return inner(State::InsideName, inner_rest, result); + } + } + State::InsideName => { + // (2) + [A-Za-z0-9] -> (2) + let Some(after_name) = + rest.find(|c| !matches!(c, 'A'..='Z' | 'a'..='z' | '0'..='9')) + else { + // fail + return false; + }; + let name = &rest[..after_name]; + let inner_rest = &rest[after_name..]; + // (2) + ' ' -> (3) + if inner_rest.starts_with(' ') { + return inner(State::BeforeEquals { name }, inner_rest, result); + } + // (2) + '=' -> (4) + if inner_rest.starts_with('=') { + return inner(State::AfterEquals { name }, &inner_rest[1..], result); + } + } + State::BeforeEquals { name } => { + // (3) + ' ' -> (3) + let Some(equals) = rest.find(|c| c != ' ') else { + // fail + return false; + }; + let inner_rest = &rest[equals..]; + + // (3) + '=' -> (4) + if inner_rest.starts_with('=') { + let inner_after_equals = &inner_rest[1..]; + return inner(State::AfterEquals { name }, inner_after_equals, result); + } + } + State::AfterEquals { name } => { + // (4) + ' ' -> (4) + let Some(first_char) = rest.find(|c| c != ' ') else { + // fail + return false; + }; + let inner_rest = &rest[first_char..]; + // (4) + ["'] -> (5)(["']) + return inner( + State::InsideValue { + name, + value_rest: inner_rest, + len: 0, + quote: None, + }, + inner_rest, + result, + ); + } + State::InsideValue { + name, + value_rest, + len, + quote, + } => { + // Here lies the whole complexity + // Basically, there's no actual way to know, if value had finished, so backtracking is the only option + // Fortunately, I assume one thing about values: they must have exited all of their quotes + // This assumption is based on python syntax -- there would indeed be no way to insert something without quotes. + // I HOPE that this is enough, and would appreciate any advice here. + + // (5)(q) + "\"" -> (5)(q) + // (5)(q) + "\'" -> (5)(q) + if rest.starts_with(r#"\""#) | rest.starts_with(r#"\'"#) { + const OFFSET: usize = r#"\""#.len(); + return inner( + State::InsideValue { + name, + value_rest, + len: len + OFFSET, + quote, + }, + &rest[OFFSET..], + result, + ); + } + + fn beginning_quote(s: &str) -> Option { + match s.chars().next() { + Some('\'') => Some(Quote::Single), + Some('\"') => Some(Quote::Double), + Some(_) => None, + None => None, + } + } + + if let Some(some_quote) = quote.as_ref() { + if let Some(rest_quote) = beginning_quote(rest) { + const OFFSET: usize = "\'".len(); + // (5)(["']) + ["'] -> (5)() + if some_quote == &rest_quote { + return inner( + State::InsideValue { + name, + value_rest, + len: len + OFFSET, + quote: None, // quote closed + }, + &rest[1..], + result, + ); + } + // (5)(["']) + ['"] -> (5)(["']) + return inner( + State::InsideValue { + name, + value_rest, + len: len + OFFSET, + quote, // quote unchanged + }, + &rest[1..], + result, + ); + } + // no action required in this case, one character would be consumed anyway + } else { + // no quote + // (5)() + ' ' -> (6) + if rest.starts_with([' ', ',']) || rest.is_empty() { + // the most tricky part here + let value = &value_rest[..len]; + result.push((name, value)); // push this name-value guess to the result + let inner_succeeded = inner(State::BeforeComa, rest, result.accessor()); + if inner_succeeded { + // found a match inside! + return true; + } + // it's too early to break out of the value, then + result.revert(); // revert the changes + + // this block is an optional match + } + + // (5)() + ["'] -> (5)(["']) + if let Some(quote) = beginning_quote(rest) { + const OFFSET: usize = "\"".len(); + return inner( + State::InsideValue { + name, + value_rest, + len: len + OFFSET, + quote: Some(quote), + }, + &rest[OFFSET..], + result, + ); + } + } + + // this state is not fin + if rest.is_empty() { + // reached the end, yet value has not ended + // fail + return false; + } + if rest.starts_with(|c| len > 0 || c != '=') { + let next_char = value_rest + .char_indices() + .nth(1) // I need an index of a 2nd character here + .map(|(i, _)| i) + .unwrap_or(value_rest.len()); // If there's no 2nd character, inner_rest should be empty + let inner_rest = &rest[next_char..]; + return inner( + State::InsideValue { + name, + value_rest, + len: len + next_char, + quote, + }, + inner_rest, + result, + ); + } + } + State::BeforeComa => { + // (6) + ' ' -> (6) + let Some(first_char) = rest.find(|c| c != ' ') else { + // fin + return true; + }; + let rest_inner = &rest[first_char..]; + // (6) + ',' -> (1) + if rest_inner.starts_with(',') { + return inner(State::Initial, &rest_inner[1..], result); + } + } + } + false + } + let mut result = Vec::new(); + let mut result_stack = InlinedStack::new(&mut result); + let succeeded = inner(State::Initial, input, result_stack.accessor()); + if !succeeded { + let _ = result.drain(..); + } + result.into_iter() +} + +#[cfg(test)] +mod find_args_tests { + use super::*; + macro_rules! test { + {$name:ident, $input:literal, [$(($output_name:literal, $output_value:literal)), *]} => { + #[test] + fn $name() { + // arrange + + // act + let output = find_args($input).into_iter().collect_vec(); + + // assert + assert_eq!(output, [$(($output_name, $output_value)), *], "Should produce expected result"); + } + }; + } + + test! {empty1, "", []} + test! {empty2, " \n \n", []} + test! {empty3, "=", []} + test! {empty4, r#"===""""#, []} + test! {empty5, r#" src == "man""#, []} + test! {single1, r#"x=1"#, [("x", "1")]} + test! {single2, r#" x =1 "#, [("x", "1")]} + test! {single3, r#" src = "path/to/th.png" "#, [("src", "\"path/to/th.png\"")]} + test! {single4, r#" src = "path/to /th.png" "#, [("src", "\"path/to /th.png\"")]} + test! {single5, r#"caption = "Some really cool text, expected to handle that comma too" "#, [("caption", "\"Some really cool text, expected to handle that comma too\"")]} + test! {double1, r#"src = "pat h/to/th.png", rows=50 "#, [("src", "\"pat h/to/th.png\""), ("rows", "50")]} + test! {quote1, r#"caption = "Text using \"quotes\" should be handled too" "#, [("caption", r#""Text using \"quotes\" should be handled too""#)]} + test! {quote2, r#"caption = "There can be a lot of stuff in caption, like a casual ref: [@fig:lol], or equation: $(x+1)^2=x^2+2x+1$ = lunar death of landlord Grim, ha-ha" "#, + [("caption", r#""There can be a lot of stuff in caption, like a casual ref: [@fig:lol], or equation: $(x+1)^2=x^2+2x+1$ = lunar death of landlord Grim, ha-ha""#)]} + + // These two are not even going to happen, lol + test! {doc1, r#""very important info, that definitely couldn't be inserted into a document statically""#, []} + test! {doc2, r#"4.54535, 0.2342"#, []} + test! {doc3, r#"src="line.png", ident="line", caption="Some caption for a line drawing. This is parsed as a regular document text, so you may [@eq:line] and $f(x) = A \cdot x^{1} + B \cdot x^{0}$ here, for example""#, + [("src", "\"line.png\""), ("ident", "\"line\""), ("caption", r#""Some caption for a line drawing. This is parsed as a regular document text, so you may [@eq:line] and $f(x) = A \cdot x^{1} + B \cdot x^{0}$ here, for example""#)]} + test! {doc4, r#"src="file.csv", rows=1..100, columns=["type", ("a", "a_err")]"#, + [("src", "\"file.csv\""), ("rows", "1..100"), ("columns", r#"["type", ("a", "a_err")]"#)]} + + test! {idk1, "lines=5, rows = 5..100, a=\"1\"", + [("lines", "5"), ("rows", "5..100"), ("a", "\"1\"")]} + test! {idk2, "src=\"some/path/idk.csv\", rows=5..=100", [("src", "\"some/path/idk.csv\""), ("rows", "5..=100")]} + test! {idk3, "src=\"some/path/idk.csv\", columns = []", [("src", "\"some/path/idk.csv\""), ("columns", "[]")]} + + test! {edge1, r#"src = 'path/to/image.jpg', caption = 'On this figure you can see "literally nothing"', ident = "meow""#, + [("src", "\'path/to/image.jpg\'"), ("caption", r#"'On this figure you can see "literally nothing"'"#), ("ident", "\"meow\"")]} +} + +fn parse_columns<'l>(input: &'l str) -> Vec> { + use regex::Captures; + const REGEX_STR: &str = r#"("([^"]+)"|\("([^"])",\s*"([^"]+)"\))"#; + static REGEX: Lazy = Lazy::new(|| Regex::new(REGEX_STR).expect("Bad regex syntax")); + REGEX + .captures_iter(input) + .map(|cap: Captures<'l>| { + // dbg!(&cap); + match (cap.get(2), cap.get(3), cap.get(4)) { + (Some(col), _, _) => CsvTableColumn::Single(col.as_str().into()), + (_, Some(value), Some(error)) => { + CsvTableColumn::ValueError(value.as_str(), error.as_str()) + } + _ => unreachable!(), + } + }) + .collect_vec() +} + +#[cfg(test)] +mod parse_columns_tests { + use super::*; + macro_rules! c { + ($col:literal) => { + CsvTableColumn::Single($col) + }; + } + macro_rules! ve { + ($value:literal, $error:literal) => { + CsvTableColumn::ValueError($value, $error) + }; + } + + macro_rules! test { + {$name:ident, $input:literal, [$($out:expr), *]} => { + #[test] + fn $name() { + // arrange + + // act + let output = parse_columns($input); + + // assert + assert_eq!(output, vec![$($out), *], "Should produce expected result"); + } + }; + } + + test! {empty, r#""#, []} + test! {single, r#""a""#, [c!("a")]} + test! {value_error, r#""a", ("b", "c")"#, [c!("a"), ve!("b", "c")]} + test! {master, r#""no", ("x", "x_err"), "y", ("z", "z_err")"#, [c!("no"), ve!("x", "x_err"), c!("y"), ve!("z", "z_err")]} + test! {doc1, r#""type",("a", "a_err")"#, [c!("type"), ve!("a", "a_err")]} +} diff --git a/src/gen/ayano/python.rs b/src/gen/ayano/python.rs new file mode 100644 index 0000000..881815b --- /dev/null +++ b/src/gen/ayano/python.rs @@ -0,0 +1,134 @@ +use pyo3::prelude::*; +use rand::random; + +static MODULE_NAME: &'static str = "ayano"; + +#[derive(Debug)] +pub struct InitToken(pub(self) String); + +impl InitToken { + fn new() -> Self { + InitToken(format!("{}_{}", MODULE_NAME, random::())) + } + fn module_name(&self) -> &str { + &self.0 + } + + fn module<'py>(&self, py: Python<'py>) -> Result<&'py PyModule, PyErr> { + PyModule::import(py, self.module_name()) + } +} + +pub fn init(code: &str) -> Result { + Python::with_gil(move |py| { + let token = InitToken::new(); + PyModule::from_code(py, code, "", token.module_name())?; + Ok(token) + }) +} + +pub fn append_static(target: &mut String, code: &str) { + for line in code.lines() { + target.push_str(format!("\n{line}").as_str()) + } +} + +pub fn append_function(target: &mut String, name: &str, code: &str) { + // First, declare a function + target.push_str(format!("\ndef {name}():").as_str()); + for line in code.lines() { + target.push_str(format!("\n {line}").as_str()) + } +} + +pub fn call_function( + token: &InitToken, + name: &str, + transform: Transform, +) -> Result +where + Err: From, + for<'py> Transform: FnOnce(&'py PyAny, Python<'py>) -> Result, +{ + Python::with_gil(|py| { + let main_module = token.module(py)?; + let function = main_module.getattr(name)?; + let res = function.call((), None)?; + Ok(transform(res, py)?) + }) +} + +#[cfg(test)] +mod tests { + use super::{call_function, init}; + + macro_rules! init_functions { + ($code:expr, [$(($fun_name:expr, $fun_code:expr)), *]) => {{ + let mut code = $code.to_string(); + for (fun_name, fun_code) in [$(($fun_name, $fun_code)), *] { + super::append_function(&mut code, fun_name, fun_code); + } + init(code.as_str()) + }}; + } + + #[test] + fn create_empty_should_ok() { + // arrange + let code = ""; + + // act + assert + let _token = init_functions!(code, []).expect("Should be able to create empty module"); + } + + #[test] + fn create_valid_should_ok() { + // arrange + let code = "x = 5\ny = x ** 2\nz = y / 10.0"; + + // act + assert + let _token = + init_functions!(code, []).expect("Should be able to create module with valid code"); + } + + #[test] + fn create_invalid_should_err() { + // arrange + let code = "x = 5;\ny = **5;\nfn a() {{\n return &'_ 0;\n}}"; + + // act + assert + let _err = init_functions!(code, []).expect_err("Should deny bad code"); + } + + #[test] + fn function_absent_should_err() { + // arrange + let code = "x = 5"; + let token = init_functions!(code, []).expect("Should be able to create module"); + + // act + assert + let _res = call_function(&token, "absent_function", |res, _| { + Ok::<_, super::PyErr>(res.str()?.to_string()) + }) + .expect_err("Should get error for non-existent function call"); + } + + #[test] + fn function_present_should_ok() { + // arrange + let code = "x = 5"; + let token = init_functions!(code, [("ok_function", "return x")]) + .expect("Should be able to create module"); + + // act + let res = call_function(&token, "ok_function", |res, _| { + Ok::<_, super::PyErr>(res.str()?.to_string()) + }); + + // assert + assert_eq!( + res.expect("Should be able to execute existing function"), + "5" + ); + } +} diff --git a/src/gen/ayano/test_code.py b/src/gen/ayano/test_code.py new file mode 100644 index 0000000..bdc5230 --- /dev/null +++ b/src/gen/ayano/test_code.py @@ -0,0 +1,5 @@ +x = 5 +y = x ** 10 +# TOSHINO KYOKO! +x, y = y, x +print(x, y) \ No newline at end of file diff --git a/src/gen/ayano/test_table.csv b/src/gen/ayano/test_table.csv new file mode 100644 index 0000000..d9d6370 --- /dev/null +++ b/src/gen/ayano/test_table.csv @@ -0,0 +1,9 @@ +a, a_err, b, c, d, d_err +1.0, 0.001, 3, 4, -5, 0.001 +1.0, 0.001, 3, 4, -5, 0.001 +1.0, 0.001, 3, 4, -5, 0.001 +1.0, 0.001, 3, 4, -5, 0.001 +1.0, 0.001, 3, 4, -5, 0.001 +1.0, 0.001, 3, 4, -5, 0.001 +1.0, 0.001, 3, 4, -5, 0.001 +1.0, 0.001, 3, 4, -5, 0.001 \ No newline at end of file diff --git a/src/gen/lab/context.rs b/src/gen/lab/context.rs new file mode 100644 index 0000000..216a58a --- /dev/null +++ b/src/gen/lab/context.rs @@ -0,0 +1,6 @@ +use std::collections::HashMap; + +#[derive(Debug, Default)] +pub struct Context { + pub encountered_footnotes: HashMap, +} diff --git a/src/gen/lab/lab_preamble.tex b/src/gen/lab/lab_preamble.tex new file mode 100644 index 0000000..8c9f433 --- /dev/null +++ b/src/gen/lab/lab_preamble.tex @@ -0,0 +1,112 @@ +\documentclass[a4paper]{article} +\usepackage{graphicx, hyperref, xcolor, gensymb, amssymb, mathtools, wrapfig, mathtools, microtype, lastpage, caption, titlesec, paracol, longtable, booktabs, cancel, soul} +\usepackage[T2A, T1]{fontenc} +\usepackage[utf8]{inputenc} +\usepackage[russian, ukrainian]{babel} +\hypersetup{ + colorlinks, + linkcolor={blue!20!black}, + citecolor={blue!50!black}, + urlcolor={blue!80!black} +} + +\usepackage[left = 25mm, right = 10mm, top=20mm, bottom=20mm, bindingoffset=0cm]{geometry} + +% КОМАНДИ + +\newcommand{\fnt}{ +\fontshape{n} +\fontsize{14pt} {19pt} +\linespread{0.8} +\selectfont +} % нормативний шрифт +\let\oldCaption\caption +\renewcommand{\caption}[1]{ +\oldCaption{\fnt #1} +} % підпис нормативним шрифтом. + +\newcommand{\tb}{ + \hspace*{10mm} +} % відступ у 5 символів "x" відповідно до ДСТУ +\newcommand{\tbln}{ + \newline + \hspace*{10mm} +} % те ж саме, але з переносом рядка +\newcommand{\tbsp}{ + \vspace*{1ex} + \newline + \hspace*{10mm} +} % те ж саме, але з додатковим відступом між рядками + +\iffalse +\let\oldSection\section +\renewcommand{\section}[1]{ + \oldSection*{\tb #1} + \addcontentsline{toc}{section}{#1} + \fnt + \tb +} % відображає у тексті та змісті наданий заголовок +\fi + +\iftrue +\let\oldSubSection\subsection +\renewcommand{\subsection}[1]{ + \oldSubSection*{\tb #1} + \addcontentsline{toc}{subsection}{#1} + \fnt + \tb +} % відображає у тексті та змісті наданий підзаголовок +\fi + +% бібліографія +\usepackage[square,sort,comma,numbers]{natbib} +\renewcommand{\bibsection}{} +\usepackage{totcount} +\newtotcounter{citnum} +% лічильник цитувань +\def\oldbibitem{} \let\oldbibitem=\bibitem +\def\bibitem{\stepcounter{citnum}\oldbibitem} + +% лічильник зображень +\newtotcounter{graphnum} +\def\oldincludegraphics{} \let\oldincludegraphics=\includegraphics +\def\includegraphics{\stepcounter{graphnum}\oldincludegraphics} + +% лічильник таблиць (НЕАВТОМАТИЧНИЙ!!!) +\newtotcounter{tabnum} + +% лічильник додатків (НЕАВТОМАТИЧНИЙ!!!) +\newtotcounter{dodnum} + +% нумерація сторінок +\usepackage{fancyhdr} +\pagestyle{fancy} +\fancyhf{} +\renewcommand{\headrulewidth}{0pt} +\setlength{\headheight}{15.3pt} +\fancyhead[R]{\fnt \thepage} + +% виноски +\renewcommand{\thefootnote}{\large\arabic{footnote})~} +\renewcommand{\footnoterule}{\rule{20mm}{0.4pt} \vspace*{0.5ex}} +% \newcommand{\vyn}[2]{ +% \footnote[#1]{\large #2} +% } % створити виноску + +\let\oldFootnote\footnote +\renewcommand{\footnote}[1]{ + \oldFootnote{\large #1} +} % перегрузка виноски + +\providecommand{\tightlist}{% + \setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}} + +% підписи малюнків/таблиць +\captionsetup[figure]{name={\fnt Рисунок~},labelsep=period} +\captionsetup[table]{name={\fnt Таблиця~},labelsep=period} + +% idk why, but this redefinition does not work, for some reason +% \let\oldCaption\caption +% \renewcommand{\caption}[1] { +% \oldCaption{\fnt #1} +% } \ No newline at end of file diff --git a/src/gen/lab/meta.rs b/src/gen/lab/meta.rs new file mode 100644 index 0000000..e9851b5 --- /dev/null +++ b/src/gen/lab/meta.rs @@ -0,0 +1,127 @@ +use std::{ + borrow::BorrowMut, + collections::{HashMap, HashSet}, +}; + +use itertools::Itertools; +use pyo3::PyErr; + +use crate::data::{Token, Tokens}; + +use super::super::ayano::{self, AyanoBuilder, AyanoExecutor}; + +#[derive(Debug)] +pub enum MetaError { + AyanoSyntax(ayano::SyntaxError), +} + +#[derive(Debug, Default)] +pub struct SourceMeta<'source, Ayano> { + pub footnotes: HashMap<&'source str, Tokens<'source>>, + pub refs: HashSet<&'source str>, + pub ayano: Ayano, +} + +impl<'meta> SourceMeta<'meta, AyanoBuilder> { + fn empty() -> Self { + Self::default() + } + + pub fn collect<'source>(token: &'meta Token<'source>) -> Result + where + 'source: 'meta, + { + let mut res: SourceMeta<'meta, _> = SourceMeta::empty(); + collect_single(token, &mut res)?; + Ok(res) + } + + pub fn init_ayano(self) -> Result, PyErr> { + Ok(SourceMeta { + ayano: self.ayano.initialize()?, + footnotes: self.footnotes, + refs: self.refs, + }) + } +} + +fn collect_single<'meta, 'r, 'source>( + token: &'meta Token<'source>, + dest: &'r mut SourceMeta<'meta, AyanoBuilder>, +) -> Result<(), MetaError> +where + 'source: 'meta, + 'meta: 'r, +{ + match token { + Token::Equation { + ident: Some(ident), .. + } => { + dest.refs.borrow_mut().insert(ident); + } + Token::Table { + header, + ident, + cells, + caption, + } => { + if let Some(ident) = ident { + dest.refs.borrow_mut().insert(ident); + } + collect_iterable(header, dest)?; + collect_iterable(cells, dest)?; + if let Some(caption) = caption { + collect_iterable(caption, dest)?; + } + } + Token::Figure { + ident: Some(ident), + caption, + src_name: _src_name, + } => { + dest.refs.insert(ident); + if let Some(caption) = caption { + collect_iterable(caption, dest)?; + } + } + Token::Text(text) => { + collect_iterable(text, dest)?; + } + Token::FootNoteContent { content, ident } => { + collect_iterable(content, dest)?; + dest.footnotes.insert(ident, content.borrowed()); + } + Token::Ayano(block) => { + dest.ayano + .add_block(block) + .map_err(MetaError::AyanoSyntax)?; + } + Token::PageDiv + | Token::Header { .. } + | Token::Href { .. } + | Token::List { .. } + | Token::Formatted(_, _) + | Token::Paragraph(_, _) + | Token::InlineMathmode(_) + | Token::Reference(_) + | Token::FootNoteReference(_) + | Token::Figure { .. } + | Token::Equation { .. } + | Token::CodeBlock { .. } => {} + Token::Error(_) => {} + } + Ok(()) +} + +fn collect_iterable<'r, 'source: 'meta, 'meta: 'r, I>( + tokens: I, + dest: &'r mut SourceMeta<'meta, AyanoBuilder>, +) -> Result<(), MetaError> +where + I: IntoIterator>, +{ + tokens + .into_iter() + .map(|token| collect_single(token, dest)) + .try_collect() +} diff --git a/src/gen/lab/mod.rs b/src/gen/lab/mod.rs new file mode 100644 index 0000000..74e5eeb --- /dev/null +++ b/src/gen/lab/mod.rs @@ -0,0 +1,631 @@ +mod context; +mod meta; + +use itertools::Itertools; + +use crate::{data::Token, path_engine::PathEngine}; +use std::{borrow::Borrow, fmt::Write, marker::PhantomData}; + +use self::{context::Context, meta::SourceMeta}; + +use super::{ayano::AyanoExecutor, GenerationError, OutputGenerator, Res}; + +#[derive(Debug)] +pub struct LabaLatex<'source, PE> { + path_engine: PE, + _phantom: PhantomData<&'source ()>, +} + +impl<'source, PE: PathEngine> LabaLatex<'source, PE> { + fn new(path_engine: PE) -> Self { + Self { + path_engine, + _phantom: PhantomData, + } + } + + fn write_table_row<'meta, 'context, 'token: 'context, W: Write + ?Sized>( + &self, + output: &mut W, + meta: &'meta SourceMeta<'source, AyanoExecutor>, + context: &'context mut Context, + cells: impl IntoIterator>, + ) -> Res<'source> + where + 'source: 'meta + 'context + 'token, + { + let mut cells = cells.into_iter(); + let Some(first_cell) = cells.next() else { + return Ok(()); + }; + // first one must be written separately + self.write_to(output, meta, context, first_cell)?; + for cell in cells { + output.write_str(" & ")?; + self.write_to(output, meta, context, cell)?; + } + output.write_str("\\\\ \\hline")?; + Ok(()) + } +} + +impl<'source, PE: PathEngine> OutputGenerator<'source, SourceMeta<'source, AyanoExecutor>, Context> + for LabaLatex<'source, PE> +{ + fn write_to<'meta, 'context, 'token, W: Write + ?Sized>( + &self, + output: &mut W, + meta: &'meta SourceMeta<'source, AyanoExecutor>, + context: &'context mut Context, + token: &'token Token<'source>, + ) -> Res<'source> + where + 'source: 'meta + 'context + 'token, + 'token: 'context, + { + match token { + Token::PageDiv => { + output.write_str(r"\clearpage")?; + } + Token::Header { order, content } => { + let command = match order { + 0 => "section", + 1 => "subsection", + 2 => "subsubsection", + 3 => "paragraph", + 4 => "subparagraph", + _ => "textbf", + }; + + write!(output, "\\{command}{{{content}}}")?; + } + Token::Equation { content, ident } => { + let label = ident + .as_ref() + .map(|ident| format!(r"\label{{eq:{}}}", ident)) + .unwrap_or_else(String::new); + write!( + output, + r" + \begin{{equation}} + {content} + {label} + \end{{equation}} + " + )?; + } + Token::Table { + header, + cells, + caption, + ident, + } => { + let columns = header.len(); + let column_format = + itertools::intersperse(std::iter::repeat("c").take(columns), "|"); + write!(output, "\\begin{{table}}[h!]\n\\begin{{center}}\n")?; + output.write_str("\\begin{tabular}{|")?; + column_format + .map(|s| { + output.write_str(s)?; + Ok::<(), GenerationError>(()) + }) + .try_collect()?; + output.write_str("|}\n")?; + // write header line + self.write_table_row(output, meta, context, header)?; + output.write_str("\\hline")?; + 'cells: { + let mut rows = cells.chunks(columns).into_iter(); + let Some(first_row) = rows.next() else { + break 'cells; + }; + self.write_table_row(output, meta, context, first_row)?; + for row in rows { + self.write_table_row(output, meta, context, row)?; + } + } + output.write_str( + r" + \end{tabular} + \stepcounter{tabnum}", + )?; + if let Some(caption) = caption { + output.write_str("\n\\caption{")?; + self.write_tokens_to(output, meta, context, caption)?; + output.write_str("}\n")?; + } + if let Some(ident) = ident.as_ref() { + write!(output, "\\label{{tab:{ident}}}\n")?; + } + output.write_str("\\end{center}\n\\end{table}")?; + } + Token::Figure { + src_name, + caption, + ident, + } => { + output.write_str( + r"\begin{figure}[h!] + \centering + \includegraphics[width=0.9\textwidth]{", + )?; + let src_path = self.path_engine.image(&src_name, None)?; + output.write_str(src_path.to_string_lossy().borrow())?; // TODO note here: non-UTF paths **should** be handled just fine. + output.write_str("}")?; + if let Some(caption) = caption.as_ref() { + output.write_str("\\caption{")?; + self.write_tokens_to(output, meta, context, caption)?; + output.write_str("}\n")?; + } + if let Some(ident) = ident.as_ref() { + write!(output, "\\label{{fig:{ident}}}\n")?; + } + output.write_str("\\end{figure}")?; + // TODO add attachments dir customization + } + Token::Ayano(block) => { + let token = meta.ayano.display_token(block)?; + self.write_to(output, meta, context, &token)?; + } + Token::List { list_type, content } => { + let start: &str = match list_type { + crate::data::ListType::Bullet => r"\begin{itemize}", + crate::data::ListType::Num => r"\begin{enumerate}", + crate::data::ListType::Latin => todo!(), + crate::data::ListType::Cyrillic => { + r"\begin{enumerate}[label=\asbuk*), ref=\asbuk*]" + } + crate::data::ListType::Roman => todo!(), + }; + let end: &str = match list_type { + crate::data::ListType::Bullet => r"\end{itemize}", + crate::data::ListType::Num | crate::data::ListType::Cyrillic => { + r"\end{enumerate}" + } + crate::data::ListType::Latin => todo!(), + crate::data::ListType::Roman => todo!(), + }; + output.write_str(start)?; + for item in content { + output.write_str("\\item ")?; + self.write_to(output, meta, context, item)?; + output.write_str("\n")?; + } + output.write_str(end)?; + } + Token::Formatted(formatting, content) => { + let start: &str = match formatting { + crate::data::Formatting::Bold => r"\textbf{ ", + crate::data::Formatting::Italic => r"\textit{ ", + // TODO ADD \usepackage{soul} TO PREAMBLE! + crate::data::Formatting::StrikeThrough => r"\st{ ", + }; + let end: &str = match formatting { + crate::data::Formatting::Bold + | crate::data::Formatting::Italic + | crate::data::Formatting::StrikeThrough => " }", + }; + output.write_str(start)?; + self.write_tokens_to(output, meta, context, content)?; + output.write_str(end)?; + } + Token::InlineMathmode(content) => { + write!(output, r"$ {content} $")?; + } + Token::Reference(ident) => { + write!(output, r"\ref{{{ident}}}")?; + } + Token::FootNoteReference(ident) => { + let (encountered, i) = { + // TODO probably should move this to separate function + let encountered_map = &mut context.encountered_footnotes; + let encountered = encountered_map.get(ident.borrow() as &str).cloned(); + let i = match encountered { + Some(i) => i, + None => { + let i: usize = encountered_map.len(); + // let ident: &'context Cow<'source, str> = ident; + encountered_map.insert(ident.to_string(), i); + i + } + }; + (encountered.is_some(), i) + }; + // write out mark + write!(output, r"\footnotemark[{}]", i + 1)?; + if !encountered { + // write out content + if let Some(content) = meta.footnotes.get(&ident.borrow()) { + write!(output, r"\footnotetext[{}]{{", i + 1)?; + self.write_tokens_to(output, meta, context, content)?; + output.write_str("}")?; + } + } + } + // might seem weird at first, but these are basically useless at this point, as their content was presumably collected into context + Token::FootNoteContent { .. } => {} + Token::Error(error) => { + eprintln!("{error}"); + // TODO add representation in the document + } + Token::Paragraph(font, content) => { + let font = match font { + crate::data::Font::Normal => "fnt", + crate::data::Font::Caption => "capfnt", + }; + write!(output, r"\{font} {content}")?; + } + Token::Href { url, display } => { + write!(output, r"\href{{{url}}}{{{display}}}")?; + } + Token::Text(content) => self.write_tokens_to(output, meta, context, content)?, + Token::CodeBlock { code, language } => { + if let Some(language) = language { + writeln!( + output, + "\n\\begin{{minted}}{{{language}}}\n{code}\n\\end{{minted}}" + )?; + } else { + writeln!(output, "\n\\begin{{minted}}\n{code}\n\\end{{minted}}")?; + } + } + }; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::path::Path; + + use crate::data::{AyanoBlock, Font, Formatting, ListType}; + + use super::*; + + macro_rules! text { + ($text:literal) => { + Token::Paragraph(Font::Normal, $text.into()) + }; + } + + macro_rules! tex_sim { + ($a:expr, $b:expr) => { + let real = $a + .chars() + .filter(|c| !c.is_ascii_whitespace()) + .collect::(); + let expected = $b + .chars() + .filter(|c| !c.is_ascii_whitespace()) + .collect::(); + + text_diff::assert_diff(real.as_str(), expected.as_str(), "", 0); + }; + } + + macro_rules! tokens { + [$($token:expr), *] => { + $crate::data::Tokens::new([$($token), *]) + }; + } + + macro_rules! test { + {$name:ident, $token:expr, $b:expr} => { + #[test] + fn $name() { + use $crate::path_engine::PrimitiveEngine; + // arrange + let token = $token; + let meta = SourceMeta::collect(&token) + .expect("Should be able to collect") + .init_ayano() + .expect("Should be able to init Ayano"); + + let generator = LabaLatex::new(&PrimitiveEngine); + let mut output_target = String::new(); + + // act + generator.write_to(&mut output_target, &meta, &mut Context::default(), &token).expect("Should be able to write"); + + // assert + tex_sim!(output_target, $b); + + } + }; + } + + // Page Divider + test! {clearpage, Token::PageDiv, r"\clearpage"} + + // Headers + test! {header0, Token::Header { order: 0, content: "HeaderText".into() }, r"\section{HeaderText}"} + test! {header1, Token::Header { order: 1, content: "HeaderText".into() }, r"\subsection{HeaderText}"} + test! {header2, Token::Header { order: 2, content: "HeaderText".into() }, r"\subsubsection{HeaderText}"} + test! {header3, Token::Header { order: 3, content: "HeaderText".into() }, r"\paragraph{HeaderText}"} + test! {header4, Token::Header { order: 4, content: "HeaderText".into() }, r"\subparagraph{HeaderText}"} + test! {header5, Token::Header { order: 5, content: "HeaderText".into() }, r"\textbf{HeaderText}"} + + // Paragraphs + test! {para1, Token::Paragraph(Font::Normal, "content, content!!!".into()), r"\fnt content, content!!!"} + test! {para2, Token::Paragraph(Font::Caption, "content, content!!!".into()), r"\capfnt content, content!!!"} + + // Formatting + test! {formatting1, Token::Formatted(Formatting::Bold, tokens!(text!("some text, idk"))), r"\textbf{ \fnt some text, idk }"} + test! {formatting2, Token::Formatted(Formatting::Italic, tokens!(text!("some text, idk"))), r"\textit{ \fnt some text, idk }"} + test! {formatting3, Token::Formatted(Formatting::StrikeThrough, tokens!(text!("some text, idk"))), r"\st{ \fnt some text, idk }"} + + // Hrefs + test! {href1, Token::Href { url: "https://www.github.com/Dzuchun".parse().unwrap(), display: "My gh page".into() }, r"\href{https://www.github.com/Dzuchun}{My gh page}"} + test! {href2, Token::Href { url: "https://github.com/Dzuchun".parse().unwrap(), display: "Моя сторінка на гітхабі".into() }, r"\href{https://github.com/Dzuchun}{Моя сторінка на гітхабі}"} + + // Equations + test! {equations_1, Token::Equation { content: r"B(\alpha, \beta) = \int \limits_{0}^{1} x^{\alpha - 1} (1-x)^{\beta-1} dx".into(), ident: None }, + r" + \begin{equation} + B(\alpha, \beta) = \int \limits_{0}^{1} x^{\alpha - 1} (1-x)^{\beta-1} dx + \end{equation} + "} + test! {equations_2, Token::Equation { content: r"B(\alpha, \beta) = \int \limits_{0}^{1} x^{\alpha - 1} (1-x)^{\beta-1} dx".into(), ident: Some("beta_def".into()) }, + r" + \begin{equation} + B(\alpha, \beta) = \int \limits_{0}^{1} x^{\alpha - 1} (1-x)^{\beta-1} dx + + \label{eq:beta_def} + \end{equation} + "} + + // Inline math + test! {inline_mathmode, Token::InlineMathmode(r"\dfrac{\partial x}{\partial t}".into()), r"$ \dfrac{\partial x}{\partial t} $"} + + // Figures + test! {fig1, Token::Figure { src_name: Path::new("apple.jpg").into(), caption: Some(tokens![text!("a very realistic-looking apple")]), ident: None }, r" + \begin{figure}[h!] + \centering + \includegraphics[width = 0.9 \textwidth]{apple.jpg} + \caption{\fnt a very realistic-looking apple} + \end{figure} + "} + test! {fig2, Token::Figure { src_name: Path::new("schema1.jpg").into(), caption: Some(tokens![text!("A schema providing a bunch of very important info on quantum refurbalidzer function")]), ident: Some("schema-1".into()) }, r" + \begin{figure}[h!] + \centering + \includegraphics[width = 0.9 \textwidth]{schema1.jpg} + \caption{\fnt A schema providing a bunch of very important info on quantum refurbalidzer function} + \label{fig:schema-1} + \end{figure} + "} + + // Tables + test! {tab1, Token::Table { + header: + tokens![ + text!("header1"), text!("header2"), text!("header3") + ], + cells: tokens![ + text!("cell_11"),text!("cell_12"),text!("cell_13"), + text!("cell_21"),text!("cell_22"),text!("cell_23") + ], + caption: Some(tokens![text!("caption...")]), + ident: Some("example".into()) }, + r" + \begin{table}[h!] + \begin{center} + \begin{tabular}{|c|c|c|} +\fnt header1 & \fnt header2 & \fnt header3 \\ \hline \hline +\fnt cell_11 & \fnt cell_12 & \fnt cell_13 \\ \hline +\fnt cell_21 & \fnt cell_22 & \fnt cell_23 \\ \hline + \end{tabular} + \stepcounter{tabnum} + \caption{ \fnt caption...} + \label{tab:example} + \end{center} + \end{table} + "} + + // Lists + test! {list_bullets, Token::List { list_type: ListType::Bullet, content: tokens![ + text!("point1"), + text!("point2"), + text!("point3") + ] }, r" + \begin{itemize} + \item \fnt point1 + \item \fnt point2 + \item \fnt point3 + \end{itemize} + "} + + test! {list_numeric, Token::List { list_type: ListType::Num, content: tokens![ + text!("point1"), + text!("point2"), + text!("point3") + ] }, r" + \begin{enumerate} + \item \fnt point1 + \item \fnt point2 + \item \fnt point3 + \end{enumerate} + "} + + test! {list_cyrillic, Token::List { list_type: ListType::Cyrillic, content: tokens![ + text!("point1"), + text!("point2"), + text!("point3") + ] }, r" + \begin{enumerate}[label=\asbuk*), ref=\asbuk*] + \item \fnt point1 + \item \fnt point2 + \item \fnt point3 + \end{enumerate} + "} + + // Text + test! {text, Token::Text(tokens![Token::PageDiv, Token::Header { order: 0, content: "Header".into() }, text!("Here's a little formula: "), Token::InlineMathmode(r"(a+b)^2 = a^2 + 2 \cdot a \cdot b + b^2".into()), text!(". It's very useful, more useful than me!")]), r" + \clearpage + \section{Header} + \fnt Here's a little formula: $(a+b)^2 = a^2 + 2 \cdot a \cdot b + b^2$\fnt . It's very useful, more useful than me! + "} + + // Footnotes + test! {footnote_single, Token::Text(tokens![ + Token::FootNoteReference("explanation".into()), + Token::FootNoteContent { content: tokens![text!("42")], ident: "explanation".into() } + ]), r" + \footnotemark[1] + \footnotetext[1]{\fnt 42} + "} + + test! {footnote_multiple, Token::Text(tokens![ + text!("Here's a first note"), + Token::FootNoteReference("explanation1".into()), + text!(", there are also second"), + Token::FootNoteReference("explanation2".into()), + text!(", and the third"), + Token::FootNoteReference("explanation3".into()), + Token::FootNoteContent { content: tokens![text!("There are things in this world that's you're not meant to see")], ident: "explanation1".into() }, + Token::FootNoteContent { content: tokens![text!("now the voice of a deity permeates")], ident: "explanation2".into() }, + Token::FootNoteContent { content: tokens![text!("see notes 1 and 2")], ident: "explanation3".into() }, + text!(". But notes will not be duplicated here!"), + Token::FootNoteReference("explanation1".into()) + + ]), r" + \fnt Here's a first note + \footnotemark[1] + \footnotetext[1]{\fnt There are things in this world that's you're not meant to see} + \fnt , there are also second + \footnotemark[2] + \footnotetext[2]{\fnt now the voice of a deity permeates} + \fnt , and the third + \footnotemark[3] + \footnotetext[3]{\fnt see notes 1 and 2} + \fnt . But notes will not be duplicated here! + \footnotemark[1] + "} + + test! {ayano_plain1, Token::Ayano(AyanoBlock{ + is_display: false, + is_static: false, + code: "1".into(), + insert_path: None + }), r"\fnt 1"} + test! {ayano_plain2, Token::Ayano(AyanoBlock{ + is_display: false, + is_static: false, + code: "1 + 2".into(), + insert_path: None + }), r"\fnt 3"} + test! {ayano_plain3, Token::Ayano(AyanoBlock{ + is_display: false, + is_static: false, + code: r" +from math import sqrt +x = 103 +y = sqrt(x) +int(y)" + .into(), + insert_path: None + }), r"\fnt 10"} + test! {ayano_plain4, Token::Ayano(AyanoBlock{ + is_display: false, + is_static: false, + code: r" +res = 0 +for i in range(101): + res += i +res" + .into(), + insert_path: None + }), r"\fnt 5050"} + test! {ayano_err1, Token::Ayano(AyanoBlock{ + is_display: false, + is_static: false, + code: r" + 'err', 1.0, 0.1" + .into(), + insert_path: None + }), r"\fnt 1.00 $\pm$ \fnt 0.10"} + test! {ayano_err2, Token::Ayano(AyanoBlock{ + is_display: false, + is_static: false, + code: r" + 'err', 1.0, 0.4" + .into(), + insert_path: None + }), r"\fnt 1.0 $\pm$ \fnt 0.4"} + test! {ayano_err3, Token::Ayano(AyanoBlock{ + is_display: false, + is_static: false, + code: r" +x = 1.0 +y = 0.4 +@dev: x, y" + .into(), + insert_path: None + }), r"\fnt 1.0 $\pm$ \fnt 0.4" + } + + test! {ayano_fig1, Token::Ayano(AyanoBlock{ + is_display: false, + is_static: false, + code: r"'fig', 'path/to/image.jpg', None, None".into(), + insert_path: None + }), r" +\begin{figure}[h!] +\centering +\includegraphics[width = 0.9 \textwidth]{path/to/image.jpg} +\end{figure}" + } + + test! {ayano_fig2, Token::Ayano(AyanoBlock{is_display: false,is_static: false,code: r"@fig: src = 'path/to/image.jpg'".into(),insert_path: None}), +r" +\begin{figure}[h!] +\centering +\includegraphics[width = 0.9 \textwidth]{path/to/image.jpg} +\end{figure}"} + test! {ayano_fig3, Token::Ayano(AyanoBlock{ is_display: false,is_static: false,code: r#"@fig: src = 'path/to/image.jpg', ident = "meow""#.into(),insert_path: None + }), +r#" +\begin{figure}[h!] +\centering +\includegraphics[width = 0.9 \textwidth]{path/to/image.jpg} +\label{fig:meow} +\end{figure}"# + } + // TODO add tests with captions + + test! {ayano_gen_tab1, Token::Ayano(AyanoBlock { is_display: false, is_static: false, code: +r#" +data = [[1, 2, 3], [4, 5, 6]] +@gen_table: lambda r,c: data[r][c]; rows=2, columns=3 +"#.into(), insert_path: None }), +r#" +\begin{table}[h!] +\begin{center} +\begin{tabular}{|c|c|c|} +\fnt 1 & \fnt 2 & \fnt 3 \\ \hline \hline +\fnt 4 & \fnt 5 & \fnt 6 \\ \hline +\end{tabular} +\stepcounter{tabnum} +\end{center} +\end{table} +"# + } + // TODO add tests with captions and csv-tables + + // TODO add minted to preamble + // TODO add -shell-escape to script util + test! {cobe_block1, Token::CodeBlock { code: r" +def f(): + x = 5 + y = x * x + return y".into(), language: Some("python".into()) }, +r" +\begin{minted}{python} +def f(): + x = 5 + y = x * x + return y +\end{minted} +"} + // TODO probably add more tests for code blocks..? +} diff --git a/src/gen/mod.rs b/src/gen/mod.rs new file mode 100644 index 0000000..384dafd --- /dev/null +++ b/src/gen/mod.rs @@ -0,0 +1,51 @@ +mod ayano; +mod lab; + +use std::fmt::Write; + +use itertools::Itertools; + +use crate::{ + data::{Token, Tokens}, + path_engine, +}; + +#[derive(Debug, derive_more::From)] +pub enum GenerationError<'source> { + IOs(std::fmt::Error), + Ayano(ayano::AyanoError<'source>), + Path(path_engine::Error), +} + +type Res<'source> = Result<(), GenerationError<'source>>; + +// TODO make this thing modular (probably use tower to handle writes or th) +pub trait OutputGenerator<'source, Meta, Context> { + fn write_to<'meta, 'context, 'token, W: Write + ?Sized>( + &self, + output: &mut W, + meta: &'meta Meta, + context: &'context mut Context, + token: &'token Token<'source>, + ) -> Res<'source> + where + 'source: 'meta + 'context + 'token, + 'token: 'context; + + fn write_tokens_to<'meta, 'context, 'token, W: Write + ?Sized>( + &self, + output: &mut W, + meta: &'meta Meta, + context: &'context mut Context, + tokens: &'token Tokens<'source>, + ) -> Res<'source> + where + 'source: 'meta + 'context + 'token, + 'token: 'context, + { + tokens + .into_iter() + .map(|t| self.write_to(output, meta, context, t)) + .try_collect() + } +} diff --git a/src/lexer.rs b/src/lexer.rs new file mode 100644 index 0000000..877b94c --- /dev/null +++ b/src/lexer.rs @@ -0,0 +1,413 @@ +use std::{borrow::Cow, path::Path}; + +use nom::{ + branch::{alt, permutation}, + bytes::complete::{is_not, tag, take_till1, take_until1}, + character::complete::{alpha1, alphanumeric1, char, digit1, multispace0, newline, space0}, + combinator::{all_consuming, map_res, opt, recognize}, + error::{context, ContextError, ErrorKind, FromExternalError, ParseError}, + multi::{fold_many1, many1_count}, + number::{self, complete::float}, + sequence::{delimited, preceded, terminated, tuple}, + FindToken, IResult, Parser, +}; +use url::Url; + +use crate::{ + data::{AyanoBlock, Token, Tokens}, + util::PermutatedArray, +}; + +pub enum LexingError {} + +type LexingResult<'source, O = Token<'source>> = Result<(&'source str, O), LexingError>; + +/// Lexes input into tokens +pub fn lex<'source, E: ParseError<&'source str>>( + input: &'source str, +) -> IResult<&'source str, Tokens<'source>, E> { + todo!() +} + +/// Lexes input into tokens, but forbids some types of tokens, like figures, tables, etc. +/// +/// Intended to be used to lex captions +pub fn limited_lex<'source, E: ParseError<&'source str>>( + input: &'source str, +) -> IResult<&'source str, Tokens<'source>, E> { + todo!() +} + +// TODO come up with some sort of clever trait implementation to optimize this out +fn parse_args<'parser, 'source, E: ParseError<&'source str>, const ARGS: usize>( + args: [&'parser mut dyn FnMut(&'source str) -> IResult<&'source str, &'source str, E>; ARGS], +) -> impl FnMut(&'source str) -> IResult<&'source str, [Option<&'source str>; ARGS], E> + 'parser { + let mut parsers: [_; ARGS] = args.map(|p| { + move |input| { + let p = &mut *p; + let (rest, value) = opt(tuple(( + space0, + p, + space0, + alt((char(','), nom::combinator::peek(char('}')))), + )))(input)?; + Ok::<(&str, Option<&str>), nom::Err>((rest, value.map(|(_, value, _, _)| value))) + } + }); + move |input| { + preceded( + opt(tuple((space0, char('\n')))), + tuple(( + opt(char('\n')), + char('{'), + permutation(PermutatedArray(&mut parsers)), + char('}'), + )), + )(input) + .map(|(rest, (_, _, out, _))| (rest, out)) + } +} + +// ARGUMENT KERNELS +fn caption_kernel<'source, E: ParseError<&'source str>>( + input: &'source str, +) -> IResult<&'source str, &'source str, E> { + delimited( + char('"'), + recognize(fold_many1( + alt((tag("\\\""), is_not("\""))), + || (), + |_, _| (), + )) + // input slice between quotes must be possible to consume fully, but not actually parsed yet, due to + // FIXME limitation of argument parsers -- they must return &'source str for now. + .and_then(recognize(all_consuming(limited_lex::<'source, E>))), + char('"'), + )(input) +} + +fn ref_kernel<'source, E: ParseError<&'source str>>( + input: &'source str, +) -> IResult<&'source str, &'source str, E> { + alphanumeric1(input) +} + +// META ARG PARSERS +fn _parse_arg<'source, E: ParseError<&'source str>>( + name: &'static str, + kernel: fn(&'source str) -> IResult<&'source str, &'source str, E>, +) -> impl FnMut(&'source str) -> IResult<&'source str, &'source str, E> { + move |input| { + let (rest, (_, _, _, _, ident)) = + tuple((tag(name), space0, char('='), space0, kernel))(input)?; + Ok((rest, ident)) + } +} + +fn parse_ref<'source, E: ParseError<&'source str>>( + input: &'source str, +) -> IResult<&'source str, &'source str, E> { + _parse_arg("ref", ref_kernel)(input) +} + +fn parse_caption<'source, E: ParseError<&'source str>>( + input: &'source str, +) -> IResult<&'source str, &'source str, E> { + _parse_arg("caption", caption_kernel)(input) +} + +fn __recognize_float<'source, E: ParseError<&'source str>>( + input: &'source str, +) -> IResult<&'source str, &'source str, E> { + recognize::<&str, f32, E, _>(float::<&str, E>)(input) +} + +fn parse_width<'source, E: ParseError<&'source str>>( + input: &'source str, +) -> IResult<&'source str, &'source str, E> { + _parse_arg("width", __recognize_float)(input) +} + +// TOKEN PARSERSu +// CONVENTION: each token should excessively match space after it + +fn div_page<'source, E: ParseError<&'source str> + ContextError<&'source str>>( + input: &'source str, +) -> IResult<&'source str, Token<'source>, E> { + let (rest, _) = context( + "page divider", + tuple((multispace0, tag("||||||"), space0, char('\n'))), + )(input)?; + Ok((rest, Token::PageDiv)) +} + +fn header< + 'source, + E: ParseError<&'source str> + + ContextError<&'source str> + + FromExternalError<&'source str, &'static str>, +>( + input: &'source str, +) -> IResult<&'source str, Token<'source>, E> { + let (rest, (_, _, order, _, content)) = context( + "header", + tuple(( + multispace0, + newline, + map_res(many1_count(char('#')), |count| { + if count <= 6 { + Ok(count) + } else { + Err("headers can't be more than 6-fold") + } + }), + space0, + take_until1("\n"), + )), + )(input)?; + Ok(( + rest, + Token::Header { + order, + content: content.into(), + }, + )) +} + +fn equation<'source, E: ParseError<&'source str> + ContextError<&'source str>>( + input: &'source str, +) -> IResult<&'source str, Token<'source>, E> { + let (rest, (_, _, content, _, [ident])) = context( + "display mathmode", + tuple(( + multispace0, + tag("$$"), + take_until1("\n$$"), + tag("\n$$"), + parse_args([&mut parse_ref]), + )), + )(input)?; + Ok(( + rest, + Token::Equation { + content: content.into(), + ident: ident.map(Cow::from), + }, + )) +} + +fn table<'source, E: ParseError<&'source str> + ContextError<&'source str>>( + input: &'source str, +) -> IResult<&'source str, Token<'source>, E> { + todo!() +} + +fn figure<'source, E: ParseError<&'source str> + ContextError<&'source str>>( + input: &'source str, +) -> IResult<&'source str, Token<'source>, E> { + // TODO add width parameter to figure, I guess + let (rest, (_, _, path, _, [ident, caption, _width])) = context( + "figure", + tuple(( + multispace0, + tag("![["), + take_until1("]]"), + tag("]]"), + parse_args([&mut parse_ref, &mut parse_caption, &mut parse_width]), + )), + )(input)?; + let caption = match caption { + Some(caption) => + // All caption must be consumed for successful lex + // any error occurring during caption parsing, must be propagated + { + Some(all_consuming(limited_lex)(caption)?.1) + } + None => None, + }; + let token = Token::Figure { + src_name: Path::new(path).into(), + caption, + ident: ident.map(Cow::from), + }; + Ok((rest, token)) +} + +fn href<'source, E: ParseError<&'source str> + ContextError<&'source str>>( + input: &'source str, +) -> IResult<&'source str, Token<'source>, E> { + let (rest, (display, url)) = context( + "href", + tuple(( + delimited(char('['), take_till1(|c| c != ']'), char(']')), + delimited(char('('), take_till1(|c| c != ')'), char(')')).and_then(|url: &str| { + Ok(( + "", + Url::parse(url).map_err(|_err| { + // TODO make a proper error + nom::Err::Error(E::from_error_kind( + "failed to parse url", + nom::error::ErrorKind::Verify, + )) + })?, + )) + }), + )), + )(input)?; + Ok(( + rest, + Token::Href { + url, + display: display.into(), + }, + )) +} + +fn code_block<'source, E: ParseError<&'source str> + ContextError<&'source str>>( + input: &'source str, +) -> IResult<&'source str, Token<'source>, E> { + let (rest, (_, (lang, _, _, code))) = context( + "code block", + tuple(( + char('\n'), + delimited( + tag("```\n"), + tuple((opt(alpha1), space0, char('\n'), take_until1("\n```"))), + tag("\n```"), + ), + )), + )(input)?; + Ok(( + rest, + Token::CodeBlock { + code: code.into(), + language: lang.map(Cow::from), + }, + )) +} + +fn ayano<'source, E: ParseError<&'source str> + ContextError<&'source str>>( + input: &'source str, +) -> IResult<&'source str, Token<'source>, E> { + // python, Ayano * ~ # + let (rest, (_, _, _, _, _, _, _, (display, insert, ident, is_static), _, code, _)) = + context( + "ayano", + tuple(( + char('\n'), + tag("```"), + space0, + tag("python,"), + space0, + tag("Ayano"), + space0, + permutation(( + opt(tuple(( + char('*'), + space0, + opt(terminated(caption_kernel, space0)), + ))), + opt(tuple((char('~'), space0, alphanumeric1, space0))), + opt(tuple(( + char('#'), + space0, + digit1.and_then(|number: &str| { + Ok::<(&str, u16), nom::Err>(( + "", + number.parse().map_err(|_| { + nom::Err::Error(E::from_error_kind( + "Identifier must be parseable into u16", + ErrorKind::Digit, + )) + })?, + )) + }), + space0, + ))), + opt(terminated(char('!'), space0)), + )), + char('\n'), + take_until1("\n```"), + tag("\n```"), + )), + )(input)?; + // type might be a bit confusing, but basically: + // - outer None means this block is not meant to be displayed + // - outer Some means this block is to be displayed + // - inner None means this block has no display caption + // - inner Some means this block has display caption + let display: Option> = display.map(|(_, _, caption)| caption); // TODO add code captions support + let insert: Option<&'source Path> = insert.map(|(_, _, path, _)| Path::new(path)); + let _ident: Option = ident.map(|(_, _, ident, _)| ident); // TODO add support for codeblock identifiers + let code: Cow<'_, str> = Cow::from(code); + Ok(( + rest, + Token::Ayano(AyanoBlock { + insert_path: insert.map(Cow::from), + code, + is_display: display.is_some(), + is_static: is_static.is_some(), + }), + )) +} + +fn list<'source, E: ParseError<&'source str> + ContextError<&'source str>>( + input: &'source str, +) -> IResult<&'source str, Token<'source>, E> { + let (rest, _) = context( + "list", + preceded(char('\n'), |mut input: &str| { + let mut newline = 1; + let mut non_whitespace = 0; + while newline > non_whitespace { + newline = input.find('\n').unwrap_or(input.len()); + non_whitespace = input + .find(|c: char| !c.is_whitespace()) + .unwrap_or(input.len()); + input = &input[(newline + 1)..]; + } + + let indent = non_whitespace; // how many bytes are to be dropped from the start of each line + input.lines().take_while(|line| preceded, many1, second)) + todo!(); + }), + )(input)?; + todo!() +} + +fn formatting<'source, E: ParseError<&'source str> + ContextError<&'source str>>( + input: &'source str, +) -> IResult<&'source str, Token<'source>, E> { + todo!() +} +fn paragraph<'source, E: ParseError<&'source str> + ContextError<&'source str>>( + input: &'source str, +) -> IResult<&'source str, Token<'source>, E> { + todo!() +} +fn inline_math<'source, E: ParseError<&'source str> + ContextError<&'source str>>( + input: &'source str, +) -> IResult<&'source str, Token<'source>, E> { + todo!() +} +fn reference<'source, E: ParseError<&'source str> + ContextError<&'source str>>( + input: &'source str, +) -> IResult<&'source str, Token<'source>, E> { + todo!() +} +fn footnote_reference<'source, E: ParseError<&'source str> + ContextError<&'source str>>( + input: &'source str, +) -> IResult<&'source str, Token<'source>, E> { + todo!() +} +fn footnote_content<'source, E: ParseError<&'source str> + ContextError<&'source str>>( + input: &'source str, +) -> IResult<&'source str, Token<'source>, E> { + todo!() +} + +fn tokens<'source, E: ParseError<&'source str> + ContextError<&'source str>>( + input: &'source str, +) -> IResult<&'source str, Token<'source>, E> { + todo!() +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..18a5f79 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,39 @@ +//! *Kyoko is being a tomato. Why? I don't know :idk:* +//! +//! This crate allows [obsidian]'s markdown transformation into a (la)tex document of the format I personally like. +//! One should note, that markdown is generally less descriptive than (la)tex, so it should code to no surprise, +//! that I introduced some custom markdown syntax to account for that. +//! +//! # Ideology +//! Although it's annoying to admit, there is no way to do a fully-streamed zero-copy-type of transformation. There are two reasons for that: +//! - Markdown allows to decipher footnotes at an point up until the end of the file. +//! On the other hand, I could not find a way to make (la)tex "insert" footnote definition after first marker to it - +//! it seems to always insert them at the bottom of the page they were encountered. +//! - Static [ayano] blocks are supposed to be available for the entire document. +//! Thus, code must be fully collected first, that executed of the second, separate path. +//! +//! Due to these two factors, I decided to store the input in it's entirety during program's execution. +//! To (sorta) compensate for that, crate follows a strict zero-copy rule through the entire codebase. +//! +//! To further decrease allocations count, it was decided that any output would be generated through an object, that can be written to by parsed tokens, in order. +//! That object would ideally have some methods to append the output with a string, or th like that... Well, [`Write`] trait matches that perfectly! +//! So all tokens can be appended to an abstract, writer-sort-of-thing. +//! [`Display`] trait implementation was considered as an alternative, but I ultimately decided that was not idiomatic. +//! +//! Actual Token representation might vary, so there can be different actual ZST formatters, defined in their separate crates. +//! +//! ### Async +//! As much as I'd like all of the processes be nice and async, I see no option for that in case of continuous output generation. +//! Well, unless there's a way to efficiently write to a single output from multiple threads, while preserving output order. +//! Any sort of "collect" function would've destroyed the purpose anyway. + +/// This module defines types that are used to represent parsed data +mod data; +mod gen; +mod lexer; +mod path_engine; + +/// This module houses "utility-like" structs and functions. +/// +/// Since this is generally a bad practice, I'd like to know suggestions on std library/other crates items I could use instead +mod util; diff --git a/src/path_engine.rs b/src/path_engine.rs new file mode 100644 index 0000000..b1dc864 --- /dev/null +++ b/src/path_engine.rs @@ -0,0 +1,193 @@ +use std::path::{Path, PathBuf}; + +#[derive(Debug, derive_more::From)] +pub enum VariantError { + Io(std::io::Error), // missing permissions, or broken symbolic links + NotExist, // file does not in fact exist in the filesystem + NotAFile, // path resolves to a directory, not a file +} + +#[derive(Debug)] +pub struct Error { + source: Option, // error resolving path relative to the target file (if present) + ayano: Option, // error resolving relative to ayano-inserted code + specific: Option, // error of an option specific to some method + execution: VariantError, // error of path resolution relative to execution directory +} + +// TODO make this thing modular (probably use tower or th) +pub trait PathEngine { + fn image(&self, path: &Path, ayano_insert: Option<&Path>) -> Result; + fn csv(&self, path: &Path, ayano_insert: Option<&Path>) -> Result; + fn ayano_insert(&self, path: &Path) -> Result; +} + +pub use default::Engine as DefaultEngine; +pub use Error as DefaultError; + +mod default { + use std::path::{Path, PathBuf}; + + use super::{Error, PathEngine}; + + #[derive(Debug)] + pub struct Engine { + source_folder: Option, + image_folder: Option, + execution_folder: PathBuf, + } + + macro_rules! try_path { + ($path:ident) => { + if $path.try_exists().is_ok_and(std::convert::identity) + // path exists and is reachable + && $path.is_file() + // and path points to the file + { + // found a match! + return Ok($path); + } + }; + } + + macro_rules! op_path { + ($path:ident) => { + $path = if let Some(path) = $path { + try_path!(path); + Some(path) + } else { + None + } + }; + } + + macro_rules! path_error { + ($path:ident) => { + match $path.try_exists() { + Err(io) => super::VariantError::Io(io), + Ok(false) => super::VariantError::NotExist, + Ok(true) => { + if !$path.is_file() { + super::VariantError::NotAFile + } else { + unreachable!("These should be all options for possible path errors. If you can see this, it's a oversight, please contact me.") + } + } + } + }; + } + + impl Engine { + pub fn create( + target_folder: impl Into>, + image_folder: impl Into>, + ) -> std::io::Result { + use std::env::current_dir as cd; + Ok(Self { + source_folder: target_folder.into(), + image_folder: image_folder.into(), + execution_folder: cd()?, + }) + } + } + + impl<'l> PathEngine for &'l Engine { + fn image(&self, path: &Path, ayano_insert: Option<&Path>) -> Result { + // first, try resolving in ayano-inserted folder + let mut ayano_attempt = ayano_insert.as_ref().map(|p| p.join(path)); + op_path!(ayano_attempt); + + // next, try resolving in a dedicated image folder + let mut image_attempt = self.image_folder.as_ref().map(|p| p.join(path)); + op_path!(image_attempt); + + // next, try resolving in the source folder + let mut source_attempt = self.source_folder.as_ref().map(|p| p.join(path)); + op_path!(source_attempt); + + // next, try resolving relative to execution folder + let execution_attempt = self.execution_folder.join(path); + try_path!(execution_attempt); + + // well, all options failed. not let's construct informative error: + Err(Error { + execution: path_error!(execution_attempt), + ayano: ayano_attempt.map(|p| path_error!(p)), + source: source_attempt.map(|p| path_error!(p)), + specific: image_attempt.map(|p| path_error!(p)), + }) + } + + fn csv(&self, path: &Path, ayano_insert: Option<&Path>) -> Result { + // first, try resolving in ayano-inserted folder + let mut ayano_attempt = ayano_insert.as_ref().map(|p| p.join(path)); + op_path!(ayano_attempt); + + // next, try resolving in the source folder + let mut source_attempt = self.source_folder.as_ref().map(|p| p.join(path)); + op_path!(source_attempt); + + // next, try resolving relative to execution folder + let execution_attempt = self.execution_folder.join(path); + try_path!(execution_attempt); + + // well, all options failed. not let's construct informative error: + Err(Error { + execution: path_error!(execution_attempt), + ayano: ayano_attempt.map(|p| path_error!(p)), + source: source_attempt.map(|p| path_error!(p)), + specific: None, + }) + } + + fn ayano_insert(&self, path: &Path) -> Result { + // first, try resolving in the source folder + let mut source_attempt = self.source_folder.as_ref().map(|p| p.join(path)); + op_path!(source_attempt); + + // next, try resolving relative to execution folder (TODO should O remove that?) + let execution_attempt = self.execution_folder.join(path); + try_path!(execution_attempt); + + // well, all options failed. not let's construct informative error: + Err(Error { + execution: path_error!(execution_attempt), + ayano: None, + source: source_attempt.map(|p| path_error!(p)), + specific: None, + }) + } + } +} + +pub use primitive::Engine as PrimitiveEngine; +mod primitive { + use super::PathEngine; + + pub struct Engine; + + impl PathEngine for &Engine { + fn image( + &self, + path: &std::path::Path, + _: Option<&std::path::Path>, + ) -> Result { + Ok(path.to_owned()) + } + + fn csv( + &self, + path: &std::path::Path, + _: Option<&std::path::Path>, + ) -> Result { + Ok(path.to_owned()) + } + + fn ayano_insert( + &self, + path: &std::path::Path, + ) -> Result { + Ok(path.to_owned()) + } + } +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..4ee10a8 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,369 @@ +use std::{ + mem::{forget, MaybeUninit}, + num::ParseIntError, + ops::{Range, RangeFrom, RangeInclusive, RangeTo, RangeToInclusive}, + panic::{catch_unwind, UnwindSafe}, + str::FromStr, +}; + +use itertools::Itertools; +use nom::{branch::Permutation, error::ParseError, Parser}; + +#[derive(Debug, Clone, PartialEq)] +pub enum GenericRange { + DoubleBounded(Range), + DoubleBoundedInclusive(RangeInclusive), + From(RangeFrom), + To(RangeTo), + ToInclusive(RangeToInclusive), + Full, +} + +impl GenericRange { + pub fn from(&self) -> Option { + match self { + GenericRange::DoubleBounded(range) => Some(range.start), + GenericRange::DoubleBoundedInclusive(range) => Some(*range.start()), + GenericRange::From(range) => Some(range.start), + GenericRange::To(_) | GenericRange::ToInclusive(_) | GenericRange::Full => None, + } + } + + pub fn to(&self) -> Option { + match self { + GenericRange::DoubleBounded(range) => Some(range.end - 1), + GenericRange::DoubleBoundedInclusive(range) => Some(*range.end()), + GenericRange::From(_) | GenericRange::Full => None, + GenericRange::ToInclusive(range) => Some(range.end), + GenericRange::To(range) => Some(range.end - 1), + } + } +} + +#[derive(Debug, PartialEq)] +pub enum GenericRangeParseError { + Format, + Object(ParseIntError), +} + +impl FromStr for GenericRange { + type Err = GenericRangeParseError; + + fn from_str(s: &str) -> Result { + if s == ".." { + return Ok(GenericRange::Full); + } + match (s.starts_with(".."), s.ends_with(".."), s.contains('=')) { + (false, false, false) => { + let Some((from, to)) = s.split_once("..") else { + return Err(GenericRangeParseError::Format); + }; + let from: usize = from + .parse() + .map_err(|err| GenericRangeParseError::Object(err))?; + let to: usize = to + .parse() + .map_err(|err| GenericRangeParseError::Object(err))?; + Ok(GenericRange::DoubleBounded(from..to)) + } + (false, false, true) => { + let Some((from, to)) = s.split_once("..=") else { + return Err(GenericRangeParseError::Format); + }; + let from: usize = from + .parse() + .map_err(|err| GenericRangeParseError::Object(err))?; + let to: usize = to + .parse() + .map_err(|err| GenericRangeParseError::Object(err))?; + Ok(GenericRange::DoubleBoundedInclusive(from..=to)) + } + (false, true, false) => { + let Some((from, "")) = s.split_once("..") else { + return Err(GenericRangeParseError::Format); + }; + let from: usize = from + .parse() + .map_err(|err| GenericRangeParseError::Object(err))?; + Ok(GenericRange::From(from..)) + } + (true, false, false) => { + let Some(("", to)) = s.split_once("..") else { + return Err(GenericRangeParseError::Format); + }; + let to: usize = to + .parse() + .map_err(|err| GenericRangeParseError::Object(err))?; + Ok(GenericRange::To(..to)) + } + (true, false, true) => { + let Some(("", to)) = s.split_once("..=") else { + return Err(GenericRangeParseError::Format); + }; + let to: usize = to + .parse() + .map_err(|err| GenericRangeParseError::Object(err))?; + Ok(GenericRange::ToInclusive(..=to)) + } + _ => Err(GenericRangeParseError::Format), + } + } +} + +#[cfg(test)] +mod generic_rage_tests { + use super::*; + macro_rules! test { + {$name:ident, $input:literal, $output:expr} => { + #[test] + fn $name() { + // arrange + + // act + let output: Result = $input.parse(); + + // assert + assert_eq!(output.expect("Should be able to parse"), $output); + } + }; + } + + test! {full, "..", GenericRange::Full} + test! {from, "1..", GenericRange::From(1..)} + test! {double_ended, "1..10", GenericRange::DoubleBounded(1..10)} + test! {double_ended_inclusive, "10..=100", GenericRange::DoubleBoundedInclusive(10..=100)} + test! {to, "..100", GenericRange::To(..100)} + test! {to_inclusive, "..=100", GenericRange::ToInclusive(..=100)} + test! {idk1, "5..=100", GenericRange::DoubleBoundedInclusive(5..=100)} +} + +#[derive(derive_more::From)] +#[repr(transparent)] +#[derive(Debug, derive_more::Deref, derive_more::DerefMut)] +pub struct Equivalent(pub T); + +impl PartialEq for Equivalent { + fn eq(&self, _: &Self) -> bool { + true + } +} + +impl Eq for Equivalent {} + +#[derive(derive_more::From)] +#[repr(transparent)] +#[derive(Debug, derive_more::Deref)] +// TODO REMOVE THAT, IT VERY-VERY BAD SEMANTICALLY!!!! +pub struct Immutable(T); + +#[derive(Debug)] +pub struct InlinedStack<'v, T> { + inner: &'v mut Vec, +} + +impl<'v, T> InlinedStack<'v, T> { + pub fn new(inner: &'v mut Vec) -> Self { + Self { inner } + } + + pub fn accessor<'l>(&'l mut self) -> InlinedStackAccessor<'l, 'v, T> { + InlinedStackAccessor { + recorded_size: self.inner.len(), + reference: self, + } + } +} + +pub struct InlinedStackAccessor<'l, 'v: 'l, T> { + reference: &'l mut InlinedStack<'v, T>, + recorded_size: usize, +} + +impl<'l, 'v: 'l, T> InlinedStackAccessor<'l, 'v, T> { + pub fn push(&mut self, t: T) { + self.reference.inner.push(t); + } + + pub fn accessor<'l1>(&'l1 mut self) -> InlinedStackAccessor<'l1, 'v, T> { + InlinedStackAccessor { + recorded_size: self.reference.inner.len(), + reference: self.reference, + } + } + + pub fn revert(&mut self) { + self.reference.inner.truncate(self.recorded_size); + } +} + +#[cfg(test)] +mod inline_stack_tests { + use super::InlinedStack; + + #[test] + fn create() { + // arrange + let mut v = vec![1, 2, 3]; + + // act + let mut stack = InlinedStack::new(&mut v); + let _accessor = stack.accessor(); + + // assert + assert_eq!(v, vec![1, 2, 3]); + } + + #[test] + fn push() { + // arrange + let mut v = vec![1, 2, 3]; + let mut stack = InlinedStack::new(&mut v); + let mut accessor = stack.accessor(); + + // act + accessor.push(4); + accessor.push(5); + + // assert + assert_eq!(v, vec![1, 2, 3, 4, 5]); + } + + #[test] + fn push_inner() { + // arrange + let mut v = vec![1, 2, 3]; + let mut stack = InlinedStack::new(&mut v); + let mut accessor = stack.accessor(); + let mut accessor_inner = accessor.accessor(); + + // act + accessor_inner.push(4); + accessor_inner.push(5); + + // assert + assert_eq!(v, vec![1, 2, 3, 4, 5]); + } + + #[test] + fn revert() { + // arrange + let mut v = vec![1, 2, 3]; + let mut stack = InlinedStack::new(&mut v); + let mut accessor = stack.accessor(); + accessor.push(4); + accessor.push(5); + + // act + accessor.revert(); + + // assert + assert_eq!(v, vec![1, 2, 3]); + } + + #[test] + fn revert_inner() { + // arrange + let mut v = vec![1, 2, 3]; + let mut stack = InlinedStack::new(&mut v); + let mut accessor = stack.accessor(); + accessor.push(4); + let mut accessor_inner = accessor.accessor(); + accessor_inner.push(5); + + // act + accessor_inner.revert(); + + // assert + assert_eq!(v, vec![1, 2, 3, 4]); + } + + #[test] + fn revert_outer() { + // arrange + let mut v = vec![1, 2, 3]; + let mut stack = InlinedStack::new(&mut v); + let mut accessor = stack.accessor(); + accessor.push(4); + let mut accessor_inner = accessor.accessor(); + accessor_inner.push(5); + + // act + accessor.revert(); + + // assert + assert_eq!(v, vec![1, 2, 3]); + } +} + +#[derive(Debug)] +pub enum ArrayCollectError { + NotEnough { expeected: usize, got: usize }, + TooMuch(usize), +} +pub trait IteratorArrayCollectExt: Sized { + fn collect_array(self) -> Result<[T; N], ArrayCollectError>; +} + +// FIXME memory leaks in case of panic +impl> IteratorArrayCollectExt for I { + // TODO ADD TESTS FOR THIS, ESPECIALLY FOR THE CASE OF VALUE DROPPING + fn collect_array(mut self) -> Result<[T; N], ArrayCollectError> { + { + // Ok, so unsafe is used here, and I am really scared of it. But I couldn't find any alternative, really + + // Create an array of uninitialized values. + let mut array: [MaybeUninit; N] = unsafe { MaybeUninit::uninit().assume_init() }; + + for i in 0..N { + match self.next() { + None => { + return Err(ArrayCollectError::NotEnough { + expeected: N, + got: i, + }); + } + Some(next) => { + array[i] = MaybeUninit::new(next); + } + } + } + if self.next().is_some() { + // there are even more elements to get + return Err(ArrayCollectError::TooMuch(N)); + } + + Ok(unsafe { std::mem::transmute_copy::<_, [T; N]>(&array) }) + } + } +} + +pub struct PermutatedArray<'l, T, const N: usize>(pub &'l mut [T; N]); + +impl<'l, O, E: ParseError<&'l str>, P: Parser<&'l str, O, E>, const N: usize> + Permutation<&'l str, [O; N], E> for PermutatedArray<'_, P, N> +{ + fn permutation(&mut self, input: &'l str) -> nom::IResult<&'l str, [O; N], E> { + // I'm so done. + // TODO implement permutations myself, so that there are no vectors inside + (0..N) + .into_iter() + .permutations(N) + .map(|perm| { + let mut input = input; + perm.into_iter() + .map(|i| { + let (rest, m) = self.0[i].parse(input).ok()?; + input = rest; + Some(m) + }) + .filter_map(std::convert::identity) + .collect_array() + .map(|arr| (input, arr)) + }) + .find_map(Result::ok) + .ok_or(nom::Err::Error(E::from_error_kind( + input, + nom::error::ErrorKind::Permutation, + ))) + } +} \ No newline at end of file From 9b6a4257c26f13a740bff947b7342e1bd35e440b Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Wed, 6 Mar 2024 23:32:31 +0200 Subject: [PATCH 02/98] ignoring vscode files --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cbab21b --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Build +/target + +# VSCode +.vscode/settings.json From 926c0e533d211c2a580d58ff6e5224f3713c9ffc Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Thu, 7 Mar 2024 00:07:34 +0200 Subject: [PATCH 03/98] mightly --- src/lexer.rs | 52 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/src/lexer.rs b/src/lexer.rs index 877b94c..1b6ba5f 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -14,8 +14,8 @@ use nom::{ use url::Url; use crate::{ - data::{AyanoBlock, Token, Tokens}, - util::PermutatedArray, + data::{AyanoBlock, ListType, Token, Tokens}, + util::{IteratorArrayCollectExt, PermutatedArray}, }; pub enum LexingError {} @@ -367,9 +367,51 @@ fn list<'source, E: ParseError<&'source str> + ContextError<&'source str>>( input = &input[(newline + 1)..]; } - let indent = non_whitespace; // how many bytes are to be dropped from the start of each line - input.lines().take_while(|line| preceded, many1, second)) - todo!(); + let indent = &input[..non_whitespace]; // how whitespace indent at the start of each line looks like + let (list_type, item_start: dyn &mut FnMut(usize) -> String) = match indent[non_whitespace..].chars().next() { + None => { + // there's not a single non-whitespace character -- it's impossible to properly parse a list here. + return Err(nom::Err::Error(E::from_error_kind("can't parse list at the empty location", ErrorKind::Eof))); + }, + Some('1') => { + // that's a numeric list! + (ListType::Num, |i| Some(format!("{}.", i+1))) + }, + Some('a') => { + // that's a latin list! + const SYMBOLS: [char; 26] = ('a'..='z').into_iter().collect_array().expect("There are exactly 2 characters in the alphabet"); + (ListType::Latin, move |i| Some(if i >= 26 { + Err(nom::Err::Error(E::from_error_kind("can't have for than 26 elemenets in a regular list", ErrorKind::Char))) + } else { + Ok(SYMBOLS[i]) + })) + }, + Some('а') => { + // that's a cyrillic list! + const SYMBOLS: [char; 26] = ('а'..='я').into_iter().collect_array().expect(""); + (ListType::Latin, move |i| Some(if i >= 26 { + Err(nom::Err::Error(E::from_error_kind("can't have for than 26 elemenets in a regular list", ErrorKind::Char))) + } else { + Ok(SYMBOLS[i]) + })) + } + Some('-') => { + // that's a bullet list! + (ListType::Num,()) + } + Some('I') => { + // that's a roman list! + unimplemented!() + }, + Some(c) => { + // that's an unknown list type + // FIXME not sure how to use these errors properly, I think I need to study more examples + return Err(nom::Err::Error(E::from_error_kind("unknown list type", ErrorKind::Char))); + }, + }; + input.lines().enumerate().take_while(|line: &&str| line.starts_with(indent)).map(|line| { + let line = line.fin_ + }) }), )(input)?; todo!() From 731e8084d14f6a4f584844fdcf893d57c1533385 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Fri, 8 Mar 2024 01:41:35 +0200 Subject: [PATCH 04/98] nightly implemented rest of the lexing parsers, although they still need: - more refining (I should establish more rigid specifications) - proper verbose error type. now I'm using.. I'm not sure what, I still need to learn more about nom's errors - test (obviously) minor changes in main modules, as while implementing lexing I start to understand what information exactly are we able to extract from input of each token --- src/data.rs | 17 +- src/gen/lab/meta.rs | 10 +- src/gen/lab/mod.rs | 28 +-- src/lexer.rs | 466 +++++++++++++++++++++++++++++++++++--------- src/util.rs | 2 +- 5 files changed, 405 insertions(+), 118 deletions(-) diff --git a/src/data.rs b/src/data.rs index be7745d..7cb4518 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1,5 +1,5 @@ use std::{ - borrow::Cow, + borrow::{Borrow, Cow}, ops::Deref, path::{Path, PathBuf}, }; @@ -123,12 +123,12 @@ pub enum Token<'source> { Table { header: Tokens<'source>, cells: Tokens<'source>, - caption: Option>, + caption: Option>>, ident: Option>, }, Figure { src_name: Pth<'source>, - caption: Option>, + caption: Option>>, ident: Option>, }, Href { @@ -140,14 +140,14 @@ pub enum Token<'source> { list_type: ListType, content: Tokens<'source>, }, - Formatted(Formatting, Tokens<'source>), + Formatted(Formatting, Box>), Text(Tokens<'source>), Paragraph(Font, Tx<'source>), InlineMathmode(Tx<'source>), Reference(Tx<'source>), FootNoteReference(Tx<'source>), FootNoteContent { - content: Tokens<'source>, + content: Box>, ident: Tx<'source>, }, CodeBlock { @@ -161,4 +161,11 @@ impl<'source> Token<'source> { pub fn text(text: impl Into>) -> Self { Self::Paragraph(Font::Normal, text.into()) } + + pub fn borrow_ref<'r>(&'r self) -> Token<'r> + where + 'source: 'r, + { + todo!() + } } diff --git a/src/gen/lab/meta.rs b/src/gen/lab/meta.rs index e9851b5..3458f05 100644 --- a/src/gen/lab/meta.rs +++ b/src/gen/lab/meta.rs @@ -17,7 +17,7 @@ pub enum MetaError { #[derive(Debug, Default)] pub struct SourceMeta<'source, Ayano> { - pub footnotes: HashMap<&'source str, Tokens<'source>>, + pub footnotes: HashMap<&'source str, Token<'source>>, pub refs: HashSet<&'source str>, pub ayano: Ayano, } @@ -71,7 +71,7 @@ where collect_iterable(header, dest)?; collect_iterable(cells, dest)?; if let Some(caption) = caption { - collect_iterable(caption, dest)?; + collect_single(caption, dest)?; } } Token::Figure { @@ -81,15 +81,15 @@ where } => { dest.refs.insert(ident); if let Some(caption) = caption { - collect_iterable(caption, dest)?; + collect_single(caption, dest)?; } } Token::Text(text) => { collect_iterable(text, dest)?; } Token::FootNoteContent { content, ident } => { - collect_iterable(content, dest)?; - dest.footnotes.insert(ident, content.borrowed()); + collect_single(content, dest)?; + dest.footnotes.insert(ident, content.borrow_ref()); } Token::Ayano(block) => { dest.ayano diff --git a/src/gen/lab/mod.rs b/src/gen/lab/mod.rs index 74e5eeb..367e41c 100644 --- a/src/gen/lab/mod.rs +++ b/src/gen/lab/mod.rs @@ -132,7 +132,7 @@ impl<'source, PE: PathEngine> OutputGenerator<'source, SourceMeta<'source, Ayano )?; if let Some(caption) = caption { output.write_str("\n\\caption{")?; - self.write_tokens_to(output, meta, context, caption)?; + self.write_to(output, meta, context, caption)?; output.write_str("}\n")?; } if let Some(ident) = ident.as_ref() { @@ -155,7 +155,7 @@ impl<'source, PE: PathEngine> OutputGenerator<'source, SourceMeta<'source, Ayano output.write_str("}")?; if let Some(caption) = caption.as_ref() { output.write_str("\\caption{")?; - self.write_tokens_to(output, meta, context, caption)?; + self.write_to(output, meta, context, caption)?; output.write_str("}\n")?; } if let Some(ident) = ident.as_ref() { @@ -207,7 +207,7 @@ impl<'source, PE: PathEngine> OutputGenerator<'source, SourceMeta<'source, Ayano | crate::data::Formatting::StrikeThrough => " }", }; output.write_str(start)?; - self.write_tokens_to(output, meta, context, content)?; + self.write_to(output, meta, context, content)?; output.write_str(end)?; } Token::InlineMathmode(content) => { @@ -238,7 +238,7 @@ impl<'source, PE: PathEngine> OutputGenerator<'source, SourceMeta<'source, Ayano // write out content if let Some(content) = meta.footnotes.get(&ident.borrow()) { write!(output, r"\footnotetext[{}]{{", i + 1)?; - self.write_tokens_to(output, meta, context, content)?; + self.write_to(output, meta, context, content)?; output.write_str("}")?; } } @@ -351,9 +351,9 @@ mod tests { test! {para2, Token::Paragraph(Font::Caption, "content, content!!!".into()), r"\capfnt content, content!!!"} // Formatting - test! {formatting1, Token::Formatted(Formatting::Bold, tokens!(text!("some text, idk"))), r"\textbf{ \fnt some text, idk }"} - test! {formatting2, Token::Formatted(Formatting::Italic, tokens!(text!("some text, idk"))), r"\textit{ \fnt some text, idk }"} - test! {formatting3, Token::Formatted(Formatting::StrikeThrough, tokens!(text!("some text, idk"))), r"\st{ \fnt some text, idk }"} + test! {formatting1, Token::Formatted(Formatting::Bold, Box::new(text!("some text, idk"))), r"\textbf{ \fnt some text, idk }"} + test! {formatting2, Token::Formatted(Formatting::Italic, Box::new(text!("some text, idk"))), r"\textit{ \fnt some text, idk }"} + test! {formatting3, Token::Formatted(Formatting::StrikeThrough, Box::new(text!("some text, idk"))), r"\st{ \fnt some text, idk }"} // Hrefs test! {href1, Token::Href { url: "https://www.github.com/Dzuchun".parse().unwrap(), display: "My gh page".into() }, r"\href{https://www.github.com/Dzuchun}{My gh page}"} @@ -379,14 +379,14 @@ mod tests { test! {inline_mathmode, Token::InlineMathmode(r"\dfrac{\partial x}{\partial t}".into()), r"$ \dfrac{\partial x}{\partial t} $"} // Figures - test! {fig1, Token::Figure { src_name: Path::new("apple.jpg").into(), caption: Some(tokens![text!("a very realistic-looking apple")]), ident: None }, r" + test! {fig1, Token::Figure { src_name: Path::new("apple.jpg").into(), caption: Some(Box::new(text!("a very realistic-looking apple"))), ident: None }, r" \begin{figure}[h!] \centering \includegraphics[width = 0.9 \textwidth]{apple.jpg} \caption{\fnt a very realistic-looking apple} \end{figure} "} - test! {fig2, Token::Figure { src_name: Path::new("schema1.jpg").into(), caption: Some(tokens![text!("A schema providing a bunch of very important info on quantum refurbalidzer function")]), ident: Some("schema-1".into()) }, r" + test! {fig2, Token::Figure { src_name: Path::new("schema1.jpg").into(), caption: Some(Box::new(text!("A schema providing a bunch of very important info on quantum refurbalidzer function"))), ident: Some("schema-1".into()) }, r" \begin{figure}[h!] \centering \includegraphics[width = 0.9 \textwidth]{schema1.jpg} @@ -405,7 +405,7 @@ mod tests { text!("cell_11"),text!("cell_12"),text!("cell_13"), text!("cell_21"),text!("cell_22"),text!("cell_23") ], - caption: Some(tokens![text!("caption...")]), + caption: Some(Box::new(text!("caption..."))), ident: Some("example".into()) }, r" \begin{table}[h!] @@ -469,7 +469,7 @@ mod tests { // Footnotes test! {footnote_single, Token::Text(tokens![ Token::FootNoteReference("explanation".into()), - Token::FootNoteContent { content: tokens![text!("42")], ident: "explanation".into() } + Token::FootNoteContent { content: Box::new(text!("42")), ident: "explanation".into() } ]), r" \footnotemark[1] \footnotetext[1]{\fnt 42} @@ -482,9 +482,9 @@ mod tests { Token::FootNoteReference("explanation2".into()), text!(", and the third"), Token::FootNoteReference("explanation3".into()), - Token::FootNoteContent { content: tokens![text!("There are things in this world that's you're not meant to see")], ident: "explanation1".into() }, - Token::FootNoteContent { content: tokens![text!("now the voice of a deity permeates")], ident: "explanation2".into() }, - Token::FootNoteContent { content: tokens![text!("see notes 1 and 2")], ident: "explanation3".into() }, + Token::FootNoteContent { content: Box::new(text!("There are things in this world that's you're not meant to see")), ident: "explanation1".into() }, + Token::FootNoteContent { content: Box::new(text!("now the voice of a deity permeates")), ident: "explanation2".into() }, + Token::FootNoteContent { content: Box::new(text!("see notes 1 and 2")), ident: "explanation3".into() }, text!(". But notes will not be duplicated here!"), Token::FootNoteReference("explanation1".into()) diff --git a/src/lexer.rs b/src/lexer.rs index 1b6ba5f..5d70744 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -1,15 +1,19 @@ -use std::{borrow::Cow, path::Path}; +use std::{ + borrow::{Borrow, Cow}, + path::Path, +}; +use itertools::Itertools; use nom::{ branch::{alt, permutation}, bytes::complete::{is_not, tag, take_till1, take_until1}, character::complete::{alpha1, alphanumeric1, char, digit1, multispace0, newline, space0}, combinator::{all_consuming, map_res, opt, recognize}, error::{context, ContextError, ErrorKind, FromExternalError, ParseError}, - multi::{fold_many1, many1_count}, - number::{self, complete::float}, + multi::{fold_many1, many0, many1_count, separated_list1}, + number::complete::float, sequence::{delimited, preceded, terminated, tuple}, - FindToken, IResult, Parser, + IResult, Parser, }; use url::Url; @@ -22,20 +26,79 @@ pub enum LexingError {} type LexingResult<'source, O = Token<'source>> = Result<(&'source str, O), LexingError>; +fn _tokens_template< + 'source, + E: ParseError<&'source str> + + ContextError<&'source str> + + FromExternalError<&'source str, &'static str>, + K: FnMut(&'source str) -> IResult<&'source str, Token<'source>, E>, +>( + tokens_kernel: impl Fn() -> K, +) -> impl FnMut(&'source str) -> IResult<&'source str, Token<'source>, E> { + move |input| { + let (rest, first_token) = tokens_kernel()(input)?; + let (_, mut rest_tokens) = all_consuming(many0(tokens_kernel()))(rest)?; + if rest_tokens.is_empty() { + Ok(("", first_token)) + } else { + rest_tokens.insert(0, first_token); + Ok(("", Token::Text(Tokens::new(rest_tokens)))) + } + } +} + /// Lexes input into tokens -pub fn lex<'source, E: ParseError<&'source str>>( +pub fn lex< + 'source, + E: ParseError<&'source str> + + ContextError<&'source str> + + FromExternalError<&'source str, &'static str>, +>( input: &'source str, -) -> IResult<&'source str, Tokens<'source>, E> { - todo!() +) -> IResult<&'source str, Token<'source>, E> { + _tokens_template(|| { + alt(( + div_page, + header, + equation, + table, + figure, + href, + code_block, + ayano, + list, + formatting, + inline_math, + reference, + footnote_content, + footnote_reference, + paragraph, + )) + })(input) } /// Lexes input into tokens, but forbids some types of tokens, like figures, tables, etc. /// /// Intended to be used to lex captions -pub fn limited_lex<'source, E: ParseError<&'source str>>( +pub fn limited_lex< + 'source, + E: ParseError<&'source str> + + ContextError<&'source str> + + FromExternalError<&'source str, &'static str>, +>( input: &'source str, -) -> IResult<&'source str, Tokens<'source>, E> { - todo!() +) -> IResult<&'source str, Token<'source>, E> { + _tokens_template(|| { + alt(( + href, + ayano, + formatting, + inline_math, + reference, + footnote_reference, + paragraph, + )) + })(input) } // TODO come up with some sort of clever trait implementation to optimize this out @@ -69,7 +132,12 @@ fn parse_args<'parser, 'source, E: ParseError<&'source str>, const ARGS: usize>( } // ARGUMENT KERNELS -fn caption_kernel<'source, E: ParseError<&'source str>>( +fn caption_kernel< + 'source, + E: ParseError<&'source str> + + ContextError<&'source str> + + FromExternalError<&'source str, &'static str>, +>( input: &'source str, ) -> IResult<&'source str, &'source str, E> { delimited( @@ -110,7 +178,12 @@ fn parse_ref<'source, E: ParseError<&'source str>>( _parse_arg("ref", ref_kernel)(input) } -fn parse_caption<'source, E: ParseError<&'source str>>( +fn parse_caption< + 'source, + E: ParseError<&'source str> + + ContextError<&'source str> + + FromExternalError<&'source str, &'static str>, +>( input: &'source str, ) -> IResult<&'source str, &'source str, E> { _parse_arg("caption", caption_kernel)(input) @@ -196,13 +269,67 @@ fn equation<'source, E: ParseError<&'source str> + ContextError<&'source str>>( )) } -fn table<'source, E: ParseError<&'source str> + ContextError<&'source str>>( +fn table< + 'source, + E: ParseError<&'source str> + + ContextError<&'source str> + + FromExternalError<&'source str, &'static str>, +>( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { - todo!() + let (rest, (header, _, cells, [ident, caption])): ( + &str, + (Vec>, _, Vec>, [Option<&str>; 2]), + ) = context( + "table", + tuple(( + // first, parse the header + preceded( + char('\n'), + delimited( + char('|'), + separated_list1(char('|'), delimited(space0, limited_lex, space0)), + char('|'), + ), + ), + tuple((space0, char('\n'), take_until1("\n"))), + // next, parse the cells + preceded( + char('\n'), + delimited( + char('|'), + separated_list1(char('|'), delimited(space0, limited_lex, space0)), + char('|'), + ), + ), + // finally, here come arguments + parse_args([&mut parse_ref, &mut parse_caption]), + )), + )(input)?; + let caption = if let Some(caption) = caption { + Some(Box::new( + context("table", all_consuming(limited_lex))(caption)?.1, + )) + } else { + None + }; + Ok(( + rest, + Token::Table { + header: Tokens::new(header), + cells: Tokens::new(cells), + caption: caption, + ident: ident.map(Cow::from), + }, + )) } -fn figure<'source, E: ParseError<&'source str> + ContextError<&'source str>>( +fn figure< + 'source, + E: ParseError<&'source str> + + ContextError<&'source str> + + FromExternalError<&'source str, &'static str>, +>( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { // TODO add width parameter to figure, I guess @@ -224,7 +351,8 @@ fn figure<'source, E: ParseError<&'source str> + ContextError<&'source str>>( Some(all_consuming(limited_lex)(caption)?.1) } None => None, - }; + } + .map(Box::new); let token = Token::Figure { src_name: Path::new(path).into(), caption, @@ -286,7 +414,12 @@ fn code_block<'source, E: ParseError<&'source str> + ContextError<&'source str>> )) } -fn ayano<'source, E: ParseError<&'source str> + ContextError<&'source str>>( +fn ayano< + 'source, + E: ParseError<&'source str> + + ContextError<&'source str> + + FromExternalError<&'source str, &'static str>, +>( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { // python, Ayano * ~ # @@ -351,105 +484,252 @@ fn ayano<'source, E: ParseError<&'source str> + ContextError<&'source str>>( )) } -fn list<'source, E: ParseError<&'source str> + ContextError<&'source str>>( - input: &'source str, +macro_rules! char_array { + ($from:literal .. $to:literal, $length:literal, $alphabet:literal) => {{ + static CHARS: ::once_cell::sync::Lazy<[char; $length]> = + ::once_cell::sync::Lazy::new(|| { + ($from..=$to).into_iter().collect_array().expect(&format!( + "There are exactly {} characters in {} alphabet", + $length, $alphabet + )) + }); + &*CHARS + }}; +} + +fn _list_inner< + 'source, + E: ParseError<&'source str> + + ContextError<&'source str> + + FromExternalError<&'source str, &'static str>, +>( + mut input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { - let (rest, _) = context( - "list", - preceded(char('\n'), |mut input: &str| { - let mut newline = 1; - let mut non_whitespace = 0; - while newline > non_whitespace { - newline = input.find('\n').unwrap_or(input.len()); - non_whitespace = input - .find(|c: char| !c.is_whitespace()) - .unwrap_or(input.len()); - input = &input[(newline + 1)..]; + let mut newline = 1; + let mut non_whitespace = 0; + while newline > non_whitespace { + newline = input.find('\n').unwrap_or(input.len()); + non_whitespace = input + .find(|c: char| !c.is_whitespace()) + .unwrap_or(input.len()); + input = &input[(newline + 1)..]; + } + + let indent = &input[..non_whitespace]; // how whitespace indent at the start of each line looks like + let (list_type, item_start): (ListType, fn(usize) -> Option>) = + match indent[non_whitespace..].chars().next() { + None => { + // there's not a single non-whitespace character -- it's impossible to properly parse a list here. + return Err(nom::Err::Error(E::from_error_kind( + "can't parse list at the empty location", + ErrorKind::Eof, + ))); } - - let indent = &input[..non_whitespace]; // how whitespace indent at the start of each line looks like - let (list_type, item_start: dyn &mut FnMut(usize) -> String) = match indent[non_whitespace..].chars().next() { - None => { - // there's not a single non-whitespace character -- it's impossible to properly parse a list here. - return Err(nom::Err::Error(E::from_error_kind("can't parse list at the empty location", ErrorKind::Eof))); - }, - Some('1') => { - // that's a numeric list! - (ListType::Num, |i| Some(format!("{}.", i+1))) - }, - Some('a') => { - // that's a latin list! - const SYMBOLS: [char; 26] = ('a'..='z').into_iter().collect_array().expect("There are exactly 2 characters in the alphabet"); - (ListType::Latin, move |i| Some(if i >= 26 { - Err(nom::Err::Error(E::from_error_kind("can't have for than 26 elemenets in a regular list", ErrorKind::Char))) - } else { - Ok(SYMBOLS[i]) - })) - }, - Some('а') => { - // that's a cyrillic list! - const SYMBOLS: [char; 26] = ('а'..='я').into_iter().collect_array().expect(""); - (ListType::Latin, move |i| Some(if i >= 26 { - Err(nom::Err::Error(E::from_error_kind("can't have for than 26 elemenets in a regular list", ErrorKind::Char))) - } else { - Ok(SYMBOLS[i]) - })) - } - Some('-') => { - // that's a bullet list! - (ListType::Num,()) - } - Some('I') => { - // that's a roman list! - unimplemented!() - }, - Some(c) => { - // that's an unknown list type - // FIXME not sure how to use these errors properly, I think I need to study more examples - return Err(nom::Err::Error(E::from_error_kind("unknown list type", ErrorKind::Char))); - }, - }; - input.lines().enumerate().take_while(|line: &&str| line.starts_with(indent)).map(|line| { - let line = line.fin_ - }) - }), - )(input)?; - todo!() + Some('1') => { + // that's a numeric list! + (ListType::Num, |i| Some(format!("{}.", i + 1).into())) + } + Some('a') => { + // that's a latin list! + (ListType::Latin, move |i| { + char_array!('a'..'z', 26, "latin") + .get(i) + .map(char::to_string) + .map(Cow::from) + }) + } + Some('а') => { + // that's a cyrillic list! + (ListType::Latin, move |i| { + char_array!('а'..'я', 32, "cyrillic") + .get(i) + .map(char::to_string) + .map(Cow::from) + }) + } + Some('-') => { + // that's a bullet list! + (ListType::Num, |_| Some("-".into())) + } + Some('I') => { + // that's a roman list! + unimplemented!("Roman numerals list is not supported yet") + // TODO add support, I guess + } + Some(_c) => { + // that's an unknown list type + // FIXME not sure how to use these errors properly, I think I need to study more examples + return Err(nom::Err::Error(E::from_error_kind( + "unknown list type", + ErrorKind::Char, + ))); + } + }; + let items: Vec<_> = input + .lines() + .take_while(|line: &&str| line.starts_with(indent)) + .enumerate() + .map(|(i, line): (usize, &str)| { + let line = line.trim_start_matches::<&str>( + item_start(i) + .ok_or(nom::Err::Error(E::from_error_kind( + "List has too much elements", + ErrorKind::Fix, + )))? + .borrow(), + ); + let (_, item) = all_consuming(limited_lex::<'_, E>)(line)?; + Ok::<_, nom::Err>(item) + }) + .try_collect()?; + let content = Tokens::new(items); + Ok(("", Token::List { list_type, content })) } -fn formatting<'source, E: ParseError<&'source str> + ContextError<&'source str>>( +fn list< + 'source, + E: ParseError<&'source str> + + ContextError<&'source str> + + FromExternalError<&'source str, &'static str>, +>( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { - todo!() + context("list", preceded(char('\n'), _list_inner))(input) } -fn paragraph<'source, E: ParseError<&'source str> + ContextError<&'source str>>( + +fn _formatting_inner< + 'source, + E: ParseError<&'source str> + + ContextError<&'source str> + + FromExternalError<&'source str, &'static str>, +>( + input: &'source str, + style: crate::data::Formatting, +) -> Result, nom::Err> { + let (_, content) = all_consuming(limited_lex)(input)?; + Ok(Token::Formatted(style, Box::new(content))) +} + +fn formatting< + 'source, + E: ParseError<&'source str> + + ContextError<&'source str> + + FromExternalError<&'source str, &'static str>, +>( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { - todo!() + use crate::data::Formatting; + context( + "formatting", + alt(( + delimited( + tag("**"), + take_until1("**").and_then(all_consuming(limited_lex)), + tag("**"), + ) + .map(|input| (Box::new(input), Formatting::Bold)), + delimited( + tag("__"), + take_until1("__").and_then(all_consuming(limited_lex)), + tag("__"), + ) + .map(|input| (Box::new(input), Formatting::Bold)), + delimited( + tag("*"), + take_until1("*").and_then(all_consuming(limited_lex)), + tag("*"), + ) + .map(|input| (Box::new(input), Formatting::Italic)), + delimited( + tag("_"), + take_until1("__").and_then(all_consuming(limited_lex)), + tag("_"), + ) + .map(|input| (Box::new(input), Formatting::Italic)), + delimited( + tag("~~"), + take_until1("~~").and_then(all_consuming(limited_lex)), + tag("~~"), + ) + .map(|input| (Box::new(input), Formatting::StrikeThrough)), + )), + ) + .map(|(token, style)| Token::Formatted(style, token)) + .parse(input) } + fn inline_math<'source, E: ParseError<&'source str> + ContextError<&'source str>>( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { - todo!() + let (rest, formula) = context( + "inline math", + delimited(char('$'), take_until1("$"), char('$')), + )(input)?; + Ok((rest, Token::InlineMathmode(formula.into()))) } + fn reference<'source, E: ParseError<&'source str> + ContextError<&'source str>>( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { - todo!() + let (rest, ident) = context( + "reference", + delimited(tag("[@"), take_until1("]"), char(']')), + )(input)?; + Ok((rest, Token::Reference(ident.into()))) } + fn footnote_reference<'source, E: ParseError<&'source str> + ContextError<&'source str>>( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { - todo!() -} -fn footnote_content<'source, E: ParseError<&'source str> + ContextError<&'source str>>( - input: &'source str, -) -> IResult<&'source str, Token<'source>, E> { - todo!() + let (rest, ident) = context( + "footnote reference", + delimited(tag("[^"), take_until1("]"), char(']')), + )(input)?; + Ok((rest, Token::FootNoteReference(ident.into()))) } -fn tokens<'source, E: ParseError<&'source str> + ContextError<&'source str>>( +fn footnote_content< + 'source, + E: ParseError<&'source str> + + ContextError<&'source str> + + FromExternalError<&'source str, &'static str>, +>( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { - todo!() + let (rest, (ident, content)) = context( + "footnote context", + delimited(tag("[^"), take_until1("]:"), tag("]:")).and(preceded( + space0, + take_until1("\n").and_then(all_consuming(limited_lex)), + )), + ) + .parse(input)?; + Ok(( + rest, + Token::FootNoteContent { + content: Box::new(content), + ident: ident.into(), + }, + )) +} + +fn paragraph<'source, E: ParseError<&'source str> + ContextError<&'source str>>( + input: &'source str, +) -> IResult<&'source str, Token<'source>, E> { + // This is the most tricky component. It must stop matching AS SOON AS any sort-of syntax is encountered. + // Should be noted, that this parsed is forward-blind, meaning it has no idea what element EXACTLY will be matched next. + let bad_index = input + .char_indices() + .filter_map(|(ind, c)| matches!(c, '|' | '\n' | '$' | '`').then_some(ind)) + .find(|&ind| { + ["|", "||||||", "\n#", "$", "$$", "`", "```"] + .into_iter() + .any(|p| input[ind..].starts_with(p)) + }) + .unwrap_or(input.len()); + let (this_text, rest) = input.split_at(bad_index); + Ok(( + rest, + Token::Paragraph(crate::data::Font::Normal, this_text.into()), + )) } diff --git a/src/util.rs b/src/util.rs index 4ee10a8..d251a46 100644 --- a/src/util.rs +++ b/src/util.rs @@ -366,4 +366,4 @@ impl<'l, O, E: ParseError<&'l str>, P: Parser<&'l str, O, E>, const N: usize> nom::error::ErrorKind::Permutation, ))) } -} \ No newline at end of file +} From eadddae9abc06f902f58d05721ab58503e71ac5a Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Sat, 9 Mar 2024 02:38:39 +0200 Subject: [PATCH 05/98] nightly - I think I understand errors now - Rewrote a couple of parsers in terms of pure parsers --- src/lexer.rs | 649 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 414 insertions(+), 235 deletions(-) diff --git a/src/lexer.rs b/src/lexer.rs index 5d70744..079d537 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -6,11 +6,11 @@ use std::{ use itertools::Itertools; use nom::{ branch::{alt, permutation}, - bytes::complete::{is_not, tag, take_till1, take_until1}, + bytes::complete::{escaped, is_not, tag, take_till1, take_until1, take_while}, character::complete::{alpha1, alphanumeric1, char, digit1, multispace0, newline, space0}, - combinator::{all_consuming, map_res, opt, recognize}, + combinator::{all_consuming, iterator, map_res, not, opt, recognize, success}, error::{context, ContextError, ErrorKind, FromExternalError, ParseError}, - multi::{fold_many1, many0, many1_count, separated_list1}, + multi::{fold_many1, many1_count, separated_list1}, number::complete::float, sequence::{delimited, preceded, terminated, tuple}, IResult, Parser, @@ -19,245 +19,175 @@ use url::Url; use crate::{ data::{AyanoBlock, ListType, Token, Tokens}, - util::{IteratorArrayCollectExt, PermutatedArray}, + util::IteratorArrayCollectExt, }; -pub enum LexingError {} +// Conventions: +// - parsers should only parse empty characters with `empty` +// - parsers should only parse empty space at the beginning, not the end +// - newline parsing should be carefully managed -type LexingResult<'source, O = Token<'source>> = Result<(&'source str, O), LexingError>; +// TODO probably move that to util or th +// I won't include the entire tuple operation library, while I can perfectly implement this myself. +mod remove_last_unit { -fn _tokens_template< - 'source, - E: ParseError<&'source str> - + ContextError<&'source str> - + FromExternalError<&'source str, &'static str>, - K: FnMut(&'source str) -> IResult<&'source str, Token<'source>, E>, ->( - tokens_kernel: impl Fn() -> K, -) -> impl FnMut(&'source str) -> IResult<&'source str, Token<'source>, E> { - move |input| { - let (rest, first_token) = tokens_kernel()(input)?; - let (_, mut rest_tokens) = all_consuming(many0(tokens_kernel()))(rest)?; - if rest_tokens.is_empty() { - Ok(("", first_token)) - } else { - rest_tokens.insert(0, first_token); - Ok(("", Token::Text(Tokens::new(rest_tokens)))) - } + pub trait Trait { + type Output; + fn remove_last_unit(self) -> Self::Output; } + + macro_rules! impl_tuple_remove_last_unit { + {$A0:ident} => { + impl<$A0> Trait for ($A0, ()) { type Output = ($A0, ); + + #[allow(non_snake_case)] + #[inline] + fn remove_last_unit(self) -> Self::Output { + let ($A0, _) = self; + ($A0, ) + } + } + }; + {$A0:ident $($Ai:ident) +} => { + impl<$A0, $($Ai), +> Trait for ($A0, $($Ai), +, ()) { + type Output = ($A0, $($Ai),+); + + #[allow(non_snake_case)] + #[inline] + fn remove_last_unit(self) -> Self::Output { + let ($A0, $($Ai), *, ()) = self; + ($A0, $($Ai), +) + } + } + + impl_tuple_remove_last_unit!{$($Ai) +} + }; + } + + impl_tuple_remove_last_unit! {A B C D E F G H I J K L M N O P Q R S T U V W} } -/// Lexes input into tokens -pub fn lex< - 'source, - E: ParseError<&'source str> - + ContextError<&'source str> - + FromExternalError<&'source str, &'static str>, ->( - input: &'source str, -) -> IResult<&'source str, Token<'source>, E> { - _tokens_template(|| { - alt(( - div_page, - header, - equation, - table, - figure, - href, - code_block, - ayano, - list, - formatting, - inline_math, - reference, - footnote_content, - footnote_reference, - paragraph, - )) - })(input) -} - -/// Lexes input into tokens, but forbids some types of tokens, like figures, tables, etc. -/// -/// Intended to be used to lex captions -pub fn limited_lex< - 'source, - E: ParseError<&'source str> - + ContextError<&'source str> - + FromExternalError<&'source str, &'static str>, ->( - input: &'source str, -) -> IResult<&'source str, Token<'source>, E> { - _tokens_template(|| { - alt(( - href, - ayano, - formatting, - inline_math, - reference, - footnote_reference, - paragraph, - )) - })(input) -} - -// TODO come up with some sort of clever trait implementation to optimize this out -fn parse_args<'parser, 'source, E: ParseError<&'source str>, const ARGS: usize>( - args: [&'parser mut dyn FnMut(&'source str) -> IResult<&'source str, &'source str, E>; ARGS], -) -> impl FnMut(&'source str) -> IResult<&'source str, [Option<&'source str>; ARGS], E> + 'parser { - let mut parsers: [_; ARGS] = args.map(|p| { - move |input| { - let p = &mut *p; - let (rest, value) = opt(tuple(( - space0, - p, - space0, - alt((char(','), nom::combinator::peek(char('}')))), - )))(input)?; - Ok::<(&str, Option<&str>), nom::Err>((rest, value.map(|(_, value, _, _)| value))) - } - }); - move |input| { - preceded( - opt(tuple((space0, char('\n')))), - tuple(( - opt(char('\n')), - char('{'), - permutation(PermutatedArray(&mut parsers)), - char('}'), +/// Generates argument parser that accounts for all necessary syntax +macro_rules! parse_args { + [$($argument_name:literal = $argument_kernel:expr), +] => { + tuple(( + space0, + tag("\n{"), // NOTE: arguments must start from an empty line + permutation(( + $( + opt(tuple(( + space0, + tuple((tag($argument_name), space0, char('='), space0, context("arg", context($argument_name, $argument_kernel)))), + space0, + alt((char(','), nom::combinator::peek(char('}')))), // this can have unexpected consequences, but whatever :idk: + ))).map(|value| value.map(|(_, (_, _, _, _, v), _, _)| v)) + ), +, + success(()) )), - )(input) - .map(|(rest, (_, _, out, _))| (rest, out)) - } + char('}'), + )).map(|(_, _, out, _)| <_ as remove_last_unit::Trait>::remove_last_unit(out)) + }; } -// ARGUMENT KERNELS +/// Parses caption (just the literal) part +/// +/// Quite tricky task, as there can be double quotes -- but escaped. +/// Apart from single, unescaped quotes, caption can be ANYTHING. +/// There seems to be just the tool for a job -- `escaped` parser +/// TODO FIXME add a mention about caption actually TERMINATING after first unescaped double-quote, and where exactly escaping backslashes are removed fn caption_kernel< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, &'static str>, + + FromExternalError<&'source str, KyomatoLexErrorKind>, >( input: &'source str, -) -> IResult<&'source str, &'source str, E> { - delimited( - char('"'), - recognize(fold_many1( - alt((tag("\\\""), is_not("\""))), - || (), - |_, _| (), - )) - // input slice between quotes must be possible to consume fully, but not actually parsed yet, due to - // FIXME limitation of argument parsers -- they must return &'source str for now. - .and_then(recognize(all_consuming(limited_lex::<'source, E>))), - char('"'), - )(input) +) -> IResult<&'source str, Token<'source>, E> { + // FIXME add `terminated(_, space0)` to all `all_consuming` + escaped(not(char('"')), '\\', char('"')) + .and_then(all_consuming(inner_lex::<'source, E>)) + .parse(input) } -fn ref_kernel<'source, E: ParseError<&'source str>>( - input: &'source str, -) -> IResult<&'source str, &'source str, E> { - alphanumeric1(input) -} - -// META ARG PARSERS -fn _parse_arg<'source, E: ParseError<&'source str>>( - name: &'static str, - kernel: fn(&'source str) -> IResult<&'source str, &'source str, E>, -) -> impl FnMut(&'source str) -> IResult<&'source str, &'source str, E> { - move |input| { - let (rest, (_, _, _, _, ident)) = - tuple((tag(name), space0, char('='), space0, kernel))(input)?; - Ok((rest, ident)) - } -} - -fn parse_ref<'source, E: ParseError<&'source str>>( - input: &'source str, -) -> IResult<&'source str, &'source str, E> { - _parse_arg("ref", ref_kernel)(input) -} - -fn parse_caption< - 'source, - E: ParseError<&'source str> - + ContextError<&'source str> - + FromExternalError<&'source str, &'static str>, ->( - input: &'source str, -) -> IResult<&'source str, &'source str, E> { - _parse_arg("caption", caption_kernel)(input) -} - -fn __recognize_float<'source, E: ParseError<&'source str>>( - input: &'source str, -) -> IResult<&'source str, &'source str, E> { - recognize::<&str, f32, E, _>(float::<&str, E>)(input) -} - -fn parse_width<'source, E: ParseError<&'source str>>( - input: &'source str, -) -> IResult<&'source str, &'source str, E> { - _parse_arg("width", __recognize_float)(input) -} - -// TOKEN PARSERSu -// CONVENTION: each token should excessively match space after it - +/// Parses page divider +/// TODO update it's definition in the manual fn div_page<'source, E: ParseError<&'source str> + ContextError<&'source str>>( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { - let (rest, _) = context( - "page divider", - tuple((multispace0, tag("||||||"), space0, char('\n'))), - )(input)?; - Ok((rest, Token::PageDiv)) + context("page divider", tuple((multispace0, tag("------")))) + .map(|_| Token::PageDiv) + .parse(input) } +/// Parses header +/// +/// - Always starts from a blank line +/// - Comprised of 1-6 '#'-symbols, and includes everything up until last empty space before line break fn header< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, &'static str>, + + FromExternalError<&'source str, KyomatoLexErrorKind>, >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { - let (rest, (_, _, order, _, content)) = context( + context( "header", tuple(( - multispace0, + multispace0::<&'source str, E>, newline, map_res(many1_count(char('#')), |count| { if count <= 6 { Ok(count) } else { - Err("headers can't be more than 6-fold") + Err(KyomatoLexErrorKind::TooDeepHeader { fold: count }) } }), space0, take_until1("\n"), )), - )(input)?; - Ok(( - rest, - Token::Header { - order, - content: content.into(), - }, - )) + ) + .map(|(_, _, order, _, content)| Token::Header { + order, + content: content.trim_end().into(), // kinda sad I'm forced to do this here, but whatever + }) + .parse(input) } -fn equation<'source, E: ParseError<&'source str> + ContextError<&'source str>>( +/// Parses display math (equation) +/// +/// - Always starts from a blank line with "$$" tag +/// - Continues up until another unescaped "$$" on a beginning of the line is encountered +/// - Possible "ref" argument (to refer to this equation) is matched afterwards +fn equation< + 'source, + E: ParseError<&'source str> + ContextError<&'source str> + FromExternalError<&'source str, E>, +>( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { - let (rest, (_, _, content, _, [ident])) = context( + let Some(ind) = input + .char_indices() + .filter_map(|(ind, c)| (c == '\n').then_some(ind)) + .find(|ind| input[*ind..].starts_with("\n$$")) + else { + return Err(nom::Err::Error(E::from_external_error( + input, + ErrorKind::CrLf, + KyomatoLexErrorKind::BadSyntax { + reason: "Display mathmode should start with a new line", + }, + ))); + }; + + let trimmed_input = &input[ind]; + let (rest, (_, _, content, _, (ident,))) = context( "display mathmode", tuple(( multispace0, tag("$$"), take_until1("\n$$"), tag("\n$$"), - parse_args([&mut parse_ref]), + parse_args!["ref" = alphanumeric1], )), )(input)?; Ok(( @@ -273,14 +203,11 @@ fn table< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, &'static str>, + + FromExternalError<&'source str, KyomatoLexErrorKind>, >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { - let (rest, (header, _, cells, [ident, caption])): ( - &str, - (Vec>, _, Vec>, [Option<&str>; 2]), - ) = context( + let (rest, (header, _, cells, (ident, caption))) = context( "table", tuple(( // first, parse the header @@ -288,7 +215,7 @@ fn table< char('\n'), delimited( char('|'), - separated_list1(char('|'), delimited(space0, limited_lex, space0)), + separated_list1(char('|'), delimited(space0, inner_lex, space0)), char('|'), ), ), @@ -298,27 +225,20 @@ fn table< char('\n'), delimited( char('|'), - separated_list1(char('|'), delimited(space0, limited_lex, space0)), + separated_list1(char('|'), delimited(space0, inner_lex, space0)), char('|'), ), ), // finally, here come arguments - parse_args([&mut parse_ref, &mut parse_caption]), + parse_args!["ref" = alphanumeric1, "caption" = caption_kernel], )), )(input)?; - let caption = if let Some(caption) = caption { - Some(Box::new( - context("table", all_consuming(limited_lex))(caption)?.1, - )) - } else { - None - }; Ok(( rest, Token::Table { header: Tokens::new(header), cells: Tokens::new(cells), - caption: caption, + caption: caption.map(Box::new), ident: ident.map(Cow::from), }, )) @@ -328,34 +248,28 @@ fn figure< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, &'static str>, + + FromExternalError<&'source str, KyomatoLexErrorKind>, >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { // TODO add width parameter to figure, I guess - let (rest, (_, _, path, _, [ident, caption, _width])) = context( + let (rest, (_, _, path, _, (ident, caption, _width))) = context( "figure", tuple(( multispace0, tag("![["), take_until1("]]"), tag("]]"), - parse_args([&mut parse_ref, &mut parse_caption, &mut parse_width]), + parse_args![ + "ref" = alphanumeric1, + "caption" = caption_kernel, + "width" = float + ], )), )(input)?; - let caption = match caption { - Some(caption) => - // All caption must be consumed for successful lex - // any error occurring during caption parsing, must be propagated - { - Some(all_consuming(limited_lex)(caption)?.1) - } - None => None, - } - .map(Box::new); let token = Token::Figure { src_name: Path::new(path).into(), - caption, + caption: caption.map(Box::new), ident: ident.map(Cow::from), }; Ok((rest, token)) @@ -418,11 +332,12 @@ fn ayano< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, &'static str>, + + FromExternalError<&'source str, KyomatoLexErrorKind>, >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { // python, Ayano * ~ # + // TODO add tests for escaping backslashes removal (generation-side) let (rest, (_, _, _, _, _, _, _, (display, insert, ident, is_static), _, code, _)) = context( "ayano", @@ -469,7 +384,7 @@ fn ayano< // - outer Some means this block is to be displayed // - inner None means this block has no display caption // - inner Some means this block has display caption - let display: Option> = display.map(|(_, _, caption)| caption); // TODO add code captions support + let display: Option>> = display.map(|(_, _, caption)| caption); // TODO add code captions support let insert: Option<&'source Path> = insert.map(|(_, _, path, _)| Path::new(path)); let _ident: Option = ident.map(|(_, _, ident, _)| ident); // TODO add support for codeblock identifiers let code: Cow<'_, str> = Cow::from(code); @@ -501,7 +416,7 @@ fn _list_inner< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, &'static str>, + + FromExternalError<&'source str, KyomatoLexErrorKind>, >( mut input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { @@ -578,7 +493,7 @@ fn _list_inner< )))? .borrow(), ); - let (_, item) = all_consuming(limited_lex::<'_, E>)(line)?; + let (_, item) = all_consuming(inner_lex::<'_, E>)(line)?; Ok::<_, nom::Err>(item) }) .try_collect()?; @@ -590,7 +505,7 @@ fn list< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, &'static str>, + + FromExternalError<&'source str, KyomatoLexErrorKind>, >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { @@ -601,12 +516,12 @@ fn _formatting_inner< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, &'static str>, + + FromExternalError<&'source str, KyomatoLexErrorKind>, >( input: &'source str, style: crate::data::Formatting, ) -> Result, nom::Err> { - let (_, content) = all_consuming(limited_lex)(input)?; + let (_, content) = all_consuming(inner_lex)(input)?; Ok(Token::Formatted(style, Box::new(content))) } @@ -614,7 +529,7 @@ fn formatting< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, &'static str>, + + FromExternalError<&'source str, KyomatoLexErrorKind>, >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { @@ -624,31 +539,31 @@ fn formatting< alt(( delimited( tag("**"), - take_until1("**").and_then(all_consuming(limited_lex)), + take_until1("**").and_then(all_consuming(inner_lex)), tag("**"), ) .map(|input| (Box::new(input), Formatting::Bold)), delimited( tag("__"), - take_until1("__").and_then(all_consuming(limited_lex)), + take_until1("__").and_then(all_consuming(inner_lex)), tag("__"), ) .map(|input| (Box::new(input), Formatting::Bold)), delimited( tag("*"), - take_until1("*").and_then(all_consuming(limited_lex)), + take_until1("*").and_then(all_consuming(inner_lex)), tag("*"), ) .map(|input| (Box::new(input), Formatting::Italic)), delimited( tag("_"), - take_until1("__").and_then(all_consuming(limited_lex)), + take_until1("__").and_then(all_consuming(inner_lex)), tag("_"), ) .map(|input| (Box::new(input), Formatting::Italic)), delimited( tag("~~"), - take_until1("~~").and_then(all_consuming(limited_lex)), + take_until1("~~").and_then(all_consuming(inner_lex)), tag("~~"), ) .map(|input| (Box::new(input), Formatting::StrikeThrough)), @@ -692,7 +607,7 @@ fn footnote_content< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, &'static str>, + + FromExternalError<&'source str, KyomatoLexErrorKind>, >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { @@ -700,7 +615,7 @@ fn footnote_content< "footnote context", delimited(tag("[^"), take_until1("]:"), tag("]:")).and(preceded( space0, - take_until1("\n").and_then(all_consuming(limited_lex)), + take_until1("\n").and_then(all_consuming(inner_lex)), )), ) .parse(input)?; @@ -733,3 +648,267 @@ fn paragraph<'source, E: ParseError<&'source str> + ContextError<&'source str>>( Token::Paragraph(crate::data::Font::Normal, this_text.into()), )) } + +pub enum KyomatoLexErrorKind { + /// Tokens were expected, but none were found. + NoTokens, + /// Headers only up to 6-fold are supported, but even more folded one was found + TooDeepHeader { + fold: usize, + }, + /// Some token types might be forbidden in various contexts + ForbiddenToken { + token: &'static str, + reason: &'static str, + }, + BadSyntax { + reason: &'static str, + }, +} + +// TODO wrap outer-layer combinator into `complete` +/// Parses one or more tokens. +/// +/// - If there's a single token to parse, it will be returned by itself, as a token +/// - If there's more than one token to parse, it will be returned as a `Token::Text` variant, +/// meaning it will allocate space on a heap for it's children +/// +/// This parser accepts another parser it should use to parse tokens. +/// This point of customization allows for different possible token types to be permitted. +fn tokens_many1< + 'kernel, + 'source: 'kernel, + E: ParseError<&'source str> + + ContextError<&'source str> + + FromExternalError<&'source str, KyomatoLexErrorKind> + + 'kernel, +>( + mut tokens_kernel: impl FnMut(&'source str) -> IResult<&'source str, Token<'source>, E> + 'kernel, +) -> impl Parser<&'source str, Token<'source>, E> + 'kernel { + enum InnerResult<'source> { + None, // Will be converted into `KyomatoLexErrorKind::NoTokens` + Single(Token<'source>), + Multiple(Vec>), + } + map_res( + all_consuming(move |input| { + // This iterator it kinda tricky to operate. + // Basically, as far as I can see - it won't return error at each step. + // Instead, error will be silently stored inside it, until it's `finish` function is called to extract it. + // Basically, this means that each return from this function MUST INVOLVE `tokens.finish()?`!!! + let mut iterator = iterator(input, &mut tokens_kernel); + + // First, let's try getting first token + let Some(first_token) = (&mut iterator).next() else { + // There is no first token! + // Let's give iterator a chance to explain itself: + let (rest, _) = iterator.finish()?; + // Well, seems like there are no tokens and no error + // (most likely input is just empty or th) + // Return empty variant + return Ok((rest, InnerResult::None)); + }; + + // Now, let's try getting a second one + let Some(second_token) = (&mut iterator).next() else { + // Well, I suppose single token is enough + // Still allow iterator to explain any problem + let (rest, _) = iterator.finish()?; + // Return single variant + return Ok((rest, InnerResult::Single(first_token))); + }; + + // Ok so there are at least two tokens now. + // Heap allocation is unavoidable + let mut tokens = vec![first_token, second_token]; + tokens.extend(&mut iterator); + // Iterator might have failed silently, so let them a chance to tell about it + let (rest, _) = iterator.finish()?; + // Return multiple variant + Ok((rest, InnerResult::Multiple(tokens))) + }), + // Lastly, map this inner result into a single actual token. + // Oh, and if there were no tokens - that's an error, so might return it as well. + |inner_result| match inner_result { + InnerResult::None => Err(KyomatoLexErrorKind::NoTokens), + InnerResult::Single(token) => Ok(token), + InnerResult::Multiple(tokens) => Ok(Token::Text(Tokens::new(tokens))), + }, + ) +} + +/// Main entry point into the lexer +/// +/// Basically, it attempts to parse as many tokens as possible, +/// and returns a single one, representing them all (a `Text` variant if there are more than one token) +pub fn lex< + 'source, + E: ParseError<&'source str> + + ContextError<&'source str> + + FromExternalError<&'source str, KyomatoLexErrorKind>, +>( + input: &'source str, +) -> IResult<&'source str, Token<'source>, E> { + context( + "unlimited lex", + tokens_many1(alt(( + // Allow them all! + div_page, + header, + equation, + table, + figure, + href, + code_block, + ayano, + list, + formatting, + inline_math, + reference, + footnote_content, + footnote_reference, + paragraph, + ))), + ) + .parse(input) +} + +/// Transforms successful parse into *failure* indicating that it was not supposed to happen +/// +/// If underlying parser gets parsed successfully, will return an error of `KyomatoLexErrorKind::ForbiddenToken` type +fn forbid< + 'source, + E: ParseError<&'source str> + + ContextError<&'source str> + + FromExternalError<&'source str, KyomatoLexErrorKind>, +>( + mut parser: impl Parser<&'source str, Token<'source>, E>, + token: &'static str, + reason: &'static str, +) -> impl Parser<&'source str, Token<'source>, E> { + move |input| { + // Attempt to parse the token + // If parser fails here -- it's alright + let _ = parser.parse(input)?; + // Token was parsed. Return failure + Err(nom::Err::Failure(E::from_external_error( + input, + ErrorKind::MapRes, + KyomatoLexErrorKind::ForbiddenToken { token, reason }, + ))) + } +} + +/// Auxiliary entry point into the lexer +/// +/// Functions similarly to `lex`, but forbids certain token types +pub fn inner_lex< + 'source, + E: ParseError<&'source str> + + ContextError<&'source str> + + FromExternalError<&'source str, KyomatoLexErrorKind>, +>( + input: &'source str, +) -> IResult<&'source str, Token<'source>, E> { + context( + "inner lex", + tokens_many1({ + alt(( + // Some tokens must be forbidden + // I'd like parser to actually tell the user that token is forbidden in this context, + // to avoid unnecessary confusion of "why wouldn't it see a figure here?!" + forbid( + div_page, + "page divider", + "pages can only be divided in the outer layer", + ), + forbid(header, "header", "you are not supposed to do that"), + forbid( + equation, + "display equation", + "LaTeX does not support display math inside captions/footnotes", + ), + forbid(table, "table", "table can only be outer element"), + forbid(figure, "figure", "figure can only be outer element"), + forbid(code_block, "code block", "you are not supposed to do that"), + forbid( + list, + "list", + "LaTeX does not support lists inside captions and footnotes well", + ), + // Rest are ok to be parsed! + href, + ayano, + formatting, + inline_math, + reference, + footnote_content, + footnote_reference, + paragraph, + )) + }), + ) + .parse(input) +} + +/// Auxiliary entry point into the lexer, used to lex inner fields of Ayano-outputted tokens +/// +/// Functions similarly to `lex`, but forbids certain token types +pub fn static_inner_lex< + 'source, + E: ParseError<&'source str> + + ContextError<&'source str> + + FromExternalError<&'source str, KyomatoLexErrorKind>, +>( + input: &'source str, +) -> IResult<&'source str, Token<'source>, E> { + context( + "static inner lex", + tokens_many1({ + alt(( + // Some tokens must be forbidden + // I'd like parser to actually tell the user that token is forbidden in this context, + // to avoid unnecessary confusion of "why wouldn't it see a figure here?!" + forbid( + div_page, + "page divider", + "pages can only be divided in the outer layer", + ), + forbid(header, "header", "you are not supposed to do that"), + forbid( + equation, + "display equation", + "LaTeX does not support display math inside captions/footnotes", + ), + forbid(table, "table", "table can only be outer element"), + forbid(figure, "figure", "figure can only be outer element"), + forbid(code_block, "code block", "you are not supposed to do that"), + forbid( + list, + "list", + "LaTeX does not support lists inside captions and footnotes well", + ), + forbid( + ayano, + "ayano", + "Ayano inside Ayano is not supported, you are not supposed to do that", + ), + forbid( + footnote_content, + "footnote content", + r"All footnote content must be static and available right away. +If you want to create a footnote with Ayano-defined content, +please include Ayano block *inside* you footnote definition.", + ), + // Rest are ok to be parsed! + href, + formatting, + inline_math, + reference, + footnote_reference, + paragraph, + )) + }), + ) + .parse(input) +} From e3e3996295e018b1f57d38b5b7c586861bc74cdb Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Sun, 10 Mar 2024 01:11:52 +0200 Subject: [PATCH 06/98] removed no-longer-used impl --- src/util.rs | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/src/util.rs b/src/util.rs index d251a46..3799d53 100644 --- a/src/util.rs +++ b/src/util.rs @@ -336,34 +336,3 @@ impl> IteratorArrayCollectExt for I { } } } - -pub struct PermutatedArray<'l, T, const N: usize>(pub &'l mut [T; N]); - -impl<'l, O, E: ParseError<&'l str>, P: Parser<&'l str, O, E>, const N: usize> - Permutation<&'l str, [O; N], E> for PermutatedArray<'_, P, N> -{ - fn permutation(&mut self, input: &'l str) -> nom::IResult<&'l str, [O; N], E> { - // I'm so done. - // TODO implement permutations myself, so that there are no vectors inside - (0..N) - .into_iter() - .permutations(N) - .map(|perm| { - let mut input = input; - perm.into_iter() - .map(|i| { - let (rest, m) = self.0[i].parse(input).ok()?; - input = rest; - Some(m) - }) - .filter_map(std::convert::identity) - .collect_array() - .map(|arr| (input, arr)) - }) - .find_map(Result::ok) - .ok_or(nom::Err::Error(E::from_error_kind( - input, - nom::error::ErrorKind::Permutation, - ))) - } -} From e549cfe0b2a6c5b1cbc56f64d7487b4a9ec5af58 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Sun, 10 Mar 2024 01:12:31 +0200 Subject: [PATCH 07/98] nightly: - keep refining parsers. might start actually writing tests tomorrow. --- src/data.rs | 2 + src/lexer.rs | 685 ++++++++++++++++++++++++++++----------------------- 2 files changed, 379 insertions(+), 308 deletions(-) diff --git a/src/data.rs b/src/data.rs index 7cb4518..f0ab542 100644 --- a/src/data.rs +++ b/src/data.rs @@ -142,6 +142,8 @@ pub enum Token<'source> { }, Formatted(Formatting, Box>), Text(Tokens<'source>), + // TODO move font and formatting exclusively to here + // TODO add a flag to indicate, if this paragraph contains escaped chars (if it was parsed from caption, basically) Paragraph(Font, Tx<'source>), InlineMathmode(Tx<'source>), Reference(Tx<'source>), diff --git a/src/lexer.rs b/src/lexer.rs index 079d537..f147e6b 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -1,27 +1,66 @@ use std::{ borrow::{Borrow, Cow}, + num::ParseIntError, path::Path, + str::FromStr, + sync::RwLock, }; use itertools::Itertools; use nom::{ branch::{alt, permutation}, - bytes::complete::{escaped, is_not, tag, take_till1, take_until1, take_while}, - character::complete::{alpha1, alphanumeric1, char, digit1, multispace0, newline, space0}, - combinator::{all_consuming, iterator, map_res, not, opt, recognize, success}, + bytes::{ + complete::{escaped, is_not, tag, take_till1, take_until, take_until1, take_while}, + streaming::tag_no_case, + }, + character::{ + complete::{alpha1, alphanumeric1, anychar, char, digit1, multispace0, newline, space0}, + is_space, + }, + combinator::{all_consuming, cut, iterator, map_res, not, opt, recognize, success}, error::{context, ContextError, ErrorKind, FromExternalError, ParseError}, - multi::{fold_many1, many1_count, separated_list1}, + multi::{many1, many1_count, separated_list1}, number::complete::float, - sequence::{delimited, preceded, terminated, tuple}, + sequence::{delimited, pair, preceded, terminated, tuple}, IResult, Parser, }; use url::Url; use crate::{ - data::{AyanoBlock, ListType, Token, Tokens}, + data::{AyanoBlock, Formatting, ListType, Token, Tokens}, util::IteratorArrayCollectExt, }; +/// Matches any sort of whitespace characters, *newline*, and a provided tag at the end +fn _multispace_line_break<'source, E: ParseError<&'source str>>( + tag: &'static str, +) -> impl Parser<&'source str, (&'source str, &'source str), E> { + move |input: &'source str| { + // here's a chars iterator + let chars = input.char_indices(); + // now, limit them to fist non-whitespace character + let non_whitespace = chars.take_while(|(_, c)| !c.is_whitespace()); + // then, filter out newline characters only + let mut newline = non_whitespace.filter_map(|(ind, c)| (c == '\n').then_some(ind)); + // lastly, find first newline character that prepends provided tag + let Some(res) = newline.find(|ind| input[(ind + 1)..].starts_with(tag)) else { + // if there's no such idex, that's an error + return Err(nom::Err::Error(E::from_error_kind( + input, + ErrorKind::MultiSpace, + ))); + }; + + // We may now construct the answer + let space = &input[..=res]; // space is up to the newline included + + // will return a copy of provided tag, since it was already matched + let rest = &input[(res + 1 + tag.len())..]; + + Ok((rest, (space, tag))) + } +} + // Conventions: // - parsers should only parse empty characters with `empty` // - parsers should only parse empty space at the beginning, not the end @@ -94,7 +133,8 @@ macro_rules! parse_args { /// Quite tricky task, as there can be double quotes -- but escaped. /// Apart from single, unescaped quotes, caption can be ANYTHING. /// There seems to be just the tool for a job -- `escaped` parser -/// TODO FIXME add a mention about caption actually TERMINATING after first unescaped double-quote, and where exactly escaping backslashes are removed +/// TODO FIXME add a mention about caption actually TERMINATING after first unescaped double-quote (and there is no cure), +/// and where exactly escaping backslashes are removed fn caption_kernel< 'source, E: ParseError<&'source str> @@ -111,6 +151,7 @@ fn caption_kernel< /// Parses page divider /// TODO update it's definition in the manual +#[inline] fn div_page<'source, E: ParseError<&'source str> + ContextError<&'source str>>( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { @@ -123,6 +164,7 @@ fn div_page<'source, E: ParseError<&'source str> + ContextError<&'source str>>( /// /// - Always starts from a blank line /// - Comprised of 1-6 '#'-symbols, and includes everything up until last empty space before line break +#[inline] fn header< 'source, E: ParseError<&'source str> @@ -158,37 +200,26 @@ fn header< /// /// - Always starts from a blank line with "$$" tag /// - Continues up until another unescaped "$$" on a beginning of the line is encountered -/// - Possible "ref" argument (to refer to this equation) is matched afterwards +/// - Possible "ref" argument (for referring to this equation) is matched afterwards fn equation< 'source, - E: ParseError<&'source str> + ContextError<&'source str> + FromExternalError<&'source str, E>, + E: ParseError<&'source str> + + ContextError<&'source str> + + FromExternalError<&'source str, KyomatoLexErrorKind>, >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { - let Some(ind) = input - .char_indices() - .filter_map(|(ind, c)| (c == '\n').then_some(ind)) - .find(|ind| input[*ind..].starts_with("\n$$")) - else { - return Err(nom::Err::Error(E::from_external_error( - input, - ErrorKind::CrLf, - KyomatoLexErrorKind::BadSyntax { - reason: "Display mathmode should start with a new line", - }, - ))); - }; - - let trimmed_input = &input[ind]; - let (rest, (_, _, content, _, (ident,))) = context( + let (rest, (content, _, (ident,))) = context( "display mathmode", - tuple(( - multispace0, - tag("$$"), - take_until1("\n$$"), - tag("\n$$"), - parse_args!["ref" = alphanumeric1], - )), + preceded( + _multispace_line_break("$$"), + // if mathmode opening found, lock into equation match + cut(tuple(( + take_until1("\n$$"), + tag("\n$$"), + parse_args!["ref" = alphanumeric1], + ))), + ), )(input)?; Ok(( rest, @@ -199,6 +230,13 @@ fn equation< )) } +/// Parses table +/// +/// - Always starts from a newline followed by '|' char +/// - First line is always interpreted as header +/// TODO - Next line is skipped for now; all tables are formatted with centered columns +/// - Following lines starting with '|' are interpreted as table cells +/// - Lastly, optional "ref" and "caption" arguments are parsed fn table< 'source, E: ParseError<&'source str> @@ -207,36 +245,35 @@ fn table< >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { - let (rest, (header, _, cells, (ident, caption))) = context( + let (rest, (_, headers, (_, _, cells, (ident, caption)))) = context( "table", tuple(( - // first, parse the header - preceded( - char('\n'), - delimited( - char('|'), - separated_list1(char('|'), delimited(space0, inner_lex, space0)), - char('|'), + // first, find the start + _multispace_line_break("|"), + terminated(separated_list1(char('|'), inner_lex), char('|')), + // at this point this is guaranteed to be a table token, so we lock in + cut(tuple(( + // first, match until the end of this line + pair(take_until("\n"), char('\n')), + // then, match format line (it's discarded right now) + pair(take_until("\n"), char('\n')), + // from here, match table cells + // while this is possible to do by just per-line collection of them into a vectors + // (and merge them afterwards), + // I can't stand unnecessary heap allocations, so here is my solution: + separated_list1( + preceded(preceded(space0, char('|')), opt(pair(space0, tag("\n|")))), + inner_lex, ), - ), - tuple((space0, char('\n'), take_until1("\n"))), - // next, parse the cells - preceded( - char('\n'), - delimited( - char('|'), - separated_list1(char('|'), delimited(space0, inner_lex, space0)), - char('|'), - ), - ), - // finally, here come arguments - parse_args!["ref" = alphanumeric1, "caption" = caption_kernel], + // finally, here come the arguments + parse_args!["ref" = alphanumeric1, "caption" = caption_kernel], + ))), )), )(input)?; Ok(( rest, Token::Table { - header: Tokens::new(header), + header: Tokens::new(headers), cells: Tokens::new(cells), caption: caption.map(Box::new), ident: ident.map(Cow::from), @@ -244,6 +281,11 @@ fn table< )) } +/// Matches figure +/// +/// - Always starts from a newline followed by "![[" sequence (that's obsidian's image inclusion syntax or th) +/// - Everything up to "]]" sequence is interpreted as path to the image +/// - Lastly, optional "ref", "caption" and "width" arguments follow. Can supplied now, and will be recognized, but it's not supported yet fn figure< 'source, E: ParseError<&'source str> @@ -253,19 +295,24 @@ fn figure< input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { // TODO add width parameter to figure, I guess - let (rest, (_, _, path, _, (ident, caption, _width))) = context( + let (rest, (_, (path, _, (ident, caption, _width)))) = context( "figure", - tuple(( - multispace0, - tag("![["), - take_until1("]]"), - tag("]]"), - parse_args![ - "ref" = alphanumeric1, - "caption" = caption_kernel, - "width" = float - ], - )), + pair( + // first, match syntax start + _multispace_line_break("![["), + // at this point, it's supposed to be figure exclusively, so lock into this syntax + cut(tuple(( + // that's gonna be a path to image + take_until1("]]"), + tag("]]"), + // and here come the args + parse_args![ + "ref" = alphanumeric1, + "caption" = caption_kernel, + "width" = float + ], + ))), + ), )(input)?; let token = Token::Figure { src_name: Path::new(path).into(), @@ -275,25 +322,26 @@ fn figure< Ok((rest, token)) } -fn href<'source, E: ParseError<&'source str> + ContextError<&'source str>>( +/// Parses markdown's hyper reference syntax +/// +/// Basic syntax is [DISPLAY](URL) +fn href< + 'source, + E: ParseError<&'source str> + + ContextError<&'source str> + + FromExternalError<&'source str, KyomatoLexErrorKind>, +>( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { let (rest, (display, url)) = context( "href", tuple(( - delimited(char('['), take_till1(|c| c != ']'), char(']')), - delimited(char('('), take_till1(|c| c != ')'), char(')')).and_then(|url: &str| { - Ok(( - "", - Url::parse(url).map_err(|_err| { - // TODO make a proper error - nom::Err::Error(E::from_error_kind( - "failed to parse url", - nom::error::ErrorKind::Verify, - )) - })?, - )) - }), + delimited(char('['), take_until("]"), char(']')), + // url should be parsed into proper format + map_res( + delimited(char('('), take_until(")"), char(')')), + |url: &str| Url::from_str(url).map_err(KyomatoLexErrorKind::BadUrl), + ), )), )(input)?; Ok(( @@ -305,29 +353,58 @@ fn href<'source, E: ParseError<&'source str> + ContextError<&'source str>>( )) } +/// Parses code block +/// +/// *WARNING!* This parser CAN parse Ayano code blocks, which (obviously) wont execute them. +/// Consider attempting to parse for Ayano block BEFORE running this parser +/// +/// - Always starts with a newline followed by "```" sequence +/// - Everything up to the end of this very line (stripped from whitespace) will be considered specified language +/// - All lines up to a newline followed by "```" sequence will be considered as code to include fn code_block<'source, E: ParseError<&'source str> + ContextError<&'source str>>( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { - let (rest, (_, (lang, _, _, code))) = context( + let (rest, (lang, code)) = context( "code block", - tuple(( - char('\n'), - delimited( - tag("```\n"), - tuple((opt(alpha1), space0, char('\n'), take_until1("\n```"))), - tag("\n```"), - ), - )), + pair( + delimited(_multispace_line_break("```"), take_until("\n"), char('\n')), + // At this point, this is a code block syntax for sure, so we lock in + cut(terminated(take_until("\n```"), tag("\n```"))), + ), )(input)?; + // Trim the language name + let lang = lang.trim(); + // If the language name turns out to be empty, it's as good as unspecified + let language = if lang.is_empty() { + None + } else { + Some(Cow::from(lang)) + }; Ok(( rest, Token::CodeBlock { code: code.into(), - language: lang.map(Cow::from), + language, }, )) } +/// Parses an Ayano block +/// +/// *because Ayano loves Kyoko* +/// +/// There's quite a lot of syntax to cover: +/// - First, this token starts from a newline followed by "```" sequence. +/// - Next, "python, Ayano" is matched with various spacing options +/// - Next comes a permutation of various block parameters, like +/// - * "" for display block with optional caption (can be omitted) +/// - ~ for insert-path. WARN: PATH SHOULD NOT CONTAIN SPACES +/// - # ident of a block. Unused, as of now. +/// Was intended to make blocks more unique, but think about it - +/// same blocks can have shared representation in python code; this code is still executed twice). +/// - ! used to mark block as static, i.e. - it's code will be executed statically, +/// and nothing would be displayed in place of it in the output +/// - Everything following this line and up to a newline followed by "\n```" will be considered a Python code to execute fn ayano< 'source, E: ParseError<&'source str> @@ -336,57 +413,55 @@ fn ayano< >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { - // python, Ayano * ~ # // TODO add tests for escaping backslashes removal (generation-side) - let (rest, (_, _, _, _, _, _, _, (display, insert, ident, is_static), _, code, _)) = - context( - "ayano", + let (rest, ((display, insert, _ident, is_static), code)) = context( + "ayano", + preceded( tuple(( - char('\n'), - tag("```"), + _multispace_line_break("```"), space0, - tag("python,"), + tag_no_case("python,"), space0, tag("Ayano"), space0, - permutation(( - opt(tuple(( - char('*'), - space0, - opt(terminated(caption_kernel, space0)), - ))), - opt(tuple((char('~'), space0, alphanumeric1, space0))), - opt(tuple(( - char('#'), - space0, - digit1.and_then(|number: &str| { - Ok::<(&str, u16), nom::Err>(( - "", - number.parse().map_err(|_| { - nom::Err::Error(E::from_error_kind( - "Identifier must be parseable into u16", - ErrorKind::Digit, - )) - })?, - )) - }), - space0, - ))), - opt(terminated(char('!'), space0)), - )), - char('\n'), - take_until1("\n```"), - tag("\n```"), )), - )(input)?; + // This is guaranteed to be Ayano block at this point, so we lock in + cut(pair( + terminated( + permutation(( + opt(preceded( + pair(char('*'), space0), + opt(terminated(caption_kernel, space0)), + )), + opt(preceded( + pair(char('~'), space0), + terminated(take_till1(char::is_whitespace), space0), + )), + opt(preceded( + pair(char('#'), space0), + terminated( + map_res(digit1, |n| { + u16::from_str(n).map_err(KyomatoLexErrorKind::BadIdent) + }), + space0, + ), + )), + opt(terminated(char('!'), space0)), + )), + char('\n'), + ), + terminated(take_until("\n```"), tag("\n```")), + )), + ), + )(input)?; // type might be a bit confusing, but basically: // - outer None means this block is not meant to be displayed // - outer Some means this block is to be displayed // - inner None means this block has no display caption // - inner Some means this block has display caption - let display: Option>> = display.map(|(_, _, caption)| caption); // TODO add code captions support - let insert: Option<&'source Path> = insert.map(|(_, _, path, _)| Path::new(path)); - let _ident: Option = ident.map(|(_, _, ident, _)| ident); // TODO add support for codeblock identifiers + // TODO add code captions support + let insert: Option<&'source Path> = insert.map(Path::new); + // TODO add support for codeblock identifiers let code: Cow<'_, str> = Cow::from(code); Ok(( rest, @@ -399,6 +474,12 @@ fn ayano< )) } +/// Creates static character array from supplied character range. +/// +/// Utilizes `once_cell` to do so. +/// +/// ### Panics +/// if collection into array was not successful (i.e. wrong length was specified) macro_rules! char_array { ($from:literal .. $to:literal, $length:literal, $alphabet:literal) => {{ static CHARS: ::once_cell::sync::Lazy<[char; $length]> = @@ -412,95 +493,118 @@ macro_rules! char_array { }}; } +/// Yeah, there is some leaking happening here.... +/// +/// Don't worry about it, I guess (?) +/// (*why would I create and format these strings each time??!*) +/// TODO I guess, #[cached] macro would do better here, or th +fn _list_item_generator<'scope>( + list_type: &'scope ListType, +) -> Box + 'scope> { + match list_type { + ListType::Bullet => Box::new(std::iter::repeat("-")), + ListType::Num => { + static STRS: RwLock> = RwLock::new(Vec::new()); + Box::new((1..).into_iter().map(|i| { + // First, try get already generated string + let read = STRS.read().unwrap(); + if let Some(res) = read.get(i) { + return &**res; + } + // Hmmm, that didn't worked. + // Give away read lock, and ask for modification + drop(read); + let mut write = STRS.write().unwrap(); + // Append all necessary strings for the array + while write.len() <= i { + let j = write.len() + 1; + write.push(format!("{}.", j).leak()); + } + write + .get(i) + .expect("Should be present now, we should've just pushed it") + })) + } + ListType::Latin => { + static STRS: once_cell::sync::Lazy<[&'static str; 26]> = + once_cell::sync::Lazy::new(|| { + char_array!('a'..'z', 26, "latin") + .map(|l| format!("{l}.").leak() as &'static str) + }); + Box::new((*STRS).into_iter().map(|b| &*b)) + } + ListType::Cyrillic => { + static STRS: once_cell::sync::Lazy<[&'static str; 32]> = + once_cell::sync::Lazy::new(|| { + char_array!('а'..'я', 32, "cyrillic") + .map(|l| format!("{l}.").leak() as &'static str) + }); + Box::new((*STRS).into_iter().map(|b| &*b)) + } + ListType::Roman => unimplemented!("Roman numerated lists are not supported yet"), + } +} + fn _list_inner< 'source, E: ParseError<&'source str> + ContextError<&'source str> + FromExternalError<&'source str, KyomatoLexErrorKind>, >( - mut input: &'source str, + input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { - let mut newline = 1; - let mut non_whitespace = 0; - while newline > non_whitespace { - newline = input.find('\n').unwrap_or(input.len()); - non_whitespace = input - .find(|c: char| !c.is_whitespace()) - .unwrap_or(input.len()); - input = &input[(newline + 1)..]; - } + // First, we need to actually detect list type - let indent = &input[..non_whitespace]; // how whitespace indent at the start of each line looks like - let (list_type, item_start): (ListType, fn(usize) -> Option>) = - match indent[non_whitespace..].chars().next() { - None => { - // there's not a single non-whitespace character -- it's impossible to properly parse a list here. - return Err(nom::Err::Error(E::from_error_kind( - "can't parse list at the empty location", - ErrorKind::Eof, - ))); - } - Some('1') => { - // that's a numeric list! - (ListType::Num, |i| Some(format!("{}.", i + 1).into())) - } - Some('a') => { - // that's a latin list! - (ListType::Latin, move |i| { - char_array!('a'..'z', 26, "latin") - .get(i) - .map(char::to_string) - .map(Cow::from) - }) - } - Some('а') => { - // that's a cyrillic list! - (ListType::Latin, move |i| { - char_array!('а'..'я', 32, "cyrillic") - .get(i) - .map(char::to_string) - .map(Cow::from) - }) - } - Some('-') => { - // that's a bullet list! - (ListType::Num, |_| Some("-".into())) - } - Some('I') => { - // that's a roman list! - unimplemented!("Roman numerals list is not supported yet") - // TODO add support, I guess - } - Some(_c) => { - // that's an unknown list type - // FIXME not sure how to use these errors properly, I think I need to study more examples - return Err(nom::Err::Error(E::from_error_kind( - "unknown list type", - ErrorKind::Char, - ))); - } + let (_, first_discriminator) = recognize(take_till1(char::is_whitespace))(input)?; + let list_type = match first_discriminator { + "1." => ListType::Num, + "a." => ListType::Latin, + "а." => ListType::Cyrillic, + "-" => ListType::Bullet, + "I" => unimplemented!("Roman numerated lists are not supported yet"), + _ => { + // that's an unknown list type + return Err(nom::Err::Error(E::from_external_error( + input, + ErrorKind::Char, + KyomatoLexErrorKind::UnknownListType, + ))); + } + }; + let mut start_generator = _list_item_generator(&list_type); + + let (rest, items) = many1(move |input| { + let Some(this_start) = start_generator.next() else { + // I'm not sure if this error is possible to happen under current implementation, but whatever :idk: + return Err(nom::Err::Error(E::from_external_error( + input, + ErrorKind::IsA, + KyomatoLexErrorKind::TooLongList, + ))); }; - let items: Vec<_> = input - .lines() - .take_while(|line: &&str| line.starts_with(indent)) - .enumerate() - .map(|(i, line): (usize, &str)| { - let line = line.trim_start_matches::<&str>( - item_start(i) - .ok_or(nom::Err::Error(E::from_error_kind( - "List has too much elements", - ErrorKind::Fix, - )))? - .borrow(), - ); - let (_, item) = all_consuming(inner_lex::<'_, E>)(line)?; - Ok::<_, nom::Err>(item) - }) - .try_collect()?; - let content = Tokens::new(items); - Ok(("", Token::List { list_type, content })) + // Match current item start and item itself + preceded(pair(tag(this_start), space0), inner_lex)(input) + })(input)?; + // Unfortunately, we can't use `map` function here, since it accepts `Fn` (not `FnOnce`), + // and I'd like to avoid giving `Copy` to every single thing in existence + Ok(( + rest, + Token::List { + list_type, + content: Tokens::new(items), + }, + )) } +/// Parses list +/// +/// There are multiple types of lists supported: +/// - bullet: every line is prepended with '-' +/// - num: every line is prepended with a number and a dot following it +/// - latin: every line is prepended with a latin letter and a dot following it. Number of entries is limited. +/// - cyrillic: every line is prepended with a cyrillic letter and a dot following it. Number of entries is limited. +/// +/// Roman numerals lack support as of now, but I plan to support them in the future too fn list< 'source, E: ParseError<&'source str> @@ -509,7 +613,7 @@ fn list< >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { - context("list", preceded(char('\n'), _list_inner))(input) + context("list", preceded(_multispace_line_break(""), _list_inner))(input) } fn _formatting_inner< @@ -525,6 +629,17 @@ fn _formatting_inner< Ok(Token::Formatted(style, Box::new(content))) } +/// Parses formatting declaration (or whatever you wanna call that) +/// +/// **WARNING**: I expect this parse to be kinda-heavy. Consider using it *as late as possible*, but not before regular text parser, obviously. +/// +/// Basically, there are 3 types of formatting: +/// - Italic: indicated by single '*' or '_' +/// - Bold: indicated by double '*' or '_' +/// - Strikethrough: indicated by double '~' +/// +/// Note: this parser will try to parse multiple times. +/// So PLEASE: be careful with the way you use '*' and '_', as it may prolong parsing time. fn formatting< 'source, E: ParseError<&'source str> @@ -533,56 +648,60 @@ fn formatting< >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { - use crate::data::Formatting; - context( + let (rest, delimiter) = preceded( + pair(opt(char('\n')), space0), + alt((tag("**"), tag("__"), tag("*"), tag("_"), tag("~~"))), + )(input)?; + // Anything starting from these characters IS a formatting token. + // Committed to parsing after this point + let formatting = match delimiter { + "**" | "__" => Formatting::Bold, + "*" | "_" => Formatting::Italic, + "~~" => Formatting::StrikeThrough, + _ => unreachable!(), + }; + let possible_ends = rest + .char_indices() + .filter_map(|(ind, _)| (rest[ind..].starts_with(delimiter)).then_some(ind)); + for end_candidate in possible_ends { + let inner_tokens = &rest[..end_candidate]; + if let Ok((_, inner)) = all_consuming(inner_lex::<'source, E>)(inner_tokens) { + return Ok(( + &rest[(end_candidate + delimiter.len())..], + Token::Formatted(formatting, Box::new(inner)), + )); + } + } + // If this point was reached, there is no correct variant for pairing delimiter. + Err(nom::Err::Failure(E::add_context( + input, "formatting", - alt(( - delimited( - tag("**"), - take_until1("**").and_then(all_consuming(inner_lex)), - tag("**"), - ) - .map(|input| (Box::new(input), Formatting::Bold)), - delimited( - tag("__"), - take_until1("__").and_then(all_consuming(inner_lex)), - tag("__"), - ) - .map(|input| (Box::new(input), Formatting::Bold)), - delimited( - tag("*"), - take_until1("*").and_then(all_consuming(inner_lex)), - tag("*"), - ) - .map(|input| (Box::new(input), Formatting::Italic)), - delimited( - tag("_"), - take_until1("__").and_then(all_consuming(inner_lex)), - tag("_"), - ) - .map(|input| (Box::new(input), Formatting::Italic)), - delimited( - tag("~~"), - take_until1("~~").and_then(all_consuming(inner_lex)), - tag("~~"), - ) - .map(|input| (Box::new(input), Formatting::StrikeThrough)), - )), - ) - .map(|(token, style)| Token::Formatted(style, token)) - .parse(input) + E::from_error_kind(input, ErrorKind::Eof), + ))) } +/// Parses inline math, delimited by '$' fn inline_math<'source, E: ParseError<&'source str> + ContextError<&'source str>>( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { - let (rest, formula) = context( + context( "inline math", delimited(char('$'), take_until1("$"), char('$')), - )(input)?; - Ok((rest, Token::InlineMathmode(formula.into()))) + ) + .map(|content: &str| Token::InlineMathmode(content.into())) + .parse(input) } +/// Parses object reference, inside a document +/// +/// Basic syntax: [@], where can be +/// - fig: for figure +/// - tab: for table +/// - eq: for equation +/// +/// In fact, these are not validated. Any sort of missing object would be detected and reported during document generation +/// +/// Also note that any whitespace at the beginning and the end would be trimmed away fn reference<'source, E: ParseError<&'source str> + ContextError<&'source str>>( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { @@ -590,7 +709,7 @@ fn reference<'source, E: ParseError<&'source str> + ContextError<&'source str>>( "reference", delimited(tag("[@"), take_until1("]"), char(']')), )(input)?; - Ok((rest, Token::Reference(ident.into()))) + Ok((rest, Token::Reference(ident.trim().into()))) } fn footnote_reference<'source, E: ParseError<&'source str> + ContextError<&'source str>>( @@ -664,6 +783,10 @@ pub enum KyomatoLexErrorKind { BadSyntax { reason: &'static str, }, + BadUrl(url::ParseError), + BadIdent(ParseIntError), + UnknownListType, + TooLongList, } // TODO wrap outer-layer combinator into `complete` @@ -799,7 +922,7 @@ fn forbid< } } -/// Auxiliary entry point into the lexer +/// Auxiliary entry point into the lexer, used to lex inner fields /// /// Functions similarly to `lex`, but forbids certain token types pub fn inner_lex< @@ -809,58 +932,6 @@ pub fn inner_lex< + FromExternalError<&'source str, KyomatoLexErrorKind>, >( input: &'source str, -) -> IResult<&'source str, Token<'source>, E> { - context( - "inner lex", - tokens_many1({ - alt(( - // Some tokens must be forbidden - // I'd like parser to actually tell the user that token is forbidden in this context, - // to avoid unnecessary confusion of "why wouldn't it see a figure here?!" - forbid( - div_page, - "page divider", - "pages can only be divided in the outer layer", - ), - forbid(header, "header", "you are not supposed to do that"), - forbid( - equation, - "display equation", - "LaTeX does not support display math inside captions/footnotes", - ), - forbid(table, "table", "table can only be outer element"), - forbid(figure, "figure", "figure can only be outer element"), - forbid(code_block, "code block", "you are not supposed to do that"), - forbid( - list, - "list", - "LaTeX does not support lists inside captions and footnotes well", - ), - // Rest are ok to be parsed! - href, - ayano, - formatting, - inline_math, - reference, - footnote_content, - footnote_reference, - paragraph, - )) - }), - ) - .parse(input) -} - -/// Auxiliary entry point into the lexer, used to lex inner fields of Ayano-outputted tokens -/// -/// Functions similarly to `lex`, but forbids certain token types -pub fn static_inner_lex< - 'source, - E: ParseError<&'source str> - + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexErrorKind>, ->( - input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { context( "static inner lex", @@ -880,8 +951,8 @@ pub fn static_inner_lex< "display equation", "LaTeX does not support display math inside captions/footnotes", ), - forbid(table, "table", "table can only be outer element"), - forbid(figure, "figure", "figure can only be outer element"), + forbid(table, "table", "table can only be an outer element"), + forbid(figure, "figure", "figure can only be an outer element"), forbid(code_block, "code block", "you are not supposed to do that"), forbid( list, @@ -891,14 +962,12 @@ pub fn static_inner_lex< forbid( ayano, "ayano", - "Ayano inside Ayano is not supported, you are not supposed to do that", + "Ayano on the inner layer is not supported, as of now", ), forbid( footnote_content, "footnote content", - r"All footnote content must be static and available right away. -If you want to create a footnote with Ayano-defined content, -please include Ayano block *inside* you footnote definition.", + "All footnote content must be outer-layer definitions", ), // Rest are ok to be parsed! href, From e96403b748704e95bc046e0d01e001cd9806ef2b Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Sun, 10 Mar 2024 14:44:46 +0200 Subject: [PATCH 08/98] finished polishing token parsers --- src/lexer.rs | 278 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 201 insertions(+), 77 deletions(-) diff --git a/src/lexer.rs b/src/lexer.rs index f147e6b..2d6a113 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -17,7 +17,7 @@ use nom::{ complete::{alpha1, alphanumeric1, anychar, char, digit1, multispace0, newline, space0}, is_space, }, - combinator::{all_consuming, cut, iterator, map_res, not, opt, recognize, success}, + combinator::{all_consuming, cut, eof, iterator, map_res, not, opt, recognize, success}, error::{context, ContextError, ErrorKind, FromExternalError, ParseError}, multi::{many1, many1_count, separated_list1}, number::complete::float, @@ -27,7 +27,7 @@ use nom::{ use url::Url; use crate::{ - data::{AyanoBlock, Formatting, ListType, Token, Tokens}, + data::{AyanoBlock, Font, Formatting, ListType, Token, Tokens}, util::IteratorArrayCollectExt, }; @@ -555,7 +555,10 @@ fn _list_inner< ) -> IResult<&'source str, Token<'source>, E> { // First, we need to actually detect list type - let (_, first_discriminator) = recognize(take_till1(char::is_whitespace))(input)?; + let (new_input, first_discriminator) = preceded( + _multispace_line_break(""), + recognize(take_till1(char::is_whitespace)), + )(input)?; let list_type = match first_discriminator { "1." => ListType::Num, "a." => ListType::Latin, @@ -565,7 +568,7 @@ fn _list_inner< _ => { // that's an unknown list type return Err(nom::Err::Error(E::from_external_error( - input, + new_input, ErrorKind::Char, KyomatoLexErrorKind::UnknownListType, ))); @@ -584,7 +587,7 @@ fn _list_inner< }; // Match current item start and item itself preceded(pair(tag(this_start), space0), inner_lex)(input) - })(input)?; + })(new_input)?; // Unfortunately, we can't use `map` function here, since it accepts `Fn` (not `FnOnce`), // and I'd like to avoid giving `Copy` to every single thing in existence Ok(( @@ -701,27 +704,37 @@ fn inline_math<'source, E: ParseError<&'source str> + ContextError<&'source str> /// /// In fact, these are not validated. Any sort of missing object would be detected and reported during document generation /// -/// Also note that any whitespace at the beginning and the end would be trimmed away +/// Also note that any whitespace at the beginning and the end (of the text inside brackets) would be trimmed away fn reference<'source, E: ParseError<&'source str> + ContextError<&'source str>>( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { let (rest, ident) = context( "reference", - delimited(tag("[@"), take_until1("]"), char(']')), + delimited(pair(space0, tag("[@")), cut(take_until1("]")), char(']')), )(input)?; Ok((rest, Token::Reference(ident.trim().into()))) } +/// Parses a footnote reference +/// +/// Acts basically the same as regular reference, but uses different symbol for identification +/// (since this sort of reference is actually supported by markdown!) fn footnote_reference<'source, E: ParseError<&'source str> + ContextError<&'source str>>( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { let (rest, ident) = context( "footnote reference", - delimited(tag("[^"), take_until1("]"), char(']')), + delimited(pair(space0, tag("[^")), cut(take_until1("]")), char(']')), )(input)?; - Ok((rest, Token::FootNoteReference(ident.into()))) + Ok((rest, Token::FootNoteReference(ident.trim().into()))) } +/// Parses definition of a footnote content +/// +/// WARNING! This parser must be checked BEFORE `footnote_reference` one. +/// This is a consequence of footnote reference and footnote definition having potentially the same syntax, but footnote definition having more of it. +/// +/// basic syntax: \n[^]:\n fn footnote_content< 'source, E: ParseError<&'source str> @@ -730,42 +743,120 @@ fn footnote_content< >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { - let (rest, (ident, content)) = context( + context( "footnote context", - delimited(tag("[^"), take_until1("]:"), tag("]:")).and(preceded( + delimited( + _multispace_line_break("[^"), + cut(take_until1("]")), + tag("]:"), + ) + .and(preceded( space0, - take_until1("\n").and_then(all_consuming(inner_lex)), + // Content can end at the end of this line, or at the end of file + alt((take_until1("\n"), take_while(|_| true))).and_then(cut(all_consuming(inner_lex))), )), ) - .parse(input)?; - Ok(( - rest, - Token::FootNoteContent { - content: Box::new(content), - ident: ident.into(), - }, - )) + .map(|(ident, content)| Token::FootNoteContent { + content: Box::new(content), + ident: ident.into(), + }) + .parse(input) } -fn paragraph<'source, E: ParseError<&'source str> + ContextError<&'source str>>( +/// Parses plaintext paragraph, up to at most the end of input/line +/// +/// WARNING: must be used AFTER ALL OTHER PARSERS. +/// This parser is able to parse virtually anything, but in most cases you expect input being interpreted in some other way. +/// +/// This is the most tricky of the parsers, since it must stop, if any other parser *might* be possible to use. +/// The only way to check, if any other parser may be used - is to ask it directly, "Ok" result meaning that it had succeeded. +/// +/// I don't feel like discarding potentially-allocating and deeply-nested parsed tokens. +/// Because of that, this parser actually outputs one OR two tokens: one is always the paragraph one, and the other - some sort of different token it had managed to parse. +fn paragraph< + 'source, + E: ParseError<&'source str> + + ContextError<&'source str> + + FromExternalError<&'source str, KyomatoLexErrorKind>, +>( input: &'source str, -) -> IResult<&'source str, Token<'source>, E> { - // This is the most tricky component. It must stop matching AS SOON AS any sort-of syntax is encountered. - // Should be noted, that this parsed is forward-blind, meaning it has no idea what element EXACTLY will be matched next. - let bad_index = input +) -> IResult<&'source str, (Token<'source>, Option>), E> { + // TODO wrap into preceded(multispace0, ...) + + // At this point we assume, that whitespace before was parsed, including any possible newlines + + // Any token starting with a newline is out of the question + // However, this still leaves a couple of candidates: + // - href (starting with '[') + // - formatting (starting with '*', '_' or '~') + // - inline_math (starting with '$') + // - reference (starting with "[@") + // - footnote_reference (starting with "[^") + + // First, let's isolate input to a newline (if there is no newline, stop at last char) + let interrupt_ind = input.find('\n').unwrap_or(input.len() - 1); + let cut_input = &input[..=interrupt_ind]; + + // Now, let's try finding interrupting token + let interuption = cut_input .char_indices() - .filter_map(|(ind, c)| matches!(c, '|' | '\n' | '$' | '`').then_some(ind)) - .find(|&ind| { - ["|", "||||||", "\n#", "$", "$$", "`", "```"] - .into_iter() - .any(|p| input[ind..].starts_with(p)) + // Parsing continues until newline is reached + .take_while(|(_, c)| c != &'\n') + // Possible breakpoints are at characters '[', '*', '_', '~', and '$' + .filter(|(_, c)| ['[', '*', '_', '~', '$'].contains(c)) + // Now, try a proper parser for each one + .map(|(ind, c)| { + let cut_cut_input = &cut_input[ind..]; + let parse_res = match c { + '[' => { + if cut_input[ind..].starts_with("[@") { + reference(cut_cut_input) + } else if cut_input[ind..].starts_with("[^") { + footnote_reference(cut_cut_input) + } else { + href(cut_cut_input) + } + } + '*' | '_' | '~' => formatting(cut_cut_input), + '$' => inline_math(cut_cut_input), + _ => unreachable!(), + }; + (ind, parse_res) }) - .unwrap_or(input.len()); - let (this_text, rest) = input.split_at(bad_index); - Ok(( - rest, - Token::Paragraph(crate::data::Font::Normal, this_text.into()), - )) + .try_fold((), |_, (ind, parse_res)| match parse_res { + // if there is a match for the following token -- return this "Ok" wrapped into "Err", forcing iterator to stop + Ok(ok) => Err(Ok((ind, ok))), + // if there is not a match, but a FAUILURE -- return it as "Err" wrapped in "Err", forcing iterator to stop + Err(nom::Err::Failure(err)) => Err(Err(nom::Err::Failure(err))), + // if parsing had failed, continue driving iterator by returning "Ok" + Err(_) => Ok(()), + }) + .err(); + + let interruption = if let Some(interruption) = interuption { + // there was an interruption! + // but it might be a FAILURE ONE. That should bubble up + Some(interruption?) + } else { + None + }; + + // Now, construct the output + let this_token_end = interruption + .as_ref() + .map(|(ind, _)| *ind) + .unwrap_or(interrupt_ind); + let this_token = Token::Paragraph(Font::Normal, Cow::from(&cut_input[..this_token_end])); + let rest_input = interruption + .as_ref() + .map(|(_, (rest, _))| *rest) + .unwrap_or(&cut_input[this_token_end..]); + let follower = interruption.map(|(_, (_, f))| f); + Ok((rest_input, (this_token, follower))) +} + +fn __equalizer(token: Token<'_>) -> (Token<'_>, Option>) { + (token, None) } pub enum KyomatoLexErrorKind { @@ -806,7 +897,8 @@ fn tokens_many1< + FromExternalError<&'source str, KyomatoLexErrorKind> + 'kernel, >( - mut tokens_kernel: impl FnMut(&'source str) -> IResult<&'source str, Token<'source>, E> + 'kernel, + mut tokens_kernel: impl FnMut(&'source str) -> IResult<&'source str, (Token<'source>, Option>), E> + + 'kernel, ) -> impl Parser<&'source str, Token<'source>, E> + 'kernel { enum InnerResult<'source> { None, // Will be converted into `KyomatoLexErrorKind::NoTokens` @@ -822,7 +914,7 @@ fn tokens_many1< let mut iterator = iterator(input, &mut tokens_kernel); // First, let's try getting first token - let Some(first_token) = (&mut iterator).next() else { + let Some((first_token, first_follower)) = (&mut iterator).next() else { // There is no first token! // Let's give iterator a chance to explain itself: let (rest, _) = iterator.finish()?; @@ -832,19 +924,45 @@ fn tokens_many1< return Ok((rest, InnerResult::None)); }; - // Now, let's try getting a second one - let Some(second_token) = (&mut iterator).next() else { - // Well, I suppose single token is enough - // Still allow iterator to explain any problem - let (rest, _) = iterator.finish()?; - // Return single variant - return Ok((rest, InnerResult::Single(first_token))); + // Here's a little tricky part: we'll need to check is there are multiple tokens in the input, + // but there are two variants for that: follower of the first token (we have now), + // or just the second token iterator would yield. + // That said, I find it natural to make this check in one go with `match`. + // The following block will continue execution if and only there are multiple tokens, + // and there are (possibly) more tokens to parse + let t1 = first_token; + let mut tokens = match (first_follower, (&mut iterator).next()) { + // Meaning there are no more tokens (or iterator failed) - return single result + (None, None) => { + let (rest, _) = iterator.finish()?; + return Ok((rest, InnerResult::Single(t1))); + } + // There are exactly two tokens (or iterator failed) - return exactly two tokens + (Some(t2), None) => { + let (rest, _) = iterator.finish()?; + return Ok((rest, InnerResult::Multiple(vec![t1, t2]))); + } + // There are at least two tokens + (None, Some((t2, None))) => { + vec![t1, t2] + } + // There are at least three tokens + (None, Some((t2, Some(t3)))) | (Some(t2), Some((t3, None))) => { + vec![t1, t2, t3] + } + // There are at least four tokens + (Some(t2), Some((t3, Some(t4)))) => { + vec![t1, t2, t3, t4] + } }; - // Ok so there are at least two tokens now. - // Heap allocation is unavoidable - let mut tokens = vec![first_token, second_token]; - tokens.extend(&mut iterator); + // Parse rest of the tokens + for (token, follower) in &mut iterator { + tokens.push(token); + if let Some(follower) = follower { + tokens.push(follower); + } + } // Iterator might have failed silently, so let them a chance to tell about it let (rest, _) = iterator.finish()?; // Return multiple variant @@ -876,20 +994,20 @@ pub fn lex< "unlimited lex", tokens_many1(alt(( // Allow them all! - div_page, - header, - equation, - table, - figure, - href, - code_block, - ayano, - list, - formatting, - inline_math, - reference, - footnote_content, - footnote_reference, + div_page.map(__equalizer), + header.map(__equalizer), + equation.map(__equalizer), + table.map(__equalizer), + figure.map(__equalizer), + href.map(__equalizer), + code_block.map(__equalizer), + ayano.map(__equalizer), + list.map(__equalizer), + formatting.map(__equalizer), + inline_math.map(__equalizer), + reference.map(__equalizer), + footnote_content.map(__equalizer), + footnote_reference.map(__equalizer), paragraph, ))), ) @@ -944,37 +1062,43 @@ pub fn inner_lex< div_page, "page divider", "pages can only be divided in the outer layer", - ), - forbid(header, "header", "you are not supposed to do that"), + ) + .map(__equalizer), + forbid(header, "header", "you are not supposed to do that").map(__equalizer), forbid( equation, "display equation", "LaTeX does not support display math inside captions/footnotes", - ), - forbid(table, "table", "table can only be an outer element"), - forbid(figure, "figure", "figure can only be an outer element"), - forbid(code_block, "code block", "you are not supposed to do that"), + ) + .map(__equalizer), + forbid(table, "table", "table can only be an outer element").map(__equalizer), + forbid(figure, "figure", "figure can only be an outer element").map(__equalizer), + forbid(code_block, "code block", "you are not supposed to do that") + .map(__equalizer), forbid( list, "list", "LaTeX does not support lists inside captions and footnotes well", - ), + ) + .map(__equalizer), forbid( ayano, "ayano", "Ayano on the inner layer is not supported, as of now", - ), + ) + .map(__equalizer), forbid( footnote_content, "footnote content", "All footnote content must be outer-layer definitions", - ), + ) + .map(__equalizer), // Rest are ok to be parsed! - href, - formatting, - inline_math, - reference, - footnote_reference, + href.map(__equalizer), + formatting.map(__equalizer), + inline_math.map(__equalizer), + reference.map(__equalizer), + footnote_reference.map(__equalizer), paragraph, )) }), From fe511ee5fbea00dd807385a0b7d894cff72825b9 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Mon, 11 Mar 2024 07:30:25 +0200 Subject: [PATCH 09/98] pseudo-nightly: - began writing tests for parsers --- Cargo.toml | 1 + src/lexer.rs | 217 ++++++++++++++++++++++++++++++++------------------- 2 files changed, 138 insertions(+), 80 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c40f5c0..9b23eba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,4 @@ url = "2.5.0" [dev-dependencies] text-diff = "0.4.0" +nom = { version = "7.1.3", features = ["alloc"] } diff --git a/src/lexer.rs b/src/lexer.rs index 2d6a113..d12390b 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -1,20 +1,13 @@ -use std::{ - borrow::{Borrow, Cow}, - num::ParseIntError, - path::Path, - str::FromStr, - sync::RwLock, -}; +use std::{borrow::Cow, num::ParseIntError, path::Path, str::FromStr, sync::RwLock}; -use itertools::Itertools; use nom::{ branch::{alt, permutation}, bytes::{ - complete::{escaped, is_not, tag, take_till1, take_until, take_until1, take_while}, + complete::{escaped, tag, take_till1, take_until, take_until1, take_while}, streaming::tag_no_case, }, character::{ - complete::{alpha1, alphanumeric1, anychar, char, digit1, multispace0, newline, space0}, + complete::{alphanumeric1, char, digit1, multispace0, newline, space0}, is_space, }, combinator::{all_consuming, cut, eof, iterator, map_res, not, opt, recognize, success}, @@ -39,9 +32,9 @@ fn _multispace_line_break<'source, E: ParseError<&'source str>>( // here's a chars iterator let chars = input.char_indices(); // now, limit them to fist non-whitespace character - let non_whitespace = chars.take_while(|(_, c)| !c.is_whitespace()); + let whitespace = chars.take_while(|(_, c)| c.is_whitespace()); // then, filter out newline characters only - let mut newline = non_whitespace.filter_map(|(ind, c)| (c == '\n').then_some(ind)); + let mut newline = whitespace.filter_map(|(ind, c)| (c == '\n').then_some(ind)); // lastly, find first newline character that prepends provided tag let Some(res) = newline.find(|ind| input[(ind + 1)..].starts_with(tag)) else { // if there's no such idex, that's an error @@ -155,7 +148,7 @@ fn caption_kernel< fn div_page<'source, E: ParseError<&'source str> + ContextError<&'source str>>( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { - context("page divider", tuple((multispace0, tag("------")))) + context("page divider", _multispace_line_break("------")) .map(|_| Token::PageDiv) .parse(input) } @@ -779,80 +772,80 @@ fn paragraph< + ContextError<&'source str> + FromExternalError<&'source str, KyomatoLexErrorKind>, >( - input: &'source str, + outer_input: &'source str, ) -> IResult<&'source str, (Token<'source>, Option>), E> { - // TODO wrap into preceded(multispace0, ...) + preceded(multispace0, |input: &'source str| { + // At this point we assume, that whitespace before was parsed, including any possible newlines - // At this point we assume, that whitespace before was parsed, including any possible newlines + // Any token starting with a newline is out of the question + // However, this still leaves a couple of candidates: + // - href (starting with '[') + // - formatting (starting with '*', '_' or '~') + // - inline_math (starting with '$') + // - reference (starting with "[@") + // - footnote_reference (starting with "[^") - // Any token starting with a newline is out of the question - // However, this still leaves a couple of candidates: - // - href (starting with '[') - // - formatting (starting with '*', '_' or '~') - // - inline_math (starting with '$') - // - reference (starting with "[@") - // - footnote_reference (starting with "[^") + // First, let's isolate input to a newline (if there is no newline, stop at last char) + let interrupt_ind = input.find('\n').unwrap_or(input.len() - 1); + let cut_input = &input[..=interrupt_ind]; - // First, let's isolate input to a newline (if there is no newline, stop at last char) - let interrupt_ind = input.find('\n').unwrap_or(input.len() - 1); - let cut_input = &input[..=interrupt_ind]; - - // Now, let's try finding interrupting token - let interuption = cut_input - .char_indices() - // Parsing continues until newline is reached - .take_while(|(_, c)| c != &'\n') - // Possible breakpoints are at characters '[', '*', '_', '~', and '$' - .filter(|(_, c)| ['[', '*', '_', '~', '$'].contains(c)) - // Now, try a proper parser for each one - .map(|(ind, c)| { - let cut_cut_input = &cut_input[ind..]; - let parse_res = match c { - '[' => { - if cut_input[ind..].starts_with("[@") { - reference(cut_cut_input) - } else if cut_input[ind..].starts_with("[^") { - footnote_reference(cut_cut_input) - } else { - href(cut_cut_input) + // Now, let's try finding interrupting token + let interuption = cut_input + .char_indices() + // Parsing continues until newline is reached + .take_while(|(_, c)| c != &'\n') + // Possible breakpoints are at characters '[', '*', '_', '~', and '$' + .filter(|(_, c)| ['[', '*', '_', '~', '$'].contains(c)) + // Now, try a proper parser for each one + .map(|(ind, c)| { + let cut_cut_input = &cut_input[ind..]; + let parse_res = match c { + '[' => { + if cut_input[ind..].starts_with("[@") { + reference(cut_cut_input) + } else if cut_input[ind..].starts_with("[^") { + footnote_reference(cut_cut_input) + } else { + href(cut_cut_input) + } } - } - '*' | '_' | '~' => formatting(cut_cut_input), - '$' => inline_math(cut_cut_input), - _ => unreachable!(), - }; - (ind, parse_res) - }) - .try_fold((), |_, (ind, parse_res)| match parse_res { - // if there is a match for the following token -- return this "Ok" wrapped into "Err", forcing iterator to stop - Ok(ok) => Err(Ok((ind, ok))), - // if there is not a match, but a FAUILURE -- return it as "Err" wrapped in "Err", forcing iterator to stop - Err(nom::Err::Failure(err)) => Err(Err(nom::Err::Failure(err))), - // if parsing had failed, continue driving iterator by returning "Ok" - Err(_) => Ok(()), - }) - .err(); + '*' | '_' | '~' => formatting(cut_cut_input), + '$' => inline_math(cut_cut_input), + _ => unreachable!(), + }; + (ind, parse_res) + }) + .try_fold((), |_, (ind, parse_res)| match parse_res { + // if there is a match for the following token -- return this "Ok" wrapped into "Err", forcing iterator to stop + Ok(ok) => Err(Ok((ind, ok))), + // if there is not a match, but a FAUILURE -- return it as "Err" wrapped in "Err", forcing iterator to stop + Err(nom::Err::Failure(err)) => Err(Err(nom::Err::Failure(err))), + // if parsing had failed, continue driving iterator by returning "Ok" + Err(_) => Ok(()), + }) + .err(); - let interruption = if let Some(interruption) = interuption { - // there was an interruption! - // but it might be a FAILURE ONE. That should bubble up - Some(interruption?) - } else { - None - }; + let interruption = if let Some(interruption) = interuption { + // there was an interruption! + // but it might be a FAILURE ONE. That should bubble up + Some(interruption?) + } else { + None + }; - // Now, construct the output - let this_token_end = interruption - .as_ref() - .map(|(ind, _)| *ind) - .unwrap_or(interrupt_ind); - let this_token = Token::Paragraph(Font::Normal, Cow::from(&cut_input[..this_token_end])); - let rest_input = interruption - .as_ref() - .map(|(_, (rest, _))| *rest) - .unwrap_or(&cut_input[this_token_end..]); - let follower = interruption.map(|(_, (_, f))| f); - Ok((rest_input, (this_token, follower))) + // Now, construct the output + let this_token_end = interruption + .as_ref() + .map(|(ind, _)| *ind) + .unwrap_or(interrupt_ind); + let this_token = Token::Paragraph(Font::Normal, Cow::from(&cut_input[..this_token_end])); + let rest_input = interruption + .as_ref() + .map(|(_, (rest, _))| *rest) + .unwrap_or(&cut_input[this_token_end..]); + let follower = interruption.map(|(_, (_, f))| f); + Ok((rest_input, (this_token, follower))) + })(outer_input) } fn __equalizer(token: Token<'_>) -> (Token<'_>, Option>) { @@ -1105,3 +1098,67 @@ pub fn inner_lex< ) .parse(input) } + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! _assert_inner { + ($result:expr, $expected:pat, $message:literal) => {}; + ($result:expr, $expected:expr, $message:literal) => { + assert_eq!($result, $expected, $message) + }; + } + + macro_rules! test { + {$name:ident, $parser:expr, $input:literal, e: $expected:expr} => { + #[test] + fn $name() { + // arrange + + // act + let result: Result<_, nom::Err>>; + result = $parser.parse($input); + + // assert + assert_eq!(result, $expected, "Parser did not produce expected result"); + } + }; + {$name:ident, $parser:expr, $input:literal, p: $expected:pat} => { + #[test] + fn $name() { + // arrange + + // act + let result: Result<_, nom::Err>>; + result = $parser.parse($input); + + // assert + if !matches!(result, $expected) { + panic!("{}: {:?}", "Parser did not produce complying result:", result); + } + } + }; + } + + // `_multispace_line_break` tests + test! {multispace_line_break1, _multispace_line_break(""), "\n", e: Ok(("", ("\n", "")))} + test! {multispace_line_break2, _multispace_line_break("1"), "\n", p: Err(_)} + test! {multispace_line_break3, _multispace_line_break("1"), " \t\t\n1benzene", e: Ok(("benzene", (" \t\t\n", "1")))} + test! {multispace_line_break4, _multispace_line_break("ben"), " \t\t\nbenzene", e: Ok(("zene", (" \t\t\n", "ben")))} + test! {multispace_line_break5, _multispace_line_break("ben"), "l \t\t\nbenzene", p: Err(_)} + + // `parse_args!` tests + test! {parse_args1, parse_args!["a" = digit1], "", p: Err(_)} + test! {parse_args2, parse_args!["a" = digit1], "{}", p: Err(_)} + test! {parse_args3, parse_args!["a" = digit1], "\n{a = 34522}", e: Ok(("", (Some("34522"), )))} + test! {parse_args3_2, parse_args!["a" = digit1], "{a = 34522}", p: Err(_)} + + // `div_page` tests + test! {div_page1, div_page, "", p: Err(_)} + test! {div_page2, div_page, "---", p: Err(_)} + test! {div_page3, div_page, "\n------", e: Ok(("", Token::PageDiv))} + test! {div_page3_2, div_page, "------", p: Err(_)} + test! {div_page4, div_page, "\n ------", p: Err(_)} + test! {div_page5, div_page, " \t\n------\t\n", e: Ok(("\t\n", Token::PageDiv))} +} From ebce9677d924f24df339c579221fc19c278ee8d5 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Tue, 12 Mar 2024 22:51:39 +0200 Subject: [PATCH 10/98] nightly: - It turns out, `nom`'s `permutation` won't work for optional arguments. Implemented my version, that actually does that. - Added a couple of tests for parsers --- src/lexer.rs | 268 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 221 insertions(+), 47 deletions(-) diff --git a/src/lexer.rs b/src/lexer.rs index d12390b..6baf37c 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -1,7 +1,7 @@ use std::{borrow::Cow, num::ParseIntError, path::Path, str::FromStr, sync::RwLock}; use nom::{ - branch::{alt, permutation}, + branch::alt, bytes::{ complete::{escaped, tag, take_till1, take_until, take_until1, take_while}, streaming::tag_no_case, @@ -9,6 +9,7 @@ use nom::{ character::{ complete::{alphanumeric1, char, digit1, multispace0, newline, space0}, is_space, + streaming::line_ending, }, combinator::{all_consuming, cut, eof, iterator, map_res, not, opt, recognize, success}, error::{context, ContextError, ErrorKind, FromExternalError, ParseError}, @@ -54,70 +55,172 @@ fn _multispace_line_break<'source, E: ParseError<&'source str>>( } } +fn _optional_permutation<'parsers, 'source, E: ParseError<&'source str>, const N: usize>( + parsers: [&'parsers mut dyn Parser<&'source str, &'source str, E>; N], +) -> impl Parser<&'source str, [Option<&'source str>; N], E> + 'parsers { + move |mut input| { + // the end result + let mut parsed = [None::<&'source str>; N]; + 'outer: for _ in 0..N { + for i in 0..N { + if parsed[i].is_none() { + match parsers[i].parse(input) { + Ok((rest, res)) => { + // this parser succeeded, advance forward + parsed[i] = Some(res); + input = rest; + continue 'outer; // continue outer iteration + } + Err(nom::Err::Failure(err)) => { + // propagate a failure + return Err(nom::Err::Failure(err)); + } + Err(_) => { + // well, do nothing + } + } + } + } + // all parser had failed to parse (success would've continued outer cycle) + // break out of outer cycle + break 'outer; + } + // return the result (input variable was updated to be rest of the input, on successful parses) + Ok((input, parsed)) + } +} + // Conventions: // - parsers should only parse empty characters with `empty` // - parsers should only parse empty space at the beginning, not the end // - newline parsing should be carefully managed -// TODO probably move that to util or th -// I won't include the entire tuple operation library, while I can perfectly implement this myself. -mod remove_last_unit { - - pub trait Trait { - type Output; - fn remove_last_unit(self) -> Self::Output; +// TODO probably move to util, or even try committing to `nom` :idk: +mod optional_permutation { + pub trait OptionalPermutation { + fn optional_permutation(&mut self, input: In) -> super::IResult; } - macro_rules! impl_tuple_remove_last_unit { - {$A0:ident} => { - impl<$A0> Trait for ($A0, ()) { type Output = ($A0, ); - - #[allow(non_snake_case)] - #[inline] - fn remove_last_unit(self) -> Self::Output { - let ($A0, _) = self; - ($A0, ) + macro_rules! impl_optional_permutation { + {$Count:expr, $type_ident:ident: $out_ident:ident} => { + // base case + impl<'source, Err, $out_ident, $type_ident> OptionalPermutation<&'source str, (Option<$out_ident>,), Err> for ($type_ident,) + where + Err: super::ParseError<&'source str>, + $type_ident: super::Parser<&'source str, $out_ident, Err>, + { + fn optional_permutation(&mut self, input: &'source str) -> super::IResult<&'source str, (Option<$out_ident>,), Err> + { + // in a debug build, let's assert that we've passed a correct type count + debug_assert!($Count == 1, "Should parse number equal to number of arguments"); + // for a single variant, just try parsing it, and propagate any sort of failure + match self.0.parse(input) { + Ok((rest, result)) => Ok((rest, (Some(result), ))), + Err(nom::Err::Failure(err)) => Err(nom::Err::Failure(err)), + Err(_) => Ok((input, (None, ))) + } } } }; - {$A0:ident $($Ai:ident) +} => { - impl<$A0, $($Ai), +> Trait for ($A0, $($Ai), +, ()) { - type Output = ($A0, $($Ai),+); + {$Count:expr, $type_ident0:ident: $out_ident0:ident, $($type_ident:ident: $out_ident:ident), *} => { + impl<'source, Err, $out_ident0, $($out_ident), *, $type_ident0, $($type_ident), *> OptionalPermutation<&'source str, (Option<$out_ident0>, $(Option<$out_ident>, ) *), Err> + for ($type_ident0, $($type_ident, ) *) + where + Err: super::ParseError<&'source str>, + $type_ident0: super::Parser<&'source str, $out_ident0, Err>, + $( + $type_ident: super::Parser<&'source str, $out_ident, Err> + ), * + { + fn optional_permutation(&mut self, mut input: &'source str) -> super::IResult<&'source str, (Option<$out_ident0>, $(Option<$out_ident>), *), Err> { + // for multiple variants, here comes chaos: + // we can't create any sort of array or vector to hold the results - + // they have different types, that's the point! + // So we are bound to creating a BUNCH of variables + let mut $out_ident0: Option<$out_ident0> = None; + $( + let mut $out_ident: Option<$out_ident> = None; + ) * + // Also, since we can't index tuples, we'll need to "deconstruct self" + // I don't feel like creating separate identifiers for that, so I guess compiler IS to complain :idk: + let ($type_ident0, $($type_ident, ) *) = self; + // Now we can use `$type_ident` as corresponding parser, and `$out_ident` as a output variable! + // Amazing! (not really; I imagine that would be a nightmare to read) - #[allow(non_snake_case)] - #[inline] - fn remove_last_unit(self) -> Self::Output { - let ($A0, $($Ai), *, ()) = self; - ($A0, $($Ai), +) + // This is a max number of iterations we're gonna need + const Count: usize = $Count; + for _ in 0..Count { + if $out_ident0.is_none() { + match $type_ident0.parse(input) { + Ok((rest, result)) => { + input = rest; + $out_ident0 = Some(result); + continue; // continues parsing attempts from the beginning + }, + Err(nom::Err::Failure(err)) => return Err(nom::Err::Failure(err)), + Err(_) => {} + } + } + $( + if $out_ident.is_none() { + match $type_ident.parse(input) { + Ok((rest, result)) => { + input = rest; + $out_ident = Some(result); + continue; // continues parsing attempts from the beginning + }, + Err(nom::Err::Failure(err)) => return Err(nom::Err::Failure(err)), + Err(_) => {} + } + } + ) * + // All parsing attempts had failed? + // Break out, we're done here. + break; + } + + Ok((input, ($out_ident0, $($out_ident, ) *))) } } - impl_tuple_remove_last_unit!{$($Ai) +} + // forward to N-1 + impl_optional_permutation!{$Count - 1, $($type_ident: $out_ident), *} }; } - impl_tuple_remove_last_unit! {A B C D E F G H I J K L M N O P Q R S T U V W} + impl_optional_permutation! {16, A: A_Out, B: B_Out, C: C_Out, D: D_Out, E: E_Out, F: F_Out, G: G_Out, H: H_Out, I: I_Out, J: J_Out, K: K_Out, L: L_Out, M: M_Out, N: N_Out, O: O_Out, P: P_Out} +} + +fn optional_permutation<'parsers, 'source: 'parsers, In, Out, E: ParseError<&'source str>>( + mut parsers: impl optional_permutation::OptionalPermutation + 'parsers, +) -> impl Parser + 'parsers { + move |input| parsers.optional_permutation(input) } /// Generates argument parser that accounts for all necessary syntax macro_rules! parse_args { [$($argument_name:literal = $argument_kernel:expr), +] => { - tuple(( + preceded( + pair( space0, tag("\n{"), // NOTE: arguments must start from an empty line - permutation(( + ), + cut(terminated( + optional_permutation(( $( - opt(tuple(( + tuple(( space0, tuple((tag($argument_name), space0, char('='), space0, context("arg", context($argument_name, $argument_kernel)))), space0, alt((char(','), nom::combinator::peek(char('}')))), // this can have unexpected consequences, but whatever :idk: - ))).map(|value| value.map(|(_, (_, _, _, _, v), _, _)| v)) + )).map(|(_, (_, _, _, _, v), _, _)| v) ), +, - success(()) )), - char('}'), - )).map(|(_, _, out, _)| <_ as remove_last_unit::Trait>::remove_last_unit(out)) + char('}'))), + ) + // If parser above had failed - it's alright, arguments are optional anyway + // Return `None` variant for everyone in this case + .or(|input| Ok((input, ($({let _ = $argument_name; None},) +)))) }; } @@ -169,8 +272,7 @@ fn header< context( "header", tuple(( - multispace0::<&'source str, E>, - newline, + _multispace_line_break(""), map_res(many1_count(char('#')), |count| { if count <= 6 { Ok(count) @@ -182,7 +284,7 @@ fn header< take_until1("\n"), )), ) - .map(|(_, _, order, _, content)| Token::Header { + .map(|(_, order, _, content)| Token::Header { order, content: content.trim_end().into(), // kinda sad I'm forced to do this here, but whatever }) @@ -205,7 +307,7 @@ fn equation< let (rest, (content, _, (ident,))) = context( "display mathmode", preceded( - _multispace_line_break("$$"), + _multispace_line_break("$$\n"), // if mathmode opening found, lock into equation match cut(tuple(( take_until1("\n$$"), @@ -421,16 +523,16 @@ fn ayano< // This is guaranteed to be Ayano block at this point, so we lock in cut(pair( terminated( - permutation(( - opt(preceded( + optional_permutation(( + preceded( pair(char('*'), space0), opt(terminated(caption_kernel, space0)), - )), - opt(preceded( + ), + preceded( pair(char('~'), space0), terminated(take_till1(char::is_whitespace), space0), - )), - opt(preceded( + ), + preceded( pair(char('#'), space0), terminated( map_res(digit1, |n| { @@ -438,8 +540,8 @@ fn ayano< }), space0, ), - )), - opt(terminated(char('!'), space0)), + ), + terminated(char('!'), space0), )), char('\n'), ), @@ -1118,7 +1220,7 @@ mod tests { // act let result: Result<_, nom::Err>>; - result = $parser.parse($input); + result = $parser.parse($input); // assert assert_eq!(result, $expected, "Parser did not produce expected result"); @@ -1153,6 +1255,7 @@ mod tests { test! {parse_args2, parse_args!["a" = digit1], "{}", p: Err(_)} test! {parse_args3, parse_args!["a" = digit1], "\n{a = 34522}", e: Ok(("", (Some("34522"), )))} test! {parse_args3_2, parse_args!["a" = digit1], "{a = 34522}", p: Err(_)} + test! {parse_args4, parse_args!["a" = digit1, "b" = alphanumeric1], "\n{b = abc12, a = 34522 }abc", e: Ok(("abc", (Some("34522"), Some("abc12"))))} // `div_page` tests test! {div_page1, div_page, "", p: Err(_)} @@ -1161,4 +1264,75 @@ mod tests { test! {div_page3_2, div_page, "------", p: Err(_)} test! {div_page4, div_page, "\n ------", p: Err(_)} test! {div_page5, div_page, " \t\n------\t\n", e: Ok(("\t\n", Token::PageDiv))} + + // `header` tests + test! {header1, header, "", p: Err(_)} + test! {header2, header, "\n#Ababa\n", e: Ok(("\n", Token::Header { order: 1, content: "Ababa".into() }))} + test! {header3, header, "\n# Ababa \t \t\n", e: Ok(("\n", Token::Header { order: 1, content: "Ababa".into() }))} + test! {header4, header, "\n### Ababa \n", e: Ok(("\n", Token::Header { order: 3, content: "Ababa".into() }))} + test! {header5, header, "\n###### Заголовок українською \n", e: Ok(("\n", Token::Header { order: 6, content: "Заголовок українською".into() }))} + test! {header6, header, "\n####### Заголовок українською \n", p: Err(_)} + + // `equation` tests + test! {equation1, equation, "", p: Err(_)} + test! {equation2, equation, "$$", p: Err(_)} + test! {equation3, equation, "$$$$", p: Err(_)} // NOTE: this thing is technically allowed in markdown, but you probably shouldn't do it + test! {equation4, equation, r" + $$ + y = x^2 + $$ + ", p: Err(_)} // NOTE: same here. Please start your equations at the start of the line + test! {equation5, equation, r" +$$ +y = x^2 +$$ +12344", e: Ok(("\n12344", Token::Equation { content: Cow::Borrowed("y = x^2"), ident: None }))} + // Like that! + test! {equation6, equation, r" +$$y = x^2 +z = y; +$$ +12344", p: Err(_)} // Don't do this and the following, too + test! {equation7, equation, r" +$$ +y = x^2 +z = y$$; +12344", p: Err(_)} + test! {equation8, equation, r" +$$ +y = x^2 +z = y +$$;12344", e: Ok((";12344", Token::Equation { content: "y = x^2\nz = y".into(), ident: None }))} + // This is alright, though + test! {equation9, equation, r#" +$$ +y = x^2 +$$ +{ref = parabola}"#, e: Ok(("", Token::Equation { content: "y = x^2".into(), ident: Some("parabola".into()) }))} + // Equation ident example + test! {equation10, equation, r#" +$$ +y = x^2 +$$ +{ref = parabola_1}"#, p: Err(_)} + // This is not ok + // TODO although that's subject to change + test! {equation11, equation, r#" + $$ + y = x^2 + $$ + {tel = +4204206969}"#, p: Err(_)} + // Don't put random garbage there! + test! {equation12, equation, r#" +$$ +y = x^2 +$$ +{ref=11 } якийсь текст тому що чому ні"#, e: Ok((" якийсь текст тому що чому ні", Token::Equation { content: "y = x^2 ".into(), ident: Some("11".into()) }))} + // Any sort of whitespaces are preserved + test! {equation13, equation, r#" +$$ +y = x^2 +$$ +{} якийсь текст тому що чому ні"#, e: Ok((" якийсь текст тому що чому ні", Token::Equation { content: "y = x^2 ".into(), ident: None }))} + // You can explicitly provide no args - fun fact } From 610ff126c63d3db91c8365175d509ba77631dc77 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Tue, 12 Mar 2024 23:11:32 +0200 Subject: [PATCH 11/98] repaired the tests (certain function turned out not implemented) --- src/data.rs | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- src/lexer.rs | 9 +++++---- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/data.rs b/src/data.rs index f0ab542..5accc61 100644 --- a/src/data.rs +++ b/src/data.rs @@ -168,6 +168,55 @@ impl<'source> Token<'source> { where 'source: 'r, { - todo!() + match self { + Token::PageDiv => Token::PageDiv, + Token::Header { order, content } => Token::Header { + order: *order, + content: Cow::Borrowed(content), + }, + Token::Equation { content, ident } => Token::Equation { + content: Cow::Borrowed(&content), + ident: ident.as_ref().map(|s| Cow::<'r, str>::Borrowed(s)), + }, + Token::Table { + header, + cells, + caption, + ident, + } => Token::Table { + header: header.borrowed(), + cells: cells.borrowed(), + caption: caption.as_ref().map(|t| Box::new(Token::borrow_ref(t))), + ident: ident.as_ref().map(|s| Cow::<'r, str>::Borrowed(s)), + }, + Token::Figure { + src_name, + caption, + ident, + } => Token::Figure { + src_name: Cow::Borrowed(src_name), + caption: caption.as_ref().map(|t| Box::new(Token::borrow_ref(t))), + ident: ident.as_ref().map(|s| Cow::<'r, str>::Borrowed(s)), + }, + Token::Href { url, display } => { + todo!() + } + Token::Ayano(_) => todo!(), + Token::List { list_type, content } => todo!(), + Token::Formatted(_, _) => todo!(), + Token::Text(_) => todo!(), + Token::Paragraph(font, content) => { + Token::Paragraph(font.clone(), Cow::Borrowed(content)) + } + Token::InlineMathmode(_) => todo!(), + Token::Reference(_) => todo!(), + Token::FootNoteReference(ident) => Token::FootNoteReference(Cow::Borrowed(ident)), + Token::FootNoteContent { content, ident } => Token::FootNoteContent { + content: Box::new(content.borrow_ref()), + ident: Cow::Borrowed(ident), + }, + Token::CodeBlock { code, language } => todo!(), + Token::Error(_) => todo!(), + } } } diff --git a/src/lexer.rs b/src/lexer.rs index 6baf37c..559cafe 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -1251,10 +1251,11 @@ mod tests { test! {multispace_line_break5, _multispace_line_break("ben"), "l \t\t\nbenzene", p: Err(_)} // `parse_args!` tests - test! {parse_args1, parse_args!["a" = digit1], "", p: Err(_)} - test! {parse_args2, parse_args!["a" = digit1], "{}", p: Err(_)} + test! {parse_args1, parse_args!["a" = digit1], "", e: Ok(("", (None,)))} + test! {parse_args2, parse_args!["a" = digit1], "{}", e: Ok(("{}", (None,)))} test! {parse_args3, parse_args!["a" = digit1], "\n{a = 34522}", e: Ok(("", (Some("34522"), )))} - test! {parse_args3_2, parse_args!["a" = digit1], "{a = 34522}", p: Err(_)} + test! {parse_args3_2, parse_args!["a" = digit1], "{a = 34522}", e: Ok(("{a = 34522}", (None,)))} + test! {parse_args3_3, parse_args!["a" = digit1], "\n{a = ", p: Err(_)} test! {parse_args4, parse_args!["a" = digit1, "b" = alphanumeric1], "\n{b = abc12, a = 34522 }abc", e: Ok(("abc", (Some("34522"), Some("abc12"))))} // `div_page` tests @@ -1334,5 +1335,5 @@ $$ y = x^2 $$ {} якийсь текст тому що чому ні"#, e: Ok((" якийсь текст тому що чому ні", Token::Equation { content: "y = x^2 ".into(), ident: None }))} - // You can explicitly provide no args - fun fact + // You can explicitly provide no args - fun fact } From 78c97c7d406d3ef17264560dfb7d700342a3f0a5 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Wed, 13 Mar 2024 09:10:44 +0200 Subject: [PATCH 12/98] wrote some more tests Ran into a problem of parsers invoking each other in recursion. I guess a little redesign would be necessary. --- src/lexer.rs | 217 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 192 insertions(+), 25 deletions(-) diff --git a/src/lexer.rs b/src/lexer.rs index 559cafe..e365ccd 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -428,24 +428,25 @@ fn href< >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { - let (rest, (display, url)) = context( + context( "href", - tuple(( - delimited(char('['), take_until("]"), char(']')), - // url should be parsed into proper format - map_res( - delimited(char('('), take_until(")"), char(')')), - |url: &str| Url::from_str(url).map_err(KyomatoLexErrorKind::BadUrl), + preceded( + multispace0, + pair( + delimited(char('['), take_until("]"), char(']')), + // url should be parsed into proper format + map_res( + delimited(char('('), take_until(")"), char(')')), + |url: &str| Url::from_str(url).map_err(KyomatoLexErrorKind::BadUrl), + ), ), - )), - )(input)?; - Ok(( - rest, - Token::Href { - url, - display: display.into(), - }, - )) + ), + ) + .map(|(display, url)| Token::Href { + url, + display: display.into(), + }) + .parse(input) } /// Parses code block @@ -771,7 +772,7 @@ fn formatting< } } // If this point was reached, there is no correct variant for pairing delimiter. - Err(nom::Err::Failure(E::add_context( + Err(nom::Err::Error(E::add_context( input, "formatting", E::from_error_kind(input, ErrorKind::Eof), @@ -784,9 +785,12 @@ fn inline_math<'source, E: ParseError<&'source str> + ContextError<&'source str> ) -> IResult<&'source str, Token<'source>, E> { context( "inline math", - delimited(char('$'), take_until1("$"), char('$')), + preceded( + multispace0, + delimited(char('$'), take_until1("$"), char('$')), + ), ) - .map(|content: &str| Token::InlineMathmode(content.into())) + .map(|content: &str| Token::InlineMathmode(content.trim().into())) .parse(input) } @@ -805,7 +809,11 @@ fn reference<'source, E: ParseError<&'source str> + ContextError<&'source str>>( ) -> IResult<&'source str, Token<'source>, E> { let (rest, ident) = context( "reference", - delimited(pair(space0, tag("[@")), cut(take_until1("]")), char(']')), + delimited( + pair(multispace0, tag("[@")), + cut(preceded(space0, take_until1("]"))), + char(']'), + ), )(input)?; Ok((rest, Token::Reference(ident.trim().into()))) } @@ -819,7 +827,11 @@ fn footnote_reference<'source, E: ParseError<&'source str> + ContextError<&'sour ) -> IResult<&'source str, Token<'source>, E> { let (rest, ident) = context( "footnote reference", - delimited(pair(space0, tag("[^")), cut(take_until1("]")), char(']')), + delimited( + pair(multispace0, tag("[^")), + cut(preceded(space0, take_until1("]"))), + char(']'), + ), )(input)?; Ok((rest, Token::FootNoteReference(ident.trim().into()))) } @@ -879,6 +891,11 @@ fn paragraph< preceded(multispace0, |input: &'source str| { // At this point we assume, that whitespace before was parsed, including any possible newlines + // If input turns out empty after that, return error + if input.is_empty() { + return Err(nom::Err::Error(E::from_error_kind(input, ErrorKind::Eof))); + } + // Any token starting with a newline is out of the question // However, this still leaves a couple of candidates: // - href (starting with '[') @@ -892,7 +909,7 @@ fn paragraph< let cut_input = &input[..=interrupt_ind]; // Now, let's try finding interrupting token - let interuption = cut_input + let interruption = cut_input .char_indices() // Parsing continues until newline is reached .take_while(|(_, c)| c != &'\n') @@ -927,7 +944,7 @@ fn paragraph< }) .err(); - let interruption = if let Some(interruption) = interuption { + let interruption = if let Some(interruption) = interruption { // there was an interruption! // but it might be a FAILURE ONE. That should bubble up Some(interruption?) @@ -944,7 +961,7 @@ fn paragraph< let rest_input = interruption .as_ref() .map(|(_, (rest, _))| *rest) - .unwrap_or(&cut_input[this_token_end..]); + .unwrap_or(&input[this_token_end..]); let follower = interruption.map(|(_, (_, f))| f); Ok((rest_input, (this_token, follower))) })(outer_input) @@ -1243,6 +1260,8 @@ mod tests { }; } + // Tests were written in order they depend on each other + // `_multispace_line_break` tests test! {multispace_line_break1, _multispace_line_break(""), "\n", e: Ok(("", ("\n", "")))} test! {multispace_line_break2, _multispace_line_break("1"), "\n", p: Err(_)} @@ -1266,6 +1285,8 @@ mod tests { test! {div_page4, div_page, "\n ------", p: Err(_)} test! {div_page5, div_page, " \t\n------\t\n", e: Ok(("\t\n", Token::PageDiv))} + // TODO add caption kernel tests + // `header` tests test! {header1, header, "", p: Err(_)} test! {header2, header, "\n#Ababa\n", e: Ok(("\n", Token::Header { order: 1, content: "Ababa".into() }))} @@ -1335,5 +1356,151 @@ $$ y = x^2 $$ {} якийсь текст тому що чому ні"#, e: Ok((" якийсь текст тому що чому ні", Token::Equation { content: "y = x^2 ".into(), ident: None }))} - // You can explicitly provide no args - fun fact + // You can explicitly provide no args - fun fact :idk: + + // TODO add table tests + + // TODO add figure tests + + // `href` tests + mod href { + use super::*; + + macro_rules! href_ok { + {$name:ident, $input:expr, $output_url:expr, $output_display:expr, $left_over:literal} => { + test! {$name, href, $input, e: Ok(($left_over, Token::Href { url: $output_url.parse().expect("This is a valid url"), display: $output_display.into() }))} + }; + } + test! {href1, href, "[link text]()", p: Err(_)} // empty url is not ok + test! {href2, href, "[](homepage.com)", p: Err(_)} // empty display is not ok too + test! {href3, href, "[невалідне посилання](http://ref ?????)", p: Err(_)} // random garbage instead of url is not accepted + href_ok! {href4, "[link text](ftp://somewebsite/files/01234)", "ftp://somewebsite/files/01234", "link text", ""} // url example + href_ok! {href5, "[текст посилання](ftp://somewebsite/files/01234)", "ftp://somewebsite/files/01234", "текст посилання", ""} // cyrillic test + href_ok! {href6, "[link text](ftp://somewebsite/files/01234), ще текст після нього", "ftp://somewebsite/files/01234", "link text", ", ще текст після нього"} + // text after url is alright + href_ok! {href7, " [link text](ftp://somewebsite/files/01234), ще текст після нього", "ftp://somewebsite/files/01234", "link text", ", ще текст після нього"} + // space before url is ignored + } + + // `code_block` tests + mod code_block { + use super::*; + macro_rules! test_ok { + {$name:ident, $input:expr, $output_lang:expr, $output_code:expr, $left_over:expr} => { + test!{$name, code_block, $input, e: Ok(($left_over, Token::CodeBlock {code: $output_code.into(), language: $output_lang.map(Cow::from)} ))} + }; + } + + macro_rules! test_err { + {$name:ident, $input:literal} => { + test!{$name, code_block, $input, p: Err(_)} + }; + } + + test_ok! {ok_empty, " \n```\n\n```\n", None::<&'static str>, "", "\n"} // a single empty line is ok + test_err! {err_no_line, " \n```\n```\n"} // no lines in the block at all - that's bad + test_ok! {ok_lang, " \n```C sus sus\n\t senpai, you are sus!\n```", Some("C sus sus"), "\t senpai, you are sus!", ""} + // you may specify whatever language you want + test_ok! {ok_cyrillic, " \n```C sus sus\n\t тиск = сила / площа;\n// (і мотивація то є сильна)\n```", + Some("C sus sus"), "\t тиск = сила / площа;\n// (і мотивація то є сильна)", ""} + // you may write whatever you want inside + test_ok! {ok_text_after, " \n```C sus sus\n\t тиск = сила / площа;\n// (і мотивація то є сильна)\n``` It's dangerous - being alone", + Some("C sus sus"), "\t тиск = сила / площа;\n// (і мотивація то є сильна)", " It's dangerous - being alone"} + // text just after the code block is ok too + } + + // TODO add Ayano block tests + + // TODO add list tests + + // `formatting` tests + mod formatting { + macro_rules! test_ok { + {$name:ident, $input:literal} => { + + }; + } + // TODO add formatting tests + } + + // `inline math` tests + mod inline_math { + use super::*; + + macro_rules! test_ok { + {$name:ident, $input:literal, $formula:literal, $left_over:literal} => { + test!{$name, inline_math, $input, e: Ok(($left_over, Token::InlineMathmode($formula.into())))} + }; + } + + test! {err_empty, inline_math, "$$", p: Err(_)} + // completely empty inline math is not ok (as it's ambiguous with display math) + test_ok! {ok_space, "$ $", "", ""} // empty space is ok though (but it will be stripped) + test_ok! {ok_char, "$ x $", "x", ""} + // you can specify a single character, but empty spaces would be tripped + test! {err_unclosed, inline_math, "$ x", p: Err(_)} + // unclosed mathmode is obviously disallowed + test_ok! {ok_cyrillic, "$ текст українською $", "текст українською", ""} + // you can put there whatever you want, however pay attention to latex's rules + test_ok! {ok_spaces, "\t \n\t \n \t $ x $", "x", ""} + // got some spaces before? don't care + test_ok! {ok_text_after, "$ x$some text after it, українською в тому числі", + "x", "some text after it, українською в тому числі"} + // text after is is ok + } + + // tests for `reference` + mod reference { + use super::*; + + macro_rules! test_ok { + {$name:ident, $input:literal, $reference:literal, $left_over:literal} => { + test!{$name, reference, $input, e: Ok(($left_over, Token::Reference($reference.into())))} + }; + } + + test! {err_empty, reference, "[@ ]", p: Err(_)} // empty reference is not ok + test_ok! {ok_char, "[@ s]", "s", ""} // single char as identifier is ok + test_ok! {ok_eq, "[@ eq:parabola]", "eq:parabola", ""} // equation ref example + test_ok! {ok_fig, "[@ fig:my_best_drawing ]", "fig:my_best_drawing", ""} // figure ref example + test_ok! {ok_tab, "[@ tab:boring_stat ]", "tab:boring_stat", ""} // table ref example + test_ok! {ok_space_before, " \n \t [@ ref]", "ref", ""} + test_ok! {ok_text_after, " \n \t [@ ref] кирилиця or th", "ref", " кирилиця or th"} + } + + // tests for `footnote reference` + mod footnote_reference { + use super::*; + + macro_rules! test_ok { + {$name:ident, $input:literal, $reference:literal, $left_over:literal} => { + test!{$name, footnote_reference, $input, e: Ok(($left_over, Token::FootNoteReference($reference.into())))} + }; + } + + test! {err_empty, reference, "[^ ]", p: Err(_)} // empty reference is not ok + test_ok! {ok_char, "[^s]", "s", ""} // single char as identifier is ok + test_ok! {ok_space_before, " \n \t [^explanation_of_42]", "explanation_of_42", ""} + test_ok! {ok_text_after, " \n \t [^y_u_do_dis_2_me] кирилиця or th", "y_u_do_dis_2_me", " кирилиця or th"} + } + + // TODO add footnote content tests + + // tests for `paragraph` + mod paragraph { + use super::*; + + macro_rules! test_ok { + {$name:ident, $input:literal, $paragraph:literal, $follower_token:expr, $left_over:literal} => { + test!{$name, paragraph, $input, e: Ok(($left_over, (Token::Paragraph(Font::Normal, $paragraph.into()), $follower_token)))} + }; + } + + test! {err_empty, paragraph, "", p: Err(_)} // paragraph won't be parsed where it's empty + test! {err_whitespace, paragraph, " \t \n\n \t", p: Err(_)} // nor if it's all whitespace + test_ok! {ok_char, " \t\n c\nfff", "c", None, "\nfff"} // single char is ok as a paragraph + test_ok! {ok_sentence, "\t \n I'm absolutely increative, so here's a bunch of garbage: ]42[t5(2[24tj4-0)jt3_94j03\nand some rest", + "I'm absolutely increative, so here's a bunch of garbage: ]42[t5(2[24tj4-0)jt3_94j03", None, "\nand some rest"} + test_ok! {ok_formatting, "\t \n You can use *bold*!", "You can use ", Some(Token::Formatted(Formatting::Italic, Box::new(Token::text("bold")))), "!"} + } } From 59c03c48a34eb3159cb76b2500d2d3e550153f63 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Thu, 14 Mar 2024 21:58:33 +0200 Subject: [PATCH 13/98] nightly: - solved recursion problem - added tests for tokens up to `table` --- src/lexer.rs | 698 ++++++++++++++++++++++++++++++++++++++++----------- src/util.rs | 50 ++++ 2 files changed, 597 insertions(+), 151 deletions(-) diff --git a/src/lexer.rs b/src/lexer.rs index e365ccd..8502a72 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -1,19 +1,30 @@ -use std::{borrow::Cow, num::ParseIntError, path::Path, str::FromStr, sync::RwLock}; +use std::{ + any::Any, + borrow::Cow, + cmp::Ordering, + error::Error, + fmt::{Debug, Display}, + num::ParseIntError, + path::Path, + str::FromStr, + sync::RwLock, +}; +use itertools::Itertools; use nom::{ branch::alt, bytes::{ - complete::{escaped, tag, take_till1, take_until, take_until1, take_while}, + complete::{tag, take_till1, take_until, take_until1, take_while}, streaming::tag_no_case, }, character::{ - complete::{alphanumeric1, char, digit1, multispace0, newline, space0}, + complete::{alphanumeric1, char, digit1, multispace0, newline, none_of, space0}, is_space, streaming::line_ending, }, combinator::{all_consuming, cut, eof, iterator, map_res, not, opt, recognize, success}, error::{context, ContextError, ErrorKind, FromExternalError, ParseError}, - multi::{many1, many1_count, separated_list1}, + multi::{fold_many1, many1, many1_count, separated_list1}, number::complete::float, sequence::{delimited, pair, preceded, terminated, tuple}, IResult, Parser, @@ -22,7 +33,7 @@ use url::Url; use crate::{ data::{AyanoBlock, Font, Formatting, ListType, Token, Tokens}, - util::IteratorArrayCollectExt, + util::{Equivalent, IteratorArrayCollectExt, VecTryExtendExt}, }; /// Matches any sort of whitespace characters, *newline*, and a provided tag at the end @@ -158,7 +169,10 @@ mod optional_permutation { continue; // continues parsing attempts from the beginning }, Err(nom::Err::Failure(err)) => return Err(nom::Err::Failure(err)), - Err(_) => {} + Err(err) => { + let _ = err; + println!("2"); + } } } $( @@ -170,7 +184,10 @@ mod optional_permutation { continue; // continues parsing attempts from the beginning }, Err(nom::Err::Failure(err)) => return Err(nom::Err::Failure(err)), - Err(_) => {} + Err(err) => { + let _ = err; + println!("2"); + } } } ) * @@ -178,7 +195,7 @@ mod optional_permutation { // Break out, we're done here. break; } - + let _ = 2; Ok((input, ($out_ident0, $($out_ident, ) *))) } } @@ -202,8 +219,8 @@ macro_rules! parse_args { [$($argument_name:literal = $argument_kernel:expr), +] => { preceded( pair( - space0, - tag("\n{"), // NOTE: arguments must start from an empty line + space0, + tag("\n{"), // NOTE: arguments must start from an empty line ), cut(terminated( optional_permutation(( @@ -212,7 +229,7 @@ macro_rules! parse_args { space0, tuple((tag($argument_name), space0, char('='), space0, context("arg", context($argument_name, $argument_kernel)))), space0, - alt((char(','), nom::combinator::peek(char('}')))), // this can have unexpected consequences, but whatever :idk: + alt((char(','), nom::combinator::peek(char('}')))), // this has unexpected consequences, but whatever :idk: )).map(|(_, (_, _, _, _, v), _, _)| v) ), +, )), @@ -226,23 +243,40 @@ macro_rules! parse_args { /// Parses caption (just the literal) part /// -/// Quite tricky task, as there can be double quotes -- but escaped. -/// Apart from single, unescaped quotes, caption can be ANYTHING. -/// There seems to be just the tool for a job -- `escaped` parser -/// TODO FIXME add a mention about caption actually TERMINATING after first unescaped double-quote (and there is no cure), -/// and where exactly escaping backslashes are removed +/// Quite a tricky task, as there can be double quotes -- but escaped. +/// Apart from double unescaped quotes, caption can be ANYTHING. +/// TODO FIXME add a mention about caption attempting to parse after EACH unescaped double quote. First one to succeed will be the output fn caption_kernel< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexErrorKind>, + + FromExternalError<&'source str, KyomatoLexError<'source>> + + 'source, >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { + // first, match opening quote + let (input, _) = char('"')(input)?; + // then, let's filter all double quotes + let double_quotes = input.char_indices().filter(|(_, c)| c == &'"'); + // out of them, choose unescaped ones + let unescaped_quotes = double_quotes.filter(|(i, _)| !&input[..*i].ends_with('\\')); + // lastly, try parsing each of them + let mut parsed = unescaped_quotes + .map(|(i, _)| inner_lex::<'source, E>(dbg!(&input[..i])).map(|(_, t)| (&input[i..], t))); + let (input, token) = + parsed + .find_map(Result::ok) + .ok_or(nom::Err::Error(E::from_external_error( + input, + ErrorKind::Escaped, + KyomatoLexError::no_tokens(input), + )))?; + // FIXME add `terminated(_, space0)` to all `all_consuming` - escaped(not(char('"')), '\\', char('"')) - .and_then(all_consuming(inner_lex::<'source, E>)) - .parse(input) + // lastly, match the actual closing quote + let (input, _) = cut(char('"'))(input)?; + Ok((input, token)) } /// Parses page divider @@ -265,7 +299,7 @@ fn header< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexErrorKind>, + + FromExternalError<&'source str, KyomatoLexError<'source>>, >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { @@ -277,7 +311,7 @@ fn header< if count <= 6 { Ok(count) } else { - Err(KyomatoLexErrorKind::TooDeepHeader { fold: count }) + Err(KyomatoLexError::too_deep_header(input, count)) } }), space0, @@ -300,7 +334,7 @@ fn equation< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexErrorKind>, + + FromExternalError<&'source str, KyomatoLexError<'source>>, >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { @@ -325,6 +359,70 @@ fn equation< )) } +/// Parses table row +/// +/// Parses ALL of it's input +fn _parse_table_row< + 'source, + E: ParseError<&'source str> + + ContextError<&'source str> + + FromExternalError<&'source str, KyomatoLexError<'source>> + + 'source, +>( + input: &'source str, +) -> IResult< + &'source str, + impl Iterator, E>> + 'source, + E, +> { + let pipes = input.char_indices().filter(|(_, c)| c == &'|'); + let unescaped_pipes = + pipes.filter(|(ind, _)| ind > &0 && !input[(ind - 1)..].starts_with(r"\|")); + let mut dividers = unescaped_pipes.map(|(ind, _)| ind); + let mut last_ind = dividers + .next() + .ok_or(nom::Err::Error(E::from_external_error( + input, + ErrorKind::Eof, + KyomatoLexError::no_tokens(input), + )))?; + let slices = std::iter::once(&input[..last_ind]).chain(dividers.filter_map(move |ind| { + if ind == last_ind { + return None; + } + let slice = &input[(last_ind + 1)..ind]; + last_ind = ind; + Some(slice) + })); + let tokens = slices.map(all_consuming(inner_lex)); + Ok(("", tokens)) +} + +fn try_fold_many1( + mut parser: impl Parser, + mut init: impl FnMut() -> O, + mut combiner: impl FnMut(O, Inner) -> Result>, +) -> impl Parser { + move |mut input: I| { + let mut output = init(); + loop { + // first, try parse the main token + // any error should break the loop + // failure should break with a failure + let res = match parser.parse(input.clone()) { + Ok(res) => res, + Err(nom::Err::Failure(fail)) => return Err(nom::Err::Failure(fail)), + Err(_) => break, + }; + input = res.0; + let next = res.1; + // then, attempt to combine it + output = combiner(output, next)?; + } + Ok((input, output)) + } +} + /// Parses table /// /// - Always starts from a newline followed by '|' char @@ -336,7 +434,8 @@ fn table< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexErrorKind>, + + FromExternalError<&'source str, KyomatoLexError<'source>> + + 'source, >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { @@ -345,23 +444,34 @@ fn table< tuple(( // first, find the start _multispace_line_break("|"), - terminated(separated_list1(char('|'), inner_lex), char('|')), + take_until1("\n").and_then(|input| { + let (_, headers) = _parse_table_row(input)?; + let headers: Vec<_> = headers.map_ok(|(_, t)| t).try_collect()?; + Ok(("", headers)) + }), // at this point this is guaranteed to be a table token, so we lock in cut(tuple(( // first, match until the end of this line pair(take_until("\n"), char('\n')), // then, match format line (it's discarded right now) - pair(take_until("\n"), char('\n')), + take_until("\n"), // from here, match table cells // while this is possible to do by just per-line collection of them into a vectors // (and merge them afterwards), // I can't stand unnecessary heap allocations, so here is my solution: - separated_list1( - preceded(preceded(space0, char('|')), opt(pair(space0, tag("\n|")))), - inner_lex, + try_fold_many1( + preceded(tag("\n|"), take_until("\n").and_then(_parse_table_row)), + Vec::>::new, + |mut acc, next| { + acc.try_extend(next.map_ok(|(_, t)| t))?; + Ok(acc) + }, ), // finally, here come the arguments - parse_args!["ref" = alphanumeric1, "caption" = caption_kernel], + parse_args![ + "ref" = take_till1(|c: char| c.is_whitespace() || c == ','), + "caption" = caption_kernel + ], ))), )), )(input)?; @@ -385,7 +495,8 @@ fn figure< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexErrorKind>, + + FromExternalError<&'source str, KyomatoLexError<'source>> + + 'source, >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { @@ -424,7 +535,7 @@ fn href< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexErrorKind>, + + FromExternalError<&'source str, KyomatoLexError<'source>>, >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { @@ -437,14 +548,16 @@ fn href< // url should be parsed into proper format map_res( delimited(char('('), take_until(")"), char(')')), - |url: &str| Url::from_str(url).map_err(KyomatoLexErrorKind::BadUrl), + |url: &str| { + Url::from_str(url).map_err(|err| KyomatoLexError::bad_url(input, err)) + }, ), ), ), ) .map(|(display, url)| Token::Href { url, - display: display.into(), + display: display.trim().into(), }) .parse(input) } @@ -505,7 +618,8 @@ fn ayano< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexErrorKind>, + + FromExternalError<&'source str, KyomatoLexError<'source>> + + 'source, >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { @@ -537,7 +651,8 @@ fn ayano< pair(char('#'), space0), terminated( map_res(digit1, |n| { - u16::from_str(n).map_err(KyomatoLexErrorKind::BadIdent) + u16::from_str(n) + .map_err(|err| KyomatoLexError::bad_indent(input, err)) }), space0, ), @@ -645,7 +760,8 @@ fn _list_inner< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexErrorKind>, + + FromExternalError<&'source str, KyomatoLexError<'source>> + + 'source, >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { @@ -666,7 +782,7 @@ fn _list_inner< return Err(nom::Err::Error(E::from_external_error( new_input, ErrorKind::Char, - KyomatoLexErrorKind::UnknownListType, + KyomatoLexError::unknown_list_type(input), ))); } }; @@ -678,7 +794,7 @@ fn _list_inner< return Err(nom::Err::Error(E::from_external_error( input, ErrorKind::IsA, - KyomatoLexErrorKind::TooLongList, + KyomatoLexError::too_long_list(input), ))); }; // Match current item start and item itself @@ -708,7 +824,8 @@ fn list< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexErrorKind>, + + FromExternalError<&'source str, KyomatoLexError<'source>> + + 'source, >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { @@ -719,7 +836,8 @@ fn _formatting_inner< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexErrorKind>, + + FromExternalError<&'source str, KyomatoLexError<'source>> + + 'source, >( input: &'source str, style: crate::data::Formatting, @@ -743,7 +861,8 @@ fn formatting< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexErrorKind>, + + FromExternalError<&'source str, KyomatoLexError<'source>> + + 'source, >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { @@ -846,7 +965,8 @@ fn footnote_content< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexErrorKind>, + + FromExternalError<&'source str, KyomatoLexError<'source>> + + 'source, >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { @@ -884,7 +1004,8 @@ fn paragraph< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexErrorKind>, + + FromExternalError<&'source str, KyomatoLexError<'source>> + + 'source, >( outer_input: &'source str, ) -> IResult<&'source str, (Token<'source>, Option>), E> { @@ -905,8 +1026,8 @@ fn paragraph< // - footnote_reference (starting with "[^") // First, let's isolate input to a newline (if there is no newline, stop at last char) - let interrupt_ind = input.find('\n').unwrap_or(input.len() - 1); - let cut_input = &input[..=interrupt_ind]; + let interrupt_ind = input.find('\n').unwrap_or(input.len()); + let cut_input = &input[..interrupt_ind]; // Now, let's try finding interrupting token let interruption = cut_input @@ -957,7 +1078,8 @@ fn paragraph< .as_ref() .map(|(ind, _)| *ind) .unwrap_or(interrupt_ind); - let this_token = Token::Paragraph(Font::Normal, Cow::from(&cut_input[..this_token_end])); + let this_token = + Token::Paragraph(Font::Normal, Cow::from(cut_input[..this_token_end].trim())); let rest_input = interruption .as_ref() .map(|(_, (rest, _))| *rest) @@ -971,7 +1093,61 @@ fn __equalizer(token: Token<'_>) -> (Token<'_>, Option>) { (token, None) } -pub enum KyomatoLexErrorKind { +#[derive(Debug, PartialEq, thiserror::Error)] +pub enum KyomatoLexError<'source> { + #[error("{}", .0)] + Single(&'source str, KyomatoLexErrorStackEntry<'source>), + #[error("STACK: {:?}", .0)] + Multiple(Vec<(&'source str, KyomatoLexErrorStackEntry<'source>)>), + #[error("ALT: {:?}", .0)] + Alt(Vec>), +} + +impl<'source> KyomatoLexError<'source> { + fn no_tokens(pos: &'source str) -> Self { + Self::Single(pos, KyomatoLexErrorStackEntry::NoTokens) + } + + fn forbidden_token(pos: &'source str, token: &'static str, reason: &'static str) -> Self { + Self::Single( + pos, + KyomatoLexErrorStackEntry::ForbiddenToken { token, reason }, + ) + } + + fn bad_indent(pos: &'source str, error: ParseIntError) -> Self { + Self::Single(pos, KyomatoLexErrorStackEntry::BadIdent(error)) + } + + fn too_long_list(pos: &'source str) -> Self { + Self::Single(pos, KyomatoLexErrorStackEntry::TooLongList) + } + + fn unknown_list_type(pos: &'source str) -> Self { + Self::Single(pos, KyomatoLexErrorStackEntry::UnknownListType) + } + + fn bad_url(pos: &'source str, err: url::ParseError) -> Self { + Self::Single(pos, KyomatoLexErrorStackEntry::BadUrl(err)) + } + + fn too_deep_header(pos: &'source str, fold: usize) -> Self { + Self::Single(pos, KyomatoLexErrorStackEntry::TooDeepHeader { fold }) + } + + fn _stack(self) -> Vec<(&'source str, KyomatoLexErrorStackEntry<'source>)> { + match self { + KyomatoLexError::Single(pos, entry) => vec![(pos, entry)], + KyomatoLexError::Multiple(stack) => stack, + err @ KyomatoLexError::Alt(_) => { + vec![("ALT", KyomatoLexErrorStackEntry::Inner(Box::new(err)))] + } + } + } +} + +#[derive(Debug, PartialEq)] +pub enum KyomatoLexErrorStackEntry<'source> { /// Tokens were expected, but none were found. NoTokens, /// Headers only up to 6-fold are supported, but even more folded one was found @@ -983,13 +1159,50 @@ pub enum KyomatoLexErrorKind { token: &'static str, reason: &'static str, }, - BadSyntax { - reason: &'static str, - }, BadUrl(url::ParseError), BadIdent(ParseIntError), UnknownListType, TooLongList, + External(Equivalent>), + Nom(ErrorKind), + Context(&'static str), + Inner(Box>), +} + +impl<'source> ParseError<&'source str> for KyomatoLexError<'source> { + fn from_error_kind(input: &'source str, kind: ErrorKind) -> Self { + Self::Single(input, KyomatoLexErrorStackEntry::Nom(kind)) + } + + fn append(input: &'source str, kind: ErrorKind, other: Self) -> Self { + let mut stack = other._stack(); + stack.push((input, KyomatoLexErrorStackEntry::Nom(kind))); + Self::Multiple(stack) + } + + fn or(self, other: Self) -> Self { + Self::Alt(vec![self, other]) + } +} + +impl<'source> ContextError<&'source str> for KyomatoLexError<'source> { + fn add_context(input: &'source str, ctx: &'static str, other: Self) -> Self { + let mut stack = other._stack(); + stack.push((input, KyomatoLexErrorStackEntry::Context(ctx))); + Self::Multiple(stack) + } +} + +impl<'source, E: Error + 'source> FromExternalError<&'source str, E> for KyomatoLexError<'source> { + fn from_external_error(input: &'source str, kind: ErrorKind, e: E) -> Self { + Self::Multiple(vec![ + (input, KyomatoLexErrorStackEntry::Nom(kind)), + ( + "EXTERNAL", + KyomatoLexErrorStackEntry::External(Equivalent(Box::new(e) as _)), + ), + ]) + } } // TODO wrap outer-layer combinator into `complete` @@ -1006,7 +1219,7 @@ fn tokens_many1< 'source: 'kernel, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexErrorKind> + + FromExternalError<&'source str, KyomatoLexError<'source>> + 'kernel, >( mut tokens_kernel: impl FnMut(&'source str) -> IResult<&'source str, (Token<'source>, Option>), E> @@ -1017,77 +1230,80 @@ fn tokens_many1< Single(Token<'source>), Multiple(Vec>), } - map_res( - all_consuming(move |input| { - // This iterator it kinda tricky to operate. - // Basically, as far as I can see - it won't return error at each step. - // Instead, error will be silently stored inside it, until it's `finish` function is called to extract it. - // Basically, this means that each return from this function MUST INVOLVE `tokens.finish()?`!!! - let mut iterator = iterator(input, &mut tokens_kernel); + move |outer_input| { + map_res( + all_consuming(|input| { + // This iterator it kinda tricky to operate. + // Basically, as far as I can see - it won't return error at each step. + // Instead, error will be silently stored inside it, until it's `finish` function is called to extract it. + // Basically, this means that each return from this function MUST INVOLVE `tokens.finish()?`!!! + let mut iterator = iterator(input, &mut tokens_kernel); - // First, let's try getting first token - let Some((first_token, first_follower)) = (&mut iterator).next() else { - // There is no first token! - // Let's give iterator a chance to explain itself: + // First, let's try getting first token + let Some((first_token, first_follower)) = (&mut iterator).next() else { + // There is no first token! + // Let's give iterator a chance to explain itself: + let (rest, _) = iterator.finish()?; + // Well, seems like there are no tokens and no error + // (most likely input is just empty or th) + // Return empty variant + return Ok((rest, InnerResult::None)); + }; + + // Here's a little tricky part: we'll need to check is there are multiple tokens in the input, + // but there are two variants for that: follower of the first token (we have now), + // or just the second token iterator would yield. + // That said, I find it natural to make this check in one go with `match`. + // The following block will continue execution if and only there are multiple tokens, + // and there are (possibly) more tokens to parse + let t1 = first_token; + let mut tokens = match (first_follower, (&mut iterator).next()) { + // Meaning there are no more tokens (or iterator failed) - return single result + (None, None) => { + let (rest, _) = iterator.finish()?; + return Ok((rest, InnerResult::Single(t1))); + } + // There are exactly two tokens (or iterator failed) - return exactly two tokens + (Some(t2), None) => { + let (rest, _) = iterator.finish()?; + return Ok((rest, InnerResult::Multiple(vec![t1, t2]))); + } + // There are at least two tokens + (None, Some((t2, None))) => { + vec![t1, t2] + } + // There are at least three tokens + (None, Some((t2, Some(t3)))) | (Some(t2), Some((t3, None))) => { + vec![t1, t2, t3] + } + // There are at least four tokens + (Some(t2), Some((t3, Some(t4)))) => { + vec![t1, t2, t3, t4] + } + }; + + // Parse rest of the tokens + for (token, follower) in &mut iterator { + tokens.push(token); + if let Some(follower) = follower { + tokens.push(follower); + } + } + // Iterator might have failed silently, so let them a chance to tell about it let (rest, _) = iterator.finish()?; - // Well, seems like there are no tokens and no error - // (most likely input is just empty or th) - // Return empty variant - return Ok((rest, InnerResult::None)); - }; - - // Here's a little tricky part: we'll need to check is there are multiple tokens in the input, - // but there are two variants for that: follower of the first token (we have now), - // or just the second token iterator would yield. - // That said, I find it natural to make this check in one go with `match`. - // The following block will continue execution if and only there are multiple tokens, - // and there are (possibly) more tokens to parse - let t1 = first_token; - let mut tokens = match (first_follower, (&mut iterator).next()) { - // Meaning there are no more tokens (or iterator failed) - return single result - (None, None) => { - let (rest, _) = iterator.finish()?; - return Ok((rest, InnerResult::Single(t1))); - } - // There are exactly two tokens (or iterator failed) - return exactly two tokens - (Some(t2), None) => { - let (rest, _) = iterator.finish()?; - return Ok((rest, InnerResult::Multiple(vec![t1, t2]))); - } - // There are at least two tokens - (None, Some((t2, None))) => { - vec![t1, t2] - } - // There are at least three tokens - (None, Some((t2, Some(t3)))) | (Some(t2), Some((t3, None))) => { - vec![t1, t2, t3] - } - // There are at least four tokens - (Some(t2), Some((t3, Some(t4)))) => { - vec![t1, t2, t3, t4] - } - }; - - // Parse rest of the tokens - for (token, follower) in &mut iterator { - tokens.push(token); - if let Some(follower) = follower { - tokens.push(follower); - } - } - // Iterator might have failed silently, so let them a chance to tell about it - let (rest, _) = iterator.finish()?; - // Return multiple variant - Ok((rest, InnerResult::Multiple(tokens))) - }), - // Lastly, map this inner result into a single actual token. - // Oh, and if there were no tokens - that's an error, so might return it as well. - |inner_result| match inner_result { - InnerResult::None => Err(KyomatoLexErrorKind::NoTokens), - InnerResult::Single(token) => Ok(token), - InnerResult::Multiple(tokens) => Ok(Token::Text(Tokens::new(tokens))), - }, - ) + // Return multiple variant + Ok((rest, InnerResult::Multiple(tokens))) + }), + // Lastly, map this inner result into a single actual token. + // Oh, and if there were no tokens - that's an error, so might return it as well. + |inner_result| match inner_result { + InnerResult::None => Err(KyomatoLexError::no_tokens(outer_input)), + InnerResult::Single(token) => Ok(token), + InnerResult::Multiple(tokens) => Ok(Token::Text(Tokens::new(tokens))), + }, + ) + .parse(outer_input) + } } /// Main entry point into the lexer @@ -1098,7 +1314,8 @@ pub fn lex< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexErrorKind>, + + FromExternalError<&'source str, KyomatoLexError<'source>> + + 'source, >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { @@ -1133,7 +1350,7 @@ fn forbid< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexErrorKind>, + + FromExternalError<&'source str, KyomatoLexError<'source>>, >( mut parser: impl Parser<&'source str, Token<'source>, E>, token: &'static str, @@ -1147,7 +1364,7 @@ fn forbid< Err(nom::Err::Failure(E::from_external_error( input, ErrorKind::MapRes, - KyomatoLexErrorKind::ForbiddenToken { token, reason }, + KyomatoLexError::forbidden_token(input, token, reason), ))) } } @@ -1159,12 +1376,13 @@ pub fn inner_lex< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexErrorKind>, + + FromExternalError<&'source str, KyomatoLexError<'source>> + + 'source, >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { context( - "static inner lex", + "inner lex", tokens_many1({ alt(( // Some tokens must be forbidden @@ -1236,11 +1454,14 @@ mod tests { // arrange // act - let result: Result<_, nom::Err>>; + let result: Result<_, nom::Err>; result = $parser.parse($input); // assert - assert_eq!(result, $expected, "Parser did not produce expected result"); + let expected = $expected; + if result != expected { + panic!("Parser did not produce expected result. Structure comparison:\n\tExpected:{:#?}\n\n\tActual:{:#?}\n\nPlain comparison:\nE:{:?}\nR:{:?}", expected, result, expected, result); + } } }; {$name:ident, $parser:expr, $input:literal, p: $expected:pat} => { @@ -1285,8 +1506,6 @@ mod tests { test! {div_page4, div_page, "\n ------", p: Err(_)} test! {div_page5, div_page, " \t\n------\t\n", e: Ok(("\t\n", Token::PageDiv))} - // TODO add caption kernel tests - // `header` tests test! {header1, header, "", p: Err(_)} test! {header2, header, "\n#Ababa\n", e: Ok(("\n", Token::Header { order: 1, content: "Ababa".into() }))} @@ -1358,10 +1577,6 @@ $$ {} якийсь текст тому що чому ні"#, e: Ok((" якийсь текст тому що чому ні", Token::Equation { content: "y = x^2 ".into(), ident: None }))} // You can explicitly provide no args - fun fact :idk: - // TODO add table tests - - // TODO add figure tests - // `href` tests mod href { use super::*; @@ -1380,6 +1595,8 @@ $$ // text after url is alright href_ok! {href7, " [link text](ftp://somewebsite/files/01234), ще текст після нього", "ftp://somewebsite/files/01234", "link text", ", ще текст після нього"} // space before url is ignored + href_ok! {href8, "[ link text](ftp://somewebsite/files/01234).", "ftp://somewebsite/files/01234", "link text", "."} + // display text will be trimmed } // `code_block` tests @@ -1409,20 +1626,6 @@ $$ // text just after the code block is ok too } - // TODO add Ayano block tests - - // TODO add list tests - - // `formatting` tests - mod formatting { - macro_rules! test_ok { - {$name:ident, $input:literal} => { - - }; - } - // TODO add formatting tests - } - // `inline math` tests mod inline_math { use super::*; @@ -1484,8 +1687,6 @@ $$ test_ok! {ok_text_after, " \n \t [^y_u_do_dis_2_me] кирилиця or th", "y_u_do_dis_2_me", " кирилиця or th"} } - // TODO add footnote content tests - // tests for `paragraph` mod paragraph { use super::*; @@ -1501,6 +1702,201 @@ $$ test_ok! {ok_char, " \t\n c\nfff", "c", None, "\nfff"} // single char is ok as a paragraph test_ok! {ok_sentence, "\t \n I'm absolutely increative, so here's a bunch of garbage: ]42[t5(2[24tj4-0)jt3_94j03\nand some rest", "I'm absolutely increative, so here's a bunch of garbage: ]42[t5(2[24tj4-0)jt3_94j03", None, "\nand some rest"} - test_ok! {ok_formatting, "\t \n You can use *bold*!", "You can use ", Some(Token::Formatted(Formatting::Italic, Box::new(Token::text("bold")))), "!"} + test_ok! {ok_formatting, "\t \n You can use *bold*!", "You can use", Some(Token::Formatted(Formatting::Italic, Box::new(Token::text("bold")))), "!"} + test_ok! {ok_formatting_in_formatting, "\t \n You can even use ~~__double-formatted__~~ things!", "You can even use", + Some(Token::Formatted(Formatting::StrikeThrough, Box::new(Token::Formatted(Formatting::Bold, Box::new(Token::text("double-formatted")))))), " things!"} + test_ok! {ok_equation, " \t Inline math is fine too: $y = x_{\text{поч}} + x_0$.", "Inline math is fine too:", + Some(Token::InlineMathmode("y = x_{\text{поч}} + x_0".into())), "."} + test_ok! {ok_href, " \t\t\n You can even [залишити посилання на свою сторінку на Гітхабі ](https://www.github.com/Dzuchun)!", "You can even", + Some(Token::Href { url: "https://www.github.com/Dzuchun".parse().expect("That's a valid url"), display: "залишити посилання на свою сторінку на Гітхабі".into() }), "!"} + test_ok! {ok_footnote_ref, "\t Footnote refs[^1] are also ok too!", "Footnote refs", Some(Token::FootNoteReference("1".into())), " are also ok too!"} + test_ok! {ok_obj_ref, "\n\t Thi[[ngs lik_e tables* ([@tab:results]) can also be referenced", "Thi[[ngs lik_e tables* (", + Some(Token::Reference("tab:results".into())), ") can also be referenced"} } + + // `inner_lex` tests + // this is one of open interfaces, so extensive testing is required here + // to achieve that, here are some macro to simplify token creation: + macro_rules! div { + () => { + Token::PageDiv + }; + } + macro_rules! head { + [# $header:literal] => { + head![1, $header] + }; + [## $header:literal] => { + head![2, $header] + }; + [### $header:literal] => { + head![3, $header] + }; + [#### $header:literal] => { + head![4, $header] + }; + [##### $header:literal] => { + head![5, $header] + }; + [###### $header:literal] => { + head![6, $header] + }; + [$order:literal, $header:literal] => { + Token::Header {order: $order, content: Cow::Borrowed($header)} + }; + } + macro_rules! eq { + ($eq:literal) => { + eq!($eq, None) + }; + ($eq:literal, ref = $ident:literal) => { + eq!($eq, Some(Cow::Borrowed($ident))) + }; + ($eq:literal, $ident:expr) => { + Token::Equation { + content: Cow::Borrowed($eq), + ident: $ident, + } + }; + (!$eq:literal) => { + Token::InlineMathmode(Cow::Borrowed($eq)) + }; + } + macro_rules! hr { + ([$display:literal]($url:literal)) => { + Token::Href { + url: Url::from_str($url).expect("Should be a valid url"), + display: Cow::Borrowed($display), + } + }; + } + macro_rules! code { + ($code:literal, lang = $lang:literal) => { + code!($code, Some(Cow::Borrowed($lang))) + }; + ($code:literal) => { + code!($code, None) + }; + ($code:literal, $lang:expr) => { + Token::CodeBlock { + code: Cow::Borrowed($code), + language: $lang, + } + }; + } + macro_rules! rf { + (@ $ident:literal) => { + Token::Reference(Cow::Borrowed($ident)) + }; + (^ $ident:literal) => { + Token::FootNoteReference(Cow::Borrowed($ident)) + }; + } + macro_rules! tx { + ($text:literal) => { + Token::Paragraph(Font::Normal, Cow::Borrowed($text)) + }; + } + macro_rules! tks { + [$($token:expr), +] => { + Token::Text(Tokens::new([$($token), +])) + }; + } + mod inner_lex { + use super::*; + + macro_rules! test_ok { + ($name:ident, $input:literal, $expected:expr) => { + test! {$name, inner_lex, $input, e: Ok(("", $expected))} + }; + } + + macro_rules! test_err { + ($name:ident, $input:literal) => { + test! {$name, inner_lex, $input, p: Err(_)} + }; + } + + test_err! {err_empty, ""} + test_ok! {ok_char_para, "a", tx!("a")} + test_ok! {ok_formatted, "*виділений текст * і ще щось потім", + tks![Token::Formatted(Formatting::Italic, Box::new(tx!("виділений текст"))), tx!("і ще щось потім")]} + test_ok! {ok_inline_math, "дивіться! ось формула для параболи: $y = x^2$", + tks![tx!("дивіться! ось формула для параболи:"), eq!(! "y = x^2")]} + test_err! {err_display_math, "якась страшна штука:\n$$\nf(x) = \\int\\lms_{0}^{x} \\log(x - 2) dx\n$$\n{ref = scary}\n\tКраще не чіпати її"} + // display math is not allowed in the inner_lex + test_ok! {ok_href, "посилання на мою [Github сторінку](https://www.example.com) - ха-ха, жартую;\tОсь вам рівняння: $x^4 + 5x^2 - 100 = 0$.", + tks![tx!("посилання на мою"), hr!(["Github сторінку"]("https://www.example.com")), tx!("- ха-ха, жартую;\tОсь вам рівняння:"), eq!(!"x^4 + 5x^2 - 100 = 0"), tx!(".")]} + test_ok! {ok_ref, "розрахунки наведено у таблиці ([@tab:calc])[^3].", + tks![tx!("розрахунки наведено у таблиці ("), rf!(@"tab:calc"), tx!(")"), rf!(^"3"), tx!(".")]} + } + + macro_rules! tab { + ($($header:expr), +; $($($cell:expr), +;), +) => { + tab!($($header), +; $($($cell), +;), +; {ref = None::<&'_ str>, caption = None}) + }; + ($($header:expr), +; $($($cell:expr), +;), +; {ref = $ident:expr, caption = $caption:expr}) => { + Token::Table {header: Tokens::new([$($header), +]), cells: Tokens::new([$($($cell), +), +]), ident: $ident.map(Cow::from), caption: $caption.map(Box::new)} + }; + } + mod table { + use super::*; + macro_rules! test_ok { + {$name:ident, $input:literal, $output:expr, $left_over:literal} => { + test!{$name, table, $input, e: Ok(($left_over, $output))} + }; + } + + macro_rules! test_err { + {$name:ident, $input:literal} => { + test!{$name, table, $input, p: Err(_)} + }; + } + + test_err! {err_empty, ""} + test_err! {err_no_rows, "|header|\n"} + test_ok! {ok_one_row, "\n\t\n|header|\n|:---:|\n|cell|\n", tab!(tx!("header"); tx!("cell");), "\n"} + test_ok! {ok_more_rows, " \n|header1 | header_2 |\n|:---:|:---:|\n|cell 11|cell[^12]|\n|$cell_{21}$|cell[@tab:22]|\n", + tab!(tx!("header1"), tx!("header_2"); + tx!("cell 11"), tks![tx!("cell"), rf!(^ "12")];, + eq!(!"cell_{21}"), tks![tx!("cell"), rf!(@"tab:22")];), "\n"} + test_ok! {ok_empty_args, " \n|header1 | header_2 |\n|:---:|:---:|\n|cell 11|cell[^12]|\n|$cell_{21}$|cell[@tab:22]|\n{}", + tab!(tx!("header1"), tx!("header_2"); + tx!("cell 11"), tks![tx!("cell"), rf!(^ "12")];, + eq!(!"cell_{21}"), tks![tx!("cell"), rf!(@"tab:22")];), ""} + test_ok! {ok_ident, " \n|header1 | header_2 |\n|:---:|:---:|\n|cell 11|cell[^12]|\n|$cell_{21}$|cell[@tab:22]|\n{ ref = example}", + tab!(tx!("header1"), tx!("header_2"); + tx!("cell 11"), tks![tx!("cell"), rf!(^ "12")];, + eq!(!"cell_{21}"), tks![tx!("cell"), rf!(@"tab:22")];; + {ref = Some("example"), caption = None}), ""} + test_ok! {ok_caption, " \n|header1 | header_2 |\n|:---:|:---:|\n|cell 11|cell[^12]|\n|$cell_{21}$|cell[@tab:22]|\n{caption = \"This table can be seen as an \\\"example\\\"\"}", + tab!(tx!("header1"), tx!("header_2"); + tx!("cell 11"), tks![tx!("cell"), rf!(^ "12")];, + eq!(!"cell_{21}"), tks![tx!("cell"), rf!(@"tab:22")];; + {ref = None::<&str>, caption = Some(tx!("This table can be seen as an \\\"example\\\""))}), ""} + test_ok! {ok_both, " \n|header1 | header_2 |\n|:---:|:---:|\n|cell 11|cell[^12]|\n|$cell_{21}$|cell[@tab:22]|\n{ref = example_table, caption = \"This table can be seen as an \\\"example\\\"\"}", + tab!(tx!("header1"), tx!("header_2"); + tx!("cell 11"), tks![tx!("cell"), rf!(^ "12")];, + eq!(!"cell_{21}"), tks![tx!("cell"), rf!(@"tab:22")];; + {ref = Some("example_table"), caption = Some(tx!("This table can be seen as an \\\"example\\\""))}), ""} + test_ok! {ok_both_reversed, " \n|header1 | header_2 |\n|:---:|:---:|\n|cell 11|cell[^12]|\n|$cell_{21}$|cell[@tab:22]|\n{caption = \"This table can be seen as an \\\"example\\\"\", ref = example_table }", + tab!(tx!("header1"), tx!("header_2"); + tx!("cell 11"), tks![tx!("cell"), rf!(^ "12")];, + eq!(!"cell_{21}"), tks![tx!("cell"), rf!(@"tab:22")];; + {ref = Some("example_table"), caption = Some(tx!("This table can be seen as an \\\"example\\\""))}), ""} + } + // TODO add table tests + // TODO add figure tests + // TODO add Ayano block tests + // TODO add list tests + // `formatting` tests + mod formatting { + macro_rules! test_ok { + {$name:ident, $input:literal} => { + + }; + } + // TODO add formatting tests + } + // TODO add footnote content tests } diff --git a/src/util.rs b/src/util.rs index 3799d53..c828b4f 100644 --- a/src/util.rs +++ b/src/util.rs @@ -336,3 +336,53 @@ impl> IteratorArrayCollectExt for I { } } } + +pub trait VecTryExtendExt { + fn try_extend(&mut self, iter: impl Iterator>) -> Result<(), E>; +} + +impl VecTryExtendExt for Vec { + fn try_extend(&mut self, iter: impl Iterator>) -> Result<(), E> { + for el in iter { + self.push(el?); + } + Ok(()) + } +} + +macro_rules! fn_ref { + ($input:expr) => {{ + fn focus_pokus_amogus(input: fn(I) -> O) -> fn(I) -> O { + input + } + focus_pokus_amogus(|i| $input(&i)) + }}; +} +macro_rules! fn_mut { + ($input:expr) => {{ + fn focus_pokus_amogus(input: fn(I) -> O) -> fn(I) -> O { + input + } + focus_pokus_amogus(|mut i| $input(&mut i)) + }}; +} + +macro_rules! fn_chain { + ($fn1:expr, $fn2:expr) => {{ + fn focus_pokus_amogus(input: fn(I) -> O) -> fn(I) -> O { + input + } + focus_pokus_amogus(|input| $fn2($fn1(input))) + }}; +} + +#[cfg(test)] +mod fn_ref_tests { + #[test] + fn works() { + // act + let _ref_pointer = fn_ref!(String::len); + let _mut_pointer = fn_mut!(Vec::::pop); + let _chain_pointer = fn_chain!(fn_mut!(Vec::::pop), Option::unwrap); + } +} From 877717a74bcae79b5995cfb5bbc5a766030188f1 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Thu, 14 Mar 2024 23:40:43 +0200 Subject: [PATCH 14/98] nightly: - implemented tests for `figure` and (partially) for `list` --- src/data.rs | 1 + src/lexer.rs | 130 ++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 94 insertions(+), 37 deletions(-) diff --git a/src/data.rs b/src/data.rs index 5accc61..be2444e 100644 --- a/src/data.rs +++ b/src/data.rs @@ -140,6 +140,7 @@ pub enum Token<'source> { list_type: ListType, content: Tokens<'source>, }, + #[deprecated = "`Font`-thing inside paragraph token does similar thing, so it would be wise to merge them"] Formatted(Formatting, Box>), Text(Tokens<'source>), // TODO move font and formatting exclusively to here diff --git a/src/lexer.rs b/src/lexer.rs index 8502a72..1f9dc9e 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -22,7 +22,7 @@ use nom::{ is_space, streaming::line_ending, }, - combinator::{all_consuming, cut, eof, iterator, map_res, not, opt, recognize, success}, + combinator::{all_consuming, cut, eof, iterator, map_res, not, opt, peek, recognize, success}, error::{context, ContextError, ErrorKind, FromExternalError, ParseError}, multi::{fold_many1, many1, many1_count, separated_list1}, number::complete::float, @@ -159,8 +159,8 @@ mod optional_permutation { // Amazing! (not really; I imagine that would be a nightmare to read) // This is a max number of iterations we're gonna need - const Count: usize = $Count; - for _ in 0..Count { + const COUNT: usize = $Count; + for _ in 0..COUNT { if $out_ident0.is_none() { match $type_ident0.parse(input) { Ok((rest, result)) => { @@ -169,10 +169,7 @@ mod optional_permutation { continue; // continues parsing attempts from the beginning }, Err(nom::Err::Failure(err)) => return Err(nom::Err::Failure(err)), - Err(err) => { - let _ = err; - println!("2"); - } + Err(_) => { } } } $( @@ -184,10 +181,7 @@ mod optional_permutation { continue; // continues parsing attempts from the beginning }, Err(nom::Err::Failure(err)) => return Err(nom::Err::Failure(err)), - Err(err) => { - let _ = err; - println!("2"); - } + Err(_) => { } } } ) * @@ -290,6 +284,12 @@ fn div_page<'source, E: ParseError<&'source str> + ContextError<&'source str>>( .parse(input) } +fn ident_kernel<'source, E: ParseError<&'source str>>( + input: &'source str, +) -> IResult<&'source str, &'source str, E> { + take_till1(|c: char| !matches!(c, 'a'..='z' | 'A'..='Z' | '_')).parse(input) +} + /// Parses header /// /// - Always starts from a blank line @@ -468,10 +468,7 @@ fn table< }, ), // finally, here come the arguments - parse_args![ - "ref" = take_till1(|c: char| c.is_whitespace() || c == ','), - "caption" = caption_kernel - ], + parse_args!["ref" = ident_kernel, "caption" = caption_kernel], ))), )), )(input)?; @@ -513,7 +510,7 @@ fn figure< tag("]]"), // and here come the args parse_args![ - "ref" = alphanumeric1, + "ref" = ident_kernel, "caption" = caption_kernel, "width" = float ], @@ -719,7 +716,7 @@ fn _list_item_generator<'scope>( Box::new((1..).into_iter().map(|i| { // First, try get already generated string let read = STRS.read().unwrap(); - if let Some(res) = read.get(i) { + if let Some(res) = read.get(i - 1) { return &**res; } // Hmmm, that didn't worked. @@ -727,12 +724,12 @@ fn _list_item_generator<'scope>( drop(read); let mut write = STRS.write().unwrap(); // Append all necessary strings for the array - while write.len() <= i { + while write.len() < i { let j = write.len() + 1; write.push(format!("{}.", j).leak()); } write - .get(i) + .get(i - 1) .expect("Should be present now, we should've just pushed it") })) } @@ -766,11 +763,9 @@ fn _list_inner< input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { // First, we need to actually detect list type + let (input, _) = _multispace_line_break("").parse(input)?; - let (new_input, first_discriminator) = preceded( - _multispace_line_break(""), - recognize(take_till1(char::is_whitespace)), - )(input)?; + let (_, first_discriminator) = recognize(take_till1(char::is_whitespace))(input)?; let list_type = match first_discriminator { "1." => ListType::Num, "a." => ListType::Latin, @@ -780,7 +775,7 @@ fn _list_inner< _ => { // that's an unknown list type return Err(nom::Err::Error(E::from_external_error( - new_input, + input, ErrorKind::Char, KyomatoLexError::unknown_list_type(input), ))); @@ -799,7 +794,7 @@ fn _list_inner< }; // Match current item start and item itself preceded(pair(tag(this_start), space0), inner_lex)(input) - })(new_input)?; + })(input)?; // Unfortunately, we can't use `map` function here, since it accepts `Fn` (not `FnOnce`), // and I'd like to avoid giving `Copy` to every single thing in existence Ok(( @@ -829,7 +824,7 @@ fn list< >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { - context("list", preceded(_multispace_line_break(""), _list_inner))(input) + context("list", _list_inner)(input) } fn _formatting_inner< @@ -1232,7 +1227,7 @@ fn tokens_many1< } move |outer_input| { map_res( - all_consuming(|input| { + |input| { // This iterator it kinda tricky to operate. // Basically, as far as I can see - it won't return error at each step. // Instead, error will be silently stored inside it, until it's `finish` function is called to extract it. @@ -1293,7 +1288,7 @@ fn tokens_many1< let (rest, _) = iterator.finish()?; // Return multiple variant Ok((rest, InnerResult::Multiple(tokens))) - }), + }, // Lastly, map this inner result into a single actual token. // Oh, and if there were no tokens - that's an error, so might return it as well. |inner_result| match inner_result { @@ -1885,18 +1880,79 @@ $$ eq!(!"cell_{21}"), tks![tx!("cell"), rf!(@"tab:22")];; {ref = Some("example_table"), caption = Some(tx!("This table can be seen as an \\\"example\\\""))}), ""} } - // TODO add table tests - // TODO add figure tests - // TODO add Ayano block tests - // TODO add list tests - // `formatting` tests - mod formatting { - macro_rules! test_ok { - {$name:ident, $input:literal} => { + macro_rules! fig { + ($path:literal) => { + fig!($path, {ref = None::<&'static str>, caption = None}) + }; + ($path:literal, {ref = $ident:expr, caption = $caption:expr}) => { + Token::Figure { + src_name: Cow::Borrowed(Path::new($path)), + ident: $ident.map(Cow::from), + caption: $caption.map(Box::new), + } + }; + } + mod figure { + use super::*; + + macro_rules! test_ok { + {$name:ident, $input:literal, $output:expr, $left_over:literal} => { + test!{$name, figure, $input, e: Ok(($left_over, $output))} }; } - // TODO add formatting tests + + macro_rules! test_err { + {$name:ident, $input:literal} => { + test!{$name, figure, $input, p: Err(_)} + }; + } + + test_err! {err_empty, ""} + test_err! {err_unclosed, "\n![[path/to/file]"} + // test_err! {err_bad_path, "\n![[/path/to////////]]"} // not sure about that? + test_ok! {ok, "\n![[path/to/file.jpg]]", fig!("path/to/file.jpg"), ""} + test_ok! {ok_empty_before, "\t\t\t\n \n\n![[path/to/file.jpg]]", fig!("path/to/file.jpg"), ""} + test_ok! {ok_text_after, "\t\t\t\n \n\n![[path/to/file.jpg]] і ще щось написане", fig!("path/to/file.jpg"), " і ще щось написане"} + test_ok! {ok_explicit_no_params, "\n![[path/to/file.jpg]]\n{}\n", fig!("path/to/file.jpg"), "\n"} + test_ok! {ok_ident, "\n![[path/to/file.jpg]]\n{ref = figure_of_doom }\n", fig!("path/to/file.jpg", {ref = Some("figure_of_doom"), caption = None}), "\n"} + test_ok! {ok_caption, "\n![[path/to/file.jpg]]\n{caption = \"Ця картинка показує графік рівняння $y = x^2$\" }\n", + fig!("path/to/file.jpg", {ref = None::<&str>, caption = Some(tks![tx!("Ця картинка показує графік рівняння"), eq!(!"y = x^2")])}), "\n"} + test_ok! {ok_both, "\n![[path/to/file.jpg]]\n{ ref = mc_erat, caption = \"This is a mee from our $\\text{Nomifactory}^{2*}$ world.\" }\n", + fig!("path/to/file.jpg", {ref = Some("mc_erat"), caption = Some(tks!(tx!("This is a mee from our"), eq!(!"\\text{Nomifactory}^{2*}"), tx!("world.")))}), "\n"} + test_ok! {ok_both_reversed, "\n![[path/to/file.jpg]]\n{ caption = \"This is a meme from our $\\text{Nomifactory}^{2*}$ world.\" , ref = mc_erat }\n", + fig!("path/to/file.jpg", {ref = Some("mc_erat"), caption = Some(tks!(tx!("This is a meme from our"), eq!(!"\\text{Nomifactory}^{2*}"), tx!("world.")))}), "\n"} + test_ok! {ok_both_left_over, "\n![[path/to/file.jpg]]\n{ caption = \"This is a meme from our $\\text{Nomifactory}^{2*}$ world.\" , ref = mc_erat }\nІще трохи тексту", + fig!("path/to/file.jpg", {ref = Some("mc_erat"), caption = Some(tks!(tx!("This is a meme from our"), eq!(!"\\text{Nomifactory}^{2*}"), tx!("world.")))}), "\nІще трохи тексту"} + } + + macro_rules! ls { + [1 $($item:expr), +] => { + ls![ListType::Num; $($item), +] + }; + [$list_type:expr; $($item:expr), +] => { + Token::List {list_type: $list_type, content: Tokens::new([$($item), +])} + }; + } + mod list { + use super::*; + macro_rules! test_ok { + {$name:ident, $input:literal, $output:expr, $left_over:expr} => { + test!{$name, list, $input, e: Ok(($left_over, $output))} + }; + } + + macro_rules! test_err { + {$name:ident, $input:literal} => { + test!{$name, list, $input, p: Err(_)} + }; + } + + test_err! {err_empty, ""} + test_ok! {ok_single, "\n1. Item 1\n", ls!(1 tx!("Item 1")), "\n"} } // TODO add footnote content tests + // TODO add Ayano block tests + // FIXME EXPL: `formatting` tests are not to be done, since this token type will be removed soon anyways + // TODO add `lex` tests (basically, whole parser tests) } From bb12c14a902fd47d248863d99e2af9a54d272c61 Mon Sep 17 00:00:00 2001 From: Artem Diachenko <46713596+Dzuchun@users.noreply.github.com> Date: Thu, 14 Mar 2024 23:45:34 +0200 Subject: [PATCH 15/98] Added Rust testing --- .github/workflows/rust.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/rust.yml diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..fd961f8 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,22 @@ +name: Rust Test + +on: + push: + branches: [ ] + pull_request: + branches: [ "indev" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose From 21e24db6deb39ab5da2a7c9a6b5aa70485e15fb4 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Sun, 17 Mar 2024 12:17:29 +0200 Subject: [PATCH 16/98] First kinda-working version - added rest of the parsing tests - reworked generation a bit - added "integration tests" to uphold expected parsing and generation --- Cargo.lock | 74 ++- Cargo.toml | 6 + src/data.rs | 36 +- src/gen/ayano.rs | 21 +- src/gen/lab/meta.rs | 3 +- src/gen/lab/mod.rs | 105 ++-- src/gen/mod.rs | 9 +- src/lexer.rs | 448 +++++++++++----- src/lib.rs | 33 ++ src/path_engine.rs | 12 +- src/util.rs | 111 +++- tests/input_files/basic.md | 23 + tests/input_files/deceit.md | 204 ++++++++ tests/input_files/tables_code.md | 36 ++ tests/lex.rs | 147 ++++++ tests/lex_files/basic.txt | 109 ++++ tests/lex_files/deceit.txt | 650 ++++++++++++++++++++++++ tests/lex_files/tables_code.txt | 279 ++++++++++ tests/transform.rs | 139 +++++ tests/transformed_files/basic.txt | 20 + tests/transformed_files/deceit.txt | 89 ++++ tests/transformed_files/tables_code.txt | 38 ++ 22 files changed, 2382 insertions(+), 210 deletions(-) create mode 100644 tests/input_files/basic.md create mode 100644 tests/input_files/deceit.md create mode 100644 tests/input_files/tables_code.md create mode 100644 tests/lex.rs create mode 100644 tests/lex_files/basic.txt create mode 100644 tests/lex_files/deceit.txt create mode 100644 tests/lex_files/tables_code.txt create mode 100644 tests/transform.rs create mode 100644 tests/transformed_files/basic.txt create mode 100644 tests/transformed_files/deceit.txt create mode 100644 tests/transformed_files/tables_code.txt diff --git a/Cargo.lock b/Cargo.lock index cc3b565..14fc87b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -152,7 +152,7 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ - "percent-encoding", + "percent-encoding 2.3.1", ] [[package]] @@ -197,6 +197,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idna" version = "0.5.0" @@ -253,9 +264,11 @@ dependencies = [ "pyo3", "rand", "regex", + "serde", "text-diff", "thiserror", - "url", + "url 2.5.0", + "url_serde", ] [[package]] @@ -274,6 +287,12 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + [[package]] name = "memchr" version = "2.7.1" @@ -334,6 +353,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "percent-encoding" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -514,6 +539,26 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" +[[package]] +name = "serde" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "smallvec" version = "1.13.1" @@ -642,6 +687,17 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" +[[package]] +name = "url" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" +dependencies = [ + "idna 0.1.5", + "matches", + "percent-encoding 1.0.1", +] + [[package]] name = "url" version = "2.5.0" @@ -649,8 +705,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", - "idna", - "percent-encoding", + "idna 0.5.0", + "percent-encoding 2.3.1", +] + +[[package]] +name = "url_serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74e7d099f1ee52f823d4bdd60c93c3602043c728f5db3b97bdb548467f7bddea" +dependencies = [ + "serde", + "url 1.7.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9b23eba..aae498e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,10 @@ name = "kyomato" version = "0.1.0" edition = "2021" +[features] +default = ["serde"] # TODO REMOVE AFTER TOKEN REWORK +serde = ["dep:serde", "dep:url_serde"] + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] @@ -16,6 +20,8 @@ rand = "0.8.5" regex = "1.10.3" thiserror = "1.0.56" url = "2.5.0" +serde = { version = "1.0.197", features = ["derive"], optional = true } +url_serde = { version = "0.2.0", optional = true } [dev-dependencies] text-diff = "0.4.0" diff --git a/src/data.rs b/src/data.rs index be2444e..9245b97 100644 --- a/src/data.rs +++ b/src/data.rs @@ -15,6 +15,7 @@ use url::Url; // In fact, for any possible appliance they can be assumed to life for as long as source does. // Hope is for lifetime variance to help with lifetime casting, or th #[derive(Debug, PartialEq)] +// #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Tokens<'source> { Owned(Box<[Token<'source>]>), Borrowed(&'source [Token<'source>]), @@ -62,6 +63,7 @@ impl<'source> Tokens<'source> { } #[derive(Debug, Clone, Hash, PartialEq)] +// #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct AyanoBlock<'code> { pub is_display: bool, pub is_static: bool, @@ -84,6 +86,7 @@ impl PartialEq for AyanoBlock<'_> { */ #[derive(Debug, Clone, PartialEq)] +// #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum ListType { Bullet, Num, @@ -93,6 +96,7 @@ pub enum ListType { } #[derive(Debug, Clone, PartialEq)] +// #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Formatting { Bold, Italic, @@ -100,6 +104,7 @@ pub enum Formatting { } #[derive(Debug, Clone, PartialEq)] +// #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Font { Normal, Caption, @@ -109,6 +114,7 @@ type Tx<'source> = Cow<'source, str>; type Pth<'source> = Cow<'source, Path>; #[derive(Debug, PartialEq)] +// #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Token<'source> { PageDiv, Header { @@ -132,6 +138,7 @@ pub enum Token<'source> { ident: Option>, }, Href { + // #[cfg_attr(feature = "serde", serde(with = "url_serde"))] url: Url, display: Tx<'source>, }, @@ -199,25 +206,34 @@ impl<'source> Token<'source> { caption: caption.as_ref().map(|t| Box::new(Token::borrow_ref(t))), ident: ident.as_ref().map(|s| Cow::<'r, str>::Borrowed(s)), }, - Token::Href { url, display } => { - todo!() + Token::Href { url, display } => Token::Href { + url: url.clone(), + display: Cow::Borrowed(&display), + }, + Token::Ayano(ayano) => Token::Ayano(ayano.clone()), + Token::List { list_type, content } => Token::List { + list_type: list_type.clone(), + content: Tokens::Borrowed(&content), + }, + Token::Formatted(formatting, content) => { + Token::Formatted(formatting.clone(), Box::new(content.borrow_ref())) } - Token::Ayano(_) => todo!(), - Token::List { list_type, content } => todo!(), - Token::Formatted(_, _) => todo!(), - Token::Text(_) => todo!(), + Token::Text(text) => Token::Text(Tokens::Borrowed(text)), Token::Paragraph(font, content) => { Token::Paragraph(font.clone(), Cow::Borrowed(content)) } - Token::InlineMathmode(_) => todo!(), - Token::Reference(_) => todo!(), + Token::InlineMathmode(content) => Token::InlineMathmode(Cow::Borrowed(&content)), + Token::Reference(ident) => Token::Reference(Cow::Borrowed(&ident)), Token::FootNoteReference(ident) => Token::FootNoteReference(Cow::Borrowed(ident)), Token::FootNoteContent { content, ident } => Token::FootNoteContent { content: Box::new(content.borrow_ref()), ident: Cow::Borrowed(ident), }, - Token::CodeBlock { code, language } => todo!(), - Token::Error(_) => todo!(), + Token::CodeBlock { code, language } => Token::CodeBlock { + code: Cow::Borrowed(&code), + language: language.as_ref().map(|c| Cow::Borrowed(c.borrow())), + }, + Token::Error(error) => Token::Error(error.clone()), } } } diff --git a/src/gen/ayano.rs b/src/gen/ayano.rs index 24272e0..03e09f6 100644 --- a/src/gen/ayano.rs +++ b/src/gen/ayano.rs @@ -114,11 +114,15 @@ pub struct AyanoExecutor { blocks: Immutable>, } -#[derive(Debug, derive_more::From, PartialEq)] +#[derive(Debug, derive_more::From, PartialEq, thiserror::Error)] pub enum AyanoError<'b> { + #[error("Python error occured: {:?}", .0)] Python(Equivalent), + #[error("There's no function for ayano block: {:?}", .0)] NoFunction(AyanoBlock<'b>), + #[error("{}", .0)] Applying(SyntaxError), + #[error("{}", .0)] Parsing(ParsingError), } @@ -625,13 +629,21 @@ enum Syntax<'source> { }, } -#[derive(Debug, derive_more::From, PartialEq)] +#[derive(Debug, derive_more::From, PartialEq, thiserror::Error)] pub enum SyntaxError { + #[error("Required argument is missing: {}", .0)] MissingArgument(&'static str), + #[error("Unknown argument is present: {}", .0)] UnknownArgument(String), + /// Right now this only means that you don't have a generator in a table syntax + #[error("Ayano syntax is fundamentally wrong")] + // TODO probably should rename and write a better message BadSyntax, + #[error("{}", .0)] RangeParse(GenericRangeParseError), + #[error("{}", .0)] IntParse(ParseIntError), + #[error("{}", **.0)] InsertIo(Equivalent), } @@ -1363,10 +1375,13 @@ r#"___generator___ = lambda row, col: -1 return "tab", [[___generator___(0, 0), ___generator___(0, 1), ], [___generator___(1, 0), ___generator___(1, 1), ], [___generator___(2, 0), ___generator___(2, 1), ], [___generator___(3, 0), ___generator___(3, 1), ], [___generator___(4, 0), ___generator___(4, 1), ], ], None, None"#} } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, thiserror::Error)] pub enum ParsingError { + #[error("{}", .0)] Float(ParseFloatError), + #[error("Python could not generate output. str: {}, error: {:?}", .0, .1)] UnexpectedSyntax(String, Equivalent), + #[error("Table syntax error: no header")] TableNoHeader, } diff --git a/src/gen/lab/meta.rs b/src/gen/lab/meta.rs index 3458f05..6c5156c 100644 --- a/src/gen/lab/meta.rs +++ b/src/gen/lab/meta.rs @@ -10,8 +10,9 @@ use crate::data::{Token, Tokens}; use super::super::ayano::{self, AyanoBuilder, AyanoExecutor}; -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub enum MetaError { + #[error("Failed to recognize Ayano syntax: {}", .0)] AyanoSyntax(ayano::SyntaxError), } diff --git a/src/gen/lab/mod.rs b/src/gen/lab/mod.rs index 367e41c..daebb70 100644 --- a/src/gen/lab/mod.rs +++ b/src/gen/lab/mod.rs @@ -1,13 +1,14 @@ mod context; mod meta; +pub use context::Context; +pub use meta::SourceMeta; + use itertools::Itertools; use crate::{data::Token, path_engine::PathEngine}; use std::{borrow::Borrow, fmt::Write, marker::PhantomData}; -use self::{context::Context, meta::SourceMeta}; - use super::{ayano::AyanoExecutor, GenerationError, OutputGenerator, Res}; #[derive(Debug)] @@ -17,7 +18,7 @@ pub struct LabaLatex<'source, PE> { } impl<'source, PE: PathEngine> LabaLatex<'source, PE> { - fn new(path_engine: PE) -> Self { + pub fn new(path_engine: PE) -> Self { Self { path_engine, _phantom: PhantomData, @@ -44,7 +45,7 @@ impl<'source, PE: PathEngine> LabaLatex<'source, PE> { output.write_str(" & ")?; self.write_to(output, meta, context, cell)?; } - output.write_str("\\\\ \\hline")?; + output.write_str("\\\\ \\hline\n")?; Ok(()) } } @@ -65,7 +66,7 @@ impl<'source, PE: PathEngine> OutputGenerator<'source, SourceMeta<'source, Ayano { match token { Token::PageDiv => { - output.write_str(r"\clearpage")?; + output.write_str("\n\\clearpage\n")?; } Token::Header { order, content } => { let command = match order { @@ -77,21 +78,16 @@ impl<'source, PE: PathEngine> OutputGenerator<'source, SourceMeta<'source, Ayano _ => "textbf", }; - write!(output, "\\{command}{{{content}}}")?; + write!(output, "\n\\{command}{{{content}}}\n")?; } Token::Equation { content, ident } => { let label = ident .as_ref() - .map(|ident| format!(r"\label{{eq:{}}}", ident)) + .map(|ident| format!("\\label{{eq:{}}}", ident)) .unwrap_or_else(String::new); write!( output, - r" - \begin{{equation}} - {content} - {label} - \end{{equation}} - " + "\n\\begin{{equation}}\n{content}\n{label}\\end{{equation}}\n" )?; } Token::Table { @@ -103,7 +99,7 @@ impl<'source, PE: PathEngine> OutputGenerator<'source, SourceMeta<'source, Ayano let columns = header.len(); let column_format = itertools::intersperse(std::iter::repeat("c").take(columns), "|"); - write!(output, "\\begin{{table}}[h!]\n\\begin{{center}}\n")?; + write!(output, "\n\\begin{{table}}[h!]\n\\begin{{center}}\n")?; output.write_str("\\begin{tabular}{|")?; column_format .map(|s| { @@ -114,7 +110,7 @@ impl<'source, PE: PathEngine> OutputGenerator<'source, SourceMeta<'source, Ayano output.write_str("|}\n")?; // write header line self.write_table_row(output, meta, context, header)?; - output.write_str("\\hline")?; + output.write_str("\\hline\n")?; 'cells: { let mut rows = cells.chunks(columns).into_iter(); let Some(first_row) = rows.next() else { @@ -125,11 +121,7 @@ impl<'source, PE: PathEngine> OutputGenerator<'source, SourceMeta<'source, Ayano self.write_table_row(output, meta, context, row)?; } } - output.write_str( - r" - \end{tabular} - \stepcounter{tabnum}", - )?; + output.write_str("\\end{tabular}\n\\stepcounter{tabnum}")?; if let Some(caption) = caption { output.write_str("\n\\caption{")?; self.write_to(output, meta, context, caption)?; @@ -138,7 +130,7 @@ impl<'source, PE: PathEngine> OutputGenerator<'source, SourceMeta<'source, Ayano if let Some(ident) = ident.as_ref() { write!(output, "\\label{{tab:{ident}}}\n")?; } - output.write_str("\\end{center}\n\\end{table}")?; + output.write_str("\\end{center}\n\\end{table}\n")?; } Token::Figure { src_name, @@ -146,13 +138,11 @@ impl<'source, PE: PathEngine> OutputGenerator<'source, SourceMeta<'source, Ayano ident, } => { output.write_str( - r"\begin{figure}[h!] - \centering - \includegraphics[width=0.9\textwidth]{", + "\\begin{figure}[h!]\n\\centering\n\\includegraphics[width=0.9\\textwidth]{", )?; let src_path = self.path_engine.image(&src_name, None)?; - output.write_str(src_path.to_string_lossy().borrow())?; // TODO note here: non-UTF paths **should** be handled just fine. - output.write_str("}")?; + output.write_str(src_path.to_string_lossy().borrow())?; // FIXME non-utf paths *might* cause an issue here + output.write_str("}\n")?; if let Some(caption) = caption.as_ref() { output.write_str("\\caption{")?; self.write_to(output, meta, context, caption)?; @@ -161,8 +151,7 @@ impl<'source, PE: PathEngine> OutputGenerator<'source, SourceMeta<'source, Ayano if let Some(ident) = ident.as_ref() { write!(output, "\\label{{fig:{ident}}}\n")?; } - output.write_str("\\end{figure}")?; - // TODO add attachments dir customization + output.write_str("\\end{figure}\n")?; } Token::Ayano(block) => { let token = meta.ayano.display_token(block)?; @@ -170,51 +159,60 @@ impl<'source, PE: PathEngine> OutputGenerator<'source, SourceMeta<'source, Ayano } Token::List { list_type, content } => { let start: &str = match list_type { - crate::data::ListType::Bullet => r"\begin{itemize}", - crate::data::ListType::Num => r"\begin{enumerate}", - crate::data::ListType::Latin => todo!(), + crate::data::ListType::Bullet => "\\begin{itemize}", + crate::data::ListType::Num => "\\begin{enumerate}", + crate::data::ListType::Latin => { + unimplemented!("Latin lists are not supported for now") + } crate::data::ListType::Cyrillic => { - r"\begin{enumerate}[label=\asbuk*), ref=\asbuk*]" + "\\begin{enumerate}[label=\\asbuk*), ref=\\asbuk*]" + } + crate::data::ListType::Roman => { + unimplemented!("Roman numeral lists are not supported for now") } - crate::data::ListType::Roman => todo!(), }; let end: &str = match list_type { - crate::data::ListType::Bullet => r"\end{itemize}", + crate::data::ListType::Bullet => "\\end{itemize}", crate::data::ListType::Num | crate::data::ListType::Cyrillic => { - r"\end{enumerate}" + "\\end{enumerate}" + } + crate::data::ListType::Latin => { + unimplemented!("Latin lists are not supported for now") + } + crate::data::ListType::Roman => { + unimplemented!("Roman numeral lists are not supported for now") } - crate::data::ListType::Latin => todo!(), - crate::data::ListType::Roman => todo!(), }; + output.write_str("\n")?; output.write_str(start)?; for item in content { - output.write_str("\\item ")?; + output.write_str("\n\\item ")?; self.write_to(output, meta, context, item)?; - output.write_str("\n")?; } + output.write_str("\n")?; output.write_str(end)?; } Token::Formatted(formatting, content) => { let start: &str = match formatting { - crate::data::Formatting::Bold => r"\textbf{ ", - crate::data::Formatting::Italic => r"\textit{ ", + crate::data::Formatting::Bold => "\\textbf{", + crate::data::Formatting::Italic => "\\textit{", // TODO ADD \usepackage{soul} TO PREAMBLE! - crate::data::Formatting::StrikeThrough => r"\st{ ", + crate::data::Formatting::StrikeThrough => "\\st{", }; let end: &str = match formatting { crate::data::Formatting::Bold | crate::data::Formatting::Italic - | crate::data::Formatting::StrikeThrough => " }", + | crate::data::Formatting::StrikeThrough => "}", }; output.write_str(start)?; self.write_to(output, meta, context, content)?; output.write_str(end)?; } Token::InlineMathmode(content) => { - write!(output, r"$ {content} $")?; + write!(output, "${content}$")?; } Token::Reference(ident) => { - write!(output, r"\ref{{{ident}}}")?; + write!(output, "\\ref{{{ident}}}")?; } Token::FootNoteReference(ident) => { let (encountered, i) = { @@ -232,12 +230,12 @@ impl<'source, PE: PathEngine> OutputGenerator<'source, SourceMeta<'source, Ayano }; (encountered.is_some(), i) }; - // write out mark - write!(output, r"\footnotemark[{}]", i + 1)?; + // write out mark (always done) + write!(output, "\\footnotemark[{}]", i + 1)?; if !encountered { - // write out content + // write out content (done only on first occurrence) if let Some(content) = meta.footnotes.get(&ident.borrow()) { - write!(output, r"\footnotetext[{}]{{", i + 1)?; + write!(output, "\\footnotetext[{}]{{", i + 1)?; self.write_to(output, meta, context, content)?; output.write_str("}")?; } @@ -250,24 +248,25 @@ impl<'source, PE: PathEngine> OutputGenerator<'source, SourceMeta<'source, Ayano // TODO add representation in the document } Token::Paragraph(font, content) => { + // TODO that's to be rewritten! let font = match font { crate::data::Font::Normal => "fnt", crate::data::Font::Caption => "capfnt", }; - write!(output, r"\{font} {content}")?; + write!(output, "\\{font} {content}")?; } Token::Href { url, display } => { - write!(output, r"\href{{{url}}}{{{display}}}")?; + write!(output, "\\href{{{url}}}{{{display}}}")?; } Token::Text(content) => self.write_tokens_to(output, meta, context, content)?, Token::CodeBlock { code, language } => { if let Some(language) = language { writeln!( output, - "\n\\begin{{minted}}{{{language}}}\n{code}\n\\end{{minted}}" + "\n\\begin{{minted}}{{{language}}}\n{code}\n\\end{{minted}}\n" )?; } else { - writeln!(output, "\n\\begin{{minted}}\n{code}\n\\end{{minted}}")?; + writeln!(output, "\n\\begin{{minted}}\n{code}\n\\end{{minted}}\n")?; } } }; diff --git a/src/gen/mod.rs b/src/gen/mod.rs index 384dafd..c589705 100644 --- a/src/gen/mod.rs +++ b/src/gen/mod.rs @@ -1,5 +1,5 @@ mod ayano; -mod lab; +pub mod lab; use std::fmt::Write; @@ -10,14 +10,17 @@ use crate::{ path_engine, }; -#[derive(Debug, derive_more::From)] +#[derive(Debug, derive_more::From, thiserror::Error)] pub enum GenerationError<'source> { + #[error("{}", .0)] IOs(std::fmt::Error), + #[error("{}", .0)] Ayano(ayano::AyanoError<'source>), + #[error("{}", .0)] Path(path_engine::Error), } -type Res<'source> = Result<(), GenerationError<'source>>; +pub type Res<'source> = Result<(), GenerationError<'source>>; // TODO make this thing modular (probably use tower to handle writes or th) pub trait OutputGenerator<'source, Meta, Context> { diff --git a/src/lexer.rs b/src/lexer.rs index 1f9dc9e..05121f5 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -24,7 +24,7 @@ use nom::{ }, combinator::{all_consuming, cut, eof, iterator, map_res, not, opt, peek, recognize, success}, error::{context, ContextError, ErrorKind, FromExternalError, ParseError}, - multi::{fold_many1, many1, many1_count, separated_list1}, + multi::{fold_many1, many0_count, many1, many1_count, separated_list1}, number::complete::float, sequence::{delimited, pair, preceded, terminated, tuple}, IResult, Parser, @@ -244,7 +244,7 @@ fn caption_kernel< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexError<'source>> + + FromExternalError<&'source str, KyomatoLexError> + 'source, >( input: &'source str, @@ -257,7 +257,7 @@ fn caption_kernel< let unescaped_quotes = double_quotes.filter(|(i, _)| !&input[..*i].ends_with('\\')); // lastly, try parsing each of them let mut parsed = unescaped_quotes - .map(|(i, _)| inner_lex::<'source, E>(dbg!(&input[..i])).map(|(_, t)| (&input[i..], t))); + .map(|(i, _)| inner_lex::<'source, E>(&input[..i]).map(|(_, t)| (&input[i..], t))); let (input, token) = parsed .find_map(Result::ok) @@ -287,7 +287,7 @@ fn div_page<'source, E: ParseError<&'source str> + ContextError<&'source str>>( fn ident_kernel<'source, E: ParseError<&'source str>>( input: &'source str, ) -> IResult<&'source str, &'source str, E> { - take_till1(|c: char| !matches!(c, 'a'..='z' | 'A'..='Z' | '_')).parse(input) + take_till1(|c: char| !matches!(c, 'a'..='z' | 'A'..='Z' | '1'..='9' | '_' | ':')).parse(input) } /// Parses header @@ -299,15 +299,17 @@ fn header< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexError<'source>>, + + FromExternalError<&'source str, KyomatoLexError>, >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { context( "header", tuple(( - _multispace_line_break(""), - map_res(many1_count(char('#')), |count| { + _multispace_line_break("#"), + map_res(many0_count(char('#')), |mut count| { + // one of the hashes was parsed already + count += 1; if count <= 6 { Ok(count) } else { @@ -334,7 +336,7 @@ fn equation< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexError<'source>>, + + FromExternalError<&'source str, KyomatoLexError>, >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { @@ -346,7 +348,7 @@ fn equation< cut(tuple(( take_until1("\n$$"), tag("\n$$"), - parse_args!["ref" = alphanumeric1], + parse_args!["ref" = ident_kernel], ))), ), )(input)?; @@ -366,7 +368,7 @@ fn _parse_table_row< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexError<'source>> + + FromExternalError<&'source str, KyomatoLexError> + 'source, >( input: &'source str, @@ -376,8 +378,7 @@ fn _parse_table_row< E, > { let pipes = input.char_indices().filter(|(_, c)| c == &'|'); - let unescaped_pipes = - pipes.filter(|(ind, _)| ind > &0 && !input[(ind - 1)..].starts_with(r"\|")); + let unescaped_pipes = pipes.filter(|(ind, _)| ind > &0 && !input[..*ind].ends_with('\\')); let mut dividers = unescaped_pipes.map(|(ind, _)| ind); let mut last_ind = dividers .next() @@ -394,7 +395,7 @@ fn _parse_table_row< last_ind = ind; Some(slice) })); - let tokens = slices.map(all_consuming(inner_lex)); + let tokens = slices.map(all_consuming(terminated(inner_lex, space0))); Ok(("", tokens)) } @@ -434,7 +435,7 @@ fn table< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexError<'source>> + + FromExternalError<&'source str, KyomatoLexError> + 'source, >( input: &'source str, @@ -451,8 +452,8 @@ fn table< }), // at this point this is guaranteed to be a table token, so we lock in cut(tuple(( - // first, match until the end of this line - pair(take_until("\n"), char('\n')), + // first, match the end of this line + char('\n'), // then, match format line (it's discarded right now) take_until("\n"), // from here, match table cells @@ -492,7 +493,7 @@ fn figure< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexError<'source>> + + FromExternalError<&'source str, KyomatoLexError> + 'source, >( input: &'source str, @@ -532,7 +533,7 @@ fn href< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexError<'source>>, + + FromExternalError<&'source str, KyomatoLexError>, >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { @@ -615,7 +616,7 @@ fn ayano< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexError<'source>> + + FromExternalError<&'source str, KyomatoLexError> + 'source, >( input: &'source str, @@ -757,7 +758,7 @@ fn _list_inner< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexError<'source>> + + FromExternalError<&'source str, KyomatoLexError> + 'source, >( input: &'source str, @@ -793,7 +794,10 @@ fn _list_inner< ))); }; // Match current item start and item itself - preceded(pair(tag(this_start), space0), inner_lex)(input) + preceded( + tuple((opt(char('\n')), tag(this_start), space0)), + take_until("\n").and_then(inner_lex), + )(input) })(input)?; // Unfortunately, we can't use `map` function here, since it accepts `Fn` (not `FnOnce`), // and I'd like to avoid giving `Copy` to every single thing in existence @@ -819,7 +823,7 @@ fn list< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexError<'source>> + + FromExternalError<&'source str, KyomatoLexError> + 'source, >( input: &'source str, @@ -831,7 +835,7 @@ fn _formatting_inner< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexError<'source>> + + FromExternalError<&'source str, KyomatoLexError> + 'source, >( input: &'source str, @@ -856,7 +860,7 @@ fn formatting< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexError<'source>> + + FromExternalError<&'source str, KyomatoLexError> + 'source, >( input: &'source str, @@ -901,7 +905,7 @@ fn inline_math<'source, E: ParseError<&'source str> + ContextError<&'source str> "inline math", preceded( multispace0, - delimited(char('$'), take_until1("$"), char('$')), + delimited(char('$'), take_till1(|c| c == '\n' || c == '$'), char('$')), ), ) .map(|content: &str| Token::InlineMathmode(content.trim().into())) @@ -924,9 +928,9 @@ fn reference<'source, E: ParseError<&'source str> + ContextError<&'source str>>( let (rest, ident) = context( "reference", delimited( - pair(multispace0, tag("[@")), - cut(preceded(space0, take_until1("]"))), - char(']'), + pair(pair(multispace0, tag("[@")), space0), + ident_kernel, + pair(space0, char(']')), ), )(input)?; Ok((rest, Token::Reference(ident.trim().into()))) @@ -943,7 +947,7 @@ fn footnote_reference<'source, E: ParseError<&'source str> + ContextError<&'sour "footnote reference", delimited( pair(multispace0, tag("[^")), - cut(preceded(space0, take_until1("]"))), + delimited(space0, ident_kernel, space0), char(']'), ), )(input)?; @@ -960,7 +964,7 @@ fn footnote_content< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexError<'source>> + + FromExternalError<&'source str, KyomatoLexError> + 'source, >( input: &'source str, @@ -968,9 +972,9 @@ fn footnote_content< context( "footnote context", delimited( - _multispace_line_break("[^"), - cut(take_until1("]")), - tag("]:"), + pair(_multispace_line_break("[^"), space0), + ident_kernel, + pair(space0, tag("]:")), ) .and(preceded( space0, @@ -980,7 +984,7 @@ fn footnote_content< ) .map(|(ident, content)| Token::FootNoteContent { content: Box::new(content), - ident: ident.into(), + ident: ident.trim().into(), }) .parse(input) } @@ -999,7 +1003,7 @@ fn paragraph< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexError<'source>> + + FromExternalError<&'source str, KyomatoLexError> + 'source, >( outer_input: &'source str, @@ -1033,7 +1037,7 @@ fn paragraph< .filter(|(_, c)| ['[', '*', '_', '~', '$'].contains(c)) // Now, try a proper parser for each one .map(|(ind, c)| { - let cut_cut_input = &cut_input[ind..]; + let cut_cut_input = &input[ind..]; let parse_res = match c { '[' => { if cut_input[ind..].starts_with("[@") { @@ -1089,60 +1093,78 @@ fn __equalizer(token: Token<'_>) -> (Token<'_>, Option>) { } #[derive(Debug, PartialEq, thiserror::Error)] -pub enum KyomatoLexError<'source> { +pub enum KyomatoLexError { #[error("{}", .0)] - Single(&'source str, KyomatoLexErrorStackEntry<'source>), + Single(Box, KyomatoLexErrorStackEntry), #[error("STACK: {:?}", .0)] - Multiple(Vec<(&'source str, KyomatoLexErrorStackEntry<'source>)>), + Multiple(Vec<(Box, KyomatoLexErrorStackEntry)>), #[error("ALT: {:?}", .0)] - Alt(Vec>), + Alt(Vec), } -impl<'source> KyomatoLexError<'source> { - fn no_tokens(pos: &'source str) -> Self { - Self::Single(pos, KyomatoLexErrorStackEntry::NoTokens) +impl KyomatoLexError { + fn no_tokens(pos: &str) -> Self { + Self::Single(Box::::from(pos), KyomatoLexErrorStackEntry::NoTokens) } - fn forbidden_token(pos: &'source str, token: &'static str, reason: &'static str) -> Self { + fn forbidden_token(pos: &str, token: &'static str, reason: &'static str) -> Self { Self::Single( - pos, + Box::::from(pos), KyomatoLexErrorStackEntry::ForbiddenToken { token, reason }, ) } - fn bad_indent(pos: &'source str, error: ParseIntError) -> Self { - Self::Single(pos, KyomatoLexErrorStackEntry::BadIdent(error)) + fn bad_indent(pos: &str, error: ParseIntError) -> Self { + Self::Single( + Box::::from(pos), + KyomatoLexErrorStackEntry::BadIdent(error), + ) } - fn too_long_list(pos: &'source str) -> Self { - Self::Single(pos, KyomatoLexErrorStackEntry::TooLongList) + fn too_long_list(pos: &str) -> Self { + Self::Single( + Box::::from(pos), + KyomatoLexErrorStackEntry::TooLongList, + ) } - fn unknown_list_type(pos: &'source str) -> Self { - Self::Single(pos, KyomatoLexErrorStackEntry::UnknownListType) + fn unknown_list_type(pos: &str) -> Self { + Self::Single( + Box::::from(pos), + KyomatoLexErrorStackEntry::UnknownListType, + ) } - fn bad_url(pos: &'source str, err: url::ParseError) -> Self { - Self::Single(pos, KyomatoLexErrorStackEntry::BadUrl(err)) + fn bad_url(pos: &str, err: url::ParseError) -> Self { + Self::Single( + Box::::from(pos), + KyomatoLexErrorStackEntry::BadUrl(err), + ) } - fn too_deep_header(pos: &'source str, fold: usize) -> Self { - Self::Single(pos, KyomatoLexErrorStackEntry::TooDeepHeader { fold }) + fn too_deep_header(pos: &str, fold: usize) -> Self { + Self::Single( + Box::::from(pos), + KyomatoLexErrorStackEntry::TooDeepHeader { fold }, + ) } - fn _stack(self) -> Vec<(&'source str, KyomatoLexErrorStackEntry<'source>)> { + fn _stack(self) -> Vec<(Box, KyomatoLexErrorStackEntry)> { match self { KyomatoLexError::Single(pos, entry) => vec![(pos, entry)], KyomatoLexError::Multiple(stack) => stack, err @ KyomatoLexError::Alt(_) => { - vec![("ALT", KyomatoLexErrorStackEntry::Inner(Box::new(err)))] + vec![( + Box::from("ALT"), + KyomatoLexErrorStackEntry::Inner(Box::new(err)), + )] } } } } #[derive(Debug, PartialEq)] -pub enum KyomatoLexErrorStackEntry<'source> { +pub enum KyomatoLexErrorStackEntry { /// Tokens were expected, but none were found. NoTokens, /// Headers only up to 6-fold are supported, but even more folded one was found @@ -1158,20 +1180,26 @@ pub enum KyomatoLexErrorStackEntry<'source> { BadIdent(ParseIntError), UnknownListType, TooLongList, - External(Equivalent>), + External(Equivalent>), Nom(ErrorKind), Context(&'static str), - Inner(Box>), + Inner(Box), } -impl<'source> ParseError<&'source str> for KyomatoLexError<'source> { +impl<'source> ParseError<&'source str> for KyomatoLexError { fn from_error_kind(input: &'source str, kind: ErrorKind) -> Self { - Self::Single(input, KyomatoLexErrorStackEntry::Nom(kind)) + Self::Single( + Box::::from(input), + KyomatoLexErrorStackEntry::Nom(kind), + ) } fn append(input: &'source str, kind: ErrorKind, other: Self) -> Self { let mut stack = other._stack(); - stack.push((input, KyomatoLexErrorStackEntry::Nom(kind))); + stack.push(( + Box::::from(input), + KyomatoLexErrorStackEntry::Nom(kind), + )); Self::Multiple(stack) } @@ -1180,20 +1208,26 @@ impl<'source> ParseError<&'source str> for KyomatoLexError<'source> { } } -impl<'source> ContextError<&'source str> for KyomatoLexError<'source> { +impl<'source> ContextError<&'source str> for KyomatoLexError { fn add_context(input: &'source str, ctx: &'static str, other: Self) -> Self { let mut stack = other._stack(); - stack.push((input, KyomatoLexErrorStackEntry::Context(ctx))); + stack.push(( + Box::::from(input), + KyomatoLexErrorStackEntry::Context(ctx), + )); Self::Multiple(stack) } } -impl<'source, E: Error + 'source> FromExternalError<&'source str, E> for KyomatoLexError<'source> { +impl<'source, E: Error + 'static> FromExternalError<&'source str, E> for KyomatoLexError { fn from_external_error(input: &'source str, kind: ErrorKind, e: E) -> Self { Self::Multiple(vec![ - (input, KyomatoLexErrorStackEntry::Nom(kind)), ( - "EXTERNAL", + Box::::from(input), + KyomatoLexErrorStackEntry::Nom(kind), + ), + ( + Box::::from("EXTERNAL"), KyomatoLexErrorStackEntry::External(Equivalent(Box::new(e) as _)), ), ]) @@ -1214,7 +1248,7 @@ fn tokens_many1< 'source: 'kernel, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexError<'source>> + + FromExternalError<&'source str, KyomatoLexError> + 'kernel, >( mut tokens_kernel: impl FnMut(&'source str) -> IResult<&'source str, (Token<'source>, Option>), E> @@ -1309,31 +1343,34 @@ pub fn lex< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexError<'source>> + + FromExternalError<&'source str, KyomatoLexError> + 'source, >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { context( "unlimited lex", - tokens_many1(alt(( - // Allow them all! - div_page.map(__equalizer), - header.map(__equalizer), - equation.map(__equalizer), - table.map(__equalizer), - figure.map(__equalizer), - href.map(__equalizer), - code_block.map(__equalizer), - ayano.map(__equalizer), - list.map(__equalizer), - formatting.map(__equalizer), - inline_math.map(__equalizer), - reference.map(__equalizer), - footnote_content.map(__equalizer), - footnote_reference.map(__equalizer), - paragraph, - ))), + all_consuming(terminated( + tokens_many1(alt(( + // Allow them all! + div_page.map(__equalizer), + header.map(__equalizer), + equation.map(__equalizer), + table.map(__equalizer), + figure.map(__equalizer), + href.map(__equalizer), + ayano.map(__equalizer), + code_block.map(__equalizer), + list.map(__equalizer), + formatting.map(__equalizer), + inline_math.map(__equalizer), + reference.map(__equalizer), + footnote_content.map(__equalizer), + footnote_reference.map(__equalizer), + paragraph, + ))), + multispace0, + )), ) .parse(input) } @@ -1345,7 +1382,7 @@ fn forbid< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexError<'source>>, + + FromExternalError<&'source str, KyomatoLexError>, >( mut parser: impl Parser<&'source str, Token<'source>, E>, token: &'static str, @@ -1371,7 +1408,7 @@ pub fn inner_lex< 'source, E: ParseError<&'source str> + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexError<'source>> + + FromExternalError<&'source str, KyomatoLexError> + 'source, >( input: &'source str, @@ -1398,6 +1435,12 @@ pub fn inner_lex< .map(__equalizer), forbid(table, "table", "table can only be an outer element").map(__equalizer), forbid(figure, "figure", "figure can only be an outer element").map(__equalizer), + forbid( + ayano, + "ayano", + "Ayano on the inner layer is not supported, as of now", + ) + .map(__equalizer), forbid(code_block, "code block", "you are not supposed to do that") .map(__equalizer), forbid( @@ -1406,12 +1449,6 @@ pub fn inner_lex< "LaTeX does not support lists inside captions and footnotes well", ) .map(__equalizer), - forbid( - ayano, - "ayano", - "Ayano on the inner layer is not supported, as of now", - ) - .map(__equalizer), forbid( footnote_content, "footnote content", @@ -1501,13 +1538,49 @@ mod tests { test! {div_page4, div_page, "\n ------", p: Err(_)} test! {div_page5, div_page, " \t\n------\t\n", e: Ok(("\t\n", Token::PageDiv))} - // `header` tests - test! {header1, header, "", p: Err(_)} - test! {header2, header, "\n#Ababa\n", e: Ok(("\n", Token::Header { order: 1, content: "Ababa".into() }))} - test! {header3, header, "\n# Ababa \t \t\n", e: Ok(("\n", Token::Header { order: 1, content: "Ababa".into() }))} - test! {header4, header, "\n### Ababa \n", e: Ok(("\n", Token::Header { order: 3, content: "Ababa".into() }))} - test! {header5, header, "\n###### Заголовок українською \n", e: Ok(("\n", Token::Header { order: 6, content: "Заголовок українською".into() }))} - test! {header6, header, "\n####### Заголовок українською \n", p: Err(_)} + macro_rules! head { + [# $header:literal] => { + head![1, $header] + }; + [## $header:literal] => { + head![2, $header] + }; + [### $header:literal] => { + head![3, $header] + }; + [#### $header:literal] => { + head![4, $header] + }; + [##### $header:literal] => { + head![5, $header] + }; + [###### $header:literal] => { + head![6, $header] + }; + [$order:literal, $header:literal] => { + Token::Header {order: $order, content: Cow::Borrowed($header)} + }; + } + mod header { + use super::*; + macro_rules! test_ok { + {$name:ident, $input:literal, $output:expr, $left_over:literal} => { + test!{$name, header, $input, e: Ok(($left_over, $output))} + }; + } + macro_rules! test_err { + {$name:ident, $input:literal} => { + test!{$name, header, $input, p: Err(_)} + }; + } + + test_err! {err_empty, ""} + test_ok! {ok_merged, "\n#Ababa\n", head!(#"Ababa"), "\n"} + test_ok! {ok_spread, "\n# Ababa \t \t\n", head!(#"Ababa"), "\n"} + test_ok! {ok_spread_3, "\n### Ababa \n", head!(###"Ababa"), "\n"} + test_ok! {ok_cyrillic_6, "\n###### Заголовок українською \n", head!(######"Заголовок українською"), "\n"} + test_err! {err_header_7, "\n####### Заголовок українською \n"} + } // `equation` tests test! {equation1, equation, "", p: Err(_)} @@ -1545,12 +1618,17 @@ $$ y = x^2 $$ {ref = parabola}"#, e: Ok(("", Token::Equation { content: "y = x^2".into(), ident: Some("parabola".into()) }))} + test! {equation9_5, equation, r#" +$$ +y = x^2 +$$ +{ref = parabola_ref}"#, e: Ok(("", Token::Equation { content: "y = x^2".into(), ident: Some("parabola_ref".into()) }))} // Equation ident example test! {equation10, equation, r#" $$ y = x^2 $$ -{ref = parabola_1}"#, p: Err(_)} +{ref = parabola_1}"#, e: Ok(("", Token::Equation { content: "y = x^2".into(), ident: Some("parabola_1".into()) }))} // This is not ok // TODO although that's subject to change test! {equation11, equation, r#" @@ -1717,29 +1795,6 @@ $$ Token::PageDiv }; } - macro_rules! head { - [# $header:literal] => { - head![1, $header] - }; - [## $header:literal] => { - head![2, $header] - }; - [### $header:literal] => { - head![3, $header] - }; - [#### $header:literal] => { - head![4, $header] - }; - [##### $header:literal] => { - head![5, $header] - }; - [###### $header:literal] => { - head![6, $header] - }; - [$order:literal, $header:literal] => { - Token::Header {order: $order, content: Cow::Borrowed($header)} - }; - } macro_rules! eq { ($eq:literal) => { eq!($eq, None) @@ -1930,6 +1985,15 @@ $$ [1 $($item:expr), +] => { ls![ListType::Num; $($item), +] }; + [a $($item:expr), +] => { + ls![ListType::Latin; $($item), +] + }; + [а $($item:expr), +] => { + ls![ListType::Cyrillic; $($item), +] + }; + [- $($item:expr), +] => { + ls![ListType::Bullet; $($item), +] + }; [$list_type:expr; $($item:expr), +] => { Token::List {list_type: $list_type, content: Tokens::new([$($item), +])} }; @@ -1949,10 +2013,132 @@ $$ } test_err! {err_empty, ""} - test_ok! {ok_single, "\n1. Item 1\n", ls!(1 tx!("Item 1")), "\n"} + test_ok! {ok_single, "\n1. Item 1\n", ls![1 tx!("Item 1")], "\n"} + test_ok! {ok_multiple, "\n1. Item 1 \n2. Gamma-function def: $\\Gamma(\\alpha) = \\int\\limits_{0}^{+\\infinity} t^{\\alpha - 1} e^{-t} dt$.\n3. Here's some table ref: ([@tab:function ]) \n4. Explained in [^source2]\n", + ls![1 tx!("Item 1"), + tks![tx!("Gamma-function def:"), eq!(!"\\Gamma(\\alpha) = \\int\\limits_{0}^{+\\infinity} t^{\\alpha - 1} e^{-t} dt"), tx!(".")], + tks![tx!("Here's some table ref: ("), rf!(@"tab:function"), tx!(")")], + tks![tx!("Explained in"), rf!(^"source2")] + ], "\n"} + test_ok! {ok_latin, "\na. Item 1 \nb. Gamma-function def: $\\Gamma(\\alpha) = \\int\\limits_{0}^{+\\infinity} t^{\\alpha - 1} e^{-t} dt$.\nc. Here's some table ref: ([@tab:function ]) \nd. Explained in [^source2]\n", + ls![a tx!("Item 1"), + tks![tx!("Gamma-function def:"), eq!(!"\\Gamma(\\alpha) = \\int\\limits_{0}^{+\\infinity} t^{\\alpha - 1} e^{-t} dt"), tx!(".")], + tks![tx!("Here's some table ref: ("), rf!(@"tab:function"), tx!(")")], + tks![tx!("Explained in"), rf!(^"source2")] + ], "\n"} + test_ok! {ok_cyrillic, "\nа. Item 1 \nб. Gamma-function def: $\\Gamma(\\alpha) = \\int\\limits_{0}^{+\\infinity} t^{\\alpha - 1} e^{-t} dt$.\nв. Here's some table ref: ([@tab:function ]) \nг. Explained in [^source2]\n", + ls![а tx!("Item 1"), + tks![tx!("Gamma-function def:"), eq!(!"\\Gamma(\\alpha) = \\int\\limits_{0}^{+\\infinity} t^{\\alpha - 1} e^{-t} dt"), tx!(".")], + tks![tx!("Here's some table ref: ("), rf!(@"tab:function"), tx!(")")], + tks![tx!("Explained in"), rf!(^"source2")] + ], "\n"} + test_ok! {ok_bullet, "\n- Item 1 \n- Gamma-function def: $\\Gamma(\\alpha) = \\int\\limits_{0}^{+\\infinity} t^{\\alpha - 1} e^{-t} dt$.\n- Here's some table ref: ([@tab:function ]) \n- Explained in [^source2]\n", + ls![- tx!("Item 1"), + tks![tx!("Gamma-function def:"), eq!(!"\\Gamma(\\alpha) = \\int\\limits_{0}^{+\\infinity} t^{\\alpha - 1} e^{-t} dt"), tx!(".")], + tks![tx!("Here's some table ref: ("), rf!(@"tab:function"), tx!(")")], + tks![tx!("Explained in"), rf!(^"source2")] + ], "\n"} + test_ok! {ok_left_over, "\n- Item 1 \n- Gamma-function def: $\\Gamma(\\alpha) = \\int\\limits_{0}^{+\\infinity} t^{\\alpha - 1} e^{-t} dt$.\n- Here's some table ref: ([@tab:function ]) \n- Explained in [^source2]\nІще трохи тексту після списку", + ls![- tx!("Item 1"), + tks![tx!("Gamma-function def:"), eq!(!"\\Gamma(\\alpha) = \\int\\limits_{0}^{+\\infinity} t^{\\alpha - 1} e^{-t} dt"), tx!(".")], + tks![tx!("Here's some table ref: ("), rf!(@"tab:function"), tx!(")")], + tks![tx!("Explained in"), rf!(^"source2")] + ], "\nІще трохи тексту після списку"} + } + + macro_rules! foot { + (^$ident:literal: $content:expr) => { + Token::FootNoteContent { + ident: Cow::Borrowed($ident), + content: Box::new($content), + } + }; + } + mod footnote_content { + use super::*; + + macro_rules! test_ok { + {$name:ident, $input:literal, $output:expr, $left_over:literal} => { + test!{$name, footnote_content, $input, e: Ok(($left_over, $output))} + }; + } + + macro_rules! test_err { + {$name:ident, $input:literal} => { + test!{$name, footnote_content, $input, p: Err(_)} + }; + } + + test_err! {err_empty, ""} + test_err! {err_no_newline, "[^1]: Content!"} + test_err! {err_unclosed, "\n[^1: Content!"} + test_err! {err_no_ident, "\n[^]: Content!"} + test_err! {err_no_content, "\n[^]: \n"} + test_ok! {ok_text, "\n[^1]: Content!", foot!(^"1": tx!("Content!")), ""} + test_ok! {ok_complex_ident, "\n[^ source_2_3]: Here's some more explanation", foot!(^"source_2_3": tx!("Here's some more explanation")), ""} + + test_ok! {ok_idk1, "\n\n[^1]: Для поливу.", foot!(^"1": tx!("Для поливу.")), ""} + } + + macro_rules! ayano { + {!$is_static:literal, *$description:expr, ~$insert_path:expr, #$_id:expr, $code:literal} => { + Token::Ayano(AyanoBlock{ + is_display: $description.is_some(), + is_static: $is_static, + code: Cow::Borrowed($code), + insert_path: $insert_path.map(Path::new).map(Cow::from), + }) + }; + } + mod ayano { + use super::*; + + macro_rules! test_ok { + {$name:ident, $input:literal, $output:expr, $left_over:literal} => { + test!{$name, ayano, $input, e: Ok(($left_over, $output))} + }; + } + + macro_rules! test_err { + {$name:ident, $input:literal} => { + test!{$name, ayano, $input, p: Err(_)} + }; + } + + test_err! {err_empty, ""} + test_ok! {ok_simple, "\n```python, Ayano * \"Code description\" ~ path/to/file # 1\nx = 1\ny = 2\nprint(x + y)\n```\n", + ayano!(!false, *Some(tx!("Code description")), ~Some("path/to/file"), #Some(1), "x = 1\ny = 2\nprint(x + y)"), "\n"} + test_ok! {ok_some, "\n```python, Ayano ! ~ path/to/file # 1\nx = 1\ny = 2\nprint(x + y)\n```\n", + ayano!(!true, *None::<&str>, ~Some("path/to/file"), #Some(1), "x = 1\ny = 2\nprint(x + y)"), "\n"} + test_ok! {ok_no_args, "\n```python, Ayano \nx = 1\ny = 2\nprint(x + y)\n```\n", + ayano!(!false, *None::<&str>, ~None::<&str>, #None, "x = 1\ny = 2\nprint(x + y)"), "\n"} } - // TODO add footnote content tests - // TODO add Ayano block tests // FIXME EXPL: `formatting` tests are not to be done, since this token type will be removed soon anyways - // TODO add `lex` tests (basically, whole parser tests) + mod lex { + use super::*; + macro_rules! test_ok { + {$name:ident, $input:literal, $output:expr} => { + test!{$name, lex, $input, e: Ok(("", $output))} + }; + } + + macro_rules! test_err { + {$name:ident, $input:literal} => { + test!{$name, lex, $input, p: Err(_)} + }; + } + + test_err! {err_empty, ""} + test_ok! {ok_some_text, "\t\t\nSome text\n", tx!("Some text")} + test_ok! {ok_multiple_test, "\t\t\nSome text\nІ ще отакий текст", tks![tx!("Some text"), tx!("І ще отакий текст")]} + test_ok! {ok_multiple_test2, "\t\t\n# Header\n## header2\n\n\tЦе початок тексту.\n- А це перший елемент списку\n- І другий елемент списку\nА оце буде рівняння: \n$$\ny = x ^ 2\n$$\n", + tks![head!(# "Header"), head!(## "header2"), tx!("Це початок тексту."), ls!(- tx!("А це перший елемент списку"), tx!("І другий елемент списку")), tx!("А оце буде рівняння:"), eq!("y = x ^ 2")]} + + test_ok! {ok_idk1, "На світлині [@ fig:refurb] можна бачити принципову схему влаштування рефурбалідзера. Неважно помитити великі очі[^1], і два термоядерних реактори, що оточують їх. Точні реакції всередині невідомі, але цілком можливо що там женуть $du + t -> {}^{5}_{6}C$.\n\n[^1]: Для поливу.", + tks![tx!("На світлині"), rf!(@ "fig:refurb"), + tx!("можна бачити принципову схему влаштування рефурбалідзера. Неважно помитити великі очі"), rf!(^"1"), + tx!(", і два термоядерних реактори, що оточують їх. Точні реакції всередині невідомі, але цілком можливо що там женуть"), eq!(!"du + t -> {}^{5}_{6}C"), + tx!("."), foot!(^"1": tx!("Для поливу."))]} + test_ok! {ok_idk2, "\n\n## Підпункт\n\n", head!(##"Підпункт")} + } } diff --git a/src/lib.rs b/src/lib.rs index 18a5f79..539015e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,12 +27,45 @@ //! Well, unless there's a way to efficiently write to a single output from multiple threads, while preserving output order. //! Any sort of "collect" function would've destroyed the purpose anyway. +use std::fmt::Write; + +use gen::OutputGenerator; +use lexer::KyomatoLexError; + /// This module defines types that are used to represent parsed data mod data; mod gen; mod lexer; mod path_engine; +/// Reexports +pub fn lex<'source>(input: &'source str) -> Result, KyomatoLexError> { + lexer::lex::<'source, KyomatoLexError>(input) + .map(|(_, t)| t) + .map_err(|err| match err { + nom::Err::Error(err) | nom::Err::Failure(err) => err, + nom::Err::Incomplete(_) => unreachable!("This is a complete input"), + }) +} +pub fn gen<'token, 'source: 'token>( + token: &'token data::Token<'source>, + mut output: impl std::io::Write, +) -> Result<(), Box> { + let buf = gen_to_string(token)?; + // TODO make a transformer struct + output.write_all(buf.as_bytes())?; + Ok(()) +} +pub fn gen_to_string<'token, 'source: 'token>( + token: &'token data::Token<'source>, +) -> Result> { + let generator = gen::lab::LabaLatex::new(&path_engine::PrimitiveEngine); + let meta = gen::lab::SourceMeta::collect(&token)?.init_ayano()?; + let mut buf = String::new(); + generator.write_to(&mut buf, &meta, &mut gen::lab::Context::default(), token)?; + Ok(buf) +} + /// This module houses "utility-like" structs and functions. /// /// Since this is generally a bad practice, I'd like to know suggestions on std library/other crates items I could use instead diff --git a/src/path_engine.rs b/src/path_engine.rs index b1dc864..aae738d 100644 --- a/src/path_engine.rs +++ b/src/path_engine.rs @@ -1,13 +1,17 @@ use std::path::{Path, PathBuf}; -#[derive(Debug, derive_more::From)] +#[derive(Debug, derive_more::From, thiserror::Error)] pub enum VariantError { + #[error("{}", .0)] Io(std::io::Error), // missing permissions, or broken symbolic links - NotExist, // file does not in fact exist in the filesystem - NotAFile, // path resolves to a directory, not a file + #[error("file does not exist")] + NotExist, // file does not in fact exist in the filesystem + #[error("path is not of a file")] + NotAFile, // path resolves to a directory, not a file } -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] +#[error("Failed to resolve path: {:?}", self)] pub struct Error { source: Option, // error resolving path relative to the target file (if present) ayano: Option, // error resolving relative to ayano-inserted code diff --git a/src/util.rs b/src/util.rs index c828b4f..033b95d 100644 --- a/src/util.rs +++ b/src/util.rs @@ -40,9 +40,11 @@ impl GenericRange { } } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, thiserror::Error)] pub enum GenericRangeParseError { + #[error("Generic range format is wrong")] Format, + #[error("{}", .0)] Object(ParseIntError), } @@ -386,3 +388,110 @@ mod fn_ref_tests { let _chain_pointer = fn_chain!(fn_mut!(Vec::::pop), Option::unwrap); } } + +struct MergeCorr { + i1: I1, + i2: I2, + buf1: Vec, + buf2: Vec, + correlation: Corr, +} + +fn merge_corr( + i1: I1, + i2: I2, + correlation: Corr, +) -> impl Iterator +where + I1: Iterator, + I2: Iterator, + Corr: FnMut(&It1, &It2) -> bool, +{ + MergeCorr { + i1, + i2, + buf1: Vec::new(), + buf2: Vec::new(), + correlation, + } +} + +impl MergeCorr +where + I1: Iterator, + I2: Iterator, + Corr: FnMut(&It1, &It2) -> bool, +{ + fn next12(&mut self) -> Option<(It1, It2)> { + for i in 0..self.buf1.len() { + for j in 0..self.buf2.len() { + if (&mut self.correlation)(&self.buf1[i], &self.buf2[j]) { + return Some((self.buf1.swap_remove(i), self.buf2.swap_remove(j))); + } + } + } + None + } + + fn next1(&mut self) -> Result<(It1, It2), bool> { + // First, try getting next element from the iterator + if let Some(e1) = self.i1.next() { + // If succeeded, try finding correspondence among already present `buf2` elements + if let Some(ind2) = self + .buf2 + .iter() + .position(|e2| (&mut self.correlation)(&e1, e2)) + { + // If succeeded, return the result + let e2 = self.buf2.swap_remove(ind2); + return Ok((e1, e2)); + } + // There's no correspondence already present. Add this element to buf1 + self.buf1.push(e1); + } + self.next12().ok_or(true) + } + fn next2(&mut self) -> Result<(It1, It2), bool> { + // First, try getting next element from the iterator + if let Some(e2) = self.i2.next() { + // If succeeded, try finding correspondence among already present `buf2` elements + if let Some(ind1) = self + .buf1 + .iter() + .position(|e1| (&mut self.correlation)(e1, &e2)) + { + // If succeeded, return the result + let e1 = self.buf1.swap_remove(ind1); + return Ok((e1, e2)); + } + // There's no correspondence already present. Add this element to buf1 + self.buf2.push(e2); + } + self.next12().ok_or(true) + } +} + +impl Iterator for MergeCorr +where + I1: Iterator, + I2: Iterator, + Corr: FnMut(&It1, &It2) -> bool, +{ + type Item = (It1, It2); + + fn next(&mut self) -> Option { + let mut try1 = true; + let mut try2 = true; + while try1 || try2 { + match self.next1() { + Ok(res) => return Some(res), + Err(e) => try1 = e, + } + match self.next2() { + Ok(res) => return Some(res), + Err(e) => try2 = e, + } + } + None + } +} diff --git a/tests/input_files/basic.md b/tests/input_files/basic.md new file mode 100644 index 0000000..2c900d9 --- /dev/null +++ b/tests/input_files/basic.md @@ -0,0 +1,23 @@ + +# Перша тема + +Тут трохи тексту і пояснень про квантові мультивироджені рефурбалідзери змішаного типу. Це нудно, тож тримайте формулу: + +$$ +Ref(z) = \int \limits_{0}^{1} t^{z-1} \cdot \exp(-t) dt +$$ +{ref = refurbalidzer_1 } + +## Підпункт + +Рефурбалідзери можуть виявитись дуже важливими для збереження екологічної безпеки нашої планети. До прикладу, в них є наступні переваги: +- Нормувальний множник $|\psi|^{2} = 3/4$ +- Кількість хромосом - $\exp(\pi)$ +- Поживна цінність - 20 дптр/см + +![[refurb.jpg]] +{ref = refurb, caption = "Принципове влаштування рефурбалідзера"} + +На світлині [@ fig:refurb] можна бачити принципову схему влаштування рефурбалідзера. Неважно помітити великі очі[^1], і два термоядерних реактори, що оточують їх. Точні реакції всередині невідомі, але цілком можливо що там женуть $du + t -> {}^{5}_{6}C$... + +[^1]: Для поливу. \ No newline at end of file diff --git a/tests/input_files/deceit.md b/tests/input_files/deceit.md new file mode 100644 index 0000000..9d88a3b --- /dev/null +++ b/tests/input_files/deceit.md @@ -0,0 +1,204 @@ +This test is dedicated to attempts of parser deception, i.e. usage of special symbols in wrong places to see if parser recognizes them as control symbols. There we go: + +# Page dividers + +Page dividers are represented by ------ + +If the line above has dashes ^^^^^^ here, it probably works alright. + +Here's a genuine divider +------ +Fun fact: there can be a paragraph right after it! +------ Some text, cause why not +But this **DOES** look cursed, don't do it. + +# Headers +Headers are most recognizable by '#'-symbol. For it to actually represent a header, it needs to at the beginning of the line and there should be no more than 6 of them. **ANYTHING** up to the end of line will be included into a header, then. + +Example: + +###### This is a long, yet formally correct header. You can add $equations$ and *formatting* here, but I provide no guarantees as of the way these would display in final document. + +Here's an example of incorrect header: + + ###### This header is considered incorrect for this application, and will be parsed as regular paragraph instead + +Another example of incorrect header, featuring 7 hash-symbols: + +####### This will be parsed as a regular paragraph + +# Equations + +There are two types of equations in markdown/latex: inline mathmode and display mathmode + +Both of them are defined by $-symbols. However, they must be paired to actually represent these. Here are examples: + +Inline: $y = x ^ 2$ + +Display: +$$ +y = x ^ 2 +$$ + +Note that display mathmode should ALWAYS start from a new line for this application. + +Basically, there's not much to deceive for: you can do that: +$ +$ +(the above should not be mathmode), and that's it, basically :sad: + +# Table +Tables are defined by |-symbol. But the actual table will happen only if they are the first thing on the line. + +Due to current implementation, there's no cure apart from display mathmode and code: + +(this should be display mathmode): +$$ +|x| +$$ + +(this should be a code block): +```bash +head /dev/random +| cat +``` + +# Figure + +Figure is defined by a ![[ sequence at a start of the line. Same points should be uphold here: + +$$ +![[ +$$ + +``` +![[ +``` + +# References + +These start with [] + +## Hyperrefs + +These are followed by parentheses (). Nothing is allowed between them, thus + +Here's a correct href: [my gh page](https://youtu.be/rickroll_link_lol) + +Here are incorrect refs: + +[my gh page] (https://youtu.be/rickroll_link_lol) + +[my gh page] +(https://youtu.be/rickroll_link_lol) + +## Footnote refs + +These are started by [^. + +Here are correct footnote refs: [^1], [^source] + +Here are incorrect ones: [^1, ^1], + +[^ +1 +] + +So on + +## Object refs + +These refer to objects like figures, tables and equations. + +Started by [@. + +Here's a correct ref: [@ fig:experiment_schema] + +Here's are bad ones: [@ fig:1, @ source3], + +[@ +eq2 +] + +**NOTE**: despite object types being prepended with 'eq', 'fig' or 'tab' in refs, you *can* use regular latex to define your custom refs. In fact, LaTeX doesn't care about all of these at all; I'm doing these transformations just for my own comfort, as I've been doing this manually. + +# Formatting +This object's type is deprecated and will be removed soon. + +It starts and ends with +- * or _ +- ** or __ +- ~~ + +**WARN**: As of now, these *might* cause cause massive problems if encountered at the start of the line. + +Basically, parsers for these tokens attempt parse everything up until they find a closing token. But if they are inserted in a middle of the text, then it's ok - they will be limited by current paragraph, and (most likely) will find no match inside it, if their use was unintended. + +# Footnote content +These start with '[^ID]:' and proceed to the end of line. + +Example of valid content: +[^source2]: Explanation of source2. + +Example of invalid content: +[^source2: This is invalid content +[^source2] : This is invalid content + [^source2]: This is invalid content + +They also must start at the beginning of the line. If fact, this: [^source2]: Some content +Is expected to be a footnote ref with a colon. + +# Code blocks + +There are two fundamental types of code blocks: normal code block and **Ayano** code block. + +## Normal code block + +These start and end with '```' on a newline. There can be **practically anything** inside. Example: + +```java +int x = 2; +int y = 4; +int[] arr = new int[2]; +arr[0] = x; +arr[1] = 4; +``` + +There can be anything after code block had closed: +```lua(Idk why vscode detects this block closing as a valid lua syntax) +local x = 'matt' +local y = 'y' +local z = x .. y -- matty!! +``` Here's some text that's ok to be here + +There can't be any spaces before delimiting ```s: + + ```python + # This here is not really a python block + x = 2 + print(x) + ``` + +## Ayano code block + +*because Ayano loves Kyoko* + +This object looks and behaves just like normal code block, but is only limited to *python* language, can have spacial arguments in a language line, may leave something behind in the document. For example: + +This Ayano block will leave plain "2" behind it: +```python, Ayano +2 +``` + +Akin to normal blocks, it's ok to leave something trailing: +```ayano +2 +``` This text is ok to be here + +And these are invalid Ayano blocks: +```ayano +2 +``` + ```ayano +2 + ``` \ No newline at end of file diff --git a/tests/input_files/tables_code.md b/tests/input_files/tables_code.md new file mode 100644 index 0000000..6d60a12 --- /dev/null +++ b/tests/input_files/tables_code.md @@ -0,0 +1,36 @@ +Наступний тест. Тут протестуємо таблиці і блоки коду. + +## Таблиці + +Звичайні таблиці; Можуть всередині мати **формули** чи *посилання* на інші штуки + +| Storage | Space | Number | +| :---------: | :-----------: | :------------------------: | +| 1 | 2 | $3 + e ^{\pi}$ | +| $e$[^euler] | $\cdot \pm 1$ | І тут ще *трохи* тексту \| | +{ref = example_1, caption = "Оце приклад таблиці. Як казав мій вчитель: **ВИКОРИСТОВУЙ БІЛЬШЕ ВИБУХІВКИ** (і \"лапок\")"} + +Таблиця цілком собі може не мати ідентифікатора або опису: + +| Кількість | Роль | +| :--------: | :-------: | +| 3 діоптрії | 7 галон | +| rust | #PANIC!!! | +{ref = table_2} + + +[^euler]: Це число Ейлера. Воно дуже корисне, ~~але не може бути записане~~ у вигляді *десяткового* дробу. + +## Блоки коду + +Наразі мають досить обмежену підтримку, адже у $\LaTeX$ немає нативного поняття блоків коду. + +На щастя, у **markdown** таке поняття є[^code]: +```rust +let x = 5; +let y = x.powi(2); +let z = (y as f64).sqrt(); +println!("x = {x}, y = {y}, z = {x}"); +``` + +[^code]: Принаймні, *Obsidian* таке робить diff --git a/tests/lex.rs b/tests/lex.rs new file mode 100644 index 0000000..6ddc54d --- /dev/null +++ b/tests/lex.rs @@ -0,0 +1,147 @@ +use std::{ffi::OsString, path::PathBuf, str::FromStr}; + +const CLEAR: &'static str = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" ; +const INPUT_DIR: &'static str = "./tests/input_files"; +const LEX_DIR: &'static str = "./tests/lex_files"; + +fn input_files() -> impl Iterator { + std::fs::read_dir(INPUT_DIR) + .expect("Should be able to access input directory") + .into_iter() + .map(|file| { + let file_path = file + .expect("Should be able to access files in input directory") + .path(); + let input = + std::fs::read_to_string(&file_path).expect("Should be able to read input files"); + (OsString::from(file_path.file_name().unwrap()), input) + }) +} + +/// Prints input file's lex into the stdout, for inspection +#[ignore = "manual"] +#[test] +fn show_lex() { + for (_, input) in input_files() { + let parsed = match kyomato::lex(input.as_str()) { + Ok(ok) => ok, + Err(err) => { + panic!("Should successfully parse the input: {err:#?}"); + } + }; + // TODO check what the actual result is + println!("{parsed:#?}"); + std::io::stdin() + .read_line(&mut String::new()) + .expect("Should be able to read a line"); + println!("{}", CLEAR); + } +} + +// TODO rewrite to use serde-json after token rework +/// Generates representation of a parsed tokens, for automated testing to compare with +#[cfg(feature = "serde")] +#[ignore = "manual"] +#[test] +fn gen_lex_output() { + use std::{fs::File, io::Write, os::unix::fs::FileExt, path::PathBuf, str::FromStr}; + { + use std::io::Read; + print!("Are you sure? This will rewrite previous test outputs: [y/N]: "); + std::io::stdout() + .flush() + .expect("Should be able to flush the stdout"); + + let mut buf = [0u8; 1]; + std::io::stdin() + .read_exact(&mut buf) + .expect("Should be able to read stdin"); + if buf[0] != b'y' { + println!("\nAborting..."); + return; + } + } + let mut output_path = PathBuf::from_str(LEX_DIR).unwrap(); + if !output_path.is_dir() { + std::fs::create_dir(&output_path).expect("Failed to create directory"); + } + for (filename, input) in input_files() { + let parsed = match kyomato::lex(input.as_str()) { + Ok(ok) => ok, + Err(err) => { + panic!("Input files should be successfully parsed: {err:#?}"); + } + }; + // create representation of the parsed tokens + // TODO this part should be replaced, once serde is supported + let repr = format!("{:#?}", parsed); + // write the result to output directory + let output_file = { + output_path.push(filename); + output_path.set_extension("txt"); + let output_file = if output_path.is_file() { + File::options() + .write(true) + .truncate(true) + .open(&output_path) + } else { + File::create(&output_path) + } + .expect("Failed to create output file"); + output_path.pop(); + output_file + }; + output_file + .write_at(repr.as_bytes(), 0) + .expect("Failed to write into output file"); + } +} + +#[test] +fn test_lex() { + let mut output_results = Vec::new(); + let mut output_path = PathBuf::from_str(LEX_DIR).unwrap(); + if !output_path.is_dir() { + std::fs::create_dir(&output_path).expect("Failed to create directory"); + } + for (filename, input) in input_files() { + let parsed = match kyomato::lex(input.as_str()) { + Ok(ok) => ok, + Err(err) => { + panic!("Should successfully parse the input: {err:#?}"); + } + }; + // create representation of the parsed tokens + // TODO this part should be replaced, once serde is supported + let repr = format!("{:#?}", parsed); + // write the result to output directory + let expected_output = { + output_path.push(&filename); + output_path.set_extension("txt"); + let output = + std::fs::read_to_string(&output_path).expect("Failed to read expected output"); + output_path.pop(); + output + }; + output_results.push((filename, repr, expected_output)); + } + let total = output_results.len(); + let success = output_results + .iter() + .filter(|(_, real, expected)| real == expected) + .count(); + let failed = total - success; + if failed > 0 { + let mut output_msg = String::new(); + output_msg.push_str(&format!("passed: ({success}/{total})\nFailures:\n")); + for (filename, real, expected) in output_results { + if real != expected { + output_msg.push_str(&format!( + "\tFilename: {}\n\tActual: {real}\n\tExpected: {expected}\n\n", + filename.to_string_lossy(), + )); + } + } + panic!("{output_msg}"); + } +} diff --git a/tests/lex_files/basic.txt b/tests/lex_files/basic.txt new file mode 100644 index 0000000..19f6cf2 --- /dev/null +++ b/tests/lex_files/basic.txt @@ -0,0 +1,109 @@ +Text( + Owned( + [ + Header { + order: 1, + content: "Перша тема", + }, + Paragraph( + Normal, + "Тут трохи тексту і пояснень про квантові мультивироджені рефурбалідзери змішаного типу. Це нудно, тож тримайте формулу:", + ), + Equation { + content: "Ref(z) = \\int \\limits_{0}^{1} t^{z-1} \\cdot \\exp(-t) dt", + ident: Some( + "refurbalidzer_1", + ), + }, + Header { + order: 2, + content: "Підпункт", + }, + Paragraph( + Normal, + "Рефурбалідзери можуть виявитись дуже важливими для збереження екологічної безпеки нашої планети. До прикладу, в них є наступні переваги:", + ), + List { + list_type: Bullet, + content: Owned( + [ + Text( + Owned( + [ + Paragraph( + Normal, + "Нормувальний множник", + ), + InlineMathmode( + "|\\psi|^{2} = 3/4", + ), + ], + ), + ), + Text( + Owned( + [ + Paragraph( + Normal, + "Кількість хромосом -", + ), + InlineMathmode( + "\\exp(\\pi)", + ), + ], + ), + ), + Paragraph( + Normal, + "Поживна цінність - 20 дптр/см", + ), + ], + ), + }, + Figure { + src_name: "refurb.jpg", + caption: Some( + Paragraph( + Normal, + "Принципове влаштування рефурбалідзера", + ), + ), + ident: Some( + "refurb", + ), + }, + Paragraph( + Normal, + "На світлині", + ), + Reference( + "fig:refurb", + ), + Paragraph( + Normal, + "можна бачити принципову схему влаштування рефурбалідзера. Неважно помітити великі очі", + ), + FootNoteReference( + "1", + ), + Paragraph( + Normal, + ", і два термоядерних реактори, що оточують їх. Точні реакції всередині невідомі, але цілком можливо що там женуть", + ), + InlineMathmode( + "du + t -> {}^{5}_{6}C", + ), + Paragraph( + Normal, + "...", + ), + FootNoteContent { + content: Paragraph( + Normal, + "Для поливу.", + ), + ident: "1", + }, + ], + ), +) \ No newline at end of file diff --git a/tests/lex_files/deceit.txt b/tests/lex_files/deceit.txt new file mode 100644 index 0000000..0a63cab --- /dev/null +++ b/tests/lex_files/deceit.txt @@ -0,0 +1,650 @@ +Text( + Owned( + [ + Paragraph( + Normal, + "This test is dedicated to attempts of parser deception, i.e. usage of special symbols in wrong places to see if parser recognizes them as control symbols. There we go:", + ), + Header { + order: 1, + content: "Page dividers", + }, + Paragraph( + Normal, + "Page dividers are represented by ------", + ), + Paragraph( + Normal, + "If the line above has dashes ^^^^^^ here, it probably works alright.", + ), + Paragraph( + Normal, + "Here's a genuine divider", + ), + PageDiv, + Paragraph( + Normal, + "Fun fact: there can be a paragraph right after it!", + ), + PageDiv, + Paragraph( + Normal, + "Some text, cause why not", + ), + Paragraph( + Normal, + "But this", + ), + Formatted( + Bold, + Paragraph( + Normal, + "DOES", + ), + ), + Paragraph( + Normal, + "look cursed, don't do it.", + ), + Header { + order: 1, + content: "Headers", + }, + Paragraph( + Normal, + "Headers are most recognizable by '#'-symbol. For it to actually represent a header, it needs to at the beginning of the line and there should be no more than 6 of them.", + ), + Formatted( + Bold, + Paragraph( + Normal, + "ANYTHING", + ), + ), + Paragraph( + Normal, + "up to the end of line will be included into a header, then.", + ), + Paragraph( + Normal, + "Example:", + ), + Header { + order: 6, + content: "This is a long, yet formally correct header. You can add $equations$ and *formatting* here, but I provide no guarantees as of the way these would display in final document.", + }, + Paragraph( + Normal, + "Here's an example of incorrect header:", + ), + Paragraph( + Normal, + "###### This header is considered incorrect for this application, and will be parsed as regular paragraph instead", + ), + Paragraph( + Normal, + "Another example of incorrect header, featuring 7 hash-symbols:", + ), + Paragraph( + Normal, + "####### This will be parsed as a regular paragraph", + ), + Header { + order: 1, + content: "Equations", + }, + Paragraph( + Normal, + "There are two types of equations in markdown/latex: inline mathmode and display mathmode", + ), + Paragraph( + Normal, + "Both of them are defined by $-symbols. However, they must be paired to actually represent these. Here are examples:", + ), + Paragraph( + Normal, + "Inline:", + ), + InlineMathmode( + "y = x ^ 2", + ), + Paragraph( + Normal, + "Display:", + ), + Equation { + content: "y = x ^ 2", + ident: None, + }, + Paragraph( + Normal, + "Note that display mathmode should ALWAYS start from a new line for this application.", + ), + Paragraph( + Normal, + "Basically, there's not much to deceive for: you can do that:", + ), + Paragraph( + Normal, + "$", + ), + Paragraph( + Normal, + "$", + ), + Paragraph( + Normal, + "(the above should not be mathmode), and that's it, basically :sad:", + ), + Header { + order: 1, + content: "Table", + }, + Paragraph( + Normal, + "Tables are defined by |-symbol. But the actual table will happen only if they are the first thing on the line.", + ), + Paragraph( + Normal, + "Due to current implementation, there's no cure apart from display mathmode and code:", + ), + Paragraph( + Normal, + "(this should be display mathmode):", + ), + Equation { + content: "|x|", + ident: None, + }, + Paragraph( + Normal, + "(this should be a code block):", + ), + CodeBlock { + code: "head /dev/random\n| cat", + language: Some( + "bash", + ), + }, + Header { + order: 1, + content: "Figure", + }, + Paragraph( + Normal, + "Figure is defined by a ![[ sequence at a start of the line. Same points should be uphold here:", + ), + Equation { + content: "![[", + ident: None, + }, + CodeBlock { + code: "![[", + language: None, + }, + Header { + order: 1, + content: "References", + }, + Paragraph( + Normal, + "These start with []", + ), + Header { + order: 2, + content: "Hyperrefs", + }, + Paragraph( + Normal, + "These are followed by parentheses (). Nothing is allowed between them, thus", + ), + Paragraph( + Normal, + "Here's a correct href:", + ), + Href { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "youtu.be", + ), + ), + port: None, + path: "/rickroll_link_lol", + query: None, + fragment: None, + }, + display: "my gh page", + }, + Paragraph( + Normal, + "Here are incorrect refs:", + ), + Paragraph( + Normal, + "[my gh page] (https://youtu.be/rickroll", + ), + Formatted( + Italic, + Paragraph( + Normal, + "link", + ), + ), + Paragraph( + Normal, + "lol)", + ), + Paragraph( + Normal, + "[my gh page]", + ), + Paragraph( + Normal, + "(https://youtu.be/rickroll", + ), + Formatted( + Italic, + Paragraph( + Normal, + "link", + ), + ), + Paragraph( + Normal, + "lol)", + ), + Header { + order: 2, + content: "Footnote refs", + }, + Paragraph( + Normal, + "These are started by [^.", + ), + Paragraph( + Normal, + "Here are correct footnote refs:", + ), + FootNoteReference( + "1", + ), + Paragraph( + Normal, + ",", + ), + FootNoteReference( + "source", + ), + Paragraph( + Normal, + "Here are incorrect ones: [^1, ^1],", + ), + Paragraph( + Normal, + "[^", + ), + Paragraph( + Normal, + "1", + ), + Paragraph( + Normal, + "]", + ), + Paragraph( + Normal, + "So on", + ), + Header { + order: 2, + content: "Object refs", + }, + Paragraph( + Normal, + "These refer to objects like figures, tables and equations.", + ), + Paragraph( + Normal, + "Started by [@.", + ), + Paragraph( + Normal, + "Here's a correct ref:", + ), + Reference( + "fig:experiment_schema", + ), + Paragraph( + Normal, + "Here's are bad ones: [@ fig:1, @ source3],", + ), + Paragraph( + Normal, + "[@", + ), + Paragraph( + Normal, + "eq2", + ), + Paragraph( + Normal, + "]", + ), + Paragraph( + Normal, + "", + ), + Formatted( + Bold, + Paragraph( + Normal, + "NOTE", + ), + ), + Paragraph( + Normal, + ": despite object types being prepended with 'eq', 'fig' or 'tab' in refs, you", + ), + Formatted( + Italic, + Paragraph( + Normal, + "can", + ), + ), + Paragraph( + Normal, + "use regular latex to define your custom refs. In fact, LaTeX doesn't care about all of these at all; I'm doing these transformations just for my own comfort, as I've been doing this manually.", + ), + Header { + order: 1, + content: "Formatting", + }, + Paragraph( + Normal, + "This object's type is deprecated and will be removed soon.", + ), + Paragraph( + Normal, + "It starts and ends with", + ), + List { + list_type: Bullet, + content: Owned( + [ + Paragraph( + Normal, + "* or _", + ), + Paragraph( + Normal, + "** or __", + ), + Paragraph( + Normal, + "~~", + ), + ], + ), + }, + Paragraph( + Normal, + "", + ), + Formatted( + Bold, + Paragraph( + Normal, + "WARN", + ), + ), + Paragraph( + Normal, + ": As of now, these", + ), + Formatted( + Italic, + Paragraph( + Normal, + "might", + ), + ), + Paragraph( + Normal, + "cause cause massive problems if encountered at the start of the line.", + ), + Paragraph( + Normal, + "Basically, parsers for these tokens attempt parse everything up until they find a closing token. But if they are inserted in a middle of the text, then it's ok - they will be limited by current paragraph, and (most likely) will find no match inside it, if their use was unintended.", + ), + Header { + order: 1, + content: "Footnote content", + }, + Paragraph( + Normal, + "These start with '", + ), + FootNoteReference( + "ID", + ), + Paragraph( + Normal, + ":' and proceed to the end of line.", + ), + Paragraph( + Normal, + "Example of valid content:", + ), + FootNoteContent { + content: Paragraph( + Normal, + "Explanation of source2.", + ), + ident: "source2", + }, + Paragraph( + Normal, + "Example of invalid content:", + ), + Paragraph( + Normal, + "[^source2: This is invalid content", + ), + FootNoteReference( + "source2", + ), + Paragraph( + Normal, + ": This is invalid content", + ), + FootNoteReference( + "source2", + ), + Paragraph( + Normal, + ": This is invalid content", + ), + Paragraph( + Normal, + "They also must start at the beginning of the line. If fact, this:", + ), + FootNoteReference( + "source2", + ), + Paragraph( + Normal, + ": Some content", + ), + Paragraph( + Normal, + "Is expected to be a footnote ref with a colon.", + ), + Header { + order: 1, + content: "Code blocks", + }, + Paragraph( + Normal, + "There are two fundamental types of code blocks: normal code block and", + ), + Formatted( + Bold, + Paragraph( + Normal, + "Ayano", + ), + ), + Paragraph( + Normal, + "code block.", + ), + Header { + order: 2, + content: "Normal code block", + }, + Paragraph( + Normal, + "These start and end with '```' on a newline. There can be", + ), + Formatted( + Bold, + Paragraph( + Normal, + "practically anything", + ), + ), + Paragraph( + Normal, + "inside. Example:", + ), + CodeBlock { + code: "int x = 2;\nint y = 4;\nint[] arr = new int[2];\narr[0] = x;\narr[1] = 4;", + language: Some( + "java", + ), + }, + Paragraph( + Normal, + "There can be anything after code block had closed:", + ), + CodeBlock { + code: "local x = 'matt'\nlocal y = 'y'\nlocal z = x .. y -- matty!!", + language: Some( + "lua(Idk why vscode detects this block closing as a valid lua syntax)", + ), + }, + Paragraph( + Normal, + "Here's some text that's ok to be here", + ), + Paragraph( + Normal, + "There can't be any spaces before delimiting ```s:", + ), + Paragraph( + Normal, + "```python", + ), + Paragraph( + Normal, + "# This here is not really a python block", + ), + Paragraph( + Normal, + "x = 2", + ), + Paragraph( + Normal, + "print(x)", + ), + Paragraph( + Normal, + "```", + ), + Header { + order: 2, + content: "Ayano code block", + }, + Paragraph( + Normal, + "", + ), + Formatted( + Italic, + Paragraph( + Normal, + "because Ayano loves Kyoko", + ), + ), + Paragraph( + Normal, + "This object looks and behaves just like normal code block, but is only limited to", + ), + Formatted( + Italic, + Paragraph( + Normal, + "python", + ), + ), + Paragraph( + Normal, + "language, can have spacial arguments in a language line, may leave something behind in the document. For example:", + ), + Paragraph( + Normal, + "This Ayano block will leave plain \"2\" behind it:", + ), + Ayano( + AyanoBlock { + is_display: false, + is_static: false, + code: "2", + insert_path: None, + }, + ), + Paragraph( + Normal, + "Akin to normal blocks, it's ok to leave something trailing:", + ), + CodeBlock { + code: "2", + language: Some( + "ayano", + ), + }, + Paragraph( + Normal, + "This text is ok to be here", + ), + Paragraph( + Normal, + "And these are invalid Ayano blocks:", + ), + CodeBlock { + code: "2", + language: Some( + "ayano", + ), + }, + Paragraph( + Normal, + "```ayano", + ), + Paragraph( + Normal, + "2", + ), + Paragraph( + Normal, + "```", + ), + ], + ), +) \ No newline at end of file diff --git a/tests/lex_files/tables_code.txt b/tests/lex_files/tables_code.txt new file mode 100644 index 0000000..bea8f77 --- /dev/null +++ b/tests/lex_files/tables_code.txt @@ -0,0 +1,279 @@ +Text( + Owned( + [ + Paragraph( + Normal, + "Наступний тест. Тут протестуємо таблиці і блоки коду.", + ), + Header { + order: 2, + content: "Таблиці", + }, + Paragraph( + Normal, + "Звичайні таблиці; Можуть всередині мати", + ), + Formatted( + Bold, + Paragraph( + Normal, + "формули", + ), + ), + Paragraph( + Normal, + "чи", + ), + Formatted( + Italic, + Paragraph( + Normal, + "посилання", + ), + ), + Paragraph( + Normal, + "на інші штуки", + ), + Table { + header: Owned( + [ + Paragraph( + Normal, + "Storage", + ), + Paragraph( + Normal, + "Space", + ), + Paragraph( + Normal, + "Number", + ), + ], + ), + cells: Owned( + [ + Paragraph( + Normal, + "1", + ), + Paragraph( + Normal, + "2", + ), + InlineMathmode( + "3 + e ^{\\pi}", + ), + Text( + Owned( + [ + InlineMathmode( + "e", + ), + FootNoteReference( + "euler", + ), + ], + ), + ), + InlineMathmode( + "\\cdot \\pm 1", + ), + Text( + Owned( + [ + Paragraph( + Normal, + "І тут ще", + ), + Formatted( + Italic, + Paragraph( + Normal, + "трохи", + ), + ), + Paragraph( + Normal, + "тексту \\|", + ), + ], + ), + ), + ], + ), + caption: Some( + Text( + Owned( + [ + Paragraph( + Normal, + "Оце приклад таблиці. Як казав мій вчитель:", + ), + Formatted( + Bold, + Paragraph( + Normal, + "ВИКОРИСТОВУЙ БІЛЬШЕ ВИБУХІВКИ", + ), + ), + Paragraph( + Normal, + "(і \\\"лапок\\\")", + ), + ], + ), + ), + ), + ident: Some( + "example_1", + ), + }, + Paragraph( + Normal, + "Таблиця цілком собі може не мати ідентифікатора або опису:", + ), + Table { + header: Owned( + [ + Paragraph( + Normal, + "Кількість", + ), + Paragraph( + Normal, + "Роль", + ), + ], + ), + cells: Owned( + [ + Paragraph( + Normal, + "3 діоптрії", + ), + Paragraph( + Normal, + "7 галон", + ), + Paragraph( + Normal, + "rust", + ), + Paragraph( + Normal, + "#PANIC!!!", + ), + ], + ), + caption: None, + ident: Some( + "table_2", + ), + }, + FootNoteContent { + content: Text( + Owned( + [ + Paragraph( + Normal, + "Це число Ейлера. Воно дуже корисне,", + ), + Formatted( + StrikeThrough, + Paragraph( + Normal, + "але не може бути записане", + ), + ), + Paragraph( + Normal, + "у вигляді", + ), + Formatted( + Italic, + Paragraph( + Normal, + "десяткового", + ), + ), + Paragraph( + Normal, + "дробу.", + ), + ], + ), + ), + ident: "euler", + }, + Header { + order: 2, + content: "Блоки коду", + }, + Paragraph( + Normal, + "Наразі мають досить обмежену підтримку, адже у", + ), + InlineMathmode( + "\\LaTeX", + ), + Paragraph( + Normal, + "немає нативного поняття блоків коду.", + ), + Paragraph( + Normal, + "На щастя, у", + ), + Formatted( + Bold, + Paragraph( + Normal, + "markdown", + ), + ), + Paragraph( + Normal, + "таке поняття є", + ), + FootNoteReference( + "code", + ), + Paragraph( + Normal, + ":", + ), + CodeBlock { + code: "let x = 5;\nlet y = x.powi(2);\nlet z = (y as f64).sqrt();\nprintln!(\"x = {x}, y = {y}, z = {x}\");", + language: Some( + "rust", + ), + }, + FootNoteContent { + content: Text( + Owned( + [ + Paragraph( + Normal, + "Принаймні,", + ), + Formatted( + Italic, + Paragraph( + Normal, + "Obsidian", + ), + ), + Paragraph( + Normal, + "таке робить", + ), + ], + ), + ), + ident: "code", + }, + ], + ), +) \ No newline at end of file diff --git a/tests/transform.rs b/tests/transform.rs new file mode 100644 index 0000000..fede5c6 --- /dev/null +++ b/tests/transform.rs @@ -0,0 +1,139 @@ +use std::{ffi::OsString, path::PathBuf, str::FromStr}; + +const CLEAR: &'static str = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" ; +const INPUT_DIR: &'static str = "./tests/input_files"; +const TRANSFORM_DIR: &'static str = "./tests/transformed_files"; + +fn input_files() -> impl Iterator { + std::fs::read_dir(INPUT_DIR) + .expect("Should be able to access input directory") + .into_iter() + .map(|file| { + let file_path = file + .expect("Should be able to access files in input directory") + .path(); + let input = + std::fs::read_to_string(&file_path).expect("Should be able to read input files"); + (OsString::from(file_path.file_name().unwrap()), input) + }) +} + +/// Prints input file's lex into the stdout, for inspection +#[ignore = "manual"] +#[test] +fn show_transform() { + for (_, input) in input_files() { + let parsed = match kyomato::lex(input.as_str()) { + Ok(ok) => ok, + Err(err) => { + panic!("Should successfully parse the input: {err:#?}"); + } + }; + let repr = kyomato::gen_to_string(&parsed).expect("Should be able to transform tokens"); + // TODO check what the actual result is + println!("{repr}"); + std::io::stdin() + .read_line(&mut String::new()) + .expect("Should be able to read a line"); + println!("{}", CLEAR); + } +} + +#[ignore = "manual"] +#[test] +fn gen_transform_output() { + use std::{fs::File, io::Write, path::PathBuf, str::FromStr}; + { + use std::io::Read; + print!("Are you sure? This will rewrite previous test outputs: [y/N]: "); + std::io::stdout() + .flush() + .expect("Should be able to flush the stdout"); + + let mut buf = [0u8; 1]; + std::io::stdin() + .read_exact(&mut buf) + .expect("Should be able to read stdin"); + if buf[0] != b'y' { + println!("\nAborting..."); + return; + } + } + let mut output_path = PathBuf::from_str(TRANSFORM_DIR).unwrap(); + if !output_path.is_dir() { + std::fs::create_dir(&output_path).expect("Failed to create directory"); + } + for (filename, input) in input_files() { + let parsed = match kyomato::lex(input.as_str()) { + Ok(ok) => ok, + Err(err) => { + panic!("Input files should be successfully parsed: {err:#?}"); + } + }; + // open output file + let mut output_file = { + output_path.push(filename); + output_path.set_extension("txt"); + let output_file = if output_path.is_file() { + File::options() + .write(true) + .truncate(true) + .open(&output_path) + } else { + File::create(&output_path) + } + .expect("Failed to create output file"); + output_path.pop(); + output_file + }; + // write the result to output file + kyomato::gen(&parsed, &mut output_file).expect("Failed to generate output"); + } +} + +#[test] +fn test_transform() { + let mut output_results = Vec::new(); + let mut output_path = PathBuf::from_str(TRANSFORM_DIR).unwrap(); + if !output_path.is_dir() { + std::fs::create_dir(&output_path).expect("Failed to create directory"); + } + for (filename, input) in input_files() { + let parsed = match kyomato::lex(input.as_str()) { + Ok(ok) => ok, + Err(err) => { + panic!("Should successfully parse the input: {err:#?}"); + } + }; + let repr = kyomato::gen_to_string(&parsed).expect("Failed to generate output"); + // write the result to output directory + let expected_output = { + output_path.push(&filename); + output_path.set_extension("txt"); + let output = + std::fs::read_to_string(&output_path).expect("Failed to read expected output"); + output_path.pop(); + output + }; + output_results.push((filename, repr, expected_output)); + } + let total = output_results.len(); + let success = output_results + .iter() + .filter(|(_, real, expected)| real == expected) + .count(); + let failed = total - success; + if failed > 0 { + let mut output_msg = String::new(); + output_msg.push_str(&format!("passed: ({success}/{total})\nFailures:\n")); + for (filename, real, expected) in output_results { + if real != expected { + output_msg.push_str(&format!( + "\tFilename: {}\n\tActual: {real}\n\tExpected: {expected}\n\n", + filename.to_string_lossy(), + )); + } + } + panic!("{output_msg}"); + } +} diff --git a/tests/transformed_files/basic.txt b/tests/transformed_files/basic.txt new file mode 100644 index 0000000..53f0ee6 --- /dev/null +++ b/tests/transformed_files/basic.txt @@ -0,0 +1,20 @@ + +\subsection{Перша тема} +\fnt Тут трохи тексту і пояснень про квантові мультивироджені рефурбалідзери змішаного типу. Це нудно, тож тримайте формулу: +\begin{equation} +Ref(z) = \int \limits_{0}^{1} t^{z-1} \cdot \exp(-t) dt +\label{eq:refurbalidzer_1}\end{equation} + +\subsubsection{Підпункт} +\fnt Рефурбалідзери можуть виявитись дуже важливими для збереження екологічної безпеки нашої планети. До прикладу, в них є наступні переваги: +\begin{itemize} +\item \fnt Нормувальний множник$|\psi|^{2} = 3/4$ +\item \fnt Кількість хромосом -$\exp(\pi)$ +\item \fnt Поживна цінність - 20 дптр/см +\end{itemize}\begin{figure}[h!] +\centering +\includegraphics[width=0.9\textwidth]{refurb.jpg} +\caption{\fnt Принципове влаштування рефурбалідзера} +\label{fig:refurb} +\end{figure} +\fnt На світлині\ref{fig:refurb}\fnt можна бачити принципову схему влаштування рефурбалідзера. Неважно помітити великі очі\footnotemark[1]\footnotetext[1]{\fnt Для поливу.}\fnt , і два термоядерних реактори, що оточують їх. Точні реакції всередині невідомі, але цілком можливо що там женуть$du + t -> {}^{5}_{6}C$\fnt ... \ No newline at end of file diff --git a/tests/transformed_files/deceit.txt b/tests/transformed_files/deceit.txt new file mode 100644 index 0000000..3ffd8f4 --- /dev/null +++ b/tests/transformed_files/deceit.txt @@ -0,0 +1,89 @@ +\fnt This test is dedicated to attempts of parser deception, i.e. usage of special symbols in wrong places to see if parser recognizes them as control symbols. There we go: +\subsection{Page dividers} +\fnt Page dividers are represented by ------\fnt If the line above has dashes ^^^^^^ here, it probably works alright.\fnt Here's a genuine divider +\clearpage +\fnt Fun fact: there can be a paragraph right after it! +\clearpage +\fnt Some text, cause why not\fnt But this\textbf{\fnt DOES}\fnt look cursed, don't do it. +\subsection{Headers} +\fnt Headers are most recognizable by '#'-symbol. For it to actually represent a header, it needs to at the beginning of the line and there should be no more than 6 of them.\textbf{\fnt ANYTHING}\fnt up to the end of line will be included into a header, then.\fnt Example: +\textbf{This is a long, yet formally correct header. You can add $equations$ and *formatting* here, but I provide no guarantees as of the way these would display in final document.} +\fnt Here's an example of incorrect header:\fnt ###### This header is considered incorrect for this application, and will be parsed as regular paragraph instead\fnt Another example of incorrect header, featuring 7 hash-symbols:\fnt ####### This will be parsed as a regular paragraph +\subsection{Equations} +\fnt There are two types of equations in markdown/latex: inline mathmode and display mathmode\fnt Both of them are defined by $-symbols. However, they must be paired to actually represent these. Here are examples:\fnt Inline:$y = x ^ 2$\fnt Display: +\begin{equation} +y = x ^ 2 +\end{equation} +\fnt Note that display mathmode should ALWAYS start from a new line for this application.\fnt Basically, there's not much to deceive for: you can do that:\fnt $\fnt $\fnt (the above should not be mathmode), and that's it, basically :sad: +\subsection{Table} +\fnt Tables are defined by |-symbol. But the actual table will happen only if they are the first thing on the line.\fnt Due to current implementation, there's no cure apart from display mathmode and code:\fnt (this should be display mathmode): +\begin{equation} +|x| +\end{equation} +\fnt (this should be a code block): +\begin{minted}{bash} +head /dev/random +| cat +\end{minted} + + +\subsection{Figure} +\fnt Figure is defined by a ![[ sequence at a start of the line. Same points should be uphold here: +\begin{equation} +![[ +\end{equation} + +\begin{minted} +![[ +\end{minted} + + +\subsection{References} +\fnt These start with [] +\subsubsection{Hyperrefs} +\fnt These are followed by parentheses (). Nothing is allowed between them, thus\fnt Here's a correct href:\href{https://youtu.be/rickroll_link_lol}{my gh page}\fnt Here are incorrect refs:\fnt [my gh page] (https://youtu.be/rickroll\textit{\fnt link}\fnt lol)\fnt [my gh page]\fnt (https://youtu.be/rickroll\textit{\fnt link}\fnt lol) +\subsubsection{Footnote refs} +\fnt These are started by [^.\fnt Here are correct footnote refs:\footnotemark[1]\fnt ,\footnotemark[2]\fnt Here are incorrect ones: [^1, ^1],\fnt [^\fnt 1\fnt ]\fnt So on +\subsubsection{Object refs} +\fnt These refer to objects like figures, tables and equations.\fnt Started by [@.\fnt Here's a correct ref:\ref{fig:experiment_schema}\fnt Here's are bad ones: [@ fig:1, @ source3],\fnt [@\fnt eq2\fnt ]\fnt \textbf{\fnt NOTE}\fnt : despite object types being prepended with 'eq', 'fig' or 'tab' in refs, you\textit{\fnt can}\fnt use regular latex to define your custom refs. In fact, LaTeX doesn't care about all of these at all; I'm doing these transformations just for my own comfort, as I've been doing this manually. +\subsection{Formatting} +\fnt This object's type is deprecated and will be removed soon.\fnt It starts and ends with +\begin{itemize} +\item \fnt * or _ +\item \fnt ** or __ +\item \fnt ~~ +\end{itemize}\fnt \textbf{\fnt WARN}\fnt : As of now, these\textit{\fnt might}\fnt cause cause massive problems if encountered at the start of the line.\fnt Basically, parsers for these tokens attempt parse everything up until they find a closing token. But if they are inserted in a middle of the text, then it's ok - they will be limited by current paragraph, and (most likely) will find no match inside it, if their use was unintended. +\subsection{Footnote content} +\fnt These start with '\footnotemark[3]\fnt :' and proceed to the end of line.\fnt Example of valid content:\fnt Example of invalid content:\fnt [^source2: This is invalid content\footnotemark[4]\footnotetext[4]{\fnt Explanation of source2.}\fnt : This is invalid content\footnotemark[4]\fnt : This is invalid content\fnt They also must start at the beginning of the line. If fact, this:\footnotemark[4]\fnt : Some content\fnt Is expected to be a footnote ref with a colon. +\subsection{Code blocks} +\fnt There are two fundamental types of code blocks: normal code block and\textbf{\fnt Ayano}\fnt code block. +\subsubsection{Normal code block} +\fnt These start and end with '```' on a newline. There can be\textbf{\fnt practically anything}\fnt inside. Example: +\begin{minted}{java} +int x = 2; +int y = 4; +int[] arr = new int[2]; +arr[0] = x; +arr[1] = 4; +\end{minted} + +\fnt There can be anything after code block had closed: +\begin{minted}{lua(Idk why vscode detects this block closing as a valid lua syntax)} +local x = 'matt' +local y = 'y' +local z = x .. y -- matty!! +\end{minted} + +\fnt Here's some text that's ok to be here\fnt There can't be any spaces before delimiting ```s:\fnt ```python\fnt # This here is not really a python block\fnt x = 2\fnt print(x)\fnt ``` +\subsubsection{Ayano code block} +\fnt \textit{\fnt because Ayano loves Kyoko}\fnt This object looks and behaves just like normal code block, but is only limited to\textit{\fnt python}\fnt language, can have spacial arguments in a language line, may leave something behind in the document. For example:\fnt This Ayano block will leave plain "2" behind it:\fnt 2\fnt Akin to normal blocks, it's ok to leave something trailing: +\begin{minted}{ayano} +2 +\end{minted} + +\fnt This text is ok to be here\fnt And these are invalid Ayano blocks: +\begin{minted}{ayano} +2 +\end{minted} + +\fnt ```ayano\fnt 2\fnt ``` \ No newline at end of file diff --git a/tests/transformed_files/tables_code.txt b/tests/transformed_files/tables_code.txt new file mode 100644 index 0000000..575985b --- /dev/null +++ b/tests/transformed_files/tables_code.txt @@ -0,0 +1,38 @@ +\fnt Наступний тест. Тут протестуємо таблиці і блоки коду. +\subsubsection{Таблиці} +\fnt Звичайні таблиці; Можуть всередині мати\textbf{\fnt формули}\fnt чи\textit{\fnt посилання}\fnt на інші штуки +\begin{table}[h!] +\begin{center} +\begin{tabular}{|c|c|c|} +\fnt Storage & \fnt Space & \fnt Number\\ \hline +\hline +\fnt 1 & \fnt 2 & $3 + e ^{\pi}$\\ \hline +$e$\footnotemark[1]\footnotetext[1]{\fnt Це число Ейлера. Воно дуже корисне,\st{\fnt але не може бути записане}\fnt у вигляді\textit{\fnt десяткового}\fnt дробу.} & $\cdot \pm 1$ & \fnt І тут ще\textit{\fnt трохи}\fnt тексту \|\\ \hline +\end{tabular} +\stepcounter{tabnum} +\caption{\fnt Оце приклад таблиці. Як казав мій вчитель:\textbf{\fnt ВИКОРИСТОВУЙ БІЛЬШЕ ВИБУХІВКИ}\fnt (і \"лапок\")} +\label{tab:example_1} +\end{center} +\end{table} +\fnt Таблиця цілком собі може не мати ідентифікатора або опису: +\begin{table}[h!] +\begin{center} +\begin{tabular}{|c|c|} +\fnt Кількість & \fnt Роль\\ \hline +\hline +\fnt 3 діоптрії & \fnt 7 галон\\ \hline +\fnt rust & \fnt #PANIC!!!\\ \hline +\end{tabular} +\stepcounter{tabnum}\label{tab:table_2} +\end{center} +\end{table} + +\subsubsection{Блоки коду} +\fnt Наразі мають досить обмежену підтримку, адже у$\LaTeX$\fnt немає нативного поняття блоків коду.\fnt На щастя, у\textbf{\fnt markdown}\fnt таке поняття є\footnotemark[2]\footnotetext[2]{\fnt Принаймні,\textit{\fnt Obsidian}\fnt таке робить}\fnt : +\begin{minted}{rust} +let x = 5; +let y = x.powi(2); +let z = (y as f64).sqrt(); +println!("x = {x}, y = {y}, z = {x}"); +\end{minted} + From 21c74c3e680a75e081d9412ce254b9df368f74e9 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Sun, 17 Mar 2024 16:14:46 +0200 Subject: [PATCH 17/98] Reworked PathEngine --- src/gen/lab/mod.rs | 24 ++- src/gen/mod.rs | 7 +- src/lib.rs | 4 +- src/path_engine.rs | 476 +++++++++++++++++++++++++++++---------------- src/util.rs | 151 +------------- tests/lex.rs | 1 - tests/transform.rs | 1 - 7 files changed, 328 insertions(+), 336 deletions(-) diff --git a/src/gen/lab/mod.rs b/src/gen/lab/mod.rs index daebb70..37add5f 100644 --- a/src/gen/lab/mod.rs +++ b/src/gen/lab/mod.rs @@ -12,12 +12,12 @@ use std::{borrow::Borrow, fmt::Write, marker::PhantomData}; use super::{ayano::AyanoExecutor, GenerationError, OutputGenerator, Res}; #[derive(Debug)] -pub struct LabaLatex<'source, PE> { +pub struct LabaLatex<'source, PE, PEErr> { path_engine: PE, - _phantom: PhantomData<&'source ()>, + _phantom: PhantomData<(&'source (), PEErr)>, } -impl<'source, PE: PathEngine> LabaLatex<'source, PE> { +impl<'source, PE: PathEngine, PEErr> LabaLatex<'source, PE, PEErr> { pub fn new(path_engine: PE) -> Self { Self { path_engine, @@ -34,6 +34,8 @@ impl<'source, PE: PathEngine> LabaLatex<'source, PE> { ) -> Res<'source> where 'source: 'meta + 'context + 'token, + PE: PathEngine, + PEErr: std::error::Error + 'source, { let mut cells = cells.into_iter(); let Some(first_cell) = cells.next() else { @@ -50,8 +52,11 @@ impl<'source, PE: PathEngine> LabaLatex<'source, PE> { } } -impl<'source, PE: PathEngine> OutputGenerator<'source, SourceMeta<'source, AyanoExecutor>, Context> - for LabaLatex<'source, PE> +impl<'source, PE, PEErr> OutputGenerator<'source, SourceMeta<'source, AyanoExecutor>, Context> + for LabaLatex<'source, PE, PEErr> +where + PE: PathEngine, + PEErr: std::error::Error + 'source, { fn write_to<'meta, 'context, 'token, W: Write + ?Sized>( &self, @@ -140,7 +145,10 @@ impl<'source, PE: PathEngine> OutputGenerator<'source, SourceMeta<'source, Ayano output.write_str( "\\begin{figure}[h!]\n\\centering\n\\includegraphics[width=0.9\\textwidth]{", )?; - let src_path = self.path_engine.image(&src_name, None)?; + let src_path = self + .path_engine + .image(src_name.as_ref(), None) + .map_err(|err| GenerationError::Path(Box::new(err)))?; output.write_str(src_path.to_string_lossy().borrow())?; // FIXME non-utf paths *might* cause an issue here output.write_str("}\n")?; if let Some(caption) = caption.as_ref() { @@ -313,7 +321,7 @@ mod tests { {$name:ident, $token:expr, $b:expr} => { #[test] fn $name() { - use $crate::path_engine::PrimitiveEngine; + use $crate::path_engine::primitive as primitive_engine; // arrange let token = $token; let meta = SourceMeta::collect(&token) @@ -321,7 +329,7 @@ mod tests { .init_ayano() .expect("Should be able to init Ayano"); - let generator = LabaLatex::new(&PrimitiveEngine); + let generator = LabaLatex::new(primitive_engine()); let mut output_target = String::new(); // act diff --git a/src/gen/mod.rs b/src/gen/mod.rs index c589705..645a50b 100644 --- a/src/gen/mod.rs +++ b/src/gen/mod.rs @@ -5,10 +5,7 @@ use std::fmt::Write; use itertools::Itertools; -use crate::{ - data::{Token, Tokens}, - path_engine, -}; +use crate::data::{Token, Tokens}; #[derive(Debug, derive_more::From, thiserror::Error)] pub enum GenerationError<'source> { @@ -17,7 +14,7 @@ pub enum GenerationError<'source> { #[error("{}", .0)] Ayano(ayano::AyanoError<'source>), #[error("{}", .0)] - Path(path_engine::Error), + Path(Box), } pub type Res<'source> = Result<(), GenerationError<'source>>; diff --git a/src/lib.rs b/src/lib.rs index 539015e..1cd8987 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,8 +27,6 @@ //! Well, unless there's a way to efficiently write to a single output from multiple threads, while preserving output order. //! Any sort of "collect" function would've destroyed the purpose anyway. -use std::fmt::Write; - use gen::OutputGenerator; use lexer::KyomatoLexError; @@ -59,7 +57,7 @@ pub fn gen<'token, 'source: 'token>( pub fn gen_to_string<'token, 'source: 'token>( token: &'token data::Token<'source>, ) -> Result> { - let generator = gen::lab::LabaLatex::new(&path_engine::PrimitiveEngine); + let generator = gen::lab::LabaLatex::new(path_engine::primitive()); let meta = gen::lab::SourceMeta::collect(&token)?.init_ayano()?; let mut buf = String::new(); generator.write_to(&mut buf, &meta, &mut gen::lab::Context::default(), token)?; diff --git a/src/path_engine.rs b/src/path_engine.rs index aae738d..a6a1ff2 100644 --- a/src/path_engine.rs +++ b/src/path_engine.rs @@ -1,8 +1,155 @@ -use std::path::{Path, PathBuf}; +use std::{ + borrow::Cow, + error::Error, + path::{Path, PathBuf}, +}; +/// Represents a context of occurred error +#[derive(Debug, Clone, Copy)] +pub enum ErrorContext { + Source, + Ayano, + Specific, + Execution, + None, +} + +pub trait PathEngineError: Error { + fn from_generic(error: Box, context: ErrorContext) -> Self; + fn append(&mut self, other: Box, context: ErrorContext); +} + +/// An error type with no state - It's only mission is to report an error happening +#[derive(Debug, thiserror::Error)] +#[error("Could not resolve a path")] +pub struct MarkerError; + +impl PathEngineError for MarkerError { + fn append(&mut self, _other: Box, _context: ErrorContext) {} + + fn from_generic(error: Box, context: ErrorContext) -> Self { + MarkerError + } +} + +/// An error holding rich information about reason resolution had failed +#[derive(Debug, thiserror::Error, Default)] +#[error("{:?}", self)] +pub struct InformativeError { + source: Option>, + ayano: Option>, + specific: Option>, + execution: Option>, + none: Option>, +} + +impl PathEngineError for InformativeError { + fn append(&mut self, other: Box, context: ErrorContext) { + match context { + ErrorContext::Source => self.source.insert(other), + ErrorContext::Ayano => self.ayano.insert(other), + ErrorContext::Specific => self.specific.insert(other), + ErrorContext::Execution => self.execution.insert(other), + ErrorContext::None => self.none.insert(other), + }; + } + + fn from_generic(error: Box, context: ErrorContext) -> Self { + let mut this = Self::default(); + this.append(error, context); + this + } +} + +type CheckType = fn(&Path) -> Result<(), Box>; + +/// A trait representing a `thing` that controls file path finding for `OutputGenerator`. +/// +/// You can implement it, but you are not supposed to. There are composition methods to build one yourself along with predefined presets. +pub trait PathEngine { + fn image<'p>( + &self, + path: impl Into>, + ayano_insert: Option<&Path>, + ) -> Result, E>; + fn csv<'p>( + &self, + path: impl Into>, + ayano_insert: Option<&Path>, + ) -> Result, E>; + fn ayano_insert<'p>(&self, path: impl Into>) -> Result, E>; + fn set_check(&mut self, new_check: CheckType); + fn get_check(&self) -> CheckType; +} + +pub trait PathEngineExt: PathEngine + Sized { + fn with_ayano(self) -> AyanoRespecting { + AyanoRespecting(self) + } + + fn with_image_folder(self, folder: impl Into) -> WithImageFolder { + WithImageFolder(self, folder.into()) + } + + fn with_execution_folder(self, folder: impl Into) -> WithExecutionFolder { + WithExecutionFolder(self, folder.into()) + } + + fn with_check(mut self, new_check: CheckType) -> impl PathEngine { + self.set_check(new_check); + self + } + + fn with_error_type(self) -> impl PathEngine + where + Self: PathEngine, + { + self + } +} + +/// A primitive path engine, that returns, well, exact paths it's given. It literally does nothing. +pub struct Engine(CheckType); +impl PathEngine for Engine { + fn image<'p>( + &self, + path: impl Into>, + ayano_insert: Option<&Path>, + ) -> Result, E> { + let path = path.into(); + let _ = (self.0)(&path).map_err(|err| E::from_generic(err, ErrorContext::None))?; + Ok(path) + } + + fn csv<'p>( + &self, + path: impl Into>, + ayano_insert: Option<&Path>, + ) -> Result, E> { + let path = path.into(); + let _ = (self.0)(&path).map_err(|err| E::from_generic(err, ErrorContext::None))?; + Ok(path) + } + + fn ayano_insert<'p>(&self, path: impl Into>) -> Result, E> { + let path = path.into(); + let _ = (self.0)(&path).map_err(|err| E::from_generic(err, ErrorContext::None))?; + Ok(path) + } + + fn set_check(&mut self, new_check: CheckType) { + self.0 = new_check; + } + + fn get_check(&self) -> CheckType { + self.0 + } +} + +/// Represents various reasons file path may not be correct #[derive(Debug, derive_more::From, thiserror::Error)] pub enum VariantError { - #[error("{}", .0)] + #[error(transparent)] Io(std::io::Error), // missing permissions, or broken symbolic links #[error("file does not exist")] NotExist, // file does not in fact exist in the filesystem @@ -10,188 +157,177 @@ pub enum VariantError { NotAFile, // path resolves to a directory, not a file } -#[derive(Debug, thiserror::Error)] -#[error("Failed to resolve path: {:?}", self)] -pub struct Error { - source: Option, // error resolving path relative to the target file (if present) - ayano: Option, // error resolving relative to ayano-inserted code - specific: Option, // error of an option specific to some method - execution: VariantError, // error of path resolution relative to execution directory +/// A path-checking function always positive about path being correct +fn no_check(path: &Path) -> Result<(), Box> { + Ok(()) } -// TODO make this thing modular (probably use tower or th) -pub trait PathEngine { - fn image(&self, path: &Path, ayano_insert: Option<&Path>) -> Result; - fn csv(&self, path: &Path, ayano_insert: Option<&Path>) -> Result; - fn ayano_insert(&self, path: &Path) -> Result; -} - -pub use default::Engine as DefaultEngine; -pub use Error as DefaultError; - -mod default { - use std::path::{Path, PathBuf}; - - use super::{Error, PathEngine}; - - #[derive(Debug)] - pub struct Engine { - source_folder: Option, - image_folder: Option, - execution_folder: PathBuf, - } - - macro_rules! try_path { - ($path:ident) => { - if $path.try_exists().is_ok_and(std::convert::identity) - // path exists and is reachable - && $path.is_file() - // and path points to the file - { - // found a match! - return Ok($path); - } - }; - } - - macro_rules! op_path { - ($path:ident) => { - $path = if let Some(path) = $path { - try_path!(path); - Some(path) +/// A path-checking function actually checking if file exists via normal means +fn fs_check(path: &Path) -> Result<(), Box> { + match path.try_exists() { + Ok(true) => { + if path.is_file() { + Ok(()) } else { - None + Err(Box::new(VariantError::NotAFile)) } - }; + } + Err(io) => Err(Box::new(VariantError::Io(io))), + Ok(false) => Err(Box::new(VariantError::NotExist)), } +} - macro_rules! path_error { - ($path:ident) => { - match $path.try_exists() { - Err(io) => super::VariantError::Io(io), - Ok(false) => super::VariantError::NotExist, - Ok(true) => { - if !$path.is_file() { - super::VariantError::NotAFile - } else { - unreachable!("These should be all options for possible path errors. If you can see this, it's a oversight, please contact me.") - } +macro_rules! try_variant { + ($this_path:expr, $this_variant:ident, $self:ident, $method:ident, $path:ident $(, $arg:tt)*) => {{ + let mut path = $path.into(); + // first, try this path + let this_error = if let Some(this_path) = $this_path.as_ref() { + let this_guess = this_path.to_owned().join(&path); + let Err(err) = $self.get_check()(&this_guess) else { + return Ok(Cow::Owned(this_guess)); + }; + Some(err) + } else { + None + }; + // Then, attempt to delegate deeper + match $self.0.$method(path $(, $arg)*) { + // if inner engine is successful, return it's result + Ok(res) => Ok(res), + // if it's not -- append this error to it's error and propagate + Err(mut err) => { + if let Some(this_error) = this_error { + err.append(this_error, ErrorContext::$this_variant); } + Err(err) } - }; - } - - impl Engine { - pub fn create( - target_folder: impl Into>, - image_folder: impl Into>, - ) -> std::io::Result { - use std::env::current_dir as cd; - Ok(Self { - source_folder: target_folder.into(), - image_folder: image_folder.into(), - execution_folder: cd()?, - }) } - } - - impl<'l> PathEngine for &'l Engine { - fn image(&self, path: &Path, ayano_insert: Option<&Path>) -> Result { - // first, try resolving in ayano-inserted folder - let mut ayano_attempt = ayano_insert.as_ref().map(|p| p.join(path)); - op_path!(ayano_attempt); - - // next, try resolving in a dedicated image folder - let mut image_attempt = self.image_folder.as_ref().map(|p| p.join(path)); - op_path!(image_attempt); - - // next, try resolving in the source folder - let mut source_attempt = self.source_folder.as_ref().map(|p| p.join(path)); - op_path!(source_attempt); - - // next, try resolving relative to execution folder - let execution_attempt = self.execution_folder.join(path); - try_path!(execution_attempt); - - // well, all options failed. not let's construct informative error: - Err(Error { - execution: path_error!(execution_attempt), - ayano: ayano_attempt.map(|p| path_error!(p)), - source: source_attempt.map(|p| path_error!(p)), - specific: image_attempt.map(|p| path_error!(p)), - }) - } - - fn csv(&self, path: &Path, ayano_insert: Option<&Path>) -> Result { - // first, try resolving in ayano-inserted folder - let mut ayano_attempt = ayano_insert.as_ref().map(|p| p.join(path)); - op_path!(ayano_attempt); - - // next, try resolving in the source folder - let mut source_attempt = self.source_folder.as_ref().map(|p| p.join(path)); - op_path!(source_attempt); - - // next, try resolving relative to execution folder - let execution_attempt = self.execution_folder.join(path); - try_path!(execution_attempt); - - // well, all options failed. not let's construct informative error: - Err(Error { - execution: path_error!(execution_attempt), - ayano: ayano_attempt.map(|p| path_error!(p)), - source: source_attempt.map(|p| path_error!(p)), - specific: None, - }) - } - - fn ayano_insert(&self, path: &Path) -> Result { - // first, try resolving in the source folder - let mut source_attempt = self.source_folder.as_ref().map(|p| p.join(path)); - op_path!(source_attempt); - - // next, try resolving relative to execution folder (TODO should O remove that?) - let execution_attempt = self.execution_folder.join(path); - try_path!(execution_attempt); - - // well, all options failed. not let's construct informative error: - Err(Error { - execution: path_error!(execution_attempt), - ayano: None, - source: source_attempt.map(|p| path_error!(p)), - specific: None, - }) - } - } + }}; } -pub use primitive::Engine as PrimitiveEngine; -mod primitive { - use super::PathEngine; - - pub struct Engine; - - impl PathEngine for &Engine { - fn image( - &self, - path: &std::path::Path, - _: Option<&std::path::Path>, - ) -> Result { - Ok(path.to_owned()) +macro_rules! impl_check_delegate { + () => { + fn set_check(&mut self, new_check: CheckType) { + self.0.set_check(new_check) } - fn csv( - &self, - path: &std::path::Path, - _: Option<&std::path::Path>, - ) -> Result { - Ok(path.to_owned()) + fn get_check(&self) -> CheckType { + self.0.get_check() } + }; +} +pub struct AyanoRespecting(PE); +impl> PathEngine for AyanoRespecting { + fn image<'p>( + &self, + path: impl Into>, + ayano_insert: Option<&Path>, + ) -> Result, E> { + try_variant!(ayano_insert, Ayano, self, image, path, ayano_insert) + } + + fn csv<'p>( + &self, + path: impl Into>, + ayano_insert: Option<&Path>, + ) -> Result, E> { + try_variant!(ayano_insert, Ayano, self, csv, path, ayano_insert) + } + + fn ayano_insert<'p>(&self, path: impl Into>) -> Result, E> { + self.0.ayano_insert(path) + } + + impl_check_delegate! {} +} + +pub struct WithImageFolder(PE, PathBuf); +impl> PathEngine for WithImageFolder { + fn image<'p>( + &self, + path: impl Into>, + ayano_insert: Option<&Path>, + ) -> Result, E> { + try_variant!(Some(&self.1), Specific, self, image, path, ayano_insert) + } + + fn csv<'p>( + &self, + path: impl Into>, + ayano_insert: Option<&Path>, + ) -> Result, E> { + self.0.csv(path, ayano_insert) + } + + fn ayano_insert<'p>(&self, path: impl Into>) -> Result, E> { + self.0.ayano_insert(path) + } + + impl_check_delegate! {} +} - fn ayano_insert( - &self, - path: &std::path::Path, - ) -> Result { - Ok(path.to_owned()) - } +pub struct WithSourceFolder(PE, PathBuf); +impl> PathEngine for WithSourceFolder { + fn image<'p>( + &self, + path: impl Into>, + ayano_insert: Option<&Path>, + ) -> Result, E> { + try_variant!(Some(&self.1), Source, self, image, path, ayano_insert) } + + fn csv<'p>( + &self, + path: impl Into>, + ayano_insert: Option<&Path>, + ) -> Result, E> { + try_variant!(Some(&self.1), Source, self, csv, path, ayano_insert) + } + + fn ayano_insert<'p>(&self, path: impl Into>) -> Result, E> { + try_variant!(Some(&self.1), Source, self, ayano_insert, path) + } + impl_check_delegate! {} +} + +pub struct WithExecutionFolder(PE, PathBuf); +impl> PathEngine for WithExecutionFolder { + fn image<'p>( + &self, + path: impl Into>, + ayano_insert: Option<&Path>, + ) -> Result, E> { + try_variant!(Some(&self.1), Source, self, image, path, ayano_insert) + } + + fn csv<'p>( + &self, + path: impl Into>, + ayano_insert: Option<&Path>, + ) -> Result, E> { + try_variant!(Some(&self.1), Source, self, csv, path, ayano_insert) + } + + fn ayano_insert<'p>(&self, path: impl Into>) -> Result, E> { + try_variant!(Some(&self.1), Source, self, ayano_insert, path) + } + impl_check_delegate! {} +} + +/// A basic engine +/// +/// Generic method. Rust will most likely complain about type inference if you use it directly; +/// Make sure to add at least one `with_th_error`. +pub fn base_gen() -> impl PathEngine { + Engine(no_check) +} + +/// The most primitive engine setup, performs no checks and has no special folders +pub fn primitive() -> impl PathEngine { + base_gen() +} + +/// A primitive engine, but with informative error and `fs_check` +pub fn primitive_dbg() -> impl PathEngine { + base_gen() } diff --git a/src/util.rs b/src/util.rs index 033b95d..bb8c077 100644 --- a/src/util.rs +++ b/src/util.rs @@ -155,7 +155,7 @@ impl Eq for Equivalent {} #[derive(derive_more::From)] #[repr(transparent)] #[derive(Debug, derive_more::Deref)] -// TODO REMOVE THAT, IT VERY-VERY BAD SEMANTICALLY!!!! +// XXX REMOVE THAT, IT VERY-VERY BAD SEMANTICALLY!!!! pub struct Immutable(T); #[derive(Debug)] @@ -306,9 +306,8 @@ pub trait IteratorArrayCollectExt: Sized { fn collect_array(self) -> Result<[T; N], ArrayCollectError>; } -// FIXME memory leaks in case of panic +// [ ] memory leaks in case of panic impl> IteratorArrayCollectExt for I { - // TODO ADD TESTS FOR THIS, ESPECIALLY FOR THE CASE OF VALUE DROPPING fn collect_array(mut self) -> Result<[T; N], ArrayCollectError> { { // Ok, so unsafe is used here, and I am really scared of it. But I couldn't find any alternative, really @@ -350,148 +349,4 @@ impl VecTryExtendExt for Vec { } Ok(()) } -} - -macro_rules! fn_ref { - ($input:expr) => {{ - fn focus_pokus_amogus(input: fn(I) -> O) -> fn(I) -> O { - input - } - focus_pokus_amogus(|i| $input(&i)) - }}; -} -macro_rules! fn_mut { - ($input:expr) => {{ - fn focus_pokus_amogus(input: fn(I) -> O) -> fn(I) -> O { - input - } - focus_pokus_amogus(|mut i| $input(&mut i)) - }}; -} - -macro_rules! fn_chain { - ($fn1:expr, $fn2:expr) => {{ - fn focus_pokus_amogus(input: fn(I) -> O) -> fn(I) -> O { - input - } - focus_pokus_amogus(|input| $fn2($fn1(input))) - }}; -} - -#[cfg(test)] -mod fn_ref_tests { - #[test] - fn works() { - // act - let _ref_pointer = fn_ref!(String::len); - let _mut_pointer = fn_mut!(Vec::::pop); - let _chain_pointer = fn_chain!(fn_mut!(Vec::::pop), Option::unwrap); - } -} - -struct MergeCorr { - i1: I1, - i2: I2, - buf1: Vec, - buf2: Vec, - correlation: Corr, -} - -fn merge_corr( - i1: I1, - i2: I2, - correlation: Corr, -) -> impl Iterator -where - I1: Iterator, - I2: Iterator, - Corr: FnMut(&It1, &It2) -> bool, -{ - MergeCorr { - i1, - i2, - buf1: Vec::new(), - buf2: Vec::new(), - correlation, - } -} - -impl MergeCorr -where - I1: Iterator, - I2: Iterator, - Corr: FnMut(&It1, &It2) -> bool, -{ - fn next12(&mut self) -> Option<(It1, It2)> { - for i in 0..self.buf1.len() { - for j in 0..self.buf2.len() { - if (&mut self.correlation)(&self.buf1[i], &self.buf2[j]) { - return Some((self.buf1.swap_remove(i), self.buf2.swap_remove(j))); - } - } - } - None - } - - fn next1(&mut self) -> Result<(It1, It2), bool> { - // First, try getting next element from the iterator - if let Some(e1) = self.i1.next() { - // If succeeded, try finding correspondence among already present `buf2` elements - if let Some(ind2) = self - .buf2 - .iter() - .position(|e2| (&mut self.correlation)(&e1, e2)) - { - // If succeeded, return the result - let e2 = self.buf2.swap_remove(ind2); - return Ok((e1, e2)); - } - // There's no correspondence already present. Add this element to buf1 - self.buf1.push(e1); - } - self.next12().ok_or(true) - } - fn next2(&mut self) -> Result<(It1, It2), bool> { - // First, try getting next element from the iterator - if let Some(e2) = self.i2.next() { - // If succeeded, try finding correspondence among already present `buf2` elements - if let Some(ind1) = self - .buf1 - .iter() - .position(|e1| (&mut self.correlation)(e1, &e2)) - { - // If succeeded, return the result - let e1 = self.buf1.swap_remove(ind1); - return Ok((e1, e2)); - } - // There's no correspondence already present. Add this element to buf1 - self.buf2.push(e2); - } - self.next12().ok_or(true) - } -} - -impl Iterator for MergeCorr -where - I1: Iterator, - I2: Iterator, - Corr: FnMut(&It1, &It2) -> bool, -{ - type Item = (It1, It2); - - fn next(&mut self) -> Option { - let mut try1 = true; - let mut try2 = true; - while try1 || try2 { - match self.next1() { - Ok(res) => return Some(res), - Err(e) => try1 = e, - } - match self.next2() { - Ok(res) => return Some(res), - Err(e) => try2 = e, - } - } - None - } -} +} \ No newline at end of file diff --git a/tests/lex.rs b/tests/lex.rs index 6ddc54d..96ae880 100644 --- a/tests/lex.rs +++ b/tests/lex.rs @@ -29,7 +29,6 @@ fn show_lex() { panic!("Should successfully parse the input: {err:#?}"); } }; - // TODO check what the actual result is println!("{parsed:#?}"); std::io::stdin() .read_line(&mut String::new()) diff --git a/tests/transform.rs b/tests/transform.rs index fede5c6..7eab252 100644 --- a/tests/transform.rs +++ b/tests/transform.rs @@ -30,7 +30,6 @@ fn show_transform() { } }; let repr = kyomato::gen_to_string(&parsed).expect("Should be able to transform tokens"); - // TODO check what the actual result is println!("{repr}"); std::io::stdin() .read_line(&mut String::new()) From d0dfb3f935248c60d47d6a0a2daf7a6d0befd8fd Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Sun, 17 Mar 2024 18:12:55 +0200 Subject: [PATCH 18/98] added tests for `PathEngine` - well, that was painful (and I didn't like that) --- src/path_engine.rs | 194 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 185 insertions(+), 9 deletions(-) diff --git a/src/path_engine.rs b/src/path_engine.rs index a6a1ff2..9fb7dd2 100644 --- a/src/path_engine.rs +++ b/src/path_engine.rs @@ -20,7 +20,7 @@ pub trait PathEngineError: Error { } /// An error type with no state - It's only mission is to report an error happening -#[derive(Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error, PartialEq)] #[error("Could not resolve a path")] pub struct MarkerError; @@ -61,6 +61,13 @@ impl PathEngineError for InformativeError { } } +#[cfg(test)] +impl PartialEq for InformativeError { + fn eq(&self, other: &Self) -> bool { + format!("{self:?}") == format!("{other:?}") + } +} + type CheckType = fn(&Path) -> Result<(), Box>; /// A trait representing a `thing` that controls file path finding for `OutputGenerator`. @@ -95,12 +102,16 @@ pub trait PathEngineExt: PathEngine + Sized { WithExecutionFolder(self, folder.into()) } - fn with_check(mut self, new_check: CheckType) -> impl PathEngine { + fn with_source_folder(self, folder: impl Into) -> WithSourceFolder { + WithSourceFolder(self, folder.into()) + } + + fn with_check(mut self, new_check: CheckType) -> impl PathEngine + Sized { self.set_check(new_check); self } - fn with_error_type(self) -> impl PathEngine + fn with_error_type(self) -> impl PathEngine + Sized where Self: PathEngine, { @@ -108,6 +119,8 @@ pub trait PathEngineExt: PathEngine + Sized { } } +impl PathEngineExt for PE where PE: PathEngine {} + /// A primitive path engine, that returns, well, exact paths it's given. It literally does nothing. pub struct Engine(CheckType); impl PathEngine for Engine { @@ -297,7 +310,7 @@ impl> PathEngine for WithExecutionFolde path: impl Into>, ayano_insert: Option<&Path>, ) -> Result, E> { - try_variant!(Some(&self.1), Source, self, image, path, ayano_insert) + try_variant!(Some(&self.1), Execution, self, image, path, ayano_insert) } fn csv<'p>( @@ -305,11 +318,11 @@ impl> PathEngine for WithExecutionFolde path: impl Into>, ayano_insert: Option<&Path>, ) -> Result, E> { - try_variant!(Some(&self.1), Source, self, csv, path, ayano_insert) + try_variant!(Some(&self.1), Execution, self, csv, path, ayano_insert) } fn ayano_insert<'p>(&self, path: impl Into>) -> Result, E> { - try_variant!(Some(&self.1), Source, self, ayano_insert, path) + try_variant!(Some(&self.1), Execution, self, ayano_insert, path) } impl_check_delegate! {} } @@ -318,16 +331,179 @@ impl> PathEngine for WithExecutionFolde /// /// Generic method. Rust will most likely complain about type inference if you use it directly; /// Make sure to add at least one `with_th_error`. -pub fn base_gen() -> impl PathEngine { +pub fn base_gen() -> impl PathEngine + Sized { Engine(no_check) } /// The most primitive engine setup, performs no checks and has no special folders -pub fn primitive() -> impl PathEngine { +pub fn primitive() -> impl PathEngine + Sized { base_gen() } /// A primitive engine, but with informative error and `fs_check` -pub fn primitive_dbg() -> impl PathEngine { +pub fn primitive_dbg() -> impl PathEngine + Sized { base_gen() } + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + + #[derive(Debug, thiserror::Error)] + #[error("A path {} does not exist", .0.to_string_lossy())] + struct DontExist(PathBuf); + + macro_rules! test_check { + ($($existing_path:literal), *) => {{ + fn test_check(path: &Path) -> Result<(), Box> { + const EXISTING_PATHS: &[&str] = &[$($existing_path), *]; // elided array sizes are unstable :( + if EXISTING_PATHS.contains(&&*path.to_string_lossy()) { + Ok(()) + } else { + Err(Box::new(DontExist(path.to_owned()))) + } + } + test_check + }}; + } + + macro_rules! test_ok { + {$name:ident, $engine:expr, $method:ident, $test_path:literal, $ayano_path:expr, $expected_res:expr} => { + test_ok!{$name, $engine.$method(Path::new($test_path), $ayano_path.map(|p: &str| Path::new(p).into())), $expected_res} + }; + {$name:ident, $engine:expr, $test_path:literal, $expected_res:expr} => { + test_ok!{$name, $engine.ayano_insert(Path::new($test_path)), $expected_res} + }; + {$name:ident, $call_expr:expr, $expected_res:expr} => { + #[test] + fn $name() { + // arrange + + // act + let engine_result = $call_expr.expect("Should be ok"); + + // assert + assert_eq!(Path::new($expected_res), engine_result, "Path engine should produce expected result"); + } + }; + } + + macro_rules! test_expr { + {$name:ident, $engine:expr, $method:ident, $test_path:literal, $ayano_path:expr, $expected_res:expr} => { + test_expr!{$name, $engine.$method(Path::new($test_path), $ayano_path.map(|p: &str| Path::new(p).into())), $expected_res} + }; + {$name:ident, $engine:expr, $test_path:literal, $expected_res:expr} => { + test_expr!{$name, $engine.ayano_insert(Path::new($test_path)), $expected_res} + }; + {$name:ident, $call_expr:expr, $expected_res:expr} => { + #[test] + fn $name() { + // arrange + + // act + let engine_result = $call_expr; + + // assert + assert_eq!($expected_res, engine_result, "Path engine should produce expected result"); + } + }; + } + macro_rules! test_pat { + {$name:ident, $engine:expr, $method:ident, $test_path:literal, $ayano_path:expr, $expected_res:pat} => { + test_pat!{$name, $engine.$method(Path::new($test_path), $ayano_path.map(|p: &str| Path::new(p).into())), $expected_res} + }; + {$name:ident, $engine:expr, $test_path:literal, $expected_res:pat} => { + test_pat!{$name, $engine.ayano_insert(Path::new($test_path)), $expected_res} + }; + {$name:ident, $call_expr:expr, $expected_res:pat} => { + #[test] + fn $name() { + // arrange + + // act + let engine_result = $call_expr; + + // assert + if !matches!(engine_result, $expected_res) { + panic!("Path engine should produce matching result: {engine_result:?}"); + } + } + }; + } + + test_ok! {ok_simple_ayano, primitive(), "scripts/calc.py", "scripts/calc.py"} + test_pat! {err_deny_ayano, primitive().with_check(test_check!()), "scripts/calc.py", Err(_)} + macro_rules! paths_set_engine { + ($($existing_path:literal), *) => { + primitive_dbg() + .with_check(test_check!($($existing_path), *)) + .with_execution_folder(PathBuf::from_str("/execution").unwrap()) + .with_source_folder(PathBuf::from_str("/source").unwrap()) + .with_image_folder(PathBuf::from_str("/image").unwrap()) + .with_ayano() + }; + } + test_ok! {ok_find_ayano_correct1, paths_set_engine!("/execution/calc.py", "/image/calc.py"), "calc.py", "/execution/calc.py"} + test_ok! {ok_find_ayano_correct2, paths_set_engine!("/execution/calc.py", "/image/calc.py", "/source/calc.py"), image, "calc.py", None, "/image/calc.py"} + test_ok! {ok_find_ayano_correct2_2, paths_set_engine!("/execution/calc.py", "/source/calc.py"), image, "calc.py", None, "/source/calc.py"} + test_ok! {ok_find_ayano_correct2_3, paths_set_engine!("/execution/calc.py"), image, "calc.py", None, "/execution/calc.py"} + test_ok! {ok_find_ayano_correct2_4, paths_set_engine!("/execution/calc.py", "/image/calc.py", "/source/calc.py", "/ayano/calc.py"), image, "calc.py", Some("/ayano"), "/ayano/calc.py"} + test_ok! {ok_find_ayano_correct3, paths_set_engine!("/execution/table.csv", "/image/table.csv", "/source/table.csv"), csv, "table.csv", None, "/source/table.csv"} + test_ok! {ok_find_ayano_correct3_2, paths_set_engine!("/execution/table.csv", "/image/table.csv"), csv, "table.csv", None, "/execution/table.csv"} + test_pat! {err_find_ayano_correct3_3, paths_set_engine!("/image/table.csv"), csv, "table.csv", None, Err(_)} + + macro_rules! err { + ($path:literal) => {{ + Some(Box::new(DontExist(PathBuf::from_str($path).unwrap())) as Box) + }}; + } + test_expr! {err_reaction1, primitive_dbg().with_check(test_check!()), csv, "file.txt", None, + Err(InformativeError { + source: None, + ayano: None, + specific: None, + execution: None, + none: err!("file.txt") + })} + test_expr! {err_reaction2, primitive_dbg().with_ayano().with_check(test_check!()), csv, "file.txt", Some("/ayano"), + Err(InformativeError { + source: None, + ayano: err!("/ayano/file.txt"), + specific: None, + execution: None, + none: err!("file.txt") + })} + test_expr! {err_reaction3, primitive_dbg().with_ayano().with_execution_folder(Path::new("/execution")).with_check(test_check!()), csv, "file.txt", Some("/ayano"), + Err(InformativeError { + source: None, + ayano: err!("/ayano/file.txt"), + specific: None, + execution: err!("/execution/file.txt"), + none: err!("file.txt") + })} + test_expr! {err_reaction4, primitive_dbg().with_ayano().with_image_folder(Path::new("/image")).with_execution_folder(Path::new("/execution")).with_check(test_check!()), csv, "file.txt", Some("/ayano"), + Err(InformativeError { + source: None, + ayano: err!("/ayano/file.txt"), + specific: None, + execution: err!("/execution/file.txt"), + none: err!("file.txt") + })} + test_expr! {err_reaction5, primitive_dbg().with_ayano().with_image_folder(Path::new("/image")).with_execution_folder(Path::new("/execution")).with_check(test_check!()), image, "file.txt", Some("/ayano"), + Err(InformativeError { + source: None, + ayano: err!("/ayano/file.txt"), + specific: err!("/image/file.txt"), + execution: err!("/execution/file.txt"), + none: err!("file.txt") + })} + test_expr! {err_reaction6, primitive_dbg().with_ayano().with_source_folder(Path::new("/source")).with_image_folder(Path::new("/image")).with_execution_folder(Path::new("/execution")).with_check(test_check!()), image, "file.txt", Some("/ayano"), + Err(InformativeError { + source: err!("/source/file.txt"), + ayano: err!("/ayano/file.txt"), + specific: err!("/image/file.txt"), + execution: err!("/execution/file.txt"), + none: err!("file.txt") + })} +} From f478392e7b18edad4efc3a06fea4b5d26575c0af Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Sun, 17 Mar 2024 19:57:26 +0200 Subject: [PATCH 19/98] Moved optional permutation to `util` module - I'll definitely need to review all of the stuff in there. I'm almost sure there are more efficient implementations for most of it. --- src/lexer.rs | 104 +------------------------------------------------- src/util.rs | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 104 insertions(+), 105 deletions(-) diff --git a/src/lexer.rs b/src/lexer.rs index 05121f5..a9df0e0 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -33,7 +33,7 @@ use url::Url; use crate::{ data::{AyanoBlock, Font, Formatting, ListType, Token, Tokens}, - util::{Equivalent, IteratorArrayCollectExt, VecTryExtendExt}, + util::{optional_permutation, Equivalent, IteratorArrayCollectExt, VecTryExtendExt}, }; /// Matches any sort of whitespace characters, *newline*, and a provided tag at the end @@ -106,108 +106,6 @@ fn _optional_permutation<'parsers, 'source, E: ParseError<&'source str>, const N // - parsers should only parse empty space at the beginning, not the end // - newline parsing should be carefully managed -// TODO probably move to util, or even try committing to `nom` :idk: -mod optional_permutation { - pub trait OptionalPermutation { - fn optional_permutation(&mut self, input: In) -> super::IResult; - } - - macro_rules! impl_optional_permutation { - {$Count:expr, $type_ident:ident: $out_ident:ident} => { - // base case - impl<'source, Err, $out_ident, $type_ident> OptionalPermutation<&'source str, (Option<$out_ident>,), Err> for ($type_ident,) - where - Err: super::ParseError<&'source str>, - $type_ident: super::Parser<&'source str, $out_ident, Err>, - { - fn optional_permutation(&mut self, input: &'source str) -> super::IResult<&'source str, (Option<$out_ident>,), Err> - { - // in a debug build, let's assert that we've passed a correct type count - debug_assert!($Count == 1, "Should parse number equal to number of arguments"); - // for a single variant, just try parsing it, and propagate any sort of failure - match self.0.parse(input) { - Ok((rest, result)) => Ok((rest, (Some(result), ))), - Err(nom::Err::Failure(err)) => Err(nom::Err::Failure(err)), - Err(_) => Ok((input, (None, ))) - } - } - } - }; - {$Count:expr, $type_ident0:ident: $out_ident0:ident, $($type_ident:ident: $out_ident:ident), *} => { - impl<'source, Err, $out_ident0, $($out_ident), *, $type_ident0, $($type_ident), *> OptionalPermutation<&'source str, (Option<$out_ident0>, $(Option<$out_ident>, ) *), Err> - for ($type_ident0, $($type_ident, ) *) - where - Err: super::ParseError<&'source str>, - $type_ident0: super::Parser<&'source str, $out_ident0, Err>, - $( - $type_ident: super::Parser<&'source str, $out_ident, Err> - ), * - { - fn optional_permutation(&mut self, mut input: &'source str) -> super::IResult<&'source str, (Option<$out_ident0>, $(Option<$out_ident>), *), Err> { - // for multiple variants, here comes chaos: - // we can't create any sort of array or vector to hold the results - - // they have different types, that's the point! - // So we are bound to creating a BUNCH of variables - let mut $out_ident0: Option<$out_ident0> = None; - $( - let mut $out_ident: Option<$out_ident> = None; - ) * - // Also, since we can't index tuples, we'll need to "deconstruct self" - // I don't feel like creating separate identifiers for that, so I guess compiler IS to complain :idk: - let ($type_ident0, $($type_ident, ) *) = self; - // Now we can use `$type_ident` as corresponding parser, and `$out_ident` as a output variable! - // Amazing! (not really; I imagine that would be a nightmare to read) - - // This is a max number of iterations we're gonna need - const COUNT: usize = $Count; - for _ in 0..COUNT { - if $out_ident0.is_none() { - match $type_ident0.parse(input) { - Ok((rest, result)) => { - input = rest; - $out_ident0 = Some(result); - continue; // continues parsing attempts from the beginning - }, - Err(nom::Err::Failure(err)) => return Err(nom::Err::Failure(err)), - Err(_) => { } - } - } - $( - if $out_ident.is_none() { - match $type_ident.parse(input) { - Ok((rest, result)) => { - input = rest; - $out_ident = Some(result); - continue; // continues parsing attempts from the beginning - }, - Err(nom::Err::Failure(err)) => return Err(nom::Err::Failure(err)), - Err(_) => { } - } - } - ) * - // All parsing attempts had failed? - // Break out, we're done here. - break; - } - let _ = 2; - Ok((input, ($out_ident0, $($out_ident, ) *))) - } - } - - // forward to N-1 - impl_optional_permutation!{$Count - 1, $($type_ident: $out_ident), *} - }; - } - - impl_optional_permutation! {16, A: A_Out, B: B_Out, C: C_Out, D: D_Out, E: E_Out, F: F_Out, G: G_Out, H: H_Out, I: I_Out, J: J_Out, K: K_Out, L: L_Out, M: M_Out, N: N_Out, O: O_Out, P: P_Out} -} - -fn optional_permutation<'parsers, 'source: 'parsers, In, Out, E: ParseError<&'source str>>( - mut parsers: impl optional_permutation::OptionalPermutation + 'parsers, -) -> impl Parser + 'parsers { - move |input| parsers.optional_permutation(input) -} - /// Generates argument parser that accounts for all necessary syntax macro_rules! parse_args { [$($argument_name:literal = $argument_kernel:expr), +] => { diff --git a/src/util.rs b/src/util.rs index bb8c077..0081d51 100644 --- a/src/util.rs +++ b/src/util.rs @@ -7,7 +7,7 @@ use std::{ }; use itertools::Itertools; -use nom::{branch::Permutation, error::ParseError, Parser}; +use nom::{branch::Permutation, error::ParseError, IResult, Parser}; #[derive(Debug, Clone, PartialEq)] pub enum GenericRange { @@ -349,4 +349,105 @@ impl VecTryExtendExt for Vec { } Ok(()) } -} \ No newline at end of file +} + +pub trait OptionalPermutation { + fn optional_permutation(&mut self, input: In) -> IResult; +} + +macro_rules! impl_optional_permutation { + {$Count:expr, $type_ident:ident: $out_ident:ident} => { + // base case + #[allow(non_snake_case)] + impl<'source, Err, $out_ident, $type_ident> OptionalPermutation<&'source str, (Option<$out_ident>,), Err> for ($type_ident,) + where + Err: ParseError<&'source str>, + $type_ident: Parser<&'source str, $out_ident, Err>, + { + fn optional_permutation(&mut self, input: &'source str) -> IResult<&'source str, (Option<$out_ident>,), Err> + { + // in a debug build, let's assert that we've passed a correct type count + debug_assert!($Count == 1, "Should parse number equal to number of arguments"); + // for a single variant, just try parsing it, and propagate any sort of failure + match self.0.parse(input) { + Ok((rest, result)) => Ok((rest, (Some(result), ))), + Err(nom::Err::Failure(err)) => Err(nom::Err::Failure(err)), + Err(_) => Ok((input, (None, ))) + } + } + } + }; + {$Count:expr, $type_ident0:ident: $out_ident0:ident, $($type_ident:ident: $out_ident:ident), *} => { + #[allow(non_snake_case)] + impl<'source, Err, $out_ident0, $($out_ident), *, $type_ident0, $($type_ident), *> OptionalPermutation<&'source str, (Option<$out_ident0>, $(Option<$out_ident>, ) *), Err> + for ($type_ident0, $($type_ident, ) *) + where + Err: ParseError<&'source str>, + $type_ident0: Parser<&'source str, $out_ident0, Err>, + $( + $type_ident: Parser<&'source str, $out_ident, Err> + ), * + { + fn optional_permutation(&mut self, mut input: &'source str) -> IResult<&'source str, (Option<$out_ident0>, $(Option<$out_ident>), *), Err> { + // for multiple variants, here comes chaos: + // we can't create any sort of array or vector to hold the results - + // they have different types, that's the point! + // So we are bound to creating a BUNCH of variables + let mut $out_ident0: Option<$out_ident0> = None; + $( + let mut $out_ident: Option<$out_ident> = None; + ) * + // Also, since we can't index tuples, we'll need to "deconstruct self" + // I don't feel like creating separate identifiers for that, so I guess compiler IS to complain :idk: + let ($type_ident0, $($type_ident, ) *) = self; + // Now we can use `$type_ident` as corresponding parser, and `$out_ident` as a output variable! + // Amazing! (not really; I imagine that would be a nightmare to read) + + // This is a max number of iterations we're gonna need + const COUNT: usize = $Count; + for _ in 0..COUNT { + if $out_ident0.is_none() { + match $type_ident0.parse(input) { + Ok((rest, result)) => { + input = rest; + $out_ident0 = Some(result); + continue; // continues parsing attempts from the beginning + }, + Err(nom::Err::Failure(err)) => return Err(nom::Err::Failure(err)), + Err(_) => { } + } + } + $( + if $out_ident.is_none() { + match $type_ident.parse(input) { + Ok((rest, result)) => { + input = rest; + $out_ident = Some(result); + continue; // continues parsing attempts from the beginning + }, + Err(nom::Err::Failure(err)) => return Err(nom::Err::Failure(err)), + Err(_) => { } + } + } + ) * + // All parsing attempts had failed? + // Break out, we're done here. + break; + } + let _ = 2; + Ok((input, ($out_ident0, $($out_ident, ) *))) + } + } + + // forward to N-1 + impl_optional_permutation!{$Count - 1, $($type_ident: $out_ident), *} + }; + } + +impl_optional_permutation! {16, A: AOut, B: BOut, C: COut, D: DOut, E: EOut, F: FOut, G: GOut, H: HOut, I: IOut, J: JOut, K: KOut, L: LOut, M: MOut, N: NOut, O: OOut, P: POut} + +pub fn optional_permutation<'parsers, 'source: 'parsers, In, Out, E: ParseError<&'source str>>( + mut parsers: impl OptionalPermutation + 'parsers, +) -> impl Parser + 'parsers { + move |input| parsers.optional_permutation(input) +} From 78468f1ba5f95dd50c40f22144a2e7a3479a4baa Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Sun, 17 Mar 2024 20:02:08 +0200 Subject: [PATCH 20/98] Added `terminating(..., space0)` to all `all_consuming` --- src/lexer.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/lexer.rs b/src/lexer.rs index a9df0e0..1235b8e 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -137,7 +137,6 @@ macro_rules! parse_args { /// /// Quite a tricky task, as there can be double quotes -- but escaped. /// Apart from double unescaped quotes, caption can be ANYTHING. -/// TODO FIXME add a mention about caption attempting to parse after EACH unescaped double quote. First one to succeed will be the output fn caption_kernel< 'source, E: ParseError<&'source str> @@ -165,7 +164,6 @@ fn caption_kernel< KyomatoLexError::no_tokens(input), )))?; - // FIXME add `terminated(_, space0)` to all `all_consuming` // lastly, match the actual closing quote let (input, _) = cut(char('"'))(input)?; Ok((input, token)) @@ -739,7 +737,7 @@ fn _formatting_inner< input: &'source str, style: crate::data::Formatting, ) -> Result, nom::Err> { - let (_, content) = all_consuming(inner_lex)(input)?; + let (_, content) = all_consuming(terminated(inner_lex, space0))(input)?; Ok(Token::Formatted(style, Box::new(content))) } @@ -780,7 +778,9 @@ fn formatting< .filter_map(|(ind, _)| (rest[ind..].starts_with(delimiter)).then_some(ind)); for end_candidate in possible_ends { let inner_tokens = &rest[..end_candidate]; - if let Ok((_, inner)) = all_consuming(inner_lex::<'source, E>)(inner_tokens) { + if let Ok((_, inner)) = + all_consuming(terminated(inner_lex::<'source, E>, space0))(inner_tokens) + { return Ok(( &rest[(end_candidate + delimiter.len())..], Token::Formatted(formatting, Box::new(inner)), @@ -877,7 +877,8 @@ fn footnote_content< .and(preceded( space0, // Content can end at the end of this line, or at the end of file - alt((take_until1("\n"), take_while(|_| true))).and_then(cut(all_consuming(inner_lex))), + alt((take_until1("\n"), take_while(|_| true))) + .and_then(cut(all_consuming(terminated(inner_lex, space0)))), )), ) .map(|(ident, content)| Token::FootNoteContent { From 4bcda1c42a2cf4bed79eff4456035a632a1b10b1 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Sun, 17 Mar 2024 23:04:56 +0200 Subject: [PATCH 21/98] dev update: - Added caption support for Ayano-generated stuff - Added tests for Ayano-generated stuff --- src/data.rs | 133 ++++++++++++++++++++++++++++++ src/gen/ayano.rs | 72 +++++++++++++--- src/gen/lab/mod.rs | 2 +- src/lexer.rs | 5 +- tests/input_files/ayano.md | 17 ++++ tests/lex_files/ayano.txt | 34 ++++++++ tests/transformed_files/ayano.txt | 22 +++++ tests/transformed_files/basic.txt | 3 +- 8 files changed, 269 insertions(+), 19 deletions(-) create mode 100644 tests/input_files/ayano.md create mode 100644 tests/lex_files/ayano.txt create mode 100644 tests/transformed_files/ayano.txt diff --git a/src/data.rs b/src/data.rs index 9245b97..58e7513 100644 --- a/src/data.rs +++ b/src/data.rs @@ -167,6 +167,76 @@ pub enum Token<'source> { Error(String), } +trait ToStaticExt { + type AsStatic; + fn to_static(&self) -> Self::AsStatic; +} + +impl ToStaticExt for Cow<'_, str> { + type AsStatic = Cow<'static, str>; + + fn to_static(&self) -> Self::AsStatic { + Cow::Owned(self.to_string()) + } +} + +impl ToStaticExt for Cow<'_, Path> { + type AsStatic = Cow<'static, Path>; + + fn to_static(&self) -> Self::AsStatic { + Cow::Owned(self.to_path_buf()) + } +} + +impl ToStaticExt for Token<'_> { + type AsStatic = Token<'static>; + + fn to_static(&self) -> Self::AsStatic { + self.to_static_token() + } +} + +impl ToStaticExt for Tokens<'_> { + type AsStatic = Tokens<'static>; + + fn to_static(&self) -> Self::AsStatic { + let slice = self.get_slice(); + Tokens::new( + slice + .into_iter() + .map(|t| t.to_static_token()) + .collect::>(), + ) + } +} + +impl ToStaticExt for Box> { + type AsStatic = Box>; + + fn to_static(&self) -> Self::AsStatic { + Box::new((&**self).to_static()) + } +} + +impl ToStaticExt for AyanoBlock<'_> { + type AsStatic = AyanoBlock<'static>; + + fn to_static(&self) -> Self::AsStatic { + let AyanoBlock { + code, + insert_path, + is_display, + is_static, + } = self; + AyanoBlock { + code: code.to_static(), + insert_path: insert_path.as_ref().map(Cow::to_static), + is_static: *is_static, + is_display: *is_display, + } + } +} + impl<'source> Token<'source> { pub fn text(text: impl Into>) -> Self { Self::Paragraph(Font::Normal, text.into()) @@ -236,4 +306,67 @@ impl<'source> Token<'source> { Token::Error(error) => Token::Error(error.clone()), } } + + pub fn to_static_token<'r>(&'r self) -> Token<'static> + where + 'source: 'r, + { + match self { + Token::PageDiv => Token::PageDiv, + Token::Header { order, content } => Token::Header { + order: *order, + content: content.to_static(), + }, + Token::Equation { content, ident } => Token::Equation { + content: content.to_static(), + ident: ident.as_ref().map(Cow::to_static), + }, + Token::Table { + header, + cells, + caption, + ident, + } => Token::Table { + header: header.to_static(), + cells: cells.to_static(), + caption: caption.as_ref().map(Box::to_static), + ident: ident.as_ref().map(Cow::to_static), + }, + Token::Figure { + src_name, + caption, + ident, + } => Token::Figure { + src_name: src_name.to_static(), + caption: caption.as_ref().map(Box::to_static), + ident: ident.as_ref().map(Cow::to_static), + }, + Token::Href { url, display } => Token::Href { + url: url.clone(), + display: display.to_static(), + }, + Token::Ayano(block) => Token::Ayano(block.to_static()), + Token::List { list_type, content } => Token::List { + list_type: list_type.clone(), + content: content.to_static(), + }, + Token::Formatted(formatting, inner) => { + Token::Formatted(formatting.clone(), inner.to_static()) + } + Token::Text(text) => Token::Text(text.to_static()), + Token::Paragraph(font, inner) => Token::Paragraph(font.clone(), inner.to_static()), + Token::InlineMathmode(math) => Token::InlineMathmode(math.to_static()), + Token::Reference(ident) => Token::Reference(ident.to_static()), + Token::FootNoteReference(ident) => Token::FootNoteReference(ident.to_static()), + Token::FootNoteContent { content, ident } => Token::FootNoteContent { + content: content.to_static(), + ident: ident.to_static(), + }, + Token::CodeBlock { code, language } => Token::CodeBlock { + code: code.to_static(), + language: language.as_ref().map(Cow::to_static), + }, + Token::Error(error) => Token::Error(error.clone()), + } + } } diff --git a/src/gen/ayano.rs b/src/gen/ayano.rs index 03e09f6..3517061 100644 --- a/src/gen/ayano.rs +++ b/src/gen/ayano.rs @@ -21,6 +21,7 @@ use std::{ use crate::{ data::{AyanoBlock, Font, Token, Tokens}, + lexer::KyomatoLexError, util::{ Equivalent, GenericRange, GenericRangeParseError, Immutable, InlinedStack, InlinedStackAccessor, @@ -382,7 +383,7 @@ fn apply_ayano<'token, 'source: 'token>( } else { String::new() }; - let mut code_lines = block.code.lines() + let mut code_lines = block.code.lines(); // I used to have this filter here, but I don't think that's a good thing anymore // There's no harm in preserving basic formatting for better debugging // TODO probably and #[cfg(test)] switch, that would simplify the code is it's added to Python module itself (i.e. this is to be done in `python` module) @@ -391,7 +392,6 @@ fn apply_ayano<'token, 'source: 'token>( !line.is_empty() && !line.starts_with('#') // remove comments }) */ - ; // create display name let display_name = if block.is_display { @@ -636,8 +636,7 @@ pub enum SyntaxError { #[error("Unknown argument is present: {}", .0)] UnknownArgument(String), /// Right now this only means that you don't have a generator in a table syntax - #[error("Ayano syntax is fundamentally wrong")] - // TODO probably should rename and write a better message + #[error("Ayano syntax is lacking fields, or it's punctuation is wrong")] BadSyntax, #[error("{}", .0)] RangeParse(GenericRangeParseError), @@ -1377,12 +1376,14 @@ return "tab", [[___generator___(0, 0), ___generator___(0, 1), ], [___generator__ #[derive(Debug, PartialEq, thiserror::Error)] pub enum ParsingError { - #[error("{}", .0)] + #[error(transparent)] Float(ParseFloatError), #[error("Python could not generate output. str: {}, error: {:?}", .0, .1)] UnexpectedSyntax(String, Equivalent), #[error("Table syntax error: no header")] TableNoHeader, + #[error(transparent)] + Token(KyomatoLexError), } impl From for ParsingError { @@ -1391,6 +1392,15 @@ impl From for ParsingError { } } +impl From> for ParsingError { + fn from(value: nom::Err) -> Self { + match value { + nom::Err::Incomplete(_) => unreachable!("Input is always complete here"), + nom::Err::Error(err) | nom::Err::Failure(err) => Self::Token(err), + } + } +} + fn parse_ayano(python_output: &PyAny) -> Result, ParsingError> { // it was quite a journey! but I believe to be close to it's end. if let Ok(tuple) = python_output.downcast::() { @@ -1453,11 +1463,17 @@ fn parse_ayano(python_output: &PyAny) -> Result, ParsingError> { let ident = (!ident.is_none()) .then_some(throw_syntax!(ident.str().and_then(|s| s.to_str()))?); let caption = throw_syntax!(tuple.get_item(3))?; - let caption = (!caption.is_none()) - .then_some(throw_syntax!(caption.str().and_then(|s| s.to_str()))?); + let caption = if caption.is_none() { + None + } else { + let caption_str = throw_syntax!(caption.str().and_then(|s| s.to_str()))?; + let (_, caption_token) = + crate::lexer::inner_lex::(caption_str)?; + Some(Box::new(caption_token.to_static_token())) + }; return Ok(Token::Figure { src_name: Cow::Owned(PathBuf::from(src)), - caption: None, // TODO MUST USE LEXER HERE + caption, ident: ident.map(|i| Cow::Owned(i.to_string())), }); } @@ -1496,13 +1512,19 @@ fn parse_ayano(python_output: &PyAny) -> Result, ParsingError> { let ident = (!ident.is_none()) .then_some(throw_syntax!(ident.str().and_then(|s| s.to_str()))?); let caption = throw_syntax!(tuple.get_item(3))?; - let caption = (!caption.is_none()) - .then_some(throw_syntax!(caption.str().and_then(|s| s.to_str()))?); + let caption = if caption.is_none() { + None + } else { + let caption_str = throw_syntax!(caption.str().and_then(|s| s.to_str()))?; + let (_, caption_token) = + crate::lexer::inner_lex::(caption_str)?; + Some(Box::new(caption_token.to_static_token())) + }; return Ok(Token::Table { header: Tokens::new(header), cells: Tokens::new(cells), ident: ident.map(|s| Cow::Owned(s.to_string())), - caption: None, // TODO MUST USE LEXER HERE + caption, }); } _ => {} @@ -1581,7 +1603,11 @@ mod parsing_tests { src_name: Cow::Owned(PathBuf::from("path/to/file.png")), caption: None, ident: Some("circle".into()) })} - // TODO add tests for figure WITH CAPTION + test! {figure_caption, |py| PyTuple::new(py, [<&'static str as IntoPy>>::into_py("fig", py), "path/to/file.png".into_py(py), "circle".into_py(py), "figure caption".into_py(py)]), + Ok(Token::Figure { + src_name: Cow::Owned(PathBuf::from("path/to/file.png")), + caption: Some(Box::new(Token::text("figure caption"))), + ident: Some("circle".into()) })} test! {table_nocaption, |py| { PyTuple::new(py, [ @@ -1629,7 +1655,27 @@ mod parsing_tests { ident: Some("table1".into()) }) } - // TODO add tests for table WITH CAPTION + test! {table_caption, |py| { + PyTuple::new(py, [ + <&'static str as IntoPy>>::into_py("tab", py), + PyList::new(py, [ + PyList::new(py, ["header1".into_py(py), 1.0.into_py(py)]), + PyList::new(py, ["cell_11".into_py(py), 0.25.into_py(py)]), + PyList::new(py, ["cell_21".into_py(py), (-2).into_py(py)]), + ]).into_py(py), + "table1".into_py(py), + "table caption with some $formula$".into_py(py), + ])}, + Ok(Token::Table { + header: Tokens::new([Token::text("header1"), Token::text("1.0")]), + cells: Tokens::new([ + Token::text("cell_11"), Token::text("0.25"), + Token::text("cell_21"), Token::text("-2"), + ]), + caption: Some(Box::new(Token::Text(Tokens::new([Token::text("table caption with some"),Token::InlineMathmode("formula".into())])))), + ident: Some("table1".into()) + }) + } } struct PrimitiveDecimal { diff --git a/src/gen/lab/mod.rs b/src/gen/lab/mod.rs index 37add5f..fc287e5 100644 --- a/src/gen/lab/mod.rs +++ b/src/gen/lab/mod.rs @@ -143,7 +143,7 @@ where ident, } => { output.write_str( - "\\begin{figure}[h!]\n\\centering\n\\includegraphics[width=0.9\\textwidth]{", + "\n\\begin{figure}[h!]\n\\centering\n\\includegraphics[width=0.9\\textwidth]{", )?; let src_path = self .path_engine diff --git a/src/lexer.rs b/src/lexer.rs index 1235b8e..ee612f9 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -170,7 +170,6 @@ fn caption_kernel< } /// Parses page divider -/// TODO update it's definition in the manual #[inline] fn div_page<'source, E: ParseError<&'source str> + ContextError<&'source str>>( input: &'source str, @@ -566,7 +565,7 @@ fn ayano< // - inner Some means this block has display caption // TODO add code captions support let insert: Option<&'source Path> = insert.map(Path::new); - // TODO add support for codeblock identifiers + // XXX add support for codeblock identifiers let code: Cow<'_, str> = Cow::from(code); Ok(( rest, @@ -1133,7 +1132,6 @@ impl<'source, E: Error + 'static> FromExternalError<&'source str, E> for Kyomato } } -// TODO wrap outer-layer combinator into `complete` /// Parses one or more tokens. /// /// - If there's a single token to parse, it will be returned by itself, as a token @@ -2012,7 +2010,6 @@ $$ test_ok! {ok_no_args, "\n```python, Ayano \nx = 1\ny = 2\nprint(x + y)\n```\n", ayano!(!false, *None::<&str>, ~None::<&str>, #None, "x = 1\ny = 2\nprint(x + y)"), "\n"} } - // FIXME EXPL: `formatting` tests are not to be done, since this token type will be removed soon anyways mod lex { use super::*; macro_rules! test_ok { diff --git a/tests/input_files/ayano.md b/tests/input_files/ayano.md new file mode 100644 index 0000000..1f0115e --- /dev/null +++ b/tests/input_files/ayano.md @@ -0,0 +1,17 @@ +This is a test of ayano-generated stuff. + +This is supposed to be a generated figure: +```python, Ayano +@fig: src="circle.png", ident="circle", caption="Some caption for a line drawing. This is parsed as a *regular document text*, so you may [@eq:line] and $f(x) = A \cdot x^{1} + B \cdot x^{0}$ here, for example" +``` + +This is expected to be a generated table: +```python, Ayano +data = [ + ["x", "y"], + [1.0, 3.0], + [3.0, 4.0], + [-1.0, -5743234.0], +] +@gen_table: lambda r,c: data[r][c]; rows=4, columns=2, caption="Caption for a table 1, *because* why not", ident="table1" +``` diff --git a/tests/lex_files/ayano.txt b/tests/lex_files/ayano.txt new file mode 100644 index 0000000..8c461f5 --- /dev/null +++ b/tests/lex_files/ayano.txt @@ -0,0 +1,34 @@ +Text( + Owned( + [ + Paragraph( + Normal, + "This is a test of ayano-generated stuff.", + ), + Paragraph( + Normal, + "This is supposed to be a generated figure:", + ), + Ayano( + AyanoBlock { + is_display: false, + is_static: false, + code: "@fig: src=\"circle.png\", ident=\"circle\", caption=\"Some caption for a line drawing. This is parsed as a *regular document text*, so you may [@eq:line] and $f(x) = A \\cdot x^{1} + B \\cdot x^{0}$ here, for example\"", + insert_path: None, + }, + ), + Paragraph( + Normal, + "This is expected to be a generated table:", + ), + Ayano( + AyanoBlock { + is_display: false, + is_static: false, + code: "data = [\n\t\t[\"x\", \"y\"],\n\t\t[1.0, 3.0],\n\t\t[3.0, 4.0],\n\t\t[-1.0, -5743234.0],\n]\n@gen_table: lambda r,c: data[r][c]; rows=4, columns=2, caption=\"Caption for a table 1, *because* why not\", ident=\"table1\"", + insert_path: None, + }, + ), + ], + ), +) \ No newline at end of file diff --git a/tests/transformed_files/ayano.txt b/tests/transformed_files/ayano.txt new file mode 100644 index 0000000..35d665b --- /dev/null +++ b/tests/transformed_files/ayano.txt @@ -0,0 +1,22 @@ +\fnt This is a test of ayano-generated stuff.\fnt This is supposed to be a generated figure: +\begin{figure}[h!] +\centering +\includegraphics[width=0.9\textwidth]{circle.png} +\caption{\fnt Some caption for a line drawing. This is parsed as a\textit{\fnt regular document text}\fnt , so you may\ref{eq:line}\fnt and$f(x) = A \cdot x^{1} + B \cdot x^{0}$\fnt here, for example} +\label{fig:circle} +\end{figure} +\fnt This is expected to be a generated table: +\begin{table}[h!] +\begin{center} +\begin{tabular}{|c|c|} +\fnt x & \fnt y\\ \hline +\hline +\fnt 1.0 & \fnt 3.0\\ \hline +\fnt 3.0 & \fnt 4.0\\ \hline +\fnt -1.0 & \fnt -5743234.0\\ \hline +\end{tabular} +\stepcounter{tabnum} +\caption{\fnt Caption for a table 1,\textit{\fnt because}\fnt why not} +\label{tab:table1} +\end{center} +\end{table} diff --git a/tests/transformed_files/basic.txt b/tests/transformed_files/basic.txt index 53f0ee6..fac0aa9 100644 --- a/tests/transformed_files/basic.txt +++ b/tests/transformed_files/basic.txt @@ -11,7 +11,8 @@ Ref(z) = \int \limits_{0}^{1} t^{z-1} \cdot \exp(-t) dt \item \fnt Нормувальний множник$|\psi|^{2} = 3/4$ \item \fnt Кількість хромосом -$\exp(\pi)$ \item \fnt Поживна цінність - 20 дптр/см -\end{itemize}\begin{figure}[h!] +\end{itemize} +\begin{figure}[h!] \centering \includegraphics[width=0.9\textwidth]{refurb.jpg} \caption{\fnt Принципове влаштування рефурбалідзера} From 6efadb402995e049f58eff47cb0d22499e07459b Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Mon, 18 Mar 2024 13:16:48 +0200 Subject: [PATCH 22/98] Added `width` argument to table syntax. Default value is 0.9 --- src/data.rs | 5 +++ src/gen/ayano.rs | 62 ++++++++++++++++++++++++------- src/gen/lab/meta.rs | 1 + src/gen/lab/mod.rs | 38 +++++++++++++++---- src/lexer.rs | 19 ++++++---- tests/input_files/ayano.md | 8 +++- tests/input_files/basic.md | 2 +- tests/lex_files/ayano.txt | 12 ++++++ tests/lex_files/basic.txt | 3 ++ tests/transformed_files/ayano.txt | 5 +++ tests/transformed_files/basic.txt | 2 +- 11 files changed, 125 insertions(+), 32 deletions(-) diff --git a/src/data.rs b/src/data.rs index 58e7513..cd31c12 100644 --- a/src/data.rs +++ b/src/data.rs @@ -136,6 +136,7 @@ pub enum Token<'source> { src_name: Pth<'source>, caption: Option>>, ident: Option>, + width: Option, }, Href { // #[cfg_attr(feature = "serde", serde(with = "url_serde"))] @@ -271,10 +272,12 @@ impl<'source> Token<'source> { src_name, caption, ident, + width, } => Token::Figure { src_name: Cow::Borrowed(src_name), caption: caption.as_ref().map(|t| Box::new(Token::borrow_ref(t))), ident: ident.as_ref().map(|s| Cow::<'r, str>::Borrowed(s)), + width: *width, }, Token::Href { url, display } => Token::Href { url: url.clone(), @@ -336,10 +339,12 @@ impl<'source> Token<'source> { src_name, caption, ident, + width, } => Token::Figure { src_name: src_name.to_static(), caption: caption.as_ref().map(Box::to_static), ident: ident.as_ref().map(Cow::to_static), + width: *width, }, Token::Href { url, display } => Token::Href { url: url.clone(), diff --git a/src/gen/ayano.rs b/src/gen/ayano.rs index 3517061..0153b72 100644 --- a/src/gen/ayano.rs +++ b/src/gen/ayano.rs @@ -612,6 +612,7 @@ enum Syntax<'source> { src: &'source str, ident: Option<&'source str>, caption: Option<&'source str>, + width: Option<&'source str>, }, CsvTable { src: &'source str, @@ -638,9 +639,9 @@ pub enum SyntaxError { /// Right now this only means that you don't have a generator in a table syntax #[error("Ayano syntax is lacking fields, or it's punctuation is wrong")] BadSyntax, - #[error("{}", .0)] + #[error(transparent)] RangeParse(GenericRangeParseError), - #[error("{}", .0)] + #[error(transparent)] IntParse(ParseIntError), #[error("{}", **.0)] InsertIo(Equivalent), @@ -802,7 +803,8 @@ impl<'l> TryFrom<&'l str> for SyntaxData<'l> { let mut src = None; let mut ident = None; let mut caption = None; - parse_args!(s, src, "src", ident, "ident", caption, "caption"); + let mut width = None; + parse_args!(s, src, "src", ident, "ident", caption, "caption", width, "width"); needed!(src, "src"); return Ok(Self { indent, @@ -810,6 +812,7 @@ impl<'l> TryFrom<&'l str> for SyntaxData<'l> { src, caption, ident, + width, }, }); } @@ -911,8 +914,11 @@ mod syntax_data_tests { test! {value_error1, "@dev: 1, 2", SyntaxData {indent: 0, syntax: Syntax::ValueError { value: "1", error: "2" }}} test! {value_error2, "@dev: 1, 2 ", SyntaxData {indent: 0, syntax: Syntax::ValueError { value: "1", error: "2" }}} test! {value_error3, "@dev: x + 1, (2 + 3) / 10 ", SyntaxData {indent: 0, syntax: Syntax::ValueError { value: "x + 1", error: "(2 + 3) / 10" }}} - test! {fig1, r#" @fig: src = "path.png" , caption = "Wow, such caption", ident="path" "#, SyntaxData {indent: 4, syntax: Syntax::Figure{ src:"\"path.png\"", caption: Some("\"Wow, such caption\""), ident: Some("\"path\"")}}} - test! {fig2, r#" @fig: caption = "Wow, such caption", ident="path" ,src = "path.png" "#, SyntaxData {indent: 4, syntax: Syntax::Figure{ src: "\"path.png\"", caption: Some("\"Wow, such caption\""), ident: Some("\"path\"")}}} + test! {fig1, r#" @fig: src = "path.png" , caption = "Wow, such caption", ident="path" "#, SyntaxData {indent: 4, syntax: Syntax::Figure{ src:"\"path.png\"", caption: Some("\"Wow, such caption\""), ident: Some("\"path\""), width: None}}} + test! {fig2, r#" @fig: caption = "Wow, such caption", ident="path" ,src = "path.png" "#, SyntaxData {indent: 4, syntax: Syntax::Figure{ src: "\"path.png\"", caption: Some("\"Wow, such caption\""), ident: Some("\"path\""), width: None}}} + test! {fig3_width, r#" @fig: caption = "Wow, such caption", width= 0.125, ident="path" ,src = "path.png" "#, SyntaxData {indent: 4, syntax: Syntax::Figure{ src: "\"path.png\"", caption: Some("\"Wow, such caption\""), ident: Some("\"path\""), width: Some("0.125")}}} + test! {fig4_width_expr, r#" @fig: caption = "Wow, such caption", width= WIDTH + 3 , ident="path" ,src = "path.png" "#, + SyntaxData {indent: 4, syntax: Syntax::Figure{ src: "\"path.png\"", caption: Some("\"Wow, such caption\""), ident: Some("\"path\""), width: Some("WIDTH + 3")}}} test! {csv_table1, r#" @csv_table: src = "path/to/table.csv", caption = "such caption, wow""#, SyntaxData {indent: 3, syntax: Syntax::CsvTable { src: "\"path/to/table.csv\"", rows: GenericRange::Full, columns: None, ident: None, caption: Some("\"such caption, wow\"") }}} test! {csv_table2, r#" @csv_table: src = "path/to/table.csv", rows = 1..1000"#, SyntaxData {indent: 3, syntax: Syntax::CsvTable { src: "\"path/to/table.csv\"", rows: GenericRange::DoubleBounded(1..1000), columns: None, ident: None, caption: None, }}} test! {csv_table3, r#" @csv_table: src = "path/to/table.csv", rows = 2.."#, SyntaxData {indent: 3, syntax: Syntax::CsvTable { src: "\"path/to/table.csv\"", rows: GenericRange::From(2..), columns: None, ident: None, caption: None }}} @@ -990,6 +996,7 @@ impl Syntax<'_> { src, ident, caption, + width, } => { writer.write_str("return \"fig\", ")?; writer.write_str(src)?; @@ -1005,6 +1012,12 @@ impl Syntax<'_> { } else { writer.write_str("None")?; } + writer.write_str(", ")?; + if let Some(width) = width { + writer.write_str(&width.to_string())?; + } else { + writer.write_str("None")?; + } } Syntax::CsvTable { src, @@ -1171,7 +1184,9 @@ mod syntax_display_tests { test! {indent1, SyntaxData {indent: 4, syntax: Syntax::Nothing("")}, " return "} test! {value_error1, SyntaxData {indent: 4, syntax: Syntax::ValueError{value:"x", error:"y"}}, " return \"err\", x, y"} test! {value_error2, SyntaxData {indent: 0, syntax: Syntax::ValueError{value:"x + 2", error:"sqrt(x/2 +y)"}}, "return \"err\", x + 2, sqrt(x/2 +y)"} - test! {figure1, SyntaxData {indent: 0, syntax: Syntax::Figure{src:"output_path", caption: Some("\"some caption text\""), ident: Some("\"fig1\"")}}, "return \"fig\", output_path, \"fig1\", \"some caption text\""} + test! {figure1, SyntaxData {indent: 0, syntax: Syntax::Figure{src:"output_path", caption: Some("\"some caption text\""), ident: Some("\"fig1\""), width: None}}, "return \"fig\", output_path, \"fig1\", \"some caption text\", None"} + test! {figure2_width, SyntaxData {indent: 0, syntax: Syntax::Figure{src:"output_path", caption: Some("\"some caption text\""), ident: Some("\"fig1\""), width: Some("0.75")}}, "return \"fig\", output_path, \"fig1\", \"some caption text\", 0.75"} + test! {figure3_width_expr, SyntaxData {indent: 0, syntax: Syntax::Figure{src:"output_path", caption: Some("\"some caption text\""), ident: Some("\"fig1\""), width: Some("W + 2")}}, "return \"fig\", output_path, \"fig1\", \"some caption text\", W + 2"} test! {csv_table1, SyntaxData { indent: 0, syntax: Syntax::CsvTable { @@ -1240,13 +1255,13 @@ mod last_line_tests { test! {nothing2, "\"resulting string!\"", "return \"resulting string!\""} test! {dev1, "@dev: 2.0, -0.34E4", "return \"err\", 2.0, -0.34E4"} - test! {fig1, "@fig: src=\"./picture.jpg\"", "return \"fig\", \"./picture.jpg\", None, None"} + test! {fig1, "@fig: src=\"./picture.jpg\"", "return \"fig\", \"./picture.jpg\", None, None, None"} test! {fig2, "@fig: caption=\"Some very cool picture\", src=\"./picture.jpg\"", - "return \"fig\", \"./picture.jpg\", None, \"Some very cool picture\""} + "return \"fig\", \"./picture.jpg\", None, \"Some very cool picture\", None"} test! {fig3, "@fig: ident=\"cool\", caption=\"Some very cool picture\", src=\"./picture.jpg\"", - "return \"fig\", \"./picture.jpg\", \"cool\", \"Some very cool picture\""} + "return \"fig\", \"./picture.jpg\", \"cool\", \"Some very cool picture\", None"} test! {fig4, "@fig: caption=\"Some very cool picture\", src=\"./picture.jpg\", ident=\"cool\"", - "return \"fig\", \"./picture.jpg\", \"cool\", \"Some very cool picture\""} + "return \"fig\", \"./picture.jpg\", \"cool\", \"Some very cool picture\", None"} test! {fig5, "@fig: caption=\"Some very cool picture\", ident=\"cool\"", -, SyntaxError::MissingArgument("src".into())} test! {fig6, "@fig: caption=\"Some very cool picture\", ident=\"cool\", coolness=1000", -, SyntaxError::UnknownArgument("coolness".to_string().into())} test! {fig7, "@fig: caption=\"Some very cool picture\", ident=\"cool\", coolness=1000, src=\"path.png\"", -, SyntaxError::UnknownArgument("coolness".to_string().into())} @@ -1471,10 +1486,18 @@ fn parse_ayano(python_output: &PyAny) -> Result, ParsingError> { crate::lexer::inner_lex::(caption_str)?; Some(Box::new(caption_token.to_static_token())) }; + let width = throw_syntax!(tuple.get_item(4))?; + let width = if width.is_none() { + None + } else { + let width_str = throw_syntax!(width.str().and_then(|s| s.to_str()))?; + Some(width_str.parse()?) + }; return Ok(Token::Figure { src_name: Cow::Owned(PathBuf::from(src)), caption, ident: ident.map(|i| Cow::Owned(i.to_string())), + width, }); } "tab" => { @@ -1598,16 +1621,27 @@ mod parsing_tests { Ok(value_error_token(1.9876543, 0.0000012))} // TODO probably should add some more tests for value-error formatting - test! {figure_nocaption1, |py| PyTuple::new(py, ["fig".into_py(py), "path/to/file.png".into_py(py), "circle".into_py(py), None::<()>.into_py(py)]), + test! {figure_nocaption1, |py| PyTuple::new(py, ["fig".into_py(py), "path/to/file.png".into_py(py), "circle".into_py(py), None::<()>.into_py(py), None::.into_py(py)]), Ok(Token::Figure { src_name: Cow::Owned(PathBuf::from("path/to/file.png")), caption: None, - ident: Some("circle".into()) })} - test! {figure_caption, |py| PyTuple::new(py, [<&'static str as IntoPy>>::into_py("fig", py), "path/to/file.png".into_py(py), "circle".into_py(py), "figure caption".into_py(py)]), + ident: Some("circle".into()), + width: None, + })} + test! {figure_caption, |py| PyTuple::new(py, ["fig".into_py(py), "path/to/file.png".into_py(py), "circle".into_py(py), "figure caption".into_py(py), None::.into_py(py)]), Ok(Token::Figure { src_name: Cow::Owned(PathBuf::from("path/to/file.png")), caption: Some(Box::new(Token::text("figure caption"))), - ident: Some("circle".into()) })} + ident: Some("circle".into()), + width: None, + })} + test! {figure_width, |py| PyTuple::new(py, ["fig".into_py(py), "path/to/file.png".into_py(py), "circle".into_py(py), "figure caption".into_py(py), 0.625.into_py(py)]), + Ok(Token::Figure { + src_name: Cow::Owned(PathBuf::from("path/to/file.png")), + caption: Some(Box::new(Token::text("figure caption"))), + ident: Some("circle".into()), + width: Some(0.625), + })} test! {table_nocaption, |py| { PyTuple::new(py, [ diff --git a/src/gen/lab/meta.rs b/src/gen/lab/meta.rs index 6c5156c..e748a42 100644 --- a/src/gen/lab/meta.rs +++ b/src/gen/lab/meta.rs @@ -79,6 +79,7 @@ where ident: Some(ident), caption, src_name: _src_name, + width: _width, } => { dest.refs.insert(ident); if let Some(caption) = caption { diff --git a/src/gen/lab/mod.rs b/src/gen/lab/mod.rs index fc287e5..0eb6d81 100644 --- a/src/gen/lab/mod.rs +++ b/src/gen/lab/mod.rs @@ -141,10 +141,15 @@ where src_name, caption, ident, + width, } => { - output.write_str( - "\n\\begin{figure}[h!]\n\\centering\n\\includegraphics[width=0.9\\textwidth]{", - )?; + output.write_str("\n\\begin{figure}[h!]\n\\centering\n\\includegraphics[width=")?; + if let Some(width) = width.as_ref() { + output.write_str(&width.to_string())? + } else { + output.write_str("0.9")?; + } + output.write_str("\\textwidth]{")?; let src_path = self .path_engine .image(src_name.as_ref(), None) @@ -386,14 +391,14 @@ mod tests { test! {inline_mathmode, Token::InlineMathmode(r"\dfrac{\partial x}{\partial t}".into()), r"$ \dfrac{\partial x}{\partial t} $"} // Figures - test! {fig1, Token::Figure { src_name: Path::new("apple.jpg").into(), caption: Some(Box::new(text!("a very realistic-looking apple"))), ident: None }, r" + test! {fig1, Token::Figure { src_name: Path::new("apple.jpg").into(), caption: Some(Box::new(text!("a very realistic-looking apple"))), ident: None, width: None }, r" \begin{figure}[h!] \centering \includegraphics[width = 0.9 \textwidth]{apple.jpg} \caption{\fnt a very realistic-looking apple} \end{figure} "} - test! {fig2, Token::Figure { src_name: Path::new("schema1.jpg").into(), caption: Some(Box::new(text!("A schema providing a bunch of very important info on quantum refurbalidzer function"))), ident: Some("schema-1".into()) }, r" + test! {fig2, Token::Figure { src_name: Path::new("schema1.jpg").into(), caption: Some(Box::new(text!("A schema providing a bunch of very important info on quantum refurbalidzer function"))), ident: Some("schema-1".into()), width: None }, r" \begin{figure}[h!] \centering \includegraphics[width = 0.9 \textwidth]{schema1.jpg} @@ -401,7 +406,14 @@ mod tests { \label{fig:schema-1} \end{figure} "} - + test! {fig3_width, Token::Figure { src_name: Path::new("schema1.jpg").into(), caption: Some(Box::new(text!("A schema providing a bunch of very important info on quantum refurbalidzer function"))), ident: Some("schema-1".into()), width: Some(0.25) }, r" + \begin{figure}[h!] + \centering + \includegraphics[width = 0.25 \textwidth]{schema1.jpg} + \caption{\fnt A schema providing a bunch of very important info on quantum refurbalidzer function} + \label{fig:schema-1} + \end{figure} + "} // Tables test! {tab1, Token::Table { header: @@ -574,7 +586,7 @@ y = 0.4 test! {ayano_fig1, Token::Ayano(AyanoBlock{ is_display: false, is_static: false, - code: r"'fig', 'path/to/image.jpg', None, None".into(), + code: r"'fig', 'path/to/image.jpg', None, None, None".into(), insert_path: None }), r" \begin{figure}[h!] @@ -583,6 +595,18 @@ y = 0.4 \end{figure}" } + test! {ayano_fig2_width, Token::Ayano(AyanoBlock{ + is_display: false, + is_static: false, + code: r"'fig', 'path/to/image.jpg', None, None, 0.625".into(), + insert_path: None + }), r" +\begin{figure}[h!] +\centering +\includegraphics[width = 0.625 \textwidth]{path/to/image.jpg} +\end{figure}" + } + test! {ayano_fig2, Token::Ayano(AyanoBlock{is_display: false,is_static: false,code: r"@fig: src = 'path/to/image.jpg'".into(),insert_path: None}), r" \begin{figure}[h!] diff --git a/src/lexer.rs b/src/lexer.rs index ee612f9..5edbd10 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -394,7 +394,7 @@ fn figure< input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { // TODO add width parameter to figure, I guess - let (rest, (_, (path, _, (ident, caption, _width)))) = context( + let (rest, (_, (path, _, (ident, caption, width)))) = context( "figure", pair( // first, match syntax start @@ -417,6 +417,7 @@ fn figure< src_name: Path::new(path).into(), caption: caption.map(Box::new), ident: ident.map(Cow::from), + width, }; Ok((rest, token)) } @@ -1835,13 +1836,14 @@ $$ macro_rules! fig { ($path:literal) => { - fig!($path, {ref = None::<&'static str>, caption = None}) + fig!($path, {ref = None::<&'static str>, caption = None, width = None}) }; - ($path:literal, {ref = $ident:expr, caption = $caption:expr}) => { + ($path:literal, {ref = $ident:expr, caption = $caption:expr, width = $width:expr}) => { Token::Figure { src_name: Cow::Borrowed(Path::new($path)), ident: $ident.map(Cow::from), caption: $caption.map(Box::new), + width: $width, } }; } @@ -1867,15 +1869,16 @@ $$ test_ok! {ok_empty_before, "\t\t\t\n \n\n![[path/to/file.jpg]]", fig!("path/to/file.jpg"), ""} test_ok! {ok_text_after, "\t\t\t\n \n\n![[path/to/file.jpg]] і ще щось написане", fig!("path/to/file.jpg"), " і ще щось написане"} test_ok! {ok_explicit_no_params, "\n![[path/to/file.jpg]]\n{}\n", fig!("path/to/file.jpg"), "\n"} - test_ok! {ok_ident, "\n![[path/to/file.jpg]]\n{ref = figure_of_doom }\n", fig!("path/to/file.jpg", {ref = Some("figure_of_doom"), caption = None}), "\n"} + test_ok! {ok_ident, "\n![[path/to/file.jpg]]\n{ref = figure_of_doom }\n", fig!("path/to/file.jpg", {ref = Some("figure_of_doom"), caption = None, width = None}), "\n"} test_ok! {ok_caption, "\n![[path/to/file.jpg]]\n{caption = \"Ця картинка показує графік рівняння $y = x^2$\" }\n", - fig!("path/to/file.jpg", {ref = None::<&str>, caption = Some(tks![tx!("Ця картинка показує графік рівняння"), eq!(!"y = x^2")])}), "\n"} + fig!("path/to/file.jpg", {ref = None::<&str>, caption = Some(tks![tx!("Ця картинка показує графік рівняння"), eq!(!"y = x^2")]), width = None}), "\n"} test_ok! {ok_both, "\n![[path/to/file.jpg]]\n{ ref = mc_erat, caption = \"This is a mee from our $\\text{Nomifactory}^{2*}$ world.\" }\n", - fig!("path/to/file.jpg", {ref = Some("mc_erat"), caption = Some(tks!(tx!("This is a mee from our"), eq!(!"\\text{Nomifactory}^{2*}"), tx!("world.")))}), "\n"} + fig!("path/to/file.jpg", {ref = Some("mc_erat"), caption = Some(tks!(tx!("This is a mee from our"), eq!(!"\\text{Nomifactory}^{2*}"), tx!("world."))), width = None}), "\n"} test_ok! {ok_both_reversed, "\n![[path/to/file.jpg]]\n{ caption = \"This is a meme from our $\\text{Nomifactory}^{2*}$ world.\" , ref = mc_erat }\n", - fig!("path/to/file.jpg", {ref = Some("mc_erat"), caption = Some(tks!(tx!("This is a meme from our"), eq!(!"\\text{Nomifactory}^{2*}"), tx!("world.")))}), "\n"} + fig!("path/to/file.jpg", {ref = Some("mc_erat"), caption = Some(tks!(tx!("This is a meme from our"), eq!(!"\\text{Nomifactory}^{2*}"), tx!("world."))), width = None}), "\n"} test_ok! {ok_both_left_over, "\n![[path/to/file.jpg]]\n{ caption = \"This is a meme from our $\\text{Nomifactory}^{2*}$ world.\" , ref = mc_erat }\nІще трохи тексту", - fig!("path/to/file.jpg", {ref = Some("mc_erat"), caption = Some(tks!(tx!("This is a meme from our"), eq!(!"\\text{Nomifactory}^{2*}"), tx!("world.")))}), "\nІще трохи тексту"} + fig!("path/to/file.jpg", {ref = Some("mc_erat"), caption = Some(tks!(tx!("This is a meme from our"), eq!(!"\\text{Nomifactory}^{2*}"), tx!("world."))), width = None}), "\nІще трохи тексту"} + test_ok! {ok_width, "\n![[path/to/file.jpg]]\n{ width = 0.5}\nTh else", fig!("path/to/file.jpg", {ref = None::<&str>, caption = None, width = Some(0.5)}), "\nTh else"} } macro_rules! ls { diff --git a/tests/input_files/ayano.md b/tests/input_files/ayano.md index 1f0115e..61faf1b 100644 --- a/tests/input_files/ayano.md +++ b/tests/input_files/ayano.md @@ -5,6 +5,12 @@ This is supposed to be a generated figure: @fig: src="circle.png", ident="circle", caption="Some caption for a line drawing. This is parsed as a *regular document text*, so you may [@eq:line] and $f(x) = A \cdot x^{1} + B \cdot x^{0}$ here, for example" ``` +Here's a table with specified width: +```python, Ayano +width = 0.2 +@fig: src = "graph.png", width = width +``` + This is expected to be a generated table: ```python, Ayano data = [ @@ -14,4 +20,4 @@ data = [ [-1.0, -5743234.0], ] @gen_table: lambda r,c: data[r][c]; rows=4, columns=2, caption="Caption for a table 1, *because* why not", ident="table1" -``` +``` \ No newline at end of file diff --git a/tests/input_files/basic.md b/tests/input_files/basic.md index 2c900d9..f85e090 100644 --- a/tests/input_files/basic.md +++ b/tests/input_files/basic.md @@ -16,7 +16,7 @@ $$ - Поживна цінність - 20 дптр/см ![[refurb.jpg]] -{ref = refurb, caption = "Принципове влаштування рефурбалідзера"} +{ref = refurb, caption = "Принципове влаштування рефурбалідзера", width = 0.4} На світлині [@ fig:refurb] можна бачити принципову схему влаштування рефурбалідзера. Неважно помітити великі очі[^1], і два термоядерних реактори, що оточують їх. Точні реакції всередині невідомі, але цілком можливо що там женуть $du + t -> {}^{5}_{6}C$... diff --git a/tests/lex_files/ayano.txt b/tests/lex_files/ayano.txt index 8c461f5..0a544ca 100644 --- a/tests/lex_files/ayano.txt +++ b/tests/lex_files/ayano.txt @@ -17,6 +17,18 @@ Text( insert_path: None, }, ), + Paragraph( + Normal, + "Here's a table with specified width:", + ), + Ayano( + AyanoBlock { + is_display: false, + is_static: false, + code: "width = 2\n@fig: src = \"graph.png\", width = width", + insert_path: None, + }, + ), Paragraph( Normal, "This is expected to be a generated table:", diff --git a/tests/lex_files/basic.txt b/tests/lex_files/basic.txt index 19f6cf2..db4d37a 100644 --- a/tests/lex_files/basic.txt +++ b/tests/lex_files/basic.txt @@ -71,6 +71,9 @@ Text( ident: Some( "refurb", ), + width: Some( + 0.4, + ), }, Paragraph( Normal, diff --git a/tests/transformed_files/ayano.txt b/tests/transformed_files/ayano.txt index 35d665b..f6543d1 100644 --- a/tests/transformed_files/ayano.txt +++ b/tests/transformed_files/ayano.txt @@ -5,6 +5,11 @@ \caption{\fnt Some caption for a line drawing. This is parsed as a\textit{\fnt regular document text}\fnt , so you may\ref{eq:line}\fnt and$f(x) = A \cdot x^{1} + B \cdot x^{0}$\fnt here, for example} \label{fig:circle} \end{figure} +\fnt Here's a table with specified width: +\begin{figure}[h!] +\centering +\includegraphics[width=0.2\textwidth]{graph.png} +\end{figure} \fnt This is expected to be a generated table: \begin{table}[h!] \begin{center} diff --git a/tests/transformed_files/basic.txt b/tests/transformed_files/basic.txt index fac0aa9..908a2e6 100644 --- a/tests/transformed_files/basic.txt +++ b/tests/transformed_files/basic.txt @@ -14,7 +14,7 @@ Ref(z) = \int \limits_{0}^{1} t^{z-1} \cdot \exp(-t) dt \end{itemize} \begin{figure}[h!] \centering -\includegraphics[width=0.9\textwidth]{refurb.jpg} +\includegraphics[width=0.4\textwidth]{refurb.jpg} \caption{\fnt Принципове влаштування рефурбалідзера} \label{fig:refurb} \end{figure} From 40239925176a16bbdfa98afd6877e4088a4555a5 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Mon, 18 Mar 2024 21:21:37 +0200 Subject: [PATCH 23/98] Converted all `Token` variants into normal structs for consistency --- src/data.rs | 72 ++++++++++++++++++++-------- src/gen/ayano.rs | 34 ++++++++----- src/gen/lab/meta.rs | 20 ++++---- src/gen/lab/mod.rs | 84 ++++++++++++++++----------------- src/lexer.rs | 66 +++++++++++++++++--------- tests/lex_files/ayano.txt | 26 +++++----- tests/lex_files/basic.txt | 48 +++++++++---------- tests/lex_files/deceit.txt | 60 +++++++++++------------ tests/lex_files/tables_code.txt | 72 ++++++++++++++-------------- 9 files changed, 272 insertions(+), 210 deletions(-) diff --git a/src/data.rs b/src/data.rs index cd31c12..721cc75 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1,7 +1,7 @@ use std::{ borrow::{Borrow, Cow}, ops::Deref, - path::{Path, PathBuf}, + path::Path, }; use url::Url; @@ -143,20 +143,30 @@ pub enum Token<'source> { url: Url, display: Tx<'source>, }, - Ayano(AyanoBlock<'source>), + Ayano { + data: AyanoBlock<'source>, + }, List { list_type: ListType, content: Tokens<'source>, }, #[deprecated = "`Font`-thing inside paragraph token does similar thing, so it would be wise to merge them"] Formatted(Formatting, Box>), - Text(Tokens<'source>), + Text { + tokens: Tokens<'source>, + }, // TODO move font and formatting exclusively to here // TODO add a flag to indicate, if this paragraph contains escaped chars (if it was parsed from caption, basically) Paragraph(Font, Tx<'source>), - InlineMathmode(Tx<'source>), - Reference(Tx<'source>), - FootNoteReference(Tx<'source>), + InlineMathmode { + content: Tx<'source>, + }, + Reference { + ident: Tx<'source>, + }, + FootNoteReference { + ident: Tx<'source>, + }, FootNoteContent { content: Box>, ident: Tx<'source>, @@ -165,7 +175,9 @@ pub enum Token<'source> { code: Tx<'source>, language: Option>, }, - Error(String), + Error { + message: String, + }, } trait ToStaticExt { @@ -283,7 +295,7 @@ impl<'source> Token<'source> { url: url.clone(), display: Cow::Borrowed(&display), }, - Token::Ayano(ayano) => Token::Ayano(ayano.clone()), + Token::Ayano { data } => Token::Ayano { data: data.clone() }, Token::List { list_type, content } => Token::List { list_type: list_type.clone(), content: Tokens::Borrowed(&content), @@ -291,13 +303,21 @@ impl<'source> Token<'source> { Token::Formatted(formatting, content) => { Token::Formatted(formatting.clone(), Box::new(content.borrow_ref())) } - Token::Text(text) => Token::Text(Tokens::Borrowed(text)), + Token::Text { tokens } => Token::Text { + tokens: Tokens::Borrowed(tokens), + }, Token::Paragraph(font, content) => { Token::Paragraph(font.clone(), Cow::Borrowed(content)) } - Token::InlineMathmode(content) => Token::InlineMathmode(Cow::Borrowed(&content)), - Token::Reference(ident) => Token::Reference(Cow::Borrowed(&ident)), - Token::FootNoteReference(ident) => Token::FootNoteReference(Cow::Borrowed(ident)), + Token::InlineMathmode { content } => Token::InlineMathmode { + content: Cow::Borrowed(&content), + }, + Token::Reference { ident } => Token::Reference { + ident: Cow::Borrowed(&ident), + }, + Token::FootNoteReference { ident } => Token::FootNoteReference { + ident: Cow::Borrowed(ident), + }, Token::FootNoteContent { content, ident } => Token::FootNoteContent { content: Box::new(content.borrow_ref()), ident: Cow::Borrowed(ident), @@ -306,7 +326,9 @@ impl<'source> Token<'source> { code: Cow::Borrowed(&code), language: language.as_ref().map(|c| Cow::Borrowed(c.borrow())), }, - Token::Error(error) => Token::Error(error.clone()), + Token::Error { message } => Token::Error { + message: message.clone(), + }, } } @@ -350,7 +372,9 @@ impl<'source> Token<'source> { url: url.clone(), display: display.to_static(), }, - Token::Ayano(block) => Token::Ayano(block.to_static()), + Token::Ayano { data } => Token::Ayano { + data: data.to_static(), + }, Token::List { list_type, content } => Token::List { list_type: list_type.clone(), content: content.to_static(), @@ -358,11 +382,19 @@ impl<'source> Token<'source> { Token::Formatted(formatting, inner) => { Token::Formatted(formatting.clone(), inner.to_static()) } - Token::Text(text) => Token::Text(text.to_static()), + Token::Text { tokens } => Token::Text { + tokens: tokens.to_static(), + }, Token::Paragraph(font, inner) => Token::Paragraph(font.clone(), inner.to_static()), - Token::InlineMathmode(math) => Token::InlineMathmode(math.to_static()), - Token::Reference(ident) => Token::Reference(ident.to_static()), - Token::FootNoteReference(ident) => Token::FootNoteReference(ident.to_static()), + Token::InlineMathmode { content } => Token::InlineMathmode { + content: content.to_static(), + }, + Token::Reference { ident } => Token::Reference { + ident: ident.to_static(), + }, + Token::FootNoteReference { ident } => Token::FootNoteReference { + ident: ident.to_static(), + }, Token::FootNoteContent { content, ident } => Token::FootNoteContent { content: content.to_static(), ident: ident.to_static(), @@ -371,7 +403,9 @@ impl<'source> Token<'source> { code: code.to_static(), language: language.as_ref().map(Cow::to_static), }, - Token::Error(error) => Token::Error(error.clone()), + Token::Error { message } => Token::Error { + message: message.clone(), + }, } } } diff --git a/src/gen/ayano.rs b/src/gen/ayano.rs index 0153b72..3c847ea 100644 --- a/src/gen/ayano.rs +++ b/src/gen/ayano.rs @@ -1563,11 +1563,15 @@ fn parse_ayano(python_output: &PyAny) -> Result, ParsingError> { #[cfg(test)] fn value_error_token(value: f64, error: f64) -> Token<'static> { - Token::Text(Tokens::new([ - Token::text(value.to_string()), - Token::InlineMathmode(r"\pm".into()), - Token::text(error.to_string()), - ])) + Token::Text { + tokens: Tokens::new([ + Token::text(value.to_string()), + Token::InlineMathmode { + content: r"\pm".into(), + }, + Token::text(error.to_string()), + ]), + } } #[cfg(test)] @@ -1614,9 +1618,9 @@ mod parsing_tests { test! {err1, |py| PyTuple::new(py, ["err".into_py(py), ["yet another not a number"].into_py(py)]), Err(crate::gen::ayano::ParsingError::UnexpectedSyntax("('err', ['yet another not a number'])".to_string(), py_err!(py)))} test! {value_error1, |py| PyTuple::new(py, ["err".into_py(py), 1.0.into_py(py), 0.5.into_py(py)]), - Ok(Token::Text(Tokens::new([Token::text("1.0"), Token::InlineMathmode(r"\pm".into()), Token::text("0.5")])))} + Ok(Token::Text{tokens: Tokens::new([Token::text("1.0"), Token::InlineMathmode{content:r"\pm".into()}, Token::text("0.5")])})} test! {value_error2, |py| PyTuple::new(py, ["err".into_py(py), "1.0".into_py(py), 0.5.into_py(py)]), - Ok(Token::Text(Tokens::new([Token::text("1.0"), Token::InlineMathmode(r"\pm".into()), Token::text("0.5")])))} + Ok(Token::Text{tokens:Tokens::new([Token::text("1.0"), Token::InlineMathmode{content:r"\pm".into()}, Token::text("0.5")])})} test! {value_error_format1, |py| PyTuple::new(py, ["err".into_py(py), "1.987654321".into_py(py), 0.00000123456.into_py(py)]), Ok(value_error_token(1.9876543, 0.0000012))} // TODO probably should add some more tests for value-error formatting @@ -1706,7 +1710,7 @@ mod parsing_tests { Token::text("cell_11"), Token::text("0.25"), Token::text("cell_21"), Token::text("-2"), ]), - caption: Some(Box::new(Token::Text(Tokens::new([Token::text("table caption with some"),Token::InlineMathmode("formula".into())])))), + caption: Some(Box::new(Token::Text{tokens:Tokens::new([Token::text("table caption with some"),Token::InlineMathmode{content:"formula".into()}])})), ident: Some("table1".into()) }) } @@ -2221,11 +2225,15 @@ pub fn value_error_format<'v, 'e>( ) -> Result, ParsingError> { let (value, error) = value_error_format_inner(value.into().as_ref(), error.into().as_ref())?; // Result is a combination of text tokens with mathmode +- in between - Ok(Token::Text(Tokens::new([ - Token::Paragraph(crate::data::Font::Normal, value.into()), - Token::InlineMathmode(r"\pm".into()), - Token::Paragraph(crate::data::Font::Normal, error.into()), - ]))) + Ok(Token::Text { + tokens: Tokens::new([ + Token::Paragraph(crate::data::Font::Normal, value.into()), + Token::InlineMathmode { + content: r"\pm".into(), + }, + Token::Paragraph(crate::data::Font::Normal, error.into()), + ]), + }) } fn value_error_format_inner<'v, 'e>( diff --git a/src/gen/lab/meta.rs b/src/gen/lab/meta.rs index e748a42..e8eefa3 100644 --- a/src/gen/lab/meta.rs +++ b/src/gen/lab/meta.rs @@ -6,7 +6,7 @@ use std::{ use itertools::Itertools; use pyo3::PyErr; -use crate::data::{Token, Tokens}; +use crate::data::Token; use super::super::ayano::{self, AyanoBuilder, AyanoExecutor}; @@ -86,17 +86,15 @@ where collect_single(caption, dest)?; } } - Token::Text(text) => { - collect_iterable(text, dest)?; + Token::Text { tokens } => { + collect_iterable(tokens, dest)?; } Token::FootNoteContent { content, ident } => { collect_single(content, dest)?; dest.footnotes.insert(ident, content.borrow_ref()); } - Token::Ayano(block) => { - dest.ayano - .add_block(block) - .map_err(MetaError::AyanoSyntax)?; + Token::Ayano { data } => { + dest.ayano.add_block(data).map_err(MetaError::AyanoSyntax)?; } Token::PageDiv | Token::Header { .. } @@ -104,13 +102,13 @@ where | Token::List { .. } | Token::Formatted(_, _) | Token::Paragraph(_, _) - | Token::InlineMathmode(_) - | Token::Reference(_) - | Token::FootNoteReference(_) + | Token::InlineMathmode { .. } + | Token::Reference { .. } + | Token::FootNoteReference { .. } | Token::Figure { .. } | Token::Equation { .. } | Token::CodeBlock { .. } => {} - Token::Error(_) => {} + Token::Error { .. } => {} } Ok(()) } diff --git a/src/gen/lab/mod.rs b/src/gen/lab/mod.rs index 0eb6d81..14edcd6 100644 --- a/src/gen/lab/mod.rs +++ b/src/gen/lab/mod.rs @@ -166,8 +166,8 @@ where } output.write_str("\\end{figure}\n")?; } - Token::Ayano(block) => { - let token = meta.ayano.display_token(block)?; + Token::Ayano { data } => { + let token = meta.ayano.display_token(data)?; self.write_to(output, meta, context, &token)?; } Token::List { list_type, content } => { @@ -221,13 +221,13 @@ where self.write_to(output, meta, context, content)?; output.write_str(end)?; } - Token::InlineMathmode(content) => { + Token::InlineMathmode { content } => { write!(output, "${content}$")?; } - Token::Reference(ident) => { + Token::Reference { ident } => { write!(output, "\\ref{{{ident}}}")?; } - Token::FootNoteReference(ident) => { + Token::FootNoteReference { ident } => { let (encountered, i) = { // TODO probably should move this to separate function let encountered_map = &mut context.encountered_footnotes; @@ -256,8 +256,8 @@ where } // might seem weird at first, but these are basically useless at this point, as their content was presumably collected into context Token::FootNoteContent { .. } => {} - Token::Error(error) => { - eprintln!("{error}"); + Token::Error { message } => { + eprintln!("{message}"); // TODO add representation in the document } Token::Paragraph(font, content) => { @@ -271,7 +271,7 @@ where Token::Href { url, display } => { write!(output, "\\href{{{url}}}{{{display}}}")?; } - Token::Text(content) => self.write_tokens_to(output, meta, context, content)?, + Token::Text { tokens } => self.write_tokens_to(output, meta, context, tokens)?, Token::CodeBlock { code, language } => { if let Some(language) = language { writeln!( @@ -388,7 +388,7 @@ mod tests { "} // Inline math - test! {inline_mathmode, Token::InlineMathmode(r"\dfrac{\partial x}{\partial t}".into()), r"$ \dfrac{\partial x}{\partial t} $"} + test! {inline_mathmode, Token::InlineMathmode{content:r"\dfrac{\partial x}{\partial t}".into()}, r"$ \dfrac{\partial x}{\partial t} $"} // Figures test! {fig1, Token::Figure { src_name: Path::new("apple.jpg").into(), caption: Some(Box::new(text!("a very realistic-looking apple"))), ident: None, width: None }, r" @@ -479,35 +479,35 @@ mod tests { "} // Text - test! {text, Token::Text(tokens![Token::PageDiv, Token::Header { order: 0, content: "Header".into() }, text!("Here's a little formula: "), Token::InlineMathmode(r"(a+b)^2 = a^2 + 2 \cdot a \cdot b + b^2".into()), text!(". It's very useful, more useful than me!")]), r" + test! {text, Token::Text{tokens:tokens![Token::PageDiv, Token::Header { order: 0, content: "Header".into() }, text!("Here's a little formula: "), Token::InlineMathmode{content:r"(a+b)^2 = a^2 + 2 \cdot a \cdot b + b^2".into()}, text!(". It's very useful, more useful than me!")]}, r" \clearpage \section{Header} \fnt Here's a little formula: $(a+b)^2 = a^2 + 2 \cdot a \cdot b + b^2$\fnt . It's very useful, more useful than me! "} // Footnotes - test! {footnote_single, Token::Text(tokens![ - Token::FootNoteReference("explanation".into()), + test! {footnote_single, Token::Text{tokens:tokens![ + Token::FootNoteReference{ident:"explanation".into()}, Token::FootNoteContent { content: Box::new(text!("42")), ident: "explanation".into() } - ]), r" + ]}, r" \footnotemark[1] \footnotetext[1]{\fnt 42} "} - test! {footnote_multiple, Token::Text(tokens![ + test! {footnote_multiple, Token::Text{tokens:tokens![ text!("Here's a first note"), - Token::FootNoteReference("explanation1".into()), + Token::FootNoteReference{ident:"explanation1".into()}, text!(", there are also second"), - Token::FootNoteReference("explanation2".into()), + Token::FootNoteReference{ident: "explanation2".into()}, text!(", and the third"), - Token::FootNoteReference("explanation3".into()), + Token::FootNoteReference{ident:"explanation3".into()}, Token::FootNoteContent { content: Box::new(text!("There are things in this world that's you're not meant to see")), ident: "explanation1".into() }, Token::FootNoteContent { content: Box::new(text!("now the voice of a deity permeates")), ident: "explanation2".into() }, Token::FootNoteContent { content: Box::new(text!("see notes 1 and 2")), ident: "explanation3".into() }, text!(". But notes will not be duplicated here!"), - Token::FootNoteReference("explanation1".into()) + Token::FootNoteReference{ident:"explanation1".into()} - ]), r" + ]}, r" \fnt Here's a first note \footnotemark[1] \footnotetext[1]{\fnt There are things in this world that's you're not meant to see} @@ -521,19 +521,19 @@ mod tests { \footnotemark[1] "} - test! {ayano_plain1, Token::Ayano(AyanoBlock{ + test! {ayano_plain1, Token::Ayano{data: AyanoBlock{ is_display: false, is_static: false, code: "1".into(), insert_path: None - }), r"\fnt 1"} - test! {ayano_plain2, Token::Ayano(AyanoBlock{ + }}, r"\fnt 1"} + test! {ayano_plain2, Token::Ayano{data: AyanoBlock{ is_display: false, is_static: false, code: "1 + 2".into(), insert_path: None - }), r"\fnt 3"} - test! {ayano_plain3, Token::Ayano(AyanoBlock{ + }}, r"\fnt 3"} + test! {ayano_plain3, Token::Ayano{data: AyanoBlock{ is_display: false, is_static: false, code: r" @@ -543,8 +543,8 @@ y = sqrt(x) int(y)" .into(), insert_path: None - }), r"\fnt 10"} - test! {ayano_plain4, Token::Ayano(AyanoBlock{ + }}, r"\fnt 10"} + test! {ayano_plain4, Token::Ayano{data: AyanoBlock{ is_display: false, is_static: false, code: r" @@ -554,24 +554,24 @@ for i in range(101): res" .into(), insert_path: None - }), r"\fnt 5050"} - test! {ayano_err1, Token::Ayano(AyanoBlock{ + }}, r"\fnt 5050"} + test! {ayano_err1, Token::Ayano{data: AyanoBlock{ is_display: false, is_static: false, code: r" 'err', 1.0, 0.1" .into(), insert_path: None - }), r"\fnt 1.00 $\pm$ \fnt 0.10"} - test! {ayano_err2, Token::Ayano(AyanoBlock{ + }}, r"\fnt 1.00 $\pm$ \fnt 0.10"} + test! {ayano_err2, Token::Ayano{data: AyanoBlock{ is_display: false, is_static: false, code: r" 'err', 1.0, 0.4" .into(), insert_path: None - }), r"\fnt 1.0 $\pm$ \fnt 0.4"} - test! {ayano_err3, Token::Ayano(AyanoBlock{ + }}, r"\fnt 1.0 $\pm$ \fnt 0.4"} + test! {ayano_err3, Token::Ayano{data: AyanoBlock{ is_display: false, is_static: false, code: r" @@ -580,41 +580,41 @@ y = 0.4 @dev: x, y" .into(), insert_path: None - }), r"\fnt 1.0 $\pm$ \fnt 0.4" + }}, r"\fnt 1.0 $\pm$ \fnt 0.4" } - test! {ayano_fig1, Token::Ayano(AyanoBlock{ + test! {ayano_fig1, Token::Ayano{data: AyanoBlock{ is_display: false, is_static: false, code: r"'fig', 'path/to/image.jpg', None, None, None".into(), insert_path: None - }), r" + }}, r" \begin{figure}[h!] \centering \includegraphics[width = 0.9 \textwidth]{path/to/image.jpg} \end{figure}" } - test! {ayano_fig2_width, Token::Ayano(AyanoBlock{ + test! {ayano_fig2_width, Token::Ayano{data: AyanoBlock{ is_display: false, is_static: false, code: r"'fig', 'path/to/image.jpg', None, None, 0.625".into(), insert_path: None - }), r" + }}, r" \begin{figure}[h!] \centering \includegraphics[width = 0.625 \textwidth]{path/to/image.jpg} \end{figure}" } - test! {ayano_fig2, Token::Ayano(AyanoBlock{is_display: false,is_static: false,code: r"@fig: src = 'path/to/image.jpg'".into(),insert_path: None}), + test! {ayano_fig2, Token::Ayano{data: AyanoBlock{is_display: false,is_static: false,code: r"@fig: src = 'path/to/image.jpg'".into(),insert_path: None}}, r" \begin{figure}[h!] \centering \includegraphics[width = 0.9 \textwidth]{path/to/image.jpg} \end{figure}"} - test! {ayano_fig3, Token::Ayano(AyanoBlock{ is_display: false,is_static: false,code: r#"@fig: src = 'path/to/image.jpg', ident = "meow""#.into(),insert_path: None - }), + test! {ayano_fig3, Token::Ayano{data: AyanoBlock{ is_display: false,is_static: false,code: r#"@fig: src = 'path/to/image.jpg', ident = "meow""#.into(),insert_path: None + }}, r#" \begin{figure}[h!] \centering @@ -624,11 +624,11 @@ r#" } // TODO add tests with captions - test! {ayano_gen_tab1, Token::Ayano(AyanoBlock { is_display: false, is_static: false, code: + test! {ayano_gen_tab1, Token::Ayano{data: AyanoBlock { is_display: false, is_static: false, code: r#" data = [[1, 2, 3], [4, 5, 6]] @gen_table: lambda r,c: data[r][c]; rows=2, columns=3 -"#.into(), insert_path: None }), +"#.into(), insert_path: None }}, r#" \begin{table}[h!] \begin{center} diff --git a/src/lexer.rs b/src/lexer.rs index 5edbd10..389fd3b 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -570,12 +570,14 @@ fn ayano< let code: Cow<'_, str> = Cow::from(code); Ok(( rest, - Token::Ayano(AyanoBlock { - insert_path: insert.map(Cow::from), - code, - is_display: display.is_some(), - is_static: is_static.is_some(), - }), + Token::Ayano { + data: AyanoBlock { + insert_path: insert.map(Cow::from), + code, + is_display: display.is_some(), + is_static: is_static.is_some(), + }, + }, )) } @@ -806,7 +808,9 @@ fn inline_math<'source, E: ParseError<&'source str> + ContextError<&'source str> delimited(char('$'), take_till1(|c| c == '\n' || c == '$'), char('$')), ), ) - .map(|content: &str| Token::InlineMathmode(content.trim().into())) + .map(|content: &str| Token::InlineMathmode { + content: content.trim().into(), + }) .parse(input) } @@ -831,7 +835,12 @@ fn reference<'source, E: ParseError<&'source str> + ContextError<&'source str>>( pair(space0, char(']')), ), )(input)?; - Ok((rest, Token::Reference(ident.trim().into()))) + Ok(( + rest, + Token::Reference { + ident: ident.trim().into(), + }, + )) } /// Parses a footnote reference @@ -849,7 +858,12 @@ fn footnote_reference<'source, E: ParseError<&'source str> + ContextError<&'sour char(']'), ), )(input)?; - Ok((rest, Token::FootNoteReference(ident.trim().into()))) + Ok(( + rest, + Token::FootNoteReference { + ident: ident.trim().into(), + }, + )) } /// Parses definition of a footnote content @@ -1226,7 +1240,9 @@ fn tokens_many1< |inner_result| match inner_result { InnerResult::None => Err(KyomatoLexError::no_tokens(outer_input)), InnerResult::Single(token) => Ok(token), - InnerResult::Multiple(tokens) => Ok(Token::Text(Tokens::new(tokens))), + InnerResult::Multiple(tokens) => Ok(Token::Text { + tokens: Tokens::new(tokens), + }), }, ) .parse(outer_input) @@ -1603,7 +1619,7 @@ $$ macro_rules! test_ok { {$name:ident, $input:literal, $formula:literal, $left_over:literal} => { - test!{$name, inline_math, $input, e: Ok(($left_over, Token::InlineMathmode($formula.into())))} + test!{$name, inline_math, $input, e: Ok(($left_over, Token::InlineMathmode{content:$formula.into()}))} }; } @@ -1629,7 +1645,7 @@ $$ macro_rules! test_ok { {$name:ident, $input:literal, $reference:literal, $left_over:literal} => { - test!{$name, reference, $input, e: Ok(($left_over, Token::Reference($reference.into())))} + test!{$name, reference, $input, e: Ok(($left_over, Token::Reference{ident: $reference.into()}))} }; } @@ -1648,7 +1664,7 @@ $$ macro_rules! test_ok { {$name:ident, $input:literal, $reference:literal, $left_over:literal} => { - test!{$name, footnote_reference, $input, e: Ok(($left_over, Token::FootNoteReference($reference.into())))} + test!{$name, footnote_reference, $input, e: Ok(($left_over, Token::FootNoteReference{ident:$reference.into()}))} }; } @@ -1677,12 +1693,12 @@ $$ test_ok! {ok_formatting_in_formatting, "\t \n You can even use ~~__double-formatted__~~ things!", "You can even use", Some(Token::Formatted(Formatting::StrikeThrough, Box::new(Token::Formatted(Formatting::Bold, Box::new(Token::text("double-formatted")))))), " things!"} test_ok! {ok_equation, " \t Inline math is fine too: $y = x_{\text{поч}} + x_0$.", "Inline math is fine too:", - Some(Token::InlineMathmode("y = x_{\text{поч}} + x_0".into())), "."} + Some(Token::InlineMathmode{content: "y = x_{\text{поч}} + x_0".into()}), "."} test_ok! {ok_href, " \t\t\n You can even [залишити посилання на свою сторінку на Гітхабі ](https://www.github.com/Dzuchun)!", "You can even", Some(Token::Href { url: "https://www.github.com/Dzuchun".parse().expect("That's a valid url"), display: "залишити посилання на свою сторінку на Гітхабі".into() }), "!"} - test_ok! {ok_footnote_ref, "\t Footnote refs[^1] are also ok too!", "Footnote refs", Some(Token::FootNoteReference("1".into())), " are also ok too!"} + test_ok! {ok_footnote_ref, "\t Footnote refs[^1] are also ok too!", "Footnote refs", Some(Token::FootNoteReference{ident: "1".into()}), " are also ok too!"} test_ok! {ok_obj_ref, "\n\t Thi[[ngs lik_e tables* ([@tab:results]) can also be referenced", "Thi[[ngs lik_e tables* (", - Some(Token::Reference("tab:results".into())), ") can also be referenced"} + Some(Token::Reference{ident: "tab:results".into()}), ") can also be referenced"} } // `inner_lex` tests @@ -1707,7 +1723,9 @@ $$ } }; (!$eq:literal) => { - Token::InlineMathmode(Cow::Borrowed($eq)) + Token::InlineMathmode { + content: Cow::Borrowed($eq), + } }; } macro_rules! hr { @@ -1734,10 +1752,14 @@ $$ } macro_rules! rf { (@ $ident:literal) => { - Token::Reference(Cow::Borrowed($ident)) + Token::Reference { + ident: Cow::Borrowed($ident), + } }; (^ $ident:literal) => { - Token::FootNoteReference(Cow::Borrowed($ident)) + Token::FootNoteReference { + ident: Cow::Borrowed($ident), + } }; } macro_rules! tx { @@ -1747,7 +1769,7 @@ $$ } macro_rules! tks { [$($token:expr), +] => { - Token::Text(Tokens::new([$($token), +])) + Token::Text{tokens: Tokens::new([$($token), +])} }; } mod inner_lex { @@ -1982,12 +2004,12 @@ $$ macro_rules! ayano { {!$is_static:literal, *$description:expr, ~$insert_path:expr, #$_id:expr, $code:literal} => { - Token::Ayano(AyanoBlock{ + Token::Ayano{data: AyanoBlock{ is_display: $description.is_some(), is_static: $is_static, code: Cow::Borrowed($code), insert_path: $insert_path.map(Path::new).map(Cow::from), - }) + }} }; } mod ayano { diff --git a/tests/lex_files/ayano.txt b/tests/lex_files/ayano.txt index 0a544ca..64f6aa3 100644 --- a/tests/lex_files/ayano.txt +++ b/tests/lex_files/ayano.txt @@ -1,5 +1,5 @@ -Text( - Owned( +Text { + tokens: Owned( [ Paragraph( Normal, @@ -9,38 +9,38 @@ Text( Normal, "This is supposed to be a generated figure:", ), - Ayano( - AyanoBlock { + Ayano { + data: AyanoBlock { is_display: false, is_static: false, code: "@fig: src=\"circle.png\", ident=\"circle\", caption=\"Some caption for a line drawing. This is parsed as a *regular document text*, so you may [@eq:line] and $f(x) = A \\cdot x^{1} + B \\cdot x^{0}$ here, for example\"", insert_path: None, }, - ), + }, Paragraph( Normal, "Here's a table with specified width:", ), - Ayano( - AyanoBlock { + Ayano { + data: AyanoBlock { is_display: false, is_static: false, - code: "width = 2\n@fig: src = \"graph.png\", width = width", + code: "width = 0.2\n@fig: src = \"graph.png\", width = width", insert_path: None, }, - ), + }, Paragraph( Normal, "This is expected to be a generated table:", ), - Ayano( - AyanoBlock { + Ayano { + data: AyanoBlock { is_display: false, is_static: false, code: "data = [\n\t\t[\"x\", \"y\"],\n\t\t[1.0, 3.0],\n\t\t[3.0, 4.0],\n\t\t[-1.0, -5743234.0],\n]\n@gen_table: lambda r,c: data[r][c]; rows=4, columns=2, caption=\"Caption for a table 1, *because* why not\", ident=\"table1\"", insert_path: None, }, - ), + }, ], ), -) \ No newline at end of file +} \ No newline at end of file diff --git a/tests/lex_files/basic.txt b/tests/lex_files/basic.txt index db4d37a..d7128ef 100644 --- a/tests/lex_files/basic.txt +++ b/tests/lex_files/basic.txt @@ -1,5 +1,5 @@ -Text( - Owned( +Text { + tokens: Owned( [ Header { order: 1, @@ -27,32 +27,32 @@ Text( list_type: Bullet, content: Owned( [ - Text( - Owned( + Text { + tokens: Owned( [ Paragraph( Normal, "Нормувальний множник", ), - InlineMathmode( - "|\\psi|^{2} = 3/4", - ), + InlineMathmode { + content: "|\\psi|^{2} = 3/4", + }, ], ), - ), - Text( - Owned( + }, + Text { + tokens: Owned( [ Paragraph( Normal, "Кількість хромосом -", ), - InlineMathmode( - "\\exp(\\pi)", - ), + InlineMathmode { + content: "\\exp(\\pi)", + }, ], ), - ), + }, Paragraph( Normal, "Поживна цінність - 20 дптр/см", @@ -79,23 +79,23 @@ Text( Normal, "На світлині", ), - Reference( - "fig:refurb", - ), + Reference { + ident: "fig:refurb", + }, Paragraph( Normal, "можна бачити принципову схему влаштування рефурбалідзера. Неважно помітити великі очі", ), - FootNoteReference( - "1", - ), + FootNoteReference { + ident: "1", + }, Paragraph( Normal, ", і два термоядерних реактори, що оточують їх. Точні реакції всередині невідомі, але цілком можливо що там женуть", ), - InlineMathmode( - "du + t -> {}^{5}_{6}C", - ), + InlineMathmode { + content: "du + t -> {}^{5}_{6}C", + }, Paragraph( Normal, "...", @@ -109,4 +109,4 @@ Text( }, ], ), -) \ No newline at end of file +} \ No newline at end of file diff --git a/tests/lex_files/deceit.txt b/tests/lex_files/deceit.txt index 0a63cab..f191bae 100644 --- a/tests/lex_files/deceit.txt +++ b/tests/lex_files/deceit.txt @@ -1,5 +1,5 @@ -Text( - Owned( +Text { + tokens: Owned( [ Paragraph( Normal, @@ -105,9 +105,9 @@ Text( Normal, "Inline:", ), - InlineMathmode( - "y = x ^ 2", - ), + InlineMathmode { + content: "y = x ^ 2", + }, Paragraph( Normal, "Display:", @@ -270,16 +270,16 @@ Text( Normal, "Here are correct footnote refs:", ), - FootNoteReference( - "1", - ), + FootNoteReference { + ident: "1", + }, Paragraph( Normal, ",", ), - FootNoteReference( - "source", - ), + FootNoteReference { + ident: "source", + }, Paragraph( Normal, "Here are incorrect ones: [^1, ^1],", @@ -316,9 +316,9 @@ Text( Normal, "Here's a correct ref:", ), - Reference( - "fig:experiment_schema", - ), + Reference { + ident: "fig:experiment_schema", + }, Paragraph( Normal, "Here's are bad ones: [@ fig:1, @ source3],", @@ -430,9 +430,9 @@ Text( Normal, "These start with '", ), - FootNoteReference( - "ID", - ), + FootNoteReference { + ident: "ID", + }, Paragraph( Normal, ":' and proceed to the end of line.", @@ -456,16 +456,16 @@ Text( Normal, "[^source2: This is invalid content", ), - FootNoteReference( - "source2", - ), + FootNoteReference { + ident: "source2", + }, Paragraph( Normal, ": This is invalid content", ), - FootNoteReference( - "source2", - ), + FootNoteReference { + ident: "source2", + }, Paragraph( Normal, ": This is invalid content", @@ -474,9 +474,9 @@ Text( Normal, "They also must start at the beginning of the line. If fact, this:", ), - FootNoteReference( - "source2", - ), + FootNoteReference { + ident: "source2", + }, Paragraph( Normal, ": Some content", @@ -601,14 +601,14 @@ Text( Normal, "This Ayano block will leave plain \"2\" behind it:", ), - Ayano( - AyanoBlock { + Ayano { + data: AyanoBlock { is_display: false, is_static: false, code: "2", insert_path: None, }, - ), + }, Paragraph( Normal, "Akin to normal blocks, it's ok to leave something trailing:", @@ -647,4 +647,4 @@ Text( ), ], ), -) \ No newline at end of file +} \ No newline at end of file diff --git a/tests/lex_files/tables_code.txt b/tests/lex_files/tables_code.txt index bea8f77..a903e66 100644 --- a/tests/lex_files/tables_code.txt +++ b/tests/lex_files/tables_code.txt @@ -1,5 +1,5 @@ -Text( - Owned( +Text { + tokens: Owned( [ Paragraph( Normal, @@ -62,26 +62,26 @@ Text( Normal, "2", ), - InlineMathmode( - "3 + e ^{\\pi}", - ), - Text( - Owned( + InlineMathmode { + content: "3 + e ^{\\pi}", + }, + Text { + tokens: Owned( [ - InlineMathmode( - "e", - ), - FootNoteReference( - "euler", - ), + InlineMathmode { + content: "e", + }, + FootNoteReference { + ident: "euler", + }, ], ), - ), - InlineMathmode( - "\\cdot \\pm 1", - ), - Text( - Owned( + }, + InlineMathmode { + content: "\\cdot \\pm 1", + }, + Text { + tokens: Owned( [ Paragraph( Normal, @@ -100,12 +100,12 @@ Text( ), ], ), - ), + }, ], ), caption: Some( - Text( - Owned( + Text { + tokens: Owned( [ Paragraph( Normal, @@ -124,7 +124,7 @@ Text( ), ], ), - ), + }, ), ident: Some( "example_1", @@ -173,8 +173,8 @@ Text( ), }, FootNoteContent { - content: Text( - Owned( + content: Text { + tokens: Owned( [ Paragraph( Normal, @@ -204,7 +204,7 @@ Text( ), ], ), - ), + }, ident: "euler", }, Header { @@ -215,9 +215,9 @@ Text( Normal, "Наразі мають досить обмежену підтримку, адже у", ), - InlineMathmode( - "\\LaTeX", - ), + InlineMathmode { + content: "\\LaTeX", + }, Paragraph( Normal, "немає нативного поняття блоків коду.", @@ -237,9 +237,9 @@ Text( Normal, "таке поняття є", ), - FootNoteReference( - "code", - ), + FootNoteReference { + ident: "code", + }, Paragraph( Normal, ":", @@ -251,8 +251,8 @@ Text( ), }, FootNoteContent { - content: Text( - Owned( + content: Text { + tokens: Owned( [ Paragraph( Normal, @@ -271,9 +271,9 @@ Text( ), ], ), - ), + }, ident: "code", }, ], ), -) \ No newline at end of file +} \ No newline at end of file From 520d85e85dc3d47b761799c21bb28380b7498c1e Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Tue, 19 Mar 2024 00:17:05 +0200 Subject: [PATCH 24/98] Reworked text tokens - Added a field to indicate that paragraph is preceded by newline, for better document structure preservation - Removed font-thing. Font is now assigned statically, at generation step - Moved formatting to text paragraph --- src/data.rs | 41 +- src/gen/ayano.rs | 23 +- src/gen/lab/meta.rs | 4 +- src/gen/lab/mod.rs | 123 +-- src/lexer.rs | 288 ++++--- tests/lex_files/ayano.txt | 36 +- tests/lex_files/basic.txt | 99 ++- tests/lex_files/deceit.txt | 1011 ++++++++++++----------- tests/lex_files/tables_code.txt | 366 ++++---- tests/transformed_files/ayano.txt | 23 +- tests/transformed_files/basic.txt | 17 +- tests/transformed_files/deceit.txt | 130 ++- tests/transformed_files/tables_code.txt | 30 +- 13 files changed, 1251 insertions(+), 940 deletions(-) diff --git a/src/data.rs b/src/data.rs index 721cc75..3b81a9e 100644 --- a/src/data.rs +++ b/src/data.rs @@ -103,13 +103,6 @@ pub enum Formatting { StrikeThrough, } -#[derive(Debug, Clone, PartialEq)] -// #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum Font { - Normal, - Caption, -} - type Tx<'source> = Cow<'source, str>; type Pth<'source> = Cow<'source, Path>; @@ -157,7 +150,11 @@ pub enum Token<'source> { }, // TODO move font and formatting exclusively to here // TODO add a flag to indicate, if this paragraph contains escaped chars (if it was parsed from caption, basically) - Paragraph(Font, Tx<'source>), + Paragraph { + is_newline: bool, + formatting: Option, + content: Tx<'source>, + }, InlineMathmode { content: Tx<'source>, }, @@ -252,7 +249,11 @@ impl ToStaticExt for AyanoBlock<'_> { impl<'source> Token<'source> { pub fn text(text: impl Into>) -> Self { - Self::Paragraph(Font::Normal, text.into()) + Self::Paragraph { + is_newline: false, + content: text.into(), + formatting: None, + } } pub fn borrow_ref<'r>(&'r self) -> Token<'r> @@ -306,9 +307,15 @@ impl<'source> Token<'source> { Token::Text { tokens } => Token::Text { tokens: Tokens::Borrowed(tokens), }, - Token::Paragraph(font, content) => { - Token::Paragraph(font.clone(), Cow::Borrowed(content)) - } + Token::Paragraph { + is_newline, + formatting, + content, + } => Token::Paragraph { + is_newline: *is_newline, + formatting: formatting.clone(), + content: Cow::Borrowed(content), + }, Token::InlineMathmode { content } => Token::InlineMathmode { content: Cow::Borrowed(&content), }, @@ -385,7 +392,15 @@ impl<'source> Token<'source> { Token::Text { tokens } => Token::Text { tokens: tokens.to_static(), }, - Token::Paragraph(font, inner) => Token::Paragraph(font.clone(), inner.to_static()), + Token::Paragraph { + is_newline, + formatting, + content, + } => Token::Paragraph { + is_newline: *is_newline, + formatting: formatting.clone(), + content: content.to_static(), + }, Token::InlineMathmode { content } => Token::InlineMathmode { content: content.to_static(), }, diff --git a/src/gen/ayano.rs b/src/gen/ayano.rs index 3c847ea..9a66356 100644 --- a/src/gen/ayano.rs +++ b/src/gen/ayano.rs @@ -20,7 +20,7 @@ use std::{ }; use crate::{ - data::{AyanoBlock, Font, Token, Tokens}, + data::{AyanoBlock, Token, Tokens}, lexer::KyomatoLexError, util::{ Equivalent, GenericRange, GenericRangeParseError, Immutable, InlinedStack, @@ -1555,10 +1555,11 @@ fn parse_ayano(python_output: &PyAny) -> Result, ParsingError> { } } // any sort of parsing had failed - return regular text - Ok(Token::Paragraph( - Font::Normal, - Cow::Owned(python_output.to_string()), - )) + Ok(Token::Paragraph { + is_newline: false, + formatting: None, + content: Cow::Owned(python_output.to_string()), + }) } #[cfg(test)] @@ -2227,11 +2228,19 @@ pub fn value_error_format<'v, 'e>( // Result is a combination of text tokens with mathmode +- in between Ok(Token::Text { tokens: Tokens::new([ - Token::Paragraph(crate::data::Font::Normal, value.into()), + Token::Paragraph { + is_newline: false, + formatting: None, + content: value.into(), + }, Token::InlineMathmode { content: r"\pm".into(), }, - Token::Paragraph(crate::data::Font::Normal, error.into()), + Token::Paragraph { + is_newline: false, + formatting: None, + content: error.into(), + }, ]), }) } diff --git a/src/gen/lab/meta.rs b/src/gen/lab/meta.rs index e8eefa3..0168d6d 100644 --- a/src/gen/lab/meta.rs +++ b/src/gen/lab/meta.rs @@ -86,7 +86,7 @@ where collect_single(caption, dest)?; } } - Token::Text { tokens } => { + Token::Text { tokens, .. } => { collect_iterable(tokens, dest)?; } Token::FootNoteContent { content, ident } => { @@ -101,7 +101,7 @@ where | Token::Href { .. } | Token::List { .. } | Token::Formatted(_, _) - | Token::Paragraph(_, _) + | Token::Paragraph { .. } | Token::InlineMathmode { .. } | Token::Reference { .. } | Token::FootNoteReference { .. } diff --git a/src/gen/lab/mod.rs b/src/gen/lab/mod.rs index 14edcd6..0d33d57 100644 --- a/src/gen/lab/mod.rs +++ b/src/gen/lab/mod.rs @@ -6,7 +6,10 @@ pub use meta::SourceMeta; use itertools::Itertools; -use crate::{data::Token, path_engine::PathEngine}; +use crate::{ + data::{Formatting, Token}, + path_engine::PathEngine, +}; use std::{borrow::Borrow, fmt::Write, marker::PhantomData}; use super::{ayano::AyanoExecutor, GenerationError, OutputGenerator, Res}; @@ -112,7 +115,7 @@ where Ok::<(), GenerationError>(()) }) .try_collect()?; - output.write_str("|}\n")?; + output.write_str("|}\\hline\n")?; // write header line self.write_table_row(output, meta, context, header)?; output.write_str("\\hline\n")?; @@ -128,7 +131,7 @@ where } output.write_str("\\end{tabular}\n\\stepcounter{tabnum}")?; if let Some(caption) = caption { - output.write_str("\n\\caption{")?; + output.write_str("\n\\caption{\\capfnt ")?; self.write_to(output, meta, context, caption)?; output.write_str("}\n")?; } @@ -157,7 +160,7 @@ where output.write_str(src_path.to_string_lossy().borrow())?; // FIXME non-utf paths *might* cause an issue here output.write_str("}\n")?; if let Some(caption) = caption.as_ref() { - output.write_str("\\caption{")?; + output.write_str("\\caption{\\capfnt ")?; self.write_to(output, meta, context, caption)?; output.write_str("}\n")?; } @@ -260,13 +263,23 @@ where eprintln!("{message}"); // TODO add representation in the document } - Token::Paragraph(font, content) => { - // TODO that's to be rewritten! - let font = match font { - crate::data::Font::Normal => "fnt", - crate::data::Font::Caption => "capfnt", + Token::Paragraph { + is_newline, + formatting, + content, + } => { + if *is_newline { + output.write_str("\n\\tbln ")?; + } + let (start, end) = match formatting { + None => ("", ""), + Some(Formatting::Bold) => ("\\textbf{", "}"), + Some(Formatting::Italic) => ("\\textit{", "}"), + Some(Formatting::StrikeThrough) => ("\\st{", "}"), }; - write!(output, "\\{font} {content}")?; + output.write_str(start)?; + output.write_str(&content)?; + output.write_str(end)?; } Token::Href { url, display } => { write!(output, "\\href{{{url}}}{{{display}}}")?; @@ -291,13 +304,17 @@ where mod tests { use std::path::Path; - use crate::data::{AyanoBlock, Font, Formatting, ListType}; + use crate::data::{AyanoBlock, Formatting, ListType}; use super::*; macro_rules! text { ($text:literal) => { - Token::Paragraph(Font::Normal, $text.into()) + Token::Paragraph { + is_newline: false, + formatting: None, + content: $text.into(), + } }; } @@ -359,13 +376,13 @@ mod tests { test! {header5, Token::Header { order: 5, content: "HeaderText".into() }, r"\textbf{HeaderText}"} // Paragraphs - test! {para1, Token::Paragraph(Font::Normal, "content, content!!!".into()), r"\fnt content, content!!!"} - test! {para2, Token::Paragraph(Font::Caption, "content, content!!!".into()), r"\capfnt content, content!!!"} + test! {para1, Token::Paragraph{is_newline: false, formatting: None, content: "content, content!!!".into()}, "content, content!!!"} + test! {para2, Token::Paragraph{is_newline: true, formatting: None, content: "content, content!!!".into()}, "\n\\tbln content, content!!!"} // Formatting - test! {formatting1, Token::Formatted(Formatting::Bold, Box::new(text!("some text, idk"))), r"\textbf{ \fnt some text, idk }"} - test! {formatting2, Token::Formatted(Formatting::Italic, Box::new(text!("some text, idk"))), r"\textit{ \fnt some text, idk }"} - test! {formatting3, Token::Formatted(Formatting::StrikeThrough, Box::new(text!("some text, idk"))), r"\st{ \fnt some text, idk }"} + test! {formatting1, Token::Paragraph{is_newline: false, formatting: Some(Formatting::Bold), content: "some text, idk".into()}, "\\textbf{some text, idk}"} + test! {formatting2, Token::Paragraph{is_newline: false, formatting: Some(Formatting::Italic), content: "some text, idk".into()}, "\\textit{some text, idk}"} + test! {formatting3, Token::Paragraph{is_newline: false, formatting: Some(Formatting::StrikeThrough), content: "some text, idk".into()}, "\\st{some text, idk}"} // Hrefs test! {href1, Token::Href { url: "https://www.github.com/Dzuchun".parse().unwrap(), display: "My gh page".into() }, r"\href{https://www.github.com/Dzuchun}{My gh page}"} @@ -395,14 +412,14 @@ mod tests { \begin{figure}[h!] \centering \includegraphics[width = 0.9 \textwidth]{apple.jpg} - \caption{\fnt a very realistic-looking apple} + \caption{\capfnt a very realistic-looking apple} \end{figure} "} test! {fig2, Token::Figure { src_name: Path::new("schema1.jpg").into(), caption: Some(Box::new(text!("A schema providing a bunch of very important info on quantum refurbalidzer function"))), ident: Some("schema-1".into()), width: None }, r" \begin{figure}[h!] \centering \includegraphics[width = 0.9 \textwidth]{schema1.jpg} - \caption{\fnt A schema providing a bunch of very important info on quantum refurbalidzer function} + \caption{\capfnt A schema providing a bunch of very important info on quantum refurbalidzer function} \label{fig:schema-1} \end{figure} "} @@ -410,7 +427,7 @@ mod tests { \begin{figure}[h!] \centering \includegraphics[width = 0.25 \textwidth]{schema1.jpg} - \caption{\fnt A schema providing a bunch of very important info on quantum refurbalidzer function} + \caption{\capfnt A schema providing a bunch of very important info on quantum refurbalidzer function} \label{fig:schema-1} \end{figure} "} @@ -429,13 +446,13 @@ mod tests { r" \begin{table}[h!] \begin{center} - \begin{tabular}{|c|c|c|} -\fnt header1 & \fnt header2 & \fnt header3 \\ \hline \hline -\fnt cell_11 & \fnt cell_12 & \fnt cell_13 \\ \hline -\fnt cell_21 & \fnt cell_22 & \fnt cell_23 \\ \hline + \begin{tabular}{|c|c|c|}\hline +header1 & header2 & header3 \\ \hline \hline +cell_11 & cell_12 & cell_13 \\ \hline +cell_21 & cell_22 & cell_23 \\ \hline \end{tabular} \stepcounter{tabnum} - \caption{ \fnt caption...} + \caption{ \capfnt caption...} \label{tab:example} \end{center} \end{table} @@ -448,9 +465,9 @@ mod tests { text!("point3") ] }, r" \begin{itemize} - \item \fnt point1 - \item \fnt point2 - \item \fnt point3 + \item point1 + \item point2 + \item point3 \end{itemize} "} @@ -460,9 +477,9 @@ mod tests { text!("point3") ] }, r" \begin{enumerate} - \item \fnt point1 - \item \fnt point2 - \item \fnt point3 + \item point1 + \item point2 + \item point3 \end{enumerate} "} @@ -472,9 +489,9 @@ mod tests { text!("point3") ] }, r" \begin{enumerate}[label=\asbuk*), ref=\asbuk*] - \item \fnt point1 - \item \fnt point2 - \item \fnt point3 + \item point1 + \item point2 + \item point3 \end{enumerate} "} @@ -482,7 +499,7 @@ mod tests { test! {text, Token::Text{tokens:tokens![Token::PageDiv, Token::Header { order: 0, content: "Header".into() }, text!("Here's a little formula: "), Token::InlineMathmode{content:r"(a+b)^2 = a^2 + 2 \cdot a \cdot b + b^2".into()}, text!(". It's very useful, more useful than me!")]}, r" \clearpage \section{Header} - \fnt Here's a little formula: $(a+b)^2 = a^2 + 2 \cdot a \cdot b + b^2$\fnt . It's very useful, more useful than me! + Here's a little formula: $(a+b)^2 = a^2 + 2 \cdot a \cdot b + b^2$. It's very useful, more useful than me! "} // Footnotes @@ -491,7 +508,7 @@ mod tests { Token::FootNoteContent { content: Box::new(text!("42")), ident: "explanation".into() } ]}, r" \footnotemark[1] - \footnotetext[1]{\fnt 42} + \footnotetext[1]{42} "} test! {footnote_multiple, Token::Text{tokens:tokens![ @@ -508,16 +525,16 @@ mod tests { Token::FootNoteReference{ident:"explanation1".into()} ]}, r" - \fnt Here's a first note + Here's a first note \footnotemark[1] - \footnotetext[1]{\fnt There are things in this world that's you're not meant to see} - \fnt , there are also second + \footnotetext[1]{There are things in this world that's you're not meant to see} + , there are also second \footnotemark[2] - \footnotetext[2]{\fnt now the voice of a deity permeates} - \fnt , and the third + \footnotetext[2]{now the voice of a deity permeates} + , and the third \footnotemark[3] - \footnotetext[3]{\fnt see notes 1 and 2} - \fnt . But notes will not be duplicated here! + \footnotetext[3]{see notes 1 and 2} + . But notes will not be duplicated here! \footnotemark[1] "} @@ -526,13 +543,13 @@ mod tests { is_static: false, code: "1".into(), insert_path: None - }}, r"\fnt 1"} + }}, r"1"} test! {ayano_plain2, Token::Ayano{data: AyanoBlock{ is_display: false, is_static: false, code: "1 + 2".into(), insert_path: None - }}, r"\fnt 3"} + }}, r"3"} test! {ayano_plain3, Token::Ayano{data: AyanoBlock{ is_display: false, is_static: false, @@ -543,7 +560,7 @@ y = sqrt(x) int(y)" .into(), insert_path: None - }}, r"\fnt 10"} + }}, r"10"} test! {ayano_plain4, Token::Ayano{data: AyanoBlock{ is_display: false, is_static: false, @@ -554,7 +571,7 @@ for i in range(101): res" .into(), insert_path: None - }}, r"\fnt 5050"} + }}, r"5050"} test! {ayano_err1, Token::Ayano{data: AyanoBlock{ is_display: false, is_static: false, @@ -562,7 +579,7 @@ res" 'err', 1.0, 0.1" .into(), insert_path: None - }}, r"\fnt 1.00 $\pm$ \fnt 0.10"} + }}, r"1.00 $\pm$ 0.10"} test! {ayano_err2, Token::Ayano{data: AyanoBlock{ is_display: false, is_static: false, @@ -570,7 +587,7 @@ res" 'err', 1.0, 0.4" .into(), insert_path: None - }}, r"\fnt 1.0 $\pm$ \fnt 0.4"} + }}, r"1.0 $\pm$ 0.4"} test! {ayano_err3, Token::Ayano{data: AyanoBlock{ is_display: false, is_static: false, @@ -580,7 +597,7 @@ y = 0.4 @dev: x, y" .into(), insert_path: None - }}, r"\fnt 1.0 $\pm$ \fnt 0.4" + }}, r"1.0 $\pm$ 0.4" } test! {ayano_fig1, Token::Ayano{data: AyanoBlock{ @@ -632,9 +649,9 @@ data = [[1, 2, 3], [4, 5, 6]] r#" \begin{table}[h!] \begin{center} -\begin{tabular}{|c|c|c|} -\fnt 1 & \fnt 2 & \fnt 3 \\ \hline \hline -\fnt 4 & \fnt 5 & \fnt 6 \\ \hline +\begin{tabular}{|c|c|c|}\hline +1 & 2 & 3 \\ \hline \hline + 4 & 5 & 6 \\ \hline \end{tabular} \stepcounter{tabnum} \end{center} diff --git a/src/lexer.rs b/src/lexer.rs index 389fd3b..3ffa474 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -1,12 +1,5 @@ use std::{ - any::Any, - borrow::Cow, - cmp::Ordering, - error::Error, - fmt::{Debug, Display}, - num::ParseIntError, - path::Path, - str::FromStr, + borrow::Cow, error::Error, fmt::Debug, num::ParseIntError, path::Path, str::FromStr, sync::RwLock, }; @@ -17,14 +10,10 @@ use nom::{ complete::{tag, take_till1, take_until, take_until1, take_while}, streaming::tag_no_case, }, - character::{ - complete::{alphanumeric1, char, digit1, multispace0, newline, none_of, space0}, - is_space, - streaming::line_ending, - }, - combinator::{all_consuming, cut, eof, iterator, map_res, not, opt, peek, recognize, success}, + character::complete::{char, digit1, multispace0, space0}, + combinator::{all_consuming, cut, iterator, map_res, opt, recognize}, error::{context, ContextError, ErrorKind, FromExternalError, ParseError}, - multi::{fold_many1, many0_count, many1, many1_count, separated_list1}, + multi::{many0_count, many1}, number::complete::float, sequence::{delimited, pair, preceded, terminated, tuple}, IResult, Parser, @@ -32,7 +21,7 @@ use nom::{ use url::Url; use crate::{ - data::{AyanoBlock, Font, Formatting, ListType, Token, Tokens}, + data::{AyanoBlock, Formatting, ListType, Token, Tokens}, util::{optional_permutation, Equivalent, IteratorArrayCollectExt, VecTryExtendExt}, }; @@ -729,6 +718,7 @@ fn list< context("list", _list_inner)(input) } +#[deprecated] fn _formatting_inner< 'source, E: ParseError<&'source str> @@ -763,10 +753,11 @@ fn formatting< >( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { - let (rest, delimiter) = preceded( - pair(opt(char('\n')), space0), - alt((tag("**"), tag("__"), tag("*"), tag("_"), tag("~~"))), - )(input)?; + // First, let's parse the empty space before paragraph + let (input, space_before) = multispace0(input)?; + // if there are newlines, this paragraph is_newline + let is_newline = space_before.contains('\n'); + let (rest, delimiter) = alt((tag("**"), tag("__"), tag("*"), tag("_"), tag("~~")))(input)?; // Anything starting from these characters IS a formatting token. // Committed to parsing after this point let formatting = match delimiter { @@ -780,13 +771,25 @@ fn formatting< .filter_map(|(ind, _)| (rest[ind..].starts_with(delimiter)).then_some(ind)); for end_candidate in possible_ends { let inner_tokens = &rest[..end_candidate]; - if let Ok((_, inner)) = - all_consuming(terminated(inner_lex::<'source, E>, space0))(inner_tokens) + if let Ok((_, (content, follower))) = + all_consuming(terminated(paragraph_inner::<'source, E>, space0))(inner_tokens) { - return Ok(( - &rest[(end_candidate + delimiter.len())..], - Token::Formatted(formatting, Box::new(inner)), - )); + return if follower.is_some() { + Err(nom::Err::Error(E::add_context( + input, + "formatting", + E::from_error_kind(input, ErrorKind::Fail), + ))) + } else { + Ok(( + &rest[(end_candidate + delimiter.len())..], + Token::Paragraph { + is_newline, + formatting: Some(formatting), + content, + }, + )) + }; } } // If this point was reached, there is no correct variant for pairing delimiter. @@ -902,6 +905,94 @@ fn footnote_content< .parse(input) } +fn paragraph_inner< + 'source, + E: ParseError<&'source str> + + ContextError<&'source str> + + FromExternalError<&'source str, KyomatoLexError> + + 'source, +>( + input: &'source str, +) -> IResult<&'source str, (Cow<'source, str>, Option>), E> { + // At this point we assume, that whitespace before was parsed, including any possible newlines + + // If input turns out empty after that, return error + if input.is_empty() { + return Err(nom::Err::Error(E::from_error_kind(input, ErrorKind::Eof))); + } + + // Any token starting with a newline is out of the question + // However, this still leaves a couple of candidates: + // - href (starting with '[') + // - formatting (starting with '*', '_' or '~') + // - inline_math (starting with '$') + // - reference (starting with "[@") + // - footnote_reference (starting with "[^") + + // First, let's isolate input to a newline (if there is no newline, stop at last char) + let interrupt_ind = input.find('\n').unwrap_or(input.len()); + let cut_input = &input[..interrupt_ind]; + + // Now, let's try finding interrupting token + let interruption = cut_input + .char_indices() + // Parsing continues until new, Option>)line is reached + .take_while(|(_, c)| c != &'\n') + // Possible breakpoints are at characters '[', '*', '_', '~', and '$' + .filter(|(_, c)| ['[', '*', '_', '~', '$'].contains(c)) + // Now, try a proper parser for each one + .map(|(ind, c)| { + let cut_cut_input = &input[ind..]; + let parse_res = match c { + '[' => { + if cut_input[ind..].starts_with("[@") { + reference(cut_cut_input) + } else if cut_input[ind..].starts_with("[^") { + footnote_reference(cut_cut_input) + } else { + href(cut_cut_input) + } + } + '*' | '_' | '~' => formatting(cut_cut_input), + '$' => inline_math(cut_cut_input), + _ => unreachable!(), + }; + (ind, parse_res) + }) + .try_fold((), |_, (ind, parse_res)| match parse_res { + // if there is a match for the following token -- return this "Ok" wrapped into "Err", forcing iterator to stop + Ok(ok) => Err(Ok((ind, ok))), + // if there is not a match, but a FAUILURE -- return it as "Err" wrapped in "Err", forcing iterator to stop + Err(nom::Err::Failure(err)) => Err(Err(nom::Err::Failure(err))), + // if parsing had failed, continue driving iterator by returning "Ok" + Err(_) => Ok(()), + }) + .err(); + + let interruption = if let Some(interruption) = interruption { + // there was an interruption! + // but it might be a FAILURE ONE. That should bubble up + Some(interruption?) + } else { + None + }; + + // Now, construct the output + let this_token_end = interruption + .as_ref() + .map(|(ind, _)| *ind) + .unwrap_or(interrupt_ind); + let rest_input = interruption + .as_ref() + .map(|(_, (rest, _))| *rest) + .unwrap_or(&input[this_token_end..]); + let follower = interruption.map(|(_, (_, f))| f); + Ok(( + rest_input, + (Cow::from(cut_input[..this_token_end].trim()), follower), + )) +} + /// Parses plaintext paragraph, up to at most the end of input/line /// /// WARNING: must be used AFTER ALL OTHER PARSERS. @@ -921,84 +1012,19 @@ fn paragraph< >( outer_input: &'source str, ) -> IResult<&'source str, (Token<'source>, Option>), E> { - preceded(multispace0, |input: &'source str| { - // At this point we assume, that whitespace before was parsed, including any possible newlines + // First, let's parse the empty space before paragraph + let (input, space_before) = multispace0(outer_input)?; + // if there are newlines, this paragraph is_newline + let is_newline = space_before.contains('\n'); - // If input turns out empty after that, return error - if input.is_empty() { - return Err(nom::Err::Error(E::from_error_kind(input, ErrorKind::Eof))); - } + let (rest, (content, follower)) = paragraph_inner(input)?; - // Any token starting with a newline is out of the question - // However, this still leaves a couple of candidates: - // - href (starting with '[') - // - formatting (starting with '*', '_' or '~') - // - inline_math (starting with '$') - // - reference (starting with "[@") - // - footnote_reference (starting with "[^") - - // First, let's isolate input to a newline (if there is no newline, stop at last char) - let interrupt_ind = input.find('\n').unwrap_or(input.len()); - let cut_input = &input[..interrupt_ind]; - - // Now, let's try finding interrupting token - let interruption = cut_input - .char_indices() - // Parsing continues until newline is reached - .take_while(|(_, c)| c != &'\n') - // Possible breakpoints are at characters '[', '*', '_', '~', and '$' - .filter(|(_, c)| ['[', '*', '_', '~', '$'].contains(c)) - // Now, try a proper parser for each one - .map(|(ind, c)| { - let cut_cut_input = &input[ind..]; - let parse_res = match c { - '[' => { - if cut_input[ind..].starts_with("[@") { - reference(cut_cut_input) - } else if cut_input[ind..].starts_with("[^") { - footnote_reference(cut_cut_input) - } else { - href(cut_cut_input) - } - } - '*' | '_' | '~' => formatting(cut_cut_input), - '$' => inline_math(cut_cut_input), - _ => unreachable!(), - }; - (ind, parse_res) - }) - .try_fold((), |_, (ind, parse_res)| match parse_res { - // if there is a match for the following token -- return this "Ok" wrapped into "Err", forcing iterator to stop - Ok(ok) => Err(Ok((ind, ok))), - // if there is not a match, but a FAUILURE -- return it as "Err" wrapped in "Err", forcing iterator to stop - Err(nom::Err::Failure(err)) => Err(Err(nom::Err::Failure(err))), - // if parsing had failed, continue driving iterator by returning "Ok" - Err(_) => Ok(()), - }) - .err(); - - let interruption = if let Some(interruption) = interruption { - // there was an interruption! - // but it might be a FAILURE ONE. That should bubble up - Some(interruption?) - } else { - None - }; - - // Now, construct the output - let this_token_end = interruption - .as_ref() - .map(|(ind, _)| *ind) - .unwrap_or(interrupt_ind); - let this_token = - Token::Paragraph(Font::Normal, Cow::from(cut_input[..this_token_end].trim())); - let rest_input = interruption - .as_ref() - .map(|(_, (rest, _))| *rest) - .unwrap_or(&input[this_token_end..]); - let follower = interruption.map(|(_, (_, f))| f); - Ok((rest_input, (this_token, follower))) - })(outer_input) + let this_token = Token::Paragraph { + is_newline, + content, + formatting: None, + }; + Ok((rest, (this_token, follower))) } fn __equalizer(token: Token<'_>) -> (Token<'_>, Option>) { @@ -1384,6 +1410,8 @@ pub fn inner_lex< #[cfg(test)] mod tests { + use nom::character::complete::alphanumeric1; + use super::*; macro_rules! _assert_inner { @@ -1680,25 +1708,37 @@ $$ macro_rules! test_ok { {$name:ident, $input:literal, $paragraph:literal, $follower_token:expr, $left_over:literal} => { - test!{$name, paragraph, $input, e: Ok(($left_over, (Token::Paragraph(Font::Normal, $paragraph.into()), $follower_token)))} + test!{$name, paragraph, $input, e: Ok(($left_over, (Token::Paragraph{ + is_newline: false, + formatting: None, + content: $paragraph.into(), + }, $follower_token)))} + }; + {$name:ident, $input:literal, $paragraph:literal, $follower_token:expr, $left_over:literal, |} => { + test!{$name, paragraph, $input, e: Ok(($left_over, (Token::Paragraph{ + is_newline: true, + formatting: None, + content: $paragraph.into(), + }, $follower_token)))} }; } test! {err_empty, paragraph, "", p: Err(_)} // paragraph won't be parsed where it's empty test! {err_whitespace, paragraph, " \t \n\n \t", p: Err(_)} // nor if it's all whitespace - test_ok! {ok_char, " \t\n c\nfff", "c", None, "\nfff"} // single char is ok as a paragraph + test_ok! {ok_char, " \t\n c\nfff", "c", None, "\nfff", |} // single char is ok as a paragraph test_ok! {ok_sentence, "\t \n I'm absolutely increative, so here's a bunch of garbage: ]42[t5(2[24tj4-0)jt3_94j03\nand some rest", - "I'm absolutely increative, so here's a bunch of garbage: ]42[t5(2[24tj4-0)jt3_94j03", None, "\nand some rest"} - test_ok! {ok_formatting, "\t \n You can use *bold*!", "You can use", Some(Token::Formatted(Formatting::Italic, Box::new(Token::text("bold")))), "!"} - test_ok! {ok_formatting_in_formatting, "\t \n You can even use ~~__double-formatted__~~ things!", "You can even use", - Some(Token::Formatted(Formatting::StrikeThrough, Box::new(Token::Formatted(Formatting::Bold, Box::new(Token::text("double-formatted")))))), " things!"} + "I'm absolutely increative, so here's a bunch of garbage: ]42[t5(2[24tj4-0)jt3_94j03", None, "\nand some rest", |} + test_ok! {ok_formatting, "\t \n You can use *bold*!", "You can use", Some(Token::Paragraph{is_newline: false, formatting: Some(Formatting::Italic), content: "bold".into()}), "!", |} + // FIXME NOT SUPPORTED + /* test_ok! {ok_formatting_in_formatting, "\t \n You can even use ~~__double-formatted__~~ things!", "You can even use", + Some(Token::Formatted(Formatting::StrikeThrough, Box::new(Token::Formatted(Formatting::Bold, Box::new(Token::text("double-formatted")))))), " things!"} */ test_ok! {ok_equation, " \t Inline math is fine too: $y = x_{\text{поч}} + x_0$.", "Inline math is fine too:", Some(Token::InlineMathmode{content: "y = x_{\text{поч}} + x_0".into()}), "."} test_ok! {ok_href, " \t\t\n You can even [залишити посилання на свою сторінку на Гітхабі ](https://www.github.com/Dzuchun)!", "You can even", - Some(Token::Href { url: "https://www.github.com/Dzuchun".parse().expect("That's a valid url"), display: "залишити посилання на свою сторінку на Гітхабі".into() }), "!"} + Some(Token::Href { url: "https://www.github.com/Dzuchun".parse().expect("That's a valid url"), display: "залишити посилання на свою сторінку на Гітхабі".into() }), "!", |} test_ok! {ok_footnote_ref, "\t Footnote refs[^1] are also ok too!", "Footnote refs", Some(Token::FootNoteReference{ident: "1".into()}), " are also ok too!"} test_ok! {ok_obj_ref, "\n\t Thi[[ngs lik_e tables* ([@tab:results]) can also be referenced", "Thi[[ngs lik_e tables* (", - Some(Token::Reference{ident: "tab:results".into()}), ") can also be referenced"} + Some(Token::Reference{ident: "tab:results".into()}), ") can also be referenced", |} } // `inner_lex` tests @@ -1764,7 +1804,23 @@ $$ } macro_rules! tx { ($text:literal) => { - Token::Paragraph(Font::Normal, Cow::Borrowed($text)) + tx!($text, None) + }; + ($text:literal, |) => { + tx!($text, None, |) + }; + ($text:literal, $formatting:expr) => { + tx!($text, $formatting, false) + }; + ($text:literal, $formatting:expr, |) => { + tx!($text, $formatting, true) + }; + ($text:literal, $formatting:expr, $newline:literal) => { + Token::Paragraph { + is_newline: $newline, + formatting: $formatting, + content: Cow::Borrowed($text), + } }; } macro_rules! tks { @@ -1790,7 +1846,9 @@ $$ test_err! {err_empty, ""} test_ok! {ok_char_para, "a", tx!("a")} test_ok! {ok_formatted, "*виділений текст * і ще щось потім", - tks![Token::Formatted(Formatting::Italic, Box::new(tx!("виділений текст"))), tx!("і ще щось потім")]} + tks![tx!("виділений текст", Some(Formatting::Italic)), tx!("і ще щось потім")]} + test_ok! {ok_formatted_newlined, "\n*виділений текст * і ще щось потім", + tks![tx!("виділений текст", Some(Formatting::Italic), |), tx!("і ще щось потім")]} test_ok! {ok_inline_math, "дивіться! ось формула для параболи: $y = x^2$", tks![tx!("дивіться! ось формула для параболи:"), eq!(! "y = x^2")]} test_err! {err_display_math, "якась страшна штука:\n$$\nf(x) = \\int\\lms_{0}^{x} \\log(x - 2) dx\n$$\n{ref = scary}\n\tКраще не чіпати її"} @@ -2050,10 +2108,10 @@ $$ } test_err! {err_empty, ""} - test_ok! {ok_some_text, "\t\t\nSome text\n", tx!("Some text")} - test_ok! {ok_multiple_test, "\t\t\nSome text\nІ ще отакий текст", tks![tx!("Some text"), tx!("І ще отакий текст")]} + test_ok! {ok_some_text, "\t\t\nSome text\n", tx!("Some text", |)} + test_ok! {ok_multiple_test, "\t\t\nSome text\nІ ще отакий текст", tks![tx!("Some text", |), tx!("І ще отакий текст", |)]} test_ok! {ok_multiple_test2, "\t\t\n# Header\n## header2\n\n\tЦе початок тексту.\n- А це перший елемент списку\n- І другий елемент списку\nА оце буде рівняння: \n$$\ny = x ^ 2\n$$\n", - tks![head!(# "Header"), head!(## "header2"), tx!("Це початок тексту."), ls!(- tx!("А це перший елемент списку"), tx!("І другий елемент списку")), tx!("А оце буде рівняння:"), eq!("y = x ^ 2")]} + tks![head!(# "Header"), head!(## "header2"), tx!("Це початок тексту.", |), ls!(- tx!("А це перший елемент списку"), tx!("І другий елемент списку")), tx!("А оце буде рівняння:", |), eq!("y = x ^ 2")]} test_ok! {ok_idk1, "На світлині [@ fig:refurb] можна бачити принципову схему влаштування рефурбалідзера. Неважно помитити великі очі[^1], і два термоядерних реактори, що оточують їх. Точні реакції всередині невідомі, але цілком можливо що там женуть $du + t -> {}^{5}_{6}C$.\n\n[^1]: Для поливу.", tks![tx!("На світлині"), rf!(@ "fig:refurb"), diff --git a/tests/lex_files/ayano.txt b/tests/lex_files/ayano.txt index 64f6aa3..1c7fb62 100644 --- a/tests/lex_files/ayano.txt +++ b/tests/lex_files/ayano.txt @@ -1,14 +1,16 @@ Text { tokens: Owned( [ - Paragraph( - Normal, - "This is a test of ayano-generated stuff.", - ), - Paragraph( - Normal, - "This is supposed to be a generated figure:", - ), + Paragraph { + is_newline: false, + formatting: None, + content: "This is a test of ayano-generated stuff.", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "This is supposed to be a generated figure:", + }, Ayano { data: AyanoBlock { is_display: false, @@ -17,10 +19,11 @@ Text { insert_path: None, }, }, - Paragraph( - Normal, - "Here's a table with specified width:", - ), + Paragraph { + is_newline: true, + formatting: None, + content: "Here's a table with specified width:", + }, Ayano { data: AyanoBlock { is_display: false, @@ -29,10 +32,11 @@ Text { insert_path: None, }, }, - Paragraph( - Normal, - "This is expected to be a generated table:", - ), + Paragraph { + is_newline: true, + formatting: None, + content: "This is expected to be a generated table:", + }, Ayano { data: AyanoBlock { is_display: false, diff --git a/tests/lex_files/basic.txt b/tests/lex_files/basic.txt index d7128ef..a09de50 100644 --- a/tests/lex_files/basic.txt +++ b/tests/lex_files/basic.txt @@ -5,10 +5,11 @@ Text { order: 1, content: "Перша тема", }, - Paragraph( - Normal, - "Тут трохи тексту і пояснень про квантові мультивироджені рефурбалідзери змішаного типу. Це нудно, тож тримайте формулу:", - ), + Paragraph { + is_newline: true, + formatting: None, + content: "Тут трохи тексту і пояснень про квантові мультивироджені рефурбалідзери змішаного типу. Це нудно, тож тримайте формулу:", + }, Equation { content: "Ref(z) = \\int \\limits_{0}^{1} t^{z-1} \\cdot \\exp(-t) dt", ident: Some( @@ -19,10 +20,11 @@ Text { order: 2, content: "Підпункт", }, - Paragraph( - Normal, - "Рефурбалідзери можуть виявитись дуже важливими для збереження екологічної безпеки нашої планети. До прикладу, в них є наступні переваги:", - ), + Paragraph { + is_newline: true, + formatting: None, + content: "Рефурбалідзери можуть виявитись дуже важливими для збереження екологічної безпеки нашої планети. До прикладу, в них є наступні переваги:", + }, List { list_type: Bullet, content: Owned( @@ -30,10 +32,11 @@ Text { Text { tokens: Owned( [ - Paragraph( - Normal, - "Нормувальний множник", - ), + Paragraph { + is_newline: false, + formatting: None, + content: "Нормувальний множник", + }, InlineMathmode { content: "|\\psi|^{2} = 3/4", }, @@ -43,30 +46,33 @@ Text { Text { tokens: Owned( [ - Paragraph( - Normal, - "Кількість хромосом -", - ), + Paragraph { + is_newline: false, + formatting: None, + content: "Кількість хромосом -", + }, InlineMathmode { content: "\\exp(\\pi)", }, ], ), }, - Paragraph( - Normal, - "Поживна цінність - 20 дптр/см", - ), + Paragraph { + is_newline: false, + formatting: None, + content: "Поживна цінність - 20 дптр/см", + }, ], ), }, Figure { src_name: "refurb.jpg", caption: Some( - Paragraph( - Normal, - "Принципове влаштування рефурбалідзера", - ), + Paragraph { + is_newline: false, + formatting: None, + content: "Принципове влаштування рефурбалідзера", + }, ), ident: Some( "refurb", @@ -75,36 +81,41 @@ Text { 0.4, ), }, - Paragraph( - Normal, - "На світлині", - ), + Paragraph { + is_newline: true, + formatting: None, + content: "На світлині", + }, Reference { ident: "fig:refurb", }, - Paragraph( - Normal, - "можна бачити принципову схему влаштування рефурбалідзера. Неважно помітити великі очі", - ), + Paragraph { + is_newline: false, + formatting: None, + content: "можна бачити принципову схему влаштування рефурбалідзера. Неважно помітити великі очі", + }, FootNoteReference { ident: "1", }, - Paragraph( - Normal, - ", і два термоядерних реактори, що оточують їх. Точні реакції всередині невідомі, але цілком можливо що там женуть", - ), + Paragraph { + is_newline: false, + formatting: None, + content: ", і два термоядерних реактори, що оточують їх. Точні реакції всередині невідомі, але цілком можливо що там женуть", + }, InlineMathmode { content: "du + t -> {}^{5}_{6}C", }, - Paragraph( - Normal, - "...", - ), + Paragraph { + is_newline: false, + formatting: None, + content: "...", + }, FootNoteContent { - content: Paragraph( - Normal, - "Для поливу.", - ), + content: Paragraph { + is_newline: false, + formatting: None, + content: "Для поливу.", + }, ident: "1", }, ], diff --git a/tests/lex_files/deceit.txt b/tests/lex_files/deceit.txt index f191bae..f7142e6 100644 --- a/tests/lex_files/deceit.txt +++ b/tests/lex_files/deceit.txt @@ -1,165 +1,193 @@ Text { tokens: Owned( [ - Paragraph( - Normal, - "This test is dedicated to attempts of parser deception, i.e. usage of special symbols in wrong places to see if parser recognizes them as control symbols. There we go:", - ), + Paragraph { + is_newline: false, + formatting: None, + content: "This test is dedicated to attempts of parser deception, i.e. usage of special symbols in wrong places to see if parser recognizes them as control symbols. There we go:", + }, Header { order: 1, content: "Page dividers", }, - Paragraph( - Normal, - "Page dividers are represented by ------", - ), - Paragraph( - Normal, - "If the line above has dashes ^^^^^^ here, it probably works alright.", - ), - Paragraph( - Normal, - "Here's a genuine divider", - ), + Paragraph { + is_newline: true, + formatting: None, + content: "Page dividers are represented by ------", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "If the line above has dashes ^^^^^^ here, it probably works alright.", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "Here's a genuine divider", + }, PageDiv, - Paragraph( - Normal, - "Fun fact: there can be a paragraph right after it!", - ), + Paragraph { + is_newline: true, + formatting: None, + content: "Fun fact: there can be a paragraph right after it!", + }, PageDiv, - Paragraph( - Normal, - "Some text, cause why not", - ), - Paragraph( - Normal, - "But this", - ), - Formatted( - Bold, - Paragraph( - Normal, - "DOES", + Paragraph { + is_newline: false, + formatting: None, + content: "Some text, cause why not", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "But this", + }, + Paragraph { + is_newline: false, + formatting: Some( + Bold, ), - ), - Paragraph( - Normal, - "look cursed, don't do it.", - ), + content: "DOES", + }, + Paragraph { + is_newline: false, + formatting: None, + content: "look cursed, don't do it.", + }, Header { order: 1, content: "Headers", }, - Paragraph( - Normal, - "Headers are most recognizable by '#'-symbol. For it to actually represent a header, it needs to at the beginning of the line and there should be no more than 6 of them.", - ), - Formatted( - Bold, - Paragraph( - Normal, - "ANYTHING", + Paragraph { + is_newline: true, + formatting: None, + content: "Headers are most recognizable by '#'-symbol. For it to actually represent a header, it needs to at the beginning of the line and there should be no more than 6 of them.", + }, + Paragraph { + is_newline: false, + formatting: Some( + Bold, ), - ), - Paragraph( - Normal, - "up to the end of line will be included into a header, then.", - ), - Paragraph( - Normal, - "Example:", - ), + content: "ANYTHING", + }, + Paragraph { + is_newline: false, + formatting: None, + content: "up to the end of line will be included into a header, then.", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "Example:", + }, Header { order: 6, content: "This is a long, yet formally correct header. You can add $equations$ and *formatting* here, but I provide no guarantees as of the way these would display in final document.", }, - Paragraph( - Normal, - "Here's an example of incorrect header:", - ), - Paragraph( - Normal, - "###### This header is considered incorrect for this application, and will be parsed as regular paragraph instead", - ), - Paragraph( - Normal, - "Another example of incorrect header, featuring 7 hash-symbols:", - ), - Paragraph( - Normal, - "####### This will be parsed as a regular paragraph", - ), + Paragraph { + is_newline: true, + formatting: None, + content: "Here's an example of incorrect header:", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "###### This header is considered incorrect for this application, and will be parsed as regular paragraph instead", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "Another example of incorrect header, featuring 7 hash-symbols:", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "####### This will be parsed as a regular paragraph", + }, Header { order: 1, content: "Equations", }, - Paragraph( - Normal, - "There are two types of equations in markdown/latex: inline mathmode and display mathmode", - ), - Paragraph( - Normal, - "Both of them are defined by $-symbols. However, they must be paired to actually represent these. Here are examples:", - ), - Paragraph( - Normal, - "Inline:", - ), + Paragraph { + is_newline: true, + formatting: None, + content: "There are two types of equations in markdown/latex: inline mathmode and display mathmode", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "Both of them are defined by $-symbols. However, they must be paired to actually represent these. Here are examples:", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "Inline:", + }, InlineMathmode { content: "y = x ^ 2", }, - Paragraph( - Normal, - "Display:", - ), + Paragraph { + is_newline: true, + formatting: None, + content: "Display:", + }, Equation { content: "y = x ^ 2", ident: None, }, - Paragraph( - Normal, - "Note that display mathmode should ALWAYS start from a new line for this application.", - ), - Paragraph( - Normal, - "Basically, there's not much to deceive for: you can do that:", - ), - Paragraph( - Normal, - "$", - ), - Paragraph( - Normal, - "$", - ), - Paragraph( - Normal, - "(the above should not be mathmode), and that's it, basically :sad:", - ), + Paragraph { + is_newline: true, + formatting: None, + content: "Note that display mathmode should ALWAYS start from a new line for this application.", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "Basically, there's not much to deceive for: you can do that:", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "$", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "$", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "(the above should not be mathmode), and that's it, basically :sad:", + }, Header { order: 1, content: "Table", }, - Paragraph( - Normal, - "Tables are defined by |-symbol. But the actual table will happen only if they are the first thing on the line.", - ), - Paragraph( - Normal, - "Due to current implementation, there's no cure apart from display mathmode and code:", - ), - Paragraph( - Normal, - "(this should be display mathmode):", - ), + Paragraph { + is_newline: true, + formatting: None, + content: "Tables are defined by |-symbol. But the actual table will happen only if they are the first thing on the line.", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "Due to current implementation, there's no cure apart from display mathmode and code:", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "(this should be display mathmode):", + }, Equation { content: "|x|", ident: None, }, - Paragraph( - Normal, - "(this should be a code block):", - ), + Paragraph { + is_newline: true, + formatting: None, + content: "(this should be a code block):", + }, CodeBlock { code: "head /dev/random\n| cat", language: Some( @@ -170,10 +198,11 @@ Text { order: 1, content: "Figure", }, - Paragraph( - Normal, - "Figure is defined by a ![[ sequence at a start of the line. Same points should be uphold here:", - ), + Paragraph { + is_newline: true, + formatting: None, + content: "Figure is defined by a ![[ sequence at a start of the line. Same points should be uphold here:", + }, Equation { content: "![[", ident: None, @@ -186,22 +215,25 @@ Text { order: 1, content: "References", }, - Paragraph( - Normal, - "These start with []", - ), + Paragraph { + is_newline: true, + formatting: None, + content: "These start with []", + }, Header { order: 2, content: "Hyperrefs", }, - Paragraph( - Normal, - "These are followed by parentheses (). Nothing is allowed between them, thus", - ), - Paragraph( - Normal, - "Here's a correct href:", - ), + Paragraph { + is_newline: true, + formatting: None, + content: "These are followed by parentheses (). Nothing is allowed between them, thus", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "Here's a correct href:", + }, Href { url: Url { scheme: "https", @@ -220,387 +252,432 @@ Text { }, display: "my gh page", }, - Paragraph( - Normal, - "Here are incorrect refs:", - ), - Paragraph( - Normal, - "[my gh page] (https://youtu.be/rickroll", - ), - Formatted( - Italic, - Paragraph( - Normal, - "link", + Paragraph { + is_newline: true, + formatting: None, + content: "Here are incorrect refs:", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "[my gh page] (https://youtu.be/rickroll", + }, + Paragraph { + is_newline: false, + formatting: Some( + Italic, ), - ), - Paragraph( - Normal, - "lol)", - ), - Paragraph( - Normal, - "[my gh page]", - ), - Paragraph( - Normal, - "(https://youtu.be/rickroll", - ), - Formatted( - Italic, - Paragraph( - Normal, - "link", + content: "link", + }, + Paragraph { + is_newline: false, + formatting: None, + content: "lol)", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "[my gh page]", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "(https://youtu.be/rickroll", + }, + Paragraph { + is_newline: false, + formatting: Some( + Italic, ), - ), - Paragraph( - Normal, - "lol)", - ), + content: "link", + }, + Paragraph { + is_newline: false, + formatting: None, + content: "lol)", + }, Header { order: 2, content: "Footnote refs", }, - Paragraph( - Normal, - "These are started by [^.", - ), - Paragraph( - Normal, - "Here are correct footnote refs:", - ), + Paragraph { + is_newline: true, + formatting: None, + content: "These are started by [^.", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "Here are correct footnote refs:", + }, FootNoteReference { ident: "1", }, - Paragraph( - Normal, - ",", - ), + Paragraph { + is_newline: false, + formatting: None, + content: ",", + }, FootNoteReference { ident: "source", }, - Paragraph( - Normal, - "Here are incorrect ones: [^1, ^1],", - ), - Paragraph( - Normal, - "[^", - ), - Paragraph( - Normal, - "1", - ), - Paragraph( - Normal, - "]", - ), - Paragraph( - Normal, - "So on", - ), + Paragraph { + is_newline: true, + formatting: None, + content: "Here are incorrect ones: [^1, ^1],", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "[^", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "1", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "]", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "So on", + }, Header { order: 2, content: "Object refs", }, - Paragraph( - Normal, - "These refer to objects like figures, tables and equations.", - ), - Paragraph( - Normal, - "Started by [@.", - ), - Paragraph( - Normal, - "Here's a correct ref:", - ), + Paragraph { + is_newline: true, + formatting: None, + content: "These refer to objects like figures, tables and equations.", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "Started by [@.", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "Here's a correct ref:", + }, Reference { ident: "fig:experiment_schema", }, - Paragraph( - Normal, - "Here's are bad ones: [@ fig:1, @ source3],", - ), - Paragraph( - Normal, - "[@", - ), - Paragraph( - Normal, - "eq2", - ), - Paragraph( - Normal, - "]", - ), - Paragraph( - Normal, - "", - ), - Formatted( - Bold, - Paragraph( - Normal, - "NOTE", + Paragraph { + is_newline: true, + formatting: None, + content: "Here's are bad ones: [@ fig:1, @ source3],", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "[@", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "eq2", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "]", + }, + Paragraph { + is_newline: true, + formatting: Some( + Bold, ), - ), - Paragraph( - Normal, - ": despite object types being prepended with 'eq', 'fig' or 'tab' in refs, you", - ), - Formatted( - Italic, - Paragraph( - Normal, - "can", + content: "NOTE", + }, + Paragraph { + is_newline: false, + formatting: None, + content: ": despite object types being prepended with 'eq', 'fig' or 'tab' in refs, you", + }, + Paragraph { + is_newline: false, + formatting: Some( + Italic, ), - ), - Paragraph( - Normal, - "use regular latex to define your custom refs. In fact, LaTeX doesn't care about all of these at all; I'm doing these transformations just for my own comfort, as I've been doing this manually.", - ), + content: "can", + }, + Paragraph { + is_newline: false, + formatting: None, + content: "use regular latex to define your custom refs. In fact, LaTeX doesn't care about all of these at all; I'm doing these transformations just for my own comfort, as I've been doing this manually.", + }, Header { order: 1, content: "Formatting", }, - Paragraph( - Normal, - "This object's type is deprecated and will be removed soon.", - ), - Paragraph( - Normal, - "It starts and ends with", - ), + Paragraph { + is_newline: true, + formatting: None, + content: "This object's type is deprecated and will be removed soon.", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "It starts and ends with", + }, List { list_type: Bullet, content: Owned( [ - Paragraph( - Normal, - "* or _", - ), - Paragraph( - Normal, - "** or __", - ), - Paragraph( - Normal, - "~~", - ), + Paragraph { + is_newline: false, + formatting: None, + content: "* or _", + }, + Paragraph { + is_newline: false, + formatting: None, + content: "** or __", + }, + Paragraph { + is_newline: false, + formatting: None, + content: "~~", + }, ], ), }, - Paragraph( - Normal, - "", - ), - Formatted( - Bold, - Paragraph( - Normal, - "WARN", + Paragraph { + is_newline: true, + formatting: Some( + Bold, ), - ), - Paragraph( - Normal, - ": As of now, these", - ), - Formatted( - Italic, - Paragraph( - Normal, - "might", + content: "WARN", + }, + Paragraph { + is_newline: false, + formatting: None, + content: ": As of now, these", + }, + Paragraph { + is_newline: false, + formatting: Some( + Italic, ), - ), - Paragraph( - Normal, - "cause cause massive problems if encountered at the start of the line.", - ), - Paragraph( - Normal, - "Basically, parsers for these tokens attempt parse everything up until they find a closing token. But if they are inserted in a middle of the text, then it's ok - they will be limited by current paragraph, and (most likely) will find no match inside it, if their use was unintended.", - ), + content: "might", + }, + Paragraph { + is_newline: false, + formatting: None, + content: "cause cause massive problems if encountered at the start of the line.", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "Basically, parsers for these tokens attempt parse everything up until they find a closing token. But if they are inserted in a middle of the text, then it's ok - they will be limited by current paragraph, and (most likely) will find no match inside it, if their use was unintended.", + }, Header { order: 1, content: "Footnote content", }, - Paragraph( - Normal, - "These start with '", - ), + Paragraph { + is_newline: true, + formatting: None, + content: "These start with '", + }, FootNoteReference { ident: "ID", }, - Paragraph( - Normal, - ":' and proceed to the end of line.", - ), - Paragraph( - Normal, - "Example of valid content:", - ), + Paragraph { + is_newline: false, + formatting: None, + content: ":' and proceed to the end of line.", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "Example of valid content:", + }, FootNoteContent { - content: Paragraph( - Normal, - "Explanation of source2.", - ), + content: Paragraph { + is_newline: false, + formatting: None, + content: "Explanation of source2.", + }, ident: "source2", }, - Paragraph( - Normal, - "Example of invalid content:", - ), - Paragraph( - Normal, - "[^source2: This is invalid content", - ), + Paragraph { + is_newline: true, + formatting: None, + content: "Example of invalid content:", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "[^source2: This is invalid content", + }, FootNoteReference { ident: "source2", }, - Paragraph( - Normal, - ": This is invalid content", - ), + Paragraph { + is_newline: false, + formatting: None, + content: ": This is invalid content", + }, FootNoteReference { ident: "source2", }, - Paragraph( - Normal, - ": This is invalid content", - ), - Paragraph( - Normal, - "They also must start at the beginning of the line. If fact, this:", - ), + Paragraph { + is_newline: false, + formatting: None, + content: ": This is invalid content", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "They also must start at the beginning of the line. If fact, this:", + }, FootNoteReference { ident: "source2", }, - Paragraph( - Normal, - ": Some content", - ), - Paragraph( - Normal, - "Is expected to be a footnote ref with a colon.", - ), + Paragraph { + is_newline: false, + formatting: None, + content: ": Some content", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "Is expected to be a footnote ref with a colon.", + }, Header { order: 1, content: "Code blocks", }, - Paragraph( - Normal, - "There are two fundamental types of code blocks: normal code block and", - ), - Formatted( - Bold, - Paragraph( - Normal, - "Ayano", + Paragraph { + is_newline: true, + formatting: None, + content: "There are two fundamental types of code blocks: normal code block and", + }, + Paragraph { + is_newline: false, + formatting: Some( + Bold, ), - ), - Paragraph( - Normal, - "code block.", - ), + content: "Ayano", + }, + Paragraph { + is_newline: false, + formatting: None, + content: "code block.", + }, Header { order: 2, content: "Normal code block", }, - Paragraph( - Normal, - "These start and end with '```' on a newline. There can be", - ), - Formatted( - Bold, - Paragraph( - Normal, - "practically anything", + Paragraph { + is_newline: true, + formatting: None, + content: "These start and end with '```' on a newline. There can be", + }, + Paragraph { + is_newline: false, + formatting: Some( + Bold, ), - ), - Paragraph( - Normal, - "inside. Example:", - ), + content: "practically anything", + }, + Paragraph { + is_newline: false, + formatting: None, + content: "inside. Example:", + }, CodeBlock { code: "int x = 2;\nint y = 4;\nint[] arr = new int[2];\narr[0] = x;\narr[1] = 4;", language: Some( "java", ), }, - Paragraph( - Normal, - "There can be anything after code block had closed:", - ), + Paragraph { + is_newline: true, + formatting: None, + content: "There can be anything after code block had closed:", + }, CodeBlock { code: "local x = 'matt'\nlocal y = 'y'\nlocal z = x .. y -- matty!!", language: Some( "lua(Idk why vscode detects this block closing as a valid lua syntax)", ), }, - Paragraph( - Normal, - "Here's some text that's ok to be here", - ), - Paragraph( - Normal, - "There can't be any spaces before delimiting ```s:", - ), - Paragraph( - Normal, - "```python", - ), - Paragraph( - Normal, - "# This here is not really a python block", - ), - Paragraph( - Normal, - "x = 2", - ), - Paragraph( - Normal, - "print(x)", - ), - Paragraph( - Normal, - "```", - ), + Paragraph { + is_newline: false, + formatting: None, + content: "Here's some text that's ok to be here", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "There can't be any spaces before delimiting ```s:", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "```python", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "# This here is not really a python block", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "x = 2", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "print(x)", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "```", + }, Header { order: 2, content: "Ayano code block", }, - Paragraph( - Normal, - "", - ), - Formatted( - Italic, - Paragraph( - Normal, - "because Ayano loves Kyoko", + Paragraph { + is_newline: true, + formatting: Some( + Italic, ), - ), - Paragraph( - Normal, - "This object looks and behaves just like normal code block, but is only limited to", - ), - Formatted( - Italic, - Paragraph( - Normal, - "python", + content: "because Ayano loves Kyoko", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "This object looks and behaves just like normal code block, but is only limited to", + }, + Paragraph { + is_newline: false, + formatting: Some( + Italic, ), - ), - Paragraph( - Normal, - "language, can have spacial arguments in a language line, may leave something behind in the document. For example:", - ), - Paragraph( - Normal, - "This Ayano block will leave plain \"2\" behind it:", - ), + content: "python", + }, + Paragraph { + is_newline: false, + formatting: None, + content: "language, can have spacial arguments in a language line, may leave something behind in the document. For example:", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "This Ayano block will leave plain \"2\" behind it:", + }, Ayano { data: AyanoBlock { is_display: false, @@ -609,42 +686,48 @@ Text { insert_path: None, }, }, - Paragraph( - Normal, - "Akin to normal blocks, it's ok to leave something trailing:", - ), + Paragraph { + is_newline: true, + formatting: None, + content: "Akin to normal blocks, it's ok to leave something trailing:", + }, CodeBlock { code: "2", language: Some( "ayano", ), }, - Paragraph( - Normal, - "This text is ok to be here", - ), - Paragraph( - Normal, - "And these are invalid Ayano blocks:", - ), + Paragraph { + is_newline: false, + formatting: None, + content: "This text is ok to be here", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "And these are invalid Ayano blocks:", + }, CodeBlock { code: "2", language: Some( "ayano", ), }, - Paragraph( - Normal, - "```ayano", - ), - Paragraph( - Normal, - "2", - ), - Paragraph( - Normal, - "```", - ), + Paragraph { + is_newline: true, + formatting: None, + content: "```ayano", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "2", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "```", + }, ], ), } \ No newline at end of file diff --git a/tests/lex_files/tables_code.txt b/tests/lex_files/tables_code.txt index a903e66..46c8c49 100644 --- a/tests/lex_files/tables_code.txt +++ b/tests/lex_files/tables_code.txt @@ -1,67 +1,76 @@ Text { tokens: Owned( [ - Paragraph( - Normal, - "Наступний тест. Тут протестуємо таблиці і блоки коду.", - ), + Paragraph { + is_newline: false, + formatting: None, + content: "Наступний тест. Тут протестуємо таблиці і блоки коду.", + }, Header { order: 2, content: "Таблиці", }, - Paragraph( - Normal, - "Звичайні таблиці; Можуть всередині мати", - ), - Formatted( - Bold, - Paragraph( - Normal, - "формули", + Paragraph { + is_newline: true, + formatting: None, + content: "Звичайні таблиці; Можуть всередині мати", + }, + Paragraph { + is_newline: false, + formatting: Some( + Bold, ), - ), - Paragraph( - Normal, - "чи", - ), - Formatted( - Italic, - Paragraph( - Normal, - "посилання", + content: "формули", + }, + Paragraph { + is_newline: false, + formatting: None, + content: "чи", + }, + Paragraph { + is_newline: false, + formatting: Some( + Italic, ), - ), - Paragraph( - Normal, - "на інші штуки", - ), + content: "посилання", + }, + Paragraph { + is_newline: false, + formatting: None, + content: "на інші штуки", + }, Table { header: Owned( [ - Paragraph( - Normal, - "Storage", - ), - Paragraph( - Normal, - "Space", - ), - Paragraph( - Normal, - "Number", - ), + Paragraph { + is_newline: false, + formatting: None, + content: "Storage", + }, + Paragraph { + is_newline: false, + formatting: None, + content: "Space", + }, + Paragraph { + is_newline: false, + formatting: None, + content: "Number", + }, ], ), cells: Owned( [ - Paragraph( - Normal, - "1", - ), - Paragraph( - Normal, - "2", - ), + Paragraph { + is_newline: false, + formatting: None, + content: "1", + }, + Paragraph { + is_newline: false, + formatting: None, + content: "2", + }, InlineMathmode { content: "3 + e ^{\\pi}", }, @@ -83,21 +92,23 @@ Text { Text { tokens: Owned( [ - Paragraph( - Normal, - "І тут ще", - ), - Formatted( - Italic, - Paragraph( - Normal, - "трохи", + Paragraph { + is_newline: false, + formatting: None, + content: "І тут ще", + }, + Paragraph { + is_newline: false, + formatting: Some( + Italic, ), - ), - Paragraph( - Normal, - "тексту \\|", - ), + content: "трохи", + }, + Paragraph { + is_newline: false, + formatting: None, + content: "тексту \\|", + }, ], ), }, @@ -107,21 +118,23 @@ Text { Text { tokens: Owned( [ - Paragraph( - Normal, - "Оце приклад таблиці. Як казав мій вчитель:", - ), - Formatted( - Bold, - Paragraph( - Normal, - "ВИКОРИСТОВУЙ БІЛЬШЕ ВИБУХІВКИ", + Paragraph { + is_newline: false, + formatting: None, + content: "Оце приклад таблиці. Як казав мій вчитель:", + }, + Paragraph { + is_newline: false, + formatting: Some( + Bold, ), - ), - Paragraph( - Normal, - "(і \\\"лапок\\\")", - ), + content: "ВИКОРИСТОВУЙ БІЛЬШЕ ВИБУХІВКИ", + }, + Paragraph { + is_newline: false, + formatting: None, + content: "(і \\\"лапок\\\")", + }, ], ), }, @@ -130,41 +143,48 @@ Text { "example_1", ), }, - Paragraph( - Normal, - "Таблиця цілком собі може не мати ідентифікатора або опису:", - ), + Paragraph { + is_newline: true, + formatting: None, + content: "Таблиця цілком собі може не мати ідентифікатора або опису:", + }, Table { header: Owned( [ - Paragraph( - Normal, - "Кількість", - ), - Paragraph( - Normal, - "Роль", - ), + Paragraph { + is_newline: false, + formatting: None, + content: "Кількість", + }, + Paragraph { + is_newline: false, + formatting: None, + content: "Роль", + }, ], ), cells: Owned( [ - Paragraph( - Normal, - "3 діоптрії", - ), - Paragraph( - Normal, - "7 галон", - ), - Paragraph( - Normal, - "rust", - ), - Paragraph( - Normal, - "#PANIC!!!", - ), + Paragraph { + is_newline: false, + formatting: None, + content: "3 діоптрії", + }, + Paragraph { + is_newline: false, + formatting: None, + content: "7 галон", + }, + Paragraph { + is_newline: false, + formatting: None, + content: "rust", + }, + Paragraph { + is_newline: false, + formatting: None, + content: "#PANIC!!!", + }, ], ), caption: None, @@ -176,32 +196,35 @@ Text { content: Text { tokens: Owned( [ - Paragraph( - Normal, - "Це число Ейлера. Воно дуже корисне,", - ), - Formatted( - StrikeThrough, - Paragraph( - Normal, - "але не може бути записане", + Paragraph { + is_newline: false, + formatting: None, + content: "Це число Ейлера. Воно дуже корисне,", + }, + Paragraph { + is_newline: false, + formatting: Some( + StrikeThrough, ), - ), - Paragraph( - Normal, - "у вигляді", - ), - Formatted( - Italic, - Paragraph( - Normal, - "десяткового", + content: "але не може бути записане", + }, + Paragraph { + is_newline: false, + formatting: None, + content: "у вигляді", + }, + Paragraph { + is_newline: false, + formatting: Some( + Italic, ), - ), - Paragraph( - Normal, - "дробу.", - ), + content: "десяткового", + }, + Paragraph { + is_newline: false, + formatting: None, + content: "дробу.", + }, ], ), }, @@ -211,39 +234,44 @@ Text { order: 2, content: "Блоки коду", }, - Paragraph( - Normal, - "Наразі мають досить обмежену підтримку, адже у", - ), + Paragraph { + is_newline: true, + formatting: None, + content: "Наразі мають досить обмежену підтримку, адже у", + }, InlineMathmode { content: "\\LaTeX", }, - Paragraph( - Normal, - "немає нативного поняття блоків коду.", - ), - Paragraph( - Normal, - "На щастя, у", - ), - Formatted( - Bold, - Paragraph( - Normal, - "markdown", + Paragraph { + is_newline: false, + formatting: None, + content: "немає нативного поняття блоків коду.", + }, + Paragraph { + is_newline: true, + formatting: None, + content: "На щастя, у", + }, + Paragraph { + is_newline: false, + formatting: Some( + Bold, ), - ), - Paragraph( - Normal, - "таке поняття є", - ), + content: "markdown", + }, + Paragraph { + is_newline: false, + formatting: None, + content: "таке поняття є", + }, FootNoteReference { ident: "code", }, - Paragraph( - Normal, - ":", - ), + Paragraph { + is_newline: false, + formatting: None, + content: ":", + }, CodeBlock { code: "let x = 5;\nlet y = x.powi(2);\nlet z = (y as f64).sqrt();\nprintln!(\"x = {x}, y = {y}, z = {x}\");", language: Some( @@ -254,21 +282,23 @@ Text { content: Text { tokens: Owned( [ - Paragraph( - Normal, - "Принаймні,", - ), - Formatted( - Italic, - Paragraph( - Normal, - "Obsidian", + Paragraph { + is_newline: false, + formatting: None, + content: "Принаймні,", + }, + Paragraph { + is_newline: false, + formatting: Some( + Italic, ), - ), - Paragraph( - Normal, - "таке робить", - ), + content: "Obsidian", + }, + Paragraph { + is_newline: false, + formatting: None, + content: "таке робить", + }, ], ), }, diff --git a/tests/transformed_files/ayano.txt b/tests/transformed_files/ayano.txt index f6543d1..e162ad5 100644 --- a/tests/transformed_files/ayano.txt +++ b/tests/transformed_files/ayano.txt @@ -1,27 +1,30 @@ -\fnt This is a test of ayano-generated stuff.\fnt This is supposed to be a generated figure: +This is a test of ayano-generated stuff. +\tbln This is supposed to be a generated figure: \begin{figure}[h!] \centering \includegraphics[width=0.9\textwidth]{circle.png} -\caption{\fnt Some caption for a line drawing. This is parsed as a\textit{\fnt regular document text}\fnt , so you may\ref{eq:line}\fnt and$f(x) = A \cdot x^{1} + B \cdot x^{0}$\fnt here, for example} +\caption{\capfnt Some caption for a line drawing. This is parsed as a\textit{regular document text}, so you may\ref{eq:line}and$f(x) = A \cdot x^{1} + B \cdot x^{0}$here, for example} \label{fig:circle} \end{figure} -\fnt Here's a table with specified width: + +\tbln Here's a table with specified width: \begin{figure}[h!] \centering \includegraphics[width=0.2\textwidth]{graph.png} \end{figure} -\fnt This is expected to be a generated table: + +\tbln This is expected to be a generated table: \begin{table}[h!] \begin{center} -\begin{tabular}{|c|c|} -\fnt x & \fnt y\\ \hline +\begin{tabular}{|c|c|}\hline +x & y\\ \hline \hline -\fnt 1.0 & \fnt 3.0\\ \hline -\fnt 3.0 & \fnt 4.0\\ \hline -\fnt -1.0 & \fnt -5743234.0\\ \hline +1.0 & 3.0\\ \hline +3.0 & 4.0\\ \hline +-1.0 & -5743234.0\\ \hline \end{tabular} \stepcounter{tabnum} -\caption{\fnt Caption for a table 1,\textit{\fnt because}\fnt why not} +\caption{\capfnt Caption for a table 1,\textit{because}why not} \label{tab:table1} \end{center} \end{table} diff --git a/tests/transformed_files/basic.txt b/tests/transformed_files/basic.txt index 908a2e6..2826b07 100644 --- a/tests/transformed_files/basic.txt +++ b/tests/transformed_files/basic.txt @@ -1,21 +1,24 @@ \subsection{Перша тема} -\fnt Тут трохи тексту і пояснень про квантові мультивироджені рефурбалідзери змішаного типу. Це нудно, тож тримайте формулу: + +\tbln Тут трохи тексту і пояснень про квантові мультивироджені рефурбалідзери змішаного типу. Це нудно, тож тримайте формулу: \begin{equation} Ref(z) = \int \limits_{0}^{1} t^{z-1} \cdot \exp(-t) dt \label{eq:refurbalidzer_1}\end{equation} \subsubsection{Підпункт} -\fnt Рефурбалідзери можуть виявитись дуже важливими для збереження екологічної безпеки нашої планети. До прикладу, в них є наступні переваги: + +\tbln Рефурбалідзери можуть виявитись дуже важливими для збереження екологічної безпеки нашої планети. До прикладу, в них є наступні переваги: \begin{itemize} -\item \fnt Нормувальний множник$|\psi|^{2} = 3/4$ -\item \fnt Кількість хромосом -$\exp(\pi)$ -\item \fnt Поживна цінність - 20 дптр/см +\item Нормувальний множник$|\psi|^{2} = 3/4$ +\item Кількість хромосом -$\exp(\pi)$ +\item Поживна цінність - 20 дптр/см \end{itemize} \begin{figure}[h!] \centering \includegraphics[width=0.4\textwidth]{refurb.jpg} -\caption{\fnt Принципове влаштування рефурбалідзера} +\caption{\capfnt Принципове влаштування рефурбалідзера} \label{fig:refurb} \end{figure} -\fnt На світлині\ref{fig:refurb}\fnt можна бачити принципову схему влаштування рефурбалідзера. Неважно помітити великі очі\footnotemark[1]\footnotetext[1]{\fnt Для поливу.}\fnt , і два термоядерних реактори, що оточують їх. Точні реакції всередині невідомі, але цілком можливо що там женуть$du + t -> {}^{5}_{6}C$\fnt ... \ No newline at end of file + +\tbln На світлині\ref{fig:refurb}можна бачити принципову схему влаштування рефурбалідзера. Неважно помітити великі очі\footnotemark[1]\footnotetext[1]{Для поливу.}, і два термоядерних реактори, що оточують їх. Точні реакції всередині невідомі, але цілком можливо що там женуть$du + t -> {}^{5}_{6}C$... \ No newline at end of file diff --git a/tests/transformed_files/deceit.txt b/tests/transformed_files/deceit.txt index 3ffd8f4..2db3589 100644 --- a/tests/transformed_files/deceit.txt +++ b/tests/transformed_files/deceit.txt @@ -1,26 +1,50 @@ -\fnt This test is dedicated to attempts of parser deception, i.e. usage of special symbols in wrong places to see if parser recognizes them as control symbols. There we go: +This test is dedicated to attempts of parser deception, i.e. usage of special symbols in wrong places to see if parser recognizes them as control symbols. There we go: \subsection{Page dividers} -\fnt Page dividers are represented by ------\fnt If the line above has dashes ^^^^^^ here, it probably works alright.\fnt Here's a genuine divider + +\tbln Page dividers are represented by ------ +\tbln If the line above has dashes ^^^^^^ here, it probably works alright. +\tbln Here's a genuine divider \clearpage -\fnt Fun fact: there can be a paragraph right after it! + +\tbln Fun fact: there can be a paragraph right after it! \clearpage -\fnt Some text, cause why not\fnt But this\textbf{\fnt DOES}\fnt look cursed, don't do it. +Some text, cause why not +\tbln But this\textbf{DOES}look cursed, don't do it. \subsection{Headers} -\fnt Headers are most recognizable by '#'-symbol. For it to actually represent a header, it needs to at the beginning of the line and there should be no more than 6 of them.\textbf{\fnt ANYTHING}\fnt up to the end of line will be included into a header, then.\fnt Example: + +\tbln Headers are most recognizable by '#'-symbol. For it to actually represent a header, it needs to at the beginning of the line and there should be no more than 6 of them.\textbf{ANYTHING}up to the end of line will be included into a header, then. +\tbln Example: \textbf{This is a long, yet formally correct header. You can add $equations$ and *formatting* here, but I provide no guarantees as of the way these would display in final document.} -\fnt Here's an example of incorrect header:\fnt ###### This header is considered incorrect for this application, and will be parsed as regular paragraph instead\fnt Another example of incorrect header, featuring 7 hash-symbols:\fnt ####### This will be parsed as a regular paragraph + +\tbln Here's an example of incorrect header: +\tbln ###### This header is considered incorrect for this application, and will be parsed as regular paragraph instead +\tbln Another example of incorrect header, featuring 7 hash-symbols: +\tbln ####### This will be parsed as a regular paragraph \subsection{Equations} -\fnt There are two types of equations in markdown/latex: inline mathmode and display mathmode\fnt Both of them are defined by $-symbols. However, they must be paired to actually represent these. Here are examples:\fnt Inline:$y = x ^ 2$\fnt Display: + +\tbln There are two types of equations in markdown/latex: inline mathmode and display mathmode +\tbln Both of them are defined by $-symbols. However, they must be paired to actually represent these. Here are examples: +\tbln Inline:$y = x ^ 2$ +\tbln Display: \begin{equation} y = x ^ 2 \end{equation} -\fnt Note that display mathmode should ALWAYS start from a new line for this application.\fnt Basically, there's not much to deceive for: you can do that:\fnt $\fnt $\fnt (the above should not be mathmode), and that's it, basically :sad: + +\tbln Note that display mathmode should ALWAYS start from a new line for this application. +\tbln Basically, there's not much to deceive for: you can do that: +\tbln $ +\tbln $ +\tbln (the above should not be mathmode), and that's it, basically :sad: \subsection{Table} -\fnt Tables are defined by |-symbol. But the actual table will happen only if they are the first thing on the line.\fnt Due to current implementation, there's no cure apart from display mathmode and code:\fnt (this should be display mathmode): + +\tbln Tables are defined by |-symbol. But the actual table will happen only if they are the first thing on the line. +\tbln Due to current implementation, there's no cure apart from display mathmode and code: +\tbln (this should be display mathmode): \begin{equation} |x| \end{equation} -\fnt (this should be a code block): + +\tbln (this should be a code block): \begin{minted}{bash} head /dev/random | cat @@ -28,7 +52,8 @@ head /dev/random \subsection{Figure} -\fnt Figure is defined by a ![[ sequence at a start of the line. Same points should be uphold here: + +\tbln Figure is defined by a ![[ sequence at a start of the line. Same points should be uphold here: \begin{equation} ![[ \end{equation} @@ -39,26 +64,60 @@ head /dev/random \subsection{References} -\fnt These start with [] + +\tbln These start with [] \subsubsection{Hyperrefs} -\fnt These are followed by parentheses (). Nothing is allowed between them, thus\fnt Here's a correct href:\href{https://youtu.be/rickroll_link_lol}{my gh page}\fnt Here are incorrect refs:\fnt [my gh page] (https://youtu.be/rickroll\textit{\fnt link}\fnt lol)\fnt [my gh page]\fnt (https://youtu.be/rickroll\textit{\fnt link}\fnt lol) + +\tbln These are followed by parentheses (). Nothing is allowed between them, thus +\tbln Here's a correct href:\href{https://youtu.be/rickroll_link_lol}{my gh page} +\tbln Here are incorrect refs: +\tbln [my gh page] (https://youtu.be/rickroll\textit{link}lol) +\tbln [my gh page] +\tbln (https://youtu.be/rickroll\textit{link}lol) \subsubsection{Footnote refs} -\fnt These are started by [^.\fnt Here are correct footnote refs:\footnotemark[1]\fnt ,\footnotemark[2]\fnt Here are incorrect ones: [^1, ^1],\fnt [^\fnt 1\fnt ]\fnt So on + +\tbln These are started by [^. +\tbln Here are correct footnote refs:\footnotemark[1],\footnotemark[2] +\tbln Here are incorrect ones: [^1, ^1], +\tbln [^ +\tbln 1 +\tbln ] +\tbln So on \subsubsection{Object refs} -\fnt These refer to objects like figures, tables and equations.\fnt Started by [@.\fnt Here's a correct ref:\ref{fig:experiment_schema}\fnt Here's are bad ones: [@ fig:1, @ source3],\fnt [@\fnt eq2\fnt ]\fnt \textbf{\fnt NOTE}\fnt : despite object types being prepended with 'eq', 'fig' or 'tab' in refs, you\textit{\fnt can}\fnt use regular latex to define your custom refs. In fact, LaTeX doesn't care about all of these at all; I'm doing these transformations just for my own comfort, as I've been doing this manually. + +\tbln These refer to objects like figures, tables and equations. +\tbln Started by [@. +\tbln Here's a correct ref:\ref{fig:experiment_schema} +\tbln Here's are bad ones: [@ fig:1, @ source3], +\tbln [@ +\tbln eq2 +\tbln ] +\tbln \textbf{NOTE}: despite object types being prepended with 'eq', 'fig' or 'tab' in refs, you\textit{can}use regular latex to define your custom refs. In fact, LaTeX doesn't care about all of these at all; I'm doing these transformations just for my own comfort, as I've been doing this manually. \subsection{Formatting} -\fnt This object's type is deprecated and will be removed soon.\fnt It starts and ends with + +\tbln This object's type is deprecated and will be removed soon. +\tbln It starts and ends with \begin{itemize} -\item \fnt * or _ -\item \fnt ** or __ -\item \fnt ~~ -\end{itemize}\fnt \textbf{\fnt WARN}\fnt : As of now, these\textit{\fnt might}\fnt cause cause massive problems if encountered at the start of the line.\fnt Basically, parsers for these tokens attempt parse everything up until they find a closing token. But if they are inserted in a middle of the text, then it's ok - they will be limited by current paragraph, and (most likely) will find no match inside it, if their use was unintended. +\item * or _ +\item ** or __ +\item ~~ +\end{itemize} +\tbln \textbf{WARN}: As of now, these\textit{might}cause cause massive problems if encountered at the start of the line. +\tbln Basically, parsers for these tokens attempt parse everything up until they find a closing token. But if they are inserted in a middle of the text, then it's ok - they will be limited by current paragraph, and (most likely) will find no match inside it, if their use was unintended. \subsection{Footnote content} -\fnt These start with '\footnotemark[3]\fnt :' and proceed to the end of line.\fnt Example of valid content:\fnt Example of invalid content:\fnt [^source2: This is invalid content\footnotemark[4]\footnotetext[4]{\fnt Explanation of source2.}\fnt : This is invalid content\footnotemark[4]\fnt : This is invalid content\fnt They also must start at the beginning of the line. If fact, this:\footnotemark[4]\fnt : Some content\fnt Is expected to be a footnote ref with a colon. + +\tbln These start with '\footnotemark[3]:' and proceed to the end of line. +\tbln Example of valid content: +\tbln Example of invalid content: +\tbln [^source2: This is invalid content\footnotemark[4]\footnotetext[4]{Explanation of source2.}: This is invalid content\footnotemark[4]: This is invalid content +\tbln They also must start at the beginning of the line. If fact, this:\footnotemark[4]: Some content +\tbln Is expected to be a footnote ref with a colon. \subsection{Code blocks} -\fnt There are two fundamental types of code blocks: normal code block and\textbf{\fnt Ayano}\fnt code block. + +\tbln There are two fundamental types of code blocks: normal code block and\textbf{Ayano}code block. \subsubsection{Normal code block} -\fnt These start and end with '```' on a newline. There can be\textbf{\fnt practically anything}\fnt inside. Example: + +\tbln These start and end with '```' on a newline. There can be\textbf{practically anything}inside. Example: \begin{minted}{java} int x = 2; int y = 4; @@ -67,23 +126,38 @@ arr[0] = x; arr[1] = 4; \end{minted} -\fnt There can be anything after code block had closed: + +\tbln There can be anything after code block had closed: \begin{minted}{lua(Idk why vscode detects this block closing as a valid lua syntax)} local x = 'matt' local y = 'y' local z = x .. y -- matty!! \end{minted} -\fnt Here's some text that's ok to be here\fnt There can't be any spaces before delimiting ```s:\fnt ```python\fnt # This here is not really a python block\fnt x = 2\fnt print(x)\fnt ``` +Here's some text that's ok to be here +\tbln There can't be any spaces before delimiting ```s: +\tbln ```python +\tbln # This here is not really a python block +\tbln x = 2 +\tbln print(x) +\tbln ``` \subsubsection{Ayano code block} -\fnt \textit{\fnt because Ayano loves Kyoko}\fnt This object looks and behaves just like normal code block, but is only limited to\textit{\fnt python}\fnt language, can have spacial arguments in a language line, may leave something behind in the document. For example:\fnt This Ayano block will leave plain "2" behind it:\fnt 2\fnt Akin to normal blocks, it's ok to leave something trailing: + +\tbln \textit{because Ayano loves Kyoko} +\tbln This object looks and behaves just like normal code block, but is only limited to\textit{python}language, can have spacial arguments in a language line, may leave something behind in the document. For example: +\tbln This Ayano block will leave plain "2" behind it:2 +\tbln Akin to normal blocks, it's ok to leave something trailing: \begin{minted}{ayano} 2 \end{minted} -\fnt This text is ok to be here\fnt And these are invalid Ayano blocks: +This text is ok to be here +\tbln And these are invalid Ayano blocks: \begin{minted}{ayano} 2 \end{minted} -\fnt ```ayano\fnt 2\fnt ``` \ No newline at end of file + +\tbln ```ayano +\tbln 2 +\tbln ``` \ No newline at end of file diff --git a/tests/transformed_files/tables_code.txt b/tests/transformed_files/tables_code.txt index 575985b..69bff4d 100644 --- a/tests/transformed_files/tables_code.txt +++ b/tests/transformed_files/tables_code.txt @@ -1,34 +1,38 @@ -\fnt Наступний тест. Тут протестуємо таблиці і блоки коду. +Наступний тест. Тут протестуємо таблиці і блоки коду. \subsubsection{Таблиці} -\fnt Звичайні таблиці; Можуть всередині мати\textbf{\fnt формули}\fnt чи\textit{\fnt посилання}\fnt на інші штуки + +\tbln Звичайні таблиці; Можуть всередині мати\textbf{формули}чи\textit{посилання}на інші штуки \begin{table}[h!] \begin{center} -\begin{tabular}{|c|c|c|} -\fnt Storage & \fnt Space & \fnt Number\\ \hline +\begin{tabular}{|c|c|c|}\hline +Storage & Space & Number\\ \hline \hline -\fnt 1 & \fnt 2 & $3 + e ^{\pi}$\\ \hline -$e$\footnotemark[1]\footnotetext[1]{\fnt Це число Ейлера. Воно дуже корисне,\st{\fnt але не може бути записане}\fnt у вигляді\textit{\fnt десяткового}\fnt дробу.} & $\cdot \pm 1$ & \fnt І тут ще\textit{\fnt трохи}\fnt тексту \|\\ \hline +1 & 2 & $3 + e ^{\pi}$\\ \hline +$e$\footnotemark[1]\footnotetext[1]{Це число Ейлера. Воно дуже корисне,\st{але не може бути записане}у вигляді\textit{десяткового}дробу.} & $\cdot \pm 1$ & І тут ще\textit{трохи}тексту \|\\ \hline \end{tabular} \stepcounter{tabnum} -\caption{\fnt Оце приклад таблиці. Як казав мій вчитель:\textbf{\fnt ВИКОРИСТОВУЙ БІЛЬШЕ ВИБУХІВКИ}\fnt (і \"лапок\")} +\caption{\capfnt Оце приклад таблиці. Як казав мій вчитель:\textbf{ВИКОРИСТОВУЙ БІЛЬШЕ ВИБУХІВКИ}(і \"лапок\")} \label{tab:example_1} \end{center} \end{table} -\fnt Таблиця цілком собі може не мати ідентифікатора або опису: + +\tbln Таблиця цілком собі може не мати ідентифікатора або опису: \begin{table}[h!] \begin{center} -\begin{tabular}{|c|c|} -\fnt Кількість & \fnt Роль\\ \hline +\begin{tabular}{|c|c|}\hline +Кількість & Роль\\ \hline \hline -\fnt 3 діоптрії & \fnt 7 галон\\ \hline -\fnt rust & \fnt #PANIC!!!\\ \hline +3 діоптрії & 7 галон\\ \hline +rust & #PANIC!!!\\ \hline \end{tabular} \stepcounter{tabnum}\label{tab:table_2} \end{center} \end{table} \subsubsection{Блоки коду} -\fnt Наразі мають досить обмежену підтримку, адже у$\LaTeX$\fnt немає нативного поняття блоків коду.\fnt На щастя, у\textbf{\fnt markdown}\fnt таке поняття є\footnotemark[2]\footnotetext[2]{\fnt Принаймні,\textit{\fnt Obsidian}\fnt таке робить}\fnt : + +\tbln Наразі мають досить обмежену підтримку, адже у$\LaTeX$немає нативного поняття блоків коду. +\tbln На щастя, у\textbf{markdown}таке поняття є\footnotemark[2]\footnotetext[2]{Принаймні,\textit{Obsidian}таке робить}: \begin{minted}{rust} let x = 5; let y = x.powi(2); From 2f715b5a0ec4e57f713bcf84d34216ca6044a627 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Tue, 19 Mar 2024 10:23:08 +0200 Subject: [PATCH 25/98] Removed `Formatting` token --- src/data.rs | 8 -------- src/gen/lab/meta.rs | 1 - src/gen/lab/mod.rs | 16 ---------------- src/lexer.rs | 15 --------------- 4 files changed, 40 deletions(-) diff --git a/src/data.rs b/src/data.rs index 3b81a9e..77ff257 100644 --- a/src/data.rs +++ b/src/data.rs @@ -143,8 +143,6 @@ pub enum Token<'source> { list_type: ListType, content: Tokens<'source>, }, - #[deprecated = "`Font`-thing inside paragraph token does similar thing, so it would be wise to merge them"] - Formatted(Formatting, Box>), Text { tokens: Tokens<'source>, }, @@ -301,9 +299,6 @@ impl<'source> Token<'source> { list_type: list_type.clone(), content: Tokens::Borrowed(&content), }, - Token::Formatted(formatting, content) => { - Token::Formatted(formatting.clone(), Box::new(content.borrow_ref())) - } Token::Text { tokens } => Token::Text { tokens: Tokens::Borrowed(tokens), }, @@ -386,9 +381,6 @@ impl<'source> Token<'source> { list_type: list_type.clone(), content: content.to_static(), }, - Token::Formatted(formatting, inner) => { - Token::Formatted(formatting.clone(), inner.to_static()) - } Token::Text { tokens } => Token::Text { tokens: tokens.to_static(), }, diff --git a/src/gen/lab/meta.rs b/src/gen/lab/meta.rs index 0168d6d..2c2b349 100644 --- a/src/gen/lab/meta.rs +++ b/src/gen/lab/meta.rs @@ -100,7 +100,6 @@ where | Token::Header { .. } | Token::Href { .. } | Token::List { .. } - | Token::Formatted(_, _) | Token::Paragraph { .. } | Token::InlineMathmode { .. } | Token::Reference { .. } diff --git a/src/gen/lab/mod.rs b/src/gen/lab/mod.rs index 0d33d57..76212bd 100644 --- a/src/gen/lab/mod.rs +++ b/src/gen/lab/mod.rs @@ -208,22 +208,6 @@ where output.write_str("\n")?; output.write_str(end)?; } - Token::Formatted(formatting, content) => { - let start: &str = match formatting { - crate::data::Formatting::Bold => "\\textbf{", - crate::data::Formatting::Italic => "\\textit{", - // TODO ADD \usepackage{soul} TO PREAMBLE! - crate::data::Formatting::StrikeThrough => "\\st{", - }; - let end: &str = match formatting { - crate::data::Formatting::Bold - | crate::data::Formatting::Italic - | crate::data::Formatting::StrikeThrough => "}", - }; - output.write_str(start)?; - self.write_to(output, meta, context, content)?; - output.write_str(end)?; - } Token::InlineMathmode { content } => { write!(output, "${content}$")?; } diff --git a/src/lexer.rs b/src/lexer.rs index 3ffa474..44946e8 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -718,21 +718,6 @@ fn list< context("list", _list_inner)(input) } -#[deprecated] -fn _formatting_inner< - 'source, - E: ParseError<&'source str> - + ContextError<&'source str> - + FromExternalError<&'source str, KyomatoLexError> - + 'source, ->( - input: &'source str, - style: crate::data::Formatting, -) -> Result, nom::Err> { - let (_, content) = all_consuming(terminated(inner_lex, space0))(input)?; - Ok(Token::Formatted(style, Box::new(content))) -} - /// Parses formatting declaration (or whatever you wanna call that) /// /// **WARNING**: I expect this parse to be kinda-heavy. Consider using it *as late as possible*, but not before regular text parser, obviously. From 50c7795be6ad988f56f756279081f020f32a9f12 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Tue, 19 Mar 2024 10:29:50 +0200 Subject: [PATCH 26/98] Renamed some of the tokens to a better-suiting names --- src/data.rs | 30 +++++++++++------------ src/gen/ayano.rs | 14 +++++------ src/gen/lab/meta.rs | 12 +++++----- src/gen/lab/mod.rs | 40 +++++++++++++++---------------- src/lexer.rs | 42 ++++++++++++++++----------------- tests/lex_files/ayano.txt | 2 +- tests/lex_files/basic.txt | 18 +++++++------- tests/lex_files/deceit.txt | 24 +++++++++---------- tests/lex_files/tables_code.txt | 28 +++++++++++----------- 9 files changed, 105 insertions(+), 105 deletions(-) diff --git a/src/data.rs b/src/data.rs index 77ff257..d24e6e7 100644 --- a/src/data.rs +++ b/src/data.rs @@ -115,7 +115,7 @@ pub enum Token<'source> { order: usize, content: Tx<'source>, }, - Equation { + DisplayMath { content: Tx<'source>, ident: Option>, }, @@ -143,7 +143,7 @@ pub enum Token<'source> { list_type: ListType, content: Tokens<'source>, }, - Text { + Multiple { tokens: Tokens<'source>, }, // TODO move font and formatting exclusively to here @@ -153,16 +153,16 @@ pub enum Token<'source> { formatting: Option, content: Tx<'source>, }, - InlineMathmode { + InlineMath { content: Tx<'source>, }, Reference { ident: Tx<'source>, }, - FootNoteReference { + FootnoteReference { ident: Tx<'source>, }, - FootNoteContent { + FootnoteContent { content: Box>, ident: Tx<'source>, }, @@ -264,7 +264,7 @@ impl<'source> Token<'source> { order: *order, content: Cow::Borrowed(content), }, - Token::Equation { content, ident } => Token::Equation { + Token::DisplayMath { content, ident } => Token::DisplayMath { content: Cow::Borrowed(&content), ident: ident.as_ref().map(|s| Cow::<'r, str>::Borrowed(s)), }, @@ -299,7 +299,7 @@ impl<'source> Token<'source> { list_type: list_type.clone(), content: Tokens::Borrowed(&content), }, - Token::Text { tokens } => Token::Text { + Token::Multiple { tokens } => Token::Multiple { tokens: Tokens::Borrowed(tokens), }, Token::Paragraph { @@ -311,16 +311,16 @@ impl<'source> Token<'source> { formatting: formatting.clone(), content: Cow::Borrowed(content), }, - Token::InlineMathmode { content } => Token::InlineMathmode { + Token::InlineMath { content } => Token::InlineMath { content: Cow::Borrowed(&content), }, Token::Reference { ident } => Token::Reference { ident: Cow::Borrowed(&ident), }, - Token::FootNoteReference { ident } => Token::FootNoteReference { + Token::FootnoteReference { ident } => Token::FootnoteReference { ident: Cow::Borrowed(ident), }, - Token::FootNoteContent { content, ident } => Token::FootNoteContent { + Token::FootnoteContent { content, ident } => Token::FootnoteContent { content: Box::new(content.borrow_ref()), ident: Cow::Borrowed(ident), }, @@ -344,7 +344,7 @@ impl<'source> Token<'source> { order: *order, content: content.to_static(), }, - Token::Equation { content, ident } => Token::Equation { + Token::DisplayMath { content, ident } => Token::DisplayMath { content: content.to_static(), ident: ident.as_ref().map(Cow::to_static), }, @@ -381,7 +381,7 @@ impl<'source> Token<'source> { list_type: list_type.clone(), content: content.to_static(), }, - Token::Text { tokens } => Token::Text { + Token::Multiple { tokens } => Token::Multiple { tokens: tokens.to_static(), }, Token::Paragraph { @@ -393,16 +393,16 @@ impl<'source> Token<'source> { formatting: formatting.clone(), content: content.to_static(), }, - Token::InlineMathmode { content } => Token::InlineMathmode { + Token::InlineMath { content } => Token::InlineMath { content: content.to_static(), }, Token::Reference { ident } => Token::Reference { ident: ident.to_static(), }, - Token::FootNoteReference { ident } => Token::FootNoteReference { + Token::FootnoteReference { ident } => Token::FootnoteReference { ident: ident.to_static(), }, - Token::FootNoteContent { content, ident } => Token::FootNoteContent { + Token::FootnoteContent { content, ident } => Token::FootnoteContent { content: content.to_static(), ident: ident.to_static(), }, diff --git a/src/gen/ayano.rs b/src/gen/ayano.rs index 9a66356..5e8cc28 100644 --- a/src/gen/ayano.rs +++ b/src/gen/ayano.rs @@ -1564,10 +1564,10 @@ fn parse_ayano(python_output: &PyAny) -> Result, ParsingError> { #[cfg(test)] fn value_error_token(value: f64, error: f64) -> Token<'static> { - Token::Text { + Token::Multiple { tokens: Tokens::new([ Token::text(value.to_string()), - Token::InlineMathmode { + Token::InlineMath { content: r"\pm".into(), }, Token::text(error.to_string()), @@ -1619,9 +1619,9 @@ mod parsing_tests { test! {err1, |py| PyTuple::new(py, ["err".into_py(py), ["yet another not a number"].into_py(py)]), Err(crate::gen::ayano::ParsingError::UnexpectedSyntax("('err', ['yet another not a number'])".to_string(), py_err!(py)))} test! {value_error1, |py| PyTuple::new(py, ["err".into_py(py), 1.0.into_py(py), 0.5.into_py(py)]), - Ok(Token::Text{tokens: Tokens::new([Token::text("1.0"), Token::InlineMathmode{content:r"\pm".into()}, Token::text("0.5")])})} + Ok(Token::Multiple{tokens: Tokens::new([Token::text("1.0"), Token::InlineMath{content:r"\pm".into()}, Token::text("0.5")])})} test! {value_error2, |py| PyTuple::new(py, ["err".into_py(py), "1.0".into_py(py), 0.5.into_py(py)]), - Ok(Token::Text{tokens:Tokens::new([Token::text("1.0"), Token::InlineMathmode{content:r"\pm".into()}, Token::text("0.5")])})} + Ok(Token::Multiple{tokens:Tokens::new([Token::text("1.0"), Token::InlineMath{content:r"\pm".into()}, Token::text("0.5")])})} test! {value_error_format1, |py| PyTuple::new(py, ["err".into_py(py), "1.987654321".into_py(py), 0.00000123456.into_py(py)]), Ok(value_error_token(1.9876543, 0.0000012))} // TODO probably should add some more tests for value-error formatting @@ -1711,7 +1711,7 @@ mod parsing_tests { Token::text("cell_11"), Token::text("0.25"), Token::text("cell_21"), Token::text("-2"), ]), - caption: Some(Box::new(Token::Text{tokens:Tokens::new([Token::text("table caption with some"),Token::InlineMathmode{content:"formula".into()}])})), + caption: Some(Box::new(Token::Multiple{tokens:Tokens::new([Token::text("table caption with some"),Token::InlineMath{content:"formula".into()}])})), ident: Some("table1".into()) }) } @@ -2226,14 +2226,14 @@ pub fn value_error_format<'v, 'e>( ) -> Result, ParsingError> { let (value, error) = value_error_format_inner(value.into().as_ref(), error.into().as_ref())?; // Result is a combination of text tokens with mathmode +- in between - Ok(Token::Text { + Ok(Token::Multiple { tokens: Tokens::new([ Token::Paragraph { is_newline: false, formatting: None, content: value.into(), }, - Token::InlineMathmode { + Token::InlineMath { content: r"\pm".into(), }, Token::Paragraph { diff --git a/src/gen/lab/meta.rs b/src/gen/lab/meta.rs index 2c2b349..a4b337b 100644 --- a/src/gen/lab/meta.rs +++ b/src/gen/lab/meta.rs @@ -55,7 +55,7 @@ where 'meta: 'r, { match token { - Token::Equation { + Token::DisplayMath { ident: Some(ident), .. } => { dest.refs.borrow_mut().insert(ident); @@ -86,10 +86,10 @@ where collect_single(caption, dest)?; } } - Token::Text { tokens, .. } => { + Token::Multiple { tokens, .. } => { collect_iterable(tokens, dest)?; } - Token::FootNoteContent { content, ident } => { + Token::FootnoteContent { content, ident } => { collect_single(content, dest)?; dest.footnotes.insert(ident, content.borrow_ref()); } @@ -101,11 +101,11 @@ where | Token::Href { .. } | Token::List { .. } | Token::Paragraph { .. } - | Token::InlineMathmode { .. } + | Token::InlineMath { .. } | Token::Reference { .. } - | Token::FootNoteReference { .. } + | Token::FootnoteReference { .. } | Token::Figure { .. } - | Token::Equation { .. } + | Token::DisplayMath { .. } | Token::CodeBlock { .. } => {} Token::Error { .. } => {} } diff --git a/src/gen/lab/mod.rs b/src/gen/lab/mod.rs index 76212bd..b941007 100644 --- a/src/gen/lab/mod.rs +++ b/src/gen/lab/mod.rs @@ -88,7 +88,7 @@ where write!(output, "\n\\{command}{{{content}}}\n")?; } - Token::Equation { content, ident } => { + Token::DisplayMath { content, ident } => { let label = ident .as_ref() .map(|ident| format!("\\label{{eq:{}}}", ident)) @@ -208,13 +208,13 @@ where output.write_str("\n")?; output.write_str(end)?; } - Token::InlineMathmode { content } => { + Token::InlineMath { content } => { write!(output, "${content}$")?; } Token::Reference { ident } => { write!(output, "\\ref{{{ident}}}")?; } - Token::FootNoteReference { ident } => { + Token::FootnoteReference { ident } => { let (encountered, i) = { // TODO probably should move this to separate function let encountered_map = &mut context.encountered_footnotes; @@ -242,7 +242,7 @@ where } } // might seem weird at first, but these are basically useless at this point, as their content was presumably collected into context - Token::FootNoteContent { .. } => {} + Token::FootnoteContent { .. } => {} Token::Error { message } => { eprintln!("{message}"); // TODO add representation in the document @@ -268,7 +268,7 @@ where Token::Href { url, display } => { write!(output, "\\href{{{url}}}{{{display}}}")?; } - Token::Text { tokens } => self.write_tokens_to(output, meta, context, tokens)?, + Token::Multiple { tokens } => self.write_tokens_to(output, meta, context, tokens)?, Token::CodeBlock { code, language } => { if let Some(language) = language { writeln!( @@ -373,13 +373,13 @@ mod tests { test! {href2, Token::Href { url: "https://github.com/Dzuchun".parse().unwrap(), display: "Моя сторінка на гітхабі".into() }, r"\href{https://github.com/Dzuchun}{Моя сторінка на гітхабі}"} // Equations - test! {equations_1, Token::Equation { content: r"B(\alpha, \beta) = \int \limits_{0}^{1} x^{\alpha - 1} (1-x)^{\beta-1} dx".into(), ident: None }, + test! {equations_1, Token::DisplayMath { content: r"B(\alpha, \beta) = \int \limits_{0}^{1} x^{\alpha - 1} (1-x)^{\beta-1} dx".into(), ident: None }, r" \begin{equation} B(\alpha, \beta) = \int \limits_{0}^{1} x^{\alpha - 1} (1-x)^{\beta-1} dx \end{equation} "} - test! {equations_2, Token::Equation { content: r"B(\alpha, \beta) = \int \limits_{0}^{1} x^{\alpha - 1} (1-x)^{\beta-1} dx".into(), ident: Some("beta_def".into()) }, + test! {equations_2, Token::DisplayMath { content: r"B(\alpha, \beta) = \int \limits_{0}^{1} x^{\alpha - 1} (1-x)^{\beta-1} dx".into(), ident: Some("beta_def".into()) }, r" \begin{equation} B(\alpha, \beta) = \int \limits_{0}^{1} x^{\alpha - 1} (1-x)^{\beta-1} dx @@ -389,7 +389,7 @@ mod tests { "} // Inline math - test! {inline_mathmode, Token::InlineMathmode{content:r"\dfrac{\partial x}{\partial t}".into()}, r"$ \dfrac{\partial x}{\partial t} $"} + test! {inline_mathmode, Token::InlineMath{content:r"\dfrac{\partial x}{\partial t}".into()}, r"$ \dfrac{\partial x}{\partial t} $"} // Figures test! {fig1, Token::Figure { src_name: Path::new("apple.jpg").into(), caption: Some(Box::new(text!("a very realistic-looking apple"))), ident: None, width: None }, r" @@ -480,33 +480,33 @@ cell_21 & cell_22 & cell_23 \\ \hline "} // Text - test! {text, Token::Text{tokens:tokens![Token::PageDiv, Token::Header { order: 0, content: "Header".into() }, text!("Here's a little formula: "), Token::InlineMathmode{content:r"(a+b)^2 = a^2 + 2 \cdot a \cdot b + b^2".into()}, text!(". It's very useful, more useful than me!")]}, r" + test! {text, Token::Multiple{tokens:tokens![Token::PageDiv, Token::Header { order: 0, content: "Header".into() }, text!("Here's a little formula: "), Token::InlineMath{content:r"(a+b)^2 = a^2 + 2 \cdot a \cdot b + b^2".into()}, text!(". It's very useful, more useful than me!")]}, r" \clearpage \section{Header} Here's a little formula: $(a+b)^2 = a^2 + 2 \cdot a \cdot b + b^2$. It's very useful, more useful than me! "} // Footnotes - test! {footnote_single, Token::Text{tokens:tokens![ - Token::FootNoteReference{ident:"explanation".into()}, - Token::FootNoteContent { content: Box::new(text!("42")), ident: "explanation".into() } + test! {footnote_single, Token::Multiple{tokens:tokens![ + Token::FootnoteReference{ident:"explanation".into()}, + Token::FootnoteContent { content: Box::new(text!("42")), ident: "explanation".into() } ]}, r" \footnotemark[1] \footnotetext[1]{42} "} - test! {footnote_multiple, Token::Text{tokens:tokens![ + test! {footnote_multiple, Token::Multiple{tokens:tokens![ text!("Here's a first note"), - Token::FootNoteReference{ident:"explanation1".into()}, + Token::FootnoteReference{ident:"explanation1".into()}, text!(", there are also second"), - Token::FootNoteReference{ident: "explanation2".into()}, + Token::FootnoteReference{ident: "explanation2".into()}, text!(", and the third"), - Token::FootNoteReference{ident:"explanation3".into()}, - Token::FootNoteContent { content: Box::new(text!("There are things in this world that's you're not meant to see")), ident: "explanation1".into() }, - Token::FootNoteContent { content: Box::new(text!("now the voice of a deity permeates")), ident: "explanation2".into() }, - Token::FootNoteContent { content: Box::new(text!("see notes 1 and 2")), ident: "explanation3".into() }, + Token::FootnoteReference{ident:"explanation3".into()}, + Token::FootnoteContent { content: Box::new(text!("There are things in this world that's you're not meant to see")), ident: "explanation1".into() }, + Token::FootnoteContent { content: Box::new(text!("now the voice of a deity permeates")), ident: "explanation2".into() }, + Token::FootnoteContent { content: Box::new(text!("see notes 1 and 2")), ident: "explanation3".into() }, text!(". But notes will not be duplicated here!"), - Token::FootNoteReference{ident:"explanation1".into()} + Token::FootnoteReference{ident:"explanation1".into()} ]}, r" Here's a first note diff --git a/src/lexer.rs b/src/lexer.rs index 44946e8..1efc5d3 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -238,7 +238,7 @@ fn equation< )(input)?; Ok(( rest, - Token::Equation { + Token::DisplayMath { content: content.into(), ident: ident.map(Cow::from), }, @@ -796,7 +796,7 @@ fn inline_math<'source, E: ParseError<&'source str> + ContextError<&'source str> delimited(char('$'), take_till1(|c| c == '\n' || c == '$'), char('$')), ), ) - .map(|content: &str| Token::InlineMathmode { + .map(|content: &str| Token::InlineMath { content: content.trim().into(), }) .parse(input) @@ -848,7 +848,7 @@ fn footnote_reference<'source, E: ParseError<&'source str> + ContextError<&'sour )(input)?; Ok(( rest, - Token::FootNoteReference { + Token::FootnoteReference { ident: ident.trim().into(), }, )) @@ -883,7 +883,7 @@ fn footnote_content< .and_then(cut(all_consuming(terminated(inner_lex, space0)))), )), ) - .map(|(ident, content)| Token::FootNoteContent { + .map(|(ident, content)| Token::FootnoteContent { content: Box::new(content), ident: ident.trim().into(), }) @@ -1251,7 +1251,7 @@ fn tokens_many1< |inner_result| match inner_result { InnerResult::None => Err(KyomatoLexError::no_tokens(outer_input)), InnerResult::Single(token) => Ok(token), - InnerResult::Multiple(tokens) => Ok(Token::Text { + InnerResult::Multiple(tokens) => Ok(Token::Multiple { tokens: Tokens::new(tokens), }), }, @@ -1522,7 +1522,7 @@ mod tests { $$ y = x^2 $$ -12344", e: Ok(("\n12344", Token::Equation { content: Cow::Borrowed("y = x^2"), ident: None }))} +12344", e: Ok(("\n12344", Token::DisplayMath { content: Cow::Borrowed("y = x^2"), ident: None }))} // Like that! test! {equation6, equation, r" $$y = x^2 @@ -1538,24 +1538,24 @@ z = y$$; $$ y = x^2 z = y -$$;12344", e: Ok((";12344", Token::Equation { content: "y = x^2\nz = y".into(), ident: None }))} +$$;12344", e: Ok((";12344", Token::DisplayMath { content: "y = x^2\nz = y".into(), ident: None }))} // This is alright, though test! {equation9, equation, r#" $$ y = x^2 $$ -{ref = parabola}"#, e: Ok(("", Token::Equation { content: "y = x^2".into(), ident: Some("parabola".into()) }))} +{ref = parabola}"#, e: Ok(("", Token::DisplayMath { content: "y = x^2".into(), ident: Some("parabola".into()) }))} test! {equation9_5, equation, r#" $$ y = x^2 $$ -{ref = parabola_ref}"#, e: Ok(("", Token::Equation { content: "y = x^2".into(), ident: Some("parabola_ref".into()) }))} +{ref = parabola_ref}"#, e: Ok(("", Token::DisplayMath { content: "y = x^2".into(), ident: Some("parabola_ref".into()) }))} // Equation ident example test! {equation10, equation, r#" $$ y = x^2 $$ -{ref = parabola_1}"#, e: Ok(("", Token::Equation { content: "y = x^2".into(), ident: Some("parabola_1".into()) }))} +{ref = parabola_1}"#, e: Ok(("", Token::DisplayMath { content: "y = x^2".into(), ident: Some("parabola_1".into()) }))} // This is not ok // TODO although that's subject to change test! {equation11, equation, r#" @@ -1568,13 +1568,13 @@ $$ $$ y = x^2 $$ -{ref=11 } якийсь текст тому що чому ні"#, e: Ok((" якийсь текст тому що чому ні", Token::Equation { content: "y = x^2 ".into(), ident: Some("11".into()) }))} +{ref=11 } якийсь текст тому що чому ні"#, e: Ok((" якийсь текст тому що чому ні", Token::DisplayMath { content: "y = x^2 ".into(), ident: Some("11".into()) }))} // Any sort of whitespaces are preserved test! {equation13, equation, r#" $$ y = x^2 $$ -{} якийсь текст тому що чому ні"#, e: Ok((" якийсь текст тому що чому ні", Token::Equation { content: "y = x^2 ".into(), ident: None }))} +{} якийсь текст тому що чому ні"#, e: Ok((" якийсь текст тому що чому ні", Token::DisplayMath { content: "y = x^2 ".into(), ident: None }))} // You can explicitly provide no args - fun fact :idk: // `href` tests @@ -1632,7 +1632,7 @@ $$ macro_rules! test_ok { {$name:ident, $input:literal, $formula:literal, $left_over:literal} => { - test!{$name, inline_math, $input, e: Ok(($left_over, Token::InlineMathmode{content:$formula.into()}))} + test!{$name, inline_math, $input, e: Ok(($left_over, Token::InlineMath{content:$formula.into()}))} }; } @@ -1677,7 +1677,7 @@ $$ macro_rules! test_ok { {$name:ident, $input:literal, $reference:literal, $left_over:literal} => { - test!{$name, footnote_reference, $input, e: Ok(($left_over, Token::FootNoteReference{ident:$reference.into()}))} + test!{$name, footnote_reference, $input, e: Ok(($left_over, Token::FootnoteReference{ident:$reference.into()}))} }; } @@ -1718,10 +1718,10 @@ $$ /* test_ok! {ok_formatting_in_formatting, "\t \n You can even use ~~__double-formatted__~~ things!", "You can even use", Some(Token::Formatted(Formatting::StrikeThrough, Box::new(Token::Formatted(Formatting::Bold, Box::new(Token::text("double-formatted")))))), " things!"} */ test_ok! {ok_equation, " \t Inline math is fine too: $y = x_{\text{поч}} + x_0$.", "Inline math is fine too:", - Some(Token::InlineMathmode{content: "y = x_{\text{поч}} + x_0".into()}), "."} + Some(Token::InlineMath{content: "y = x_{\text{поч}} + x_0".into()}), "."} test_ok! {ok_href, " \t\t\n You can even [залишити посилання на свою сторінку на Гітхабі ](https://www.github.com/Dzuchun)!", "You can even", Some(Token::Href { url: "https://www.github.com/Dzuchun".parse().expect("That's a valid url"), display: "залишити посилання на свою сторінку на Гітхабі".into() }), "!", |} - test_ok! {ok_footnote_ref, "\t Footnote refs[^1] are also ok too!", "Footnote refs", Some(Token::FootNoteReference{ident: "1".into()}), " are also ok too!"} + test_ok! {ok_footnote_ref, "\t Footnote refs[^1] are also ok too!", "Footnote refs", Some(Token::FootnoteReference{ident: "1".into()}), " are also ok too!"} test_ok! {ok_obj_ref, "\n\t Thi[[ngs lik_e tables* ([@tab:results]) can also be referenced", "Thi[[ngs lik_e tables* (", Some(Token::Reference{ident: "tab:results".into()}), ") can also be referenced", |} } @@ -1742,13 +1742,13 @@ $$ eq!($eq, Some(Cow::Borrowed($ident))) }; ($eq:literal, $ident:expr) => { - Token::Equation { + Token::DisplayMath { content: Cow::Borrowed($eq), ident: $ident, } }; (!$eq:literal) => { - Token::InlineMathmode { + Token::InlineMath { content: Cow::Borrowed($eq), } }; @@ -1782,7 +1782,7 @@ $$ } }; (^ $ident:literal) => { - Token::FootNoteReference { + Token::FootnoteReference { ident: Cow::Borrowed($ident), } }; @@ -1810,7 +1810,7 @@ $$ } macro_rules! tks { [$($token:expr), +] => { - Token::Text{tokens: Tokens::new([$($token), +])} + Token::Multiple{tokens: Tokens::new([$($token), +])} }; } mod inner_lex { @@ -2013,7 +2013,7 @@ $$ macro_rules! foot { (^$ident:literal: $content:expr) => { - Token::FootNoteContent { + Token::FootnoteContent { ident: Cow::Borrowed($ident), content: Box::new($content), } diff --git a/tests/lex_files/ayano.txt b/tests/lex_files/ayano.txt index 1c7fb62..dcd6fe9 100644 --- a/tests/lex_files/ayano.txt +++ b/tests/lex_files/ayano.txt @@ -1,4 +1,4 @@ -Text { +Multiple { tokens: Owned( [ Paragraph { diff --git a/tests/lex_files/basic.txt b/tests/lex_files/basic.txt index a09de50..9d43b1c 100644 --- a/tests/lex_files/basic.txt +++ b/tests/lex_files/basic.txt @@ -1,4 +1,4 @@ -Text { +Multiple { tokens: Owned( [ Header { @@ -10,7 +10,7 @@ Text { formatting: None, content: "Тут трохи тексту і пояснень про квантові мультивироджені рефурбалідзери змішаного типу. Це нудно, тож тримайте формулу:", }, - Equation { + DisplayMath { content: "Ref(z) = \\int \\limits_{0}^{1} t^{z-1} \\cdot \\exp(-t) dt", ident: Some( "refurbalidzer_1", @@ -29,7 +29,7 @@ Text { list_type: Bullet, content: Owned( [ - Text { + Multiple { tokens: Owned( [ Paragraph { @@ -37,13 +37,13 @@ Text { formatting: None, content: "Нормувальний множник", }, - InlineMathmode { + InlineMath { content: "|\\psi|^{2} = 3/4", }, ], ), }, - Text { + Multiple { tokens: Owned( [ Paragraph { @@ -51,7 +51,7 @@ Text { formatting: None, content: "Кількість хромосом -", }, - InlineMathmode { + InlineMath { content: "\\exp(\\pi)", }, ], @@ -94,7 +94,7 @@ Text { formatting: None, content: "можна бачити принципову схему влаштування рефурбалідзера. Неважно помітити великі очі", }, - FootNoteReference { + FootnoteReference { ident: "1", }, Paragraph { @@ -102,7 +102,7 @@ Text { formatting: None, content: ", і два термоядерних реактори, що оточують їх. Точні реакції всередині невідомі, але цілком можливо що там женуть", }, - InlineMathmode { + InlineMath { content: "du + t -> {}^{5}_{6}C", }, Paragraph { @@ -110,7 +110,7 @@ Text { formatting: None, content: "...", }, - FootNoteContent { + FootnoteContent { content: Paragraph { is_newline: false, formatting: None, diff --git a/tests/lex_files/deceit.txt b/tests/lex_files/deceit.txt index f7142e6..40ebaf2 100644 --- a/tests/lex_files/deceit.txt +++ b/tests/lex_files/deceit.txt @@ -1,4 +1,4 @@ -Text { +Multiple { tokens: Owned( [ Paragraph { @@ -123,7 +123,7 @@ Text { formatting: None, content: "Inline:", }, - InlineMathmode { + InlineMath { content: "y = x ^ 2", }, Paragraph { @@ -131,7 +131,7 @@ Text { formatting: None, content: "Display:", }, - Equation { + DisplayMath { content: "y = x ^ 2", ident: None, }, @@ -179,7 +179,7 @@ Text { formatting: None, content: "(this should be display mathmode):", }, - Equation { + DisplayMath { content: "|x|", ident: None, }, @@ -203,7 +203,7 @@ Text { formatting: None, content: "Figure is defined by a ![[ sequence at a start of the line. Same points should be uphold here:", }, - Equation { + DisplayMath { content: "![[", ident: None, }, @@ -310,7 +310,7 @@ Text { formatting: None, content: "Here are correct footnote refs:", }, - FootNoteReference { + FootnoteReference { ident: "1", }, Paragraph { @@ -318,7 +318,7 @@ Text { formatting: None, content: ",", }, - FootNoteReference { + FootnoteReference { ident: "source", }, Paragraph { @@ -486,7 +486,7 @@ Text { formatting: None, content: "These start with '", }, - FootNoteReference { + FootnoteReference { ident: "ID", }, Paragraph { @@ -499,7 +499,7 @@ Text { formatting: None, content: "Example of valid content:", }, - FootNoteContent { + FootnoteContent { content: Paragraph { is_newline: false, formatting: None, @@ -517,7 +517,7 @@ Text { formatting: None, content: "[^source2: This is invalid content", }, - FootNoteReference { + FootnoteReference { ident: "source2", }, Paragraph { @@ -525,7 +525,7 @@ Text { formatting: None, content: ": This is invalid content", }, - FootNoteReference { + FootnoteReference { ident: "source2", }, Paragraph { @@ -538,7 +538,7 @@ Text { formatting: None, content: "They also must start at the beginning of the line. If fact, this:", }, - FootNoteReference { + FootnoteReference { ident: "source2", }, Paragraph { diff --git a/tests/lex_files/tables_code.txt b/tests/lex_files/tables_code.txt index 46c8c49..0c7e077 100644 --- a/tests/lex_files/tables_code.txt +++ b/tests/lex_files/tables_code.txt @@ -1,4 +1,4 @@ -Text { +Multiple { tokens: Owned( [ Paragraph { @@ -71,25 +71,25 @@ Text { formatting: None, content: "2", }, - InlineMathmode { + InlineMath { content: "3 + e ^{\\pi}", }, - Text { + Multiple { tokens: Owned( [ - InlineMathmode { + InlineMath { content: "e", }, - FootNoteReference { + FootnoteReference { ident: "euler", }, ], ), }, - InlineMathmode { + InlineMath { content: "\\cdot \\pm 1", }, - Text { + Multiple { tokens: Owned( [ Paragraph { @@ -115,7 +115,7 @@ Text { ], ), caption: Some( - Text { + Multiple { tokens: Owned( [ Paragraph { @@ -192,8 +192,8 @@ Text { "table_2", ), }, - FootNoteContent { - content: Text { + FootnoteContent { + content: Multiple { tokens: Owned( [ Paragraph { @@ -239,7 +239,7 @@ Text { formatting: None, content: "Наразі мають досить обмежену підтримку, адже у", }, - InlineMathmode { + InlineMath { content: "\\LaTeX", }, Paragraph { @@ -264,7 +264,7 @@ Text { formatting: None, content: "таке поняття є", }, - FootNoteReference { + FootnoteReference { ident: "code", }, Paragraph { @@ -278,8 +278,8 @@ Text { "rust", ), }, - FootNoteContent { - content: Text { + FootnoteContent { + content: Multiple { tokens: Owned( [ Paragraph { From 4ecf8cd1cb90105bdacfc37fcf09bdcfb58425e5 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Fri, 22 Mar 2024 17:01:08 +0200 Subject: [PATCH 27/98] Started developing provider concept - involves unsafe now :/ --- src/data.rs | 417 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 417 insertions(+) diff --git a/src/data.rs b/src/data.rs index d24e6e7..8417e6c 100644 --- a/src/data.rs +++ b/src/data.rs @@ -416,3 +416,420 @@ impl<'source> Token<'source> { } } } + +/// This module contains, documents and helps developing my concept for refined token borrowing. +/// +/// Main idea is to ALWAYS delegate token information storage. This is expected to minimize token size. +/// Problem with that idea - some tokens need to own their data, they need to be independent and live indefinitely. +/// +/// To allow this, there would be a special token type - so-called `Owner` - holding text buffer for all their children to interpret and refer to. +/// A now-implemented with cloning `to_static` method would instead wrap token(s) into a new `Owner`, allowing them to exist indefinitely, +/// while in fact only cheap pointers were substituted. +/// +/// The next step would be to realize that tokens don't even need to own their children - instead upper-standing owned could own them instead, +/// providing references to children tokens. This should **DRAMATICALLY** decrease number of heap allocations, as it would allow tokens for centralized data storage, +/// allocating only two memory arrays. +/// +/// Now, here's a thing - vectors can be re-allocated in-memory, when their size reached their capacity. This presents another challenge: we should somehow guarantee, +/// that data referred by child tokens will not be moved out of the pointers (compiler will **force** us to do that, actually). I see two possible solutions on that matter: +/// +/// - while token tree creation, `Owner` holds data in a regular vector, giving children only a *promise* to data (not a pointer). +/// once generation is complete, token tree is not supposed to be changed. at this point, `Owner` will allow children to *obtain* the pointers to now-immovable data. +/// - same as before, but children never get to see pointers themselves. this will allow `Owner` to freely mutate data, but will add an extra cost for some sort of +/// internal identifier resolution and a couple more function calls. +/// +/// Being a masochist I am, of course I went on with the first option. Following is a log of the development +/// +/// # LOG +/// +/// After brief prototyping, this looks really promising; New tokens type's size is only 40 (5 bytes). Paired with TWO allocations per tree this should be insane. +/// +/// When I revised once again my design, It seems that there's no reason to make `Owner` a variant of token. In fact, we can call it a `TokenOwner` and call it a day. +/// Any place concerned about token's life would accept `TokenOwner`. +/// +/// Since `TokenOwner` will own EVERYTHING about `Token`s inside it, ~~there's no need for lifetimes on it.~~ +/// it's lifetime should not be used in any way, it's just a way to avoid `'static` lifetime, which can be used here, since, +/// IN FACT, data DOES NOT live to the end of the program. I would *probably ok* to use them, but I won't do that. +/// +/// Seems like there's no crate achieving exactly what I want, meaning I'll have to result to **unsafe** myself. +/// +/// New idiom: `Token` can't exist int a vacuum anymore, it's an internal implementation of `TokenTree` +/// +/// `Owner` was renamed into `Provider`. Now inner token and text storage is generalized by a trait; +/// `Provider` can decide themselves, if they want to actually store the data, or keep as a reference to th. +/// +/// There are now three traits related to provider concept: +/// - `UnbackedProvider` represents a provider that's ready to accept new things to be stored. +/// - `BackedProvider` represents a provider that's no longer able to accept new values, but instead can provide references to them +/// - `FixedProvider` represents a provider that's no longer able nor add nor provide values. +/// It can be as minified as possible, only storing data without any sort of bookkeeping. +/// +/// +#[cfg(test)] +mod borrowing_concept { + use std::marker::PhantomData; + use std::mem::MaybeUninit; + use std::ops::Range; + use std::pin::Pin; + + use crate::util::Immutable; + + trait UnbakedProvider<'owner> { + type TP; + type CP; + type MCP; + type Baked: BakedProvider<'owner, TP = Self::TP, CP = Self::CP, MCP = Self::MCP>; + fn register_text<'r>(&'r mut self, text: &str) -> Self::TP; + fn register_child<'r>( + &'r mut self, + child: UnbakedThing, + ) -> Self::CP; + fn register_children<'r>( + &'r mut self, + children: impl IntoIterator>, + ) -> Self::MCP; + fn bake(self) -> Self::Baked; + } + + trait BakedProvider<'owner> { + type TP; + type CP; + type MCP; + type Fixed: FixedProvider<'owner>; + fn get_text<'r>(&'r mut self, promise: Self::TP) -> &'owner str; + fn get_child<'r>(&'r mut self, promise: Self::CP) -> &'owner Thing<'owner>; + fn get_children<'r>(&'r mut self, promise: Self::MCP) -> &'owner [Thing<'owner>]; + fn fix(self) -> Self::Fixed; + } + + trait FixedProvider<'owner> { + type SP: FixedProvider<'static>; + fn to_static(&self) -> Self::SP; + fn inner(&self) -> &Thing<'owner>; + } + + #[derive(Debug, Clone)] + struct TextPromise(Range); + #[derive(Debug, Clone)] + struct ChildPromise(usize); + #[derive(Debug, Clone)] + struct ChildrenPromise(Range); + + struct UnbakedOwner<'owner> { + text: String, + children: Vec>, + _phantom: PhantomData<&'owner ()>, + } + + impl UnbakedOwner<'_> { + fn new() -> Self { + Self { + text: String::new(), + children: Vec::new(), + _phantom: PhantomData, + } + } + } + + impl<'owner> UnbakedProvider<'owner> for UnbakedOwner<'owner> { + type TP = TextPromise; + type CP = ChildPromise; + type MCP = ChildrenPromise; + type Baked = BakedOwner<'owner>; + + fn register_text<'r>(&'r mut self, text: &str) -> Self::TP { + let slice_start = self.text.len(); + self.text.push_str(text); + let slice_end = self.text.len(); + TextPromise(slice_start..slice_end) + } + + fn register_child<'r>( + &'r mut self, + child: UnbakedThing, + ) -> Self::CP { + let ind = self.children.len(); + self.children.push(child); + ChildPromise(ind) + } + + fn register_children<'r>( + &'r mut self, + children: impl IntoIterator>, + ) -> Self::MCP { + let slice_start = self.children.len(); + self.children.extend(children); + let slice_end = self.children.len(); + ChildrenPromise(slice_start..slice_end) + } + + fn bake(self) -> Self::Baked { + let children_num = self.children.len(); + let mut baked_children = Vec::with_capacity(children_num); + for _ in 0..children_num { + baked_children.push(MaybeUninit::uninit()); + } + BakedOwner { + text: Immutable::from(Pin::new(self.text.into_boxed_str())), + unbaked_children: self.children.into_boxed_slice(), + baked_children: Pin::new(baked_children.into_boxed_slice()), + baked_children_mask: vec![false; children_num].into_boxed_slice(), + _phantom: PhantomData, + } + } + } + + struct BakedOwner<'owner> { + text: Immutable>>, + unbaked_children: Box<[UnbakedThing]>, + baked_children: Pin>]>>, + baked_children_mask: Box<[bool]>, + _phantom: PhantomData &'owner ()>, + } + + impl<'owner> BakedOwner<'owner> { + fn bake_a_child(&mut self, child_ind: usize) { + let unbaked = self.unbaked_children[child_ind].clone(); + let baked = unbaked.bake(self); + self.baked_children[child_ind].write(baked); + } + } + + impl<'owner> BakedProvider<'owner> for BakedOwner<'owner> { + type TP = TextPromise; + type CP = ChildPromise; + type MCP = ChildrenPromise; + type Fixed = FixedOwner<'owner>; + + fn get_text<'r>(&'r mut self, promise: Self::TP) -> &'owner str { + // SAFETY: + // This struct has a phantom field indicating that it lives exactly for `'owner`. + // Along with rest of the fields being **pinned** **and immutable**, this means that returned + // immutable reference will indeed live for `'owner`. + unsafe { std::mem::transmute(&self.text[promise.0]) } + } + + fn get_child<'r>(&'r mut self, ChildPromise(ind): Self::CP) -> &'owner Thing<'owner> { + // first, check if child is init + if !self.baked_children_mask[ind] { + // it's not, make it init + self.bake_a_child(ind); + } + // SAFETY: + // we keep track of unbaked children separately, and we've just checked above that this exact token is, in fact, initialized + let reference = unsafe { self.baked_children[ind].assume_init_ref() }; + // SAFETY: + // This struct has a phantom field indicating that it lives exactly for `'owner`. + // Along with rest of the fields being **pinned** **and immutable**, this means that returned + // immutable reference will indeed live for `'owner`. + unsafe { &*(reference as *const _) } + } + + fn get_children<'r>( + &'r mut self, + ChildrenPromise(range): Self::MCP, + ) -> &'owner [Thing<'owner>] { + // first, check if all children in slice are init + for ind in range.clone() { + if !self.baked_children_mask[ind] { + // this child is not init, so init it + self.bake_a_child(ind); + } + } + // SAFETY: + // we keep track of unbaked children separately, and we've check that all of the tokens in slice to, in fact, exist + let slice: &'r [Thing<'owner>] = + unsafe { std::mem::transmute(&self.baked_children[range]) }; + // SAFETY: + // This struct has a phantom field indicating that it lives exactly for `'owner`. + // Along with rest of the fields being **pinned** **and immutable**, this means that returned + // immutable reference will indeed live for `'owner`. + unsafe { &*(slice as *const _) } + } + + fn fix(self) -> Self::Fixed { + let Self { + text, + baked_children_mask, + baked_children, + .. + } = self; + assert!( + baked_children_mask.into_iter().all(|b| *b), + "Not all children were initialized, so we cannot create output struct" + ); + // SAFETY: + // we keep track of unbaked children separately, and should've panicked above, if any of them were not properly init. + let children: Immutable]>>> = + unsafe { std::mem::transmute(baked_children) }; + FixedOwner { + _text: text, + children, + _phantom: PhantomData, + } + } + } + + struct FixedOwner<'owner> { + _text: Immutable>>, + children: Immutable]>>>, + _phantom: PhantomData &'owner ()>, + } + + impl<'owner> FixedProvider<'owner> for FixedOwner<'owner> { + type SP = FixedOwner<'static>; + + fn to_static(&self) -> Self::SP { + todo!() + } + + fn inner(&self) -> &'owner Thing<'owner> { + // SAFETY: + // This struct has a phantom field indicating that it lives exactly for `'owner`. + // Along with rest of the fields being **pinned** **and immutable**, this means that returned + // immutable reference will indeed live for `'owner`. + let inner: &'owner Thing<'owner> = unsafe { &*(&self.children[0] as *const _) }; + inner + } + } + + #[derive(Debug, Clone)] + enum UnbakedThing { + Text { text: TP }, + Child { text: TP, child1: CP, child2: CP }, + Multiple { children: MCP }, + } + + impl<'owner, TP, CP, MCP> UnbakedThing { + fn bake( + self, + provider: &mut impl BakedProvider<'owner, TP = TP, CP = CP, MCP = MCP>, + ) -> Thing<'owner> { + match self { + UnbakedThing::Child { + text, + child1, + child2, + } => Thing::Child { + text: provider.get_text(text), + child1: provider.get_child(child1), + child2: provider.get_child(child2), + }, + UnbakedThing::Multiple { children } => Thing::Multiple { + children: provider.get_children(children), + }, + UnbakedThing::Text { text } => Thing::Text { + text: provider.get_text(text), + }, + } + } + } + #[derive(Debug)] + enum Thing<'source> { + Text { + text: &'source str, + }, + Child { + text: &'source str, + child1: &'source Thing<'source>, + child2: &'source Thing<'source>, + }, + Multiple { + children: &'source [Thing<'source>], + }, + } + + #[cfg(test)] + mod tests { + use crate::data::borrowing_concept::{BakedProvider, UnbakedProvider, UnbakedThing}; + + #[test] + // #[cfg(miri)] + fn miri_fuzz() { + use crate::data::borrowing_concept::{ + ChildPromise, ChildrenPromise, TextPromise, Thing, UnbakedOwner, + }; + + // arrange + fn gen_thing<'owner, R: rand::Rng + ?Sized>( + provider: &mut UnbakedOwner<'owner>, + rng: &mut R, + ) -> ( + UnbakedThing, + Thing<'static>, + ) { + fn gen_text(rng: &mut R) -> &'static str { + // let it be of some length + let l: usize = rng.gen_range(3..=10); + let mut s = String::new(); + for _ in 0..l { + s.push(rng.gen()) + } + Box::leak(s.into_boxed_str()) + } + let generated = if rng.gen_bool(0.6) { + // assume 60% are text variants + // we need some random text for it + let text = gen_text(rng); + ( + UnbakedThing::Text { + text: provider.register_text(text), + }, + Thing::Text { text }, + ) + } else if rng.gen_bool(0.9) { + // say, 36% are child owners + // we need some text and children for it + let text = gen_text(rng); + let (c1_pr, ch1) = gen_thing(provider, rng); + let (c2_pr, ch2) = gen_thing(provider, rng); + ( + UnbakedThing::Child { + text: provider.register_text(text), + child1: provider.register_child(c1_pr), + child2: provider.register_child(c2_pr), + }, + Thing::Child { + text, + child1: Box::leak(Box::new(ch1)), + child2: Box::leak(Box::new(ch2)), + }, + ) + } else { + // rest 4% own 1-5 children + let num_of_children = rng.gen_range(1..=5); + let mut children = Vec::with_capacity(num_of_children); + let mut children_pr = Vec::with_capacity(num_of_children); + for _ in 0..num_of_children { + let (ch_pr, child) = gen_thing(provider, rng); + children.push(child); + children_pr.push(ch_pr.clone()); + } + let children_pr = provider.register_children(children_pr); + ( + UnbakedThing::Multiple { + children: children_pr, + }, + Thing::Multiple { + children: &*Box::leak(children.into_boxed_slice()), + }, + ) + }; + // average coefficient is 0.6 * 0 + 0.36 * 2 + 2.5 * 0.04 = 0.82 + // meaning this thing is likely to die at some point (I hope for that, at least) + generated + } + let mut provider = UnbakedOwner::new(); + + // act + let (promise, thing) = gen_thing(&mut provider, &mut rand::thread_rng()); + let mut provider = provider.bake(); + let baked_thing = promise.bake(&mut provider); + + // assert + let thing_str = format!("{thing:?}"); + let baked_str = format!("{baked_thing:?}"); + assert_eq!(thing_str, baked_str, "Should produce identical strs"); + } + } +} From 2bd1dd770a32eecebf2ea833772548410ce83fe9 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Sat, 23 Mar 2024 00:26:50 +0200 Subject: [PATCH 28/98] nightly; completely broken state --- src/data.rs | 193 +++++++++++++++++++++++++++++----------------------- 1 file changed, 109 insertions(+), 84 deletions(-) diff --git a/src/data.rs b/src/data.rs index 8417e6c..901cf2b 100644 --- a/src/data.rs +++ b/src/data.rs @@ -467,18 +467,18 @@ impl<'source> Token<'source> { /// #[cfg(test)] mod borrowing_concept { - use std::marker::PhantomData; use std::mem::MaybeUninit; - use std::ops::Range; + use std::ops::{Deref, Range}; use std::pin::Pin; + use std::rc::Rc; use crate::util::Immutable; - trait UnbakedProvider<'owner> { + trait UnbakedProvider { type TP; type CP; type MCP; - type Baked: BakedProvider<'owner, TP = Self::TP, CP = Self::CP, MCP = Self::MCP>; + type Baked: BakedProvider; fn register_text<'r>(&'r mut self, text: &str) -> Self::TP; fn register_child<'r>( &'r mut self, @@ -491,22 +491,21 @@ mod borrowing_concept { fn bake(self) -> Self::Baked; } - trait BakedProvider<'owner> { + trait BakedProvider { type TP; type CP; type MCP; - type Fixed: FixedProvider<'owner>; - fn get_text<'r>(&'r mut self, promise: Self::TP) -> &'owner str; - fn get_child<'r>(&'r mut self, promise: Self::CP) -> &'owner Thing<'owner>; - fn get_children<'r>(&'r mut self, promise: Self::MCP) -> &'owner [Thing<'owner>]; + type TA; + type CA; + type MCA; + type Fixed: FixedProvider; + fn get_text(&mut self, promise: Self::TP) -> Self::TA; + fn get_child(&mut self, promise: Self::CP) -> Self::CA; + fn get_children(&mut self, promise: Self::MCP) -> Self::MCA; fn fix(self) -> Self::Fixed; } - trait FixedProvider<'owner> { - type SP: FixedProvider<'static>; - fn to_static(&self) -> Self::SP; - fn inner(&self) -> &Thing<'owner>; - } + trait FixedProvider {} #[derive(Debug, Clone)] struct TextPromise(Range); @@ -515,27 +514,52 @@ mod borrowing_concept { #[derive(Debug, Clone)] struct ChildrenPromise(Range); - struct UnbakedOwner<'owner> { - text: String, - children: Vec>, - _phantom: PhantomData<&'owner ()>, + struct SharedText(Rc, Range); + impl Deref for SharedText { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0[self.1.clone()] + } } - impl UnbakedOwner<'_> { + struct SharedThing(Rc<[T]>, usize); + impl Deref for SharedThing { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0[self.1] + } + } + + struct SharedThings(Rc<[T]>, Range); + impl Deref for SharedThings { + type Target = [T]; + + fn deref(&self) -> &Self::Target { + &self.0[self.1.clone()] + } + } + + struct UnbakedOwner { + text: String, + children: Vec>, + } + + impl UnbakedOwner { fn new() -> Self { Self { text: String::new(), children: Vec::new(), - _phantom: PhantomData, } } } - impl<'owner> UnbakedProvider<'owner> for UnbakedOwner<'owner> { + impl UnbakedProvider for UnbakedOwner { type TP = TextPromise; type CP = ChildPromise; type MCP = ChildrenPromise; - type Baked = BakedOwner<'owner>; + type Baked = BakedOwner; fn register_text<'r>(&'r mut self, text: &str) -> Self::TP { let slice_start = self.text.len(); @@ -574,49 +598,49 @@ mod borrowing_concept { unbaked_children: self.children.into_boxed_slice(), baked_children: Pin::new(baked_children.into_boxed_slice()), baked_children_mask: vec![false; children_num].into_boxed_slice(), - _phantom: PhantomData, } } } - struct BakedOwner<'owner> { + #[derive(Debug)] + struct BakedOwner { text: Immutable>>, unbaked_children: Box<[UnbakedThing]>, - baked_children: Pin>]>>, + baked_children: Pin]>>, baked_children_mask: Box<[bool]>, - _phantom: PhantomData &'owner ()>, } - impl<'owner> BakedOwner<'owner> { + impl BakedOwner { fn bake_a_child(&mut self, child_ind: usize) { let unbaked = self.unbaked_children[child_ind].clone(); let baked = unbaked.bake(self); self.baked_children[child_ind].write(baked); + self.baked_children_mask[child_ind] = true; } } - impl<'owner> BakedProvider<'owner> for BakedOwner<'owner> { + impl BakedProvider for BakedOwner { type TP = TextPromise; type CP = ChildPromise; type MCP = ChildrenPromise; - type Fixed = FixedOwner<'owner>; + type TA = SharedText; + type CA = SharedThing; + type MCA = SharedThings; + type Fixed = FixedOwner; - fn get_text<'r>(&'r mut self, promise: Self::TP) -> &'owner str { - // SAFETY: - // This struct has a phantom field indicating that it lives exactly for `'owner`. - // Along with rest of the fields being **pinned** **and immutable**, this means that returned - // immutable reference will indeed live for `'owner`. - unsafe { std::mem::transmute(&self.text[promise.0]) } + fn get_text(&mut self, TextPromise(range): Self::TP) -> Self::TA { + SharedText(Rc::clone(&self.text), range) } - fn get_child<'r>(&'r mut self, ChildPromise(ind): Self::CP) -> &'owner Thing<'owner> { + fn get_child<'r>(&'r mut self, ChildPromise(ind): Self::CP) -> Self::CA { + todo!(); // first, check if child is init if !self.baked_children_mask[ind] { // it's not, make it init self.bake_a_child(ind); } // SAFETY: - // we keep track of unbaked children separately, and we've just checked above that this exact token is, in fact, initialized + // we keep track of unbaked children separately, and we've just checked above that this exact child is, in fact, initialized let reference = unsafe { self.baked_children[ind].assume_init_ref() }; // SAFETY: // This struct has a phantom field indicating that it lives exactly for `'owner`. @@ -625,10 +649,8 @@ mod borrowing_concept { unsafe { &*(reference as *const _) } } - fn get_children<'r>( - &'r mut self, - ChildrenPromise(range): Self::MCP, - ) -> &'owner [Thing<'owner>] { + fn get_children<'r>(&'r mut self, ChildrenPromise(range): Self::MCP) -> Self::MCA { + todo!(); // first, check if all children in slice are init for ind in range.clone() { if !self.baked_children_mask[ind] { @@ -638,7 +660,7 @@ mod borrowing_concept { } // SAFETY: // we keep track of unbaked children separately, and we've check that all of the tokens in slice to, in fact, exist - let slice: &'r [Thing<'owner>] = + let slice: &'r [OwnedThing] = unsafe { std::mem::transmute(&self.baked_children[range]) }; // SAFETY: // This struct has a phantom field indicating that it lives exactly for `'owner`. @@ -656,42 +678,27 @@ mod borrowing_concept { } = self; assert!( baked_children_mask.into_iter().all(|b| *b), - "Not all children were initialized, so we cannot create output struct" + "Not all children were initialized, so we cannot create output struct: {:?}", + baked_children_mask ); // SAFETY: // we keep track of unbaked children separately, and should've panicked above, if any of them were not properly init. - let children: Immutable]>>> = + let children: Immutable>> = unsafe { std::mem::transmute(baked_children) }; FixedOwner { _text: text, - children, - _phantom: PhantomData, + _children: baked_children, } } } - struct FixedOwner<'owner> { - _text: Immutable>>, - children: Immutable]>>>, - _phantom: PhantomData &'owner ()>, + #[derive(Debug)] + struct FixedOwner { + _text: Rc, + _children: Rc<[OwnedThing]>, } - impl<'owner> FixedProvider<'owner> for FixedOwner<'owner> { - type SP = FixedOwner<'static>; - - fn to_static(&self) -> Self::SP { - todo!() - } - - fn inner(&self) -> &'owner Thing<'owner> { - // SAFETY: - // This struct has a phantom field indicating that it lives exactly for `'owner`. - // Along with rest of the fields being **pinned** **and immutable**, this means that returned - // immutable reference will indeed live for `'owner`. - let inner: &'owner Thing<'owner> = unsafe { &*(&self.children[0] as *const _) }; - inner - } - } + impl<'owner> FixedProvider for FixedOwner {} #[derive(Debug, Clone)] enum UnbakedThing { @@ -703,7 +710,7 @@ mod borrowing_concept { impl<'owner, TP, CP, MCP> UnbakedThing { fn bake( self, - provider: &mut impl BakedProvider<'owner, TP = TP, CP = CP, MCP = MCP>, + provider: &mut impl BakedProvider, ) -> Thing<'owner> { match self { UnbakedThing::Child { @@ -724,35 +731,48 @@ mod borrowing_concept { } } } + + trait Thing { + type TA: Deref; + type CA: Deref; + type MCA: Deref; + } + #[derive(Debug)] - enum Thing<'source> { + enum OwnedThing { Text { - text: &'source str, + text: SharedText, }, Child { - text: &'source str, - child1: &'source Thing<'source>, - child2: &'source Thing<'source>, + text: SharedText, + child1: SharedThing, + child2: SharedThing, }, Multiple { - children: &'source [Thing<'source>], + children: SharedThings, }, } + impl Thing for OwnedThing { + type TA = SharedText; + type CA = SharedThing; + type MCA = SharedThings; + } + #[cfg(test)] mod tests { use crate::data::borrowing_concept::{BakedProvider, UnbakedProvider, UnbakedThing}; #[test] // #[cfg(miri)] - fn miri_fuzz() { + fn unsafe_fuzz() { use crate::data::borrowing_concept::{ ChildPromise, ChildrenPromise, TextPromise, Thing, UnbakedOwner, }; // arrange fn gen_thing<'owner, R: rand::Rng + ?Sized>( - provider: &mut UnbakedOwner<'owner>, + provider: &mut UnbakedOwner, rng: &mut R, ) -> ( UnbakedThing, @@ -819,17 +839,22 @@ mod borrowing_concept { // meaning this thing is likely to die at some point (I hope for that, at least) generated } - let mut provider = UnbakedOwner::new(); + let rng = &mut rand::thread_rng(); + for _ in 0..10_000 { + let mut provider = UnbakedOwner::new(); - // act - let (promise, thing) = gen_thing(&mut provider, &mut rand::thread_rng()); - let mut provider = provider.bake(); - let baked_thing = promise.bake(&mut provider); + // act + let (promise, thing) = gen_thing(&mut provider, rng); + let mut provider = provider.bake(); + let baked_thing = promise.bake(&mut provider); - // assert - let thing_str = format!("{thing:?}"); - let baked_str = format!("{baked_thing:?}"); - assert_eq!(thing_str, baked_str, "Should produce identical strs"); + // assert + let _provider = provider.fix(); // all children were init + let thing_str = format!("{thing:?}"); + let baked_str = format!("{baked_thing:?}"); + assert_eq!(thing_str, baked_str, "Should produce identical strs"); + // println!("{provider:#?}"); + } } } } From d7e2317d2d826f5d8dfe190d626e4c4444ea6171 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Sat, 23 Mar 2024 11:12:52 +0200 Subject: [PATCH 29/98] Reworked shared data system Now uses no `unsafe`, as I fear it so much :/ --- src/data.rs | 448 +++++++++++++++++----------------------------------- 1 file changed, 142 insertions(+), 306 deletions(-) diff --git a/src/data.rs b/src/data.rs index 901cf2b..562840c 100644 --- a/src/data.rs +++ b/src/data.rs @@ -464,7 +464,8 @@ impl<'source> Token<'source> { /// - `FixedProvider` represents a provider that's no longer able nor add nor provide values. /// It can be as minified as possible, only storing data without any sort of bookkeeping. /// -/// +/// Despite me being a masochist, I really do fear using `unsafe` in my code. There's just too many things to keep track of. +/// So after writing the entire module using self-borrowing stuff and `unsafe`, I'll resort to second option stated above: basically, ever-unbaked children. #[cfg(test)] mod borrowing_concept { use std::mem::MaybeUninit; @@ -474,386 +475,221 @@ mod borrowing_concept { use crate::util::Immutable; + struct TextPromise(Range); + struct ChildPromise(usize); + struct ChildrenPromise(Range); + + enum Thing { + Text { + text: TextPromise, + }, + Child { + text: TextPromise, + child1: ChildPromise, + child2: ChildPromise, + }, + Multiple { + children: ChildrenPromise, + }, + } + + impl Thing { + fn write_to(&self, provider: &P, out: &mut W) -> std::fmt::Result + where + P: BakedProvider, + W: std::fmt::Write + ?Sized, + { + match self { + Thing::Text { text } => write!(out, "Text({})", provider.get_text(text)), + Thing::Child { + text, + child1, + child2, + } => { + write!(out, "Child({}, ", provider.get_text(text))?; + provider.get_child(child1).write_to(provider, out)?; + out.write_str(", ")?; + provider.get_child(child2).write_to(provider, out)?; + out.write_str(")")?; + Ok(()) + } + Thing::Multiple { children } => { + out.write_str("Multiple[")?; + let children = provider.get_children(children); + if let Some(first) = children.first() { + first.write_to(provider, out)?; + for child in &children[1..] { + out.write_str(", ")?; + child.write_to(provider, out)?; + } + } + out.write_str("]")?; + Ok(()) + } + } + } + } + trait UnbakedProvider { - type TP; - type CP; - type MCP; - type Baked: BakedProvider; - fn register_text<'r>(&'r mut self, text: &str) -> Self::TP; - fn register_child<'r>( - &'r mut self, - child: UnbakedThing, - ) -> Self::CP; - fn register_children<'r>( - &'r mut self, - children: impl IntoIterator>, - ) -> Self::MCP; + type Baked: BakedProvider; + fn register_text(&mut self, text: &str) -> TextPromise; + fn register_child(&mut self, child: Thing) -> ChildPromise; + fn register_children<'s>( + &'s mut self, + children: impl FnMut(&mut &'s mut Self) -> Option, + ) -> ChildrenPromise; fn bake(self) -> Self::Baked; } trait BakedProvider { - type TP; - type CP; - type MCP; - type TA; - type CA; - type MCA; - type Fixed: FixedProvider; - fn get_text(&mut self, promise: Self::TP) -> Self::TA; - fn get_child(&mut self, promise: Self::CP) -> Self::CA; - fn get_children(&mut self, promise: Self::MCP) -> Self::MCA; - fn fix(self) -> Self::Fixed; + fn get_text<'s>(&'s self, promise: &TextPromise) -> &'s str; + fn get_child<'s>(&'s self, promise: &ChildPromise) -> &'s Thing; + fn get_children<'s>(&'s self, promise: &ChildrenPromise) -> &'s [Thing]; } - trait FixedProvider {} - - #[derive(Debug, Clone)] - struct TextPromise(Range); - #[derive(Debug, Clone)] - struct ChildPromise(usize); - #[derive(Debug, Clone)] - struct ChildrenPromise(Range); - - struct SharedText(Rc, Range); - impl Deref for SharedText { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0[self.1.clone()] - } - } - - struct SharedThing(Rc<[T]>, usize); - impl Deref for SharedThing { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.0[self.1] - } - } - - struct SharedThings(Rc<[T]>, Range); - impl Deref for SharedThings { - type Target = [T]; - - fn deref(&self) -> &Self::Target { - &self.0[self.1.clone()] - } - } - - struct UnbakedOwner { + struct UnbakedOwningProvider { text: String, - children: Vec>, + things: Vec, } - impl UnbakedOwner { + impl UnbakedOwningProvider { fn new() -> Self { Self { text: String::new(), - children: Vec::new(), + things: Vec::new(), } } } - impl UnbakedProvider for UnbakedOwner { - type TP = TextPromise; - type CP = ChildPromise; - type MCP = ChildrenPromise; - type Baked = BakedOwner; + impl UnbakedProvider for UnbakedOwningProvider { + type Baked = BakedOwningProvider; - fn register_text<'r>(&'r mut self, text: &str) -> Self::TP { - let slice_start = self.text.len(); + fn register_text(&mut self, text: &str) -> TextPromise { + let range_start = self.text.len(); self.text.push_str(text); - let slice_end = self.text.len(); - TextPromise(slice_start..slice_end) + let range_end = self.text.len(); + TextPromise(range_start..range_end) } - fn register_child<'r>( - &'r mut self, - child: UnbakedThing, - ) -> Self::CP { - let ind = self.children.len(); - self.children.push(child); + fn register_child(&mut self, child: Thing) -> ChildPromise { + let ind = self.things.len(); + self.things.push(child); ChildPromise(ind) } - fn register_children<'r>( - &'r mut self, - children: impl IntoIterator>, - ) -> Self::MCP { - let slice_start = self.children.len(); - self.children.extend(children); - let slice_end = self.children.len(); - ChildrenPromise(slice_start..slice_end) + fn register_children<'s>( + mut self: &'s mut Self, + mut children: impl FnMut(&mut &'s mut Self) -> Option, + ) -> ChildrenPromise { + let range_start = self.things.len(); + while let Some(thing) = children(&mut self) { + self.things.push(thing); + } + let range_end = self.things.len(); + ChildrenPromise(range_start..range_end) } fn bake(self) -> Self::Baked { - let children_num = self.children.len(); - let mut baked_children = Vec::with_capacity(children_num); - for _ in 0..children_num { - baked_children.push(MaybeUninit::uninit()); - } - BakedOwner { - text: Immutable::from(Pin::new(self.text.into_boxed_str())), - unbaked_children: self.children.into_boxed_slice(), - baked_children: Pin::new(baked_children.into_boxed_slice()), - baked_children_mask: vec![false; children_num].into_boxed_slice(), + BakedOwningProvider { + text: self.text.into_boxed_str(), + things: self.things.into_boxed_slice(), } } } - #[derive(Debug)] - struct BakedOwner { - text: Immutable>>, - unbaked_children: Box<[UnbakedThing]>, - baked_children: Pin]>>, - baked_children_mask: Box<[bool]>, + struct BakedOwningProvider { + text: Box, + things: Box<[Thing]>, } - impl BakedOwner { - fn bake_a_child(&mut self, child_ind: usize) { - let unbaked = self.unbaked_children[child_ind].clone(); - let baked = unbaked.bake(self); - self.baked_children[child_ind].write(baked); - self.baked_children_mask[child_ind] = true; - } - } - - impl BakedProvider for BakedOwner { - type TP = TextPromise; - type CP = ChildPromise; - type MCP = ChildrenPromise; - type TA = SharedText; - type CA = SharedThing; - type MCA = SharedThings; - type Fixed = FixedOwner; - - fn get_text(&mut self, TextPromise(range): Self::TP) -> Self::TA { - SharedText(Rc::clone(&self.text), range) + impl BakedProvider for BakedOwningProvider { + fn get_text<'s>(&'s self, TextPromise(range): &TextPromise) -> &'s str { + &self.text[range.clone()] } - fn get_child<'r>(&'r mut self, ChildPromise(ind): Self::CP) -> Self::CA { - todo!(); - // first, check if child is init - if !self.baked_children_mask[ind] { - // it's not, make it init - self.bake_a_child(ind); - } - // SAFETY: - // we keep track of unbaked children separately, and we've just checked above that this exact child is, in fact, initialized - let reference = unsafe { self.baked_children[ind].assume_init_ref() }; - // SAFETY: - // This struct has a phantom field indicating that it lives exactly for `'owner`. - // Along with rest of the fields being **pinned** **and immutable**, this means that returned - // immutable reference will indeed live for `'owner`. - unsafe { &*(reference as *const _) } + fn get_child<'s>(&'s self, &ChildPromise(ind): &ChildPromise) -> &'s Thing { + &self.things[ind] } - fn get_children<'r>(&'r mut self, ChildrenPromise(range): Self::MCP) -> Self::MCA { - todo!(); - // first, check if all children in slice are init - for ind in range.clone() { - if !self.baked_children_mask[ind] { - // this child is not init, so init it - self.bake_a_child(ind); - } - } - // SAFETY: - // we keep track of unbaked children separately, and we've check that all of the tokens in slice to, in fact, exist - let slice: &'r [OwnedThing] = - unsafe { std::mem::transmute(&self.baked_children[range]) }; - // SAFETY: - // This struct has a phantom field indicating that it lives exactly for `'owner`. - // Along with rest of the fields being **pinned** **and immutable**, this means that returned - // immutable reference will indeed live for `'owner`. - unsafe { &*(slice as *const _) } + fn get_children<'s>(&'s self, ChildrenPromise(range): &ChildrenPromise) -> &'s [Thing] { + &self.things[range.clone()] } - - fn fix(self) -> Self::Fixed { - let Self { - text, - baked_children_mask, - baked_children, - .. - } = self; - assert!( - baked_children_mask.into_iter().all(|b| *b), - "Not all children were initialized, so we cannot create output struct: {:?}", - baked_children_mask - ); - // SAFETY: - // we keep track of unbaked children separately, and should've panicked above, if any of them were not properly init. - let children: Immutable>> = - unsafe { std::mem::transmute(baked_children) }; - FixedOwner { - _text: text, - _children: baked_children, - } - } - } - - #[derive(Debug)] - struct FixedOwner { - _text: Rc, - _children: Rc<[OwnedThing]>, - } - - impl<'owner> FixedProvider for FixedOwner {} - - #[derive(Debug, Clone)] - enum UnbakedThing { - Text { text: TP }, - Child { text: TP, child1: CP, child2: CP }, - Multiple { children: MCP }, - } - - impl<'owner, TP, CP, MCP> UnbakedThing { - fn bake( - self, - provider: &mut impl BakedProvider, - ) -> Thing<'owner> { - match self { - UnbakedThing::Child { - text, - child1, - child2, - } => Thing::Child { - text: provider.get_text(text), - child1: provider.get_child(child1), - child2: provider.get_child(child2), - }, - UnbakedThing::Multiple { children } => Thing::Multiple { - children: provider.get_children(children), - }, - UnbakedThing::Text { text } => Thing::Text { - text: provider.get_text(text), - }, - } - } - } - - trait Thing { - type TA: Deref; - type CA: Deref; - type MCA: Deref; - } - - #[derive(Debug)] - enum OwnedThing { - Text { - text: SharedText, - }, - Child { - text: SharedText, - child1: SharedThing, - child2: SharedThing, - }, - Multiple { - children: SharedThings, - }, - } - - impl Thing for OwnedThing { - type TA = SharedText; - type CA = SharedThing; - type MCA = SharedThings; } #[cfg(test)] mod tests { - use crate::data::borrowing_concept::{BakedProvider, UnbakedProvider, UnbakedThing}; + use crate::data::borrowing_concept::{UnbakedOwningProvider, UnbakedProvider}; #[test] - // #[cfg(miri)] - fn unsafe_fuzz() { - use crate::data::borrowing_concept::{ - ChildPromise, ChildrenPromise, TextPromise, Thing, UnbakedOwner, - }; + fn fuzz() { + use crate::data::borrowing_concept::Thing; // arrange fn gen_thing<'owner, R: rand::Rng + ?Sized>( - provider: &mut UnbakedOwner, + provider: &mut UnbakedOwningProvider, rng: &mut R, - ) -> ( - UnbakedThing, - Thing<'static>, - ) { - fn gen_text(rng: &mut R) -> &'static str { + ) -> Thing { + fn gen_text(rng: &mut R) -> String { // let it be of some length let l: usize = rng.gen_range(3..=10); let mut s = String::new(); for _ in 0..l { - s.push(rng.gen()) + s.push(if rng.gen_bool(0.5) { + rng.gen_range('a'..='z') + } else if rng.gen_bool(0.4) { + rng.gen_range('а'..='я') + } else { + [' ', ',', '.'][rng.gen_range(0..3)] + }) } - Box::leak(s.into_boxed_str()) + s } let generated = if rng.gen_bool(0.6) { // assume 60% are text variants // we need some random text for it let text = gen_text(rng); - ( - UnbakedThing::Text { - text: provider.register_text(text), - }, - Thing::Text { text }, - ) + Thing::Text { + text: provider.register_text(&text), + } } else if rng.gen_bool(0.9) { // say, 36% are child owners // we need some text and children for it let text = gen_text(rng); - let (c1_pr, ch1) = gen_thing(provider, rng); - let (c2_pr, ch2) = gen_thing(provider, rng); - ( - UnbakedThing::Child { - text: provider.register_text(text), - child1: provider.register_child(c1_pr), - child2: provider.register_child(c2_pr), - }, - Thing::Child { - text, - child1: Box::leak(Box::new(ch1)), - child2: Box::leak(Box::new(ch2)), - }, - ) + let child1 = gen_thing(provider, rng); + let child2 = gen_thing(provider, rng); + Thing::Child { + text: provider.register_text(&text), + child1: provider.register_child(child1), + child2: provider.register_child(child2), + } } else { // rest 4% own 1-5 children - let num_of_children = rng.gen_range(1..=5); - let mut children = Vec::with_capacity(num_of_children); - let mut children_pr = Vec::with_capacity(num_of_children); - for _ in 0..num_of_children { - let (ch_pr, child) = gen_thing(provider, rng); - children.push(child); - children_pr.push(ch_pr.clone()); + let mut inner = (0..rng.gen_range(1..=5)).into_iter(); + let generator = move |provider: &mut &mut UnbakedOwningProvider| { + inner.next().map(|_| gen_thing(*provider, rng)) + }; + Thing::Multiple { + children: provider.register_children(generator), } - let children_pr = provider.register_children(children_pr); - ( - UnbakedThing::Multiple { - children: children_pr, - }, - Thing::Multiple { - children: &*Box::leak(children.into_boxed_slice()), - }, - ) }; // average coefficient is 0.6 * 0 + 0.36 * 2 + 2.5 * 0.04 = 0.82 // meaning this thing is likely to die at some point (I hope for that, at least) generated } let rng = &mut rand::thread_rng(); - for _ in 0..10_000 { - let mut provider = UnbakedOwner::new(); + for _ in 0..10 { + let mut provider = UnbakedOwningProvider::new(); // act - let (promise, thing) = gen_thing(&mut provider, rng); - let mut provider = provider.bake(); - let baked_thing = promise.bake(&mut provider); + let thing = gen_thing(&mut provider, rng); + let provider = provider.bake(); // assert - let _provider = provider.fix(); // all children were init - let thing_str = format!("{thing:?}"); - let baked_str = format!("{baked_thing:?}"); - assert_eq!(thing_str, baked_str, "Should produce identical strs"); - // println!("{provider:#?}"); + let mut output = String::new(); + thing + .write_to(&provider, &mut output) + .expect("Should be able to write into string"); + println!("{output}"); } } } From 3d28bd1eba1607ea4663f843d0a485a2c1207409 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Sun, 24 Mar 2024 11:32:25 +0200 Subject: [PATCH 30/98] daily commit - exact changes would be explained later, I'm in a hurry rn --- src/data.rs | 529 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 480 insertions(+), 49 deletions(-) diff --git a/src/data.rs b/src/data.rs index 562840c..7e75aed 100644 --- a/src/data.rs +++ b/src/data.rs @@ -438,7 +438,7 @@ impl<'source> Token<'source> { /// - same as before, but children never get to see pointers themselves. this will allow `Owner` to freely mutate data, but will add an extra cost for some sort of /// internal identifier resolution and a couple more function calls. /// -/// Being a masochist I am, of course I went on with the first option. Following is a log of the development +/// ~~Being a masochist I am, of course I went on with the first option. Following is a log of the development~~ /// /// # LOG /// @@ -466,37 +466,47 @@ impl<'source> Token<'source> { /// /// Despite me being a masochist, I really do fear using `unsafe` in my code. There's just too many things to keep track of. /// So after writing the entire module using self-borrowing stuff and `unsafe`, I'll resort to second option stated above: basically, ever-unbaked children. +/// +/// A format of so-called "promise" that provider uses to return actual child/text can be different and it ultimately of to provider to decide: +/// - the simplest would be a provider that owns all of the data tokens refer to +/// - a provider could be relaying calls to some other provider to take care of +/// - a provider could refer to some plain text buffer, like &str to recognize and store it's data +/// +/// All of that means, that both tokens and providers must be defined at generic over promise types. +/// This will allow provider to select in which way they would store and manipulate data. +/// +/// This approach is less optimized then the `unsafe` one, but `miri` scared me away from pointer manipulation, so here I am, I guess +/// +/// I see now, that providers can be composed into each other to build desired behavior step-by-step. That's really desireable too. #[cfg(test)] mod borrowing_concept { - use std::mem::MaybeUninit; - use std::ops::{Deref, Range}; - use std::pin::Pin; - use std::rc::Rc; - - use crate::util::Immutable; + use std::{marker::PhantomData, ops::Range}; + #[derive(Debug, derive_more::From, derive_more::AsRef)] struct TextPromise(Range); + #[derive(Debug, derive_more::From, derive_more::AsRef)] struct ChildPromise(usize); + #[derive(Debug, derive_more::From, derive_more::AsRef)] struct ChildrenPromise(Range); - enum Thing { + enum Thing { Text { - text: TextPromise, + text: Text, }, Child { - text: TextPromise, - child1: ChildPromise, - child2: ChildPromise, + text: Text, + child1: Child, + child2: Child, }, Multiple { - children: ChildrenPromise, + children: Children, }, } - impl Thing { + impl Thing { fn write_to(&self, provider: &P, out: &mut W) -> std::fmt::Result where - P: BakedProvider, + P: BakedProvider, W: std::fmt::Write + ?Sized, { match self { @@ -530,26 +540,333 @@ mod borrowing_concept { } } - trait UnbakedProvider { - type Baked: BakedProvider; - fn register_text(&mut self, text: &str) -> TextPromise; - fn register_child(&mut self, child: Thing) -> ChildPromise; + trait UnbakedProvider { + type Baked: BakedProvider; + fn register_text(&mut self, text: &str) -> Text; + fn register_child(&mut self, child: Thing) -> Child; fn register_children<'s>( &'s mut self, - children: impl FnMut(&mut &'s mut Self) -> Option, - ) -> ChildrenPromise; + children: impl FnMut(&mut &'s mut Self) -> Option>, + ) -> Children; fn bake(self) -> Self::Baked; } - trait BakedProvider { - fn get_text<'s>(&'s self, promise: &TextPromise) -> &'s str; - fn get_child<'s>(&'s self, promise: &ChildPromise) -> &'s Thing; - fn get_children<'s>(&'s self, promise: &ChildrenPromise) -> &'s [Thing]; + trait BakedProvider { + fn get_text<'s>(&'s self, promise: &Text) -> &'s str; + fn get_child<'s>(&'s self, promise: &Child) -> &'s Thing; + fn get_children<'s>(&'s self, promise: &Children) -> &'s [Thing]; } - struct UnbakedOwningProvider { + trait UnbakedProviderExt: UnbakedProvider { + fn with_text_ref<'text>( + self, + text: &'text str, + ) -> UnbakedTextRefProvider<'text, Text, Child, Children, Self> { + UnbakedTextRefProvider { text, inner: self } + } + } + impl> + UnbakedProviderExt for P + { + } + + enum NeverText {} + enum NeverChild {} + enum NeverChildren {} + + /// Provider that holds nothing and panics at any call to it. + struct UnbakedBottomProvider; + impl UnbakedProvider + for UnbakedBottomProvider + { + type Baked = BakedBottomProvider; + fn register_text(&mut self, text: &str) -> Text { + unimplemented!("This provider does not support text saving") + } + + fn register_child(&mut self, child: Thing) -> Child { + unimplemented!("This provider does not support child saving") + } + + fn register_children<'s>( + &'s mut self, + children: impl FnMut(&mut &'s mut Self) -> Option>, + ) -> Children { + unimplemented!("This provider does not support children saving") + } + + fn bake(self) -> Self::Baked { + BakedBottomProvider + } + } + struct BakedBottomProvider; + impl BakedProvider for BakedBottomProvider { + fn get_text<'s>(&'s self, promise: &Text) -> &'s str { + unimplemented!("This provider does not provide text") + } + + fn get_child<'s>(&'s self, promise: &Child) -> &'s Thing { + unimplemented!("This provider does not provide children") + } + + fn get_children<'s>(&'s self, promise: &Children) -> &'s [Thing] { + unimplemented!("This provider does not provide children") + } + } + + trait MaybeTextRef<'text>: From<&'text str> { + fn text_ref(&self) -> Option<&'text str>; + } + + struct UnbakedTextRefProvider<'text, Text, Child, Children, P> { + text: &'text str, + inner: P, + } + + impl<'text, Text, Child, Children, P> UnbakedProvider + for UnbakedTextRefProvider<'text, Text, Child, Children, P> + where + P: UnbakedProvider, + { + type Baked = BakedTextRefProvider<'text, Text, Child, Children, P>; + + fn register_text(&mut self, text: &str) -> Text { + // first, check out if this text, as a pointer, points inside this provider's referee: + 'nah: { + let referee_ptr = self.text.as_ptr() as usize; + let text_ptr = text.as_ptr() as usize; + if text_ptr < referee_ptr { + break 'nah; + } + let index_guess = text_ptr - referee_ptr; + let self_len = self.text.len(); + if index_guess >= self_len { + break 'nah; + } + // yeah, seems like it + // lastly, lets rule out partial overlap: + let text_len = text.len(); + if text_len + index_guess > self_len { + // buffers overlap partially, which is weird, probably should + // TODO warn about that too + break 'nah; + } + // let's check that literally, for a good measure: + // TODO probably add cfg to remove that in release build? + if !self.text[index_guess..].starts_with(text) { + // huh? that's something weird, probably must + // TODO warn about it + break 'nah; + } + // ok, so it seems like we've found a match using nothing but pointer-things. + // that's great, cause it's literally O(1) for any buffer size + return &self.text[index_guess..index_guess + text_len].into(); + } + + // then, let's try finding this sort of sequence in the entire referee: + if let Some(ind) = self.text.find(text) { + // well, return corresponding slice, then + return TextRefPromise::Ref(&self.text[ind..ind + text.len()]); + } + + // lastly, refer to inner provider + self.inner.register_text(text) + } + + fn register_child(&mut self, child: Thing) -> Child { + self.inner.register_child(child) + } + + fn register_children<'s>( + &'s mut self, + children: impl FnMut(&mut &'s mut Self) -> Option>, + ) -> Children { + self.inner.register_children(children) + } + + fn bake(self) -> Self::Baked { + BakedTextRefProvider { inner: self.inner } + } + } + + struct BakedTextRefProvider<'text, Text, Child, Children, P> { + inner: P, + } + + impl<'text, Text, Child, Children, P> BakedProvider + for BakedTextRefProvider<'text, Text, Child, Children, P> + where + P: BakedProvider, + Text: MaybeTextRef<'text>, + { + fn get_text<'s>(&'s self, promise: &Text) -> &'s str { + if let Some(res) = promise.text_ref() { + res + } else { + self.inner.get_text(promise) + } + } + + fn get_child<'s>(&'s self, promise: &Child) -> &'s Thing { + self.inner.get_child(promise) + } + + fn get_children<'s>(&'s self, promise: &Children) -> &'s [Thing] { + self.inner.get_children(promise) + } + } + + trait MaybeTextOwnRange: From> { + fn own_range(&self) -> &Range; + } + + struct UnbakedTextOwnProvider { text: String, - things: Vec, + inner: P, + } + + impl UnbakedProvider + for UnbakedTextOwnProvider + where + P: UnbakedProvider, + { + type Baked = BakedTextOwnProvider; + + fn register_text(&mut self, text: &str) -> Text { + let range_start = self.text.len(); + self.text.push_str(text); + let range_end = self.text.len(); + Text::from(range_start..range_end) + } + + fn register_child(&mut self, child: Thing) -> Child { + self.inner.register_child(child) + } + + fn register_children<'s>( + &'s mut self, + children: impl FnMut(&mut &'s mut Self) -> Option>, + ) -> Children { + self.register_children(children) + } + + fn bake(self) -> Self::Baked { + BakedTextOwnProvider { + text: self.text.into_boxed_str(), + inner: self.inner.bake(), + } + } + } + + struct BakedTextOwnProvider { + text: Box, + inner: P, + } + + impl BakedProvider + for BakedTextOwnProvider + where + P: BakedProvider, + Text: MaybeTextOwnRange, + { + fn get_text<'s>(&'s self, promise: &Text) -> &'s str { + if let Some(range) = promise.own_range() { + &self.text[range.clone()] + } else { + self.inner.get_text(promise) + } + } + + fn get_child<'s>(&'s self, promise: &Child) -> &'s Thing { + self.inner.get_child(promise) + } + + fn get_children<'s>(&'s self, promise: &Children) -> &'s [Thing] { + self.inner.get_children(promise) + } + } + + trait MaybeChildInd: From { + fn own_ind(&self) -> usize; + } + + trait MaybeChildrenRange: From> { + fn own_range(&self) -> &Range; + } + + struct UnbakedChildrenOwnProvider { + children: Vec>, + inner: P, + } + + impl UnbakedProvider + for UnbakedChildrenOwnProvider + where + P: UnbakedProvider, + { + type Baked = BakedChildrenOwnProvider; + + fn register_text(&mut self, text: &str) -> Text { + self.inner.register_text(text) + } + + fn register_child(&mut self, child: Thing) -> Child { + let ind = self.things.len(); + self.things.push(child); + ChildPromise(ind) + } + + fn register_children<'s>( + &'s mut self, + children: impl FnMut(&mut &'s mut Self) -> Option>, + ) -> Children { + todo!() + } + + fn bake(self) -> Self::Baked { + BakedChildrenOwnProvider { + children: self.children.into_boxed_slice(), + inner: self.inner.bake(), + } + } + } + + struct BakedChildrenOwnProvider { + children: Box<[Thing]>, + inner: P, + } + + impl BakedProvider + for BakedChildrenOwnProvider + where + P: BakedProvider, + Child: MaybeChildInd, + Children: MaybeChildrenRange, + { + fn get_text<'s>(&'s self, promise: &Text) -> &'s str { + self.inner.get_text(promise) + } + + fn get_child<'s>(&'s self, promise: &Child) -> &'s Thing { + if let Some(ind) = promise.own_ind() { + &self.children[ind] + } else { + self.inner.get_child(promise) + } + } + + fn get_children<'s>(&'s self, promise: &Children) -> &'s [Thing] { + if let Some(range) = promise.own_range() { + &self.children[range.clone()] + } else { + self.inner.get_children(promise) + } + } + } + + struct UnbakedOwningProvider { + text: String, + things: Vec, } impl UnbakedOwningProvider { @@ -562,6 +879,9 @@ mod borrowing_concept { } impl UnbakedProvider for UnbakedOwningProvider { + type Text = TextPromise; + type Child = ChildPromise; + type Children = ChildrenPromise; type Baked = BakedOwningProvider; fn register_text(&mut self, text: &str) -> TextPromise { @@ -571,7 +891,7 @@ mod borrowing_concept { TextPromise(range_start..range_end) } - fn register_child(&mut self, child: Thing) -> ChildPromise { + fn register_child(&mut self, child: OwnedThing) -> ChildPromise { let ind = self.things.len(); self.things.push(child); ChildPromise(ind) @@ -579,7 +899,7 @@ mod borrowing_concept { fn register_children<'s>( mut self: &'s mut Self, - mut children: impl FnMut(&mut &'s mut Self) -> Option, + mut children: impl FnMut(&mut &'s mut Self) -> Option, ) -> ChildrenPromise { let range_start = self.things.len(); while let Some(thing) = children(&mut self) { @@ -597,38 +917,150 @@ mod borrowing_concept { } } - struct BakedOwningProvider { - text: Box, - things: Box<[Thing]>, + enum TextRefPromise<'text> { + Ref(&'text str), + Owned(Range), } - impl BakedProvider for BakedOwningProvider { - fn get_text<'s>(&'s self, TextPromise(range): &TextPromise) -> &'s str { - &self.text[range.clone()] + type RefThing<'text> = Thing, ChildPromise, ChildrenPromise>; + + struct UnbakedTextRefProvider<'text, P> { + text: &'text str, + inner: P, + } + + impl<'text, P> UnbakedProvider for UnbakedTextRefProvider<'text, P> + where + P: UnbakedProvider, + { + type Text = TextRefPromise<'text>; + type Child = ChildPromise; + type Children = ChildrenPromise; + type Baked = BakedTextRefProvider<'text, P::Baked>; + + fn register_text(&mut self, text: &str) -> Self::Text { + // first, check out if this text, as a pointer, points inside this provider's referee: + 'nah: { + let referee_ptr = self.text.as_ptr() as usize; + let text_ptr = text.as_ptr() as usize; + if text_ptr < referee_ptr { + break 'nah; + } + let index_guess = text_ptr - referee_ptr; + let self_len = self.text.len(); + if index_guess >= self_len { + break 'nah; + } + // yeah, seems like it + // lastly, lets rule out partial overlap: + let text_len = text.len(); + if text_len + index_guess > self_len { + // buffers overlap partially, which is weird, probably should + // TODO warn about that too + break 'nah; + } + // let's check that literally, for a good measure: + // TODO probably add cfg to remove that in release build? + if !self.text[index_guess..].starts_with(text) { + // huh? that's something weird, probably must + // TODO warn about it + break 'nah; + } + // ok, so it seems like we've found a match using nothing but pointer-things. + // that's great, cause it's literally O(1) for any buffer size + return TextRefPromise::Ref(&self.text[index_guess..index_guess + text_len]); + } + + // then, let's try finding this sort of sequence in the entire referee: + if let Some(ind) = self.text.find(text) { + // well, return corresponding slice, then + return TextRefPromise::Ref(&self.text[ind..ind + text.len()]); + } + + // lastly, refer to inner provider + TextRefPromise::Owned(self.inner.register_text(text)) } - fn get_child<'s>(&'s self, &ChildPromise(ind): &ChildPromise) -> &'s Thing { - &self.things[ind] + fn register_child( + &mut self, + child: Thing, + ) -> Self::Child { + self.inner.register_child(child) } - fn get_children<'s>(&'s self, ChildrenPromise(range): &ChildrenPromise) -> &'s [Thing] { + fn register_children<'s>( + &'s mut self, + children: impl FnMut( + &mut &'s mut Self, + ) -> Option>, + ) -> Self::Children { + self.inner.register_children(children) + } + + fn bake(self) -> Self::Baked { + BakedTextRefProvider { + inner: self.inner.bake(), + owned_text: todo!(), + _phantom: PhantomData, + } + } + } + + struct BakedTextRefProvider<'text, P> { + owned_text: Box, + inner: P, + _phantom: PhantomData<&'text ()>, + } + + impl<'text, P> BakedProvider for BakedTextRefProvider<'text, P> + where + P: BakedProvider< + Text = TextRefPromise<'text>, + Child = ChildPromise, + Children = ChildrenPromise, + >, + { + type Text = TextRefPromise<'text>; + type Child = ChildPromise; + type Children = ChildrenPromise; + + fn get_text<'s>(&'s self, promise: &Self::Text) -> &'s str { + match promise { + TextRefPromise::Ref(reference) => reference, + TextRefPromise::Owned(owning) => &self.owned_text[owning.clone()], + } + } + + fn get_child<'s>( + &'s self, + ChildPromise(ind): &Self::Child, + ) -> &'s Thing { + &self.things[*ind] + } + + fn get_children<'s>( + &'s self, + ChildrenPromise(range): &Self::Children, + ) -> &'s [Thing] { &self.things[range.clone()] } } #[cfg(test)] mod tests { - use crate::data::borrowing_concept::{UnbakedOwningProvider, UnbakedProvider}; + use crate::data::borrowing_concept::{OwnedThing, UnbakedOwningProvider, UnbakedProvider}; #[test] - fn fuzz() { + fn owned_fuzz() { use crate::data::borrowing_concept::Thing; // arrange fn gen_thing<'owner, R: rand::Rng + ?Sized>( provider: &mut UnbakedOwningProvider, rng: &mut R, - ) -> Thing { + nodes_left: &mut usize, + ) -> OwnedThing { + *nodes_left = nodes_left.saturating_sub(1); fn gen_text(rng: &mut R) -> String { // let it be of some length let l: usize = rng.gen_range(3..=10); @@ -644,8 +1076,8 @@ mod borrowing_concept { } s } - let generated = if rng.gen_bool(0.6) { - // assume 60% are text variants + let generated = if *nodes_left == 0 || rng.gen_bool(0.6) { + // assume 60% or anything over specified number are text variants // we need some random text for it let text = gen_text(rng); Thing::Text { @@ -655,8 +1087,8 @@ mod borrowing_concept { // say, 36% are child owners // we need some text and children for it let text = gen_text(rng); - let child1 = gen_thing(provider, rng); - let child2 = gen_thing(provider, rng); + let child1 = gen_thing(provider, rng, nodes_left); + let child2 = gen_thing(provider, rng, nodes_left); Thing::Child { text: provider.register_text(&text), child1: provider.register_child(child1), @@ -666,7 +1098,7 @@ mod borrowing_concept { // rest 4% own 1-5 children let mut inner = (0..rng.gen_range(1..=5)).into_iter(); let generator = move |provider: &mut &mut UnbakedOwningProvider| { - inner.next().map(|_| gen_thing(*provider, rng)) + inner.next().map(|_| gen_thing(*provider, rng, nodes_left)) }; Thing::Multiple { children: provider.register_children(generator), @@ -677,11 +1109,11 @@ mod borrowing_concept { generated } let rng = &mut rand::thread_rng(); - for _ in 0..10 { + for _ in 0..100_000 { let mut provider = UnbakedOwningProvider::new(); // act - let thing = gen_thing(&mut provider, rng); + let thing = gen_thing(&mut provider, rng, &mut 100); let provider = provider.bake(); // assert @@ -689,7 +1121,6 @@ mod borrowing_concept { thing .write_to(&provider, &mut output) .expect("Should be able to write into string"); - println!("{output}"); } } } From 12c0f04ed572833d104115d4a555f4d2e1d4dadc Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Sun, 24 Mar 2024 23:08:33 +0200 Subject: [PATCH 31/98] updated provider system, but ownership transfer is yet to be made --- src/data.rs | 704 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 412 insertions(+), 292 deletions(-) diff --git a/src/data.rs b/src/data.rs index 7e75aed..964b062 100644 --- a/src/data.rs +++ b/src/data.rs @@ -482,14 +482,7 @@ impl<'source> Token<'source> { mod borrowing_concept { use std::{marker::PhantomData, ops::Range}; - #[derive(Debug, derive_more::From, derive_more::AsRef)] - struct TextPromise(Range); - #[derive(Debug, derive_more::From, derive_more::AsRef)] - struct ChildPromise(usize); - #[derive(Debug, derive_more::From, derive_more::AsRef)] - struct ChildrenPromise(Range); - - enum Thing { + pub enum Thing { Text { text: Text, }, @@ -540,18 +533,18 @@ mod borrowing_concept { } } - trait UnbakedProvider { + pub trait UnbakedProvider { type Baked: BakedProvider; fn register_text(&mut self, text: &str) -> Text; fn register_child(&mut self, child: Thing) -> Child; fn register_children<'s>( &'s mut self, - children: impl FnMut(&mut &'s mut Self) -> Option>, + children: impl Iterator>, ) -> Children; fn bake(self) -> Self::Baked; } - trait BakedProvider { + pub trait BakedProvider { fn get_text<'s>(&'s self, promise: &Text) -> &'s str; fn get_child<'s>(&'s self, promise: &Child) -> &'s Thing; fn get_children<'s>(&'s self, promise: &Children) -> &'s [Thing]; @@ -561,74 +554,58 @@ mod borrowing_concept { fn with_text_ref<'text>( self, text: &'text str, - ) -> UnbakedTextRefProvider<'text, Text, Child, Children, Self> { - UnbakedTextRefProvider { text, inner: self } + ) -> UnbakedTextRefProvider<'text, Text, Child, Children, Self> + where + Self: Sized, + { + UnbakedTextRefProvider::<'text, Text, Child, Children, Self> { + text, + inner: self, + _phantom: PhantomData, + } + } + fn text_owned(self) -> UnbakedTextOwnProvider + where + Self: Sized, + { + UnbakedTextOwnProvider:: { + text: String::new(), + inner: self, + _phantom: PhantomData, + } + } + + fn things_owned(self) -> UnbakedChildrenOwnProvider + where + Self: Sized, + { + UnbakedChildrenOwnProvider:: { + children: Vec::new(), + inner: self, + } } } impl> UnbakedProviderExt for P { } - - enum NeverText {} - enum NeverChild {} - enum NeverChildren {} - - /// Provider that holds nothing and panics at any call to it. - struct UnbakedBottomProvider; - impl UnbakedProvider - for UnbakedBottomProvider - { - type Baked = BakedBottomProvider; - fn register_text(&mut self, text: &str) -> Text { - unimplemented!("This provider does not support text saving") - } - - fn register_child(&mut self, child: Thing) -> Child { - unimplemented!("This provider does not support child saving") - } - - fn register_children<'s>( - &'s mut self, - children: impl FnMut(&mut &'s mut Self) -> Option>, - ) -> Children { - unimplemented!("This provider does not support children saving") - } - - fn bake(self) -> Self::Baked { - BakedBottomProvider - } - } - struct BakedBottomProvider; - impl BakedProvider for BakedBottomProvider { - fn get_text<'s>(&'s self, promise: &Text) -> &'s str { - unimplemented!("This provider does not provide text") - } - - fn get_child<'s>(&'s self, promise: &Child) -> &'s Thing { - unimplemented!("This provider does not provide children") - } - - fn get_children<'s>(&'s self, promise: &Children) -> &'s [Thing] { - unimplemented!("This provider does not provide children") - } - } - trait MaybeTextRef<'text>: From<&'text str> { fn text_ref(&self) -> Option<&'text str>; } - struct UnbakedTextRefProvider<'text, Text, Child, Children, P> { + pub struct UnbakedTextRefProvider<'text, Text, Child, Children, P> { text: &'text str, inner: P, + _phantom: PhantomData<(Text, Child, Children)>, } impl<'text, Text, Child, Children, P> UnbakedProvider for UnbakedTextRefProvider<'text, Text, Child, Children, P> where P: UnbakedProvider, + Text: MaybeTextRef<'text>, { - type Baked = BakedTextRefProvider<'text, Text, Child, Children, P>; + type Baked = BakedTextRefProvider<'text, Text, Child, Children, P::Baked>; fn register_text(&mut self, text: &str) -> Text { // first, check out if this text, as a pointer, points inside this provider's referee: @@ -660,13 +637,13 @@ mod borrowing_concept { } // ok, so it seems like we've found a match using nothing but pointer-things. // that's great, cause it's literally O(1) for any buffer size - return &self.text[index_guess..index_guess + text_len].into(); + return self.text[index_guess..index_guess + text_len].into(); } // then, let's try finding this sort of sequence in the entire referee: if let Some(ind) = self.text.find(text) { // well, return corresponding slice, then - return TextRefPromise::Ref(&self.text[ind..ind + text.len()]); + return self.text[ind..ind + text.len()].into(); } // lastly, refer to inner provider @@ -679,18 +656,22 @@ mod borrowing_concept { fn register_children<'s>( &'s mut self, - children: impl FnMut(&mut &'s mut Self) -> Option>, + children: impl Iterator>, ) -> Children { self.inner.register_children(children) } fn bake(self) -> Self::Baked { - BakedTextRefProvider { inner: self.inner } + Self::Baked { + inner: self.inner.bake(), + _phantom: PhantomData, + } } } - struct BakedTextRefProvider<'text, Text, Child, Children, P> { + pub struct BakedTextRefProvider<'text, Text, Child, Children, P> { inner: P, + _phantom: PhantomData<(&'text (), Text, Child, Children)>, } impl<'text, Text, Child, Children, P> BakedProvider @@ -717,20 +698,22 @@ mod borrowing_concept { } trait MaybeTextOwnRange: From> { - fn own_range(&self) -> &Range; + fn own_range(&self) -> Option<&Range>; } - struct UnbakedTextOwnProvider { + pub struct UnbakedTextOwnProvider { text: String, inner: P, + _phantom: PhantomData<(Text, Child, Children)>, } impl UnbakedProvider for UnbakedTextOwnProvider where P: UnbakedProvider, + Text: MaybeTextOwnRange, { - type Baked = BakedTextOwnProvider; + type Baked = BakedTextOwnProvider; fn register_text(&mut self, text: &str) -> Text { let range_start = self.text.len(); @@ -745,22 +728,24 @@ mod borrowing_concept { fn register_children<'s>( &'s mut self, - children: impl FnMut(&mut &'s mut Self) -> Option>, + children: impl Iterator>, ) -> Children { - self.register_children(children) + self.inner.register_children(children) } fn bake(self) -> Self::Baked { - BakedTextOwnProvider { + Self::Baked { text: self.text.into_boxed_str(), inner: self.inner.bake(), + _phantom: PhantomData, } } } - struct BakedTextOwnProvider { + pub struct BakedTextOwnProvider { text: Box, inner: P, + _phantom: PhantomData<(Text, Child, Children)>, } impl BakedProvider @@ -787,14 +772,14 @@ mod borrowing_concept { } trait MaybeChildInd: From { - fn own_ind(&self) -> usize; + fn own_ind(&self) -> Option; } trait MaybeChildrenRange: From> { - fn own_range(&self) -> &Range; + fn own_range(&self) -> Option<&Range>; } - struct UnbakedChildrenOwnProvider { + pub struct UnbakedChildrenOwnProvider { children: Vec>, inner: P, } @@ -803,6 +788,8 @@ mod borrowing_concept { for UnbakedChildrenOwnProvider where P: UnbakedProvider, + Child: MaybeChildInd, + Children: MaybeChildrenRange, { type Baked = BakedChildrenOwnProvider; @@ -811,16 +798,19 @@ mod borrowing_concept { } fn register_child(&mut self, child: Thing) -> Child { - let ind = self.things.len(); - self.things.push(child); - ChildPromise(ind) + let ind = self.children.len(); + self.children.push(child); + Child::from(ind) } fn register_children<'s>( &'s mut self, - children: impl FnMut(&mut &'s mut Self) -> Option>, + children: impl Iterator>, ) -> Children { - todo!() + let range_start = self.children.len(); + self.children.extend(children); + let range_end = self.children.len(); + Children::from(range_start..range_end) } fn bake(self) -> Self::Baked { @@ -831,7 +821,7 @@ mod borrowing_concept { } } - struct BakedChildrenOwnProvider { + pub struct BakedChildrenOwnProvider { children: Box<[Thing]>, inner: P, } @@ -864,256 +854,386 @@ mod borrowing_concept { } } - struct UnbakedOwningProvider { - text: String, - things: Vec, + enum NeverText {} + impl<'text> From<&'text str> for NeverText { + fn from(_: &'text str) -> Self { + unimplemented!("This data type is not supported") + } } - - impl UnbakedOwningProvider { - fn new() -> Self { - Self { - text: String::new(), - things: Vec::new(), - } + impl<'text> MaybeTextRef<'text> for NeverText { + fn text_ref(&self) -> Option<&'text str> { + unimplemented!("This data type is not supported") + } + } + impl From> for NeverText { + fn from(_: Range) -> Self { + unimplemented!("This data type is not supported") + } + } + impl MaybeTextOwnRange for NeverText { + fn own_range(&self) -> Option<&Range> { + unimplemented!("This data type is not supported") + } + } + enum NeverChild {} + impl From for NeverChild { + fn from(_: usize) -> Self { + unimplemented!("This data type is not supported") + } + } + impl MaybeChildInd for NeverChild { + fn own_ind(&self) -> Option { + unimplemented!("This data type is not supported") + } + } + enum NeverChildren {} + impl From> for NeverChildren { + fn from(_: Range) -> Self { + unimplemented!("This data type is not supported") + } + } + impl MaybeChildrenRange for NeverChildren { + fn own_range(&self) -> Option<&Range> { + unimplemented!("This data type is not supported") } } - impl UnbakedProvider for UnbakedOwningProvider { - type Text = TextPromise; - type Child = ChildPromise; - type Children = ChildrenPromise; - type Baked = BakedOwningProvider; - - fn register_text(&mut self, text: &str) -> TextPromise { - let range_start = self.text.len(); - self.text.push_str(text); - let range_end = self.text.len(); - TextPromise(range_start..range_end) - } - - fn register_child(&mut self, child: OwnedThing) -> ChildPromise { - let ind = self.things.len(); - self.things.push(child); - ChildPromise(ind) - } - - fn register_children<'s>( - mut self: &'s mut Self, - mut children: impl FnMut(&mut &'s mut Self) -> Option, - ) -> ChildrenPromise { - let range_start = self.things.len(); - while let Some(thing) = children(&mut self) { - self.things.push(thing); - } - let range_end = self.things.len(); - ChildrenPromise(range_start..range_end) - } - - fn bake(self) -> Self::Baked { - BakedOwningProvider { - text: self.text.into_boxed_str(), - things: self.things.into_boxed_slice(), - } + #[derive(Debug, derive_more::From)] + pub struct TextRef<'text>(&'text str); + impl<'text> MaybeTextRef<'text> for TextRef<'text> { + fn text_ref(&self) -> Option<&'text str> { + Some(self.0) } } - - enum TextRefPromise<'text> { + #[derive(Debug, derive_more::From)] + pub struct TextOwn(Range); + impl MaybeTextOwnRange for TextOwn { + fn own_range(&self) -> Option<&Range> { + Some(&self.0) + } + } + #[derive(Debug, derive_more::From)] + pub enum TextMaybeOwn<'text> { Ref(&'text str), - Owned(Range), + Own(Range), + } + impl<'text> MaybeTextRef<'text> for TextMaybeOwn<'text> { + fn text_ref(&self) -> Option<&'text str> { + if let Self::Ref(s) = self { + Some(s) + } else { + None + } + } + } + impl MaybeTextOwnRange for TextMaybeOwn<'_> { + fn own_range(&self) -> Option<&Range> { + if let Self::Own(range) = self { + Some(range) + } else { + None + } + } } - type RefThing<'text> = Thing, ChildPromise, ChildrenPromise>; - - struct UnbakedTextRefProvider<'text, P> { - text: &'text str, - inner: P, + #[derive(Debug, derive_more::From)] + pub struct ChildInd(usize); + impl MaybeChildInd for ChildInd { + fn own_ind(&self) -> Option { + Some(self.0) + } + } + #[derive(Debug, derive_more::From)] + pub struct ChildrenRange(Range); + impl MaybeChildrenRange for ChildrenRange { + fn own_range(&self) -> Option<&Range> { + Some(&self.0) + } } - impl<'text, P> UnbakedProvider for UnbakedTextRefProvider<'text, P> - where - P: UnbakedProvider, + /// Provider that holds nothing and panics at any call to it. + struct UnbakedBottomProvider( + PhantomData<(Text, Child, Children)>, + ); + impl UnbakedProvider + for UnbakedBottomProvider { - type Text = TextRefPromise<'text>; - type Child = ChildPromise; - type Children = ChildrenPromise; - type Baked = BakedTextRefProvider<'text, P::Baked>; - - fn register_text(&mut self, text: &str) -> Self::Text { - // first, check out if this text, as a pointer, points inside this provider's referee: - 'nah: { - let referee_ptr = self.text.as_ptr() as usize; - let text_ptr = text.as_ptr() as usize; - if text_ptr < referee_ptr { - break 'nah; - } - let index_guess = text_ptr - referee_ptr; - let self_len = self.text.len(); - if index_guess >= self_len { - break 'nah; - } - // yeah, seems like it - // lastly, lets rule out partial overlap: - let text_len = text.len(); - if text_len + index_guess > self_len { - // buffers overlap partially, which is weird, probably should - // TODO warn about that too - break 'nah; - } - // let's check that literally, for a good measure: - // TODO probably add cfg to remove that in release build? - if !self.text[index_guess..].starts_with(text) { - // huh? that's something weird, probably must - // TODO warn about it - break 'nah; - } - // ok, so it seems like we've found a match using nothing but pointer-things. - // that's great, cause it's literally O(1) for any buffer size - return TextRefPromise::Ref(&self.text[index_guess..index_guess + text_len]); - } - - // then, let's try finding this sort of sequence in the entire referee: - if let Some(ind) = self.text.find(text) { - // well, return corresponding slice, then - return TextRefPromise::Ref(&self.text[ind..ind + text.len()]); - } - - // lastly, refer to inner provider - TextRefPromise::Owned(self.inner.register_text(text)) + type Baked = BakedBottomProvider; + fn register_text(&mut self, _: &str) -> Text { + unimplemented!("This provider does not support text saving") } - fn register_child( - &mut self, - child: Thing, - ) -> Self::Child { - self.inner.register_child(child) + fn register_child(&mut self, _: Thing) -> Child { + unimplemented!("This provider does not support child saving") } fn register_children<'s>( &'s mut self, - children: impl FnMut( - &mut &'s mut Self, - ) -> Option>, - ) -> Self::Children { - self.inner.register_children(children) + _: impl Iterator>, + ) -> Children { + unimplemented!("This provider does not support children saving") } fn bake(self) -> Self::Baked { - BakedTextRefProvider { - inner: self.inner.bake(), - owned_text: todo!(), - _phantom: PhantomData, - } + BakedBottomProvider(PhantomData) + } + } + impl UnbakedBottomProvider { + fn new() -> Self { + UnbakedBottomProvider(PhantomData) + } + } + struct BakedBottomProvider( + PhantomData<(Text, Child, Children)>, + ); + impl BakedProvider for BakedBottomProvider { + fn get_text<'s>(&'s self, _: &Text) -> &'s str { + unimplemented!("This provider does not provide text") + } + + fn get_child<'s>(&'s self, _: &Child) -> &'s Thing { + unimplemented!("This provider does not provide children") + } + + fn get_children<'s>(&'s self, _: &Children) -> &'s [Thing] { + unimplemented!("This provider does not provide children") } } - struct BakedTextRefProvider<'text, P> { - owned_text: Box, - inner: P, - _phantom: PhantomData<&'text ()>, + pub fn text_ref_provider<'text>( + text: &'text str, + ) -> impl UnbakedProvider, ChildInd, ChildrenRange> { + UnbakedBottomProvider::new() + .with_text_ref(text) + .things_owned() } - impl<'text, P> BakedProvider for BakedTextRefProvider<'text, P> + pub fn text_own_provider() -> impl UnbakedProvider { + UnbakedBottomProvider::new().text_owned().things_owned() + } + + pub fn text_maybe_own_provider<'text>( + ref_text: &'text str, + ) -> impl UnbakedProvider, ChildInd, ChildrenRange> { + UnbakedBottomProvider::new() + .text_owned() + .with_text_ref(ref_text) + .things_owned() + } + + trait AsOwnedPromise { + type TransformContext; + fn to_owned_promise(&self, from: &FromProvider, to: &mut ToProvider) -> OwnedPromise; + } + + // TODO I don't like that. It kinda restricts provider's inner structure + // Takes out text buffer. This trait must not be made public. + trait TakeText { + fn take_out(&mut self) -> Box + where + Self: Sized; + } + + impl<'text, Text, Child, Children, P> TakeText + for BakedTextRefProvider<'text, Text, Child, Children, P> where - P: BakedProvider< - Text = TextRefPromise<'text>, - Child = ChildPromise, - Children = ChildrenPromise, - >, + P: TakeText, { - type Text = TextRefPromise<'text>; - type Child = ChildPromise; - type Children = ChildrenPromise; - - fn get_text<'s>(&'s self, promise: &Self::Text) -> &'s str { - match promise { - TextRefPromise::Ref(reference) => reference, - TextRefPromise::Owned(owning) => &self.owned_text[owning.clone()], - } - } - - fn get_child<'s>( - &'s self, - ChildPromise(ind): &Self::Child, - ) -> &'s Thing { - &self.things[*ind] - } - - fn get_children<'s>( - &'s self, - ChildrenPromise(range): &Self::Children, - ) -> &'s [Thing] { - &self.things[range.clone()] + fn take_out(&mut self) -> Box + where + Self: Sized, + { + self.inner.take_out() } } + impl TakeText + for BakedTextOwnProvider + { + fn take_out(&mut self) -> Box + where + Self: Sized, + { + std::mem::replace(&mut self.text, String::new().into_boxed_str()) + } + } + + impl TakeText + for BakedChildrenOwnProvider + where + P: TakeText, + { + fn take_out(&mut self) -> Box + where + Self: Sized, + { + self.inner.take_out() + } + } + + // pub fn to_owning_provider>(provider: P, root: Child) -> (P) + #[cfg(test)] mod tests { - use crate::data::borrowing_concept::{OwnedThing, UnbakedOwningProvider, UnbakedProvider}; - - #[test] - fn owned_fuzz() { - use crate::data::borrowing_concept::Thing; - - // arrange - fn gen_thing<'owner, R: rand::Rng + ?Sized>( - provider: &mut UnbakedOwningProvider, - rng: &mut R, - nodes_left: &mut usize, - ) -> OwnedThing { - *nodes_left = nodes_left.saturating_sub(1); - fn gen_text(rng: &mut R) -> String { - // let it be of some length - let l: usize = rng.gen_range(3..=10); - let mut s = String::new(); - for _ in 0..l { - s.push(if rng.gen_bool(0.5) { - rng.gen_range('a'..='z') - } else if rng.gen_bool(0.4) { - rng.gen_range('а'..='я') - } else { - [' ', ',', '.'][rng.gen_range(0..3)] - }) - } - s - } - let generated = if *nodes_left == 0 || rng.gen_bool(0.6) { - // assume 60% or anything over specified number are text variants - // we need some random text for it - let text = gen_text(rng); - Thing::Text { - text: provider.register_text(&text), - } - } else if rng.gen_bool(0.9) { - // say, 36% are child owners - // we need some text and children for it - let text = gen_text(rng); - let child1 = gen_thing(provider, rng, nodes_left); - let child2 = gen_thing(provider, rng, nodes_left); - Thing::Child { - text: provider.register_text(&text), - child1: provider.register_child(child1), - child2: provider.register_child(child2), - } + fn gen_text(rng: &mut R) -> String { + // let it be of some length + let l: usize = rng.gen_range(3..=10); + let mut s = String::new(); + for _ in 0..l { + s.push(if rng.gen_bool(0.5) { + rng.gen_range('a'..='z') + } else if rng.gen_bool(0.4) { + rng.gen_range('а'..='я') } else { - // rest 4% own 1-5 children - let mut inner = (0..rng.gen_range(1..=5)).into_iter(); - let generator = move |provider: &mut &mut UnbakedOwningProvider| { - inner.next().map(|_| gen_thing(*provider, rng, nodes_left)) - }; - Thing::Multiple { - children: provider.register_children(generator), - } - }; - // average coefficient is 0.6 * 0 + 0.36 * 2 + 2.5 * 0.04 = 0.82 - // meaning this thing is likely to die at some point (I hope for that, at least) - generated + [' ', ',', '.'][rng.gen_range(0..3)] + }) } + s + } + + fn gen_contained_text<'text, R: rand::Rng + ?Sized>( + text: &'text str, + ) -> impl FnMut(&mut R) -> &'text str { + |rng| { + // basically just select two random indices + let mut ind1 = rng.gen_range(0..text.len()); + let mut ind2 = rng.gen_range(0..text.len()); + // swap them, if needed + if ind1 > ind2 { + std::mem::swap(&mut ind1, &mut ind2); + } + &text[ind1..ind2] + } + } + + fn gen_maybe_contained_text<'text, R: rand::Rng + ?Sized>( + text: &'text str, + ) -> impl FnMut(&mut R) -> std::borrow::Cow<'text, str> { + let mut contained = gen_contained_text(text); + move |rng| { + if rng.gen_bool(0.5) { + contained(rng).into() + } else { + gen_text(rng).into() + } + } + } + + fn generate_thing< + 'owner, + R: rand::Rng + ?Sized, + Text, + Child, + Children, + P: UnbakedProvider, + S: AsRef, + >( + provider: &mut P, + rng: &mut R, + text_gen: &mut impl FnMut(&mut R) -> S, + children_buf: &mut Vec>, + nodes_left: &mut usize, + ) -> Thing { + *nodes_left = nodes_left.saturating_sub(1); + let generated = if *nodes_left == 0 || rng.gen_bool(0.6) { + // assume 60% or anything over specified number are text variants + // we need some random text for it + let text = text_gen(rng); + Thing::Text { + text: provider.register_text(text.as_ref()), + } + } else if rng.gen_bool(0.9) { + // say, 36% are child owners + // we need some text and children for it + let text = text_gen(rng); + let child1 = generate_thing(provider, rng, text_gen, children_buf, nodes_left); + let child2 = generate_thing(provider, rng, text_gen, children_buf, nodes_left); + Thing::Child { + text: provider.register_text(text.as_ref()), + child1: provider.register_child(child1), + child2: provider.register_child(child2), + } + } else { + // rest 4% own 1-5 children + let buf_start = children_buf.len(); + for _ in 0..rng.gen_range(1..=5) { + let child = generate_thing(provider, rng, text_gen, children_buf, nodes_left); + children_buf.push(child); + } + let children = provider.register_children(children_buf.drain(buf_start..)); + Thing::Multiple { children } + }; + // average coefficient is 0.6 * 0 + 0.36 * 2 + 2.5 * 0.04 = 0.82 + // meaning this thing is likely to die at some point (I hope for that, at least) + generated + } + + use crate::data::borrowing_concept::UnbakedProvider; + + use super::{text_maybe_own_provider, text_own_provider, text_ref_provider, Thing}; + + // TODO check these versions of fuzzes to actually run correctly + #[test] + fn ref_fuzz() { + // arrange let rng = &mut rand::thread_rng(); + let text = "Lorem ipsum dolor sir ammet, concestesometghing idk never learned latin"; for _ in 0..100_000 { - let mut provider = UnbakedOwningProvider::new(); + let mut provider = text_ref_provider(text); // act - let thing = gen_thing(&mut provider, rng, &mut 100); + let thing = generate_thing( + &mut provider, + rng, + &mut gen_contained_text(text), + &mut Vec::new(), + &mut 100, + ); + let provider = provider.bake(); + + // assert + let mut output = String::new(); + thing + .write_to(&provider, &mut output) + .expect("Should be able to write into string"); + } + } + #[test] + fn owned_fuzz() { + // arrange + let rng = &mut rand::thread_rng(); + for _ in 0..100_000 { + let mut provider = text_own_provider(); + + // act + let thing = + generate_thing(&mut provider, rng, &mut gen_text, &mut Vec::new(), &mut 100); + let provider = provider.bake(); + + // assert + let mut output = String::new(); + thing + .write_to(&provider, &mut output) + .expect("Should be able to write into string"); + } + } + + #[test] + fn maybe_owned_fuzz() { + // arrange + let rng = &mut rand::thread_rng(); + let text = "Lorem ipsum dolor sir ammet, concestesometghing idk never learned latin"; + for _ in 0..100_000 { + let mut provider = text_maybe_own_provider(text); + + // act + let thing = generate_thing( + &mut provider, + rng, + &mut gen_maybe_contained_text(text), + &mut Vec::new(), + &mut 100, + ); let provider = provider.bake(); // assert From 74e8c82e0d16706a5069a23299890cd4aa892be2 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Mon, 25 Mar 2024 00:12:40 +0200 Subject: [PATCH 32/98] nightly (unfinished) --- src/data.rs | 63 +++++++++++++---------------------------------------- 1 file changed, 15 insertions(+), 48 deletions(-) diff --git a/src/data.rs b/src/data.rs index 964b062..632c8a5 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1021,54 +1021,21 @@ mod borrowing_concept { .things_owned() } - trait AsOwnedPromise { - type TransformContext; - fn to_owned_promise(&self, from: &FromProvider, to: &mut ToProvider) -> OwnedPromise; - } - - // TODO I don't like that. It kinda restricts provider's inner structure - // Takes out text buffer. This trait must not be made public. - trait TakeText { - fn take_out(&mut self) -> Box - where - Self: Sized; - } - - impl<'text, Text, Child, Children, P> TakeText - for BakedTextRefProvider<'text, Text, Child, Children, P> - where - P: TakeText, - { - fn take_out(&mut self) -> Box - where - Self: Sized, - { - self.inner.take_out() - } - } - - impl TakeText - for BakedTextOwnProvider - { - fn take_out(&mut self) -> Box - where - Self: Sized, - { - std::mem::replace(&mut self.text, String::new().into_boxed_str()) - } - } - - impl TakeText - for BakedChildrenOwnProvider - where - P: TakeText, - { - fn take_out(&mut self) -> Box - where - Self: Sized, - { - self.inner.take_out() - } + pub fn transfer_nodes< + FromText, + FromChild, + FromChildren, + ToText, + ToChild, + ToChildren, + FromProvider, + ToProvider, + >( + root: Thing, + from_provider: &FromProvider, + to_provider: &mut ToProvider, + ) -> Thing { + todo!() } // pub fn to_owning_provider>(provider: P, root: Child) -> (P) From 08594430a4913580c486b35ed296d28ca438d5d5 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Mon, 25 Mar 2024 10:17:17 +0200 Subject: [PATCH 33/98] Added sore fuzz-tests for last two functions - namely, provider moving and conversion to owning provider --- src/data.rs | 441 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 404 insertions(+), 37 deletions(-) diff --git a/src/data.rs b/src/data.rs index 632c8a5..2d67e12 100644 --- a/src/data.rs +++ b/src/data.rs @@ -478,9 +478,18 @@ impl<'source> Token<'source> { /// This approach is less optimized then the `unsafe` one, but `miri` scared me away from pointer manipulation, so here I am, I guess /// /// I see now, that providers can be composed into each other to build desired behavior step-by-step. That's really desireable too. +/// +/// All of the wrapper providers were implemented, along with a special `FullOwning`-provider to allow text ownership transfer. +/// All the basic functions are fuzz-tested now. +/// +/// The main motivation behind all of this mess is to "only pay for what you need" - tokens may or may not own their data, +/// but if you happen to know that for sure, you'll end up using much less memory, as their size will not include any sort of discriminator. +/// +/// Also, you have the opportunity to actually convert any provider referring to data into fully-owning provider, if you happen to own data they refer +/// to or a copy of the data they refer to. That's an unsafe process, unfortunately, as provider has no way to know if you give it the correct data. #[cfg(test)] mod borrowing_concept { - use std::{marker::PhantomData, ops::Range}; + use std::{borrow::Cow, marker::PhantomData, ops::Range}; pub enum Thing { Text { @@ -564,6 +573,19 @@ mod borrowing_concept { _phantom: PhantomData, } } + fn with_text_find( + self, + text: impl Into, + ) -> UnbakedTextFindProvider + where + Self: Sized, + { + UnbakedTextFindProvider { + text: text.into(), + inner: self, + _phantom: PhantomData, + } + } fn text_owned(self) -> UnbakedTextOwnProvider where Self: Sized, @@ -599,6 +621,37 @@ mod borrowing_concept { _phantom: PhantomData<(Text, Child, Children)>, } + fn try_find_pointer_match<'h, 'f>(haystack: &'h str, find: &'f str) -> Option> { + let referee_ptr = haystack.as_ptr() as usize; + let text_ptr = find.as_ptr() as usize; + if text_ptr < referee_ptr { + return None; + } + let index_guess = text_ptr - referee_ptr; + let self_len = haystack.len(); + if index_guess >= self_len { + return None; + } + // yeah, seems like it + // lastly, lets rule out partial overlap: + let text_len = find.len(); + if text_len + index_guess > self_len { + // buffers overlap partially, which is weird, probably should + // TODO warn about that too + return None; + } + // let's check that literally, for a good measure: + // TODO probably add cfg to remove that in release build? + if !haystack[index_guess..].starts_with(find) { + // huh? that's something weird, probably must + // TODO warn about it + return None; + } + // ok, so it seems like we've found a match using nothing but pointer-things. + // that's great, cause it's literally O(1) for any buffer size + return Some(index_guess..index_guess + text_len); + } + impl<'text, Text, Child, Children, P> UnbakedProvider for UnbakedTextRefProvider<'text, Text, Child, Children, P> where @@ -609,35 +662,8 @@ mod borrowing_concept { fn register_text(&mut self, text: &str) -> Text { // first, check out if this text, as a pointer, points inside this provider's referee: - 'nah: { - let referee_ptr = self.text.as_ptr() as usize; - let text_ptr = text.as_ptr() as usize; - if text_ptr < referee_ptr { - break 'nah; - } - let index_guess = text_ptr - referee_ptr; - let self_len = self.text.len(); - if index_guess >= self_len { - break 'nah; - } - // yeah, seems like it - // lastly, lets rule out partial overlap: - let text_len = text.len(); - if text_len + index_guess > self_len { - // buffers overlap partially, which is weird, probably should - // TODO warn about that too - break 'nah; - } - // let's check that literally, for a good measure: - // TODO probably add cfg to remove that in release build? - if !self.text[index_guess..].starts_with(text) { - // huh? that's something weird, probably must - // TODO warn about it - break 'nah; - } - // ok, so it seems like we've found a match using nothing but pointer-things. - // that's great, cause it's literally O(1) for any buffer size - return self.text[index_guess..index_guess + text_len].into(); + if let Some(range) = try_find_pointer_match(self.text, text) { + return self.text[range].into(); } // then, let's try finding this sort of sequence in the entire referee: @@ -770,6 +796,78 @@ mod borrowing_concept { self.inner.get_children(promise) } } + pub struct UnbakedTextFindProvider { + text: String, + inner: P, + _phantom: PhantomData<(Text, Child, Children)>, + } + + impl UnbakedProvider + for UnbakedTextFindProvider + where + P: UnbakedProvider, + Text: MaybeTextOwnRange, + { + type Baked = BakedTextOwnProvider; + + fn register_text(&mut self, text: &str) -> Text { + if let Some(ind) = self.text.find(text) { + return Text::from(ind..ind + text.len()); + } + let range_start = self.text.len(); + self.text.push_str(text); + let range_end = self.text.len(); + Text::from(range_start..range_end) + } + + fn register_child(&mut self, child: Thing) -> Child { + self.inner.register_child(child) + } + + fn register_children<'s>( + &'s mut self, + children: impl Iterator>, + ) -> Children { + self.inner.register_children(children) + } + + fn bake(self) -> Self::Baked { + Self::Baked { + text: self.text.into_boxed_str(), + inner: self.inner.bake(), + _phantom: PhantomData, + } + } + } + + pub struct BakedTextFindProvider { + text: Box, + inner: P, + _phantom: PhantomData<(Text, Child, Children)>, + } + + impl BakedProvider + for BakedTextFindProvider + where + P: BakedProvider, + Text: MaybeTextOwnRange, + { + fn get_text<'s>(&'s self, promise: &Text) -> &'s str { + if let Some(range) = promise.own_range() { + &self.text[range.clone()] + } else { + self.inner.get_text(promise) + } + } + + fn get_child<'s>(&'s self, promise: &Child) -> &'s Thing { + self.inner.get_child(promise) + } + + fn get_children<'s>(&'s self, promise: &Children) -> &'s [Thing] { + self.inner.get_children(promise) + } + } trait MaybeChildInd: From { fn own_ind(&self) -> Option; @@ -1021,7 +1119,25 @@ mod borrowing_concept { .things_owned() } - pub fn transfer_nodes< + pub fn text_maybe_own_find_provider<'text>( + ref_text: &'text str, + own_text: impl Into, + ) -> impl UnbakedProvider, ChildInd, ChildrenRange> { + UnbakedBottomProvider::new() + .with_text_find(own_text) + .with_text_ref(ref_text) + .things_owned() + } + + pub fn text_own_find_provider( + own_text: impl Into, + ) -> impl UnbakedProvider { + UnbakedBottomProvider::new() + .with_text_find(own_text) + .things_owned() + } + + fn transfer_nodes< FromText, FromChild, FromChildren, @@ -1031,14 +1147,166 @@ mod borrowing_concept { FromProvider, ToProvider, >( - root: Thing, + root: &Thing, from_provider: &FromProvider, to_provider: &mut ToProvider, - ) -> Thing { - todo!() + buf: &mut Vec>, + ) -> Thing + where + FromProvider: BakedProvider, + ToProvider: UnbakedProvider, + { + match root { + Thing::Text { text } => { + let s = from_provider.get_text(&text); + let text = to_provider.register_text(s); + Thing::Text { text } + } + Thing::Child { + text, + child1, + child2, + } => { + let s = from_provider.get_text(text); + let text = to_provider.register_text(s); + let ch1 = from_provider.get_child(child1); + let ch1 = transfer_nodes(ch1, from_provider, to_provider, buf); + let child1 = to_provider.register_child(ch1); + let ch2 = from_provider.get_child(child2); + let ch2 = transfer_nodes(ch2, from_provider, to_provider, buf); + let child2 = to_provider.register_child(ch2); + Thing::Child { + text, + child1, + child2, + } + } + Thing::Multiple { children } => { + let ch = from_provider.get_children(children); + let start_ind = buf.len(); + for child in ch { + let to_child = transfer_nodes(child, from_provider, to_provider, buf); + buf.push(to_child); + } + let children = to_provider.register_children(buf.drain(start_ind..)); + Thing::Multiple { children } + } + } } - // pub fn to_owning_provider>(provider: P, root: Child) -> (P) + struct UnbakedFullOwner<'text> { + yet_unowned_text: Cow<'text, str>, + owned_text: String, + children: Vec>, + } + + impl<'text> UnbakedFullOwner<'text> { + pub fn new(yet_unowned: &'text str) -> Self { + Self { + yet_unowned_text: yet_unowned.into(), + owned_text: String::new(), + children: Vec::new(), + } + } + pub unsafe fn drop_unowned(self) -> UnbakedFullOwner<'static> { + let Self { + owned_text, + children, + .. + } = self; + UnbakedFullOwner { + yet_unowned_text: "".into(), + owned_text, + children, + } + } + // TODO add panic info + pub unsafe fn provide(&mut self, data: impl Into) { + let s = data.into(); + // assert_eq!(self.yet_unowned_text, s, "Provided data must be the same!"); + self.yet_unowned_text = Cow::Owned(s) + } + } + + impl<'text> UnbakedProvider for UnbakedFullOwner<'text> { + type Baked = FullOwner; + + fn register_text(&mut self, text: &str) -> TextOwn { + if let Some(range) = try_find_pointer_match(&self.yet_unowned_text, text) { + return TextOwn(range); + } + if let Some(ind) = self.yet_unowned_text.find(text) { + return TextOwn(ind..ind + text.len()); + } + let len = self.yet_unowned_text.len(); + if let Some(mut ind) = self.owned_text.find(text) { + ind += len; + return TextOwn(ind..ind + text.len()); + } + let start_ind = self.owned_text.len() + len; + self.owned_text.push_str(text); + let end_ind = self.owned_text.len() + len; + TextOwn(start_ind..end_ind) + } + + fn register_child(&mut self, child: Thing) -> ChildInd { + let ind = self.children.len(); + self.children.push(child); + ChildInd(ind) + } + + fn register_children<'s>( + &'s mut self, + children: impl Iterator>, + ) -> ChildrenRange { + let start_ind = self.children.len(); + self.children.extend(children); + let end_ind = self.children.len(); + ChildrenRange(start_ind..end_ind) + } + + fn bake(self) -> Self::Baked { + let Cow::Owned(s) = self.yet_unowned_text else { + panic!("Full Owner cannot be baked until data is provided to it") + }; + FullOwner { + now_owned_text: s.into_boxed_str(), + owned_text: self.owned_text.into_boxed_str(), + children: self.children.into_boxed_slice(), + } + } + } + + struct FullOwner { + now_owned_text: Box, + owned_text: Box, + children: Box<[Thing]>, + } + + impl BakedProvider for FullOwner { + fn get_text<'s>(&'s self, TextOwn(range): &TextOwn) -> &'s str { + let (start, end) = (range.start, range.end); + let len = self.now_owned_text.len(); + if start >= len { + return &self.owned_text[start - len..end - len]; + } + &self.now_owned_text[start..end] + } + + fn get_child<'s>( + &'s self, + &ChildInd(i): &ChildInd, + ) -> &'s Thing { + &self.children[i] + } + + fn get_children<'s>( + &'s self, + ChildrenRange(range): &ChildrenRange, + ) -> &'s [Thing] { + &self.children[range.clone()] + } + } #[cfg(test)] mod tests { @@ -1135,9 +1403,11 @@ mod borrowing_concept { generated } - use crate::data::borrowing_concept::UnbakedProvider; + use crate::data::borrowing_concept::{UnbakedFullOwner, UnbakedProvider}; - use super::{text_maybe_own_provider, text_own_provider, text_ref_provider, Thing}; + use super::{ + text_maybe_own_provider, text_own_provider, text_ref_provider, transfer_nodes, Thing, + }; // TODO check these versions of fuzzes to actually run correctly #[test] @@ -1210,5 +1480,102 @@ mod borrowing_concept { .expect("Should be able to write into string"); } } + + #[test] + fn fuzz_transfer() { + // arrange + let rng = &mut rand::thread_rng(); + let text = "Lorem ipsum dolor sir ammet, concestesometghing idk never learned latin"; + for _ in 0..100_000 { + let mut provider = text_maybe_own_provider(text); + let thing = generate_thing( + &mut provider, + rng, + &mut gen_maybe_contained_text(text), + &mut Vec::new(), + &mut 100, + ); + let original_provider = provider.bake(); + + // act + let mut moved_provider = text_own_provider(); + let moved_thing = transfer_nodes( + &thing, + &original_provider, + &mut moved_provider, + &mut Vec::new(), + ); + let moved_provider = moved_provider.bake(); + + // assert + let mut original = String::new(); + thing + .write_to(&original_provider, &mut original) + .expect("Should be able to write into string"); + let mut moved = String::new(); + moved_thing + .write_to(&moved_provider, &mut moved) + .expect("Should be able to write into string"); + assert_eq!( + original, moved, + "Moved things should produce the same result" + ); + } + } + + #[test] + fn fuzz_to_owned() { + // arrange + let rng = &mut rand::thread_rng(); + for _ in 0..100_000 { + let text = + "Lorem ipsum dolor sir ammet, concestesometghing idk never learned latin" + .to_string() + .into_boxed_str(); + let mut provider = text_maybe_own_provider(&text); + let thing = generate_thing( + &mut provider, + rng, + &mut gen_maybe_contained_text(&text), + &mut Vec::new(), + &mut 100, + ); + let original_provider = provider.bake(); + + // act + let mut owning_provider = UnbakedFullOwner::new(&text); + let owning_thing = transfer_nodes( + &thing, + &original_provider, + &mut owning_provider, + &mut Vec::new(), + ); + let mut original = String::new(); + thing + .write_to(&original_provider, &mut original) + .expect("Should be able to write into string"); + // SAFETY: + // data provided and data `OwnedProvider` referred to here is the same, + // as it's contained in the same immutable variable + let owning_provider = unsafe { + let mut owning_provider = owning_provider.drop_unowned(); + drop(original_provider); + owning_provider.provide(text); + owning_provider + }; + + let owning_provider = owning_provider.bake(); + + // assert + let mut owned = String::new(); + owning_thing + .write_to(&owning_provider, &mut owned) + .expect("Should be able to write into string"); + assert_eq!( + original, owned, + "Owned things should produce the same result" + ); + } + } } } From 3cff327afc61aebd691b82a1ca4d143a6961b1d8 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Mon, 25 Mar 2024 13:43:48 +0200 Subject: [PATCH 34/98] Refactored multiple traits into one - renamed a lot of stuff for it all to be more neutral --- src/data.rs | 654 ++++++++++++++++++++++++++++------------------------ 1 file changed, 351 insertions(+), 303 deletions(-) diff --git a/src/data.rs b/src/data.rs index 2d67e12..1d80f95 100644 --- a/src/data.rs +++ b/src/data.rs @@ -487,10 +487,71 @@ impl<'source> Token<'source> { /// /// Also, you have the opportunity to actually convert any provider referring to data into fully-owning provider, if you happen to own data they refer /// to or a copy of the data they refer to. That's an unsafe process, unfortunately, as provider has no way to know if you give it the correct data. +/// +/// Almost everything in the module is getting renamed to resemble more neutral "tree thing" #[cfg(test)] mod borrowing_concept { use std::{borrow::Cow, marker::PhantomData, ops::Range}; + struct ThingTree { + root: Leaf, + provider: Storage, + _phantom: PhantomData<(Text, Leaves)>, + } + + impl ThingTree { + fn new(thing: Thing, mut storage: ToStorage) -> Self + where + ToStorage: UnsealedStorage, + { + let child = storage.register_leaf(thing); + Self { + root: child, + provider: storage.seal(), + _phantom: PhantomData, + } + } + + fn root(&self) -> &Leaf { + &self.root + } + + fn root_thing(&self) -> &Thing + where + Storage: SealedStorage, + { + self.provider.get_leaf(&self.root) + } + + fn storage(&self) -> &Storage { + &self.provider + } + + fn move_to( + &self, + other_storage: &mut ToStorage, + ) -> Thing + where + ToStorage: UnsealedStorage, + Storage: SealedStorage, + { + transfer_nodes( + self.root_thing(), + self.storage(), + other_storage, + &mut Vec::new(), + ) + } + + fn write_to(&self, output: &mut W) -> std::fmt::Result + where + Storage: SealedStorage, + { + let thing = self.root_thing(); + thing.write_to(&self.provider, output) + } + } + pub enum Thing { Text { text: Text, @@ -505,10 +566,10 @@ mod borrowing_concept { }, } - impl Thing { + impl Thing { fn write_to(&self, provider: &P, out: &mut W) -> std::fmt::Result where - P: BakedProvider, + P: SealedStorage, W: std::fmt::Write + ?Sized, { match self { @@ -519,15 +580,15 @@ mod borrowing_concept { child2, } => { write!(out, "Child({}, ", provider.get_text(text))?; - provider.get_child(child1).write_to(provider, out)?; + provider.get_leaf(child1).write_to(provider, out)?; out.write_str(", ")?; - provider.get_child(child2).write_to(provider, out)?; + provider.get_leaf(child2).write_to(provider, out)?; out.write_str(")")?; Ok(()) } Thing::Multiple { children } => { out.write_str("Multiple[")?; - let children = provider.get_children(children); + let children = provider.get_leaves(children); if let Some(first) = children.first() { first.write_to(provider, out)?; for child in &children[1..] { @@ -542,83 +603,86 @@ mod borrowing_concept { } } - pub trait UnbakedProvider { - type Baked: BakedProvider; + pub trait UnsealedStorage { + type Sealed: SealedStorage; fn register_text(&mut self, text: &str) -> Text; - fn register_child(&mut self, child: Thing) -> Child; - fn register_children<'s>( + fn register_leaf(&mut self, leaf: Thing) -> Leaf; + fn register_leaves<'s>( &'s mut self, - children: impl Iterator>, - ) -> Children; - fn bake(self) -> Self::Baked; + leaves: impl Iterator>, + ) -> Leaves; + fn seal(self) -> Self::Sealed; } - pub trait BakedProvider { + pub trait SealedStorage { fn get_text<'s>(&'s self, promise: &Text) -> &'s str; - fn get_child<'s>(&'s self, promise: &Child) -> &'s Thing; - fn get_children<'s>(&'s self, promise: &Children) -> &'s [Thing]; + fn get_leaf<'s>(&'s self, promise: &Leaf) -> &'s Thing; + fn get_leaves<'s>(&'s self, promise: &Leaves) -> &'s [Thing]; } - trait UnbakedProviderExt: UnbakedProvider { + trait UnsealedStorageExt: UnsealedStorage { fn with_text_ref<'text>( self, text: &'text str, - ) -> UnbakedTextRefProvider<'text, Text, Child, Children, Self> + ) -> UnsealedTextRefStorage<'text, Text, Leaf, Leaves, Self> where Self: Sized, { - UnbakedTextRefProvider::<'text, Text, Child, Children, Self> { + UnsealedTextRefStorage::<'text, Text, Leaf, Leaves, Self> { text, inner: self, _phantom: PhantomData, } } + fn with_text_find( self, text: impl Into, - ) -> UnbakedTextFindProvider + ) -> UnsealedTextFindStorage where Self: Sized, { - UnbakedTextFindProvider { + UnsealedTextFindStorage { text: text.into(), inner: self, _phantom: PhantomData, } } - fn text_owned(self) -> UnbakedTextOwnProvider + + fn text_owned(self) -> UnsealedTextOwnStorage where Self: Sized, { - UnbakedTextOwnProvider:: { + UnsealedTextOwnStorage:: { text: String::new(), inner: self, _phantom: PhantomData, } } - fn things_owned(self) -> UnbakedChildrenOwnProvider + fn leaves_owned(self) -> UnsealedLeavesOwnStorage where Self: Sized, { - UnbakedChildrenOwnProvider:: { - children: Vec::new(), + UnsealedLeavesOwnStorage:: { + things: Vec::new(), inner: self, } } } - impl> - UnbakedProviderExt for P + impl> + UnsealedStorageExt for S { } - trait MaybeTextRef<'text>: From<&'text str> { - fn text_ref(&self) -> Option<&'text str>; + trait OptPromiseInfo: From { + fn info(&self) -> Option<&PromiseInfo>; } - pub struct UnbakedTextRefProvider<'text, Text, Child, Children, P> { + struct TextRefStorageType; + pub struct UnsealedTextRefStorage<'text, Text, Leaf, Leaves, InnerStorage> { text: &'text str, - inner: P, - _phantom: PhantomData<(Text, Child, Children)>, + inner: InnerStorage, + _phantom: PhantomData<(Text, Leaf, Leaves)>, } fn try_find_pointer_match<'h, 'f>(haystack: &'h str, find: &'f str) -> Option> { @@ -633,7 +697,7 @@ mod borrowing_concept { return None; } // yeah, seems like it - // lastly, lets rule out partial overlap: + // lastly, let's rule out partial overlap: let text_len = find.len(); if text_len + index_guess > self_len { // buffers overlap partially, which is weird, probably should @@ -652,13 +716,13 @@ mod borrowing_concept { return Some(index_guess..index_guess + text_len); } - impl<'text, Text, Child, Children, P> UnbakedProvider - for UnbakedTextRefProvider<'text, Text, Child, Children, P> + impl<'text, Text, Leaf, Leaves, InnerStorage> UnsealedStorage + for UnsealedTextRefStorage<'text, Text, Leaf, Leaves, InnerStorage> where - P: UnbakedProvider, - Text: MaybeTextRef<'text>, + InnerStorage: UnsealedStorage, + Text: OptPromiseInfo<&'text str, TextRefStorageType>, { - type Baked = BakedTextRefProvider<'text, Text, Child, Children, P::Baked>; + type Sealed = TextRefStorage<'text, Text, Leaf, Leaves, InnerStorage::Sealed>; fn register_text(&mut self, text: &str) -> Text { // first, check out if this text, as a pointer, points inside this provider's referee: @@ -676,70 +740,67 @@ mod borrowing_concept { self.inner.register_text(text) } - fn register_child(&mut self, child: Thing) -> Child { - self.inner.register_child(child) + fn register_leaf(&mut self, leaf: Thing) -> Leaf { + self.inner.register_leaf(leaf) } - fn register_children<'s>( + fn register_leaves<'s>( &'s mut self, - children: impl Iterator>, - ) -> Children { - self.inner.register_children(children) + leaves: impl Iterator>, + ) -> Leaves { + self.inner.register_leaves(leaves) } - fn bake(self) -> Self::Baked { - Self::Baked { - inner: self.inner.bake(), + fn seal(self) -> Self::Sealed { + Self::Sealed { + inner: self.inner.seal(), _phantom: PhantomData, } } } - pub struct BakedTextRefProvider<'text, Text, Child, Children, P> { - inner: P, - _phantom: PhantomData<(&'text (), Text, Child, Children)>, + pub struct TextRefStorage<'text, Text, Leaf, Leaves, InnerStorage> { + inner: InnerStorage, + _phantom: PhantomData<(&'text (), Text, Leaf, Leaves)>, } - impl<'text, Text, Child, Children, P> BakedProvider - for BakedTextRefProvider<'text, Text, Child, Children, P> + impl<'text, Text, Leaf, Leaves, InnerStorage> SealedStorage + for TextRefStorage<'text, Text, Leaf, Leaves, InnerStorage> where - P: BakedProvider, - Text: MaybeTextRef<'text>, + InnerStorage: SealedStorage, + Text: OptPromiseInfo<&'text str, TextRefStorageType>, { fn get_text<'s>(&'s self, promise: &Text) -> &'s str { - if let Some(res) = promise.text_ref() { + if let Some(res) = promise.info() { res } else { self.inner.get_text(promise) } } - fn get_child<'s>(&'s self, promise: &Child) -> &'s Thing { - self.inner.get_child(promise) + fn get_leaf<'s>(&'s self, promise: &Leaf) -> &'s Thing { + self.inner.get_leaf(promise) } - fn get_children<'s>(&'s self, promise: &Children) -> &'s [Thing] { - self.inner.get_children(promise) + fn get_leaves<'s>(&'s self, promise: &Leaves) -> &'s [Thing] { + self.inner.get_leaves(promise) } } - trait MaybeTextOwnRange: From> { - fn own_range(&self) -> Option<&Range>; - } - - pub struct UnbakedTextOwnProvider { + struct TextOwnStorageType; + pub struct UnsealedTextOwnStorage { text: String, - inner: P, - _phantom: PhantomData<(Text, Child, Children)>, + inner: InnerStorage, + _phantom: PhantomData<(Text, Leaf, Leaves)>, } - impl UnbakedProvider - for UnbakedTextOwnProvider + impl UnsealedStorage + for UnsealedTextOwnStorage where - P: UnbakedProvider, - Text: MaybeTextOwnRange, + InnerStorage: UnsealedStorage, + Text: OptPromiseInfo, TextOwnStorageType>, { - type Baked = BakedTextOwnProvider; + type Sealed = TextOwnStorage; fn register_text(&mut self, text: &str) -> Text { let range_start = self.text.len(); @@ -748,67 +809,69 @@ mod borrowing_concept { Text::from(range_start..range_end) } - fn register_child(&mut self, child: Thing) -> Child { - self.inner.register_child(child) + fn register_leaf(&mut self, leaf: Thing) -> Leaf { + self.inner.register_leaf(leaf) } - fn register_children<'s>( + fn register_leaves<'s>( &'s mut self, - children: impl Iterator>, - ) -> Children { - self.inner.register_children(children) + leaves: impl Iterator>, + ) -> Leaves { + self.inner.register_leaves(leaves) } - fn bake(self) -> Self::Baked { - Self::Baked { + fn seal(self) -> Self::Sealed { + Self::Sealed { text: self.text.into_boxed_str(), - inner: self.inner.bake(), + inner: self.inner.seal(), _phantom: PhantomData, } } } - pub struct BakedTextOwnProvider { + pub struct TextOwnStorage { text: Box, - inner: P, - _phantom: PhantomData<(Text, Child, Children)>, + inner: InnerStorage, + _phantom: PhantomData<(Text, Leaf, Leaves)>, } - impl BakedProvider - for BakedTextOwnProvider + impl SealedStorage + for TextOwnStorage where - P: BakedProvider, - Text: MaybeTextOwnRange, + InnerStorage: SealedStorage, + Text: OptPromiseInfo, TextOwnStorageType>, { fn get_text<'s>(&'s self, promise: &Text) -> &'s str { - if let Some(range) = promise.own_range() { + if let Some(range) = promise.info() { &self.text[range.clone()] } else { self.inner.get_text(promise) } } - fn get_child<'s>(&'s self, promise: &Child) -> &'s Thing { - self.inner.get_child(promise) + fn get_leaf<'s>(&'s self, promise: &Leaf) -> &'s Thing { + self.inner.get_leaf(promise) } - fn get_children<'s>(&'s self, promise: &Children) -> &'s [Thing] { - self.inner.get_children(promise) + fn get_leaves<'s>(&'s self, promise: &Leaves) -> &'s [Thing] { + self.inner.get_leaves(promise) } } - pub struct UnbakedTextFindProvider { + + struct TextFindStorageType; + pub struct UnsealedTextFindStorage { text: String, - inner: P, - _phantom: PhantomData<(Text, Child, Children)>, + inner: InnerStorage, + _phantom: PhantomData<(Text, Leaf, Leaves)>, } - impl UnbakedProvider - for UnbakedTextFindProvider + impl UnsealedStorage + for UnsealedTextFindStorage where - P: UnbakedProvider, - Text: MaybeTextOwnRange, + InnerStorage: UnsealedStorage, + Text: OptPromiseInfo, TextFindStorageType>, { - type Baked = BakedTextOwnProvider; + type Sealed = TextFindStorage; fn register_text(&mut self, text: &str) -> Text { if let Some(ind) = self.text.find(text) { @@ -820,193 +883,191 @@ mod borrowing_concept { Text::from(range_start..range_end) } - fn register_child(&mut self, child: Thing) -> Child { - self.inner.register_child(child) + fn register_leaf(&mut self, leaf: Thing) -> Leaf { + self.inner.register_leaf(leaf) } - fn register_children<'s>( + fn register_leaves<'s>( &'s mut self, - children: impl Iterator>, - ) -> Children { - self.inner.register_children(children) + leaves: impl Iterator>, + ) -> Leaves { + self.inner.register_leaves(leaves) } - fn bake(self) -> Self::Baked { - Self::Baked { + fn seal(self) -> Self::Sealed { + Self::Sealed { text: self.text.into_boxed_str(), - inner: self.inner.bake(), + inner: self.inner.seal(), _phantom: PhantomData, } } } - pub struct BakedTextFindProvider { + pub struct TextFindStorage { text: Box, - inner: P, - _phantom: PhantomData<(Text, Child, Children)>, + inner: InnerStorage, + _phantom: PhantomData<(Text, Leaf, Leaves)>, } - impl BakedProvider - for BakedTextFindProvider + impl SealedStorage + for TextFindStorage where - P: BakedProvider, - Text: MaybeTextOwnRange, + InnerStorage: SealedStorage, + Text: OptPromiseInfo, TextFindStorageType>, { fn get_text<'s>(&'s self, promise: &Text) -> &'s str { - if let Some(range) = promise.own_range() { + if let Some(range) = promise.info() { &self.text[range.clone()] } else { self.inner.get_text(promise) } } - fn get_child<'s>(&'s self, promise: &Child) -> &'s Thing { - self.inner.get_child(promise) + fn get_leaf<'s>(&'s self, promise: &Leaf) -> &'s Thing { + self.inner.get_leaf(promise) } - fn get_children<'s>(&'s self, promise: &Children) -> &'s [Thing] { - self.inner.get_children(promise) + fn get_leaves<'s>(&'s self, promise: &Leaves) -> &'s [Thing] { + self.inner.get_leaves(promise) } } - trait MaybeChildInd: From { - fn own_ind(&self) -> Option; - } - - trait MaybeChildrenRange: From> { - fn own_range(&self) -> Option<&Range>; - } - - pub struct UnbakedChildrenOwnProvider { - children: Vec>, + struct LeavesOwnStorageType; + pub struct UnsealedLeavesOwnStorage { + things: Vec>, inner: P, } - impl UnbakedProvider - for UnbakedChildrenOwnProvider + impl UnsealedStorage + for UnsealedLeavesOwnStorage where - P: UnbakedProvider, - Child: MaybeChildInd, - Children: MaybeChildrenRange, + InnerStorage: UnsealedStorage, + Leaf: OptPromiseInfo, + Leaves: OptPromiseInfo, LeavesOwnStorageType>, { - type Baked = BakedChildrenOwnProvider; + type Sealed = LeavesOwnStorage; fn register_text(&mut self, text: &str) -> Text { self.inner.register_text(text) } - fn register_child(&mut self, child: Thing) -> Child { - let ind = self.children.len(); - self.children.push(child); - Child::from(ind) + fn register_leaf(&mut self, leaf: Thing) -> Leaf { + let ind = self.things.len(); + self.things.push(leaf); + Leaf::from(ind) } - fn register_children<'s>( + fn register_leaves<'s>( &'s mut self, - children: impl Iterator>, - ) -> Children { - let range_start = self.children.len(); - self.children.extend(children); - let range_end = self.children.len(); - Children::from(range_start..range_end) + leaves: impl Iterator>, + ) -> Leaves { + let range_start = self.things.len(); + self.things.extend(leaves); + let range_end = self.things.len(); + Leaves::from(range_start..range_end) } - fn bake(self) -> Self::Baked { - BakedChildrenOwnProvider { - children: self.children.into_boxed_slice(), - inner: self.inner.bake(), + fn seal(self) -> Self::Sealed { + LeavesOwnStorage { + things: self.things.into_boxed_slice(), + inner: self.inner.seal(), } } } - pub struct BakedChildrenOwnProvider { - children: Box<[Thing]>, - inner: P, + pub struct LeavesOwnStorage { + things: Box<[Thing]>, + inner: InnerStorage, } - impl BakedProvider - for BakedChildrenOwnProvider + impl SealedStorage + for LeavesOwnStorage where - P: BakedProvider, - Child: MaybeChildInd, - Children: MaybeChildrenRange, + InnerStorage: SealedStorage, + Leaf: OptPromiseInfo, + Leaves: OptPromiseInfo, LeavesOwnStorageType>, { fn get_text<'s>(&'s self, promise: &Text) -> &'s str { self.inner.get_text(promise) } - fn get_child<'s>(&'s self, promise: &Child) -> &'s Thing { - if let Some(ind) = promise.own_ind() { - &self.children[ind] + fn get_leaf<'s>(&'s self, promise: &Leaf) -> &'s Thing { + if let Some(ind) = promise.info() { + &self.things[*ind] } else { - self.inner.get_child(promise) + self.inner.get_leaf(promise) } } - fn get_children<'s>(&'s self, promise: &Children) -> &'s [Thing] { - if let Some(range) = promise.own_range() { - &self.children[range.clone()] + fn get_leaves<'s>(&'s self, promise: &Leaves) -> &'s [Thing] { + if let Some(range) = promise.info() { + &self.things[range.clone()] } else { - self.inner.get_children(promise) + self.inner.get_leaves(promise) } } } enum NeverText {} - impl<'text> From<&'text str> for NeverText { - fn from(_: &'text str) -> Self { - unimplemented!("This data type is not supported") - } - } - impl<'text> MaybeTextRef<'text> for NeverText { - fn text_ref(&self) -> Option<&'text str> { - unimplemented!("This data type is not supported") - } - } impl From> for NeverText { fn from(_: Range) -> Self { unimplemented!("This data type is not supported") } } - impl MaybeTextOwnRange for NeverText { - fn own_range(&self) -> Option<&Range> { + impl<'text> From<&'text str> for NeverText { + fn from(_: &'text str) -> Self { unimplemented!("This data type is not supported") } } - enum NeverChild {} - impl From for NeverChild { + impl OptPromiseInfo for NeverText + where + NeverText: From, + { + fn info(&self) -> Option<&T> { + unimplemented!("This data type is not supported") + } + } + enum NeverLeaf {} + impl From for NeverLeaf { fn from(_: usize) -> Self { unimplemented!("This data type is not supported") } } - impl MaybeChildInd for NeverChild { - fn own_ind(&self) -> Option { + impl OptPromiseInfo for NeverLeaf + where + NeverLeaf: From, + { + fn info(&self) -> Option<&T> { unimplemented!("This data type is not supported") } } - enum NeverChildren {} - impl From> for NeverChildren { + enum NeverLeaves {} + impl From> for NeverLeaves { fn from(_: Range) -> Self { unimplemented!("This data type is not supported") } } - impl MaybeChildrenRange for NeverChildren { - fn own_range(&self) -> Option<&Range> { + impl OptPromiseInfo for NeverLeaves + where + NeverLeaves: From, + { + fn info(&self) -> Option<&T> { unimplemented!("This data type is not supported") } } #[derive(Debug, derive_more::From)] pub struct TextRef<'text>(&'text str); - impl<'text> MaybeTextRef<'text> for TextRef<'text> { - fn text_ref(&self) -> Option<&'text str> { - Some(self.0) + impl<'text, StorageType> OptPromiseInfo<&'text str, StorageType> for TextRef<'text> { + fn info(&self) -> Option<&&'text str> { + Some(&self.0) } } + #[derive(Debug, derive_more::From)] pub struct TextOwn(Range); - impl MaybeTextOwnRange for TextOwn { - fn own_range(&self) -> Option<&Range> { + impl OptPromiseInfo, StorageType> for TextOwn { + fn info(&self) -> Option<&Range> { Some(&self.0) } } @@ -1015,8 +1076,8 @@ mod borrowing_concept { Ref(&'text str), Own(Range), } - impl<'text> MaybeTextRef<'text> for TextMaybeOwn<'text> { - fn text_ref(&self) -> Option<&'text str> { + impl<'text, StorageType> OptPromiseInfo<&'text str, StorageType> for TextMaybeOwn<'text> { + fn info(&self) -> Option<&&'text str> { if let Self::Ref(s) = self { Some(s) } else { @@ -1024,8 +1085,8 @@ mod borrowing_concept { } } } - impl MaybeTextOwnRange for TextMaybeOwn<'_> { - fn own_range(&self) -> Option<&Range> { + impl OptPromiseInfo, StorageType> for TextMaybeOwn<'_> { + fn info(&self) -> Option<&Range> { if let Self::Own(range) = self { Some(range) } else { @@ -1035,106 +1096,107 @@ mod borrowing_concept { } #[derive(Debug, derive_more::From)] - pub struct ChildInd(usize); - impl MaybeChildInd for ChildInd { - fn own_ind(&self) -> Option { - Some(self.0) + pub struct LeafInd(usize); + impl OptPromiseInfo for LeafInd { + fn info(&self) -> Option<&usize> { + Some(&self.0) } } + #[derive(Debug, derive_more::From)] - pub struct ChildrenRange(Range); - impl MaybeChildrenRange for ChildrenRange { - fn own_range(&self) -> Option<&Range> { + pub struct LeavesRange(Range); + impl OptPromiseInfo, StorageType> for LeavesRange { + fn info(&self) -> Option<&Range> { Some(&self.0) } } /// Provider that holds nothing and panics at any call to it. - struct UnbakedBottomProvider( - PhantomData<(Text, Child, Children)>, + struct UnsealedBottomStorage( + PhantomData<(Text, Leaf, Leaves)>, ); - impl UnbakedProvider - for UnbakedBottomProvider + impl UnsealedStorage + for UnsealedBottomStorage { - type Baked = BakedBottomProvider; + type Sealed = BottomStorage; fn register_text(&mut self, _: &str) -> Text { unimplemented!("This provider does not support text saving") } - fn register_child(&mut self, _: Thing) -> Child { + fn register_leaf(&mut self, _: Thing) -> Leaf { unimplemented!("This provider does not support child saving") } - fn register_children<'s>( + fn register_leaves<'s>( &'s mut self, - _: impl Iterator>, - ) -> Children { + _: impl Iterator>, + ) -> Leaves { unimplemented!("This provider does not support children saving") } - fn bake(self) -> Self::Baked { - BakedBottomProvider(PhantomData) + fn seal(self) -> Self::Sealed { + BottomStorage(PhantomData) } } - impl UnbakedBottomProvider { + impl UnsealedBottomStorage { fn new() -> Self { - UnbakedBottomProvider(PhantomData) + UnsealedBottomStorage(PhantomData) } } - struct BakedBottomProvider( - PhantomData<(Text, Child, Children)>, + struct BottomStorage( + PhantomData<(Text, Leaf, Leaves)>, ); - impl BakedProvider for BakedBottomProvider { + impl SealedStorage for BottomStorage { fn get_text<'s>(&'s self, _: &Text) -> &'s str { unimplemented!("This provider does not provide text") } - fn get_child<'s>(&'s self, _: &Child) -> &'s Thing { + fn get_leaf<'s>(&'s self, _: &Leaf) -> &'s Thing { unimplemented!("This provider does not provide children") } - fn get_children<'s>(&'s self, _: &Children) -> &'s [Thing] { + fn get_leaves<'s>(&'s self, _: &Leaves) -> &'s [Thing] { unimplemented!("This provider does not provide children") } } pub fn text_ref_provider<'text>( text: &'text str, - ) -> impl UnbakedProvider, ChildInd, ChildrenRange> { - UnbakedBottomProvider::new() + ) -> impl UnsealedStorage, LeafInd, LeavesRange> { + UnsealedBottomStorage::new() .with_text_ref(text) - .things_owned() + .leaves_owned() } - pub fn text_own_provider() -> impl UnbakedProvider { - UnbakedBottomProvider::new().text_owned().things_owned() + pub fn text_own_provider() -> impl UnsealedStorage { + UnsealedBottomStorage::new().text_owned().leaves_owned() } pub fn text_maybe_own_provider<'text>( ref_text: &'text str, - ) -> impl UnbakedProvider, ChildInd, ChildrenRange> { - UnbakedBottomProvider::new() + ) -> impl UnsealedStorage, LeafInd, LeavesRange> { + UnsealedBottomStorage::new() .text_owned() .with_text_ref(ref_text) - .things_owned() + .leaves_owned() } pub fn text_maybe_own_find_provider<'text>( ref_text: &'text str, own_text: impl Into, - ) -> impl UnbakedProvider, ChildInd, ChildrenRange> { - UnbakedBottomProvider::new() + ) -> impl UnsealedStorage, LeafInd, LeavesRange> { + UnsealedBottomStorage::new() .with_text_find(own_text) .with_text_ref(ref_text) - .things_owned() + .leaves_owned() } pub fn text_own_find_provider( own_text: impl Into, - ) -> impl UnbakedProvider { - UnbakedBottomProvider::new() + ) -> impl UnsealedStorage { + UnsealedBottomStorage::new() .with_text_find(own_text) - .things_owned() + .leaves_owned() } fn transfer_nodes< @@ -1153,8 +1215,8 @@ mod borrowing_concept { buf: &mut Vec>, ) -> Thing where - FromProvider: BakedProvider, - ToProvider: UnbakedProvider, + FromProvider: SealedStorage, + ToProvider: UnsealedStorage, { match root { Thing::Text { text } => { @@ -1169,12 +1231,12 @@ mod borrowing_concept { } => { let s = from_provider.get_text(text); let text = to_provider.register_text(s); - let ch1 = from_provider.get_child(child1); + let ch1 = from_provider.get_leaf(child1); let ch1 = transfer_nodes(ch1, from_provider, to_provider, buf); - let child1 = to_provider.register_child(ch1); - let ch2 = from_provider.get_child(child2); + let child1 = to_provider.register_leaf(ch1); + let ch2 = from_provider.get_leaf(child2); let ch2 = transfer_nodes(ch2, from_provider, to_provider, buf); - let child2 = to_provider.register_child(ch2); + let child2 = to_provider.register_leaf(ch2); Thing::Child { text, child1, @@ -1182,13 +1244,13 @@ mod borrowing_concept { } } Thing::Multiple { children } => { - let ch = from_provider.get_children(children); + let ch = from_provider.get_leaves(children); let start_ind = buf.len(); for child in ch { let to_child = transfer_nodes(child, from_provider, to_provider, buf); buf.push(to_child); } - let children = to_provider.register_children(buf.drain(start_ind..)); + let children = to_provider.register_leaves(buf.drain(start_ind..)); Thing::Multiple { children } } } @@ -1197,7 +1259,7 @@ mod borrowing_concept { struct UnbakedFullOwner<'text> { yet_unowned_text: Cow<'text, str>, owned_text: String, - children: Vec>, + children: Vec>, } impl<'text> UnbakedFullOwner<'text> { @@ -1228,8 +1290,8 @@ mod borrowing_concept { } } - impl<'text> UnbakedProvider for UnbakedFullOwner<'text> { - type Baked = FullOwner; + impl<'text> UnsealedStorage for UnbakedFullOwner<'text> { + type Sealed = FullOwner; fn register_text(&mut self, text: &str) -> TextOwn { if let Some(range) = try_find_pointer_match(&self.yet_unowned_text, text) { @@ -1249,23 +1311,23 @@ mod borrowing_concept { TextOwn(start_ind..end_ind) } - fn register_child(&mut self, child: Thing) -> ChildInd { + fn register_leaf(&mut self, child: Thing) -> LeafInd { let ind = self.children.len(); self.children.push(child); - ChildInd(ind) + LeafInd(ind) } - fn register_children<'s>( + fn register_leaves<'s>( &'s mut self, - children: impl Iterator>, - ) -> ChildrenRange { + children: impl Iterator>, + ) -> LeavesRange { let start_ind = self.children.len(); self.children.extend(children); let end_ind = self.children.len(); - ChildrenRange(start_ind..end_ind) + LeavesRange(start_ind..end_ind) } - fn bake(self) -> Self::Baked { + fn seal(self) -> Self::Sealed { let Cow::Owned(s) = self.yet_unowned_text else { panic!("Full Owner cannot be baked until data is provided to it") }; @@ -1280,10 +1342,10 @@ mod borrowing_concept { struct FullOwner { now_owned_text: Box, owned_text: Box, - children: Box<[Thing]>, + children: Box<[Thing]>, } - impl BakedProvider for FullOwner { + impl SealedStorage for FullOwner { fn get_text<'s>(&'s self, TextOwn(range): &TextOwn) -> &'s str { let (start, end) = (range.start, range.end); let len = self.now_owned_text.len(); @@ -1293,17 +1355,17 @@ mod borrowing_concept { &self.now_owned_text[start..end] } - fn get_child<'s>( + fn get_leaf<'s>( &'s self, - &ChildInd(i): &ChildInd, - ) -> &'s Thing { + &LeafInd(i): &LeafInd, + ) -> &'s Thing { &self.children[i] } - fn get_children<'s>( + fn get_leaves<'s>( &'s self, - ChildrenRange(range): &ChildrenRange, - ) -> &'s [Thing] { + LeavesRange(range): &LeavesRange, + ) -> &'s [Thing] { &self.children[range.clone()] } } @@ -1360,7 +1422,7 @@ mod borrowing_concept { Text, Child, Children, - P: UnbakedProvider, + P: UnsealedStorage, S: AsRef, >( provider: &mut P, @@ -1385,8 +1447,8 @@ mod borrowing_concept { let child2 = generate_thing(provider, rng, text_gen, children_buf, nodes_left); Thing::Child { text: provider.register_text(text.as_ref()), - child1: provider.register_child(child1), - child2: provider.register_child(child2), + child1: provider.register_leaf(child1), + child2: provider.register_leaf(child2), } } else { // rest 4% own 1-5 children @@ -1395,7 +1457,7 @@ mod borrowing_concept { let child = generate_thing(provider, rng, text_gen, children_buf, nodes_left); children_buf.push(child); } - let children = provider.register_children(children_buf.drain(buf_start..)); + let children = provider.register_leaves(children_buf.drain(buf_start..)); Thing::Multiple { children } }; // average coefficient is 0.6 * 0 + 0.36 * 2 + 2.5 * 0.04 = 0.82 @@ -1403,10 +1465,10 @@ mod borrowing_concept { generated } - use crate::data::borrowing_concept::{UnbakedFullOwner, UnbakedProvider}; + use crate::data::borrowing_concept::{UnbakedFullOwner, UnsealedStorage}; use super::{ - text_maybe_own_provider, text_own_provider, text_ref_provider, transfer_nodes, Thing, + text_maybe_own_provider, text_own_provider, text_ref_provider, Thing, ThingTree, }; // TODO check these versions of fuzzes to actually run correctly @@ -1426,12 +1488,11 @@ mod borrowing_concept { &mut Vec::new(), &mut 100, ); - let provider = provider.bake(); + let tree = ThingTree::new(thing, provider); // assert let mut output = String::new(); - thing - .write_to(&provider, &mut output) + tree.write_to(&mut output) .expect("Should be able to write into string"); } } @@ -1445,12 +1506,11 @@ mod borrowing_concept { // act let thing = generate_thing(&mut provider, rng, &mut gen_text, &mut Vec::new(), &mut 100); - let provider = provider.bake(); + let tree = ThingTree::new(thing, provider); // assert let mut output = String::new(); - thing - .write_to(&provider, &mut output) + tree.write_to(&mut output) .expect("Should be able to write into string"); } } @@ -1471,12 +1531,11 @@ mod borrowing_concept { &mut Vec::new(), &mut 100, ); - let provider = provider.bake(); + let tree = ThingTree::new(thing, provider); // assert let mut output = String::new(); - thing - .write_to(&provider, &mut output) + tree.write_to(&mut output) .expect("Should be able to write into string"); } } @@ -1495,26 +1554,21 @@ mod borrowing_concept { &mut Vec::new(), &mut 100, ); - let original_provider = provider.bake(); + let original_tree = ThingTree::new(thing, provider); // act let mut moved_provider = text_own_provider(); - let moved_thing = transfer_nodes( - &thing, - &original_provider, - &mut moved_provider, - &mut Vec::new(), - ); - let moved_provider = moved_provider.bake(); + let moved_thing = original_tree.move_to(&mut moved_provider); + let moved_tree = ThingTree::new(moved_thing, moved_provider); // assert let mut original = String::new(); - thing - .write_to(&original_provider, &mut original) + original_tree + .write_to(&mut original) .expect("Should be able to write into string"); let mut moved = String::new(); - moved_thing - .write_to(&moved_provider, &mut moved) + moved_tree + .write_to(&mut moved) .expect("Should be able to write into string"); assert_eq!( original, moved, @@ -1540,36 +1594,30 @@ mod borrowing_concept { &mut Vec::new(), &mut 100, ); - let original_provider = provider.bake(); + let original_tree = ThingTree::new(thing, provider); // act let mut owning_provider = UnbakedFullOwner::new(&text); - let owning_thing = transfer_nodes( - &thing, - &original_provider, - &mut owning_provider, - &mut Vec::new(), - ); + let owning_thing = original_tree.move_to(&mut owning_provider); let mut original = String::new(); - thing - .write_to(&original_provider, &mut original) + original_tree + .write_to(&mut original) .expect("Should be able to write into string"); + drop(original_tree); // SAFETY: // data provided and data `OwnedProvider` referred to here is the same, // as it's contained in the same immutable variable let owning_provider = unsafe { let mut owning_provider = owning_provider.drop_unowned(); - drop(original_provider); owning_provider.provide(text); owning_provider }; - - let owning_provider = owning_provider.bake(); + let owning_tree = ThingTree::new(owning_thing, owning_provider); // assert let mut owned = String::new(); - owning_thing - .write_to(&owning_provider, &mut owned) + owning_tree + .write_to(&mut owned) .expect("Should be able to write into string"); assert_eq!( original, owned, From 511f32147660b1209aafd9da9cc4848592c3cc7f Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Mon, 25 Mar 2024 14:00:07 +0200 Subject: [PATCH 35/98] Finished storage concept. Next step - integration into the actual codebase --- src/data.rs | 120 +++++++++++++++------------------------------------- 1 file changed, 34 insertions(+), 86 deletions(-) diff --git a/src/data.rs b/src/data.rs index 1d80f95..700fba9 100644 --- a/src/data.rs +++ b/src/data.rs @@ -417,78 +417,26 @@ impl<'source> Token<'source> { } } -/// This module contains, documents and helps developing my concept for refined token borrowing. +/// # On token storage /// -/// Main idea is to ALWAYS delegate token information storage. This is expected to minimize token size. -/// Problem with that idea - some tokens need to own their data, they need to be independent and live indefinitely. +/// Basically, tokens are not manipulated directly; Instead, they are aggregated into so-called "trees" to allow efficient data storage. /// -/// To allow this, there would be a special token type - so-called `Owner` - holding text buffer for all their children to interpret and refer to. -/// A now-implemented with cloning `to_static` method would instead wrap token(s) into a new `Owner`, allowing them to exist indefinitely, -/// while in fact only cheap pointers were substituted. +/// Tree consists of storage and a root, "pointing" somewhere inside it. /// -/// The next step would be to realize that tokens don't even need to own their children - instead upper-standing owned could own them instead, -/// providing references to children tokens. This should **DRAMATICALLY** decrease number of heap allocations, as it would allow tokens for centralized data storage, -/// allocating only two memory arrays. +/// To create a tree, `Unsealed Storage` must be created first. While storage is unsealed, one can add a text, a token or a token slice (iterator of tokens) +/// for it to store. Once data was stored, `Unsealed Storage` will return a `Promise` to said data. Exact form of this `Promise` is up to storage to +/// decide, and that's exactly what allows token tree to only store stuff it needs, and the way it needs. /// -/// Now, here's a thing - vectors can be re-allocated in-memory, when their size reached their capacity. This presents another challenge: we should somehow guarantee, -/// that data referred by child tokens will not be moved out of the pointers (compiler will **force** us to do that, actually). I see two possible solutions on that matter: +/// Once all tokens were added to `Unsealed Storage`, it can be transformed into a regular `Storage`. `Storage` allows to see text and token(s) with +/// the promises returned by `Unsealed Storage`. /// -/// - while token tree creation, `Owner` holds data in a regular vector, giving children only a *promise* to data (not a pointer). -/// once generation is complete, token tree is not supposed to be changed. at this point, `Owner` will allow children to *obtain* the pointers to now-immovable data. -/// - same as before, but children never get to see pointers themselves. this will allow `Owner` to freely mutate data, but will add an extra cost for some sort of -/// internal identifier resolution and a couple more function calls. +/// ## Capabilities +/// If you happen to know, that all of your data is contained within a certain string, you may pass a reference to it, for storage to use. +/// At any time, if you decide to transfer your data from one storage to another - you can absolutely do it via dedicated method. +/// This also covers situations where you don't wanna hold onto some reference, and decide to copy over all of the data /// -/// ~~Being a masochist I am, of course I went on with the first option. Following is a log of the development~~ -/// -/// # LOG -/// -/// After brief prototyping, this looks really promising; New tokens type's size is only 40 (5 bytes). Paired with TWO allocations per tree this should be insane. -/// -/// When I revised once again my design, It seems that there's no reason to make `Owner` a variant of token. In fact, we can call it a `TokenOwner` and call it a day. -/// Any place concerned about token's life would accept `TokenOwner`. -/// -/// Since `TokenOwner` will own EVERYTHING about `Token`s inside it, ~~there's no need for lifetimes on it.~~ -/// it's lifetime should not be used in any way, it's just a way to avoid `'static` lifetime, which can be used here, since, -/// IN FACT, data DOES NOT live to the end of the program. I would *probably ok* to use them, but I won't do that. -/// -/// Seems like there's no crate achieving exactly what I want, meaning I'll have to result to **unsafe** myself. -/// -/// New idiom: `Token` can't exist int a vacuum anymore, it's an internal implementation of `TokenTree` -/// -/// `Owner` was renamed into `Provider`. Now inner token and text storage is generalized by a trait; -/// `Provider` can decide themselves, if they want to actually store the data, or keep as a reference to th. -/// -/// There are now three traits related to provider concept: -/// - `UnbackedProvider` represents a provider that's ready to accept new things to be stored. -/// - `BackedProvider` represents a provider that's no longer able to accept new values, but instead can provide references to them -/// - `FixedProvider` represents a provider that's no longer able nor add nor provide values. -/// It can be as minified as possible, only storing data without any sort of bookkeeping. -/// -/// Despite me being a masochist, I really do fear using `unsafe` in my code. There's just too many things to keep track of. -/// So after writing the entire module using self-borrowing stuff and `unsafe`, I'll resort to second option stated above: basically, ever-unbaked children. -/// -/// A format of so-called "promise" that provider uses to return actual child/text can be different and it ultimately of to provider to decide: -/// - the simplest would be a provider that owns all of the data tokens refer to -/// - a provider could be relaying calls to some other provider to take care of -/// - a provider could refer to some plain text buffer, like &str to recognize and store it's data -/// -/// All of that means, that both tokens and providers must be defined at generic over promise types. -/// This will allow provider to select in which way they would store and manipulate data. -/// -/// This approach is less optimized then the `unsafe` one, but `miri` scared me away from pointer manipulation, so here I am, I guess -/// -/// I see now, that providers can be composed into each other to build desired behavior step-by-step. That's really desireable too. -/// -/// All of the wrapper providers were implemented, along with a special `FullOwning`-provider to allow text ownership transfer. -/// All the basic functions are fuzz-tested now. -/// -/// The main motivation behind all of this mess is to "only pay for what you need" - tokens may or may not own their data, -/// but if you happen to know that for sure, you'll end up using much less memory, as their size will not include any sort of discriminator. -/// -/// Also, you have the opportunity to actually convert any provider referring to data into fully-owning provider, if you happen to own data they refer -/// to or a copy of the data they refer to. That's an unsafe process, unfortunately, as provider has no way to know if you give it the correct data. -/// -/// Almost everything in the module is getting renamed to resemble more neutral "tree thing" +/// There's also an ability to move the data into the storage, which can be useful for large text buffers. +/// Please note that this last operation is hard to define properly, so some of the methods used for it, are marked `unsafe` (since they break the invariant) #[cfg(test)] mod borrowing_concept { use std::{borrow::Cow, marker::PhantomData, ops::Range}; @@ -1256,30 +1204,30 @@ mod borrowing_concept { } } - struct UnbakedFullOwner<'text> { + struct UnsealedFullOwner<'text> { yet_unowned_text: Cow<'text, str>, owned_text: String, - children: Vec>, + things: Vec>, } - impl<'text> UnbakedFullOwner<'text> { + impl<'text> UnsealedFullOwner<'text> { pub fn new(yet_unowned: &'text str) -> Self { Self { yet_unowned_text: yet_unowned.into(), owned_text: String::new(), - children: Vec::new(), + things: Vec::new(), } } - pub unsafe fn drop_unowned(self) -> UnbakedFullOwner<'static> { + pub unsafe fn drop_unowned(self) -> UnsealedFullOwner<'static> { let Self { owned_text, - children, + things: children, .. } = self; - UnbakedFullOwner { + UnsealedFullOwner { yet_unowned_text: "".into(), owned_text, - children, + things: children, } } // TODO add panic info @@ -1290,7 +1238,7 @@ mod borrowing_concept { } } - impl<'text> UnsealedStorage for UnbakedFullOwner<'text> { + impl<'text> UnsealedStorage for UnsealedFullOwner<'text> { type Sealed = FullOwner; fn register_text(&mut self, text: &str) -> TextOwn { @@ -1312,8 +1260,8 @@ mod borrowing_concept { } fn register_leaf(&mut self, child: Thing) -> LeafInd { - let ind = self.children.len(); - self.children.push(child); + let ind = self.things.len(); + self.things.push(child); LeafInd(ind) } @@ -1321,9 +1269,9 @@ mod borrowing_concept { &'s mut self, children: impl Iterator>, ) -> LeavesRange { - let start_ind = self.children.len(); - self.children.extend(children); - let end_ind = self.children.len(); + let start_ind = self.things.len(); + self.things.extend(children); + let end_ind = self.things.len(); LeavesRange(start_ind..end_ind) } @@ -1334,7 +1282,7 @@ mod borrowing_concept { FullOwner { now_owned_text: s.into_boxed_str(), owned_text: self.owned_text.into_boxed_str(), - children: self.children.into_boxed_slice(), + things: self.things.into_boxed_slice(), } } } @@ -1342,7 +1290,7 @@ mod borrowing_concept { struct FullOwner { now_owned_text: Box, owned_text: Box, - children: Box<[Thing]>, + things: Box<[Thing]>, } impl SealedStorage for FullOwner { @@ -1359,14 +1307,14 @@ mod borrowing_concept { &'s self, &LeafInd(i): &LeafInd, ) -> &'s Thing { - &self.children[i] + &self.things[i] } fn get_leaves<'s>( &'s self, LeavesRange(range): &LeavesRange, ) -> &'s [Thing] { - &self.children[range.clone()] + &self.things[range.clone()] } } @@ -1465,7 +1413,7 @@ mod borrowing_concept { generated } - use crate::data::borrowing_concept::{UnbakedFullOwner, UnsealedStorage}; + use crate::data::borrowing_concept::{UnsealedFullOwner, UnsealedStorage}; use super::{ text_maybe_own_provider, text_own_provider, text_ref_provider, Thing, ThingTree, @@ -1597,7 +1545,7 @@ mod borrowing_concept { let original_tree = ThingTree::new(thing, provider); // act - let mut owning_provider = UnbakedFullOwner::new(&text); + let mut owning_provider = UnsealedFullOwner::new(&text); let owning_thing = original_tree.move_to(&mut owning_provider); let mut original = String::new(); original_tree From 129873a9e5002755b688e5fc32599c317d2b3350 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Mon, 25 Mar 2024 16:32:36 +0200 Subject: [PATCH 36/98] Added double-quotes escaping in captions --- src/data.rs | 2 +- src/gen/lab/context.rs | 1 + src/gen/lab/mod.rs | 52 ++++++++++++++++++++++++- tests/transformed_files/tables_code.txt | 2 +- 4 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/data.rs b/src/data.rs index 700fba9..d7fed91 100644 --- a/src/data.rs +++ b/src/data.rs @@ -437,7 +437,7 @@ impl<'source> Token<'source> { /// /// There's also an ability to move the data into the storage, which can be useful for large text buffers. /// Please note that this last operation is hard to define properly, so some of the methods used for it, are marked `unsafe` (since they break the invariant) -#[cfg(test)] +#[cfg(borrowing_concept)] mod borrowing_concept { use std::{borrow::Cow, marker::PhantomData, ops::Range}; diff --git a/src/gen/lab/context.rs b/src/gen/lab/context.rs index 216a58a..86d2a20 100644 --- a/src/gen/lab/context.rs +++ b/src/gen/lab/context.rs @@ -3,4 +3,5 @@ use std::collections::HashMap; #[derive(Debug, Default)] pub struct Context { pub encountered_footnotes: HashMap, + pub inside_caption: bool, } diff --git a/src/gen/lab/mod.rs b/src/gen/lab/mod.rs index b941007..1209cc3 100644 --- a/src/gen/lab/mod.rs +++ b/src/gen/lab/mod.rs @@ -132,7 +132,9 @@ where output.write_str("\\end{tabular}\n\\stepcounter{tabnum}")?; if let Some(caption) = caption { output.write_str("\n\\caption{\\capfnt ")?; + context.inside_caption = true; self.write_to(output, meta, context, caption)?; + context.inside_caption = false; output.write_str("}\n")?; } if let Some(ident) = ident.as_ref() { @@ -161,7 +163,9 @@ where output.write_str("}\n")?; if let Some(caption) = caption.as_ref() { output.write_str("\\caption{\\capfnt ")?; + context.inside_caption = true; self.write_to(output, meta, context, caption)?; + context.inside_caption = false; output.write_str("}\n")?; } if let Some(ident) = ident.as_ref() { @@ -262,7 +266,19 @@ where Some(Formatting::StrikeThrough) => ("\\st{", "}"), }; output.write_str(start)?; - output.write_str(&content)?; + if context.inside_caption { + // We are generating text that was inside table/figure caption + // meaning, we must un-escape double quotes + let mut last_ind = 0; + for (ind, _) in content.match_indices("\\\"") { + output.write_str(&content[last_ind..ind])?; + output.write_char('\"')?; + last_ind = ind + 2; + } + output.write_str(&content[last_ind..])?; + } else { + output.write_str(&content)?; + } output.write_str(end)?; } Token::Href { url, display } => { @@ -415,6 +431,13 @@ mod tests { \label{fig:schema-1} \end{figure} "} + test! {fig4_width, Token::Figure { src_name: Path::new("schema1.jpg").into(), caption: Some(Box::new(text!("This caption contains \\\"double quotes\\\""))), ident: None, width: None }, r#" + \begin{figure}[h!] + \centering + \includegraphics[width = 0.9 \textwidth]{schema1.jpg} + \caption{\capfnt This caption contains "double quotes"} + \end{figure} + "#} // Tables test! {tab1, Token::Table { header: @@ -441,6 +464,31 @@ cell_21 & cell_22 & cell_23 \\ \hline \end{center} \end{table} "} + test! {tab_quotes, Token::Table { + header: + tokens![ + text!("header1"), text!("header2"), text!("header3") + ], + cells: tokens![ + text!("cell_11"),text!("cell_12"),text!("cell_13"), + text!("cell_21"),text!("cell_22"),text!("cell_23") + ], + caption: Some(Box::new(text!("We like \\\"math\\\" so much!"))), + ident: Some("example".into()) }, + r#" + \begin{table}[h!] + \begin{center} + \begin{tabular}{|c|c|c|}\hline +header1 & header2 & header3 \\ \hline \hline +cell_11 & cell_12 & cell_13 \\ \hline +cell_21 & cell_22 & cell_23 \\ \hline + \end{tabular} + \stepcounter{tabnum} + \caption{ \capfnt We like "math" so much!} + \label{tab:example} + \end{center} + \end{table} + "#} // Lists test! {list_bullets, Token::List { list_type: ListType::Bullet, content: tokens![ @@ -642,7 +690,7 @@ r#" \end{table} "# } - // TODO add tests with captions and csv-tables + // TODO add tests with csv-tables // TODO add minted to preamble // TODO add -shell-escape to script util diff --git a/tests/transformed_files/tables_code.txt b/tests/transformed_files/tables_code.txt index 69bff4d..d761e97 100644 --- a/tests/transformed_files/tables_code.txt +++ b/tests/transformed_files/tables_code.txt @@ -11,7 +11,7 @@ Storage & Space & Number\\ \hline $e$\footnotemark[1]\footnotetext[1]{Це число Ейлера. Воно дуже корисне,\st{але не може бути записане}у вигляді\textit{десяткового}дробу.} & $\cdot \pm 1$ & І тут ще\textit{трохи}тексту \|\\ \hline \end{tabular} \stepcounter{tabnum} -\caption{\capfnt Оце приклад таблиці. Як казав мій вчитель:\textbf{ВИКОРИСТОВУЙ БІЛЬШЕ ВИБУХІВКИ}(і \"лапок\")} +\caption{\capfnt Оце приклад таблиці. Як казав мій вчитель:\textbf{ВИКОРИСТОВУЙ БІЛЬШЕ ВИБУХІВКИ}(і "лапок")} \label{tab:example_1} \end{center} \end{table} From 5ebd970229c8c586ed3685b7128f27aef81fcbed Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Mon, 25 Mar 2024 20:29:36 +0200 Subject: [PATCH 37/98] sync commit (sync along machines; nothing to see here) --- src/data.rs | 63 ++++++++++++++++++++--- src/gen/ayano.rs | 19 +++++-- src/gen/lab/mod.rs | 103 +++++++++++++++++++++++++----------- src/lexer.rs | 126 +++++++++++++++++++++++++++++---------------- 4 files changed, 225 insertions(+), 86 deletions(-) diff --git a/src/data.rs b/src/data.rs index d7fed91..d16dda0 100644 --- a/src/data.rs +++ b/src/data.rs @@ -66,6 +66,7 @@ impl<'source> Tokens<'source> { // #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct AyanoBlock<'code> { pub is_display: bool, + pub is_space_before: bool, pub is_static: bool, pub code: Tx<'code>, pub insert_path: Option>, @@ -132,6 +133,7 @@ pub enum Token<'source> { width: Option, }, Href { + space_before: bool, // #[cfg_attr(feature = "serde", serde(with = "url_serde"))] url: Url, display: Tx<'source>, @@ -150,16 +152,20 @@ pub enum Token<'source> { // TODO add a flag to indicate, if this paragraph contains escaped chars (if it was parsed from caption, basically) Paragraph { is_newline: bool, + space_before: bool, formatting: Option, content: Tx<'source>, }, InlineMath { + space_before: bool, content: Tx<'source>, }, Reference { + space_before: bool, ident: Tx<'source>, }, FootnoteReference { + space_before: bool, ident: Tx<'source>, }, FootnoteContent { @@ -235,12 +241,14 @@ impl ToStaticExt for AyanoBlock<'_> { insert_path, is_display, is_static, + is_space_before, } = self; AyanoBlock { code: code.to_static(), insert_path: insert_path.as_ref().map(Cow::to_static), is_static: *is_static, is_display: *is_display, + is_space_before: *is_space_before, } } } @@ -248,6 +256,7 @@ impl ToStaticExt for AyanoBlock<'_> { impl<'source> Token<'source> { pub fn text(text: impl Into>) -> Self { Self::Paragraph { + space_before: false, is_newline: false, content: text.into(), formatting: None, @@ -290,9 +299,14 @@ impl<'source> Token<'source> { ident: ident.as_ref().map(|s| Cow::<'r, str>::Borrowed(s)), width: *width, }, - Token::Href { url, display } => Token::Href { + Token::Href { + url, + display, + space_before, + } => Token::Href { url: url.clone(), display: Cow::Borrowed(&display), + space_before: *space_before, }, Token::Ayano { data } => Token::Ayano { data: data.clone() }, Token::List { list_type, content } => Token::List { @@ -306,19 +320,33 @@ impl<'source> Token<'source> { is_newline, formatting, content, + space_before, } => Token::Paragraph { is_newline: *is_newline, formatting: formatting.clone(), content: Cow::Borrowed(content), + space_before: *space_before, }, - Token::InlineMath { content } => Token::InlineMath { + Token::InlineMath { + content, + space_before, + } => Token::InlineMath { content: Cow::Borrowed(&content), + space_before: *space_before, }, - Token::Reference { ident } => Token::Reference { + Token::Reference { + ident, + space_before, + } => Token::Reference { ident: Cow::Borrowed(&ident), + space_before: *space_before, }, - Token::FootnoteReference { ident } => Token::FootnoteReference { + Token::FootnoteReference { + ident, + space_before, + } => Token::FootnoteReference { ident: Cow::Borrowed(ident), + space_before: *space_before, }, Token::FootnoteContent { content, ident } => Token::FootnoteContent { content: Box::new(content.borrow_ref()), @@ -370,9 +398,14 @@ impl<'source> Token<'source> { ident: ident.as_ref().map(Cow::to_static), width: *width, }, - Token::Href { url, display } => Token::Href { + Token::Href { + url, + display, + space_before, + } => Token::Href { url: url.clone(), display: display.to_static(), + space_before: *space_before, }, Token::Ayano { data } => Token::Ayano { data: data.to_static(), @@ -386,21 +419,35 @@ impl<'source> Token<'source> { }, Token::Paragraph { is_newline, + space_before, formatting, content, } => Token::Paragraph { is_newline: *is_newline, formatting: formatting.clone(), content: content.to_static(), + space_before: *space_before, }, - Token::InlineMath { content } => Token::InlineMath { + Token::InlineMath { + content, + space_before, + } => Token::InlineMath { content: content.to_static(), + space_before: *space_before, }, - Token::Reference { ident } => Token::Reference { + Token::Reference { + ident, + space_before, + } => Token::Reference { ident: ident.to_static(), + space_before: *space_before, }, - Token::FootnoteReference { ident } => Token::FootnoteReference { + Token::FootnoteReference { + ident, + space_before, + } => Token::FootnoteReference { ident: ident.to_static(), + space_before: *space_before, }, Token::FootnoteContent { content, ident } => Token::FootnoteContent { content: content.to_static(), diff --git a/src/gen/ayano.rs b/src/gen/ayano.rs index 5e8cc28..f499209 100644 --- a/src/gen/ayano.rs +++ b/src/gen/ayano.rs @@ -185,6 +185,7 @@ mod ayano_tests { is_static: false, code: $code.into(), insert_path: None, + is_space_before: false, }; let mut builder = AyanoBuilder::new(); @@ -231,12 +232,14 @@ y = x / 100 insert_path: None, is_display: false, is_static: true, + is_space_before: false, }; let function_block = AyanoBlock { code: $function_code.into(), insert_path: None, is_display: false, is_static: false, + is_space_before: false, }; builder.add_block(&static_block).expect("Should be able to add static block"); builder.add_block(&function_block).expect("Should be able to add function block"); @@ -273,18 +276,21 @@ y", "2"} insert_path: None, is_display: false, is_static: true, + is_space_before: false, }; let function1_block = AyanoBlock { code: $function1_code.into(), insert_path: None, is_display: false, is_static: false, + is_space_before: false, }; let function2_block = AyanoBlock { code: $function2_code.into(), insert_path: None, is_display: false, is_static: false, + is_space_before: false, }; builder.add_block(&static_block).expect("Should be able to add static block"); builder.add_block(&function1_block).expect("Should be able to add function block"); @@ -480,6 +486,7 @@ mod apply_ayano_tests { insert_path: None, is_display: false, is_static: false, + is_space_before: false, }; // act @@ -578,6 +585,7 @@ y = x + 2 code: code.into(), insert_path: Some(insert_path.into()), is_static: false, + is_space_before: false, }; // act @@ -1558,6 +1566,7 @@ fn parse_ayano(python_output: &PyAny) -> Result, ParsingError> { Ok(Token::Paragraph { is_newline: false, formatting: None, + space_before: todo!(), content: Cow::Owned(python_output.to_string()), }) } @@ -1569,6 +1578,7 @@ fn value_error_token(value: f64, error: f64) -> Token<'static> { Token::text(value.to_string()), Token::InlineMath { content: r"\pm".into(), + space_before: false, }, Token::text(error.to_string()), ]), @@ -1619,9 +1629,9 @@ mod parsing_tests { test! {err1, |py| PyTuple::new(py, ["err".into_py(py), ["yet another not a number"].into_py(py)]), Err(crate::gen::ayano::ParsingError::UnexpectedSyntax("('err', ['yet another not a number'])".to_string(), py_err!(py)))} test! {value_error1, |py| PyTuple::new(py, ["err".into_py(py), 1.0.into_py(py), 0.5.into_py(py)]), - Ok(Token::Multiple{tokens: Tokens::new([Token::text("1.0"), Token::InlineMath{content:r"\pm".into()}, Token::text("0.5")])})} + Ok(Token::Multiple{tokens: Tokens::new([Token::text("1.0"), Token::InlineMath{content:r"\pm".into(), space_before: false}, Token::text("0.5")])})} test! {value_error2, |py| PyTuple::new(py, ["err".into_py(py), "1.0".into_py(py), 0.5.into_py(py)]), - Ok(Token::Multiple{tokens:Tokens::new([Token::text("1.0"), Token::InlineMath{content:r"\pm".into()}, Token::text("0.5")])})} + Ok(Token::Multiple{tokens:Tokens::new([Token::text("1.0"), Token::InlineMath{content:r"\pm".into(), space_before: false}, Token::text("0.5")])})} test! {value_error_format1, |py| PyTuple::new(py, ["err".into_py(py), "1.987654321".into_py(py), 0.00000123456.into_py(py)]), Ok(value_error_token(1.9876543, 0.0000012))} // TODO probably should add some more tests for value-error formatting @@ -1711,7 +1721,7 @@ mod parsing_tests { Token::text("cell_11"), Token::text("0.25"), Token::text("cell_21"), Token::text("-2"), ]), - caption: Some(Box::new(Token::Multiple{tokens:Tokens::new([Token::text("table caption with some"),Token::InlineMath{content:"formula".into()}])})), + caption: Some(Box::new(Token::Multiple{tokens:Tokens::new([Token::text("table caption with some"),Token::InlineMath{content:"formula".into(), space_before: false}])})), ident: Some("table1".into()) }) } @@ -2232,14 +2242,17 @@ pub fn value_error_format<'v, 'e>( is_newline: false, formatting: None, content: value.into(), + space_before: false, }, Token::InlineMath { content: r"\pm".into(), + space_before: false, }, Token::Paragraph { is_newline: false, formatting: None, content: error.into(), + space_before: false, }, ]), }) diff --git a/src/gen/lab/mod.rs b/src/gen/lab/mod.rs index 1209cc3..dc93e41 100644 --- a/src/gen/lab/mod.rs +++ b/src/gen/lab/mod.rs @@ -212,13 +212,31 @@ where output.write_str("\n")?; output.write_str(end)?; } - Token::InlineMath { content } => { + Token::InlineMath { + content, + space_before, + } => { + if *space_before { + output.write_char(' ')?; + } write!(output, "${content}$")?; } - Token::Reference { ident } => { + Token::Reference { + ident, + space_before, + } => { + if *space_before { + output.write_char(' ')?; + } write!(output, "\\ref{{{ident}}}")?; } - Token::FootnoteReference { ident } => { + Token::FootnoteReference { + ident, + space_before, + } => { + if *space_before { + output.write_char(' ')?; + } let (encountered, i) = { // TODO probably should move this to separate function let encountered_map = &mut context.encountered_footnotes; @@ -255,9 +273,12 @@ where is_newline, formatting, content, + space_before, } => { if *is_newline { output.write_str("\n\\tbln ")?; + } else if *space_before { + output.write_char(' ')?; } let (start, end) = match formatting { None => ("", ""), @@ -281,7 +302,14 @@ where } output.write_str(end)?; } - Token::Href { url, display } => { + Token::Href { + url, + display, + space_before, + } => { + if *space_before { + output.write_char(' ')?; + } write!(output, "\\href{{{url}}}{{{display}}}")?; } Token::Multiple { tokens } => self.write_tokens_to(output, meta, context, tokens)?, @@ -311,6 +339,7 @@ mod tests { macro_rules! text { ($text:literal) => { Token::Paragraph { + space_before: false, is_newline: false, formatting: None, content: $text.into(), @@ -376,17 +405,17 @@ mod tests { test! {header5, Token::Header { order: 5, content: "HeaderText".into() }, r"\textbf{HeaderText}"} // Paragraphs - test! {para1, Token::Paragraph{is_newline: false, formatting: None, content: "content, content!!!".into()}, "content, content!!!"} - test! {para2, Token::Paragraph{is_newline: true, formatting: None, content: "content, content!!!".into()}, "\n\\tbln content, content!!!"} + test! {para1, Token::Paragraph{is_newline: false, formatting: None, content: "content, content!!!".into(), space_before: false}, "content, content!!!"} + test! {para2, Token::Paragraph{is_newline: true, formatting: None, content: "content, content!!!".into(), space_before: false}, "\n\\tbln content, content!!!"} // Formatting - test! {formatting1, Token::Paragraph{is_newline: false, formatting: Some(Formatting::Bold), content: "some text, idk".into()}, "\\textbf{some text, idk}"} - test! {formatting2, Token::Paragraph{is_newline: false, formatting: Some(Formatting::Italic), content: "some text, idk".into()}, "\\textit{some text, idk}"} - test! {formatting3, Token::Paragraph{is_newline: false, formatting: Some(Formatting::StrikeThrough), content: "some text, idk".into()}, "\\st{some text, idk}"} + test! {formatting1, Token::Paragraph{is_newline: false, formatting: Some(Formatting::Bold), content: "some text, idk".into(), space_before: false}, "\\textbf{some text, idk}"} + test! {formatting2, Token::Paragraph{is_newline: false, formatting: Some(Formatting::Italic), content: "some text, idk".into(), space_before: false}, "\\textit{some text, idk}"} + test! {formatting3, Token::Paragraph{is_newline: false, formatting: Some(Formatting::StrikeThrough), content: "some text, idk".into(), space_before: false}, "\\st{some text, idk}"} // Hrefs - test! {href1, Token::Href { url: "https://www.github.com/Dzuchun".parse().unwrap(), display: "My gh page".into() }, r"\href{https://www.github.com/Dzuchun}{My gh page}"} - test! {href2, Token::Href { url: "https://github.com/Dzuchun".parse().unwrap(), display: "Моя сторінка на гітхабі".into() }, r"\href{https://github.com/Dzuchun}{Моя сторінка на гітхабі}"} + test! {href1, Token::Href { url: "https://www.github.com/Dzuchun".parse().unwrap(), display: "My gh page".into(), space_before: false }, r"\href{https://www.github.com/Dzuchun}{My gh page}"} + test! {href2, Token::Href { url: "https://github.com/Dzuchun".parse().unwrap(), display: "Моя сторінка на гітхабі".into(), space_before: false }, r"\href{https://github.com/Dzuchun}{Моя сторінка на гітхабі}"} // Equations test! {equations_1, Token::DisplayMath { content: r"B(\alpha, \beta) = \int \limits_{0}^{1} x^{\alpha - 1} (1-x)^{\beta-1} dx".into(), ident: None }, @@ -405,7 +434,7 @@ mod tests { "} // Inline math - test! {inline_mathmode, Token::InlineMath{content:r"\dfrac{\partial x}{\partial t}".into()}, r"$ \dfrac{\partial x}{\partial t} $"} + test! {inline_mathmode, Token::InlineMath{content:r"\dfrac{\partial x}{\partial t}".into(), space_before: false}, r"$ \dfrac{\partial x}{\partial t} $"} // Figures test! {fig1, Token::Figure { src_name: Path::new("apple.jpg").into(), caption: Some(Box::new(text!("a very realistic-looking apple"))), ident: None, width: None }, r" @@ -528,7 +557,7 @@ cell_21 & cell_22 & cell_23 \\ \hline "} // Text - test! {text, Token::Multiple{tokens:tokens![Token::PageDiv, Token::Header { order: 0, content: "Header".into() }, text!("Here's a little formula: "), Token::InlineMath{content:r"(a+b)^2 = a^2 + 2 \cdot a \cdot b + b^2".into()}, text!(". It's very useful, more useful than me!")]}, r" + test! {text, Token::Multiple{tokens:tokens![Token::PageDiv, Token::Header { order: 0, content: "Header".into() }, text!("Here's a little formula: "), Token::InlineMath{content:r"(a+b)^2 = a^2 + 2 \cdot a \cdot b + b^2".into(), space_before: false}, text!(". It's very useful, more useful than me!")]}, r" \clearpage \section{Header} Here's a little formula: $(a+b)^2 = a^2 + 2 \cdot a \cdot b + b^2$. It's very useful, more useful than me! @@ -536,7 +565,7 @@ cell_21 & cell_22 & cell_23 \\ \hline // Footnotes test! {footnote_single, Token::Multiple{tokens:tokens![ - Token::FootnoteReference{ident:"explanation".into()}, + Token::FootnoteReference{ident:"explanation".into(), space_before: false}, Token::FootnoteContent { content: Box::new(text!("42")), ident: "explanation".into() } ]}, r" \footnotemark[1] @@ -545,16 +574,16 @@ cell_21 & cell_22 & cell_23 \\ \hline test! {footnote_multiple, Token::Multiple{tokens:tokens![ text!("Here's a first note"), - Token::FootnoteReference{ident:"explanation1".into()}, + Token::FootnoteReference{ident:"explanation1".into(), space_before: false}, text!(", there are also second"), - Token::FootnoteReference{ident: "explanation2".into()}, + Token::FootnoteReference{ident: "explanation2".into(), space_before: false}, text!(", and the third"), - Token::FootnoteReference{ident:"explanation3".into()}, + Token::FootnoteReference{ident:"explanation3".into(), space_before: false}, Token::FootnoteContent { content: Box::new(text!("There are things in this world that's you're not meant to see")), ident: "explanation1".into() }, Token::FootnoteContent { content: Box::new(text!("now the voice of a deity permeates")), ident: "explanation2".into() }, Token::FootnoteContent { content: Box::new(text!("see notes 1 and 2")), ident: "explanation3".into() }, text!(". But notes will not be duplicated here!"), - Token::FootnoteReference{ident:"explanation1".into()} + Token::FootnoteReference{ident:"explanation1".into(), space_before: false} ]}, r" Here's a first note @@ -574,13 +603,15 @@ cell_21 & cell_22 & cell_23 \\ \hline is_display: false, is_static: false, code: "1".into(), - insert_path: None + insert_path: None, + is_space_before: false, }}, r"1"} test! {ayano_plain2, Token::Ayano{data: AyanoBlock{ is_display: false, is_static: false, code: "1 + 2".into(), - insert_path: None + insert_path: None, + is_space_before: false, }}, r"3"} test! {ayano_plain3, Token::Ayano{data: AyanoBlock{ is_display: false, @@ -591,7 +622,8 @@ x = 103 y = sqrt(x) int(y)" .into(), - insert_path: None + insert_path: None, + is_space_before: false, }}, r"10"} test! {ayano_plain4, Token::Ayano{data: AyanoBlock{ is_display: false, @@ -602,7 +634,8 @@ for i in range(101): res += i res" .into(), - insert_path: None + insert_path: None, + is_space_before: false, }}, r"5050"} test! {ayano_err1, Token::Ayano{data: AyanoBlock{ is_display: false, @@ -610,7 +643,8 @@ res" code: r" 'err', 1.0, 0.1" .into(), - insert_path: None + insert_path: None, + is_space_before: false, }}, r"1.00 $\pm$ 0.10"} test! {ayano_err2, Token::Ayano{data: AyanoBlock{ is_display: false, @@ -618,7 +652,8 @@ res" code: r" 'err', 1.0, 0.4" .into(), - insert_path: None + insert_path: None, + is_space_before: false, }}, r"1.0 $\pm$ 0.4"} test! {ayano_err3, Token::Ayano{data: AyanoBlock{ is_display: false, @@ -628,7 +663,8 @@ x = 1.0 y = 0.4 @dev: x, y" .into(), - insert_path: None + insert_path: None, + is_space_before: false, }}, r"1.0 $\pm$ 0.4" } @@ -636,7 +672,8 @@ y = 0.4 is_display: false, is_static: false, code: r"'fig', 'path/to/image.jpg', None, None, None".into(), - insert_path: None + insert_path: None, + is_space_before: false, }}, r" \begin{figure}[h!] \centering @@ -648,7 +685,8 @@ y = 0.4 is_display: false, is_static: false, code: r"'fig', 'path/to/image.jpg', None, None, 0.625".into(), - insert_path: None + insert_path: None, + is_space_before: false, }}, r" \begin{figure}[h!] \centering @@ -656,13 +694,15 @@ y = 0.4 \end{figure}" } - test! {ayano_fig2, Token::Ayano{data: AyanoBlock{is_display: false,is_static: false,code: r"@fig: src = 'path/to/image.jpg'".into(),insert_path: None}}, + test! {ayano_fig2, Token::Ayano{data: AyanoBlock{is_display: false,is_static: false,code: r"@fig: src = 'path/to/image.jpg'".into(),insert_path: None, + is_space_before: false,}}, r" \begin{figure}[h!] \centering \includegraphics[width = 0.9 \textwidth]{path/to/image.jpg} \end{figure}"} - test! {ayano_fig3, Token::Ayano{data: AyanoBlock{ is_display: false,is_static: false,code: r#"@fig: src = 'path/to/image.jpg', ident = "meow""#.into(),insert_path: None + test! {ayano_fig3, Token::Ayano{data: AyanoBlock{ is_display: false,is_static: false,code: r#"@fig: src = 'path/to/image.jpg', ident = "meow""#.into(),insert_path: None, + is_space_before: false, }}, r#" \begin{figure}[h!] @@ -677,7 +717,8 @@ r#" r#" data = [[1, 2, 3], [4, 5, 6]] @gen_table: lambda r,c: data[r][c]; rows=2, columns=3 -"#.into(), insert_path: None }}, +"#.into(), insert_path: None, + is_space_before: false, }}, r#" \begin{table}[h!] \begin{center} @@ -689,7 +730,7 @@ r#" \end{center} \end{table} "# - } + } // TODO add tests with csv-tables // TODO add minted to preamble diff --git a/src/lexer.rs b/src/lexer.rs index 1efc5d3..1546969 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -424,7 +424,7 @@ fn href< ) -> IResult<&'source str, Token<'source>, E> { context( "href", - preceded( + pair( multispace0, pair( delimited(char('['), take_until("]"), char(']')), @@ -438,9 +438,10 @@ fn href< ), ), ) - .map(|(display, url)| Token::Href { + .map(|(before, (display, url))| Token::Href { url, display: display.trim().into(), + space_before: before.ends_with(' '), }) .parse(input) } @@ -507,17 +508,13 @@ fn ayano< input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { // TODO add tests for escaping backslashes removal (generation-side) - let (rest, ((display, insert, _ident, is_static), code)) = context( + let (rest, ((before, _), ((display, insert, _ident, is_static), code))) = context( "ayano", - preceded( - tuple(( + pair( + terminated( _multispace_line_break("```"), - space0, - tag_no_case("python,"), - space0, - tag("Ayano"), - space0, - )), + tuple((space0, tag_no_case("python,"), space0, tag("Ayano"), space0)), + ), // This is guaranteed to be Ayano block at this point, so we lock in cut(pair( terminated( @@ -565,6 +562,7 @@ fn ayano< code, is_display: display.is_some(), is_static: is_static.is_some(), + is_space_before: before.contains(' '), }, }, )) @@ -742,6 +740,7 @@ fn formatting< let (input, space_before) = multispace0(input)?; // if there are newlines, this paragraph is_newline let is_newline = space_before.contains('\n'); + let is_space_before = space_before.ends_with(' '); let (rest, delimiter) = alt((tag("**"), tag("__"), tag("*"), tag("_"), tag("~~")))(input)?; // Anything starting from these characters IS a formatting token. // Committed to parsing after this point @@ -770,6 +769,7 @@ fn formatting< &rest[(end_candidate + delimiter.len())..], Token::Paragraph { is_newline, + space_before: is_space_before, formatting: Some(formatting), content, }, @@ -791,13 +791,14 @@ fn inline_math<'source, E: ParseError<&'source str> + ContextError<&'source str> ) -> IResult<&'source str, Token<'source>, E> { context( "inline math", - preceded( + pair( multispace0, delimited(char('$'), take_till1(|c| c == '\n' || c == '$'), char('$')), ), ) - .map(|content: &str| Token::InlineMath { + .map(|(before, content): (&str, &str)| Token::InlineMath { content: content.trim().into(), + space_before: before.ends_with(' '), }) .parse(input) } @@ -815,11 +816,10 @@ fn inline_math<'source, E: ParseError<&'source str> + ContextError<&'source str> fn reference<'source, E: ParseError<&'source str> + ContextError<&'source str>>( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { - let (rest, ident) = context( + let (rest, (before, _, _, ident)) = context( "reference", - delimited( - pair(pair(multispace0, tag("[@")), space0), - ident_kernel, + terminated( + tuple((multispace0, tag("[@"), space0, ident_kernel)), pair(space0, char(']')), ), )(input)?; @@ -827,6 +827,7 @@ fn reference<'source, E: ParseError<&'source str> + ContextError<&'source str>>( rest, Token::Reference { ident: ident.trim().into(), + space_before: before.ends_with(' '), }, )) } @@ -838,11 +839,13 @@ fn reference<'source, E: ParseError<&'source str> + ContextError<&'source str>>( fn footnote_reference<'source, E: ParseError<&'source str> + ContextError<&'source str>>( input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { - let (rest, ident) = context( + let (rest, (before, ident)) = context( "footnote reference", - delimited( - pair(multispace0, tag("[^")), - delimited(space0, ident_kernel, space0), + terminated( + pair( + multispace0, + delimited(pair(tag("[^"), space0), ident_kernel, space0), + ), char(']'), ), )(input)?; @@ -850,6 +853,7 @@ fn footnote_reference<'source, E: ParseError<&'source str> + ContextError<&'sour rest, Token::FootnoteReference { ident: ident.trim().into(), + space_before: before.ends_with(' '), }, )) } @@ -1008,6 +1012,7 @@ fn paragraph< is_newline, content, formatting: None, + space_before: space_before.ends_with(' '), }; Ok((rest, (this_token, follower))) } @@ -1582,10 +1587,13 @@ $$ use super::*; macro_rules! href_ok { - {$name:ident, $input:expr, $output_url:expr, $output_display:expr, $left_over:literal} => { - test! {$name, href, $input, e: Ok(($left_over, Token::Href { url: $output_url.parse().expect("This is a valid url"), display: $output_display.into() }))} - }; - } + {$name:ident, $input:expr, $output_url:expr, $output_display:expr, $left_over:literal ^} => { + test! {$name, href, $input, e: Ok(($left_over, Token::Href { url: $output_url.parse().expect("This is a valid url"), display: $output_display.into(), space_before: true }))} + }; + {$name:ident, $input:expr, $output_url:expr, $output_display:expr, $left_over:literal} => { + test! {$name, href, $input, e: Ok(($left_over, Token::Href { url: $output_url.parse().expect("This is a valid url"), display: $output_display.into(), space_before: false }))} + }; + } test! {href1, href, "[link text]()", p: Err(_)} // empty url is not ok test! {href2, href, "[](homepage.com)", p: Err(_)} // empty display is not ok too test! {href3, href, "[невалідне посилання](http://ref ?????)", p: Err(_)} // random garbage instead of url is not accepted @@ -1593,10 +1601,11 @@ $$ href_ok! {href5, "[текст посилання](ftp://somewebsite/files/01234)", "ftp://somewebsite/files/01234", "текст посилання", ""} // cyrillic test href_ok! {href6, "[link text](ftp://somewebsite/files/01234), ще текст після нього", "ftp://somewebsite/files/01234", "link text", ", ще текст після нього"} // text after url is alright - href_ok! {href7, " [link text](ftp://somewebsite/files/01234), ще текст після нього", "ftp://somewebsite/files/01234", "link text", ", ще текст після нього"} + href_ok! {href7, " [link text](ftp://somewebsite/files/01234), ще текст після нього", "ftp://somewebsite/files/01234", "link text", ", ще текст після нього" ^} // space before url is ignored href_ok! {href8, "[ link text](ftp://somewebsite/files/01234).", "ftp://somewebsite/files/01234", "link text", "."} // display text will be trimmed + href_ok! {href9_space_before, " [link text](ftp://somewebsite/files/01234).", "ftp://somewebsite/files/01234", "link text", "."^} } // `code_block` tests @@ -1632,7 +1641,7 @@ $$ macro_rules! test_ok { {$name:ident, $input:literal, $formula:literal, $left_over:literal} => { - test!{$name, inline_math, $input, e: Ok(($left_over, Token::InlineMath{content:$formula.into()}))} + test!{$name, inline_math, $input, e: Ok(($left_over, Token::InlineMath{content:$formula.into(), space_before: false}))} }; } @@ -1645,7 +1654,7 @@ $$ // unclosed mathmode is obviously disallowed test_ok! {ok_cyrillic, "$ текст українською $", "текст українською", ""} // you can put there whatever you want, however pay attention to latex's rules - test_ok! {ok_spaces, "\t \n\t \n \t $ x $", "x", ""} + test_ok! {ok_spaces, "\t \n\t \n \t$ x $", "x", ""} // got some spaces before? don't care test_ok! {ok_text_after, "$ x$some text after it, українською в тому числі", "x", "some text after it, українською в тому числі"} @@ -1658,7 +1667,7 @@ $$ macro_rules! test_ok { {$name:ident, $input:literal, $reference:literal, $left_over:literal} => { - test!{$name, reference, $input, e: Ok(($left_over, Token::Reference{ident: $reference.into()}))} + test!{$name, reference, $input, e: Ok(($left_over, Token::Reference{ident: $reference.into(), space_before: false}))} }; } @@ -1677,14 +1686,14 @@ $$ macro_rules! test_ok { {$name:ident, $input:literal, $reference:literal, $left_over:literal} => { - test!{$name, footnote_reference, $input, e: Ok(($left_over, Token::FootnoteReference{ident:$reference.into()}))} + test!{$name, footnote_reference, $input, e: Ok(($left_over, Token::FootnoteReference{ident:$reference.into(), space_before: false}))} }; } test! {err_empty, reference, "[^ ]", p: Err(_)} // empty reference is not ok test_ok! {ok_char, "[^s]", "s", ""} // single char as identifier is ok - test_ok! {ok_space_before, " \n \t [^explanation_of_42]", "explanation_of_42", ""} - test_ok! {ok_text_after, " \n \t [^y_u_do_dis_2_me] кирилиця or th", "y_u_do_dis_2_me", " кирилиця or th"} + test_ok! {ok_space_before, " \n \t[^explanation_of_42]", "explanation_of_42", ""} + test_ok! {ok_text_after, " \n \t[^y_u_do_dis_2_me] кирилиця or th", "y_u_do_dis_2_me", " кирилиця or th"} } // tests for `paragraph` @@ -1697,6 +1706,7 @@ $$ is_newline: false, formatting: None, content: $paragraph.into(), + space_before: false, }, $follower_token)))} }; {$name:ident, $input:literal, $paragraph:literal, $follower_token:expr, $left_over:literal, |} => { @@ -1704,6 +1714,7 @@ $$ is_newline: true, formatting: None, content: $paragraph.into(), + space_before: false, }, $follower_token)))} }; } @@ -1713,17 +1724,17 @@ $$ test_ok! {ok_char, " \t\n c\nfff", "c", None, "\nfff", |} // single char is ok as a paragraph test_ok! {ok_sentence, "\t \n I'm absolutely increative, so here's a bunch of garbage: ]42[t5(2[24tj4-0)jt3_94j03\nand some rest", "I'm absolutely increative, so here's a bunch of garbage: ]42[t5(2[24tj4-0)jt3_94j03", None, "\nand some rest", |} - test_ok! {ok_formatting, "\t \n You can use *bold*!", "You can use", Some(Token::Paragraph{is_newline: false, formatting: Some(Formatting::Italic), content: "bold".into()}), "!", |} + test_ok! {ok_formatting, "\t \n You can use *bold*!", "You can use", Some(Token::Paragraph{is_newline: false, formatting: Some(Formatting::Italic), content: "bold".into(), space_before: false}), "!", |} // FIXME NOT SUPPORTED /* test_ok! {ok_formatting_in_formatting, "\t \n You can even use ~~__double-formatted__~~ things!", "You can even use", Some(Token::Formatted(Formatting::StrikeThrough, Box::new(Token::Formatted(Formatting::Bold, Box::new(Token::text("double-formatted")))))), " things!"} */ test_ok! {ok_equation, " \t Inline math is fine too: $y = x_{\text{поч}} + x_0$.", "Inline math is fine too:", - Some(Token::InlineMath{content: "y = x_{\text{поч}} + x_0".into()}), "."} + Some(Token::InlineMath{content: "y = x_{\text{поч}} + x_0".into(), space_before: false}), "."} test_ok! {ok_href, " \t\t\n You can even [залишити посилання на свою сторінку на Гітхабі ](https://www.github.com/Dzuchun)!", "You can even", - Some(Token::Href { url: "https://www.github.com/Dzuchun".parse().expect("That's a valid url"), display: "залишити посилання на свою сторінку на Гітхабі".into() }), "!", |} - test_ok! {ok_footnote_ref, "\t Footnote refs[^1] are also ok too!", "Footnote refs", Some(Token::FootnoteReference{ident: "1".into()}), " are also ok too!"} + Some(Token::Href { url: "https://www.github.com/Dzuchun".parse().expect("That's a valid url"), display: "залишити посилання на свою сторінку на Гітхабі".into(), space_before: false }), "!", |} + test_ok! {ok_footnote_ref, "\t Footnote refs[^1] are also ok too!", "Footnote refs", Some(Token::FootnoteReference{ident: "1".into(), space_before: false}), " are also ok too!"} test_ok! {ok_obj_ref, "\n\t Thi[[ngs lik_e tables* ([@tab:results]) can also be referenced", "Thi[[ngs lik_e tables* (", - Some(Token::Reference{ident: "tab:results".into()}), ") can also be referenced", |} + Some(Token::Reference{ident: "tab:results".into(), space_before: false}), ") can also be referenced", |} } // `inner_lex` tests @@ -1750,14 +1761,29 @@ $$ (!$eq:literal) => { Token::InlineMath { content: Cow::Borrowed($eq), + space_before: false, + } + }; + (^!$eq:literal) => { + Token::InlineMath { + content: Cow::Borrowed($eq), + space_before: true, } }; } macro_rules! hr { + (^ [$display:literal]($url:literal)) => { + Token::Href { + url: Url::from_str($url).expect("Should be a valid url"), + display: Cow::Borrowed($display), + space_before: true, + } + }; ([$display:literal]($url:literal)) => { Token::Href { url: Url::from_str($url).expect("Should be a valid url"), display: Cow::Borrowed($display), + space_before: false, } }; } @@ -1779,11 +1805,13 @@ $$ (@ $ident:literal) => { Token::Reference { ident: Cow::Borrowed($ident), + space_before: false, } }; (^ $ident:literal) => { Token::FootnoteReference { ident: Cow::Borrowed($ident), + space_before: false, } }; } @@ -1794,17 +1822,26 @@ $$ ($text:literal, |) => { tx!($text, None, |) }; + (^ $text:literal) => { + tx!(^ $text, None) + }; ($text:literal, $formatting:expr) => { tx!($text, $formatting, false) }; ($text:literal, $formatting:expr, |) => { tx!($text, $formatting, true) }; + (^ $text:literal, $formatting:expr) => { + tx!($text, $formatting, false, true) + }; ($text:literal, $formatting:expr, $newline:literal) => { + tx!($text, $formatting, $newline, false) + }; + ($text:literal, $formatting:expr, $newline:literal, $space_before:literal) => { Token::Paragraph { is_newline: $newline, formatting: $formatting, - content: Cow::Borrowed($text), + content: Cow::Borrowed($text), space_before: $space_before, } }; } @@ -1831,15 +1868,15 @@ $$ test_err! {err_empty, ""} test_ok! {ok_char_para, "a", tx!("a")} test_ok! {ok_formatted, "*виділений текст * і ще щось потім", - tks![tx!("виділений текст", Some(Formatting::Italic)), tx!("і ще щось потім")]} + tks![tx!("виділений текст", Some(Formatting::Italic)), tx!(^"і ще щось потім")]} test_ok! {ok_formatted_newlined, "\n*виділений текст * і ще щось потім", - tks![tx!("виділений текст", Some(Formatting::Italic), |), tx!("і ще щось потім")]} + tks![tx!("виділений текст", Some(Formatting::Italic), |), tx!(^"і ще щось потім")]} test_ok! {ok_inline_math, "дивіться! ось формула для параболи: $y = x^2$", tks![tx!("дивіться! ось формула для параболи:"), eq!(! "y = x^2")]} test_err! {err_display_math, "якась страшна штука:\n$$\nf(x) = \\int\\lms_{0}^{x} \\log(x - 2) dx\n$$\n{ref = scary}\n\tКраще не чіпати її"} // display math is not allowed in the inner_lex test_ok! {ok_href, "посилання на мою [Github сторінку](https://www.example.com) - ха-ха, жартую;\tОсь вам рівняння: $x^4 + 5x^2 - 100 = 0$.", - tks![tx!("посилання на мою"), hr!(["Github сторінку"]("https://www.example.com")), tx!("- ха-ха, жартую;\tОсь вам рівняння:"), eq!(!"x^4 + 5x^2 - 100 = 0"), tx!(".")]} + tks![tx!("посилання на мою"), hr!(^ ["Github сторінку"]("https://www.example.com")), tx!(^ "- ха-ха, жартую;\tОсь вам рівняння:"), eq!(^!"x^4 + 5x^2 - 100 = 0"), tx!(".")]} test_ok! {ok_ref, "розрахунки наведено у таблиці ([@tab:calc])[^3].", tks![tx!("розрахунки наведено у таблиці ("), rf!(@"tab:calc"), tx!(")"), rf!(^"3"), tx!(".")]} } @@ -1938,11 +1975,11 @@ $$ test_ok! {ok_caption, "\n![[path/to/file.jpg]]\n{caption = \"Ця картинка показує графік рівняння $y = x^2$\" }\n", fig!("path/to/file.jpg", {ref = None::<&str>, caption = Some(tks![tx!("Ця картинка показує графік рівняння"), eq!(!"y = x^2")]), width = None}), "\n"} test_ok! {ok_both, "\n![[path/to/file.jpg]]\n{ ref = mc_erat, caption = \"This is a mee from our $\\text{Nomifactory}^{2*}$ world.\" }\n", - fig!("path/to/file.jpg", {ref = Some("mc_erat"), caption = Some(tks!(tx!("This is a mee from our"), eq!(!"\\text{Nomifactory}^{2*}"), tx!("world."))), width = None}), "\n"} + fig!("path/to/file.jpg", {ref = Some("mc_erat"), caption = Some(tks!(tx!("This is a mee from our"), eq!(!"\\text{Nomifactory}^{2*}"), tx!(^ "world."))), width = None}), "\n"} test_ok! {ok_both_reversed, "\n![[path/to/file.jpg]]\n{ caption = \"This is a meme from our $\\text{Nomifactory}^{2*}$ world.\" , ref = mc_erat }\n", - fig!("path/to/file.jpg", {ref = Some("mc_erat"), caption = Some(tks!(tx!("This is a meme from our"), eq!(!"\\text{Nomifactory}^{2*}"), tx!("world."))), width = None}), "\n"} + fig!("path/to/file.jpg", {ref = Some("mc_erat"), caption = Some(tks!(tx!("This is a meme from our"), eq!(!"\\text{Nomifactory}^{2*}"), tx!(^ "world."))), width = None}), "\n"} test_ok! {ok_both_left_over, "\n![[path/to/file.jpg]]\n{ caption = \"This is a meme from our $\\text{Nomifactory}^{2*}$ world.\" , ref = mc_erat }\nІще трохи тексту", - fig!("path/to/file.jpg", {ref = Some("mc_erat"), caption = Some(tks!(tx!("This is a meme from our"), eq!(!"\\text{Nomifactory}^{2*}"), tx!("world."))), width = None}), "\nІще трохи тексту"} + fig!("path/to/file.jpg", {ref = Some("mc_erat"), caption = Some(tks!(tx!("This is a meme from our"), eq!(!"\\text{Nomifactory}^{2*}"), tx!(^ "world."))), width = None}), "\nІще трохи тексту"} test_ok! {ok_width, "\n![[path/to/file.jpg]]\n{ width = 0.5}\nTh else", fig!("path/to/file.jpg", {ref = None::<&str>, caption = None, width = Some(0.5)}), "\nTh else"} } @@ -2052,6 +2089,7 @@ $$ is_static: $is_static, code: Cow::Borrowed($code), insert_path: $insert_path.map(Path::new).map(Cow::from), + is_space_before: false, }} }; } From a49041f317b62cd9106e10ba4585c084b44cf1de Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Mon, 25 Mar 2024 23:28:04 +0200 Subject: [PATCH 38/98] Fixed issue with some of the inner tokens sticking together, when they definitely shouldn't do that --- src/gen/ayano.rs | 19 ++-- src/lexer.rs | 97 ++++++++++++-------- tests/lex_files/ayano.txt | 7 ++ tests/lex_files/basic.txt | 16 ++++ tests/lex_files/deceit.txt | 117 ++++++++++++++++++++++++ tests/lex_files/tables_code.txt | 44 +++++++++ tests/transformed_files/ayano.txt | 4 +- tests/transformed_files/basic.txt | 6 +- tests/transformed_files/deceit.txt | 32 +++---- tests/transformed_files/tables_code.txt | 20 ++-- 10 files changed, 287 insertions(+), 75 deletions(-) diff --git a/src/gen/ayano.rs b/src/gen/ayano.rs index f499209..898d891 100644 --- a/src/gen/ayano.rs +++ b/src/gen/ayano.rs @@ -162,7 +162,7 @@ impl AyanoExecutor { &self, token: &'token AyanoBlock<'source>, ) -> AyanoResult<'source, Token<'static>> { - self.call_block(token, |res, _| Ok(parse_ayano(res)?)) + self.call_block(token, |res, _| Ok(parse_ayano(res, token.is_space_before)?)) } pub fn display_blocks<'s>(&'s self) -> impl IntoIterator { @@ -1424,7 +1424,10 @@ impl From> for ParsingError { } } -fn parse_ayano(python_output: &PyAny) -> Result, ParsingError> { +fn parse_ayano( + python_output: &PyAny, + is_space_before: bool, +) -> Result, ParsingError> { // it was quite a journey! but I believe to be close to it's end. if let Ok(tuple) = python_output.downcast::() { if let Ok(discriminator) = tuple @@ -1521,13 +1524,13 @@ fn parse_ayano(python_output: &PyAny) -> Result, ParsingError> { .ok_or(ParsingError::TableNoHeader)? .downcast::())? .into_iter() - .map(|h: &PyAny| parse_ayano(h)) + .map(|h: &PyAny| parse_ayano(h, false)) .try_collect()?; let cells: Vec<_> = rows .map(|row| { Ok(throw_downcast!(row.downcast::())? .into_iter() - .map(|c: &PyAny| parse_ayano(c))) + .map(|c: &PyAny| parse_ayano(c, false))) }) .try_fold(Vec::new(), |acc, next: Result<_, ParsingError>| { // OLD: there might be a better way with iterators directly, but I kinda can't come up with it @@ -1564,9 +1567,9 @@ fn parse_ayano(python_output: &PyAny) -> Result, ParsingError> { } // any sort of parsing had failed - return regular text Ok(Token::Paragraph { - is_newline: false, + is_newline: false, // TODO probably add that to block too formatting: None, - space_before: todo!(), + space_before: is_space_before, content: Cow::Owned(python_output.to_string()), }) } @@ -1606,7 +1609,7 @@ mod parsing_tests { let python_output = $input(py); // act - let result = super::parse_ayano(python_output); + let result = super::parse_ayano(python_output, false); // assert assert_eq!(result, $output, "Parsing should produce expected tokens"); @@ -1721,7 +1724,7 @@ mod parsing_tests { Token::text("cell_11"), Token::text("0.25"), Token::text("cell_21"), Token::text("-2"), ]), - caption: Some(Box::new(Token::Multiple{tokens:Tokens::new([Token::text("table caption with some"),Token::InlineMath{content:"formula".into(), space_before: false}])})), + caption: Some(Box::new(Token::Multiple{tokens:Tokens::new([Token::text("table caption with some"),Token::InlineMath{content:"formula".into(), space_before: true}])})), ident: Some("table1".into()) }) } diff --git a/src/lexer.rs b/src/lexer.rs index 1546969..dc4b950 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -931,7 +931,20 @@ fn paragraph_inner< .filter(|(_, c)| ['[', '*', '_', '~', '$'].contains(c)) // Now, try a proper parser for each one .map(|(ind, c)| { - let cut_cut_input = &input[ind..]; + // at this point, there's definitely something to be parsed + // for respective parsers to work properly, we must preserve ~~the timeline~~ whitespace in front of them + // meaning, first index to include into parser input would be last non-whitespace char: + let cut_start = + input[..ind] + .rfind(|c: char| !c.is_whitespace()) + .and_then(|last_non_whitespace| { + input[last_non_whitespace..] + .char_indices() + .nth(1) + .map(|(last_char_length, _)| last_char_length + last_non_whitespace) + }); + // TODO probably there's a more efficient way to do the thing above + let cut_cut_input = &input[cut_start.unwrap_or(0)..]; let parse_res = match c { '[' => { if cut_input[ind..].starts_with("[@") { @@ -1676,8 +1689,8 @@ $$ test_ok! {ok_eq, "[@ eq:parabola]", "eq:parabola", ""} // equation ref example test_ok! {ok_fig, "[@ fig:my_best_drawing ]", "fig:my_best_drawing", ""} // figure ref example test_ok! {ok_tab, "[@ tab:boring_stat ]", "tab:boring_stat", ""} // table ref example - test_ok! {ok_space_before, " \n \t [@ ref]", "ref", ""} - test_ok! {ok_text_after, " \n \t [@ ref] кирилиця or th", "ref", " кирилиця or th"} + // test_ok! {ok_space_before, " \n \t[@ ref]", "ref", ""} + test_ok! {ok_text_after, " \n \t[@ ref] кирилиця or th", "ref", " кирилиця or th"} } // tests for `footnote reference` @@ -1721,19 +1734,19 @@ $$ test! {err_empty, paragraph, "", p: Err(_)} // paragraph won't be parsed where it's empty test! {err_whitespace, paragraph, " \t \n\n \t", p: Err(_)} // nor if it's all whitespace - test_ok! {ok_char, " \t\n c\nfff", "c", None, "\nfff", |} // single char is ok as a paragraph - test_ok! {ok_sentence, "\t \n I'm absolutely increative, so here's a bunch of garbage: ]42[t5(2[24tj4-0)jt3_94j03\nand some rest", + test_ok! {ok_char, " \t\nc\nfff", "c", None, "\nfff", |} // single char is ok as a paragraph + test_ok! {ok_sentence, "\t \nI'm absolutely increative, so here's a bunch of garbage: ]42[t5(2[24tj4-0)jt3_94j03\nand some rest", "I'm absolutely increative, so here's a bunch of garbage: ]42[t5(2[24tj4-0)jt3_94j03", None, "\nand some rest", |} - test_ok! {ok_formatting, "\t \n You can use *bold*!", "You can use", Some(Token::Paragraph{is_newline: false, formatting: Some(Formatting::Italic), content: "bold".into(), space_before: false}), "!", |} + test_ok! {ok_formatting, "\t \nYou can use *bold*!", "You can use", Some(Token::Paragraph{is_newline: false, formatting: Some(Formatting::Italic), content: "bold".into(), space_before: true}), "!", |} // FIXME NOT SUPPORTED /* test_ok! {ok_formatting_in_formatting, "\t \n You can even use ~~__double-formatted__~~ things!", "You can even use", Some(Token::Formatted(Formatting::StrikeThrough, Box::new(Token::Formatted(Formatting::Bold, Box::new(Token::text("double-formatted")))))), " things!"} */ - test_ok! {ok_equation, " \t Inline math is fine too: $y = x_{\text{поч}} + x_0$.", "Inline math is fine too:", - Some(Token::InlineMath{content: "y = x_{\text{поч}} + x_0".into(), space_before: false}), "."} - test_ok! {ok_href, " \t\t\n You can even [залишити посилання на свою сторінку на Гітхабі ](https://www.github.com/Dzuchun)!", "You can even", - Some(Token::Href { url: "https://www.github.com/Dzuchun".parse().expect("That's a valid url"), display: "залишити посилання на свою сторінку на Гітхабі".into(), space_before: false }), "!", |} - test_ok! {ok_footnote_ref, "\t Footnote refs[^1] are also ok too!", "Footnote refs", Some(Token::FootnoteReference{ident: "1".into(), space_before: false}), " are also ok too!"} - test_ok! {ok_obj_ref, "\n\t Thi[[ngs lik_e tables* ([@tab:results]) can also be referenced", "Thi[[ngs lik_e tables* (", + test_ok! {ok_equation, " \tInline math is fine too: $y = x_{\text{поч}} + x_0$.", "Inline math is fine too:", + Some(Token::InlineMath{content: "y = x_{\text{поч}} + x_0".into(), space_before: true}), "."} + test_ok! {ok_href, " \t\t\nYou can even [залишити посилання на свою сторінку на Гітхабі ](https://www.github.com/Dzuchun)!", "You can even", + Some(Token::Href { url: "https://www.github.com/Dzuchun".parse().expect("That's a valid url"), display: "залишити посилання на свою сторінку на Гітхабі".into(), space_before: true }), "!", |} + test_ok! {ok_footnote_ref, "\tFootnote refs[^1] are also ok too!", "Footnote refs", Some(Token::FootnoteReference{ident: "1".into(), space_before: false}), " are also ok too!"} + test_ok! {ok_obj_ref, "\n\tThi[[ngs lik_e tables* ([@tab:results]) can also be referenced", "Thi[[ngs lik_e tables* (", Some(Token::Reference{ident: "tab:results".into(), space_before: false}), ") can also be referenced", |} } @@ -1808,12 +1821,24 @@ $$ space_before: false, } }; + (^@ $ident:literal) => { + Token::Reference { + ident: Cow::Borrowed($ident), + space_before: true, + } + }; (^ $ident:literal) => { Token::FootnoteReference { ident: Cow::Borrowed($ident), space_before: false, } }; + (^^ $ident:literal) => { + Token::FootnoteReference { + ident: Cow::Borrowed($ident), + space_before: true, + } + }; } macro_rules! tx { ($text:literal) => { @@ -1872,7 +1897,7 @@ $$ test_ok! {ok_formatted_newlined, "\n*виділений текст * і ще щось потім", tks![tx!("виділений текст", Some(Formatting::Italic), |), tx!(^"і ще щось потім")]} test_ok! {ok_inline_math, "дивіться! ось формула для параболи: $y = x^2$", - tks![tx!("дивіться! ось формула для параболи:"), eq!(! "y = x^2")]} + tks![tx!("дивіться! ось формула для параболи:"), eq!(^! "y = x^2")]} test_err! {err_display_math, "якась страшна штука:\n$$\nf(x) = \\int\\lms_{0}^{x} \\log(x - 2) dx\n$$\n{ref = scary}\n\tКраще не чіпати її"} // display math is not allowed in the inner_lex test_ok! {ok_href, "посилання на мою [Github сторінку](https://www.example.com) - ха-ха, жартую;\tОсь вам рівняння: $x^4 + 5x^2 - 100 = 0$.", @@ -1907,30 +1932,30 @@ $$ test_err! {err_no_rows, "|header|\n"} test_ok! {ok_one_row, "\n\t\n|header|\n|:---:|\n|cell|\n", tab!(tx!("header"); tx!("cell");), "\n"} test_ok! {ok_more_rows, " \n|header1 | header_2 |\n|:---:|:---:|\n|cell 11|cell[^12]|\n|$cell_{21}$|cell[@tab:22]|\n", - tab!(tx!("header1"), tx!("header_2"); + tab!(tx!("header1"), tx!(^"header_2"); tx!("cell 11"), tks![tx!("cell"), rf!(^ "12")];, eq!(!"cell_{21}"), tks![tx!("cell"), rf!(@"tab:22")];), "\n"} test_ok! {ok_empty_args, " \n|header1 | header_2 |\n|:---:|:---:|\n|cell 11|cell[^12]|\n|$cell_{21}$|cell[@tab:22]|\n{}", - tab!(tx!("header1"), tx!("header_2"); + tab!(tx!("header1"), tx!(^"header_2"); tx!("cell 11"), tks![tx!("cell"), rf!(^ "12")];, eq!(!"cell_{21}"), tks![tx!("cell"), rf!(@"tab:22")];), ""} test_ok! {ok_ident, " \n|header1 | header_2 |\n|:---:|:---:|\n|cell 11|cell[^12]|\n|$cell_{21}$|cell[@tab:22]|\n{ ref = example}", - tab!(tx!("header1"), tx!("header_2"); + tab!(tx!("header1"), tx!(^"header_2"); tx!("cell 11"), tks![tx!("cell"), rf!(^ "12")];, eq!(!"cell_{21}"), tks![tx!("cell"), rf!(@"tab:22")];; {ref = Some("example"), caption = None}), ""} test_ok! {ok_caption, " \n|header1 | header_2 |\n|:---:|:---:|\n|cell 11|cell[^12]|\n|$cell_{21}$|cell[@tab:22]|\n{caption = \"This table can be seen as an \\\"example\\\"\"}", - tab!(tx!("header1"), tx!("header_2"); + tab!(tx!("header1"), tx!(^"header_2"); tx!("cell 11"), tks![tx!("cell"), rf!(^ "12")];, eq!(!"cell_{21}"), tks![tx!("cell"), rf!(@"tab:22")];; {ref = None::<&str>, caption = Some(tx!("This table can be seen as an \\\"example\\\""))}), ""} test_ok! {ok_both, " \n|header1 | header_2 |\n|:---:|:---:|\n|cell 11|cell[^12]|\n|$cell_{21}$|cell[@tab:22]|\n{ref = example_table, caption = \"This table can be seen as an \\\"example\\\"\"}", - tab!(tx!("header1"), tx!("header_2"); + tab!(tx!("header1"), tx!(^"header_2"); tx!("cell 11"), tks![tx!("cell"), rf!(^ "12")];, eq!(!"cell_{21}"), tks![tx!("cell"), rf!(@"tab:22")];; {ref = Some("example_table"), caption = Some(tx!("This table can be seen as an \\\"example\\\""))}), ""} test_ok! {ok_both_reversed, " \n|header1 | header_2 |\n|:---:|:---:|\n|cell 11|cell[^12]|\n|$cell_{21}$|cell[@tab:22]|\n{caption = \"This table can be seen as an \\\"example\\\"\", ref = example_table }", - tab!(tx!("header1"), tx!("header_2"); + tab!(tx!("header1"), tx!(^"header_2"); tx!("cell 11"), tks![tx!("cell"), rf!(^ "12")];, eq!(!"cell_{21}"), tks![tx!("cell"), rf!(@"tab:22")];; {ref = Some("example_table"), caption = Some(tx!("This table can be seen as an \\\"example\\\""))}), ""} @@ -1973,13 +1998,13 @@ $$ test_ok! {ok_explicit_no_params, "\n![[path/to/file.jpg]]\n{}\n", fig!("path/to/file.jpg"), "\n"} test_ok! {ok_ident, "\n![[path/to/file.jpg]]\n{ref = figure_of_doom }\n", fig!("path/to/file.jpg", {ref = Some("figure_of_doom"), caption = None, width = None}), "\n"} test_ok! {ok_caption, "\n![[path/to/file.jpg]]\n{caption = \"Ця картинка показує графік рівняння $y = x^2$\" }\n", - fig!("path/to/file.jpg", {ref = None::<&str>, caption = Some(tks![tx!("Ця картинка показує графік рівняння"), eq!(!"y = x^2")]), width = None}), "\n"} + fig!("path/to/file.jpg", {ref = None::<&str>, caption = Some(tks![tx!("Ця картинка показує графік рівняння"), eq!(^!"y = x^2")]), width = None}), "\n"} test_ok! {ok_both, "\n![[path/to/file.jpg]]\n{ ref = mc_erat, caption = \"This is a mee from our $\\text{Nomifactory}^{2*}$ world.\" }\n", - fig!("path/to/file.jpg", {ref = Some("mc_erat"), caption = Some(tks!(tx!("This is a mee from our"), eq!(!"\\text{Nomifactory}^{2*}"), tx!(^ "world."))), width = None}), "\n"} + fig!("path/to/file.jpg", {ref = Some("mc_erat"), caption = Some(tks!(tx!("This is a mee from our"), eq!(^!"\\text{Nomifactory}^{2*}"), tx!(^ "world."))), width = None}), "\n"} test_ok! {ok_both_reversed, "\n![[path/to/file.jpg]]\n{ caption = \"This is a meme from our $\\text{Nomifactory}^{2*}$ world.\" , ref = mc_erat }\n", - fig!("path/to/file.jpg", {ref = Some("mc_erat"), caption = Some(tks!(tx!("This is a meme from our"), eq!(!"\\text{Nomifactory}^{2*}"), tx!(^ "world."))), width = None}), "\n"} + fig!("path/to/file.jpg", {ref = Some("mc_erat"), caption = Some(tks!(tx!("This is a meme from our"), eq!(^!"\\text{Nomifactory}^{2*}"), tx!(^ "world."))), width = None}), "\n"} test_ok! {ok_both_left_over, "\n![[path/to/file.jpg]]\n{ caption = \"This is a meme from our $\\text{Nomifactory}^{2*}$ world.\" , ref = mc_erat }\nІще трохи тексту", - fig!("path/to/file.jpg", {ref = Some("mc_erat"), caption = Some(tks!(tx!("This is a meme from our"), eq!(!"\\text{Nomifactory}^{2*}"), tx!(^ "world."))), width = None}), "\nІще трохи тексту"} + fig!("path/to/file.jpg", {ref = Some("mc_erat"), caption = Some(tks!(tx!("This is a meme from our"), eq!(^!"\\text{Nomifactory}^{2*}"), tx!(^ "world."))), width = None}), "\nІще трохи тексту"} test_ok! {ok_width, "\n![[path/to/file.jpg]]\n{ width = 0.5}\nTh else", fig!("path/to/file.jpg", {ref = None::<&str>, caption = None, width = Some(0.5)}), "\nTh else"} } @@ -2018,33 +2043,33 @@ $$ test_ok! {ok_single, "\n1. Item 1\n", ls![1 tx!("Item 1")], "\n"} test_ok! {ok_multiple, "\n1. Item 1 \n2. Gamma-function def: $\\Gamma(\\alpha) = \\int\\limits_{0}^{+\\infinity} t^{\\alpha - 1} e^{-t} dt$.\n3. Here's some table ref: ([@tab:function ]) \n4. Explained in [^source2]\n", ls![1 tx!("Item 1"), - tks![tx!("Gamma-function def:"), eq!(!"\\Gamma(\\alpha) = \\int\\limits_{0}^{+\\infinity} t^{\\alpha - 1} e^{-t} dt"), tx!(".")], + tks![tx!("Gamma-function def:"), eq!(^!"\\Gamma(\\alpha) = \\int\\limits_{0}^{+\\infinity} t^{\\alpha - 1} e^{-t} dt"), tx!(".")], tks![tx!("Here's some table ref: ("), rf!(@"tab:function"), tx!(")")], - tks![tx!("Explained in"), rf!(^"source2")] + tks![tx!("Explained in"), rf!(^^"source2")] ], "\n"} test_ok! {ok_latin, "\na. Item 1 \nb. Gamma-function def: $\\Gamma(\\alpha) = \\int\\limits_{0}^{+\\infinity} t^{\\alpha - 1} e^{-t} dt$.\nc. Here's some table ref: ([@tab:function ]) \nd. Explained in [^source2]\n", ls![a tx!("Item 1"), - tks![tx!("Gamma-function def:"), eq!(!"\\Gamma(\\alpha) = \\int\\limits_{0}^{+\\infinity} t^{\\alpha - 1} e^{-t} dt"), tx!(".")], + tks![tx!("Gamma-function def:"), eq!(^!"\\Gamma(\\alpha) = \\int\\limits_{0}^{+\\infinity} t^{\\alpha - 1} e^{-t} dt"), tx!(".")], tks![tx!("Here's some table ref: ("), rf!(@"tab:function"), tx!(")")], - tks![tx!("Explained in"), rf!(^"source2")] + tks![tx!("Explained in"), rf!(^^"source2")] ], "\n"} test_ok! {ok_cyrillic, "\nа. Item 1 \nб. Gamma-function def: $\\Gamma(\\alpha) = \\int\\limits_{0}^{+\\infinity} t^{\\alpha - 1} e^{-t} dt$.\nв. Here's some table ref: ([@tab:function ]) \nг. Explained in [^source2]\n", ls![а tx!("Item 1"), - tks![tx!("Gamma-function def:"), eq!(!"\\Gamma(\\alpha) = \\int\\limits_{0}^{+\\infinity} t^{\\alpha - 1} e^{-t} dt"), tx!(".")], + tks![tx!("Gamma-function def:"), eq!(^!"\\Gamma(\\alpha) = \\int\\limits_{0}^{+\\infinity} t^{\\alpha - 1} e^{-t} dt"), tx!(".")], tks![tx!("Here's some table ref: ("), rf!(@"tab:function"), tx!(")")], - tks![tx!("Explained in"), rf!(^"source2")] + tks![tx!("Explained in"), rf!(^^"source2")] ], "\n"} test_ok! {ok_bullet, "\n- Item 1 \n- Gamma-function def: $\\Gamma(\\alpha) = \\int\\limits_{0}^{+\\infinity} t^{\\alpha - 1} e^{-t} dt$.\n- Here's some table ref: ([@tab:function ]) \n- Explained in [^source2]\n", ls![- tx!("Item 1"), - tks![tx!("Gamma-function def:"), eq!(!"\\Gamma(\\alpha) = \\int\\limits_{0}^{+\\infinity} t^{\\alpha - 1} e^{-t} dt"), tx!(".")], + tks![tx!("Gamma-function def:"), eq!(^!"\\Gamma(\\alpha) = \\int\\limits_{0}^{+\\infinity} t^{\\alpha - 1} e^{-t} dt"), tx!(".")], tks![tx!("Here's some table ref: ("), rf!(@"tab:function"), tx!(")")], - tks![tx!("Explained in"), rf!(^"source2")] + tks![tx!("Explained in"), rf!(^^"source2")] ], "\n"} test_ok! {ok_left_over, "\n- Item 1 \n- Gamma-function def: $\\Gamma(\\alpha) = \\int\\limits_{0}^{+\\infinity} t^{\\alpha - 1} e^{-t} dt$.\n- Here's some table ref: ([@tab:function ]) \n- Explained in [^source2]\nІще трохи тексту після списку", ls![- tx!("Item 1"), - tks![tx!("Gamma-function def:"), eq!(!"\\Gamma(\\alpha) = \\int\\limits_{0}^{+\\infinity} t^{\\alpha - 1} e^{-t} dt"), tx!(".")], + tks![tx!("Gamma-function def:"), eq!(^!"\\Gamma(\\alpha) = \\int\\limits_{0}^{+\\infinity} t^{\\alpha - 1} e^{-t} dt"), tx!(".")], tks![tx!("Here's some table ref: ("), rf!(@"tab:function"), tx!(")")], - tks![tx!("Explained in"), rf!(^"source2")] + tks![tx!("Explained in"), rf!(^^"source2")] ], "\nІще трохи тексту після списку"} } @@ -2137,9 +2162,9 @@ $$ tks![head!(# "Header"), head!(## "header2"), tx!("Це початок тексту.", |), ls!(- tx!("А це перший елемент списку"), tx!("І другий елемент списку")), tx!("А оце буде рівняння:", |), eq!("y = x ^ 2")]} test_ok! {ok_idk1, "На світлині [@ fig:refurb] можна бачити принципову схему влаштування рефурбалідзера. Неважно помитити великі очі[^1], і два термоядерних реактори, що оточують їх. Точні реакції всередині невідомі, але цілком можливо що там женуть $du + t -> {}^{5}_{6}C$.\n\n[^1]: Для поливу.", - tks![tx!("На світлині"), rf!(@ "fig:refurb"), - tx!("можна бачити принципову схему влаштування рефурбалідзера. Неважно помитити великі очі"), rf!(^"1"), - tx!(", і два термоядерних реактори, що оточують їх. Точні реакції всередині невідомі, але цілком можливо що там женуть"), eq!(!"du + t -> {}^{5}_{6}C"), + tks![tx!("На світлині"), rf!(^@ "fig:refurb"), + tx!(^"можна бачити принципову схему влаштування рефурбалідзера. Неважно помитити великі очі"), rf!(^"1"), + tx!(", і два термоядерних реактори, що оточують їх. Точні реакції всередині невідомі, але цілком можливо що там женуть"), eq!(^!"du + t -> {}^{5}_{6}C"), tx!("."), foot!(^"1": tx!("Для поливу."))]} test_ok! {ok_idk2, "\n\n## Підпункт\n\n", head!(##"Підпункт")} } diff --git a/tests/lex_files/ayano.txt b/tests/lex_files/ayano.txt index dcd6fe9..a12a15c 100644 --- a/tests/lex_files/ayano.txt +++ b/tests/lex_files/ayano.txt @@ -3,17 +3,20 @@ Multiple { [ Paragraph { is_newline: false, + space_before: false, formatting: None, content: "This is a test of ayano-generated stuff.", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "This is supposed to be a generated figure:", }, Ayano { data: AyanoBlock { is_display: false, + is_space_before: false, is_static: false, code: "@fig: src=\"circle.png\", ident=\"circle\", caption=\"Some caption for a line drawing. This is parsed as a *regular document text*, so you may [@eq:line] and $f(x) = A \\cdot x^{1} + B \\cdot x^{0}$ here, for example\"", insert_path: None, @@ -21,12 +24,14 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Here's a table with specified width:", }, Ayano { data: AyanoBlock { is_display: false, + is_space_before: false, is_static: false, code: "width = 0.2\n@fig: src = \"graph.png\", width = width", insert_path: None, @@ -34,12 +39,14 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "This is expected to be a generated table:", }, Ayano { data: AyanoBlock { is_display: false, + is_space_before: false, is_static: false, code: "data = [\n\t\t[\"x\", \"y\"],\n\t\t[1.0, 3.0],\n\t\t[3.0, 4.0],\n\t\t[-1.0, -5743234.0],\n]\n@gen_table: lambda r,c: data[r][c]; rows=4, columns=2, caption=\"Caption for a table 1, *because* why not\", ident=\"table1\"", insert_path: None, diff --git a/tests/lex_files/basic.txt b/tests/lex_files/basic.txt index 9d43b1c..b6073ff 100644 --- a/tests/lex_files/basic.txt +++ b/tests/lex_files/basic.txt @@ -7,6 +7,7 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Тут трохи тексту і пояснень про квантові мультивироджені рефурбалідзери змішаного типу. Це нудно, тож тримайте формулу:", }, @@ -22,6 +23,7 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Рефурбалідзери можуть виявитись дуже важливими для збереження екологічної безпеки нашої планети. До прикладу, в них є наступні переваги:", }, @@ -34,10 +36,12 @@ Multiple { [ Paragraph { is_newline: false, + space_before: false, formatting: None, content: "Нормувальний множник", }, InlineMath { + space_before: true, content: "|\\psi|^{2} = 3/4", }, ], @@ -48,10 +52,12 @@ Multiple { [ Paragraph { is_newline: false, + space_before: false, formatting: None, content: "Кількість хромосом -", }, InlineMath { + space_before: true, content: "\\exp(\\pi)", }, ], @@ -59,6 +65,7 @@ Multiple { }, Paragraph { is_newline: false, + space_before: false, formatting: None, content: "Поживна цінність - 20 дптр/см", }, @@ -70,6 +77,7 @@ Multiple { caption: Some( Paragraph { is_newline: false, + space_before: false, formatting: None, content: "Принципове влаштування рефурбалідзера", }, @@ -83,36 +91,44 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "На світлині", }, Reference { + space_before: true, ident: "fig:refurb", }, Paragraph { is_newline: false, + space_before: true, formatting: None, content: "можна бачити принципову схему влаштування рефурбалідзера. Неважно помітити великі очі", }, FootnoteReference { + space_before: false, ident: "1", }, Paragraph { is_newline: false, + space_before: false, formatting: None, content: ", і два термоядерних реактори, що оточують їх. Точні реакції всередині невідомі, але цілком можливо що там женуть", }, InlineMath { + space_before: true, content: "du + t -> {}^{5}_{6}C", }, Paragraph { is_newline: false, + space_before: false, formatting: None, content: "...", }, FootnoteContent { content: Paragraph { is_newline: false, + space_before: false, formatting: None, content: "Для поливу.", }, diff --git a/tests/lex_files/deceit.txt b/tests/lex_files/deceit.txt index 40ebaf2..45e4efb 100644 --- a/tests/lex_files/deceit.txt +++ b/tests/lex_files/deceit.txt @@ -3,6 +3,7 @@ Multiple { [ Paragraph { is_newline: false, + space_before: false, formatting: None, content: "This test is dedicated to attempts of parser deception, i.e. usage of special symbols in wrong places to see if parser recognizes them as control symbols. There we go:", }, @@ -12,38 +13,45 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Page dividers are represented by ------", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "If the line above has dashes ^^^^^^ here, it probably works alright.", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Here's a genuine divider", }, PageDiv, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Fun fact: there can be a paragraph right after it!", }, PageDiv, Paragraph { is_newline: false, + space_before: true, formatting: None, content: "Some text, cause why not", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "But this", }, Paragraph { is_newline: false, + space_before: true, formatting: Some( Bold, ), @@ -51,6 +59,7 @@ Multiple { }, Paragraph { is_newline: false, + space_before: true, formatting: None, content: "look cursed, don't do it.", }, @@ -60,11 +69,13 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Headers are most recognizable by '#'-symbol. For it to actually represent a header, it needs to at the beginning of the line and there should be no more than 6 of them.", }, Paragraph { is_newline: false, + space_before: true, formatting: Some( Bold, ), @@ -72,11 +83,13 @@ Multiple { }, Paragraph { is_newline: false, + space_before: true, formatting: None, content: "up to the end of line will be included into a header, then.", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Example:", }, @@ -86,21 +99,25 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Here's an example of incorrect header:", }, Paragraph { is_newline: true, + space_before: true, formatting: None, content: "###### This header is considered incorrect for this application, and will be parsed as regular paragraph instead", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Another example of incorrect header, featuring 7 hash-symbols:", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "####### This will be parsed as a regular paragraph", }, @@ -110,24 +127,29 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "There are two types of equations in markdown/latex: inline mathmode and display mathmode", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Both of them are defined by $-symbols. However, they must be paired to actually represent these. Here are examples:", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Inline:", }, InlineMath { + space_before: true, content: "y = x ^ 2", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Display:", }, @@ -137,26 +159,31 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Note that display mathmode should ALWAYS start from a new line for this application.", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Basically, there's not much to deceive for: you can do that:", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "$", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "$", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "(the above should not be mathmode), and that's it, basically :sad:", }, @@ -166,16 +193,19 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Tables are defined by |-symbol. But the actual table will happen only if they are the first thing on the line.", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Due to current implementation, there's no cure apart from display mathmode and code:", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "(this should be display mathmode):", }, @@ -185,6 +215,7 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "(this should be a code block):", }, @@ -200,6 +231,7 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Figure is defined by a ![[ sequence at a start of the line. Same points should be uphold here:", }, @@ -217,6 +249,7 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "These start with []", }, @@ -226,15 +259,18 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "These are followed by parentheses (). Nothing is allowed between them, thus", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Here's a correct href:", }, Href { + space_before: true, url: Url { scheme: "https", cannot_be_a_base: false, @@ -254,16 +290,19 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Here are incorrect refs:", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "[my gh page] (https://youtu.be/rickroll", }, Paragraph { is_newline: false, + space_before: false, formatting: Some( Italic, ), @@ -271,21 +310,25 @@ Multiple { }, Paragraph { is_newline: false, + space_before: false, formatting: None, content: "lol)", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "[my gh page]", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "(https://youtu.be/rickroll", }, Paragraph { is_newline: false, + space_before: false, formatting: Some( Italic, ), @@ -293,6 +336,7 @@ Multiple { }, Paragraph { is_newline: false, + space_before: false, formatting: None, content: "lol)", }, @@ -302,47 +346,57 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "These are started by [^.", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Here are correct footnote refs:", }, FootnoteReference { + space_before: true, ident: "1", }, Paragraph { is_newline: false, + space_before: false, formatting: None, content: ",", }, FootnoteReference { + space_before: true, ident: "source", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Here are incorrect ones: [^1, ^1],", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "[^", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "1", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "]", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "So on", }, @@ -352,44 +406,53 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "These refer to objects like figures, tables and equations.", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Started by [@.", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Here's a correct ref:", }, Reference { + space_before: true, ident: "fig:experiment_schema", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Here's are bad ones: [@ fig:1, @ source3],", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "[@", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "eq2", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "]", }, Paragraph { is_newline: true, + space_before: false, formatting: Some( Bold, ), @@ -397,11 +460,13 @@ Multiple { }, Paragraph { is_newline: false, + space_before: false, formatting: None, content: ": despite object types being prepended with 'eq', 'fig' or 'tab' in refs, you", }, Paragraph { is_newline: false, + space_before: true, formatting: Some( Italic, ), @@ -409,6 +474,7 @@ Multiple { }, Paragraph { is_newline: false, + space_before: true, formatting: None, content: "use regular latex to define your custom refs. In fact, LaTeX doesn't care about all of these at all; I'm doing these transformations just for my own comfort, as I've been doing this manually.", }, @@ -418,11 +484,13 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "This object's type is deprecated and will be removed soon.", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "It starts and ends with", }, @@ -432,16 +500,19 @@ Multiple { [ Paragraph { is_newline: false, + space_before: false, formatting: None, content: "* or _", }, Paragraph { is_newline: false, + space_before: false, formatting: None, content: "** or __", }, Paragraph { is_newline: false, + space_before: false, formatting: None, content: "~~", }, @@ -450,6 +521,7 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: Some( Bold, ), @@ -457,11 +529,13 @@ Multiple { }, Paragraph { is_newline: false, + space_before: false, formatting: None, content: ": As of now, these", }, Paragraph { is_newline: false, + space_before: true, formatting: Some( Italic, ), @@ -469,11 +543,13 @@ Multiple { }, Paragraph { is_newline: false, + space_before: true, formatting: None, content: "cause cause massive problems if encountered at the start of the line.", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Basically, parsers for these tokens attempt parse everything up until they find a closing token. But if they are inserted in a middle of the text, then it's ok - they will be limited by current paragraph, and (most likely) will find no match inside it, if their use was unintended.", }, @@ -483,25 +559,30 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "These start with '", }, FootnoteReference { + space_before: false, ident: "ID", }, Paragraph { is_newline: false, + space_before: false, formatting: None, content: ":' and proceed to the end of line.", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Example of valid content:", }, FootnoteContent { content: Paragraph { is_newline: false, + space_before: false, formatting: None, content: "Explanation of source2.", }, @@ -509,45 +590,55 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Example of invalid content:", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "[^source2: This is invalid content", }, FootnoteReference { + space_before: false, ident: "source2", }, Paragraph { is_newline: false, + space_before: true, formatting: None, content: ": This is invalid content", }, FootnoteReference { + space_before: true, ident: "source2", }, Paragraph { is_newline: false, + space_before: false, formatting: None, content: ": This is invalid content", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "They also must start at the beginning of the line. If fact, this:", }, FootnoteReference { + space_before: true, ident: "source2", }, Paragraph { is_newline: false, + space_before: false, formatting: None, content: ": Some content", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Is expected to be a footnote ref with a colon.", }, @@ -557,11 +648,13 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "There are two fundamental types of code blocks: normal code block and", }, Paragraph { is_newline: false, + space_before: true, formatting: Some( Bold, ), @@ -569,6 +662,7 @@ Multiple { }, Paragraph { is_newline: false, + space_before: true, formatting: None, content: "code block.", }, @@ -578,11 +672,13 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "These start and end with '```' on a newline. There can be", }, Paragraph { is_newline: false, + space_before: true, formatting: Some( Bold, ), @@ -590,6 +686,7 @@ Multiple { }, Paragraph { is_newline: false, + space_before: true, formatting: None, content: "inside. Example:", }, @@ -601,6 +698,7 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "There can be anything after code block had closed:", }, @@ -612,36 +710,43 @@ Multiple { }, Paragraph { is_newline: false, + space_before: true, formatting: None, content: "Here's some text that's ok to be here", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "There can't be any spaces before delimiting ```s:", }, Paragraph { is_newline: true, + space_before: true, formatting: None, content: "```python", }, Paragraph { is_newline: true, + space_before: true, formatting: None, content: "# This here is not really a python block", }, Paragraph { is_newline: true, + space_before: true, formatting: None, content: "x = 2", }, Paragraph { is_newline: true, + space_before: true, formatting: None, content: "print(x)", }, Paragraph { is_newline: true, + space_before: true, formatting: None, content: "```", }, @@ -651,6 +756,7 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: Some( Italic, ), @@ -658,11 +764,13 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "This object looks and behaves just like normal code block, but is only limited to", }, Paragraph { is_newline: false, + space_before: true, formatting: Some( Italic, ), @@ -670,17 +778,20 @@ Multiple { }, Paragraph { is_newline: false, + space_before: true, formatting: None, content: "language, can have spacial arguments in a language line, may leave something behind in the document. For example:", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "This Ayano block will leave plain \"2\" behind it:", }, Ayano { data: AyanoBlock { is_display: false, + is_space_before: false, is_static: false, code: "2", insert_path: None, @@ -688,6 +799,7 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Akin to normal blocks, it's ok to leave something trailing:", }, @@ -699,11 +811,13 @@ Multiple { }, Paragraph { is_newline: false, + space_before: true, formatting: None, content: "This text is ok to be here", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "And these are invalid Ayano blocks:", }, @@ -715,16 +829,19 @@ Multiple { }, Paragraph { is_newline: true, + space_before: true, formatting: None, content: "```ayano", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "2", }, Paragraph { is_newline: true, + space_before: true, formatting: None, content: "```", }, diff --git a/tests/lex_files/tables_code.txt b/tests/lex_files/tables_code.txt index 0c7e077..4bf00ee 100644 --- a/tests/lex_files/tables_code.txt +++ b/tests/lex_files/tables_code.txt @@ -3,6 +3,7 @@ Multiple { [ Paragraph { is_newline: false, + space_before: false, formatting: None, content: "Наступний тест. Тут протестуємо таблиці і блоки коду.", }, @@ -12,11 +13,13 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Звичайні таблиці; Можуть всередині мати", }, Paragraph { is_newline: false, + space_before: true, formatting: Some( Bold, ), @@ -24,11 +27,13 @@ Multiple { }, Paragraph { is_newline: false, + space_before: true, formatting: None, content: "чи", }, Paragraph { is_newline: false, + space_before: true, formatting: Some( Italic, ), @@ -36,6 +41,7 @@ Multiple { }, Paragraph { is_newline: false, + space_before: true, formatting: None, content: "на інші штуки", }, @@ -44,16 +50,19 @@ Multiple { [ Paragraph { is_newline: false, + space_before: true, formatting: None, content: "Storage", }, Paragraph { is_newline: false, + space_before: true, formatting: None, content: "Space", }, Paragraph { is_newline: false, + space_before: true, formatting: None, content: "Number", }, @@ -63,30 +72,36 @@ Multiple { [ Paragraph { is_newline: false, + space_before: true, formatting: None, content: "1", }, Paragraph { is_newline: false, + space_before: true, formatting: None, content: "2", }, InlineMath { + space_before: true, content: "3 + e ^{\\pi}", }, Multiple { tokens: Owned( [ InlineMath { + space_before: true, content: "e", }, FootnoteReference { + space_before: false, ident: "euler", }, ], ), }, InlineMath { + space_before: true, content: "\\cdot \\pm 1", }, Multiple { @@ -94,11 +109,13 @@ Multiple { [ Paragraph { is_newline: false, + space_before: true, formatting: None, content: "І тут ще", }, Paragraph { is_newline: false, + space_before: true, formatting: Some( Italic, ), @@ -106,6 +123,7 @@ Multiple { }, Paragraph { is_newline: false, + space_before: true, formatting: None, content: "тексту \\|", }, @@ -120,11 +138,13 @@ Multiple { [ Paragraph { is_newline: false, + space_before: false, formatting: None, content: "Оце приклад таблиці. Як казав мій вчитель:", }, Paragraph { is_newline: false, + space_before: true, formatting: Some( Bold, ), @@ -132,6 +152,7 @@ Multiple { }, Paragraph { is_newline: false, + space_before: true, formatting: None, content: "(і \\\"лапок\\\")", }, @@ -145,6 +166,7 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Таблиця цілком собі може не мати ідентифікатора або опису:", }, @@ -153,11 +175,13 @@ Multiple { [ Paragraph { is_newline: false, + space_before: true, formatting: None, content: "Кількість", }, Paragraph { is_newline: false, + space_before: true, formatting: None, content: "Роль", }, @@ -167,21 +191,25 @@ Multiple { [ Paragraph { is_newline: false, + space_before: true, formatting: None, content: "3 діоптрії", }, Paragraph { is_newline: false, + space_before: true, formatting: None, content: "7 галон", }, Paragraph { is_newline: false, + space_before: true, formatting: None, content: "rust", }, Paragraph { is_newline: false, + space_before: true, formatting: None, content: "#PANIC!!!", }, @@ -198,11 +226,13 @@ Multiple { [ Paragraph { is_newline: false, + space_before: false, formatting: None, content: "Це число Ейлера. Воно дуже корисне,", }, Paragraph { is_newline: false, + space_before: true, formatting: Some( StrikeThrough, ), @@ -210,11 +240,13 @@ Multiple { }, Paragraph { is_newline: false, + space_before: true, formatting: None, content: "у вигляді", }, Paragraph { is_newline: false, + space_before: true, formatting: Some( Italic, ), @@ -222,6 +254,7 @@ Multiple { }, Paragraph { is_newline: false, + space_before: true, formatting: None, content: "дробу.", }, @@ -236,24 +269,29 @@ Multiple { }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "Наразі мають досить обмежену підтримку, адже у", }, InlineMath { + space_before: true, content: "\\LaTeX", }, Paragraph { is_newline: false, + space_before: true, formatting: None, content: "немає нативного поняття блоків коду.", }, Paragraph { is_newline: true, + space_before: false, formatting: None, content: "На щастя, у", }, Paragraph { is_newline: false, + space_before: true, formatting: Some( Bold, ), @@ -261,14 +299,17 @@ Multiple { }, Paragraph { is_newline: false, + space_before: true, formatting: None, content: "таке поняття є", }, FootnoteReference { + space_before: false, ident: "code", }, Paragraph { is_newline: false, + space_before: false, formatting: None, content: ":", }, @@ -284,11 +325,13 @@ Multiple { [ Paragraph { is_newline: false, + space_before: false, formatting: None, content: "Принаймні,", }, Paragraph { is_newline: false, + space_before: true, formatting: Some( Italic, ), @@ -296,6 +339,7 @@ Multiple { }, Paragraph { is_newline: false, + space_before: true, formatting: None, content: "таке робить", }, diff --git a/tests/transformed_files/ayano.txt b/tests/transformed_files/ayano.txt index e162ad5..8246c44 100644 --- a/tests/transformed_files/ayano.txt +++ b/tests/transformed_files/ayano.txt @@ -3,7 +3,7 @@ This is a test of ayano-generated stuff. \begin{figure}[h!] \centering \includegraphics[width=0.9\textwidth]{circle.png} -\caption{\capfnt Some caption for a line drawing. This is parsed as a\textit{regular document text}, so you may\ref{eq:line}and$f(x) = A \cdot x^{1} + B \cdot x^{0}$here, for example} +\caption{\capfnt Some caption for a line drawing. This is parsed as a \textit{regular document text}, so you may \ref{eq:line} and $f(x) = A \cdot x^{1} + B \cdot x^{0}$ here, for example} \label{fig:circle} \end{figure} @@ -24,7 +24,7 @@ x & y\\ \hline -1.0 & -5743234.0\\ \hline \end{tabular} \stepcounter{tabnum} -\caption{\capfnt Caption for a table 1,\textit{because}why not} +\caption{\capfnt Caption for a table 1, \textit{because} why not} \label{tab:table1} \end{center} \end{table} diff --git a/tests/transformed_files/basic.txt b/tests/transformed_files/basic.txt index 2826b07..cea582c 100644 --- a/tests/transformed_files/basic.txt +++ b/tests/transformed_files/basic.txt @@ -10,8 +10,8 @@ Ref(z) = \int \limits_{0}^{1} t^{z-1} \cdot \exp(-t) dt \tbln Рефурбалідзери можуть виявитись дуже важливими для збереження екологічної безпеки нашої планети. До прикладу, в них є наступні переваги: \begin{itemize} -\item Нормувальний множник$|\psi|^{2} = 3/4$ -\item Кількість хромосом -$\exp(\pi)$ +\item Нормувальний множник $|\psi|^{2} = 3/4$ +\item Кількість хромосом - $\exp(\pi)$ \item Поживна цінність - 20 дптр/см \end{itemize} \begin{figure}[h!] @@ -21,4 +21,4 @@ Ref(z) = \int \limits_{0}^{1} t^{z-1} \cdot \exp(-t) dt \label{fig:refurb} \end{figure} -\tbln На світлині\ref{fig:refurb}можна бачити принципову схему влаштування рефурбалідзера. Неважно помітити великі очі\footnotemark[1]\footnotetext[1]{Для поливу.}, і два термоядерних реактори, що оточують їх. Точні реакції всередині невідомі, але цілком можливо що там женуть$du + t -> {}^{5}_{6}C$... \ No newline at end of file +\tbln На світлині \ref{fig:refurb} можна бачити принципову схему влаштування рефурбалідзера. Неважно помітити великі очі\footnotemark[1]\footnotetext[1]{Для поливу.}, і два термоядерних реактори, що оточують їх. Точні реакції всередині невідомі, але цілком можливо що там женуть $du + t -> {}^{5}_{6}C$... \ No newline at end of file diff --git a/tests/transformed_files/deceit.txt b/tests/transformed_files/deceit.txt index 2db3589..1aa0f37 100644 --- a/tests/transformed_files/deceit.txt +++ b/tests/transformed_files/deceit.txt @@ -8,11 +8,11 @@ This test is dedicated to attempts of parser deception, i.e. usage of special sy \tbln Fun fact: there can be a paragraph right after it! \clearpage -Some text, cause why not -\tbln But this\textbf{DOES}look cursed, don't do it. + Some text, cause why not +\tbln But this \textbf{DOES} look cursed, don't do it. \subsection{Headers} -\tbln Headers are most recognizable by '#'-symbol. For it to actually represent a header, it needs to at the beginning of the line and there should be no more than 6 of them.\textbf{ANYTHING}up to the end of line will be included into a header, then. +\tbln Headers are most recognizable by '#'-symbol. For it to actually represent a header, it needs to at the beginning of the line and there should be no more than 6 of them. \textbf{ANYTHING} up to the end of line will be included into a header, then. \tbln Example: \textbf{This is a long, yet formally correct header. You can add $equations$ and *formatting* here, but I provide no guarantees as of the way these would display in final document.} @@ -24,7 +24,7 @@ Some text, cause why not \tbln There are two types of equations in markdown/latex: inline mathmode and display mathmode \tbln Both of them are defined by $-symbols. However, they must be paired to actually represent these. Here are examples: -\tbln Inline:$y = x ^ 2$ +\tbln Inline: $y = x ^ 2$ \tbln Display: \begin{equation} y = x ^ 2 @@ -69,7 +69,7 @@ head /dev/random \subsubsection{Hyperrefs} \tbln These are followed by parentheses (). Nothing is allowed between them, thus -\tbln Here's a correct href:\href{https://youtu.be/rickroll_link_lol}{my gh page} +\tbln Here's a correct href: \href{https://youtu.be/rickroll_link_lol}{my gh page} \tbln Here are incorrect refs: \tbln [my gh page] (https://youtu.be/rickroll\textit{link}lol) \tbln [my gh page] @@ -77,7 +77,7 @@ head /dev/random \subsubsection{Footnote refs} \tbln These are started by [^. -\tbln Here are correct footnote refs:\footnotemark[1],\footnotemark[2] +\tbln Here are correct footnote refs: \footnotemark[1], \footnotemark[2] \tbln Here are incorrect ones: [^1, ^1], \tbln [^ \tbln 1 @@ -87,12 +87,12 @@ head /dev/random \tbln These refer to objects like figures, tables and equations. \tbln Started by [@. -\tbln Here's a correct ref:\ref{fig:experiment_schema} +\tbln Here's a correct ref: \ref{fig:experiment_schema} \tbln Here's are bad ones: [@ fig:1, @ source3], \tbln [@ \tbln eq2 \tbln ] -\tbln \textbf{NOTE}: despite object types being prepended with 'eq', 'fig' or 'tab' in refs, you\textit{can}use regular latex to define your custom refs. In fact, LaTeX doesn't care about all of these at all; I'm doing these transformations just for my own comfort, as I've been doing this manually. +\tbln \textbf{NOTE}: despite object types being prepended with 'eq', 'fig' or 'tab' in refs, you \textit{can} use regular latex to define your custom refs. In fact, LaTeX doesn't care about all of these at all; I'm doing these transformations just for my own comfort, as I've been doing this manually. \subsection{Formatting} \tbln This object's type is deprecated and will be removed soon. @@ -102,22 +102,22 @@ head /dev/random \item ** or __ \item ~~ \end{itemize} -\tbln \textbf{WARN}: As of now, these\textit{might}cause cause massive problems if encountered at the start of the line. +\tbln \textbf{WARN}: As of now, these \textit{might} cause cause massive problems if encountered at the start of the line. \tbln Basically, parsers for these tokens attempt parse everything up until they find a closing token. But if they are inserted in a middle of the text, then it's ok - they will be limited by current paragraph, and (most likely) will find no match inside it, if their use was unintended. \subsection{Footnote content} \tbln These start with '\footnotemark[3]:' and proceed to the end of line. \tbln Example of valid content: \tbln Example of invalid content: -\tbln [^source2: This is invalid content\footnotemark[4]\footnotetext[4]{Explanation of source2.}: This is invalid content\footnotemark[4]: This is invalid content -\tbln They also must start at the beginning of the line. If fact, this:\footnotemark[4]: Some content +\tbln [^source2: This is invalid content\footnotemark[4]\footnotetext[4]{Explanation of source2.} : This is invalid content \footnotemark[4]: This is invalid content +\tbln They also must start at the beginning of the line. If fact, this: \footnotemark[4]: Some content \tbln Is expected to be a footnote ref with a colon. \subsection{Code blocks} -\tbln There are two fundamental types of code blocks: normal code block and\textbf{Ayano}code block. +\tbln There are two fundamental types of code blocks: normal code block and \textbf{Ayano} code block. \subsubsection{Normal code block} -\tbln These start and end with '```' on a newline. There can be\textbf{practically anything}inside. Example: +\tbln These start and end with '```' on a newline. There can be \textbf{practically anything} inside. Example: \begin{minted}{java} int x = 2; int y = 4; @@ -134,7 +134,7 @@ local y = 'y' local z = x .. y -- matty!! \end{minted} -Here's some text that's ok to be here + Here's some text that's ok to be here \tbln There can't be any spaces before delimiting ```s: \tbln ```python \tbln # This here is not really a python block @@ -144,14 +144,14 @@ Here's some text that's ok to be here \subsubsection{Ayano code block} \tbln \textit{because Ayano loves Kyoko} -\tbln This object looks and behaves just like normal code block, but is only limited to\textit{python}language, can have spacial arguments in a language line, may leave something behind in the document. For example: +\tbln This object looks and behaves just like normal code block, but is only limited to \textit{python} language, can have spacial arguments in a language line, may leave something behind in the document. For example: \tbln This Ayano block will leave plain "2" behind it:2 \tbln Akin to normal blocks, it's ok to leave something trailing: \begin{minted}{ayano} 2 \end{minted} -This text is ok to be here + This text is ok to be here \tbln And these are invalid Ayano blocks: \begin{minted}{ayano} 2 diff --git a/tests/transformed_files/tables_code.txt b/tests/transformed_files/tables_code.txt index d761e97..20cb473 100644 --- a/tests/transformed_files/tables_code.txt +++ b/tests/transformed_files/tables_code.txt @@ -1,17 +1,17 @@ Наступний тест. Тут протестуємо таблиці і блоки коду. \subsubsection{Таблиці} -\tbln Звичайні таблиці; Можуть всередині мати\textbf{формули}чи\textit{посилання}на інші штуки +\tbln Звичайні таблиці; Можуть всередині мати \textbf{формули} чи \textit{посилання} на інші штуки \begin{table}[h!] \begin{center} \begin{tabular}{|c|c|c|}\hline -Storage & Space & Number\\ \hline + Storage & Space & Number\\ \hline \hline -1 & 2 & $3 + e ^{\pi}$\\ \hline -$e$\footnotemark[1]\footnotetext[1]{Це число Ейлера. Воно дуже корисне,\st{але не може бути записане}у вигляді\textit{десяткового}дробу.} & $\cdot \pm 1$ & І тут ще\textit{трохи}тексту \|\\ \hline + 1 & 2 & $3 + e ^{\pi}$\\ \hline + $e$\footnotemark[1]\footnotetext[1]{Це число Ейлера. Воно дуже корисне, \st{але не може бути записане} у вигляді \textit{десяткового} дробу.} & $\cdot \pm 1$ & І тут ще \textit{трохи} тексту \|\\ \hline \end{tabular} \stepcounter{tabnum} -\caption{\capfnt Оце приклад таблиці. Як казав мій вчитель:\textbf{ВИКОРИСТОВУЙ БІЛЬШЕ ВИБУХІВКИ}(і "лапок")} +\caption{\capfnt Оце приклад таблиці. Як казав мій вчитель: \textbf{ВИКОРИСТОВУЙ БІЛЬШЕ ВИБУХІВКИ} (і "лапок")} \label{tab:example_1} \end{center} \end{table} @@ -20,10 +20,10 @@ $e$\footnotemark[1]\footnotetext[1]{Це число Ейлера. Воно ду \begin{table}[h!] \begin{center} \begin{tabular}{|c|c|}\hline -Кількість & Роль\\ \hline + Кількість & Роль\\ \hline \hline -3 діоптрії & 7 галон\\ \hline -rust & #PANIC!!!\\ \hline + 3 діоптрії & 7 галон\\ \hline + rust & #PANIC!!!\\ \hline \end{tabular} \stepcounter{tabnum}\label{tab:table_2} \end{center} @@ -31,8 +31,8 @@ rust & #PANIC!!!\\ \hline \subsubsection{Блоки коду} -\tbln Наразі мають досить обмежену підтримку, адже у$\LaTeX$немає нативного поняття блоків коду. -\tbln На щастя, у\textbf{markdown}таке поняття є\footnotemark[2]\footnotetext[2]{Принаймні,\textit{Obsidian}таке робить}: +\tbln Наразі мають досить обмежену підтримку, адже у $\LaTeX$ немає нативного поняття блоків коду. +\tbln На щастя, у \textbf{markdown} таке поняття є\footnotemark[2]\footnotetext[2]{Принаймні, \textit{Obsidian} таке робить}: \begin{minted}{rust} let x = 5; let y = x.powi(2); From d15b2e0fd7b5a2fafb00dd7dad079b5b60f4deff Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Tue, 26 Mar 2024 00:54:34 +0200 Subject: [PATCH 39/98] Added preamble and postamble support --- src/data.rs | 16 +++ src/gen/lab/lab_preamble.tex | 50 +++++++- src/gen/lab/meta.rs | 4 +- src/gen/lab/mod.rs | 76 ++++++++++- src/gen/mod.rs | 15 +++ src/lib.rs | 15 ++- tests/transform.rs | 6 +- tests/transformed_files/ayano.txt | 161 +++++++++++++++++++++++ tests/transformed_files/basic.txt | 162 +++++++++++++++++++++++- tests/transformed_files/deceit.txt | 162 +++++++++++++++++++++++- tests/transformed_files/tables_code.txt | 161 +++++++++++++++++++++++ 11 files changed, 815 insertions(+), 13 deletions(-) diff --git a/src/data.rs b/src/data.rs index d16dda0..3ecb164 100644 --- a/src/data.rs +++ b/src/data.rs @@ -6,6 +6,22 @@ use std::{ use url::Url; +#[derive(Debug, Default)] +pub struct TitleInfo<'source> { + pub header_line1: Option>, + pub header_line2: Option>, + pub document_type: Option>, + pub title_line1: Option>, + pub title_line2: Option>, + pub title_line3: Option>, + pub title_line4: Option>, + pub author_line1: Option>, + pub author_line2: Option>, + pub author_line3: Option>, + pub date: Option>, + pub prof: Option>, +} + // For whatever reason, std library Cow requires ToOwned for it's contained value to be cloned // That makes NO SENSE in my case. // I do not need a functionality to obtain owned value here, diff --git a/src/gen/lab/lab_preamble.tex b/src/gen/lab/lab_preamble.tex index 8c9f433..8ccd597 100644 --- a/src/gen/lab/lab_preamble.tex +++ b/src/gen/lab/lab_preamble.tex @@ -1,5 +1,5 @@ \documentclass[a4paper]{article} -\usepackage{graphicx, hyperref, xcolor, gensymb, amssymb, mathtools, wrapfig, mathtools, microtype, lastpage, caption, titlesec, paracol, longtable, booktabs, cancel, soul} +\usepackage{graphicx, hyperref, xcolor, gensymb, amssymb, mathtools, wrapfig, mathtools, microtype, lastpage, caption, titlesec, paracol, longtable, booktabs, cancel} \usepackage[T2A, T1]{fontenc} \usepackage[utf8]{inputenc} \usepackage[russian, ukrainian]{babel} @@ -109,4 +109,50 @@ % \let\oldCaption\caption % \renewcommand{\caption}[1] { % \oldCaption{\fnt #1} -% } \ No newline at end of file +% } + +% markdown strikeouts +\usepackage{soul} + +% codeblocks +\usepackage{minted} + +\titleformat*{\section}{\fnt\LARGE\bfseries} +\titleformat*{\subsection}{\fnt\Large\bfseries} +\titleformat*{\subsubsection}{\fnt\Large} + +\begin{document} +\thispagestyle{empty} % [A]:for cropping +% ------------ +% source: https://tex.stackexchange.com/questions/173651/need-help-to-create-such-a-beautiful-title-page +\begingroup +\centering +\obeylines +\topskip0pt % source: https://tex.stackexchange.com/questions/2326/vertically-center-text-on-a-page +\vspace*{\fill} +\textbf{ +\LARGE $header-line1$ +\LARGE $header-line2$ +\vspace{100pt} +\LARGE $document_type$ +\Large $title-line1$ +\Large $title-line2$ +\Large $title-line3$ +\huge $title-line4$ +\vspace{50pt} +\large $author-line1$ +\large $author-line2$ +\large $author-line3$ +\large $date$ +\vspace*{\fill} +\hspace{\fill}\large $prof$ \rule{20mm}{0.4pt} +\vspace{10pt} +} +\endgroup +% ------------ +\newpage +\tableofcontents + +\newpage +\fnt + diff --git a/src/gen/lab/meta.rs b/src/gen/lab/meta.rs index a4b337b..e1b45bf 100644 --- a/src/gen/lab/meta.rs +++ b/src/gen/lab/meta.rs @@ -6,7 +6,7 @@ use std::{ use itertools::Itertools; use pyo3::PyErr; -use crate::data::Token; +use crate::data::{TitleInfo, Token}; use super::super::ayano::{self, AyanoBuilder, AyanoExecutor}; @@ -21,6 +21,7 @@ pub struct SourceMeta<'source, Ayano> { pub footnotes: HashMap<&'source str, Token<'source>>, pub refs: HashSet<&'source str>, pub ayano: Ayano, + pub title_info: TitleInfo<'source>, } impl<'meta> SourceMeta<'meta, AyanoBuilder> { @@ -42,6 +43,7 @@ impl<'meta> SourceMeta<'meta, AyanoBuilder> { ayano: self.ayano.initialize()?, footnotes: self.footnotes, refs: self.refs, + title_info: self.title_info, }) } } diff --git a/src/gen/lab/mod.rs b/src/gen/lab/mod.rs index dc93e41..1a6d96c 100644 --- a/src/gen/lab/mod.rs +++ b/src/gen/lab/mod.rs @@ -5,9 +5,10 @@ pub use context::Context; pub use meta::SourceMeta; use itertools::Itertools; +use regex::Replacer; use crate::{ - data::{Formatting, Token}, + data::{Formatting, TitleInfo, Token}, path_engine::PathEngine, }; use std::{borrow::Borrow, fmt::Write, marker::PhantomData}; @@ -326,6 +327,77 @@ where }; Ok(()) } + + fn write_preamble<'meta, W: Write + ?Sized>( + &self, + output: &mut W, + meta: &'meta SourceMeta<'source, AyanoExecutor>, + ) -> Res<'source> { + let mut preamble = include_str!("lab_preamble.tex").to_string(); + const PATTERNS: [&'static str; 12] = [ + "$header-line1$", + "$header-line2$", + "$document-type$", + "$title-line1$", + "$title-line2$", + "$title-line3$", + "$title-line4$", + "$author-line1$", + "$author-line2$", + "$author-line3$", + "$date$", + "$prof$", + ]; + let TitleInfo { + header_line1, + header_line2, + document_type, + title_line1, + title_line2, + title_line3, + title_line4, + author_line1, + author_line2, + author_line3, + date, + prof, + } = &meta.title_info; + let replacements = [ + header_line1, + header_line2, + document_type, + title_line1, + title_line2, + title_line3, + title_line4, + author_line1, + author_line2, + author_line3, + date, + prof, + ]; + for i in 0..PATTERNS.len() { + let pat = PATTERNS[i]; + if let Some(ind) = preamble.find(pat) { + preamble.replace_range( + ind..ind + pat.len(), + &replacements[i].as_ref().unwrap_or(&"".into()), + ); + } + } + output.write_str(&preamble)?; + Ok(()) + } + + fn write_postamble<'meta, 'context, W: Write + ?Sized>( + &self, + output: &mut W, + _: &'meta SourceMeta<'source, AyanoExecutor>, + _: &'context mut Context, + ) -> Res<'source> { + output.write_str("\n\n\\end{document}")?; + Ok(()) + } } #[cfg(test)] @@ -695,7 +767,7 @@ y = 0.4 } test! {ayano_fig2, Token::Ayano{data: AyanoBlock{is_display: false,is_static: false,code: r"@fig: src = 'path/to/image.jpg'".into(),insert_path: None, - is_space_before: false,}}, + is_space_before: false,}}, r" \begin{figure}[h!] \centering diff --git a/src/gen/mod.rs b/src/gen/mod.rs index 645a50b..e16c56b 100644 --- a/src/gen/mod.rs +++ b/src/gen/mod.rs @@ -48,4 +48,19 @@ pub trait OutputGenerator<'source, Meta, Context> { .map(|t| self.write_to(output, meta, context, t)) .try_collect() } + fn write_preamble<'meta, W: Write + ?Sized>( + &self, + _: &mut W, + _: &'meta Meta, + ) -> Res<'source> { + Ok(()) + } + fn write_postamble<'meta, 'context, W: Write + ?Sized>( + &self, + _: &mut W, + _: &'meta Meta, + _: &'context mut Context, + ) -> Res<'source> { + Ok(()) + } } diff --git a/src/lib.rs b/src/lib.rs index 1cd8987..a052e23 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,6 +27,7 @@ //! Well, unless there's a way to efficiently write to a single output from multiple threads, while preserving output order. //! Any sort of "collect" function would've destroyed the purpose anyway. +use data::TitleInfo; use gen::OutputGenerator; use lexer::KyomatoLexError; @@ -47,20 +48,28 @@ pub fn lex<'source>(input: &'source str) -> Result, Kyomato } pub fn gen<'token, 'source: 'token>( token: &'token data::Token<'source>, + title_info: impl Into>>, mut output: impl std::io::Write, ) -> Result<(), Box> { - let buf = gen_to_string(token)?; + let buf = gen_to_string(token, title_info)?; // TODO make a transformer struct output.write_all(buf.as_bytes())?; Ok(()) } pub fn gen_to_string<'token, 'source: 'token>( token: &'token data::Token<'source>, + title_info: impl Into>>, ) -> Result> { let generator = gen::lab::LabaLatex::new(path_engine::primitive()); - let meta = gen::lab::SourceMeta::collect(&token)?.init_ayano()?; + let mut meta = gen::lab::SourceMeta::collect(&token)?.init_ayano()?; + if let Some(title_info) = title_info.into() { + meta.title_info = title_info; + } let mut buf = String::new(); - generator.write_to(&mut buf, &meta, &mut gen::lab::Context::default(), token)?; + generator.write_preamble(&mut buf, &meta)?; + let mut context = gen::lab::Context::default(); + generator.write_to(&mut buf, &meta, &mut context, token)?; + generator.write_postamble(&mut buf, &meta, &mut context)?; Ok(buf) } diff --git a/tests/transform.rs b/tests/transform.rs index 7eab252..498603f 100644 --- a/tests/transform.rs +++ b/tests/transform.rs @@ -29,7 +29,7 @@ fn show_transform() { panic!("Should successfully parse the input: {err:#?}"); } }; - let repr = kyomato::gen_to_string(&parsed).expect("Should be able to transform tokens"); + let repr = kyomato::gen_to_string(&parsed, None).expect("Should be able to transform tokens"); println!("{repr}"); std::io::stdin() .read_line(&mut String::new()) @@ -86,7 +86,7 @@ fn gen_transform_output() { output_file }; // write the result to output file - kyomato::gen(&parsed, &mut output_file).expect("Failed to generate output"); + kyomato::gen(&parsed, None, &mut output_file).expect("Failed to generate output"); } } @@ -104,7 +104,7 @@ fn test_transform() { panic!("Should successfully parse the input: {err:#?}"); } }; - let repr = kyomato::gen_to_string(&parsed).expect("Failed to generate output"); + let repr = kyomato::gen_to_string(&parsed, None).expect("Failed to generate output"); // write the result to output directory let expected_output = { output_path.push(&filename); diff --git a/tests/transformed_files/ayano.txt b/tests/transformed_files/ayano.txt index 8246c44..fc29250 100644 --- a/tests/transformed_files/ayano.txt +++ b/tests/transformed_files/ayano.txt @@ -1,3 +1,161 @@ +\documentclass[a4paper]{article} +\usepackage{graphicx, hyperref, xcolor, gensymb, amssymb, mathtools, wrapfig, mathtools, microtype, lastpage, caption, titlesec, paracol, longtable, booktabs, cancel} +\usepackage[T2A, T1]{fontenc} +\usepackage[utf8]{inputenc} +\usepackage[russian, ukrainian]{babel} +\hypersetup{ + colorlinks, + linkcolor={blue!20!black}, + citecolor={blue!50!black}, + urlcolor={blue!80!black} +} + +\usepackage[left = 25mm, right = 10mm, top=20mm, bottom=20mm, bindingoffset=0cm]{geometry} + +% КОМАНДИ + +\newcommand{\fnt}{ +\fontshape{n} +\fontsize{14pt} {19pt} +\linespread{0.8} +\selectfont +} % нормативний шрифт +\let\oldCaption\caption +\renewcommand{\caption}[1]{ +\oldCaption{\fnt #1} +} % підпис нормативним шрифтом. + +\newcommand{\tb}{ + \hspace*{10mm} +} % відступ у 5 символів "x" відповідно до ДСТУ +\newcommand{\tbln}{ + \newline + \hspace*{10mm} +} % те ж саме, але з переносом рядка +\newcommand{\tbsp}{ + \vspace*{1ex} + \newline + \hspace*{10mm} +} % те ж саме, але з додатковим відступом між рядками + +\iffalse +\let\oldSection\section +\renewcommand{\section}[1]{ + \oldSection*{\tb #1} + \addcontentsline{toc}{section}{#1} + \fnt + \tb +} % відображає у тексті та змісті наданий заголовок +\fi + +\iftrue +\let\oldSubSection\subsection +\renewcommand{\subsection}[1]{ + \oldSubSection*{\tb #1} + \addcontentsline{toc}{subsection}{#1} + \fnt + \tb +} % відображає у тексті та змісті наданий підзаголовок +\fi + +% бібліографія +\usepackage[square,sort,comma,numbers]{natbib} +\renewcommand{\bibsection}{} +\usepackage{totcount} +\newtotcounter{citnum} +% лічильник цитувань +\def\oldbibitem{} \let\oldbibitem=\bibitem +\def\bibitem{\stepcounter{citnum}\oldbibitem} + +% лічильник зображень +\newtotcounter{graphnum} +\def\oldincludegraphics{} \let\oldincludegraphics=\includegraphics +\def\includegraphics{\stepcounter{graphnum}\oldincludegraphics} + +% лічильник таблиць (НЕАВТОМАТИЧНИЙ!!!) +\newtotcounter{tabnum} + +% лічильник додатків (НЕАВТОМАТИЧНИЙ!!!) +\newtotcounter{dodnum} + +% нумерація сторінок +\usepackage{fancyhdr} +\pagestyle{fancy} +\fancyhf{} +\renewcommand{\headrulewidth}{0pt} +\setlength{\headheight}{15.3pt} +\fancyhead[R]{\fnt \thepage} + +% виноски +\renewcommand{\thefootnote}{\large\arabic{footnote})~} +\renewcommand{\footnoterule}{\rule{20mm}{0.4pt} \vspace*{0.5ex}} +% \newcommand{\vyn}[2]{ +% \footnote[#1]{\large #2} +% } % створити виноску + +\let\oldFootnote\footnote +\renewcommand{\footnote}[1]{ + \oldFootnote{\large #1} +} % перегрузка виноски + +\providecommand{\tightlist}{% + \setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}} + +% підписи малюнків/таблиць +\captionsetup[figure]{name={\fnt Рисунок~},labelsep=period} +\captionsetup[table]{name={\fnt Таблиця~},labelsep=period} + +% idk why, but this redefinition does not work, for some reason +% \let\oldCaption\caption +% \renewcommand{\caption}[1] { +% \oldCaption{\fnt #1} +% } + +% markdown strikeouts +\usepackage{soul} + +% codeblocks +\usepackage{minted} + +\titleformat*{\section}{\fnt\LARGE\bfseries} +\titleformat*{\subsection}{\fnt\Large\bfseries} +\titleformat*{\subsubsection}{\fnt\Large} + +\begin{document} +\thispagestyle{empty} % [A]:for cropping +% ------------ +% source: https://tex.stackexchange.com/questions/173651/need-help-to-create-such-a-beautiful-title-page +\begingroup +\centering +\obeylines +\topskip0pt % source: https://tex.stackexchange.com/questions/2326/vertically-center-text-on-a-page +\vspace*{\fill} +\textbf{ +\LARGE +\LARGE +\vspace{100pt} +\LARGE $document_type$ +\Large +\Large +\Large +\huge +\vspace{50pt} +\large +\large +\large +\large +\vspace*{\fill} +\hspace{\fill}\large \rule{20mm}{0.4pt} +\vspace{10pt} +} +\endgroup +% ------------ +\newpage +\tableofcontents + +\newpage +\fnt + This is a test of ayano-generated stuff. \tbln This is supposed to be a generated figure: \begin{figure}[h!] @@ -28,3 +186,6 @@ x & y\\ \hline \label{tab:table1} \end{center} \end{table} + + +\end{document} \ No newline at end of file diff --git a/tests/transformed_files/basic.txt b/tests/transformed_files/basic.txt index cea582c..7ec34d7 100644 --- a/tests/transformed_files/basic.txt +++ b/tests/transformed_files/basic.txt @@ -1,3 +1,161 @@ +\documentclass[a4paper]{article} +\usepackage{graphicx, hyperref, xcolor, gensymb, amssymb, mathtools, wrapfig, mathtools, microtype, lastpage, caption, titlesec, paracol, longtable, booktabs, cancel} +\usepackage[T2A, T1]{fontenc} +\usepackage[utf8]{inputenc} +\usepackage[russian, ukrainian]{babel} +\hypersetup{ + colorlinks, + linkcolor={blue!20!black}, + citecolor={blue!50!black}, + urlcolor={blue!80!black} +} + +\usepackage[left = 25mm, right = 10mm, top=20mm, bottom=20mm, bindingoffset=0cm]{geometry} + +% КОМАНДИ + +\newcommand{\fnt}{ +\fontshape{n} +\fontsize{14pt} {19pt} +\linespread{0.8} +\selectfont +} % нормативний шрифт +\let\oldCaption\caption +\renewcommand{\caption}[1]{ +\oldCaption{\fnt #1} +} % підпис нормативним шрифтом. + +\newcommand{\tb}{ + \hspace*{10mm} +} % відступ у 5 символів "x" відповідно до ДСТУ +\newcommand{\tbln}{ + \newline + \hspace*{10mm} +} % те ж саме, але з переносом рядка +\newcommand{\tbsp}{ + \vspace*{1ex} + \newline + \hspace*{10mm} +} % те ж саме, але з додатковим відступом між рядками + +\iffalse +\let\oldSection\section +\renewcommand{\section}[1]{ + \oldSection*{\tb #1} + \addcontentsline{toc}{section}{#1} + \fnt + \tb +} % відображає у тексті та змісті наданий заголовок +\fi + +\iftrue +\let\oldSubSection\subsection +\renewcommand{\subsection}[1]{ + \oldSubSection*{\tb #1} + \addcontentsline{toc}{subsection}{#1} + \fnt + \tb +} % відображає у тексті та змісті наданий підзаголовок +\fi + +% бібліографія +\usepackage[square,sort,comma,numbers]{natbib} +\renewcommand{\bibsection}{} +\usepackage{totcount} +\newtotcounter{citnum} +% лічильник цитувань +\def\oldbibitem{} \let\oldbibitem=\bibitem +\def\bibitem{\stepcounter{citnum}\oldbibitem} + +% лічильник зображень +\newtotcounter{graphnum} +\def\oldincludegraphics{} \let\oldincludegraphics=\includegraphics +\def\includegraphics{\stepcounter{graphnum}\oldincludegraphics} + +% лічильник таблиць (НЕАВТОМАТИЧНИЙ!!!) +\newtotcounter{tabnum} + +% лічильник додатків (НЕАВТОМАТИЧНИЙ!!!) +\newtotcounter{dodnum} + +% нумерація сторінок +\usepackage{fancyhdr} +\pagestyle{fancy} +\fancyhf{} +\renewcommand{\headrulewidth}{0pt} +\setlength{\headheight}{15.3pt} +\fancyhead[R]{\fnt \thepage} + +% виноски +\renewcommand{\thefootnote}{\large\arabic{footnote})~} +\renewcommand{\footnoterule}{\rule{20mm}{0.4pt} \vspace*{0.5ex}} +% \newcommand{\vyn}[2]{ +% \footnote[#1]{\large #2} +% } % створити виноску + +\let\oldFootnote\footnote +\renewcommand{\footnote}[1]{ + \oldFootnote{\large #1} +} % перегрузка виноски + +\providecommand{\tightlist}{% + \setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}} + +% підписи малюнків/таблиць +\captionsetup[figure]{name={\fnt Рисунок~},labelsep=period} +\captionsetup[table]{name={\fnt Таблиця~},labelsep=period} + +% idk why, but this redefinition does not work, for some reason +% \let\oldCaption\caption +% \renewcommand{\caption}[1] { +% \oldCaption{\fnt #1} +% } + +% markdown strikeouts +\usepackage{soul} + +% codeblocks +\usepackage{minted} + +\titleformat*{\section}{\fnt\LARGE\bfseries} +\titleformat*{\subsection}{\fnt\Large\bfseries} +\titleformat*{\subsubsection}{\fnt\Large} + +\begin{document} +\thispagestyle{empty} % [A]:for cropping +% ------------ +% source: https://tex.stackexchange.com/questions/173651/need-help-to-create-such-a-beautiful-title-page +\begingroup +\centering +\obeylines +\topskip0pt % source: https://tex.stackexchange.com/questions/2326/vertically-center-text-on-a-page +\vspace*{\fill} +\textbf{ +\LARGE +\LARGE +\vspace{100pt} +\LARGE $document_type$ +\Large +\Large +\Large +\huge +\vspace{50pt} +\large +\large +\large +\large +\vspace*{\fill} +\hspace{\fill}\large \rule{20mm}{0.4pt} +\vspace{10pt} +} +\endgroup +% ------------ +\newpage +\tableofcontents + +\newpage +\fnt + \subsection{Перша тема} @@ -21,4 +179,6 @@ Ref(z) = \int \limits_{0}^{1} t^{z-1} \cdot \exp(-t) dt \label{fig:refurb} \end{figure} -\tbln На світлині \ref{fig:refurb} можна бачити принципову схему влаштування рефурбалідзера. Неважно помітити великі очі\footnotemark[1]\footnotetext[1]{Для поливу.}, і два термоядерних реактори, що оточують їх. Точні реакції всередині невідомі, але цілком можливо що там женуть $du + t -> {}^{5}_{6}C$... \ No newline at end of file +\tbln На світлині \ref{fig:refurb} можна бачити принципову схему влаштування рефурбалідзера. Неважно помітити великі очі\footnotemark[1]\footnotetext[1]{Для поливу.}, і два термоядерних реактори, що оточують їх. Точні реакції всередині невідомі, але цілком можливо що там женуть $du + t -> {}^{5}_{6}C$... + +\end{document} \ No newline at end of file diff --git a/tests/transformed_files/deceit.txt b/tests/transformed_files/deceit.txt index 1aa0f37..4ff4ee0 100644 --- a/tests/transformed_files/deceit.txt +++ b/tests/transformed_files/deceit.txt @@ -1,3 +1,161 @@ +\documentclass[a4paper]{article} +\usepackage{graphicx, hyperref, xcolor, gensymb, amssymb, mathtools, wrapfig, mathtools, microtype, lastpage, caption, titlesec, paracol, longtable, booktabs, cancel} +\usepackage[T2A, T1]{fontenc} +\usepackage[utf8]{inputenc} +\usepackage[russian, ukrainian]{babel} +\hypersetup{ + colorlinks, + linkcolor={blue!20!black}, + citecolor={blue!50!black}, + urlcolor={blue!80!black} +} + +\usepackage[left = 25mm, right = 10mm, top=20mm, bottom=20mm, bindingoffset=0cm]{geometry} + +% КОМАНДИ + +\newcommand{\fnt}{ +\fontshape{n} +\fontsize{14pt} {19pt} +\linespread{0.8} +\selectfont +} % нормативний шрифт +\let\oldCaption\caption +\renewcommand{\caption}[1]{ +\oldCaption{\fnt #1} +} % підпис нормативним шрифтом. + +\newcommand{\tb}{ + \hspace*{10mm} +} % відступ у 5 символів "x" відповідно до ДСТУ +\newcommand{\tbln}{ + \newline + \hspace*{10mm} +} % те ж саме, але з переносом рядка +\newcommand{\tbsp}{ + \vspace*{1ex} + \newline + \hspace*{10mm} +} % те ж саме, але з додатковим відступом між рядками + +\iffalse +\let\oldSection\section +\renewcommand{\section}[1]{ + \oldSection*{\tb #1} + \addcontentsline{toc}{section}{#1} + \fnt + \tb +} % відображає у тексті та змісті наданий заголовок +\fi + +\iftrue +\let\oldSubSection\subsection +\renewcommand{\subsection}[1]{ + \oldSubSection*{\tb #1} + \addcontentsline{toc}{subsection}{#1} + \fnt + \tb +} % відображає у тексті та змісті наданий підзаголовок +\fi + +% бібліографія +\usepackage[square,sort,comma,numbers]{natbib} +\renewcommand{\bibsection}{} +\usepackage{totcount} +\newtotcounter{citnum} +% лічильник цитувань +\def\oldbibitem{} \let\oldbibitem=\bibitem +\def\bibitem{\stepcounter{citnum}\oldbibitem} + +% лічильник зображень +\newtotcounter{graphnum} +\def\oldincludegraphics{} \let\oldincludegraphics=\includegraphics +\def\includegraphics{\stepcounter{graphnum}\oldincludegraphics} + +% лічильник таблиць (НЕАВТОМАТИЧНИЙ!!!) +\newtotcounter{tabnum} + +% лічильник додатків (НЕАВТОМАТИЧНИЙ!!!) +\newtotcounter{dodnum} + +% нумерація сторінок +\usepackage{fancyhdr} +\pagestyle{fancy} +\fancyhf{} +\renewcommand{\headrulewidth}{0pt} +\setlength{\headheight}{15.3pt} +\fancyhead[R]{\fnt \thepage} + +% виноски +\renewcommand{\thefootnote}{\large\arabic{footnote})~} +\renewcommand{\footnoterule}{\rule{20mm}{0.4pt} \vspace*{0.5ex}} +% \newcommand{\vyn}[2]{ +% \footnote[#1]{\large #2} +% } % створити виноску + +\let\oldFootnote\footnote +\renewcommand{\footnote}[1]{ + \oldFootnote{\large #1} +} % перегрузка виноски + +\providecommand{\tightlist}{% + \setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}} + +% підписи малюнків/таблиць +\captionsetup[figure]{name={\fnt Рисунок~},labelsep=period} +\captionsetup[table]{name={\fnt Таблиця~},labelsep=period} + +% idk why, but this redefinition does not work, for some reason +% \let\oldCaption\caption +% \renewcommand{\caption}[1] { +% \oldCaption{\fnt #1} +% } + +% markdown strikeouts +\usepackage{soul} + +% codeblocks +\usepackage{minted} + +\titleformat*{\section}{\fnt\LARGE\bfseries} +\titleformat*{\subsection}{\fnt\Large\bfseries} +\titleformat*{\subsubsection}{\fnt\Large} + +\begin{document} +\thispagestyle{empty} % [A]:for cropping +% ------------ +% source: https://tex.stackexchange.com/questions/173651/need-help-to-create-such-a-beautiful-title-page +\begingroup +\centering +\obeylines +\topskip0pt % source: https://tex.stackexchange.com/questions/2326/vertically-center-text-on-a-page +\vspace*{\fill} +\textbf{ +\LARGE +\LARGE +\vspace{100pt} +\LARGE $document_type$ +\Large +\Large +\Large +\huge +\vspace{50pt} +\large +\large +\large +\large +\vspace*{\fill} +\hspace{\fill}\large \rule{20mm}{0.4pt} +\vspace{10pt} +} +\endgroup +% ------------ +\newpage +\tableofcontents + +\newpage +\fnt + This test is dedicated to attempts of parser deception, i.e. usage of special symbols in wrong places to see if parser recognizes them as control symbols. There we go: \subsection{Page dividers} @@ -160,4 +318,6 @@ local z = x .. y -- matty!! \tbln ```ayano \tbln 2 -\tbln ``` \ No newline at end of file +\tbln ``` + +\end{document} \ No newline at end of file diff --git a/tests/transformed_files/tables_code.txt b/tests/transformed_files/tables_code.txt index 20cb473..524e835 100644 --- a/tests/transformed_files/tables_code.txt +++ b/tests/transformed_files/tables_code.txt @@ -1,3 +1,161 @@ +\documentclass[a4paper]{article} +\usepackage{graphicx, hyperref, xcolor, gensymb, amssymb, mathtools, wrapfig, mathtools, microtype, lastpage, caption, titlesec, paracol, longtable, booktabs, cancel} +\usepackage[T2A, T1]{fontenc} +\usepackage[utf8]{inputenc} +\usepackage[russian, ukrainian]{babel} +\hypersetup{ + colorlinks, + linkcolor={blue!20!black}, + citecolor={blue!50!black}, + urlcolor={blue!80!black} +} + +\usepackage[left = 25mm, right = 10mm, top=20mm, bottom=20mm, bindingoffset=0cm]{geometry} + +% КОМАНДИ + +\newcommand{\fnt}{ +\fontshape{n} +\fontsize{14pt} {19pt} +\linespread{0.8} +\selectfont +} % нормативний шрифт +\let\oldCaption\caption +\renewcommand{\caption}[1]{ +\oldCaption{\fnt #1} +} % підпис нормативним шрифтом. + +\newcommand{\tb}{ + \hspace*{10mm} +} % відступ у 5 символів "x" відповідно до ДСТУ +\newcommand{\tbln}{ + \newline + \hspace*{10mm} +} % те ж саме, але з переносом рядка +\newcommand{\tbsp}{ + \vspace*{1ex} + \newline + \hspace*{10mm} +} % те ж саме, але з додатковим відступом між рядками + +\iffalse +\let\oldSection\section +\renewcommand{\section}[1]{ + \oldSection*{\tb #1} + \addcontentsline{toc}{section}{#1} + \fnt + \tb +} % відображає у тексті та змісті наданий заголовок +\fi + +\iftrue +\let\oldSubSection\subsection +\renewcommand{\subsection}[1]{ + \oldSubSection*{\tb #1} + \addcontentsline{toc}{subsection}{#1} + \fnt + \tb +} % відображає у тексті та змісті наданий підзаголовок +\fi + +% бібліографія +\usepackage[square,sort,comma,numbers]{natbib} +\renewcommand{\bibsection}{} +\usepackage{totcount} +\newtotcounter{citnum} +% лічильник цитувань +\def\oldbibitem{} \let\oldbibitem=\bibitem +\def\bibitem{\stepcounter{citnum}\oldbibitem} + +% лічильник зображень +\newtotcounter{graphnum} +\def\oldincludegraphics{} \let\oldincludegraphics=\includegraphics +\def\includegraphics{\stepcounter{graphnum}\oldincludegraphics} + +% лічильник таблиць (НЕАВТОМАТИЧНИЙ!!!) +\newtotcounter{tabnum} + +% лічильник додатків (НЕАВТОМАТИЧНИЙ!!!) +\newtotcounter{dodnum} + +% нумерація сторінок +\usepackage{fancyhdr} +\pagestyle{fancy} +\fancyhf{} +\renewcommand{\headrulewidth}{0pt} +\setlength{\headheight}{15.3pt} +\fancyhead[R]{\fnt \thepage} + +% виноски +\renewcommand{\thefootnote}{\large\arabic{footnote})~} +\renewcommand{\footnoterule}{\rule{20mm}{0.4pt} \vspace*{0.5ex}} +% \newcommand{\vyn}[2]{ +% \footnote[#1]{\large #2} +% } % створити виноску + +\let\oldFootnote\footnote +\renewcommand{\footnote}[1]{ + \oldFootnote{\large #1} +} % перегрузка виноски + +\providecommand{\tightlist}{% + \setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}} + +% підписи малюнків/таблиць +\captionsetup[figure]{name={\fnt Рисунок~},labelsep=period} +\captionsetup[table]{name={\fnt Таблиця~},labelsep=period} + +% idk why, but this redefinition does not work, for some reason +% \let\oldCaption\caption +% \renewcommand{\caption}[1] { +% \oldCaption{\fnt #1} +% } + +% markdown strikeouts +\usepackage{soul} + +% codeblocks +\usepackage{minted} + +\titleformat*{\section}{\fnt\LARGE\bfseries} +\titleformat*{\subsection}{\fnt\Large\bfseries} +\titleformat*{\subsubsection}{\fnt\Large} + +\begin{document} +\thispagestyle{empty} % [A]:for cropping +% ------------ +% source: https://tex.stackexchange.com/questions/173651/need-help-to-create-such-a-beautiful-title-page +\begingroup +\centering +\obeylines +\topskip0pt % source: https://tex.stackexchange.com/questions/2326/vertically-center-text-on-a-page +\vspace*{\fill} +\textbf{ +\LARGE +\LARGE +\vspace{100pt} +\LARGE $document_type$ +\Large +\Large +\Large +\huge +\vspace{50pt} +\large +\large +\large +\large +\vspace*{\fill} +\hspace{\fill}\large \rule{20mm}{0.4pt} +\vspace{10pt} +} +\endgroup +% ------------ +\newpage +\tableofcontents + +\newpage +\fnt + Наступний тест. Тут протестуємо таблиці і блоки коду. \subsubsection{Таблиці} @@ -40,3 +198,6 @@ let z = (y as f64).sqrt(); println!("x = {x}, y = {y}, z = {x}"); \end{minted} + + +\end{document} \ No newline at end of file From 68a5dff1a976d7a7adb0d152f83669b8f2d12881 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Tue, 26 Mar 2024 01:17:27 +0200 Subject: [PATCH 40/98] nightly (unfinished) --- src/lexer.rs | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/src/lexer.rs b/src/lexer.rs index dc4b950..f4eb5c5 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -21,7 +21,7 @@ use nom::{ use url::Url; use crate::{ - data::{AyanoBlock, Formatting, ListType, Token, Tokens}, + data::{AyanoBlock, Formatting, ListType, TitleInfo, Token, Tokens}, util::{optional_permutation, Equivalent, IteratorArrayCollectExt, VecTryExtendExt}, }; @@ -1412,7 +1412,7 @@ pub fn inner_lex< } #[cfg(test)] -mod tests { +mod token_tests { use nom::character::complete::alphanumeric1; use super::*; @@ -2169,3 +2169,41 @@ $$ test_ok! {ok_idk2, "\n\n## Підпункт\n\n", head!(##"Підпункт")} } } + +pub fn title_info<'source, Err: nom::error::ParseError<&'source str>>( + mut input: &'source str, +) -> IResult<&'source str, TitleInfo<'source>, Err> { + const FIELD_NAMES: [&'static str; 12] = [ + "header-line1", + "header-line2", + "document-type", + "title-line1", + "title-line2", + "title-line3", + "title-line4", + "author-line1", + "author-line2", + "author-line3", + "date", + "prof", + ]; + let (rest, _) = tag("---")(input)?; + input = rest; + + let mut res = TitleInfo::default(); + // --- + // article-type: ЗВІТ + // title-line1: про Виконання лабораторної роботи + // title-line2: з практикуму "Методи реєстрації іонізуючого випромінювання" + // title-line3: + // title-line4: СТАТИСТИЧНІ ТА СИСТЕМАТИЧНІ ПОХИБКИ ПРИ ДЕТЕКТУВАННІ ЯДЕРНОГО ВИПРОМІНЮВАННЯ + // date: листопад 2023 + // keywords: ВИПРОМІНЮВАННЯ, РОЗПОДІЛ, СТАТИСТИЧНА ПРИРОДА. + // abstract: |- + // Об’єкт дослідження - розподіл кількості зареєстрованих частинок протягом деякого однакового проміжку часу (часу усереднення). + // Мета роботи - визначити умови застосовності розподілів Пуассона та Гауса, пересвідчитись у статистичній природi процесу радiактивного розпаду. Для виконання використано радiактивний зразок, детектор, підсилювач та формувач сигналiв, лічильник імпульсів. + // prof: Голінка-Безшийко Л.О. + // --- + let (input, _) = cut(tag("---"))(input)?; + Ok((todo!(), res)) +} From 60e2df73700a94f13cf9958fcdecbb8d47d38a62 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Tue, 26 Mar 2024 08:51:40 +0200 Subject: [PATCH 41/98] Added title page info support to public interface and tests --- src/data.rs | 2 +- src/gen/lab/lab_preamble.tex | 2 +- src/gen/lab/mod.rs | 6 +- src/lexer.rs | 188 +++++++++++++++++++----- src/lib.rs | 16 +- src/util.rs | 2 +- tests/input_files/title_info.md | 23 +++ tests/lex.rs | 8 +- tests/lex_files/ayano.txt | 14 ++ tests/lex_files/basic.txt | 14 ++ tests/lex_files/deceit.txt | 14 ++ tests/lex_files/tables_code.txt | 14 ++ tests/lex_files/title_info.txt | 94 ++++++++++++ tests/transform.rs | 13 +- tests/transformed_files/ayano.txt | 2 +- tests/transformed_files/basic.txt | 2 +- tests/transformed_files/deceit.txt | 2 +- tests/transformed_files/tables_code.txt | 2 +- tests/transformed_files/title_info.txt | 169 +++++++++++++++++++++ 19 files changed, 527 insertions(+), 60 deletions(-) create mode 100644 tests/input_files/title_info.md create mode 100644 tests/lex_files/title_info.txt create mode 100644 tests/transformed_files/title_info.txt diff --git a/src/data.rs b/src/data.rs index 3ecb164..fdfb24a 100644 --- a/src/data.rs +++ b/src/data.rs @@ -6,7 +6,7 @@ use std::{ use url::Url; -#[derive(Debug, Default)] +#[derive(Debug, Default, PartialEq)] pub struct TitleInfo<'source> { pub header_line1: Option>, pub header_line2: Option>, diff --git a/src/gen/lab/lab_preamble.tex b/src/gen/lab/lab_preamble.tex index 8ccd597..a21e158 100644 --- a/src/gen/lab/lab_preamble.tex +++ b/src/gen/lab/lab_preamble.tex @@ -134,7 +134,7 @@ \LARGE $header-line1$ \LARGE $header-line2$ \vspace{100pt} -\LARGE $document_type$ +\LARGE $document-type$ \Large $title-line1$ \Large $title-line2$ \Large $title-line3$ diff --git a/src/gen/lab/mod.rs b/src/gen/lab/mod.rs index 1a6d96c..f2653ee 100644 --- a/src/gen/lab/mod.rs +++ b/src/gen/lab/mod.rs @@ -4,13 +4,11 @@ mod meta; pub use context::Context; pub use meta::SourceMeta; -use itertools::Itertools; -use regex::Replacer; - use crate::{ data::{Formatting, TitleInfo, Token}, path_engine::PathEngine, }; +use itertools::Itertools; use std::{borrow::Borrow, fmt::Write, marker::PhantomData}; use super::{ayano::AyanoExecutor, GenerationError, OutputGenerator, Res}; @@ -767,7 +765,7 @@ y = 0.4 } test! {ayano_fig2, Token::Ayano{data: AyanoBlock{is_display: false,is_static: false,code: r"@fig: src = 'path/to/image.jpg'".into(),insert_path: None, - is_space_before: false,}}, + is_space_before: false,}}, r" \begin{figure}[h!] \centering diff --git a/src/lexer.rs b/src/lexer.rs index f4eb5c5..dafa096 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -2171,39 +2171,159 @@ $$ } pub fn title_info<'source, Err: nom::error::ParseError<&'source str>>( - mut input: &'source str, + input: &'source str, ) -> IResult<&'source str, TitleInfo<'source>, Err> { - const FIELD_NAMES: [&'static str; 12] = [ - "header-line1", - "header-line2", - "document-type", - "title-line1", - "title-line2", - "title-line3", - "title-line4", - "author-line1", - "author-line2", - "author-line3", - "date", - "prof", - ]; - let (rest, _) = tag("---")(input)?; - input = rest; - - let mut res = TitleInfo::default(); - // --- - // article-type: ЗВІТ - // title-line1: про Виконання лабораторної роботи - // title-line2: з практикуму "Методи реєстрації іонізуючого випромінювання" - // title-line3: - // title-line4: СТАТИСТИЧНІ ТА СИСТЕМАТИЧНІ ПОХИБКИ ПРИ ДЕТЕКТУВАННІ ЯДЕРНОГО ВИПРОМІНЮВАННЯ - // date: листопад 2023 - // keywords: ВИПРОМІНЮВАННЯ, РОЗПОДІЛ, СТАТИСТИЧНА ПРИРОДА. - // abstract: |- - // Об’єкт дослідження - розподіл кількості зареєстрованих частинок протягом деякого однакового проміжку часу (часу усереднення). - // Мета роботи - визначити умови застосовності розподілів Пуассона та Гауса, пересвідчитись у статистичній природi процесу радiактивного розпаду. Для виконання використано радiактивний зразок, детектор, підсилювач та формувач сигналiв, лічильник імпульсів. - // prof: Голінка-Безшийко Л.О. - // --- - let (input, _) = cut(tag("---"))(input)?; - Ok((todo!(), res)) + let Ok((rest, _)) = tag::<_, _, Err>("---")(input) else { + return Ok((input, TitleInfo::default())); + }; + cut(|mut input: &'source str| { + macro_rules! match_field { + ($name:literal) => { + preceded::<_, _, _, Err, _, _>( + tuple((char('\n'), tag($name), space0, char(':'), space0)), + take_till1(|c| c == '\n'), + ) + .map(Cow::from) + }; + } + let ( + input, + ( + header_line1, + header_line2, + document_type, + title_line1, + title_line2, + title_line3, + title_line4, + author_line1, + author_line2, + author_line3, + date, + prof, + ), + ) = optional_permutation(( + match_field!("header-line1"), + match_field!("header-line2"), + match_field!("document-type"), + match_field!("title-line1"), + match_field!("title-line2"), + match_field!("title-line3"), + match_field!("title-line4"), + match_field!("author-line1"), + match_field!("author-line2"), + match_field!("author-line3"), + match_field!("date"), + match_field!("prof"), + )) + .parse(input)?; + let res = TitleInfo { + header_line1, + header_line2, + document_type, + title_line1, + title_line2, + title_line3, + title_line4, + author_line1, + author_line2, + author_line3, + date, + prof, + }; + let (input, _) = tag("\n---")(input)?; + Ok((input, res)) + })(rest) +} + +#[cfg(test)] +mod title_info_tests { + use super::*; + macro_rules! title_info { + () => { + TitleInfo::default() + }; + ($name:ident: $value:literal $(, $other_name:ident: $other_value:literal) *) => {{ + let mut info = title_info!($($other_name: $other_value), *); + info.$name = Some($value.into()); + info + }}; + } + macro_rules! test_ok { + {$name:ident, $input:literal, [$($expected_name:ident: $expected_value:literal), *], $left_over:literal} => { + #[test] + fn $name() { + // arrange + let input = $input; + let expected_info = title_info!($($expected_name: $expected_value), *); + + // act + let (rest, actual_info) = title_info::<'_, nom::error::VerboseError<& '_ str>>(input).expect("Should succeed parsing title info in this case"); + + // assert + assert_eq!(expected_info, actual_info, "Should produce identical info"); + assert_eq!(rest, $left_over, "Should produce identical left-over"); + } + }; + } + + test_ok! {ok_empty, "", [], ""} + test_ok! {ok_no_fields, "---\n---\n", [], "\n"} + test_ok! {ok_single_field, "---\nauthor-line3 : дяч дзучунович\n---\n", [author_line3: "дяч дзучунович"], "\n"} + test_ok! {ok_all_fields, r"--- +header-line1: Навч. заклад +header-line2: Факультет +document-type: ПРОТОКОЛ +title-line1: Виконання марної роботи +title-line2: з історії стародавнього Єгипту +title-line3: (пинальна частина) +title-line4: НАЗВА РОБОТИ +author-line1: виконувало: +author-line2: дяч дзучунович +author-line3: (perfectly still) +date: вербень 2077 +prof: me :idk: +--- +", [ + header_line1: "Навч. заклад", + header_line2: "Факультет", + document_type: "ПРОТОКОЛ", + title_line1: "Виконання марної роботи", + title_line2: "з історії стародавнього Єгипту", + title_line3: "(пинальна частина)", + title_line4: "НАЗВА РОБОТИ", + author_line1: "виконувало:", + author_line2: "дяч дзучунович", + author_line3: "(perfectly still)", + date: "вербень 2077", + prof: "me :idk:" + ], "\n"} + test_ok! {ok_all_fields_permutated, r"--- +author-line2: дяч дзучунович +header-line1: Навч. заклад +title-line2: з історії стародавнього Єгипту +header-line2: Факультет +author-line1: виконувало: +author-line3: (perfectly still) +document-type: ПРОТОКОЛ +date: вербень 2077 +title-line4: НАЗВА РОБОТИ +title-line1: Виконання марної роботи +prof: me :idk: +title-line3: (пинальна частина) +--- +", [ + header_line1: "Навч. заклад", + header_line2: "Факультет", + document_type: "ПРОТОКОЛ", + title_line1: "Виконання марної роботи", + title_line2: "з історії стародавнього Єгипту", + title_line3: "(пинальна частина)", + title_line4: "НАЗВА РОБОТИ", + author_line1: "виконувало:", + author_line2: "дяч дзучунович", + author_line3: "(perfectly still)", + date: "вербень 2077", + prof: "me :idk:" + ], "\n"} } diff --git a/src/lib.rs b/src/lib.rs index a052e23..765cb46 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,13 +38,19 @@ mod lexer; mod path_engine; /// Reexports -pub fn lex<'source>(input: &'source str) -> Result, KyomatoLexError> { - lexer::lex::<'source, KyomatoLexError>(input) - .map(|(_, t)| t) - .map_err(|err| match err { +pub fn lex<'source>( + input: &'source str, +) -> Result<(data::TitleInfo<'source>, data::Token<'source>), KyomatoLexError> { + let (input, title_info) = + lexer::title_info::<'source, KyomatoLexError>(input).map_err(|err| match err { nom::Err::Error(err) | nom::Err::Failure(err) => err, nom::Err::Incomplete(_) => unreachable!("This is a complete input"), - }) + })?; + let (_, tokens) = lexer::lex::<'source, KyomatoLexError>(input).map_err(|err| match err { + nom::Err::Error(err) | nom::Err::Failure(err) => err, + nom::Err::Incomplete(_) => unreachable!("This is a complete input"), + })?; + Ok((title_info, tokens)) } pub fn gen<'token, 'source: 'token>( token: &'token data::Token<'source>, diff --git a/src/util.rs b/src/util.rs index 0081d51..fbe8a6f 100644 --- a/src/util.rs +++ b/src/util.rs @@ -434,7 +434,7 @@ macro_rules! impl_optional_permutation { // Break out, we're done here. break; } - let _ = 2; + let _ = 2; // <-- his name is Joey Ok((input, ($out_ident0, $($out_ident, ) *))) } } diff --git a/tests/input_files/title_info.md b/tests/input_files/title_info.md new file mode 100644 index 0000000..d04c17c --- /dev/null +++ b/tests/input_files/title_info.md @@ -0,0 +1,23 @@ +--- +author-line2: дяч дзучунович +header-line1: Навч. заклад +title-line2: з історії стародавнього Єгипту +header-line2: Факультет +author-line1: виконувало: +author-line3: (perfectly still) +document-type: ПРОТОКОЛ +date: вербень 2077 +title-line4: НАЗВА РОБОТИ +title-line1: Виконання марної роботи +prof: me :idk: +title-line3: (пинальна частина) +--- + +# First para + +This text is *just* a dummy, so that document won't feel ~~too~~ **empty** + +Have a some oily maccaroni: +$$ +\gamma = \lim_{n \to \infty} \sum\limits_{i=1}^{n} \dfrac{1}{n} - \ln(n) +$$ \ No newline at end of file diff --git a/tests/lex.rs b/tests/lex.rs index 96ae880..caafb6a 100644 --- a/tests/lex.rs +++ b/tests/lex.rs @@ -65,7 +65,7 @@ fn gen_lex_output() { std::fs::create_dir(&output_path).expect("Failed to create directory"); } for (filename, input) in input_files() { - let parsed = match kyomato::lex(input.as_str()) { + let (title_info, tokens) = match kyomato::lex(input.as_str()) { Ok(ok) => ok, Err(err) => { panic!("Input files should be successfully parsed: {err:#?}"); @@ -73,7 +73,7 @@ fn gen_lex_output() { }; // create representation of the parsed tokens // TODO this part should be replaced, once serde is supported - let repr = format!("{:#?}", parsed); + let repr = format!("{:#?}\n{:#?}", title_info, tokens); // write the result to output directory let output_file = { output_path.push(filename); @@ -104,7 +104,7 @@ fn test_lex() { std::fs::create_dir(&output_path).expect("Failed to create directory"); } for (filename, input) in input_files() { - let parsed = match kyomato::lex(input.as_str()) { + let (title_info, tokens) = match kyomato::lex(input.as_str()) { Ok(ok) => ok, Err(err) => { panic!("Should successfully parse the input: {err:#?}"); @@ -112,7 +112,7 @@ fn test_lex() { }; // create representation of the parsed tokens // TODO this part should be replaced, once serde is supported - let repr = format!("{:#?}", parsed); + let repr = format!("{:#?}\n{:#?}", title_info, tokens); // write the result to output directory let expected_output = { output_path.push(&filename); diff --git a/tests/lex_files/ayano.txt b/tests/lex_files/ayano.txt index a12a15c..09cebe2 100644 --- a/tests/lex_files/ayano.txt +++ b/tests/lex_files/ayano.txt @@ -1,3 +1,17 @@ +TitleInfo { + header_line1: None, + header_line2: None, + document_type: None, + title_line1: None, + title_line2: None, + title_line3: None, + title_line4: None, + author_line1: None, + author_line2: None, + author_line3: None, + date: None, + prof: None, +} Multiple { tokens: Owned( [ diff --git a/tests/lex_files/basic.txt b/tests/lex_files/basic.txt index b6073ff..fbea639 100644 --- a/tests/lex_files/basic.txt +++ b/tests/lex_files/basic.txt @@ -1,3 +1,17 @@ +TitleInfo { + header_line1: None, + header_line2: None, + document_type: None, + title_line1: None, + title_line2: None, + title_line3: None, + title_line4: None, + author_line1: None, + author_line2: None, + author_line3: None, + date: None, + prof: None, +} Multiple { tokens: Owned( [ diff --git a/tests/lex_files/deceit.txt b/tests/lex_files/deceit.txt index 45e4efb..10a94fa 100644 --- a/tests/lex_files/deceit.txt +++ b/tests/lex_files/deceit.txt @@ -1,3 +1,17 @@ +TitleInfo { + header_line1: None, + header_line2: None, + document_type: None, + title_line1: None, + title_line2: None, + title_line3: None, + title_line4: None, + author_line1: None, + author_line2: None, + author_line3: None, + date: None, + prof: None, +} Multiple { tokens: Owned( [ diff --git a/tests/lex_files/tables_code.txt b/tests/lex_files/tables_code.txt index 4bf00ee..e10ce6b 100644 --- a/tests/lex_files/tables_code.txt +++ b/tests/lex_files/tables_code.txt @@ -1,3 +1,17 @@ +TitleInfo { + header_line1: None, + header_line2: None, + document_type: None, + title_line1: None, + title_line2: None, + title_line3: None, + title_line4: None, + author_line1: None, + author_line2: None, + author_line3: None, + date: None, + prof: None, +} Multiple { tokens: Owned( [ diff --git a/tests/lex_files/title_info.txt b/tests/lex_files/title_info.txt new file mode 100644 index 0000000..5621911 --- /dev/null +++ b/tests/lex_files/title_info.txt @@ -0,0 +1,94 @@ +TitleInfo { + header_line1: Some( + "Навч. заклад", + ), + header_line2: Some( + "Факультет", + ), + document_type: Some( + "ПРОТОКОЛ", + ), + title_line1: Some( + "Виконання марної роботи", + ), + title_line2: Some( + "з історії стародавнього Єгипту", + ), + title_line3: Some( + "(пинальна частина)", + ), + title_line4: Some( + "НАЗВА РОБОТИ", + ), + author_line1: Some( + "виконувало:", + ), + author_line2: Some( + "дяч дзучунович", + ), + author_line3: Some( + "(perfectly still)", + ), + date: Some( + "вербень 2077", + ), + prof: Some( + "me :idk:", + ), +} +Multiple { + tokens: Owned( + [ + Header { + order: 1, + content: "First para", + }, + Paragraph { + is_newline: true, + space_before: false, + formatting: None, + content: "This text is", + }, + Paragraph { + is_newline: false, + space_before: true, + formatting: Some( + Italic, + ), + content: "just", + }, + Paragraph { + is_newline: false, + space_before: true, + formatting: None, + content: "a dummy, so that document won't feel", + }, + Paragraph { + is_newline: false, + space_before: true, + formatting: Some( + StrikeThrough, + ), + content: "too", + }, + Paragraph { + is_newline: false, + space_before: true, + formatting: Some( + Bold, + ), + content: "empty", + }, + Paragraph { + is_newline: true, + space_before: false, + formatting: None, + content: "Have a some oily maccaroni:", + }, + DisplayMath { + content: "\\gamma = \\lim_{n \\to \\infty} \\sum\\limits_{i=1}^{n} \\dfrac{1}{n} - \\ln(n)", + ident: None, + }, + ], + ), +} \ No newline at end of file diff --git a/tests/transform.rs b/tests/transform.rs index 498603f..11022c8 100644 --- a/tests/transform.rs +++ b/tests/transform.rs @@ -23,13 +23,14 @@ fn input_files() -> impl Iterator { #[test] fn show_transform() { for (_, input) in input_files() { - let parsed = match kyomato::lex(input.as_str()) { + let (title_info, tokens) = match kyomato::lex(input.as_str()) { Ok(ok) => ok, Err(err) => { panic!("Should successfully parse the input: {err:#?}"); } }; - let repr = kyomato::gen_to_string(&parsed, None).expect("Should be able to transform tokens"); + let repr = kyomato::gen_to_string(&tokens, title_info) + .expect("Should be able to transform tokens"); println!("{repr}"); std::io::stdin() .read_line(&mut String::new()) @@ -63,7 +64,7 @@ fn gen_transform_output() { std::fs::create_dir(&output_path).expect("Failed to create directory"); } for (filename, input) in input_files() { - let parsed = match kyomato::lex(input.as_str()) { + let (title_info, tokens) = match kyomato::lex(input.as_str()) { Ok(ok) => ok, Err(err) => { panic!("Input files should be successfully parsed: {err:#?}"); @@ -86,7 +87,7 @@ fn gen_transform_output() { output_file }; // write the result to output file - kyomato::gen(&parsed, None, &mut output_file).expect("Failed to generate output"); + kyomato::gen(&tokens, title_info, &mut output_file).expect("Failed to generate output"); } } @@ -98,13 +99,13 @@ fn test_transform() { std::fs::create_dir(&output_path).expect("Failed to create directory"); } for (filename, input) in input_files() { - let parsed = match kyomato::lex(input.as_str()) { + let (title_info, tokens) = match kyomato::lex(input.as_str()) { Ok(ok) => ok, Err(err) => { panic!("Should successfully parse the input: {err:#?}"); } }; - let repr = kyomato::gen_to_string(&parsed, None).expect("Failed to generate output"); + let repr = kyomato::gen_to_string(&tokens, title_info).expect("Failed to generate output"); // write the result to output directory let expected_output = { output_path.push(&filename); diff --git a/tests/transformed_files/ayano.txt b/tests/transformed_files/ayano.txt index fc29250..93b0444 100644 --- a/tests/transformed_files/ayano.txt +++ b/tests/transformed_files/ayano.txt @@ -134,7 +134,7 @@ \LARGE \LARGE \vspace{100pt} -\LARGE $document_type$ +\LARGE \Large \Large \Large diff --git a/tests/transformed_files/basic.txt b/tests/transformed_files/basic.txt index 7ec34d7..4c89fb8 100644 --- a/tests/transformed_files/basic.txt +++ b/tests/transformed_files/basic.txt @@ -134,7 +134,7 @@ \LARGE \LARGE \vspace{100pt} -\LARGE $document_type$ +\LARGE \Large \Large \Large diff --git a/tests/transformed_files/deceit.txt b/tests/transformed_files/deceit.txt index 4ff4ee0..3cc830e 100644 --- a/tests/transformed_files/deceit.txt +++ b/tests/transformed_files/deceit.txt @@ -134,7 +134,7 @@ \LARGE \LARGE \vspace{100pt} -\LARGE $document_type$ +\LARGE \Large \Large \Large diff --git a/tests/transformed_files/tables_code.txt b/tests/transformed_files/tables_code.txt index 524e835..ce44d66 100644 --- a/tests/transformed_files/tables_code.txt +++ b/tests/transformed_files/tables_code.txt @@ -134,7 +134,7 @@ \LARGE \LARGE \vspace{100pt} -\LARGE $document_type$ +\LARGE \Large \Large \Large diff --git a/tests/transformed_files/title_info.txt b/tests/transformed_files/title_info.txt new file mode 100644 index 0000000..fff6790 --- /dev/null +++ b/tests/transformed_files/title_info.txt @@ -0,0 +1,169 @@ +\documentclass[a4paper]{article} +\usepackage{graphicx, hyperref, xcolor, gensymb, amssymb, mathtools, wrapfig, mathtools, microtype, lastpage, caption, titlesec, paracol, longtable, booktabs, cancel} +\usepackage[T2A, T1]{fontenc} +\usepackage[utf8]{inputenc} +\usepackage[russian, ukrainian]{babel} +\hypersetup{ + colorlinks, + linkcolor={blue!20!black}, + citecolor={blue!50!black}, + urlcolor={blue!80!black} +} + +\usepackage[left = 25mm, right = 10mm, top=20mm, bottom=20mm, bindingoffset=0cm]{geometry} + +% КОМАНДИ + +\newcommand{\fnt}{ +\fontshape{n} +\fontsize{14pt} {19pt} +\linespread{0.8} +\selectfont +} % нормативний шрифт +\let\oldCaption\caption +\renewcommand{\caption}[1]{ +\oldCaption{\fnt #1} +} % підпис нормативним шрифтом. + +\newcommand{\tb}{ + \hspace*{10mm} +} % відступ у 5 символів "x" відповідно до ДСТУ +\newcommand{\tbln}{ + \newline + \hspace*{10mm} +} % те ж саме, але з переносом рядка +\newcommand{\tbsp}{ + \vspace*{1ex} + \newline + \hspace*{10mm} +} % те ж саме, але з додатковим відступом між рядками + +\iffalse +\let\oldSection\section +\renewcommand{\section}[1]{ + \oldSection*{\tb #1} + \addcontentsline{toc}{section}{#1} + \fnt + \tb +} % відображає у тексті та змісті наданий заголовок +\fi + +\iftrue +\let\oldSubSection\subsection +\renewcommand{\subsection}[1]{ + \oldSubSection*{\tb #1} + \addcontentsline{toc}{subsection}{#1} + \fnt + \tb +} % відображає у тексті та змісті наданий підзаголовок +\fi + +% бібліографія +\usepackage[square,sort,comma,numbers]{natbib} +\renewcommand{\bibsection}{} +\usepackage{totcount} +\newtotcounter{citnum} +% лічильник цитувань +\def\oldbibitem{} \let\oldbibitem=\bibitem +\def\bibitem{\stepcounter{citnum}\oldbibitem} + +% лічильник зображень +\newtotcounter{graphnum} +\def\oldincludegraphics{} \let\oldincludegraphics=\includegraphics +\def\includegraphics{\stepcounter{graphnum}\oldincludegraphics} + +% лічильник таблиць (НЕАВТОМАТИЧНИЙ!!!) +\newtotcounter{tabnum} + +% лічильник додатків (НЕАВТОМАТИЧНИЙ!!!) +\newtotcounter{dodnum} + +% нумерація сторінок +\usepackage{fancyhdr} +\pagestyle{fancy} +\fancyhf{} +\renewcommand{\headrulewidth}{0pt} +\setlength{\headheight}{15.3pt} +\fancyhead[R]{\fnt \thepage} + +% виноски +\renewcommand{\thefootnote}{\large\arabic{footnote})~} +\renewcommand{\footnoterule}{\rule{20mm}{0.4pt} \vspace*{0.5ex}} +% \newcommand{\vyn}[2]{ +% \footnote[#1]{\large #2} +% } % створити виноску + +\let\oldFootnote\footnote +\renewcommand{\footnote}[1]{ + \oldFootnote{\large #1} +} % перегрузка виноски + +\providecommand{\tightlist}{% + \setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}} + +% підписи малюнків/таблиць +\captionsetup[figure]{name={\fnt Рисунок~},labelsep=period} +\captionsetup[table]{name={\fnt Таблиця~},labelsep=period} + +% idk why, but this redefinition does not work, for some reason +% \let\oldCaption\caption +% \renewcommand{\caption}[1] { +% \oldCaption{\fnt #1} +% } + +% markdown strikeouts +\usepackage{soul} + +% codeblocks +\usepackage{minted} + +\titleformat*{\section}{\fnt\LARGE\bfseries} +\titleformat*{\subsection}{\fnt\Large\bfseries} +\titleformat*{\subsubsection}{\fnt\Large} + +\begin{document} +\thispagestyle{empty} % [A]:for cropping +% ------------ +% source: https://tex.stackexchange.com/questions/173651/need-help-to-create-such-a-beautiful-title-page +\begingroup +\centering +\obeylines +\topskip0pt % source: https://tex.stackexchange.com/questions/2326/vertically-center-text-on-a-page +\vspace*{\fill} +\textbf{ +\LARGE Навч. заклад +\LARGE Факультет +\vspace{100pt} +\LARGE ПРОТОКОЛ +\Large Виконання марної роботи +\Large з історії стародавнього Єгипту +\Large (пинальна частина) +\huge НАЗВА РОБОТИ +\vspace{50pt} +\large виконувало: +\large дяч дзучунович +\large (perfectly still) +\large вербень 2077 +\vspace*{\fill} +\hspace{\fill}\large me :idk: \rule{20mm}{0.4pt} +\vspace{10pt} +} +\endgroup +% ------------ +\newpage +\tableofcontents + +\newpage +\fnt + + +\subsection{First para} + +\tbln This text is \textit{just} a dummy, so that document won't feel \st{too} \textbf{empty} +\tbln Have a some oily maccaroni: +\begin{equation} +\gamma = \lim_{n \to \infty} \sum\limits_{i=1}^{n} \dfrac{1}{n} - \ln(n) +\end{equation} + + +\end{document} \ No newline at end of file From a95b406d769520ffa55569a87646de22c68701c2 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Tue, 26 Mar 2024 11:22:00 +0200 Subject: [PATCH 42/98] Added support for displayed ayano block description - looks kinda ugly now, might want to make them prettier --- src/data.rs | 50 ++++- src/gen/ayano.rs | 51 ++--- src/gen/lab/mod.rs | 88 +++++++-- src/gen/mod.rs | 16 +- src/lexer.rs | 84 ++++---- src/util.rs | 13 ++ tests/input_files/ayano_display_blocks.md | 25 +++ tests/lex_files/ayano.txt | 7 +- tests/lex_files/ayano_display_blocks.txt | 70 +++++++ tests/lex_files/basic.txt | 5 +- tests/lex_files/deceit.txt | 3 +- tests/lex_files/tables_code.txt | 1 + tests/lex_files/title_info.txt | 1 + .../ayano_display_blocks.txt | 187 ++++++++++++++++++ 14 files changed, 499 insertions(+), 102 deletions(-) create mode 100644 tests/input_files/ayano_display_blocks.md create mode 100644 tests/lex_files/ayano_display_blocks.txt create mode 100644 tests/transformed_files/ayano_display_blocks.txt diff --git a/src/data.rs b/src/data.rs index fdfb24a..074cbc2 100644 --- a/src/data.rs +++ b/src/data.rs @@ -6,6 +6,8 @@ use std::{ use url::Url; +use crate::util::HashIgnored; + #[derive(Debug, Default, PartialEq)] pub struct TitleInfo<'source> { pub header_line1: Option>, @@ -20,6 +22,7 @@ pub struct TitleInfo<'source> { pub author_line3: Option>, pub date: Option>, pub prof: Option>, + pub code_section_title: Option>, } // For whatever reason, std library Cow requires ToOwned for it's contained value to be cloned @@ -30,7 +33,7 @@ pub struct TitleInfo<'source> { // Also, vectors of parsed tokens are obviously not intended to live longer than the source. // In fact, for any possible appliance they can be assumed to life for as long as source does. // Hope is for lifetime variance to help with lifetime casting, or th -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Hash)] // #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Tokens<'source> { Owned(Box<[Token<'source>]>), @@ -47,6 +50,14 @@ impl<'source> Deref for Tokens<'source> { } } } +impl<'source> Clone for Tokens<'source> { + fn clone(&self) -> Self { + match self { + Self::Owned(arg0) => Self::Owned(arg0.clone()), + Self::Borrowed(arg0) => Self::Borrowed(arg0), + } + } +} impl<'r, 'source: 'r> IntoIterator for &'r Tokens<'source> { type Item = &'r Token<'source>; @@ -78,10 +89,29 @@ impl<'source> Tokens<'source> { } } +#[derive(Debug, Clone, Hash, PartialEq)] +pub enum DisplayState<'code> { + NotDisplayed, + DisplayedNoCaption, + Caption(Box>), +} + +impl<'code> DisplayState<'code> { + fn to_static(&self) -> DisplayState<'static> { + match self { + DisplayState::NotDisplayed => DisplayState::NotDisplayed, + DisplayState::DisplayedNoCaption => DisplayState::DisplayedNoCaption, + DisplayState::Caption(caption) => { + DisplayState::Caption(Box::new(caption.to_static_token())) + } + } + } +} + #[derive(Debug, Clone, Hash, PartialEq)] // #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct AyanoBlock<'code> { - pub is_display: bool, + pub display_state: DisplayState<'code>, pub is_space_before: bool, pub is_static: bool, pub code: Tx<'code>, @@ -102,7 +132,7 @@ impl PartialEq for AyanoBlock<'_> { } */ -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Hash)] // #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum ListType { Bullet, @@ -112,7 +142,7 @@ pub enum ListType { Roman, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Hash)] // #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Formatting { Bold, @@ -123,7 +153,7 @@ pub enum Formatting { type Tx<'source> = Cow<'source, str>; type Pth<'source> = Cow<'source, Path>; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone, Hash)] // #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Token<'source> { PageDiv, @@ -146,7 +176,7 @@ pub enum Token<'source> { src_name: Pth<'source>, caption: Option>>, ident: Option>, - width: Option, + width: Option>, }, Href { space_before: bool, @@ -255,7 +285,7 @@ impl ToStaticExt for AyanoBlock<'_> { let AyanoBlock { code, insert_path, - is_display, + display_state, is_static, is_space_before, } = self; @@ -263,7 +293,7 @@ impl ToStaticExt for AyanoBlock<'_> { code: code.to_static(), insert_path: insert_path.as_ref().map(Cow::to_static), is_static: *is_static, - is_display: *is_display, + display_state: display_state.to_static(), is_space_before: *is_space_before, } } @@ -313,7 +343,7 @@ impl<'source> Token<'source> { src_name: Cow::Borrowed(src_name), caption: caption.as_ref().map(|t| Box::new(Token::borrow_ref(t))), ident: ident.as_ref().map(|s| Cow::<'r, str>::Borrowed(s)), - width: *width, + width: width.clone(), }, Token::Href { url, @@ -412,7 +442,7 @@ impl<'source> Token<'source> { src_name: src_name.to_static(), caption: caption.as_ref().map(Box::to_static), ident: ident.as_ref().map(Cow::to_static), - width: *width, + width: width.clone(), }, Token::Href { url, diff --git a/src/gen/ayano.rs b/src/gen/ayano.rs index 898d891..2b80ce9 100644 --- a/src/gen/ayano.rs +++ b/src/gen/ayano.rs @@ -20,10 +20,10 @@ use std::{ }; use crate::{ - data::{AyanoBlock, Token, Tokens}, + data::{AyanoBlock, DisplayState, Token, Tokens}, lexer::KyomatoLexError, util::{ - Equivalent, GenericRange, GenericRangeParseError, Immutable, InlinedStack, + Equivalent, GenericRange, GenericRangeParseError, HashIgnored, Immutable, InlinedStack, InlinedStackAccessor, }, }; @@ -35,14 +35,14 @@ mod python; #[derive(Debug)] pub struct DisplayInfo { pub code: String, - pub name: String, + pub caption: Token<'static>, } #[derive(Debug, PartialEq)] struct CodelessBlockInfo { ident: u64, function_name: Option, - display_name: Option, + caption: Option>, insert_path: Option, } @@ -89,9 +89,9 @@ impl AyanoBuilder { BlockInfo { ident, function_name: info.function_name, - display_info: info.display_name.map(|name| DisplayInfo { + display_info: info.caption.map(|name| DisplayInfo { code: transformed_code, - name, + caption: name, }), insert_path: info.insert_path, }, @@ -181,7 +181,7 @@ mod ayano_tests { fn $name() { // arrange let block = AyanoBlock { - is_display: false, + display_state: DisplayState::NotDisplayed, is_static: false, code: $code.into(), insert_path: None, @@ -230,14 +230,14 @@ y = x / 100 let static_block = AyanoBlock { code: $static_code.into(), insert_path: None, - is_display: false, + display_state: DisplayState::NotDisplayed, is_static: true, is_space_before: false, }; let function_block = AyanoBlock { code: $function_code.into(), insert_path: None, - is_display: false, + display_state: DisplayState::NotDisplayed, is_static: false, is_space_before: false, }; @@ -274,21 +274,21 @@ y", "2"} let static_block = AyanoBlock { code: $static_code.into(), insert_path: None, - is_display: false, + display_state: DisplayState::NotDisplayed, is_static: true, is_space_before: false, }; let function1_block = AyanoBlock { code: $function1_code.into(), insert_path: None, - is_display: false, + display_state: DisplayState::NotDisplayed, is_static: false, is_space_before: false, }; let function2_block = AyanoBlock { code: $function2_code.into(), insert_path: None, - is_display: false, + display_state: DisplayState::NotDisplayed, is_static: false, is_space_before: false, }; @@ -400,10 +400,15 @@ fn apply_ayano<'token, 'source: 'token>( */ // create display name - let display_name = if block.is_display { - Some(block_name(ident)) - } else { - None + let display_name = match &block.display_state { + crate::data::DisplayState::NotDisplayed => None, + crate::data::DisplayState::DisplayedNoCaption => Some(Token::Paragraph { + is_newline: true, + space_before: false, + formatting: None, + content: block_name(ident).into(), + }), + crate::data::DisplayState::Caption(tokens) => Some(tokens.to_static_token()), }; // return out static block here @@ -414,7 +419,7 @@ fn apply_ayano<'token, 'source: 'token>( CodelessBlockInfo { ident, function_name: None, - display_name, + caption: display_name, insert_path, }, )); @@ -432,7 +437,7 @@ fn apply_ayano<'token, 'source: 'token>( CodelessBlockInfo { ident, function_name, - display_name, + caption: display_name, insert_path, }, )); @@ -458,7 +463,7 @@ fn apply_ayano<'token, 'source: 'token>( ident, function_name, insert_path, - display_name, + caption: display_name, }, )) } @@ -484,7 +489,7 @@ mod apply_ayano_tests { let block = AyanoBlock { code: $input.into(), insert_path: None, - is_display: false, + display_state: DisplayState::NotDisplayed, is_static: false, is_space_before: false, }; @@ -581,7 +586,7 @@ y = x + 2 let code = "@dev: x,y"; let block = AyanoBlock { - is_display: false, + display_state: DisplayState::NotDisplayed, code: code.into(), insert_path: Some(insert_path.into()), is_static: false, @@ -1502,7 +1507,7 @@ fn parse_ayano( None } else { let width_str = throw_syntax!(width.str().and_then(|s| s.to_str()))?; - Some(width_str.parse()?) + Some(HashIgnored(width_str.parse()?)) }; return Ok(Token::Figure { src_name: Cow::Owned(PathBuf::from(src)), @@ -1658,7 +1663,7 @@ mod parsing_tests { src_name: Cow::Owned(PathBuf::from("path/to/file.png")), caption: Some(Box::new(Token::text("figure caption"))), ident: Some("circle".into()), - width: Some(0.625), + width: Some(HashIgnored(0.625)), })} test! {table_nocaption, |py| { diff --git a/src/gen/lab/mod.rs b/src/gen/lab/mod.rs index f2653ee..126761e 100644 --- a/src/gen/lab/mod.rs +++ b/src/gen/lab/mod.rs @@ -5,11 +5,16 @@ pub use context::Context; pub use meta::SourceMeta; use crate::{ - data::{Formatting, TitleInfo, Token}, + data::{AyanoBlock, Formatting, TitleInfo, Token}, + gen::ayano::DisplayInfo, path_engine::PathEngine, }; use itertools::Itertools; -use std::{borrow::Borrow, fmt::Write, marker::PhantomData}; +use std::{ + borrow::{Borrow, Cow}, + fmt::Write, + marker::PhantomData, +}; use super::{ayano::AyanoExecutor, GenerationError, OutputGenerator, Res}; @@ -359,6 +364,7 @@ where author_line3, date, prof, + code_section_title: _code_section_title, } = &meta.title_info; let replacements = [ header_line1, @@ -390,9 +396,51 @@ where fn write_postamble<'meta, 'context, W: Write + ?Sized>( &self, output: &mut W, - _: &'meta SourceMeta<'source, AyanoExecutor>, - _: &'context mut Context, + meta: &'meta SourceMeta<'source, AyanoExecutor>, + context: &'context mut Context, ) -> Res<'source> { + let mut display_ayano_blocks = meta.ayano.display_blocks().into_iter(); + if let Some(first) = display_ayano_blocks.next() { + fn write_display_block< + 'source, + 'meta, + 'context, + C, + G: OutputGenerator<'source, SourceMeta<'source, AyanoExecutor>, C>, + W: Write + ?Sized, + >( + generator: &G, + output: &mut W, + meta: &'meta SourceMeta<'source, AyanoExecutor>, + context: &'context mut C, + block: &'meta DisplayInfo, + ) -> Res<'source> + where + 'source: 'meta + 'context, + { + let block_code_block = Token::CodeBlock { + code: Cow::Owned(block.code.clone()), + language: Some("python".into()), + }; + generator.write_to(output, meta, context, &block_code_block)?; + generator.write_to(output, meta, context, &block.caption)?; + Ok(()) + } + let blocks_section_header = Token::Header { + order: 1, + content: meta + .title_info + .code_section_title + .as_ref() + .unwrap_or(&"".into()) + .clone(), + }; + self.write_to(output, meta, context, &blocks_section_header)?; + write_display_block(self, output, meta, context, first)?; + for block in display_ayano_blocks { + write_display_block(self, output, meta, context, block)?; + } + } output.write_str("\n\n\\end{document}")?; Ok(()) } @@ -402,7 +450,10 @@ where mod tests { use std::path::Path; - use crate::data::{AyanoBlock, Formatting, ListType}; + use crate::{ + data::{AyanoBlock, DisplayState, Formatting, ListType}, + util::HashIgnored, + }; use super::*; @@ -522,7 +573,7 @@ mod tests { \label{fig:schema-1} \end{figure} "} - test! {fig3_width, Token::Figure { src_name: Path::new("schema1.jpg").into(), caption: Some(Box::new(text!("A schema providing a bunch of very important info on quantum refurbalidzer function"))), ident: Some("schema-1".into()), width: Some(0.25) }, r" + test! {fig3_width, Token::Figure { src_name: Path::new("schema1.jpg").into(), caption: Some(Box::new(text!("A schema providing a bunch of very important info on quantum refurbalidzer function"))), ident: Some("schema-1".into()), width: Some(HashIgnored(0.25)) }, r" \begin{figure}[h!] \centering \includegraphics[width = 0.25 \textwidth]{schema1.jpg} @@ -670,21 +721,21 @@ cell_21 & cell_22 & cell_23 \\ \hline "} test! {ayano_plain1, Token::Ayano{data: AyanoBlock{ - is_display: false, + display_state: DisplayState::NotDisplayed, is_static: false, code: "1".into(), insert_path: None, is_space_before: false, }}, r"1"} test! {ayano_plain2, Token::Ayano{data: AyanoBlock{ - is_display: false, + display_state: DisplayState::NotDisplayed, is_static: false, code: "1 + 2".into(), insert_path: None, is_space_before: false, }}, r"3"} test! {ayano_plain3, Token::Ayano{data: AyanoBlock{ - is_display: false, + display_state: DisplayState::NotDisplayed, is_static: false, code: r" from math import sqrt @@ -696,7 +747,7 @@ int(y)" is_space_before: false, }}, r"10"} test! {ayano_plain4, Token::Ayano{data: AyanoBlock{ - is_display: false, + display_state: DisplayState::NotDisplayed, is_static: false, code: r" res = 0 @@ -708,7 +759,7 @@ res" is_space_before: false, }}, r"5050"} test! {ayano_err1, Token::Ayano{data: AyanoBlock{ - is_display: false, + display_state: DisplayState::NotDisplayed, is_static: false, code: r" 'err', 1.0, 0.1" @@ -717,7 +768,7 @@ res" is_space_before: false, }}, r"1.00 $\pm$ 0.10"} test! {ayano_err2, Token::Ayano{data: AyanoBlock{ - is_display: false, + display_state: DisplayState::NotDisplayed, is_static: false, code: r" 'err', 1.0, 0.4" @@ -726,7 +777,7 @@ res" is_space_before: false, }}, r"1.0 $\pm$ 0.4"} test! {ayano_err3, Token::Ayano{data: AyanoBlock{ - is_display: false, + display_state: DisplayState::NotDisplayed, is_static: false, code: r" x = 1.0 @@ -739,7 +790,7 @@ y = 0.4 } test! {ayano_fig1, Token::Ayano{data: AyanoBlock{ - is_display: false, + display_state: DisplayState::NotDisplayed, is_static: false, code: r"'fig', 'path/to/image.jpg', None, None, None".into(), insert_path: None, @@ -752,7 +803,7 @@ y = 0.4 } test! {ayano_fig2_width, Token::Ayano{data: AyanoBlock{ - is_display: false, + display_state: DisplayState::NotDisplayed, is_static: false, code: r"'fig', 'path/to/image.jpg', None, None, 0.625".into(), insert_path: None, @@ -764,14 +815,13 @@ y = 0.4 \end{figure}" } - test! {ayano_fig2, Token::Ayano{data: AyanoBlock{is_display: false,is_static: false,code: r"@fig: src = 'path/to/image.jpg'".into(),insert_path: None, - is_space_before: false,}}, + test! {ayano_fig2, Token::Ayano{data: AyanoBlock{display_state: DisplayState::NotDisplayed,is_static: false,code: r"@fig: src = 'path/to/image.jpg'".into(),insert_path: None,is_space_before: false,}}, r" \begin{figure}[h!] \centering \includegraphics[width = 0.9 \textwidth]{path/to/image.jpg} \end{figure}"} - test! {ayano_fig3, Token::Ayano{data: AyanoBlock{ is_display: false,is_static: false,code: r#"@fig: src = 'path/to/image.jpg', ident = "meow""#.into(),insert_path: None, + test! {ayano_fig3, Token::Ayano{data: AyanoBlock{ display_state: DisplayState::NotDisplayed,is_static: false,code: r#"@fig: src = 'path/to/image.jpg', ident = "meow""#.into(),insert_path: None, is_space_before: false, }}, r#" @@ -783,7 +833,7 @@ r#" } // TODO add tests with captions - test! {ayano_gen_tab1, Token::Ayano{data: AyanoBlock { is_display: false, is_static: false, code: + test! {ayano_gen_tab1, Token::Ayano{data: AyanoBlock { display_state: DisplayState::NotDisplayed, is_static: false, code: r#" data = [[1, 2, 3], [4, 5, 6]] @gen_table: lambda r,c: data[r][c]; rows=2, columns=3 diff --git a/src/gen/mod.rs b/src/gen/mod.rs index e16c56b..6c0b576 100644 --- a/src/gen/mod.rs +++ b/src/gen/mod.rs @@ -29,8 +29,8 @@ pub trait OutputGenerator<'source, Meta, Context> { token: &'token Token<'source>, ) -> Res<'source> where - 'source: 'meta + 'context + 'token, - 'token: 'context; + 'source: 'meta + 'context + 'token, + 'token: 'context; fn write_tokens_to<'meta, 'context, 'token, W: Write + ?Sized>( &self, @@ -40,19 +40,15 @@ pub trait OutputGenerator<'source, Meta, Context> { tokens: &'token Tokens<'source>, ) -> Res<'source> where - 'source: 'meta + 'context + 'token, - 'token: 'context, - { + 'source: 'meta + 'context + 'token, + 'token: 'context, + { tokens .into_iter() .map(|t| self.write_to(output, meta, context, t)) .try_collect() } - fn write_preamble<'meta, W: Write + ?Sized>( - &self, - _: &mut W, - _: &'meta Meta, - ) -> Res<'source> { + fn write_preamble<'meta, W: Write + ?Sized>(&self, _: &mut W, _: &'meta Meta) -> Res<'source> { Ok(()) } fn write_postamble<'meta, 'context, W: Write + ?Sized>( diff --git a/src/lexer.rs b/src/lexer.rs index dafa096..a3da545 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -21,8 +21,10 @@ use nom::{ use url::Url; use crate::{ - data::{AyanoBlock, Formatting, ListType, TitleInfo, Token, Tokens}, - util::{optional_permutation, Equivalent, IteratorArrayCollectExt, VecTryExtendExt}, + data::{AyanoBlock, DisplayState, Formatting, ListType, TitleInfo, Token, Tokens}, + util::{ + optional_permutation, Equivalent, HashIgnored, IteratorArrayCollectExt, VecTryExtendExt, + }, }; /// Matches any sort of whitespace characters, *newline*, and a provided tag at the end @@ -406,7 +408,7 @@ fn figure< src_name: Path::new(path).into(), caption: caption.map(Box::new), ident: ident.map(Cow::from), - width, + width: width.map(HashIgnored), }; Ok((rest, token)) } @@ -551,6 +553,11 @@ fn ayano< // - inner None means this block has no display caption // - inner Some means this block has display caption // TODO add code captions support + let display_state = match display { + None => DisplayState::NotDisplayed, + Some(None) => DisplayState::DisplayedNoCaption, + Some(Some(caption)) => DisplayState::Caption(Box::new(caption)), + }; let insert: Option<&'source Path> = insert.map(Path::new); // XXX add support for codeblock identifiers let code: Cow<'_, str> = Cow::from(code); @@ -560,7 +567,7 @@ fn ayano< data: AyanoBlock { insert_path: insert.map(Cow::from), code, - is_display: display.is_some(), + display_state, is_static: is_static.is_some(), is_space_before: before.contains(' '), }, @@ -1970,7 +1977,7 @@ $$ src_name: Cow::Borrowed(Path::new($path)), ident: $ident.map(Cow::from), caption: $caption.map(Box::new), - width: $width, + width: $width.map(HashIgnored), } }; } @@ -2110,7 +2117,7 @@ $$ macro_rules! ayano { {!$is_static:literal, *$description:expr, ~$insert_path:expr, #$_id:expr, $code:literal} => { Token::Ayano{data: AyanoBlock{ - is_display: $description.is_some(), + display_state: $description, is_static: $is_static, code: Cow::Borrowed($code), insert_path: $insert_path.map(Path::new).map(Cow::from), @@ -2119,6 +2126,8 @@ $$ }; } mod ayano { + use crate::data::DisplayState; + use super::*; macro_rules! test_ok { @@ -2135,11 +2144,13 @@ $$ test_err! {err_empty, ""} test_ok! {ok_simple, "\n```python, Ayano * \"Code description\" ~ path/to/file # 1\nx = 1\ny = 2\nprint(x + y)\n```\n", - ayano!(!false, *Some(tx!("Code description")), ~Some("path/to/file"), #Some(1), "x = 1\ny = 2\nprint(x + y)"), "\n"} + ayano!(!false, *DisplayState::Caption(tx!("Code description").into()), ~Some("path/to/file"), #Some(1), "x = 1\ny = 2\nprint(x + y)"), "\n"} test_ok! {ok_some, "\n```python, Ayano ! ~ path/to/file # 1\nx = 1\ny = 2\nprint(x + y)\n```\n", - ayano!(!true, *None::<&str>, ~Some("path/to/file"), #Some(1), "x = 1\ny = 2\nprint(x + y)"), "\n"} + ayano!(!true, *DisplayState::NotDisplayed, ~Some("path/to/file"), #Some(1), "x = 1\ny = 2\nprint(x + y)"), "\n"} test_ok! {ok_no_args, "\n```python, Ayano \nx = 1\ny = 2\nprint(x + y)\n```\n", - ayano!(!false, *None::<&str>, ~None::<&str>, #None, "x = 1\ny = 2\nprint(x + y)"), "\n"} + ayano!(!false, *DisplayState::NotDisplayed, ~None::<&str>, #None, "x = 1\ny = 2\nprint(x + y)"), "\n"} + test_ok! {ok_displayed_no_name, "\n```python, Ayano * \nx = 1\ny = 2\nprint(x + y)\n```\n", + ayano!(!false, *DisplayState::DisplayedNoCaption, ~None::<&str>, #None, "x = 1\ny = 2\nprint(x + y)"), "\n"} } mod lex { use super::*; @@ -2201,6 +2212,7 @@ pub fn title_info<'source, Err: nom::error::ParseError<&'source str>>( author_line3, date, prof, + code_section_title, ), ) = optional_permutation(( match_field!("header-line1"), @@ -2215,6 +2227,7 @@ pub fn title_info<'source, Err: nom::error::ParseError<&'source str>>( match_field!("author-line3"), match_field!("date"), match_field!("prof"), + match_field!("code-section-title"), )) .parse(input)?; let res = TitleInfo { @@ -2230,6 +2243,7 @@ pub fn title_info<'source, Err: nom::error::ParseError<&'source str>>( author_line3, date, prof, + code_section_title, }; let (input, _) = tag("\n---")(input)?; Ok((input, res)) @@ -2285,19 +2299,19 @@ date: вербень 2077 prof: me :idk: --- ", [ - header_line1: "Навч. заклад", - header_line2: "Факультет", - document_type: "ПРОТОКОЛ", - title_line1: "Виконання марної роботи", - title_line2: "з історії стародавнього Єгипту", - title_line3: "(пинальна частина)", - title_line4: "НАЗВА РОБОТИ", - author_line1: "виконувало:", - author_line2: "дяч дзучунович", - author_line3: "(perfectly still)", - date: "вербень 2077", - prof: "me :idk:" - ], "\n"} + header_line1: "Навч. заклад", + header_line2: "Факультет", + document_type: "ПРОТОКОЛ", + title_line1: "Виконання марної роботи", + title_line2: "з історії стародавнього Єгипту", + title_line3: "(пинальна частина)", + title_line4: "НАЗВА РОБОТИ", + author_line1: "виконувало:", + author_line2: "дяч дзучунович", + author_line3: "(perfectly still)", + date: "вербень 2077", + prof: "me :idk:" + ], "\n"} test_ok! {ok_all_fields_permutated, r"--- author-line2: дяч дзучунович header-line1: Навч. заклад @@ -2313,17 +2327,17 @@ prof: me :idk: title-line3: (пинальна частина) --- ", [ - header_line1: "Навч. заклад", - header_line2: "Факультет", - document_type: "ПРОТОКОЛ", - title_line1: "Виконання марної роботи", - title_line2: "з історії стародавнього Єгипту", - title_line3: "(пинальна частина)", - title_line4: "НАЗВА РОБОТИ", - author_line1: "виконувало:", - author_line2: "дяч дзучунович", - author_line3: "(perfectly still)", - date: "вербень 2077", - prof: "me :idk:" - ], "\n"} + header_line1: "Навч. заклад", + header_line2: "Факультет", + document_type: "ПРОТОКОЛ", + title_line1: "Виконання марної роботи", + title_line2: "з історії стародавнього Єгипту", + title_line3: "(пинальна частина)", + title_line4: "НАЗВА РОБОТИ", + author_line1: "виконувало:", + author_line2: "дяч дзучунович", + author_line3: "(perfectly still)", + date: "вербень 2077", + prof: "me :idk:" + ], "\n"} } diff --git a/src/util.rs b/src/util.rs index fbe8a6f..b74a715 100644 --- a/src/util.rs +++ b/src/util.rs @@ -451,3 +451,16 @@ pub fn optional_permutation<'parsers, 'source: 'parsers, In, Out, E: ParseError< ) -> impl Parser + 'parsers { move |input| parsers.optional_permutation(input) } + +// FIXME this is SURELY not idiomatic. I should move to ident system +#[derive( + Debug, + derive_more::Deref, + derive_more::DerefMut, + PartialEq, + Clone, +)] +pub struct HashIgnored(pub T); +impl std::hash::Hash for HashIgnored { + fn hash(&self, state: &mut H) {} +} diff --git a/tests/input_files/ayano_display_blocks.md b/tests/input_files/ayano_display_blocks.md new file mode 100644 index 0000000..1e97506 --- /dev/null +++ b/tests/input_files/ayano_display_blocks.md @@ -0,0 +1,25 @@ +--- +code-section-title: Код +--- + +# Display blocks + +This feature allows you to insert certain `Ayano` blocks to the end of the output document, to document your exact actions for displayed results. + +Here's an example: + +```python, Ayano * +# You should be able to see this comment in the output doc +42 +``` + +Display blocks can have descriptions too! Here's one: + +```python, Ayano * "me, fooling around" +# This code block has description +# Might as well add some python syntax to check out minted's syntax highlighting: +for i in range(100): + s = str(i) + s = s[::-1] +@dev: -2.034e4, 145.64 +``` \ No newline at end of file diff --git a/tests/lex_files/ayano.txt b/tests/lex_files/ayano.txt index 09cebe2..6128484 100644 --- a/tests/lex_files/ayano.txt +++ b/tests/lex_files/ayano.txt @@ -11,6 +11,7 @@ TitleInfo { author_line3: None, date: None, prof: None, + code_section_title: None, } Multiple { tokens: Owned( @@ -29,7 +30,7 @@ Multiple { }, Ayano { data: AyanoBlock { - is_display: false, + display_state: NotDisplayed, is_space_before: false, is_static: false, code: "@fig: src=\"circle.png\", ident=\"circle\", caption=\"Some caption for a line drawing. This is parsed as a *regular document text*, so you may [@eq:line] and $f(x) = A \\cdot x^{1} + B \\cdot x^{0}$ here, for example\"", @@ -44,7 +45,7 @@ Multiple { }, Ayano { data: AyanoBlock { - is_display: false, + display_state: NotDisplayed, is_space_before: false, is_static: false, code: "width = 0.2\n@fig: src = \"graph.png\", width = width", @@ -59,7 +60,7 @@ Multiple { }, Ayano { data: AyanoBlock { - is_display: false, + display_state: NotDisplayed, is_space_before: false, is_static: false, code: "data = [\n\t\t[\"x\", \"y\"],\n\t\t[1.0, 3.0],\n\t\t[3.0, 4.0],\n\t\t[-1.0, -5743234.0],\n]\n@gen_table: lambda r,c: data[r][c]; rows=4, columns=2, caption=\"Caption for a table 1, *because* why not\", ident=\"table1\"", diff --git a/tests/lex_files/ayano_display_blocks.txt b/tests/lex_files/ayano_display_blocks.txt new file mode 100644 index 0000000..0eb9fb2 --- /dev/null +++ b/tests/lex_files/ayano_display_blocks.txt @@ -0,0 +1,70 @@ +TitleInfo { + header_line1: None, + header_line2: None, + document_type: None, + title_line1: None, + title_line2: None, + title_line3: None, + title_line4: None, + author_line1: None, + author_line2: None, + author_line3: None, + date: None, + prof: None, + code_section_title: Some( + "Код", + ), +} +Multiple { + tokens: Owned( + [ + Header { + order: 1, + content: "Display blocks", + }, + Paragraph { + is_newline: true, + space_before: false, + formatting: None, + content: "This feature allows you to insert certain `Ayano` blocks to the end of the output document, to document your exact actions for displayed results.", + }, + Paragraph { + is_newline: true, + space_before: false, + formatting: None, + content: "Here's an example:", + }, + Ayano { + data: AyanoBlock { + display_state: DisplayedNoCaption, + is_space_before: false, + is_static: false, + code: "# You should be able to see this comment in the output doc\n42", + insert_path: None, + }, + }, + Paragraph { + is_newline: true, + space_before: false, + formatting: None, + content: "Display blocks can have descriptions too! Here's one:", + }, + Ayano { + data: AyanoBlock { + display_state: Caption( + Paragraph { + is_newline: false, + space_before: false, + formatting: None, + content: "me, fooling around", + }, + ), + is_space_before: false, + is_static: false, + code: "# This code block has description\n# Might as well add some python syntax to check out minted's syntax highlighting:\nfor i in range(100):\n s = str(i)\n s = s[::-1]\n@dev: -2.034e4, 145.64", + insert_path: None, + }, + }, + ], + ), +} \ No newline at end of file diff --git a/tests/lex_files/basic.txt b/tests/lex_files/basic.txt index fbea639..888c2fd 100644 --- a/tests/lex_files/basic.txt +++ b/tests/lex_files/basic.txt @@ -11,6 +11,7 @@ TitleInfo { author_line3: None, date: None, prof: None, + code_section_title: None, } Multiple { tokens: Owned( @@ -100,7 +101,9 @@ Multiple { "refurb", ), width: Some( - 0.4, + HashIgnored( + 0.4, + ), ), }, Paragraph { diff --git a/tests/lex_files/deceit.txt b/tests/lex_files/deceit.txt index 10a94fa..4ce3c3b 100644 --- a/tests/lex_files/deceit.txt +++ b/tests/lex_files/deceit.txt @@ -11,6 +11,7 @@ TitleInfo { author_line3: None, date: None, prof: None, + code_section_title: None, } Multiple { tokens: Owned( @@ -804,7 +805,7 @@ Multiple { }, Ayano { data: AyanoBlock { - is_display: false, + display_state: NotDisplayed, is_space_before: false, is_static: false, code: "2", diff --git a/tests/lex_files/tables_code.txt b/tests/lex_files/tables_code.txt index e10ce6b..765b681 100644 --- a/tests/lex_files/tables_code.txt +++ b/tests/lex_files/tables_code.txt @@ -11,6 +11,7 @@ TitleInfo { author_line3: None, date: None, prof: None, + code_section_title: None, } Multiple { tokens: Owned( diff --git a/tests/lex_files/title_info.txt b/tests/lex_files/title_info.txt index 5621911..dbc59ba 100644 --- a/tests/lex_files/title_info.txt +++ b/tests/lex_files/title_info.txt @@ -35,6 +35,7 @@ TitleInfo { prof: Some( "me :idk:", ), + code_section_title: None, } Multiple { tokens: Owned( diff --git a/tests/transformed_files/ayano_display_blocks.txt b/tests/transformed_files/ayano_display_blocks.txt new file mode 100644 index 0000000..583224d --- /dev/null +++ b/tests/transformed_files/ayano_display_blocks.txt @@ -0,0 +1,187 @@ +\documentclass[a4paper]{article} +\usepackage{graphicx, hyperref, xcolor, gensymb, amssymb, mathtools, wrapfig, mathtools, microtype, lastpage, caption, titlesec, paracol, longtable, booktabs, cancel} +\usepackage[T2A, T1]{fontenc} +\usepackage[utf8]{inputenc} +\usepackage[russian, ukrainian]{babel} +\hypersetup{ + colorlinks, + linkcolor={blue!20!black}, + citecolor={blue!50!black}, + urlcolor={blue!80!black} +} + +\usepackage[left = 25mm, right = 10mm, top=20mm, bottom=20mm, bindingoffset=0cm]{geometry} + +% КОМАНДИ + +\newcommand{\fnt}{ +\fontshape{n} +\fontsize{14pt} {19pt} +\linespread{0.8} +\selectfont +} % нормативний шрифт +\let\oldCaption\caption +\renewcommand{\caption}[1]{ +\oldCaption{\fnt #1} +} % підпис нормативним шрифтом. + +\newcommand{\tb}{ + \hspace*{10mm} +} % відступ у 5 символів "x" відповідно до ДСТУ +\newcommand{\tbln}{ + \newline + \hspace*{10mm} +} % те ж саме, але з переносом рядка +\newcommand{\tbsp}{ + \vspace*{1ex} + \newline + \hspace*{10mm} +} % те ж саме, але з додатковим відступом між рядками + +\iffalse +\let\oldSection\section +\renewcommand{\section}[1]{ + \oldSection*{\tb #1} + \addcontentsline{toc}{section}{#1} + \fnt + \tb +} % відображає у тексті та змісті наданий заголовок +\fi + +\iftrue +\let\oldSubSection\subsection +\renewcommand{\subsection}[1]{ + \oldSubSection*{\tb #1} + \addcontentsline{toc}{subsection}{#1} + \fnt + \tb +} % відображає у тексті та змісті наданий підзаголовок +\fi + +% бібліографія +\usepackage[square,sort,comma,numbers]{natbib} +\renewcommand{\bibsection}{} +\usepackage{totcount} +\newtotcounter{citnum} +% лічильник цитувань +\def\oldbibitem{} \let\oldbibitem=\bibitem +\def\bibitem{\stepcounter{citnum}\oldbibitem} + +% лічильник зображень +\newtotcounter{graphnum} +\def\oldincludegraphics{} \let\oldincludegraphics=\includegraphics +\def\includegraphics{\stepcounter{graphnum}\oldincludegraphics} + +% лічильник таблиць (НЕАВТОМАТИЧНИЙ!!!) +\newtotcounter{tabnum} + +% лічильник додатків (НЕАВТОМАТИЧНИЙ!!!) +\newtotcounter{dodnum} + +% нумерація сторінок +\usepackage{fancyhdr} +\pagestyle{fancy} +\fancyhf{} +\renewcommand{\headrulewidth}{0pt} +\setlength{\headheight}{15.3pt} +\fancyhead[R]{\fnt \thepage} + +% виноски +\renewcommand{\thefootnote}{\large\arabic{footnote})~} +\renewcommand{\footnoterule}{\rule{20mm}{0.4pt} \vspace*{0.5ex}} +% \newcommand{\vyn}[2]{ +% \footnote[#1]{\large #2} +% } % створити виноску + +\let\oldFootnote\footnote +\renewcommand{\footnote}[1]{ + \oldFootnote{\large #1} +} % перегрузка виноски + +\providecommand{\tightlist}{% + \setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}} + +% підписи малюнків/таблиць +\captionsetup[figure]{name={\fnt Рисунок~},labelsep=period} +\captionsetup[table]{name={\fnt Таблиця~},labelsep=period} + +% idk why, but this redefinition does not work, for some reason +% \let\oldCaption\caption +% \renewcommand{\caption}[1] { +% \oldCaption{\fnt #1} +% } + +% markdown strikeouts +\usepackage{soul} + +% codeblocks +\usepackage{minted} + +\titleformat*{\section}{\fnt\LARGE\bfseries} +\titleformat*{\subsection}{\fnt\Large\bfseries} +\titleformat*{\subsubsection}{\fnt\Large} + +\begin{document} +\thispagestyle{empty} % [A]:for cropping +% ------------ +% source: https://tex.stackexchange.com/questions/173651/need-help-to-create-such-a-beautiful-title-page +\begingroup +\centering +\obeylines +\topskip0pt % source: https://tex.stackexchange.com/questions/2326/vertically-center-text-on-a-page +\vspace*{\fill} +\textbf{ +\LARGE +\LARGE +\vspace{100pt} +\LARGE +\Large +\Large +\Large +\huge +\vspace{50pt} +\large +\large +\large +\large +\vspace*{\fill} +\hspace{\fill}\large \rule{20mm}{0.4pt} +\vspace{10pt} +} +\endgroup +% ------------ +\newpage +\tableofcontents + +\newpage +\fnt + + +\subsection{Display blocks} + +\tbln This feature allows you to insert certain `Ayano` blocks to the end of the output document, to document your exact actions for displayed results. +\tbln Here's an example:42 +\tbln Display blocks can have descriptions too! Here's one:-20340$\pm$150 +\subsection{Код} + +\begin{minted}{python} +# This code block has description +# Might as well add some python syntax to check out minted's syntax highlighting: +for i in range(100): + s = str(i) + s = s[::-1] +return "err", -2.034e4, 145.64 + +\end{minted} + +me, fooling around +\begin{minted}{python} +# You should be able to see this comment in the output doc +return 42 + +\end{minted} + + +\tbln ayano_block__1574475637377647406 + +\end{document} \ No newline at end of file From ae24906a1f97c084d2fc0329e877dc197fe81161 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Tue, 26 Mar 2024 19:35:19 +0200 Subject: [PATCH 43/98] Added support for latin lists --- src/gen/lab/mod.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/gen/lab/mod.rs b/src/gen/lab/mod.rs index 126761e..a841985 100644 --- a/src/gen/lab/mod.rs +++ b/src/gen/lab/mod.rs @@ -186,7 +186,7 @@ where crate::data::ListType::Bullet => "\\begin{itemize}", crate::data::ListType::Num => "\\begin{enumerate}", crate::data::ListType::Latin => { - unimplemented!("Latin lists are not supported for now") + "\\begin{enumerate}[label=\\alph*), ref=\\alph*]" } crate::data::ListType::Cyrillic => { "\\begin{enumerate}[label=\\asbuk*), ref=\\asbuk*]" @@ -197,12 +197,9 @@ where }; let end: &str = match list_type { crate::data::ListType::Bullet => "\\end{itemize}", - crate::data::ListType::Num | crate::data::ListType::Cyrillic => { - "\\end{enumerate}" - } - crate::data::ListType::Latin => { - unimplemented!("Latin lists are not supported for now") - } + crate::data::ListType::Num + | crate::data::ListType::Cyrillic + | crate::data::ListType::Latin => "\\end{enumerate}", crate::data::ListType::Roman => { unimplemented!("Roman numeral lists are not supported for now") } From f1da0bca6a6427921cac34aad0ceac797c67b55f Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Tue, 26 Mar 2024 20:10:52 +0200 Subject: [PATCH 44/98] Fixed a little issue related to headers --- src/lexer.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lexer.rs b/src/lexer.rs index a3da545..1acb988 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -193,21 +193,21 @@ fn header< "header", tuple(( _multispace_line_break("#"), - map_res(many0_count(char('#')), |mut count| { + map_res(many0_count(char('#')), |mut hashes_count| { // one of the hashes was parsed already - count += 1; - if count <= 6 { - Ok(count) + hashes_count += 1; + if hashes_count <= 6 { + Ok(hashes_count) } else { - Err(KyomatoLexError::too_deep_header(input, count)) + Err(KyomatoLexError::too_deep_header(input, hashes_count)) } }), space0, take_until1("\n"), )), ) - .map(|(_, order, _, content)| Token::Header { - order, + .map(|(_, hashes, _, content)| Token::Header { + order: hashes - 1, content: content.trim_end().into(), // kinda sad I'm forced to do this here, but whatever }) .parse(input) From 72b60e58734ab02f8ed3fe93a2a413cd3a8087b0 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Tue, 26 Mar 2024 20:11:47 +0200 Subject: [PATCH 45/98] Added closing paren escaping to href --- src/lexer.rs | 41 +++++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/src/lexer.rs b/src/lexer.rs index 1acb988..c2f5b83 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -413,6 +413,36 @@ fn figure< Ok((rest, token)) } +fn href_kernel< + 'source, + E: ParseError<&'source str> + + ContextError<&'source str> + + FromExternalError<&'source str, KyomatoLexError>, +>( + input: &'source str, +) -> IResult<&'source str, Cow<'source, str>, E> { + //start with opening paren + let (input, _) = char('(')(input)?; + // then, find all the closing parens + let closing_parens = input.char_indices().filter(|(_, c)| c == &')'); + // among them, find ones that are not escaped + let mut unescaped_parens = closing_parens + .enumerate() + .filter(|(_, (i, _))| !input[..=*i].ends_with("\\)")); + // I need only one here + let (num, (end, _)) = unescaped_parens + .next() + .ok_or(nom::Err::Error(E::from_error_kind(input, ErrorKind::Eof)))?; + let rest = &input[end + 1..]; + let url: Cow<'source, str> = if num > 0 { + // escaping backslashes must be removed from input, unfortunately; meaning, it will be cloned + input[..end].replace("\\)", ")").into() // yep, this does clone the thing. + } else { + input[..end].into() + }; + Ok((rest, url)) +} + /// Parses markdown's hyper reference syntax /// /// Basic syntax is [DISPLAY](URL) @@ -431,12 +461,9 @@ fn href< pair( delimited(char('['), take_until("]"), char(']')), // url should be parsed into proper format - map_res( - delimited(char('('), take_until(")"), char(')')), - |url: &str| { - Url::from_str(url).map_err(|err| KyomatoLexError::bad_url(input, err)) - }, - ), + map_res(href_kernel, |url| { + Url::from_str(&url).map_err(|err| KyomatoLexError::bad_url(input, err)) + }), ), ), ) @@ -1626,6 +1653,8 @@ $$ href_ok! {href8, "[ link text](ftp://somewebsite/files/01234).", "ftp://somewebsite/files/01234", "link text", "."} // display text will be trimmed href_ok! {href9_space_before, " [link text](ftp://somewebsite/files/01234).", "ftp://somewebsite/files/01234", "link text", "."^} + // display text will be trimmed + href_ok! {href10_escaped_paren, " [cyrillic code](https://en.wikipedia.org/wiki/Cyrillic_(Unicode_block\\)).", "https://en.wikipedia.org/wiki/Cyrillic_(Unicode_block)", "cyrillic code", "."^} } // `code_block` tests From b0104e4a0f870bd45d0ddc2bfadeea54e7f4d530 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Tue, 26 Mar 2024 21:40:33 +0200 Subject: [PATCH 46/98] Reworked token borrowing a bit It's actually quite easy, idk why I struggled with it so much :/ --- src/data.rs | 79 ++++++++++++++++----------------------------- src/gen/lab/meta.rs | 26 +++++++-------- 2 files changed, 41 insertions(+), 64 deletions(-) diff --git a/src/data.rs b/src/data.rs index 074cbc2..bbc7988 100644 --- a/src/data.rs +++ b/src/data.rs @@ -2,13 +2,14 @@ use std::{ borrow::{Borrow, Cow}, ops::Deref, path::Path, + rc::Rc, }; use url::Url; use crate::util::HashIgnored; -#[derive(Debug, Default, PartialEq)] +#[derive(Debug, Default, PartialEq, Clone)] pub struct TitleInfo<'source> { pub header_line1: Option>, pub header_line2: Option>, @@ -33,29 +34,15 @@ pub struct TitleInfo<'source> { // Also, vectors of parsed tokens are obviously not intended to live longer than the source. // In fact, for any possible appliance they can be assumed to life for as long as source does. // Hope is for lifetime variance to help with lifetime casting, or th -#[derive(Debug, PartialEq, Hash)] +#[derive(Debug, PartialEq, Hash, Clone)] // #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum Tokens<'source> { - Owned(Box<[Token<'source>]>), - Borrowed(&'source [Token<'source>]), -} +pub struct Tokens<'source>(Rc<[Token<'source>]>); impl<'source> Deref for Tokens<'source> { type Target = [Token<'source>]; fn deref(&self) -> &Self::Target { - match &self { - Tokens::Owned(b) => &b[..], - Tokens::Borrowed(s) => s, - } - } -} -impl<'source> Clone for Tokens<'source> { - fn clone(&self) -> Self { - match self { - Self::Owned(arg0) => Self::Owned(arg0.clone()), - Self::Borrowed(arg0) => Self::Borrowed(arg0), - } + &*self.0 } } @@ -71,21 +58,14 @@ impl<'r, 'source: 'r> IntoIterator for &'r Tokens<'source> { impl<'source> Tokens<'source> { pub fn new(value: impl Into]>>) -> Self { - Self::Owned(value.into()) + Self(Rc::from(value.into())) } pub fn get_slice<'r>(&'r self) -> &'r [Token<'source>] where 'source: 'r, { - match &self { - Tokens::Owned(b) => &b[..], - Tokens::Borrowed(s) => s, - } - } - - pub fn borrowed(&self) -> Tokens<'_> { - Tokens::Borrowed(self.get_slice()) + self } } @@ -309,19 +289,16 @@ impl<'source> Token<'source> { } } - pub fn borrow_ref<'r>(&'r self) -> Token<'r> - where - 'source: 'r, - { + pub fn borrow_ref(&self) -> Token<'source> { match self { Token::PageDiv => Token::PageDiv, Token::Header { order, content } => Token::Header { order: *order, - content: Cow::Borrowed(content), + content: content.clone(), }, Token::DisplayMath { content, ident } => Token::DisplayMath { - content: Cow::Borrowed(&content), - ident: ident.as_ref().map(|s| Cow::<'r, str>::Borrowed(s)), + content: content.clone(), + ident: ident.clone(), }, Token::Table { header, @@ -329,10 +306,10 @@ impl<'source> Token<'source> { caption, ident, } => Token::Table { - header: header.borrowed(), - cells: cells.borrowed(), + header: header.clone(), + cells: cells.clone(), caption: caption.as_ref().map(|t| Box::new(Token::borrow_ref(t))), - ident: ident.as_ref().map(|s| Cow::<'r, str>::Borrowed(s)), + ident: ident.clone(), }, Token::Figure { src_name, @@ -340,9 +317,9 @@ impl<'source> Token<'source> { ident, width, } => Token::Figure { - src_name: Cow::Borrowed(src_name), - caption: caption.as_ref().map(|t| Box::new(Token::borrow_ref(t))), - ident: ident.as_ref().map(|s| Cow::<'r, str>::Borrowed(s)), + src_name: src_name.clone(), + caption: caption.clone(), + ident: ident.clone(), width: width.clone(), }, Token::Href { @@ -351,16 +328,16 @@ impl<'source> Token<'source> { space_before, } => Token::Href { url: url.clone(), - display: Cow::Borrowed(&display), + display: display.clone(), space_before: *space_before, }, Token::Ayano { data } => Token::Ayano { data: data.clone() }, Token::List { list_type, content } => Token::List { list_type: list_type.clone(), - content: Tokens::Borrowed(&content), + content: content.clone(), }, Token::Multiple { tokens } => Token::Multiple { - tokens: Tokens::Borrowed(tokens), + tokens: tokens.clone(), }, Token::Paragraph { is_newline, @@ -370,37 +347,37 @@ impl<'source> Token<'source> { } => Token::Paragraph { is_newline: *is_newline, formatting: formatting.clone(), - content: Cow::Borrowed(content), + content: content.clone(), space_before: *space_before, }, Token::InlineMath { content, space_before, } => Token::InlineMath { - content: Cow::Borrowed(&content), + content: content.clone(), space_before: *space_before, }, Token::Reference { ident, space_before, } => Token::Reference { - ident: Cow::Borrowed(&ident), + ident: ident.clone(), space_before: *space_before, }, Token::FootnoteReference { ident, space_before, } => Token::FootnoteReference { - ident: Cow::Borrowed(ident), + ident: ident.clone(), space_before: *space_before, }, Token::FootnoteContent { content, ident } => Token::FootnoteContent { - content: Box::new(content.borrow_ref()), - ident: Cow::Borrowed(ident), + content: content.clone(), + ident: ident.clone(), }, Token::CodeBlock { code, language } => Token::CodeBlock { - code: Cow::Borrowed(&code), - language: language.as_ref().map(|c| Cow::Borrowed(c.borrow())), + code: code.clone(), + language: language.clone(), }, Token::Error { message } => Token::Error { message: message.clone(), diff --git a/src/gen/lab/meta.rs b/src/gen/lab/meta.rs index e1b45bf..17b7f2b 100644 --- a/src/gen/lab/meta.rs +++ b/src/gen/lab/meta.rs @@ -1,5 +1,5 @@ use std::{ - borrow::BorrowMut, + borrow::{BorrowMut, Cow}, collections::{HashMap, HashSet}, }; @@ -18,27 +18,27 @@ pub enum MetaError { #[derive(Debug, Default)] pub struct SourceMeta<'source, Ayano> { - pub footnotes: HashMap<&'source str, Token<'source>>, - pub refs: HashSet<&'source str>, + pub footnotes: HashMap, Token<'source>>, + pub refs: HashSet>, pub ayano: Ayano, pub title_info: TitleInfo<'source>, } -impl<'meta> SourceMeta<'meta, AyanoBuilder> { +impl<'source> SourceMeta<'source, AyanoBuilder> { fn empty() -> Self { Self::default() } - pub fn collect<'source>(token: &'meta Token<'source>) -> Result + pub fn collect<'meta>(token: &'meta Token<'source>) -> Result where 'source: 'meta, { - let mut res: SourceMeta<'meta, _> = SourceMeta::empty(); + let mut res = SourceMeta::empty(); collect_single(token, &mut res)?; Ok(res) } - pub fn init_ayano(self) -> Result, PyErr> { + pub fn init_ayano(self) -> Result, PyErr> { Ok(SourceMeta { ayano: self.ayano.initialize()?, footnotes: self.footnotes, @@ -50,7 +50,7 @@ impl<'meta> SourceMeta<'meta, AyanoBuilder> { fn collect_single<'meta, 'r, 'source>( token: &'meta Token<'source>, - dest: &'r mut SourceMeta<'meta, AyanoBuilder>, + dest: &'r mut SourceMeta<'source, AyanoBuilder>, ) -> Result<(), MetaError> where 'source: 'meta, @@ -60,7 +60,7 @@ where Token::DisplayMath { ident: Some(ident), .. } => { - dest.refs.borrow_mut().insert(ident); + dest.refs.borrow_mut().insert(ident.clone()); } Token::Table { header, @@ -69,7 +69,7 @@ where caption, } => { if let Some(ident) = ident { - dest.refs.borrow_mut().insert(ident); + dest.refs.borrow_mut().insert(ident.clone()); } collect_iterable(header, dest)?; collect_iterable(cells, dest)?; @@ -83,7 +83,7 @@ where src_name: _src_name, width: _width, } => { - dest.refs.insert(ident); + dest.refs.insert(ident.clone()); if let Some(caption) = caption { collect_single(caption, dest)?; } @@ -93,7 +93,7 @@ where } Token::FootnoteContent { content, ident } => { collect_single(content, dest)?; - dest.footnotes.insert(ident, content.borrow_ref()); + dest.footnotes.insert(ident.clone(), content.borrow_ref()); } Token::Ayano { data } => { dest.ayano.add_block(data).map_err(MetaError::AyanoSyntax)?; @@ -116,7 +116,7 @@ where fn collect_iterable<'r, 'source: 'meta, 'meta: 'r, I>( tokens: I, - dest: &'r mut SourceMeta<'meta, AyanoBuilder>, + dest: &'r mut SourceMeta<'source, AyanoBuilder>, ) -> Result<(), MetaError> where I: IntoIterator>, From 202a02bd87303d39d9afa587806fbdf7d7c6bdb0 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Tue, 26 Mar 2024 21:47:03 +0200 Subject: [PATCH 47/98] Added example for a basic syntax + repaired the tests after previous changes --- examples/basic_syntax/images/loss_meme.png | Bin 0 -> 57914 bytes examples/basic_syntax/input.md | 108 +++++++ examples/basic_syntax/main.rs | 50 ++++ examples/basic_syntax/output.tex | 279 ++++++++++++++++++ src/gen/lab/mod.rs | 30 +- src/gen/mod.rs | 42 ++- src/lexer.rs | 64 ++-- src/lib.rs | 36 ++- src/path_engine.rs | 4 +- src/util.rs | 42 ++- tests/lex_files/ayano.txt | 2 +- tests/lex_files/ayano_display_blocks.txt | 4 +- tests/lex_files/basic.txt | 12 +- tests/lex_files/deceit.txt | 34 +-- tests/lex_files/tables_code.txt | 24 +- tests/lex_files/title_info.txt | 4 +- .../ayano_display_blocks.txt | 18 +- tests/transformed_files/basic.txt | 7 +- tests/transformed_files/deceit.txt | 28 +- tests/transformed_files/tables_code.txt | 4 +- tests/transformed_files/title_info.txt | 2 +- 21 files changed, 665 insertions(+), 129 deletions(-) create mode 100644 examples/basic_syntax/images/loss_meme.png create mode 100644 examples/basic_syntax/input.md create mode 100644 examples/basic_syntax/main.rs create mode 100644 examples/basic_syntax/output.tex diff --git a/examples/basic_syntax/images/loss_meme.png b/examples/basic_syntax/images/loss_meme.png new file mode 100644 index 0000000000000000000000000000000000000000..1a6aa46c77b43074e466f1903c0f2f572ec21aac GIT binary patch literal 57914 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 J5G9cOLJT4?ZlwvN6*ou%)w;GiFx_=leaM`Q&V? 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 literal 0 HcmV?d00001 diff --git a/examples/basic_syntax/input.md b/examples/basic_syntax/input.md new file mode 100644 index 0000000..4e62ebb --- /dev/null +++ b/examples/basic_syntax/input.md @@ -0,0 +1,108 @@ +Here's a quick rundown of supported markdown syntax: + +# Headers +###### Up to 6 deep + +Inline math: $y = x^2\qquad \text{- is a parabola equation}$ + +Display math: +$$ +\Gamma(z) = \int\limits_{0}^{+\infty} \exp(-t) t^{z-1} dt \qquad \text{- is an Euler's Gamma-function definition} +$$ +{ref = gamma_function} + +Page breaks: + +------ + +This text must appear on the next page + +Figures (take note of the caption and ident): +![[loss_meme.png]] +{ref = loss_meme, caption = "These are all loss functions, technically making this image a loss meme", width = 0.4} + +Tables: +| Number of appendages | Name | +| :------------------: | :----------: | +| 1 | Monopedal | +| 2 | Bipedal | +| 3 | Tripedal | +| 4 | Quadropedal | +| 5 | Pentapedal | +| 6 | Hexapedal | +| 7 | Heptopedal | +| 8 | Octopedal | +| 9 | why. | +| 1001 | are you sure | +| $\omega_0$ | are you ok? | +| $\zeta_1$ | cracker | +{ref = pedals, caption= "Source: trust **me**"} + +Hyper references: [some website](https://www.somewebsite.com) + +Footnotes: say, I need to explain this text, but not include the actual explanation right where I write it[^1] + +Object references: this refers to table: [@tab:pedals], this refers to figure: [@fig:loss_meme], an this one to the equation at the beginning: [@eq:gamma_function]. + +Lists are supported too; Here's a bullet list: +- First item +- Second item +- Third item + +Here's an enumeration: +1. First item +2. Second item +3. Third item + +Here are alphabetic enumerations (latin and cyrillic are only supported, for now) +a. First item +b. Second item +c. Third item + +а. First item +б. Second item +в. Third item +Cyrillic implementation detail: наразі у якості порядку елементів береться буквально 'а'..'я'. Тобто, згідно з [таблицею юнікоду](https://en.wikipedia.org/wiki/Cyrillic_(Unicode_block\)), тут поки що реалізовано терористичний алфавіт: +а. Перший елемент +б. Другий елемент +в. Третій елемент +г. Четвертий елемент +д. Шостий елемент (не вистачає ґ) +Це може змінитись у майбутньому, тож краще поки що утриматись від використання кириличних списків. + +Finally, code blocks are supported: +```lua +print("Hello lua!") +function fact (n) + if n == 0 or n == 1 then + return 1 + else + return n * fact(n-1) + end +end + +print(fact(7)) +``` +```rust +#[repr(transparent)] +#[derive(Debug, derive_more::From, derive_more::Deref, derive_more::DerefMut)] +pub struct Equivalent(pub T); + +impl PartialEq for Equivalent { + fn eq(&self, _: &Self) -> bool { + true + } +} + +impl Eq for Equivalent {} + +// This is a real type used in this project's codebase to allow error comparison for testing +// Although I might decide to remove it in the future +``` +These blocks are powered by [minted](https://github.com/gpoore/minted), so to get full list of supported languages, check their docs out. + +This is all of the markdown syntax supported for now. However, there's an *otherside* to `kyomato`'s capabilities - namely, `ayano` blocks. + +------ + +[^1]: You can absolutely do this! *Note*, that despite this footnote definition is on a separate page at the end, actual document output includes the footnote in a more convenient spot. \ No newline at end of file diff --git a/examples/basic_syntax/main.rs b/examples/basic_syntax/main.rs new file mode 100644 index 0000000..1bc4ba9 --- /dev/null +++ b/examples/basic_syntax/main.rs @@ -0,0 +1,50 @@ +use std::path::PathBuf; + +use kyomato::path_engine::PathEngineExt; + +fn main() { + //! This is a simple example of programmatic output generation + //! + //! You might want to use CLI interface instead + + // # Preparations + // not mandatory; you may specify paths by hand everywhere. + let mut workpath = PathBuf::from("examples/basic_syntax"); + + // read the input + workpath.push("input.md"); + let input = std::fs::read_to_string(&workpath).unwrap(); + // you must have a complete input to process it properly + + // open the output file + workpath.set_file_name("output.tex"); + let output = std::fs::OpenOptions::new() + .read(true) + .write(true) + .truncate(true) + .create(true) + .open(&workpath) + .unwrap(); + let mut output = kyomato::FmtToIo::new(output); + + // construct path engine + // this thing is responsible for file path resolution in the generated document + workpath.pop(); + workpath.push("images"); + let path_engine = kyomato::path_engine::base_gen() + // in our case, there's a dedicated folder for images, so we inform engine about that + .with_image_folder(&workpath) + // this sort of call is required - with it, you specify an error type the engine will return + // it's a no-op; so you may specify engine's type instead or th + .with_error_type::() + // this adds file checking to the engine. + // basically, this will ensure that any file engine happens to refer to, actually exists on the disk + // you may omit this call, if you don't want that + .with_check(kyomato::path_engine::fs_check); + + // parse input and write to output + kyomato::LabaLatex::parse_and_write(path_engine, &input, &mut output) + // this part is just for debug purposes + .map_err(|err| (err, output)) + .unwrap() +} diff --git a/examples/basic_syntax/output.tex b/examples/basic_syntax/output.tex new file mode 100644 index 0000000..5e9ae65 --- /dev/null +++ b/examples/basic_syntax/output.tex @@ -0,0 +1,279 @@ +\documentclass[a4paper]{article} +\usepackage{graphicx, hyperref, xcolor, gensymb, amssymb, mathtools, wrapfig, mathtools, microtype, lastpage, caption, titlesec, paracol, longtable, booktabs, cancel} +\usepackage[T2A, T1]{fontenc} +\usepackage[utf8]{inputenc} +\usepackage[russian, ukrainian]{babel} +\hypersetup{ + colorlinks, + linkcolor={blue!20!black}, + citecolor={blue!50!black}, + urlcolor={blue!80!black} +} + +\usepackage[left = 25mm, right = 10mm, top=20mm, bottom=20mm, bindingoffset=0cm]{geometry} + +% КОМАНДИ + +\newcommand{\fnt}{ +\fontshape{n} +\fontsize{14pt} {19pt} +\linespread{0.8} +\selectfont +} % нормативний шрифт +\let\oldCaption\caption +\renewcommand{\caption}[1]{ +\oldCaption{\fnt #1} +} % підпис нормативним шрифтом. + +\newcommand{\tb}{ + \hspace*{10mm} +} % відступ у 5 символів "x" відповідно до ДСТУ +\newcommand{\tbln}{ + \newline + \hspace*{10mm} +} % те ж саме, але з переносом рядка +\newcommand{\tbsp}{ + \vspace*{1ex} + \newline + \hspace*{10mm} +} % те ж саме, але з додатковим відступом між рядками + +\iffalse +\let\oldSection\section +\renewcommand{\section}[1]{ + \oldSection*{\tb #1} + \addcontentsline{toc}{section}{#1} + \fnt + \tb +} % відображає у тексті та змісті наданий заголовок +\fi + +\iftrue +\let\oldSubSection\subsection +\renewcommand{\subsection}[1]{ + \oldSubSection*{\tb #1} + \addcontentsline{toc}{subsection}{#1} + \fnt + \tb +} % відображає у тексті та змісті наданий підзаголовок +\fi + +% бібліографія +\usepackage[square,sort,comma,numbers]{natbib} +\renewcommand{\bibsection}{} +\usepackage{totcount} +\newtotcounter{citnum} +% лічильник цитувань +\def\oldbibitem{} \let\oldbibitem=\bibitem +\def\bibitem{\stepcounter{citnum}\oldbibitem} + +% лічильник зображень +\newtotcounter{graphnum} +\def\oldincludegraphics{} \let\oldincludegraphics=\includegraphics +\def\includegraphics{\stepcounter{graphnum}\oldincludegraphics} + +% лічильник таблиць (НЕАВТОМАТИЧНИЙ!!!) +\newtotcounter{tabnum} + +% лічильник додатків (НЕАВТОМАТИЧНИЙ!!!) +\newtotcounter{dodnum} + +% нумерація сторінок +\usepackage{fancyhdr} +\pagestyle{fancy} +\fancyhf{} +\renewcommand{\headrulewidth}{0pt} +\setlength{\headheight}{15.3pt} +\fancyhead[R]{\fnt \thepage} + +% виноски +\renewcommand{\thefootnote}{\large\arabic{footnote})~} +\renewcommand{\footnoterule}{\rule{20mm}{0.4pt} \vspace*{0.5ex}} +% \newcommand{\vyn}[2]{ +% \footnote[#1]{\large #2} +% } % створити виноску + +\let\oldFootnote\footnote +\renewcommand{\footnote}[1]{ + \oldFootnote{\large #1} +} % перегрузка виноски + +\providecommand{\tightlist}{% + \setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}} + +% підписи малюнків/таблиць +\captionsetup[figure]{name={\fnt Рисунок~},labelsep=period} +\captionsetup[table]{name={\fnt Таблиця~},labelsep=period} + +% idk why, but this redefinition does not work, for some reason +% \let\oldCaption\caption +% \renewcommand{\caption}[1] { +% \oldCaption{\fnt #1} +% } + +% markdown strikeouts +\usepackage{soul} + +% codeblocks +\usepackage{minted} + +\titleformat*{\section}{\fnt\LARGE\bfseries} +\titleformat*{\subsection}{\fnt\Large\bfseries} +\titleformat*{\subsubsection}{\fnt\Large} + +\begin{document} +\thispagestyle{empty} % [A]:for cropping +% ------------ +% source: https://tex.stackexchange.com/questions/173651/need-help-to-create-such-a-beautiful-title-page +\begingroup +\centering +\obeylines +\topskip0pt % source: https://tex.stackexchange.com/questions/2326/vertically-center-text-on-a-page +\vspace*{\fill} +\textbf{ +\LARGE +\LARGE +\vspace{100pt} +\LARGE +\Large +\Large +\Large +\huge +\vspace{50pt} +\large +\large +\large +\large +\vspace*{\fill} +\hspace{\fill}\large \rule{20mm}{0.4pt} +\vspace{10pt} +} +\endgroup +% ------------ +\newpage +\tableofcontents + +\newpage +\fnt + +Here's a quick rundown of supported markdown syntax: +\section{Headers} + +\textbf{Up to 6 deep} + +\tbln Inline math: $y = x^2\qquad \text{- is a parabola equation}$ +\tbln Display math: +\begin{equation} +\Gamma(z) = \int\limits_{0}^{+\infty} \exp(-t) t^{z-1} dt \qquad \text{- is an Euler's Gamma-function definition} +\label{eq:gamma_function} +\end{equation} + +\tbln Page breaks: +\clearpage + +\tbln This text must appear on the next page +\tbln Figures (take note of the caption and ident): +\begin{figure}[h!] +\centering +\includegraphics[width=0.4\textwidth]{examples/basic_syntax/images/loss_meme.png} +\caption{\capfnt These are all loss functions, technically making this image a loss meme} +\label{fig:loss_meme} +\end{figure} + +\tbln Tables: +\begin{table}[h!] +\begin{center} +\begin{tabular}{|c|c|}\hline + Number of appendages & Name\\ \hline +\hline + 1 & Monopedal\\ \hline + 2 & Bipedal\\ \hline + 3 & Tripedal\\ \hline + 4 & Quadropedal\\ \hline + 5 & Pentapedal\\ \hline + 6 & Hexapedal\\ \hline + 7 & Heptopedal\\ \hline + 8 & Octopedal\\ \hline + 9 & why.\\ \hline + 1001 & are you sure\\ \hline + $\omega_0$ & are you ok?\\ \hline + $\zeta_1$ & cracker\\ \hline +\end{tabular} +\stepcounter{tabnum} +\caption{\capfnt Source: trust \textbf{me}} +\label{tab:pedals} +\end{center} +\end{table} + +\tbln Hyper references: \href{https://www.somewebsite.com/}{some website} +\tbln Footnotes: say, I need to explain this text, but not include the actual explanation right where I write it\footnotemark[1]\footnotetext[1]{You can absolutely do this! \textit{Note}, that despite this footnote definition is on a separate page at the end, actual document output includes the footnote in a more convenient spot.} +\tbln Object references: this refers to table: \ref{tab:pedals}, this refers to figure: \ref{fig:loss_meme}, an this one to the equation at the beginning: \ref{eq:gamma_function}. +\tbln Lists are supported too; Here's a bullet list: +\begin{itemize} +\item First item +\item Second item +\item Third item +\end{itemize} +\tbln Here's an enumeration: +\begin{enumerate} +\item First item +\item Second item +\item Third item +\end{enumerate} +\tbln Here are alphabetic enumerations (latin and cyrillic are only supported, for now) +\begin{enumerate}[label=\alph*), ref=\alph*] +\item First item +\item Second item +\item Third item +\end{enumerate} +\tbln а. First item +\tbln б. Second item +\tbln в. Third item +\tbln Cyrillic implementation detail: наразі у якості порядку елементів береться буквально 'а'..'я'. Тобто, згідно з \href{https://en.wikipedia.org/wiki/Cyrillic_(Unicode_block)}{таблицею юнікоду}, тут поки що реалізовано терористичний алфавіт: +\begin{enumerate}[label=\asbuk*), ref=\asbuk*] +\item Перший елемент +\item Другий елемент +\item Третій елемент +\item Четвертий елемент +\item Шостий елемент (не вистачає ґ) +\end{enumerate} +\tbln Це може змінитись у майбутньому, тож краще поки що утриматись від використання кириличних списків. +\tbln Finally, code blocks are supported: +\begin{minted}{lua} +print("Hello lua!") +function fact (n) + if n == 0 or n == 1 then + return 1 + else + return n * fact(n-1) + end +end + +print(fact(7)) +\end{minted} + + +\begin{minted}{rust} +#[repr(transparent)] +#[derive(Debug, derive_more::From, derive_more::Deref, derive_more::DerefMut)] +pub struct Equivalent(pub T); + +impl PartialEq for Equivalent { + fn eq(&self, _: &Self) -> bool { + true + } +} + +impl Eq for Equivalent {} + +// This is a real type used in this project's codebase to allow error comparison for testing +// Although I might decide to remove it in the future +\end{minted} + + +\tbln These blocks are powered by \href{https://github.com/gpoore/minted}{minted}, so to get full list of supported languages, check their docs out. +\tbln This is all of the markdown syntax supported for now. However, there's an \textit{otherside} to `kyomato`'s capabilities - namely, `ayano` blocks. +\clearpage + + +\end{document} \ No newline at end of file diff --git a/src/gen/lab/mod.rs b/src/gen/lab/mod.rs index a841985..21e958d 100644 --- a/src/gen/lab/mod.rs +++ b/src/gen/lab/mod.rs @@ -5,9 +5,9 @@ pub use context::Context; pub use meta::SourceMeta; use crate::{ - data::{AyanoBlock, Formatting, TitleInfo, Token}, + data::{self, AyanoBlock, Formatting, TitleInfo, Token}, gen::ayano::DisplayInfo, - path_engine::PathEngine, + path_engine::{self, PathEngine}, }; use itertools::Itertools; use std::{ @@ -57,6 +57,28 @@ impl<'source, PE: PathEngine, PEErr> LabaLatex<'source, PE, PEErr> { output.write_str("\\\\ \\hline\n")?; Ok(()) } + + pub fn parse_and_write( + path_engine: PE, + input: &'source str, + output: &mut W, + ) -> Result<(), Box> + where + PE: PathEngine, + PEErr: std::error::Error + 'source, + { + let (title_info, tokens) = crate::parse_all(input)?; + let generator: LabaLatex<'source, PE, PEErr> = Self::new(path_engine); + let mut meta: SourceMeta<'source, AyanoExecutor> = SourceMeta::collect(&tokens) + .map_err(|err| GenerationError::Meta(Box::new(err)))? + .init_ayano()?; + meta.title_info = title_info; + generator.write_preamble(output, &meta)?; + let mut context = Context::default(); + generator.write_to(output, &meta, &mut context, &tokens)?; + generator.write_postamble(output, &meta, &mut context)?; + Ok(()) + } } impl<'source, PE, PEErr> OutputGenerator<'source, SourceMeta<'source, AyanoExecutor>, Context> @@ -95,7 +117,7 @@ where Token::DisplayMath { content, ident } => { let label = ident .as_ref() - .map(|ident| format!("\\label{{eq:{}}}", ident)) + .map(|ident| format!("\\label{{eq:{}}}\n", ident)) .unwrap_or_else(String::new); write!( output, @@ -257,7 +279,7 @@ where write!(output, "\\footnotemark[{}]", i + 1)?; if !encountered { // write out content (done only on first occurrence) - if let Some(content) = meta.footnotes.get(&ident.borrow()) { + if let Some(content) = meta.footnotes.get(ident) { write!(output, "\\footnotetext[{}]{{", i + 1)?; self.write_to(output, meta, context, content)?; output.write_str("}")?; diff --git a/src/gen/mod.rs b/src/gen/mod.rs index 6c0b576..cdffa13 100644 --- a/src/gen/mod.rs +++ b/src/gen/mod.rs @@ -7,14 +7,36 @@ use itertools::Itertools; use crate::data::{Token, Tokens}; -#[derive(Debug, derive_more::From, thiserror::Error)] +#[derive(Debug, thiserror::Error)] pub enum GenerationError<'source> { - #[error("{}", .0)] - IOs(std::fmt::Error), - #[error("{}", .0)] + #[error(transparent)] + Write(std::fmt::Error), + #[error(transparent)] Ayano(ayano::AyanoError<'source>), - #[error("{}", .0)] + #[error(transparent)] Path(Box), + #[error(transparent)] + Meta(Box), + #[error(transparent)] + Python(pyo3::PyErr), +} + +impl From for GenerationError<'_> { + fn from(value: std::fmt::Error) -> Self { + Self::Write(value) + } +} + +impl<'source> From> for GenerationError<'source> { + fn from(value: ayano::AyanoError<'source>) -> Self { + Self::Ayano(value) + } +} + +impl From for GenerationError<'_> { + fn from(value: pyo3::PyErr) -> Self { + Self::Python(value) + } } pub type Res<'source> = Result<(), GenerationError<'source>>; @@ -29,8 +51,8 @@ pub trait OutputGenerator<'source, Meta, Context> { token: &'token Token<'source>, ) -> Res<'source> where - 'source: 'meta + 'context + 'token, - 'token: 'context; + 'source: 'meta + 'context + 'token, + 'token: 'context; fn write_tokens_to<'meta, 'context, 'token, W: Write + ?Sized>( &self, @@ -40,9 +62,9 @@ pub trait OutputGenerator<'source, Meta, Context> { tokens: &'token Tokens<'source>, ) -> Res<'source> where - 'source: 'meta + 'context + 'token, - 'token: 'context, - { + 'source: 'meta + 'context + 'token, + 'token: 'context, + { tokens .into_iter() .map(|t| self.write_to(output, meta, context, t)) diff --git a/src/lexer.rs b/src/lexer.rs index c2f5b83..c06d1f6 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -1519,22 +1519,22 @@ mod token_tests { macro_rules! head { [# $header:literal] => { - head![1, $header] + head![0, $header] }; [## $header:literal] => { - head![2, $header] + head![1, $header] }; [### $header:literal] => { - head![3, $header] + head![2, $header] }; [#### $header:literal] => { - head![4, $header] + head![3, $header] }; [##### $header:literal] => { - head![5, $header] + head![4, $header] }; [###### $header:literal] => { - head![6, $header] + head![5, $header] }; [$order:literal, $header:literal] => { Token::Header {order: $order, content: Cow::Borrowed($header)} @@ -2328,19 +2328,19 @@ date: вербень 2077 prof: me :idk: --- ", [ - header_line1: "Навч. заклад", - header_line2: "Факультет", - document_type: "ПРОТОКОЛ", - title_line1: "Виконання марної роботи", - title_line2: "з історії стародавнього Єгипту", - title_line3: "(пинальна частина)", - title_line4: "НАЗВА РОБОТИ", - author_line1: "виконувало:", - author_line2: "дяч дзучунович", - author_line3: "(perfectly still)", - date: "вербень 2077", - prof: "me :idk:" - ], "\n"} + header_line1: "Навч. заклад", + header_line2: "Факультет", + document_type: "ПРОТОКОЛ", + title_line1: "Виконання марної роботи", + title_line2: "з історії стародавнього Єгипту", + title_line3: "(пинальна частина)", + title_line4: "НАЗВА РОБОТИ", + author_line1: "виконувало:", + author_line2: "дяч дзучунович", + author_line3: "(perfectly still)", + date: "вербень 2077", + prof: "me :idk:" + ], "\n"} test_ok! {ok_all_fields_permutated, r"--- author-line2: дяч дзучунович header-line1: Навч. заклад @@ -2356,17 +2356,17 @@ prof: me :idk: title-line3: (пинальна частина) --- ", [ - header_line1: "Навч. заклад", - header_line2: "Факультет", - document_type: "ПРОТОКОЛ", - title_line1: "Виконання марної роботи", - title_line2: "з історії стародавнього Єгипту", - title_line3: "(пинальна частина)", - title_line4: "НАЗВА РОБОТИ", - author_line1: "виконувало:", - author_line2: "дяч дзучунович", - author_line3: "(perfectly still)", - date: "вербень 2077", - prof: "me :idk:" - ], "\n"} + header_line1: "Навч. заклад", + header_line2: "Факультет", + document_type: "ПРОТОКОЛ", + title_line1: "Виконання марної роботи", + title_line2: "з історії стародавнього Єгипту", + title_line3: "(пинальна частина)", + title_line4: "НАЗВА РОБОТИ", + author_line1: "виконувало:", + author_line2: "дяч дзучунович", + author_line3: "(perfectly still)", + date: "вербень 2077", + prof: "me :idk:" + ], "\n"} } diff --git a/src/lib.rs b/src/lib.rs index 765cb46..7bf8296 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,15 +27,16 @@ //! Well, unless there's a way to efficiently write to a single output from multiple threads, while preserving output order. //! Any sort of "collect" function would've destroyed the purpose anyway. -use data::TitleInfo; +use data::{TitleInfo, Token}; use gen::OutputGenerator; use lexer::KyomatoLexError; /// This module defines types that are used to represent parsed data mod data; mod gen; +pub use gen::lab::LabaLatex; mod lexer; -mod path_engine; +pub mod path_engine; /// Reexports pub fn lex<'source>( @@ -79,7 +80,38 @@ pub fn gen_to_string<'token, 'source: 'token>( Ok(buf) } +pub fn parse_title_info<'source>( + input: &mut &'source str, +) -> Result, KyomatoLexError> { + let (rest, title_info) = + lexer::title_info::<'source, KyomatoLexError>(input).map_err(|err| match err { + nom::Err::Error(err) | nom::Err::Failure(err) => err, + nom::Err::Incomplete(_) => unreachable!("This input is complete"), + })?; + *input = rest; + Ok(title_info) +} + +pub fn parse_tokens<'source>(input: &mut &'source str) -> Result, KyomatoLexError> { + let (rest, tokens) = + lexer::lex::<'source, KyomatoLexError>(input).map_err(|err| match err { + nom::Err::Error(err) | nom::Err::Failure(err) => err, + nom::Err::Incomplete(_) => unreachable!("This input is complete"), + })?; + *input = rest; + Ok(tokens) +} + +pub fn parse_all<'source>( + mut input: &'source str, +) -> Result<(TitleInfo<'source>, Token<'source>), KyomatoLexError> { + let title_info = parse_title_info(&mut input)?; + let tokens = parse_tokens(&mut input)?; + Ok((title_info, tokens)) +} + /// This module houses "utility-like" structs and functions. /// /// Since this is generally a bad practice, I'd like to know suggestions on std library/other crates items I could use instead mod util; +pub use util::FmtToIo; diff --git a/src/path_engine.rs b/src/path_engine.rs index 9fb7dd2..0d64fe4 100644 --- a/src/path_engine.rs +++ b/src/path_engine.rs @@ -171,12 +171,12 @@ pub enum VariantError { } /// A path-checking function always positive about path being correct -fn no_check(path: &Path) -> Result<(), Box> { +pub fn no_check(path: &Path) -> Result<(), Box> { Ok(()) } /// A path-checking function actually checking if file exists via normal means -fn fs_check(path: &Path) -> Result<(), Box> { +pub fn fs_check(path: &Path) -> Result<(), Box> { match path.try_exists() { Ok(true) => { if path.is_file() { diff --git a/src/util.rs b/src/util.rs index b74a715..5c705a1 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,4 +1,5 @@ use std::{ + collections::VecDeque, mem::{forget, MaybeUninit}, num::ParseIntError, ops::{Range, RangeFrom, RangeInclusive, RangeTo, RangeToInclusive}, @@ -139,9 +140,8 @@ mod generic_rage_tests { test! {idk1, "5..=100", GenericRange::DoubleBoundedInclusive(5..=100)} } -#[derive(derive_more::From)] #[repr(transparent)] -#[derive(Debug, derive_more::Deref, derive_more::DerefMut)] +#[derive(Debug, derive_more::From, derive_more::Deref, derive_more::DerefMut)] pub struct Equivalent(pub T); impl PartialEq for Equivalent { @@ -453,14 +453,36 @@ pub fn optional_permutation<'parsers, 'source: 'parsers, In, Out, E: ParseError< } // FIXME this is SURELY not idiomatic. I should move to ident system -#[derive( - Debug, - derive_more::Deref, - derive_more::DerefMut, - PartialEq, - Clone, -)] +#[derive(Debug, derive_more::Deref, derive_more::DerefMut, PartialEq, Clone)] pub struct HashIgnored(pub T); impl std::hash::Hash for HashIgnored { - fn hash(&self, state: &mut H) {} + fn hash(&self, _state: &mut H) {} +} + +#[derive(Debug)] +pub struct FmtToIo(W, VecDeque); +impl FmtToIo { + pub fn new(io: W) -> Self + where + W: std::io::Write, + { + Self(io, VecDeque::new()) + } + + pub fn get_error(&mut self) -> Option { + self.1.pop_front() + } + + pub fn get_all_errors(&mut self) -> impl Iterator + '_ { + self.1.drain(..) + } +} +impl std::fmt::Write for FmtToIo { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + if let Err(err) = self.0.write_all(s.as_bytes()) { + self.1.push_back(err); + return Err(std::fmt::Error); + } + Ok(()) + } } diff --git a/tests/lex_files/ayano.txt b/tests/lex_files/ayano.txt index 6128484..9f2e458 100644 --- a/tests/lex_files/ayano.txt +++ b/tests/lex_files/ayano.txt @@ -14,7 +14,7 @@ TitleInfo { code_section_title: None, } Multiple { - tokens: Owned( + tokens: Tokens( [ Paragraph { is_newline: false, diff --git a/tests/lex_files/ayano_display_blocks.txt b/tests/lex_files/ayano_display_blocks.txt index 0eb9fb2..5f8e364 100644 --- a/tests/lex_files/ayano_display_blocks.txt +++ b/tests/lex_files/ayano_display_blocks.txt @@ -16,10 +16,10 @@ TitleInfo { ), } Multiple { - tokens: Owned( + tokens: Tokens( [ Header { - order: 1, + order: 0, content: "Display blocks", }, Paragraph { diff --git a/tests/lex_files/basic.txt b/tests/lex_files/basic.txt index 888c2fd..374f165 100644 --- a/tests/lex_files/basic.txt +++ b/tests/lex_files/basic.txt @@ -14,10 +14,10 @@ TitleInfo { code_section_title: None, } Multiple { - tokens: Owned( + tokens: Tokens( [ Header { - order: 1, + order: 0, content: "Перша тема", }, Paragraph { @@ -33,7 +33,7 @@ Multiple { ), }, Header { - order: 2, + order: 1, content: "Підпункт", }, Paragraph { @@ -44,10 +44,10 @@ Multiple { }, List { list_type: Bullet, - content: Owned( + content: Tokens( [ Multiple { - tokens: Owned( + tokens: Tokens( [ Paragraph { is_newline: false, @@ -63,7 +63,7 @@ Multiple { ), }, Multiple { - tokens: Owned( + tokens: Tokens( [ Paragraph { is_newline: false, diff --git a/tests/lex_files/deceit.txt b/tests/lex_files/deceit.txt index 4ce3c3b..9516ba9 100644 --- a/tests/lex_files/deceit.txt +++ b/tests/lex_files/deceit.txt @@ -14,7 +14,7 @@ TitleInfo { code_section_title: None, } Multiple { - tokens: Owned( + tokens: Tokens( [ Paragraph { is_newline: false, @@ -23,7 +23,7 @@ Multiple { content: "This test is dedicated to attempts of parser deception, i.e. usage of special symbols in wrong places to see if parser recognizes them as control symbols. There we go:", }, Header { - order: 1, + order: 0, content: "Page dividers", }, Paragraph { @@ -79,7 +79,7 @@ Multiple { content: "look cursed, don't do it.", }, Header { - order: 1, + order: 0, content: "Headers", }, Paragraph { @@ -109,7 +109,7 @@ Multiple { content: "Example:", }, Header { - order: 6, + order: 5, content: "This is a long, yet formally correct header. You can add $equations$ and *formatting* here, but I provide no guarantees as of the way these would display in final document.", }, Paragraph { @@ -137,7 +137,7 @@ Multiple { content: "####### This will be parsed as a regular paragraph", }, Header { - order: 1, + order: 0, content: "Equations", }, Paragraph { @@ -203,7 +203,7 @@ Multiple { content: "(the above should not be mathmode), and that's it, basically :sad:", }, Header { - order: 1, + order: 0, content: "Table", }, Paragraph { @@ -241,7 +241,7 @@ Multiple { ), }, Header { - order: 1, + order: 0, content: "Figure", }, Paragraph { @@ -259,7 +259,7 @@ Multiple { language: None, }, Header { - order: 1, + order: 0, content: "References", }, Paragraph { @@ -269,7 +269,7 @@ Multiple { content: "These start with []", }, Header { - order: 2, + order: 1, content: "Hyperrefs", }, Paragraph { @@ -356,7 +356,7 @@ Multiple { content: "lol)", }, Header { - order: 2, + order: 1, content: "Footnote refs", }, Paragraph { @@ -416,7 +416,7 @@ Multiple { content: "So on", }, Header { - order: 2, + order: 1, content: "Object refs", }, Paragraph { @@ -494,7 +494,7 @@ Multiple { content: "use regular latex to define your custom refs. In fact, LaTeX doesn't care about all of these at all; I'm doing these transformations just for my own comfort, as I've been doing this manually.", }, Header { - order: 1, + order: 0, content: "Formatting", }, Paragraph { @@ -511,7 +511,7 @@ Multiple { }, List { list_type: Bullet, - content: Owned( + content: Tokens( [ Paragraph { is_newline: false, @@ -569,7 +569,7 @@ Multiple { content: "Basically, parsers for these tokens attempt parse everything up until they find a closing token. But if they are inserted in a middle of the text, then it's ok - they will be limited by current paragraph, and (most likely) will find no match inside it, if their use was unintended.", }, Header { - order: 1, + order: 0, content: "Footnote content", }, Paragraph { @@ -658,7 +658,7 @@ Multiple { content: "Is expected to be a footnote ref with a colon.", }, Header { - order: 1, + order: 0, content: "Code blocks", }, Paragraph { @@ -682,7 +682,7 @@ Multiple { content: "code block.", }, Header { - order: 2, + order: 1, content: "Normal code block", }, Paragraph { @@ -766,7 +766,7 @@ Multiple { content: "```", }, Header { - order: 2, + order: 1, content: "Ayano code block", }, Paragraph { diff --git a/tests/lex_files/tables_code.txt b/tests/lex_files/tables_code.txt index 765b681..e030975 100644 --- a/tests/lex_files/tables_code.txt +++ b/tests/lex_files/tables_code.txt @@ -14,7 +14,7 @@ TitleInfo { code_section_title: None, } Multiple { - tokens: Owned( + tokens: Tokens( [ Paragraph { is_newline: false, @@ -23,7 +23,7 @@ Multiple { content: "Наступний тест. Тут протестуємо таблиці і блоки коду.", }, Header { - order: 2, + order: 1, content: "Таблиці", }, Paragraph { @@ -61,7 +61,7 @@ Multiple { content: "на інші штуки", }, Table { - header: Owned( + header: Tokens( [ Paragraph { is_newline: false, @@ -83,7 +83,7 @@ Multiple { }, ], ), - cells: Owned( + cells: Tokens( [ Paragraph { is_newline: false, @@ -102,7 +102,7 @@ Multiple { content: "3 + e ^{\\pi}", }, Multiple { - tokens: Owned( + tokens: Tokens( [ InlineMath { space_before: true, @@ -120,7 +120,7 @@ Multiple { content: "\\cdot \\pm 1", }, Multiple { - tokens: Owned( + tokens: Tokens( [ Paragraph { is_newline: false, @@ -149,7 +149,7 @@ Multiple { ), caption: Some( Multiple { - tokens: Owned( + tokens: Tokens( [ Paragraph { is_newline: false, @@ -186,7 +186,7 @@ Multiple { content: "Таблиця цілком собі може не мати ідентифікатора або опису:", }, Table { - header: Owned( + header: Tokens( [ Paragraph { is_newline: false, @@ -202,7 +202,7 @@ Multiple { }, ], ), - cells: Owned( + cells: Tokens( [ Paragraph { is_newline: false, @@ -237,7 +237,7 @@ Multiple { }, FootnoteContent { content: Multiple { - tokens: Owned( + tokens: Tokens( [ Paragraph { is_newline: false, @@ -279,7 +279,7 @@ Multiple { ident: "euler", }, Header { - order: 2, + order: 1, content: "Блоки коду", }, Paragraph { @@ -336,7 +336,7 @@ Multiple { }, FootnoteContent { content: Multiple { - tokens: Owned( + tokens: Tokens( [ Paragraph { is_newline: false, diff --git a/tests/lex_files/title_info.txt b/tests/lex_files/title_info.txt index dbc59ba..8a8253d 100644 --- a/tests/lex_files/title_info.txt +++ b/tests/lex_files/title_info.txt @@ -38,10 +38,10 @@ TitleInfo { code_section_title: None, } Multiple { - tokens: Owned( + tokens: Tokens( [ Header { - order: 1, + order: 0, content: "First para", }, Paragraph { diff --git a/tests/transformed_files/ayano_display_blocks.txt b/tests/transformed_files/ayano_display_blocks.txt index 583224d..7f347a1 100644 --- a/tests/transformed_files/ayano_display_blocks.txt +++ b/tests/transformed_files/ayano_display_blocks.txt @@ -157,13 +157,21 @@ \fnt -\subsection{Display blocks} +\section{Display blocks} \tbln This feature allows you to insert certain `Ayano` blocks to the end of the output document, to document your exact actions for displayed results. \tbln Here's an example:42 \tbln Display blocks can have descriptions too! Here's one:-20340$\pm$150 \subsection{Код} +\begin{minted}{python} +# You should be able to see this comment in the output doc +return 42 + +\end{minted} + + +\tbln ayano_block__1574475637377647406 \begin{minted}{python} # This code block has description # Might as well add some python syntax to check out minted's syntax highlighting: @@ -175,13 +183,5 @@ return "err", -2.034e4, 145.64 \end{minted} me, fooling around -\begin{minted}{python} -# You should be able to see this comment in the output doc -return 42 - -\end{minted} - - -\tbln ayano_block__1574475637377647406 \end{document} \ No newline at end of file diff --git a/tests/transformed_files/basic.txt b/tests/transformed_files/basic.txt index 4c89fb8..7db3083 100644 --- a/tests/transformed_files/basic.txt +++ b/tests/transformed_files/basic.txt @@ -157,14 +157,15 @@ \fnt -\subsection{Перша тема} +\section{Перша тема} \tbln Тут трохи тексту і пояснень про квантові мультивироджені рефурбалідзери змішаного типу. Це нудно, тож тримайте формулу: \begin{equation} Ref(z) = \int \limits_{0}^{1} t^{z-1} \cdot \exp(-t) dt -\label{eq:refurbalidzer_1}\end{equation} +\label{eq:refurbalidzer_1} +\end{equation} -\subsubsection{Підпункт} +\subsection{Підпункт} \tbln Рефурбалідзери можуть виявитись дуже важливими для збереження екологічної безпеки нашої планети. До прикладу, в них є наступні переваги: \begin{itemize} diff --git a/tests/transformed_files/deceit.txt b/tests/transformed_files/deceit.txt index 3cc830e..2ed56e3 100644 --- a/tests/transformed_files/deceit.txt +++ b/tests/transformed_files/deceit.txt @@ -157,7 +157,7 @@ \fnt This test is dedicated to attempts of parser deception, i.e. usage of special symbols in wrong places to see if parser recognizes them as control symbols. There we go: -\subsection{Page dividers} +\section{Page dividers} \tbln Page dividers are represented by ------ \tbln If the line above has dashes ^^^^^^ here, it probably works alright. @@ -168,7 +168,7 @@ This test is dedicated to attempts of parser deception, i.e. usage of special sy \clearpage Some text, cause why not \tbln But this \textbf{DOES} look cursed, don't do it. -\subsection{Headers} +\section{Headers} \tbln Headers are most recognizable by '#'-symbol. For it to actually represent a header, it needs to at the beginning of the line and there should be no more than 6 of them. \textbf{ANYTHING} up to the end of line will be included into a header, then. \tbln Example: @@ -178,7 +178,7 @@ This test is dedicated to attempts of parser deception, i.e. usage of special sy \tbln ###### This header is considered incorrect for this application, and will be parsed as regular paragraph instead \tbln Another example of incorrect header, featuring 7 hash-symbols: \tbln ####### This will be parsed as a regular paragraph -\subsection{Equations} +\section{Equations} \tbln There are two types of equations in markdown/latex: inline mathmode and display mathmode \tbln Both of them are defined by $-symbols. However, they must be paired to actually represent these. Here are examples: @@ -193,7 +193,7 @@ y = x ^ 2 \tbln $ \tbln $ \tbln (the above should not be mathmode), and that's it, basically :sad: -\subsection{Table} +\section{Table} \tbln Tables are defined by |-symbol. But the actual table will happen only if they are the first thing on the line. \tbln Due to current implementation, there's no cure apart from display mathmode and code: @@ -209,7 +209,7 @@ head /dev/random \end{minted} -\subsection{Figure} +\section{Figure} \tbln Figure is defined by a ![[ sequence at a start of the line. Same points should be uphold here: \begin{equation} @@ -221,10 +221,10 @@ head /dev/random \end{minted} -\subsection{References} +\section{References} \tbln These start with [] -\subsubsection{Hyperrefs} +\subsection{Hyperrefs} \tbln These are followed by parentheses (). Nothing is allowed between them, thus \tbln Here's a correct href: \href{https://youtu.be/rickroll_link_lol}{my gh page} @@ -232,7 +232,7 @@ head /dev/random \tbln [my gh page] (https://youtu.be/rickroll\textit{link}lol) \tbln [my gh page] \tbln (https://youtu.be/rickroll\textit{link}lol) -\subsubsection{Footnote refs} +\subsection{Footnote refs} \tbln These are started by [^. \tbln Here are correct footnote refs: \footnotemark[1], \footnotemark[2] @@ -241,7 +241,7 @@ head /dev/random \tbln 1 \tbln ] \tbln So on -\subsubsection{Object refs} +\subsection{Object refs} \tbln These refer to objects like figures, tables and equations. \tbln Started by [@. @@ -251,7 +251,7 @@ head /dev/random \tbln eq2 \tbln ] \tbln \textbf{NOTE}: despite object types being prepended with 'eq', 'fig' or 'tab' in refs, you \textit{can} use regular latex to define your custom refs. In fact, LaTeX doesn't care about all of these at all; I'm doing these transformations just for my own comfort, as I've been doing this manually. -\subsection{Formatting} +\section{Formatting} \tbln This object's type is deprecated and will be removed soon. \tbln It starts and ends with @@ -262,7 +262,7 @@ head /dev/random \end{itemize} \tbln \textbf{WARN}: As of now, these \textit{might} cause cause massive problems if encountered at the start of the line. \tbln Basically, parsers for these tokens attempt parse everything up until they find a closing token. But if they are inserted in a middle of the text, then it's ok - they will be limited by current paragraph, and (most likely) will find no match inside it, if their use was unintended. -\subsection{Footnote content} +\section{Footnote content} \tbln These start with '\footnotemark[3]:' and proceed to the end of line. \tbln Example of valid content: @@ -270,10 +270,10 @@ head /dev/random \tbln [^source2: This is invalid content\footnotemark[4]\footnotetext[4]{Explanation of source2.} : This is invalid content \footnotemark[4]: This is invalid content \tbln They also must start at the beginning of the line. If fact, this: \footnotemark[4]: Some content \tbln Is expected to be a footnote ref with a colon. -\subsection{Code blocks} +\section{Code blocks} \tbln There are two fundamental types of code blocks: normal code block and \textbf{Ayano} code block. -\subsubsection{Normal code block} +\subsection{Normal code block} \tbln These start and end with '```' on a newline. There can be \textbf{practically anything} inside. Example: \begin{minted}{java} @@ -299,7 +299,7 @@ local z = x .. y -- matty!! \tbln x = 2 \tbln print(x) \tbln ``` -\subsubsection{Ayano code block} +\subsection{Ayano code block} \tbln \textit{because Ayano loves Kyoko} \tbln This object looks and behaves just like normal code block, but is only limited to \textit{python} language, can have spacial arguments in a language line, may leave something behind in the document. For example: diff --git a/tests/transformed_files/tables_code.txt b/tests/transformed_files/tables_code.txt index ce44d66..941c681 100644 --- a/tests/transformed_files/tables_code.txt +++ b/tests/transformed_files/tables_code.txt @@ -157,7 +157,7 @@ \fnt Наступний тест. Тут протестуємо таблиці і блоки коду. -\subsubsection{Таблиці} +\subsection{Таблиці} \tbln Звичайні таблиці; Можуть всередині мати \textbf{формули} чи \textit{посилання} на інші штуки \begin{table}[h!] @@ -187,7 +187,7 @@ \end{center} \end{table} -\subsubsection{Блоки коду} +\subsection{Блоки коду} \tbln Наразі мають досить обмежену підтримку, адже у $\LaTeX$ немає нативного поняття блоків коду. \tbln На щастя, у \textbf{markdown} таке поняття є\footnotemark[2]\footnotetext[2]{Принаймні, \textit{Obsidian} таке робить}: diff --git a/tests/transformed_files/title_info.txt b/tests/transformed_files/title_info.txt index fff6790..41fc9f5 100644 --- a/tests/transformed_files/title_info.txt +++ b/tests/transformed_files/title_info.txt @@ -157,7 +157,7 @@ \fnt -\subsection{First para} +\section{First para} \tbln This text is \textit{just} a dummy, so that document won't feel \st{too} \textbf{empty} \tbln Have a some oily maccaroni: From f5f5d77f12278cad98af29cb49f2588b5890cfef Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Wed, 27 Mar 2024 09:41:34 +0200 Subject: [PATCH 48/98] Repaired example for basic syntax - fixed issue of some latex tokens begin allergic to \newlive thing - added more params to `minted` code blocks, so they would look more readable - added support for cyrillic and latin lists - fixed parsing issue related to lists begin parsed incorrectly --- Cargo.lock | 12 ++ Cargo.toml | 1 + examples/basic_syntax/input.md | 4 +- examples/basic_syntax/output.tex | 107 +++++-------- src/data.rs | 33 ++++ src/gen/lab/context.rs | 4 +- src/gen/lab/lab_preamble.tex | 44 ++---- src/gen/lab/mod.rs | 55 ++++--- src/lexer.rs | 87 +++++++---- tests/input_files/edgecases.md | 12 ++ tests/lex_files/edgecases.txt | 85 +++++++++++ tests/transformed_files/ayano.txt | 45 ++---- .../ayano_display_blocks.txt | 64 +++----- tests/transformed_files/basic.txt | 53 ++----- tests/transformed_files/deceit.txt | 117 +++++--------- tests/transformed_files/edgecases.txt | 144 ++++++++++++++++++ tests/transformed_files/tables_code.txt | 57 ++----- tests/transformed_files/title_info.txt | 47 ++---- 18 files changed, 535 insertions(+), 436 deletions(-) create mode 100644 tests/input_files/edgecases.md create mode 100644 tests/lex_files/edgecases.txt create mode 100644 tests/transformed_files/edgecases.txt diff --git a/Cargo.lock b/Cargo.lock index 14fc87b..8c51461 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -265,6 +265,7 @@ dependencies = [ "rand", "regex", "serde", + "smart-default", "text-diff", "thiserror", "url 2.5.0", @@ -565,6 +566,17 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +[[package]] +name = "smart-default" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "strsim" version = "0.10.0" diff --git a/Cargo.toml b/Cargo.toml index aae498e..777749d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ thiserror = "1.0.56" url = "2.5.0" serde = { version = "1.0.197", features = ["derive"], optional = true } url_serde = { version = "0.2.0", optional = true } +smart-default = "0.7.1" [dev-dependencies] text-diff = "0.4.0" diff --git a/examples/basic_syntax/input.md b/examples/basic_syntax/input.md index 4e62ebb..b22debc 100644 --- a/examples/basic_syntax/input.md +++ b/examples/basic_syntax/input.md @@ -7,7 +7,7 @@ Inline math: $y = x^2\qquad \text{- is a parabola equation}$ Display math: $$ -\Gamma(z) = \int\limits_{0}^{+\infty} \exp(-t) t^{z-1} dt \qquad \text{- is an Euler's Gamma-function definition} +\Gamma(z) = \int\limits_{0}^{+\infty} \exp(-t) t^{z-1} dt \qquad \text{- це Euler's Gamma-function definition} $$ {ref = gamma_function} @@ -58,10 +58,10 @@ Here are alphabetic enumerations (latin and cyrillic are only supported, for now a. First item b. Second item c. Third item - а. First item б. Second item в. Third item + Cyrillic implementation detail: наразі у якості порядку елементів береться буквально 'а'..'я'. Тобто, згідно з [таблицею юнікоду](https://en.wikipedia.org/wiki/Cyrillic_(Unicode_block\)), тут поки що реалізовано терористичний алфавіт: а. Перший елемент б. Другий елемент diff --git a/examples/basic_syntax/output.tex b/examples/basic_syntax/output.tex index 5e9ae65..3492273 100644 --- a/examples/basic_syntax/output.tex +++ b/examples/basic_syntax/output.tex @@ -20,44 +20,22 @@ \linespread{0.8} \selectfont } % нормативний шрифт -\let\oldCaption\caption -\renewcommand{\caption}[1]{ -\oldCaption{\fnt #1} -} % підпис нормативним шрифтом. +\newcommand{\capfnt}{ + \fnt +} % норматичний шрифт для підписів (тимчасово співпадає зі звичайним шрифтом) \newcommand{\tb}{ \hspace*{10mm} } % відступ у 5 символів "x" відповідно до ДСТУ \newcommand{\tbln}{ \newline - \hspace*{10mm} + \tb } % те ж саме, але з переносом рядка \newcommand{\tbsp}{ \vspace*{1ex} - \newline - \hspace*{10mm} + \tbln } % те ж саме, але з додатковим відступом між рядками -\iffalse -\let\oldSection\section -\renewcommand{\section}[1]{ - \oldSection*{\tb #1} - \addcontentsline{toc}{section}{#1} - \fnt - \tb -} % відображає у тексті та змісті наданий заголовок -\fi - -\iftrue -\let\oldSubSection\subsection -\renewcommand{\subsection}[1]{ - \oldSubSection*{\tb #1} - \addcontentsline{toc}{subsection}{#1} - \fnt - \tb -} % відображає у тексті та змісті наданий підзаголовок -\fi - % бібліографія \usepackage[square,sort,comma,numbers]{natbib} \renewcommand{\bibsection}{} @@ -105,18 +83,15 @@ \captionsetup[figure]{name={\fnt Рисунок~},labelsep=period} \captionsetup[table]{name={\fnt Таблиця~},labelsep=period} -% idk why, but this redefinition does not work, for some reason -% \let\oldCaption\caption -% \renewcommand{\caption}[1] { -% \oldCaption{\fnt #1} -% } - % markdown strikeouts \usepackage{soul} % codeblocks \usepackage{minted} +% enumeration +\usepackage[shortlabels]{enumitem} + \titleformat*{\section}{\fnt\LARGE\bfseries} \titleformat*{\subsection}{\fnt\Large\bfseries} \titleformat*{\subsubsection}{\fnt\Large} @@ -128,7 +103,7 @@ \begingroup \centering \obeylines -\topskip0pt % source: https://tex.stackexchange.com/questions/2326/vertically-center-text-on-a-page +\topskip 0pt % source: https://tex.stackexchange.com/questions/2326/vertically-center-text-on-a-page \vspace*{\fill} \textbf{ \LARGE @@ -155,23 +130,19 @@ \newpage \fnt - Here's a quick rundown of supported markdown syntax: \section{Headers} \textbf{Up to 6 deep} - -\tbln Inline math: $y = x^2\qquad \text{- is a parabola equation}$ +\tb Inline math: $y = x^2\qquad \text{- is a parabola equation}$ \tbln Display math: \begin{equation} -\Gamma(z) = \int\limits_{0}^{+\infty} \exp(-t) t^{z-1} dt \qquad \text{- is an Euler's Gamma-function definition} +\Gamma(z) = \int\limits_{0}^{+\infty} \exp(-t) t^{z-1} dt \qquad \text{- це Euler's Gamma-function definition} \label{eq:gamma_function} \end{equation} - -\tbln Page breaks: +\tb Page breaks: \clearpage - -\tbln This text must appear on the next page +\tb This text must appear on the next page \tbln Figures (take note of the caption and ident): \begin{figure}[h!] \centering @@ -179,9 +150,9 @@ Here's a quick rundown of supported markdown syntax: \caption{\capfnt These are all loss functions, technically making this image a loss meme} \label{fig:loss_meme} \end{figure} - -\tbln Tables: +\tb Tables: \begin{table}[h!] +\fnt \begin{center} \begin{tabular}{|c|c|}\hline Number of appendages & Name\\ \hline @@ -204,8 +175,7 @@ Here's a quick rundown of supported markdown syntax: \label{tab:pedals} \end{center} \end{table} - -\tbln Hyper references: \href{https://www.somewebsite.com/}{some website} +\tb Hyper references: \href{https://www.somewebsite.com/}{some website} \tbln Footnotes: say, I need to explain this text, but not include the actual explanation right where I write it\footnotemark[1]\footnotetext[1]{You can absolutely do this! \textit{Note}, that despite this footnote definition is on a separate page at the end, actual document output includes the footnote in a more convenient spot.} \tbln Object references: this refers to table: \ref{tab:pedals}, this refers to figure: \ref{fig:loss_meme}, an this one to the equation at the beginning: \ref{eq:gamma_function}. \tbln Lists are supported too; Here's a bullet list: @@ -213,33 +183,31 @@ Here's a quick rundown of supported markdown syntax: \item First item \item Second item \item Third item -\end{itemize} -\tbln Here's an enumeration: +\end{itemize}\tb Here's an enumeration: \begin{enumerate} \item First item \item Second item \item Third item +\end{enumerate}\tb Here are alphabetic enumerations (latin and cyrillic are only supported, for now) +\begin{enumerate} +\item[a.] First item +\item[b.] Second item +\item[c.] Third item \end{enumerate} -\tbln Here are alphabetic enumerations (latin and cyrillic are only supported, for now) -\begin{enumerate}[label=\alph*), ref=\alph*] -\item First item -\item Second item -\item Third item -\end{enumerate} -\tbln а. First item -\tbln б. Second item -\tbln в. Third item -\tbln Cyrillic implementation detail: наразі у якості порядку елементів береться буквально 'а'..'я'. Тобто, згідно з \href{https://en.wikipedia.org/wiki/Cyrillic_(Unicode_block)}{таблицею юнікоду}, тут поки що реалізовано терористичний алфавіт: -\begin{enumerate}[label=\asbuk*), ref=\asbuk*] -\item Перший елемент -\item Другий елемент -\item Третій елемент -\item Четвертий елемент -\item Шостий елемент (не вистачає ґ) -\end{enumerate} -\tbln Це може змінитись у майбутньому, тож краще поки що утриматись від використання кириличних списків. +\begin{enumerate} +\item[а.] First item +\item[б.] Second item +\item[в.] Third item +\end{enumerate}\tb Cyrillic implementation detail: наразі у якості порядку елементів береться буквально 'а'..'я'. Тобто, згідно з \href{https://en.wikipedia.org/wiki/Cyrillic_(Unicode_block)}{таблицею юнікоду}, тут поки що реалізовано терористичний алфавіт: +\begin{enumerate} +\item[а.] Перший елемент +\item[б.] Другий елемент +\item[в.] Третій елемент +\item[г.] Четвертий елемент +\item[д.] Шостий елемент (не вистачає ґ) +\end{enumerate}\tb Це може змінитись у майбутньому, тож краще поки що утриматись від використання кириличних списків. \tbln Finally, code blocks are supported: -\begin{minted}{lua} +\begin{minted}[linenos, mathescape, autogobble, breaklines]{lua} print("Hello lua!") function fact (n) if n == 0 or n == 1 then @@ -253,7 +221,7 @@ print(fact(7)) \end{minted} -\begin{minted}{rust} +\begin{minted}[linenos, mathescape, autogobble, breaklines]{rust} #[repr(transparent)] #[derive(Debug, derive_more::From, derive_more::Deref, derive_more::DerefMut)] pub struct Equivalent(pub T); @@ -270,8 +238,7 @@ impl Eq for Equivalent {} // Although I might decide to remove it in the future \end{minted} - -\tbln These blocks are powered by \href{https://github.com/gpoore/minted}{minted}, so to get full list of supported languages, check their docs out. +\tb These blocks are powered by \href{https://github.com/gpoore/minted}{minted}, so to get full list of supported languages, check their docs out. \tbln This is all of the markdown syntax supported for now. However, there's an \textit{otherside} to `kyomato`'s capabilities - namely, `ayano` blocks. \clearpage diff --git a/src/data.rs b/src/data.rs index bbc7988..9ce4e7a 100644 --- a/src/data.rs +++ b/src/data.rs @@ -485,6 +485,39 @@ impl<'source> Token<'source> { }, } } + + /// Defines if newline after this token will cause problems + /// + /// None means that it depends on previous tokens + pub fn is_newline_allergic(&self) -> Option { + match self { + Token::Ayano { + data: AyanoBlock { + is_static: false, .. + }, + } + | Token::Href { .. } + | Token::Paragraph { .. } + | Token::InlineMath { .. } + | Token::Reference { .. } + | Token::FootnoteReference { .. } + | Token::FootnoteContent { .. } + | Token::Error { .. } => Some(false), + Token::PageDiv + | Token::Header { .. } + | Token::DisplayMath { .. } + | Token::Table { .. } + | Token::Figure { .. } + | Token::List { .. } + | Token::CodeBlock { .. } => Some(true), + Token::Ayano { + data: AyanoBlock { + is_static: true, .. + }, + } + | Token::Multiple { .. } => None, + } + } } /// # On token storage diff --git a/src/gen/lab/context.rs b/src/gen/lab/context.rs index 86d2a20..9c092ac 100644 --- a/src/gen/lab/context.rs +++ b/src/gen/lab/context.rs @@ -1,7 +1,9 @@ use std::collections::HashMap; -#[derive(Debug, Default)] +#[derive(Debug, smart_default::SmartDefault)] pub struct Context { pub encountered_footnotes: HashMap, pub inside_caption: bool, + #[default(true)] + pub allergic_to_newline: bool, } diff --git a/src/gen/lab/lab_preamble.tex b/src/gen/lab/lab_preamble.tex index a21e158..e8d1b86 100644 --- a/src/gen/lab/lab_preamble.tex +++ b/src/gen/lab/lab_preamble.tex @@ -20,44 +20,22 @@ \linespread{0.8} \selectfont } % нормативний шрифт -\let\oldCaption\caption -\renewcommand{\caption}[1]{ -\oldCaption{\fnt #1} -} % підпис нормативним шрифтом. +\newcommand{\capfnt}{ + \fnt +} % норматичний шрифт для підписів (тимчасово співпадає зі звичайним шрифтом) \newcommand{\tb}{ \hspace*{10mm} } % відступ у 5 символів "x" відповідно до ДСТУ \newcommand{\tbln}{ \newline - \hspace*{10mm} + \tb } % те ж саме, але з переносом рядка \newcommand{\tbsp}{ \vspace*{1ex} - \newline - \hspace*{10mm} + \tbln } % те ж саме, але з додатковим відступом між рядками -\iffalse -\let\oldSection\section -\renewcommand{\section}[1]{ - \oldSection*{\tb #1} - \addcontentsline{toc}{section}{#1} - \fnt - \tb -} % відображає у тексті та змісті наданий заголовок -\fi - -\iftrue -\let\oldSubSection\subsection -\renewcommand{\subsection}[1]{ - \oldSubSection*{\tb #1} - \addcontentsline{toc}{subsection}{#1} - \fnt - \tb -} % відображає у тексті та змісті наданий підзаголовок -\fi - % бібліографія \usepackage[square,sort,comma,numbers]{natbib} \renewcommand{\bibsection}{} @@ -105,18 +83,15 @@ \captionsetup[figure]{name={\fnt Рисунок~},labelsep=period} \captionsetup[table]{name={\fnt Таблиця~},labelsep=period} -% idk why, but this redefinition does not work, for some reason -% \let\oldCaption\caption -% \renewcommand{\caption}[1] { -% \oldCaption{\fnt #1} -% } - % markdown strikeouts \usepackage{soul} % codeblocks \usepackage{minted} +% enumeration +\usepackage[shortlabels]{enumitem} + \titleformat*{\section}{\fnt\LARGE\bfseries} \titleformat*{\subsection}{\fnt\Large\bfseries} \titleformat*{\subsubsection}{\fnt\Large} @@ -128,7 +103,7 @@ \begingroup \centering \obeylines -\topskip0pt % source: https://tex.stackexchange.com/questions/2326/vertically-center-text-on-a-page +\topskip 0pt % source: https://tex.stackexchange.com/questions/2326/vertically-center-text-on-a-page \vspace*{\fill} \textbf{ \LARGE $header-line1$ @@ -155,4 +130,3 @@ \newpage \fnt - diff --git a/src/gen/lab/mod.rs b/src/gen/lab/mod.rs index 21e958d..c353e79 100644 --- a/src/gen/lab/mod.rs +++ b/src/gen/lab/mod.rs @@ -133,7 +133,10 @@ where let columns = header.len(); let column_format = itertools::intersperse(std::iter::repeat("c").take(columns), "|"); - write!(output, "\n\\begin{{table}}[h!]\n\\begin{{center}}\n")?; + write!( + output, + "\n\\begin{{table}}[h!]\n\\fnt\n\\begin{{center}}\n" + )?; output.write_str("\\begin{tabular}{|")?; column_format .map(|s| { @@ -206,13 +209,9 @@ where Token::List { list_type, content } => { let start: &str = match list_type { crate::data::ListType::Bullet => "\\begin{itemize}", - crate::data::ListType::Num => "\\begin{enumerate}", - crate::data::ListType::Latin => { - "\\begin{enumerate}[label=\\alph*), ref=\\alph*]" - } - crate::data::ListType::Cyrillic => { - "\\begin{enumerate}[label=\\asbuk*), ref=\\asbuk*]" - } + crate::data::ListType::Num + | crate::data::ListType::Cyrillic + | crate::data::ListType::Latin => "\\begin{enumerate}", crate::data::ListType::Roman => { unimplemented!("Roman numeral lists are not supported for now") } @@ -226,10 +225,22 @@ where unimplemented!("Roman numeral lists are not supported for now") } }; + let mut ident_producer = match list_type { + data::ListType::Bullet | data::ListType::Num => None, + t @ data::ListType::Latin + | t @ data::ListType::Cyrillic + | t @ data::ListType::Roman => Some(crate::lexer::_list_item_generator(&t)), + }; output.write_str("\n")?; output.write_str(start)?; for item in content { - output.write_str("\n\\item ")?; + if let Some(ident_producer) = ident_producer.as_mut() { + output.write_str("\n\\item[")?; + output.write_str(ident_producer.next().expect("List should not have more elements than there are items in the iterator"))?; + output.write_str("] ")?; + } else { + output.write_str("\n\\item ")?; + } self.write_to(output, meta, context, item)?; } output.write_str("\n")?; @@ -299,7 +310,11 @@ where space_before, } => { if *is_newline { - output.write_str("\n\\tbln ")?; + if context.allergic_to_newline { + output.write_str("\\tb ")?; + } else { + output.write_str("\n\\tbln ")?; + } } else if *space_before { output.write_char(' ')?; } @@ -340,13 +355,16 @@ where if let Some(language) = language { writeln!( output, - "\n\\begin{{minted}}{{{language}}}\n{code}\n\\end{{minted}}\n" + "\n\\begin{{minted}}[linenos, mathescape, autogobble, breaklines]{{{language}}}\n{code}\n\\end{{minted}}\n" )?; } else { writeln!(output, "\n\\begin{{minted}}\n{code}\n\\end{{minted}}\n")?; } } }; + if let Some(new_value) = token.is_newline_allergic() { + context.allergic_to_newline = new_value; + } Ok(()) } @@ -546,7 +564,7 @@ mod tests { // Paragraphs test! {para1, Token::Paragraph{is_newline: false, formatting: None, content: "content, content!!!".into(), space_before: false}, "content, content!!!"} - test! {para2, Token::Paragraph{is_newline: true, formatting: None, content: "content, content!!!".into(), space_before: false}, "\n\\tbln content, content!!!"} + test! {para2, Token::Paragraph{is_newline: true, formatting: None, content: "content, content!!!".into(), space_before: false}, "\\tb content, content!!!"} // Formatting test! {formatting1, Token::Paragraph{is_newline: false, formatting: Some(Formatting::Bold), content: "some text, idk".into(), space_before: false}, "\\textbf{some text, idk}"} @@ -621,6 +639,7 @@ mod tests { ident: Some("example".into()) }, r" \begin{table}[h!] + \fnt \begin{center} \begin{tabular}{|c|c|c|}\hline header1 & header2 & header3 \\ \hline \hline @@ -646,6 +665,7 @@ cell_21 & cell_22 & cell_23 \\ \hline ident: Some("example".into()) }, r#" \begin{table}[h!] + \fnt \begin{center} \begin{tabular}{|c|c|c|}\hline header1 & header2 & header3 \\ \hline \hline @@ -689,10 +709,10 @@ cell_21 & cell_22 & cell_23 \\ \hline text!("point2"), text!("point3") ] }, r" - \begin{enumerate}[label=\asbuk*), ref=\asbuk*] - \item point1 - \item point2 - \item point3 + \begin{enumerate} + \item[а.] point1 + \item[б.] point2 + \item[в.] point3 \end{enumerate} "} @@ -860,6 +880,7 @@ data = [[1, 2, 3], [4, 5, 6]] is_space_before: false, }}, r#" \begin{table}[h!] +\fnt \begin{center} \begin{tabular}{|c|c|c|}\hline 1 & 2 & 3 \\ \hline \hline @@ -880,7 +901,7 @@ def f(): y = x * x return y".into(), language: Some("python".into()) }, r" -\begin{minted}{python} +\begin{minted}[linenos, mathescape, autogobble, breaklines]{python} def f(): x = 5 y = x * x diff --git a/src/lexer.rs b/src/lexer.rs index c06d1f6..ce85132 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -27,6 +27,35 @@ use crate::{ }, }; +/// Matches any sort of whitespace characters, *newline*, and a provided tag at the end +fn _multispace_line_break_some<'source, E: ParseError<&'source str>>( +) -> impl Parser<&'source str, &'source str, E> { + move |input: &'source str| { + // here's a chars iterator + let chars = input.char_indices(); + // now, limit them to fist non-whitespace character + let whitespace = chars.take_while(|(_, c)| c.is_whitespace()); + // then, filter out newline characters only + let newline = whitespace.filter_map(|(ind, c)| (c == '\n').then_some(ind)); + // lastly, find first newline character that prepends provided tag + let Some(res) = newline.last() else { + // if there's no newlines, that's an error + return Err(nom::Err::Error(E::from_error_kind( + input, + ErrorKind::MultiSpace, + ))); + }; + + // We may now construct the answer + let space = &input[..=res]; // space is up to the newline included + + // will return a copy of provided tag, since it was already matched + let rest = &input[(res + 1)..]; + + Ok((rest, space)) + } +} + /// Matches any sort of whitespace characters, *newline*, and a provided tag at the end fn _multispace_line_break<'source, E: ParseError<&'source str>>( tag: &'static str, @@ -626,7 +655,7 @@ macro_rules! char_array { /// Don't worry about it, I guess (?) /// (*why would I create and format these strings each time??!*) /// TODO I guess, #[cached] macro would do better here, or th -fn _list_item_generator<'scope>( +pub(crate) fn _list_item_generator<'scope>( list_type: &'scope ListType, ) -> Box + 'scope> { match list_type { @@ -683,7 +712,7 @@ fn _list_inner< input: &'source str, ) -> IResult<&'source str, Token<'source>, E> { // First, we need to actually detect list type - let (input, _) = _multispace_line_break("").parse(input)?; + let (input, _) = _multispace_line_break_some().parse(input)?; let (_, first_discriminator) = recognize(take_till1(char::is_whitespace))(input)?; let list_type = match first_discriminator { @@ -2207,6 +2236,8 @@ $$ tx!(", і два термоядерних реактори, що оточують їх. Точні реакції всередині невідомі, але цілком можливо що там женуть"), eq!(^!"du + t -> {}^{5}_{6}C"), tx!("."), foot!(^"1": tx!("Для поливу."))]} test_ok! {ok_idk2, "\n\n## Підпункт\n\n", head!(##"Підпункт")} + test_ok! {ok_list_newline, "Some text\n- And then\n- The list strikes\n", tks!(tx!("Some text"), ls!(- tx!("And then"), tx!("The list strikes")))} + test_ok! {ok_list_empty_newline, "Some text\n\n- And then\n- The list strikes\n", tks!(tx!("Some text"), ls!(- tx!("And then"), tx!("The list strikes")))} } } @@ -2328,19 +2359,19 @@ date: вербень 2077 prof: me :idk: --- ", [ - header_line1: "Навч. заклад", - header_line2: "Факультет", - document_type: "ПРОТОКОЛ", - title_line1: "Виконання марної роботи", - title_line2: "з історії стародавнього Єгипту", - title_line3: "(пинальна частина)", - title_line4: "НАЗВА РОБОТИ", - author_line1: "виконувало:", - author_line2: "дяч дзучунович", - author_line3: "(perfectly still)", - date: "вербень 2077", - prof: "me :idk:" - ], "\n"} + header_line1: "Навч. заклад", + header_line2: "Факультет", + document_type: "ПРОТОКОЛ", + title_line1: "Виконання марної роботи", + title_line2: "з історії стародавнього Єгипту", + title_line3: "(пинальна частина)", + title_line4: "НАЗВА РОБОТИ", + author_line1: "виконувало:", + author_line2: "дяч дзучунович", + author_line3: "(perfectly still)", + date: "вербень 2077", + prof: "me :idk:" + ], "\n"} test_ok! {ok_all_fields_permutated, r"--- author-line2: дяч дзучунович header-line1: Навч. заклад @@ -2356,17 +2387,17 @@ prof: me :idk: title-line3: (пинальна частина) --- ", [ - header_line1: "Навч. заклад", - header_line2: "Факультет", - document_type: "ПРОТОКОЛ", - title_line1: "Виконання марної роботи", - title_line2: "з історії стародавнього Єгипту", - title_line3: "(пинальна частина)", - title_line4: "НАЗВА РОБОТИ", - author_line1: "виконувало:", - author_line2: "дяч дзучунович", - author_line3: "(perfectly still)", - date: "вербень 2077", - prof: "me :idk:" - ], "\n"} + header_line1: "Навч. заклад", + header_line2: "Факультет", + document_type: "ПРОТОКОЛ", + title_line1: "Виконання марної роботи", + title_line2: "з історії стародавнього Єгипту", + title_line3: "(пинальна частина)", + title_line4: "НАЗВА РОБОТИ", + author_line1: "виконувало:", + author_line2: "дяч дзучунович", + author_line3: "(perfectly still)", + date: "вербень 2077", + prof: "me :idk:" + ], "\n"} } diff --git a/tests/input_files/edgecases.md b/tests/input_files/edgecases.md new file mode 100644 index 0000000..4a04b38 --- /dev/null +++ b/tests/input_files/edgecases.md @@ -0,0 +1,12 @@ +# List-after-para + +Some para +- List item 1 +- List item 2 + +Some para + +- List item 1 +- List item 2 + +*This bug resulted in lists to be parsed as series of paragraphs.* \ No newline at end of file diff --git a/tests/lex_files/edgecases.txt b/tests/lex_files/edgecases.txt new file mode 100644 index 0000000..6da8667 --- /dev/null +++ b/tests/lex_files/edgecases.txt @@ -0,0 +1,85 @@ +TitleInfo { + header_line1: None, + header_line2: None, + document_type: None, + title_line1: None, + title_line2: None, + title_line3: None, + title_line4: None, + author_line1: None, + author_line2: None, + author_line3: None, + date: None, + prof: None, + code_section_title: None, +} +Multiple { + tokens: Tokens( + [ + Paragraph { + is_newline: false, + space_before: false, + formatting: None, + content: "# List-after-para", + }, + Paragraph { + is_newline: true, + space_before: false, + formatting: None, + content: "Some para", + }, + List { + list_type: Bullet, + content: Tokens( + [ + Paragraph { + is_newline: false, + space_before: false, + formatting: None, + content: "List item 1", + }, + Paragraph { + is_newline: false, + space_before: false, + formatting: None, + content: "List item 2", + }, + ], + ), + }, + Paragraph { + is_newline: true, + space_before: false, + formatting: None, + content: "Some para", + }, + List { + list_type: Bullet, + content: Tokens( + [ + Paragraph { + is_newline: false, + space_before: false, + formatting: None, + content: "List item 1", + }, + Paragraph { + is_newline: false, + space_before: false, + formatting: None, + content: "List item 2", + }, + ], + ), + }, + Paragraph { + is_newline: true, + space_before: false, + formatting: Some( + Italic, + ), + content: "This bug resulted in lists to be parsed as series of paragraphs.", + }, + ], + ), +} \ No newline at end of file diff --git a/tests/transformed_files/ayano.txt b/tests/transformed_files/ayano.txt index 93b0444..f24a7a8 100644 --- a/tests/transformed_files/ayano.txt +++ b/tests/transformed_files/ayano.txt @@ -20,44 +20,22 @@ \linespread{0.8} \selectfont } % нормативний шрифт -\let\oldCaption\caption -\renewcommand{\caption}[1]{ -\oldCaption{\fnt #1} -} % підпис нормативним шрифтом. +\newcommand{\capfnt}{ + \fnt +} % норматичний шрифт для підписів (тимчасово співпадає зі звичайним шрифтом) \newcommand{\tb}{ \hspace*{10mm} } % відступ у 5 символів "x" відповідно до ДСТУ \newcommand{\tbln}{ \newline - \hspace*{10mm} + \tb } % те ж саме, але з переносом рядка \newcommand{\tbsp}{ \vspace*{1ex} - \newline - \hspace*{10mm} + \tbln } % те ж саме, але з додатковим відступом між рядками -\iffalse -\let\oldSection\section -\renewcommand{\section}[1]{ - \oldSection*{\tb #1} - \addcontentsline{toc}{section}{#1} - \fnt - \tb -} % відображає у тексті та змісті наданий заголовок -\fi - -\iftrue -\let\oldSubSection\subsection -\renewcommand{\subsection}[1]{ - \oldSubSection*{\tb #1} - \addcontentsline{toc}{subsection}{#1} - \fnt - \tb -} % відображає у тексті та змісті наданий підзаголовок -\fi - % бібліографія \usepackage[square,sort,comma,numbers]{natbib} \renewcommand{\bibsection}{} @@ -105,18 +83,15 @@ \captionsetup[figure]{name={\fnt Рисунок~},labelsep=period} \captionsetup[table]{name={\fnt Таблиця~},labelsep=period} -% idk why, but this redefinition does not work, for some reason -% \let\oldCaption\caption -% \renewcommand{\caption}[1] { -% \oldCaption{\fnt #1} -% } - % markdown strikeouts \usepackage{soul} % codeblocks \usepackage{minted} +% enumeration +\usepackage[shortlabels]{enumitem} + \titleformat*{\section}{\fnt\LARGE\bfseries} \titleformat*{\subsection}{\fnt\Large\bfseries} \titleformat*{\subsubsection}{\fnt\Large} @@ -128,7 +103,7 @@ \begingroup \centering \obeylines -\topskip0pt % source: https://tex.stackexchange.com/questions/2326/vertically-center-text-on-a-page +\topskip 0pt % source: https://tex.stackexchange.com/questions/2326/vertically-center-text-on-a-page \vspace*{\fill} \textbf{ \LARGE @@ -155,7 +130,6 @@ \newpage \fnt - This is a test of ayano-generated stuff. \tbln This is supposed to be a generated figure: \begin{figure}[h!] @@ -173,6 +147,7 @@ This is a test of ayano-generated stuff. \tbln This is expected to be a generated table: \begin{table}[h!] +\fnt \begin{center} \begin{tabular}{|c|c|}\hline x & y\\ \hline diff --git a/tests/transformed_files/ayano_display_blocks.txt b/tests/transformed_files/ayano_display_blocks.txt index 7f347a1..8573dfb 100644 --- a/tests/transformed_files/ayano_display_blocks.txt +++ b/tests/transformed_files/ayano_display_blocks.txt @@ -20,44 +20,22 @@ \linespread{0.8} \selectfont } % нормативний шрифт -\let\oldCaption\caption -\renewcommand{\caption}[1]{ -\oldCaption{\fnt #1} -} % підпис нормативним шрифтом. +\newcommand{\capfnt}{ + \fnt +} % норматичний шрифт для підписів (тимчасово співпадає зі звичайним шрифтом) \newcommand{\tb}{ \hspace*{10mm} } % відступ у 5 символів "x" відповідно до ДСТУ \newcommand{\tbln}{ \newline - \hspace*{10mm} + \tb } % те ж саме, але з переносом рядка \newcommand{\tbsp}{ \vspace*{1ex} - \newline - \hspace*{10mm} + \tbln } % те ж саме, але з додатковим відступом між рядками -\iffalse -\let\oldSection\section -\renewcommand{\section}[1]{ - \oldSection*{\tb #1} - \addcontentsline{toc}{section}{#1} - \fnt - \tb -} % відображає у тексті та змісті наданий заголовок -\fi - -\iftrue -\let\oldSubSection\subsection -\renewcommand{\subsection}[1]{ - \oldSubSection*{\tb #1} - \addcontentsline{toc}{subsection}{#1} - \fnt - \tb -} % відображає у тексті та змісті наданий підзаголовок -\fi - % бібліографія \usepackage[square,sort,comma,numbers]{natbib} \renewcommand{\bibsection}{} @@ -105,18 +83,15 @@ \captionsetup[figure]{name={\fnt Рисунок~},labelsep=period} \captionsetup[table]{name={\fnt Таблиця~},labelsep=period} -% idk why, but this redefinition does not work, for some reason -% \let\oldCaption\caption -% \renewcommand{\caption}[1] { -% \oldCaption{\fnt #1} -% } - % markdown strikeouts \usepackage{soul} % codeblocks \usepackage{minted} +% enumeration +\usepackage[shortlabels]{enumitem} + \titleformat*{\section}{\fnt\LARGE\bfseries} \titleformat*{\subsection}{\fnt\Large\bfseries} \titleformat*{\subsubsection}{\fnt\Large} @@ -128,7 +103,7 @@ \begingroup \centering \obeylines -\topskip0pt % source: https://tex.stackexchange.com/questions/2326/vertically-center-text-on-a-page +\topskip 0pt % source: https://tex.stackexchange.com/questions/2326/vertically-center-text-on-a-page \vspace*{\fill} \textbf{ \LARGE @@ -155,24 +130,14 @@ \newpage \fnt - \section{Display blocks} - -\tbln This feature allows you to insert certain `Ayano` blocks to the end of the output document, to document your exact actions for displayed results. +\tb This feature allows you to insert certain `Ayano` blocks to the end of the output document, to document your exact actions for displayed results. \tbln Here's an example:42 \tbln Display blocks can have descriptions too! Here's one:-20340$\pm$150 \subsection{Код} -\begin{minted}{python} -# You should be able to see this comment in the output doc -return 42 - -\end{minted} - - -\tbln ayano_block__1574475637377647406 -\begin{minted}{python} +\begin{minted}[linenos, mathescape, autogobble, breaklines]{python} # This code block has description # Might as well add some python syntax to check out minted's syntax highlighting: for i in range(100): @@ -183,5 +148,12 @@ return "err", -2.034e4, 145.64 \end{minted} me, fooling around +\begin{minted}[linenos, mathescape, autogobble, breaklines]{python} +# You should be able to see this comment in the output doc +return 42 + +\end{minted} + +\tb ayano_block__1574475637377647406 \end{document} \ No newline at end of file diff --git a/tests/transformed_files/basic.txt b/tests/transformed_files/basic.txt index 7db3083..e9d3a6a 100644 --- a/tests/transformed_files/basic.txt +++ b/tests/transformed_files/basic.txt @@ -20,44 +20,22 @@ \linespread{0.8} \selectfont } % нормативний шрифт -\let\oldCaption\caption -\renewcommand{\caption}[1]{ -\oldCaption{\fnt #1} -} % підпис нормативним шрифтом. +\newcommand{\capfnt}{ + \fnt +} % норматичний шрифт для підписів (тимчасово співпадає зі звичайним шрифтом) \newcommand{\tb}{ \hspace*{10mm} } % відступ у 5 символів "x" відповідно до ДСТУ \newcommand{\tbln}{ \newline - \hspace*{10mm} + \tb } % те ж саме, але з переносом рядка \newcommand{\tbsp}{ \vspace*{1ex} - \newline - \hspace*{10mm} + \tbln } % те ж саме, але з додатковим відступом між рядками -\iffalse -\let\oldSection\section -\renewcommand{\section}[1]{ - \oldSection*{\tb #1} - \addcontentsline{toc}{section}{#1} - \fnt - \tb -} % відображає у тексті та змісті наданий заголовок -\fi - -\iftrue -\let\oldSubSection\subsection -\renewcommand{\subsection}[1]{ - \oldSubSection*{\tb #1} - \addcontentsline{toc}{subsection}{#1} - \fnt - \tb -} % відображає у тексті та змісті наданий підзаголовок -\fi - % бібліографія \usepackage[square,sort,comma,numbers]{natbib} \renewcommand{\bibsection}{} @@ -105,18 +83,15 @@ \captionsetup[figure]{name={\fnt Рисунок~},labelsep=period} \captionsetup[table]{name={\fnt Таблиця~},labelsep=period} -% idk why, but this redefinition does not work, for some reason -% \let\oldCaption\caption -% \renewcommand{\caption}[1] { -% \oldCaption{\fnt #1} -% } - % markdown strikeouts \usepackage{soul} % codeblocks \usepackage{minted} +% enumeration +\usepackage[shortlabels]{enumitem} + \titleformat*{\section}{\fnt\LARGE\bfseries} \titleformat*{\subsection}{\fnt\Large\bfseries} \titleformat*{\subsubsection}{\fnt\Large} @@ -128,7 +103,7 @@ \begingroup \centering \obeylines -\topskip0pt % source: https://tex.stackexchange.com/questions/2326/vertically-center-text-on-a-page +\topskip 0pt % source: https://tex.stackexchange.com/questions/2326/vertically-center-text-on-a-page \vspace*{\fill} \textbf{ \LARGE @@ -155,19 +130,16 @@ \newpage \fnt - \section{Перша тема} - -\tbln Тут трохи тексту і пояснень про квантові мультивироджені рефурбалідзери змішаного типу. Це нудно, тож тримайте формулу: +\tb Тут трохи тексту і пояснень про квантові мультивироджені рефурбалідзери змішаного типу. Це нудно, тож тримайте формулу: \begin{equation} Ref(z) = \int \limits_{0}^{1} t^{z-1} \cdot \exp(-t) dt \label{eq:refurbalidzer_1} \end{equation} \subsection{Підпункт} - -\tbln Рефурбалідзери можуть виявитись дуже важливими для збереження екологічної безпеки нашої планети. До прикладу, в них є наступні переваги: +\tb Рефурбалідзери можуть виявитись дуже важливими для збереження екологічної безпеки нашої планети. До прикладу, в них є наступні переваги: \begin{itemize} \item Нормувальний множник $|\psi|^{2} = 3/4$ \item Кількість хромосом - $\exp(\pi)$ @@ -179,7 +151,6 @@ Ref(z) = \int \limits_{0}^{1} t^{z-1} \cdot \exp(-t) dt \caption{\capfnt Принципове влаштування рефурбалідзера} \label{fig:refurb} \end{figure} - -\tbln На світлині \ref{fig:refurb} можна бачити принципову схему влаштування рефурбалідзера. Неважно помітити великі очі\footnotemark[1]\footnotetext[1]{Для поливу.}, і два термоядерних реактори, що оточують їх. Точні реакції всередині невідомі, але цілком можливо що там женуть $du + t -> {}^{5}_{6}C$... +\tb На світлині \ref{fig:refurb} можна бачити принципову схему влаштування рефурбалідзера. Неважно помітити великі очі\footnotemark[1]\footnotetext[1]{Для поливу.}, і два термоядерних реактори, що оточують їх. Точні реакції всередині невідомі, але цілком можливо що там женуть $du + t -> {}^{5}_{6}C$... \end{document} \ No newline at end of file diff --git a/tests/transformed_files/deceit.txt b/tests/transformed_files/deceit.txt index 2ed56e3..61f0ee2 100644 --- a/tests/transformed_files/deceit.txt +++ b/tests/transformed_files/deceit.txt @@ -20,44 +20,22 @@ \linespread{0.8} \selectfont } % нормативний шрифт -\let\oldCaption\caption -\renewcommand{\caption}[1]{ -\oldCaption{\fnt #1} -} % підпис нормативним шрифтом. +\newcommand{\capfnt}{ + \fnt +} % норматичний шрифт для підписів (тимчасово співпадає зі звичайним шрифтом) \newcommand{\tb}{ \hspace*{10mm} } % відступ у 5 символів "x" відповідно до ДСТУ \newcommand{\tbln}{ \newline - \hspace*{10mm} + \tb } % те ж саме, але з переносом рядка \newcommand{\tbsp}{ \vspace*{1ex} - \newline - \hspace*{10mm} + \tbln } % те ж саме, але з додатковим відступом між рядками -\iffalse -\let\oldSection\section -\renewcommand{\section}[1]{ - \oldSection*{\tb #1} - \addcontentsline{toc}{section}{#1} - \fnt - \tb -} % відображає у тексті та змісті наданий заголовок -\fi - -\iftrue -\let\oldSubSection\subsection -\renewcommand{\subsection}[1]{ - \oldSubSection*{\tb #1} - \addcontentsline{toc}{subsection}{#1} - \fnt - \tb -} % відображає у тексті та змісті наданий підзаголовок -\fi - % бібліографія \usepackage[square,sort,comma,numbers]{natbib} \renewcommand{\bibsection}{} @@ -105,18 +83,15 @@ \captionsetup[figure]{name={\fnt Рисунок~},labelsep=period} \captionsetup[table]{name={\fnt Таблиця~},labelsep=period} -% idk why, but this redefinition does not work, for some reason -% \let\oldCaption\caption -% \renewcommand{\caption}[1] { -% \oldCaption{\fnt #1} -% } - % markdown strikeouts \usepackage{soul} % codeblocks \usepackage{minted} +% enumeration +\usepackage[shortlabels]{enumitem} + \titleformat*{\section}{\fnt\LARGE\bfseries} \titleformat*{\subsection}{\fnt\Large\bfseries} \titleformat*{\subsubsection}{\fnt\Large} @@ -128,7 +103,7 @@ \begingroup \centering \obeylines -\topskip0pt % source: https://tex.stackexchange.com/questions/2326/vertically-center-text-on-a-page +\topskip 0pt % source: https://tex.stackexchange.com/questions/2326/vertically-center-text-on-a-page \vspace*{\fill} \textbf{ \LARGE @@ -155,63 +130,53 @@ \newpage \fnt - This test is dedicated to attempts of parser deception, i.e. usage of special symbols in wrong places to see if parser recognizes them as control symbols. There we go: \section{Page dividers} - -\tbln Page dividers are represented by ------ +\tb Page dividers are represented by ------ \tbln If the line above has dashes ^^^^^^ here, it probably works alright. \tbln Here's a genuine divider \clearpage - -\tbln Fun fact: there can be a paragraph right after it! +\tb Fun fact: there can be a paragraph right after it! \clearpage Some text, cause why not \tbln But this \textbf{DOES} look cursed, don't do it. \section{Headers} - -\tbln Headers are most recognizable by '#'-symbol. For it to actually represent a header, it needs to at the beginning of the line and there should be no more than 6 of them. \textbf{ANYTHING} up to the end of line will be included into a header, then. +\tb Headers are most recognizable by '#'-symbol. For it to actually represent a header, it needs to at the beginning of the line and there should be no more than 6 of them. \textbf{ANYTHING} up to the end of line will be included into a header, then. \tbln Example: \textbf{This is a long, yet formally correct header. You can add $equations$ and *formatting* here, but I provide no guarantees as of the way these would display in final document.} - -\tbln Here's an example of incorrect header: +\tb Here's an example of incorrect header: \tbln ###### This header is considered incorrect for this application, and will be parsed as regular paragraph instead \tbln Another example of incorrect header, featuring 7 hash-symbols: \tbln ####### This will be parsed as a regular paragraph \section{Equations} - -\tbln There are two types of equations in markdown/latex: inline mathmode and display mathmode +\tb There are two types of equations in markdown/latex: inline mathmode and display mathmode \tbln Both of them are defined by $-symbols. However, they must be paired to actually represent these. Here are examples: \tbln Inline: $y = x ^ 2$ \tbln Display: \begin{equation} y = x ^ 2 \end{equation} - -\tbln Note that display mathmode should ALWAYS start from a new line for this application. +\tb Note that display mathmode should ALWAYS start from a new line for this application. \tbln Basically, there's not much to deceive for: you can do that: \tbln $ \tbln $ \tbln (the above should not be mathmode), and that's it, basically :sad: \section{Table} - -\tbln Tables are defined by |-symbol. But the actual table will happen only if they are the first thing on the line. +\tb Tables are defined by |-symbol. But the actual table will happen only if they are the first thing on the line. \tbln Due to current implementation, there's no cure apart from display mathmode and code: \tbln (this should be display mathmode): \begin{equation} |x| \end{equation} - -\tbln (this should be a code block): -\begin{minted}{bash} +\tb (this should be a code block): +\begin{minted}[linenos, mathescape, autogobble, breaklines]{bash} head /dev/random | cat \end{minted} \section{Figure} - -\tbln Figure is defined by a ![[ sequence at a start of the line. Same points should be uphold here: +\tb Figure is defined by a ![[ sequence at a start of the line. Same points should be uphold here: \begin{equation} ![[ \end{equation} @@ -222,19 +187,16 @@ head /dev/random \section{References} - -\tbln These start with [] +\tb These start with [] \subsection{Hyperrefs} - -\tbln These are followed by parentheses (). Nothing is allowed between them, thus +\tb These are followed by parentheses (). Nothing is allowed between them, thus \tbln Here's a correct href: \href{https://youtu.be/rickroll_link_lol}{my gh page} \tbln Here are incorrect refs: \tbln [my gh page] (https://youtu.be/rickroll\textit{link}lol) \tbln [my gh page] \tbln (https://youtu.be/rickroll\textit{link}lol) \subsection{Footnote refs} - -\tbln These are started by [^. +\tb These are started by [^. \tbln Here are correct footnote refs: \footnotemark[1], \footnotemark[2] \tbln Here are incorrect ones: [^1, ^1], \tbln [^ @@ -242,8 +204,7 @@ head /dev/random \tbln ] \tbln So on \subsection{Object refs} - -\tbln These refer to objects like figures, tables and equations. +\tb These refer to objects like figures, tables and equations. \tbln Started by [@. \tbln Here's a correct ref: \ref{fig:experiment_schema} \tbln Here's are bad ones: [@ fig:1, @ source3], @@ -252,31 +213,26 @@ head /dev/random \tbln ] \tbln \textbf{NOTE}: despite object types being prepended with 'eq', 'fig' or 'tab' in refs, you \textit{can} use regular latex to define your custom refs. In fact, LaTeX doesn't care about all of these at all; I'm doing these transformations just for my own comfort, as I've been doing this manually. \section{Formatting} - -\tbln This object's type is deprecated and will be removed soon. +\tb This object's type is deprecated and will be removed soon. \tbln It starts and ends with \begin{itemize} \item * or _ \item ** or __ \item ~~ -\end{itemize} -\tbln \textbf{WARN}: As of now, these \textit{might} cause cause massive problems if encountered at the start of the line. +\end{itemize}\tb \textbf{WARN}: As of now, these \textit{might} cause cause massive problems if encountered at the start of the line. \tbln Basically, parsers for these tokens attempt parse everything up until they find a closing token. But if they are inserted in a middle of the text, then it's ok - they will be limited by current paragraph, and (most likely) will find no match inside it, if their use was unintended. \section{Footnote content} - -\tbln These start with '\footnotemark[3]:' and proceed to the end of line. +\tb These start with '\footnotemark[3]:' and proceed to the end of line. \tbln Example of valid content: \tbln Example of invalid content: \tbln [^source2: This is invalid content\footnotemark[4]\footnotetext[4]{Explanation of source2.} : This is invalid content \footnotemark[4]: This is invalid content \tbln They also must start at the beginning of the line. If fact, this: \footnotemark[4]: Some content \tbln Is expected to be a footnote ref with a colon. \section{Code blocks} - -\tbln There are two fundamental types of code blocks: normal code block and \textbf{Ayano} code block. +\tb There are two fundamental types of code blocks: normal code block and \textbf{Ayano} code block. \subsection{Normal code block} - -\tbln These start and end with '```' on a newline. There can be \textbf{practically anything} inside. Example: -\begin{minted}{java} +\tb These start and end with '```' on a newline. There can be \textbf{practically anything} inside. Example: +\begin{minted}[linenos, mathescape, autogobble, breaklines]{java} int x = 2; int y = 4; int[] arr = new int[2]; @@ -284,9 +240,8 @@ arr[0] = x; arr[1] = 4; \end{minted} - -\tbln There can be anything after code block had closed: -\begin{minted}{lua(Idk why vscode detects this block closing as a valid lua syntax)} +\tb There can be anything after code block had closed: +\begin{minted}[linenos, mathescape, autogobble, breaklines]{lua(Idk why vscode detects this block closing as a valid lua syntax)} local x = 'matt' local y = 'y' local z = x .. y -- matty!! @@ -300,23 +255,21 @@ local z = x .. y -- matty!! \tbln print(x) \tbln ``` \subsection{Ayano code block} - -\tbln \textit{because Ayano loves Kyoko} +\tb \textit{because Ayano loves Kyoko} \tbln This object looks and behaves just like normal code block, but is only limited to \textit{python} language, can have spacial arguments in a language line, may leave something behind in the document. For example: \tbln This Ayano block will leave plain "2" behind it:2 \tbln Akin to normal blocks, it's ok to leave something trailing: -\begin{minted}{ayano} +\begin{minted}[linenos, mathescape, autogobble, breaklines]{ayano} 2 \end{minted} This text is ok to be here \tbln And these are invalid Ayano blocks: -\begin{minted}{ayano} +\begin{minted}[linenos, mathescape, autogobble, breaklines]{ayano} 2 \end{minted} - -\tbln ```ayano +\tb ```ayano \tbln 2 \tbln ``` diff --git a/tests/transformed_files/edgecases.txt b/tests/transformed_files/edgecases.txt new file mode 100644 index 0000000..7bbf591 --- /dev/null +++ b/tests/transformed_files/edgecases.txt @@ -0,0 +1,144 @@ +\documentclass[a4paper]{article} +\usepackage{graphicx, hyperref, xcolor, gensymb, amssymb, mathtools, wrapfig, mathtools, microtype, lastpage, caption, titlesec, paracol, longtable, booktabs, cancel} +\usepackage[T2A, T1]{fontenc} +\usepackage[utf8]{inputenc} +\usepackage[russian, ukrainian]{babel} +\hypersetup{ + colorlinks, + linkcolor={blue!20!black}, + citecolor={blue!50!black}, + urlcolor={blue!80!black} +} + +\usepackage[left = 25mm, right = 10mm, top=20mm, bottom=20mm, bindingoffset=0cm]{geometry} + +% КОМАНДИ + +\newcommand{\fnt}{ +\fontshape{n} +\fontsize{14pt} {19pt} +\linespread{0.8} +\selectfont +} % нормативний шрифт +\newcommand{\capfnt}{ + \fnt +} % норматичний шрифт для підписів (тимчасово співпадає зі звичайним шрифтом) + +\newcommand{\tb}{ + \hspace*{10mm} +} % відступ у 5 символів "x" відповідно до ДСТУ +\newcommand{\tbln}{ + \newline + \tb +} % те ж саме, але з переносом рядка +\newcommand{\tbsp}{ + \vspace*{1ex} + \tbln +} % те ж саме, але з додатковим відступом між рядками + +% бібліографія +\usepackage[square,sort,comma,numbers]{natbib} +\renewcommand{\bibsection}{} +\usepackage{totcount} +\newtotcounter{citnum} +% лічильник цитувань +\def\oldbibitem{} \let\oldbibitem=\bibitem +\def\bibitem{\stepcounter{citnum}\oldbibitem} + +% лічильник зображень +\newtotcounter{graphnum} +\def\oldincludegraphics{} \let\oldincludegraphics=\includegraphics +\def\includegraphics{\stepcounter{graphnum}\oldincludegraphics} + +% лічильник таблиць (НЕАВТОМАТИЧНИЙ!!!) +\newtotcounter{tabnum} + +% лічильник додатків (НЕАВТОМАТИЧНИЙ!!!) +\newtotcounter{dodnum} + +% нумерація сторінок +\usepackage{fancyhdr} +\pagestyle{fancy} +\fancyhf{} +\renewcommand{\headrulewidth}{0pt} +\setlength{\headheight}{15.3pt} +\fancyhead[R]{\fnt \thepage} + +% виноски +\renewcommand{\thefootnote}{\large\arabic{footnote})~} +\renewcommand{\footnoterule}{\rule{20mm}{0.4pt} \vspace*{0.5ex}} +% \newcommand{\vyn}[2]{ +% \footnote[#1]{\large #2} +% } % створити виноску + +\let\oldFootnote\footnote +\renewcommand{\footnote}[1]{ + \oldFootnote{\large #1} +} % перегрузка виноски + +\providecommand{\tightlist}{% + \setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}} + +% підписи малюнків/таблиць +\captionsetup[figure]{name={\fnt Рисунок~},labelsep=period} +\captionsetup[table]{name={\fnt Таблиця~},labelsep=period} + +% markdown strikeouts +\usepackage{soul} + +% codeblocks +\usepackage{minted} + +% enumeration +\usepackage[shortlabels]{enumitem} + +\titleformat*{\section}{\fnt\LARGE\bfseries} +\titleformat*{\subsection}{\fnt\Large\bfseries} +\titleformat*{\subsubsection}{\fnt\Large} + +\begin{document} +\thispagestyle{empty} % [A]:for cropping +% ------------ +% source: https://tex.stackexchange.com/questions/173651/need-help-to-create-such-a-beautiful-title-page +\begingroup +\centering +\obeylines +\topskip 0pt % source: https://tex.stackexchange.com/questions/2326/vertically-center-text-on-a-page +\vspace*{\fill} +\textbf{ +\LARGE +\LARGE +\vspace{100pt} +\LARGE +\Large +\Large +\Large +\huge +\vspace{50pt} +\large +\large +\large +\large +\vspace*{\fill} +\hspace{\fill}\large \rule{20mm}{0.4pt} +\vspace{10pt} +} +\endgroup +% ------------ +\newpage +\tableofcontents + +\newpage +\fnt +# List-after-para +\tbln Some para +\begin{itemize} +\item List item 1 +\item List item 2 +\end{itemize}\tb Some para +\begin{itemize} +\item List item 1 +\item List item 2 +\end{itemize}\tb \textit{This bug resulted in lists to be parsed as series of paragraphs.} + +\end{document} \ No newline at end of file diff --git a/tests/transformed_files/tables_code.txt b/tests/transformed_files/tables_code.txt index 941c681..96c937b 100644 --- a/tests/transformed_files/tables_code.txt +++ b/tests/transformed_files/tables_code.txt @@ -20,44 +20,22 @@ \linespread{0.8} \selectfont } % нормативний шрифт -\let\oldCaption\caption -\renewcommand{\caption}[1]{ -\oldCaption{\fnt #1} -} % підпис нормативним шрифтом. +\newcommand{\capfnt}{ + \fnt +} % норматичний шрифт для підписів (тимчасово співпадає зі звичайним шрифтом) \newcommand{\tb}{ \hspace*{10mm} } % відступ у 5 символів "x" відповідно до ДСТУ \newcommand{\tbln}{ \newline - \hspace*{10mm} + \tb } % те ж саме, але з переносом рядка \newcommand{\tbsp}{ \vspace*{1ex} - \newline - \hspace*{10mm} + \tbln } % те ж саме, але з додатковим відступом між рядками -\iffalse -\let\oldSection\section -\renewcommand{\section}[1]{ - \oldSection*{\tb #1} - \addcontentsline{toc}{section}{#1} - \fnt - \tb -} % відображає у тексті та змісті наданий заголовок -\fi - -\iftrue -\let\oldSubSection\subsection -\renewcommand{\subsection}[1]{ - \oldSubSection*{\tb #1} - \addcontentsline{toc}{subsection}{#1} - \fnt - \tb -} % відображає у тексті та змісті наданий підзаголовок -\fi - % бібліографія \usepackage[square,sort,comma,numbers]{natbib} \renewcommand{\bibsection}{} @@ -105,18 +83,15 @@ \captionsetup[figure]{name={\fnt Рисунок~},labelsep=period} \captionsetup[table]{name={\fnt Таблиця~},labelsep=period} -% idk why, but this redefinition does not work, for some reason -% \let\oldCaption\caption -% \renewcommand{\caption}[1] { -% \oldCaption{\fnt #1} -% } - % markdown strikeouts \usepackage{soul} % codeblocks \usepackage{minted} +% enumeration +\usepackage[shortlabels]{enumitem} + \titleformat*{\section}{\fnt\LARGE\bfseries} \titleformat*{\subsection}{\fnt\Large\bfseries} \titleformat*{\subsubsection}{\fnt\Large} @@ -128,7 +103,7 @@ \begingroup \centering \obeylines -\topskip0pt % source: https://tex.stackexchange.com/questions/2326/vertically-center-text-on-a-page +\topskip 0pt % source: https://tex.stackexchange.com/questions/2326/vertically-center-text-on-a-page \vspace*{\fill} \textbf{ \LARGE @@ -155,12 +130,11 @@ \newpage \fnt - Наступний тест. Тут протестуємо таблиці і блоки коду. \subsection{Таблиці} - -\tbln Звичайні таблиці; Можуть всередині мати \textbf{формули} чи \textit{посилання} на інші штуки +\tb Звичайні таблиці; Можуть всередині мати \textbf{формули} чи \textit{посилання} на інші штуки \begin{table}[h!] +\fnt \begin{center} \begin{tabular}{|c|c|c|}\hline Storage & Space & Number\\ \hline @@ -173,9 +147,9 @@ \label{tab:example_1} \end{center} \end{table} - -\tbln Таблиця цілком собі може не мати ідентифікатора або опису: +\tb Таблиця цілком собі може не мати ідентифікатора або опису: \begin{table}[h!] +\fnt \begin{center} \begin{tabular}{|c|c|}\hline Кількість & Роль\\ \hline @@ -188,10 +162,9 @@ \end{table} \subsection{Блоки коду} - -\tbln Наразі мають досить обмежену підтримку, адже у $\LaTeX$ немає нативного поняття блоків коду. +\tb Наразі мають досить обмежену підтримку, адже у $\LaTeX$ немає нативного поняття блоків коду. \tbln На щастя, у \textbf{markdown} таке поняття є\footnotemark[2]\footnotetext[2]{Принаймні, \textit{Obsidian} таке робить}: -\begin{minted}{rust} +\begin{minted}[linenos, mathescape, autogobble, breaklines]{rust} let x = 5; let y = x.powi(2); let z = (y as f64).sqrt(); diff --git a/tests/transformed_files/title_info.txt b/tests/transformed_files/title_info.txt index 41fc9f5..701c2c9 100644 --- a/tests/transformed_files/title_info.txt +++ b/tests/transformed_files/title_info.txt @@ -20,44 +20,22 @@ \linespread{0.8} \selectfont } % нормативний шрифт -\let\oldCaption\caption -\renewcommand{\caption}[1]{ -\oldCaption{\fnt #1} -} % підпис нормативним шрифтом. +\newcommand{\capfnt}{ + \fnt +} % норматичний шрифт для підписів (тимчасово співпадає зі звичайним шрифтом) \newcommand{\tb}{ \hspace*{10mm} } % відступ у 5 символів "x" відповідно до ДСТУ \newcommand{\tbln}{ \newline - \hspace*{10mm} + \tb } % те ж саме, але з переносом рядка \newcommand{\tbsp}{ \vspace*{1ex} - \newline - \hspace*{10mm} + \tbln } % те ж саме, але з додатковим відступом між рядками -\iffalse -\let\oldSection\section -\renewcommand{\section}[1]{ - \oldSection*{\tb #1} - \addcontentsline{toc}{section}{#1} - \fnt - \tb -} % відображає у тексті та змісті наданий заголовок -\fi - -\iftrue -\let\oldSubSection\subsection -\renewcommand{\subsection}[1]{ - \oldSubSection*{\tb #1} - \addcontentsline{toc}{subsection}{#1} - \fnt - \tb -} % відображає у тексті та змісті наданий підзаголовок -\fi - % бібліографія \usepackage[square,sort,comma,numbers]{natbib} \renewcommand{\bibsection}{} @@ -105,18 +83,15 @@ \captionsetup[figure]{name={\fnt Рисунок~},labelsep=period} \captionsetup[table]{name={\fnt Таблиця~},labelsep=period} -% idk why, but this redefinition does not work, for some reason -% \let\oldCaption\caption -% \renewcommand{\caption}[1] { -% \oldCaption{\fnt #1} -% } - % markdown strikeouts \usepackage{soul} % codeblocks \usepackage{minted} +% enumeration +\usepackage[shortlabels]{enumitem} + \titleformat*{\section}{\fnt\LARGE\bfseries} \titleformat*{\subsection}{\fnt\Large\bfseries} \titleformat*{\subsubsection}{\fnt\Large} @@ -128,7 +103,7 @@ \begingroup \centering \obeylines -\topskip0pt % source: https://tex.stackexchange.com/questions/2326/vertically-center-text-on-a-page +\topskip 0pt % source: https://tex.stackexchange.com/questions/2326/vertically-center-text-on-a-page \vspace*{\fill} \textbf{ \LARGE Навч. заклад @@ -155,11 +130,9 @@ \newpage \fnt - \section{First para} - -\tbln This text is \textit{just} a dummy, so that document won't feel \st{too} \textbf{empty} +\tb This text is \textit{just} a dummy, so that document won't feel \st{too} \textbf{empty} \tbln Have a some oily maccaroni: \begin{equation} \gamma = \lim_{n \to \infty} \sum\limits_{i=1}^{n} \dfrac{1}{n} - \ln(n) From e1f8aa692439b628b7fba924933e164ce126fd36 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Wed, 27 Mar 2024 10:48:54 +0200 Subject: [PATCH 49/98] Fixed issue of displayed code blocks have hash-dependent (hence - non-deterministic) write order. It's a workaround rn, but it will be much more refined, once shared storage is implemented --- src/data.rs | 29 ++++-- src/gen/ayano.rs | 114 +++++++++++++++-------- src/gen/lab/mod.rs | 25 +++-- src/lexer.rs | 9 +- src/util.rs | 16 ++-- tests/lex_files/ayano.txt | 9 ++ tests/lex_files/ayano_display_blocks.txt | 6 ++ tests/lex_files/basic.txt | 4 +- tests/lex_files/deceit.txt | 3 + 9 files changed, 148 insertions(+), 67 deletions(-) diff --git a/src/data.rs b/src/data.rs index 9ce4e7a..7cb1e54 100644 --- a/src/data.rs +++ b/src/data.rs @@ -7,7 +7,7 @@ use std::{ use url::Url; -use crate::util::HashIgnored; +use crate::util::StaticDebug; #[derive(Debug, Default, PartialEq, Clone)] pub struct TitleInfo<'source> { @@ -34,7 +34,7 @@ pub struct TitleInfo<'source> { // Also, vectors of parsed tokens are obviously not intended to live longer than the source. // In fact, for any possible appliance they can be assumed to life for as long as source does. // Hope is for lifetime variance to help with lifetime casting, or th -#[derive(Debug, PartialEq, Hash, Clone)] +#[derive(Debug, PartialEq, Clone)] // #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Tokens<'source>(Rc<[Token<'source>]>); @@ -69,7 +69,7 @@ impl<'source> Tokens<'source> { } } -#[derive(Debug, Clone, Hash, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub enum DisplayState<'code> { NotDisplayed, DisplayedNoCaption, @@ -88,9 +88,10 @@ impl<'code> DisplayState<'code> { } } -#[derive(Debug, Clone, Hash, PartialEq)] +#[derive(Debug, Clone)] // #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct AyanoBlock<'code> { + pub ident: StaticDebug, pub display_state: DisplayState<'code>, pub is_space_before: bool, pub is_static: bool, @@ -98,6 +99,16 @@ pub struct AyanoBlock<'code> { pub insert_path: Option>, } +impl PartialEq for AyanoBlock<'_> { + fn eq(&self, other: &Self) -> bool { + self.display_state == other.display_state + && self.is_space_before == other.is_space_before + && self.is_static == other.is_static + && self.code == other.code + && self.insert_path == other.insert_path + } +} + // an abandoned idea. this would result in weird behavior, if underlying data happened to be moved. // WARN! note this semantics: AyanoBlocks are deemed the same, if they are literally referencing the same data, /* @@ -112,7 +123,7 @@ impl PartialEq for AyanoBlock<'_> { } */ -#[derive(Debug, Clone, PartialEq, Hash)] +#[derive(Debug, Clone, PartialEq)] // #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum ListType { Bullet, @@ -122,7 +133,7 @@ pub enum ListType { Roman, } -#[derive(Debug, Clone, PartialEq, Hash)] +#[derive(Debug, Clone, PartialEq)] // #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Formatting { Bold, @@ -133,7 +144,7 @@ pub enum Formatting { type Tx<'source> = Cow<'source, str>; type Pth<'source> = Cow<'source, Path>; -#[derive(Debug, PartialEq, Clone, Hash)] +#[derive(Debug, PartialEq, Clone)] // #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Token<'source> { PageDiv, @@ -156,7 +167,7 @@ pub enum Token<'source> { src_name: Pth<'source>, caption: Option>>, ident: Option>, - width: Option>, + width: Option, }, Href { space_before: bool, @@ -263,6 +274,7 @@ impl ToStaticExt for AyanoBlock<'_> { fn to_static(&self) -> Self::AsStatic { let AyanoBlock { + ident, code, insert_path, display_state, @@ -270,6 +282,7 @@ impl ToStaticExt for AyanoBlock<'_> { is_space_before, } = self; AyanoBlock { + ident: ident.clone(), code: code.to_static(), insert_path: insert_path.as_ref().map(Cow::to_static), is_static: *is_static, diff --git a/src/gen/ayano.rs b/src/gen/ayano.rs index 2b80ce9..c91eb10 100644 --- a/src/gen/ayano.rs +++ b/src/gen/ayano.rs @@ -6,13 +6,13 @@ use pyo3::{ types::{PyList, PyTuple}, PyAny, Python, }; +use rand::Rng; use regex::Regex; use std::{ borrow::Cow, - collections::{hash_map::DefaultHasher, HashMap}, + collections::BinaryHeap, fmt::{Arguments, Debug, Write}, fs::File, - hash::{Hash, Hasher}, io::{BufRead, BufReader}, num::{ParseFloatError, ParseIntError}, path::PathBuf, @@ -23,8 +23,8 @@ use crate::{ data::{AyanoBlock, DisplayState, Token, Tokens}, lexer::KyomatoLexError, util::{ - Equivalent, GenericRange, GenericRangeParseError, HashIgnored, Immutable, InlinedStack, - InlinedStackAccessor, + Equivalent, GenericRange, GenericRangeParseError, Immutable, InlinedStack, + InlinedStackAccessor, StaticDebug, }, }; @@ -54,10 +54,42 @@ struct BlockInfo { insert_path: Option, // none means there's no script to insert additionally } +impl<'source> PartialEq> for BlockInfo { + fn eq(&self, other: &AyanoBlock<'source>) -> bool { + self.ident.eq(&other.ident) + } +} + +impl<'source> PartialOrd> for BlockInfo { + fn partial_cmp(&self, other: &AyanoBlock<'source>) -> Option { + self.ident.partial_cmp(&other.ident) + } +} + +impl PartialEq for BlockInfo { + fn eq(&self, other: &Self) -> bool { + self.ident == other.ident + } +} + +impl Eq for BlockInfo {} + +impl PartialOrd for BlockInfo { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for BlockInfo { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.ident.cmp(&other.ident) + } +} + #[derive(Debug)] pub struct AyanoBuilder { code: String, - blocks: HashMap, + blocks: Vec, } impl Default for AyanoBuilder { @@ -70,7 +102,7 @@ impl AyanoBuilder { pub fn new() -> Self { Self { code: String::new(), - blocks: HashMap::new(), + blocks: Vec::new(), } } @@ -84,24 +116,22 @@ impl AyanoBuilder { } else { python::append_static(&mut self.code, &transformed_code); } - self.blocks.insert( + self.blocks.push(BlockInfo { ident, - BlockInfo { - ident, - function_name: info.function_name, - display_info: info.caption.map(|name| DisplayInfo { - code: transformed_code, - caption: name, - }), - insert_path: info.insert_path, - }, - ); + function_name: info.function_name, + display_info: info.caption.map(|name| DisplayInfo { + code: transformed_code, + caption: name, + }), + insert_path: info.insert_path, + }); Ok(()) } pub fn initialize(self) -> Result { - let Self { code, blocks } = self; + let Self { code, mut blocks } = self; let token = init(&code.as_str())?; + blocks.sort_unstable(); Ok(AyanoExecutor { token, blocks: blocks.into(), @@ -112,7 +142,7 @@ impl AyanoBuilder { #[derive(Debug)] pub struct AyanoExecutor { token: InitToken, - blocks: Immutable>, + blocks: Box<[BlockInfo]>, } #[derive(Debug, derive_more::From, PartialEq, thiserror::Error)] @@ -145,11 +175,14 @@ impl AyanoExecutor { where for<'py> Transform: FnOnce(&'py PyAny, Python<'py>) -> AyanoResult<'source, Out>, { - let hash = calculate_hash(token); - let Some(info) = self.blocks.get(&hash) else { + let Ok(info_ind) = self + .blocks + .binary_search_by_key(&*token.ident, |bi| bi.ident) + else { // this block was not passed here before return Err(AyanoError::NoFunction(token.clone())); }; + let info = &self.blocks[info_ind]; let Some(function_name) = info.function_name.as_ref() else { // this block was passed here before, but it's not a function return Err(AyanoError::NoFunction(token.clone())); @@ -167,7 +200,7 @@ impl AyanoExecutor { pub fn display_blocks<'s>(&'s self) -> impl IntoIterator { self.blocks - .values() + .into_iter() .filter_map(|info| info.display_info.as_ref()) } } @@ -181,6 +214,7 @@ mod ayano_tests { fn $name() { // arrange let block = AyanoBlock { + ident: StaticDebug(rand::thread_rng().gen()), display_state: DisplayState::NotDisplayed, is_static: false, code: $code.into(), @@ -228,6 +262,7 @@ y = x / 100 // arrange let mut builder = AyanoBuilder::new(); let static_block = AyanoBlock { + ident: StaticDebug(rand::thread_rng().gen()), code: $static_code.into(), insert_path: None, display_state: DisplayState::NotDisplayed, @@ -235,6 +270,7 @@ y = x / 100 is_space_before: false, }; let function_block = AyanoBlock { + ident: StaticDebug(rand::thread_rng().gen()), code: $function_code.into(), insert_path: None, display_state: DisplayState::NotDisplayed, @@ -272,6 +308,7 @@ y", "2"} // arrange let mut builder = AyanoBuilder::new(); let static_block = AyanoBlock { + ident: StaticDebug(rand::thread_rng().gen()), code: $static_code.into(), insert_path: None, display_state: DisplayState::NotDisplayed, @@ -279,6 +316,7 @@ y", "2"} is_space_before: false, }; let function1_block = AyanoBlock { + ident: StaticDebug(rand::thread_rng().gen()), code: $function1_code.into(), insert_path: None, display_state: DisplayState::NotDisplayed, @@ -286,6 +324,7 @@ y", "2"} is_space_before: false, }; let function2_block = AyanoBlock { + ident: StaticDebug(rand::thread_rng().gen()), code: $function2_code.into(), insert_path: None, display_state: DisplayState::NotDisplayed, @@ -316,18 +355,12 @@ y", "2"} // TODO add function for display block tests } -fn function_name(ident: u64) -> String { - format!("f_{ident}") +fn function_name(ident: &impl std::ops::Deref) -> String { + format!("f_{}", **ident) } -fn block_name(ident: u64) -> String { - format!("ayano_block__{ident}") -} - -fn calculate_hash(t: &T) -> u64 { - let mut s = DefaultHasher::new(); - t.hash(&mut s); - s.finish() +fn block_name(ident: &impl std::ops::Deref) -> String { + format!("ayano_block__{}", **ident) } #[derive( @@ -371,7 +404,6 @@ impl CsvTableColumn<'_> { fn apply_ayano<'token, 'source: 'token>( block: &'token AyanoBlock<'source>, ) -> Result<(String, CodelessBlockInfo), SyntaxError> { - let ident = calculate_hash(block); // FIRST OF ALL, // read the code from insert-file (if present) let insert_path = block.insert_path.as_ref().map(|path| path.to_path_buf()); @@ -406,7 +438,7 @@ fn apply_ayano<'token, 'source: 'token>( is_newline: true, space_before: false, formatting: None, - content: block_name(ident).into(), + content: block_name(&block.ident).into(), }), crate::data::DisplayState::Caption(tokens) => Some(tokens.to_static_token()), }; @@ -417,7 +449,7 @@ fn apply_ayano<'token, 'source: 'token>( return Ok(( res, CodelessBlockInfo { - ident, + ident: *block.ident, function_name: None, caption: display_name, insert_path, @@ -425,7 +457,7 @@ fn apply_ayano<'token, 'source: 'token>( )); } // block is a function-block at this point - let function_name = Some(function_name(ident)); + let function_name = Some(function_name(&block.ident)); // take out last line, as it may contain ayano syntax let Some(last_line) = code_lines.next_back() else { @@ -435,7 +467,7 @@ fn apply_ayano<'token, 'source: 'token>( return Ok(( res, CodelessBlockInfo { - ident, + ident: *block.ident, function_name, caption: display_name, insert_path, @@ -460,7 +492,7 @@ fn apply_ayano<'token, 'source: 'token>( Ok(( res, CodelessBlockInfo { - ident, + ident: *block.ident, function_name, insert_path, caption: display_name, @@ -487,6 +519,7 @@ mod apply_ayano_tests { fn $name() { // arrange let block = AyanoBlock { + ident: StaticDebug(rand::thread_rng().gen()), code: $input.into(), insert_path: None, display_state: DisplayState::NotDisplayed, @@ -586,6 +619,7 @@ y = x + 2 let code = "@dev: x,y"; let block = AyanoBlock { + ident: StaticDebug(rand::thread_rng().gen()), display_state: DisplayState::NotDisplayed, code: code.into(), insert_path: Some(insert_path.into()), @@ -1507,7 +1541,7 @@ fn parse_ayano( None } else { let width_str = throw_syntax!(width.str().and_then(|s| s.to_str()))?; - Some(HashIgnored(width_str.parse()?)) + Some(width_str.parse()?) }; return Ok(Token::Figure { src_name: Cow::Owned(PathBuf::from(src)), @@ -1663,7 +1697,7 @@ mod parsing_tests { src_name: Cow::Owned(PathBuf::from("path/to/file.png")), caption: Some(Box::new(Token::text("figure caption"))), ident: Some("circle".into()), - width: Some(HashIgnored(0.625)), + width: Some(0.625), })} test! {table_nocaption, |py| { diff --git a/src/gen/lab/mod.rs b/src/gen/lab/mod.rs index c353e79..b304cbb 100644 --- a/src/gen/lab/mod.rs +++ b/src/gen/lab/mod.rs @@ -487,9 +487,10 @@ where mod tests { use std::path::Path; + use rand::Rng; + use crate::{ - data::{AyanoBlock, DisplayState, Formatting, ListType}, - util::HashIgnored, + data::{AyanoBlock, DisplayState, Formatting, ListType}, util::StaticDebug, }; use super::*; @@ -610,7 +611,7 @@ mod tests { \label{fig:schema-1} \end{figure} "} - test! {fig3_width, Token::Figure { src_name: Path::new("schema1.jpg").into(), caption: Some(Box::new(text!("A schema providing a bunch of very important info on quantum refurbalidzer function"))), ident: Some("schema-1".into()), width: Some(HashIgnored(0.25)) }, r" + test! {fig3_width, Token::Figure { src_name: Path::new("schema1.jpg").into(), caption: Some(Box::new(text!("A schema providing a bunch of very important info on quantum refurbalidzer function"))), ident: Some("schema-1".into()), width: Some(0.25) }, r" \begin{figure}[h!] \centering \includegraphics[width = 0.25 \textwidth]{schema1.jpg} @@ -760,6 +761,7 @@ cell_21 & cell_22 & cell_23 \\ \hline "} test! {ayano_plain1, Token::Ayano{data: AyanoBlock{ + ident: StaticDebug(rand::thread_rng().gen()), display_state: DisplayState::NotDisplayed, is_static: false, code: "1".into(), @@ -767,6 +769,7 @@ cell_21 & cell_22 & cell_23 \\ \hline is_space_before: false, }}, r"1"} test! {ayano_plain2, Token::Ayano{data: AyanoBlock{ + ident: StaticDebug(rand::thread_rng().gen()), display_state: DisplayState::NotDisplayed, is_static: false, code: "1 + 2".into(), @@ -774,6 +777,7 @@ cell_21 & cell_22 & cell_23 \\ \hline is_space_before: false, }}, r"3"} test! {ayano_plain3, Token::Ayano{data: AyanoBlock{ + ident: StaticDebug(rand::thread_rng().gen()), display_state: DisplayState::NotDisplayed, is_static: false, code: r" @@ -786,6 +790,7 @@ int(y)" is_space_before: false, }}, r"10"} test! {ayano_plain4, Token::Ayano{data: AyanoBlock{ + ident: StaticDebug(rand::thread_rng().gen()), display_state: DisplayState::NotDisplayed, is_static: false, code: r" @@ -798,6 +803,7 @@ res" is_space_before: false, }}, r"5050"} test! {ayano_err1, Token::Ayano{data: AyanoBlock{ + ident: StaticDebug(rand::thread_rng().gen()), display_state: DisplayState::NotDisplayed, is_static: false, code: r" @@ -807,6 +813,7 @@ res" is_space_before: false, }}, r"1.00 $\pm$ 0.10"} test! {ayano_err2, Token::Ayano{data: AyanoBlock{ + ident: StaticDebug(rand::thread_rng().gen()), display_state: DisplayState::NotDisplayed, is_static: false, code: r" @@ -816,6 +823,7 @@ res" is_space_before: false, }}, r"1.0 $\pm$ 0.4"} test! {ayano_err3, Token::Ayano{data: AyanoBlock{ + ident: StaticDebug(rand::thread_rng().gen()), display_state: DisplayState::NotDisplayed, is_static: false, code: r" @@ -829,6 +837,7 @@ y = 0.4 } test! {ayano_fig1, Token::Ayano{data: AyanoBlock{ + ident: StaticDebug(rand::thread_rng().gen()), display_state: DisplayState::NotDisplayed, is_static: false, code: r"'fig', 'path/to/image.jpg', None, None, None".into(), @@ -842,6 +851,7 @@ y = 0.4 } test! {ayano_fig2_width, Token::Ayano{data: AyanoBlock{ + ident: StaticDebug(rand::thread_rng().gen()), display_state: DisplayState::NotDisplayed, is_static: false, code: r"'fig', 'path/to/image.jpg', None, None, 0.625".into(), @@ -854,13 +864,15 @@ y = 0.4 \end{figure}" } - test! {ayano_fig2, Token::Ayano{data: AyanoBlock{display_state: DisplayState::NotDisplayed,is_static: false,code: r"@fig: src = 'path/to/image.jpg'".into(),insert_path: None,is_space_before: false,}}, + test! {ayano_fig2, Token::Ayano{data: AyanoBlock{ + ident: StaticDebug(rand::thread_rng().gen()),display_state: DisplayState::NotDisplayed,is_static: false,code: r"@fig: src = 'path/to/image.jpg'".into(),insert_path: None,is_space_before: false,}}, r" \begin{figure}[h!] \centering \includegraphics[width = 0.9 \textwidth]{path/to/image.jpg} \end{figure}"} - test! {ayano_fig3, Token::Ayano{data: AyanoBlock{ display_state: DisplayState::NotDisplayed,is_static: false,code: r#"@fig: src = 'path/to/image.jpg', ident = "meow""#.into(),insert_path: None, + test! {ayano_fig3, Token::Ayano{data: AyanoBlock{ + ident: StaticDebug(rand::thread_rng().gen()), display_state: DisplayState::NotDisplayed,is_static: false,code: r#"@fig: src = 'path/to/image.jpg', ident = "meow""#.into(),insert_path: None, is_space_before: false, }}, r#" @@ -872,7 +884,8 @@ r#" } // TODO add tests with captions - test! {ayano_gen_tab1, Token::Ayano{data: AyanoBlock { display_state: DisplayState::NotDisplayed, is_static: false, code: + test! {ayano_gen_tab1, Token::Ayano{data: AyanoBlock { + ident: StaticDebug(rand::thread_rng().gen()),display_state: DisplayState::NotDisplayed, is_static: false, code: r#" data = [[1, 2, 3], [4, 5, 6]] @gen_table: lambda r,c: data[r][c]; rows=2, columns=3 diff --git a/src/lexer.rs b/src/lexer.rs index ce85132..8ab8729 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -18,12 +18,13 @@ use nom::{ sequence::{delimited, pair, preceded, terminated, tuple}, IResult, Parser, }; +use rand::Rng; use url::Url; use crate::{ data::{AyanoBlock, DisplayState, Formatting, ListType, TitleInfo, Token, Tokens}, util::{ - optional_permutation, Equivalent, HashIgnored, IteratorArrayCollectExt, VecTryExtendExt, + optional_permutation, Equivalent, IteratorArrayCollectExt, StaticDebug, VecTryExtendExt }, }; @@ -437,7 +438,7 @@ fn figure< src_name: Path::new(path).into(), caption: caption.map(Box::new), ident: ident.map(Cow::from), - width: width.map(HashIgnored), + width, }; Ok((rest, token)) } @@ -621,6 +622,7 @@ fn ayano< rest, Token::Ayano { data: AyanoBlock { + ident: StaticDebug(rand::thread_rng().gen()), insert_path: insert.map(Cow::from), code, display_state, @@ -2035,7 +2037,7 @@ $$ src_name: Cow::Borrowed(Path::new($path)), ident: $ident.map(Cow::from), caption: $caption.map(Box::new), - width: $width.map(HashIgnored), + width: $width, } }; } @@ -2175,6 +2177,7 @@ $$ macro_rules! ayano { {!$is_static:literal, *$description:expr, ~$insert_path:expr, #$_id:expr, $code:literal} => { Token::Ayano{data: AyanoBlock{ + ident: StaticDebug(rand::thread_rng().gen()), display_state: $description, is_static: $is_static, code: Cow::Borrowed($code), diff --git a/src/util.rs b/src/util.rs index 5c705a1..fcdfbd9 100644 --- a/src/util.rs +++ b/src/util.rs @@ -452,13 +452,6 @@ pub fn optional_permutation<'parsers, 'source: 'parsers, In, Out, E: ParseError< move |input| parsers.optional_permutation(input) } -// FIXME this is SURELY not idiomatic. I should move to ident system -#[derive(Debug, derive_more::Deref, derive_more::DerefMut, PartialEq, Clone)] -pub struct HashIgnored(pub T); -impl std::hash::Hash for HashIgnored { - fn hash(&self, _state: &mut H) {} -} - #[derive(Debug)] pub struct FmtToIo(W, VecDeque); impl FmtToIo { @@ -486,3 +479,12 @@ impl std::fmt::Write for FmtToIo { Ok(()) } } + +#[derive(derive_more::Deref, derive_more::DerefMut, Clone)] +pub struct StaticDebug(pub T); + +impl std::fmt::Debug for StaticDebug { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("StaticDebug").field(&R).finish() + } +} \ No newline at end of file diff --git a/tests/lex_files/ayano.txt b/tests/lex_files/ayano.txt index 9f2e458..1e39f77 100644 --- a/tests/lex_files/ayano.txt +++ b/tests/lex_files/ayano.txt @@ -30,6 +30,9 @@ Multiple { }, Ayano { data: AyanoBlock { + ident: StaticDebug( + 0, + ), display_state: NotDisplayed, is_space_before: false, is_static: false, @@ -45,6 +48,9 @@ Multiple { }, Ayano { data: AyanoBlock { + ident: StaticDebug( + 0, + ), display_state: NotDisplayed, is_space_before: false, is_static: false, @@ -60,6 +66,9 @@ Multiple { }, Ayano { data: AyanoBlock { + ident: StaticDebug( + 0, + ), display_state: NotDisplayed, is_space_before: false, is_static: false, diff --git a/tests/lex_files/ayano_display_blocks.txt b/tests/lex_files/ayano_display_blocks.txt index 5f8e364..a09c33e 100644 --- a/tests/lex_files/ayano_display_blocks.txt +++ b/tests/lex_files/ayano_display_blocks.txt @@ -36,6 +36,9 @@ Multiple { }, Ayano { data: AyanoBlock { + ident: StaticDebug( + 0, + ), display_state: DisplayedNoCaption, is_space_before: false, is_static: false, @@ -51,6 +54,9 @@ Multiple { }, Ayano { data: AyanoBlock { + ident: StaticDebug( + 0, + ), display_state: Caption( Paragraph { is_newline: false, diff --git a/tests/lex_files/basic.txt b/tests/lex_files/basic.txt index 374f165..cbea70d 100644 --- a/tests/lex_files/basic.txt +++ b/tests/lex_files/basic.txt @@ -101,9 +101,7 @@ Multiple { "refurb", ), width: Some( - HashIgnored( - 0.4, - ), + 0.4, ), }, Paragraph { diff --git a/tests/lex_files/deceit.txt b/tests/lex_files/deceit.txt index 9516ba9..5c20bbf 100644 --- a/tests/lex_files/deceit.txt +++ b/tests/lex_files/deceit.txt @@ -805,6 +805,9 @@ Multiple { }, Ayano { data: AyanoBlock { + ident: StaticDebug( + 0, + ), display_state: NotDisplayed, is_space_before: false, is_static: false, From a00371ad93e2e8d6831778179e701c7652af27ce Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Wed, 27 Mar 2024 11:00:07 +0200 Subject: [PATCH 50/98] My bad; that should actually fix it --- src/gen/ayano.rs | 26 ++----------------- .../ayano_display_blocks.txt | 14 +++++----- 2 files changed, 9 insertions(+), 31 deletions(-) diff --git a/src/gen/ayano.rs b/src/gen/ayano.rs index c91eb10..548765a 100644 --- a/src/gen/ayano.rs +++ b/src/gen/ayano.rs @@ -60,32 +60,12 @@ impl<'source> PartialEq> for BlockInfo { } } -impl<'source> PartialOrd> for BlockInfo { - fn partial_cmp(&self, other: &AyanoBlock<'source>) -> Option { - self.ident.partial_cmp(&other.ident) - } -} - impl PartialEq for BlockInfo { fn eq(&self, other: &Self) -> bool { self.ident == other.ident } } -impl Eq for BlockInfo {} - -impl PartialOrd for BlockInfo { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for BlockInfo { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.ident.cmp(&other.ident) - } -} - #[derive(Debug)] pub struct AyanoBuilder { code: String, @@ -131,7 +111,6 @@ impl AyanoBuilder { pub fn initialize(self) -> Result { let Self { code, mut blocks } = self; let token = init(&code.as_str())?; - blocks.sort_unstable(); Ok(AyanoExecutor { token, blocks: blocks.into(), @@ -175,14 +154,13 @@ impl AyanoExecutor { where for<'py> Transform: FnOnce(&'py PyAny, Python<'py>) -> AyanoResult<'source, Out>, { - let Ok(info_ind) = self + let Some(info) = self .blocks - .binary_search_by_key(&*token.ident, |bi| bi.ident) + .into_iter().find(|bi| bi.ident == *token.ident) else { // this block was not passed here before return Err(AyanoError::NoFunction(token.clone())); }; - let info = &self.blocks[info_ind]; let Some(function_name) = info.function_name.as_ref() else { // this block was passed here before, but it's not a function return Err(AyanoError::NoFunction(token.clone())); diff --git a/tests/transformed_files/ayano_display_blocks.txt b/tests/transformed_files/ayano_display_blocks.txt index 8573dfb..631fa80 100644 --- a/tests/transformed_files/ayano_display_blocks.txt +++ b/tests/transformed_files/ayano_display_blocks.txt @@ -137,6 +137,13 @@ \tbln Display blocks can have descriptions too! Here's one:-20340$\pm$150 \subsection{Код} +\begin{minted}[linenos, mathescape, autogobble, breaklines]{python} +# You should be able to see this comment in the output doc +return 42 + +\end{minted} + +\tb ayano_block__3649291653820225953 \begin{minted}[linenos, mathescape, autogobble, breaklines]{python} # This code block has description # Might as well add some python syntax to check out minted's syntax highlighting: @@ -148,12 +155,5 @@ return "err", -2.034e4, 145.64 \end{minted} me, fooling around -\begin{minted}[linenos, mathescape, autogobble, breaklines]{python} -# You should be able to see this comment in the output doc -return 42 - -\end{minted} - -\tb ayano_block__1574475637377647406 \end{document} \ No newline at end of file From 2b41880c8cf24e296c029875f7827716cdafdaf7 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Wed, 27 Mar 2024 11:06:35 +0200 Subject: [PATCH 51/98] will this fix it? --- src/gen/ayano.rs | 7 ++----- tests/transformed_files/ayano_display_blocks.txt | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/gen/ayano.rs b/src/gen/ayano.rs index 548765a..6fdec95 100644 --- a/src/gen/ayano.rs +++ b/src/gen/ayano.rs @@ -154,10 +154,7 @@ impl AyanoExecutor { where for<'py> Transform: FnOnce(&'py PyAny, Python<'py>) -> AyanoResult<'source, Out>, { - let Some(info) = self - .blocks - .into_iter().find(|bi| bi.ident == *token.ident) - else { + let Some(info) = self.blocks.into_iter().find(|bi| bi.ident == *token.ident) else { // this block was not passed here before return Err(AyanoError::NoFunction(token.clone())); }; @@ -416,7 +413,7 @@ fn apply_ayano<'token, 'source: 'token>( is_newline: true, space_before: false, formatting: None, - content: block_name(&block.ident).into(), + content: "ayano-unnamed".into(), }), crate::data::DisplayState::Caption(tokens) => Some(tokens.to_static_token()), }; diff --git a/tests/transformed_files/ayano_display_blocks.txt b/tests/transformed_files/ayano_display_blocks.txt index 631fa80..e712a57 100644 --- a/tests/transformed_files/ayano_display_blocks.txt +++ b/tests/transformed_files/ayano_display_blocks.txt @@ -143,7 +143,7 @@ return 42 \end{minted} -\tb ayano_block__3649291653820225953 +\tb ayano-unnamed \begin{minted}[linenos, mathescape, autogobble, breaklines]{python} # This code block has description # Might as well add some python syntax to check out minted's syntax highlighting: From a7cf8ca86d72bd8c4ee0095d3be20f8d32614d3b Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Wed, 27 Mar 2024 16:47:10 +0200 Subject: [PATCH 52/98] Added an example for basic Ayano syntax --- Cargo.lock | 8 + Cargo.toml | 2 + examples-util/Cargo.toml | 9 + examples-util/src/lib.rs | 19 ++ examples/basic_ayano/input.md | 56 +++++ examples/basic_ayano/main.rs | 23 ++ examples/basic_ayano/output.tex | 219 ++++++++++++++++++ examples/basic_syntax/main.rs | 30 +-- src/data.rs | 8 +- src/gen/ayano.rs | 33 ++- src/gen/lab/mod.rs | 53 +++-- src/lexer.rs | 59 ++--- tests/lex_files/ayano.txt | 6 +- tests/lex_files/ayano_display_blocks.txt | 4 +- tests/lex_files/deceit.txt | 2 +- .../ayano_display_blocks.txt | 14 +- 16 files changed, 435 insertions(+), 110 deletions(-) create mode 100644 examples-util/Cargo.toml create mode 100644 examples-util/src/lib.rs create mode 100644 examples/basic_ayano/input.md create mode 100644 examples/basic_ayano/main.rs create mode 100644 examples/basic_ayano/output.tex diff --git a/Cargo.lock b/Cargo.lock index 8c51461..6c1cae3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -140,6 +140,13 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[package]] +name = "examples-util" +version = "0.1.0" +dependencies = [ + "kyomato", +] + [[package]] name = "fnv" version = "1.0.7" @@ -258,6 +265,7 @@ version = "0.1.0" dependencies = [ "cached", "derive_more", + "examples-util", "itertools", "nom", "once_cell", diff --git a/Cargo.toml b/Cargo.toml index 777749d..1388ae5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,4 @@ +workspace = { members = ["examples-util"] } [package] name = "kyomato" version = "0.1.0" @@ -27,3 +28,4 @@ smart-default = "0.7.1" [dev-dependencies] text-diff = "0.4.0" nom = { version = "7.1.3", features = ["alloc"] } +examples-util = { path = "examples-util" } diff --git a/examples-util/Cargo.toml b/examples-util/Cargo.toml new file mode 100644 index 0000000..6d9b640 --- /dev/null +++ b/examples-util/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "examples-util" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +kyomato = {path = ".."} \ No newline at end of file diff --git a/examples-util/src/lib.rs b/examples-util/src/lib.rs new file mode 100644 index 0000000..faf81e5 --- /dev/null +++ b/examples-util/src/lib.rs @@ -0,0 +1,19 @@ +use std::path::Path; + +pub fn setup_io( + input_path: impl AsRef, + output_path: impl AsRef, +) -> Result<(String, impl std::fmt::Write + std::fmt::Debug), std::io::Error> { + let input = std::fs::read_to_string(input_path.as_ref())?; + // you must have a complete input to process it properly + + // open the output file + let output = std::fs::OpenOptions::new() + .read(true) + .write(true) + .truncate(true) + .create(true) + .open(output_path.as_ref())?; + let output = kyomato::FmtToIo::new(output); + Ok((input, output)) +} diff --git a/examples/basic_ayano/input.md b/examples/basic_ayano/input.md new file mode 100644 index 0000000..8f73a72 --- /dev/null +++ b/examples/basic_ayano/input.md @@ -0,0 +1,56 @@ +Here's a basic Ayano explanation: + +Essentially, Ayano blocks are regular code blocks, having `python, Ayano` marked as their language. This sort of codeblocks are treated completely differently by the parser - these blocks are supposed to allow arbitrary python code execution for data generation and analysis. + +For this example, we'll only consider examples that do not interact with the filesystem. + +Say, you have some sort of dataset, you wish to find a mean a deviation of. Here's a way to do that with Ayano (without filesystem interaction): +```python, Ayano ! * "That's a static block, declaring initial dataset" +# first, you declare the dataset itself in a *static* Ayano block +# (note the exclamation mark above) +data = [1, 1, 2, 3, 2, 5, 5, 6, 2, 1, 4, 9] +# Static block essentially translated into a code initially executed by python, when the entire module it loaded. +# Keep that in mind: ALL changes introduced by static blocks will be visible by ALL function blocks. +``` +You might notice, that there's actually nothing displayed above in the document. That's due to *static blocks* leaving nothing behind them. To actually display the dataset, we would use a regular, non-static Ayano block: + + +```python, Ayano +data +``` +Above you should see a python's `str` representation of initial dataset. + +Now, let's try calculating mean and the deviation. To achieve that, I'll use `statistics` module: + +```python, Ayano * "mean and deviation calculations" +from statistics import stdev, mean +mean_val = mean(data) +deviation = stdev(data) +@dev: mean_val, deviation +``` + +You should see dataset's mean value and deviation above. You might notice, that's it's in a good-looking format. That's because of special format used for value-deviation sort-of-values, that's described in the docs. If you wish to circumvent it, you absolutely can: + +```python, Ayano * "mean and deviation calculations, but without value-error formatting" +from statistics import stdev, mean +mean_val = mean(data) +deviation = stdev(data) +f"{mean_val} $\pm$ {deviation}" +``` + +Ayano can generate tables too! For example, let's generate a table of our dataset, but also add information on if it's greater or less than the mean value, it's square and absolute deviation from the mean: +```python, Ayano * "that chaotic table generation example" +# look at the last line first +# that's a table generation syntax - it involves a cell generator, number of rows, number of columns and optional ident and caption arguments +# you could use python's function as generator too (a general requirement is for the thing to be callable with two integer args) +from statistics import mean +from math import sqrt +header = ["initial data", "cmp to mean", "square", "absdev"] +mean_val = mean(data) +cells = list([[("err", val, sqrt(val)), "greater" if val >= mean_val else "less", val * val, abs(val - mean_val)] for val in data]) +@gen_table: lambda r,c: header[c] if r == 0 else cells[r-1][c]; rows = 13, columns = 4, ident="crazy_table", caption = "A crazy table containing a bunch of stuff" +``` + +Above you can see that crazy table, and you also can refer to it ([@tab:crazy_table]). + +That's pretty much it for Ayano blocks that do not access the filesystem. \ No newline at end of file diff --git a/examples/basic_ayano/main.rs b/examples/basic_ayano/main.rs new file mode 100644 index 0000000..7324a6b --- /dev/null +++ b/examples/basic_ayano/main.rs @@ -0,0 +1,23 @@ +fn main() { + //! This is ayano-involving example of programmatic output generation + //! + //! You might want to use CLI interface instead + + // that's a utility setup call; essentially, it reads input from one file, and opens output file for writing + let (input, mut output) = examples_util::setup_io( + "examples/basic_ayano/input.md", + "examples/basic_ayano/output.tex", + ).unwrap(); + + // construct path engine + // in this example, no external files are accessed (i.e. there are no images or special Ayano syntax) + // so it's ok to pass in a blank primitive engine + let path_engine = kyomato::path_engine::primitive_dbg(); + // _dbg stands for "informative error", just in case + + // parse input and write to output + kyomato::LabaLatex::parse_and_write(path_engine, &input, &mut output) + // this part is just for debug purposes + .map_err(|err| (err, output)) + .unwrap() +} diff --git a/examples/basic_ayano/output.tex b/examples/basic_ayano/output.tex new file mode 100644 index 0000000..811cdf1 --- /dev/null +++ b/examples/basic_ayano/output.tex @@ -0,0 +1,219 @@ +\documentclass[a4paper]{article} +\usepackage{graphicx, hyperref, xcolor, gensymb, amssymb, mathtools, wrapfig, mathtools, microtype, lastpage, caption, titlesec, paracol, longtable, booktabs, cancel} +\usepackage[T2A, T1]{fontenc} +\usepackage[utf8]{inputenc} +\usepackage[russian, ukrainian]{babel} +\hypersetup{ + colorlinks, + linkcolor={blue!20!black}, + citecolor={blue!50!black}, + urlcolor={blue!80!black} +} + +\usepackage[left = 25mm, right = 10mm, top=20mm, bottom=20mm, bindingoffset=0cm]{geometry} + +% КОМАНДИ + +\newcommand{\fnt}{ +\fontshape{n} +\fontsize{14pt} {19pt} +\linespread{0.8} +\selectfont +} % нормативний шрифт +\newcommand{\capfnt}{ + \fnt +} % норматичний шрифт для підписів (тимчасово співпадає зі звичайним шрифтом) + +\newcommand{\tb}{ + \hspace*{10mm} +} % відступ у 5 символів "x" відповідно до ДСТУ +\newcommand{\tbln}{ + \newline + \tb +} % те ж саме, але з переносом рядка +\newcommand{\tbsp}{ + \vspace*{1ex} + \tbln +} % те ж саме, але з додатковим відступом між рядками + +% бібліографія +\usepackage[square,sort,comma,numbers]{natbib} +\renewcommand{\bibsection}{} +\usepackage{totcount} +\newtotcounter{citnum} +% лічильник цитувань +\def\oldbibitem{} \let\oldbibitem=\bibitem +\def\bibitem{\stepcounter{citnum}\oldbibitem} + +% лічильник зображень +\newtotcounter{graphnum} +\def\oldincludegraphics{} \let\oldincludegraphics=\includegraphics +\def\includegraphics{\stepcounter{graphnum}\oldincludegraphics} + +% лічильник таблиць (НЕАВТОМАТИЧНИЙ!!!) +\newtotcounter{tabnum} + +% лічильник додатків (НЕАВТОМАТИЧНИЙ!!!) +\newtotcounter{dodnum} + +% нумерація сторінок +\usepackage{fancyhdr} +\pagestyle{fancy} +\fancyhf{} +\renewcommand{\headrulewidth}{0pt} +\setlength{\headheight}{15.3pt} +\fancyhead[R]{\fnt \thepage} + +% виноски +\renewcommand{\thefootnote}{\large\arabic{footnote})~} +\renewcommand{\footnoterule}{\rule{20mm}{0.4pt} \vspace*{0.5ex}} +% \newcommand{\vyn}[2]{ +% \footnote[#1]{\large #2} +% } % створити виноску + +\let\oldFootnote\footnote +\renewcommand{\footnote}[1]{ + \oldFootnote{\large #1} +} % перегрузка виноски + +\providecommand{\tightlist}{% + \setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}} + +% підписи малюнків/таблиць +\captionsetup[figure]{name={\fnt Рисунок~},labelsep=period} +\captionsetup[table]{name={\fnt Таблиця~},labelsep=period} + +% markdown strikeouts +\usepackage{soul} + +% codeblocks +\usepackage{minted} + +% enumeration +\usepackage[shortlabels]{enumitem} + +\titleformat*{\section}{\fnt\LARGE\bfseries} +\titleformat*{\subsection}{\fnt\Large\bfseries} +\titleformat*{\subsubsection}{\fnt\Large} + +\begin{document} +\thispagestyle{empty} % [A]:for cropping +% ------------ +% source: https://tex.stackexchange.com/questions/173651/need-help-to-create-such-a-beautiful-title-page +\begingroup +\centering +\obeylines +\topskip 0pt % source: https://tex.stackexchange.com/questions/2326/vertically-center-text-on-a-page +\vspace*{\fill} +\textbf{ +\LARGE +\LARGE +\vspace{100pt} +\LARGE +\Large +\Large +\Large +\huge +\vspace{50pt} +\large +\large +\large +\large +\vspace*{\fill} +\hspace{\fill}\large \rule{20mm}{0.4pt} +\vspace{10pt} +} +\endgroup +% ------------ +\newpage +\tableofcontents + +\newpage +\fnt +Here's a basic Ayano explanation: +\tbln Essentially, Ayano blocks are regular code blocks, having `python, Ayano` marked as their language. This sort of codeblocks are treated completely differently by the parser - these blocks are supposed to allow arbitrary python code execution for data generation and analysis. +\tbln For this example, we'll only consider examples that do not interact with the filesystem. +\tbln Say, you have some sort of dataset, you wish to find a mean a deviation of. Here's a way to do that with Ayano (without filesystem interaction): +\tbln You might notice, that there's actually nothing displayed above in the document. That's due to \textit{static blocks} leaving nothing behind them. To actually display the dataset, we would use a regular, non-static Ayano block: \newline +[1, 1, 2, 3, 2, 5, 5, 6, 2, 1, 4, 9] +\tbln Above you should see a python's `str` representation of initial dataset. +\tbln Now, let's try calculating mean and the deviation. To achieve that, I'll use `statistics` module: 3.4$\pm$2.5 +\tbln You should see dataset's mean value and deviation above. You might notice, that's it's in a good-looking format. That's because of special format used for value-deviation sort-of-values, that's described in the docs. If you wish to circumvent it, you absolutely can: 3.4166666666666665 $\pm$ 2.4664414311581235 +\tbln Ayano can generate tables too! For example, let's generate a table of our dataset, but also add information on if it's greater or less than the mean value, it's square and absolute deviation from the mean: +\begin{table}[h!] +\fnt +\begin{center} +\begin{tabular}{|c|c|c|c|}\hline +initial data & cmp to mean & square & absdev\\ \hline +\hline +1.0$\pm$1.0 & less & 1 & 2.4166666666666665\\ \hline +1.0$\pm$1.0 & less & 1 & 2.4166666666666665\\ \hline +2.0$\pm$1.4 & less & 4 & 1.4166666666666665\\ \hline +3.0$\pm$1.7 & less & 9 & 0.4166666666666665\\ \hline +2.0$\pm$1.4 & less & 4 & 1.4166666666666665\\ \hline +5.0$\pm$2.2 & greater & 25 & 1.5833333333333335\\ \hline +5.0$\pm$2.2 & greater & 25 & 1.5833333333333335\\ \hline +6.0$\pm$2.4 & greater & 36 & 2.5833333333333335\\ \hline +2.0$\pm$1.4 & less & 4 & 1.4166666666666665\\ \hline +1.0$\pm$1.0 & less & 1 & 2.4166666666666665\\ \hline +4.0$\pm$2.0 & greater & 16 & 0.5833333333333335\\ \hline +9$\pm$3 & greater & 81 & 5.583333333333334\\ \hline +\end{tabular} +\stepcounter{tabnum} +\caption{\capfnt A crazy table containing a bunch of stuff} +\label{tab:crazy_table} +\end{center} +\end{table} + +\tbln Above you can see that crazy table, and you also can refer to it (\ref{tab:crazy_table}). +\tbln That's pretty much it for Ayano blocks that do not access the filesystem. +\subsection{Code blocks} + +\begin{minted}[linenos, mathescape, autogobble, breaklines]{python} +# first, you declare the dataset itself in a *static* Ayano block +# (note the exclamation mark above) +data = [1, 1, 2, 3, 2, 5, 5, 6, 2, 1, 4, 9] +# Static block essentially translated into a code initially executed by python, when the entire module it loaded. +# Keep that in mind: ALL changes introduced by static blocks will be visible by ALL function blocks. +\end{minted} + +That's a static block, declaring initial dataset +\newline + +\begin{minted}[linenos, mathescape, autogobble, breaklines]{python} +from statistics import stdev, mean +mean_val = mean(data) +deviation = stdev(data) +@dev: mean_val, deviation +\end{minted} + +mean and deviation calculations +\newline + +\begin{minted}[linenos, mathescape, autogobble, breaklines]{python} +from statistics import stdev, mean +mean_val = mean(data) +deviation = stdev(data) +f"{mean_val} $\pm$ {deviation}" +\end{minted} + +mean and deviation calculations, but without value-error formatting +\newline + +\begin{minted}[linenos, mathescape, autogobble, breaklines]{python} +# look at the last line first +# that's a table generation syntax - it involves a cell generator, number of rows, number of columns and optional ident and caption arguments +# you could use python's function as generator too (a general requirement is for the thing to be callable with two integer args) +from statistics import mean +from math import sqrt +header = ["initial data", "cmp to mean", "square", "absdev"] +mean_val = mean(data) +cells = list([[("err", val, sqrt(val)), "greater" if val >= mean_val else "less", val * val, abs(val - mean_val)] for val in data]) +@gen_table: lambda r,c: header[c] if r == 0 else cells[r-1][c]; rows = 13, columns = 4, ident="crazy_table", caption = "A crazy table containing a bunch of stuff" +\end{minted} + +that chaotic table generation example +\newline + + +\end{document} \ No newline at end of file diff --git a/examples/basic_syntax/main.rs b/examples/basic_syntax/main.rs index 1bc4ba9..c334c09 100644 --- a/examples/basic_syntax/main.rs +++ b/examples/basic_syntax/main.rs @@ -1,5 +1,3 @@ -use std::path::PathBuf; - use kyomato::path_engine::PathEngineExt; fn main() { @@ -7,33 +5,17 @@ fn main() { //! //! You might want to use CLI interface instead - // # Preparations - // not mandatory; you may specify paths by hand everywhere. - let mut workpath = PathBuf::from("examples/basic_syntax"); - - // read the input - workpath.push("input.md"); - let input = std::fs::read_to_string(&workpath).unwrap(); - // you must have a complete input to process it properly - - // open the output file - workpath.set_file_name("output.tex"); - let output = std::fs::OpenOptions::new() - .read(true) - .write(true) - .truncate(true) - .create(true) - .open(&workpath) - .unwrap(); - let mut output = kyomato::FmtToIo::new(output); + // that's a utility setup call; essentially, it reads input from one file, and opens output file for writing + let (input, mut output) = examples_util::setup_io( + "examples/basic_syntax/input.md", + "examples/basic_syntax/output.tex", + ).unwrap(); // construct path engine // this thing is responsible for file path resolution in the generated document - workpath.pop(); - workpath.push("images"); let path_engine = kyomato::path_engine::base_gen() // in our case, there's a dedicated folder for images, so we inform engine about that - .with_image_folder(&workpath) + .with_image_folder("examples/basic_syntax/images") // this sort of call is required - with it, you specify an error type the engine will return // it's a no-op; so you may specify engine's type instead or th .with_error_type::() diff --git a/src/data.rs b/src/data.rs index 7cb1e54..73bd129 100644 --- a/src/data.rs +++ b/src/data.rs @@ -93,7 +93,7 @@ impl<'code> DisplayState<'code> { pub struct AyanoBlock<'code> { pub ident: StaticDebug, pub display_state: DisplayState<'code>, - pub is_space_before: bool, + pub spaces_before: usize, pub is_static: bool, pub code: Tx<'code>, pub insert_path: Option>, @@ -102,7 +102,7 @@ pub struct AyanoBlock<'code> { impl PartialEq for AyanoBlock<'_> { fn eq(&self, other: &Self) -> bool { self.display_state == other.display_state - && self.is_space_before == other.is_space_before + && self.spaces_before == other.spaces_before && self.is_static == other.is_static && self.code == other.code && self.insert_path == other.insert_path @@ -279,7 +279,7 @@ impl ToStaticExt for AyanoBlock<'_> { insert_path, display_state, is_static, - is_space_before, + spaces_before, } = self; AyanoBlock { ident: ident.clone(), @@ -287,7 +287,7 @@ impl ToStaticExt for AyanoBlock<'_> { insert_path: insert_path.as_ref().map(Cow::to_static), is_static: *is_static, display_state: display_state.to_static(), - is_space_before: *is_space_before, + spaces_before: *spaces_before, } } } diff --git a/src/gen/ayano.rs b/src/gen/ayano.rs index 6fdec95..6eeda71 100644 --- a/src/gen/ayano.rs +++ b/src/gen/ayano.rs @@ -100,7 +100,7 @@ impl AyanoBuilder { ident, function_name: info.function_name, display_info: info.caption.map(|name| DisplayInfo { - code: transformed_code, + code: block.code.to_string(), caption: name, }), insert_path: info.insert_path, @@ -170,7 +170,7 @@ impl AyanoExecutor { &self, token: &'token AyanoBlock<'source>, ) -> AyanoResult<'source, Token<'static>> { - self.call_block(token, |res, _| Ok(parse_ayano(res, token.is_space_before)?)) + self.call_block(token, |res, _| Ok(parse_ayano(res)?)) } pub fn display_blocks<'s>(&'s self) -> impl IntoIterator { @@ -194,7 +194,7 @@ mod ayano_tests { is_static: false, code: $code.into(), insert_path: None, - is_space_before: false, + spaces_before: 0, }; let mut builder = AyanoBuilder::new(); @@ -242,7 +242,7 @@ y = x / 100 insert_path: None, display_state: DisplayState::NotDisplayed, is_static: true, - is_space_before: false, + spaces_before: 0, }; let function_block = AyanoBlock { ident: StaticDebug(rand::thread_rng().gen()), @@ -250,7 +250,7 @@ y = x / 100 insert_path: None, display_state: DisplayState::NotDisplayed, is_static: false, - is_space_before: false, + spaces_before: 0, }; builder.add_block(&static_block).expect("Should be able to add static block"); builder.add_block(&function_block).expect("Should be able to add function block"); @@ -288,7 +288,7 @@ y", "2"} insert_path: None, display_state: DisplayState::NotDisplayed, is_static: true, - is_space_before: false, + spaces_before: 0, }; let function1_block = AyanoBlock { ident: StaticDebug(rand::thread_rng().gen()), @@ -296,7 +296,7 @@ y", "2"} insert_path: None, display_state: DisplayState::NotDisplayed, is_static: false, - is_space_before: false, + spaces_before: 0, }; let function2_block = AyanoBlock { ident: StaticDebug(rand::thread_rng().gen()), @@ -304,7 +304,7 @@ y", "2"} insert_path: None, display_state: DisplayState::NotDisplayed, is_static: false, - is_space_before: false, + spaces_before: 0, }; builder.add_block(&static_block).expect("Should be able to add static block"); builder.add_block(&function1_block).expect("Should be able to add function block"); @@ -499,7 +499,7 @@ mod apply_ayano_tests { insert_path: None, display_state: DisplayState::NotDisplayed, is_static: false, - is_space_before: false, + spaces_before: 0, }; // act @@ -599,7 +599,7 @@ y = x + 2 code: code.into(), insert_path: Some(insert_path.into()), is_static: false, - is_space_before: false, + spaces_before: 0, }; // act @@ -1438,10 +1438,7 @@ impl From> for ParsingError { } } -fn parse_ayano( - python_output: &PyAny, - is_space_before: bool, -) -> Result, ParsingError> { +fn parse_ayano(python_output: &PyAny) -> Result, ParsingError> { // it was quite a journey! but I believe to be close to it's end. if let Ok(tuple) = python_output.downcast::() { if let Ok(discriminator) = tuple @@ -1538,13 +1535,13 @@ fn parse_ayano( .ok_or(ParsingError::TableNoHeader)? .downcast::())? .into_iter() - .map(|h: &PyAny| parse_ayano(h, false)) + .map(|h: &PyAny| parse_ayano(h)) .try_collect()?; let cells: Vec<_> = rows .map(|row| { Ok(throw_downcast!(row.downcast::())? .into_iter() - .map(|c: &PyAny| parse_ayano(c, false))) + .map(|c: &PyAny| parse_ayano(c))) }) .try_fold(Vec::new(), |acc, next: Result<_, ParsingError>| { // OLD: there might be a better way with iterators directly, but I kinda can't come up with it @@ -1583,7 +1580,7 @@ fn parse_ayano( Ok(Token::Paragraph { is_newline: false, // TODO probably add that to block too formatting: None, - space_before: is_space_before, + space_before: false, content: Cow::Owned(python_output.to_string()), }) } @@ -1623,7 +1620,7 @@ mod parsing_tests { let python_output = $input(py); // act - let result = super::parse_ayano(python_output, false); + let result = super::parse_ayano(python_output); // assert assert_eq!(result, $output, "Parsing should produce expected tokens"); diff --git a/src/gen/lab/mod.rs b/src/gen/lab/mod.rs index b304cbb..5b553d2 100644 --- a/src/gen/lab/mod.rs +++ b/src/gen/lab/mod.rs @@ -133,10 +133,7 @@ where let columns = header.len(); let column_format = itertools::intersperse(std::iter::repeat("c").take(columns), "|"); - write!( - output, - "\n\\begin{{table}}[h!]\n\\fnt\n\\begin{{center}}\n" - )?; + write!(output, "\n\\begin{{table}}[h!]\n\\fnt\n\\begin{{center}}\n")?; output.write_str("\\begin{tabular}{|")?; column_format .map(|s| { @@ -203,8 +200,15 @@ where output.write_str("\\end{figure}\n")?; } Token::Ayano { data } => { - let token = meta.ayano.display_token(data)?; - self.write_to(output, meta, context, &token)?; + if !data.is_static { + if data.spaces_before == 1 { + output.write_char(' ')?; + } else if data.spaces_before >= 2 { + output.write_str(" \\newline\n")?; + } + let token = meta.ayano.display_token(data)?; + self.write_to(output, meta, context, &token)?; + } } Token::List { list_type, content } => { let start: &str = match list_type { @@ -461,6 +465,7 @@ where }; generator.write_to(output, meta, context, &block_code_block)?; generator.write_to(output, meta, context, &block.caption)?; + output.write_str("\n\\newline\n")?; Ok(()) } let blocks_section_header = Token::Header { @@ -469,7 +474,7 @@ where .title_info .code_section_title .as_ref() - .unwrap_or(&"".into()) + .unwrap_or(&"Code blocks".into()) .clone(), }; self.write_to(output, meta, context, &blocks_section_header)?; @@ -490,7 +495,8 @@ mod tests { use rand::Rng; use crate::{ - data::{AyanoBlock, DisplayState, Formatting, ListType}, util::StaticDebug, + data::{AyanoBlock, DisplayState, Formatting, ListType}, + util::StaticDebug, }; use super::*; @@ -766,7 +772,7 @@ cell_21 & cell_22 & cell_23 \\ \hline is_static: false, code: "1".into(), insert_path: None, - is_space_before: false, + spaces_before: 0, }}, r"1"} test! {ayano_plain2, Token::Ayano{data: AyanoBlock{ ident: StaticDebug(rand::thread_rng().gen()), @@ -774,7 +780,7 @@ cell_21 & cell_22 & cell_23 \\ \hline is_static: false, code: "1 + 2".into(), insert_path: None, - is_space_before: false, + spaces_before: 0, }}, r"3"} test! {ayano_plain3, Token::Ayano{data: AyanoBlock{ ident: StaticDebug(rand::thread_rng().gen()), @@ -787,7 +793,7 @@ y = sqrt(x) int(y)" .into(), insert_path: None, - is_space_before: false, + spaces_before: 0, }}, r"10"} test! {ayano_plain4, Token::Ayano{data: AyanoBlock{ ident: StaticDebug(rand::thread_rng().gen()), @@ -800,7 +806,7 @@ for i in range(101): res" .into(), insert_path: None, - is_space_before: false, + spaces_before: 0, }}, r"5050"} test! {ayano_err1, Token::Ayano{data: AyanoBlock{ ident: StaticDebug(rand::thread_rng().gen()), @@ -810,7 +816,7 @@ res" 'err', 1.0, 0.1" .into(), insert_path: None, - is_space_before: false, + spaces_before: 0, }}, r"1.00 $\pm$ 0.10"} test! {ayano_err2, Token::Ayano{data: AyanoBlock{ ident: StaticDebug(rand::thread_rng().gen()), @@ -820,7 +826,7 @@ res" 'err', 1.0, 0.4" .into(), insert_path: None, - is_space_before: false, + spaces_before: 0, }}, r"1.0 $\pm$ 0.4"} test! {ayano_err3, Token::Ayano{data: AyanoBlock{ ident: StaticDebug(rand::thread_rng().gen()), @@ -832,7 +838,7 @@ y = 0.4 @dev: x, y" .into(), insert_path: None, - is_space_before: false, + spaces_before: 0, }}, r"1.0 $\pm$ 0.4" } @@ -842,7 +848,7 @@ y = 0.4 is_static: false, code: r"'fig', 'path/to/image.jpg', None, None, None".into(), insert_path: None, - is_space_before: false, + spaces_before: 0, }}, r" \begin{figure}[h!] \centering @@ -856,7 +862,7 @@ y = 0.4 is_static: false, code: r"'fig', 'path/to/image.jpg', None, None, 0.625".into(), insert_path: None, - is_space_before: false, + spaces_before: 0, }}, r" \begin{figure}[h!] \centering @@ -865,7 +871,8 @@ y = 0.4 } test! {ayano_fig2, Token::Ayano{data: AyanoBlock{ - ident: StaticDebug(rand::thread_rng().gen()),display_state: DisplayState::NotDisplayed,is_static: false,code: r"@fig: src = 'path/to/image.jpg'".into(),insert_path: None,is_space_before: false,}}, + ident: StaticDebug(rand::thread_rng().gen()),display_state: DisplayState::NotDisplayed,is_static: false,code: r"@fig: src = 'path/to/image.jpg'".into(),insert_path: None, + spaces_before: 0,}}, r" \begin{figure}[h!] \centering @@ -873,7 +880,7 @@ r" \end{figure}"} test! {ayano_fig3, Token::Ayano{data: AyanoBlock{ ident: StaticDebug(rand::thread_rng().gen()), display_state: DisplayState::NotDisplayed,is_static: false,code: r#"@fig: src = 'path/to/image.jpg', ident = "meow""#.into(),insert_path: None, - is_space_before: false, + spaces_before: 0, }}, r#" \begin{figure}[h!] @@ -884,13 +891,13 @@ r#" } // TODO add tests with captions - test! {ayano_gen_tab1, Token::Ayano{data: AyanoBlock { - ident: StaticDebug(rand::thread_rng().gen()),display_state: DisplayState::NotDisplayed, is_static: false, code: + test! {ayano_gen_tab1, Token::Ayano{data: AyanoBlock { + ident: StaticDebug(rand::thread_rng().gen()),display_state: DisplayState::NotDisplayed, is_static: false, code: r#" data = [[1, 2, 3], [4, 5, 6]] @gen_table: lambda r,c: data[r][c]; rows=2, columns=3 "#.into(), insert_path: None, - is_space_before: false, }}, + spaces_before: 0, }}, r#" \begin{table}[h!] \fnt @@ -903,7 +910,7 @@ r#" \end{center} \end{table} "# - } + } // TODO add tests with csv-tables // TODO add minted to preamble diff --git a/src/lexer.rs b/src/lexer.rs index 8ab8729..f404859 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -24,7 +24,7 @@ use url::Url; use crate::{ data::{AyanoBlock, DisplayState, Formatting, ListType, TitleInfo, Token, Tokens}, util::{ - optional_permutation, Equivalent, IteratorArrayCollectExt, StaticDebug, VecTryExtendExt + optional_permutation, Equivalent, IteratorArrayCollectExt, StaticDebug, VecTryExtendExt, }, }; @@ -618,6 +618,7 @@ fn ayano< let insert: Option<&'source Path> = insert.map(Path::new); // XXX add support for codeblock identifiers let code: Cow<'_, str> = Cow::from(code); + let num_of_newlines: usize = before.chars().filter(|c| c == &'\n').count(); Ok(( rest, Token::Ayano { @@ -627,7 +628,7 @@ fn ayano< code, display_state, is_static: is_static.is_some(), - is_space_before: before.contains(' '), + spaces_before: num_of_newlines - 1, }, }, )) @@ -2182,7 +2183,7 @@ $$ is_static: $is_static, code: Cow::Borrowed($code), insert_path: $insert_path.map(Path::new).map(Cow::from), - is_space_before: false, + spaces_before: 0, }} }; } @@ -2362,19 +2363,19 @@ date: вербень 2077 prof: me :idk: --- ", [ - header_line1: "Навч. заклад", - header_line2: "Факультет", - document_type: "ПРОТОКОЛ", - title_line1: "Виконання марної роботи", - title_line2: "з історії стародавнього Єгипту", - title_line3: "(пинальна частина)", - title_line4: "НАЗВА РОБОТИ", - author_line1: "виконувало:", - author_line2: "дяч дзучунович", - author_line3: "(perfectly still)", - date: "вербень 2077", - prof: "me :idk:" - ], "\n"} + header_line1: "Навч. заклад", + header_line2: "Факультет", + document_type: "ПРОТОКОЛ", + title_line1: "Виконання марної роботи", + title_line2: "з історії стародавнього Єгипту", + title_line3: "(пинальна частина)", + title_line4: "НАЗВА РОБОТИ", + author_line1: "виконувало:", + author_line2: "дяч дзучунович", + author_line3: "(perfectly still)", + date: "вербень 2077", + prof: "me :idk:" + ], "\n"} test_ok! {ok_all_fields_permutated, r"--- author-line2: дяч дзучунович header-line1: Навч. заклад @@ -2390,17 +2391,17 @@ prof: me :idk: title-line3: (пинальна частина) --- ", [ - header_line1: "Навч. заклад", - header_line2: "Факультет", - document_type: "ПРОТОКОЛ", - title_line1: "Виконання марної роботи", - title_line2: "з історії стародавнього Єгипту", - title_line3: "(пинальна частина)", - title_line4: "НАЗВА РОБОТИ", - author_line1: "виконувало:", - author_line2: "дяч дзучунович", - author_line3: "(perfectly still)", - date: "вербень 2077", - prof: "me :idk:" - ], "\n"} + header_line1: "Навч. заклад", + header_line2: "Факультет", + document_type: "ПРОТОКОЛ", + title_line1: "Виконання марної роботи", + title_line2: "з історії стародавнього Єгипту", + title_line3: "(пинальна частина)", + title_line4: "НАЗВА РОБОТИ", + author_line1: "виконувало:", + author_line2: "дяч дзучунович", + author_line3: "(perfectly still)", + date: "вербень 2077", + prof: "me :idk:" + ], "\n"} } diff --git a/tests/lex_files/ayano.txt b/tests/lex_files/ayano.txt index 1e39f77..43b2194 100644 --- a/tests/lex_files/ayano.txt +++ b/tests/lex_files/ayano.txt @@ -34,7 +34,7 @@ Multiple { 0, ), display_state: NotDisplayed, - is_space_before: false, + spaces_before: 0, is_static: false, code: "@fig: src=\"circle.png\", ident=\"circle\", caption=\"Some caption for a line drawing. This is parsed as a *regular document text*, so you may [@eq:line] and $f(x) = A \\cdot x^{1} + B \\cdot x^{0}$ here, for example\"", insert_path: None, @@ -52,7 +52,7 @@ Multiple { 0, ), display_state: NotDisplayed, - is_space_before: false, + spaces_before: 0, is_static: false, code: "width = 0.2\n@fig: src = \"graph.png\", width = width", insert_path: None, @@ -70,7 +70,7 @@ Multiple { 0, ), display_state: NotDisplayed, - is_space_before: false, + spaces_before: 0, is_static: false, code: "data = [\n\t\t[\"x\", \"y\"],\n\t\t[1.0, 3.0],\n\t\t[3.0, 4.0],\n\t\t[-1.0, -5743234.0],\n]\n@gen_table: lambda r,c: data[r][c]; rows=4, columns=2, caption=\"Caption for a table 1, *because* why not\", ident=\"table1\"", insert_path: None, diff --git a/tests/lex_files/ayano_display_blocks.txt b/tests/lex_files/ayano_display_blocks.txt index a09c33e..76a92f9 100644 --- a/tests/lex_files/ayano_display_blocks.txt +++ b/tests/lex_files/ayano_display_blocks.txt @@ -40,7 +40,7 @@ Multiple { 0, ), display_state: DisplayedNoCaption, - is_space_before: false, + spaces_before: 1, is_static: false, code: "# You should be able to see this comment in the output doc\n42", insert_path: None, @@ -65,7 +65,7 @@ Multiple { content: "me, fooling around", }, ), - is_space_before: false, + spaces_before: 1, is_static: false, code: "# This code block has description\n# Might as well add some python syntax to check out minted's syntax highlighting:\nfor i in range(100):\n s = str(i)\n s = s[::-1]\n@dev: -2.034e4, 145.64", insert_path: None, diff --git a/tests/lex_files/deceit.txt b/tests/lex_files/deceit.txt index 5c20bbf..6296225 100644 --- a/tests/lex_files/deceit.txt +++ b/tests/lex_files/deceit.txt @@ -809,7 +809,7 @@ Multiple { 0, ), display_state: NotDisplayed, - is_space_before: false, + spaces_before: 0, is_static: false, code: "2", insert_path: None, diff --git a/tests/transformed_files/ayano_display_blocks.txt b/tests/transformed_files/ayano_display_blocks.txt index e712a57..129ef3d 100644 --- a/tests/transformed_files/ayano_display_blocks.txt +++ b/tests/transformed_files/ayano_display_blocks.txt @@ -133,27 +133,29 @@ \section{Display blocks} \tb This feature allows you to insert certain `Ayano` blocks to the end of the output document, to document your exact actions for displayed results. -\tbln Here's an example:42 -\tbln Display blocks can have descriptions too! Here's one:-20340$\pm$150 +\tbln Here's an example: 42 +\tbln Display blocks can have descriptions too! Here's one: -20340$\pm$150 \subsection{Код} \begin{minted}[linenos, mathescape, autogobble, breaklines]{python} # You should be able to see this comment in the output doc -return 42 - +42 \end{minted} \tb ayano-unnamed +\newline + \begin{minted}[linenos, mathescape, autogobble, breaklines]{python} # This code block has description # Might as well add some python syntax to check out minted's syntax highlighting: for i in range(100): s = str(i) s = s[::-1] -return "err", -2.034e4, 145.64 - +@dev: -2.034e4, 145.64 \end{minted} me, fooling around +\newline + \end{document} \ No newline at end of file From 3a9a339e07dd1b3b7af3c4f26107f6e015134912 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Thu, 28 Mar 2024 10:51:05 +0200 Subject: [PATCH 53/98] Added example with advance Ayano syntax --- .gitignore | 2 + Cargo.lock | 7 + Cargo.toml | 1 + examples/advanced_ayano/2 | 1 + .../data/20240327230336_8798.csv | 16 + examples/advanced_ayano/data/3 | 1 + .../data/calculations_table.csv | 5 + examples/advanced_ayano/data/doing_stuff.py | 42 ++ examples/advanced_ayano/data/read3.py | 3 + examples/advanced_ayano/main.rs | 40 ++ examples/advanced_ayano/output.tex | 495 ++++++++++++++++++ .../advanced_ayano/scripts/calculate_e.py | 5 + .../advanced_ayano/scripts/generate_sine.py | 12 + .../advanced_ayano/scripts/toshino_kyoko.py | 7 + examples/advanced_ayano/source/input.md | 114 ++++ examples/advanced_ayano/test.py | 41 ++ src/data.rs | 14 +- src/gen/ayano.rs | 288 +++++++--- src/gen/ayano/python.rs | 36 +- src/gen/lab/meta.rs | 51 +- src/gen/lab/mod.rs | 30 +- src/lexer.rs | 103 ++-- src/lib.rs | 5 +- src/path_engine.rs | 172 ++++-- 24 files changed, 1267 insertions(+), 224 deletions(-) create mode 100644 examples/advanced_ayano/2 create mode 100644 examples/advanced_ayano/data/20240327230336_8798.csv create mode 100644 examples/advanced_ayano/data/3 create mode 100644 examples/advanced_ayano/data/calculations_table.csv create mode 100644 examples/advanced_ayano/data/doing_stuff.py create mode 100644 examples/advanced_ayano/data/read3.py create mode 100644 examples/advanced_ayano/main.rs create mode 100644 examples/advanced_ayano/output.tex create mode 100644 examples/advanced_ayano/scripts/calculate_e.py create mode 100644 examples/advanced_ayano/scripts/generate_sine.py create mode 100644 examples/advanced_ayano/scripts/toshino_kyoko.py create mode 100644 examples/advanced_ayano/source/input.md create mode 100644 examples/advanced_ayano/test.py diff --git a/.gitignore b/.gitignore index cbab21b..316e4f9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ # VSCode .vscode/settings.json +examples/advanced_ayano/sine_graph.png +examples/advanced_ayano/source/other_sine_graph.png diff --git a/Cargo.lock b/Cargo.lock index 6c1cae3..0460767 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -269,6 +269,7 @@ dependencies = [ "itertools", "nom", "once_cell", + "pathdiff", "pyo3", "rand", "regex", @@ -362,6 +363,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + [[package]] name = "percent-encoding" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 1388ae5..7366a6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ url = "2.5.0" serde = { version = "1.0.197", features = ["derive"], optional = true } url_serde = { version = "0.2.0", optional = true } smart-default = "0.7.1" +pathdiff = "0.2.1" [dev-dependencies] text-diff = "0.4.0" diff --git a/examples/advanced_ayano/2 b/examples/advanced_ayano/2 new file mode 100644 index 0000000..d8263ee --- /dev/null +++ b/examples/advanced_ayano/2 @@ -0,0 +1 @@ +2 \ No newline at end of file diff --git a/examples/advanced_ayano/data/20240327230336_8798.csv b/examples/advanced_ayano/data/20240327230336_8798.csv new file mode 100644 index 0000000..1a33bab --- /dev/null +++ b/examples/advanced_ayano/data/20240327230336_8798.csv @@ -0,0 +1,16 @@ +"First Name","Last Name","Age","Salary","Tenacity" +"Wilson","Bennett","19","5639","97155" +"Hailey","Robinson","21","8419","68689" +"Edith","Morris","24","8134","10937" +"Adison","Brown","20","6131","41745" +"Vivian","Cunningham","22","7834","35514" +"Heather","Roberts","24","4881","73425" +"Michael","Armstrong","23","5781","30899" +"Miranda","Martin","19","8283","33619" +"Alexander","Kelley","29","1990","98055" +"Chloe","Spencer","24","779","89818" +"Sam","Johnston","25","1147","71225" +"Alissa","Spencer","18","3877","64965" +"Abraham","Kelley","20","1622","65308" +"Edwin","Stewart","20","4376","01364" +"Adam","Grant","19","9616","38385" diff --git a/examples/advanced_ayano/data/3 b/examples/advanced_ayano/data/3 new file mode 100644 index 0000000..e440e5c --- /dev/null +++ b/examples/advanced_ayano/data/3 @@ -0,0 +1 @@ +3 \ No newline at end of file diff --git a/examples/advanced_ayano/data/calculations_table.csv b/examples/advanced_ayano/data/calculations_table.csv new file mode 100644 index 0000000..f0baa03 --- /dev/null +++ b/examples/advanced_ayano/data/calculations_table.csv @@ -0,0 +1,5 @@ +'name' 'value' 'err' '%' +'tau' 4.3535223 0.0034342 8e-06 +'pi' 4334.34434 10 2.3e-05 +'chi' -2332.332424 1e-07 -0.0 +'zeta' 4444.0 0.033 0.0 diff --git a/examples/advanced_ayano/data/doing_stuff.py b/examples/advanced_ayano/data/doing_stuff.py new file mode 100644 index 0000000..f9acb44 --- /dev/null +++ b/examples/advanced_ayano/data/doing_stuff.py @@ -0,0 +1,42 @@ +import csv + +# let's just create a bunch of variables and fill a local generated table with them +with open("calculations_table.csv", "w") as f: + writer = csv.DictWriter( + f, + fieldnames=[ + "name", + "value", + "err", + "%", + ], + quoting=csv.QUOTE_NONNUMERIC, + delimiter=" ", + quotechar="'", + ) + writer.writeheader() + + tau = (4.3535223, 0.0034342) + pi = (4334.34434, 10) + chi = (-2332.332424, 1e-7) + zeta = (4444.00, 0.033) + + def write_var(writer, val, name): + writer.writerow( + { + "name": name, + "value": val[0], + "err": val[1], + "%": round(val[1] / val[0] / 100, ndigits=6), + } + ) + + write_var(writer, tau, 'tau') + write_var(writer, pi, 'pi') + write_var(writer, chi, 'chi') + write_var(writer, zeta, 'zeta') + +# say, we do something undesirable in this script next, and we do not want kyomato to execute that +# TOSHINO KYOKO! <-- this terminates the execution + +exit(-42) \ No newline at end of file diff --git a/examples/advanced_ayano/data/read3.py b/examples/advanced_ayano/data/read3.py new file mode 100644 index 0000000..99766fa --- /dev/null +++ b/examples/advanced_ayano/data/read3.py @@ -0,0 +1,3 @@ +three = None +with open("3", "r") as f: + three = int(f.readline()) \ No newline at end of file diff --git a/examples/advanced_ayano/main.rs b/examples/advanced_ayano/main.rs new file mode 100644 index 0000000..c127490 --- /dev/null +++ b/examples/advanced_ayano/main.rs @@ -0,0 +1,40 @@ +use kyomato::path_engine::PathEngineExt; + +fn main() { + //! This is ayano-involving example of programmatic output generation + //! + //! Some advanced setup is used here + //! + //! You might want to use CLI interface instead + + // we'll just assume that to demonstrate a certain thing, look it in the input + std::env::set_current_dir("examples/advanced_ayano").unwrap(); + // that's a utility setup call; essentially, it reads input from one file, and opens output file for writing + let (input, mut output) = examples_util::setup_io("source/input.md", "output.tex").unwrap(); + + // construct path engine + // there are a couple of parts of the path engine. you should specify them in the increasing priority order + let path_engine = kyomato::path_engine::base_gen() + // execution folder: you might want generator to check it, if all other options had failed + .with_execution_folder(".") + // source folder: you'll want to check it sometime. this is intended to be a folder where input markdown is located + .with_source_folder("source") + // image folder: a folder to exclusively check for images referred by the generator + .with_image_folder("images") + // this call will make sure that the engine actually respects ayano-insert directory supplied to it's methods + .with_ayano() + // this adds a check to the engine, that will ensure that returned path actually points to something in the system + // without it, first proposed variant will be used (in this case, this turns out to be ayano-insert directory) + .with_check(kyomato::path_engine::fs_check) + // this last call is a no-op used to explicitly set error type. You may implement your own error too! + // (the one actually used here is kinda bad, consider using `kyomato::path_engine::MarkerError` for more memory-efficiency) + .with_error_type::(); + // please note that most of the above is only used by actual latex engine you plan to use. + // That is, take care of paths being the same for kyomato and the engine, or use some sort of absolute paths. + + // parse input and write to output + kyomato::LabaLatex::parse_and_write(path_engine, &input, &mut output) + // this part is just for debug purposes + .map_err(|err| (err, output)) + .unwrap() +} diff --git a/examples/advanced_ayano/output.tex b/examples/advanced_ayano/output.tex new file mode 100644 index 0000000..5bfdae8 --- /dev/null +++ b/examples/advanced_ayano/output.tex @@ -0,0 +1,495 @@ +\documentclass[a4paper]{article} +\usepackage{graphicx, hyperref, xcolor, gensymb, amssymb, mathtools, wrapfig, mathtools, microtype, lastpage, caption, titlesec, paracol, longtable, booktabs, cancel} +\usepackage[T2A, T1]{fontenc} +\usepackage[utf8]{inputenc} +\usepackage[russian, ukrainian]{babel} +\hypersetup{ + colorlinks, + linkcolor={blue!20!black}, + citecolor={blue!50!black}, + urlcolor={blue!80!black} +} + +\usepackage[left = 25mm, right = 10mm, top=20mm, bottom=20mm, bindingoffset=0cm]{geometry} + +% КОМАНДИ + +\newcommand{\fnt}{ +\fontshape{n} +\fontsize{14pt} {19pt} +\linespread{0.8} +\selectfont +} % нормативний шрифт +\newcommand{\capfnt}{ + \fnt +} % норматичний шрифт для підписів (тимчасово співпадає зі звичайним шрифтом) + +\newcommand{\tb}{ + \hspace*{10mm} +} % відступ у 5 символів "x" відповідно до ДСТУ +\newcommand{\tbln}{ + \newline + \tb +} % те ж саме, але з переносом рядка +\newcommand{\tbsp}{ + \vspace*{1ex} + \tbln +} % те ж саме, але з додатковим відступом між рядками + +% бібліографія +\usepackage[square,sort,comma,numbers]{natbib} +\renewcommand{\bibsection}{} +\usepackage{totcount} +\newtotcounter{citnum} +% лічильник цитувань +\def\oldbibitem{} \let\oldbibitem=\bibitem +\def\bibitem{\stepcounter{citnum}\oldbibitem} + +% лічильник зображень +\newtotcounter{graphnum} +\def\oldincludegraphics{} \let\oldincludegraphics=\includegraphics +\def\includegraphics{\stepcounter{graphnum}\oldincludegraphics} + +% лічильник таблиць (НЕАВТОМАТИЧНИЙ!!!) +\newtotcounter{tabnum} + +% лічильник додатків (НЕАВТОМАТИЧНИЙ!!!) +\newtotcounter{dodnum} + +% нумерація сторінок +\usepackage{fancyhdr} +\pagestyle{fancy} +\fancyhf{} +\renewcommand{\headrulewidth}{0pt} +\setlength{\headheight}{15.3pt} +\fancyhead[R]{\fnt \thepage} + +% виноски +\renewcommand{\thefootnote}{\large\arabic{footnote})~} +\renewcommand{\footnoterule}{\rule{20mm}{0.4pt} \vspace*{0.5ex}} +% \newcommand{\vyn}[2]{ +% \footnote[#1]{\large #2} +% } % створити виноску + +\let\oldFootnote\footnote +\renewcommand{\footnote}[1]{ + \oldFootnote{\large #1} +} % перегрузка виноски + +\providecommand{\tightlist}{% + \setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}} + +% підписи малюнків/таблиць +\captionsetup[figure]{name={\fnt Рисунок~},labelsep=period} +\captionsetup[table]{name={\fnt Таблиця~},labelsep=period} + +% markdown strikeouts +\usepackage{soul} + +% codeblocks +\usepackage{minted} + +% enumeration +\usepackage[shortlabels]{enumitem} + +\titleformat*{\section}{\fnt\LARGE\bfseries} +\titleformat*{\subsection}{\fnt\Large\bfseries} +\titleformat*{\subsubsection}{\fnt\Large} + +\begin{document} +\thispagestyle{empty} % [A]:for cropping +% ------------ +% source: https://tex.stackexchange.com/questions/173651/need-help-to-create-such-a-beautiful-title-page +\begingroup +\centering +\obeylines +\topskip 0pt % source: https://tex.stackexchange.com/questions/2326/vertically-center-text-on-a-page +\vspace*{\fill} +\textbf{ +\LARGE +\LARGE +\vspace{100pt} +\LARGE +\Large +\Large +\Large +\huge +\vspace{50pt} +\large +\large +\large +\large +\vspace*{\fill} +\hspace{\fill}\large \rule{20mm}{0.4pt} +\vspace{10pt} +} +\endgroup +% ------------ +\newpage +\tableofcontents + +\newpage +\fnt +This is an example of advanced `Ayano` features. +\tbln First, there are insert blocks. These can load some code from specified python script, and insert it at the start: +\tbln Note that their displayed version at the end actually includes the loaded script. These blocks can be used for display too: \\ +e = 2.7182818284590455, pi = 3.141592653589793, pi != 2.7182818284590455 +\tbln Sometimes you wish to only include part of the script, and do something else after some point. To achieve that, you can include `# TOSHINO KYOKO!` comment (note the space and casing; you can append anything after it at that line). Once this comment is seen by `Ayano`, it stops inserting the file, and appends the actual code you've provided in the block (possibly none). Comment itself is not included: 2 +\tbln Ayano blocks can actually interact with the filesystem themselves. The caveat is - where exactly python thinks it's current directory is. +\tbln Well, the rule of thumb turns out to be quite simple: +\begin{itemize} +\item if there is an insertion declared for a block, it's execution will be in the directory that file is in +\item if there is no insertion declared, python will continue it's execution at `kyomato`'s process directory +\end{itemize}5 +\tbln Ayano can be used to insert csv tables (possibly generated at runtime too). Here's a special syntax for it: +\begin{table}[h!] +\fnt +\begin{center} +\begin{tabular}{|c|c|c|c|c|}\hline +First Name & Last Name & Age & Salary & Tenacity\\ \hline +\hline +Wilson & Bennett & 19 & 5639 & 97155\\ \hline +Hailey & Robinson & 21 & 8419 & 68689\\ \hline +Edith & Morris & 24 & 8134 & 10937\\ \hline +Adison & Brown & 20 & 6131 & 41745\\ \hline +Vivian & Cunningham & 22 & 7834 & 35514\\ \hline +Heather & Roberts & 24 & 4881 & 73425\\ \hline +Michael & Armstrong & 23 & 5781 & 30899\\ \hline +Miranda & Martin & 19 & 8283 & 33619\\ \hline +Alexander & Kelley & 29 & 1990 & 98055\\ \hline +Chloe & Spencer & 24 & 779 & 89818\\ \hline +Sam & Johnston & 25 & 1147 & 71225\\ \hline +Alissa & Spencer & 18 & 3877 & 64965\\ \hline +Abraham & Kelley & 20 & 1622 & 65308\\ \hline +Edwin & Stewart & 20 & 4376 & 01364\\ \hline +Adam & Grant & 19 & 9616 & 38385\\ \hline +\end{tabular} +\stepcounter{tabnum} +\caption{\capfnt cred: \href{http://randat.com/}{randat}} +\label{tab:random_table1} +\end{center} +\end{table} + +\tb This can be combined with insertion blocks to achieve generated table insertion; you can specify a path relative to the inserted file, +\begin{table}[h!] +\fnt +\begin{center} +\begin{tabular}{|c|c|c|c|}\hline +name & value & err & \%\\ \hline +\hline +tau & 4.3535223 & 0.0034342 & 8e-06\\ \hline +pi & 4334.34434 & 10 & 2.3e-05\\ \hline +chi & -2332.332424 & 1e-07 & -0.0\\ \hline +zeta & 4444.0 & 0.033 & 0.0\\ \hline +\end{tabular} +\stepcounter{tabnum}\end{center} +\end{table} + +\tb ... but you've not forced to do that; you may do execution-relative path too: +\begin{table}[h!] +\fnt +\begin{center} +\begin{tabular}{|c|c|c|c|}\hline +name & value & err & \%\\ \hline +\hline +tau & 4.3535223 & 0.0034342 & 8e-06\\ \hline +pi & 4334.34434 & 10 & 2.3e-05\\ \hline +chi & -2332.332424 & 1e-07 & -0.0\\ \hline +zeta & 4444.0 & 0.033 & 0.0\\ \hline +\end{tabular} +\stepcounter{tabnum}\end{center} +\end{table} + +\tb An of course, once data was already generated (it happened twice here, twice, in fact), you can insert it without any script: +\begin{table}[h!] +\fnt +\begin{center} +\begin{tabular}{|c|c|c|c|}\hline +name & value & err & \%\\ \hline +\hline +tau & 4.3535223 & 0.0034342 & 8e-06\\ \hline +pi & 4334.34434 & 10 & 2.3e-05\\ \hline +chi & -2332.332424 & 1e-07 & -0.0\\ \hline +zeta & 4444.0 & 0.033 & 0.0\\ \hline +\end{tabular} +\stepcounter{tabnum}\end{center} +\end{table} + +\tb You can also restrict the columns and rows you want, for example: +\begin{table}[h!] +\fnt +\begin{center} +\begin{tabular}{|c|}\hline +name\\ \hline +\hline +chi\\ \hline +\end{tabular} +\stepcounter{tabnum} +\caption{\capfnt That's a-bit-cut table!} +\end{center} +\end{table} + +\tb There's also a special columns syntax to display some columns in value-error format: +\begin{table}[h!] +\fnt +\begin{center} +\begin{tabular}{|c|c|c|}\hline +name & value & \%\\ \hline +\hline +tau & 4.354$\pm$0.003 & 8e-06\\ \hline +pi & 4334$\pm$10 & 2.3e-05\\ \hline +chi & -2332.33242400$\pm$0.00000010 & -0.0\\ \hline +zeta & 4444.00$\pm$0.03 & 0.0\\ \hline +\end{tabular} +\stepcounter{tabnum} +\caption{\capfnt That's a table with value-error formatting!} +\end{center} +\end{table} + +\tb That's pretty much it, regarding tables. +\tbln Ayano also allows you to include dynamically-generated figures. You can go two ways about that: +\subsubsection{1st way} + +\tb Generate a figure in a static block, and save it somewhere. After that, include it as an image: +\begin{figure}[h!] +\centering +\includegraphics[width=0.9\textwidth]{./sine_graph.png} +\caption{\capfnt that's a nice little sine graph!} +\label{fig:sine} +\end{figure} + +\subsubsection{2nd way (more idiomatic, in my opinion)} + +\tb Generate a figure in function block, and insert it with a special syntax: +\begin{figure}[h!] +\centering +\includegraphics[width=0.9\textwidth]{././sine_graph.png} +\caption{\capfnt That's another way to include a figure from Ayano!} +\label{fig:other_sine_graph} +\end{figure} + +\tb The only real upside I can see here - you can generate-and-include figures with prior-unknown names or locations +\tbln Oh, and you also can specify width of the figure this way, if it matters to you, that is +\tbln And of course you can generate a figure with inserted block too! +\begin{figure}[h!] +\centering +\includegraphics[width=0.2\textwidth]{./source/other_sine_graph.png} +\caption{\capfnt The same sine. Again.} +\label{fig:yet_another_sine_graph} +\end{figure} + +\tb Note, that in the last example, script actually saves sine into a folder near input markdown fine, and yet `Kyomato` will be able to find it (if configured properly, that is) +\subsection{Code blocks} + +\begin{minted}[linenos, mathescape, autogobble, breaklines]{python} +e = 1.0 +f = 1.0 +for i in range(1, 100): + f /= i + e += f +not_pi = e +\end{minted} + +That's a way to calculate pi \\ + +\begin{minted}[linenos, mathescape, autogobble, breaklines]{python} +e = 1.0 +f = 1.0 +for i in range(1, 100): + f /= i + e += f +from math import pi +f"e = {e}, pi = {pi}, pi != {not_pi}" +\end{minted} + +Ooops, that actually turns out to be a formula for e :( \\ + +\begin{minted}[linenos, mathescape, autogobble, breaklines]{python} +# You will see this comment +x = 2 +# And you'll see this one +# you'll see this comment, and the 2 +x +\end{minted} + +the all-mighty \textbf{2}!!! \\ + +\begin{minted}[linenos, mathescape, autogobble, breaklines]{python} +three = None +with open("3", "r") as f: + three = int(f.readline()) + +\end{minted} + +Here you can see an empty static block displayed as inserted script \\ + +\begin{minted}[linenos, mathescape, autogobble, breaklines]{python} +two = None +with open("2", "r") as f: + two = int(f.readline()) +\end{minted} + +You can clearly see that this block has no insert statement, yet it knows \textbf{exactly} where to find the file \\ + +\begin{minted}[linenos, mathescape, autogobble, breaklines]{python} +two + three +\end{minted} + +This block here uses variables prior-defined in static blocks. \\ + +\begin{minted}[linenos, mathescape, autogobble, breaklines]{python} +@csv_table: src=data/20240327230336_8798.csv, caption="cred: [randat](http://randat.com/)", ident = "random_table1" +\end{minted} + +table insertion example \\ + +\begin{minted}[linenos, mathescape, autogobble, breaklines]{python} +import csv + +# let's just create a bunch of variables and fill a local generated table with them +with open("calculations_table.csv", "w") as f: + writer = csv.DictWriter( + f, + fieldnames=[ + "name", + "value", + "err", + "%", + ], + quoting=csv.QUOTE_NONNUMERIC, + delimiter=" ", + quotechar="'", + ) + writer.writeheader() + + tau = (4.3535223, 0.0034342) + pi = (4334.34434, 10) + chi = (-2332.332424, 1e-7) + zeta = (4444.00, 0.033) + + def write_var(writer, val, name): + writer.writerow( + { + "name": name, + "value": val[0], + "err": val[1], + "%": round(val[1] / val[0] / 100, ndigits=6), + } + ) + + write_var(writer, tau, 'tau') + write_var(writer, pi, 'pi') + write_var(writer, chi, 'chi') + write_var(writer, zeta, 'zeta') + +# say, we do something undesirable in this script next, and we do not want kyomato to execute that +@csv_table: src=calculations_table.csv +\end{minted} + +An example of runtime-generated table insertion \\ + +\begin{minted}[linenos, mathescape, autogobble, breaklines]{python} +import csv + +# let's just create a bunch of variables and fill a local generated table with them +with open("calculations_table.csv", "w") as f: + writer = csv.DictWriter( + f, + fieldnames=[ + "name", + "value", + "err", + "%", + ], + quoting=csv.QUOTE_NONNUMERIC, + delimiter=" ", + quotechar="'", + ) + writer.writeheader() + + tau = (4.3535223, 0.0034342) + pi = (4334.34434, 10) + chi = (-2332.332424, 1e-7) + zeta = (4444.00, 0.033) + + def write_var(writer, val, name): + writer.writerow( + { + "name": name, + "value": val[0], + "err": val[1], + "%": round(val[1] / val[0] / 100, ndigits=6), + } + ) + + write_var(writer, tau, 'tau') + write_var(writer, pi, 'pi') + write_var(writer, chi, 'chi') + write_var(writer, zeta, 'zeta') + +# say, we do something undesirable in this script next, and we do not want kyomato to execute that +@csv_table: src=data/calculations_table.csv +\end{minted} + +An example of runtime-generated table insertion with execution-relative path \\ + +\begin{minted}[linenos, mathescape, autogobble, breaklines]{python} +@csv_table: src=data/calculations_table.csv +\end{minted} + +An example of runtime-generated table insertion with execution-relative path \\ + +\begin{minted}[linenos, mathescape, autogobble, breaklines]{python} +@csv_table: src=data/calculations_table.csv, rows = 3..4, columns = ["name"], caption = "That's a-bit-cut table!" +\end{minted} + +This is only a part of the table above, due to specified column and row restrictions \\ + +\begin{minted}[linenos, mathescape, autogobble, breaklines]{python} +@csv_table: src=data/calculations_table.csv, columns = ["name", ("value", "err"), "%"], caption = "That's a table with value-error formatting!" +\end{minted} + +You can see values having error right beside them, and formatted accordingly! \\ + +\begin{minted}[linenos, mathescape, autogobble, breaklines]{python} +import numpy as np +import matplotlib.pyplot as plt +from math import pi +x = np.arange(-pi, pi, 0.01) +y = np.sin(x) +plt.plot(x, y) +plt.savefig("sine_graph.png") +\end{minted} + +some figure generation \\ + +\begin{minted}[linenos, mathescape, autogobble, breaklines]{python} +import numpy as np +import matplotlib.pyplot as plt +from math import pi +x = np.arange(-pi, pi, 0.01) +y = np.sin(x) +plt.plot(x, y) +plt.savefig("sine_graph.png") +@fig: src = sine_graph.png, ident = "other_sine_graph", caption = "That's another way to include a figure from Ayano!", width = 0.9 +\end{minted} + +that's a figure insertion syntax, all-in-one! \\ + +\begin{minted}[linenos, mathescape, autogobble, breaklines]{python} +import numpy as np +import matplotlib.pyplot as plt +from math import pi +x = np.arange(-pi, pi, 0.01) +y = np.sin(x) +plt.plot(x, y) +plt.savefig("../source/other_sine_graph.png") + +# let's say, that there's some debugging going on here: +@fig: src = other_sine_graph.png, ident = "yet_another_sine_graph", caption = "The same sine. Again.", width = 0.2 +\end{minted} + +That's a third way to do the same thing! \\ + + +\end{document} \ No newline at end of file diff --git a/examples/advanced_ayano/scripts/calculate_e.py b/examples/advanced_ayano/scripts/calculate_e.py new file mode 100644 index 0000000..8bf4308 --- /dev/null +++ b/examples/advanced_ayano/scripts/calculate_e.py @@ -0,0 +1,5 @@ +e = 1.0 +f = 1.0 +for i in range(1, 100): + f /= i + e += f diff --git a/examples/advanced_ayano/scripts/generate_sine.py b/examples/advanced_ayano/scripts/generate_sine.py new file mode 100644 index 0000000..5e5fc6c --- /dev/null +++ b/examples/advanced_ayano/scripts/generate_sine.py @@ -0,0 +1,12 @@ +import numpy as np +import matplotlib.pyplot as plt +from math import pi +x = np.arange(-pi, pi, 0.01) +y = np.sin(x) +plt.plot(x, y) +plt.savefig("../source/other_sine_graph.png") + +# let's say, that there's some debugging going on here: +# TOSHINO KYOKO! +print("I love beeeeans") +exit(-2222222) \ No newline at end of file diff --git a/examples/advanced_ayano/scripts/toshino_kyoko.py b/examples/advanced_ayano/scripts/toshino_kyoko.py new file mode 100644 index 0000000..20acd4c --- /dev/null +++ b/examples/advanced_ayano/scripts/toshino_kyoko.py @@ -0,0 +1,7 @@ +# You will see this comment +x = 2 +# And you'll see this one +# TOSHINO KYOKO! +# But not this one +x = 3 +# And the x variable will still be 2 \ No newline at end of file diff --git a/examples/advanced_ayano/source/input.md b/examples/advanced_ayano/source/input.md new file mode 100644 index 0000000..b060f93 --- /dev/null +++ b/examples/advanced_ayano/source/input.md @@ -0,0 +1,114 @@ +This is an example of advanced `Ayano` features. + +First, there are insert blocks. These can load some code from specified python script, and insert it at the start: + + +```python, Ayano ! ~ scripts/calculate_e.py * "That's a way to calculate pi" +not_pi = e +``` + +Note that their displayed version at the end actually includes the loaded script. These blocks can be used for display too: + + +```python, Ayano ~ scripts/calculate_e.py * "Ooops, that actually turns out to be a formula for e :(" +from math import pi +f"e = {e}, pi = {pi}, pi != {not_pi}" +``` + +Sometimes you wish to only include part of the script, and do something else after some point. To achieve that, you can include `# TOSHINO KYOKO!` comment (note the space and casing; you can append anything after it at that line). Once this comment is seen by `Ayano`, it stops inserting the file, and appends the actual code you've provided in the block (possibly none). Comment itself is not included: + +```python, Ayano ~ scripts/toshino_kyoko.py * "the all-mighty **2**!!!" +# you'll see this comment, and the 2 +x +``` + +Ayano blocks can actually interact with the filesystem themselves. The caveat is - where exactly python thinks it's current directory is. +Well, the rule of thumb turns out to be quite simple: + +- if there is an insertion declared for a block, it's execution will be in the directory that file is in +- if there is no insertion declared, python will continue it's execution at `kyomato`'s process directory +```python, Ayano ! ~ data/read3.py * "Here you can see an empty static block displayed as inserted script" +``` +```python, Ayano ! * "You can clearly see that this block has no insert statement, yet it knows **exactly** where to find the file" +two = None +with open("2", "r") as f: + two = int(f.readline()) +``` + + +```python, Ayano * "This block here uses variables prior-defined in static blocks." +two + three +``` + +Ayano can be used to insert csv tables (possibly generated at runtime too). Here's a special syntax for it: +```python, Ayano * "table insertion example" +@csv_table: src=data/20240327230336_8798.csv, caption="cred: [randat](http://randat.com/)", ident = "random_table1" +``` + +This can be combined with insertion blocks to achieve generated table insertion; you can specify a path relative to the inserted file, +```python, Ayano * "An example of runtime-generated table insertion" ~ data/doing_stuff.py +@csv_table: src=calculations_table.csv +``` + +... but you've not forced to do that; you may do execution-relative path too: +```python, Ayano * "An example of runtime-generated table insertion with execution-relative path" ~ data/doing_stuff.py +@csv_table: src=data/calculations_table.csv +``` + +An of course, once data was already generated (it happened twice here, twice, in fact), you can insert it without any script: +```python, Ayano * "An example of runtime-generated table insertion with execution-relative path" +@csv_table: src=data/calculations_table.csv +``` + +You can also restrict the columns and rows you want, for example: +```python, Ayano * "This is only a part of the table above, due to specified column and row restrictions" +@csv_table: src=data/calculations_table.csv, rows = 3..4, columns = ["name"], caption = "That's a-bit-cut table!" +``` + +There's also a special columns syntax to display some columns in value-error format: +```python, Ayano * "You can see values having error right beside them, and formatted accordingly!" +@csv_table: src=data/calculations_table.csv, columns = ["name", ("value", "err"), "%"], caption = "That's a table with value-error formatting!" +``` + +That's pretty much it, regarding tables. + +Ayano also allows you to include dynamically-generated figures. You can go two ways about that: + +### 1st way + +Generate a figure in a static block, and save it somewhere. After that, include it as an image: +```python, Ayano ! * "some figure generation" +import numpy as np +import matplotlib.pyplot as plt +from math import pi +x = np.arange(-pi, pi, 0.01) +y = np.sin(x) +plt.plot(x, y) +plt.savefig("sine_graph.png") +``` +![[sine_graph.png]] +{caption = "that's a nice little sine graph!", ref = sine} + +### 2nd way (more idiomatic, in my opinion) + +Generate a figure in function block, and insert it with a special syntax: +```python, Ayano * "that's a figure insertion syntax, all-in-one!" +import numpy as np +import matplotlib.pyplot as plt +from math import pi +x = np.arange(-pi, pi, 0.01) +y = np.sin(x) +plt.plot(x, y) +plt.savefig("sine_graph.png") +@fig: src = sine_graph.png, ident = "other_sine_graph", caption = "That's another way to include a figure from Ayano!", width = 0.9 +``` +The only real upside I can see here - you can generate-and-include figures with prior-unknown names or locations + +Oh, and you also can specify width of the figure this way, if it matters to you, that is + +And of course you can generate a figure with inserted block too! +```python, Ayano * "That's a third way to do the same thing!" ~ scripts/generate_sine.py +@fig: src = other_sine_graph.png, ident = "yet_another_sine_graph", caption = "The same sine. Again.", width = 0.2 +``` + +Note, that in the last example, script actually saves sine into a folder near input markdown fine, and yet `Kyomato` will be able to find it (if configured properly, that is) \ No newline at end of file diff --git a/examples/advanced_ayano/test.py b/examples/advanced_ayano/test.py new file mode 100644 index 0000000..d23d548 --- /dev/null +++ b/examples/advanced_ayano/test.py @@ -0,0 +1,41 @@ + +import os +___cwd___ = os.getcwd() +os.chdir('scripts') +e = 1.0 +f = 1.0 +for i in range(1, 100): + f /= i + e += f +not_pi = e +os.chdir(___cwd___) +def f_10738591751140221069(): + e = 1.0 + f = 1.0 + for i in range(1, 100): + f /= i + e += f + from math import pi + return f"e = {e}, pi = {pi}, pi != {not_pi}" +def f_13523737368002237989(): + # You will see this comment + x = 2 + # And you'll see this one + # you'll see this comment, and the 2 + return x +import os +___cwd___ = os.getcwd() +os.chdir('data') +three = None +with open("3", "r") as f: + three = int(f.readline()) +os.chdir(___cwd___) +import os +___cwd___ = os.getcwd() +os.chdir('') +two = None +with open("2", "r") as f: + two = int(f.readline()) +os.chdir(___cwd___) +def f_5124826474091756568(): + return two + three \ No newline at end of file diff --git a/src/data.rs b/src/data.rs index 73bd129..807009c 100644 --- a/src/data.rs +++ b/src/data.rs @@ -504,12 +504,7 @@ impl<'source> Token<'source> { /// None means that it depends on previous tokens pub fn is_newline_allergic(&self) -> Option { match self { - Token::Ayano { - data: AyanoBlock { - is_static: false, .. - }, - } - | Token::Href { .. } + Token::Href { .. } | Token::Paragraph { .. } | Token::InlineMath { .. } | Token::Reference { .. } @@ -523,12 +518,7 @@ impl<'source> Token<'source> { | Token::Figure { .. } | Token::List { .. } | Token::CodeBlock { .. } => Some(true), - Token::Ayano { - data: AyanoBlock { - is_static: true, .. - }, - } - | Token::Multiple { .. } => None, + Token::Ayano { .. } | Token::Multiple { .. } => None, } } } diff --git a/src/gen/ayano.rs b/src/gen/ayano.rs index 6eeda71..5e33282 100644 --- a/src/gen/ayano.rs +++ b/src/gen/ayano.rs @@ -15,13 +15,14 @@ use std::{ fs::File, io::{BufRead, BufReader}, num::{ParseFloatError, ParseIntError}, - path::PathBuf, + path::{self, Path, PathBuf}, str::FromStr, }; use crate::{ data::{AyanoBlock, DisplayState, Token, Tokens}, lexer::KyomatoLexError, + path_engine::{self, PathEngine}, util::{ Equivalent, GenericRange, GenericRangeParseError, Immutable, InlinedStack, InlinedStackAccessor, StaticDebug, @@ -39,11 +40,12 @@ pub struct DisplayInfo { } #[derive(Debug, PartialEq)] -struct CodelessBlockInfo { +struct DisplayCodeBlockInfo { ident: u64, function_name: Option, caption: Option>, insert_path: Option, + display_code: Option, } #[derive(Debug)] @@ -86,21 +88,40 @@ impl AyanoBuilder { } } - pub fn add_block(&mut self, block: &AyanoBlock<'_>) -> Result<(), SyntaxError> { - let (transformed_code, info) = apply_ayano(block)?; + pub fn add_block< + 'source, + 'error: 'source, + PEErr: std::error::Error + 'error, + PE: PathEngine, + >( + &mut self, + block: &AyanoBlock<'source>, + path_engine: &PE, + ) -> Result<(), SyntaxError<'error>> { + let (transformed_code, info) = apply_ayano(block, path_engine)?; let ident = info.ident; // Now, append the code to python handler if let Some(function_name) = info.function_name.as_ref() { python::append_function(&mut self.code, &function_name, &transformed_code); } else { - python::append_static(&mut self.code, &transformed_code); + let insert_path = if let Some(insert_path) = info.insert_path.as_deref() { + let mut path = path_engine + .ayano_insert(insert_path) + .map_err(|err| SyntaxError::PathEngine(Equivalent(Box::new(err))))? + .into_owned(); + path.pop(); + path + } else { + PathBuf::from(".") + }; + python::append_static(&mut self.code, &transformed_code, insert_path); } self.blocks.push(BlockInfo { ident, function_name: info.function_name, display_info: info.caption.map(|name| DisplayInfo { - code: block.code.to_string(), + code: info.display_code.unwrap_or(transformed_code), caption: name, }), insert_path: info.insert_path, @@ -130,10 +151,12 @@ pub enum AyanoError<'b> { Python(Equivalent), #[error("There's no function for ayano block: {:?}", .0)] NoFunction(AyanoBlock<'b>), - #[error("{}", .0)] - Applying(SyntaxError), - #[error("{}", .0)] + #[error(transparent)] + Applying(SyntaxError<'b>), + #[error(transparent)] Parsing(ParsingError), + #[error("{}", (.0).0)] + PathEngine(Equivalent>), } impl From for AyanoError<'_> { @@ -146,10 +169,18 @@ type AyanoResult<'b, T = ()> = Result>; impl AyanoExecutor { // this could easily be part of `display_token`, but I separated it for debugging purposes - fn call_block<'token, 'source: 'token, Out, Transform>( + fn call_block< + 'token, + 'source: 'token, + Out, + Transform, + PEErr: std::error::Error + 'source, + PE: PathEngine, + >( &self, token: &'token AyanoBlock<'source>, transform: Transform, + path_engine: &PE, ) -> AyanoResult<'source, Out> where for<'py> Transform: FnOnce(&'py PyAny, Python<'py>) -> AyanoResult<'source, Out>, @@ -162,15 +193,30 @@ impl AyanoExecutor { // this block was passed here before, but it's not a function return Err(AyanoError::NoFunction(token.clone())); }; - - python::call_function(&self.token, function_name, transform) + let insert_path = if let Some(insert_path) = token.insert_path.as_deref() { + let mut path = path_engine + .ayano_insert(insert_path) + .map_err(|err| SyntaxError::PathEngine(Equivalent(Box::new(err))))? + .into_owned(); + path.pop(); + path + } else { + PathBuf::from(".") + }; + python::call_function(&self.token, function_name, transform, insert_path) } - pub fn display_token<'token, 'source: 'token>( + pub fn display_token< + 'token, + 'source: 'token, + PEErr: std::error::Error + 'source, + PE: PathEngine, + >( &self, token: &'token AyanoBlock<'source>, - ) -> AyanoResult<'source, Token<'static>> { - self.call_block(token, |res, _| Ok(parse_ayano(res)?)) + path_engine: &PE, + ) -> AyanoResult<'source, Token<'source>> { + self.call_block(token, |res, _| Ok(parse_ayano(res)?), path_engine) } pub fn display_blocks<'s>(&'s self) -> impl IntoIterator { @@ -199,10 +245,10 @@ mod ayano_tests { let mut builder = AyanoBuilder::new(); // act - builder.add_block(&block).expect("Should be able to add block"); + builder.add_block(&block, &path_engine::primitive()).expect("Should be able to add block"); // dbg!(&handler); let executor = builder.initialize().expect("Should be able to initialize"); - let output = executor.call_block(&block, |res, _| Ok::<_, super::AyanoError<'_>>(res.str()?.to_string())).expect("Should be able to call this function"); + let output = executor.call_block(&block, |res, _| Ok::<_, super::AyanoError<'_>>(res.str()?.to_string()), &path_engine::primitive()).expect("Should be able to call this function"); // assert assert_eq!(output, $output); @@ -252,14 +298,14 @@ y = x / 100 is_static: false, spaces_before: 0, }; - builder.add_block(&static_block).expect("Should be able to add static block"); - builder.add_block(&function_block).expect("Should be able to add function block"); + builder.add_block(&static_block, &path_engine::primitive()).expect("Should be able to add static block"); + builder.add_block(&function_block, &path_engine::primitive()).expect("Should be able to add function block"); // dbg!(&builder); let executor = builder.initialize().expect("Should be able to initialize"); // act - let static_output = executor.call_block(&static_block, |res, _| Ok(res.str()?.to_string())); - let function_output = executor.call_block(&function_block, |res, _| Ok(res.str()?.to_string())); + let static_output = executor.call_block(&static_block, |res, _| Ok(res.str()?.to_string()), &path_engine::primitive()); + let function_output = executor.call_block(&function_block, |res, _| Ok(res.str()?.to_string()), &path_engine::primitive()); // assert assert_eq!(static_output, Err(AyanoError::NoFunction(static_block.clone())), "Static block shouldn't be callable!"); @@ -306,16 +352,16 @@ y", "2"} is_static: false, spaces_before: 0, }; - builder.add_block(&static_block).expect("Should be able to add static block"); - builder.add_block(&function1_block).expect("Should be able to add function block"); - builder.add_block(&function2_block).expect("Should be able to add function block"); + builder.add_block(&static_block, &path_engine::primitive()).expect("Should be able to add static block"); + builder.add_block(&function1_block, &path_engine::primitive()).expect("Should be able to add function block"); + builder.add_block(&function2_block, &path_engine::primitive()).expect("Should be able to add function block"); // dbg!(&builder); let executor = builder.initialize().expect("Should be able to initialize"); // act - let static_output = executor.call_block(&static_block, |res, _| Ok(res.str()?.to_string())); - let function1_output = executor.call_block(&function1_block, |res, _| Ok(res.str()?.to_string())); - let function2_output = executor.call_block(&function1_block, |res, _| Ok(res.str()?.to_string())); + let static_output = executor.call_block(&static_block, |res, _| Ok(res.str()?.to_string()), &path_engine::primitive()); + let function1_output = executor.call_block(&function1_block, |res, _| Ok(res.str()?.to_string()), &path_engine::primitive()); + let function2_output = executor.call_block(&function1_block, |res, _| Ok(res.str()?.to_string()), &path_engine::primitive()); // assert assert_eq!(static_output, Err(AyanoError::NoFunction(static_block.clone())), "Static block shouldn't be callable!"); @@ -376,12 +422,28 @@ impl CsvTableColumn<'_> { } } -fn apply_ayano<'token, 'source: 'token>( +fn apply_ayano< + 'token, + 'source: 'token, + 'error: 'source, + PEErr: std::error::Error + 'error, + PE: PathEngine, +>( block: &'token AyanoBlock<'source>, -) -> Result<(String, CodelessBlockInfo), SyntaxError> { + path_engine: &PE, +) -> Result<(String, DisplayCodeBlockInfo), SyntaxError<'error>> { // FIRST OF ALL, // read the code from insert-file (if present) - let insert_path = block.insert_path.as_ref().map(|path| path.to_path_buf()); + let insert_path = if let Some(path) = block.insert_path.as_ref() { + Some( + path_engine + .ayano_insert(path.clone()) + .map_err(|err| SyntaxError::PathEngine(Equivalent(Box::new(err))))? + .into_owned(), + ) + } else { + None + }; let mut res = if let Some(insert_path) = insert_path.as_ref() { let mut res = String::new(); for line in BufReader::new(File::open(insert_path).map_err(Equivalent::from)?).lines() { @@ -396,7 +458,11 @@ fn apply_ayano<'token, 'source: 'token>( } else { String::new() }; - let mut code_lines = block.code.lines(); + let code: &'source str = match block.code.clone() { + Cow::Borrowed(s) => s, + Cow::Owned(_) => unreachable!("We'd just cloned the cow"), + }; + let mut code_lines = code.lines(); // I used to have this filter here, but I don't think that's a good thing anymore // There's no harm in preserving basic formatting for better debugging // TODO probably and #[cfg(test)] switch, that would simplify the code is it's added to Python module itself (i.e. this is to be done in `python` module) @@ -423,11 +489,12 @@ fn apply_ayano<'token, 'source: 'token>( res.extend(itertools::intersperse(code_lines, "\n")); return Ok(( res, - CodelessBlockInfo { + DisplayCodeBlockInfo { ident: *block.ident, function_name: None, caption: display_name, insert_path, + display_code: None, }, )); } @@ -441,11 +508,12 @@ fn apply_ayano<'token, 'source: 'token>( // no Ayano syntax is expected there, and that code should not be altered return Ok(( res, - CodelessBlockInfo { + DisplayCodeBlockInfo { ident: *block.ident, function_name, caption: display_name, insert_path, + display_code: None, }, )); }; @@ -456,21 +524,30 @@ fn apply_ayano<'token, 'source: 'token>( res.push('\n'); // and a newline character } + // create display code (basically, the same code, but with last line left unchanged) + let mut display_code = res.clone(); + display_code.push_str(last_line); + // extract syntax from the last line, - let syntax = SyntaxData::try_from(last_line)?; + let syntax: SyntaxData<'source> = SyntaxData::try_from(last_line)?; // and write code corresponding to it syntax - .write_to(&mut res) + .write_to( + &mut res, + path_engine, + insert_path.as_deref().map(Cow::Borrowed), + ) .expect("Should always be able to write into an owned String"); res.push('\n'); // newline for a good measure Ok(( res, - CodelessBlockInfo { + DisplayCodeBlockInfo { ident: *block.ident, function_name, insert_path, caption: display_name, + display_code: Some(display_code), }, )) } @@ -503,7 +580,7 @@ mod apply_ayano_tests { }; // act - let actual = apply_ayano(&block).map(|(s, _)| s); + let actual = apply_ayano(&block, &path_engine::primitive()).map(|(s, _)| s); // assert assert_eq!(actual, $output, "Should yield expected result"); @@ -603,8 +680,8 @@ y = x + 2 }; // act - let transformed_code = - apply_ayano(&block).expect("Should be able to apply ayano transform"); + let transformed_code = apply_ayano(&block, &path_engine::primitive()) + .expect("Should be able to apply ayano transform"); // assert assert_eq!( @@ -653,7 +730,7 @@ enum Syntax<'source> { } #[derive(Debug, derive_more::From, PartialEq, thiserror::Error)] -pub enum SyntaxError { +pub enum SyntaxError<'error> { #[error("Required argument is missing: {}", .0)] MissingArgument(&'static str), #[error("Unknown argument is present: {}", .0)] @@ -667,6 +744,8 @@ pub enum SyntaxError { IntParse(ParseIntError), #[error("{}", **.0)] InsertIo(Equivalent), + #[error("{}", **.0)] + PathEngine(Equivalent>), } macro_rules! or_bs { @@ -796,7 +875,7 @@ macro_rules! needed { // Can't use FromStr here, as if requires parsing from &str with any lifetime impl<'l> TryFrom<&'l str> for SyntaxData<'l> { - type Error = SyntaxError; + type Error = SyntaxError<'static>; fn try_from(mut s: &'l str) -> Result { // First, cut off empty space (but record number of them) @@ -936,17 +1015,17 @@ mod syntax_data_tests { test! {value_error1, "@dev: 1, 2", SyntaxData {indent: 0, syntax: Syntax::ValueError { value: "1", error: "2" }}} test! {value_error2, "@dev: 1, 2 ", SyntaxData {indent: 0, syntax: Syntax::ValueError { value: "1", error: "2" }}} test! {value_error3, "@dev: x + 1, (2 + 3) / 10 ", SyntaxData {indent: 0, syntax: Syntax::ValueError { value: "x + 1", error: "(2 + 3) / 10" }}} - test! {fig1, r#" @fig: src = "path.png" , caption = "Wow, such caption", ident="path" "#, SyntaxData {indent: 4, syntax: Syntax::Figure{ src:"\"path.png\"", caption: Some("\"Wow, such caption\""), ident: Some("\"path\""), width: None}}} - test! {fig2, r#" @fig: caption = "Wow, such caption", ident="path" ,src = "path.png" "#, SyntaxData {indent: 4, syntax: Syntax::Figure{ src: "\"path.png\"", caption: Some("\"Wow, such caption\""), ident: Some("\"path\""), width: None}}} - test! {fig3_width, r#" @fig: caption = "Wow, such caption", width= 0.125, ident="path" ,src = "path.png" "#, SyntaxData {indent: 4, syntax: Syntax::Figure{ src: "\"path.png\"", caption: Some("\"Wow, such caption\""), ident: Some("\"path\""), width: Some("0.125")}}} + test! {fig1, r#" @fig: src = path.png , caption = "Wow, such caption", ident="path" "#, SyntaxData {indent: 4, syntax: Syntax::Figure{ src:"path.png", caption: Some("\"Wow, such caption\""), ident: Some("\"path\""), width: None}}} + test! {fig2, r#" @fig: caption = "Wow, such caption", ident="path" ,src = path.png "#, SyntaxData {indent: 4, syntax: Syntax::Figure{ src: "path.png", caption: Some("\"Wow, such caption\""), ident: Some("\"path\""), width: None}}} + test! {fig3_width, r#" @fig: caption = "Wow, such caption", width= 0.125, ident="path" ,src = path.png "#, SyntaxData {indent: 4, syntax: Syntax::Figure{ src: "path.png", caption: Some("\"Wow, such caption\""), ident: Some("\"path\""), width: Some("0.125")}}} test! {fig4_width_expr, r#" @fig: caption = "Wow, such caption", width= WIDTH + 3 , ident="path" ,src = "path.png" "#, SyntaxData {indent: 4, syntax: Syntax::Figure{ src: "\"path.png\"", caption: Some("\"Wow, such caption\""), ident: Some("\"path\""), width: Some("WIDTH + 3")}}} - test! {csv_table1, r#" @csv_table: src = "path/to/table.csv", caption = "such caption, wow""#, SyntaxData {indent: 3, syntax: Syntax::CsvTable { src: "\"path/to/table.csv\"", rows: GenericRange::Full, columns: None, ident: None, caption: Some("\"such caption, wow\"") }}} - test! {csv_table2, r#" @csv_table: src = "path/to/table.csv", rows = 1..1000"#, SyntaxData {indent: 3, syntax: Syntax::CsvTable { src: "\"path/to/table.csv\"", rows: GenericRange::DoubleBounded(1..1000), columns: None, ident: None, caption: None, }}} - test! {csv_table3, r#" @csv_table: src = "path/to/table.csv", rows = 2.."#, SyntaxData {indent: 3, syntax: Syntax::CsvTable { src: "\"path/to/table.csv\"", rows: GenericRange::From(2..), columns: None, ident: None, caption: None }}} - test! {csv_table4, r#" @csv_table: src = "path/to/table.csv", rows = 2.., columns = ["a", ("b", "b_err")]"#, SyntaxData {indent: 3, syntax: Syntax::CsvTable { src: "\"path/to/table.csv\"", rows: GenericRange::From(2..), columns: Some(vec![c!("a"),c!("b","b_err")]), caption: None, ident: None }}} - test! {csv_table5, r#" @csv_table: src = "path/to/table.csv", rows = 2.., columns = ["a", ("b", "b_err")], caption = "Lol, there's a bunch of random stuff going on, this should not be included into a syntax data""#, - SyntaxData {indent: 4, syntax: Syntax::CsvTable { src: "\"path/to/table.csv\"", rows: GenericRange::From(2..), columns: Some(vec![c!("a"),c!("b","b_err")]), + test! {csv_table1, r#" @csv_table: src = path/to/table.csv, caption = "such caption, wow""#, SyntaxData {indent: 3, syntax: Syntax::CsvTable { src: "path/to/table.csv", rows: GenericRange::Full, columns: None, ident: None, caption: Some("\"such caption, wow\"") }}} + test! {csv_table2, r#" @csv_table: src = path/to/table.csv, rows = 1..1000"#, SyntaxData {indent: 3, syntax: Syntax::CsvTable { src: "path/to/table.csv", rows: GenericRange::DoubleBounded(1..1000), columns: None, ident: None, caption: None, }}} + test! {csv_table3, r#" @csv_table: src = path/to/table.csv, rows = 2.."#, SyntaxData {indent: 3, syntax: Syntax::CsvTable { src: "path/to/table.csv", rows: GenericRange::From(2..), columns: None, ident: None, caption: None }}} + test! {csv_table4, r#" @csv_table: src = path/to/table.csv, rows = 2.., columns = ["a", ("b", "b_err")]"#, SyntaxData {indent: 3, syntax: Syntax::CsvTable { src: "path/to/table.csv", rows: GenericRange::From(2..), columns: Some(vec![c!("a"),c!("b","b_err")]), caption: None, ident: None }}} + test! {csv_table5, r#" @csv_table: src = path/to/table.csv, rows = 2.., columns = ["a", ("b", "b_err")], caption = "Lol, there's a bunch of random stuff going on, this should not be included into a syntax data""#, + SyntaxData {indent: 4, syntax: Syntax::CsvTable { src: "path/to/table.csv", rows: GenericRange::From(2..), columns: Some(vec![c!("a"),c!("b","b_err")]), caption: Some("\"Lol, there's a bunch of random stuff going on, this should not be included into a syntax data\""), // ha-ha, get spec-changed, lol ident: None, }}} test! {get_table1, r#" @gen_table: lambda row, col: 1; rows = 10, columns = 10, caption = "Some awesome caption""#, @@ -994,15 +1073,26 @@ impl<'w, W: Write + ?Sized> CodeWriter<'w, W> { } impl SyntaxData<'_> { - fn write_to<'w, W: Write + ?Sized>(&self, dest: &'w mut W) -> std::fmt::Result { + fn write_to<'w, W: Write + ?Sized, PEErr: std::error::Error, PE: PathEngine>( + &self, + dest: &'w mut W, + path_engine: &PE, + insert_path: Option>, + ) -> std::fmt::Result { let mut writer: CodeWriter<'w, W> = CodeWriter::new(dest, self.indent)?; - self.syntax.write_code(&mut writer)?; + self.syntax + .write_code(&mut writer, path_engine, insert_path)?; Ok(()) } } impl Syntax<'_> { - fn write_code(&self, writer: &mut CodeWriter<'_, W>) -> std::fmt::Result { + fn write_code>( + &self, + writer: &mut CodeWriter<'_, W>, + path_engine: &PE, + insert_path: Option>, + ) -> std::fmt::Result { match self { Syntax::Nothing(s) => { writer.write_str("return ")?; @@ -1020,9 +1110,20 @@ impl Syntax<'_> { caption, width, } => { - writer.write_str("return \"fig\", ")?; + let src = Path::new(src); + let src = match path_engine.csv(src, insert_path.as_deref()) { + Ok(existing_table) => existing_table, + Err(error) => { + // figure was not found, but this can be ok, if program generates it itself + // but it would be wise to log this as a warn + eprintln!("Figure file not found at pre-analysis: {error:?}"); + Cow::Borrowed(Path::new(src)) + } + }; + let src = &*src.to_string_lossy(); + writer.write_str("return \"fig\", \"")?; writer.write_str(src)?; - writer.write_str(", ")?; + writer.write_str("\", ")?; if let Some(ident) = ident { writer.write_str(ident)?; } else { @@ -1048,11 +1149,37 @@ impl Syntax<'_> { ident, caption, } => { + let src = Path::new(src); + let mut src = match path_engine.csv(src, insert_path.as_deref()) { + Ok(existing_table) => existing_table, + Err(error) => { + // table was not found, but this can be ok, if program generates it itself + // but it would be wise to log this as a warn + eprintln!("Table file not found at pre-analysis: {error:?}"); + Cow::Borrowed(src) + } + }; + // the problem here - this type of syntax specifically relies on Python reading the file themselves + // and if there is an insert path - Python will be executed at the insert directory + // to check for that, we do + if let Some(insert_directory) = insert_path.as_ref().map(|p| p.parent()).flatten() { + // if there's no insert path - ignore that + // if insert path has no parent - it's a file in current execution directory - no need to relocate the path + // I could've implemented this thing myself, but why would I, was already done: + if let Some(rebased) = pathdiff::diff_paths(&src, insert_directory) { + src = rebased.into(); + } + } + let src = &*src.to_string_lossy(); writer.write_line("import csv")?; - writer.write_str("with open(")?; + writer.write_str("with open('")?; writer.write_str(src)?; - writer.write_line(") as ___csv_file___:")?; - writer.write_line(" ___reader___ = csv.DictReader(___csv_file___, delimiter=',', skipinitialspace=True, strict=True)")?; + writer.write_line("') as ___csv_file___:")?; + writer.write_line( + " ___dialect___ = csv.Sniffer().sniff(___csv_file___.read(1024))", + )?; + writer.write_line(" ___csv_file___.seek(0)")?; + writer.write_line(" ___reader___ = csv.DictReader(___csv_file___, dialect=___dialect___, skipinitialspace=True)")?; // if there is a lower bound, skip some lines if let Some(mut from) = rows.from() { // if it's 0 or 1, there's effectively no lines to skip @@ -1108,7 +1235,7 @@ impl Syntax<'_> { // If a dedicated line generator is necessary, write out one now if let Some(rows_to) = rows.to() { - let line_iterations = rows_to - rows.from().unwrap_or_default(); + let line_iterations = rows_to - rows.from().unwrap_or_default() + 1; writer.write_line(" def ___line_generator___():")?; write!(writer, " for i in range({line_iterations}):")?; // it's again numbers, can't do much better writer.breakline()?; @@ -1194,7 +1321,7 @@ mod syntax_display_tests { let mut actual_output = String::new(); // act - $input.write_to(&mut actual_output).expect("Should be able to write into string"); + $input.write_to(&mut actual_output, &path_engine::primitive(), None).expect("Should be able to write into string"); // assert assert_eq!(actual_output, expected_output, "Should produce expected result"); @@ -1259,7 +1386,7 @@ mod last_line_tests { // act let actual_output = SyntaxData::try_from($input).map(move |data| { let mut res = String::new(); - data.write_to(&mut res).expect("Should be able to write"); + data.write_to(&mut res, &path_engine::primitive(), None).expect("Should be able to write"); res }); @@ -1527,17 +1654,16 @@ fn parse_ayano(python_output: &PyAny) -> Result, ParsingError> { // any inconsistency will be treated as syntax error from now on. // expected syntax: ('tab', [$([$(), +]), +], 'ident'|None, 'caption'|None) - let list = - throw_downcast!(throw_syntax!(tuple.get_item(1))?.downcast::())?; - let mut rows = list.into_iter(); - let header: Vec<_> = throw_downcast!(rows - .next() - .ok_or(ParsingError::TableNoHeader)? - .downcast::())? - .into_iter() - .map(|h: &PyAny| parse_ayano(h)) - .try_collect()?; + // println!("{:?}", tuple); + let header: Vec<_> = + throw_downcast!(throw_syntax!(tuple.get_item(1))?.downcast::())? + .into_iter() + .map(|h: &PyAny| parse_ayano(h)) + .try_collect()?; + let rows = + throw_downcast!(throw_syntax!(tuple.get_item(2))?.downcast::())?; let cells: Vec<_> = rows + .into_iter() .map(|row| { Ok(throw_downcast!(row.downcast::())? .into_iter() @@ -1553,10 +1679,10 @@ fn parse_ayano(python_output: &PyAny) -> Result, ParsingError> { }) })?; - let ident = throw_syntax!(tuple.get_item(2))?; + let ident = throw_syntax!(tuple.get_item(3))?; let ident = (!ident.is_none()) .then_some(throw_syntax!(ident.str().and_then(|s| s.to_str()))?); - let caption = throw_syntax!(tuple.get_item(3))?; + let caption = throw_syntax!(tuple.get_item(4))?; let caption = if caption.is_none() { None } else { @@ -2650,13 +2776,12 @@ mod find_args_tests { fn parse_columns<'l>(input: &'l str) -> Vec> { use regex::Captures; - const REGEX_STR: &str = r#"("([^"]+)"|\("([^"])",\s*"([^"]+)"\))"#; + const REGEX_STR: &str = r#"[\[,\s]\("([^"]+)",\s*"([^"]+)"\)|([,\[\s]"([^"]+)")"#; static REGEX: Lazy = Lazy::new(|| Regex::new(REGEX_STR).expect("Bad regex syntax")); REGEX .captures_iter(input) .map(|cap: Captures<'l>| { - // dbg!(&cap); - match (cap.get(2), cap.get(3), cap.get(4)) { + match (cap.get(4), cap.get(1), cap.get(2)) { (Some(col), _, _) => CsvTableColumn::Single(col.as_str().into()), (_, Some(value), Some(error)) => { CsvTableColumn::ValueError(value.as_str(), error.as_str()) @@ -2697,8 +2822,11 @@ mod parse_columns_tests { } test! {empty, r#""#, []} - test! {single, r#""a""#, [c!("a")]} - test! {value_error, r#""a", ("b", "c")"#, [c!("a"), ve!("b", "c")]} - test! {master, r#""no", ("x", "x_err"), "y", ("z", "z_err")"#, [c!("no"), ve!("x", "x_err"), c!("y"), ve!("z", "z_err")]} - test! {doc1, r#""type",("a", "a_err")"#, [c!("type"), ve!("a", "a_err")]} + test! {single, r#"["a"]"#, [c!("a")]} + test! {value_error, r#"["a", ("b", "c")]"#, [c!("a"), ve!("b", "c")]} + test! {master, r#"["no", ("x", "x_err"), "y", ("z", "z_err")]"#, [c!("no"), ve!("x", "x_err"), c!("y"), ve!("z", "z_err")]} + test! {doc1, r#"["type",("a", "a_err")]"#, [c!("type"), ve!("a", "a_err")]} + test! {maybe_bug1, r#"["name", ("value", "err"), "a"]"#, [c!("name"), ve!("value", "err"), c!("a")]} + test! {bug1, r#"["name", ("value", "err"), "%"]"#, [c!("name"), ve!("value", "err"), c!("%")]} + } diff --git a/src/gen/ayano/python.rs b/src/gen/ayano/python.rs index 881815b..1cc4f4a 100644 --- a/src/gen/ayano/python.rs +++ b/src/gen/ayano/python.rs @@ -1,3 +1,5 @@ +use std::{borrow::Cow, path::Path}; + use pyo3::prelude::*; use rand::random; @@ -27,10 +29,18 @@ pub fn init(code: &str) -> Result { }) } -pub fn append_static(target: &mut String, code: &str) { +pub fn append_static(target: &mut String, code: &str, execution_path: impl AsRef) { + target.push_str( + format!( + "\nimport os\n___cwd___ = os.getcwd()\nos.chdir('{}')", + execution_path.as_ref().to_string_lossy() + ) + .as_str(), + ); for line in code.lines() { target.push_str(format!("\n{line}").as_str()) } + target.push_str("\nos.chdir(___cwd___)"); } pub fn append_function(target: &mut String, name: &str, code: &str) { @@ -45,6 +55,7 @@ pub fn call_function( token: &InitToken, name: &str, transform: Transform, + execution_path: impl AsRef, ) -> Result where Err: From, @@ -52,8 +63,13 @@ where { Python::with_gil(|py| { let main_module = token.module(py)?; + let os = PyModule::import(py, "os")?; + let cwd = os.getattr("getcwd")?.call0()?; + let chdir = os.getattr("chdir")?; + chdir.call1((execution_path.as_ref(),))?; let function = main_module.getattr(name)?; let res = function.call((), None)?; + chdir.call1((cwd,))?; Ok(transform(res, py)?) }) } @@ -107,9 +123,12 @@ mod tests { let token = init_functions!(code, []).expect("Should be able to create module"); // act + assert - let _res = call_function(&token, "absent_function", |res, _| { - Ok::<_, super::PyErr>(res.str()?.to_string()) - }) + let _res = call_function( + &token, + "absent_function", + |res, _| Ok::<_, super::PyErr>(res.str()?.to_string()), + "", + ) .expect_err("Should get error for non-existent function call"); } @@ -121,9 +140,12 @@ mod tests { .expect("Should be able to create module"); // act - let res = call_function(&token, "ok_function", |res, _| { - Ok::<_, super::PyErr>(res.str()?.to_string()) - }); + let res = call_function( + &token, + "ok_function", + |res, _| Ok::<_, super::PyErr>(res.str()?.to_string()), + "", + ); // assert assert_eq!( diff --git a/src/gen/lab/meta.rs b/src/gen/lab/meta.rs index 17b7f2b..a4aaf99 100644 --- a/src/gen/lab/meta.rs +++ b/src/gen/lab/meta.rs @@ -6,14 +6,17 @@ use std::{ use itertools::Itertools; use pyo3::PyErr; -use crate::data::{TitleInfo, Token}; +use crate::{ + data::{TitleInfo, Token}, + path_engine::PathEngine, +}; use super::super::ayano::{self, AyanoBuilder, AyanoExecutor}; #[derive(Debug, thiserror::Error)] -pub enum MetaError { +pub enum MetaError<'l> { #[error("Failed to recognize Ayano syntax: {}", .0)] - AyanoSyntax(ayano::SyntaxError), + AyanoSyntax(ayano::SyntaxError<'l>), } #[derive(Debug, Default)] @@ -29,12 +32,15 @@ impl<'source> SourceMeta<'source, AyanoBuilder> { Self::default() } - pub fn collect<'meta>(token: &'meta Token<'source>) -> Result + pub fn collect<'meta, PEErr: std::error::Error + 'source, PE: PathEngine>( + token: &'meta Token<'source>, + path_engine: &PE, + ) -> Result> where 'source: 'meta, { let mut res = SourceMeta::empty(); - collect_single(token, &mut res)?; + collect_single(token, &mut res, path_engine)?; Ok(res) } @@ -48,10 +54,11 @@ impl<'source> SourceMeta<'source, AyanoBuilder> { } } -fn collect_single<'meta, 'r, 'source>( +fn collect_single<'meta, 'r, 'source, PEErr: std::error::Error + 'source, PE: PathEngine>( token: &'meta Token<'source>, dest: &'r mut SourceMeta<'source, AyanoBuilder>, -) -> Result<(), MetaError> + path_engine: &PE, +) -> Result<(), MetaError<'source>> where 'source: 'meta, 'meta: 'r, @@ -71,10 +78,10 @@ where if let Some(ident) = ident { dest.refs.borrow_mut().insert(ident.clone()); } - collect_iterable(header, dest)?; - collect_iterable(cells, dest)?; + collect_iterable(header, dest, path_engine)?; + collect_iterable(cells, dest, path_engine)?; if let Some(caption) = caption { - collect_single(caption, dest)?; + collect_single(caption, dest, path_engine)?; } } Token::Figure { @@ -85,18 +92,20 @@ where } => { dest.refs.insert(ident.clone()); if let Some(caption) = caption { - collect_single(caption, dest)?; + collect_single(caption, dest, path_engine)?; } } Token::Multiple { tokens, .. } => { - collect_iterable(tokens, dest)?; + collect_iterable(tokens, dest, path_engine)?; } Token::FootnoteContent { content, ident } => { - collect_single(content, dest)?; + collect_single(content, dest, path_engine)?; dest.footnotes.insert(ident.clone(), content.borrow_ref()); } Token::Ayano { data } => { - dest.ayano.add_block(data).map_err(MetaError::AyanoSyntax)?; + dest.ayano + .add_block(data, path_engine) + .map_err(MetaError::AyanoSyntax)?; } Token::PageDiv | Token::Header { .. } @@ -114,15 +123,23 @@ where Ok(()) } -fn collect_iterable<'r, 'source: 'meta, 'meta: 'r, I>( +fn collect_iterable< + 'r, + 'source: 'meta, + 'meta: 'r, + I, + PEErr: std::error::Error + 'source, + PE: PathEngine, +>( tokens: I, dest: &'r mut SourceMeta<'source, AyanoBuilder>, -) -> Result<(), MetaError> + path_engine: &PE, +) -> Result<(), MetaError<'source>> where I: IntoIterator>, { tokens .into_iter() - .map(|token| collect_single(token, dest)) + .map(|token| collect_single(token, dest, path_engine)) .try_collect() } diff --git a/src/gen/lab/mod.rs b/src/gen/lab/mod.rs index 5b553d2..471e427 100644 --- a/src/gen/lab/mod.rs +++ b/src/gen/lab/mod.rs @@ -68,10 +68,11 @@ impl<'source, PE: PathEngine, PEErr> LabaLatex<'source, PE, PEErr> { PEErr: std::error::Error + 'source, { let (title_info, tokens) = crate::parse_all(input)?; + let mut meta: SourceMeta<'source, AyanoExecutor> = + SourceMeta::collect(&tokens, &path_engine) + .map_err(|err| GenerationError::Meta(Box::new(err)))? + .init_ayano()?; let generator: LabaLatex<'source, PE, PEErr> = Self::new(path_engine); - let mut meta: SourceMeta<'source, AyanoExecutor> = SourceMeta::collect(&tokens) - .map_err(|err| GenerationError::Meta(Box::new(err)))? - .init_ayano()?; meta.title_info = title_info; generator.write_preamble(output, &meta)?; let mut context = Context::default(); @@ -203,10 +204,10 @@ where if !data.is_static { if data.spaces_before == 1 { output.write_char(' ')?; - } else if data.spaces_before >= 2 { - output.write_str(" \\newline\n")?; + } else if data.spaces_before >= 2 && !context.allergic_to_newline { + output.write_str(" \\\\\n")?; } - let token = meta.ayano.display_token(data)?; + let token = meta.ayano.display_token(data, &self.path_engine)?; self.write_to(output, meta, context, &token)?; } } @@ -313,9 +314,10 @@ where content, space_before, } => { + let mut content = content.clone(); if *is_newline { if context.allergic_to_newline { - output.write_str("\\tb ")?; + output.write_str("\n\\tb ")?; } else { output.write_str("\n\\tbln ")?; } @@ -329,6 +331,12 @@ where Some(Formatting::StrikeThrough) => ("\\st{", "}"), }; output.write_str(start)?; + if content.contains('%') { + // there are percentage signs in the paragraph + // most certainly this was is not intended to make a latex comment (because why?) + // TODO add an option to opt this out + content = content.replace('%', "\\%").into(); + } if context.inside_caption { // We are generating text that was inside table/figure caption // meaning, we must un-escape double quotes @@ -465,7 +473,7 @@ where }; generator.write_to(output, meta, context, &block_code_block)?; generator.write_to(output, meta, context, &block.caption)?; - output.write_str("\n\\newline\n")?; + output.write_str(" \\\\\n")?; Ok(()) } let blocks_section_header = Token::Header { @@ -540,7 +548,7 @@ mod tests { use $crate::path_engine::primitive as primitive_engine; // arrange let token = $token; - let meta = SourceMeta::collect(&token) + let meta = SourceMeta::collect(&token, &path_engine::primitive()) .expect("Should be able to collect") .init_ayano() .expect("Should be able to init Ayano"); @@ -871,8 +879,8 @@ y = 0.4 } test! {ayano_fig2, Token::Ayano{data: AyanoBlock{ - ident: StaticDebug(rand::thread_rng().gen()),display_state: DisplayState::NotDisplayed,is_static: false,code: r"@fig: src = 'path/to/image.jpg'".into(),insert_path: None, - spaces_before: 0,}}, + ident: StaticDebug(rand::thread_rng().gen()),display_state: DisplayState::NotDisplayed,is_static: false,code: r"@fig: src = 'path/to/image.jpg'".into(),insert_path: None, + spaces_before: 0,}}, r" \begin{figure}[h!] \centering diff --git a/src/lexer.rs b/src/lexer.rs index f404859..23a14af 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -11,7 +11,7 @@ use nom::{ streaming::tag_no_case, }, character::complete::{char, digit1, multispace0, space0}, - combinator::{all_consuming, cut, iterator, map_res, opt, recognize}, + combinator::{all_consuming, complete, cut, iterator, map_res, opt, recognize}, error::{context, ContextError, ErrorKind, FromExternalError, ParseError}, multi::{many0_count, many1}, number::complete::float, @@ -576,30 +576,27 @@ fn ayano< ), // This is guaranteed to be Ayano block at this point, so we lock in cut(pair( - terminated( - optional_permutation(( - preceded( - pair(char('*'), space0), - opt(terminated(caption_kernel, space0)), + optional_permutation(( + preceded( + pair(char('*'), space0), + opt(terminated(caption_kernel, space0)), + ), + preceded( + pair(char('~'), space0), + terminated(take_till1(char::is_whitespace), space0), + ), + preceded( + pair(char('#'), space0), + terminated( + map_res(digit1, |n| { + u16::from_str(n) + .map_err(|err| KyomatoLexError::bad_indent(input, err)) + }), + space0, ), - preceded( - pair(char('~'), space0), - terminated(take_till1(char::is_whitespace), space0), - ), - preceded( - pair(char('#'), space0), - terminated( - map_res(digit1, |n| { - u16::from_str(n) - .map_err(|err| KyomatoLexError::bad_indent(input, err)) - }), - space0, - ), - ), - terminated(char('!'), space0), - )), - char('\n'), - ), + ), + terminated(char('!'), space0), + )), terminated(take_until("\n```"), tag("\n```")), )), ), @@ -617,7 +614,7 @@ fn ayano< }; let insert: Option<&'source Path> = insert.map(Path::new); // XXX add support for codeblock identifiers - let code: Cow<'_, str> = Cow::from(code); + let code: Cow<'_, str> = Cow::from(code.trim_start_matches('\n')); let num_of_newlines: usize = before.chars().filter(|c| c == &'\n').count(); Ok(( rest, @@ -1359,7 +1356,7 @@ pub fn lex< ) -> IResult<&'source str, Token<'source>, E> { context( "unlimited lex", - all_consuming(terminated( + complete(all_consuming(terminated( tokens_many1(alt(( // Allow them all! div_page.map(__equalizer), @@ -1379,7 +1376,7 @@ pub fn lex< paragraph, ))), multispace0, - )), + ))), ) .parse(input) } @@ -2363,19 +2360,19 @@ date: вербень 2077 prof: me :idk: --- ", [ - header_line1: "Навч. заклад", - header_line2: "Факультет", - document_type: "ПРОТОКОЛ", - title_line1: "Виконання марної роботи", - title_line2: "з історії стародавнього Єгипту", - title_line3: "(пинальна частина)", - title_line4: "НАЗВА РОБОТИ", - author_line1: "виконувало:", - author_line2: "дяч дзучунович", - author_line3: "(perfectly still)", - date: "вербень 2077", - prof: "me :idk:" - ], "\n"} + header_line1: "Навч. заклад", + header_line2: "Факультет", + document_type: "ПРОТОКОЛ", + title_line1: "Виконання марної роботи", + title_line2: "з історії стародавнього Єгипту", + title_line3: "(пинальна частина)", + title_line4: "НАЗВА РОБОТИ", + author_line1: "виконувало:", + author_line2: "дяч дзучунович", + author_line3: "(perfectly still)", + date: "вербень 2077", + prof: "me :idk:" + ], "\n"} test_ok! {ok_all_fields_permutated, r"--- author-line2: дяч дзучунович header-line1: Навч. заклад @@ -2391,17 +2388,17 @@ prof: me :idk: title-line3: (пинальна частина) --- ", [ - header_line1: "Навч. заклад", - header_line2: "Факультет", - document_type: "ПРОТОКОЛ", - title_line1: "Виконання марної роботи", - title_line2: "з історії стародавнього Єгипту", - title_line3: "(пинальна частина)", - title_line4: "НАЗВА РОБОТИ", - author_line1: "виконувало:", - author_line2: "дяч дзучунович", - author_line3: "(perfectly still)", - date: "вербень 2077", - prof: "me :idk:" - ], "\n"} + header_line1: "Навч. заклад", + header_line2: "Факультет", + document_type: "ПРОТОКОЛ", + title_line1: "Виконання марної роботи", + title_line2: "з історії стародавнього Єгипту", + title_line3: "(пинальна частина)", + title_line4: "НАЗВА РОБОТИ", + author_line1: "виконувало:", + author_line2: "дяч дзучунович", + author_line3: "(perfectly still)", + date: "вербень 2077", + prof: "me :idk:" + ], "\n"} } diff --git a/src/lib.rs b/src/lib.rs index 7bf8296..3e09fdf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,8 +67,9 @@ pub fn gen_to_string<'token, 'source: 'token>( token: &'token data::Token<'source>, title_info: impl Into>>, ) -> Result> { - let generator = gen::lab::LabaLatex::new(path_engine::primitive()); - let mut meta = gen::lab::SourceMeta::collect(&token)?.init_ayano()?; + let path_engine = path_engine::primitive(); + let mut meta = gen::lab::SourceMeta::collect(&token, &path_engine)?.init_ayano()?; + let generator = gen::lab::LabaLatex::new(path_engine); if let Some(title_info) = title_info.into() { meta.title_info = title_info; } diff --git a/src/path_engine.rs b/src/path_engine.rs index 0d64fe4..c8f7efa 100644 --- a/src/path_engine.rs +++ b/src/path_engine.rs @@ -17,6 +17,7 @@ pub enum ErrorContext { pub trait PathEngineError: Error { fn from_generic(error: Box, context: ErrorContext) -> Self; fn append(&mut self, other: Box, context: ErrorContext); + fn record_path<'l>(&mut self, path: impl Into>); } /// An error type with no state - It's only mission is to report an error happening @@ -30,6 +31,8 @@ impl PathEngineError for MarkerError { fn from_generic(error: Box, context: ErrorContext) -> Self { MarkerError } + + fn record_path<'l>(&mut self, path: impl Into>) {} } /// An error holding rich information about reason resolution had failed @@ -41,6 +44,7 @@ pub struct InformativeError { specific: Option>, execution: Option>, none: Option>, + path: Option, } impl PathEngineError for InformativeError { @@ -59,6 +63,12 @@ impl PathEngineError for InformativeError { this.append(error, context); this } + + fn record_path<'l>(&mut self, path: impl Into>) { + if self.path.is_none() { + self.path = Some(path.into().into_owned()) + } + } } #[cfg(test)] @@ -127,26 +137,38 @@ impl PathEngine for Engine { fn image<'p>( &self, path: impl Into>, - ayano_insert: Option<&Path>, + ayano_insert_dir: Option<&Path>, ) -> Result, E> { let path = path.into(); - let _ = (self.0)(&path).map_err(|err| E::from_generic(err, ErrorContext::None))?; + if let Err(err) = (self.0)(&path) { + let mut error = E::from_generic(err, ErrorContext::None); + error.record_path(path); + return Err(error); + } Ok(path) } fn csv<'p>( &self, path: impl Into>, - ayano_insert: Option<&Path>, + ayano_insert_dir: Option<&Path>, ) -> Result, E> { let path = path.into(); - let _ = (self.0)(&path).map_err(|err| E::from_generic(err, ErrorContext::None))?; + if let Err(err) = (self.0)(&path) { + let mut error = E::from_generic(err, ErrorContext::None); + error.record_path(path); + return Err(error); + } Ok(path) } fn ayano_insert<'p>(&self, path: impl Into>) -> Result, E> { let path = path.into(); - let _ = (self.0)(&path).map_err(|err| E::from_generic(err, ErrorContext::None))?; + if let Err(err) = (self.0)(&path) { + let mut error = E::from_generic(err, ErrorContext::None); + error.record_path(path); + return Err(error); + } Ok(path) } @@ -195,7 +217,7 @@ macro_rules! try_variant { let mut path = $path.into(); // first, try this path let this_error = if let Some(this_path) = $this_path.as_ref() { - let this_guess = this_path.to_owned().join(&path); + let this_guess = this_path.join(&path); let Err(err) = $self.get_check()(&this_guess) else { return Ok(Cow::Owned(this_guess)); }; @@ -229,6 +251,14 @@ macro_rules! impl_check_delegate { } }; } + +fn try_ayano<'p>( + path: impl Into>, + ayano_insert: &Path, +) -> Result, Box> { + todo!() +} + pub struct AyanoRespecting(PE); impl> PathEngine for AyanoRespecting { fn image<'p>( @@ -236,7 +266,33 @@ impl> PathEngine for AyanoRespecting>, ayano_insert: Option<&Path>, ) -> Result, E> { - try_variant!(ayano_insert, Ayano, self, image, path, ayano_insert) + let path = path.into(); + // ayano insertion is kinda different in a way that `ayano_insert` is a file that's being inserted + // first, try this path + let this_error = if let Some(ayano_insert) = ayano_insert { + // ayano_insert is presumably a valid script path + // hence, if there's not parent, file must be located at the current directory + let ayano_insert_dir = ayano_insert.parent().unwrap_or(Path::new("./")); + let this_guess = ayano_insert_dir.join(&path); + let Err(err) = self.get_check()(&this_guess) else { + return Ok(Cow::Owned(this_guess)); + }; + Some(err) + } else { + None + }; + // Then, attempt to delegate deeper + match self.0.image(path, ayano_insert) { + // if inner engine is successful, return it's result + Ok(res) => Ok(res), + // if it's not -- append this error to it's error and propagate + Err(mut err) => { + if let Some(this_error) = this_error { + err.append(this_error, ErrorContext::Ayano); + } + Err(err) + } + } } fn csv<'p>( @@ -244,7 +300,33 @@ impl> PathEngine for AyanoRespecting>, ayano_insert: Option<&Path>, ) -> Result, E> { - try_variant!(ayano_insert, Ayano, self, csv, path, ayano_insert) + let path = path.into(); + // ayano insertion is kinda different in a way that `ayano_insert` is a file that's being inserted + // first, try this path + let this_error = if let Some(ayano_insert) = ayano_insert { + // ayano_insert is presumably a valid script path + // hence, if there's not parent, file must be located at the current directory + let ayano_insert_dir = ayano_insert.parent().unwrap_or(Path::new("./")); + let this_guess = ayano_insert_dir.join(&path); + let Err(err) = self.get_check()(&this_guess) else { + return Ok(Cow::Owned(this_guess)); + }; + Some(err) + } else { + None + }; + // Then, attempt to delegate deeper + match self.0.csv(path, ayano_insert) { + // if inner engine is successful, return it's result + Ok(res) => Ok(res), + // if it's not -- append this error to it's error and propagate + Err(mut err) => { + if let Some(this_error) = this_error { + err.append(this_error, ErrorContext::Ayano); + } + Err(err) + } + } } fn ayano_insert<'p>(&self, path: impl Into>) -> Result, E> { @@ -438,20 +520,20 @@ mod tests { ($($existing_path:literal), *) => { primitive_dbg() .with_check(test_check!($($existing_path), *)) - .with_execution_folder(PathBuf::from_str("/execution").unwrap()) - .with_source_folder(PathBuf::from_str("/source").unwrap()) - .with_image_folder(PathBuf::from_str("/image").unwrap()) + .with_execution_folder(PathBuf::from_str("./execution").unwrap()) + .with_source_folder(PathBuf::from_str("./source").unwrap()) + .with_image_folder(PathBuf::from_str("./image").unwrap()) .with_ayano() }; } - test_ok! {ok_find_ayano_correct1, paths_set_engine!("/execution/calc.py", "/image/calc.py"), "calc.py", "/execution/calc.py"} - test_ok! {ok_find_ayano_correct2, paths_set_engine!("/execution/calc.py", "/image/calc.py", "/source/calc.py"), image, "calc.py", None, "/image/calc.py"} - test_ok! {ok_find_ayano_correct2_2, paths_set_engine!("/execution/calc.py", "/source/calc.py"), image, "calc.py", None, "/source/calc.py"} - test_ok! {ok_find_ayano_correct2_3, paths_set_engine!("/execution/calc.py"), image, "calc.py", None, "/execution/calc.py"} - test_ok! {ok_find_ayano_correct2_4, paths_set_engine!("/execution/calc.py", "/image/calc.py", "/source/calc.py", "/ayano/calc.py"), image, "calc.py", Some("/ayano"), "/ayano/calc.py"} - test_ok! {ok_find_ayano_correct3, paths_set_engine!("/execution/table.csv", "/image/table.csv", "/source/table.csv"), csv, "table.csv", None, "/source/table.csv"} - test_ok! {ok_find_ayano_correct3_2, paths_set_engine!("/execution/table.csv", "/image/table.csv"), csv, "table.csv", None, "/execution/table.csv"} - test_pat! {err_find_ayano_correct3_3, paths_set_engine!("/image/table.csv"), csv, "table.csv", None, Err(_)} + test_ok! {ok_find_ayano_correct1, paths_set_engine!("./execution/calc.py", "./image/calc.py"), "calc.py", "./execution/calc.py"} + test_ok! {ok_find_ayano_correct2, paths_set_engine!("./execution/calc.py", "./image/calc.py", "./source/calc.py"), image, "calc.py", None, "./image/calc.py"} + test_ok! {ok_find_ayano_correct2_2, paths_set_engine!("./execution/calc.py", "./source/calc.py"), image, "calc.py", None, "./source/calc.py"} + test_ok! {ok_find_ayano_correct2_3, paths_set_engine!("./execution/calc.py"), image, "calc.py", None, "./execution/calc.py"} + test_ok! {ok_find_ayano_correct2_4, paths_set_engine!("./execution/calc.py", "./image/calc.py", "./source/calc.py", "./ayano/calc.py"), image, "calc.py", Some("./ayano/calc.py"), "./ayano/calc.py"} + test_ok! {ok_find_ayano_correct3, paths_set_engine!("./execution/table.csv", "./image/table.csv", "./source/table.csv"), csv, "table.csv", None, "./source/table.csv"} + test_ok! {ok_find_ayano_correct3_2, paths_set_engine!("./execution/table.csv", "./image/table.csv"), csv, "table.csv", None, "./execution/table.csv"} + test_pat! {err_find_ayano_correct3_3, paths_set_engine!("./image/table.csv"), csv, "table.csv", None, Err(_)} macro_rules! err { ($path:literal) => {{ @@ -464,46 +546,52 @@ mod tests { ayano: None, specific: None, execution: None, - none: err!("file.txt") + none: err!("file.txt"), + path: Some(PathBuf::from("file.txt")), })} - test_expr! {err_reaction2, primitive_dbg().with_ayano().with_check(test_check!()), csv, "file.txt", Some("/ayano"), + test_expr! {err_reaction2, primitive_dbg().with_ayano().with_check(test_check!()), csv, "file.txt", Some("./ayano/script.py"), Err(InformativeError { source: None, - ayano: err!("/ayano/file.txt"), + ayano: err!("./ayano/file.txt"), specific: None, execution: None, - none: err!("file.txt") + none: err!("file.txt"), + path: Some(PathBuf::from("file.txt")), })} - test_expr! {err_reaction3, primitive_dbg().with_ayano().with_execution_folder(Path::new("/execution")).with_check(test_check!()), csv, "file.txt", Some("/ayano"), + test_expr! {err_reaction3, primitive_dbg().with_ayano().with_execution_folder(Path::new("./execution")).with_check(test_check!()), csv, "file.txt", Some("./ayano/script.py"), Err(InformativeError { source: None, - ayano: err!("/ayano/file.txt"), + ayano: err!("./ayano/file.txt"), specific: None, - execution: err!("/execution/file.txt"), - none: err!("file.txt") + execution: err!("./execution/file.txt"), + none: err!("file.txt"), + path: Some(PathBuf::from("file.txt")), })} - test_expr! {err_reaction4, primitive_dbg().with_ayano().with_image_folder(Path::new("/image")).with_execution_folder(Path::new("/execution")).with_check(test_check!()), csv, "file.txt", Some("/ayano"), + test_expr! {err_reaction4, primitive_dbg().with_ayano().with_image_folder(Path::new("./image")).with_execution_folder(Path::new("./execution")).with_check(test_check!()), csv, "file.txt", Some("./ayano/script.py"), Err(InformativeError { source: None, - ayano: err!("/ayano/file.txt"), + ayano: err!("./ayano/file.txt"), specific: None, - execution: err!("/execution/file.txt"), - none: err!("file.txt") + execution: err!("./execution/file.txt"), + none: err!("file.txt"), + path: Some(PathBuf::from("file.txt")), })} - test_expr! {err_reaction5, primitive_dbg().with_ayano().with_image_folder(Path::new("/image")).with_execution_folder(Path::new("/execution")).with_check(test_check!()), image, "file.txt", Some("/ayano"), + test_expr! {err_reaction5, primitive_dbg().with_ayano().with_image_folder(Path::new("./image")).with_execution_folder(Path::new("./execution")).with_check(test_check!()), image, "file.txt", Some("./ayano/script.py"), Err(InformativeError { source: None, - ayano: err!("/ayano/file.txt"), - specific: err!("/image/file.txt"), - execution: err!("/execution/file.txt"), - none: err!("file.txt") + ayano: err!("./ayano/file.txt"), + specific: err!("./image/file.txt"), + execution: err!("./execution/file.txt"), + none: err!("file.txt"), + path: Some(PathBuf::from("file.txt")), })} - test_expr! {err_reaction6, primitive_dbg().with_ayano().with_source_folder(Path::new("/source")).with_image_folder(Path::new("/image")).with_execution_folder(Path::new("/execution")).with_check(test_check!()), image, "file.txt", Some("/ayano"), + test_expr! {err_reaction6, primitive_dbg().with_ayano().with_source_folder(Path::new("./source")).with_image_folder(Path::new("./image")).with_execution_folder(Path::new("./execution")).with_check(test_check!()), image, "file.txt", Some("./ayano/script.py"), Err(InformativeError { - source: err!("/source/file.txt"), - ayano: err!("/ayano/file.txt"), - specific: err!("/image/file.txt"), - execution: err!("/execution/file.txt"), - none: err!("file.txt") + source: err!("./source/file.txt"), + ayano: err!("./ayano/file.txt"), + specific: err!("./image/file.txt"), + execution: err!("./execution/file.txt"), + none: err!("file.txt"), + path: Some(PathBuf::from("file.txt")), })} } From e541d952b11b32be396fb3eccaf772203a49a95e Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Thu, 28 Mar 2024 12:02:47 +0200 Subject: [PATCH 54/98] Complied with all the tests --- examples/advanced_ayano/output.tex | 16 +- examples/advanced_ayano/source/input.md | 16 +- examples/basic_ayano/output.tex | 16 +- examples/basic_syntax/output.tex | 18 +- src/gen/ayano.rs | 217 +++++++++++------- src/gen/ayano/python.rs | 4 +- src/gen/lab/mod.rs | 8 +- tests/transformed_files/ayano.txt | 4 +- .../ayano_display_blocks.txt | 8 +- tests/transformed_files/basic.txt | 3 + tests/transformed_files/deceit.txt | 23 +- tests/transformed_files/edgecases.txt | 6 +- tests/transformed_files/tables_code.txt | 3 + tests/transformed_files/title_info.txt | 1 + 14 files changed, 215 insertions(+), 128 deletions(-) diff --git a/examples/advanced_ayano/output.tex b/examples/advanced_ayano/output.tex index 5bfdae8..7c47397 100644 --- a/examples/advanced_ayano/output.tex +++ b/examples/advanced_ayano/output.tex @@ -338,7 +338,7 @@ two + three This block here uses variables prior-defined in static blocks. \\ \begin{minted}[linenos, mathescape, autogobble, breaklines]{python} -@csv_table: src=data/20240327230336_8798.csv, caption="cred: [randat](http://randat.com/)", ident = "random_table1" +@csv_table: src="data/20240327230336_8798.csv", caption="cred: [randat](http://randat.com/)", ident = "random_table1" \end{minted} table insertion example \\ @@ -383,7 +383,7 @@ with open("calculations_table.csv", "w") as f: write_var(writer, zeta, 'zeta') # say, we do something undesirable in this script next, and we do not want kyomato to execute that -@csv_table: src=calculations_table.csv +@csv_table: src="calculations_table.csv" \end{minted} An example of runtime-generated table insertion \\ @@ -428,25 +428,25 @@ with open("calculations_table.csv", "w") as f: write_var(writer, zeta, 'zeta') # say, we do something undesirable in this script next, and we do not want kyomato to execute that -@csv_table: src=data/calculations_table.csv +@csv_table: src="data/calculations_table.csv" \end{minted} An example of runtime-generated table insertion with execution-relative path \\ \begin{minted}[linenos, mathescape, autogobble, breaklines]{python} -@csv_table: src=data/calculations_table.csv +@csv_table: src="data/calculations_table.csv" \end{minted} An example of runtime-generated table insertion with execution-relative path \\ \begin{minted}[linenos, mathescape, autogobble, breaklines]{python} -@csv_table: src=data/calculations_table.csv, rows = 3..4, columns = ["name"], caption = "That's a-bit-cut table!" +@csv_table: src="data/calculations_table.csv", rows = 3..4, columns = ["name"], caption = "That's a-bit-cut table!" \end{minted} This is only a part of the table above, due to specified column and row restrictions \\ \begin{minted}[linenos, mathescape, autogobble, breaklines]{python} -@csv_table: src=data/calculations_table.csv, columns = ["name", ("value", "err"), "%"], caption = "That's a table with value-error formatting!" +@csv_table: src="data/calculations_table.csv", columns = ["name", ("value", "err"), "%"], caption = "That's a table with value-error formatting!" \end{minted} You can see values having error right beside them, and formatted accordingly! \\ @@ -471,7 +471,7 @@ x = np.arange(-pi, pi, 0.01) y = np.sin(x) plt.plot(x, y) plt.savefig("sine_graph.png") -@fig: src = sine_graph.png, ident = "other_sine_graph", caption = "That's another way to include a figure from Ayano!", width = 0.9 +@fig: src = "sine_graph.png", ident = "other_sine_graph", caption = "That's another way to include a figure from Ayano!", width = 0.9 \end{minted} that's a figure insertion syntax, all-in-one! \\ @@ -486,7 +486,7 @@ plt.plot(x, y) plt.savefig("../source/other_sine_graph.png") # let's say, that there's some debugging going on here: -@fig: src = other_sine_graph.png, ident = "yet_another_sine_graph", caption = "The same sine. Again.", width = 0.2 +@fig: src = "other_sine_graph.png", ident = "yet_another_sine_graph", caption = "The same sine. Again.", width = 0.2 \end{minted} That's a third way to do the same thing! \\ diff --git a/examples/advanced_ayano/source/input.md b/examples/advanced_ayano/source/input.md index b060f93..c545f10 100644 --- a/examples/advanced_ayano/source/input.md +++ b/examples/advanced_ayano/source/input.md @@ -42,32 +42,32 @@ two + three Ayano can be used to insert csv tables (possibly generated at runtime too). Here's a special syntax for it: ```python, Ayano * "table insertion example" -@csv_table: src=data/20240327230336_8798.csv, caption="cred: [randat](http://randat.com/)", ident = "random_table1" +@csv_table: src="data/20240327230336_8798.csv", caption="cred: [randat](http://randat.com/)", ident = "random_table1" ``` This can be combined with insertion blocks to achieve generated table insertion; you can specify a path relative to the inserted file, ```python, Ayano * "An example of runtime-generated table insertion" ~ data/doing_stuff.py -@csv_table: src=calculations_table.csv +@csv_table: src="calculations_table.csv" ``` ... but you've not forced to do that; you may do execution-relative path too: ```python, Ayano * "An example of runtime-generated table insertion with execution-relative path" ~ data/doing_stuff.py -@csv_table: src=data/calculations_table.csv +@csv_table: src="data/calculations_table.csv" ``` An of course, once data was already generated (it happened twice here, twice, in fact), you can insert it without any script: ```python, Ayano * "An example of runtime-generated table insertion with execution-relative path" -@csv_table: src=data/calculations_table.csv +@csv_table: src="data/calculations_table.csv" ``` You can also restrict the columns and rows you want, for example: ```python, Ayano * "This is only a part of the table above, due to specified column and row restrictions" -@csv_table: src=data/calculations_table.csv, rows = 3..4, columns = ["name"], caption = "That's a-bit-cut table!" +@csv_table: src="data/calculations_table.csv", rows = 3..4, columns = ["name"], caption = "That's a-bit-cut table!" ``` There's also a special columns syntax to display some columns in value-error format: ```python, Ayano * "You can see values having error right beside them, and formatted accordingly!" -@csv_table: src=data/calculations_table.csv, columns = ["name", ("value", "err"), "%"], caption = "That's a table with value-error formatting!" +@csv_table: src="data/calculations_table.csv", columns = ["name", ("value", "err"), "%"], caption = "That's a table with value-error formatting!" ``` That's pretty much it, regarding tables. @@ -100,7 +100,7 @@ x = np.arange(-pi, pi, 0.01) y = np.sin(x) plt.plot(x, y) plt.savefig("sine_graph.png") -@fig: src = sine_graph.png, ident = "other_sine_graph", caption = "That's another way to include a figure from Ayano!", width = 0.9 +@fig: src = "sine_graph.png", ident = "other_sine_graph", caption = "That's another way to include a figure from Ayano!", width = 0.9 ``` The only real upside I can see here - you can generate-and-include figures with prior-unknown names or locations @@ -108,7 +108,7 @@ Oh, and you also can specify width of the figure this way, if it matters to you, And of course you can generate a figure with inserted block too! ```python, Ayano * "That's a third way to do the same thing!" ~ scripts/generate_sine.py -@fig: src = other_sine_graph.png, ident = "yet_another_sine_graph", caption = "The same sine. Again.", width = 0.2 +@fig: src = "other_sine_graph.png", ident = "yet_another_sine_graph", caption = "The same sine. Again.", width = 0.2 ``` Note, that in the last example, script actually saves sine into a folder near input markdown fine, and yet `Kyomato` will be able to find it (if configured properly, that is) \ No newline at end of file diff --git a/examples/basic_ayano/output.tex b/examples/basic_ayano/output.tex index 811cdf1..8d59e02 100644 --- a/examples/basic_ayano/output.tex +++ b/examples/basic_ayano/output.tex @@ -134,7 +134,7 @@ Here's a basic Ayano explanation: \tbln Essentially, Ayano blocks are regular code blocks, having `python, Ayano` marked as their language. This sort of codeblocks are treated completely differently by the parser - these blocks are supposed to allow arbitrary python code execution for data generation and analysis. \tbln For this example, we'll only consider examples that do not interact with the filesystem. \tbln Say, you have some sort of dataset, you wish to find a mean a deviation of. Here's a way to do that with Ayano (without filesystem interaction): -\tbln You might notice, that there's actually nothing displayed above in the document. That's due to \textit{static blocks} leaving nothing behind them. To actually display the dataset, we would use a regular, non-static Ayano block: \newline +\tbln You might notice, that there's actually nothing displayed above in the document. That's due to \textit{static blocks} leaving nothing behind them. To actually display the dataset, we would use a regular, non-static Ayano block: \\ [1, 1, 2, 3, 2, 5, 5, 6, 2, 1, 4, 9] \tbln Above you should see a python's `str` representation of initial dataset. \tbln Now, let's try calculating mean and the deviation. To achieve that, I'll use `statistics` module: 3.4$\pm$2.5 @@ -165,7 +165,7 @@ initial data & cmp to mean & square & absdev\\ \hline \end{center} \end{table} -\tbln Above you can see that crazy table, and you also can refer to it (\ref{tab:crazy_table}). +\tb Above you can see that crazy table, and you also can refer to it (\ref{tab:crazy_table}). \tbln That's pretty much it for Ayano blocks that do not access the filesystem. \subsection{Code blocks} @@ -177,8 +177,7 @@ data = [1, 1, 2, 3, 2, 5, 5, 6, 2, 1, 4, 9] # Keep that in mind: ALL changes introduced by static blocks will be visible by ALL function blocks. \end{minted} -That's a static block, declaring initial dataset -\newline +That's a static block, declaring initial dataset \\ \begin{minted}[linenos, mathescape, autogobble, breaklines]{python} from statistics import stdev, mean @@ -187,8 +186,7 @@ deviation = stdev(data) @dev: mean_val, deviation \end{minted} -mean and deviation calculations -\newline +mean and deviation calculations \\ \begin{minted}[linenos, mathescape, autogobble, breaklines]{python} from statistics import stdev, mean @@ -197,8 +195,7 @@ deviation = stdev(data) f"{mean_val} $\pm$ {deviation}" \end{minted} -mean and deviation calculations, but without value-error formatting -\newline +mean and deviation calculations, but without value-error formatting \\ \begin{minted}[linenos, mathescape, autogobble, breaklines]{python} # look at the last line first @@ -212,8 +209,7 @@ cells = list([[("err", val, sqrt(val)), "greater" if val >= mean_val else "less" @gen_table: lambda r,c: header[c] if r == 0 else cells[r-1][c]; rows = 13, columns = 4, ident="crazy_table", caption = "A crazy table containing a bunch of stuff" \end{minted} -that chaotic table generation example -\newline +that chaotic table generation example \\ \end{document} \ No newline at end of file diff --git a/examples/basic_syntax/output.tex b/examples/basic_syntax/output.tex index 3492273..c3e2f8f 100644 --- a/examples/basic_syntax/output.tex +++ b/examples/basic_syntax/output.tex @@ -134,14 +134,17 @@ Here's a quick rundown of supported markdown syntax: \section{Headers} \textbf{Up to 6 deep} + \tb Inline math: $y = x^2\qquad \text{- is a parabola equation}$ \tbln Display math: \begin{equation} \Gamma(z) = \int\limits_{0}^{+\infty} \exp(-t) t^{z-1} dt \qquad \text{- це Euler's Gamma-function definition} \label{eq:gamma_function} \end{equation} + \tb Page breaks: \clearpage + \tb This text must appear on the next page \tbln Figures (take note of the caption and ident): \begin{figure}[h!] @@ -150,6 +153,7 @@ Here's a quick rundown of supported markdown syntax: \caption{\capfnt These are all loss functions, technically making this image a loss meme} \label{fig:loss_meme} \end{figure} + \tb Tables: \begin{table}[h!] \fnt @@ -175,6 +179,7 @@ Here's a quick rundown of supported markdown syntax: \label{tab:pedals} \end{center} \end{table} + \tb Hyper references: \href{https://www.somewebsite.com/}{some website} \tbln Footnotes: say, I need to explain this text, but not include the actual explanation right where I write it\footnotemark[1]\footnotetext[1]{You can absolutely do this! \textit{Note}, that despite this footnote definition is on a separate page at the end, actual document output includes the footnote in a more convenient spot.} \tbln Object references: this refers to table: \ref{tab:pedals}, this refers to figure: \ref{fig:loss_meme}, an this one to the equation at the beginning: \ref{eq:gamma_function}. @@ -183,12 +188,14 @@ Here's a quick rundown of supported markdown syntax: \item First item \item Second item \item Third item -\end{itemize}\tb Here's an enumeration: +\end{itemize} +\tb Here's an enumeration: \begin{enumerate} \item First item \item Second item \item Third item -\end{enumerate}\tb Here are alphabetic enumerations (latin and cyrillic are only supported, for now) +\end{enumerate} +\tb Here are alphabetic enumerations (latin and cyrillic are only supported, for now) \begin{enumerate} \item[a.] First item \item[b.] Second item @@ -198,14 +205,16 @@ Here's a quick rundown of supported markdown syntax: \item[а.] First item \item[б.] Second item \item[в.] Third item -\end{enumerate}\tb Cyrillic implementation detail: наразі у якості порядку елементів береться буквально 'а'..'я'. Тобто, згідно з \href{https://en.wikipedia.org/wiki/Cyrillic_(Unicode_block)}{таблицею юнікоду}, тут поки що реалізовано терористичний алфавіт: +\end{enumerate} +\tb Cyrillic implementation detail: наразі у якості порядку елементів береться буквально 'а'..'я'. Тобто, згідно з \href{https://en.wikipedia.org/wiki/Cyrillic_(Unicode_block)}{таблицею юнікоду}, тут поки що реалізовано терористичний алфавіт: \begin{enumerate} \item[а.] Перший елемент \item[б.] Другий елемент \item[в.] Третій елемент \item[г.] Четвертий елемент \item[д.] Шостий елемент (не вистачає ґ) -\end{enumerate}\tb Це може змінитись у майбутньому, тож краще поки що утриматись від використання кириличних списків. +\end{enumerate} +\tb Це може змінитись у майбутньому, тож краще поки що утриматись від використання кириличних списків. \tbln Finally, code blocks are supported: \begin{minted}[linenos, mathescape, autogobble, breaklines]{lua} print("Hello lua!") @@ -238,6 +247,7 @@ impl Eq for Equivalent {} // Although I might decide to remove it in the future \end{minted} + \tb These blocks are powered by \href{https://github.com/gpoore/minted}{minted}, so to get full list of supported languages, check their docs out. \tbln This is all of the markdown syntax supported for now. However, there's an \textit{otherside} to `kyomato`'s capabilities - namely, `ayano` blocks. \clearpage diff --git a/src/gen/ayano.rs b/src/gen/ayano.rs index 5e33282..7515dc3 100644 --- a/src/gen/ayano.rs +++ b/src/gen/ayano.rs @@ -270,11 +270,11 @@ y = x / 100 function! {csv_table1, "@csv_table: src=\"src/gen/ayano/test_table.csv\", columns=[(\"a\", \"a_err\")]", "('tab', ['a'], [[('err', '1.0', '0.001')], [('err', '1.0', '0.001')], [('err', '1.0', '0.001')], [('err', '1.0', '0.001')], [('err', '1.0', '0.001')], [('err', '1.0', '0.001')], [('err', '1.0', '0.001')], [('err', '1.0', '0.001')]], None, None)"} - function! {csv_table2, "@csv_table: src=\"src/gen/ayano/test_table.csv\", rows = 3..5, columns=[(\"a\", \"a_err\"), \"b\", (\"d\", \"d_err\")]", + function! {csv_table2, "@csv_table: src=\"src/gen/ayano/test_table.csv\", rows = 3..4, columns=[(\"a\", \"a_err\"), \"b\", (\"d\", \"d_err\")]", "('tab', ['a', 'b', 'd'], [[('err', '1.0', '0.001'), '3', ('err', '-5', '0.001')]], None, None)"} function! {gen_table1, "@gen_table: lambda r,c: r + c; rows=4, columns=2", - "('tab', [[0, 1], [1, 2], [2, 3], [3, 4]], None, None)"} + "('tab', [0, 1], [[1, 2], [2, 3], [3, 4]], None, None)"} macro_rules! static_function { {$name:ident, $static_code:literal, $function_code:literal, $expected_output:literal} => { @@ -320,7 +320,7 @@ r"y = x + 1 y", "2"} static_function! {function_static_err, "x, x_err = 1.0, 0.025", "@dev: x, x_err", "('err', 1.0, 0.025)"} static_function! {function_static_gen_tab1, "table = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]", "@gen_table: lambda r,c: table[r][c]; rows = 2, columns = 3", - "('tab', [[1, 2, 3], [5, 6, 7]], None, None)"} + "('tab', [1, 2, 3], [[5, 6, 7]], None, None)"} macro_rules! static_function_function { {$name:ident, $static_code:literal, $function1_code:literal, $function2_code:literal, $expected_output1:literal, $expected_output2:literal} => { @@ -1110,20 +1110,30 @@ impl Syntax<'_> { caption, width, } => { - let src = Path::new(src); - let src = match path_engine.csv(src, insert_path.as_deref()) { - Ok(existing_table) => existing_table, - Err(error) => { - // figure was not found, but this can be ok, if program generates it itself - // but it would be wise to log this as a warn - eprintln!("Figure file not found at pre-analysis: {error:?}"); - Cow::Borrowed(Path::new(src)) - } - }; - let src = &*src.to_string_lossy(); - writer.write_str("return \"fig\", \"")?; - writer.write_str(src)?; - writer.write_str("\", ")?; + let mut src: Cow<'_, str> = Cow::Borrowed(src); + if src.starts_with(['\'', '\"']) && src.ends_with(['\'', '\"']) { + // if this insert ident is seemingly a static string, + // try checking it out, and warning if it does not exist + // TODO add option to opt this out + let src_path = src.trim_matches(['\'', '\"']); + let src_path = Path::new(&*src_path); + let src_path = match path_engine.csv(src_path, insert_path.as_deref()) { + Ok(existing_table) => existing_table, + Err(error) => { + // figure was not found, but this can be ok, if program generates it itself + // but it would be wise to log this as a warn + eprintln!("Figure file not found at pre-analysis: {error:?}"); + Cow::Borrowed(Path::new(src_path)) + } + }; + let mut s = src_path.to_string_lossy().into_owned(); + s.push('\"'); + s.insert(0, '\"'); + src = s.into(); + } + writer.write_str("return \"fig\", ")?; + writer.write_str(&src)?; + writer.write_str(", ")?; if let Some(ident) = ident { writer.write_str(ident)?; } else { @@ -1149,32 +1159,44 @@ impl Syntax<'_> { ident, caption, } => { - let src = Path::new(src); - let mut src = match path_engine.csv(src, insert_path.as_deref()) { - Ok(existing_table) => existing_table, - Err(error) => { - // table was not found, but this can be ok, if program generates it itself - // but it would be wise to log this as a warn - eprintln!("Table file not found at pre-analysis: {error:?}"); - Cow::Borrowed(src) - } - }; - // the problem here - this type of syntax specifically relies on Python reading the file themselves - // and if there is an insert path - Python will be executed at the insert directory - // to check for that, we do - if let Some(insert_directory) = insert_path.as_ref().map(|p| p.parent()).flatten() { - // if there's no insert path - ignore that - // if insert path has no parent - it's a file in current execution directory - no need to relocate the path - // I could've implemented this thing myself, but why would I, was already done: - if let Some(rebased) = pathdiff::diff_paths(&src, insert_directory) { - src = rebased.into(); + let mut src: Cow<'_, str> = Cow::Borrowed(src); + if src.starts_with(['\'', '\"']) && src.ends_with(['\'', '\"']) { + // if this insert ident is seemingly a static string, + // try checking it out, and warning if it does not exist + // TODO add option to opt this out + let src_path = src.trim_matches(['\'', '\"']); + let src_path = Path::new(&*src_path); + let mut src_path = match path_engine.csv(src_path, insert_path.as_deref()) { + Ok(existing_table) => existing_table, + Err(error) => { + // table was not found, but this can be ok, if program generates it itself + // but it would be wise to log this as a warn + eprintln!("Table file not found at pre-analysis: {error:?}"); + Cow::Borrowed(Path::new(src_path)) + } + }; + // the problem here - this type of syntax specifically relies on Python reading the file themselves + // and if there is an insert path - Python will be executed at the insert directory + // to check for that, we do + if let Some(insert_directory) = + insert_path.as_ref().map(|p| p.parent()).flatten() + { + // if there's no insert path - ignore that + // if insert path has no parent - it's a file in current execution directory - no need to relocate the path + // I could've implemented this thing myself, but why would I, was already done: + if let Some(rebased) = pathdiff::diff_paths(&src_path, insert_directory) { + src_path = rebased.into(); + } } + let mut s = src_path.to_string_lossy().into_owned(); + s.push('\"'); + s.insert(0, '\"'); + src = s.into(); } - let src = &*src.to_string_lossy(); writer.write_line("import csv")?; - writer.write_str("with open('")?; - writer.write_str(src)?; - writer.write_line("') as ___csv_file___:")?; + writer.write_str("with open(")?; + writer.write_str(&src)?; + writer.write_line(") as ___csv_file___:")?; writer.write_line( " ___dialect___ = csv.Sniffer().sniff(___csv_file___.read(1024))", )?; @@ -1235,7 +1257,7 @@ impl Syntax<'_> { // If a dedicated line generator is necessary, write out one now if let Some(rows_to) = rows.to() { - let line_iterations = rows_to - rows.from().unwrap_or_default() + 1; + let line_iterations = rows_to - rows.from().unwrap_or(1) + 1; writer.write_line(" def ___line_generator___():")?; write!(writer, " for i in range({line_iterations}):")?; // it's again numbers, can't do much better writer.breakline()?; @@ -1283,7 +1305,11 @@ impl Syntax<'_> { writer.write_str("___generator___ = ")?; writer.write_line(generator)?; writer.write_str("return \"tab\", [")?; - for row_index in 0..rows { + for column_index in 0..columns { + write!(writer, "___generator___(0, {column_index}), ")?; + } + writer.write_str("], [")?; + for row_index in 1..rows { writer.write_str("[")?; for column_index in 0..columns { write!(writer, "___generator___({row_index}, {column_index}), ")?; @@ -1347,7 +1373,9 @@ mod syntax_display_tests { }}, r#"import csv with open(file_path) as ___csv_file___: - ___reader___ = csv.DictReader(___csv_file___, delimiter=',', skipinitialspace=True, strict=True) + ___dialect___ = csv.Sniffer().sniff(___csv_file___.read(1024)) + ___csv_file___.seek(0) + ___reader___ = csv.DictReader(___csv_file___, dialect=___dialect___, skipinitialspace=True) def ___line_generator___(): for i in range(10): line = ___reader___.__next__() @@ -1356,11 +1384,11 @@ with open(file_path) as ___csv_file___: } test! {gen_table1, SyntaxData {indent: 0, syntax: Syntax::GenTable { generator: "lambda r,c:1", rows: 2, columns: 3, ident: None, caption: Some("caption_variable") }}, r#"___generator___ = lambda r,c:1 -return "tab", [[___generator___(0, 0), ___generator___(0, 1), ___generator___(0, 2), ], [___generator___(1, 0), ___generator___(1, 1), ___generator___(1, 2), ], ], None, caption_variable"# +return "tab", [___generator___(0, 0), ___generator___(0, 1), ___generator___(0, 2), ], [[___generator___(1, 0), ___generator___(1, 1), ___generator___(1, 2), ], ], None, caption_variable"# } test! {gen_table2, SyntaxData {indent: 4, syntax: Syntax::GenTable { generator: "lambda r,c:1", rows: 2, columns: 3, ident: Some("\"table2\""), caption: None }}, r#" ___generator___ = lambda r,c:1 - return "tab", [[___generator___(0, 0), ___generator___(0, 1), ___generator___(0, 2), ], [___generator___(1, 0), ___generator___(1, 1), ___generator___(1, 2), ], ], "table2", None"# + return "tab", [___generator___(0, 0), ___generator___(0, 1), ___generator___(0, 2), ], [[___generator___(1, 0), ___generator___(1, 1), ___generator___(1, 2), ], ], "table2", None"# } } @@ -1418,7 +1446,9 @@ mod last_line_tests { test! {csv_tab1e_simple, "@csv_table: src=\"some/path/idk.csv\"", r#" import csv with open("some/path/idk.csv") as ___csv_file___: - ___reader___ = csv.DictReader(___csv_file___, delimiter=',', skipinitialspace=True, strict=True) + ___dialect___ = csv.Sniffer().sniff(___csv_file___.read(1024)) + ___csv_file___.seek(0) + ___reader___ = csv.DictReader(___csv_file___, dialect=___dialect___, skipinitialspace=True) return "tab", list(___reader___.fieldnames), list([list([line[f] for f in line]) for line in ___reader___]), None, None "#} @@ -1426,19 +1456,25 @@ with open("some/path/idk.csv") as ___csv_file___: test! {csv_tab1e_rows1, "@csv_table: src=\"some/path/idk.csv\", rows=..", r#" import csv with open("some/path/idk.csv") as ___csv_file___: - ___reader___ = csv.DictReader(___csv_file___, delimiter=',', skipinitialspace=True, strict=True) + ___dialect___ = csv.Sniffer().sniff(___csv_file___.read(1024)) + ___csv_file___.seek(0) + ___reader___ = csv.DictReader(___csv_file___, dialect=___dialect___, skipinitialspace=True) return "tab", list(___reader___.fieldnames), list([list([line[f] for f in line]) for line in ___reader___]), None, None "#} test! {csv_tab1e_rows2, "@csv_table: src=\"some/path/idk.csv\", rows=1..", r#" import csv with open("some/path/idk.csv") as ___csv_file___: - ___reader___ = csv.DictReader(___csv_file___, delimiter=',', skipinitialspace=True, strict=True) + ___dialect___ = csv.Sniffer().sniff(___csv_file___.read(1024)) + ___csv_file___.seek(0) + ___reader___ = csv.DictReader(___csv_file___, dialect=___dialect___, skipinitialspace=True) return "tab", list(___reader___.fieldnames), list([list([line[f] for f in line]) for line in ___reader___]), None, None "#} test! {csv_tab1e_rows3, "@csv_table: src=\"some/path/idk.csv\", rows=10..", r#" import csv with open("some/path/idk.csv") as ___csv_file___: - ___reader___ = csv.DictReader(___csv_file___, delimiter=',', skipinitialspace=True, strict=True) + ___dialect___ = csv.Sniffer().sniff(___csv_file___.read(1024)) + ___csv_file___.seek(0) + ___reader___ = csv.DictReader(___csv_file___, dialect=___dialect___, skipinitialspace=True) for i in range(9): ___reader___.__next__() return "tab", list(___reader___.fieldnames), list([list([line[f] for f in line]) for line in ___reader___]), None, None @@ -1446,17 +1482,21 @@ with open("some/path/idk.csv") as ___csv_file___: test! {csv_tab1e_rows4, "@csv_table: src=\"some/path/idk.csv\", rows=..10", r#" import csv with open("some/path/idk.csv") as ___csv_file___: - ___reader___ = csv.DictReader(___csv_file___, delimiter=',', skipinitialspace=True, strict=True) + ___dialect___ = csv.Sniffer().sniff(___csv_file___.read(1024)) + ___csv_file___.seek(0) + ___reader___ = csv.DictReader(___csv_file___, dialect=___dialect___, skipinitialspace=True) def ___line_generator___(): for i in range(9): line = ___reader___.__next__() yield list([line[f] for f in line]) return "tab", list(___reader___.fieldnames), list(___line_generator___()), None, None "#} - test! {csv_tab1e_rows5, "@csv_table: src=\"some/path/idk.csv\", rows=5..=100", r#" + test! {csv_tab1e_rows5, "@csv_table: src=\"some/path/idk.csv\", rows=5..100", r#" import csv with open("some/path/idk.csv") as ___csv_file___: - ___reader___ = csv.DictReader(___csv_file___, delimiter=',', skipinitialspace=True, strict=True) + ___dialect___ = csv.Sniffer().sniff(___csv_file___.read(1024)) + ___csv_file___.seek(0) + ___reader___ = csv.DictReader(___csv_file___, dialect=___dialect___, skipinitialspace=True) for i in range(4): ___reader___.__next__() def ___line_generator___(): @@ -1465,12 +1505,14 @@ with open("some/path/idk.csv") as ___csv_file___: yield list([line[f] for f in line]) return "tab", list(___reader___.fieldnames), list(___line_generator___()), None, None "#} - test! {csv_tab1e_rows6, "@csv_table: src=\"some/path/idk.csv\", rows=1..100", r#" + test! {csv_tab1e_rows6, "@csv_table: src=\"some/path/idk.csv\", rows=1..=100", r#" import csv with open("some/path/idk.csv") as ___csv_file___: - ___reader___ = csv.DictReader(___csv_file___, delimiter=',', skipinitialspace=True, strict=True) + ___dialect___ = csv.Sniffer().sniff(___csv_file___.read(1024)) + ___csv_file___.seek(0) + ___reader___ = csv.DictReader(___csv_file___, dialect=___dialect___, skipinitialspace=True) def ___line_generator___(): - for i in range(98): + for i in range(100): line = ___reader___.__next__() yield list([line[f] for f in line]) return "tab", list(___reader___.fieldnames), list(___line_generator___()), None, None @@ -1480,25 +1522,33 @@ with open("some/path/idk.csv") as ___csv_file___: test! {csv_tab1e_columns1, r#"@csv_table: src="some/path/idk.csv", columns = []"#, r#" import csv with open("some/path/idk.csv") as ___csv_file___: - ___reader___ = csv.DictReader(___csv_file___, delimiter=',', skipinitialspace=True, strict=True) + ___dialect___ = csv.Sniffer().sniff(___csv_file___.read(1024)) + ___csv_file___.seek(0) + ___reader___ = csv.DictReader(___csv_file___, dialect=___dialect___, skipinitialspace=True) return "tab", [], list([[] for line in ___reader___]), None, None "#} test! {csv_tab1e_columns2, r#"@csv_table: src="some/path/idk.csv", columns = ["a"]"#, r#" import csv with open("some/path/idk.csv") as ___csv_file___: - ___reader___ = csv.DictReader(___csv_file___, delimiter=',', skipinitialspace=True, strict=True) + ___dialect___ = csv.Sniffer().sniff(___csv_file___.read(1024)) + ___csv_file___.seek(0) + ___reader___ = csv.DictReader(___csv_file___, dialect=___dialect___, skipinitialspace=True) return "tab", ["a", ], list([[line["a"], ] for line in ___reader___]), None, None "#} test! {csv_tab1e_columns3, r#"@csv_table: src="some/path/idk.csv", columns = ["a", ("b", "b_err")]"#, r#" import csv with open("some/path/idk.csv") as ___csv_file___: - ___reader___ = csv.DictReader(___csv_file___, delimiter=',', skipinitialspace=True, strict=True) + ___dialect___ = csv.Sniffer().sniff(___csv_file___.read(1024)) + ___csv_file___.seek(0) + ___reader___ = csv.DictReader(___csv_file___, dialect=___dialect___, skipinitialspace=True) return "tab", ["a", "b", ], list([[line["a"], ("err", line["b"], line["b_err"]), ] for line in ___reader___]), None, None "#} test! {csv_table_combined1, r#"@csv_table: src="some/path/idk.csv", columns = ["a", ("b", "b_err")], rows =..10"#, r#" import csv with open("some/path/idk.csv") as ___csv_file___: - ___reader___ = csv.DictReader(___csv_file___, delimiter=',', skipinitialspace=True, strict=True) + ___dialect___ = csv.Sniffer().sniff(___csv_file___.read(1024)) + ___csv_file___.seek(0) + ___reader___ = csv.DictReader(___csv_file___, dialect=___dialect___, skipinitialspace=True) def ___line_generator___(): for i in range(9): line = ___reader___.__next__() @@ -1508,7 +1558,23 @@ with open("some/path/idk.csv") as ___csv_file___: test! {csv_table_combined2, r#"@csv_table: src="some/path/idk.csv", columns = ["a", ("b", "b_err")], rows =10..=100"#, r#" import csv with open("some/path/idk.csv") as ___csv_file___: - ___reader___ = csv.DictReader(___csv_file___, delimiter=',', skipinitialspace=True, strict=True) + ___dialect___ = csv.Sniffer().sniff(___csv_file___.read(1024)) + ___csv_file___.seek(0) + ___reader___ = csv.DictReader(___csv_file___, dialect=___dialect___, skipinitialspace=True) + for i in range(9): + ___reader___.__next__() + def ___line_generator___(): + for i in range(91): + line = ___reader___.__next__() + yield [line["a"], ("err", line["b"], line["b_err"]), ] + return "tab", ["a", "b", ], list(___line_generator___()), None, None + "#} + test! {csv_table_combined3, r#"@csv_table: src="some/path/idk.csv", columns = ["a", ("b", "b_err")], rows =10..100"#, r#" +import csv +with open("some/path/idk.csv") as ___csv_file___: + ___dialect___ = csv.Sniffer().sniff(___csv_file___.read(1024)) + ___csv_file___.seek(0) + ___reader___ = csv.DictReader(___csv_file___, dialect=___dialect___, skipinitialspace=True) for i in range(9): ___reader___.__next__() def ___line_generator___(): @@ -1517,25 +1583,13 @@ with open("some/path/idk.csv") as ___csv_file___: yield [line["a"], ("err", line["b"], line["b_err"]), ] return "tab", ["a", "b", ], list(___line_generator___()), None, None "#} - test! {csv_table_combined3, r#"@csv_table: src="some/path/idk.csv", columns = ["a", ("b", "b_err")], rows =10..100"#, r#" -import csv -with open("some/path/idk.csv") as ___csv_file___: - ___reader___ = csv.DictReader(___csv_file___, delimiter=',', skipinitialspace=True, strict=True) - for i in range(9): - ___reader___.__next__() - def ___line_generator___(): - for i in range(89): - line = ___reader___.__next__() - yield [line["a"], ("err", line["b"], line["b_err"]), ] - return "tab", ["a", "b", ], list(___line_generator___()), None, None - "#} test! {gen_table1, r#"@gen_table: lambda row, col: 1; rows= 5, columns =2"#, r#"___generator___ = lambda row, col: 1 -return "tab", [[___generator___(0, 0), ___generator___(0, 1), ], [___generator___(1, 0), ___generator___(1, 1), ], [___generator___(2, 0), ___generator___(2, 1), ], [___generator___(3, 0), ___generator___(3, 1), ], [___generator___(4, 0), ___generator___(4, 1), ], ], None, None"#} +return "tab", [___generator___(0, 0), ___generator___(0, 1), ], [[___generator___(1, 0), ___generator___(1, 1), ], [___generator___(2, 0), ___generator___(2, 1), ], [___generator___(3, 0), ___generator___(3, 1), ], [___generator___(4, 0), ___generator___(4, 1), ], ], None, None"#} test! {gen_table2, r#"@gen_table: lambda row, col: -1;columns =2, rows= 5"#, r#"___generator___ = lambda row, col: -1 -return "tab", [[___generator___(0, 0), ___generator___(0, 1), ], [___generator___(1, 0), ___generator___(1, 1), ], [___generator___(2, 0), ___generator___(2, 1), ], [___generator___(3, 0), ___generator___(3, 1), ], [___generator___(4, 0), ___generator___(4, 1), ], ], None, None"#} +return "tab", [___generator___(0, 0), ___generator___(0, 1), ], [[___generator___(1, 0), ___generator___(1, 1), ], [___generator___(2, 0), ___generator___(2, 1), ], [___generator___(3, 0), ___generator___(3, 1), ], [___generator___(4, 0), ___generator___(4, 1), ], ], None, None"#} } #[derive(Debug, PartialEq, thiserror::Error)] @@ -1801,8 +1855,8 @@ mod parsing_tests { test! {table_nocaption, |py| { PyTuple::new(py, [ "tab".into_py(py), + PyList::new(py, ["header1".into_py(py), 1.0.into_py(py)]).into_py(py), PyList::new(py, [ - PyList::new(py, ["header1".into_py(py), 1.0.into_py(py)]), PyList::new(py, ["cell_11".into_py(py), 0.25.into_py(py)]), PyList::new(py, ["cell_21".into_py(py), (-2).into_py(py)]), ]).into_py(py), @@ -1822,8 +1876,8 @@ mod parsing_tests { test! {table_value_error, |py| { PyTuple::new(py, [ "tab".into_py(py), + PyList::new(py, ["header1".into_py(py), 1.0.into_py(py)]).into_py(py), PyList::new(py, [ - PyList::new(py, ["header1".into_py(py), 1.0.into_py(py)]), PyList::new(py, [PyTuple::new(py, [ "err".into_py(py), 1.012345.into_py(py), @@ -1847,8 +1901,8 @@ mod parsing_tests { test! {table_caption, |py| { PyTuple::new(py, [ <&'static str as IntoPy>>::into_py("tab", py), + PyList::new(py, ["header1".into_py(py), 1.0.into_py(py)]).into_py(py), PyList::new(py, [ - PyList::new(py, ["header1".into_py(py), 1.0.into_py(py)]), PyList::new(py, ["cell_11".into_py(py), 0.25.into_py(py)]), PyList::new(py, ["cell_21".into_py(py), (-2).into_py(py)]), ]).into_py(py), @@ -2780,15 +2834,15 @@ fn parse_columns<'l>(input: &'l str) -> Vec> { static REGEX: Lazy = Lazy::new(|| Regex::new(REGEX_STR).expect("Bad regex syntax")); REGEX .captures_iter(input) - .map(|cap: Captures<'l>| { - match (cap.get(4), cap.get(1), cap.get(2)) { + .map( + |cap: Captures<'l>| match (cap.get(4), cap.get(1), cap.get(2)) { (Some(col), _, _) => CsvTableColumn::Single(col.as_str().into()), (_, Some(value), Some(error)) => { CsvTableColumn::ValueError(value.as_str(), error.as_str()) } _ => unreachable!(), - } - }) + }, + ) .collect_vec() } @@ -2828,5 +2882,4 @@ mod parse_columns_tests { test! {doc1, r#"["type",("a", "a_err")]"#, [c!("type"), ve!("a", "a_err")]} test! {maybe_bug1, r#"["name", ("value", "err"), "a"]"#, [c!("name"), ve!("value", "err"), c!("a")]} test! {bug1, r#"["name", ("value", "err"), "%"]"#, [c!("name"), ve!("value", "err"), c!("%")]} - } diff --git a/src/gen/ayano/python.rs b/src/gen/ayano/python.rs index 1cc4f4a..146a943 100644 --- a/src/gen/ayano/python.rs +++ b/src/gen/ayano/python.rs @@ -127,7 +127,7 @@ mod tests { &token, "absent_function", |res, _| Ok::<_, super::PyErr>(res.str()?.to_string()), - "", + ".", ) .expect_err("Should get error for non-existent function call"); } @@ -144,7 +144,7 @@ mod tests { &token, "ok_function", |res, _| Ok::<_, super::PyErr>(res.str()?.to_string()), - "", + ".", ); // assert diff --git a/src/gen/lab/mod.rs b/src/gen/lab/mod.rs index 471e427..ea0132c 100644 --- a/src/gen/lab/mod.rs +++ b/src/gen/lab/mod.rs @@ -868,7 +868,7 @@ y = 0.4 ident: StaticDebug(rand::thread_rng().gen()), display_state: DisplayState::NotDisplayed, is_static: false, - code: r"'fig', 'path/to/image.jpg', None, None, 0.625".into(), + code: "'fig', \"path/to/image.jpg\", None, None, 0.625".into(), insert_path: None, spaces_before: 0, }}, r" @@ -878,16 +878,14 @@ y = 0.4 \end{figure}" } - test! {ayano_fig2, Token::Ayano{data: AyanoBlock{ - ident: StaticDebug(rand::thread_rng().gen()),display_state: DisplayState::NotDisplayed,is_static: false,code: r"@fig: src = 'path/to/image.jpg'".into(),insert_path: None, - spaces_before: 0,}}, + test! {ayano_fig2, Token::Ayano{data: AyanoBlock{ident: StaticDebug(rand::thread_rng().gen()),display_state: DisplayState::NotDisplayed,is_static: false,code: "@fig: src = \"path/to/image.jpg\"".into(),insert_path: None,spaces_before: 0,}}, r" \begin{figure}[h!] \centering \includegraphics[width = 0.9 \textwidth]{path/to/image.jpg} \end{figure}"} test! {ayano_fig3, Token::Ayano{data: AyanoBlock{ - ident: StaticDebug(rand::thread_rng().gen()), display_state: DisplayState::NotDisplayed,is_static: false,code: r#"@fig: src = 'path/to/image.jpg', ident = "meow""#.into(),insert_path: None, + ident: StaticDebug(rand::thread_rng().gen()), display_state: DisplayState::NotDisplayed,is_static: false,code: r#"@fig: src = "path/to/image.jpg", ident = "meow""#.into(),insert_path: None, spaces_before: 0, }}, r#" diff --git a/tests/transformed_files/ayano.txt b/tests/transformed_files/ayano.txt index f24a7a8..54ed3a7 100644 --- a/tests/transformed_files/ayano.txt +++ b/tests/transformed_files/ayano.txt @@ -139,13 +139,13 @@ This is a test of ayano-generated stuff. \label{fig:circle} \end{figure} -\tbln Here's a table with specified width: +\tb Here's a table with specified width: \begin{figure}[h!] \centering \includegraphics[width=0.2\textwidth]{graph.png} \end{figure} -\tbln This is expected to be a generated table: +\tb This is expected to be a generated table: \begin{table}[h!] \fnt \begin{center} diff --git a/tests/transformed_files/ayano_display_blocks.txt b/tests/transformed_files/ayano_display_blocks.txt index 129ef3d..1f8cefc 100644 --- a/tests/transformed_files/ayano_display_blocks.txt +++ b/tests/transformed_files/ayano_display_blocks.txt @@ -132,6 +132,7 @@ \fnt \section{Display blocks} + \tb This feature allows you to insert certain `Ayano` blocks to the end of the output document, to document your exact actions for displayed results. \tbln Here's an example: 42 \tbln Display blocks can have descriptions too! Here's one: -20340$\pm$150 @@ -142,8 +143,8 @@ 42 \end{minted} -\tb ayano-unnamed -\newline + +\tb ayano-unnamed \\ \begin{minted}[linenos, mathescape, autogobble, breaklines]{python} # This code block has description @@ -154,8 +155,7 @@ for i in range(100): @dev: -2.034e4, 145.64 \end{minted} -me, fooling around -\newline +me, fooling around \\ \end{document} \ No newline at end of file diff --git a/tests/transformed_files/basic.txt b/tests/transformed_files/basic.txt index e9d3a6a..9ff616e 100644 --- a/tests/transformed_files/basic.txt +++ b/tests/transformed_files/basic.txt @@ -132,6 +132,7 @@ \fnt \section{Перша тема} + \tb Тут трохи тексту і пояснень про квантові мультивироджені рефурбалідзери змішаного типу. Це нудно, тож тримайте формулу: \begin{equation} Ref(z) = \int \limits_{0}^{1} t^{z-1} \cdot \exp(-t) dt @@ -139,6 +140,7 @@ Ref(z) = \int \limits_{0}^{1} t^{z-1} \cdot \exp(-t) dt \end{equation} \subsection{Підпункт} + \tb Рефурбалідзери можуть виявитись дуже важливими для збереження екологічної безпеки нашої планети. До прикладу, в них є наступні переваги: \begin{itemize} \item Нормувальний множник $|\psi|^{2} = 3/4$ @@ -151,6 +153,7 @@ Ref(z) = \int \limits_{0}^{1} t^{z-1} \cdot \exp(-t) dt \caption{\capfnt Принципове влаштування рефурбалідзера} \label{fig:refurb} \end{figure} + \tb На світлині \ref{fig:refurb} можна бачити принципову схему влаштування рефурбалідзера. Неважно помітити великі очі\footnotemark[1]\footnotetext[1]{Для поливу.}, і два термоядерних реактори, що оточують їх. Точні реакції всередині невідомі, але цілком можливо що там женуть $du + t -> {}^{5}_{6}C$... \end{document} \ No newline at end of file diff --git a/tests/transformed_files/deceit.txt b/tests/transformed_files/deceit.txt index 61f0ee2..b1a5ea2 100644 --- a/tests/transformed_files/deceit.txt +++ b/tests/transformed_files/deceit.txt @@ -132,23 +132,28 @@ \fnt This test is dedicated to attempts of parser deception, i.e. usage of special symbols in wrong places to see if parser recognizes them as control symbols. There we go: \section{Page dividers} + \tb Page dividers are represented by ------ \tbln If the line above has dashes ^^^^^^ here, it probably works alright. \tbln Here's a genuine divider \clearpage + \tb Fun fact: there can be a paragraph right after it! \clearpage Some text, cause why not \tbln But this \textbf{DOES} look cursed, don't do it. \section{Headers} + \tb Headers are most recognizable by '#'-symbol. For it to actually represent a header, it needs to at the beginning of the line and there should be no more than 6 of them. \textbf{ANYTHING} up to the end of line will be included into a header, then. \tbln Example: \textbf{This is a long, yet formally correct header. You can add $equations$ and *formatting* here, but I provide no guarantees as of the way these would display in final document.} + \tb Here's an example of incorrect header: \tbln ###### This header is considered incorrect for this application, and will be parsed as regular paragraph instead \tbln Another example of incorrect header, featuring 7 hash-symbols: \tbln ####### This will be parsed as a regular paragraph \section{Equations} + \tb There are two types of equations in markdown/latex: inline mathmode and display mathmode \tbln Both of them are defined by $-symbols. However, they must be paired to actually represent these. Here are examples: \tbln Inline: $y = x ^ 2$ @@ -156,18 +161,21 @@ This test is dedicated to attempts of parser deception, i.e. usage of special sy \begin{equation} y = x ^ 2 \end{equation} + \tb Note that display mathmode should ALWAYS start from a new line for this application. \tbln Basically, there's not much to deceive for: you can do that: \tbln $ \tbln $ \tbln (the above should not be mathmode), and that's it, basically :sad: \section{Table} + \tb Tables are defined by |-symbol. But the actual table will happen only if they are the first thing on the line. \tbln Due to current implementation, there's no cure apart from display mathmode and code: \tbln (this should be display mathmode): \begin{equation} |x| \end{equation} + \tb (this should be a code block): \begin{minted}[linenos, mathescape, autogobble, breaklines]{bash} head /dev/random @@ -176,6 +184,7 @@ head /dev/random \section{Figure} + \tb Figure is defined by a ![[ sequence at a start of the line. Same points should be uphold here: \begin{equation} ![[ @@ -187,8 +196,10 @@ head /dev/random \section{References} + \tb These start with [] \subsection{Hyperrefs} + \tb These are followed by parentheses (). Nothing is allowed between them, thus \tbln Here's a correct href: \href{https://youtu.be/rickroll_link_lol}{my gh page} \tbln Here are incorrect refs: @@ -196,6 +207,7 @@ head /dev/random \tbln [my gh page] \tbln (https://youtu.be/rickroll\textit{link}lol) \subsection{Footnote refs} + \tb These are started by [^. \tbln Here are correct footnote refs: \footnotemark[1], \footnotemark[2] \tbln Here are incorrect ones: [^1, ^1], @@ -204,6 +216,7 @@ head /dev/random \tbln ] \tbln So on \subsection{Object refs} + \tb These refer to objects like figures, tables and equations. \tbln Started by [@. \tbln Here's a correct ref: \ref{fig:experiment_schema} @@ -213,15 +226,18 @@ head /dev/random \tbln ] \tbln \textbf{NOTE}: despite object types being prepended with 'eq', 'fig' or 'tab' in refs, you \textit{can} use regular latex to define your custom refs. In fact, LaTeX doesn't care about all of these at all; I'm doing these transformations just for my own comfort, as I've been doing this manually. \section{Formatting} + \tb This object's type is deprecated and will be removed soon. \tbln It starts and ends with \begin{itemize} \item * or _ \item ** or __ \item ~~ -\end{itemize}\tb \textbf{WARN}: As of now, these \textit{might} cause cause massive problems if encountered at the start of the line. +\end{itemize} +\tb \textbf{WARN}: As of now, these \textit{might} cause cause massive problems if encountered at the start of the line. \tbln Basically, parsers for these tokens attempt parse everything up until they find a closing token. But if they are inserted in a middle of the text, then it's ok - they will be limited by current paragraph, and (most likely) will find no match inside it, if their use was unintended. \section{Footnote content} + \tb These start with '\footnotemark[3]:' and proceed to the end of line. \tbln Example of valid content: \tbln Example of invalid content: @@ -229,8 +245,10 @@ head /dev/random \tbln They also must start at the beginning of the line. If fact, this: \footnotemark[4]: Some content \tbln Is expected to be a footnote ref with a colon. \section{Code blocks} + \tb There are two fundamental types of code blocks: normal code block and \textbf{Ayano} code block. \subsection{Normal code block} + \tb These start and end with '```' on a newline. There can be \textbf{practically anything} inside. Example: \begin{minted}[linenos, mathescape, autogobble, breaklines]{java} int x = 2; @@ -240,6 +258,7 @@ arr[0] = x; arr[1] = 4; \end{minted} + \tb There can be anything after code block had closed: \begin{minted}[linenos, mathescape, autogobble, breaklines]{lua(Idk why vscode detects this block closing as a valid lua syntax)} local x = 'matt' @@ -255,6 +274,7 @@ local z = x .. y -- matty!! \tbln print(x) \tbln ``` \subsection{Ayano code block} + \tb \textit{because Ayano loves Kyoko} \tbln This object looks and behaves just like normal code block, but is only limited to \textit{python} language, can have spacial arguments in a language line, may leave something behind in the document. For example: \tbln This Ayano block will leave plain "2" behind it:2 @@ -269,6 +289,7 @@ local z = x .. y -- matty!! 2 \end{minted} + \tb ```ayano \tbln 2 \tbln ``` diff --git a/tests/transformed_files/edgecases.txt b/tests/transformed_files/edgecases.txt index 7bbf591..94bbdc0 100644 --- a/tests/transformed_files/edgecases.txt +++ b/tests/transformed_files/edgecases.txt @@ -135,10 +135,12 @@ \begin{itemize} \item List item 1 \item List item 2 -\end{itemize}\tb Some para +\end{itemize} +\tb Some para \begin{itemize} \item List item 1 \item List item 2 -\end{itemize}\tb \textit{This bug resulted in lists to be parsed as series of paragraphs.} +\end{itemize} +\tb \textit{This bug resulted in lists to be parsed as series of paragraphs.} \end{document} \ No newline at end of file diff --git a/tests/transformed_files/tables_code.txt b/tests/transformed_files/tables_code.txt index 96c937b..682fa20 100644 --- a/tests/transformed_files/tables_code.txt +++ b/tests/transformed_files/tables_code.txt @@ -132,6 +132,7 @@ \fnt Наступний тест. Тут протестуємо таблиці і блоки коду. \subsection{Таблиці} + \tb Звичайні таблиці; Можуть всередині мати \textbf{формули} чи \textit{посилання} на інші штуки \begin{table}[h!] \fnt @@ -147,6 +148,7 @@ \label{tab:example_1} \end{center} \end{table} + \tb Таблиця цілком собі може не мати ідентифікатора або опису: \begin{table}[h!] \fnt @@ -162,6 +164,7 @@ \end{table} \subsection{Блоки коду} + \tb Наразі мають досить обмежену підтримку, адже у $\LaTeX$ немає нативного поняття блоків коду. \tbln На щастя, у \textbf{markdown} таке поняття є\footnotemark[2]\footnotetext[2]{Принаймні, \textit{Obsidian} таке робить}: \begin{minted}[linenos, mathescape, autogobble, breaklines]{rust} diff --git a/tests/transformed_files/title_info.txt b/tests/transformed_files/title_info.txt index 701c2c9..1069a26 100644 --- a/tests/transformed_files/title_info.txt +++ b/tests/transformed_files/title_info.txt @@ -132,6 +132,7 @@ \fnt \section{First para} + \tb This text is \textit{just} a dummy, so that document won't feel \st{too} \textbf{empty} \tbln Have a some oily maccaroni: \begin{equation} From 887ee56ab4c8410a252c6cd7a9906777d5de56fc Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Thu, 28 Mar 2024 12:19:48 +0200 Subject: [PATCH 55/98] Added usage of dynamic file locations to example --- examples/advanced_ayano/output.tex | 39 +++++++++++++++++++++++++ examples/advanced_ayano/source/input.md | 16 +++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/examples/advanced_ayano/output.tex b/examples/advanced_ayano/output.tex index 7c47397..0d2924e 100644 --- a/examples/advanced_ayano/output.tex +++ b/examples/advanced_ayano/output.tex @@ -279,6 +279,29 @@ zeta & 4444.00$\pm$0.03 & 0.0\\ \hline \end{figure} \tb Note, that in the last example, script actually saves sine into a folder near input markdown fine, and yet `Kyomato` will be able to find it (if configured properly, that is) +\tbln Note that essentially all of the paths used to identify files, are specified with quotes. That's because it can actually be dynamic, and determined at runtime! +\begin{figure}[h!] +\centering +\includegraphics[width=0.2\textwidth]{./source/other_sine_graph.png} +\caption{\capfnt The same sine. Again.} +\label{fig:yet_another_sine_graph} +\end{figure} + +\begin{table}[h!] +\fnt +\begin{center} +\begin{tabular}{|c|c|c|c|}\hline +name & value & err & \%\\ \hline +\hline +tau & 4.3535223 & 0.0034342 & 8e-06\\ \hline +pi & 4334.34434 & 10 & 2.3e-05\\ \hline +chi & -2332.332424 & 1e-07 & -0.0\\ \hline +zeta & 4444.0 & 0.033 & 0.0\\ \hline +\end{tabular} +\stepcounter{tabnum}\end{center} +\end{table} + +\tb Note, of course, that this removes `Kyomato`'s file validity checking and other path processing. So if you happen to resort to that, make sure that Python is able to find the file by themselves, from the directory it executes in. \subsection{Code blocks} \begin{minted}[linenos, mathescape, autogobble, breaklines]{python} @@ -491,5 +514,21 @@ plt.savefig("../source/other_sine_graph.png") That's a third way to do the same thing! \\ +\begin{minted}[linenos, mathescape, autogobble, breaklines]{python} +fig_path= 'source/other_sine_graph.png' +@fig: src = fig_path, ident = "yet_another_sine_graph", caption = "The same sine. Again.", width = 0.2 +\end{minted} + + +\tb ayano-unnamed \\ + +\begin{minted}[linenos, mathescape, autogobble, breaklines]{python} +tab_path = 'data/calculations_table.csv' +@csv_table: src=tab_path +\end{minted} + + +\tb ayano-unnamed \\ + \end{document} \ No newline at end of file diff --git a/examples/advanced_ayano/source/input.md b/examples/advanced_ayano/source/input.md index c545f10..b3ae648 100644 --- a/examples/advanced_ayano/source/input.md +++ b/examples/advanced_ayano/source/input.md @@ -111,4 +111,18 @@ And of course you can generate a figure with inserted block too! @fig: src = "other_sine_graph.png", ident = "yet_another_sine_graph", caption = "The same sine. Again.", width = 0.2 ``` -Note, that in the last example, script actually saves sine into a folder near input markdown fine, and yet `Kyomato` will be able to find it (if configured properly, that is) \ No newline at end of file +Note, that in the last example, script actually saves sine into a folder near input markdown fine, and yet `Kyomato` will be able to find it (if configured properly, that is) + +Note that essentially all of the paths used to identify files, are specified with quotes. That's because it can actually be dynamic, and determined at runtime! + +```python, Ayano * +fig_path= 'source/other_sine_graph.png' +@fig: src = fig_path, ident = "yet_another_sine_graph", caption = "The same sine. Again.", width = 0.2 +``` + +```python, Ayano * +tab_path = 'data/calculations_table.csv' +@csv_table: src=tab_path +``` + +Note, of course, that this removes `Kyomato`'s file validity checking and other path processing. So if you happen to resort to that, make sure that Python is able to find the file by themselves, from the directory it executes in. \ No newline at end of file From aa0583b66a58e9a489e3bfe0792775cba3ba0abe Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Thu, 28 Mar 2024 16:21:07 +0200 Subject: [PATCH 56/98] Added example with title info (no, I won't disclose said subreddit, you can probably reverse-engineer it yourself) --- README.md | 121 +++++++++++++++++++++++++++- examples/title_info/input.md | 23 ++++++ examples/title_info/main.rs | 23 ++++++ examples/title_info/output.tex | 140 +++++++++++++++++++++++++++++++++ 4 files changed, 305 insertions(+), 2 deletions(-) create mode 100644 examples/title_info/input.md create mode 100644 examples/title_info/main.rs create mode 100644 examples/title_info/output.tex diff --git a/README.md b/README.md index ee8ddcd..2732fac 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,120 @@ -*Kyoko's being a tomato. -Why? I don't know :idk:* +*Kyoko is being a tomato. Why? I don't know :idk:* +# Overview + +`Kyomato` allows transformation of extended markdown subset into a $\LaTeX$ document, ready for compilation. +You can use helper CLI script or write your own, to then compile said $\LaTeX$, of transform in some other way. + +### WHY NOT PANDOC??? + +Please, the "**Why?**" paragraph at the very bottom about the reasons I developed this thing. I doubt you can effortlessly do all that with `pandoc` +(I'm not willing to die trying). + +# Walkthrough +**PLEASE NOTE**: rules described below are fairly strict, since there's no literal markdown parser inside. `Kyomato`'s design is "*un-optimal*" right now, since I need it to work **NOW**. + +## Markdown-inspired syntax + +There are couple of elements looking similar to markdown[^not_inherited], namely: +- **Indents are NOT SUPPORTED**: the parser is quite primitive now, so it explicitly relies on some elements having nothing in front of them. +- Headers: + - up to 6 deep + - only hash-variant (`### Head of 3rd order`) is supported + - current $\LaTeX$ representation is a placeholder +- Formatting + - **bold text** + - *italic text* + - ~~strikethrough~~ text + - **NOT** `monospace` text yet, because I kinda forgot about it +- Footnotes: + - (probably full support?) + - inserted **near the first use** in the output, instead of **where they are actually defined**. +- Lists + - limited item support - make sure to include only one-line-things as items + - (the above also means, there are no nested lists, for now) + - only bullet, arabic, latin and cyrillic enumerations are supported, for now. +- Inline and Display latex mathmode (not sure how it's properly called, it's the thing that allows for write fancy formulae) +- Tables: columns formatting is ignored, for now +- Figures: with obsidian-like insert (`![[path/to.image.png]]`), so that Obsidian would actively display them +- Separators: these act like page breaks in the resulting $\LaTeX$ +- Hyper references (`href`): limited support, `href`s must be defined right near them~~, mainly because I forgot you can do that right up until right now~~ +- Code blocks: implemented with [Minted] + +## Latex +The are a couple capabilities, markdown does not usually provide: +- Assigning **identifiers** to `tables`, `figures` and `equations` (display math) + - Syntax (*in meta block*): `ref = REF`. `REF` cannot contain whitespace + - Example (*in meta block*): `ref = graph` will transform to one of + - `\label{tab:graph}`, if inserted after table + - `\label{fig:graph}`, if inserted after figure + - `\label{eq:graph}`, if inserted after equation + - there's no way to opt this behavior out, for now +- Referring to `tables`, `figures` and `equations` (display math) + - Syntax: `[@REF]` + - Example: `[@fig:graph]` transforms into $\LaTeX$'s `\ref{fig:graph}` +- Assigning captions for `figures` and `tables`: + - Syntax (*in meta block*): `caption = "CAPTION"`. `CAPTION` can only contain escaped double quotes (`\"`) + - Example: (*in meta block*): `caption = "Wow, this table is such data; Much amaze"` +- Assigning width to `figures`: + - Syntax (*in meta block*): `width = WIDTH`. `WIDTH` must be a valid decimal for your $\LaTeX$ engine + - Example (*in meta block*): `width = 0.9` + - Width is measured in terms out width of your document. So `1.0` will scare the figure to the width of the document + - `0.9` is the default value + +### A meta block +This is a one-line braces-delimited block after `equation`, `figure` or `table`. It is expected to be placed on the next line. Comma-separated arguments described above can be placed there in any order and with any amount of whitespace between them. + +Take a look at the [examples] for a better idea of this block's usage. + +## Yaml +What's `yaml` doing here? Well, it's related to `pandoc` allowing you substitute stuff in a so-called "templates" with certain strings. + +`Kyomato` similar thing too - check out corresponding [example][examples] (specifically, `title_info` one). + +# Ideology +Although it's annoying to admit, there is no way to do a fully-streamed zero-copy-type of transformation. There are two reasons for that: +- Markdown allows to decipher footnotes at an point up until the end of the file. +On the other hand, I could not find a way to make (la)tex "insert" footnote definition after first marker to it - +it seems to always insert them at the bottom of the page they were encountered. +- Static [ayano] blocks are supposed to be available for the entire document. +Thus, code must be fully collected first, that executed of the second, separate path. +//! +Due to these two factors, I decided to store the input in it's entirety during program's execution. +To (sorta) compensate for that, crate follows a strict zero-copy rule through the entire codebase. +//! +To further decrease allocations count, it was decided that any output would be generated through an object, that can be written to by parsed tokens, in order. +That object would ideally have some methods to append the output with a string, or th like that... Well, [`Write`] trait matches that perfectly! +So all tokens can be appended to an abstract, writer-sort-of-thing. +[`Display`] trait implementation was considered as an alternative, but I ultimately decided that was not idiomatic. +//! +Actual Token representation might vary, so there can be different actual ZST formatters, defined in their separate crates. +//! +### Async +As much as I'd like all of the processes be nice and async, I see no option for that in case of continuous output generation. +Well, unless there's a way to efficiently write to a single output from multiple threads, while preserving output order. +Any sort of "collect" function would've destroyed the purpose anyway. + + +# Why? + +This crate allows to transform [Obsidian]'s markdown files into a $\LaTeX$ document of the format I personally like. +The general idea is - markdown is by design much less descriptive than $\LaTeX$, +but I've established a common style with my $\LaTeX$, and found myself just plainly copying stuff all over the place. + +Automatic source transformation provided by editor **can** be used to mitigate that, +but in the end you still end up with a giant lumps latex syntax, that mean nothing to you, while you're writing the document. + +Guess what is also a pain to copy around and check for validity? Data! +I often need a couple of tables inside of the document I write. And the most painful thing to do is to search for each and every +location you've inserted the data in, and replacing it with new, updated value (this mostly relates to calculation error fixing). +Along with that, I'd like to have some sort of documentation on exact actions I've done with the data to get the result I display. And these places should be kept in mind, too! It's only logical to delegate all of that to the machine. + +At times before that, I used `Kile` to write my $\LaTeX$, `Alphaplot` to plot data and approximate and, `Libreoffice` to create spreadsheets and calculate stuff. Some sort of mistype in any of those lead to my *physical pain*, un-sync info everywhere, and a couple of minutes of me intensely finding out every place in my work, that needs to be updated, while keeping in mind all of the workflow I had at the moment of said distraction. I hope to completely eliminate situations like these with this project, reducing number of programs I use to a single markdown editor (which is `Obsidian`, for now). + +If you can relate to problems above, I hope this project will help you too. + +[^not_inherited]: I can't say these are implemented properly, since that would be a lie - their implementation is quite primitive right now, and should be used with caution or th. Keep in mind, that document you write must be translated to $\LaTeX$. + +[Obsidian]: https://obsidian.md/ +[Minted]: https://github.com/gpoore/minted +[examples]: https://github.com/Dzuchun/kyomato/tree/indev/examples \ No newline at end of file diff --git a/examples/title_info/input.md b/examples/title_info/input.md new file mode 100644 index 0000000..f324d67 --- /dev/null +++ b/examples/title_info/input.md @@ -0,0 +1,23 @@ +--- +header-line1: Open source community +header-line2: rustacean division +document-type: ARTICLE +title-line1: on correct programming socks selection +title-line2: to post at undisclosed subreddit +title-line3: (practical guide) +title-line4: Part 2: coping with your actual biological gender +author-line1: authored: +author-line2: Dzuchun at all or th, idk what that means lol +author-line3: (perfectly still) +date: Marsh 2024 +prof: me, I guess +code-section-title: Code blocks +--- + +# This is a complete disaster + +In the source above, you can see an example of title info. The fields defined there can be of any form, and any of them can be omitted. + +I have no idea what `yaml` actually looks like, so that's probably not and actual `yaml`, but a curious format to supply some text to `Kyomato`. + +The fields list might expand in the future. \ No newline at end of file diff --git a/examples/title_info/main.rs b/examples/title_info/main.rs new file mode 100644 index 0000000..45686f6 --- /dev/null +++ b/examples/title_info/main.rs @@ -0,0 +1,23 @@ +fn main() { + //! This is title info showing example of programmatic output generation + //! + //! You might want to use CLI interface instead + + // that's a utility setup call; essentially, it reads input from one file, and opens output file for writing + let (input, mut output) = examples_util::setup_io( + "examples/title_info/input.md", + "examples/title_info/output.tex", + ).unwrap(); + + // construct path engine + // in this example, no external files are accessed (i.e. there are no images or special Ayano syntax) + // so it's ok to pass in a blank primitive engine + let path_engine = kyomato::path_engine::primitive_dbg(); + // _dbg stands for "informative error", just in case + + // parse input and write to output + kyomato::LabaLatex::parse_and_write(path_engine, &input, &mut output) + // this part is just for debug purposes + .map_err(|err| (err, output)) + .unwrap() +} diff --git a/examples/title_info/output.tex b/examples/title_info/output.tex new file mode 100644 index 0000000..070f407 --- /dev/null +++ b/examples/title_info/output.tex @@ -0,0 +1,140 @@ +\documentclass[a4paper]{article} +\usepackage{graphicx, hyperref, xcolor, gensymb, amssymb, mathtools, wrapfig, mathtools, microtype, lastpage, caption, titlesec, paracol, longtable, booktabs, cancel} +\usepackage[T2A, T1]{fontenc} +\usepackage[utf8]{inputenc} +\usepackage[russian, ukrainian]{babel} +\hypersetup{ + colorlinks, + linkcolor={blue!20!black}, + citecolor={blue!50!black}, + urlcolor={blue!80!black} +} + +\usepackage[left = 25mm, right = 10mm, top=20mm, bottom=20mm, bindingoffset=0cm]{geometry} + +% КОМАНДИ + +\newcommand{\fnt}{ +\fontshape{n} +\fontsize{14pt} {19pt} +\linespread{0.8} +\selectfont +} % нормативний шрифт +\newcommand{\capfnt}{ + \fnt +} % норматичний шрифт для підписів (тимчасово співпадає зі звичайним шрифтом) + +\newcommand{\tb}{ + \hspace*{10mm} +} % відступ у 5 символів "x" відповідно до ДСТУ +\newcommand{\tbln}{ + \newline + \tb +} % те ж саме, але з переносом рядка +\newcommand{\tbsp}{ + \vspace*{1ex} + \tbln +} % те ж саме, але з додатковим відступом між рядками + +% бібліографія +\usepackage[square,sort,comma,numbers]{natbib} +\renewcommand{\bibsection}{} +\usepackage{totcount} +\newtotcounter{citnum} +% лічильник цитувань +\def\oldbibitem{} \let\oldbibitem=\bibitem +\def\bibitem{\stepcounter{citnum}\oldbibitem} + +% лічильник зображень +\newtotcounter{graphnum} +\def\oldincludegraphics{} \let\oldincludegraphics=\includegraphics +\def\includegraphics{\stepcounter{graphnum}\oldincludegraphics} + +% лічильник таблиць (НЕАВТОМАТИЧНИЙ!!!) +\newtotcounter{tabnum} + +% лічильник додатків (НЕАВТОМАТИЧНИЙ!!!) +\newtotcounter{dodnum} + +% нумерація сторінок +\usepackage{fancyhdr} +\pagestyle{fancy} +\fancyhf{} +\renewcommand{\headrulewidth}{0pt} +\setlength{\headheight}{15.3pt} +\fancyhead[R]{\fnt \thepage} + +% виноски +\renewcommand{\thefootnote}{\large\arabic{footnote})~} +\renewcommand{\footnoterule}{\rule{20mm}{0.4pt} \vspace*{0.5ex}} +% \newcommand{\vyn}[2]{ +% \footnote[#1]{\large #2} +% } % створити виноску + +\let\oldFootnote\footnote +\renewcommand{\footnote}[1]{ + \oldFootnote{\large #1} +} % перегрузка виноски + +\providecommand{\tightlist}{% + \setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}} + +% підписи малюнків/таблиць +\captionsetup[figure]{name={\fnt Рисунок~},labelsep=period} +\captionsetup[table]{name={\fnt Таблиця~},labelsep=period} + +% markdown strikeouts +\usepackage{soul} + +% codeblocks +\usepackage{minted} + +% enumeration +\usepackage[shortlabels]{enumitem} + +\titleformat*{\section}{\fnt\LARGE\bfseries} +\titleformat*{\subsection}{\fnt\Large\bfseries} +\titleformat*{\subsubsection}{\fnt\Large} + +\begin{document} +\thispagestyle{empty} % [A]:for cropping +% ------------ +% source: https://tex.stackexchange.com/questions/173651/need-help-to-create-such-a-beautiful-title-page +\begingroup +\centering +\obeylines +\topskip 0pt % source: https://tex.stackexchange.com/questions/2326/vertically-center-text-on-a-page +\vspace*{\fill} +\textbf{ +\LARGE Open source community +\LARGE rustacean division +\vspace{100pt} +\LARGE ARTICLE +\Large on correct programming socks selection +\Large to post at undisclosed subreddit +\Large (practical guide) +\huge Part 2: coping with your actual biological gender +\vspace{50pt} +\large authored: +\large Dzuchun at all or th, idk what that means lol +\large (perfectly still) +\large Mashle 2024 +\vspace*{\fill} +\hspace{\fill}\large me, I guess \rule{20mm}{0.4pt} +\vspace{10pt} +} +\endgroup +% ------------ +\newpage +\tableofcontents + +\newpage +\fnt + +\section{This is a complete disaster} + +\tb In the source above, you can see an example of title info. The fields defined there can be of any form, and any of them can be omitted. +\tbln I have no idea what `yaml` actually looks like, so that's probably not and actual `yaml`, but a curious format to supply some text to `Kyomato`. +\tbln The fields list might expand in the future. + +\end{document} \ No newline at end of file From 922581cb5de1c4400c1be7833bdfb13a939c11a9 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Fri, 29 Mar 2024 00:32:11 +0200 Subject: [PATCH 57/98] Added most of the title doc --- README.md | 154 ++++++++++++++++++++++++++++++++++++++++++++------- src/lexer.rs | 2 +- src/lib.rs | 29 +--------- 3 files changed, 135 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 2732fac..4a649db 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,13 @@ You can use helper CLI script or write your own, to then compile said $\LaTeX$, Please, the "**Why?**" paragraph at the very bottom about the reasons I developed this thing. I doubt you can effortlessly do all that with `pandoc` (I'm not willing to die trying). +# Examples + +For programmatic examples of usage, see [examples] + +## CLI +TODO!!! + # Walkthrough **PLEASE NOTE**: rules described below are fairly strict, since there's no literal markdown parser inside. `Kyomato`'s design is "*un-optimal*" right now, since I need it to work **NOW**. @@ -69,30 +76,132 @@ Take a look at the [examples] for a better idea of this block's usage. ## Yaml What's `yaml` doing here? Well, it's related to `pandoc` allowing you substitute stuff in a so-called "templates" with certain strings. -`Kyomato` similar thing too - check out corresponding [example][examples] (specifically, `title_info` one). +Although there's neither implemented `yaml` parser, nor fully-supported arbitrary pattern system yet, `Kyomato` allows to do a similar thing: check out corresponding [example][examples] (specifically, `title_info` one). + +## Ayano + +*because Ayano loves Kyoko* + +`Kyomato` has a very special type of syntax, that actually makes it so useful to me: `Ayano` blocks. + +Basically, `Kyomato` can execute arbitrary Python code during **output generation** (for example, to perform some computations, or `rm-rf`ing your entire hard drive, that's up to you, really). `Kyomato` is also able to insert results of said computations into the output (or refer to them, in case of figures, as they can't be inserted into latex directly). `Ayano` blocks are powered by [pyo3]. + +Through the following explanations, there will be a couple of *technical notes*, describing exact inner working of `Ayano` blocks. + +There are two types of `Ayano` blocks: +- *Static blocks are executed all at once **just before** output starts actually generating.* There's nothing left in place of it in the output. All static blocks share the environment and their variables are visible to all function blocks. +- *Function blocks are **compiled** together with static blocks into a single Python module,* but are executed on demand, during the output generation process. Function blocks leave **something** instead of them in the output. + +`Ayano` blocks are regular code blocks, with their language set to `Python, Ayano`. Once `Kyomato` encounters alike block, it may react in a couple of ways, depending on what's specified *after* `Ayano` keyword. + +A general syntax: `Python, Ayano ! * "DESCRIPTION" ~ PATH`. It is comprised of three optional independent parts (in any order): +- Static block declaration (`!`): + - *Makes block static* + - *Omitting it makes block a function* +- Display declaration (`* "DESCRIPTION"`): + - Adds this block of code to a special section at the very end of the output + - Description is optional - you can just leave it as `*` + - Description must escape any double quotes + - If description is specified, it will be displayed under block's representation in a said special section +- Insert declaration (`~ PATH`): + - Must point to some Python script with path relative to the execution folder + - Said script is inserted at the beginning of the block, and will be executed and/or displayed with block's code + - *A block marked with that, will execute it's code in the directory of a pointed script* + +As mentioned prior, all function blocks are replaces with **something**. You can see them as functions that leave in the output their result. *Anything having a `__str__` Python representation can be a valid output.* + +Additionally, block's last line can be a special `Ayano Syntax`. These are mostly shortcuts to certain already-existing $\LaTeX$ structures, like figures and tables: +- Trailing return: basically, you can omit the actual `return` keyword, and it will be appended for you by `Ayano`. That's jut about convenience - you can still put that `return` each time, `Ayano` shouldn't complain about that. +- Value-Error formatting syntax: + - Accepts a value and an error, outputting them with $\LaTeX$'s $\pm$ (`\pm`) in between + - Syntax: `@dev: VALUE, ERROR`. `VALUE` and `ERROR` must be valid Python objects that can be parsed into float + - Example: `@dev: 4.54535, 0.2342` + - Additionally, a certain manipulation is performed on parsed value and error: + - *All operations are performed in decimal, so no binary jumpscares* + - An error is rounded to + - Two digits, if it starts with `1` or `2` + - Single digit otherwise + - A value itself is rounded or appended to the same digit as error +- Figure syntax: + - Transforms into an inserted figure on output + - Syntax: `@fig: src = SRC, ident = IDENT, caption = CAPTION` + - Arguments can come in any order and with any whitespace + - All arguments must be something Python can `str()` + - `SRC` argument is mandatory, and is supposed to point to image file to include + - If `Ayano` detects `SRC` to be something starting and ending with (double) quotes, a special check will be performed to inform you, if file it points to does not exist. This should not halt `Kyomato`, as said file can be generated during block execution + - `IDENT` is optional, and specifies figure's `ref` + - `CAPTION` is optional, and specifies figure's `caption` + - `CAPTION`'s content is run though regular parser, *with minor limitations*. + - Example: `@fig: src="line.png", ident="line", caption="Figure caption"` +- CSV-Table syntax: + - Reads and inserts a CSV table into the output + - Syntax: `@csv_table: src = SRC, rows = ROWS, columns = COLUMNS, ident = IDENT, caption = CAPTION` + - Arguments can come in any order and with any whitespace + - `SRC`, `IDENT` and `CAPTION` arguments must be something Python can `str()` + - `SRC` argument is mandatory, and is supposed to point to csv table file. can actually not be a csv table, just some format that [python's csv module] can manage to read. + - If `Ayano` detects `SRC` to be something starting and ending with (double) quotes, a special check will be performed to inform you, if file it points to does not exist. This should not halt `Kyomato`, as said file can be generated during block execution + - `ROWS` must have a [Rust range format]. It defines rows of the table to include (0th row is considered a header, that's always included). *This argument it parsed and used internally by `Ayano`, so please follow the formatting strictly* + - Omitting `ROWS` will include all rows + - `COLUMNS` defines table columns to include. Here's an example of the syntax: `["column1", "column2", ("value_column", "error_column")]`: + - *This argument it parsed and used internally by `Ayano`, so please follow the formatting strictly* + - Syntax resembles an array of elements, where each element can be of two types: + - Single column include - representing a single column from the table, included under it's name into the output table + - Value-error column include - representing **value column** and **error column**, that will be merged using *value-error syntax* and included as a *single column* to the output, under **value column**'s name + - Order of columns in the output table will be the same as specified by this syntax + - Omitting `COLUMNS` will include all columns in some order, and without any fancy formatting + - `IDENT` is optional, and specifies table's `ref` + - `CAPTION` is optional, and specifies table's `caption` + - `CAPTION`'s content is run though regular parser, *with minor limitations*. + - *Table's cells are run trough regular parser too, so you can format text, add referenced and so on* + - This specific syntax expects Python to read the input file. This is important, if your block has an insert declaration - make sure that Python will be able to read the intended file without any fancy path searching, just as it is +- Generated-Table syntax: + - Transforms into a runtime-generated table, by calling a special generator object with table's row and column + - Syntax: `@gen_table: GENERATOR; rows=ROWS, columns=COLUMNS, caption=CAPTION, ident=IDENT` + - Arguments except `GENERATOR` can come in any order and with any whitespace + - `IDENT` and `CAPTION` arguments must be something Python can `str()` + - `IDENT` is optional, and specifies table's `ref` + - `CAPTION` is optional, and specifies table's `caption` + - `CAPTION`'s content is run though regular parser, *with minor limitations*. + - *Table's cells are run trough regular parser too, so you can format text, add referenced and so on* + - `ROWS` and `COLUMNS` arguments **must be** a plain decimal integers, specifying number of rows and columns respectively generated for table + - *`ROWS` and `COLUMNS` are parsed and used internally by `Ayano`, so please follow the formatting strictly* **(that's temporary, there are plans to do these runtime-determined too)** + - `GENERATOR` must be something that can be called by Python with two arguments: cell's row and column respectively. For example, it can name a function with fitting signature, or be a Python lambda: `lamba r,c f"That's cell ({r}, {c})"` + - `GENERATOR` must come first, and expected to have a semicolon after it + +*There are ways to trigger explained behavior without actually using this fancy syntax. Internally, `Ayano` still converts it to a valid Python code, so if you happen to write the same code yourself, `Ayano` will happily detect and react to it. However, the only practical one I see is value-error formatting one, here's it's transformed syntax: `("err", value, error)`. You can, of course, use anything indexable by `0`, `1` and `2`, that should work fine too.* + +For more info on `Ayano` blocks usage, check out [examples]. + +## Path engines + +There are several directories your code can potentially refer to: +- If you're inside `Ayano` block with insert declaration, you might refer to stuff relative to insert declaration's directory +- Any point of source file can potentially point a file right next it +- In [Obsidian], there'a dedicated directory for pictures, so your source might refer to it +- Lastly, you might refer to a local file in the directory you actually run `Kyomato` from + +To control and direct to all of the described locations, a `PathEngine` (`PE`) is used. Basically, it checks all of the locations in a certain order, dependant on the requested file type and optionally preforms an extra check, like "if file actually exists, and I can access it?" or "if path points to a valid directory", or "does path look **SUS**"? + +See [examples] for more explanation on that part # Ideology -Although it's annoying to admit, there is no way to do a fully-streamed zero-copy-type of transformation. There are two reasons for that: -- Markdown allows to decipher footnotes at an point up until the end of the file. -On the other hand, I could not find a way to make (la)tex "insert" footnote definition after first marker to it - -it seems to always insert them at the bottom of the page they were encountered. -- Static [ayano] blocks are supposed to be available for the entire document. +This is a completely + +Although it's annoying to admit, there is no way to do this sort of transformation as fully-streamed and O(1) memo. There are two reasons for that: +- Markdown allows to define footnote content at any point up until the end of the file. I couldn't find any way for $\LaTeX$ to look up ahead for this sort of stuff - it seems to always insert them at the bottom of the page definition was encountered. +- Some `Ayano` blocks are supposed to leave something behind them, meaning we must execute them at the time of output generation. This can be achieved with O(1) memo, but would likely involve recompiling Python module each time, leading to longer execution time Thus, code must be fully collected first, that executed of the second, separate path. -//! -Due to these two factors, I decided to store the input in it's entirety during program's execution. -To (sorta) compensate for that, crate follows a strict zero-copy rule through the entire codebase. -//! -To further decrease allocations count, it was decided that any output would be generated through an object, that can be written to by parsed tokens, in order. -That object would ideally have some methods to append the output with a string, or th like that... Well, [`Write`] trait matches that perfectly! -So all tokens can be appended to an abstract, writer-sort-of-thing. -[`Display`] trait implementation was considered as an alternative, but I ultimately decided that was not idiomatic. -//! -Actual Token representation might vary, so there can be different actual ZST formatters, defined in their separate crates. -//! -### Async + +The above means, that it would be wise to read the entire input first, and store it in a large buffer. Then, various struct will just refer a part of that buffer, and only create their new owned buffer, once data they refer to is not really relevant. + +For the most of the codebase, that's the design I'm following right now, but there are a couple of hiccups here and there... + +There's a plan for complete rework of data storage, that will allow us to use much smaller tokens (they are, like, `120b` each now, which is... BAD, REALLY BAD). + +## Not Async As much as I'd like all of the processes be nice and async, I see no option for that in case of continuous output generation. -Well, unless there's a way to efficiently write to a single output from multiple threads, while preserving output order. -Any sort of "collect" function would've destroyed the purpose anyway. +Well, unless there's a way to efficiently write to a single output from multiple threads, while preserving output and `Ayano` block execution order. +Any sort of "collect" function would destroyed the purpose, so that's not an easy thing to design. # Why? @@ -117,4 +226,7 @@ If you can relate to problems above, I hope this project will help you too. [Obsidian]: https://obsidian.md/ [Minted]: https://github.com/gpoore/minted -[examples]: https://github.com/Dzuchun/kyomato/tree/indev/examples \ No newline at end of file +[examples]: https://github.com/Dzuchun/kyomato/tree/indev/examples +[pyo3]: https://github.com/PyO3/pyo3 +[python's csv module]: https://docs.python.org/3/library/csv.html +[Rust range format]: https://doc.rust-lang.org/std/ops/struct.Range.html#examples \ No newline at end of file diff --git a/src/lexer.rs b/src/lexer.rs index 23a14af..1c09f5c 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -720,7 +720,7 @@ fn _list_inner< "a." => ListType::Latin, "а." => ListType::Cyrillic, "-" => ListType::Bullet, - "I" => unimplemented!("Roman numerated lists are not supported yet"), + "I." => unimplemented!("Roman numerated lists are not supported yet"), _ => { // that's an unknown list type return Err(nom::Err::Error(E::from_external_error( diff --git a/src/lib.rs b/src/lib.rs index 3e09fdf..6d2bbe1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,31 +1,4 @@ -//! *Kyoko is being a tomato. Why? I don't know :idk:* -//! -//! This crate allows [obsidian]'s markdown transformation into a (la)tex document of the format I personally like. -//! One should note, that markdown is generally less descriptive than (la)tex, so it should code to no surprise, -//! that I introduced some custom markdown syntax to account for that. -//! -//! # Ideology -//! Although it's annoying to admit, there is no way to do a fully-streamed zero-copy-type of transformation. There are two reasons for that: -//! - Markdown allows to decipher footnotes at an point up until the end of the file. -//! On the other hand, I could not find a way to make (la)tex "insert" footnote definition after first marker to it - -//! it seems to always insert them at the bottom of the page they were encountered. -//! - Static [ayano] blocks are supposed to be available for the entire document. -//! Thus, code must be fully collected first, that executed of the second, separate path. -//! -//! Due to these two factors, I decided to store the input in it's entirety during program's execution. -//! To (sorta) compensate for that, crate follows a strict zero-copy rule through the entire codebase. -//! -//! To further decrease allocations count, it was decided that any output would be generated through an object, that can be written to by parsed tokens, in order. -//! That object would ideally have some methods to append the output with a string, or th like that... Well, [`Write`] trait matches that perfectly! -//! So all tokens can be appended to an abstract, writer-sort-of-thing. -//! [`Display`] trait implementation was considered as an alternative, but I ultimately decided that was not idiomatic. -//! -//! Actual Token representation might vary, so there can be different actual ZST formatters, defined in their separate crates. -//! -//! ### Async -//! As much as I'd like all of the processes be nice and async, I see no option for that in case of continuous output generation. -//! Well, unless there's a way to efficiently write to a single output from multiple threads, while preserving output order. -//! Any sort of "collect" function would've destroyed the purpose anyway. +#![doc = include_str!("../README.md")] use data::{TitleInfo, Token}; use gen::OutputGenerator; From 481fd3a4198ba81707ee7e9efac8c6f52a02443c Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Sat, 30 Mar 2024 01:29:55 +0200 Subject: [PATCH 58/98] A lot of changes, really - Renamed main crate to `kyomato-lib`, while `kyomato` name was given to a nested crate that contains an actual `Kyomato` cli - Created a helper script for easier `Kyomato usage`, check docs for details - Added a bit of description for the script --- .gitignore | 3 + Cargo.lock | 209 ++++++++++++- Cargo.toml | 13 +- README.md | 27 +- examples-util/Cargo.toml | 2 +- examples-util/src/lib.rs | 2 +- examples/advanced_ayano/images/nomi.jpg | Bin 0 -> 56853 bytes examples/advanced_ayano/main.rs | 10 +- examples/advanced_ayano/output.tex | 38 ++- examples/advanced_ayano/source/input.md | 37 +-- examples/advanced_ayano/source/sine_graph.png | Bin 0 -> 24124 bytes examples/basic_ayano/main.rs | 4 +- examples/basic_ayano/output.tex | 5 +- examples/basic_syntax/main.rs | 13 +- examples/basic_syntax/output.tex | 3 + examples/title_info/input.md | 2 +- examples/title_info/main.rs | 4 +- examples/title_info/output.tex | 3 + kyomato-scripts/image_folder/meme.png | Bin 0 -> 251134 bytes kyomato-scripts/kyomato-helper.bash | 65 ++++ kyomato-scripts/test_input/input.md | 56 ++++ .../test_input/other_sine_graph.png | Bin 0 -> 24124 bytes kyomato-scripts/test_input/results.csv | 11 + .../test_input/scripts/generate_sine.py | 12 + kyomato-scripts/test_input/sine_graph.png | Bin 0 -> 23760 bytes kyomato/Cargo.toml | 11 + kyomato/src/main.rs | 280 ++++++++++++++++++ src/gen/ayano.rs | 35 ++- src/gen/lab/lab_preamble.tex | 3 + src/gen/lab/mod.rs | 17 +- src/path_engine.rs | 21 ++ src/util.rs | 6 +- tests/lex.rs | 6 +- tests/transform.rs | 12 +- tests/transformed_files/ayano.txt | 3 + .../ayano_display_blocks.txt | 3 + tests/transformed_files/basic.txt | 3 + tests/transformed_files/deceit.txt | 15 +- tests/transformed_files/edgecases.txt | 5 +- tests/transformed_files/tables_code.txt | 5 +- tests/transformed_files/title_info.txt | 3 + 41 files changed, 851 insertions(+), 96 deletions(-) create mode 100644 examples/advanced_ayano/images/nomi.jpg create mode 100644 examples/advanced_ayano/source/sine_graph.png create mode 100644 kyomato-scripts/image_folder/meme.png create mode 100755 kyomato-scripts/kyomato-helper.bash create mode 100644 kyomato-scripts/test_input/input.md create mode 100644 kyomato-scripts/test_input/other_sine_graph.png create mode 100644 kyomato-scripts/test_input/results.csv create mode 100644 kyomato-scripts/test_input/scripts/generate_sine.py create mode 100644 kyomato-scripts/test_input/sine_graph.png create mode 100644 kyomato/Cargo.toml create mode 100644 kyomato/src/main.rs diff --git a/.gitignore b/.gitignore index 316e4f9..e7af667 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ .vscode/settings.json examples/advanced_ayano/sine_graph.png examples/advanced_ayano/source/other_sine_graph.png + +/**/.kyomato_tmp +/**/*.pdf \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 0460767..d47bf89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,54 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +[[package]] +name = "anstream" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -80,6 +128,52 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim 0.11.0", +] + +[[package]] +name = "clap_derive" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "clap_lex" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "convert_case" version = "0.4.0" @@ -106,7 +200,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.10.0", "syn 1.0.109", ] @@ -144,7 +238,7 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" name = "examples-util" version = "0.1.0" dependencies = [ - "kyomato", + "kyomato-lib", ] [[package]] @@ -198,6 +292,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "ident_case" version = "1.0.1" @@ -262,6 +362,15 @@ dependencies = [ [[package]] name = "kyomato" version = "0.1.0" +dependencies = [ + "clap", + "derive_more", + "kyomato-lib", +] + +[[package]] +name = "kyomato-lib" +version = "0.1.0" dependencies = [ "cached", "derive_more", @@ -360,7 +469,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -451,7 +560,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fc910d4851847827daf9d6cdd4a823fbdaab5b8818325c5e97a86da79e8881f" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "syn 2.0.48", @@ -598,6 +707,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strsim" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" + [[package]] name = "syn" version = "1.0.109" @@ -746,6 +861,12 @@ dependencies = [ "url 1.7.2", ] +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "version_check" version = "0.9.4" @@ -770,19 +891,43 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.4", +] + [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", ] [[package]] @@ -791,42 +936,84 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" + [[package]] name = "zerocopy" version = "0.7.32" diff --git a/Cargo.toml b/Cargo.toml index 7366a6d..135fa5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ -workspace = { members = ["examples-util"] } +workspace = { members = ["examples-util", "kyomato"] } [package] -name = "kyomato" +name = "kyomato-lib" version = "0.1.0" edition = "2021" @@ -30,3 +30,12 @@ pathdiff = "0.2.1" text-diff = "0.4.0" nom = { version = "7.1.3", features = ["alloc"] } examples-util = { path = "examples-util" } + +# source: https://stackoverflow.com/a/54842093 +[profile.lowsize] +inherits = "release" +opt-level = 'z' # Optimize for size +lto = true # Enable link-time optimization +codegen-units = 1 # Reduce number of codegen units to increase optimizations +panic = 'abort' # Abort on panic +strip = true # Strip symbols from binary* diff --git a/README.md b/README.md index 4a649db..f1108ff 100644 --- a/README.md +++ b/README.md @@ -7,15 +7,34 @@ You can use helper CLI script or write your own, to then compile said $\LaTeX$, ### WHY NOT PANDOC??? -Please, the "**Why?**" paragraph at the very bottom about the reasons I developed this thing. I doubt you can effortlessly do all that with `pandoc` +Please, see the "**Why?**" paragraph at the very bottom about the reasons I developed this thing. I doubt you can effortlessly do all that with `pandoc` (I'm not willing to die trying). # Examples For programmatic examples of usage, see [examples] -## CLI -TODO!!! +# CLI +`Kyomato` cli binary can be found at a nested `kyomato` crate. It can be built with usual +```sh +cd kyomato +cargo build --release +``` +After than a binary will be at `./target/release/kyomato`. + +There's a helper script at `./kyomato-scripts/kyomato-helper.bash` for simplified usage. Here's a quick walkthrough: +- Build the binary or download already built one +- Add it to the `PATH` envvar. In some shells, you can prepend your command with `PATH=$PATH:/path/to/kyomato` for that. Just make sure that provided path is absolute (you can run it through `realpath` for that) +- Run +```bash +kyomato-helper.bash ./path/to/input.md +``` + +For example, here's a command I actually tested it with at `kyomato-scripts` directory: +```fish +PATH=$PATH:$(realpath ../target/release) KYOMATO_IMAGES=$(realpath ./image_folder) ./kyomato-helper.bash test_input/input.md +``` +After running it, result should be located at `test_input.input.pdf`. # Walkthrough **PLEASE NOTE**: rules described below are fairly strict, since there's no literal markdown parser inside. `Kyomato`'s design is "*un-optimal*" right now, since I need it to work **NOW**. @@ -107,6 +126,7 @@ A general syntax: `Python, Ayano ! * "DESCRIPTION" ~ PATH`. It is comprised of t - Must point to some Python script with path relative to the execution folder - Said script is inserted at the beginning of the block, and will be executed and/or displayed with block's code - *A block marked with that, will execute it's code in the directory of a pointed script* + - *Blocks without this mark, execute their scripts in the directory of the source, if available* As mentioned prior, all function blocks are replaces with **something**. You can see them as functions that leave in the output their result. *Anything having a `__str__` Python representation can be a valid output.* @@ -185,7 +205,6 @@ To control and direct to all of the described locations, a `PathEngine` (`PE`) i See [examples] for more explanation on that part # Ideology -This is a completely Although it's annoying to admit, there is no way to do this sort of transformation as fully-streamed and O(1) memo. There are two reasons for that: - Markdown allows to define footnote content at any point up until the end of the file. I couldn't find any way for $\LaTeX$ to look up ahead for this sort of stuff - it seems to always insert them at the bottom of the page definition was encountered. diff --git a/examples-util/Cargo.toml b/examples-util/Cargo.toml index 6d9b640..cdf674a 100644 --- a/examples-util/Cargo.toml +++ b/examples-util/Cargo.toml @@ -6,4 +6,4 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -kyomato = {path = ".."} \ No newline at end of file +kyomato-lib = {path = ".."} \ No newline at end of file diff --git a/examples-util/src/lib.rs b/examples-util/src/lib.rs index faf81e5..08b0bdb 100644 --- a/examples-util/src/lib.rs +++ b/examples-util/src/lib.rs @@ -14,6 +14,6 @@ pub fn setup_io( .truncate(true) .create(true) .open(output_path.as_ref())?; - let output = kyomato::FmtToIo::new(output); + let output = kyomato_lib::FmtToIo::new(output); Ok((input, output)) } diff --git a/examples/advanced_ayano/images/nomi.jpg b/examples/advanced_ayano/images/nomi.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cf3552c085f8229f64381b248ec46e9397b774c4 GIT binary patch literal 56853 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 KkK*q9zX<@E_j<(u literal 0 HcmV?d00001 diff --git a/examples/advanced_ayano/main.rs b/examples/advanced_ayano/main.rs index c127490..306b682 100644 --- a/examples/advanced_ayano/main.rs +++ b/examples/advanced_ayano/main.rs @@ -1,4 +1,4 @@ -use kyomato::path_engine::PathEngineExt; +use kyomato_lib::path_engine::PathEngineExt; fn main() { //! This is ayano-involving example of programmatic output generation @@ -14,7 +14,7 @@ fn main() { // construct path engine // there are a couple of parts of the path engine. you should specify them in the increasing priority order - let path_engine = kyomato::path_engine::base_gen() + let path_engine = kyomato_lib::path_engine::base_gen() // execution folder: you might want generator to check it, if all other options had failed .with_execution_folder(".") // source folder: you'll want to check it sometime. this is intended to be a folder where input markdown is located @@ -25,15 +25,15 @@ fn main() { .with_ayano() // this adds a check to the engine, that will ensure that returned path actually points to something in the system // without it, first proposed variant will be used (in this case, this turns out to be ayano-insert directory) - .with_check(kyomato::path_engine::fs_check) + .with_check(kyomato_lib::path_engine::fs_check) // this last call is a no-op used to explicitly set error type. You may implement your own error too! // (the one actually used here is kinda bad, consider using `kyomato::path_engine::MarkerError` for more memory-efficiency) - .with_error_type::(); + .with_error_type::(); // please note that most of the above is only used by actual latex engine you plan to use. // That is, take care of paths being the same for kyomato and the engine, or use some sort of absolute paths. // parse input and write to output - kyomato::LabaLatex::parse_and_write(path_engine, &input, &mut output) + kyomato_lib::LabaLatex::parse_and_write(path_engine, &input, &mut output) // this part is just for debug purposes .map_err(|err| (err, output)) .unwrap() diff --git a/examples/advanced_ayano/output.tex b/examples/advanced_ayano/output.tex index 0d2924e..8964a8c 100644 --- a/examples/advanced_ayano/output.tex +++ b/examples/advanced_ayano/output.tex @@ -92,6 +92,9 @@ % enumeration \usepackage[shortlabels]{enumitem} +% scaleable font (whatever that means) +\usepackage[normalem]{ulem} + \titleformat*{\section}{\fnt\LARGE\bfseries} \titleformat*{\subsection}{\fnt\Large\bfseries} \titleformat*{\subsubsection}{\fnt\Large} @@ -133,8 +136,8 @@ This is an example of advanced `Ayano` features. \tbln First, there are insert blocks. These can load some code from specified python script, and insert it at the start: \tbln Note that their displayed version at the end actually includes the loaded script. These blocks can be used for display too: \\ -e = 2.7182818284590455, pi = 3.141592653589793, pi != 2.7182818284590455 -\tbln Sometimes you wish to only include part of the script, and do something else after some point. To achieve that, you can include `# TOSHINO KYOKO!` comment (note the space and casing; you can append anything after it at that line). Once this comment is seen by `Ayano`, it stops inserting the file, and appends the actual code you've provided in the block (possibly none). Comment itself is not included: 2 +[0.0pt]e = 2.7182818284590455, pi = 3.141592653589793, pi != 2.7182818284590455 +\tbln Sometimes you wish to only include part of the script, and do something else after some point. To achieve that, you can include `\# TOSHINO KYOKO!` comment (note the space and casing; you can append anything after it at that line). Once this comment is seen by `Ayano`, it stops inserting the file, and appends the actual code you've provided in the block (possibly none). Comment itself is not included: 2 \tbln Ayano blocks can actually interact with the filesystem themselves. The caveat is - where exactly python thinks it's current directory is. \tbln Well, the rule of thumb turns out to be quite simple: \begin{itemize} @@ -170,7 +173,7 @@ Adam & Grant & 19 & 9616 & 38385\\ \hline \end{center} \end{table} -\tb This can be combined with insertion blocks to achieve generated table insertion; you can specify a path relative to the inserted file, +\tb This can be combined with insertion blocks to achieve generated table insertion; for `csv-table` syntax, please always specify path relative to the inserted script folder: \begin{table}[h!] \fnt \begin{center} @@ -253,7 +256,7 @@ zeta & 4444.00$\pm$0.03 & 0.0\\ \hline \tb Generate a figure in a static block, and save it somewhere. After that, include it as an image: \begin{figure}[h!] \centering -\includegraphics[width=0.9\textwidth]{./sine_graph.png} +\includegraphics[width=0.9\textwidth]{source/sine_graph.png} \caption{\capfnt that's a nice little sine graph!} \label{fig:sine} \end{figure} @@ -263,7 +266,7 @@ zeta & 4444.00$\pm$0.03 & 0.0\\ \hline \tb Generate a figure in function block, and insert it with a special syntax: \begin{figure}[h!] \centering -\includegraphics[width=0.9\textwidth]{././sine_graph.png} +\includegraphics[width=0.9\textwidth]{./source/sine_graph.png} \caption{\capfnt That's another way to include a figure from Ayano!} \label{fig:other_sine_graph} \end{figure} @@ -282,7 +285,7 @@ zeta & 4444.00$\pm$0.03 & 0.0\\ \hline \tbln Note that essentially all of the paths used to identify files, are specified with quotes. That's because it can actually be dynamic, and determined at runtime! \begin{figure}[h!] \centering -\includegraphics[width=0.2\textwidth]{./source/other_sine_graph.png} +\includegraphics[width=0.2\textwidth]{source/other_sine_graph.png} \caption{\capfnt The same sine. Again.} \label{fig:yet_another_sine_graph} \end{figure} @@ -302,6 +305,13 @@ zeta & 4444.0 & 0.033 & 0.0\\ \hline \end{table} \tb Note, of course, that this removes `Kyomato`'s file validity checking and other path processing. So if you happen to resort to that, make sure that Python is able to find the file by themselves, from the directory it executes in. +\begin{figure}[h!] +\centering +\includegraphics[width=0.5\textwidth]{images/nomi.jpg} +\caption{\capfnt We've actually completed Nomi some time ago, lol} +\label{fig:nomi} +\end{figure} + \subsection{Code blocks} \begin{minted}[linenos, mathescape, autogobble, breaklines]{python} @@ -348,7 +358,7 @@ Here you can see an empty static block displayed as inserted script \\ \begin{minted}[linenos, mathescape, autogobble, breaklines]{python} two = None -with open("2", "r") as f: +with open("../2", "r") as f: two = int(f.readline()) \end{minted} @@ -361,7 +371,7 @@ two + three This block here uses variables prior-defined in static blocks. \\ \begin{minted}[linenos, mathescape, autogobble, breaklines]{python} -@csv_table: src="data/20240327230336_8798.csv", caption="cred: [randat](http://randat.com/)", ident = "random_table1" +@csv_table: src="../data/20240327230336_8798.csv", caption="cred: [randat](http://randat.com/)", ident = "random_table1" \end{minted} table insertion example \\ @@ -451,25 +461,25 @@ with open("calculations_table.csv", "w") as f: write_var(writer, zeta, 'zeta') # say, we do something undesirable in this script next, and we do not want kyomato to execute that -@csv_table: src="data/calculations_table.csv" +@csv_table: src="../data/calculations_table.csv" \end{minted} An example of runtime-generated table insertion with execution-relative path \\ \begin{minted}[linenos, mathescape, autogobble, breaklines]{python} -@csv_table: src="data/calculations_table.csv" +@csv_table: src="../data/calculations_table.csv" \end{minted} An example of runtime-generated table insertion with execution-relative path \\ \begin{minted}[linenos, mathescape, autogobble, breaklines]{python} -@csv_table: src="data/calculations_table.csv", rows = 3..4, columns = ["name"], caption = "That's a-bit-cut table!" +@csv_table: src="../data/calculations_table.csv", rows = 3..4, columns = ["name"], caption = "That's a-bit-cut table!" \end{minted} This is only a part of the table above, due to specified column and row restrictions \\ \begin{minted}[linenos, mathescape, autogobble, breaklines]{python} -@csv_table: src="data/calculations_table.csv", columns = ["name", ("value", "err"), "%"], caption = "That's a table with value-error formatting!" +@csv_table: src="../data/calculations_table.csv", columns = ["name", ("value", "err"), "%"], caption = "That's a table with value-error formatting!" \end{minted} You can see values having error right beside them, and formatted accordingly! \\ @@ -515,7 +525,7 @@ plt.savefig("../source/other_sine_graph.png") That's a third way to do the same thing! \\ \begin{minted}[linenos, mathescape, autogobble, breaklines]{python} -fig_path= 'source/other_sine_graph.png' +fig_path= 'other_sine_graph.png' @fig: src = fig_path, ident = "yet_another_sine_graph", caption = "The same sine. Again.", width = 0.2 \end{minted} @@ -523,7 +533,7 @@ fig_path= 'source/other_sine_graph.png' \tb ayano-unnamed \\ \begin{minted}[linenos, mathescape, autogobble, breaklines]{python} -tab_path = 'data/calculations_table.csv' +tab_path = '../data/calculations_table.csv' @csv_table: src=tab_path \end{minted} diff --git a/examples/advanced_ayano/source/input.md b/examples/advanced_ayano/source/input.md index b3ae648..5e77542 100644 --- a/examples/advanced_ayano/source/input.md +++ b/examples/advanced_ayano/source/input.md @@ -3,21 +3,21 @@ This is an example of advanced `Ayano` features. First, there are insert blocks. These can load some code from specified python script, and insert it at the start: -```python, Ayano ! ~ scripts/calculate_e.py * "That's a way to calculate pi" +```python, Ayano ! ~ ../scripts/calculate_e.py * "That's a way to calculate pi" not_pi = e ``` Note that their displayed version at the end actually includes the loaded script. These blocks can be used for display too: -```python, Ayano ~ scripts/calculate_e.py * "Ooops, that actually turns out to be a formula for e :(" +```python, Ayano ~ ../scripts/calculate_e.py * "Ooops, that actually turns out to be a formula for e :(" from math import pi f"e = {e}, pi = {pi}, pi != {not_pi}" ``` Sometimes you wish to only include part of the script, and do something else after some point. To achieve that, you can include `# TOSHINO KYOKO!` comment (note the space and casing; you can append anything after it at that line). Once this comment is seen by `Ayano`, it stops inserting the file, and appends the actual code you've provided in the block (possibly none). Comment itself is not included: -```python, Ayano ~ scripts/toshino_kyoko.py * "the all-mighty **2**!!!" +```python, Ayano ~ ../scripts/toshino_kyoko.py * "the all-mighty **2**!!!" # you'll see this comment, and the 2 x ``` @@ -27,11 +27,11 @@ Well, the rule of thumb turns out to be quite simple: - if there is an insertion declared for a block, it's execution will be in the directory that file is in - if there is no insertion declared, python will continue it's execution at `kyomato`'s process directory -```python, Ayano ! ~ data/read3.py * "Here you can see an empty static block displayed as inserted script" +```python, Ayano ! ~ ../data/read3.py * "Here you can see an empty static block displayed as inserted script" ``` ```python, Ayano ! * "You can clearly see that this block has no insert statement, yet it knows **exactly** where to find the file" two = None -with open("2", "r") as f: +with open("../2", "r") as f: two = int(f.readline()) ``` @@ -42,32 +42,32 @@ two + three Ayano can be used to insert csv tables (possibly generated at runtime too). Here's a special syntax for it: ```python, Ayano * "table insertion example" -@csv_table: src="data/20240327230336_8798.csv", caption="cred: [randat](http://randat.com/)", ident = "random_table1" +@csv_table: src="../data/20240327230336_8798.csv", caption="cred: [randat](http://randat.com/)", ident = "random_table1" ``` -This can be combined with insertion blocks to achieve generated table insertion; you can specify a path relative to the inserted file, -```python, Ayano * "An example of runtime-generated table insertion" ~ data/doing_stuff.py +This can be combined with insertion blocks to achieve generated table insertion; for `csv-table` syntax, please always specify path relative to the inserted script folder: +```python, Ayano * "An example of runtime-generated table insertion" ~ ../data/doing_stuff.py @csv_table: src="calculations_table.csv" ``` ... but you've not forced to do that; you may do execution-relative path too: -```python, Ayano * "An example of runtime-generated table insertion with execution-relative path" ~ data/doing_stuff.py -@csv_table: src="data/calculations_table.csv" +```python, Ayano * "An example of runtime-generated table insertion with execution-relative path" ~ ../data/doing_stuff.py +@csv_table: src="../data/calculations_table.csv" ``` An of course, once data was already generated (it happened twice here, twice, in fact), you can insert it without any script: ```python, Ayano * "An example of runtime-generated table insertion with execution-relative path" -@csv_table: src="data/calculations_table.csv" +@csv_table: src="../data/calculations_table.csv" ``` You can also restrict the columns and rows you want, for example: ```python, Ayano * "This is only a part of the table above, due to specified column and row restrictions" -@csv_table: src="data/calculations_table.csv", rows = 3..4, columns = ["name"], caption = "That's a-bit-cut table!" +@csv_table: src="../data/calculations_table.csv", rows = 3..4, columns = ["name"], caption = "That's a-bit-cut table!" ``` There's also a special columns syntax to display some columns in value-error format: ```python, Ayano * "You can see values having error right beside them, and formatted accordingly!" -@csv_table: src="data/calculations_table.csv", columns = ["name", ("value", "err"), "%"], caption = "That's a table with value-error formatting!" +@csv_table: src="../data/calculations_table.csv", columns = ["name", ("value", "err"), "%"], caption = "That's a table with value-error formatting!" ``` That's pretty much it, regarding tables. @@ -107,7 +107,7 @@ The only real upside I can see here - you can generate-and-include figures with Oh, and you also can specify width of the figure this way, if it matters to you, that is And of course you can generate a figure with inserted block too! -```python, Ayano * "That's a third way to do the same thing!" ~ scripts/generate_sine.py +```python, Ayano * "That's a third way to do the same thing!" ~ ../scripts/generate_sine.py @fig: src = "other_sine_graph.png", ident = "yet_another_sine_graph", caption = "The same sine. Again.", width = 0.2 ``` @@ -116,13 +116,16 @@ Note, that in the last example, script actually saves sine into a folder near in Note that essentially all of the paths used to identify files, are specified with quotes. That's because it can actually be dynamic, and determined at runtime! ```python, Ayano * -fig_path= 'source/other_sine_graph.png' +fig_path= 'other_sine_graph.png' @fig: src = fig_path, ident = "yet_another_sine_graph", caption = "The same sine. Again.", width = 0.2 ``` ```python, Ayano * -tab_path = 'data/calculations_table.csv' +tab_path = '../data/calculations_table.csv' @csv_table: src=tab_path ``` -Note, of course, that this removes `Kyomato`'s file validity checking and other path processing. So if you happen to resort to that, make sure that Python is able to find the file by themselves, from the directory it executes in. \ No newline at end of file +Note, of course, that this removes `Kyomato`'s file validity checking and other path processing. So if you happen to resort to that, make sure that Python is able to find the file by themselves, from the directory it executes in. + +![[nomi.jpg]] +{ref= nomi, caption = "We've actually completed Nomi some time ago, lol", width= 0.5} \ No newline at end of file diff --git a/examples/advanced_ayano/source/sine_graph.png b/examples/advanced_ayano/source/sine_graph.png new file mode 100644 index 0000000000000000000000000000000000000000..bed9e05754891dfbeffbb7a6decfa3f10703d7d7 GIT binary patch literal 24124 zcmeAS@N?(olHy`uVBq!ia0y~yU}|7sV0^&A#=yW}dhyN^1_lPp64!{5;QX|b^2DN4 z2H(Vzf}H%4oXjMJvecsD%=|oKJqtY}9fgdNl7eC@ef?ax0=@jAbbW>2|Bf&)FmM)l zL>4nJ@F#*W;|lxbnG6iED?MEtLn`LHy<1)qlKSY_$M-%Sij6#~69hOHUKdetU~o;m z%3-#FarLoH9h14~ay88N* zv-!#bQIpEmKG&Sv`8+4@-pt_j*csOq7H(B zf`a^unKYD?lpZC9Fu1z7xY+b?EdWWk2QrEX3JQKS&}sl{b7Ku~adA0vcm+dOM@L76 z#HeO^1lSd0*#mQ@PMx|j|Gu4+Sx(2J zI%^))-Vt8O5Z4fI|BaW2Cj=Dbg~s!0eojlT`=AR-6$dFw6?Yaf=tXbqdA)9T--QbS zIcC!F^?$!g%5N@t8I+Wi^x^a8#kse)?fHJMTD^CsX|`DG?y{rTuTNhWyW6Px+Z!nl zeUJ;H?kr-Evo7m-@gn11-S4-XrS=y;_glB`R~8$OghNu2lE2+g6@J?<0-Mv$u9DbX z^wi6(N5ZkD#^&V7lShspH?H|n5Vk%pHh4KG^*<8InU)&4%y|y??6b$-+}tc_mUAQX zxXs5S!e{1MPrq{IN>^?2u^vTB%b8QP!SP!(&TDFV<2&eUmBHl#;$wU?;{Pub4@+QTf4 zb!WoV#?-{h%H*s3hpug~+~>HK^QC}u!RfLaZ}L7W&hY46F`3oC+Iseb2M@e{H#IkF zYHBK~tGA2kM$IYnDJ%Py*>7F=$YoyjyPZ-eb(ECSmv33p^5ux?6V>yJFaDdlMeCN| zjg6bV9u$wl3B}u1xcw?Yd>9&m}~iK5CS1VsQHDe)RwA>;Aj`m;YaR>`&SK z|FaAdnI0eSU;o3>-hTdu4I5r{Xe%j&Za8)7#=b<>_G$i0lg~L>6+ii3pV|C)n*Pjt zYb~FNI|crI7V&j-Nzbm0ee+Sw%89J30ByI%!?-fZ^+_tBW%)FAMGa`F72N|DV2oJ$n86 z_Fv519VaxWiil4%sn_bAy!^tx685+MciYcrxFr2$*_OLcS<3fLnf6xcMacU%_bzpD zao?D?ZCE|LfiBm5<$izNhc){?^FRCS5>=Uc zo=2}w{qOQ`GW&lRE@I;g7o1{sTz4O9&6gK}+j4KSIrPifR=nMMUBV@dtoo7>wl3!648!CZcC}XKcT0pBw8Pdsxa6(> zv{gJV;l+i8XJ#6=FP~pGYkvK|nKy3S*nDbfzF=oZi2mv|Jx}JRU3xP8d1mM~@rJ`J zldkRVinWTo2GDIMJ%Dt($uGOufL?r@Vi1TSX$jMu=NY%2S@Fz9BGp@y$(T%Qu!^oBZWe zJkJ5fSsUIz5`R(mbgf(Q$<~X%w{tyUy=bo>IFY+J;k_TQTfl*M-1fm34JgVS!lnEYFpJ?`!CzTRE6D|V=Z>e!W` zsY{oxsrj$CX!>3Er|?Z&+_J}Wr_HZt{&n{|-|Y2&TAEGN_UKH$ z@IS`+(4Fr-?!uPopvpTayYJcaNuOSse!2Z!pCQb2ck71Zb<0wJa~wW-F7Alx^~YbA z$9sHx-8+BnjJVhH6P`|C*VNTCl=XeGW+g*chsW9zx8@!@=*j%Du12+WnmMPT{^jrg z_&$uX~*~!JXM^>XO2gPft_c`Wef5 zFYy9Qw)1Jr!?e4#i72n+5 zc(O!i;^iPl5y6R46E~%Z&8_>v{BN6+*^h4vo*w4T{u7p`)>{6q^XyA(tz5l-`P)N*KW=t;etPe|y*+K`Q}f#M;Q=12Cwo2EH#genxX?-$ zPlUR4^D0GEH@umgD06H7@e-Yh zTww3(8eN{8Y4*KL>W1Hjlj^hh`0L&V8rSa^6_M)uZ+m~^`D=5gg^9J>OB~y%T)8Z{ zlL4%Wm+$TU=D(k#GtLNZyY|}SHT&}*`vnVMMjSKc&h>q{Hhz+~f~?o2oBz)BiinEM z=u=$;iXic4KLXX;?RGSFulf6CGwbCi^VZuu;+9X>UgK=SzW)D|$J6u!SzmipJ~vyl z$>{RNBZooh=|rWru4dxv-O4W_HchpW3i$MUCQEC}%)X~ZR^Mb!Y-@ZUlv8<8yWVj1 zvcES=R6Z7foL6-3THObRyZ!5z9QdU6`&H(aS2ymKs7x0%eUT=#_#czQ{vORW^Z##r z=(`$z+qfpnj;TR!wOo4iC;c|z&gI~E zFP=7U%bboz-PU{rPN?-?}-T39#EsITi*kd>H|0Q$JZVRYbfn z;njQ2X9AMnx-*LJmw>Wv@=jUz#|k;y&pqe}ov(LvcBFafoR=OjEo=Y&2;3!>`(Q<= zeD;y&EM{3*R+DaXWy_!3_D`Sjg86UbYxVzEK6H8H3HI=sC4auC{{8lsyJ5=>agO&_ z?&d78T5vqHe3RCX?+hgoA0?{GbByMA*tCN372>pBwkKL+-~F0% znB~&C684kw_BmFQDi}b)voh$={(nBxANk5`ef9YJZ~qUgC_o z{B@q6`i*(jC++eBC2+rN??e1w&duR+h?>9u@}}CBhc2bzR~F2Uy!u9rAxf`$%A7Yo zhdY<(1%uoYY*nVx&vyTA|Mas;iXXg^*6loAvczirZ~1!(%WdZ@v$*+ty?w&esWbaj zN~eH5dg)ju|HVtElx8bf-T6E3rG?zV^>qw){s^{CGry>QZA02K?`6uKvP(f}S>5yB zNA4v4vy2MQyRH|S@EOfHQM~2lt=UJ;J!b6is_i|KE6t-mX_p7c(OM^MXB>4sJ&z$F z^n-_z-p+dkO6P^GADz$n@Yb<5TF^cHj(4VK=Sz>E<2!sAYrfwt&%C^hx2~@4$dMx( zYJPsY+_(4jy4@RY=BVf1-Br3;!Z`QVmf${34UHAOmjwk2BdwR;^%kgnv#I^!U%k7} zud~d)>vv&outnO>i%Tnvv(x=dWIbO=Y}aToPCJwE=Eg>wuUCRq)YOa;4luOwNIJQ> z9n(C%|Kl;~l+@JFoS8Fcnq*#TiO$=(^wN$42M%~W-n41chtHo^_XZ0I7J9xG_gVaW zW2#7W+!UUARo4_mLtC0|()c>hZOO|^t)7$B z1jWRTJvlkq-}2tW0s$eRppB|3DidbUK7F_Rer)EPvuUeM^i))qnEAQ898tC8f4ltZ zs{Kq81j^LkF4i}ja`Tyc#cTRr(<*BEq#Z|La8dbebA}3EU9EKV84U($aFuv}r;z zGBKI;1qB8-Z{AF=sR;`c`~3X;^Lf=;>*KHPEKXnMJNww3oy9ZlYOU((>SAg>9$m70 z`TTDY!s>oU9u|b;Bqk<4FAq{u(n~wM>E@S{v)yN=K0flm^T)5Bv-?#3i{9ELEWO*7 z%c10!^qadAvn(cFVbzOgs88K|Q|oU|7JBRc_Wby~{eR0A zEnX~XkicM^er`$f@xDD@uSGAnz1G4hTwpaHe$Q%N1rnU4rk29a%V&KdUigXM$clLvvHphfklZGPk`y(8#QQmh0cN)JUJX zR#SicsHmx_StXOb{r1bdyUV8OHOu1Wpd)bHyqWd8gU>xJXT+h@#>5Yvqc$^5^_wfn{W{qXEmf$8Y~?Ja$oan%f7FrmkXch{P^|r@t#Mihs5ssy>H%pbJssV zt8X$HR?~JU&Q6V-_EO-Tmc6}v`{BmRFRy%UPxJ8xPMyLA(3Y@5h(nA^ z{M0q2M-sP3MtqiAyk^Ul5|u(>h}yT^oGe8>vfr+K z*_F8FrA3f$RDVTl@-+FmUY8=ipG~=3c*GYpFjdhIyOQ&W%pa+S$J77IuCL15$g^fn zYuduKH!r-4W4NWV_mJT8#KWCOWI>9r6oy4g{w$VTK5uQ$kp}OM9Y3rtPh2wVtAyRc zM|_I2m)Yl9_3dC3br76rrT26BOc&8Li%+a_|2#X+Ox8WCVf}pjcJ@D_4f7)7Hf5Bt6OXilqU%cFxx?%) zC;gLSxUGEr;)RPx3Y1o6zE%1?=|Y9aPk!$C2eq_~r9EP%aV_ZZkezDvdA)_jF~4dH zLuYvtS@$f@*U@oLc0Bcd{^+cV-k&2SGKGPlU@Z+ObF;GDl*Tln{^aDSZ%3O=_OIYQ zx%ScN|KDGAiinCy_^GS|Rd`GIPR;phvTs|G*-sZC(}Eo@ErL!jYO{Cd>&?ha-CTD~ zd)ea-ZctllWpY_gjIYnV?+g|E|E$e+$Gx)kbvW$yrbC9?Vfxz1h0mVN;8oFcU(cna zRC;D&{`^CEnBm!d@Ww%b9~ZTnazJP%0GVwxqrjWw<7#n`^^jbn~O`j zEmkf7#Sr@x5%G&PZ|xa9IQKU%m|$?Y^GK*mOpUB&o#BrgUMGM3lDu4a#1(8LW9NMz zwKKQe^%`C7{aNzT;!)Boe!czjCH~Ko%JQOeEqZq_ff`8W;SmLMHcZ*h;PJYEpO;Bm z+D~QOGUxA%Yw`;;%VxXYD?dN0Pg&1_HNfS_S=F~V)9*vwZqCfZmn zt>-u}Zw33uub<85c*MkdL{tmS=4U=o{nOrr`vrJp@XnRCNd3z*YQEiwy6||H{j5Ia zyhVR`&j%z_Ufx+@EIqwG*Q!qf)LD4cHfN`0$z4<4hBxybuXz48*=)j&-USy5CN<@= z97zA^J&W~A0N7%U(reu;MLn_K!oEltohp$jG=7_3GfQ|H|6H$a8xmiJFMHg<2v&Hk zGQ(bQ=6vRY9p{xRuC7kEn7Cv1f(ccsE+vn6@DPGr;BH0#XUn^L`(7H&>EyD0nmx*xxOUD}!*o_2m-?B(T(N<|0F>i-(? z=BVFD%=i7S{n+=op!wB*ZObhZe_q&_qOIw@50tNFMvEMfG)^-pdlNCQ>XoLDurR3h zc5dTQR90Sm>iE*7OD9g98oIIc#f62B%C*DSr5x!HJaYW_Qa$4$4zCyK)8#jJ= zb93^^$?D;6USC?ueYlM``O%S1_1^eRDV)u0yh`Tg)7^Td=6;%??AD`TZ$JO%=jY*X zq-`n;dZo?NetmhFK2;l5%cvL_e7NK1bHYBo|D*o7XE&!?CZWtwCaCZvm}<5+M-j(zrDEFET$V(@bl^P&zJq}Lx0^pHC5Z+ z>RD)bZ0J)%Zf@?A=g+U-(%Erkn)12BC3pX7GFTe&ul{qAHCyV=q$d|XMILAnU7sO# zp?OCQ|NNTB%|BkPUjO3CO5y48b(W{6 z>8_TTJa=xb-*J{17KKWD?TeGRxw&^Y7z+zpnoIBhr_R?mF>!x~T=?BwtG+9ObMtFv zJ)Wj7>$QF2rj%tbErPsb_A&fBm9{zb*Ue39CV%_8DtNhHkDTqSdA8M;*2ml5tNk9k zZs#+pe?QOH&--r3sOB@H;quEhH;jyp&wtKwaXEK(qjF`*y`P*6?7Qq&$~|Oo4+0Y=d&FiPVd2A|!N|Aq zTQUUy{{C+N`mu_d+NDLV-Cgs4e|=qk$xvJ`ro!*Ih~P?I^*-5!hqv`J-ViU{|L&LK zRXvR*qMm2h>HkmsxjOBz$J&DK^8&~E<^5}FZ1z@tT@=5+ZqNUJzf)3D4ARa>Ecmnd zCSiY40|NpaVA~(-4%?>+iC|~y@QE%^;pt!1+s%?C- zr{-7|N9?IEwEzES^Tc8iLCd*MEGNI{o?N>vp(>_${Y#4=_ttp(#zp=6)D{_Z>Sl{w z+fBpQI9zpGIZ*H3?Hb1Uu`q`x0yq`1VYd601Fm-x= zKd*pyK}W}OEj=xjn$zwKZ{Ag{KQ;f#j%ja|JxXHu9R3}um+(_I1((LIs%L*s5a65S zth{Lb?)xP&9|atp)fg^(zF}0he4X(ex2!HuHbC-eU}XHm zGiMFwxUHHD_OIopAEFGCWTp$>+BNy1$)lHx*zO(DtJVe$XjQ&@z3j1vX%M4`V4>j5 zmnTc?H}NzqYo2c~$BpGpT%1gg6thG7-2clS95FoHxnvipX5P`Urcyt7d!)+G?~8q} z-&^*$gF(PHMdWv;mq6u`8uOziGAq?U6^h=21NY{Z*xSf6>~HLs@KgTuzozEzjhhek z7@~51HC&n`w7GFf6xhH9+2Z&^$HPvg&G!HeE|thk^io=1&!NuP68YOB?oa;Z!X;85 zg%ZzfY9_3Bvz|fY-S-U-O_bERGj4R8)pcjMpzu(3%_gJ6oll%VjXQ~LKWBUXxXpDl zG2iz}s`<=5d9-va{C>}!&sd=G%PyyEuTFP% z7O41KkjBYhF00A{Z|A9)?%`>;%D(b zX(XHb+FvDm{o?-#e1EXxlOe+{>-v=Y>r4)3KB)nFBPsr?7=yHPo8XNVD<7IDr7y4j zq6!)lv5l6L{2XU0>wXR#zl^sh)hjMC=oC8b_d0vahs`_0SHEXwaCtYs+1UJefs~~@ zC^p!h{QV%s;MsRy@Y1U0hbBtT)jn;KzL}cu+OW3aO8$KlS@(0zpvJE5>2n^-cYk(g zkczdtS0FX9IDPYNONl~$hTLucXFpoxbhz_L49Ia;HpSlG>t!1)DP6n!vfZzw%Y`oI zyq`>{wrFWi;G6j8ugT@YCo*7#O6hrg6K`&2)Oh`S0m$O>>+2Yz57+ZMEMB6y%XZFe za9h)L<^6r04Qm_bek)MY-+i$}W+L~cpSL#7*-(=aziaN<)nYUHl%IoIaZ9F&h>Be( z`@Nh&rblM&?R}lMvq9B^V>olbpUwMt9{GKFrK}X>JxSbWsb#yRNAjZf9wS-zB~e>; zWXXU=JXMmjC*3qCx(15k)ZN5eK_R+%A)7M?^H3b=18UIOu;iBdJiPZ+D3#2TiL4mw=LCVt<1~Zf0 zV(ydE+Aep5tY25lzWMBG*}82>+iI*&mdIFg<7h^efUfs}Qa9Uq=qd;ontMtvcPrj~`XOJ!T zUk=WuPkinyVmLR$uz6$h@lRKR{U^?taiW2dS;8oVLsC+5xy=5Yn?@k3o}Hb2d{^mf z&_vkv*mBv<&d#aFSlQSnO`RH=`58K-#Xo811Ku}g-6{L;wUl<4$+|CbbBQk%|DEX- zJSS(y|Aj9tirDf38RDzon!5E$9ku;_$9cM5>QpLsSvr#x2oopt1KzrEl4 zdwXAASjeou?}w7CRmp?d`TJIyoJ-p*C?ph=DJ;0s&;4`p%Wd^~4BPhAo6K=r@~qBJzGX1E{bQk6TUnuAJVSR^SH$)_*}}rYso#ztJ-RXJXxC;dx5!9I_dc10o|Dz4 zev`MYn)2a8LDaUKlUuLHRbS$Hm@g_OHfjF+c#s{ZmQ}vLc%iI`=eMg1k3^bPpGWVj zqPNbVUQ6t>gKepseUDGn!roq5oxJDQtJN~rWjzlMw}-!ZeP^ffFf@9ymMnQtd+ zZ*Q-+??=+5%a{GD=X46IKPs@$+O}cChR@cTf*(0jmo9BtR?FlNw%mGSb)}@AO3-GR z-sT$uHe3F*hrXZ1s~lSNlj(={?6YAn(@$^myH&1rdAa}do9XjYkM&5JkhRMemp30xSS9n}j^zm~4`R8QI z?;KpcexKL6mR^22((u{N=kq|>D*bq$tct3tVb&FmNs}gN^=T>B0BHJ!AH{w#yw?)~);7w&Ct-UeG+z{_5K$GM22{H5!!NdJ--z@wEB>=X1&XdvjAy zPczKFXLIQAVa5}uPOaLa_F#%;u!g4Q!+*cuFL&)06TN+y|NnocHIcdFm-EqQsV zb;k~iZ}0A&-jsU!#pUJxYv<4JxT1URj*Jq29ecy~qxD8}+?IU1`KFA|^$%miHEX-& zOLm+nQ2MEhZ9FR|@oM_{i{^h#%WfUc{97VqwmbUI_H7lUvEn@b&use2d;c)8b~j zxU331)gLdAC9BZS=EAq{c!`YQ;#={54P2**$99=dp10#niOf%MrE>Lz`ofxJ%BEl3 zUa^~)%yB!iX2FGmAesHl2Yz0j-FErJe^9$(rJA+bx@FOUMyk{1nmNnaeP(vCX-K}{ z%-~{KuabU0?{Mama8TiSRpoj6C&|7kZOj~;+4&Z|9oa7~JPr!lyZceHcIs^#U7ojE zphA0<pK8~!)2^DGxE)IDB(huh(}?f-?K(l3(*tmx>p%KXRa+G(@Ht*ySU z`*m%y(mb_ar*5B6>ri_#dD8>s%Y`hU(Gc#im=KTKb1yTV=#^QzG~T6d@x1N)yKmj& za=6?Zyz$g29(AF;UY#8=3#J^MFMLLk@1a80hSxe}+K;?;#e8Nst{Z=A>bsQBr+zMb zTwwz?@5%g#eP2#r+YtQ$)W>%_Vrn^yZL?wq2ZQVWJ;@fmpjJ#w!Cc$)39g$Jtl>th8+to6dcZUiHWXwK5xm7 zuZQwBpZHTZ>7|97FKd9y6Pu?g=|XeAGcz<+tJE?vu+NSLlG;|2ZHe!lAuK zl0oKWVgExDrFVzlJ#`IRZq1mn^Q-VFFK=G;Pd=cydan5NcP7ikO z>WS=Q6Z-9S*Xo$*%uTiGa|cDerF&SUWTWQOa^oBKs#xyX?Q0Y6+#&v-jn9mopY_1D zXSIhjZ9q+!6P8}l|Al3}w*_TXUVizC-S@a)p?6}qtrK8zx{mgz-@4=^OoyZb6NKzYh7cCWOu(!WVyk0!^GeF z&=oaMm~0LycqB3R`?UJxjRt93K!I{TZTsZAKd-AM+FhMwuoX0-_e5u@&X0~`PbT{s zE7gCmzFg??s-;75@m7@{@YqWFK`rgYFORKcc#~BQn%d-17g{OJBxUw%zG~t!3BSjd zpfr2V>D8^ zbKI8XU7TdF)gL6o^X;vd@~;=?8CqgKf?CF7F&a7~z)Adx<)Y$Cp?Qd<%Jzf2NZ~3Jek(<*lZp{v_sjYo^YpeF= z^z-Xh{!n__QXA{o(W|RlOQVz? zJ@N|Q$9>?~v+F{TcK19-??YGkAVqMxkhHY-`FXZ8ZGQgWFM4IEw|E<`^sytI!v0eK zj|lr4l)sA!3J%u()+=e;_T))Q*t!_b1090OC(fTw|M%yoXfhkWoX_K9y)*4k$ zb^iSEhXoHxtT=giZ0=tc78Ku-e%sB-elN@Jvr3=0rGi^ro=1{%BFx%P$iIIuy1KfhYt?dM?AoW) z$M}8fH_m?YL#ZgOT5MnA-L#1@`}3y z=1s7jnzi-sDUZFqyiS}vxe{!wLE$4889w);Vq#)tcFRlL+|AFauTo&VJ!|s%XpxDt z%8yje==gh|A>sY%?EfxVx7T|gdLqn_s=Dm)6;C~s>AQ0#|F-`=|8)jZTPA((|ClXO zHsAJ(A6S?CecD&kPv$O9zL=e@?q}O3{ASyBm81g(mkV8Ng1&66UvzKr$}fBWMqUnl zr7~T=!O_vNqOx+XiE;UZ1C5i@KGcZ{2^F3C_!YL`&GN1xV@+)>ClAk>EmvDEn9cR% zd%^DT?a}tEC;1&GR!*Hdt?1%4f5sE&lUX)Hqrl9!)o zz7Oe>A1Tg>u;p5}upC7i%8HZUnV_5;pF>z!*iuu*+gmaZ@2UJ8u_=Y~?(XvE z`Sri0^Y{H!GrwCBe00XYzrQ~p6_5Y$@wmLOsOZrxnU`l6CbMnaxN&P;-SVW(w=Hjc z)nkat{cXNfbr(n2l4Wo2ZJM{DsdP$|Sro5awpCw;M{i+wDMM_i?|S8uM>hn!;$F5i zC#%HT%zPJqFY0Z*v&+BxueQHa`F7#D%CC5z{^QSQ_9-iwy1ZuCb&_Yn@>TK|CY;sH zV^|Q^^sZAQX!4wPexaGc`waGcKN|JR)#a6$sDt?Rst?XnqRf=ICf}GkE5WMIWA0R| z_p*O-TiGEpc6#4xMSF9ZLcsr$v`d?ZoDVh4QUf?b1 z+}rgwE_X$~wo>UmYf#hH((eD>V`bhidVl;hH<;t*vgFxHhFy%a@1A>@SI&H)>i6?0 zBI2NqLiw8;J7(p%YEFqLyTwy^qC`e;V$?#W7ivoE{Yul@S_&pikxvJe=IKxV1k~l` z79IBHgfyN!-Ucw12-p4Ny&+Vk%&_nE7L^{4(i65bT(3NAWnkgialb%HaN@1SOfTGS z#r-uPriyy!_L9wtv_xI{Dqzd47A>8_2r5EV;IVVb{b5)82aIRcJD_ z*VSAukP`e3YCP=*HJ-YqK#iv(i(iPIKl|sGVd#~Itxx367|n4zQoB^=pZQr!AHLp> zU9a>mgA)5xt_8DSUirPERpx`5K--vv>{%3S--y-x{lmLlP;h0eRzu#g zwC$6nEx8)DS?^!4W5tC6CBg5L=k4e@cXYPqradnTdZ*P13Z7rg^rCGM+x@eOGAzX% zQ;yD`-KYGh?9;R<=jScAsB;hS&_0~$;*vF=Yk~BqubS|NOUQoWh1`;puw_21d-wgrI}^gvoqf`(44 z7ko{*bA5{*I4~5|UzzUca?b5wXL8AzKl}{GtL`L&OtaK#$XoL;m7hUi?)DkZzrRcP zDL?uZB<8bJ({B@}(yvn$pwQ?EWGqRZTlKeZ?N_Tu$=Zplr?m*KoCF${=nZDPar0Z) ztIFRJekw{yeOwD>pYGR+7h3lBon(}d+Y+wX;kpZAU-J1y*p=*k#t1enh_R$NF#N77 zUvGw+-bwENsaAa*r?q}Pm1PKOYI~3&l>>5Dq*g=Tx#QRSzl0rT%$V_I;zJXqb(#0i z>AJ}OWo?+CEoU~z&E<%V=mDz*hp+Mnrc_>BnZg}?UEfgF{m9c4of$26((3C8J-J9y}iA^{YG|R7?q-^77{N z{ms%)JNNyOcTZC-CU$gW2QaR2uG_oWvHi^dfQW)S1yX{a&2;|qLp!}$wSTmgK52^{ zuu^$-s)FxAi^8WH3se(bHmsk=bl};8g)AmHDP}z#9o&J8Yn=Dg*4(^T#lq0{gdfu2 zH%t%DVw%|cT61g87Ad21V5co&ia1`f?^z;f+;&Qya_L#8%Y`mIH}}8bWtb!#};{dK9#9-o?#LNI<|LBSWM7P9gj6Z*Nzd^oNt3YG<*Vf0M509@sdTnj=&VRpN`%76@eaQe#32f60 zUUucl$}6)zoU`@(_`3IQ+Q#&M*`T6p)uTmDx8BX+JMi#C;EgwW|Bcj@f?k7G>rcIM zB}7ao;=roV)g8UPtPh&`?H0uEuUoQo>BW_oPhS6OA;Sk=+j2iTf3NF&yV?^cPAmvr z9VYtw@87?ioSZ9fPBu7l?SI{mk9tb{HOCpHay_-kzw@C$dx3!I5B_D3 zJ34l#O#kQqc&c_dXqe@cyus#dloEE*j4)a z7-()|qO$uc8^6Rv#h*WaPBjY`Ej$@ecI)uR?V^)@tzE||a$a!44@HLP!)=dWKL>R` zJF<&jGR>Gf*Z1?Yv;Md4@2M=_w0ZOSR}!qOtbcxkmt zDV*nRKJ#o&JNxLEbpD!8ptVhFc8FbkSRf!KR+gt8(w$RgZ7H~zA!F}fdg|}~(3qGr zCnhRuYHL5fTYi7-m)#c^yBCAja|sF_JTX!E=cm*9k$Wl(UteEu|MnyD)Y`{0+2N4k zS%D&txG$YAEtE{({g~v*pkm&qtg$5YYO|}0&5ViJ4DuBZ8jb57Z!3L$ZOM`)8|wf6 zn_O4^;elgQbMxw&n!-Xu&@}7wb94Xvc-%iV?Z4mB^z-v7s;ei@o9B0ao~^&-vFq2x z}OUi;L4{8vn-)g+JJ6Eps6Ei~&E*)OqZ z*U@=D8I_de&MaNtysPBpp$7+>C2gxrR)?)ky1FV9G(VZWe($zR<_*^6@6Nmd4OeSw zK72MiUrkZ55tNQDT?*Q?dGpD0=a#Kl!O`&c_V&+zKA)dFVZwoRvAbuO=gV!sQ^ajs z_2t2%Zhf<&b*8EsDm_n*Jze_#M^LS#%PTd|82In!)Baxw=eFPBp*L3o17%sX`-<~N;bs`yvi^Mw+$ zK?^!hc;|!a@*_`g>YU!>(_Rq%^XE?s8NNAw&n4Fzn&cin;^-o_Etf=jGq!zJ)&}9ahR+tG5^HzF|N8&mL#cX!ral z@0SanQ7p=E`7)sz)RHRX)@qPb`m{;N@gP@z&Qs<=XazOfMW4 zt>4)nBXy~%Sw;WX8PHro*q_tU3`4oE;KhhJf6mcCVrmw z^=~$V3i}u3HO`u6RbZ%RXHK%>F;mf$UFK?A^0JRG9+*vO$x0ZIO?R5O9 z`~F+~fB(aoF6VqfO^7?TvJC0{zc<|cYNV!AwC>BPmkVFx_P(8cyDx)7WeL-Zg;w8p%?Wv*_v41w&3|jt7?0XcnKtD@(q(prH*X5W zA%ia+pgj=$VG_}GA8U^6iv$mcxIH-&RQIFDAY0y#sbf#v_dlY7mhoB*aynTT3d(Lr zU%F7%WlqE(;A)HR!on66E`f2`oM>tW7}8z*uU1O?5PF}*klnkwwrA>Odx+t^6@T{fJr$7BZ#|16dPI|hY zd4fy{uWxg-6sWauFNDE%&ieWGw{P8Ha|k#ka#zkT=2Bnp>Q21~@0F>GpPGV}$0{&sQtj6P+hAoUdtTnvhdZ#}b0EE(MEE*!e>Na|D3rcNt^?SI<4<;|qsT$b}p?!ciSwGQRyofyjsofCV75&-+wo4 z;@7+OKF0<5%OBkkv`Y73KXCK2|BW|QJB0*S>Q%W;^?LPxU5(fV_jlh@(w9j4DbLe+ zI(vJjkKON_gD%JKCW3lSa~CmbOf7j=YAId2`*E&z>UZ;5tRkRVhVy>VK7-1%=Ydx~ ze>0rp;j$zzka5c7SR=WH7tF=rg^31_I)(PncH&?^xlr)p^}C>4;W2kHlZN`svX7-X zYp)C1M;k^z-;-n2S25$#B%!D!K5PlmzjuH5oS$GZvBTpoNa@C#Z)IKw?|b5QT>Cf zyx(d{LBSymPq*yi|H)jPd(cE}{*;}d{zU1V&@i_Hv&22mw7ZO~#8>I48 zGJ3sV}y0(Hmw59SY(=M^x1t(Uy-(DyTDv4KqCe%U;UVho`+V$(x zCrp?S{O{b$l1~@ipt0-z+UV_lj~*pGdh|%^TWedJkf31T(?Zy4 zr^=>ZjJ%%t@pjx57u(8BxHYb7*%nF$vT{y|C_A*S_7bQl?0;|xyp8kO+1c~A?63U% zEG0dC{UMHsF7py z?ntMw_Bz)R3z?SI*16AT?wr3O@v%iqb3nGe=ly-@R()3#kBk44Z8+i`Dj4<3|a%b&NF+8rdZ*i&Ca0M{UpZ_44BS{r$cD%W`&pITZ_wA8Y={b&Kh8@bjzN z+R8Rem>|&A)pg?R*|jFz6%P(D{`&sD{?_9kzkWS>{kr;E{q}X8Qf7UJ|NrUeUh{X& z&pfW$yowVgDuu=BT510uy|`<4sX$6l@O)wln}LQ#$GN%I;X8l+{CN@HPFq>==GE2J zg;sMz#kStJDtRFQ+9sWGk*Q(E42eB^_eO5X2s}Dt%^DrEygL%Kj1ubt!GSyyKZ~EcF&=`Wa9X6Gkw0Gq@-s|j7(u+p^K|4C{Kh*O`bNb?Q&$@`Y3&UeY2b! z2QDmhetBb~a{0YVby@4Ol&7brn&jNr@W^oe{(n}WC51meJOu4|`0@Mq=dUurG! zvF`Z(#^x>C${8;x{nS0nbjtGPrYc2HH+S2f49hR;LH()<6;TIE{fXu_PVeg1v-XXLX1;6)hqrV?M{@&cSD) z+#NbTOKyBp%2{#kudtw@D{H{H={Tsrx;?);boYB+)By!1TIFXZ2J zUCbwVj>`F}lc3etsY{od{8;+u*8U}8hcoK}r4M(yxY*3*S`Z_UFCV|Ds)v2S?Q@wH z{PHGqJYp{XVhsjOvnYqvTHRk$`rpOHCzx>x_qE5bs&8cSt4~PInfJB)a^aDqBI2S# zlRjuMNJroIT)aR4WTp#ifZmMoy;V=X*2*-zu)Tcpr`W;h2<2w1;DYtcEALu{j>XTo3cG-5u8@DofAZwMClol{)Ts@MuJ-FoN zWri;{2D58-U*b_03ZL-hs?&nAyFcE3$L z67$_1QM_Gdwh$4~tEfb)RYyEu_ zS@*2Y&@i`$<^DxBd1PG562Kyy}x~ zP1uy8cDHMi=9WF2dq28@eP$BEu=LoMJs$gl8Z5sk4wLLPl+_Eda9oUT4Jv3$?GxKuJPED>H5EFE-Qm_-3iGR3|_sjFFXzsnia#_ zaQ%DzvMozM`w{$h2MNE}`rn)36Z=2je9*c|7ndhKAq-2ywn+apa$obSMC0zUU6!Ec z&ebDY+K*pc?+-X1=9Y9o)g`W~`K5=FQjr))wFz(D$z^@@ObhG@an$N4ju80g`oDwBmd>S;hvV`;JlDdymK&#}q`s7tpu7YxFxB3>}ba3*@ zS;^qF_T#sYs{c2A`pS^J>Hpz(y03ZFCvi`-c%0u7#eTy-c#*puXh6SX4zsAk%Fv0^ z?|Lk%uiulqROWA%PFUp3mmWoO-`?JF`W_q1c;oVK&p*-@;6zrukV#{!-j8Y5H8<>e z88*e|koo(Weag>IXlXxwaE5(lzIM`Ne-YdEKL<-x1O+YKwHkt6JKZi(+xT`b!xry3 z9iUP8&L=S{b^rAj`L9`c;?tsozWIrpU#Ggb9J%{qYIIMB@w$PK1?Vt zJW@Gh+UokhkB-N-l)uU|o6ylQM;z+_UHH@F9JAY++r#53mnNM#(8%on>n~(D?n$`a zzB-xy-|7steD#N?UFGIgpR{@7%lrM8=Nx{saqEKLx%J>PM$S#f(cs&6>Nsd`;jLwU zbGy!_Jzh3DZVL+aWE?!T8`3LXhx=~Z>9s7hXA z|6})e3#NZqEe~pocg$H|wTwAL)zowi!q zZcfgXUp~*VECwz8%Dug9?UwuZ@Bd%2F=)5dy?<6a^Z)zI-gI}(pGWV%>~`MlwQj|h zB`o_SyTO}8xvFBVu7Co6Cxf0X^NymYUNU^{u0NSy+}^Iw*WN6u9X98x;kWnq&zIk? zJ-#AvvEQ$|>GNyd0s;g;ONuYL%RfCaQ8`p@^@a@-{`~plH9Ka8o1whc^w%h#G zif^+W?tBt*?Oa78-_qMB)ElmC_cH>W&!RWMTl~PAH#xJ+^Xo4CdvQ64LYg8dz#L}nd$QqudRtx@BO*9Q&>GLru#*K1qU~`_3z0O zFTX1NJO9Rm+rswIhVJQi+#@qrzV!HHZ*+O`%Y9q_{5u=1B(JRD^8cNy%bmFsnSQ+4 ze16`yZmIowoSd8vKgF%;PH$Qh{LX&r%HZWiw{y2YeEvK<^Z$#Bi+4Wi((dT!NO*E$ zV$IK|(?L^jUtjyzmv?art34>O(%yFF%$Z_~Y)e`2_S5%;XFe_V{-}2C%%Ap`9-rcu zs;bJ&wPdr8Hk7WZzVy4T-Pc`6aAH=%6R892aupAH?(fLGyUSElQ}fOJ`cG1Y(57Bb zefrN&PnRrRYM6Yi$GGq9&CTlf?%i9meEH?g>F0y{K7Iuq>G8ei_@v_NN_&J?@3!@N z^K8HH-c+ILZM;iUe{)>kXmjt@OyQT&duA>UtDSZC+rDdmgaubB_lO)=ut1@ysp*`> zy7>Kd7IXcst%-D=Z&%9@vA4?9Z~0`f%fB;*Q*1Yi?4CaTWrUn|@olz6m+!Gi=X6!{+z6BP0sHgO zrT>=qiD>f0zTEip=g&XS?f*ZVSN+cN<&N9i^PhwE7Ah(>#_lcyt;WjR`Bbdx%Zr0Y z#p7chKUZCz_WjeMlP5G~w%5Fj+S0f+eqYfucF$f7UCoCtda6MsN^?H{uRA8^K#6*# zn_eu#!auWjO`JFpG$JF?e)N6+|G%EczkK}o zuwB0H!K&5k76mW&11&H(a`foNijPTqOdh9%WXwL7^TJB5;l$bX8`E#Q9qzQb3<~a` zJu{Q1$@|vZyUPFV>bPQPc6yV}WyL$8b#-=p?aa$BdnP0-cxh4U7MZEJQlEE8bRbve zSF4NPK0dAZ{o>Z?Tbqh6XE~+EHLqVg<7(`DwXz?{%al7iLbUhR?>@-0ph9lv8Y>HIXoiJ;BbF+m;?)t7!xxi7k; z{8`9d?H$upYHg;OrS7(DX`WVIyQaU+)#a7O-d)|NyLMKIWXm>x+!V>8w`2aFJJ9uZ?6;o zev~zxNS?k|^kVt`M5{?19ajuQ9SSSwR>wO!wYM|8*jT9a((^R0`l{$_KGi)PhgABa z_uXjw&u#yo4HPOKyupkj+Xbp$$9sNx-`~HsrNMG>ft+22RnXiCe=gU**q*M%FuVG1 z?}u~C_wg?3=(wV*)$m9xr_4Inz5dtz*NopcKR#?-V*7yg)Hx6JnwJp_Vdk=K>tDY! zmiBOQ$?D}=u!GnAu|gm3-wvhKa-e-=U)B9*_dVGLsy6>mjs_i>vG>;H+Ak@WC%U*S z5nIX775(x;EraJof5r>t*R56b{%n8f^6t2ncJkxu@=F_2v@1;%9(+3wT0JJ~?c$O( zk!!(@xpwCL>7^HX8A|><6$I~nn^+zB^`-HbQ}I5Xsp;oMCv)arz6nmhsag$>rd8eh zEhk;OTR1`c!1vR)Puk8%wVD*|5oI&?hU|NW4eMUd@7(dE3KUaESVSEPz3v7-XMVX*gdxt-5ypb!mdN!YO*%4t-cy=giH9XoT#o8KR^{2Qm z>MJNu`R4a8*|OwFiO%#XTdjN9;`>w%gdI?uuTgdH`#w-BBxwTIf*o5w=o!A*mBi&b zP29F$Pxay6>a8|s@}E3S3I4NQI^$d-Xh^8&OnAlbAG7*Ym6X_4GIZVUmN1wy#ZI45 zV7760=zP0mi;1r->aLvp_r2}XsvP~_nO=YGYkC)Z8eiV%;&Q}5)M2G?Rjl!>P+57_ z1^3Twk2HQ=dbsn9ww8{<-?`B*LN>9gGm7;5;6JE)dnU*V(5ApwJ^QxX9bPrzHp7dl zpJcMr_Gem5v~+D{&8d3Kw#a|YvIC#gvQL4n}Kxng@K z{X2AP{e3gpmDi?D-!J{Lu!gmDn)%E9)$`A7EL@h{*#VkioMLq}ZTsct{_-1ly)<}Z z);;6Ma?csBEw@{0>uIH2o^5{d@eh5GsC!FzXE{AdH3xfM&y6+UmCWqBe(&vl*J?D{ zUiVG-dM4cG_^LwnKH0>}udiQzeB=Dnr}+=6!JDsj1O*EnwHkudZ|>crHTA%av-Rmu zuN;5W;GS;TfX$7)(f#|{lqhn1V(#KnEt`9C?V?Jl#! z{vORWmE|I!f%nOw>))KoeX_5Zb#Ax$WPb+h_5XTaIBuT-4m2OufLA87@A~ZzT)%18 z=lPnObB;W#aeZaK{!rzQ1urwMCY`=@^vdrSMN!jJ3mVj9r0m-DHikpO+mAKC#_7!o zPL=QVpwp}-w!S`D|2oKMo7&92Qpam&>bkex?^EcoDZ3FJT=kvlR>|93uu~aD9X?9f zuG=+v&Fp<@vlR}lsWthTW59WT*L~YUe;MmTugo96oczbcZkv!0vz}tbouH_eawAz^ z7neINTnj3ulm-4>X`aS8yH+VP!P;?s%khZM$JWZ&zcP`1yXK9TZ^Hl8e>-h$rW>6; zc51^#m;U*nj_tC=ogE$0fs7*k!XiR$$B);gq~5Td5^479N!{^>;mJ3*B+Yx7u{<;= z=Ey4ld5TZE4{p&rtjhHNr?xDI~2851+J@9~MBH+-C) zpRwD|j2Qg<#D7zqk~ zU-CU)^Z@swdWTZyx_gR2_OkA87Fseq5jt^A@B8J?ZG{!Te@LA!Ex4~&_V2l865sj5 zr_&y)G_1Wlf64W)^@q}~KiS)H;^!sk0Za>arC!#*fAK;4aZdf)%nogTmH56?%y`%Q ze%ZY3{%fCFY^mpue*SOr3)}gR^R-zLVw)RV?*9{WR9u^-+uq?3yYzd$`3|-Zt>SSD z>i_@y@%#7X6@iO4SM{Vu+WdaAxuvZwXn$QT8=uUHWxlhudNp-)o*ZPCSJBYuXlP(q zxBp+&w>LL~nbQ^CExLX5y1=jJU)Q|*q_UF*l&kk%vpv@+zI$WVukI($WmjF?KYycX z=hgiCz6W;SeYyR6@}Jz+oR!yGb;IjD&nzwu`RC%Y$oj%q`DUu224G^>C~wcr##aCU)w73Cdzs9U;FTyjm!&JX}gx zO3KT{g+)7TjYTe_j*iX}&}K*3_5Wuc#@j9PJts8l zHlJ{967Q1rjU@?!PcCpQ4^Qr^G+%n<^!i5@Zg(!dv_=G^!M}kN}Jndo>|{-Keba> z{nMS|^P$B_h3sa#=knR=waQITnmTo3^5-bNHUGu#zbR#z@1cIh%0Zx#W$H7>m*+0a zuypC@8@+BV-jS%DQLLGpw0?1|hs4|LuQ@I*M=nchHk`Nn{YJ9;%B`*0FR!k?UiqIF zzCq)Abh?yr;vtq}{qpf2i|gv_K&O|?4ZJh?Y~J!*7oA+?B<*jO{PHU4{#M8N-Y!wtI49e#Q{<*u`b=$=4L*B?QSi6(ACA|-)RrjG2Pm@<9wW)jHl9nMs3Lm z-1z@MBeUq{CPhU>5mC{Jvu8(VihVfH$o%E~{r_8ze*-u2@9Zjgv`fZ!*29cT-~%fs zZcbXlz{bidC=&9O_4D)9zO&6*T3R+_TvV$1@`BMJFi_BYy57^J)8m*F)YRDW^Yg`Y zqmF!d2;Rt$^!Qlso~!An0a{_=oB4y`PwNbPI#=_@rWyUxt5ZWQjj)i@9x$Ms) zVV6-V+yCvnb0;P#IXQAiLE=%V1_sbr>(^ITlYe}8ILD%} z>9qd-IhmK2&HXCXproXv^zKydHO3OxD0dGRmm_~){JqVa$EpsJxati$gNWRdSdTr; zXMKLEZf~boUERKiDoRR9Mce9X?|*wTdm-o?P-XW%1_3d#G>guTj*dC2mEHRu6j=Bi zH*osSw2vV*HTBKy?fq@M(n*_d_N1HuA0gUv=8RAO-ua24U;fOQH*fZ;mp@m|nl~Rz zy?lOrlTM$ErBg` z&RNzKjayqXo&Wy&nt6SlZ2jNY@tRs%MzUv9_k1|S{pJ0A`TF{LDbuVJtG>eb`+n=? z-r92T{Q3SFGbE)Eo! zEXv-@c$uNMF!l7bKab`AD<~>9wzRO+ecyfGu;fL6S?;YAt6A=sj0{R|+}T+yX_h0= z+S*!CT|K++aZz05Q&Bbld1v}$t$o}JH9_0Z-rU&ut3IVt)Z zw9aKi;^7$v*|7RsP8+XunpKy#RmBH|v$M_9UtU@oQ}uG`vd24?sQJ$N za7ufi_|csVSOmlPl#!9mKz9FSM55f9{W{m{?f4%Jd?= z^XKpFt(G*&5HK_}toift_>H%v^-O=9{{5}cl83Eh7c{?9;C%F-YquCE`8A(Bay4?= z(Pn&**tN!2T`7$;Y8y=M_Dk`!~-r3jF!^0mIYN?Z#SC?tV1%=g~lhvlJU%&q9uF~x6GcydY&9klcaWCeS zum7`=bxzUbqVju{&o$@IpI`UuWQmFLy~^jZ+BP|2~YncE5 literal 0 HcmV?d00001 diff --git a/examples/basic_ayano/main.rs b/examples/basic_ayano/main.rs index 7324a6b..b51390e 100644 --- a/examples/basic_ayano/main.rs +++ b/examples/basic_ayano/main.rs @@ -12,11 +12,11 @@ fn main() { // construct path engine // in this example, no external files are accessed (i.e. there are no images or special Ayano syntax) // so it's ok to pass in a blank primitive engine - let path_engine = kyomato::path_engine::primitive_dbg(); + let path_engine = kyomato_lib::path_engine::primitive_dbg(); // _dbg stands for "informative error", just in case // parse input and write to output - kyomato::LabaLatex::parse_and_write(path_engine, &input, &mut output) + kyomato_lib::LabaLatex::parse_and_write(path_engine, &input, &mut output) // this part is just for debug purposes .map_err(|err| (err, output)) .unwrap() diff --git a/examples/basic_ayano/output.tex b/examples/basic_ayano/output.tex index 8d59e02..79c06d9 100644 --- a/examples/basic_ayano/output.tex +++ b/examples/basic_ayano/output.tex @@ -92,6 +92,9 @@ % enumeration \usepackage[shortlabels]{enumitem} +% scaleable font (whatever that means) +\usepackage[normalem]{ulem} + \titleformat*{\section}{\fnt\LARGE\bfseries} \titleformat*{\subsection}{\fnt\Large\bfseries} \titleformat*{\subsubsection}{\fnt\Large} @@ -135,7 +138,7 @@ Here's a basic Ayano explanation: \tbln For this example, we'll only consider examples that do not interact with the filesystem. \tbln Say, you have some sort of dataset, you wish to find a mean a deviation of. Here's a way to do that with Ayano (without filesystem interaction): \tbln You might notice, that there's actually nothing displayed above in the document. That's due to \textit{static blocks} leaving nothing behind them. To actually display the dataset, we would use a regular, non-static Ayano block: \\ -[1, 1, 2, 3, 2, 5, 5, 6, 2, 1, 4, 9] +[0.0pt][1, 1, 2, 3, 2, 5, 5, 6, 2, 1, 4, 9] \tbln Above you should see a python's `str` representation of initial dataset. \tbln Now, let's try calculating mean and the deviation. To achieve that, I'll use `statistics` module: 3.4$\pm$2.5 \tbln You should see dataset's mean value and deviation above. You might notice, that's it's in a good-looking format. That's because of special format used for value-deviation sort-of-values, that's described in the docs. If you wish to circumvent it, you absolutely can: 3.4166666666666665 $\pm$ 2.4664414311581235 diff --git a/examples/basic_syntax/main.rs b/examples/basic_syntax/main.rs index c334c09..60b98ba 100644 --- a/examples/basic_syntax/main.rs +++ b/examples/basic_syntax/main.rs @@ -1,4 +1,4 @@ -use kyomato::path_engine::PathEngineExt; +use kyomato_lib::path_engine::PathEngineExt; fn main() { //! This is a simple example of programmatic output generation @@ -9,23 +9,24 @@ fn main() { let (input, mut output) = examples_util::setup_io( "examples/basic_syntax/input.md", "examples/basic_syntax/output.tex", - ).unwrap(); + ) + .unwrap(); // construct path engine // this thing is responsible for file path resolution in the generated document - let path_engine = kyomato::path_engine::base_gen() + let path_engine = kyomato_lib::path_engine::base_gen() // in our case, there's a dedicated folder for images, so we inform engine about that .with_image_folder("examples/basic_syntax/images") // this sort of call is required - with it, you specify an error type the engine will return // it's a no-op; so you may specify engine's type instead or th - .with_error_type::() + .with_error_type::() // this adds file checking to the engine. // basically, this will ensure that any file engine happens to refer to, actually exists on the disk // you may omit this call, if you don't want that - .with_check(kyomato::path_engine::fs_check); + .with_check(kyomato_lib::path_engine::fs_check); // parse input and write to output - kyomato::LabaLatex::parse_and_write(path_engine, &input, &mut output) + kyomato_lib::LabaLatex::parse_and_write(path_engine, &input, &mut output) // this part is just for debug purposes .map_err(|err| (err, output)) .unwrap() diff --git a/examples/basic_syntax/output.tex b/examples/basic_syntax/output.tex index c3e2f8f..148bb63 100644 --- a/examples/basic_syntax/output.tex +++ b/examples/basic_syntax/output.tex @@ -92,6 +92,9 @@ % enumeration \usepackage[shortlabels]{enumitem} +% scaleable font (whatever that means) +\usepackage[normalem]{ulem} + \titleformat*{\section}{\fnt\LARGE\bfseries} \titleformat*{\subsection}{\fnt\Large\bfseries} \titleformat*{\subsubsection}{\fnt\Large} diff --git a/examples/title_info/input.md b/examples/title_info/input.md index f324d67..57513d1 100644 --- a/examples/title_info/input.md +++ b/examples/title_info/input.md @@ -9,7 +9,7 @@ title-line4: Part 2: coping with your actual biological gender author-line1: authored: author-line2: Dzuchun at all or th, idk what that means lol author-line3: (perfectly still) -date: Marsh 2024 +date: Mashle 2024 prof: me, I guess code-section-title: Code blocks --- diff --git a/examples/title_info/main.rs b/examples/title_info/main.rs index 45686f6..855d33e 100644 --- a/examples/title_info/main.rs +++ b/examples/title_info/main.rs @@ -12,11 +12,11 @@ fn main() { // construct path engine // in this example, no external files are accessed (i.e. there are no images or special Ayano syntax) // so it's ok to pass in a blank primitive engine - let path_engine = kyomato::path_engine::primitive_dbg(); + let path_engine = kyomato_lib::path_engine::primitive_dbg(); // _dbg stands for "informative error", just in case // parse input and write to output - kyomato::LabaLatex::parse_and_write(path_engine, &input, &mut output) + kyomato_lib::LabaLatex::parse_and_write(path_engine, &input, &mut output) // this part is just for debug purposes .map_err(|err| (err, output)) .unwrap() diff --git a/examples/title_info/output.tex b/examples/title_info/output.tex index 070f407..fe7d798 100644 --- a/examples/title_info/output.tex +++ b/examples/title_info/output.tex @@ -92,6 +92,9 @@ % enumeration \usepackage[shortlabels]{enumitem} +% scaleable font (whatever that means) +\usepackage[normalem]{ulem} + \titleformat*{\section}{\fnt\LARGE\bfseries} \titleformat*{\subsection}{\fnt\Large\bfseries} \titleformat*{\subsubsection}{\fnt\Large} diff --git a/kyomato-scripts/image_folder/meme.png b/kyomato-scripts/image_folder/meme.png new file mode 100644 index 0000000000000000000000000000000000000000..eece55a511ec0b767dbd168a57fb9ec0c08f1c79 GIT binary patch literal 251134 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ν6$+hpO)QrCGyVA z%#rO<%P+f{2&u}-edzE zv+BD1QbnxS(ALybVZ!kP4)$BOe$CmP$NoFMu5<0}ZI|y}^?coBb|=rAnYngP;R^xz z>JN(h{+_>|^8Ks%ryKopXSI%&?Vh`FV_;61wUbBEMv2RpCoc)|{PgJ)C)@JQ>BRVxn`--EFel zZ~GQhbv<0@xb6LoqbG%rfB5;gr@uESD5xz_YR6rLu%N$bQPQzvZkiff+C-jKuV8C! zy3F(S`yN%#y_XV`DmTW?&rG}gS7G+~ZxwH@p2M%yH|o!~ z&0Syr^vSoPS8twP^A<@vR`amv$m>VNTMKqw`MGDuo1()XifiLfZulEiIz8*N*Q@=Z zxAV8z=a=@q6`G{>Uczwe%{L;`L!bORT72T6l#qvB-M;l(-c0XWpcVY_@qQcEr$+Yv z|L$5&%h!JNRh{jF=q*Fuqt9+1g6x@N25zqlYGk z#hqNzf$=i~cpR}ge`_$(n)6aowUN*L0n-6}PnrUQY zbL7GUhp>;*a&n6{t>RjL-CIX&>4ps*i{1GI`;T+BA6^r4^<1~UjrZwIVQFb;6(3ul zM{Hd+*Ll7Ck!80({Qm3X)AM=r^17}>2_8P4l zcQ}62*kX>~F*ox^B|MxsUFf#JwnKL{IygT`M zc_;WSfBE{YqF8rRP)VGHe|Wfhr^}pMx2kv!FW9|%_P496KeB(i@&8k|kDuR$y2EOH zvbD`^$J9U;T--M4-n_h+JoAM&C9mb*@yRb&XKiiWv#DNw?ON?0H-rBl+x1m?#@xAz zGd%>jT0Q35I-lMY)+cM}$W^v({1Ku@{_t)5cS3C2Nx&Lf=-JJ7(kN=-# zEok|q;Oqyp$E!}?>sj;flBH+p3oD_F4*XJbXpJ=#2Q`*B=`MS`I%{y?)-ZtI|g9)obY;1rKdLR{WV&$G9eX z_q4z}e& zUH@d3>c;EoKL6~h`e*$vw!Ck@estertJ}R}%I(^^f2;O!{@3{vR?a9tZ|?TbJZ#PR zUXPw#n&ap1U;OIIN!KS^wrrX5RQs0L)%q6?p-mlm`G2N+oExTp= zdNrZWf=^F6%~hB4+x)noU!oVjb^GM)2j1;iu%O|${VxWF^83Z|<#&to-}z1lRW=h2 zvx|vquYCVn;D4!}`TUjZQf&l}E0$`__3LX-i+uT6&}+HpOtm=Q=RaS1=4omrnh3fx z_WSgFZ2hE_6SSyh!M&%6?v94f4{tu=J2_NNUuehLB{GeVoK$+;x%$pUe4F!j_4Nx^ z?i}K;|KL#;Z};!QVULxC-_B@WTYLY=&*JpRZF_h%Sr#uoEFvOu@$y~ISZC#p8B<)8 z95XW|g&!yP^!KNqNY$MG_m8;c^}@Dksi!5Sq!uh#Ah7+mbA7yi=v7mf#epWiN~YQj z|DGJN(3?K{c-@y~9nF95w*T*(oxhW}|9bb`y!4_ZrvB@vpXo+NUfnk>M5}XICJU3; z!<{du-R5jfGxVLDq2lB3Zw|_5x>nbtx8)_D-Nh?o`Nt~zdVWeqfr50#|2FCGn>N+* zz6|NAusL?)Muu7R)OEKtwDgwk+BNOrLdRR->lg3ZrZ-va+WB*_yE9|=O)_5Yzv2B- zaq-Cq92$1N+j+jIpzG*|Uo|#=uL$4#5T?8KbfIPJn)v;V(%KV~D`z}9y zbm%AFDQzvw@^2>Ew{8_$&As_})tWUa@6T=*>}=Wi(yZj}v3~jbDG3H1o|&5RKX0gC zFzG${(eT(Yw~s&nUb%A3_`-GX_;~*8>$wm9?Bsj(diIYq&+FRL=haP`yE}i$k|iy> z-|g%V&^Yq_|I^z$w}-@Aok*4;C!XPCciF}LR>lOI2iNc8*n zEL8ka70@3`S_VhHH>%G3Z8o_~KVU8v;%l7Os+I{Zr#|2ZACVD*JfAaAA&zW;1)TKHL zzMM#0m7P_%E5>4u->zMH0daAiL0rCgx1_l}) zd>~R;`BOyfTzvcC_PE2(zZd`bp!jH`gnR#HGiDw0m#?2PYnGMJN0zOnypqS){gl`kde!vBuHH|dywudyPw49`S#hIl zTf*j>9?OH41Z)0jWU$*Wzx?vaCr2!n`^hcew@+~Cf<1ehX3kUyijvn3;(8@5Ram{I zf09hm3B`7qq936LZ*%G{|9dRI{%L<;*Y?lCPEnupSKiiJ_xXnMG^;D({g`L`|6T3WDk&YHO0 zeG3mdnyfT5GxI8_>Uz17^Vo5>nG*8JDJc^53I`nOzyt>*wA+)lyrCbMu-~R_4EZzkDrwT(tA%_e(t2CTx{&WE-pFtQ-bi}_L)Q{FkC{iNp1K?Qp6S~9 z`v)#AcHflgQ(WzP@Ko3m41T)o8f;6<56fu`)noX+T8qLZj0& zHB=gtRxiy+szr0;$yeFt)eLX|@X)86kpH|PlaDdXy z@-=&!F01#a%SPO~6&7^=!ikf@#|wAO*%1>q>vUV2nw8bLfZ$+nqnRmc7QOZp-1GCz zjSnhZHQg#A5)pr2Hr;>oo?OvWCNE#UJ0{O@;m#eW+*sS=eftis`eu6LE#J9$mY&|P zx#FxpaK+8%pMCaN?)Lkg%U+k1mj(p~A8+NBW%*O7k-Dkqs+QL3RgzK?4~}#`j-0eJ z=O@=PHTA;XcUulC^2t?9eZB4dgD0Jjk1Ptz+woD&>fF3#rANJX?Xrs8o2J{&zqfD7 zq;FTVE$jcuWM9im$jX{jU@@sWs_4f9fsodvF^eu3#ax)8;H!OMf5UcN13kX{X@ORO z5sm&VT*lTZOGPFqF6Cn}NW3y;5?egCqVGdz~8G*Fcc z(sJZnVq|bpTw{%V4y$RqOwkUudxzgAx$RiZXS}r_qkiG6Uw)da&TXyM_i>QE zfuCmAyZwCskN2YTdBt2lu6Mt3w#B8Uimu=DM=1N+y7SY1{;AtzCm%O|`m55OC^9>9Q55Btky5ir?^oymkzF8(_W|Pf=*2L_b zwb=dsf<2qMHf`cMH`ms=Hd)w-qeZ~S-+$q2E9Yiri&(ume&Sx5YR8UqvNiu;W~|y} z{qpteqaMpUU6g!$dM0}<)nS+RO+I}scgLN)ZF#rnRR8 zBhIgXf9KG>Z%>y58%~+P5$x=_=^4|ZEx&xct-oz)Q~2CIAu!|W(dwz26HY0%%ha(J zTuoedPiOrn>w`-kyU$Dfal*L!l#TU!^?UVw5B&e7F$PWp4L8Kbihua|C+FTK(e>xu zEo6?F_}Jv}yZ6g1Te5{`{dI3&UtJCD>Y(D&Mt9`+@6Hv^$c%KeFyVHvnyVz%UHF%` zy2I_zlc!IOO-x)I69rGY9Qx!CF3{;BA@d^rysx?Ojr;x@Cfz|@0U2V7s&?z;8x^E_ zHxxb9n!~3aJbChw$AwOtH|N~i!`iU&q$khe1>3i`A6u3dlV={}6*XIBLbUU=C@Q1+T`p?jCV8X|G{2dlv8I?lqYi)7sN~*afS1$Mz&oelGbMvN#@YQF23R<>;Qv8nwfh(schIX4|z3;jsUHLLzUvu`H z84srZ{XK2X`vQvZF`*j z=UJRP>9hB;r2nVz_?!F=b#-;ur2qYVJ}-9DT4nA->SvEK%su?`^K*uZZ!?!~PPMQ5 zY5+oYPf7jMoA9kC2XOqXTDx!xn)rnCoku`s!yB@^ZwnOoWJ_Li&CNI zu8T`I81QBBSw8u2Pbg%m$Tf!j$M&wCw%A(rd3(ELhq~^+l{{h|i@H5;&-paN=kv+e z+Z)z>{J*z?W3q^or-8_AsZ~EdeZ8&uxq{M}z>dZ__N)vli7Qp88GBP?!@luZ7`X}7g+nWu3 z{7&~RnSJ04i&%hC@~H(ZGc=b>;#jzN$*-y50nc>|FTFIGbw=~3<&zJq*)*55sj)g` z-S}GBuHGJddeViRpHiwmez|+jd;N3KWc`)rVwcJkoj7o&)g#)+fYIl}?UGkaao47c zZ7i=8ewo6gyx8d6T_K3(S{z_8E9Tv5mnQ-}MvBD!kyS;_H^Kb5)ImPP$dqPJ2 zJAvXSChanHuAHlkcqd8D`x$oGQuOM&=&a0lM<+P*S^jbQ6SVSYrWw13T8T)O)~!a}_tYhjwS$ zjGBzcSy9@b4LuT*Hmq6FkbCy(4DninA9MXa$$Z;s>*SfZ(cSd9*j5D-QL)0<$WX6& z7XpkO-Ik|Ynwm6;sCjvcbZt>HFl~<2<(=}1(?#o4g+^NINnxf53v`Mboo4si@5mIN ze|zslSzgf>T0a{l?!;I4OQgL!tlKVgkN=Eq_4{KXO-rX~T|VLWoa4ozr`?>de+x2$ z^ZZrim0YcvQ6F8_Yne>hA~R>gqzoH3lc_G886S54d~$BBMN6jXG8eUL|Cp+M4gDu@ z*W|^_IRE7vFB!xJ~MA%!c`1fiJ!a6j-f!EI27Z zsAVk@9Umj4G*SC(rF->j|0DBJ$Y$WqX?@mr zy3%5A!Rskj<%OWW%=XiMx~>FLF>Yl6D-TbyG71>b9kvW*fKeFiU==^ zoFby3e0JWnDzodMi)JopG4yXgyOZJ1XFeY@7O$lnxj)=AF;LY_Z4D7+HSle`;?pAF zadyE*1OD}Sr!P2rTFqUwcbCZi>%9i5%h)bI+#$N8zVV3p^OO1pA3Z#q&-U3FcP`|a zXJ0yRN#@PX=LF+}rE=D-U%c-~f1*a(BsF!m_QDt2d|Y%`Gc*=H7GX2XoMghww*2st z(!Yx=*6rW4{k+mq)ppR(*v1b(tCv5Y9ugovEq2YkbAmT_TD^bO*uKoew>ReC^eO@97;x@fWFk#(Ub;umHcK5u;=q9%WT zSKIpgwYKFZArCI8T$-}L7jpJvWFRaN@6}55UwmH>i zYc_H$)-&{dAwRo;H7nQb*!~GirY>1#&@Z*0|E`IprS#JmS6Hf+^$8_#G;$wY`&OId zM(&GCuHP#<%-MclJo__Z6&I`0*;${8N;Es%eEoWQA709l??^XIZ=U7M_ugL2uKxX% zJ-uIS9QWSf`TzFtiB_ke##_b>a?UBCBB<8Q7FqEq%}O4rpl^xOUj zD7*RktJ2bx%g=OgyT{PjG z*6sbRzpD*z&gm|G^|3<0Nykc#K{K^1KG#h8^|kyBdRxx69e>QY=1iz{d2Ijfx5v^p zpWVLj^_?Bh(~ccKwxjTToZY_*%Qdw1pa1K(J27vsdEsr@@{LKZ(hQZAl{afI8Kvx5 zvuDn)HZ~bP_C^PV>KG>ulUVoje{ABi?f2NpSLg4V8lAU$Zol1!PSyUM%a*nMw*SO1 zYgyrM+492Ay4yFFy|sF`?|Iy`t=o#e+kP)Ry>`3)&bQZcuYcUgE}!)DROp?`@9h!$ z6P3;1?d+_!|0B3$kBRxwO{r^ZzBKw@xOB-$Yi82&Tl%iFRm7Y09 zRjFO~Ri;q$*-H}cxtL4;FI}nMlpW%DcI67oCm;T4uXx{acIC`N2BOQh?3l36pykRT zoS=-H-3Jlxtm9fXo@1OThFKo+NB@^q@ zGmV!s?s)rcw~-{b=-<7zS4>SFef_DSp~1wMx1D|W-GlF5=O1|c?E(Lji9Tu@Us|ny zQ{VaTUtLM;^JTO1RYkZqzF#WNpuH~BMvQ&I#+99~ZdqM=X;RADd*dzN96$B%bw3u% zRD4YZO?VX)G#q-A^ltBa<(?j%#|0J~txi*>hfnZ0^7CZB{Nr=G?TtJRJ+ zV4mQ{k~1}FKsIFn`?FO%p0E5({ztNEC`5*aEXnzjg6h#{c8KVjO3!Eo@=7o zKR+hhFEMpK_I~I0!#3Xz%Rjm2@LtI^RMParOzypRwyl|YtYPl&XsLn>srdSj3YvF2 z4MMLa$7)PeomITjeQKsciTLt%H%ac+m8U-|iH5Mvd6Tu`>DP0CCd;SEe%ofWGj*G= zfoPuqpL@Yi*@`!OpK9&2KL6oJZ8(!MLFgL8p5spy?mu_s^%uGjbmdpGA=XzyrXu5G>>WNiqte4iEKWbgZPd^H!Sc@@3?hNKYF`s>}-pQo!yq!cC1dAU;a*YE1hp=#>KHMLhS*s()JS|*;u z(fzojiSJ>9OD@WZHgfv1>E{JHTUM@FwQS9zqQHW`DMp@JQ+MsCp5vz;E3TIy#WvBS zWziy`b93w*Gs>nt_#$%H+1bb6-_X|7RYgWgs`pXZZp-p_I&$m94;vWx3NIB|tQ=OK zoV@r*lHsIImc6#IEKHRiiW1V&iu(Hfn>KMBJMM0L+}zXCQ?k|7RmQ$-%FUSyeC=~r zuJlxTDB#yVdEY)cla+$A<{k}E`{y8MH-CPC#hlPicQ>~euiu`%V=(D>{TGd8iI0kQ zW<>9t^uR};|G2l-Oqa_hi!O4=t)FhZ+}}``=hNT!wd}FmFJ8Xedi(LwwC#!8Z+ogv z{!&&xaen-P4;K{LGL1}3jyTlYGu$qJ6O%G+g4fauCcMiJADmlW+mY9jDi?9H96 z9-{t79^ZKDC-ia0QkV3mJ^uc_amD)0@k&dxq&(@@V zH~Un|+cL+yV!qrvT9XR2e3WF}ZH#k#mNJkI11bMuEv!P1i4&b;5O(Hy@~!)>y$k|M1S2w>4+a zW}lG8^>*_7zkltoEm6wlxR;eRLuc0cy1euo>x070YTr!EPq%ASUFp&BT~@r-@ktP#}A2vFEg7i2eYI%$yEJS==D7dn!gR6EIh})yzS%1!q=@Cvr0-!jSMdA zU-92V#mCcAGj6`V_nReGR;*k3_-pfeBO{|TU#^JkT9#OTqti{vr>DR0N$+736O*DB zTbq6Sd<<+&Pk#AgQa6A8$B%_Iar(x_)0}LKjC|tfPqiy7NoiSlkkQKe{NhY0?eJ~E z0Rabo_8EWq{&j+fNR7y(9jCb$+y1Ke7uNR74HhJ;xRZ>aHjD7p$s;YN= z{8{6nIAi9_lLALnl9LyoIdjIq)YR2ARdl)k{3Ds=7w%s5^vsmBy*u~CtW>Rl$k@XV z9PC$yOq%yj@6@SNH@`pP{!wdJRkcgv#ryMb+{Ig2TT4pg6eM`k&Oe-%d%f`0lg>4d zf+He2PCs?JTsp<*;*w*XkKI?UT>JQd_V3OX=PxDYLjA`h_a_Q3_n&)a+tZ?*7RAp@ zF|+1Wu07aAKG`i45nopr5z zkvaKUlhTEUCoULxWpJnlIz`T$v*~Y?N~o5r&rA{PH#uuoJBBAHeW;Fc3CfIn`SxgE z-)X-1J(cF=>*G)Eycee?%yy~4b}4I!=#PnJy`T2fM0Igba7yIpbaBpDu*A=2<{RtV zXMc8P)-dJ8Z|-r3V+dk<_JpNh{{4d!S9rWGPgo|q-TCA1?e88oo?X4->+X*kY+53U zMrKFP|8A?$m2qQlSvfg2u-l z@3`(g?|#DLbNKoFr!FqcTrx$X$S?YufXACtDW#E|e7?zAlI$~&buT%UsyxjvDZyFc zqHpgOBZqU^%dcstPP#m|xqS6$!9M4wP>|1R-YI5204XKK#h=ktzhYi^!+ zu0}?G-u!6&Z}TO?+cGBx92SX8Ey?>?^pyMRmDPW%{(k+Pzjsm9y3*>6Q?D8Qeq#SG zDApkMdgQ(&*1xs2-5xildV3oQdp%qG@MG1-9kwcyJGI}x*1EO-&bFsdMS1x7AAYUM z%*a^s_)etI$0N7x()rpScNvw(YR!u_+kP^o=;YT+v->6I)kW7@&eePL@y68m-M9O@ z&h1Q7axVSfOLm^KQa8S^)c;x-y>sK*&iQlSJz3pv<9aFJ&wlCXo13dFH|^mN zmR-rya`ss3N6$ND=1*JYrfe{r>GvUC{;#F~+_y_^CO*%%FEzhl_kFl5 zUXhq2wea=^$w}4WS3fPa{`P44wa?2W)jr-!T;_KC=X>>YA43Y*b|)^ll=<&PU-XXp z(?(mSlk)n?7&fr=Rcb`-&zutvQRyxyc+q9=QocJvF9IyEM#$#H^Il&99E?>@` zdRDOWSCJy;)g`-HTJ%Egw(k)L&(1GU-65pD^1INsTT?~szPNXvx-&a(`6KW8{K{Wx z6a1E27XJBhTWPsddBCrk_UHAc|G74Qf978gi!(QFXdIjw>1FrV{$AHvMP}6}_rLGn zVRonJ<M*{WbO@pp`xJVO7T$UN?Ax}mAgLq12Rkn+bh^y)o%&QqUvmSK z*-I-e2}jjS921XSVR=z@uD{<{s=rxe!P$1<30xl15+7B9G(C=VxF++sCUImsJ3pFa zn5Utom=IyM(kOS5%zPDFL$4Da#v+}Il?%L2is$XqI@yzW&KU zY#g2@^Aj!OdyGYmfHsFIPCK z93_vPQMn|&;nH@t;}<@Ad;0e-5}&e&r#a=h;A-CO^6x+OTI~D8E7UyKV)d+S}znlewu>ULLLlU8t_eR}ZPhPF%nf8{PcT=e3h{jtEH!*3=&KXJQA+Dnt|-|gd! z8@A^x`f=~Puw|`4d9C!-|39mJ-dFb=cs9FpPNnSoRj&ntl#Bjx%v)x9Se93?EmOp*8eY2?WpPD zcy#)|#*%_L8|(l0Ps@tg+B33`em?-m7vHo{#&oSkS>vtP;16m~e13oQ0 zFKo3@eu~rQCl7zWc>j1+?71t(|C{pv@}AlE&E#Y9`PB8lf0+McSi57^tN(xWIqo_p zEt!49L;l+#qdB2xRos3{{JpIo*-~@ez0h=xi&f0c*9rMi?jLLSHyd4AlD@nk%uLAg zv(?Yt_C12L+-9=5rA&KrgCY6<2l;znf8W?MO{001^t$bL^3L=uJ|DvsxTR-l#<8@L zbKLjO@SKd6DXrTOr`4*->OarYKlH%)!?$l0tQL`e<@NJV@9v+sgwG32QQ$qDc}mmt zv|P;O85}HXnrEN9OjiGSVpkg1Ts`gF6#+SUD<5AJPM7GuWqIeOXT|F%ra1H4vsPJJ z8D;Tm9GNU}F!Rf&AfMEi2h`h-X=<%=;^b{TAabqx{OZgfZ>`R4*)WO6<zV-9Qw9*+HS%MfcFQ1yQ z^nmt)jVop>p6pn1w#{VnoxQte3T^N6c`5NyWupt1uZKum=|4vO4dtcA^P~39GFpDF zvr|&8^0A52s~Iyq?zS19_nvwDc(Zia#@!qHg6ie&TYr$s=jo5wxc<4F)4GfH$F@we zESuoWcU>Z|cJrpyvD0RSL~h!7<;&S?X9cfzhhEX1Td%(`eCvbzh5GNlR)5IbBl*9t zx^P?GvHg$ZV%}PR$lc3*zscx%*V-TDce?9;w~K9^EFe26)-Rkb z8y#o(vGKD-Ry61QDp6&NS$EG}-Zu5aPIHehbKG8-2{5@=%649?d*do9_H$*H+Mhe+ z6Z7QHwUr4Sw$)JX-JNObRw9-8r}(?aI|ZGtlo`luF|Z)0Y1 zFXeX1;<(%PPTyrF+s1XmN7{Ey$g{6nFyTi5|F$M0lYF_2^NeqnEnfdmLRm&-&ws-+ zERtMNnU-yPG}#yh0>ESh_qGv`is z$&QJ9&;M-Y6q(e=64=%_FT6*0f6IxFwM){LdGvlwJgb6Pvw#J-JJ7p%wL=ClKyd9?$O)3 zCVsdmKEaRw(!)a??ic6C$t*Q>srGxkB1hp`v6Ooz@4H`9f85%xEVGXPO?T!ww$NEz z(NTdZmoqpkK1yg#VQjnN>FtpYMQgQdsxouy6+_9?P{9klm=>lfw zTjKF|vO^t3b%GK>qxawCiqGFa^!&rwQ|r?9)>)R8Z1k%7Sz5g0a}xhw?V0uZ6K$#+ zK5k~$)9l~#=f%?0-)k;7O`I-v`nU-3NO4mF_b3L~u0Be7@iR;obSFwzgh18-x{APj0O1iC{VV_Hg87r!Ai??%DEB zl#xE*b8G^i`ozA&D(Yw#1`HbFk^pcynZrY-{MN98=t%juIfaW?0LAk{p9Y)ms|W~ z4{)4M&|a%*Xx8xmS$(_FWtZK4i!OaW{MvZEt?R$F`;T2I?thSb|J;|$_vhU|e|h~w zjg@IncK>hQGxz%>nYsNN1YTAsdxyPQdX-}~v$BO-(hlzLr{1b8clYf1Hc>|YK;%uA z4^NL@tod0H{yybbtNX`o`Rey-xyoO=ZE+AhR`EXRs-cndk28u(F82z~JQ#V`X~|=Q zl_wMbNPiQk{p%T`A`-!dC$_HS9xmi25Yoj zT64zl(1~ZO<{Jyo*qrXmeWA7ay{wI2cBN#lYo$l}tlwr^i|cZ_YHjys96I;JXTsj> z>z|sh_ZiGJ*dS?@A>^#p_noKEIoWhpTID15H&)ft%|GNXKWDJo((>Z{^KH|FR&{el z1co>z#tB|BQJ8mN2H)bH+%vn5NzUe6ylk0=wIlbFKmOm(lIZ z&0>BECUXNUdK7)Gg-`oXZT|k4u8%fPcaTb2y1+w&Hj|4jN79z;*jp?4eSPM^bv$u0 zbDq@O{ne7)oH&I|qtjVsXJILK-t6WXGap^OnE3D4Oy@aVXCDcC>k;$27U-2~ud>d! z`pTUTb$ph4{v&0EjmB4wUT)QT8T5M1&!aA-3!|!bmWDGK%N^`E6Jso{WP9o8o5``C zS>7FeAAMxa>lIt~uT(iG|5u-3*}m^vkKX2sPx$hFUrz3Jm)qR2*-phr-Y%`5`~LOK ze0`tkKDLl|5W$;-SWrk6ZG^SF4h;Y|MusR{(?5s8{Vrq=f}$UK75qSz_I_s z-qYU%>%LjH%>R3C(h>U~hyTxJ<9-MWE zA)D*(l)T7pU#TNY{j737xZM{x{4qss?eY`PHgC@fc6ICAn&ai~&-(LUw)@XZy4D}P z=SIzM*1xxD@(KR`y@!(>cP&fG={%(RXO;E%uZ?hQJfmV4xa{c}RR_=0eOH)6 ztWERsW7}+#6&j5MJ54S&a&jDXbl{o%FrdB&-A0fmifxe}WVm$4t8IgfMU zVrAaRbH6_Q^X}c*9V~yoykYqN_T_U9PTqyl+x9b^Y&(@yW_DG`_>qj<>}US-?M?(7 zamn(VbckW$gl9D@ZP~YlOqVZuHsSEr4}W4;249(K5p>qKK9RLlv2p90!0B=OD(+5I zsQR$nrM^?|l8ohEF~O>Dw<8}_pA-+c9;jxdV%;vYF3c|UlG1b|`}WQ>}hxDf=`97T;A+H%}Fno z@o6yeS@Y-LJR;3(uO`^L@KWX>&gP3AQ^J3-?OeH}MQu6nR32vyADh%g*+RRIclxLp z96#={n^~ewX_8OiyP`zn0|9EMLUxp>N1y(DGivJjQr9@1sSa6EPQqalp)9jjddyTa z^<$5}zpwGouON@;YrB_)2yi)9u&JudVl);ybWwsoKjQeUp#7=oS|vdjS%UA{woLZ? zb;vMhR&!SWN#@X{BFlv5%Yvs0~vtK}ebc38jy??ap?|E+U8v{9w* zY|6DQ->$kOWN~G3m;@)JtZNU531(Z#{k3mS7ii{ka?>m`Md8CW4{JS#+Ui$35 zPvG3yr&?dV2)b-J^-IxfF+Kgz=VzZ4pHOUH9inq}>r)lgBYT>x%vEx@PM)@P{_|V@ z?-XTW#W^o-Bzp!FZo4M>seSvKMVE91J!HHB-+W)B>~T7z?M;;PUnhY|pTq3m0d!C@=6TtD%{uMm#lqm?W6b*0Td z_aSH2{)%U*=gzPNwmF&SSn#l)+w$b$w5)5-o-ds6LhoR0_Us?en74mk{{5cQ-t(;v z%i~^M;g(&ZqRYB#=FV-8ZgJhSPj8-eT4|~?-+TF-U0XU{hMq}3`6k--aq*ImMOrU| zQpJTYn+HdOrrCEeWgInCFl{;885WT_b?TL-6MIXFPCOL47!(toxa`?7C1VAi1da`l zmslr#+WP$a$2SS~ZSM|bzLMyauwJG0qc?VTL6+Jq&iy-cnfGtcHZIj;U+FW4$x_@s zl~3<{drY;@Q1r+6*gxb5nr}K^nddnw`DANo?sImV5u?WK21kGKoKY zdXclE;%3dG|2My9U}sxq(0b%b3C}`~47L52gN}Y})yr9E5p=mg+i|Wn|MJ+!ZyvBH z&3)2+HMOR+RJp$BwcfXD;SDp72^4Q+JYu?X!Lv;sPbVDy?H}~qBy59Di(Qk@~vQYRx_GyBVg`{o%jhEnO6D+b&c0QeunKj0ux6 zmjuk2#q)9N^SHa!6Pdq`FE8Xix@}cWURL(A12_5V^55tEs|c2^FF*X1$NzlM zrr-b5-|v0=Wyh)A@84SnzuULhd~KL^=GE%ys?pn0lcjmgRN0sqnLOtm@UHTh_vEB- z(7g#ZnhqQ)`ix8mZ%8Cd=OpLEs6>ZtO#QkoYxP^#==XbXPbt1<|Loq?Rgq>A2K?2v zcE4xdoN4^_%pK$XpLef&U0(WR@{b;y+u0BLdN(ZE)$=ozwSupNulKw*Al9GHe{N(AYc2Zjhla(SbTiO+;sWP%PqdQ z&)=_$;#p*UFwU*5gXzTo%#`s81I{e27xeVZ(j3S$GC&aa6- zy?6Poo%_F9EWYKG(W_{({2af*6q)DEtHU;L+0}HbNrI!d%vAfew&jg$eiIi)?ky60 zw~=qnu_!s)vYwl)W}UaPwqzbVu^uGYgk=C-qi^m6Iz3~R8`y7pNNzS_TXW?_4$J= z`}yyNpL{l+s}yLy^T6QU2?;Zy_~s9y^B5FumP~21YB2bC{J7w$=M`qhr}xcENi1ZP zpJCj+Vp`4a{nPt;+!y8=J+W$OJ$Xr~yILyUz+TGjhh@u^69F^Ijz89~+tADWcJ3NY z-G_g>(~Otz5ebYvn0)u|pX=|K=e&@2xb<+st2qK3f1e4v&Q0@Qo_nkA--8mS2CvLL z%PN=t@m%mL%k!7G;R4eKm*Vl)<^Arj{v&WUUs|sZOyapYtyGskKg87TKe_1m0Rf5t=ci6q2b2cm$rI5xoT>pJds0JS9kJ% zbJMen7A<g-^p=sMMLTA*sufZ@9$|0PJaC7b>H4Or!6=* zKc5!*^KhDJ%OlsC&k@$?#$85JA3GOI{meMcy#~gzdUeX|3Z_~=_UqKPClrx zYx|@*q2TAA%LKW=Q4_*cR9_J_vXiN&&0*01br z(=^hpxqpAlD!i$jKQZ}vKhl*L* zfBk9-TGZG4sbiq-AP&y8DrPqzIuuDt)qvS!Q1KKmKgvsDv2mzwUAb-XR0 z+4JeqO5-D~2bWLNef)j;nWZW+PiHE4vmRWT@GFV!>sO(?(|yjZYnSKGsf zilCk6`TpkX_k1#19k%{q%4<%giyBj>hQ7JCb#m!zGtdm=i4OwHGK;>P5WKeb{(*^y z+a+Xqk4)m?=8oK0WV-T-Md?>#i@HA+tgWnT;&#tl+|F-gW8+g=YWnu=+sW6rWqp;h zx^>&D#K zyYGM1mboFH(SPi5;jdp+g+-P=ms7sY5#u@h;H3ONhK2|euR>4GzkmN;xqdzP{soWh zZ0+vpItPt(K?iPadaBLYe)!ARZxha@evIiaew#aG`qay}Z&zR3JJWFSMAPi+pHFG) z2S!D;1$8z4t8;T})6~;Ue0WIp%eS)6m*#O#3gi~kJ7mJ!Cu4VJ*H>wax;Gl3S51#R zF7(?rFCp)=&A0c}txvU^tK&jqoN{ArZ_nGUu>9(U3l5o?lFrV~B_&&e!{hr?qg)mT zy1BK1re3d{oP4L^v#yM7(S*b8e1Va%&AGRyOr2_IVNp?Od2K#Bhe&?Xu?!~l7YPX~ zJ{!*1yVNuK&mrOMV(=RcYEzk6{0{0Wasu8%G4pGF-Q85}E3ADJJBE zbNa1F$!_JMxh=1>HY^HsIdMqV*J+CcGw;6hJsqwgjq7WttTfniWCBxp4$rY}K_!MM zQLLUZ5o||tB)Dd{uB~Z#-~77qedGJb9P=0I*j%ZabN}uJF5}lp9W|ZdB0>f2ZYTPB z7a2)5Ep0p^eCnX7YI@O!^S}E(+~$soc#_WfQo!2nn1zkWvE|`9OU{Ye+%`T`Y|DHl zY}zE>^q=wB;gjFJI3g?M(xKBQ`0Qw7pY4&DfUtTk_`J_Z8amf3Mj8 zzrlPe`{bNi>)CALSG&Dhow;nERP!v`A2K(8W(&3*e6j7lz`qmmkNHdgADKJNZ+YPy z2A?lqzZz~{IaAepdeUt(@L^k^*nBh7IHfp}jh+2sl=0$?owIILMMd{-{P|0_#VN7K zukZ45Kf~>*cCxavCMG5dOAi?EIz2SloMtNAuYRN8?$-u2Q@eB1r1R4cP5K@i%<~BfkN3~bwVjsx(1K4r+W*0Cx#Z;JI~AYTZkccG9T{nv zetw>sP^ZGul%HLpJoDrgq`Hk47pgJ;{oA4}c+AcHV@&xbec9`&m%o}T32fOWr#w;P zMDsD#C6_g`UQ1`#*7dX)0mUOD|2X=k9AY_#63b3rO4Sy0N|0bX zd$!o3<^{vq&Ce&DP0O$^-+1BQAy@Ag8TpTo&E8h@?M>#CAf2c^Q_l4j=iJ!QdGDT_ zm~LFtWbQ7uo<1Jo?xT9Qj+f5fo^ac2d6d7w=9QfuDqf|&x>4D`epgvuFPkQ+9scmQ z++0~%1|EK1qt%sWtgHu3%gf9E--+kQPh*JON_@)8o1 zyIbDzeO;_yW9G$}qz~!&`vs3*o>3>ZeKWh8TSa-T>fy!7HG3D&eN@rZk;J5Cl!{VgM z53Lm(t;^KsF1%g3VXZ+APojcATj#OE3@6eQ`U+Pre6k6N>d%Ek;HP1T7R zCJD95f7jr?i@*w-+a-_BMrUU1IM6hG>QqA`nlKf$X}nqdd}H->zo3;O>3(xvGJV6t)txvt&E)y~OrK%FjwMr!m-`EL zALaDbJZp7!!MCqW}UE9sC7AHrl?#vO>pM2Q7#v$nW6AoVfMVIRiERkem6H;WB^34sB!ZhW18Dx6E{`wSFbKya?Hmk*!kS?tD#5QCjn_@V@3eo9l;nr+34Rgb6uv+I&xWJs``ZKVzUf}|dykyh@ol*>#~nEwi$CW)J2$&ukyozr z$3dHpt236(EO%x5-1k_3ul;1&=E?eLYfY!6KHQ}@E%jmLJ=Qu$cEhRr-Y=fAYsKE@ zl^JU}Tb-U1Ssr?5(3V(m_nEYSgV}6FvF^zyQ!0MB`}=L1UC{3*Q~&vEbcc)59IMhv z-->fB`iytRW&2vl>^t#y&DLYva&68ZHu1GN@2q+B7q9fqpO5-ZX3xwLt~LEyqo006 zZ%3ZK?X=gsW-OCb=~%&iv(h|&?^N-5_uZ`K?t2pv{N1|HFWhQh>TQkt$G`tQS*^Tu z&Yt|>_tSH4Pp-8sP804mp6#(bIOqPiJ5P3XW={xu#xDKN^{*sXtAX$2X_cRE{{G~B zXV2HLxH}(hR))NK{?904PX#CYyqX8%%=4bj7v_~{+$N-@KXuENO)c?}duQAhGf7@4 z!_|2~?9%s7=Tdv?>t>XM%gsBbGNUZJN(IJQ*La<-(-o81`j2#&gitz zia!;c-FZm8`kT!Bo3%3x`R%qYnHizgeM!!XOQ@GurMmrvgxu4Jsvcd@&?DZDQmWcF zwDc!N%~;l%Ig3LjqtS@9zl`_eB)^Z_-&eAq>%4s8vopuj#Wt;%8oTd>b1|P>vEj$@ z$;*Gt-G0Yq(Q01hmkV0@!?+ZWJo)^7bwlCQpDC61Ii@X-=~eZ8G_C!EN|3h^Tg1LY z;&#jWK6FifEdT9odU3Ak{-|)qdEL1;-^dmJ@OtgZ*O&U^>odtarmN?@S{8Mu<81Bv zIgjp_zg1hU6TsyEOZ$mcf{&n%Z z%1#Zce-ya-n&X~npB$eiZ(nvNuRe#Pe8uzQ|16#rS#G(eu{0ys_F(e;`j&S2U*}Xd zFn+Mf7mBg+D>%IA=9?#WYPWnDc5xaQ3&yiQu;APGKK@!=QXNxH>gLFxl_LH&|1|6? z{shjeeU>R#|K_*N-^J?sS>`_z&%b}aGkyLu_I~?QC$9-lzHYKL)@=6NBEPCnbJW&l zG8ydK@tAeXwLQjC$-ydKa}z(@P8H2q`!iur!_%^ksSge0PwoC_9~tL4Idh|;%){E& zqZw;|-u>jI^Wa-W({7>G8%`g7tEO_S-DxeAo~`2L#8J0>kI-6E|9>CY#Tzeo7SFD# zje6}_V6#k<_W@p;r#~+r+{C%*`WIdkHcRJ4QyUkG{{HR$<1>5x zk$Ag5EOXcOUlZEWozME7aq4G7@9xek;X(P4f9^eMy|evys@?x9c|lxGQ$=4i3AkDL zZd)sypTaZK|I{Q7ldcI<*63(1DlV9?&xftoi0Syg@ z=j%PQW&f-T?5NpRVUlFHf0fKzs}&_|Qx1J@Iym`x^^bY&-$bPjPHe2Zw{J)5sgB0y zzcrrv^0h4Et7BCt4PXlOZrtJ&aca?lOCk$CY&~MGdaA`BL6U7+=B3=qnvX9e&8}Yk z)cA~T$pwz!PhWN%Pv@Jz#Psp!b&*^5EMM8S>XL+8U{C&g`@`wm-z&a$FJRQQ(P0gW zT)>+XA5)a?TDN0Def<8JcAJBf0@Q^J*QNQszCOWXSGLfcrX?#gSuJG6zlC2o6D_W8 zd8f{@#pLnUnm?20Ke@ZVi2wb*-+6hF;eV~1idnas*Ej9|@wK#tMd46R&1*NVdd5Y< zQsP?-Hm@xD^k3MT-%vsCz5K+Cw8xJ=U6);#b3{AR=JW9nvyM&saB=^?cpcr-@`l@* z*3MpC@1=iNBHB!EPry}=$Ulh zd%Z&UP2ru(FkSfc@^kxd?b1Fw`#RHt^{Q-Nmp|L5sc}%N^0C~mm1pkFK3aF}xyku$ zJ6CUF<3Bs$aX?=Ch9wEHftu3Y-WMIN7<5Rez3%qP=v3F(${b$lYjLLQkkUUhk!6ZP zu3QnbbQY-2dvJc~!9`6>Q`)wt7>Bee*WCF0WWxFSr)cu@3D{XUb#RPV@woC)xV^fuu9~2T0o)oxN z!M&sNUtSJ0;J{?F(1g`c3hjO*!X#0fA=w` z59(XL{t5P1-*@fGEV0wcg<{8q|9F?H&(E9V8f|rS`EKisjm~pQWNZGOewepMV`H_^ zbT^g*A>30!ICMWOJ^kmC(X)`N1yu{?PqwXf+_oq8(_Q8C7w3QddDdpCvsRbS=#Hwz z;^!YXM>?^(wn)F8asTGw{4Hmm$$yhyrjYdc-qFvUAyXCx$DSzZd$Ut7Cu<`Y_k}~w zTCKA~b+kJ^*`7Yqf6QRZ0r|5@8J*89mVEv$%eqgoFlK&4rs3nq$`V!{+vc#O?sKaM znAvf`U@_Mu%@a=#aI;SRZQ!g>K}F~DEXxVO9Df4{jK^LSD!B3vv{G5_Vh!hPB!*ap1kLiQ_Y(3 zr~lc^J3+A?I$4URR#*Nw5V>zdviYek7vlO@r!I|Hc6Q;sxSE%N>qK9_dj0b2r8m+N zM=m@5WHsEXba?iwyOtAqYYr~0FpW+)c=)#-kCAH)=V>Xc9sh4mJej(AdRXDNsmVNX z52wCadi;gu*7kP!+nxDh#fRJPx&2(XKDhet-8Hdy8m7N_R{h=H;o`CK;^M7GbXPdd z`Dqtlw?&lS_xen;bGMhD$XF4-ZQ|_wUA?B8!zSA7ZvJ1Baz1s&~Vo5s||M{@{dy!S}XcX0KVn zHSy*B!+-DB%}d>0(=fA?^HHM3w>RG_o(23)ogI2+Z=Y`d?K6j_{^gOmf4J8F4Fk*3 z&G`=(|M%|+`Mvqh{}0+G`THi%o^G;m&gYu z$-X_ZD(d+9ec8H#%zd`i`sZaN60V*7Uhw=7mrJ4I+W4RLbK<|-y^DxFy=tel#rD^A zayPI3oi#IBF!G3e?Jwt?-{1GATwfV@Ak`pmqn_f8X@5@c4-LL~api+gL1tery_|Y2 zmaFJ&IP;?GNde2>il6VZXbw`m3}Aj)M+*$3K2F6k2w4)uNl$M(LvV8=)4 zWo`=uZaHWkdgy!PTph)_(foa9U^I*53@DwS+WtPee~{Ff5Lq~cZD8@;*% z|29ixvd2H287!(2G4Z|dQzxJ1Gd~%X-&8rqa~i%hoBx3+L-E4T#%YP|8B$vGZyixV@&W#%c9;$Rs zGc8(D-cbMdBHyvr1vknoelJRU7qPi({hJ%d7tYh0<|e$S{-BfFTpo3QzNSkzPC0n@ zW@mgj*?iSpuz7y^!sS_7aVO90ntd{|*5{F-VA5e<TID?0)^)$xHYDw9nw%T4clia&P?e z_57(99ghE-`{t&6()a6fkFyxd;xL=iB(imeuCN(t5?v zja~dP_Yd7<|5kiIeE#FTCc<*?>Dnx{jj_8=gsyTv*jL~U@7R%aQVh^|JR|iMdp9>v|n%gSW+$iz4oo5-Is;j zohvuqGTIe))7dJd!`z@^s^B}0^EI#SPK90d*V8iFEMr^9ae9Mi*?jfuch72WoWdOc zTjpHXU#Lt-#NGIZ~Vh}|0Nlb z`&xgVp8Z1Fx_5W|j97OQ`Q4jmJm=e5{PM?={iW)^xA%Xzuz0r`yAYGd*Z0q)|8MQ* zy}oz)sTY!BXJ?&_D;4-YN&o4*SnJG}ML*WxW&f11t}*`i54Uss zubg6NTGeLS!R#N9T)Of1)VN2ig1g)=%k28k*wypX=JS2~M_;o}zIYQU_y1h^i&s+f zW3JhT99ZkicAKvvpQAZT;?bNz&BgzPhPZm*A#KZ(r4NQwZdAcX7l`nVs+aO4QoL9%#x>{TwOKXM8zSVqLS*0ddEQz7wBqgH|iJnN3!{ zYTxm##^v5uJ3*x-0Y6S1E{N4Ru5518^`7-W$ZLgnSHAyMn0JL=h^09zQ0i$_MW6Z3 zIPKcNGgeW`7k@-0iypbSum8l(8*QD6s%=v|oRxT)c`KqeELhUuuQ$Va;-!d<9Mv{Q z?cdpY)+V<6-&|)|E;cP`cV^Ebmc_}BZMOb5c)ld1(&Whe{J3ND?-kjeOlf=Wx6Md> z;)8XnXJusHJ^z1q^Yq6jd03>FEtd7qWj~&8wO4a(ylG(hA=}+A*WH|RZ(DiM#Mn6Z zk3S8|=I#HGw=w;$V{DeS?Bai+cfUA!IXx8UoVk5N&|aVWFBIbTw0Ky!tT~WoqdFnT z*i1wvXV5nN|ATYy)wU^Nz?T{Tm1WRxJSQA|MeZ_9BVOylfKdR zr_%p_W4WcUOPILkD!%OHtIFg(+k+3jm>Tmn_67% zHGIpNc5hF2{gL8{P7}hqE?rDC2{1lZJ(+<)fx*+oF(f6*W~$WkU8|WbS#9htTtC}W z(6ZdhZHHdqfd$W%8d?$+8rXMP3mmvV-}dO9h@RNwWRv#8;(JT>-MoHyQ^=%sF@0XQ z)84<(*tutSX}HnJ39pl$o?m{8uTW|7qn8gI(^`~kc7B#I%rtnh^Gd7JoYQGH=Xx#{ z{F9N=aWAGXc;**Ob6vAeN?Givq<3-!-=X%N z^@34Nm&8?n`5OJ3*Poa*C3O+Q!lsJAR=1yAiXvGlEQgIg9t-k#`gpOze2I#{9eoC$ z40m!}FASX5zMRD=W66XVk16~=(pR#so8ppexaWz*!jmw>I`$&rUD&c9y)l@4)9{6N4O1EzMstf8j)v{l<&Cbd9rT-Zl}|eQ?wH zctlL>u}@uI^K5P&?rW@bv0&NfA0nZn>Azv&sxC#vAEzEJUQy%pdZ|~sIHz|9_o9DM z|EGLj-7+KS=(AG?Hl-ZoIu-X%@4TO+dDu0b1@=ELiF9q9BRyUH=C=!1g-d%T^vbVG zzdX%piS>!S-(?Sdf3Q@x@LEj0vX=3oZIg4a=+15qwr*eaLh{uK!x+K#ZBJCIy+0o+ zX3p97kZn_+`}2LdTlnJqGWp~zPZzu_oM&a;?t3n=#`fIA=jEGA4vV!+fA;Y1a)Yk) z4TZj%A8&A(t?l3Rcc-w*tY^vMS2n)dk0 zm;Mg**kY{CwxjN9jh2>_r0w4Mo68v2MLOm>s)w3dwgkKDWNxauDia_!`&#PFZGSn| zg`VCuyX!=f!H31~{X758y|mobHz&qkeznG-Wv|T&zC0E05Od3%vzPVw_llC|jBh@% zCHYNhd!Z#+1gvuj=7#*1;^_SMYrE?(YNW_N=7^|pe)yh7z~ zRIKA(rQcij^-83q-}J~Gi`u$A*zMXpJ!i!vn|ilRKii~F?9}vobE9)kbc-2FXGUAb zT$M7dBd6E4NA9oWb~^N6;@dCrFYj6ww1kW0Bz=CywI{~hdb|9imH*#6n&sX)#JeqF z$!`&B{?fo3H%?5GH08K>xP1OQSM3)T?>6OqRts9J{HFS6YS6r6JjXe1Kg?#jGHH|3 zAw}i_Mo(w6_ItVD zy)C}d8&Xahef$66`R8hmNee$6(3~%m5~a4x;A5*>#(bsBqRWSF?y1e>?#|MFDSf{1 z{<^(?zIRujyb(6x#A4qro`boc(?4%Kn{BZ4+3srokFDwD#edgW{d;lEH{^J7N6m+Z zZ;sybHND0bZo4)!$5-}fHWx`Mb{{wQoBL$hFE=S=wSBJ_bqR@tg-(=}RJ?h;{iUW4 zix*4MAE(nNT{IM#%6NDuE^<0^NaEZH38mKx>g&wwYn>!2E*&YU6_FB`KYZQd)ch3J z)(MSorcRh~v~@-&$4O(^h_+4x1r?qx99kQEe{i=hpLo$Ct!{fi;gNsuz^lm4&gK z3>H?D6IRXB?-iW2%H`AL4O6%8@4x?J@%~vQAMO92sQ2Uk8h`Lm=;xikw@GKt3O)At z|408fw&q+MVLCB~|0(bOuzc>q{W<aUME$Wo^k8AIyOPopRY*SV#=DTlO!E^iM$M)?x&%(p?lH5$r zyqV>dCB)hCeA4-?TuSnx57zWd5lwV;R=I7Z;hi3+)tu^a&u($!zKMcG3x#w{?tS2h zw02pvO(=J^Meg1o8a5(9oPTN_#X3x7dl2m9X*PS=mW4w1!&iI}@cPY|Y{7VH3e$a8 zPRSk5YJ1;nZ{d67zjR9F&SiVV8s0xzZqmkKlEZ4mu-qxB!(n@;&^UEcObq?Fn-Gzw~qXp&t8f-T$}Vn0n#;kq}M$6I=GU#3kHXmV2viqmR!L z7f+AnllG)OWJ;Ur{z6`QS*^iI?cBS2u2l8)Z47j)`LTWO?;E$Towg3wO^ChhBNi?? z`Am;6r~6BP>m^2-X;Y^zwBh&G%9~>^Z6hzku0C6Fx(~aMPn)BLoNG(Yl=Xs&vw!qh z1N;$tCCd|=*;kSWs^ek#ge*7W74W6BAoz@Re`TFsa3dsZ%w>7AvaA~2y|sIkcQ z(%)u}!(3M+w(zlhyvn*wfTLmcwI93M4REshDqtp z>i<7I^|!r#*iE6Ob(8X+2VWwkx`id?osf@in8KlGX*BKW{(kwV5^J8~5NG;SXw^dqcQfJgHyovMd zs&s~Cald&7%~Rw`*fWEixNUSlOm&~gds;dzATsJ;^6xmMr@tM4hFkufJ#$;;@6?Un zAMa*QoV+V6`twz(eba=SI6L_(Z<^fjHxPAt_*+PEnftEGX1V!(Z6W^0rgAxO7Z%ne z%FUHLl@^h?@5>$IKj*EtZP>AAN~P-2oOzMwKfH2%lW%=KxxMd0jk~n6~O4)d`bKeqkz;Fr_9FC7>_-x<~*Fd{Nhc8zJTY(UwJhJ_RSZc zZD#O^A+&>Gk6*|HBOhVMfMucFjU4S2AAHy&<2P?w!6GgmEWy~kR>W?Ctn}01)zgAj zvdsJRY2*B0t_+S&m5#Gv+gy%!Rg^rh3llxsDXF;Rp>g&#omqh=1pcjWoih6}@I#m$%=J~J-E`rx8C?|@2h{)iLF zv|AthqNXR&z`ip|J&C(HQPbL*{m12AE!=WfW$UyBb#=Pt=hr=(`mS!C zU1i_jw|8^wOZ#l!KL7FIRp_4TpD{_TigiCO)t&hqyGBdnK!;n0sH(t6mVM`pyO$m4 zbJv(@+MH5upZt7R!7(?_kZ@<6roOh0t09xJUOQ=t$FmiAyl4-^`vC(}Q3D_(F)_GrAqlqhkcfFm<< z!skGNj<>J5|4RPh_H^b|CKA zRd$swQ`4n8q83hRTxjZL$XVxmm}Amv5sPICxso=!(&f|J+E#X)T~uB&Cwg6cuPV#U zd43_^4W@k(xO70AO-W|aS@A!A>unEwJLmUmkztolcZQ8mq={3h(!oWQzH4%3IWF4K zvhQ8pz7Icw@6+*WFfwz#vM}sn* z9=~5dy#s#ExTtyaeMZq==9Wc*KDQ5@PwNbw_V9VpiV(Ji%`+}2EL8VkxitCf6164E z9A4XaYaUdXP#^Tt%)=&z7DRnA$EdkGYM*Nj$_LVL}F*%=~{n-rgoXVO;kGB%+jBJCikhbPJr=)Hn0p z&$soxbn(x@XQ3rL%WrJh$9ntwhh67|rFe3j9|b$G$aZ8xWAD7^l8Cn~}tG49aQaIaG@PNe?) zep7df;*H;I9*i@Wc`8lZu|nthwdi|?US3{ax9D$JXz0gl*7pxSbY|C&+V=I@ZHuxm zCtP!{uZeAc|FfsRKYq)dO*-!?@3UK6FMGCW)57(~ySlp6PIov3ulC5?wK1oyt}<3troYPTPRpI*Xr<`O+{Ic1WV2+ zP1A75@XRZi5>q6jY5FijBvFbnJKJkb1dDOXWNDd$-piP#8O=N<@6F2T<)ozc$ma8! zr6xTG=JIJ=clDaMUg)V{r%T|@J4;U23N2sEJpJF3?qJpjCi}xRnfJOdGwxF|x9eNT z`Niw=f{;@S<|caZ96n&;>vQ?Yj3BwfO;1*y+x=v#+RI&2RJd03woRPZ@4)hxFJq$f zj|=@r>*rc{OEg=u8gBV!Aozpr#U+bF*S}QC@oz2tc(mDFvtPX6`x&-0*F_=c@>FJd zKi*dEe9cj)+q`nsj7O6!Skq?oO;ovWdGFZu z^0$xw#s0o=`Re%{&lQ(C#6P(#{%4C}*4a0%Yi<-Ae)4~=`uEMbzqv1WB;H!~Ha+F@ z^RuZp+oIJPU$Wdg{aJRp;Y#7t>rCpGrcO3oAzZjP+-YyH2DD!wbR_)8ryP>=J|)~9scov!Tp$$AYh1xmNh%L7me>0aMe`mcxSA z)O7Xw7dsB=u(@e}hE?z!w!S?0q3@eJG3Sy>tgkpHk7>F%Uy?uTwW9Ie-XZG)# zzh1k2b8|NPc{|}d*I&KL%DeSTtgEYw(bv;c^1R(=k?8!rbNjv(&zR<#8*BSiRP@a3 z{S&5tf0OiZk?M?TM^{c(pJ7}sw>z$8p6P3|fWW|`m+h;B8g}in%G*(>{ruLsq*-cL zO}E-&Y~%zyvdeEt#~u4q}`CD*YhWWtABGqWihwrp(M7}B;l z)_IPkOrlTbl4}{NCnDLl=^IYzywX&-wsOl?9e-KNbF$peKHd}c&hTp~;BZ*=u#)F% z`NbESQEAsME1RC&7#I@BxNLnvE_1aTYnUffdcuT~_Y783k9HmX5G$TLt0g(`&qbAO z3^DTC(;1c=aago;&x-sdVv{95IdQZos;5RTWz&B>(X&@!>x`c1R~o{!olDze%^o1W0sCiB316*=1I z20h6>$G$0SpWyS;?R>`Sa*wa=l9EyH+{D9k-F@mbhdmQ)=dP_u{O{P+t5LrzPkFW0 zV-IKLEG>bTg(2?ZiSCai4<;P>VA{4&le0AAgPd`e4SlOK->Ci;GK)O?a=pdV!V9G9#H~){`BXk6+fT%XbsL|Kiny zcdMj+6-Pc=cJh!5WBmJfDQ+fKJ8L^rgEMc7+&1$(|G4#`VxE?6`U$>7F3$)lmz?G~ zK{ZR(Bo*xr$(r-A=I5CO!n|Vok;nGefBd{AZnM~JzKtm-i@LnL1eug3YAjY(UK6#s zFGTC;9829je=h0SoS)6ITv)j+rKrdH-JWh|_HP@Lo=O?bG&#A^kd5tEu(EuH9%UF**AonQ`W$T9QQkV$&Fsx-so|Hq@}KOf|;@7Z`!a7)mc+?o9X zP5bW2iRr}n+`f5v%b92g9Xa*sr#B}5-aN;q^3$Sr^@$n^B3zp*P6nMit^MNl>zOg( zvBBP+o|-R9x*+RWWo4gB_^aLA`r7s0&v)?)ckP^;jv}1j}{KX6r>-e==V%-xpG+d^aTY*o}B2+=)DINPTH$9wAb&^a%i3Zth<(J zMe5Ax;FD*$qgH707*G0UaoPO9$$52$(l~f#=kM9keQ-^GZbN^6k!;J9A7zg~n-Lc) z?>v`%|Gs6Y?8V+Wm!HfrJ~=Z{ip_BHBG;3XYUbzOytG-zUOjS?%(TmU-|N{&f7_~h zdeYQ2G8&yrJi0tZj4kIl_^}xhq6IQ$G4S^>vC76B}ewe)XDe?Tn@ANC~>nZ zOU~}fx$yX!spV@U&y@OZHJKqa^`t{7tJOreUY=&Q;|a^mr!Y)eqNnxqz%11#Q*OJS z?qS_%Y`JW%Li~G+!_TdDZE$G_Q0RDUR^qxlD7^fC`nIf2uGmF7|A$x7SLJOrFzEE^O43v-zC7 z?NCqLxpUUDZ+^dee)1=~qz|p@KjmC@TlM_1Y~9LZ*_i?H+*{7o?!931*-0m3^Qq~~Yo6p)=P$1k+t2@C=}J$tsi6faS(C1Lv`nnwTWC6Y+R@^Zo@JpSLWVp% zCX&Y=?mNkuw{~T!yXTxhZU0gYms^DrC(=A3#MVCK;5gX0(sP=N_m8Z2{?%TObo4YI zx>y~(o&CyUwr6m3wEOEOR~d^9_pLkll$8(P`MmD?q&Z#>EX;1^OzZq;xI|=0kfxft zaLOjhS+k=h<>iwz3MTN*-Uhl<?+2@am@ve>wG^76@^YHG($ zrd+zQ%~vPR%WA69T))E0XSWB2hB_O~RGED8!i_73Zk{vt@l%V~nT@rAoiur-DYu-s9e$e(D=_xR;t_}sm5Qvf8FON`!DFCoSvI# z+sW(S<)1&D{QSnI1@l&i>m?mOq*kVv=kzRRi@}bxK;^|ZZ9Z=3%zaw1Xz%3rd#jr6 z`Sfj&Twp%=seOFy)bw5Fir#*5y|bxCQ*@%ns+El|i<@s;mYm}E@QJa3U8f+Iutu+< z)@ehpWEO32&S^(a@b^x9C9jg;wdm{Ah=~SSC5Kq&w`CUu^=UOYGPEA@EiPjI(K=`5 z^9Nfcu5z*WHec@xdD$cwm$_Yg%D0(*Z@sduBd0Wqs+O zDE-wm>z>nY*_~4^dE_>S-TM}BVbwaF-rbBpy%x4SREv^iJJ`y-ENSwaogip>9UM%Y)_4M=p~h3kmz%E@Cu0(d8K{P_OZ+?iKp7Kb5l|T zQg5>?b$BFKyucvcU604mOH61?9S)JFmsog zR<=#P;!2w)t*-xEuE8tGBlpuv|NjdC@ArR-*G1U6-uEz0eaboS=aK*3SEuejH))#J zIgS}uHauqjfAUwC@}2Kf`PTiqykGI}pZlNQFm}gozw3G9#O0rdrdw29SZiXM8}ImN z!|7Lgx9=YK&^Wnfk$>Q20kwqKih8sBHP5ea4@ru$-1jFyJel|FpSRaI_PR}YRnxTp z=P!m$`{wZ9yWlW$VP#>w`F*=tmC6M(t*6iU^I)T&)#re4F~=xA-P5anTv!*Sbm-wf zbt}#Lds=4q=$zao5vYE8`Tl#JpG~EASlS(#Z)>o$F1z#dV{y$GjD}A>mNukOxCHBug^IM*; zoF2csWaJd|>Fbdrv#a7GIPS&Ap5FIFq_1ufUi$*Npeb4 zHNX7Ro0n~Qmg&l5HWU=B(UI2D`tcz&eA5=2mNKK1Uv2>>MI03;v_&^v?{k`=cJWfs zqSC7psY?ZTCdkfLD$Q$XU<^F$koq~VY~^{cs$apT%b#WxUR$yLZ;1^1QqjK03EHix zw>6gT4cf;YCdNbLlJD?pii(p&skkyq~$XMZkE$ z_ogD={<=Ar8$HEc>lbceUtr31$khF6SVF7Mvh%%xf*lHxM1cDU*$_wK=AQCw%WWmh1gA)7ic9&jZ0I@8l^*9?h9GBc;H~w0b_* z{rNYA+Pp2d-TRYQ_x=38bsv7;Yu{fHr@nI2)ax7G#_o6j8p59bt#)$wJo`T8DRIBu z*Vz4h|K@FINbb%~*G3ceZ+6}_&;LK0^(()q?4tV$+s`V&Ucr*WHea`%pDesK=JQ7W z%U_S~Ur?>Ve9CdMK=@tQS^(jcyX^luHlF`m|HWECtE}+yUG9o^_kweFr8Tl4sKq{{NfI;(6+i9GW# zf1kC(t5SJC)%u!Z)A@B3Gkh+7|M+&h{=+xH=@w61e$L)~`sbI=v-IDdDZGCD*o3F4 zcQ)D{X=Yzu`Tm&1cRBy!?~fGb*CopE=Inm_SEYLXg1-sj4?kzU`h-ty^e^$*Wg9bA?yOvq?;7yGe0kdZ zgA+YkrcJx{`AdJXdsz0r+LRZ(>z6NE);4jX;I;f4CpsS=-|+UjUg?_X{STYk=0)hZ zg@%gWy?ZymDt_sd&3W67?%mg&zvEy+K^W(+bklhie=hzs{x+%KyYg}Q!nT!n=9@Ax zC4S0~v$j6a{ao(BIbQ9`+q*=sxGbJBqpRXV31i=>O`DVsxtG0vGELOG@@0;Ge|bq^ zmR75Yl8pD0pY7stPyWshD@^qk4&_!}5)^zRd9uXJIoa1Hwn^uj+WMN!Ru#}bx=Dtf|0;I4hw5#v3c+jUnvBzVWz7NoMKS)6$AL*@Lo zxetF_Q1DF^t;xA978LdI)CYt0ON#TXuP+MQ`0tyzh{nIVpU3Z3L6YqH*QkQ#DkUuRzrNuuZO1ND@X3n!kS-qX& z6~B(0eY3;z(24SW2ev3n*qi%3yUK21n}-BGz7AA7W!zh`4r zwd`8;_DU1Obk$pGo(&x>D?2)UcC}qN<1%eYTYz7phim$N*Izdb{QES$ynQw?JY#$J z=XU?4Z%gf$Jic;*)hy;?ir%sLYkfcPAODmS9p|oL-FJe+i%FrybL%G0q@YN-Lo6*k z2^!reFY+AsdiCRLs-ac&F79|nWoIx`^NzuW5d=6l~4_WygEQ@-#0 zf%5t=FOeBncl!O?dHT$pEt_Ni6yN=uf9+`erq^#Ka- z8#i`NnkN^C=WmXrqbJGLc+p2(fj7Oc2A!$W5$C` zr`IQxq_jk=X}OwxEif?9u{hY6VYXSm;=~yVCoiS8I4AmCPIHS-`PAxj> zt)Z{KeCN`st6pijwY6tvW~OFkO^VLj+kEca<(!+ldf(pGU%vM(i?6S*%4E-V`_lMt zm&f>N$RvH9mTO{ae);N6%lc`5CiU;yRn?|9wfp|+kJ*U|7dS#3muGM}`W@J~Fp#h8 z`^N3n4Sf#-!^}(r1O+m@6Ma(+-pG7S4Ky>kR&?}ISkgjLtWRCY93(7kw zz1}81gEO9C@++GMuQb??CrAHHopLSX_}u5r>zht?O3rLxnZmg6SC-7$Jq>lSOUz4o zgAZkAa*6mv{cFASKW1sjL7o?mMc9| z&Gbz-Z4*)!J1i;T9%0rdFz*gS4yRQ{lF7B?DjMU zx7pd|Qfc4cp>1q4)bFpVSS%Ze>R7Y6_3HZE%}xbKULm zwAmSvJNsswF}QJ^-$CpCp=oDD=h)t9o9dP1l6+^=#->{_ZNL4~BR8(=nmv8$#vMvW z4tY72=XZU~Y|q{nvU#7+=d*u>kIZ@$vr)sXKE2CT`qauZ{-0kQG`zNMed_H?7NwUS zzDSriDONuD_oB^v{ya#2GKX9K{K9pfo4T`h*2YSTDjn^=Wi-e8>CLSh+uojK`)+UU zRc~;JEoarV&<#sHKi{~V>|TbW_YUB2$Yo6r6ewx+MsSIy2- zxb@Xy@n_dF@m(9Mu5xXQ`?)RBV*QVYZHcf;{py~I8~<{yT}}^cv^=KqnCaN6 znLbx;{tB}^eYAd|;JqE~U8~=z-LCF(NR@RnIrqWb-tVmS|CWTiOvm(dt9mw#UBC36Tg6Hzgyv(BpzD^4Tw&h-~-PXPP{ztuAxBE7i{bjo_>(?(f zE9>(cvc5(wx~Or}r>)KH`pNjHuw&caTEBQ@b#~WRX-#eY;|4rtId`1GpJ(P(+&(K) z_f;XtOK*3Lxc=TBHrHeSo1JlJ=sKmvbWq{?o1OE-=hsh@o&J8wvTc1QPqN%Dk2&{U zzTocXZMmDv?y_0k^52kjRLgH(&i6Y%wP()pQDBML{k%%9{*B=IPQKIOYm#zudY;d# z?YjN0!kX!NNN8}ddHuFC{o6a{gvu=__&m+`opk6-o7HF=e{*yr2b*JyEglva?{AE>)wV$@>YUde*9`h7t+ z{4V}s`cQoOx76FIf77)@>sH1v_g%|~R$MH|w2b^QG}& z-w&+#c5qFZ^4cZrkG5P}*LSr(W`bVws=r~YYsPv2`x?% zK?&c#C~g+JG(mxPd+5puFE&}ECG$pQ&G~1z=AhxMi)&&w&tcx{{OA1hdC#YIidYJ& z=R_S@>SQg{x=1HsgMw6|cd_zY1;6LZ(%th~KuD|g7e9g^T%a1q2)Al|;vpN3RnaxLo)hvJB zcy96YM$D^y{nOw5UVmu)w-oXAx(`1;&%Aw^|KpO4oeLV~v^|fLeYkVc&$sT!GwiKy z&!6yO))SxbK+%s^H0q!HPJC}4Ipfb)`LAcKHZK0e;a~Ho+vjQg;-;0N`up#^f4|R~ zZTZhT4}5wa-Y%cMIdkg4gYt783b$`=NcjD3>7R%5ii*~kiCyAw>3nvESw3CKc=Ntv ztLwXE&o;j~doDh!UUKH;?yW|(#`foHO`o^D5&vJeQDY^y)=Li1P9!WBsrW<{*@$YXAcO{87PqoPl*X?V3dU|?r zaIkl2sp&)y7Dbl#JD%U|>Fd34t*j|~-QK>wZ;wBxpIXn znV(cxYuflyD#?tc-;QGSgYNF74jK#jtfn z&3f)xflsIXv1QQSYr9GI_SIF3(l_~S-G1C|_A75`8-bkLW^E;obI+G(EuG-8tlh0M ze9G>Ztfw431hTe?i5eb$_RGol?Mb$ll9@p^mnZW#a7$fv+O5gxr1(LIYs<0)-=@nQ zUu(M{Y61tx^#fBjpWyFHy1|uL#c(m%EHqWJah1`Wl_pgMB1V-WYHv@9+_%1RZ1Y~` z3?`Pd3cPOfvS(%8ynCVHVT6Fdl-A@ct%pP0c}*s4W%;?LcU@1i&WgqMlOIo==XmQJ z?>WDqAC?j~zvX}U`)&T$u#9EaaX(kCN&oRue~;??FMEIA{(0~3X8Y><#mg(s7Z+c% zsGHJg`TlMFFYY_vzx*-%GrctC#L@q0UzUl_GCQqOYWVnajEl|nkkW-YA@>bR6)#_X zzxVet^XhOr{jXnh&F`Pu?Ek-`#Hv@IsldX=ZvJr--z6qHo^HEc`C#AsqPO#2cOHE- z=S{}q#f#_G*!3q$urxaw&E%Qr(K1K;TJQ0+mcx0T`&O=;`R$w8k|`~R6ED3qIhY`D zx=7UXV#btRyR4QiUoI&pcg*09bi=h-0U99@=St`APU~^9Jg@y;vaK;S(nU$|(@&ee z#{yifT!FrJ&B+pM+fozRn4WyDWNJLH=KK#&rk~G`>z|9*U#HwI|NqB<9+n=ehAGM5y2U-AeKOJ;FmIeog*?24dH z_hKLBR`EK06Ro+9{C&wwHgUXNuvchP?rn>@&U=Mi3R^AGw@B>x^?cS0)_XHF-!Bf_ zc4(vMwj!>-i)$hqZd+X`*?u$M`&EUmKQr4xj~O*uZ?l}1>gc3Pv9)E2y);hu`d(Lg zXTMm6`@+4oj$S;M3nnvd;SQ?hUG?GBvr~m>MzKp9Cm3IM-+x6fcB1Z~^W|T6XGykc zX9`HkcJ5HHo$a0_t;_W1`p3&P-*fbDM@dD7M){@cc&rQvX+Lb@;&VzSpzYv>Npt3U z8BXzfl+k^vU`}sp;3;LtKF^c?PV!Xm{%ui|H)F|KfkT=Gck{1SD66gfX=wlK_)_Vk zYAd-P9-dz^BhQHbFH23C&&$s8EV_Wmwo9*5U4xO0xz&P(RzQtT0ZYcAg z*LwJK;g`P0Tsixy-*3MV_4&qPdyDf4Pb~ZrfBHPDG3>EF+utALHPLUkQB#s}kk-^i zn?1h2w0O1Wyv2OJ`MM0Bwy&=EnY7`h{`>vEUVlHR|6v!~x}S#U7wj|TSon5(oOveVbJKMDyRnchQ*~Zk*RzIKx5=ZEcP{IUry)+1-k7nVDfrV+l)^&A}^Q zL?*t{Qu?}8VxvaXj;RunQtmB>Z`3Lqm<1;A)Y>kq7yEhcx0B{R4uM|>O2raH^KJ+H z-&3r-ysIUcY5Saadynrg6FKg~8MAivmVK94wB@mPu`vTy%cZj_)rrtu|W*MG3lY*mWy0GuhaG z&g*BB+5Il8*L8CAbl?nM)^NQwr|wMu&yU_G=bTFoxUZ4?YnJ5-FUxzZaSAQrX_tf) zMW&hV{yh2r?)`I8Gnp8Rr@4J~a`>L4xNg0<6L*+!R$Q55!>=mY(yr&{O+7&dbzA9W zs;|5rzx((StrlhpCS6vqo!48$+1q~nes6o^{{2nuwpWCdFr%X7Gco!5!&PkDK z3^uU(xiP3Ud8sLveM7wXfk!=$f=mJ@*&8Y`ptc=Tya4!?jZ~YSBzL4aJtlGC2n>2z_5)pm~n{=QJa;&&hRX`pwPt z)b7`8uDZ7yoaP37(0&9=|x$dVv4#{OUvvvw)u-H{zH5Fuu>zY-RuK_@5s? zCI8L59TazJQWr<6Re$2mCu?Q4v0maVC@PpS=kc`SKrZRljuwsl%ms^<3ML7<=rm1B zNP3{!k(8oyjy5lP)}|GQ<+@Av%ppC3;Xlb-*}`4t$bcY5Uv#x-0^Wq6!-Sg9Ls_W4-N z)Oh`r-P1*BkRs^(%?}P!EcmDWI;JQ-Ep)oej~lY@7q~2cDJ%W(&Igh9c6Uo#gF~^e zr)^rWib;L8Ud!EVXE7eDP;T)AsV0}}mNQN@T(DSnU?P(Q~T=tM|5WT-_BISl4;qc^>P`Nd=C}gUT8ointwI z_bg91E=1UP*A=HdE5cZFS20Xk%+*s=__>LD_USwOcQ16*=ry`?ZsQ>i#WM<$$Cv4^ zov~rFPioMV*2N~r4$Uw+Q+|8*j_)4U$$f`7TQ44R@IIcLZTqu9FR$^}waw=H_>;id zznjxX|C-X(FN`Y_f266gp7|5Q6H^;@rGN6L<%fMN_umjO-TpMQaQ01C1*{5oE| zPEr{UU#U;k>@d4FdE0Nr$xd0mG1OiZJt0*&rO8@pRS!+4amKr*d^)EyE zT(e$!`n*c7-zP#&IK@o+;*-8F#i2?~jc0%73XLgmVm%L>Iqb6WS!`9C#!FYf%>2Da zf3ND^cHUm>=q2WrrhWkioo5rQ&VM}V87Dh`iTXr^1c%@~Q(hHFT%M6)HTQXm)#25` zlW#Z*FF6oeXSsRHvew6yp80v7Ogs4lH@5CtrD-&G`HU}okvY$g@2K7Uto42Eq<8yX zdT&Ub6rkf(S2ZhM&&y4qXKI4z+vyzFHvD}npXT|+ao@+*=QEctcy@et?TI9r$uHINXC`S}oL04LcQxk|oh9rITd!~Rt?NaY+Z6+FB2lO{=J9Hl(A+oyeMDBRc>V(Hho_UewJ)+U<+-Aus^MzTu;muOFX6fGOu;_P@Wd}{IcMOIQy zj7dfw5gZe`GFAA$^%Z9*J>C{`)huY|&-K5SFL@O7tJiSFu~pR(dp~e2KYxxPMDo;y z%KQ5--!0GFvT^C``-it`TmATF{!MUgykBT?$Gq^qe}`g)vaaVir5%%a^X6l}+Scv= zWWO%|*UHR(cTMPA`G-&6*YuyduQ_-B#B=@ITU_TJT@c67agKFKapdJk^Yy1Jjan0E zd*9+|--TOur@Y(2nVvQ0WA*=^e|q)SDg|8qa7(=DlF_<-&u%&W*lRDQ8|4xnex3Ec zaQD%OO(l};{Clmkf33S4{^?Kg`F$&yZ(q1@VA|T-l`jsmPYBwS_*gBj_Gv<+<;|Vh z=I3pH^DuCW>pgjMb8}4P)vGqY&s?7Vg@0e^?J~YM_sz4TVqG@C;u77IVN*>mT zkm%#D+OopTuWsD2tn1e#gNq9~7ak}O=__HJ6vDLG#%<+AuV*>yXV-VdKPx*pKR&A5 zrXcD^-A~I!bz4@nUwU@T^JjyoP1?qY^)I(8d~I3MW^%#lu>teDDbvkYEnTo_-L#L5 zj^>j>cI&M8BE`wEWwwukjq~JB5zn32we|;3$v3*JA+EXWv;^C!^Oj#3w-ia{yv#Qi zTH<8EwJ>%=_6)U*0-4*Uwv{=Vj!}U}9x+KQd)O3uL}rQ*JCx8kqq}PN6qkI&FejH}G!Po~jYC@yHwft4kkfwAQ~h)H}7{SlGRz zrQ6>xu)E<^c}C^uQGLV2vymEGCTQ1vUcbWI-hW?`^tm6-HXrZ$mHqF(uO??CVfBf( zuf%1?OWExP>%3R+q`mli(07|Fu4}SN6!YyknLhQs-4<$xB7-w>))l{X6HA zZrARA4xAREt)~Ba>D!C`1-tamiSGY-{mRn$%JCazP5%k>*T1#g_P>Asl4VLKGH%!I z`Ef*X+uv*L8s}L!t{;`GdQ!h}eeI-Ur|ahGT5l83npjfvKiDF>ZgQxb`OaUaEN|oD z{yf@m5LzZ_{eI8nj%^lS7yDnjHZSSw?tha^P4}wIZI!qtS@-4V`3=9$^6DzyL0;2x ze{C!;$jaJOAh_?%C+&=koJD_+6t25bs`@*wu5+wP_*|jQ@Cr_C+Eif?j?3F7b-QxPHvrkX*^A+Y`$+Rx)`S-8R=W@#VujWQ`eLfxf ztT#bcLP9}+gK0zF-KtgDSrI!j1ZSJ&t0u5Nd2pa>~q~x5gx6<_IPuCUO6#h}!88h!x1*f-kRzzg9YjGu~=|c@g z&5Im85mOvYyzFMrVAbRP<$3={u!yAU-W_T$Us@gP&Cu$bqGjeb_q#%9f_VC}pRea| z=WV^l((E{CawbY9rI&7%e>~4%x4q_lR&cD?)ytc`Vhe64J4m#J|C-MF z;mzMyTZO)I|7B_{SzcWjxNo7?kJx3;jCzVoc6hCJdafJa-93FN57%3XCsPg;23c^Z ztlM_oL7_+EvXk+Yt&-={E$5y2^xtBnzb*bf-Jei$%1!5|a?~remv7%FM_fO3%e>aoRU ziQw{`l9CNm*ruf>ODqy_H2uEy2a}QcHm~JEQ}Xt7U1YcaEqp(OF)CWz$j+`!plRX3 zMkb4|`qyTgZs;nnwOzG=0yS65>-Obp{=+Gk9?#ee8+jqYGwmV`| ziey{nr%T<&f9Kwv{d7+E!tLJe{PK^UJ)3cG5*Gsl^$cC3#=nr}{hOg_#Ku^~Zm?OJZjx<4F@#tcleqN7fxTrx2+ zQ*2QY>YX&pZ_@dvFJ9R=>&QL-{k?vA_P3bx|E9+$U4FJUVw$Uk4C8|3&vj0RuY2_6 zWZA4)(H1p70uuM0)z$qn&pQ0a-rc)ri>?k^yfy9cv9n)F<}Y2hZk|h0)2mlfTB}z{ z$V$3#I0hGgUh_RHENtTW=N`*1gI0v?`EbYhPQ~T5J-xjT%89mYaK4w2FQ$oHEt@>F3sI zbG zqN?W68l%5Hp8F!oS9PEMQnjpf<%fIOx6f57{ugIj9j4Yj`Lyr>?w95*&kxVH{Ga>h zlpsr!#xLco)lDu{428RT zjdSm=7c5&k^IcWj%Q;L&J^X5&WqS|26zt9m-dz}Z(L(X?-rQ3K({lWp<(BWxoztUr zDU#u4LEnmoC1sYyE(cOy`F18-7hhvnt+sPIdZE6c#$!X}?e&$jrz8|vFl+hV<5ZZi zq&M>z-`<{$kIKxp>7*nC32Ew6b(d%Ww6#1tpTrM^Wgb{=52bS(^ewdeZH17&T!x{HiYlx&}9{qd0hmg2jyb93$&O1k&U zE!w$r>cfYMkCraYxwmQSfemxtWR%|)lTuq9w*J_*Ts2`%HDTd7uV1Q(^$T*jTNJ+G z2#?*|zE|J&{Hm+j>8)RGZ#(33dd$T4(#lM&)vI2ft1n>(ed?QymqzPYjs12_&FL~Jgz{rmT?PW-;6GWUzO zZy!GLL*j1wc~FbKt(|RRP*S8=?cV1rR;)O%E%)3UtCvmb`O?$$V%_53TTWSAATvAl zboiyqcU_Z{jhUHi^LAff$XIp$>xQzsY*NzFFW1auDmqmXxG#!)2B~9n`UwSvafHh zuDbQDlPN|iMww0v1+K?c+s6O95mn2=FOA) zxBmVw*?pgu>Wkz(`u+WF>g8p+dx{e5Z*O~_kZ~dRQu)`d*FWBm|MBMh)y_3~OPLow z*ipv7tms>G=;4(cCbEZvZ}jy(y^)b4cEoFE?pvw6n>%_$ucjtgb1a=8bx7*aKHu0T zjs8s~XM=*6oo~F%H1g|PIeEEe=hTMWrs^HRa{c;)2hvT;m}ztQKlt&E)F|gR^MiD%$Pp4^x!t`8(XKartc5pyH$9; zINx88W4hv3<)vLEEBcl{EbVU$e4(Yf)J3vMXV$(suD@@nv>#W?^!72Bv^j8^?->IP zUiOumoExp;j@^FsYUd&)H&@S=8Inq39LBj4du+@OruS`a%zfY?y^vRcFGw@Tlko!g z+nMKT9{du%u=3RI=_2l`^U5E4FF)~3HgQe;l$9>OniwLd>7_ots@-`__ra%VDa$X- z>MwskJHDdgwspjABZs;#D#?G>Pp~QO>yobPf7WpLg!2DWD?*R|TKam0xo#8hSDET( zj7D+me+xG&mc?E@ve^IoL>U={Jz3n-@80uT^;|>UNi-pSp9U_YS|B zyIpB+p`Xnq*V9b~Za?J~sHH%M0@I)DWpUthQ3S>|N4(Z~aY4 zN4>=KBTs#N>>eMy_*Yf(#Yaa=L`$DmEmoG`xpd)zNBQ@g=6~iL?Vf(&X63V{&q*64 zE?>U9e)pa8S!?3=Hy-Z4U!57gsP@!p=dKr#9!?4;GLLTcj{f<<^mc@lwqx`uUDoQv zPq(y-wfjYG?^9nuOP{()sf0wym0b4!>Y`^yH9OS9s=qvKG($OY?nA#gx{|h(EbycEc=6 zbGq!;U5S=nXD_|})o`~~KzQBCHA`9?_OzS{nH9YF@fOudJ(|6nWPC0w$e64!@;!Bi zVd_JTj|&`4G&YsOQ->cF4TFjti3-5EGd8@MT|IUxue(p)@`?@)Qe_Pdag)d+z zeH~_8_{CtKg?Eeejlb{itKa&~D_8yJ$DZ#0;Td;oujlT0ul&E~X$R8>9*JMRfB!|T zf7bfq>Str0`1*gdD{Bw8^d;`HY`526dVW^eqS@ak)V%3#&7A0aT`+cM@5cOp$`woh zFFfcdSNEb(zW<1k^OZl}?i>G!muFq@r;mSP#e>P>vHKP;p4-<~CeXC$w*84G-|fHd zdabel^MA$bm;IhiJ)WL-%WE#XU+*cmIUnmzz5mmE$l2*F`@hbI=WANT;|o&xb92=* zvaKKL#b0cgETJXS{wT2QHv7z)AI1yxYIg1uurOUUtKir#uKmZ?|K{DaoL}*<>XB`6 z?vFmMpCJFP&bJ`y_rH3Tk59Irm|wZn;4J%Gqqxp(x+Q9nGoCjGH(cSBZ}I>C^3CmY z=XMtyV>_|+y={H0{15eG^&eQ@6z5K#;J9~+SZ3@J@h=Ra3ky~{a$XPZlhzIi7h1k; zm00HU8FG@x{QLw>e0mZDBsmnAmt}Wwu?Vy^a!9mjpT0QtsmASNC!YmQ<9*3vl;8NY z;q8Jcw?76mIj${S^03J)&E4bYO6H?Km$y#Zd%ms8p-e15H>Gh^)0V{*bsw$n>1>5-GgEG`i09K!^%fNZaXVCN51;k?x;?OCQK+^+ z-{X1Fb=6@rJk38a>{=@$GEZdX3k%QpjR!ecIa{1xxJ*0x(SqCC+n`lNgH_|Lm*|!w z)0)n`G&wGO+2_gTB{Hr?(~k?XnavVfTJd~RnpDd&JBd{PVr6&tz5ub#4I9?~^t1Nn+AYhAG8FH!i$7Ge)DU0x{|Eood-oq5SM=JH8~^y9dEu+=IMfT?P9GiE|JaxBRa8QP!UQ40+k_ehLT4t`}Gn-(bMA=3<@G+0!1f z)txU+>AtAxyOUsUHQ#yK)u7M!_A7+kZkjwxWmk^!NoIec*v-t6JqCZ`*&f}I!t>%T8=yz#U=p!|EgMD3T;Q*!tA6x z=aNe1|VX`ha^ zbC~NW1?+D4@M-N4vo~pP8vm~(79cMe}z{_jZbjT=SEX|B3PB88GB+TAy|%sDmte*3A^4ae9uZ&%4qW}m<0 z&#m%FudR8Vl6rX;td&uY|DpY(m5)cm2d4@%Zy`v4<8BS($$=#X_?0vlle!^U4-$< zqtYD*u3tVN#{S{_DF;!HwTkZ>UVdJj$J8jK*J1KSrlGgY`Qz5#l6g0-o-v%hLw24? zo^;R@CynE**SE!9m}}*=Q{gJ}@^d;2cDwhkzLL=EKgr)m$=A`NAw+GNnSoDdnB}2q z&vu;neDj;}8(FDTuNfSvi+DR3TUw7^%9z5xF4EI?0n2O?)v&(Rr%&G~cq*6GG%3Jk zVJ`dOQwhpD^Ky;lYu_2XO%6_|x^MV*^3?COYJbk&3x0O|&kXtew;vdn=8NghGZU{7 z(>E^P#~;@1xcaBtjf?X|n}3D%efYI{<&I}^Mb5S-5@tBK>Rvt`ue9x1(WThq9OinC zY|cL0->YxSVqf{~@02xn+O~b>`FC7i)K1;r&SOcc6 z6TDrf{OyQ<)%1CZy6fxpZ#HgDvF;Dp^;1fqso-%^(c#)DYj*YoDqE!)t0HOG|e z3e1X+qSvcOTyB}5W+ZV)L(}#7=7>%~Cf@E2z56b8OZtC2%jURdvT&2}?b-Lf8+`k^ z-|y4P{Gg4m%Q#pz2p?uSmF?(psJFXt(@Uo({dcc7np%22x~PBY$HM>4pDq|riT#@0 zQ^g=^@^hlytNULQ=bSM~4drERkc!&`m&eax`|b2wiey&^5g`NnLydv>b`Q}piGhg*+VJ=&qO zDmdiX_tF)0+cobm3BBO^XxrZ@uFI_U%dV1-ek$ubeQD^b4?pb|nEtu8ENb^M-L;OE z8PSas=1sf9y2@!snuhuLl4CuO|Gn6BVp~{V=Y8gfoA3O-_to>ktkX+`IHGqdL`C0N zc00w@^TM(tCoFTcw3q7%DW+R;a4IT$PN1S8~WKrZ+sxOg1c8c0}ZelGGC3 zTPl;czbs@*aZ0Q@wuxu?<{yueK8lGp)(f$fg6DC?1j}6+CN~uZ&M`mFKKGq!u;`2{ zNov=ooMGE~RQ<=%HUI3j*}kroxvA`LT=3-O=Vj{kKSUY25`8~?)MqpZ4LQYrVdoa^ zrlQO@Cj*;8dzak)F1RLp>8pAC%~M*W#3$bQZ!y(;Py3gN&o=~fihtNDzu?oa^2MJe zwRq?cVBxN?H&0r)HN00BEEB%5Qgc&!pU5`N z%Y_ZEtpDYEt^d#aDVaIrwo*W3|23|uz7yr8T_n_Q&YOHB?7c&_>xx3dv`EuyM-0M} zSaP3ogqc5HlJ0fa!pu+0^QrZ}mOnQXzFo0cIBjaif-lRyC;Iy*O$$@%VYqd!`C3Yq zf8g(X`&CLmiF$>-SNQm+=gh%b5{jELc!l-tTx#z6U7BmW@M>7n5ocj8Wf9jIjv-ZC zTbAXVujUB5n(W(?;O{RKxufgJ#OH>OMW?*o*ZgJX;g(n8lPte#Ew#=&G*9A?Js0d@TPySFuf6QrReP44FE@Vc!~NQM_gn3~5h`z| zH*O64X%pn(rqf<~cX7|s2a<~?XEyF!*(Qo3)*FH<>ET-xEG6hf;D56C!f#x)w3{h$IS&%i>G88 zZIW#bdsp{^mqEt8(*N5#LxU~NQ#POer{{4sLzi2qiD~w$Mi+TgskVn%`La({o4O)r zg( zX@kzDB~ukj)FxX^w$!creuic9qkGGL&lfe^cfoOm_~8Y051ze_yU?euU#UIuX1Czp{tpi~7u}w9|LgS?DYiE!|DL(0_*Y6*&F8OI@^(oDpD_M0)qZ8& zqjQgZzyVCNKZjC=@@tnz=OfiuS<;%eMcW;iK_O`g79UwGTuReoX7PeV#k>c)ioJkMl$9 zbN4fTeVr1JtC`3#-^#dcS4z|CbFr?$HZHX$#~y7Hv&zc$Xpwy-B-nFp$);KNJ{ta7 z9<*fHCYg73M?YK<3BD;K)G0J8($_R6FE?>wwg(4$ie<`*8K(ZN+csOwP1W?~E;%5V zCA^_QbXtJci4EaFzcNFnurKTp-QhOz+CGQ-Tjef&?0DwOaO!WOS~l-dwk<}BSR%R) z1ZhP|?a?~-=ypO;9NWr(qaMMc5wV>PMuA>CR~5KuWW{zn+{ltwm?pHT>wtNJ-^M9# z7Ij(OTJ&$r4~qlFZ2P*(LKBu-$^<<8{VK8P`L=zw(}Y*3uq>H!Sa_xgzl{CSbq_V& zj4HSAK4Cj{eb20}g;r~hS~X4RQSLeD@%XgU3IWfK>Bl$xzxwV>=<29Ywy!ha{yk;( zzr?|+c7Yd1v+~p8?0|Yvfyf@s+NFM6X7gt*d%PxW>%Yy{*I#Desu3?P%02ldkA2-A z%`D--S=mBcu3ot$)PFf<_5wvY^*0)?k`u$qwgpM^PhNTU^Q@<_6|Fq$#3~p+s!vhK zi-=JXA5_55iiHdgLt1K|@>*W`%|8?%A}Ytzw1x79jt%0dHejTLauaC zwU-$|Kc8D^bZuLDdZyK`=_4mKC%5)C>+Wwb+U18mc`}Lm{ zCRWUdQ7aGI$FffB{IA^&?Jf@`yC+JWUy;5heaZfFo9|v*W(zIP5QQ;$1CZrZtkgCC}JTL zIs3>iJ+-L^dwd@*Gs<|Yv_@6Mb%O2tY%x`)x0&(NGjoon?B$hVJ;kwg219<2D@S+9 z-!s1zRpjp%x3FK1yi*{tlsAz3<{OLt>%Jc+?);oFl~tkQU6-N?(=%b^4X1)QqTkJ% z@#x#TL_Tqyn-fykaTJ$kOXtrHb)H-zc+Js!yGd>Qm6KsvPFpXo*rjsuSyjtp2dj43 z`7K%{#FV%?vXONSL&O;0zNtvvtq#R)ya=F?nMT>h>Rn{`XNP$_YOqK;7Vq?%tB+kd`# zY%O5+=6yp&<@$ATZw{Yb6@BJT2LEmLP5PD6*Um*Ae)2%C&;EGLl8hTC?$`eOU+^W~ z?#TB(m7neYbu*UReRSNn?|G}L;n}DylU$39|2*nDKL6`C)$qCr`)l0At>?+PiM8$c zzb${mZ=Z*ejA5~#bMpS)IsW2YRMxe9^Z);?KK;`2ns?P7iS4z2+k0k)|EjrsB-A|r z!Nd1=e6Ka-?3>|V_iOQ;|1bGB`mT8_|NmX=pC60gKXh*o-v7i)^{BFa)g00A*7z^W z*6jbKd*)?**KW3~pYQ*Fun(-3OAg@{s_gf#dl~F7W9s^9>E-)3v@1s*=qos0{(jNx zrjnP3>aM!Zmbmp%F9Y|6gzAF!$?2eS?1w_$Pjp%DNVF z=)iP)ujBLoXgA;gr4o1Fa8>Bqti3$+$|oo{9%-vn5;5i zIiGiv+QEB|msh@h6ZR@e?oemKVLt;K<-c#{XRs&+jbm%cJN zqwOh@Q^YcOc)F{dCyw9oH1~b?xnpPkd`q5%SxVQpTQN|NHEj+4lXlzxh7> z^snz@cXzJ{j+X7)`1EgU<~s%_4kM>em+vL+yJdZJMG%K#r@K(gvvbedYyZxe$9eDG zO|Q9MC7qQujuuQ~o2McD)q1DR&x45@{!N(Fd|;xG_tCm=qh+EOmxkR4YRTG|`u={7 z_LgPaj?D7gJbP6|Z0g0%1Gf~Cu1#YS^m&k~({jLADtg8eW=D6Q#N@&yTPqGtGOA>& zVt-+6+)*eJsW_v6*ShWaj@M5(dj#yPWjUjKgo_K*`HKxU&RVyx`bSh2XZ;?9WA|13o73~E7tVcTAa0I`g+@q?N@sGm&@Kho&0;7k@>y}uA5)o+*~id zo9~e7``;SR&#pTD&F;+e{dN42Sv!9=*PnS)<*D(o;L|t%!q*3??)_;y{o-!;x7G7D zKa9%Tl_cEPmGtd@L;ap~_4cqg54z{qHcj)kJLX#aSmt7P<$3G-PO-jIPBP#AajE?M zqdVLGGOpjBrJNjFe|Vks{sXeR-#vc#cKOefy5$=`-1>j0*IRa9Rgqc#zX$I(DJV+b z`>C5(sd38P?jz&2x^Mb7l+QoT|Np3}?o&R0!NX@_Y$?XP?!{RPa_L2&oBIYN#gt2MH66a0gU#y z*Z+OPKf|Z&W260l+h29tX19jtEqeaD{%rX!#=z6^HE+XS$^BHD|FeGk<_DkdKP;7= zp75!;=6iSiBI{j!_CFSXpKQ0c-Llxa@>OI0r^)Q!nY&ouX5ZNUxcW)EN%`A*?K^6v zKRfq-{NZ7J!XkXnVCiSsqJ2V@o_=kLl5sr z*Uw*lJ~R2t6_rFehEF`cC7UZwX7PPvmAkWO+Uo!Rs&n4`=8^yZ$9Da#@JUA!CZr~G zHg=^7&v-HOz}Fq0zMU%VTCnBQ`*7I>e06`5XPgwAFzL=dcSeaH+xNTl^M@Vu*t+T~ z&xapeHAlnKZu%cS(zu14`I6Gw{CD$QK8l;~pSOFv{mE~S)o(=1S1>YYFk|hRvSLMZ z*U=4~EJkw2-IP6+UiisWrNw*fb7P3b6XuBP+&1>zo0^THHJ0)eDQNNhXyypiW_q9{ zE7In8{lkd}M*WnRIeE{F=l?WW9ky0Yh^zQx>-2(z>x}QJAMokTz3Q_3)8y3}v%Y&b zy0Aq>p8R%FC*I1|D@t{v)8#uiPQ?FRzyC>-?rh7lKP>g%8ozI}d%IC_9=otwz?q=F zFFOCjoDw!!ro=5;q9ntmIOkKv?vgo^re|K9vR`P<^n@*I_w;NyUoE}ORQp_iZO*kojB6;Qox!`rG z>84>GGTI-O@J?DBmsE14`-g@~*6Wk=?G6RE8n`bCp7BPqZb!2G=|}EAPo7+qeNyFq z#UqXS;=-o-pFjI0O_`+oKfbG+^l{$XIbLg+C7<8rUi9a)e%&z^!EI|I4y62de0SsW zfsF6XHd8C#al0=zmdT!RdTY0;f6p@qu4R6*Y3~zew0+ile&@(}%fhGbALRb){QrJZ z^WX3KO8fiw4f~Got@&Dgc#iRM)rz&RB6b$4ZvS`r_>10k4|%=a8~G-A=RYX7FHlVS zl(+Bi+`_Eq>lbY=S7&!~;G54<^ybgI=N-p&+V_8yWV3a+I(^&v@_@?`;?KIa$GiSF zw>$T}T_<|oPl4Ib?jB;czw2D_nf<42Wy8DU`;XT8KiT<&tv~P> z+2#6wwOdRsC}h}wukx$;9sK9B?QMfajl8SFH{7({(hjzP*-?bT+*n%^K;Bep?jQi)gQvD z-tCO^A>-~Z$9D?%d}+b z>7~xG78FyiouIvT&a+(~y2@V6;jAmln$*-|^t`KA z=SKap^m$9a%zUHpkxl0?^KprfOa<#qoh%(XJWSXmrYHw68OYfl&XkdCc;~>Sw6^wK z#O1x)|4s@1y7)@`e>tJ&c}osveS2elVCVDs?Liaf8o4auQ|PeR`22^B*aov_(La~? zT@GI1n&c__%kcldr|MnPWsW>L`z7c8!G@Rvx6RKyYn;2gV&A*J+1IOWeyp`wI92c! zQ(HibP=JO)X`{Ay>E`?#;oRyxM`88b_nl8MWQxCRC@4O$qCW6iZAC|0TXDtZw03K` z8_ij>&vzZ=TVP>pcj&>(!zQ`AIO-2T3z|-;9kcS$2KP@C3DnD+m0md#UNuP(2D;Q#O0)O!=H>%Y9cRP_4yfBl!s-!1-k zj6eAMtnHKP9=-cC^~f#nPTeT)vh&XSFFoGAWy^?ZjuDslD?t#xXlTNC%Ix!wEJZ0BvELnS8(@<%m z#}X6Invbf}C!I_S@S1r3dF0M4)0;VMP757u=efezL*iAbp25ox!hkK@kyz*SX zaK<~U1J>qlmay6Pe(K(eO_%yVPifHU%R1eV`SqqnB5Us1tvhz?m?-h^S=yY)vjLM2 zTv1b;=-{HXPfV0|r%qaqjIl}AJdeanmM6poeli{VtUGzha=}&`r|IfWxf3kojplE^ zmTujuBmGLcQJwX7u;n|M*$Vlpix)3m6S=po?^~orpOeSQ#r<|?{_@)#S(Y+G)zolL zlY8ej-Cwhr4o4L~KfsdSbN|ctl19?q(A*_kJzbpl_X zsKOqG7hLz>i!^R=?QB{4#Nexu(Tca-;T!8tFI~F-cc1OqZ@K+G>??F*?_9Wga?Ucf zPww*{Ec|GbIXV68oYXb>*-MX!y(nFLXG@JC_bcg_t9T1@3Z~34KX>S6_Kvzp@vU0t zAI_VcGrQ~aw5!iIEL=Q!*B!}kea|l>wtp7WOAGa84xXIcQ+av6cFO6fxax~+M=s=j z>%CrlbDC~Tzg?g4dzoF&wog8-lCf6uap(O1Q?`A+cWPSiYtGw=a`$)6|0%wAp0@eA zbVqmf&9|LP&%X$mK8_T`J#5|J?iQXyKwR5 ziG9re#hdic&XT@&inaT>VfjO*ClBn@ZtFi@<$F4EPYUbH$?TO69%%0RUbgwcukFPT zm+`K+cYEjim5zI^-E{H$e(^2u#oAXYyJ{_GUthZ@|GT4DOc`tMyF$6zPnplw?`ryd z{`Q%sh*|38@6~M6xuvI<2IgNru`R6c&8_3dyz`S>ugh4?t*fiJA6KO2+cq`kao(w*_{T*lCI_cZpL4!xovMi{ zOZ)EQ5yfvqP3G*JF8?O##P0i5dhGM>om^Qnvfj%`}$bV zWPY=#TcW>X!G{^<)p{2XDbyy}U60)}(ef>4-}_tcx0<@Pe!3-{pLTYUpVhPT$G*Hv zTk?$<}pV%Jw2T{!|Ke&W@}5)dxw5XpHFmru5+x4 zU+LEM!k^1Z&&=IFq00K$#(#!~KCgS=JAK`<;)g~~3z^*cWSq+No|aCJbBJEofAU-_ z&)eHaCp{IvSo?_a9M7*=cTQX~|6u<9#e??$U*4B2KfC%#OsPLi9q zKZa$|(bMlvrT&(A-uURRSH1mo{#WZa|9fq{j$L6-iE(cJzht#?`<~n7la6WUEqgBf zUT5w1pqhaEn#aC(zAcW9+*u_0S-h<9&y7IP!bc4eH!V*Unbh0K(Kq)zWs<9T5U02M zPgczK122QsSFT>2UR2a{ZChkTUgPb$67BpQAJy{qzYWvd{m83N*7DFV-q|nSzEu-8&1sW<6aSzWB$yCq~LAMUHJ=Q|*sS0jAv zMnYA%fr-U|HGO_gejWSv8K0SFEUw7%@`}hqj}{M$Lr0FV^euMa$m9Q(|Mb_cc`5LSjJJC*^J?o`k+$2Lzjj+^^d0|OR+-@0 z@;qsCWsX(**%Zae2cH!+nx{{+{lZ@2^5Adg+5-~3-)wA>KWqu#v`p)k!_;jzy=+*^ z`+Gi%A7?)q#h%SG_wCy~FMG`|ZQarBV|6@y-zV2T8OwA3X9XXZzV)hbGpm5zgQKSo z?2+@^F{@u*Hf29&(B+o2k9S!AyuJTVS3IsnF}>vzt)|2yNR-TRZf_kWXq+g$Z=w*7(Zu=0!LKOFykl@7hV zKYY`iww-f?=co9;{}H*X)lh6rl*jpdwL1{;YYY3mz1Doad$OWSe}ojrTVKZ=K5;;IMa85Gcb@K8{(b)!c5$YqBEP@AUAlMi z+g{pHgGrGo0pfgQR1UZ)53#{lTJ$Y$yuIxbkuw0>h;UFt(#X{T^@Y@ z!igtOSb`@{zTBp_>TC4P$LuU#2j95NkAikpY$xqS2}#~o^8$7 ziRwG^o)-Q4!W({R4v(VKzGcFK&MLf%?m3Ial*HTLOWDq0GHMKvE{Ir)~} zU-fy5V?@BUaz24Dg`OubI)nFowtb&^agi$zFQ1{!jpJ8~XBTd1Pd^p7^qeB+w|pGjN4~DF4^Y`ljzyI5A`q+MH*8hBge^vV2@Wp#JUVbBKJ$t*s=}z0r*6D|>>rct1ADnhJ z=k)#wU$t)kx_oWs$M37T0)oHSx1U+Z&UrqrspqP}uCK=x;^Z!D{@nR?O~l4YF?#N1 zvlp(-6>Dz(yC!05oBMV?U{fmq2f??^3gR zv**#1o_x04^6kdXV)nIbb5k})PSdk)m%Y9Hp@wU*x3+t~T;j_~sdMZ~o9^9=|tK0RTqvUf4_Qjx>X&N;XCw6@FdnZNb*xu+A`<(KT+$GBm~I&b&qIp5yZ z&8o3G|H?GG@=f6WucihU&I{k(uGq3*b+(|e@Z*Yk#}_vLE_70qIlge~)?VA))5JVx-g1M%4C+< za))#izIA*)KbM~8^nUugWszqa3$NmV(1^qdVF@h@7uc4)zu&E_qT82Lm@BKu7{%42 z%kMVz4$X44^}p5&b7R0FMPv~dBZ`g_5UVZc_CS}IDOt%Gh4$R zY41HNj@y>L5_#bNK-R8ig^9w2yS1|xE?ju2s=Dlb-LYEvx)+WNYHFvRo!DLemP_J< zGDA;qZ}Foag+iSy=~dMda^GfNTzv4trGlfyIkh+H_qcDfYnd|1X(0ns8yovan|g

z-~E2!wm|167rXZACkDdD3=(-dFE6)@J+WC!=Gm0z3mYoR&0jh-z7e*bp&_#2@L>~; z*5{Rf-uUhRy5;Rg_me5B6uI`l{iEq1cbZw-{b;c>sgc#%iHVWdP5VXP>P9?H z>izn#U+Bt7f!t0F4fZu()_2yP;rKC0aPDV==EWMZA53SQI$yBONPstjJE+ODZnedP zg;ria>lQF-7&ktw*?9E*>}%B$B6ADdizg}aEoBMX<&hkyVIrh@$)Q8{qm`J;VU5R0 z%)DiqnqC|-OIL*ReHJo&62Q1pg>~t{2${tkCppAUe%g^%XyS7(qhfUtpT%xnwCGy~wipxnW z90gxUC2LPwu3@<0pP@;^LBl$&>0*5IL^2*Xm2z@=p0N1$TE*cOlURS1`jJ^}Vs#bE z1w^0{{Cb)OY0A3tS0bZkTM z^U3>($pCRQmVf$i?tKwO`jA zQ_GXoeO>l%&ECoC%he=4o5;_wonHU)?X!<>BDwY*)jr~NbI~smr>KOMlqC%IwgpYM z%Vm#v9K5vb?w;q5diOk=)4D}v5{G|stoUjNFYa4T2cB^*;kOVJ=n}}wQ<{HHVH49* z-a`>fHh8?={%P$5I}hG~saucv&svmW9s13&ROUE){k8rJmm87~ZE>?%_c|x>^*hF8 zhYYu9{K)cA6jaIlq_(3ZOq!E@N!SbqhOlj?mIhtnK6&g{)jqo_E>&TUmx=)#uODc2 zRLe~_xqj)1h(R2OHdn-arf=IdHyjj7Y%LJ^`ZhO3P5p$gPtq3tHs_#+moFWyy_}KU zBp=JPOx4pluwJ^sM>G9Kq~bim+9;_XJny1qR($#|U$IQ+MaxtLalXly+UA5W2y+OE z{uESjDeBTsrr>qnx1?IO_@vl;V(LrTv*?lO21f3nl}#Ms3KP7hM1Rr0CR_T|5)bztF+m;>?8Cm*l=pw)n-x zdh2ka-;>X0xZMMs44&zHjG4Ce+NBvQ7MjgE z5LnlJhwJ{5`3X^XJzoB<-X&Kd6Q>IF**K6s-xCJ+L&SxZU)th-WI@w@Xj>>zD zF98X%fW8>aF?N`D_nieijvfiVi7~sVk zxS&n*;fM9h&wbe2Shy^NBU450!3>*l^~Ha#x6UomYBrTRAI4x1owJ+U{!xf$$cgP& z1dnasAjW7Mv#IeA`wFoOu9N>NX=qD*cHUW7Z+ZRkzypKb>dP%=o-|6+35wn_@ri8np^tl&wC4z% z+TX0p)7Lv=8r#R!*LP*-Kl^SUBsXbSj&Jl=?uAiTt6E=+?{2G0b@;{e=$&QTvQ>f0 z_dncfn((Xa+5DLocklmj=FeW{*7VMU+V{-E1^9h1K37sdI{VxA5 z@7wNTuu#9-aBdh|;fzgpj)e*Or5DY)vwQyhclZ6PZ4Un0xjE8sck|QYTbAizvVrbO zG5p!0Za)>SaxtDt4|UaO>2eMH$f7puP*1PYXUnc#zJfnGr}g~ozUE_W^s3vzebE8N zOcu`bA9hWX3cFTPSYmWGz|7q@VxxxOLT9fj-!@cLU79ra;36dj&&Kj|dCG!)1=064 znxif&@r0XQE4aLe&uXco)CDJHpNW>L5&~M=&gq_t_|RpuVSV$|phc~kZ{q{AqrYV7 zn4WI_@2welaMN?vZ$ToA)jzj#=cEP(EhufhzmnBIqsL5jTNr1g*R)N?E{ILsvv4tQ zlZeC?*M}2(BJLTLi<#fJ`9+*d#&fxanWi6YqGc5(a+BgTN_p)t`+Fvoj)vwjZl^ezu(9 zCpfJu_@>Tsqf95K2}W%Xo#r*a6p?-{RZ&thV{NeTn)R!nPpVjS`1PhS$^E4GSeyR>|>}mu79b zHfP0z=r?m`Ejd?Muy(1c%))bpFN(R}%$oDyt*>s62p7+@qB$p0Bd2*3-FA>qRNT2{ zVdwiVUkgfuwcXEe3fo`7%I;rde|k~v9*vpen$ZV13S>q&23*q!}8 z{}%7OXNxS0chucrU6QHETtjSo9cwJo|r5VX4|WRqexFPj$bFtT}pFj@A4*zZUDVZ?62IvId)7p9Olv zi$@8xRYgDeo9i2MGse5z%(aT+V_vqw}d{Hm2B{g#{HDcJ7 zpmXdM*SA}Ki#K)8x*@bWy2jq4bAwuku?A1&Quzxw&3tkBn2*UkKO@p+O@`lcnHEFbx4H@=Fz`=~#E-@Jn3 zPRH_p$9#NG#Kq-~Vyzk=~~-PDm_Q zSCcrk#FOKslFsYNg-TvYYaB#3PCAhPllzaNc68B{?~OAyazvjg`d%-Yz2VlBFN-#D z9P}!4YZ9uJcwy0dbzkh{=mS@e`SWwNtu1LN)sFsi`OWICl^*k49{!xS^w`104bLN& zaQwcvfun_Ekz+T*b|>v6csdSgobl$a#;1iaFVt`#EC+MsgHN7@A4FtWo@arC{enMKWppJ__bMkKYewI z{R()MH<6m$Wo5_RB=|4L)_7^`)lUl48W3tug%%W#* zR?2m|E+4ZzyJr6VrsXPLEfZP!XZj^B3%cRM7kt4Zx4BW(d*ZjvM-}HUSgP-GVUO6q zCA&@cv4x2-uHcP262`JC^#69z%bD9A9q3DE%2t==QD9|cQT=0`EOC;(S-?;zQFR{2 z@3nHN`O^0qo4Tjz{QW#X*Z!Yw@FG4nV0Gx)&5Pqay8rKtxHNOYmQVYxiq&sj z6}tAh1H;qKY^K+LQanE0`+WUl+h_C1($cEWMK>@>O)`|4#L4BfV9k+2rxqner=3h} z&3*0*>^c|Qo_KCJIrGj(O&_i1PfW>GD{Fcev1HcX)>`85${{JC`yqp!aa5k*@9pBB zO>z}vKbYUH&#|9z%H4V2q6IuPf0t-kzh(aukx=j8nb>-urB`0%!OxdriNa2g6E56v z&{J5VG4rxG|DyQDOWRRV!q98&O5{yPEHS3a!9gzimD~WuF*}9an%z^ z)KHd~b>iZ5xx>y0xsAtMcs)fIDb7<^WxMF2hWcemKcAkan#>0xtE8@Eg}%;NDQ+b2 z)yZVV(r@*l%~5N!{yv*p(R%j0t^%*b%V-9eol!h?#0 zsRx7?=$ueUzsDu7!JW-~Ad2PE!H*OFWuD9Tls={SVbK<6FOHUw4uwOZ+}&$WtXj{~ z?wY$w(Cox4zLNQulGeJh9S#Uu(e=$jqKnz{=mf@>_URug&Q$DE`k`aAYU+V2_B($! zL^`be8uur6OI?^H;~}<}5oNPdLk;{wtKu|S)Gtq~y&1Shcu`!m*j1N|&Lb1nu?KC^ zo%_uCuKJ0m3oXSKt=2pFD8*KIr%gZ4{3iujlQ%S$dIjAEl-|=GPG{`Xiq_^HhrD zsWgdsbDU-h6id&_e3j$sx6E+yvrqOG75OJoT15wQ*&gRVYKCu7qMnK|-XB z&kfFrA&N;s3cZ>)n^g}PF8idXZdA#)W19Q2AQy=P5zX0ub2lmZoM24VapUOOkg-~W zLm+IzN1X=?ni#hk`P5Am&xV9mc9xp+ubz{Cp3k_l{?C1%w^^bK!d6FJt;!Q<;LTZ@ z!|+U{V}iYCpQ}V;aL+A~I-OO^Yu;b>+fD)vqKi_VZfj>< zA=mxDRNqzNHruW}6$rjB{BGaN5;Q1NrF9UH@OaOpLxS()?3r-Np>f3yK`# z{c9P*e~OE2syLg#8dkA%y-eoc9pTn*y&fNpu?V%Fe|6QhNYzTIcdj0v;Z>HNl+m!me;KhZ5={Jt;xxm$am_xDd zrI~GXuH}EGK6z^^-A`Mhu2!*L%q(1^W)SiH_D!b>*L#B62RsblT17=PCbP`r`TR9^ z_m5}a?rJC&_8#0?$F^69Px0l3RhB2K!`7sDR~x^&plGAIF>2wa$|c{HvR1f zp*DEpzIVY5Tz*Ais~Qq{Kg=`Wc7OptI6Xa~ zU-Zs>u}F`=K0Sl51erzyG525Ff{#gX+z4*rnX>0!!3>rqn|6dXU5iK!2vca`nh_;g zF>RXvHCMJ9PMiz4HERSU0(M>cC&P#dY0TC+=|+UtxTV%JaYE7uDs54IOAaQm8(5Frk#wL_b+DJ)Azn z7f&|cYkOwC{rsQYVnt6rC@$J4@%1Y&Xe8Lmx-#YUVbu@2lb(7Bb*e00tbFC#HNhe$ zyVuGKmM@>J>OE~!^6xS+{Wu?;c@Ms<6xNB_G-a;;n}nR4HNP}hUn@=y6+B?zXrLMY z@TleEM`yXb9=NnQM;u^o5o#%{Igp*08_ir_ds0;=h;3zT&Mkfh2T2~iTV7kz3zw>< zs2s{_S==}w-$&nd!nKqG9S5?s68`n@{jqkm;(Gj2==IC_I^Eww>u%5ETD{V1f>w*e z$piORuK3B*kYIRoRYiS^VhU?iD331dO8=;crYnr`o3mIw*!`uZuuC;gCE>(UpXaUWpFTDY>8O{ub!0bhHaNNn_RhKY-M+J^54ZWCw+v9M0^)= z_zJput2lXUxD=asCEo~AR`H6q5B0A0c$%HwJHbI>v98~}3sPZcI9(4kbss%E!F1M? zZ6(Wf@4tQfc;dv1K?fWHg#wQ)+H~X`sO0;lo8Xt;a5_CXsQ%{PbSuU^uNb~EpIsYs z-KtxwR5$~&L!vOH9JgUSoPSV zq~uK6-Cw>wJ^dH&TsjqEoqh2|1}H>(kMv2^HVc`*Qn{P)A` z(@&cquXQ`WeT~rZxBtrWeBSPf&FtSdZ^$;BS<-bi`&?>8#e~&y)x6x|dT9v@`YS(+ zPgt|^;faOKp$$=MpQi?$u3N!q9OmP>DK&e=Bm1LUKE5duo22V&V#)DvQn@L=jNQ4p zyAHfr@t}0|wB+Dfhxh)~>1=mgK9S97hlSRwY^~2y!k>b>*02gPsj5bZuV&Qzo6DQz zp!!sCanR}5ABJm>{;5hnyCvt>C6k(q>04LG@*c=c-B#O>H1W_ReoduJ=YtdXEi<)< zV`?@MbNA?BIUd2ocTB5A?8uq}TcsAtZu#U`6%oy~DZ^eN;MDwG{ZZY*pY*L<4?bA9 zQE`6ewM+e$dKO~Q8(j7>6}K+jtaz-IcPo#w#Z2vjnxrn<$C`3-FH)7}A9gg-o|llB zY^yExYUb>Y3r|{BPMkEMK=9Oq1rv>&qBQ+Pih5FuuCzLDIdNK{E*^zV8q%HaAsn9Lk&T3^I z&B}f)At||R=T6q^x&qxtC8ebwirep>Evg;9`OTcCE=qz@(z9pInUPRa)6>@Gw#GDN zb7ay+iJ3laHH!WwQb##kKg9G~7C$pIGU}N!$#}2r(&zFrGD`w9HoTc5n&Cg$=h}pm zQ!e-9)g7;vkd;)N>0@MP$LDZ@lW~Ded(-Ua|9;mUw7&n2Yb|qzT}cgIrqP56hA+ker-*el~sE2uP-lOEngFLzRz#wyNjAD zHUzFJ3DMH?l|HSvK0@W<8iR;KZ{+4Zx^ctrUfID`lWpf8rNrvS*L{#Pa(?{h@G@SP zDJt2{b3U@TyB*Kqv0RgztC|wC#K7aNy3)_Lc>-S36gO_Q(-N(-j@4B6=(@zg8^-#! z{B?GY;Zx6w`3+k%RxYcYc}^xvOO~7SQk#X@yoH)#>C-M$EL{=YIEn96#FBy;o7z@P z@G$Aqd|+89=f6C|*JWnE%VcM7Pd|$Y?x?nzA9Q$k99g5L``E_F=;X>yL$PaSo0jaG zCMGgZ+2ZNb6HB<3++mu>>R;`l$zeTBf>o+8LT=ie%YmnPH@`Y@qfG6yp5*ytjn_s~ z&KIX7HRb-b3}S6w>|}C0`AVxOZ>I+HOGCcir509G)3|&i9I`E(yiMGaReI86zV-KC ze9mULt&uk(bHm%6)8bP?O+l6G&A{`|tA9Bu`KI4nzT@8Nc`92KM3Q2%4>>JZtD3Ca z8+2*CDX*8H@T9`EE2lQet?hJM%Y0r;k&(ex+>O^G=b+Rm8yx7FdY?9w{Zyk5F z$%-?57EL)+8apLOgo|~})+3J#?^Rrm_EgLMaWmHb*@uPB3?Rj)pKiD&)aL@eHnh;@ zyz*49sq^e7dI<2fpG@1VBIN1j*5J=46N3E%cqP&$RK8Sr^DS!X(H}exIMlDW? zK`TYJZY^y{&C2SkuyHe+tvt~qVau_vHm_d2l8}>IwsGS`AGPKwlL{9&(nJU+_!# zBb&eU7B+F_Tb~zue7rn~hpW+rn}btehsWxI)^E`-oP;(BU9W0!3M}hMy=iWCr26^U zW5wJ%yb^j+LZqd#7V#{L-S+KE(bYedTR8te`eV^#fA^P2_pDbt7cQPFx;608wa-s^ zoi-#Jon~I{_wnJ?(<=n!YLyPj+x}2d?@`p!d4BEs=5ej zZ;!gm)h-(+_b18vkiM{(oMpIN9eylHwALA3jEA zu5OoDPwjH^4EKDkwECxzd6Hq>#!aWZ7J3AlE#zAC&}hvoRmb{M%vV_gN*LF!dKAd` zV!x)$%|4Z@%gk)+f8Bh$HMjF)wCl_)vC>z@kJ(Fih1@F8jqYUMyKGfLM##i9Ei2ct zFAMEzna_0S)n5;lO^UI$E7mMe+IXV;|1bLmnNm-Sc9z*n^o!Q}D8FFbxq*p;<;za##-#)yZm}qs>CNlIPJ2$sZ8kfhc7T4=9 zH+i+hl%)xIc5%e`s_c~MRpe0G^kYi1E=Q8@ck||hzHuj}Unp`7(QOU8W|%4E$5P~B6Raw+?xNal zCA&UIZ?DfP4!C_~*?G3DMThR6nRMCF+rtFpF>7n8VTZ}F>0)_y2_nGmX1u3t4bCITwg58m$lYz z>V&Pl3qx*SS!VWEj`@n1-w*Q_GtDmBuHF9Ne{GrZ)n#T|UmE)?YD_XwzF|E{FJ#9` zwhYr4=T47|45!kC!h)?v{mF&@rq@@t9}Q`o#HO)HuuWKb%1TboSpCIasxGk(Y$7Mm zIK@3*zwVw&kdAS7>T?^;0njVlkap|NZ-=B-^l<#}IdB%;WrHZH3-W{9V(zw5> ze$tkZW2+AOhaH;3UuEcNG(9}*$Lp2qOG;+ee=nHm;;9y=x4Gkl)2!Em9{W`_jb;NT;?lgTUYw-m%XID zZh_VXuQfZOjSlpuS!}~()utF!(W0uPvoe5r3FPJjMu6Y)~xT7i0;{N&y(FrDu zzLzvTRb70a98Ti%yu4v*$g<0ocf{;}7B%h+ne_hN{0ClE&3YXxUdsF`+VN<9h@+FT zeiDOu?8ExKwg-%4WJyiZ3%bI3YEsvVwd~7Y zA9$G-bGmDBgs%qEqKoRkF5g()lvlCC=$8|N33JaYp+(EWEtXdGdTZIZi`?4wC_i|% zowGuz@VgA<^ev$hSKJQVVq1}$b@{!}U%vOJLYIGIQ!tWwvihoq*RC%i%nmDF8A^H2 z(aT!UtQD|v#(Kp#p6y{wvqE_1Y6eYPRke<(Lu%Qg20f1_BJB00pV`73-Gx<*Qs){8 zEz(&0TFOYODW+4fzb)+I%&7tz-l{AokNm!sk}e=36{# z{@r7`a!jCc{?*%BRGweRR|FKSI%Yj>=$NxF?E)L%t zR=VStNo}q0n#;9oe%z=zC1{ct#<+fo_Y>C>FWoxA97VJOS86qEJSQp^8khgJ@^Q47 ztFzC#aE3(ztS9{$jd_*6y;>_ZKWN30l~M<}5A7;n&2(+~)AjlKf3No**z#_|Wd@7u zR~J`W*M+%!&*jUSed~)P(~8?1#j85Ac;}eeU0%_ymG@Hofw@oK_SxI_{qx=TW9j;u zSC#)yPQSls|Ni>BTlMR|X4m|cuHW|7hELWira1fS^ZWnKZJTdh_lHG)e(lnye`|jq z`%q!?dY!Co6_4knAD_bayJ@8C@c#K|`o2R44xGCm@teKb@!Ol5yHj^Hv-5v^_I&?_ z)Ln<&`;<=4KR5IF)6LI+9Gm~|XWQ%h|4#p3l=-W#kMG{EU*Wck~ddw;Q%w%IhbsZG=Hb`5bfFG*nKjn!DpeY7jI>S*Dg zwq3SYmy5jmyNBy)73b^I@!w;=@9HgGe|q=FyYr^0uBlF|-CdLP{TuK6wq)m6$*+Gm zzl-|4=Gro|tt*|9o;K{CYq0!lrpuOT|AJSaw+ZK4b^2-2@xHk+|DLM;`x0H>c>3wH zcXOjkLLBC z|2Og-Oz22hA-w(g;c~n0R%N^Ae%!tN=I-|l@8fr#*}m_u>>EDz$&bq=Z`U(88q{>X z+I8;DnPjWoRWqu-G%P;ux1l&=#?qs%I$_dr>!+`;{T!X5ZRZ=C$G?A9P3Onm<%~ZL ziT`ieC(C^1>^TDy8?W$k{;ov{Tcc*qpS?K7Oy=Eti5n*$9kQwazWqLHPr=Iddp{ok z^5v`Whw=~CuK(}p{AgWLR%&Q#do+CiH{LmZ=Jq>3=A{ZuQBtey3yuXR1VtMKtI#y$RXp1jy7{G_PTYpK#C6~pf-(;iNYz4O0f(q;X< zKL7g|!=_KaS^vS{`nr9`*G8K&a`E? zPOII!CaTQ*|K#rAYZy)^q{_iB=<$le4`gwLD(WM<;cQr1y zZhhef!J{4*Tv=R7VjaX>E|iowuHL|@eaFW!pTpq7vhy|nuN4MtExU2WZ0pON7s?LJ z$$M6|Dzxs{=kwJQ1zl1i)kI#Ur#hnzFJd{yem-Za;x$U^;Daz zqp$m*ta_rT@TQapZ?9ezoz!U&)-AgIwzG+lV`%85mnzfi{slWN)Dzk$A=b_6YWVSC zZ#IL7w`*0^?+c}}^$HF;Vvm2ZhwuFLuX={ASX*2B$t6KQ{xHVmnP;!ouGqWpR5eG7 z)$YI)*8bzi&zwnSl6Cy^k6~xdw0H01=IDust)9AdYv{W>`y#9Nx3;oQTFDY#_tm>3 zA+wON`>2zS*wL)5lYCS@RM=GPy?4vly1h+PtUF^>*U6MemyXV7DtP^jx2Dd%{QaGz ziWCu+MsM-^iZXGJkDLsCeXFjlY}V7GtZK*JCvA-S`OyAvdWmm(&VNTW^<(B=uHL$( zwMe7zr?`0GZrci*xJ@^C(l$$OzUiYR_x$g7J>y-~{PHoi{q6knSLWFkybu7z)WwUj zdp$b&wj-1gNx^8~l#dz0%i>_;r9b8lBdt2@K{@JoIH!rM;nGS9hq&8@+ z(%rdat;NdOg-NS_9xT4*=)3%~kI$Wlf3@E~EU@5Yb3Sv%=dvKr#C64L_SVL}8zWq% zsoKQJzi7LEsc7es>ixTX(!DkSnUt!~>BbHj3Iq`Tfzvfi0h&5r%F6-89GB7{BGyJ@eaJ@R=Qa1akKenl69s1`ua;ZuQo4R>9SZ* z=%Yoai;U-_on>FSj{D7dbnGkt4f8UF-FF)mIQIOw6uGXpVC%Hwi`)4n?rv=l)}NDB znkqT@=8-R7M1CLpHrYeP?924)k5wmjY~EntuOsH@a3IBKqR%IRm*%jw0BRAjv5NE0 z7ykQdzVq8bQNKAhy{o2W-+9_?|2tE6{_E?#%#Ev7m6U!@+IZ{7P3!!}Wz`!ZbX=4Y z)zsA`V*dQ}pD}mlv*PuC1-IMn3S8Z<;`v5K=Gn2Y*=|2hpEr1! ztME1FZRllB)2Q{UWTXBrmF5X)ZoZXw`@UyZ^JTNGFD=jQXWLk-eRze`^DV8c^~SPS zp3A-3;=5ngX!cndi;9ld*X?eY+?CF+oA=SWJR)L7z24WFzrE&_&(+0m$zE%!o9;Y! z+e+X0YOM88EIiD^%NwX6vW;aqgZ<7A--?-+J4nyjw$e8@D|q&;FV;?9A8gT@mv7RrYX3e) zb5n`P+O$6l$~=VrugY)eESZ$InYCMEW01~+U=bIVOIechlOCmM9h`C3Gw}M4#Bg=1 z4~a($*dM&v87`pO`Shr3n|t}khhONR)s4Yy?@)`YnPCKP0}wDZ5aeEW9$s#PvZi8Xfi!ToZR z&ZfP1_wwlLuZpLQ%Jc3&`7rVEidm;x<^KxozI$<=>wf(!-_~AUW|n*7!7aC-+^oy# z33_IXGtd5+w~c8>sKM`$Gyi2?S~n`hig7<@R=l&RDRk0|Q%^14e*J7Q{VD5u`T0qQ z6t!hasw-xko^)=>$t9Ql1odv zwJ3D)@b*vgTKeMEyNB1-&i+yJuk`(n?C)_laqro-2fZwrb?;u?x<9X`%h*=!DZ87r zG0AN9M|P&3cel>=$ujf2k#Q1IOiY$w6>dY*e zRkX52=k$tIU28OWczLg0(miAU@ap-!ZKt0ed;2Ni#PgGBMk$vvKkbyU-CO%)Q{=Zd zH@go%Jo5Z=px*SGyYuBIdd$dBak7!S|KVSgM$xaM#%Io*6X-u`S^CPvZvOnFg$?Uo zM(BhsTP7#e?|L%Ln59{fB_T0!UeU^y+ixG9e8Y2CdH0E@#Y=)cjb@shKJC4y=9}oI zn_qT)+IREF`i$9i+gIcsy}!(C>%q*~x4uNqYVO_ty79S{!S2rZE4(q`8PcAAcU`%E z-(TUvL3jDuA7{?btNbYwes6crs=PNolTRLbes2G{e*Hfk*W>;x>xglezrT}K^{cSj z?GIz{ZI4yHxkf9pR6_6GIVb-n!`y53tuN8b=HHFW=5sUu*m!HkGOjG?-c`34w)*D& zl3lcUeZ8Ekk>Y}d;cDALCjMG&<*`U`+NPd19gZobd%x+vkY-hTJx_Go_t^hycAdTI zEIzOI^wo&#zrL@17CL{KVf|st|CiHOWwONH`LH+kmQL;FPuou||F`?&&9_&!{)<|D z|7)hJtV!dt`&QDoR~OFNBE{{*%pNS_ViFrYeR%-4`tH)*=GPX7?yYH5NWb@EQ{n_g zuQ|8nGxP;=vo7b)jEekNFZCs+XUV^_<^P}VG<}w4w`<;2T_@f1zbZC`&W}6)<8SA^ z)X7EW)oFKk=T8a~Gk;}0ZR@p;CA$JMX6;(HS9<;R_}e=B{V$2l-*Pv4>+Q13x$WDM z&s?05wmSXQ?Le{CTjg8VzJ71I_*ZrBOR3K}rS;mYz7=Y3^k2R4hS&U+%Z_h3YO|`o zZZ~(v=Jaio%QshEjrjleTHb#wn^&Jn(9 zW4E?+iEnP!<;}^f!eTDH{b7GMXMMGv%$4V!se$*e*CpKnKYZ)d(G z-B?(9_tqM}v+uU9s$OdoxT^PIshix?f0Ip<_pDBmTdfxCl9#nIr=Xqt#SHT`KhN?t zg)aE-dHq-zyGH04<;-ck?fcVZna_T^mG(ZISzd(Ugk~he8r5js+-))k6pw9t^Lxv) zql(9}?qB_Kb&u-VCaq@|4V42NF6l@=OR9|)*Hq4CQaj~)@A#gD_cuRfFtglKDs9U7 zH}XE)*W8f*>t?T=yLJZK+HXIOeVMh)?5Oj8KH+DV6P~P*kFHO+vSP_@(eQNZpIaZj zFo{->GGCZ>EoXLPSfp^z+4Y}R#;q>A{Ayiy#taSRu1O#7?zoU=Bq;SiyJPFY*uCoA zhuS_x8k-b}g@;a7Wa?T*~DYGv9_ zi(D^d{|(}I@;$Nj|JUoUFWa9kU(M{3Jg@g>`2Xwr^RE6ryT< z!*nF8uJ`ZFQ=hK5>@-Wi_Vgml94YBpJsoLN?poY4*{W%=_I&=~gPZDn!Y*BTHdDhi zclp`6?N!giKEIDNpPIEh?0U=FGe3`&t`ZH%Gi#Uny;jsApJn+XjkW9>c)v##re!xQ z`XuPe=_xcpW#P5wSN(I}u3RE&A{`l+^{Y-T#pi^^jqLNYCuIMeVp=OJkeQ>tS?SRx z?HzBp(p+P<>zRLBKCfRfWip$>DYZfkBfZ^DkykvAB)^!U*?la=lshRmHgl``$wdL$ z(ZZo2hW)er7d#Vw{cM5*<0jTmUObJ5cKOG3P7K~OgJm`2r0{Fk_sV@SeHZ+7zLrL5 zhavM3^DB{O)eShqa%Op1&02jma1zU@Mf(`%aEVM%X`B<`HdUnS#1q4b$G0rBU-fm1 z7uT}3$~b$ySMNg^3%9OkJ=?b6<_0e=xz((JlM8N&G6a3}SnJ~{)G^&wx?$D4{@jT8 z)w#j4$yfjPzTVh&o* z=$bOSDDc(M9bXr1`PI8^m*p&t>wfjU=da{1yLxoH+0NG@Jgb{`&6KFu=JPTRe4OvR zXMd=?&FWc4(_Wedv*aIDz18=lPJ3Ohv0l3E@eqM6x8A=kaD06^XWIR+n44+eqkGS% zJzh~aC*=5&Rj*!OVO=$6*)PNBr9mt2Oj#W!`~2(u*A2;cw;ua>v8Vf5L|N&%Ene~V z*Izk?Z2oxl&D|@yQ|hEo=f+4!b>16g#F+5sEw{HW^iN%T$Tzrn2lwmi zGnbqVnXNxbMPa3Ci7ES2nG*aqtPBHmQ>pb<&ceLHYdqPol7px zyU8@^XsmU_?oCH|YgcMdTlTtj(Xy<+GfqpFUd&y`8&ewpD^uggoV6~T)oX26$2|H~ znzz~X*Y&yeYrpOHclP?r&K|IRHUG!Vj$hlkC%ihCxtVump>VEi@9etG|IZ!~+Z47y zXV&bd%V9BF!WOlsi_ZSL?C78KMFAOR3+K7=KDaJgyoG7!?B1(YSG%hMZrmt8`%ZIG z-@0yJ#;0XhnaoZvj{083=k)l8nAgu`4yDElw>7%fFj+2~5`X+=O;{G|tnE?$v!=%1 zy|QhR&=#i9uMK6^3SS~$|H*vSkk2ZxooP+)qVs95^rBy`-Fm4wf7?HkYw4S}Zk<`T zF*GQv;j&odflTj-r>s0@$4t4C$~8@((ueK2m-d5GVlPkHJ>_^dr;h8VisVt>8)?z( zC0j$d+zlg72Nzg&%rgDdb(*23!CmCVsVSOICcijo;up;o$v>g^SGI!A!!pGwpG-Go z>#J($oJlUK@|Ydd;itail!(FNslA2SaVH;d$#`+nByRQ0h;K4;dj7T&cE z{k(>KrP`?-n~p|Bhkj-`p^+z)Hnr)=F{g>^j!4Z5>Wo;|a6PNxqvcf}#T3QJ#9JYb zYxbrTgs*0inVj&hj6KcX+5#Rnweq$z>uxQZdf01f*)&(KpDOc$I2$6?Iox`p zB)Y-&t^d&;%V~$>ye6FLw&*$;Reda0De|n_nXp4apOgX=rd-=FU1)1nTj&I<+!?Zd zlTxKEH^v0(W~OkMMlare$l>#cKz&upwb0X zOy?#X&0jgq>){rgSRLc8>bozsr=L#ZSg-d|vw%_GUKTTHde>XoXy?eWh%J&V_$#J-SCCpFOFI;(@bz?NHkv*KPm zzW#OCvsSM6`>Gh8$l&almRA*pQ6@o^8_nWY$*idAb1pr&Zh6Q7w|z?#qxQT=nwAij zZJB(1|Hr@H_aU|4 z-TJ(ffBo8a>8i}Rg0jm$@ADK~Kek|*6SMfgNXG0^zwJWv!%t_H?l086v7l^oZ|Sak z0bV%_i%b~TY++b_`{BD;tb*lAow}P2eEK0PCTC~x>BEf3vm%z2--g>q%@I1*pWD>?Y*uBrA?t^3e_-`FDMrUmEb6DuFq-uUgFD3G(r zXPZVtoYK#Vi7%z6cd8sN$?&`4V9v1n{G4M#Z`!yT&aKm%yr;ZfeWOWsr|OXnZS^bI z?=RGIvi*IKKSlJ-QWn0-&h7iZ^D)d_dTNH8r0a{@jj0mNt6vMBtg~@1N^4wtG;-0! z-^Z+XGQHR||8BZpvc4rxLa4}uWqE-!d>c)u`t_X`>8hPzXnCx|a{&6ybb zY?;^&^SmkV^&~R==hQNvKOi8Wka+IlHtiJqdCEqc9o}8j$-c=i7{gbyFScS2-!8xT zYCr*Ub|SIz^gI|7W7m)R;79-bnR0ByxIf0o2^;Mz>6(`_<0be0l*H0G6^h-*b6Ol^ z?MnHI{|ijqr?9f5ZNc%z+^W{C{d${zWk}i1Q+%*xapjqgTJ{OwWe$j$i0yBZP&WR@ zJugPq-G3RU)vhezt9R^OuH6q_9en%=yUxAe9Xr{LIpnO>j`lt?-2C~5q?I-g76QZM(l^Q(@FF$;H*h<2|_K#8K`pGqk#eHs%m;G4t|Mr)fOzo?U zL1ta2nHpWTnZ-SmJ2Te7$H=C#p2{>qe{fTU^RjtW10!_lZ~w&=!|c9T}`@a)QCoAsLV8u{|&=M}48 zbhnN;X=&EsUN0=vbGGM6)ZPC}?%YZ{)_p$d#T75rL}SqwE%B|!a}SF}K6v>3zpyud z{zc0#l}&Y=jZ<&U+WGq`^NsKSIuEY>{jof9r(vA*;ke~9Ul#C`X0~4VIC;*mKi4~+ za@`1j_VdB~{}&m|YVCr=%%5%zy>hj3NrJv?{=TmACGzzPW}lq@pZn8M>y}_ip|HjU zi@9T8r|G6PED*l`>$B8$y^rtsCH_~)^y|CiZ)i@9Rj{#Vo++X~$Fl#+q1C1e-#;9; zS6aCA=FY<$EyCWS9q!>AZ*HAGAwSOkyFc7Zt;tcC0Y9C-b-L#y-{hkHvfkoPCa{mimh9?_er?;MYVN?Kkr?= z^*jBOSXj8+12I_*WoyeHXaBdU+V+*}p8o4?zvN57ErHJdHzwESd;N57KYyw8mq}W8 z_me9s{QthK?>rX%Qp&RBnc?Fz>NDomIvJRpI%poRD*ndZf0^f=-^^*p^OabuKU$sL z`ls@NGg?(gR=Q$K#LZr-`rYQ`Pqf31Se z>l>|?FYbA>B>2M2Y6Is3nd%oMpM2ofzjkW=kKgtEyR7^#-0$A{I@3C_yy%7W^MaB+ z4p-J(G&Vo~Rler_0~PJOra0lxtFyP7t=sl;%i+C#s;lbunRNZxzIf3$9i`u4N&nWW zw4D7kVc+Nb|NQ^|;;(ELy>QWR%Iekoe!i;D7thF3uT7TRD%`2{z+|P!{4<|kyw?|= zy}vKP(D2S(-+e0{Xo{YwnQ`c$&!ZPFT+Lrb*v)%*aq9d@GW8A)h7aXmI2PWD+-2`+ zH`{Gf>x;MVk7*q`to?q|pYHD;#IH-dy4@;NqHL|NDG@ zV$YF}Uw?8`)Eo&4T=cpBq@K;e)J==NRRp#O)l_u1+y4{nJCYo7)OY#@{;T^=o%*6| z)XCFyy!FlYz(YaV&(mhNud+AYaj9g*x#esBU60ScTx|2;r2?1J+h~!N(&fe4s%MYEnT6SLN{ZNKskB_(w)05`Q#U;_nK|?SQ`JNEa};9dEYB}tn0~i!(ix+# zAJlHnX}Qm2R<1sAs_VNycliS^@xOTY>EWyQ?z88orD{6r?D23Ad+|m_VP5~lzaRZ8 z4GbOo7bLuWwc=^z^HZvKRIM9jtuL&V@~WCvZNK}(q1*Y%m$pRxytvbHQNLW$#wf{G zybB}N*wp;D`d;^y`OHhD+P}6t{>n|8eyu~_S2#NAz{B?nPd*7oMm{-N9e%>+zFU9# zi`VZDrf%Q7&8S~4+WBnu4%xCfYR13zdcNELx!7y-_mg7YU%ts7%)LHQNc+s)U443% z5zhFt`l51ibYjht?FwMDZ?O{`Q;0ycp zm+#&`Il%qAaQ+S7?=Ki+cS)-5J2QDDYg2Oj!hk3L{P*$y6teDr19oNd*3G|<e{L7v}!;8_W1Rir_ZdNt#f}@>;2qk2ejiC zy>2~!ar1wMI#;!=Hm}BF)eH9!uTq^xiZfGE&vDdj7tJe{aO*<^BIMKVr7ba`Pw2 zKZ@fI)&Egt+ADiPZRVrJzjt(r74800k*xH1dVQNf{gHRf%U{|Tilpb;@11Kc@KvU> z)pU(3_o)E>Bu|BOqx>84s(~@-!U1#rP32GV7O$_fGn+lV_j9Ol=!N-BXMa_%J+)%~ z_ukJj|C;Y7%Ymo)yF`(3bl%@J>< zjo;rn+Z}x|^ZG}5>*NkFA`0DBVp}xedTn6>fJ9y z51rX-dvfwedEx5!vAg1A4hni2{I6hIE8%SW%}7~a*8LiDN%6Fq-2V@}`QbaKcrnYb zDvh0QO_%-+`xSR~hj#Se^+j)QeYyEyLD6su_T(Jso+?ZcgL35&Wp zb1wJ)NSWh4{hpe+spF%C{TCBD4!yGIUv76K)pz6fqrpG==dN7p8@t2f-8cVvjj`3X`IO4GCxB7UcO_xLry3E zdwZ8H)*of8x~tzX#I2tB$cO)0wS3|4TkJpXl)YguTR8RTaxI-%=a#?Rv$T#c?^)WK zt1|bzYHs+%E(1ek;!TOt1JFkjCaRTe^ z$q}m@A~`m1nx#0e^r8H#gGOZnXCED!)Aiux{j*))?V4g9d|X_~)}Ck@rjqz7spJXA zLr3Yj#D=ChORCQcrX9~0n%{fKdx4wxnwbsz>gLS1_l$a{lyyx)Mq<(a0EJJhD}%C6 z|7d)9Q&eb~9#ie^6Mb*kwlCy+Bq8Otr&#>vHkXGdMVhYr@0p-#uxWCi^7=`|mKSvN z);N4PP#2~jt58$h_wfAulvLBhuV&43QaIv${M?2F-UpLQtjbvff&@WIwr=Z*=9oOg zQ?ay_C-c}p;S~)pi?!}Lq-T}bK8Oe0sxqR_;4m};uxx7D@OT<)2&JplzJ9zVO z)O)=pT`gB0>pq+?jYDVc#tl~I&RsgKv(s1lo4YfYg85u$-Q&N-)-8`(wK(_k)}+1D zd(yJlimJPp?z|Tgo4l22+S!;RSM*n$t;^R4zP$0IR;!DUs`$#6e|NNfo3wg~F)xo{ zlIDbyu7X05ADm1kI=*yDEbL(sctNpc*mUp)9GpY@oUp-{DI?neX=uS)SCoAt)Zn7KBn(chcaMjE< zKIAZ+A|AUqRm#4emC@POY5*ZcIP= z=O!K$bLd|F;&Yjy$*uE6dHc38+Rdr-C~+5mn*4VM+xJZixPHvBy1U%1!suV}o1K+m zVc{BW#tOQ|B|mhY&5JvfC+(!5wfvI;hnW(Svtq+MT)Pv`Zns>JZ@ujQQYQIQYQDt?@x7q z@R#qx8#yAr2)zqvoU13@|LpLWTb>cuU~2(pK5Gy-X*zdGvi`J#wHbetk6C+K^({Po!v0WIkRxvOg(#8@xz$R@mRto?CB@65TmDSyfq zoLMyE(EDu<&%Hcya*~4g^X|DVP3sejsxGkoj_hdRd?G57x*@vu0K<=E8z29v z)L9Mcf9h}v6&jT=Uvz4mvL@l%jEP?vG`$;xm+$XVaEqMQ#39DH{Vv~2i%q&-LEg%G zk_*-MvVSwI$k10?-oD{B+YJttO&)fvzW#wdt2yHApUsQ=KOuug=HanJ7HPo$^ zFWw)RoS=-d_)qo7LpvyXZ`CbknMXSIT&fn;YCV zGhDOky6k=;heYo>r|lgc!P0<@sher|fF@|5X1G`H*gF2$X03_RMqj2b;*;KX)pg4UCA;>p!fS~;4y?#u{mSj)8Q)83 zcc#6~Dax(d7jxy8-5M$1&Pl=ttq)zWw2mxqTl&tA<>Z!g0v;xn7gOyw$tlQ&-1z(~ zTEy0>Usq_6WvkjW&GbV{u1)LhT`HYo>u^2%eoDz&mj%1qSKPfIsN-T3p-gCn9ke1F=Ar@OcpRx{LzlAR~gYx7JXEPVC zm>yf7SP(B~t@6mQkujfBm3QUV)aK~qvI+T&yX@XFpHhFg!0hpoZI`06Ro`YUYrpm8 zR~jR?6z^Us-ZfITS{c=4iNc#z8Zx5N`xTVWEv+nhnVS+l>Ehnynm6_yF_nMUSQj(t zoR3QT>34^O{Q~BA8hlS+o3GxH`bL4fOPY=82fMX%kug;bC0Daqx|6}<2FCL&l+?Eg>3i(5SIXMn*bk{=Iw@Ef@5S>(XQA-o{WFZGOtKS{A6;bT-_JFhZ#-lF0=FKZ8+S2yq@7`o-=EO z4Z9rs(V3cJVaGiqSWo?3Jms(Li(2cC>D?&}Q#~e4SQc>Z?$e7)zia)ilq$V&Y>~o4 zyE>ldQR|MKG>?wy@IN+l*AHO^wzhAZ61W$vwqyx(6h14>GjkQ&hFyA#I-m9`Y{{FK zJ40Xh*`r;SyH74G3Vw4XVaipu6}>0+wz`%Z8-{E?cE`_UK^otF){|X)F5FU14og$E zUfp6cYb^L909n-x1dmTQ*ojNEj3#$>H;9&J~Cu=K0FOZw!&!vDs-z;c6Tp3oxR_Ge22 zG#-V?c5s(h+EjLXM5W}Evrqc3a5<$Q+$=Qu>EqKcjy$`c{Pb7tvdNKi^kr8!c1=o& z(DE)bSByL^748|c-#zW4VbBKg^aZnRuD+c&&MrAFf3+F=3TJ-0rM2*} z`pwd3CC*Xy>v9DBH4iR6xqD6Q4iB4Mhvt1=|47c#>C__yy;&Psyms)4&yNTeR5I7w zyuqq(=i=G2$Il6T{T-oE_ITpsV=_X?HtpTovPw9!5sZB5z#f4R;*iZ08QWdQ!}^C1@UFfAh?4 zdo`oVxfR!rUfB3pC0A~f*=#|U+J{#1)GlV8@w+|3X!R3sDGlC7f34O#UbT%(`5dsgcfn@S2RTpY6e@?7wY_rR>v=`VWLOq#JHe6!vgtFJ1;3dX-{VPCcE(QiPb5LI&m5*C#}mRy0erzcNm;| z%PkHX{C{uRmik5^azFd2oCmY6Uk; z>+9}$r#5^)8tjqG9yuvQ$4pp5Oh;^k+N3Go{s#BupWizYduLJefrpz@oc2Bu6`ypo zN3Cq40MGs@CuedN$^V;l>9FLKiF0rCy;EAeD`RsYkZdSt6V-^Q5X#92O-+XDic%W!RakRlrn~bLx z3qCw~R{Nx_dbG0X`VVjh?^aQ+TB)5tFcyzUWh4*Eioz5liEf3AQ%(=zh_dTx{W~KjgY*wSUC!0>PC#j@)?1yrX|l zm+FnPPWyW9=Blq*6FTSKd7*jI&+k=-b6ULpanHv8;(h;BImgu6JFmCC(a||0JhMXH zNOMW!;)-)~mY7)hm`roe{P2DDj>4Jh^7mL23Vy|$J?&-@df;8~!DmkA*FMxxtNd`$ zHB7GOa(My6&kxxte{Ef4{gQP;cA1Fue*AFHc9Q(QZtLa37PWlkuKr&pUhU+V@#+x! z^W*u4@4nt@82@$7LT#~As=U5MNiVgO`}~XlA4~Ph?govwJ=!Pi@~de^s%U7qL1>fK z4&S|JeE$h=mj6E|Aw%T*&&5;NzZr(=2o&sI@a0PC9REv^b>ErgYHCFL*LOdT|HZX4 z!Ctb*K{0Q4XHMYZsk)PPRNiD*-m0^hy-dg^SH@zK=%0m|hk_5({^XkYQ?hAcAd~*w z^o8rC?}UAsG}ULF&jLOf^>zQcZho9S?OeQVyTS_J&2D{{eesIxyklAGx+Z;k8EPd| zIqmp+gSwYYjPqv;r{7l)Hxb)6Cqixdd5Mb-2Lvo*b{^WR?Hy4K8qa*L`RljmEuXC% zL6xqK*FJlcumAYs)$WX=64l?{E`NR2c}nZ@kDbYSlGQ5C{$^($M^9dJM?(FA^?`Na z`yz_nm#(r}ZN~1T*&lFf@zS6>`wAvroyz{Dm+!-&*Xt()-g%LB-70>?*GCVkYBz?> zzEk#Z`^(J-ZFB#`F7{e7%gIxDYH;ASM^D1Vc?+b@U$CwcDxA-_>(0M*QC~z1c!G|6 z?B7xKQ0t@9w7vTClFom8tW#Rvbkuvs)~Kjz&4Z7lC*Rpu#v4Ds|FHfZkDmwhgo0)* z$xw^Gq#?#NW3rZi-aSy(U-C$3_r5Q=I?3#wPhE5N-7CEqu_m(B(l_H!Mqj ziA>wr++iEW6{mgUjL26R*U%$kkqW<`ES}Xkx%vr5)9Hg=f?T`Y{B$R3ab&C%DGug7 zV{foT$a(#~#$Eqi-cE?p@D}0HdNyNWqn&kS_lLi`S1i+WyI0e_X#afG-99>>|8M^O z>|&9~^P4{|-~N(Q9=~-#tb)pcoI^A2x^W$gICw+t+2JE)8-#Cn+B3JyZ}fbm-?7#Qxw9clrO8Qa(?!Gc`EJ41X7u9Ug~%C&*CHjd zvP)-LmTBH!S@iM2)ztk~G8y`=#y8q~IVV45@cfs=ke8f2vvFI;-~5hyrDuEf#*A~vzDC8W)o)Pb2jWw z$x^$Y2HC{fcDKxP3A`+DzW>qFuhDn-A|D@G`Xaw;_xl%3mp>ZrDbCO@dw;WAc-owX z$@ZTkpWI4Iy#J4Rg37_olV+;%Deuo_sNHLA@cRb;;~!5?%NES?QM!8TjowyHhNBw= z-`vot`g zsyVKgaW8aU*RQfS_qJT;d|z^H=8IXSN=ClD8JW4|_vhBIuQllU_pmlY=yF8KGu9JF zqwgLS_lUkXMY7e@tZiQT_QaGG;>Y!(+xmj-pB>9IJIch8VQ`e`lc(&hf8P`2?)07e z!gI#*t%!+U443Y`*2LN*$-6$m%4;?niX3;_@N}*21grGNQ~KxdKHYLuLNqjD5vR0P z&WfiMZCr*=BSSCoo3QBw3AZdfHO-J$@W%Z9{Ynql8J_0*TFia*dCY}dTkGVzB~Dj3 zOHb4A^$>Ue_)a_Dxo2y_$JY6-c^`#jI=oM4XS#o!q$M=1CoQl+jqmZ@@O#HLEnREz z_|$pj`~#iYr+?WQ4tnh>b-3g%b1m_7+KgI1j+H;f=eNh)*7>qR z>zD~+&>qfNc{{zQ^1YR1-jr-ncGIZ*(v+1Z$pP6-Q{#JDmMG+O~ubn@w&%ioIkdO|m@Nio88mxt9RayYG`Y*?KpBnp{?!Sv54T+m+drt zUwALOINis((Do?jg|GJxGT+#g&u_F*@`c?)6`rkX3q5_`&H9>ID6vT=c}bwlvU+|a zPR461k3V!+-g9rCqKb|rtE=P1bB8zi9Qopy@jHh{?$QOjYPn6Zn+rFrWS*6`Q<-7o zl*fKk0)1s}ZI`c=l>YQ_`Qn#fiYG>I;&6V+R$iHQ`of((W!z@7qIW0E|ErjiX3o^h zCX#Zx;fZzox1{G8OliM;UUd5%oS?a5Zko4AqIjF)3)T)^-iqfJ;=Y{zd|_LO<)^vD z1sO3#(=A!&XlQLxZF%+O=h}*JHWOFl9fwa?p0}GGzI>+;Qs^Mm%jw`5!Lw&ch&-4@CGmIZzqn-+KUJrcWLs1{hdMW$KM_29u}eV1k( z5f(h&HreXYOtubXsg`FVQP0mEeDQ|$M9*vujjqcbueC~MFce8#GT~AGmh>@m`hi)+ zObJ$Y5lbd6oc=<3^PCsVcZ>Uub}Ty3r5-3S)uBx3lVfeY@rn}nmb67%4r#cx9alUS zB@n-X=lA5ddfg#xMp9A+Q%*9iWHZsV6aCp{mU5}LA#-u0+`*2EJf>;~Pl*{UvFvqx zzUb$Pva@b_sY>~!J4(-IZYj>VRx06Ue1%W?*qr1*4MRyCi_&(3P{FfnV^7S#z!*I9 zspM1%!&!!#6J<^?23<^Cuxpw;d-gd_jT2AyhOT4Gvw!E(@p<-~1-~=bZ!C}SZ@+m` zg+<1=xY5}2(3=)D%Q+&EuVc4)IoLgB$rYbImvN!tTc%cnbv|E9-p=UZ*INIOd-J9E zfP%XHSC^b>Sd+TT|6B3bB*$XAm5Y}ewmvPMzVPW6VVT`GrnEG)7;B~7%fSAd{14lBs!KYu%S&tCxVsv?Ok4jEs+_ss3N%a0=}2lq89s;mjqiKvk3;p2V$i6$ho;>04SfgjPz$4D>I-;@cF6o7h*RDMdh_QIb?C3};_joGB5$C3em!YuV#h(=@ddR=!s-GQ~+}Ei~tGLgsV_BS0VRkfMb;kjLLc2VM zFU9sB8h`m-)Z*TBp6wkw>(@U`o|k{u9b8s$nD76;b9_g>i!qR-^CWlIhSwIw>)FL3CO%$RF0o|VnccU0#^6IWzO(?;!We7+MG zyi!}TsBh8I1uu7~Ubxlg80YCAF48gW)jo4+gV}%75AMF1yR_ji!y<*hmpdGnHh-CX z;IBhwBio{HO{r1hzgWKQ5F>qpV zl7!(bL2>R0-i3F%5RtS6ZX&dO?}&?(zW4jr^4SHfjWkFrfYPQ-U@Sm^PlJY$#-4$xtST8 z-3&Kxl#SioceJ8d*=SZvlgy=}ig;1awX?1+;XEt?ZZ;n8t9#mGSs#p;=#;mOSBEVwLW!f8&Kdqhz3f!Y#Ech3R(A?z>pevO45t zwa&|FS;%3act~JwiEDI2#%YgNR%y@M3^uRj+!$$WDAs*&+9LxU>BVW>DNh82xjbIF zp6TeEpqmhO+i=38w+_X|InfQdx0jY};BYK?6`h>TJS+WJ!ffLSx$heEIySK0&OD#6 zJ9oZ?QIqOhrl_=SikEHJBO}`~xg|`aFDy$_@MdyrW7}{dY=X(L330brJZJYNWi#!X z(9&Qb;@y~}*}=)_aBg9(PWLny>05oV4HFAxAHQ^4e^0XSf3A(%VYyQY=k_FDxN?ib z@@j)~l-Q(4JFeW=#>*-8sZ~cRiStYlSH_IHY^u{7_Gz~65o}kC$lI-b(;@p{i^Zk> zXD(8?@=uidMia2^xX!ilO)5|~d$Nqou-;rD7<2wGR3C+`=vCA#Ev|4ThYdr6TC5*W< z`W^dbE-kHoDY{H#bw?g&hR;e36SKuT*D_Z|H(a^XkYvYySS+EN%jI@U=B*cILK*p8 zigUXXcdX`J_Ts{y$6?$_P6s1rItpBzv~_}$;M$jJPo9-KZcb`Y+RZTa$->nci%iY> zj)g5Z42!llP`V}Xq~-GC?Tj}$=P`zwFFa8=V^-g_cYf>z8d;G$H}P`po-`$1S)6T! zTM^6MWUm8L(gI?q9sjmz`669~m9IOFPI{4--LT}?LUrkhy_~5GSDQMf#I-Da@8(@> z9+h^^>0ViwduKQ6^h%=~?ygXmD33=sBx5~Hmm6vwnb^u0-R1vcYKGIsT!r8RYo4WX zoQ>VkV!@@Wq@&3wTqfyob#3C7NQXr`In&Cy-=v6_TrRs@(y)VfrD3Dr>V|JSk0;n% zY`=2=BBm0dL7Dhxi6Xp zw(z8;aO|Fcr^(rxT|xdbPw6$E8OwTiL^Nn{Wt{49^5T-%SoEUum+GD$&sJx=jA?K= zoO8r_)o0Lcc=$-@bVhyaD-WZ>E^Euckwh*=!z$d{) zw^u8Ii{-{S7R$Rw)@_eF8aw~JD_T;w1~}N<>$&0~_PRXp$hP)*ty8j^ zOtaY@owV?|yHS-*W&2{MQ+*|yGOW)Y*40T!eXV?c-j77tvQ*8uzkFWGpB#A&_Z(Te zPx3!c*^hSfEB?P@ek|3EW}0Qf$$n4f=rZQav#_AI%;kkD;; zqcyQdy0bUFe0V?U@U^uPUNh&N6jRc<&aXND%Ex2x#5a8Z$!oDC)i|h%%}}Ca z@r$_EAFVf@I^Fktve?fBa{%FR6xeX3Il?o2drnmU@%sN3zmAnD&%a_@7yQQH z|Lya8+kVUK5U#!-8^20^R(et^1Fyj+iF^BG`Tsw+z47nHWRI7U2TnLoVp);Kwrz&9 zqh#A!Ud33Mqnr8do440j+g~~ws8Zymsh5z+(JQqegfH#GsafqHoY23oeTLnOi`k1# zd;RB_7yp;R%%;e^ZF_$E*45{0!*8W7FbsFuRxcm7=f9!N{$Gq4t%2ok8>(adirdXT z+y4FtGUmwT^G?@4XNajcWliSP;Ph@QGH4F}zd>@gTm{$KNuPB)=5l0Te)aM1>zB=^ zKED-bPW$*djp_H#g_{olQIVheXk}HKy7%=xt5XdtG9Jwq6zs zmN~C**ZRdr{y7D23zpot!?pV%%ZkNnT?PxLS*fm4aN4Y!ep_QRInQlxJ;kP} zE3Nd@HO9bF_mJzo4k_xUqZB-5zv%nZ~=Aj%^>OZF;_A zU+=V+w|!wC)j5z*Rq^O|=RJe;$Ex>#G_6hAc6Nu_)OAa*Z#Vaqey`~#ef!G2r*-^~s=cf} z9O3)I+ox!Jp?mK~N1mRZ-Kh_H)=g3CnCdfS!I6u%6CWMm5&f&2EOGYLzS6e#=#;<* zyUuM87C60uoBjBYYrbhG_TMs5k`Lkg@#XX858Pp|KhFQa>Gk^g{J>>0O9J!doHy9?iSn)gnGBg-Yf!*j!nI$p=k-&M1_WoHDB=3SP+ zzQqBn6kL8j?&8?ZAR#N&a?E6x{~M=!8|KQbe$rCdd9YwH|BYoGg3gY~yE~>G6WQb? zELH4!zUOk5w1w0g7rPbi;sL46-?|q}=Q6v?aQWCCiA}s(yNb9r*=ftZUD|_CePb&@uCUyUrB{)^SlE)s92f5Ot2$fo*sF=@W8Jqk_wwYvesTPoZP6Sy>C^A?>*9{bInRH5 z%=hcgdK1aRb5fr#P%UGU5;J!WPu7Uowt6y?iN-QM2dD3bn-$g;9SGdC#bs&a35Jty z-)|{BIZkwg~aLwm^4R1dFzOW%d^UXW4gc7DDN4|gB~EY|aM!BFho(QeoFNntZB_WIQrwz5Kt;-MnJA;@*Q?Jr zpZm?k^)paqx?Yp+QA3MYTa-C%JNFpzL}r)Pl=j=psh^wOn(|y{3!_BeQU+`OV4WQb zJ`++@tkWMJ2^F_GE_SvtQN@t)zy0HSBE6P# zznN?F$#0pI?=geqV?J{}o!swYbFt&!4v(7#Go!D&dNUp2v`;#1l-OA&8TRmz(0oyo z%828iefBf_Q+y=N-0vp>fx_jbz)_DnYHck8WiFxlGhaED@92(jle^z62J7%~l zZ9>NU?umuVm9$TWZC4Cs6--(uUKrhOw~s&eRE$iAw}y&TY^qx2WLL(ukI!+*{yqHo zLjTLM>fc)&SsUEEovYQ_=UBBc@3aV7TRc&m*F<&Jote}1PN(!O^ek@De(lQ^vij)U zm;UC99G{+#jy%TS;z>vm-m}|e)T=JK@EE0dJ14K}u*+syrQ{qCZObe`u)A8&9!Zg^*+i(_Vso~gyb|C--a6Hn?fPkep5|IVJHdL1gp35*K!)Po&V*B}Tq=(!`4b+YPQKi zC-vXBU?3QM>3Etf-`oc;o8x7_#j;to{C%4HvSRt3;=1m(kXu3R! znl9X`GRGio&PA)6LKo(3x@a}!aO1VG3|HY@c?Wb-Vj8?2ZD4ZPH=%S9|Ao2!7u#|; z-%X3Tt?_)qMHY^rL#%x{d%Yu!LUvA(*K9NNEojV)xqf#Av!Txs2~`&M*m+BMdjscs zl(Q__eRS>zR)(rf-NP-a&Y`O2IkWFQEa5S}nUu-MoqyYR(d92)R&P=k?>?hz`|`2~ zci!IYOLIC*Kc+|}&w3!_vt+S$l#o^NGMKwM+E65u&&5Dxj*Y&KSLJBgh*bA zwcng4o_haNV<{V#o8}##MS`5C108}GSq?U3-pX70ZnN8kNs|mzOs6&mU(ygLSYYZU zq51WTr?Y=sP{v`w@MMh?K7Whk1 zdMn2YKlz4J(Vks3<=X@ms!n9HL>5Y~>0@!*yzF?yWm&D0U5jm}J1<))I4iGGDu&RfsxnN?W2nBoD0+Q}qszr}%84fpTtug(?h@4Xc;3UH5_UY) z)g?IX3ai|bIX)W=E(V_IRTA`HXc+Edw^Dgx){%^BEt;AWd}NnBX*tH&Fk?bPgiLAp z>4nzqCLXiB7D!z=$nz?vP4XaDa&_i|lbcqonfuUi#h!(=`)kG4cujiz+QZ69=d|Ae zsod<$M8;Jao&i%k^B2FI7QH-Uf>v~swNCCVvrC#+jgOpe`;fTbl6%*S@4=#iSw5d8 z2P@e2`hS=q^}xsbnOCLMwHg*pVGOw?&dwl`vQYWy%c-9m z7;SkRHk)ly-BG!;x?_v~)TWS|yE_&=S*E>8tIO*7U8Z~cbLM8R>aqQ2FZxZ{a+C9~ zH-@K;?jE?;a&eJlc$j{~-e<#;5yUh1{C4RUDa5<9W^d}ylhEXAE)r{3{yzA|G%!9k-3 zKjv+^-?G(p-op1)C7)_vt+`*i=eO3)8w-*X=f#>kDL=5Rt4i-;J<`&`t#enPTEy7x z_8W!xKb*gFcT{=&w)=Ew&qf{z&;GU9Z+;qlo4qc&B{WS{UrJ9vbj_0E^>Y|JcGXR) zI?1Fl?dY=k{)-HUI9Ov@emRAH(+|CEAXZ1adq6Len+R<&o=C6EuSYlbA_qw{wm#F z9P>~8V1K!Pxq;K4_0OWd?O*PG^roMIt?r3#Hm!(x{eRk> zowli=hb_@n!jHW(Go^B_@HX?jj#b=BC;z4>1^akZtW)h#IX&;z1P#7vq3t- zUQH!o);4RC_rEfunWnow{dw#1{ev5Fjwnno{TH!7=TOmSr+wD3=`YPXk2h7e)z|(J zere^Pc7Bq)Ci8UJ)K{x{uQL1HtcqL~E^yjv-I{8F+tViPa%v9N3k+gf6s7s8L#yN6 z_l4my%I8z&OA8Ch)fU{}A9BC)`-cXb@+Sv{Y~EPU^>YkV@aWsWS8-K<*JPEy25jq_ z&s@{K=Ego(-AZ2Uf6LBm!Ovfwx#T^wHsIfMN!e2uK8oym^t0h{&SBsDmy)ILT2!;z zYy;zV2`Zfm(~(>~W5S0uatl4Sz5a1NtRl(agrFWT*5 zu-3TI*i*9MVA+os94tAzmNYIu|GRWm)4IodyHk#ymJ*EaERMXOl+y0F%%NX?$AZP_ zKQ@YPeZjDP-?XW_`4@ay{XX^I&oi5wa|~y@e~R1Frmvx|qp)tvdFK7wba%;eEn2>1 zUzPiOmU2_|vLoGPvjtdAUpMW#rd-4yDcRi@@bKgBr!UMhwIj-9r8+L0vF5hAVYbYp zG2>F_WT^|2{d&43z2?eavb)?c#g>50jW zO0j}3zbD=AmAke3b;-%5Krx3frgL{} ziq@?^c5TD`uJ3&|Y8pQ5vt-l{bpG2YXxMRIVP?&Vw&LPO&8h-Me ztDnEAW;0CN!^e8o?D7+~+b0vO+xC3x%iSO1SW@5O7^-so`;@PpA*>f=?_JMnFHO74 zqI2=0^ehprX@~Yabv>MVvU0A@Hd_-Hb=jLm_qun8esF8~6OmZdcU`YN)jxJSyW8@} z+O=ErT8_HaMV#E^(~*-Fxoq{N30v-)UVghHe%{}nq^-v`NFD3EzWU=Ao40$oc`_Hg zSl-}x%AMc*;$EHoMv3h*FSsI3&OfRzzsD~?vN99ro#j7WixGl+|+v1E`IF2afjhr zaR#2%;$P~vW`<2JTl)LYy7gDLv2XNoEar{4?-}O*;v9E>>!O>%Qft^Yu5#2_EkE0p z`Bv7KW9OGoI4-m4yh@Jo*)5kcH|e>qxo>s*-px5vdqW&cUQfxf+}2>Qv?W*N!4v`O zCl)`=Rs}>wEW0vKaDr!Z@GY?q1)Oiw{l2jtEOIntyu|7KZOe9*T=OdCtIIyeyg#&Q zd7?z6eRS~Yj&xOq)2r2$)+_w&``IXeZ|BRhJMZ@%k~kez{b$ebriRV0f9R?R2n($% zwzOt>ks9?Tpkc1hs}*c#j<>KBEox~|Ek519+_>)Po9AAK%q}@jmamE2!JwvpYyJJY z2#14GI)_%Y%{=nx`Qj7F7PZn(8DxukH}GFRTHU{(q+*Q*|B_!j&41hszZX!mbumxk z{^W?;qGo#)7QA8q^y=iCOTQg?*WIsfzqm2#7UTYNTjD#+Hu-N}Zlb}P6ngXVmb&ZS z`G5ZOYx^c~GdVcgy>fhTd~Q#Omi%22n`ovO&J8LHCdeosn3xuznz=~lme77Vuuz5!CHTFyE1+sG&G{5`&`tAjXUW>5FaU!-Y=k|%el$|6W zE%)X8lqs8vr&h(B<5DYnP-^pig2Da9@-yc%_fO*zG@O5Eig8NfYo8k#Vpk1V5$6%l>W)*CgyhE5#95rkKNa4)wk#$y)7TMU5z{aZ0huKPoWqE zuLByI(wkHhpJW(#+)}?+Z|=mT!_gG_?DN~E^Osm$nd~~H8SmB!rA?{RV?1iYpqe>p zr%H)k$8ILu^p@vxfh%p()wBHDW^i1-xN;fq!)5MqK9gip)jpcuT&rT~%(X=QP*{rC z+}{ho9WJ%uP`T*Lyp<>M*G9v{_uRA2#-Ch1?YPA6z(0v9J^Q^P=1=qD$-B|#vRBLH zNKF{K-eI$6SAVr!I-tVSEVWB6^i$=kjpFib2Y#JZNWZz;>%k=UIfo4bW4^6Av}B>d zVwb5Bo1$JtKlsi5!@TI`8-e!{5^Sg5@BMSf)2ZZu+T(Yw45GY#hk0I3e*ATc!nND$ z^L*JAoxiYrxoFtyDS7nH3)y>$RZL>d|2EgHY&p8*pcb!PsNX_G~=Cf8e4k7cQp1miiUzoX;8cd}*;t z-{t5ek&FYiJ6g<)a(4P}o3ttO2k+kNk1M~P(DvWY_x*c^$|Mh^&L=NKuJKR5ey!c| z`0l>H=Wp{1?JrTAoVK*gl;igVvu_@c=D)J`b%^ZXi2GwctM<$8Hs9{!$Ar|j$sPau z$v#y-&YJ@qKrHv@`fe z>%ikTFqdKRqoig9+=eOT}6J3NnRVMD)zcg+M%W1n;%O3oix9Q0Q`C!kq51-d9 zy{vq^@;paU?#uL+#DJTBe(aU@{juT3A^zzT)8%+q8&-u)TBVjEeD=r3KcTtNPd3f` zniST&&C#|~`RsyAd}o*RyoeLXX9}G0|0~PVoL_Zoa+d!2v2oj)I+>I21wBn&F57YuWw3V?aSL7(VVdQrQV^n;CVeEC?yS5wU#R(z zru?sn8UH8B*d;y}3)$Vf@8*|8rJK1+KY!iCes#<7#Pw^x)vqns+YUG9QPjX)tac}l#cE4xm-~MM_yr=n}sttQ|PdkJ} zU0HDN`SPUGzxS0?UVi?!@A|H_VfuC)U)Rq1^&Z`IOP3 zIvy=a%VqaF=Y^Zhb^R7za4}uue&xa9>DyeVDS|QsD2aBe2*pSKe&&!X`$Az_W9rX{ zY03zxNghoq8KS=N4X6C17HQ@&-0^h1c)?EvlD


}tpS`6m1qc5VJqSyw4oEd@#d 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 literal 0 HcmV?d00001 diff --git a/kyomato-scripts/kyomato-helper.bash b/kyomato-scripts/kyomato-helper.bash new file mode 100755 index 0000000..0834600 --- /dev/null +++ b/kyomato-scripts/kyomato-helper.bash @@ -0,0 +1,65 @@ +#!/bin/bash + +# This script aims to help with kyomato usage, boiling it to a single command +# You can try running it as +# > KYOMATO_IMAGES=$(realpath ./image_folder) ./kyomato-helper.bash test_input/input.md +# From the directory it's located in + +# It's functions are as follows: +# - Create a hidden directory near specified input file +# - Clear it, so that build is always clean +# - Run kyomato to transform input file into `.tex` +# - Run pdflatex twice on resulting `.tex`, to generate all tables correctly +# - Move resulting `.pdf` right near input file. Temporary hidden folder can be deleted manually, or used for debug + +# check if kyomato exists: +( kyomato -V > /dev/null ) || { echo "can't reach kyomato"; exit -1; } + +# check if pdflatex exists: +( pdflatex -v > /dev/null ) || { echo "can't reach pdflatex"; exit -1; } + +# input file path is expected at first argument +input_file=$1 +if [[ -z $input_file ]] then + echo "input file must be supplied"; + exit -1; +fi +if [[ ! -f $input_file ]] then + echo "File is expected as input"; + exit -1; +fi +echo "input file is ${input_file}" + +# Determine working directory +source_directory=$( dirname $input_file ) +if [[ -z source_directory ]] then + echo "dirname had failed. please contact me"; + exit -1; +fi +input_name=$( basename $input_file | cut -f 1 -d '.') +working_directory=$source_directory/.kyomato_tmp +echo "working directory is ${working_directory}" + +# Ensure working directory exists +mkdir -p -v $working_directory +if [[ ! -z "$(dir $working_directory)" ]] then + # Clean up the working directory from previous runs + rm -v -r --preserve-root=all $working_directory/*; +fi + +# Actually run kyomato +execution_dir=$(pwd) +cd $working_directory +echo "running kyomato..." +( kyomato --in ../$( basename $input_file ) --debug-paths --exec $execution_dir > source.tex 2> kyomato_err.log ) || { echo "kyomato had failed, please contact me"; exit -1; } + +# run pdflatex +echo "running pdflatex(1/2) ..." +( pdflatex -halt-on-error -file-line-error -interaction=nonstopmode -output-format=pdf -output-directory=. -shell-escape source.tex &> pdflatex1.log ) || { echo "pdflatex had failed, please contact me"; exit -1; } +echo "running pdflatex(2/2) ..." +( pdflatex -halt-on-error -file-line-error -interaction=nonstopmode -output-format=pdf -output-directory=. -shell-escape source.tex &> pdflatex2.log ) || { echo "pdflatex had failed, on the second run! please contact me"; exit -1; } +# yep, latex must be run twice +# because latex is weird + +# at the end, move out generated pdf +mv -v -u source.pdf ../$input_name.pdf \ No newline at end of file diff --git a/kyomato-scripts/test_input/input.md b/kyomato-scripts/test_input/input.md new file mode 100644 index 0000000..0af3be3 --- /dev/null +++ b/kyomato-scripts/test_input/input.md @@ -0,0 +1,56 @@ +--- +header-line1: A document generation example +code-section-title: Code blocks +--- + +# First part + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lectus mauris ultrices eros in cursus turpis massa tincidunt dui. Et netus et malesuada fames ac turpis egestas. Malesuada fames ac turpis egestas sed tempus urna. Curabitur gravida arcu ac tortor dignissim convallis. Enim ut sem viverra aliquet eget sit amet. Ultricies leo integer malesuada nunc vel risus commodo viverra maecenas. Quam adipiscing vitae proin sagittis nisl rhoncus mattis. Sem integer vitae justo eget. Lobortis mattis aliquam faucibus purus in massa tempor nec. Vitae congue mauris rhoncus aenean vel elit. Laoreet sit amet cursus sit amet dictum. Et netus et malesuada fames. Sed augue lacus viverra vitae. Porttitor leo a diam sollicitudin tempor. Nulla aliquet enim tortor at. + +$$ +\tau = 1.04 \cdot 10^{-6} \cdot \dfrac{1-\cos(\theta)}{\ln\left\{ \dfrac{[N(0) - N(90)] \cdot \cos ^{1.6}(\theta)}{N(\theta) - N(90)} \right\}}= +$$ +{ref = tau} + +# Second part + +Velit scelerisque in dictum non consectetur a erat nam at[@eq:tau]. A diam maecenas sed enim ut sem viverra. Aliquam faucibus purus in massa tempor nec. Magnis dis parturient montes nascetur ridiculus. Accumsan sit amet nulla facilisi morbi tempus. Eu tincidunt tortor aliquam nulla facilisi cras fermentum. Volutpat lacus laoreet non curabitur gravida arcu ac. Ut tristique et egestas quis ipsum suspendisse ultrices gravida. Donec pretium vulputate sapien nec sagittis aliquam malesuada bibendum arcu. Iaculis eu non diam phasellus vestibulum lorem sed. Amet consectetur adipiscing elit duis. Mi bibendum neque egestas congue quisque egestas. In pellentesque massa placerat duis. Felis imperdiet proin fermentum leo vel orci porta non. + +![[meme.png]] +{caption = "Lol why?", ref = meme} + +Senectus et netus et malesuada fames ac turpis egestas integer[@fig:meme]. Et sollicitudin ac orci phasellus egestas. Id interdum velit laoreet id donec ultrices tincidunt arcu non. Odio tempor orci dapibus ultrices. Sed lectus vestibulum mattis ullamcorper. Nulla pellentesque dignissim enim sit amet venenatis. Lacus laoreet non curabitur gravida arcu ac. Sed odio morbi quis commodo odio. Dui id ornare arcu odio ut. Mauris ultrices eros in cursus. + +```python, Ayano * "Ac turpis egestas sed tempus urna et pharetra pharetra massa." +# Lorem dolor sed viverra ipsum nunc aliquet. Arcu dictum varius duis at consectetur lorem donec massa sapien. Eu feugiat pretium nibh ipsum consequat nisl vel pretium lectus. +x = [1,2, 3, 4, 5] +@dev: x[2], 0.1 +``` +------ +# Conclusion + +Ut aliquam purus sit amet luctus venenatis lectus magna fringilla. Egestas purus viverra accumsan in. Odio euismod lacinia at quis risus sed vulputate odio ut. Cursus euismod quis viverra nibh cras pulvinar mattis nunc sed. Volutpat maecenas volutpat blandit aliquam etiam. Sodales ut eu sem integer vitae justo eget magna fermentum. + +```python, Ayano * "That's a table!" +@csv_table: src="results.csv", rows=2..=10, caption = "That's a caption!" +``` + +In eu mi bibendum neque egestas. Mattis vulputate enim nulla aliquet. Sit amet nulla facilisi morbi tempus iaculis urna. Aliquam purus sit amet luctus venenatis lectus. Convallis a cras semper auctor neque vitae tempus quam pellentesque. Vel turpis nunc eget lorem dolor sed. Mauris pellentesque pulvinar pellentesque habitant morbi. Ligula ullamcorper malesuada proin libero nunc. Tortor at auctor urna nunc id cursus metus aliquam eleifend. Mi tempus imperdiet nulla malesuada. Tellus in metus vulputate eu scelerisque. Quis lectus nulla at volutpat diam ut venenatis tellus. + +Generate a figure in function block, and insert it with a special syntax: +```python, Ayano * "that's a figure insertion syntax, all-in-one!" +import numpy as np +import matplotlib.pyplot as plt +from math import pi +x = np.arange(-pi, pi, 0.01) +y = np.sin(x) +plt.plot(x, y) +plt.savefig("sine_graph.png") +@fig: src = "sine_graph.png", ident = "other_sine_graph", caption = "That's another way to include a figure from Ayano!", width = 0.9 +``` + + +And of course you can generate a figure with inserted block too! +```python, Ayano * "That's a third way to do the same thing!" ~ scripts/generate_sine.py +@fig: src = "other_sine_graph.png", ident = "yet_another_sine_graph", caption = "The same sine. Again.", width = 0.2 +``` \ No newline at end of file diff --git a/kyomato-scripts/test_input/other_sine_graph.png b/kyomato-scripts/test_input/other_sine_graph.png new file mode 100644 index 0000000000000000000000000000000000000000..bed9e05754891dfbeffbb7a6decfa3f10703d7d7 GIT binary patch literal 24124 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 literal 0 HcmV?d00001 diff --git a/kyomato-scripts/test_input/results.csv b/kyomato-scripts/test_input/results.csv new file mode 100644 index 0000000..ce08cb3 --- /dev/null +++ b/kyomato-scripts/test_input/results.csv @@ -0,0 +1,11 @@ +'dataset' 'theta' 'tau' 'tau_err' '%' +'counts' 30.0 6.695212475440718e-07 1.2899671973307503e-06 192.67 +'counts' 60.0 4.2402616158686483e-07 5.142772156895383e-07 121.28 +'counts' 45.0 -8.045091470679962e-07 6.618084062607329e-07 -82.26 +'provided1' 45.0 3.29268327957351e-07 2.0471885401181245e-07 62.17 +'provided2' 30.0 3.0219144232940006e-06 2.3786429056933766e-05 787.13 +'provided2' 40.0 2.8245329154178373e-07 2.6019976229797025e-07 92.12 +'provided2' 60.0 1.114681732370397e-06 2.1996000386518054e-06 197.33 +'provided3' 30.0 2.1550690414157404e-06 9.543180301947427e-06 442.82 +'provided3' 45.0 7.516418500767245e-07 8.770054083968072e-07 116.68 +'provided3' 60.0 5.477416674543067e-07 5.261473464560824e-07 96.06 diff --git a/kyomato-scripts/test_input/scripts/generate_sine.py b/kyomato-scripts/test_input/scripts/generate_sine.py new file mode 100644 index 0000000..b25f473 --- /dev/null +++ b/kyomato-scripts/test_input/scripts/generate_sine.py @@ -0,0 +1,12 @@ +import numpy as np +import matplotlib.pyplot as plt +from math import pi +x = np.arange(-pi, pi, 0.01) +y = np.sin(x) +plt.plot(x, y) +plt.savefig("../other_sine_graph.png") + +# let's say, that there's some debugging going on here: +# TOSHINO KYOKO! +print("I love beeeeans") +exit(-2222222) \ No newline at end of file diff --git a/kyomato-scripts/test_input/sine_graph.png b/kyomato-scripts/test_input/sine_graph.png new file mode 100644 index 0000000000000000000000000000000000000000..eb82aeeacca146e6db2743fc8c6af4a6193cf05d GIT binary patch literal 23760 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, + /// Sets a dedicated image directory. If omitted, "KYOMATO_IMAGES" envvar will be checked. + #[arg(short, long = "im")] + image_directory: Option, + /// Sets path engine errors to `InformativeError`, providing a breakdown on path search + #[arg(long)] + debug_paths: bool, + /// Current directory a valid path for files. If provided without a value, will get current directory from environment. + /// + /// This is intended to be set a directory of the initial command caller, in case they rely on some file local to them personally + #[arg(long = "exec")] + execution_directory: Option>, +} + +fn main() { + // parse the arguments + let mut config = Config::parse(); + if config.image_directory.is_none() { + config.image_directory = std::env::var_os("KYOMATO_IMAGES").map(PathBuf::from); + } + // read the input + let mut input = String::new(); + let input_file_directory = config + .input_file + .as_deref() + .map(|path| path.parent().unwrap_or(Path::new("./")).to_owned()); + if let Some(input_file) = config.input_file { + input = std::fs::read_to_string(input_file).expect("Error while reading input from file"); + } else { + std::io::stdin() + .read_to_string(&mut input) + .expect("Error while reading input from stdin"); + } + + // now, about path engine... + // their behavior is *encoded* in types, so all of them will have a different type + // I wouldn't like to build them with trait objects, since + // 1. this will add dynamic dispatch at each nested call (a lot) + // 2. there's no option to do that anyway, as error type is a generic argument of the trait + // so... + // yes, this IS ugly and results in much bigger binary + // but it's static dispatch everywhere, yay! + // tested: + // - on lowsize, single branch is 2.4M against 2.6M with all branches + // - on release, single branch is 8.1M against 8.6M with all branches + // so it does indeed has an impact, but it's like 500KB :idk: + match ( + config.debug_paths, + config.execution_directory, + input_file_directory, + config.image_directory, + ) { + (false, None, None, None) => main_inner( + input, + base_gen() + .with_check(fs_check) + .with_ayano() + .with_error_type::(), + ), + (false, None, None, Some(image_directory)) => main_inner( + input, + base_gen() + .with_check(fs_check) + .with_image_folder(image_directory) + .with_ayano() + .with_error_type::(), + ), + (false, None, Some(input_file_dir), None) => main_inner( + input, + base_gen() + .with_check(fs_check) + .with_source_folder(input_file_dir) + .with_ayano() + .with_error_type::(), + ), + (false, None, Some(input_file_dir), Some(image_directory)) => main_inner( + input, + base_gen() + .with_check(fs_check) + .with_source_folder(input_file_dir) + .with_image_folder(image_directory) + .with_ayano() + .with_error_type::(), + ), + (false, Some(execution_dir), None, None) => { + main_inner( + input, + base_gen() + .with_check(fs_check) + .with_execution_folder(execution_dir.unwrap_or_else(|| { + std::env::current_dir().expect("Failed to retrieve cwd") + })) + .with_ayano() + .with_error_type::(), + ) + } + (false, Some(execution_dir), None, Some(image_directory)) => { + main_inner( + input, + base_gen() + .with_check(fs_check) + .with_execution_folder(execution_dir.unwrap_or_else(|| { + std::env::current_dir().expect("Failed to retrieve cwd") + })) + .with_image_folder(image_directory) + .with_ayano() + .with_error_type::(), + ) + } + (false, Some(execution_dir), Some(input_file_dir), None) => { + main_inner( + input, + base_gen() + .with_check(fs_check) + .with_execution_folder(execution_dir.unwrap_or_else(|| { + std::env::current_dir().expect("Failed to retrieve cwd") + })) + .with_source_folder(input_file_dir) + .with_ayano() + .with_error_type::(), + ) + } + (false, Some(execution_dir), Some(input_file_dir), Some(image_directory)) => { + main_inner( + input, + base_gen() + .with_check(fs_check) + .with_execution_folder(execution_dir.unwrap_or_else(|| { + std::env::current_dir().expect("Failed to retrieve cwd") + })) + .with_source_folder(input_file_dir) + .with_image_folder(image_directory) + .with_ayano() + .with_error_type::(), + ) + } + (true, None, None, None) => main_inner( + input, + base_gen() + .with_check(fs_check) + .with_ayano() + .with_error_type::(), + ), + (true, None, None, Some(image_directory)) => main_inner( + input, + base_gen() + .with_check(fs_check) + .with_image_folder(image_directory) + .with_ayano() + .with_error_type::(), + ), + (true, None, Some(input_file_dir), None) => main_inner( + input, + base_gen() + .with_check(fs_check) + .with_source_folder(input_file_dir) + .with_ayano() + .with_error_type::(), + ), + (true, None, Some(input_file_dir), Some(image_directory)) => main_inner( + input, + base_gen() + .with_check(fs_check) + .with_source_folder(input_file_dir) + .with_image_folder(image_directory) + .with_ayano() + .with_error_type::(), + ), + (true, Some(execution_dir), None, None) => { + main_inner( + input, + base_gen() + .with_check(fs_check) + .with_execution_folder(execution_dir.unwrap_or_else(|| { + std::env::current_dir().expect("Failed to retrieve cwd") + })) + .with_ayano() + .with_error_type::(), + ) + } + (true, Some(execution_dir), None, Some(image_directory)) => { + main_inner( + input, + base_gen() + .with_check(fs_check) + .with_execution_folder(execution_dir.unwrap_or_else(|| { + std::env::current_dir().expect("Failed to retrieve cwd") + })) + .with_image_folder(image_directory) + .with_ayano() + .with_error_type::(), + ) + } + (true, Some(execution_dir), Some(input_file_dir), None) => { + main_inner( + input, + base_gen() + .with_check(fs_check) + .with_execution_folder(execution_dir.unwrap_or_else(|| { + std::env::current_dir().expect("Failed to retrieve cwd") + })) + .with_source_folder(input_file_dir) + .with_ayano() + .with_error_type::(), + ) + } + (true, Some(execution_dir), Some(input_file_dir), Some(image_directory)) => { + main_inner( + input, + base_gen() + .with_check(fs_check) + .with_execution_folder(execution_dir.unwrap_or_else(|| { + std::env::current_dir().expect("Failed to retrieve cwd") + })) + .with_source_folder(input_file_dir) + .with_image_folder(image_directory) + .with_ayano() + .with_error_type::(), + ) + } + } +} + +fn main_inner>(input: String, path_engine: PE) { + // we'll always write into stdout + // std opting-out was relevant due to directory of source file should be considered by path engine + // output can go anywhere and nothing should depend on it + // also, I'll use buffered writer, cause idk seems fitting + let output = std::io::BufWriter::new(std::io::stdout()); + // that's a transformer, since `Kyomato` works with `std::fmt::Write` + // it's able to recover IO errors, you'll see + let mut fmt_output = kyomato_lib::FmtToIo::new(output); + // that's a helper function to write with `LabaLatex`, as it's the only transformer for now + // TODO make more general + kyomato_lib::LabaLatex::parse_and_write(path_engine, &input, &mut fmt_output) + .expect("Error while writing output"); + // now, try recovering errors from that fancy writer + let (mut output, errors) = fmt_output.into_parts(); + let mut errors = errors.into_iter(); + if let Some(first_error) = errors.next() { + // if there were any errors, errprint them, and terminate (panic) + eprintln!("{first_error:?}"); + for error in errors { + eprintln!("{error:?}"); + } + panic!("There were writing errors"); + } + // lastly, explicitly flush the buffered writer, as it's advised by it's docs + output.flush().expect("Error while flushing the output"); +} diff --git a/src/gen/ayano.rs b/src/gen/ayano.rs index 7515dc3..3de843e 100644 --- a/src/gen/ayano.rs +++ b/src/gen/ayano.rs @@ -105,7 +105,9 @@ impl AyanoBuilder { if let Some(function_name) = info.function_name.as_ref() { python::append_function(&mut self.code, &function_name, &transformed_code); } else { + // We must insert an execution path for this block now, so here we go: let insert_path = if let Some(insert_path) = info.insert_path.as_deref() { + // if this block has insert declaration - use it let mut path = path_engine .ayano_insert(insert_path) .map_err(|err| SyntaxError::PathEngine(Equivalent(Box::new(err))))? @@ -113,7 +115,13 @@ impl AyanoBuilder { path.pop(); path } else { - PathBuf::from(".") + // If there's no insert declaration - try out source folder + if let Some(source_folder) = path_engine.get_source_folder() { + source_folder.to_owned() + } else { + // if there's no insert folder - well, use current directory + PathBuf::from(".") + } }; python::append_static(&mut self.code, &transformed_code, insert_path); } @@ -193,7 +201,9 @@ impl AyanoExecutor { // this block was passed here before, but it's not a function return Err(AyanoError::NoFunction(token.clone())); }; - let insert_path = if let Some(insert_path) = token.insert_path.as_deref() { + // We must insert an execution path for this block now, so here we go: + let insert_path = if let Some(insert_path) = info.insert_path.as_deref() { + // if this block has insert declaration - use it let mut path = path_engine .ayano_insert(insert_path) .map_err(|err| SyntaxError::PathEngine(Equivalent(Box::new(err))))? @@ -201,7 +211,13 @@ impl AyanoExecutor { path.pop(); path } else { - PathBuf::from(".") + // If there's no insert declaration - try out source folder + if let Some(source_folder) = path_engine.get_source_folder() { + source_folder.to_owned() + } else { + // if there's no insert folder - well, use current directory + PathBuf::from(".") + } }; python::call_function(&self.token, function_name, transform, insert_path) } @@ -1117,7 +1133,7 @@ impl Syntax<'_> { // TODO add option to opt this out let src_path = src.trim_matches(['\'', '\"']); let src_path = Path::new(&*src_path); - let src_path = match path_engine.csv(src_path, insert_path.as_deref()) { + let mut src_path = match path_engine.csv(src_path, insert_path.as_deref()) { Ok(existing_table) => existing_table, Err(error) => { // figure was not found, but this can be ok, if program generates it itself @@ -1187,6 +1203,17 @@ impl Syntax<'_> { if let Some(rebased) = pathdiff::diff_paths(&src_path, insert_directory) { src_path = rebased.into(); } + } else { + // if there's a source directory available, python will be executed from it + if let Some(source_directory) = path_engine.get_source_folder() { + // if there's no source path - ignore that + // if source path has no parent - source is in a current execution directory - no need to relocate the path + if let Some(rebased) = pathdiff::diff_paths(&src_path, source_directory) + { + src_path = rebased.into(); + } + // I could've implemented this thing myself, but why would I, was already done: + } } let mut s = src_path.to_string_lossy().into_owned(); s.push('\"'); diff --git a/src/gen/lab/lab_preamble.tex b/src/gen/lab/lab_preamble.tex index e8d1b86..74e29eb 100644 --- a/src/gen/lab/lab_preamble.tex +++ b/src/gen/lab/lab_preamble.tex @@ -92,6 +92,9 @@ % enumeration \usepackage[shortlabels]{enumitem} +% scaleable font (whatever that means) +\usepackage[normalem]{ulem} + \titleformat*{\section}{\fnt\LARGE\bfseries} \titleformat*{\subsection}{\fnt\Large\bfseries} \titleformat*{\subsubsection}{\fnt\Large} diff --git a/src/gen/lab/mod.rs b/src/gen/lab/mod.rs index ea0132c..f23f735 100644 --- a/src/gen/lab/mod.rs +++ b/src/gen/lab/mod.rs @@ -205,7 +205,7 @@ where if data.spaces_before == 1 { output.write_char(' ')?; } else if data.spaces_before >= 2 && !context.allergic_to_newline { - output.write_str(" \\\\\n")?; + output.write_str(" \\\\\n[0.0pt]")?; } let token = meta.ayano.display_token(data, &self.path_engine)?; self.write_to(output, meta, context, &token)?; @@ -331,11 +331,14 @@ where Some(Formatting::StrikeThrough) => ("\\st{", "}"), }; output.write_str(start)?; - if content.contains('%') { + if content.contains(['%', '_', '#', '[', ']']) { // there are percentage signs in the paragraph // most certainly this was is not intended to make a latex comment (because why?) // TODO add an option to opt this out - content = content.replace('%', "\\%").into(); + // TODO add multiple replacements at once + let content_inner = content.replace('%', "\\%"); + let content_inner = content_inner.replace('#', "\\#"); + content = content_inner.replace('_', "\\_").into(); } if context.inside_caption { // We are generating text that was inside table/figure caption @@ -658,8 +661,8 @@ mod tests { \begin{center} \begin{tabular}{|c|c|c|}\hline header1 & header2 & header3 \\ \hline \hline -cell_11 & cell_12 & cell_13 \\ \hline -cell_21 & cell_22 & cell_23 \\ \hline +cell\_11 & cell\_12 & cell\_13 \\ \hline +cell\_21 & cell\_22 & cell\_23 \\ \hline \end{tabular} \stepcounter{tabnum} \caption{ \capfnt caption...} @@ -684,8 +687,8 @@ cell_21 & cell_22 & cell_23 \\ \hline \begin{center} \begin{tabular}{|c|c|c|}\hline header1 & header2 & header3 \\ \hline \hline -cell_11 & cell_12 & cell_13 \\ \hline -cell_21 & cell_22 & cell_23 \\ \hline +cell\_11 & cell\_12 & cell\_13 \\ \hline +cell\_21 & cell\_22 & cell\_23 \\ \hline \end{tabular} \stepcounter{tabnum} \caption{ \capfnt We like "math" so much!} diff --git a/src/path_engine.rs b/src/path_engine.rs index c8f7efa..4848c5e 100644 --- a/src/path_engine.rs +++ b/src/path_engine.rs @@ -97,6 +97,7 @@ pub trait PathEngine { fn ayano_insert<'p>(&self, path: impl Into>) -> Result, E>; fn set_check(&mut self, new_check: CheckType); fn get_check(&self) -> CheckType; + fn get_source_folder(&self) -> Option<&Path>; } pub trait PathEngineExt: PathEngine + Sized { @@ -179,6 +180,10 @@ impl PathEngine for Engine { fn get_check(&self) -> CheckType { self.0 } + + fn get_source_folder(&self) -> Option<&Path> { + None + } } /// Represents various reasons file path may not be correct @@ -334,6 +339,10 @@ impl> PathEngine for AyanoRespecting Option<&Path> { + self.0.get_source_folder() + } } pub struct WithImageFolder(PE, PathBuf); @@ -359,6 +368,10 @@ impl> PathEngine for WithImageFolder Option<&Path> { + self.0.get_source_folder() + } } pub struct WithSourceFolder(PE, PathBuf); @@ -383,6 +396,10 @@ impl> PathEngine for WithSourceFolder

Option<&Path> { + Some(&self.1) + } } pub struct WithExecutionFolder(PE, PathBuf); @@ -407,6 +424,10 @@ impl> PathEngine for WithExecutionFolde try_variant!(Some(&self.1), Execution, self, ayano_insert, path) } impl_check_delegate! {} + + fn get_source_folder(&self) -> Option<&Path> { + self.0.get_source_folder() + } } /// A basic engine diff --git a/src/util.rs b/src/util.rs index fcdfbd9..3eb91bf 100644 --- a/src/util.rs +++ b/src/util.rs @@ -469,6 +469,10 @@ impl FmtToIo { pub fn get_all_errors(&mut self) -> impl Iterator + '_ { self.1.drain(..) } + + pub fn into_parts(self) -> (W, impl IntoIterator) { + (self.0, self.1) + } } impl std::fmt::Write for FmtToIo { fn write_str(&mut self, s: &str) -> std::fmt::Result { @@ -487,4 +491,4 @@ impl std::fmt::Debug for StaticDebug { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_tuple("StaticDebug").field(&R).finish() } -} \ No newline at end of file +} diff --git a/tests/lex.rs b/tests/lex.rs index caafb6a..c1a570a 100644 --- a/tests/lex.rs +++ b/tests/lex.rs @@ -23,7 +23,7 @@ fn input_files() -> impl Iterator { #[test] fn show_lex() { for (_, input) in input_files() { - let parsed = match kyomato::lex(input.as_str()) { + let parsed = match kyomato_lib::lex(input.as_str()) { Ok(ok) => ok, Err(err) => { panic!("Should successfully parse the input: {err:#?}"); @@ -65,7 +65,7 @@ fn gen_lex_output() { std::fs::create_dir(&output_path).expect("Failed to create directory"); } for (filename, input) in input_files() { - let (title_info, tokens) = match kyomato::lex(input.as_str()) { + let (title_info, tokens) = match kyomato_lib::lex(input.as_str()) { Ok(ok) => ok, Err(err) => { panic!("Input files should be successfully parsed: {err:#?}"); @@ -104,7 +104,7 @@ fn test_lex() { std::fs::create_dir(&output_path).expect("Failed to create directory"); } for (filename, input) in input_files() { - let (title_info, tokens) = match kyomato::lex(input.as_str()) { + let (title_info, tokens) = match kyomato_lib::lex(input.as_str()) { Ok(ok) => ok, Err(err) => { panic!("Should successfully parse the input: {err:#?}"); diff --git a/tests/transform.rs b/tests/transform.rs index 11022c8..9407307 100644 --- a/tests/transform.rs +++ b/tests/transform.rs @@ -23,13 +23,13 @@ fn input_files() -> impl Iterator { #[test] fn show_transform() { for (_, input) in input_files() { - let (title_info, tokens) = match kyomato::lex(input.as_str()) { + let (title_info, tokens) = match kyomato_lib::lex(input.as_str()) { Ok(ok) => ok, Err(err) => { panic!("Should successfully parse the input: {err:#?}"); } }; - let repr = kyomato::gen_to_string(&tokens, title_info) + let repr = kyomato_lib::gen_to_string(&tokens, title_info) .expect("Should be able to transform tokens"); println!("{repr}"); std::io::stdin() @@ -64,7 +64,7 @@ fn gen_transform_output() { std::fs::create_dir(&output_path).expect("Failed to create directory"); } for (filename, input) in input_files() { - let (title_info, tokens) = match kyomato::lex(input.as_str()) { + let (title_info, tokens) = match kyomato_lib::lex(input.as_str()) { Ok(ok) => ok, Err(err) => { panic!("Input files should be successfully parsed: {err:#?}"); @@ -87,7 +87,7 @@ fn gen_transform_output() { output_file }; // write the result to output file - kyomato::gen(&tokens, title_info, &mut output_file).expect("Failed to generate output"); + kyomato_lib::gen(&tokens, title_info, &mut output_file).expect("Failed to generate output"); } } @@ -99,13 +99,13 @@ fn test_transform() { std::fs::create_dir(&output_path).expect("Failed to create directory"); } for (filename, input) in input_files() { - let (title_info, tokens) = match kyomato::lex(input.as_str()) { + let (title_info, tokens) = match kyomato_lib::lex(input.as_str()) { Ok(ok) => ok, Err(err) => { panic!("Should successfully parse the input: {err:#?}"); } }; - let repr = kyomato::gen_to_string(&tokens, title_info).expect("Failed to generate output"); + let repr = kyomato_lib::gen_to_string(&tokens, title_info).expect("Failed to generate output"); // write the result to output directory let expected_output = { output_path.push(&filename); diff --git a/tests/transformed_files/ayano.txt b/tests/transformed_files/ayano.txt index 54ed3a7..de1040d 100644 --- a/tests/transformed_files/ayano.txt +++ b/tests/transformed_files/ayano.txt @@ -92,6 +92,9 @@ % enumeration \usepackage[shortlabels]{enumitem} +% scaleable font (whatever that means) +\usepackage[normalem]{ulem} + \titleformat*{\section}{\fnt\LARGE\bfseries} \titleformat*{\subsection}{\fnt\Large\bfseries} \titleformat*{\subsubsection}{\fnt\Large} diff --git a/tests/transformed_files/ayano_display_blocks.txt b/tests/transformed_files/ayano_display_blocks.txt index 1f8cefc..9e8b1b4 100644 --- a/tests/transformed_files/ayano_display_blocks.txt +++ b/tests/transformed_files/ayano_display_blocks.txt @@ -92,6 +92,9 @@ % enumeration \usepackage[shortlabels]{enumitem} +% scaleable font (whatever that means) +\usepackage[normalem]{ulem} + \titleformat*{\section}{\fnt\LARGE\bfseries} \titleformat*{\subsection}{\fnt\Large\bfseries} \titleformat*{\subsubsection}{\fnt\Large} diff --git a/tests/transformed_files/basic.txt b/tests/transformed_files/basic.txt index 9ff616e..6eba618 100644 --- a/tests/transformed_files/basic.txt +++ b/tests/transformed_files/basic.txt @@ -92,6 +92,9 @@ % enumeration \usepackage[shortlabels]{enumitem} +% scaleable font (whatever that means) +\usepackage[normalem]{ulem} + \titleformat*{\section}{\fnt\LARGE\bfseries} \titleformat*{\subsection}{\fnt\Large\bfseries} \titleformat*{\subsubsection}{\fnt\Large} diff --git a/tests/transformed_files/deceit.txt b/tests/transformed_files/deceit.txt index b1a5ea2..09993c4 100644 --- a/tests/transformed_files/deceit.txt +++ b/tests/transformed_files/deceit.txt @@ -92,6 +92,9 @@ % enumeration \usepackage[shortlabels]{enumitem} +% scaleable font (whatever that means) +\usepackage[normalem]{ulem} + \titleformat*{\section}{\fnt\LARGE\bfseries} \titleformat*{\subsection}{\fnt\Large\bfseries} \titleformat*{\subsubsection}{\fnt\Large} @@ -144,14 +147,14 @@ This test is dedicated to attempts of parser deception, i.e. usage of special sy \tbln But this \textbf{DOES} look cursed, don't do it. \section{Headers} -\tb Headers are most recognizable by '#'-symbol. For it to actually represent a header, it needs to at the beginning of the line and there should be no more than 6 of them. \textbf{ANYTHING} up to the end of line will be included into a header, then. +\tb Headers are most recognizable by '\#'-symbol. For it to actually represent a header, it needs to at the beginning of the line and there should be no more than 6 of them. \textbf{ANYTHING} up to the end of line will be included into a header, then. \tbln Example: \textbf{This is a long, yet formally correct header. You can add $equations$ and *formatting* here, but I provide no guarantees as of the way these would display in final document.} \tb Here's an example of incorrect header: -\tbln ###### This header is considered incorrect for this application, and will be parsed as regular paragraph instead +\tbln \#\#\#\#\#\# This header is considered incorrect for this application, and will be parsed as regular paragraph instead \tbln Another example of incorrect header, featuring 7 hash-symbols: -\tbln ####### This will be parsed as a regular paragraph +\tbln \#\#\#\#\#\#\# This will be parsed as a regular paragraph \section{Equations} \tb There are two types of equations in markdown/latex: inline mathmode and display mathmode @@ -230,8 +233,8 @@ head /dev/random \tb This object's type is deprecated and will be removed soon. \tbln It starts and ends with \begin{itemize} -\item * or _ -\item ** or __ +\item * or \_ +\item ** or \_\_ \item ~~ \end{itemize} \tb \textbf{WARN}: As of now, these \textit{might} cause cause massive problems if encountered at the start of the line. @@ -269,7 +272,7 @@ local z = x .. y -- matty!! Here's some text that's ok to be here \tbln There can't be any spaces before delimiting ```s: \tbln ```python -\tbln # This here is not really a python block +\tbln \# This here is not really a python block \tbln x = 2 \tbln print(x) \tbln ``` diff --git a/tests/transformed_files/edgecases.txt b/tests/transformed_files/edgecases.txt index 94bbdc0..42f8103 100644 --- a/tests/transformed_files/edgecases.txt +++ b/tests/transformed_files/edgecases.txt @@ -92,6 +92,9 @@ % enumeration \usepackage[shortlabels]{enumitem} +% scaleable font (whatever that means) +\usepackage[normalem]{ulem} + \titleformat*{\section}{\fnt\LARGE\bfseries} \titleformat*{\subsection}{\fnt\Large\bfseries} \titleformat*{\subsubsection}{\fnt\Large} @@ -130,7 +133,7 @@ \newpage \fnt -# List-after-para +\# List-after-para \tbln Some para \begin{itemize} \item List item 1 diff --git a/tests/transformed_files/tables_code.txt b/tests/transformed_files/tables_code.txt index 682fa20..2426426 100644 --- a/tests/transformed_files/tables_code.txt +++ b/tests/transformed_files/tables_code.txt @@ -92,6 +92,9 @@ % enumeration \usepackage[shortlabels]{enumitem} +% scaleable font (whatever that means) +\usepackage[normalem]{ulem} + \titleformat*{\section}{\fnt\LARGE\bfseries} \titleformat*{\subsection}{\fnt\Large\bfseries} \titleformat*{\subsubsection}{\fnt\Large} @@ -157,7 +160,7 @@ Кількість & Роль\\ \hline \hline 3 діоптрії & 7 галон\\ \hline - rust & #PANIC!!!\\ \hline + rust & \#PANIC!!!\\ \hline \end{tabular} \stepcounter{tabnum}\label{tab:table_2} \end{center} diff --git a/tests/transformed_files/title_info.txt b/tests/transformed_files/title_info.txt index 1069a26..e0714d4 100644 --- a/tests/transformed_files/title_info.txt +++ b/tests/transformed_files/title_info.txt @@ -92,6 +92,9 @@ % enumeration \usepackage[shortlabels]{enumitem} +% scaleable font (whatever that means) +\usepackage[normalem]{ulem} + \titleformat*{\section}{\fnt\LARGE\bfseries} \titleformat*{\subsection}{\fnt\Large\bfseries} \titleformat*{\subsubsection}{\fnt\Large} From e6ad2d9645f5990a39d935dcf2aecfa9776b14e9 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Fri, 23 Aug 2024 13:39:37 +0300 Subject: [PATCH 59/98] Added QM bra-ket support to Lab generator --- src/gen/lab/lab_preamble.tex | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/gen/lab/lab_preamble.tex b/src/gen/lab/lab_preamble.tex index 74e29eb..3321b4f 100644 --- a/src/gen/lab/lab_preamble.tex +++ b/src/gen/lab/lab_preamble.tex @@ -99,6 +99,9 @@ \titleformat*{\subsection}{\fnt\Large\bfseries} \titleformat*{\subsubsection}{\fnt\Large} +% bra and ket from QM +\usepackage{braket} + \begin{document} \thispagestyle{empty} % [A]:for cropping % ------------ From 78bdd83e2cb923ac2faf54354864d15d09dfeab3 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Fri, 23 Aug 2024 14:07:01 +0300 Subject: [PATCH 60/98] Reworked python error Error contains actual python trace and full module source now --- src/gen/ayano.rs | 11 +++- src/gen/ayano/python.rs | 117 ++++++++++++++++++++++++++++++++++------ src/gen/lab/meta.rs | 3 +- 3 files changed, 114 insertions(+), 17 deletions(-) diff --git a/src/gen/ayano.rs b/src/gen/ayano.rs index 3de843e..cd8fe5c 100644 --- a/src/gen/ayano.rs +++ b/src/gen/ayano.rs @@ -6,6 +6,7 @@ use pyo3::{ types::{PyList, PyTuple}, PyAny, Python, }; +pub use python::TracedPyErr; use rand::Rng; use regex::Regex; use std::{ @@ -137,7 +138,7 @@ impl AyanoBuilder { Ok(()) } - pub fn initialize(self) -> Result { + pub fn initialize(self) -> Result { let Self { code, mut blocks } = self; let token = init(&code.as_str())?; Ok(AyanoExecutor { @@ -157,6 +158,8 @@ pub struct AyanoExecutor { pub enum AyanoError<'b> { #[error("Python error occured: {:?}", .0)] Python(Equivalent), + #[error("Traced python error occured: {}", (.0).0)] + TracedPython(Equivalent), #[error("There's no function for ayano block: {:?}", .0)] NoFunction(AyanoBlock<'b>), #[error(transparent)] @@ -173,6 +176,12 @@ impl From for AyanoError<'_> { } } +impl From for AyanoError<'_> { + fn from(value: TracedPyErr) -> Self { + Equivalent(value).into() + } +} + type AyanoResult<'b, T = ()> = Result>; impl AyanoExecutor { diff --git a/src/gen/ayano/python.rs b/src/gen/ayano/python.rs index 146a943..5f608fc 100644 --- a/src/gen/ayano/python.rs +++ b/src/gen/ayano/python.rs @@ -1,4 +1,9 @@ -use std::{borrow::Cow, path::Path}; +use std::{ + borrow::Cow, + fmt::{Debug, Display}, + fs::read, + path::Path, +}; use pyo3::prelude::*; use rand::random; @@ -6,14 +11,24 @@ use rand::random; static MODULE_NAME: &'static str = "ayano"; #[derive(Debug)] -pub struct InitToken(pub(self) String); +pub struct InitToken { + module_name: Box, + code: Box, // FIX: idk, probably improve on that +} + +fn module_name() -> Box { + format!("{}_{}", MODULE_NAME, random::()).into_boxed_str() +} impl InitToken { - fn new() -> Self { - InitToken(format!("{}_{}", MODULE_NAME, random::())) + fn new(module_name: Box, code: &str) -> Self { + Self { + module_name, + code: Box::from(code), + } } fn module_name(&self) -> &str { - &self.0 + &self.module_name } fn module<'py>(&self, py: Python<'py>) -> Result<&'py PyModule, PyErr> { @@ -21,10 +36,12 @@ impl InitToken { } } -pub fn init(code: &str) -> Result { +pub fn init(code: &str) -> Result { Python::with_gil(move |py| { - let token = InitToken::new(); - PyModule::from_code(py, code, "", token.module_name())?; + let module_name = module_name(); + PyModule::from_code(py, code, "", &module_name) + .map_err(|err| TracedPyErr::from_regular(err, py, code))?; + let token = InitToken::new(module_name, code); Ok(token) }) } @@ -51,17 +68,62 @@ pub fn append_function(target: &mut String, name: &str, code: &str) { } } -pub fn call_function( +#[derive(derive_more::Deref)] +struct CodeString(String); + +impl Debug for CodeString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("") + } +} + +impl Display for CodeString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("\n")?; + for (i, line) in self.0.lines().enumerate() { + writeln!(f, "{i:5}:\t |{line}")?; + } + f.write_str("\n")?; + Ok(()) + } +} + +// TODO that's a temporary solution to actually be able to debug my code +#[derive(Debug, thiserror::Error)] +#[error("Head: {}, Trace: {:?}, Code: {}", .head, .trace, .code)] +pub struct TracedPyErr { + head: PyErr, + trace: Option>, + code: CodeString, // FIX: please change that :skull: +} + +impl TracedPyErr { + fn from_regular(err: PyErr, py: Python<'_>, code: &str) -> Self { + let trace_string = err.traceback(py).map(|trace| trace.format()); + Self { + head: err, + trace: trace_string, + code: CodeString(code.to_owned()), // FIX: please change that + } + } +} + +pub fn call_function( token: &InitToken, name: &str, transform: Transform, execution_path: impl AsRef, ) -> Result where - Err: From, - for<'py> Transform: FnOnce(&'py PyAny, Python<'py>) -> Result, + Err: From + From, + for<'py> Transform: FnOnce(&'py PyAny, Python<'py>) -> Result, { - Python::with_gil(|py| { + fn call_inner<'py>( + token: &InitToken, + name: &str, + execution_path: impl AsRef, + py: Python<'py>, + ) -> Result<&'py PyAny, PyErr> { let main_module = token.module(py)?; let os = PyModule::import(py, "os")?; let cwd = os.getattr("getcwd")?.call0()?; @@ -70,13 +132,38 @@ where let function = main_module.getattr(name)?; let res = function.call((), None)?; chdir.call1((cwd,))?; + Ok(res) + } + Python::with_gil::<_, Result<_, Err>>(|py| { + let res = call_inner(token, name, execution_path, py) + .map_err(|err| TracedPyErr::from_regular(err, py, &token.code))?; Ok(transform(res, py)?) }) } #[cfg(test)] mod tests { - use super::{call_function, init}; + use crate::util::Equivalent; + + use super::*; + + #[derive(Debug, derive_more::From, PartialEq)] + enum DummyError { + Python(Equivalent), + TracedPython(Equivalent), + } + + impl From for DummyError { + fn from(value: PyErr) -> Self { + Self::Python(Equivalent(value)) + } + } + + impl From for DummyError { + fn from(value: TracedPyErr) -> Self { + Self::TracedPython(Equivalent(value)) + } + } macro_rules! init_functions { ($code:expr, [$(($fun_name:expr, $fun_code:expr)), *]) => {{ @@ -123,7 +210,7 @@ mod tests { let token = init_functions!(code, []).expect("Should be able to create module"); // act + assert - let _res = call_function( + let _res: DummyError = call_function( &token, "absent_function", |res, _| Ok::<_, super::PyErr>(res.str()?.to_string()), @@ -140,7 +227,7 @@ mod tests { .expect("Should be able to create module"); // act - let res = call_function( + let res: Result<_, DummyError> = call_function( &token, "ok_function", |res, _| Ok::<_, super::PyErr>(res.str()?.to_string()), diff --git a/src/gen/lab/meta.rs b/src/gen/lab/meta.rs index a4aaf99..b3a9a5e 100644 --- a/src/gen/lab/meta.rs +++ b/src/gen/lab/meta.rs @@ -8,6 +8,7 @@ use pyo3::PyErr; use crate::{ data::{TitleInfo, Token}, + gen::ayano::TracedPyErr, path_engine::PathEngine, }; @@ -44,7 +45,7 @@ impl<'source> SourceMeta<'source, AyanoBuilder> { Ok(res) } - pub fn init_ayano(self) -> Result, PyErr> { + pub fn init_ayano(self) -> Result, TracedPyErr> { Ok(SourceMeta { ayano: self.ayano.initialize()?, footnotes: self.footnotes, From 5b799d12b60a250168de05cd511f4c8c1237da09 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Fri, 23 Aug 2024 17:39:20 +0300 Subject: [PATCH 61/98] Added monospace formatting --- src/data.rs | 1 + src/gen/lab/mod.rs | 1 + src/lexer.rs | 33 +++++++++++++++++++++++++++++++-- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/data.rs b/src/data.rs index 807009c..1cb874c 100644 --- a/src/data.rs +++ b/src/data.rs @@ -139,6 +139,7 @@ pub enum Formatting { Bold, Italic, StrikeThrough, + MonoSpace, } type Tx<'source> = Cow<'source, str>; diff --git a/src/gen/lab/mod.rs b/src/gen/lab/mod.rs index f23f735..c1e92f1 100644 --- a/src/gen/lab/mod.rs +++ b/src/gen/lab/mod.rs @@ -329,6 +329,7 @@ where Some(Formatting::Bold) => ("\\textbf{", "}"), Some(Formatting::Italic) => ("\\textit{", "}"), Some(Formatting::StrikeThrough) => ("\\st{", "}"), + Some(Formatting::MonoSpace) => ("\\texttt{", "}"), }; output.write_str(start)?; if content.contains(['%', '_', '#', '[', ']']) { diff --git a/src/lexer.rs b/src/lexer.rs index 1c09f5c..ecabfa3 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -848,6 +848,32 @@ fn formatting< ))) } +/// Parses monospace formatting declaration +fn monospace< + 'source, + E: ParseError<&'source str> + + ContextError<&'source str> + + FromExternalError<&'source str, KyomatoLexError> + + 'source, +>( + input: &'source str, +) -> IResult<&'source str, Token<'source>, E> { + context( + "monospace formatting", + pair( + multispace0, + delimited(char('`'), take_till1(|c| c == '\n' || c == '`'), char('`')), + ), + ) + .map(|(before, content): (&str, &str)| Token::Paragraph { + formatting: Some(Formatting::MonoSpace), + content: content.trim().into(), + is_newline: before.contains('\n'), + space_before: before.ends_with(' '), + }) + .parse(input) +} + /// Parses inline math, delimited by '$' fn inline_math<'source, E: ParseError<&'source str> + ContextError<&'source str>>( input: &'source str, @@ -977,6 +1003,7 @@ fn paragraph_inner< // However, this still leaves a couple of candidates: // - href (starting with '[') // - formatting (starting with '*', '_' or '~') + // - monospace formatting (starting with '`') // - inline_math (starting with '$') // - reference (starting with "[@") // - footnote_reference (starting with "[^") @@ -990,8 +1017,8 @@ fn paragraph_inner< .char_indices() // Parsing continues until new, Option>)line is reached .take_while(|(_, c)| c != &'\n') - // Possible breakpoints are at characters '[', '*', '_', '~', and '$' - .filter(|(_, c)| ['[', '*', '_', '~', '$'].contains(c)) + // Possible breakpoints are at characters '[', '*', '_', '~', '$' and '`' + .filter(|(_, c)| ['[', '*', '_', '~', '$', '`'].contains(c)) // Now, try a proper parser for each one .map(|(ind, c)| { // at this point, there's definitely something to be parsed @@ -1019,6 +1046,7 @@ fn paragraph_inner< } } '*' | '_' | '~' => formatting(cut_cut_input), + '`' => monospace(cut_cut_input), '$' => inline_math(cut_cut_input), _ => unreachable!(), }; @@ -1803,6 +1831,7 @@ $$ test_ok! {ok_sentence, "\t \nI'm absolutely increative, so here's a bunch of garbage: ]42[t5(2[24tj4-0)jt3_94j03\nand some rest", "I'm absolutely increative, so here's a bunch of garbage: ]42[t5(2[24tj4-0)jt3_94j03", None, "\nand some rest", |} test_ok! {ok_formatting, "\t \nYou can use *bold*!", "You can use", Some(Token::Paragraph{is_newline: false, formatting: Some(Formatting::Italic), content: "bold".into(), space_before: true}), "!", |} + test_ok! {ok_mono, "\t \nPlease use `PathBuf`.", "Please use", Some(Token::Paragraph { is_newline: false, space_before: true, formatting: Some(Formatting::MonoSpace), content: "PathBuf".into() }), ".", |} // FIXME NOT SUPPORTED /* test_ok! {ok_formatting_in_formatting, "\t \n You can even use ~~__double-formatted__~~ things!", "You can even use", Some(Token::Formatted(Formatting::StrikeThrough, Box::new(Token::Formatted(Formatting::Bold, Box::new(Token::text("double-formatted")))))), " things!"} */ From fafc1e4e41f04e7d7ea67af9b315427317eb4096 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Fri, 23 Aug 2024 17:49:20 +0300 Subject: [PATCH 62/98] Reworked meta collection --- src/gen/lab/meta.rs | 4 +++- src/gen/lab/mod.rs | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/gen/lab/meta.rs b/src/gen/lab/meta.rs index b3a9a5e..5eb02a0 100644 --- a/src/gen/lab/meta.rs +++ b/src/gen/lab/meta.rs @@ -28,11 +28,13 @@ pub struct SourceMeta<'source, Ayano> { pub title_info: TitleInfo<'source>, } -impl<'source> SourceMeta<'source, AyanoBuilder> { +impl<'source, Ayano: Default> SourceMeta<'source, Ayano> { fn empty() -> Self { Self::default() } +} +impl<'source> SourceMeta<'source, AyanoBuilder> { pub fn collect<'meta, PEErr: std::error::Error + 'source, PE: PathEngine>( token: &'meta Token<'source>, path_engine: &PE, diff --git a/src/gen/lab/mod.rs b/src/gen/lab/mod.rs index c1e92f1..0021437 100644 --- a/src/gen/lab/mod.rs +++ b/src/gen/lab/mod.rs @@ -2,6 +2,7 @@ mod context; mod meta; pub use context::Context; +use meta::MetaError; pub use meta::SourceMeta; use crate::{ @@ -88,6 +89,9 @@ where PE: PathEngine, PEErr: std::error::Error + 'source, { + // TODO: rename to `CollectionError`, or th + type MetaError = Box; + fn write_to<'meta, 'context, 'token, W: Write + ?Sized>( &self, output: &mut W, @@ -498,6 +502,20 @@ where output.write_str("\n\n\\end{document}")?; Ok(()) } + + fn collect_context<'token>( + &self, + tokens: &'token Token<'source>, + ) -> Result, Self::MetaError> + where + 'source: 'token, + { + Ok(SourceMeta::collect(tokens, &self.path_engine)?.init_ayano()?) + } + + fn default_context(&self) -> Context { + context::Context::default() + } } #[cfg(test)] From da4b8aad2bdcd13bf2957ff75880319fccf837d3 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Fri, 23 Aug 2024 17:50:40 +0300 Subject: [PATCH 63/98] Added negative alignment to tables in laba-latex --- src/gen/lab/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/gen/lab/mod.rs b/src/gen/lab/mod.rs index 0021437..137c126 100644 --- a/src/gen/lab/mod.rs +++ b/src/gen/lab/mod.rs @@ -139,7 +139,7 @@ where let column_format = itertools::intersperse(std::iter::repeat("c").take(columns), "|"); write!(output, "\n\\begin{{table}}[h!]\n\\fnt\n\\begin{{center}}\n")?; - output.write_str("\\begin{tabular}{|")?; + output.write_str("\\hskip-1.5cm\\begin{tabular}{|")?; column_format .map(|s| { output.write_str(s)?; @@ -678,7 +678,7 @@ mod tests { \begin{table}[h!] \fnt \begin{center} - \begin{tabular}{|c|c|c|}\hline + \hskip-1.5cm\begin{tabular}{|c|c|c|}\hline header1 & header2 & header3 \\ \hline \hline cell\_11 & cell\_12 & cell\_13 \\ \hline cell\_21 & cell\_22 & cell\_23 \\ \hline @@ -704,7 +704,7 @@ cell\_21 & cell\_22 & cell\_23 \\ \hline \begin{table}[h!] \fnt \begin{center} - \begin{tabular}{|c|c|c|}\hline + \hskip-1.5cm\begin{tabular}{|c|c|c|}\hline header1 & header2 & header3 \\ \hline \hline cell\_11 & cell\_12 & cell\_13 \\ \hline cell\_21 & cell\_22 & cell\_23 \\ \hline @@ -930,7 +930,7 @@ r#" \begin{table}[h!] \fnt \begin{center} -\begin{tabular}{|c|c|c|}\hline +\hskip-1.5cm\begin{tabular}{|c|c|c|}\hline 1 & 2 & 3 \\ \hline \hline 4 & 5 & 6 \\ \hline \end{tabular} From acdcd5693ec14331ce7e84fd5612677062235fa5 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Fri, 23 Aug 2024 17:52:10 +0300 Subject: [PATCH 64/98] Handled '\' and '%' in laba-latex paragraphs Most likely you do not intend to define latex comment while writing '%' inside the paragraph --- src/gen/lab/mod.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/gen/lab/mod.rs b/src/gen/lab/mod.rs index 137c126..7c88b51 100644 --- a/src/gen/lab/mod.rs +++ b/src/gen/lab/mod.rs @@ -337,11 +337,12 @@ where }; output.write_str(start)?; if content.contains(['%', '_', '#', '[', ']']) { - // there are percentage signs in the paragraph - // most certainly this was is not intended to make a latex comment (because why?) + // there are special symbols in the paragraph + // most certainly they were not intended to be special latex symbols (because why?) // TODO add an option to opt this out // TODO add multiple replacements at once - let content_inner = content.replace('%', "\\%"); + let content_inner = content.replace('\\', "\\\\"); + let content_inner = content_inner.replace('%', "\\%"); let content_inner = content_inner.replace('#', "\\#"); content = content_inner.replace('_', "\\_").into(); } From 99645be53608495c5fee1556373d406cb56ac98c Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Fri, 23 Aug 2024 17:53:46 +0300 Subject: [PATCH 65/98] Handled missing language to "minted" blocks Apparently, language parameter is mandatory, so "text" should be passed in by default --- src/gen/lab/mod.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/gen/lab/mod.rs b/src/gen/lab/mod.rs index 7c88b51..d57af6a 100644 --- a/src/gen/lab/mod.rs +++ b/src/gen/lab/mod.rs @@ -373,14 +373,11 @@ where } Token::Multiple { tokens } => self.write_tokens_to(output, meta, context, tokens)?, Token::CodeBlock { code, language } => { - if let Some(language) = language { - writeln!( - output, - "\n\\begin{{minted}}[linenos, mathescape, autogobble, breaklines]{{{language}}}\n{code}\n\\end{{minted}}\n" - )?; - } else { - writeln!(output, "\n\\begin{{minted}}\n{code}\n\\end{{minted}}\n")?; - } + let language = language.as_deref().unwrap_or("text"); + writeln!( + output, + "\n\\begin{{minted}}[linenos, mathescape, autogobble, breaklines]{{{language}}}\n{code}\n\\end{{minted}}\n" + )?; } }; if let Some(new_value) = token.is_newline_allergic() { From 235fbadda78e8354f7f274d906b50a21615ee532 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Fri, 23 Aug 2024 17:55:41 +0300 Subject: [PATCH 66/98] Added pagebreak before code section in laba-latex --- src/gen/lab/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gen/lab/mod.rs b/src/gen/lab/mod.rs index d57af6a..7564922 100644 --- a/src/gen/lab/mod.rs +++ b/src/gen/lab/mod.rs @@ -482,6 +482,7 @@ where output.write_str(" \\\\\n")?; Ok(()) } + self.write_to(output, meta, context, &Token::PageDiv)?; let blocks_section_header = Token::Header { order: 1, content: meta From 04e87496e36270c4df40ed65f453a78d13f73943 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Fri, 23 Aug 2024 17:59:25 +0300 Subject: [PATCH 67/98] Allowed `ayano` to parse output as a token --- src/gen/ayano.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/gen/ayano.rs b/src/gen/ayano.rs index cd8fe5c..f663c06 100644 --- a/src/gen/ayano.rs +++ b/src/gen/ayano.rs @@ -1792,13 +1792,13 @@ fn parse_ayano(python_output: &PyAny) -> Result, ParsingError> { } } } - // any sort of parsing had failed - return regular text - Ok(Token::Paragraph { - is_newline: false, // TODO probably add that to block too - formatting: None, - space_before: false, - content: Cow::Owned(python_output.to_string()), - }) + // any sort of parsing had failed - try parsing inner token + let output_str = python_output.to_string(); + let (rest, res) = crate::lexer::inner_lex(&output_str)?; + if !rest.is_empty() { + eprintln!("Table cell failed to parse entirely: {output_str}"); + } + Ok(res.to_static_token()) } #[cfg(test)] From 42a1c490450b40810a8de72d8753099b5eb1058f Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Fri, 23 Aug 2024 18:05:12 +0300 Subject: [PATCH 68/98] Addition to "Reworked meta collection" commit I was quite sure that I've commited it... Oh well, still getting used to `nvim` --- src/gen/mod.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/gen/mod.rs b/src/gen/mod.rs index cdffa13..e100ad5 100644 --- a/src/gen/mod.rs +++ b/src/gen/mod.rs @@ -42,7 +42,10 @@ impl From for GenerationError<'_> { pub type Res<'source> = Result<(), GenerationError<'source>>; // TODO make this thing modular (probably use tower to handle writes or th) +// TODO REMOVE LIFETIME FROM HERE!!! pub trait OutputGenerator<'source, Meta, Context> { + type MetaError: Into>; + fn write_to<'meta, 'context, 'token, W: Write + ?Sized>( &self, output: &mut W, @@ -81,4 +84,13 @@ pub trait OutputGenerator<'source, Meta, Context> { ) -> Res<'source> { Ok(()) } + + fn collect_context<'token>( + &self, + tokens: &'token Token<'source>, + ) -> Result + where + 'source: 'token; + + fn default_context(&self) -> Context; } From 86bb0ad4c22f09ef5ff6ce6387c4d0ed1374062d Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Fri, 23 Aug 2024 18:08:04 +0300 Subject: [PATCH 69/98] `PrimitiveDecimal` enhancements and edge-cases --- src/gen/ayano.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/gen/ayano.rs b/src/gen/ayano.rs index f663c06..d6f3017 100644 --- a/src/gen/ayano.rs +++ b/src/gen/ayano.rs @@ -1957,6 +1957,7 @@ mod parsing_tests { } } +#[derive(Debug)] struct PrimitiveDecimal { digits: Vec, zero: isize, @@ -2049,7 +2050,7 @@ impl PrimitiveDecimal { .iter() .position(|ch| ch != &'0') .map(|pos| self.zero.wrapping_sub_unsigned(pos)) - .unwrap_or(isize::MIN) + .unwrap_or(0) } pub fn get_digit(&self, digit: isize) -> char { @@ -2223,6 +2224,7 @@ mod primitive_decimal_tests { test! {exponent5, "1E-3", "1", -3} test! {exponent6, "-0001.001E-1", "0001001", 2, -} test! {exponent7, "-0001.001E10", "0001001", 13, -} + test! {edge1, "0.0", "00", 0} } mod to_string { @@ -2313,9 +2315,9 @@ mod primitive_decimal_tests { }; } - test! {zero1, "0", isize::MIN} - test! {zero2, "-0.00", isize::MIN} - test! {zero3, "000.0000", isize::MIN} + test! {zero1, "0", 0} + test! {zero2, "-0.00", 0} + test! {zero3, "000.0000", 0} test! {one1, "-00001.00000", 0} test! {one2, "00010.00000E-1", 0} test! {one3, "-0.0001e4", 0} @@ -2541,6 +2543,7 @@ mod value_error_tests { test! {src2, ["1.3354792", "0.02417797"] => ["1.335", "0.024"]} test! {src3, ["157863.44", "377.36711"] => ["157900", "400"]} test! {src4, ["0.1437765", "0.019734"] => ["0.144", "0.020"]} + test! {edge1, ["0.0", "0.0"] => ["0", "0"]} } fn find_args<'l>(input: &'l str) -> impl IntoIterator { From 6ea3288cf086229b2c0d4791d161889e282e74ca Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Fri, 23 Aug 2024 18:09:32 +0300 Subject: [PATCH 70/98] Footnote content parsing fix --- src/data.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/data.rs b/src/data.rs index 1cb874c..2f59c28 100644 --- a/src/data.rs +++ b/src/data.rs @@ -510,7 +510,6 @@ impl<'source> Token<'source> { | Token::InlineMath { .. } | Token::Reference { .. } | Token::FootnoteReference { .. } - | Token::FootnoteContent { .. } | Token::Error { .. } => Some(false), Token::PageDiv | Token::Header { .. } @@ -519,7 +518,7 @@ impl<'source> Token<'source> { | Token::Figure { .. } | Token::List { .. } | Token::CodeBlock { .. } => Some(true), - Token::Ayano { .. } | Token::Multiple { .. } => None, + Token::Ayano { .. } | Token::Multiple { .. } | Token::FootnoteContent { .. } => None, } } } From 8003254d1e2c8e83e360c213854d0346c41b76b6 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Fri, 23 Aug 2024 18:10:10 +0300 Subject: [PATCH 71/98] Allowed `'0'` in idents --- src/lexer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lexer.rs b/src/lexer.rs index ecabfa3..b4d9c7a 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -203,7 +203,7 @@ fn div_page<'source, E: ParseError<&'source str> + ContextError<&'source str>>( fn ident_kernel<'source, E: ParseError<&'source str>>( input: &'source str, ) -> IResult<&'source str, &'source str, E> { - take_till1(|c: char| !matches!(c, 'a'..='z' | 'A'..='Z' | '1'..='9' | '_' | ':')).parse(input) + take_till1(|c: char| !matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | ':')).parse(input) } /// Parses header From 1f61332ef3c20d3676eaa77f4d3faae9c7ce3a4d Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Fri, 23 Aug 2024 18:15:06 +0300 Subject: [PATCH 72/98] Some other part of meta collection rework --- src/lib.rs | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6d2bbe1..b350e81 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,29 +26,34 @@ pub fn lex<'source>( })?; Ok((title_info, tokens)) } -pub fn gen<'token, 'source: 'token>( +pub fn gen<'token, 'source: 'token, Meta, Context>( token: &'token data::Token<'source>, title_info: impl Into>>, mut output: impl std::io::Write, -) -> Result<(), Box> { - let buf = gen_to_string(token, title_info)?; +) -> Result<(), Box> +where + Meta: MetaExt<'source>, +{ + let buf = gen_to_string(token, title_info, generator)?; // TODO make a transformer struct output.write_all(buf.as_bytes())?; Ok(()) } -pub fn gen_to_string<'token, 'source: 'token>( +pub fn gen_to_string<'token, 'source: 'token, Meta, Context>( token: &'token data::Token<'source>, title_info: impl Into>>, ) -> Result> { - let path_engine = path_engine::primitive(); - let mut meta = gen::lab::SourceMeta::collect(&token, &path_engine)?.init_ayano()?; - let generator = gen::lab::LabaLatex::new(path_engine); +) -> Result> +where + Meta: MetaExt<'source>, +{ + let mut meta = generator.collect_context(token).map_err(|b| b.into())?; if let Some(title_info) = title_info.into() { - meta.title_info = title_info; + meta.set_title_info(title_info); // TODO make title declarations tokens too! } let mut buf = String::new(); generator.write_preamble(&mut buf, &meta)?; - let mut context = gen::lab::Context::default(); + let mut context = generator.default_context(); generator.write_to(&mut buf, &meta, &mut context, token)?; generator.write_postamble(&mut buf, &meta, &mut context)?; Ok(buf) From 0aaf494eabeadcce528d6df84170d370bee135c3 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Fri, 23 Aug 2024 18:29:07 +0300 Subject: [PATCH 73/98] Random additional tests and formatting --- src/gen/ayano.rs | 7 +++++-- src/gen/lab/meta.rs | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/gen/ayano.rs b/src/gen/ayano.rs index d6f3017..073ed05 100644 --- a/src/gen/ayano.rs +++ b/src/gen/ayano.rs @@ -2597,6 +2597,7 @@ fn find_args<'l>(input: &'l str) -> impl IntoIterator Single, Double, } + // TODO rewrite this to use loop, it nests soo deep omg fn inner<'l>( state: State<'l>, rest: &'l str, @@ -2775,11 +2776,11 @@ fn find_args<'l>(input: &'l str) -> impl IntoIterator return false; } if rest.starts_with(|c| len > 0 || c != '=') { - let next_char = value_rest + let next_char = rest .char_indices() .nth(1) // I need an index of a 2nd character here .map(|(i, _)| i) - .unwrap_or(value_rest.len()); // If there's no 2nd character, inner_rest should be empty + .unwrap_or(rest.len()); // If there's no 2nd character, inner_rest should be empty let inner_rest = &rest[next_char..]; return inner( State::InsideValue { @@ -2865,6 +2866,8 @@ mod find_args_tests { test! {edge1, r#"src = 'path/to/image.jpg', caption = 'On this figure you can see "literally nothing"', ident = "meow""#, [("src", "\'path/to/image.jpg\'"), ("caption", r#"'On this figure you can see "literally nothing"'"#), ("ident", "\"meow\"")]} + test! {edge2, r#"src="data/own/bg.csv", ident="own_bg", caption="Результати вимірювань фону""#, + [("src", "\"data/own/bg.csv\""), ("ident", "\"own_bg\""), ("caption", r#""Результати вимірювань фону""#)]} } fn parse_columns<'l>(input: &'l str) -> Vec> { diff --git a/src/gen/lab/meta.rs b/src/gen/lab/meta.rs index 5eb02a0..9e4a491 100644 --- a/src/gen/lab/meta.rs +++ b/src/gen/lab/meta.rs @@ -4,7 +4,6 @@ use std::{ }; use itertools::Itertools; -use pyo3::PyErr; use crate::{ data::{TitleInfo, Token}, From 2aba712c8d85bf34a306fa40562038b63587283c Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Fri, 23 Aug 2024 18:34:01 +0300 Subject: [PATCH 74/98] Added new generator: Compact Latex Designed to generate latex that compiles into very information-dense document. Was implemented for the sole reason of generating cheatsheets --- kyomato/src/main.rs | 42 +++++- src/gen/comp/context.rs | 6 + src/gen/comp/mod.rs | 279 ++++++++++++++++++++++++++++++++++++++ src/gen/comp/preamble.tex | 114 ++++++++++++++++ src/gen/mod.rs | 3 + 5 files changed, 438 insertions(+), 6 deletions(-) create mode 100644 src/gen/comp/context.rs create mode 100644 src/gen/comp/mod.rs create mode 100644 src/gen/comp/preamble.tex diff --git a/kyomato/src/main.rs b/kyomato/src/main.rs index b732a98..071eafa 100644 --- a/kyomato/src/main.rs +++ b/kyomato/src/main.rs @@ -5,7 +5,8 @@ use std::{ use clap::{Parser, ValueEnum}; use kyomato_lib::path_engine::{ - base_gen, fs_check, InformativeError, MarkerError, PathEngine, PathEngineError, PathEngineExt, + self, base_gen, fs_check, InformativeError, MarkerError, PathEngine, PathEngineError, + PathEngineExt, }; #[derive(Debug, ValueEnum, Clone, Default, derive_more::Display)] @@ -13,6 +14,8 @@ enum Generator { #[default] #[display(fmt = "lab-protocol")] LabProtocol, + #[display(fmt = "compact")] + Compact, } #[derive(Debug, Parser)] @@ -86,6 +89,7 @@ fn main() { .with_check(fs_check) .with_ayano() .with_error_type::(), + config.generator, ), (false, None, None, Some(image_directory)) => main_inner( input, @@ -94,6 +98,7 @@ fn main() { .with_image_folder(image_directory) .with_ayano() .with_error_type::(), + config.generator, ), (false, None, Some(input_file_dir), None) => main_inner( input, @@ -102,6 +107,7 @@ fn main() { .with_source_folder(input_file_dir) .with_ayano() .with_error_type::(), + config.generator, ), (false, None, Some(input_file_dir), Some(image_directory)) => main_inner( input, @@ -111,6 +117,7 @@ fn main() { .with_image_folder(image_directory) .with_ayano() .with_error_type::(), + config.generator, ), (false, Some(execution_dir), None, None) => { main_inner( @@ -122,6 +129,7 @@ fn main() { })) .with_ayano() .with_error_type::(), + config.generator, ) } (false, Some(execution_dir), None, Some(image_directory)) => { @@ -135,6 +143,7 @@ fn main() { .with_image_folder(image_directory) .with_ayano() .with_error_type::(), + config.generator, ) } (false, Some(execution_dir), Some(input_file_dir), None) => { @@ -148,6 +157,7 @@ fn main() { .with_source_folder(input_file_dir) .with_ayano() .with_error_type::(), + config.generator, ) } (false, Some(execution_dir), Some(input_file_dir), Some(image_directory)) => { @@ -162,6 +172,7 @@ fn main() { .with_image_folder(image_directory) .with_ayano() .with_error_type::(), + config.generator, ) } (true, None, None, None) => main_inner( @@ -170,6 +181,7 @@ fn main() { .with_check(fs_check) .with_ayano() .with_error_type::(), + config.generator, ), (true, None, None, Some(image_directory)) => main_inner( input, @@ -178,6 +190,7 @@ fn main() { .with_image_folder(image_directory) .with_ayano() .with_error_type::(), + config.generator, ), (true, None, Some(input_file_dir), None) => main_inner( input, @@ -186,6 +199,7 @@ fn main() { .with_source_folder(input_file_dir) .with_ayano() .with_error_type::(), + config.generator, ), (true, None, Some(input_file_dir), Some(image_directory)) => main_inner( input, @@ -195,6 +209,7 @@ fn main() { .with_image_folder(image_directory) .with_ayano() .with_error_type::(), + config.generator, ), (true, Some(execution_dir), None, None) => { main_inner( @@ -206,6 +221,7 @@ fn main() { })) .with_ayano() .with_error_type::(), + config.generator, ) } (true, Some(execution_dir), None, Some(image_directory)) => { @@ -219,6 +235,7 @@ fn main() { .with_image_folder(image_directory) .with_ayano() .with_error_type::(), + config.generator, ) } (true, Some(execution_dir), Some(input_file_dir), None) => { @@ -232,6 +249,7 @@ fn main() { .with_source_folder(input_file_dir) .with_ayano() .with_error_type::(), + config.generator, ) } (true, Some(execution_dir), Some(input_file_dir), Some(image_directory)) => { @@ -246,12 +264,17 @@ fn main() { .with_image_folder(image_directory) .with_ayano() .with_error_type::(), + config.generator, ) } } } -fn main_inner>(input: String, path_engine: PE) { +fn main_inner>( + input: String, + path_engine: PE, + generator: Generator, +) { // we'll always write into stdout // std opting-out was relevant due to directory of source file should be considered by path engine // output can go anywhere and nothing should depend on it @@ -260,10 +283,17 @@ fn main_inner>(input: String, path // that's a transformer, since `Kyomato` works with `std::fmt::Write` // it's able to recover IO errors, you'll see let mut fmt_output = kyomato_lib::FmtToIo::new(output); - // that's a helper function to write with `LabaLatex`, as it's the only transformer for now - // TODO make more general - kyomato_lib::LabaLatex::parse_and_write(path_engine, &input, &mut fmt_output) - .expect("Error while writing output"); + let res = match generator { + Generator::LabProtocol => { + kyomato_lib::LabaLatex::parse_and_write(path_engine, &input, &mut fmt_output) + } + Generator::Compact => { + kyomato_lib::CompLatex::parse_and_write(path_engine, &input, &mut fmt_output) + } + }; + if let Err(error) = res { + panic!("Failed to write output; Error debug: {error:?}\nError display: {error}\n"); + } // now, try recovering errors from that fancy writer let (mut output, errors) = fmt_output.into_parts(); let mut errors = errors.into_iter(); diff --git a/src/gen/comp/context.rs b/src/gen/comp/context.rs new file mode 100644 index 0000000..d9ede45 --- /dev/null +++ b/src/gen/comp/context.rs @@ -0,0 +1,6 @@ +#[derive(Debug, smart_default::SmartDefault)] +pub struct Context { + pub inside_caption: bool, + #[default(true)] + pub allergic_to_newline: bool, +} diff --git a/src/gen/comp/mod.rs b/src/gen/comp/mod.rs new file mode 100644 index 0000000..7863a89 --- /dev/null +++ b/src/gen/comp/mod.rs @@ -0,0 +1,279 @@ +mod context; + +pub use context::Context; + +use crate::{ + data::{self, Formatting, Token}, + path_engine::PathEngine, +}; +use std::{borrow::Borrow, convert::Infallible, fmt::Write, marker::PhantomData}; + +use super::{GenerationError, OutputGenerator, Res}; + +#[derive(Debug)] +pub struct CompLatex<'source, PE, PEErr> { + path_engine: PE, + _phantom: PhantomData<(&'source (), PEErr)>, +} + +impl<'source, PE: PathEngine, PEErr> CompLatex<'source, PE, PEErr> { + pub fn new(path_engine: PE) -> Self { + Self { + path_engine, + _phantom: PhantomData, + } + } + + fn write_table_row<'meta, 'context, 'token: 'context, W: Write + ?Sized>( + &self, + _output: &mut W, + _context: &'context mut Context, + _cells: impl IntoIterator>, + ) -> Res<'source> + where + 'source: 'meta + 'context + 'token, + PE: PathEngine, + PEErr: std::error::Error + 'source, + { + Ok(()) + } + + pub fn parse_and_write( + path_engine: PE, + input: &'source str, + output: &mut W, + ) -> Result<(), Box> + where + PE: PathEngine, + PEErr: std::error::Error + 'source, + { + let (_, tokens) = crate::parse_all(input)?; + let generator: CompLatex<'source, PE, PEErr> = Self::new(path_engine); + generator.write_preamble(output, &())?; + let mut context = Context::default(); + generator.write_to(output, &(), &mut context, &tokens)?; + generator.write_postamble(output, &(), &mut context)?; + Ok(()) + } +} + +#[derive(Debug, thiserror::Error)] +#[error("A dummy error, should be never thrown")] +pub struct DummyMetaError; + +impl<'source, PE, PEErr> OutputGenerator<'source, (), Context> for CompLatex<'source, PE, PEErr> +where + PE: PathEngine, + PEErr: std::error::Error + 'source, +{ + type MetaError = DummyMetaError; + + fn write_to<'meta, 'context, 'token, W: Write + ?Sized>( + &self, + output: &mut W, + _: &(), + context: &'context mut Context, + token: &'token Token<'source>, + ) -> Res<'source> + where + 'source: 'meta + 'context + 'token, + 'token: 'context, + { + match token { + Token::PageDiv => { + output.write_str("\n\\clearpage\n")?; + } + Token::Header { order, content } => { + let command = match order { + 0..=4 => "subparagraph", + _ => "textbf", + }; + write!(output, "\n\\{command}{{{content}}}\n")?; + } + Token::DisplayMath { content, .. } => { + write!(output, "\n$$\n{content}\n$$\n")?; + } + Token::Table { header, cells, .. } => { + // ignoring tables + } + Token::Figure { + src_name, width, .. + } => { + output.write_str("\n\\begin{figure}[h!]\n\\centering\n\\includegraphics[width=")?; + if let Some(width) = width.as_ref() { + output.write_str(&width.to_string())? + } else { + output.write_str("0.3")?; + } + output.write_str("\\textwidth]{")?; + let src_path = self + .path_engine + .image(src_name.as_ref(), None) + .map_err(|err| GenerationError::Path(Box::new(err)))?; + output.write_str(src_path.to_string_lossy().borrow())?; // FIXME non-utf paths *might* cause an issue here + output.write_str("}\n\\end{figure}\n")?; + } + Token::Ayano { .. } => { + // do nothing, lol + } + Token::List { list_type, content } => { + let start: &str = match list_type { + crate::data::ListType::Bullet => "\\begin{itemize}", + crate::data::ListType::Num + | crate::data::ListType::Cyrillic + | crate::data::ListType::Latin => "\\begin{enumerate}", + crate::data::ListType::Roman => { + unimplemented!("Roman numeral lists are not supported for now") + } + }; + let end: &str = match list_type { + crate::data::ListType::Bullet => "\\end{itemize}", + crate::data::ListType::Num + | crate::data::ListType::Cyrillic + | crate::data::ListType::Latin => "\\end{enumerate}", + crate::data::ListType::Roman => { + unimplemented!("Roman numeral lists are not supported for now") + } + }; + let mut ident_producer = match list_type { + data::ListType::Bullet | data::ListType::Num => None, + t @ data::ListType::Latin + | t @ data::ListType::Cyrillic + | t @ data::ListType::Roman => Some(crate::lexer::_list_item_generator(&t)), + }; + output.write_str("\n")?; + output.write_str(start)?; + for item in content { + if let Some(ident_producer) = ident_producer.as_mut() { + output.write_str("\n\\item[")?; + output.write_str(ident_producer.next().expect("List should not have more elements than there are items in the iterator"))?; + output.write_str("] ")?; + } else { + output.write_str("\n\\item ")?; + } + self.write_to(output, &(), context, item)?; + } + output.write_str("\n")?; + output.write_str(end)?; + } + Token::InlineMath { + content, + space_before, + } => { + if *space_before { + output.write_char(' ')?; + } + write!(output, "${content}$")?; + } + Token::Reference { .. } => { + // do nothing, lol + } + Token::FootnoteReference { .. } => { + // yeah, same here - do nothing + } + // might seem weird at first, but these are basically useless at this point, as their content was presumably collected into context + Token::FootnoteContent { .. } => {} + Token::Error { message } => { + eprintln!("{message}"); + // TODO add representation in the document + } + Token::Paragraph { + is_newline, + formatting, + content, + space_before, + } => { + let mut content = content.clone(); + if *is_newline { + if context.allergic_to_newline { + output.write_str("\n\\tb ")?; + } else { + output.write_str("\n\\tbln ")?; + } + } else if *space_before { + output.write_char(' ')?; + } + let (start, end) = match formatting { + None => ("", ""), + Some(Formatting::Bold) => ("\\textbf{", "}"), + Some(Formatting::Italic) => ("\\textit{", "}"), + Some(Formatting::StrikeThrough) => ("\\st{", "}"), + Some(Formatting::MonoSpace) => ("\\texttt{", "}"), + }; + output.write_str(start)?; + if content.contains(['%', '_', '#', '[', ']']) { + // there are special symbols in the paragraph + // most certainly they were not intended to be special latex symbols (because why?) + // TODO add an option to opt this out + // TODO add multiple replacements at once + let content_inner = content.replace('\\', "\\\\"); + let content_inner = content_inner.replace('%', "\\%"); + let content_inner = content_inner.replace('#', "\\#"); + content = content_inner.replace('_', "\\_").into(); + } + if context.inside_caption { + // We are generating text that was inside table/figure caption + // meaning, we must un-escape double quotes + let mut last_ind = 0; + for (ind, _) in content.match_indices("\\\"") { + output.write_str(&content[last_ind..ind])?; + output.write_char('\"')?; + last_ind = ind + 2; + } + output.write_str(&content[last_ind..])?; + } else { + output.write_str(&content)?; + } + output.write_str(end)?; + } + Token::Href { + url, + display, + space_before, + } => { + if *space_before { + output.write_char(' ')?; + } + write!(output, "\\href{{{url}}}{{{display}}}")?; + } + Token::Multiple { tokens } => self.write_tokens_to(output, &(), context, tokens)?, + Token::CodeBlock { code, language } => { + // ignore + } + }; + if let Some(new_value) = token.is_newline_allergic() { + context.allergic_to_newline = new_value; + } + Ok(()) + } + + fn write_preamble<'meta, W: Write + ?Sized>(&self, output: &mut W, _: &()) -> Res<'source> { + const PREAMBLE: &str = include_str!("preamble.tex"); + output.write_str(PREAMBLE)?; + Ok(()) + } + + fn write_postamble<'meta, 'context, W: Write + ?Sized>( + &self, + output: &mut W, + _: &(), + _context: &'context mut Context, + ) -> Res<'source> { + output.write_str("\n\n\\end{document}")?; + Ok(()) + } + + fn collect_context<'token>( + &self, + _tokens: &'token Token<'source>, + ) -> Result<(), Self::MetaError> + where + 'source: 'token, + { + Ok(()) + } + + fn default_context(&self) -> Context { + context::Context::default() + } +} diff --git a/src/gen/comp/preamble.tex b/src/gen/comp/preamble.tex new file mode 100644 index 0000000..ae4ccf1 --- /dev/null +++ b/src/gen/comp/preamble.tex @@ -0,0 +1,114 @@ +\documentclass[a4paper, twocolumn]{article} +\usepackage{graphicx, hyperref, xcolor, gensymb, amssymb, mathtools, wrapfig, mathtools, microtype, lastpage, caption, titlesec, paracol, longtable, booktabs, cancel} +\usepackage[T2A, T1]{fontenc} +\usepackage[utf8]{inputenc} +\usepackage[russian, ukrainian]{babel} +\hypersetup{ + colorlinks, + linkcolor={blue!20!black}, + citecolor={blue!50!black}, + urlcolor={blue!80!black} +} + +\usepackage[left = 4mm, right = 4mm, top=4mm, bottom=4mm, bindingoffset=0cm]{geometry} + +% change line spacing +\renewcommand{\baselinestretch}{0.5} +\usepackage{enumitem} +\setlist{nosep} % or \setlist{noitemsep} to leave space around whole list + +% КОМАНДИ + +\newcommand{\fnt}{ +\fontshape{n} +\fontsize{8pt} {8pt} +\linespread{0.05} +\selectfont +} % нормативний шрифт +\newcommand{\capfnt}{ + \fnt +} % норматичний шрифт для підписів (тимчасово співпадає зі звичайним шрифтом) + +\newcommand{\tb}{ + \hspace*{1mm} +} % відступ у 5 символів "x" відповідно до ДСТУ +\newcommand{\tbln}{ + \newline + \tb +} % те ж саме, але з переносом рядка +\newcommand{\tbsp}{ + \vspace*{0.2ex} + \tbln +} % те ж саме, але з додатковим відступом між рядками + +% бібліографія +\usepackage[square,sort,comma,numbers]{natbib} +\renewcommand{\bibsection}{} +\usepackage{totcount} +\newtotcounter{citnum} +% лічильник цитувань +\def\oldbibitem{} \let\oldbibitem=\bibitem +\def\bibitem{\stepcounter{citnum}\oldbibitem} + +% лічильник зображень +\newtotcounter{graphnum} +\def\oldincludegraphics{} \let\oldincludegraphics=\includegraphics +\def\includegraphics{\stepcounter{graphnum}\oldincludegraphics} + +% лічильник таблиць (НЕАВТОМАТИЧНИЙ!!!) +\newtotcounter{tabnum} + +% лічильник додатків (НЕАВТОМАТИЧНИЙ!!!) +\newtotcounter{dodnum} + +% нумерація сторінок +\usepackage{fancyhdr} +\pagestyle{fancy} +\fancyhf{} +\renewcommand{\headrulewidth}{0pt} +\setlength{\headheight}{15.3pt} +\fancyhead[R]{\fnt \thepage} + +% виноски +\renewcommand{\thefootnote}{\large\arabic{footnote})~} +\renewcommand{\footnoterule}{\rule{20mm}{0.4pt} \vspace*{0.5ex}} +% \newcommand{\vyn}[2]{ +% \footnote[#1]{\large #2} +% } % створити виноску + +\let\oldFootnote\footnote +\renewcommand{\footnote}[1]{ + \oldFootnote{\large #1} +} % перегрузка виноски + +\providecommand{\tightlist}{% + \setlength{\itemsep}{-4pt}\setlength{\parskip}{-4pt}} + +% підписи малюнків/таблиць +\captionsetup[figure]{name={\fnt Рисунок~},labelsep=period} +\captionsetup[table]{name={\fnt Таблиця~},labelsep=period} + +% markdown strikeouts +\usepackage{soul} + +% codeblocks +\usepackage{minted} + +% enumeration +% \usepackage[shortlabels]{enumitem} + +% scaleable font (whatever that means) +\usepackage[normalem]{ulem} + +\titleformat*{\section}{\fnt\LARGE\bfseries} +\titleformat*{\subsection}{\fnt\Large\bfseries} +\titleformat*{\subsubsection}{\fnt\Large} + +% bra and ket from QM +\usepackage{braket} + +% multicol magic +\usepackage{multicol} + +\begin{document} +\fnt diff --git a/src/gen/mod.rs b/src/gen/mod.rs index e100ad5..5a8f8d8 100644 --- a/src/gen/mod.rs +++ b/src/gen/mod.rs @@ -1,6 +1,9 @@ mod ayano; pub mod lab; +/// Compact latex format +pub mod comp; + use std::fmt::Write; use itertools::Itertools; From d1e83d432c2f835db0c1082956369366195625d8 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Fri, 23 Aug 2024 18:37:12 +0300 Subject: [PATCH 75/98] Random formatting a some leftover syntax I forgot This commit and a couple before represent a set of random additions I've made to the code, while utilizing it. I guess, I've learned my lesson now, so I'll make sure to commit these changes ASAP instead. --- kyomato/src/main.rs | 2 +- src/lexer.rs | 66 ++++++++++++++++++++++++--------------------- src/lib.rs | 32 ++++++++++++++++++++-- tests/transform.rs | 25 ++++++++++++++--- 4 files changed, 88 insertions(+), 37 deletions(-) diff --git a/kyomato/src/main.rs b/kyomato/src/main.rs index 071eafa..59b64e9 100644 --- a/kyomato/src/main.rs +++ b/kyomato/src/main.rs @@ -39,7 +39,7 @@ struct Config { #[arg(long)] debug_paths: bool, /// Current directory a valid path for files. If provided without a value, will get current directory from environment. - /// + /// /// This is intended to be set a directory of the initial command caller, in case they rely on some file local to them personally #[arg(long = "exec")] execution_directory: Option>, diff --git a/src/lexer.rs b/src/lexer.rs index b4d9c7a..67edaca 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -2374,7 +2374,7 @@ mod title_info_tests { test_ok! {ok_empty, "", [], ""} test_ok! {ok_no_fields, "---\n---\n", [], "\n"} test_ok! {ok_single_field, "---\nauthor-line3 : дяч дзучунович\n---\n", [author_line3: "дяч дзучунович"], "\n"} - test_ok! {ok_all_fields, r"--- + test_ok! {ok_all_fields, r#"--- header-line1: Навч. заклад header-line2: Факультет document-type: ПРОТОКОЛ @@ -2388,21 +2388,25 @@ author-line3: (perfectly still) date: вербень 2077 prof: me :idk: --- -", [ - header_line1: "Навч. заклад", - header_line2: "Факультет", - document_type: "ПРОТОКОЛ", - title_line1: "Виконання марної роботи", - title_line2: "з історії стародавнього Єгипту", - title_line3: "(пинальна частина)", - title_line4: "НАЗВА РОБОТИ", - author_line1: "виконувало:", - author_line2: "дяч дзучунович", - author_line3: "(perfectly still)", - date: "вербень 2077", - prof: "me :idk:" - ], "\n"} - test_ok! {ok_all_fields_permutated, r"--- +"#, [ + header_line1: "Навч. заклад", + header_line2: "Факультет", + document_type: "ПРОТОКОЛ", + title_line1: "Виконання марної роботи", + title_line2: "з історії стародавнього Єгипту", + title_line3: "(пинальна частина)", + title_line4: "НАЗВА РОБОТИ", + author_line1: "виконувало:", + author_line2: "дяч дзучунович", + author_line3: "(perfectly still)", + date: "вербень 2077", + prof: "me :idk:" + ], + "\n" + } + + test_ok! {ok_all_fields_permutated, +r#"--- author-line2: дяч дзучунович header-line1: Навч. заклад title-line2: з історії стародавнього Єгипту @@ -2416,18 +2420,20 @@ title-line1: Виконання марної роботи prof: me :idk: title-line3: (пинальна частина) --- -", [ - header_line1: "Навч. заклад", - header_line2: "Факультет", - document_type: "ПРОТОКОЛ", - title_line1: "Виконання марної роботи", - title_line2: "з історії стародавнього Єгипту", - title_line3: "(пинальна частина)", - title_line4: "НАЗВА РОБОТИ", - author_line1: "виконувало:", - author_line2: "дяч дзучунович", - author_line3: "(perfectly still)", - date: "вербень 2077", - prof: "me :idk:" - ], "\n"} +"#, [ + header_line1: "Навч. заклад", + header_line2: "Факультет", + document_type: "ПРОТОКОЛ", + title_line1: "Виконання марної роботи", + title_line2: "з історії стародавнього Єгипту", + title_line3: "(пинальна частина)", + title_line4: "НАЗВА РОБОТИ", + author_line1: "виконувало:", + author_line2: "дяч дзучунович", + author_line3: "(perfectly still)", + date: "вербень 2077", + prof: "me :idk:" + ], + "\n" + } } diff --git a/src/lib.rs b/src/lib.rs index b350e81..b7ce17e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,14 @@ #![doc = include_str!("../README.md")] use data::{TitleInfo, Token}; -use gen::OutputGenerator; +use gen::lab; +pub use gen::OutputGenerator; use lexer::KyomatoLexError; /// This module defines types that are used to represent parsed data mod data; mod gen; +pub use gen::comp::CompLatex; pub use gen::lab::LabaLatex; mod lexer; pub mod path_engine; @@ -29,6 +31,7 @@ pub fn lex<'source>( pub fn gen<'token, 'source: 'token, Meta, Context>( token: &'token data::Token<'source>, title_info: impl Into>>, + generator: impl OutputGenerator<'token, Meta, Context>, mut output: impl std::io::Write, ) -> Result<(), Box> where @@ -42,7 +45,7 @@ where pub fn gen_to_string<'token, 'source: 'token, Meta, Context>( token: &'token data::Token<'source>, title_info: impl Into>>, -) -> Result> { + generator: impl OutputGenerator<'token, Meta, Context>, ) -> Result> where Meta: MetaExt<'source>, @@ -94,3 +97,28 @@ pub fn parse_all<'source>( /// Since this is generally a bad practice, I'd like to know suggestions on std library/other crates items I could use instead mod util; pub use util::FmtToIo; + +// TODO rename & move to approprite place, it's me patching holes in panic (I'm sorry me, from the +// future, ми все проїбали ((((( ) +pub trait MetaExt<'source> { + fn set_title_info<'title>(&mut self, title_info: TitleInfo<'title>) + where + 'title: 'source; +} + +impl<'source, Aynao> MetaExt<'source> for lab::SourceMeta<'source, Aynao> { + fn set_title_info<'title>(&mut self, title_info: TitleInfo<'title>) + where + 'title: 'source, + { + self.title_info = title_info; + } +} + +impl<'source> MetaExt<'source> for () { + fn set_title_info<'title>(&mut self, _title_info: TitleInfo) + where + 'title: 'source, + { + } +} diff --git a/tests/transform.rs b/tests/transform.rs index 9407307..7452592 100644 --- a/tests/transform.rs +++ b/tests/transform.rs @@ -1,5 +1,7 @@ use std::{ffi::OsString, path::PathBuf, str::FromStr}; +use kyomato_lib::path_engine::PathEngine; + const CLEAR: &'static str = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" ; const INPUT_DIR: &'static str = "./tests/input_files"; const TRANSFORM_DIR: &'static str = "./tests/transformed_files"; @@ -29,8 +31,12 @@ fn show_transform() { panic!("Should successfully parse the input: {err:#?}"); } }; - let repr = kyomato_lib::gen_to_string(&tokens, title_info) - .expect("Should be able to transform tokens"); + let repr = kyomato_lib::gen_to_string( + &tokens, + title_info, + kyomato_lib::LabaLatex::new(kyomato_lib::path_engine::primitive()), + ) + .expect("Should be able to transform tokens"); println!("{repr}"); std::io::stdin() .read_line(&mut String::new()) @@ -87,7 +93,13 @@ fn gen_transform_output() { output_file }; // write the result to output file - kyomato_lib::gen(&tokens, title_info, &mut output_file).expect("Failed to generate output"); + kyomato_lib::gen( + &tokens, + title_info, + kyomato_lib::LabaLatex::new(kyomato_lib::path_engine::primitive()), + &mut output_file, + ) + .expect("Failed to generate output"); } } @@ -105,7 +117,12 @@ fn test_transform() { panic!("Should successfully parse the input: {err:#?}"); } }; - let repr = kyomato_lib::gen_to_string(&tokens, title_info).expect("Failed to generate output"); + let repr = kyomato_lib::gen_to_string( + &tokens, + title_info, + kyomato_lib::LabaLatex::new(kyomato_lib::path_engine::primitive()), + ) + .expect("Failed to generate output"); // write the result to output directory let expected_output = { output_path.push(&filename); From 3f89050cb4678bc0858e04d72241af7b53a57763 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Fri, 23 Aug 2024 18:50:08 +0300 Subject: [PATCH 76/98] Updated expected output of integration tests --- tests/lex_files/ayano_display_blocks.txt | 16 +++++++++++++++- tests/transformed_files/ayano.txt | 5 ++++- tests/transformed_files/ayano_display_blocks.txt | 7 ++++++- tests/transformed_files/basic.txt | 3 +++ tests/transformed_files/deceit.txt | 5 ++++- tests/transformed_files/edgecases.txt | 3 +++ tests/transformed_files/tables_code.txt | 7 +++++-- tests/transformed_files/title_info.txt | 3 +++ 8 files changed, 43 insertions(+), 6 deletions(-) diff --git a/tests/lex_files/ayano_display_blocks.txt b/tests/lex_files/ayano_display_blocks.txt index 76a92f9..21f0ba1 100644 --- a/tests/lex_files/ayano_display_blocks.txt +++ b/tests/lex_files/ayano_display_blocks.txt @@ -26,7 +26,21 @@ Multiple { is_newline: true, space_before: false, formatting: None, - content: "This feature allows you to insert certain `Ayano` blocks to the end of the output document, to document your exact actions for displayed results.", + content: "This feature allows you to insert certain", + }, + Paragraph { + is_newline: false, + space_before: true, + formatting: Some( + MonoSpace, + ), + content: "Ayano", + }, + Paragraph { + is_newline: false, + space_before: true, + formatting: None, + content: "blocks to the end of the output document, to document your exact actions for displayed results.", }, Paragraph { is_newline: true, diff --git a/tests/transformed_files/ayano.txt b/tests/transformed_files/ayano.txt index de1040d..4581132 100644 --- a/tests/transformed_files/ayano.txt +++ b/tests/transformed_files/ayano.txt @@ -99,6 +99,9 @@ \titleformat*{\subsection}{\fnt\Large\bfseries} \titleformat*{\subsubsection}{\fnt\Large} +% bra and ket from QM +\usepackage{braket} + \begin{document} \thispagestyle{empty} % [A]:for cropping % ------------ @@ -152,7 +155,7 @@ This is a test of ayano-generated stuff. \begin{table}[h!] \fnt \begin{center} -\begin{tabular}{|c|c|}\hline +\hskip-1.5cm\begin{tabular}{|c|c|}\hline x & y\\ \hline \hline 1.0 & 3.0\\ \hline diff --git a/tests/transformed_files/ayano_display_blocks.txt b/tests/transformed_files/ayano_display_blocks.txt index 9e8b1b4..42e56e1 100644 --- a/tests/transformed_files/ayano_display_blocks.txt +++ b/tests/transformed_files/ayano_display_blocks.txt @@ -99,6 +99,9 @@ \titleformat*{\subsection}{\fnt\Large\bfseries} \titleformat*{\subsubsection}{\fnt\Large} +% bra and ket from QM +\usepackage{braket} + \begin{document} \thispagestyle{empty} % [A]:for cropping % ------------ @@ -136,9 +139,11 @@ \section{Display blocks} -\tb This feature allows you to insert certain `Ayano` blocks to the end of the output document, to document your exact actions for displayed results. +\tb This feature allows you to insert certain \texttt{Ayano} blocks to the end of the output document, to document your exact actions for displayed results. \tbln Here's an example: 42 \tbln Display blocks can have descriptions too! Here's one: -20340$\pm$150 +\clearpage + \subsection{Код} \begin{minted}[linenos, mathescape, autogobble, breaklines]{python} diff --git a/tests/transformed_files/basic.txt b/tests/transformed_files/basic.txt index 6eba618..73712f9 100644 --- a/tests/transformed_files/basic.txt +++ b/tests/transformed_files/basic.txt @@ -99,6 +99,9 @@ \titleformat*{\subsection}{\fnt\Large\bfseries} \titleformat*{\subsubsection}{\fnt\Large} +% bra and ket from QM +\usepackage{braket} + \begin{document} \thispagestyle{empty} % [A]:for cropping % ------------ diff --git a/tests/transformed_files/deceit.txt b/tests/transformed_files/deceit.txt index 09993c4..c6c7e3f 100644 --- a/tests/transformed_files/deceit.txt +++ b/tests/transformed_files/deceit.txt @@ -99,6 +99,9 @@ \titleformat*{\subsection}{\fnt\Large\bfseries} \titleformat*{\subsubsection}{\fnt\Large} +% bra and ket from QM +\usepackage{braket} + \begin{document} \thispagestyle{empty} % [A]:for cropping % ------------ @@ -193,7 +196,7 @@ head /dev/random ![[ \end{equation} -\begin{minted} +\begin{minted}[linenos, mathescape, autogobble, breaklines]{text} ![[ \end{minted} diff --git a/tests/transformed_files/edgecases.txt b/tests/transformed_files/edgecases.txt index 42f8103..0509ac3 100644 --- a/tests/transformed_files/edgecases.txt +++ b/tests/transformed_files/edgecases.txt @@ -99,6 +99,9 @@ \titleformat*{\subsection}{\fnt\Large\bfseries} \titleformat*{\subsubsection}{\fnt\Large} +% bra and ket from QM +\usepackage{braket} + \begin{document} \thispagestyle{empty} % [A]:for cropping % ------------ diff --git a/tests/transformed_files/tables_code.txt b/tests/transformed_files/tables_code.txt index 2426426..bee4ef9 100644 --- a/tests/transformed_files/tables_code.txt +++ b/tests/transformed_files/tables_code.txt @@ -99,6 +99,9 @@ \titleformat*{\subsection}{\fnt\Large\bfseries} \titleformat*{\subsubsection}{\fnt\Large} +% bra and ket from QM +\usepackage{braket} + \begin{document} \thispagestyle{empty} % [A]:for cropping % ------------ @@ -140,7 +143,7 @@ \begin{table}[h!] \fnt \begin{center} -\begin{tabular}{|c|c|c|}\hline +\hskip-1.5cm\begin{tabular}{|c|c|c|}\hline Storage & Space & Number\\ \hline \hline 1 & 2 & $3 + e ^{\pi}$\\ \hline @@ -156,7 +159,7 @@ \begin{table}[h!] \fnt \begin{center} -\begin{tabular}{|c|c|}\hline +\hskip-1.5cm\begin{tabular}{|c|c|}\hline Кількість & Роль\\ \hline \hline 3 діоптрії & 7 галон\\ \hline diff --git a/tests/transformed_files/title_info.txt b/tests/transformed_files/title_info.txt index e0714d4..37b54c0 100644 --- a/tests/transformed_files/title_info.txt +++ b/tests/transformed_files/title_info.txt @@ -99,6 +99,9 @@ \titleformat*{\subsection}{\fnt\Large\bfseries} \titleformat*{\subsubsection}{\fnt\Large} +% bra and ket from QM +\usepackage{braket} + \begin{document} \thispagestyle{empty} % [A]:for cropping % ------------ From dbc3f64912e2e86685918cfb192855337455dfc6 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Sat, 24 Aug 2024 11:44:24 +0300 Subject: [PATCH 77/98] I have no idea what this file was for, let's hope it wasn't important --- examples/advanced_ayano/2 | 1 - 1 file changed, 1 deletion(-) delete mode 100644 examples/advanced_ayano/2 diff --git a/examples/advanced_ayano/2 b/examples/advanced_ayano/2 deleted file mode 100644 index d8263ee..0000000 --- a/examples/advanced_ayano/2 +++ /dev/null @@ -1 +0,0 @@ -2 \ No newline at end of file From cd2a25ad77c39ab6437318715c031c5d0f948164 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Sat, 24 Aug 2024 11:55:46 +0300 Subject: [PATCH 78/98] clippy fixes except `clippy::too_many_lines` These are much harder to solve, and that's exactly what I'm going to do now. Basically, it's finally time to refactor `kyomato` into several crates. --- examples/advanced_ayano/main.rs | 4 +- examples/basic_ayano/main.rs | 5 +- examples/basic_syntax/main.rs | 4 +- examples/title_info/main.rs | 5 +- kyomato/src/main.rs | 201 +++--- src/data.rs | 1183 +------------------------------ src/gen/ayano.rs | 322 +++++---- src/gen/ayano/python.rs | 10 +- src/gen/comp/mod.rs | 68 +- src/gen/lab/meta.rs | 32 +- src/gen/lab/mod.rs | 97 ++- src/gen/mod.rs | 8 +- src/lexer.rs | 82 +-- src/lib.rs | 12 +- src/path_engine.rs | 52 +- src/util.rs | 46 +- tests/lex.rs | 7 +- tests/transform.rs | 9 +- 18 files changed, 453 insertions(+), 1694 deletions(-) diff --git a/examples/advanced_ayano/main.rs b/examples/advanced_ayano/main.rs index 306b682..dcd9b5b 100644 --- a/examples/advanced_ayano/main.rs +++ b/examples/advanced_ayano/main.rs @@ -1,4 +1,4 @@ -use kyomato_lib::path_engine::PathEngineExt; +use kyomato_lib::path_engine::EngineExt; fn main() { //! This is ayano-involving example of programmatic output generation @@ -36,5 +36,5 @@ fn main() { kyomato_lib::LabaLatex::parse_and_write(path_engine, &input, &mut output) // this part is just for debug purposes .map_err(|err| (err, output)) - .unwrap() + .unwrap(); } diff --git a/examples/basic_ayano/main.rs b/examples/basic_ayano/main.rs index b51390e..46b2899 100644 --- a/examples/basic_ayano/main.rs +++ b/examples/basic_ayano/main.rs @@ -7,7 +7,8 @@ fn main() { let (input, mut output) = examples_util::setup_io( "examples/basic_ayano/input.md", "examples/basic_ayano/output.tex", - ).unwrap(); + ) + .unwrap(); // construct path engine // in this example, no external files are accessed (i.e. there are no images or special Ayano syntax) @@ -19,5 +20,5 @@ fn main() { kyomato_lib::LabaLatex::parse_and_write(path_engine, &input, &mut output) // this part is just for debug purposes .map_err(|err| (err, output)) - .unwrap() + .unwrap(); } diff --git a/examples/basic_syntax/main.rs b/examples/basic_syntax/main.rs index 60b98ba..3b683b1 100644 --- a/examples/basic_syntax/main.rs +++ b/examples/basic_syntax/main.rs @@ -1,4 +1,4 @@ -use kyomato_lib::path_engine::PathEngineExt; +use kyomato_lib::path_engine::EngineExt; fn main() { //! This is a simple example of programmatic output generation @@ -29,5 +29,5 @@ fn main() { kyomato_lib::LabaLatex::parse_and_write(path_engine, &input, &mut output) // this part is just for debug purposes .map_err(|err| (err, output)) - .unwrap() + .unwrap(); } diff --git a/examples/title_info/main.rs b/examples/title_info/main.rs index 855d33e..ea878d7 100644 --- a/examples/title_info/main.rs +++ b/examples/title_info/main.rs @@ -7,7 +7,8 @@ fn main() { let (input, mut output) = examples_util::setup_io( "examples/title_info/input.md", "examples/title_info/output.tex", - ).unwrap(); + ) + .unwrap(); // construct path engine // in this example, no external files are accessed (i.e. there are no images or special Ayano syntax) @@ -19,5 +20,5 @@ fn main() { kyomato_lib::LabaLatex::parse_and_write(path_engine, &input, &mut output) // this part is just for debug purposes .map_err(|err| (err, output)) - .unwrap() + .unwrap(); } diff --git a/kyomato/src/main.rs b/kyomato/src/main.rs index 59b64e9..03d97ad 100644 --- a/kyomato/src/main.rs +++ b/kyomato/src/main.rs @@ -5,8 +5,7 @@ use std::{ use clap::{Parser, ValueEnum}; use kyomato_lib::path_engine::{ - self, base_gen, fs_check, InformativeError, MarkerError, PathEngine, PathEngineError, - PathEngineExt, + base_gen, fs_check, EngineError, EngineExt, InformativeError, MarkerError, PathEngine, }; #[derive(Debug, ValueEnum, Clone, Default, derive_more::Display)] @@ -42,7 +41,7 @@ struct Config { /// /// This is intended to be set a directory of the initial command caller, in case they rely on some file local to them personally #[arg(long = "exec")] - execution_directory: Option>, + execution_directory: Option, } fn main() { @@ -119,62 +118,46 @@ fn main() { .with_error_type::(), config.generator, ), - (false, Some(execution_dir), None, None) => { - main_inner( - input, - base_gen() - .with_check(fs_check) - .with_execution_folder(execution_dir.unwrap_or_else(|| { - std::env::current_dir().expect("Failed to retrieve cwd") - })) - .with_ayano() - .with_error_type::(), - config.generator, - ) - } - (false, Some(execution_dir), None, Some(image_directory)) => { - main_inner( - input, - base_gen() - .with_check(fs_check) - .with_execution_folder(execution_dir.unwrap_or_else(|| { - std::env::current_dir().expect("Failed to retrieve cwd") - })) - .with_image_folder(image_directory) - .with_ayano() - .with_error_type::(), - config.generator, - ) - } - (false, Some(execution_dir), Some(input_file_dir), None) => { - main_inner( - input, - base_gen() - .with_check(fs_check) - .with_execution_folder(execution_dir.unwrap_or_else(|| { - std::env::current_dir().expect("Failed to retrieve cwd") - })) - .with_source_folder(input_file_dir) - .with_ayano() - .with_error_type::(), - config.generator, - ) - } - (false, Some(execution_dir), Some(input_file_dir), Some(image_directory)) => { - main_inner( - input, - base_gen() - .with_check(fs_check) - .with_execution_folder(execution_dir.unwrap_or_else(|| { - std::env::current_dir().expect("Failed to retrieve cwd") - })) - .with_source_folder(input_file_dir) - .with_image_folder(image_directory) - .with_ayano() - .with_error_type::(), - config.generator, - ) - } + (false, Some(execution_dir), None, None) => main_inner( + input, + base_gen() + .with_check(fs_check) + .with_execution_folder(execution_dir) + .with_ayano() + .with_error_type::(), + config.generator, + ), + (false, Some(execution_dir), None, Some(image_directory)) => main_inner( + input, + base_gen() + .with_check(fs_check) + .with_execution_folder(execution_dir) + .with_image_folder(image_directory) + .with_ayano() + .with_error_type::(), + config.generator, + ), + (false, Some(execution_dir), Some(input_file_dir), None) => main_inner( + input, + base_gen() + .with_check(fs_check) + .with_execution_folder(execution_dir) + .with_source_folder(input_file_dir) + .with_ayano() + .with_error_type::(), + config.generator, + ), + (false, Some(execution_dir), Some(input_file_dir), Some(image_directory)) => main_inner( + input, + base_gen() + .with_check(fs_check) + .with_execution_folder(execution_dir) + .with_source_folder(input_file_dir) + .with_image_folder(image_directory) + .with_ayano() + .with_error_type::(), + config.generator, + ), (true, None, None, None) => main_inner( input, base_gen() @@ -211,66 +194,50 @@ fn main() { .with_error_type::(), config.generator, ), - (true, Some(execution_dir), None, None) => { - main_inner( - input, - base_gen() - .with_check(fs_check) - .with_execution_folder(execution_dir.unwrap_or_else(|| { - std::env::current_dir().expect("Failed to retrieve cwd") - })) - .with_ayano() - .with_error_type::(), - config.generator, - ) - } - (true, Some(execution_dir), None, Some(image_directory)) => { - main_inner( - input, - base_gen() - .with_check(fs_check) - .with_execution_folder(execution_dir.unwrap_or_else(|| { - std::env::current_dir().expect("Failed to retrieve cwd") - })) - .with_image_folder(image_directory) - .with_ayano() - .with_error_type::(), - config.generator, - ) - } - (true, Some(execution_dir), Some(input_file_dir), None) => { - main_inner( - input, - base_gen() - .with_check(fs_check) - .with_execution_folder(execution_dir.unwrap_or_else(|| { - std::env::current_dir().expect("Failed to retrieve cwd") - })) - .with_source_folder(input_file_dir) - .with_ayano() - .with_error_type::(), - config.generator, - ) - } - (true, Some(execution_dir), Some(input_file_dir), Some(image_directory)) => { - main_inner( - input, - base_gen() - .with_check(fs_check) - .with_execution_folder(execution_dir.unwrap_or_else(|| { - std::env::current_dir().expect("Failed to retrieve cwd") - })) - .with_source_folder(input_file_dir) - .with_image_folder(image_directory) - .with_ayano() - .with_error_type::(), - config.generator, - ) - } + (true, Some(execution_dir), None, None) => main_inner( + input, + base_gen() + .with_check(fs_check) + .with_execution_folder(execution_dir) + .with_ayano() + .with_error_type::(), + config.generator, + ), + (true, Some(execution_dir), None, Some(image_directory)) => main_inner( + input, + base_gen() + .with_check(fs_check) + .with_execution_folder(execution_dir) + .with_image_folder(image_directory) + .with_ayano() + .with_error_type::(), + config.generator, + ), + (true, Some(execution_dir), Some(input_file_dir), None) => main_inner( + input, + base_gen() + .with_check(fs_check) + .with_execution_folder(execution_dir) + .with_source_folder(input_file_dir) + .with_ayano() + .with_error_type::(), + config.generator, + ), + (true, Some(execution_dir), Some(input_file_dir), Some(image_directory)) => main_inner( + input, + base_gen() + .with_check(fs_check) + .with_execution_folder(execution_dir) + .with_source_folder(input_file_dir) + .with_image_folder(image_directory) + .with_ayano() + .with_error_type::(), + config.generator, + ), } } -fn main_inner>( +fn main_inner>( input: String, path_engine: PE, generator: Generator, @@ -288,7 +255,7 @@ fn main_inner>( kyomato_lib::LabaLatex::parse_and_write(path_engine, &input, &mut fmt_output) } Generator::Compact => { - kyomato_lib::CompLatex::parse_and_write(path_engine, &input, &mut fmt_output) + kyomato_lib::Generator::parse_and_write(path_engine, &input, &mut fmt_output) } }; if let Err(error) = res { diff --git a/src/data.rs b/src/data.rs index 2f59c28..5b755d0 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1,9 +1,4 @@ -use std::{ - borrow::{Borrow, Cow}, - ops::Deref, - path::Path, - rc::Rc, -}; +use std::{borrow::Cow, ops::Deref, path::Path, rc::Rc}; use url::Url; @@ -42,7 +37,7 @@ impl<'source> Deref for Tokens<'source> { type Target = [Token<'source>]; fn deref(&self) -> &Self::Target { - &*self.0 + &self.0 } } @@ -52,7 +47,7 @@ impl<'r, 'source: 'r> IntoIterator for &'r Tokens<'source> { type IntoIter = std::slice::Iter<'r, Token<'source>>; fn into_iter(self) -> Self::IntoIter { - self.get_slice().into_iter() + self.get_slice().iter() } } @@ -253,12 +248,7 @@ impl ToStaticExt for Tokens<'_> { fn to_static(&self) -> Self::AsStatic { let slice = self.get_slice(); - Tokens::new( - slice - .into_iter() - .map(|t| t.to_static_token()) - .collect::>(), - ) + Tokens::new(slice.iter().map(Token::to_static_token).collect::>()) } } @@ -266,7 +256,7 @@ impl ToStaticExt for Box> { type AsStatic = Box>; fn to_static(&self) -> Self::AsStatic { - Box::new((&**self).to_static()) + Box::new((**self).to_static()) } } @@ -334,7 +324,7 @@ impl<'source> Token<'source> { src_name: src_name.clone(), caption: caption.clone(), ident: ident.clone(), - width: width.clone(), + width: *width, }, Token::Href { url, @@ -433,7 +423,7 @@ impl<'source> Token<'source> { src_name: src_name.to_static(), caption: caption.as_ref().map(Box::to_static), ident: ident.as_ref().map(Cow::to_static), - width: width.clone(), + width: *width, }, Token::Href { url, @@ -522,1162 +512,3 @@ impl<'source> Token<'source> { } } } - -/// # On token storage -/// -/// Basically, tokens are not manipulated directly; Instead, they are aggregated into so-called "trees" to allow efficient data storage. -/// -/// Tree consists of storage and a root, "pointing" somewhere inside it. -/// -/// To create a tree, `Unsealed Storage` must be created first. While storage is unsealed, one can add a text, a token or a token slice (iterator of tokens) -/// for it to store. Once data was stored, `Unsealed Storage` will return a `Promise` to said data. Exact form of this `Promise` is up to storage to -/// decide, and that's exactly what allows token tree to only store stuff it needs, and the way it needs. -/// -/// Once all tokens were added to `Unsealed Storage`, it can be transformed into a regular `Storage`. `Storage` allows to see text and token(s) with -/// the promises returned by `Unsealed Storage`. -/// -/// ## Capabilities -/// If you happen to know, that all of your data is contained within a certain string, you may pass a reference to it, for storage to use. -/// At any time, if you decide to transfer your data from one storage to another - you can absolutely do it via dedicated method. -/// This also covers situations where you don't wanna hold onto some reference, and decide to copy over all of the data -/// -/// There's also an ability to move the data into the storage, which can be useful for large text buffers. -/// Please note that this last operation is hard to define properly, so some of the methods used for it, are marked `unsafe` (since they break the invariant) -#[cfg(borrowing_concept)] -mod borrowing_concept { - use std::{borrow::Cow, marker::PhantomData, ops::Range}; - - struct ThingTree { - root: Leaf, - provider: Storage, - _phantom: PhantomData<(Text, Leaves)>, - } - - impl ThingTree { - fn new(thing: Thing, mut storage: ToStorage) -> Self - where - ToStorage: UnsealedStorage, - { - let child = storage.register_leaf(thing); - Self { - root: child, - provider: storage.seal(), - _phantom: PhantomData, - } - } - - fn root(&self) -> &Leaf { - &self.root - } - - fn root_thing(&self) -> &Thing - where - Storage: SealedStorage, - { - self.provider.get_leaf(&self.root) - } - - fn storage(&self) -> &Storage { - &self.provider - } - - fn move_to( - &self, - other_storage: &mut ToStorage, - ) -> Thing - where - ToStorage: UnsealedStorage, - Storage: SealedStorage, - { - transfer_nodes( - self.root_thing(), - self.storage(), - other_storage, - &mut Vec::new(), - ) - } - - fn write_to(&self, output: &mut W) -> std::fmt::Result - where - Storage: SealedStorage, - { - let thing = self.root_thing(); - thing.write_to(&self.provider, output) - } - } - - pub enum Thing { - Text { - text: Text, - }, - Child { - text: Text, - child1: Child, - child2: Child, - }, - Multiple { - children: Children, - }, - } - - impl Thing { - fn write_to(&self, provider: &P, out: &mut W) -> std::fmt::Result - where - P: SealedStorage, - W: std::fmt::Write + ?Sized, - { - match self { - Thing::Text { text } => write!(out, "Text({})", provider.get_text(text)), - Thing::Child { - text, - child1, - child2, - } => { - write!(out, "Child({}, ", provider.get_text(text))?; - provider.get_leaf(child1).write_to(provider, out)?; - out.write_str(", ")?; - provider.get_leaf(child2).write_to(provider, out)?; - out.write_str(")")?; - Ok(()) - } - Thing::Multiple { children } => { - out.write_str("Multiple[")?; - let children = provider.get_leaves(children); - if let Some(first) = children.first() { - first.write_to(provider, out)?; - for child in &children[1..] { - out.write_str(", ")?; - child.write_to(provider, out)?; - } - } - out.write_str("]")?; - Ok(()) - } - } - } - } - - pub trait UnsealedStorage { - type Sealed: SealedStorage; - fn register_text(&mut self, text: &str) -> Text; - fn register_leaf(&mut self, leaf: Thing) -> Leaf; - fn register_leaves<'s>( - &'s mut self, - leaves: impl Iterator>, - ) -> Leaves; - fn seal(self) -> Self::Sealed; - } - - pub trait SealedStorage { - fn get_text<'s>(&'s self, promise: &Text) -> &'s str; - fn get_leaf<'s>(&'s self, promise: &Leaf) -> &'s Thing; - fn get_leaves<'s>(&'s self, promise: &Leaves) -> &'s [Thing]; - } - - trait UnsealedStorageExt: UnsealedStorage { - fn with_text_ref<'text>( - self, - text: &'text str, - ) -> UnsealedTextRefStorage<'text, Text, Leaf, Leaves, Self> - where - Self: Sized, - { - UnsealedTextRefStorage::<'text, Text, Leaf, Leaves, Self> { - text, - inner: self, - _phantom: PhantomData, - } - } - - fn with_text_find( - self, - text: impl Into, - ) -> UnsealedTextFindStorage - where - Self: Sized, - { - UnsealedTextFindStorage { - text: text.into(), - inner: self, - _phantom: PhantomData, - } - } - - fn text_owned(self) -> UnsealedTextOwnStorage - where - Self: Sized, - { - UnsealedTextOwnStorage:: { - text: String::new(), - inner: self, - _phantom: PhantomData, - } - } - - fn leaves_owned(self) -> UnsealedLeavesOwnStorage - where - Self: Sized, - { - UnsealedLeavesOwnStorage:: { - things: Vec::new(), - inner: self, - } - } - } - impl> - UnsealedStorageExt for S - { - } - trait OptPromiseInfo: From { - fn info(&self) -> Option<&PromiseInfo>; - } - - struct TextRefStorageType; - pub struct UnsealedTextRefStorage<'text, Text, Leaf, Leaves, InnerStorage> { - text: &'text str, - inner: InnerStorage, - _phantom: PhantomData<(Text, Leaf, Leaves)>, - } - - fn try_find_pointer_match<'h, 'f>(haystack: &'h str, find: &'f str) -> Option> { - let referee_ptr = haystack.as_ptr() as usize; - let text_ptr = find.as_ptr() as usize; - if text_ptr < referee_ptr { - return None; - } - let index_guess = text_ptr - referee_ptr; - let self_len = haystack.len(); - if index_guess >= self_len { - return None; - } - // yeah, seems like it - // lastly, let's rule out partial overlap: - let text_len = find.len(); - if text_len + index_guess > self_len { - // buffers overlap partially, which is weird, probably should - // TODO warn about that too - return None; - } - // let's check that literally, for a good measure: - // TODO probably add cfg to remove that in release build? - if !haystack[index_guess..].starts_with(find) { - // huh? that's something weird, probably must - // TODO warn about it - return None; - } - // ok, so it seems like we've found a match using nothing but pointer-things. - // that's great, cause it's literally O(1) for any buffer size - return Some(index_guess..index_guess + text_len); - } - - impl<'text, Text, Leaf, Leaves, InnerStorage> UnsealedStorage - for UnsealedTextRefStorage<'text, Text, Leaf, Leaves, InnerStorage> - where - InnerStorage: UnsealedStorage, - Text: OptPromiseInfo<&'text str, TextRefStorageType>, - { - type Sealed = TextRefStorage<'text, Text, Leaf, Leaves, InnerStorage::Sealed>; - - fn register_text(&mut self, text: &str) -> Text { - // first, check out if this text, as a pointer, points inside this provider's referee: - if let Some(range) = try_find_pointer_match(self.text, text) { - return self.text[range].into(); - } - - // then, let's try finding this sort of sequence in the entire referee: - if let Some(ind) = self.text.find(text) { - // well, return corresponding slice, then - return self.text[ind..ind + text.len()].into(); - } - - // lastly, refer to inner provider - self.inner.register_text(text) - } - - fn register_leaf(&mut self, leaf: Thing) -> Leaf { - self.inner.register_leaf(leaf) - } - - fn register_leaves<'s>( - &'s mut self, - leaves: impl Iterator>, - ) -> Leaves { - self.inner.register_leaves(leaves) - } - - fn seal(self) -> Self::Sealed { - Self::Sealed { - inner: self.inner.seal(), - _phantom: PhantomData, - } - } - } - - pub struct TextRefStorage<'text, Text, Leaf, Leaves, InnerStorage> { - inner: InnerStorage, - _phantom: PhantomData<(&'text (), Text, Leaf, Leaves)>, - } - - impl<'text, Text, Leaf, Leaves, InnerStorage> SealedStorage - for TextRefStorage<'text, Text, Leaf, Leaves, InnerStorage> - where - InnerStorage: SealedStorage, - Text: OptPromiseInfo<&'text str, TextRefStorageType>, - { - fn get_text<'s>(&'s self, promise: &Text) -> &'s str { - if let Some(res) = promise.info() { - res - } else { - self.inner.get_text(promise) - } - } - - fn get_leaf<'s>(&'s self, promise: &Leaf) -> &'s Thing { - self.inner.get_leaf(promise) - } - - fn get_leaves<'s>(&'s self, promise: &Leaves) -> &'s [Thing] { - self.inner.get_leaves(promise) - } - } - - struct TextOwnStorageType; - pub struct UnsealedTextOwnStorage { - text: String, - inner: InnerStorage, - _phantom: PhantomData<(Text, Leaf, Leaves)>, - } - - impl UnsealedStorage - for UnsealedTextOwnStorage - where - InnerStorage: UnsealedStorage, - Text: OptPromiseInfo, TextOwnStorageType>, - { - type Sealed = TextOwnStorage; - - fn register_text(&mut self, text: &str) -> Text { - let range_start = self.text.len(); - self.text.push_str(text); - let range_end = self.text.len(); - Text::from(range_start..range_end) - } - - fn register_leaf(&mut self, leaf: Thing) -> Leaf { - self.inner.register_leaf(leaf) - } - - fn register_leaves<'s>( - &'s mut self, - leaves: impl Iterator>, - ) -> Leaves { - self.inner.register_leaves(leaves) - } - - fn seal(self) -> Self::Sealed { - Self::Sealed { - text: self.text.into_boxed_str(), - inner: self.inner.seal(), - _phantom: PhantomData, - } - } - } - - pub struct TextOwnStorage { - text: Box, - inner: InnerStorage, - _phantom: PhantomData<(Text, Leaf, Leaves)>, - } - - impl SealedStorage - for TextOwnStorage - where - InnerStorage: SealedStorage, - Text: OptPromiseInfo, TextOwnStorageType>, - { - fn get_text<'s>(&'s self, promise: &Text) -> &'s str { - if let Some(range) = promise.info() { - &self.text[range.clone()] - } else { - self.inner.get_text(promise) - } - } - - fn get_leaf<'s>(&'s self, promise: &Leaf) -> &'s Thing { - self.inner.get_leaf(promise) - } - - fn get_leaves<'s>(&'s self, promise: &Leaves) -> &'s [Thing] { - self.inner.get_leaves(promise) - } - } - - struct TextFindStorageType; - pub struct UnsealedTextFindStorage { - text: String, - inner: InnerStorage, - _phantom: PhantomData<(Text, Leaf, Leaves)>, - } - - impl UnsealedStorage - for UnsealedTextFindStorage - where - InnerStorage: UnsealedStorage, - Text: OptPromiseInfo, TextFindStorageType>, - { - type Sealed = TextFindStorage; - - fn register_text(&mut self, text: &str) -> Text { - if let Some(ind) = self.text.find(text) { - return Text::from(ind..ind + text.len()); - } - let range_start = self.text.len(); - self.text.push_str(text); - let range_end = self.text.len(); - Text::from(range_start..range_end) - } - - fn register_leaf(&mut self, leaf: Thing) -> Leaf { - self.inner.register_leaf(leaf) - } - - fn register_leaves<'s>( - &'s mut self, - leaves: impl Iterator>, - ) -> Leaves { - self.inner.register_leaves(leaves) - } - - fn seal(self) -> Self::Sealed { - Self::Sealed { - text: self.text.into_boxed_str(), - inner: self.inner.seal(), - _phantom: PhantomData, - } - } - } - - pub struct TextFindStorage { - text: Box, - inner: InnerStorage, - _phantom: PhantomData<(Text, Leaf, Leaves)>, - } - - impl SealedStorage - for TextFindStorage - where - InnerStorage: SealedStorage, - Text: OptPromiseInfo, TextFindStorageType>, - { - fn get_text<'s>(&'s self, promise: &Text) -> &'s str { - if let Some(range) = promise.info() { - &self.text[range.clone()] - } else { - self.inner.get_text(promise) - } - } - - fn get_leaf<'s>(&'s self, promise: &Leaf) -> &'s Thing { - self.inner.get_leaf(promise) - } - - fn get_leaves<'s>(&'s self, promise: &Leaves) -> &'s [Thing] { - self.inner.get_leaves(promise) - } - } - - struct LeavesOwnStorageType; - pub struct UnsealedLeavesOwnStorage { - things: Vec>, - inner: P, - } - - impl UnsealedStorage - for UnsealedLeavesOwnStorage - where - InnerStorage: UnsealedStorage, - Leaf: OptPromiseInfo, - Leaves: OptPromiseInfo, LeavesOwnStorageType>, - { - type Sealed = LeavesOwnStorage; - - fn register_text(&mut self, text: &str) -> Text { - self.inner.register_text(text) - } - - fn register_leaf(&mut self, leaf: Thing) -> Leaf { - let ind = self.things.len(); - self.things.push(leaf); - Leaf::from(ind) - } - - fn register_leaves<'s>( - &'s mut self, - leaves: impl Iterator>, - ) -> Leaves { - let range_start = self.things.len(); - self.things.extend(leaves); - let range_end = self.things.len(); - Leaves::from(range_start..range_end) - } - - fn seal(self) -> Self::Sealed { - LeavesOwnStorage { - things: self.things.into_boxed_slice(), - inner: self.inner.seal(), - } - } - } - - pub struct LeavesOwnStorage { - things: Box<[Thing]>, - inner: InnerStorage, - } - - impl SealedStorage - for LeavesOwnStorage - where - InnerStorage: SealedStorage, - Leaf: OptPromiseInfo, - Leaves: OptPromiseInfo, LeavesOwnStorageType>, - { - fn get_text<'s>(&'s self, promise: &Text) -> &'s str { - self.inner.get_text(promise) - } - - fn get_leaf<'s>(&'s self, promise: &Leaf) -> &'s Thing { - if let Some(ind) = promise.info() { - &self.things[*ind] - } else { - self.inner.get_leaf(promise) - } - } - - fn get_leaves<'s>(&'s self, promise: &Leaves) -> &'s [Thing] { - if let Some(range) = promise.info() { - &self.things[range.clone()] - } else { - self.inner.get_leaves(promise) - } - } - } - - enum NeverText {} - impl From> for NeverText { - fn from(_: Range) -> Self { - unimplemented!("This data type is not supported") - } - } - impl<'text> From<&'text str> for NeverText { - fn from(_: &'text str) -> Self { - unimplemented!("This data type is not supported") - } - } - impl OptPromiseInfo for NeverText - where - NeverText: From, - { - fn info(&self) -> Option<&T> { - unimplemented!("This data type is not supported") - } - } - enum NeverLeaf {} - impl From for NeverLeaf { - fn from(_: usize) -> Self { - unimplemented!("This data type is not supported") - } - } - impl OptPromiseInfo for NeverLeaf - where - NeverLeaf: From, - { - fn info(&self) -> Option<&T> { - unimplemented!("This data type is not supported") - } - } - enum NeverLeaves {} - impl From> for NeverLeaves { - fn from(_: Range) -> Self { - unimplemented!("This data type is not supported") - } - } - impl OptPromiseInfo for NeverLeaves - where - NeverLeaves: From, - { - fn info(&self) -> Option<&T> { - unimplemented!("This data type is not supported") - } - } - - #[derive(Debug, derive_more::From)] - pub struct TextRef<'text>(&'text str); - impl<'text, StorageType> OptPromiseInfo<&'text str, StorageType> for TextRef<'text> { - fn info(&self) -> Option<&&'text str> { - Some(&self.0) - } - } - - #[derive(Debug, derive_more::From)] - pub struct TextOwn(Range); - impl OptPromiseInfo, StorageType> for TextOwn { - fn info(&self) -> Option<&Range> { - Some(&self.0) - } - } - #[derive(Debug, derive_more::From)] - pub enum TextMaybeOwn<'text> { - Ref(&'text str), - Own(Range), - } - impl<'text, StorageType> OptPromiseInfo<&'text str, StorageType> for TextMaybeOwn<'text> { - fn info(&self) -> Option<&&'text str> { - if let Self::Ref(s) = self { - Some(s) - } else { - None - } - } - } - impl OptPromiseInfo, StorageType> for TextMaybeOwn<'_> { - fn info(&self) -> Option<&Range> { - if let Self::Own(range) = self { - Some(range) - } else { - None - } - } - } - - #[derive(Debug, derive_more::From)] - pub struct LeafInd(usize); - impl OptPromiseInfo for LeafInd { - fn info(&self) -> Option<&usize> { - Some(&self.0) - } - } - - #[derive(Debug, derive_more::From)] - pub struct LeavesRange(Range); - impl OptPromiseInfo, StorageType> for LeavesRange { - fn info(&self) -> Option<&Range> { - Some(&self.0) - } - } - - /// Provider that holds nothing and panics at any call to it. - struct UnsealedBottomStorage( - PhantomData<(Text, Leaf, Leaves)>, - ); - impl UnsealedStorage - for UnsealedBottomStorage - { - type Sealed = BottomStorage; - fn register_text(&mut self, _: &str) -> Text { - unimplemented!("This provider does not support text saving") - } - - fn register_leaf(&mut self, _: Thing) -> Leaf { - unimplemented!("This provider does not support child saving") - } - - fn register_leaves<'s>( - &'s mut self, - _: impl Iterator>, - ) -> Leaves { - unimplemented!("This provider does not support children saving") - } - - fn seal(self) -> Self::Sealed { - BottomStorage(PhantomData) - } - } - impl UnsealedBottomStorage { - fn new() -> Self { - UnsealedBottomStorage(PhantomData) - } - } - struct BottomStorage( - PhantomData<(Text, Leaf, Leaves)>, - ); - impl SealedStorage for BottomStorage { - fn get_text<'s>(&'s self, _: &Text) -> &'s str { - unimplemented!("This provider does not provide text") - } - - fn get_leaf<'s>(&'s self, _: &Leaf) -> &'s Thing { - unimplemented!("This provider does not provide children") - } - - fn get_leaves<'s>(&'s self, _: &Leaves) -> &'s [Thing] { - unimplemented!("This provider does not provide children") - } - } - - pub fn text_ref_provider<'text>( - text: &'text str, - ) -> impl UnsealedStorage, LeafInd, LeavesRange> { - UnsealedBottomStorage::new() - .with_text_ref(text) - .leaves_owned() - } - - pub fn text_own_provider() -> impl UnsealedStorage { - UnsealedBottomStorage::new().text_owned().leaves_owned() - } - - pub fn text_maybe_own_provider<'text>( - ref_text: &'text str, - ) -> impl UnsealedStorage, LeafInd, LeavesRange> { - UnsealedBottomStorage::new() - .text_owned() - .with_text_ref(ref_text) - .leaves_owned() - } - - pub fn text_maybe_own_find_provider<'text>( - ref_text: &'text str, - own_text: impl Into, - ) -> impl UnsealedStorage, LeafInd, LeavesRange> { - UnsealedBottomStorage::new() - .with_text_find(own_text) - .with_text_ref(ref_text) - .leaves_owned() - } - - pub fn text_own_find_provider( - own_text: impl Into, - ) -> impl UnsealedStorage { - UnsealedBottomStorage::new() - .with_text_find(own_text) - .leaves_owned() - } - - fn transfer_nodes< - FromText, - FromChild, - FromChildren, - ToText, - ToChild, - ToChildren, - FromProvider, - ToProvider, - >( - root: &Thing, - from_provider: &FromProvider, - to_provider: &mut ToProvider, - buf: &mut Vec>, - ) -> Thing - where - FromProvider: SealedStorage, - ToProvider: UnsealedStorage, - { - match root { - Thing::Text { text } => { - let s = from_provider.get_text(&text); - let text = to_provider.register_text(s); - Thing::Text { text } - } - Thing::Child { - text, - child1, - child2, - } => { - let s = from_provider.get_text(text); - let text = to_provider.register_text(s); - let ch1 = from_provider.get_leaf(child1); - let ch1 = transfer_nodes(ch1, from_provider, to_provider, buf); - let child1 = to_provider.register_leaf(ch1); - let ch2 = from_provider.get_leaf(child2); - let ch2 = transfer_nodes(ch2, from_provider, to_provider, buf); - let child2 = to_provider.register_leaf(ch2); - Thing::Child { - text, - child1, - child2, - } - } - Thing::Multiple { children } => { - let ch = from_provider.get_leaves(children); - let start_ind = buf.len(); - for child in ch { - let to_child = transfer_nodes(child, from_provider, to_provider, buf); - buf.push(to_child); - } - let children = to_provider.register_leaves(buf.drain(start_ind..)); - Thing::Multiple { children } - } - } - } - - struct UnsealedFullOwner<'text> { - yet_unowned_text: Cow<'text, str>, - owned_text: String, - things: Vec>, - } - - impl<'text> UnsealedFullOwner<'text> { - pub fn new(yet_unowned: &'text str) -> Self { - Self { - yet_unowned_text: yet_unowned.into(), - owned_text: String::new(), - things: Vec::new(), - } - } - pub unsafe fn drop_unowned(self) -> UnsealedFullOwner<'static> { - let Self { - owned_text, - things: children, - .. - } = self; - UnsealedFullOwner { - yet_unowned_text: "".into(), - owned_text, - things: children, - } - } - // TODO add panic info - pub unsafe fn provide(&mut self, data: impl Into) { - let s = data.into(); - // assert_eq!(self.yet_unowned_text, s, "Provided data must be the same!"); - self.yet_unowned_text = Cow::Owned(s) - } - } - - impl<'text> UnsealedStorage for UnsealedFullOwner<'text> { - type Sealed = FullOwner; - - fn register_text(&mut self, text: &str) -> TextOwn { - if let Some(range) = try_find_pointer_match(&self.yet_unowned_text, text) { - return TextOwn(range); - } - if let Some(ind) = self.yet_unowned_text.find(text) { - return TextOwn(ind..ind + text.len()); - } - let len = self.yet_unowned_text.len(); - if let Some(mut ind) = self.owned_text.find(text) { - ind += len; - return TextOwn(ind..ind + text.len()); - } - let start_ind = self.owned_text.len() + len; - self.owned_text.push_str(text); - let end_ind = self.owned_text.len() + len; - TextOwn(start_ind..end_ind) - } - - fn register_leaf(&mut self, child: Thing) -> LeafInd { - let ind = self.things.len(); - self.things.push(child); - LeafInd(ind) - } - - fn register_leaves<'s>( - &'s mut self, - children: impl Iterator>, - ) -> LeavesRange { - let start_ind = self.things.len(); - self.things.extend(children); - let end_ind = self.things.len(); - LeavesRange(start_ind..end_ind) - } - - fn seal(self) -> Self::Sealed { - let Cow::Owned(s) = self.yet_unowned_text else { - panic!("Full Owner cannot be baked until data is provided to it") - }; - FullOwner { - now_owned_text: s.into_boxed_str(), - owned_text: self.owned_text.into_boxed_str(), - things: self.things.into_boxed_slice(), - } - } - } - - struct FullOwner { - now_owned_text: Box, - owned_text: Box, - things: Box<[Thing]>, - } - - impl SealedStorage for FullOwner { - fn get_text<'s>(&'s self, TextOwn(range): &TextOwn) -> &'s str { - let (start, end) = (range.start, range.end); - let len = self.now_owned_text.len(); - if start >= len { - return &self.owned_text[start - len..end - len]; - } - &self.now_owned_text[start..end] - } - - fn get_leaf<'s>( - &'s self, - &LeafInd(i): &LeafInd, - ) -> &'s Thing { - &self.things[i] - } - - fn get_leaves<'s>( - &'s self, - LeavesRange(range): &LeavesRange, - ) -> &'s [Thing] { - &self.things[range.clone()] - } - } - - #[cfg(test)] - mod tests { - fn gen_text(rng: &mut R) -> String { - // let it be of some length - let l: usize = rng.gen_range(3..=10); - let mut s = String::new(); - for _ in 0..l { - s.push(if rng.gen_bool(0.5) { - rng.gen_range('a'..='z') - } else if rng.gen_bool(0.4) { - rng.gen_range('а'..='я') - } else { - [' ', ',', '.'][rng.gen_range(0..3)] - }) - } - s - } - - fn gen_contained_text<'text, R: rand::Rng + ?Sized>( - text: &'text str, - ) -> impl FnMut(&mut R) -> &'text str { - |rng| { - // basically just select two random indices - let mut ind1 = rng.gen_range(0..text.len()); - let mut ind2 = rng.gen_range(0..text.len()); - // swap them, if needed - if ind1 > ind2 { - std::mem::swap(&mut ind1, &mut ind2); - } - &text[ind1..ind2] - } - } - - fn gen_maybe_contained_text<'text, R: rand::Rng + ?Sized>( - text: &'text str, - ) -> impl FnMut(&mut R) -> std::borrow::Cow<'text, str> { - let mut contained = gen_contained_text(text); - move |rng| { - if rng.gen_bool(0.5) { - contained(rng).into() - } else { - gen_text(rng).into() - } - } - } - - fn generate_thing< - 'owner, - R: rand::Rng + ?Sized, - Text, - Child, - Children, - P: UnsealedStorage, - S: AsRef, - >( - provider: &mut P, - rng: &mut R, - text_gen: &mut impl FnMut(&mut R) -> S, - children_buf: &mut Vec>, - nodes_left: &mut usize, - ) -> Thing { - *nodes_left = nodes_left.saturating_sub(1); - let generated = if *nodes_left == 0 || rng.gen_bool(0.6) { - // assume 60% or anything over specified number are text variants - // we need some random text for it - let text = text_gen(rng); - Thing::Text { - text: provider.register_text(text.as_ref()), - } - } else if rng.gen_bool(0.9) { - // say, 36% are child owners - // we need some text and children for it - let text = text_gen(rng); - let child1 = generate_thing(provider, rng, text_gen, children_buf, nodes_left); - let child2 = generate_thing(provider, rng, text_gen, children_buf, nodes_left); - Thing::Child { - text: provider.register_text(text.as_ref()), - child1: provider.register_leaf(child1), - child2: provider.register_leaf(child2), - } - } else { - // rest 4% own 1-5 children - let buf_start = children_buf.len(); - for _ in 0..rng.gen_range(1..=5) { - let child = generate_thing(provider, rng, text_gen, children_buf, nodes_left); - children_buf.push(child); - } - let children = provider.register_leaves(children_buf.drain(buf_start..)); - Thing::Multiple { children } - }; - // average coefficient is 0.6 * 0 + 0.36 * 2 + 2.5 * 0.04 = 0.82 - // meaning this thing is likely to die at some point (I hope for that, at least) - generated - } - - use crate::data::borrowing_concept::{UnsealedFullOwner, UnsealedStorage}; - - use super::{ - text_maybe_own_provider, text_own_provider, text_ref_provider, Thing, ThingTree, - }; - - // TODO check these versions of fuzzes to actually run correctly - #[test] - fn ref_fuzz() { - // arrange - let rng = &mut rand::thread_rng(); - let text = "Lorem ipsum dolor sir ammet, concestesometghing idk never learned latin"; - for _ in 0..100_000 { - let mut provider = text_ref_provider(text); - - // act - let thing = generate_thing( - &mut provider, - rng, - &mut gen_contained_text(text), - &mut Vec::new(), - &mut 100, - ); - let tree = ThingTree::new(thing, provider); - - // assert - let mut output = String::new(); - tree.write_to(&mut output) - .expect("Should be able to write into string"); - } - } - #[test] - fn owned_fuzz() { - // arrange - let rng = &mut rand::thread_rng(); - for _ in 0..100_000 { - let mut provider = text_own_provider(); - - // act - let thing = - generate_thing(&mut provider, rng, &mut gen_text, &mut Vec::new(), &mut 100); - let tree = ThingTree::new(thing, provider); - - // assert - let mut output = String::new(); - tree.write_to(&mut output) - .expect("Should be able to write into string"); - } - } - - #[test] - fn maybe_owned_fuzz() { - // arrange - let rng = &mut rand::thread_rng(); - let text = "Lorem ipsum dolor sir ammet, concestesometghing idk never learned latin"; - for _ in 0..100_000 { - let mut provider = text_maybe_own_provider(text); - - // act - let thing = generate_thing( - &mut provider, - rng, - &mut gen_maybe_contained_text(text), - &mut Vec::new(), - &mut 100, - ); - let tree = ThingTree::new(thing, provider); - - // assert - let mut output = String::new(); - tree.write_to(&mut output) - .expect("Should be able to write into string"); - } - } - - #[test] - fn fuzz_transfer() { - // arrange - let rng = &mut rand::thread_rng(); - let text = "Lorem ipsum dolor sir ammet, concestesometghing idk never learned latin"; - for _ in 0..100_000 { - let mut provider = text_maybe_own_provider(text); - let thing = generate_thing( - &mut provider, - rng, - &mut gen_maybe_contained_text(text), - &mut Vec::new(), - &mut 100, - ); - let original_tree = ThingTree::new(thing, provider); - - // act - let mut moved_provider = text_own_provider(); - let moved_thing = original_tree.move_to(&mut moved_provider); - let moved_tree = ThingTree::new(moved_thing, moved_provider); - - // assert - let mut original = String::new(); - original_tree - .write_to(&mut original) - .expect("Should be able to write into string"); - let mut moved = String::new(); - moved_tree - .write_to(&mut moved) - .expect("Should be able to write into string"); - assert_eq!( - original, moved, - "Moved things should produce the same result" - ); - } - } - - #[test] - fn fuzz_to_owned() { - // arrange - let rng = &mut rand::thread_rng(); - for _ in 0..100_000 { - let text = - "Lorem ipsum dolor sir ammet, concestesometghing idk never learned latin" - .to_string() - .into_boxed_str(); - let mut provider = text_maybe_own_provider(&text); - let thing = generate_thing( - &mut provider, - rng, - &mut gen_maybe_contained_text(&text), - &mut Vec::new(), - &mut 100, - ); - let original_tree = ThingTree::new(thing, provider); - - // act - let mut owning_provider = UnsealedFullOwner::new(&text); - let owning_thing = original_tree.move_to(&mut owning_provider); - let mut original = String::new(); - original_tree - .write_to(&mut original) - .expect("Should be able to write into string"); - drop(original_tree); - // SAFETY: - // data provided and data `OwnedProvider` referred to here is the same, - // as it's contained in the same immutable variable - let owning_provider = unsafe { - let mut owning_provider = owning_provider.drop_unowned(); - owning_provider.provide(text); - owning_provider - }; - let owning_tree = ThingTree::new(owning_thing, owning_provider); - - // assert - let mut owned = String::new(); - owning_tree - .write_to(&mut owned) - .expect("Should be able to write into string"); - assert_eq!( - original, owned, - "Owned things should produce the same result" - ); - } - } - } -} diff --git a/src/gen/ayano.rs b/src/gen/ayano.rs index 073ed05..11ca738 100644 --- a/src/gen/ayano.rs +++ b/src/gen/ayano.rs @@ -7,27 +7,23 @@ use pyo3::{ PyAny, Python, }; pub use python::TracedPyErr; -use rand::Rng; use regex::Regex; use std::{ borrow::Cow, - collections::BinaryHeap, - fmt::{Arguments, Debug, Write}, + fmt::{Arguments, Debug, Display, Write}, fs::File, io::{BufRead, BufReader}, num::{ParseFloatError, ParseIntError}, - path::{self, Path, PathBuf}, + ops::RangeFrom, + path::{Path, PathBuf}, str::FromStr, }; use crate::{ - data::{AyanoBlock, DisplayState, Token, Tokens}, + data::{AyanoBlock, Token, Tokens}, lexer::KyomatoLexError, - path_engine::{self, PathEngine}, - util::{ - Equivalent, GenericRange, GenericRangeParseError, Immutable, InlinedStack, - InlinedStackAccessor, StaticDebug, - }, + path_engine::PathEngine, + util::{Equivalent, GenericRange, GenericRangeParseError, InlinedStack, InlinedStackAccessor}, }; use self::python::{init, InitToken}; @@ -70,18 +66,18 @@ impl PartialEq for BlockInfo { } #[derive(Debug)] -pub struct AyanoBuilder { +pub struct Builder { code: String, blocks: Vec, } -impl Default for AyanoBuilder { +impl Default for Builder { fn default() -> Self { Self::new() } } -impl AyanoBuilder { +impl Builder { pub fn new() -> Self { Self { code: String::new(), @@ -104,7 +100,7 @@ impl AyanoBuilder { // Now, append the code to python handler if let Some(function_name) = info.function_name.as_ref() { - python::append_function(&mut self.code, &function_name, &transformed_code); + python::append_function(&mut self.code, function_name, &transformed_code); } else { // We must insert an execution path for this block now, so here we go: let insert_path = if let Some(insert_path) = info.insert_path.as_deref() { @@ -138,10 +134,10 @@ impl AyanoBuilder { Ok(()) } - pub fn initialize(self) -> Result { - let Self { code, mut blocks } = self; - let token = init(&code.as_str())?; - Ok(AyanoExecutor { + pub fn initialize(self) -> Result { + let Self { code, blocks } = self; + let token = init(code.as_str())?; + Ok(Executor { token, blocks: blocks.into(), }) @@ -149,13 +145,13 @@ impl AyanoBuilder { } #[derive(Debug)] -pub struct AyanoExecutor { +pub struct Executor { token: InitToken, blocks: Box<[BlockInfo]>, } #[derive(Debug, derive_more::From, PartialEq, thiserror::Error)] -pub enum AyanoError<'b> { +pub enum Error<'b> { #[error("Python error occured: {:?}", .0)] Python(Equivalent), #[error("Traced python error occured: {}", (.0).0)] @@ -170,21 +166,21 @@ pub enum AyanoError<'b> { PathEngine(Equivalent>), } -impl From for AyanoError<'_> { +impl From for Error<'_> { fn from(value: PyErr) -> Self { Equivalent(value).into() } } -impl From for AyanoError<'_> { +impl From for Error<'_> { fn from(value: TracedPyErr) -> Self { Equivalent(value).into() } } -type AyanoResult<'b, T = ()> = Result>; +type AyanoResult<'b, T = ()> = Result>; -impl AyanoExecutor { +impl Executor { // this could easily be part of `display_token`, but I separated it for debugging purposes fn call_block< 'token, @@ -202,13 +198,13 @@ impl AyanoExecutor { where for<'py> Transform: FnOnce(&'py PyAny, Python<'py>) -> AyanoResult<'source, Out>, { - let Some(info) = self.blocks.into_iter().find(|bi| bi.ident == *token.ident) else { + let Some(info) = self.blocks.iter().find(|bi| bi.ident == *token.ident) else { // this block was not passed here before - return Err(AyanoError::NoFunction(token.clone())); + return Err(Error::NoFunction(token.clone())); }; let Some(function_name) = info.function_name.as_ref() else { // this block was passed here before, but it's not a function - return Err(AyanoError::NoFunction(token.clone())); + return Err(Error::NoFunction(token.clone())); }; // We must insert an execution path for this block now, so here we go: let insert_path = if let Some(insert_path) = info.insert_path.as_deref() { @@ -244,9 +240,9 @@ impl AyanoExecutor { self.call_block(token, |res, _| Ok(parse_ayano(res)?), path_engine) } - pub fn display_blocks<'s>(&'s self) -> impl IntoIterator { + pub fn display_blocks(&self) -> impl IntoIterator { self.blocks - .into_iter() + .iter() .filter_map(|info| info.display_info.as_ref()) } } @@ -258,22 +254,24 @@ mod ayano_tests { {$name:ident, $code:literal, $output:literal} => { #[test] fn $name() { + use rand::Rng; + use $crate::path_engine; // arrange let block = AyanoBlock { - ident: StaticDebug(rand::thread_rng().gen()), - display_state: DisplayState::NotDisplayed, + ident: $crate::util::StaticDebug(rand::thread_rng().gen()), + display_state: $crate::data::DisplayState::NotDisplayed, is_static: false, code: $code.into(), insert_path: None, spaces_before: 0, }; - let mut builder = AyanoBuilder::new(); + let mut builder = Builder::new(); // act builder.add_block(&block, &path_engine::primitive()).expect("Should be able to add block"); // dbg!(&handler); let executor = builder.initialize().expect("Should be able to initialize"); - let output = executor.call_block(&block, |res, _| Ok::<_, super::AyanoError<'_>>(res.str()?.to_string()), &path_engine::primitive()).expect("Should be able to call this function"); + let output = executor.call_block(&block, |res, _| Ok::<_, super::Error<'_>>(res.str()?.to_string()), &path_engine::primitive()).expect("Should be able to call this function"); // assert assert_eq!(output, $output); @@ -305,21 +303,23 @@ y = x / 100 {$name:ident, $static_code:literal, $function_code:literal, $expected_output:literal} => { #[test] fn $name() { + use rand::Rng; + use $crate::path_engine; // arrange - let mut builder = AyanoBuilder::new(); + let mut builder = Builder::new(); let static_block = AyanoBlock { - ident: StaticDebug(rand::thread_rng().gen()), + ident: $crate::util::StaticDebug(rand::thread_rng().gen()), code: $static_code.into(), insert_path: None, - display_state: DisplayState::NotDisplayed, + display_state: $crate::data::DisplayState::NotDisplayed, is_static: true, spaces_before: 0, }; let function_block = AyanoBlock { - ident: StaticDebug(rand::thread_rng().gen()), + ident: $crate::util::StaticDebug(rand::thread_rng().gen()), code: $function_code.into(), insert_path: None, - display_state: DisplayState::NotDisplayed, + display_state: $crate::data::DisplayState::NotDisplayed, is_static: false, spaces_before: 0, }; @@ -333,7 +333,7 @@ y = x / 100 let function_output = executor.call_block(&function_block, |res, _| Ok(res.str()?.to_string()), &path_engine::primitive()); // assert - assert_eq!(static_output, Err(AyanoError::NoFunction(static_block.clone())), "Static block shouldn't be callable!"); + assert_eq!(static_output, Err(Error::NoFunction(static_block.clone())), "Static block shouldn't be callable!"); assert_eq!(function_output, Ok($expected_output.to_string()), "Function should give expected result"); } }; @@ -351,29 +351,31 @@ y", "2"} {$name:ident, $static_code:literal, $function1_code:literal, $function2_code:literal, $expected_output1:literal, $expected_output2:literal} => { #[test] fn $name() { + use rand::Rng; + use $crate::path_engine; // arrange - let mut builder = AyanoBuilder::new(); + let mut builder = Builder::new(); let static_block = AyanoBlock { - ident: StaticDebug(rand::thread_rng().gen()), + ident: $crate::util::StaticDebug(rand::thread_rng().gen()), code: $static_code.into(), insert_path: None, - display_state: DisplayState::NotDisplayed, + display_state: $crate::data::DisplayState::NotDisplayed, is_static: true, spaces_before: 0, }; let function1_block = AyanoBlock { - ident: StaticDebug(rand::thread_rng().gen()), + ident: $crate::util::StaticDebug(rand::thread_rng().gen()), code: $function1_code.into(), insert_path: None, - display_state: DisplayState::NotDisplayed, + display_state: $crate::data::DisplayState::NotDisplayed, is_static: false, spaces_before: 0, }; let function2_block = AyanoBlock { - ident: StaticDebug(rand::thread_rng().gen()), + ident: $crate::util::StaticDebug(rand::thread_rng().gen()), code: $function2_code.into(), insert_path: None, - display_state: DisplayState::NotDisplayed, + display_state: $crate::data::DisplayState::NotDisplayed, is_static: false, spaces_before: 0, }; @@ -389,7 +391,7 @@ y", "2"} let function2_output = executor.call_block(&function1_block, |res, _| Ok(res.str()?.to_string()), &path_engine::primitive()); // assert - assert_eq!(static_output, Err(AyanoError::NoFunction(static_block.clone())), "Static block shouldn't be callable!"); + assert_eq!(static_output, Err(Error::NoFunction(static_block.clone())), "Static block shouldn't be callable!"); assert_eq!(function1_output, Ok($expected_output1.to_string()), "Function should give expected result"); assert_eq!(function2_output, Ok($expected_output2.to_string()), "Function should give expected result"); } @@ -405,10 +407,6 @@ fn function_name(ident: &impl std::ops::Deref) -> String { format!("f_{}", **ident) } -fn block_name(ident: &impl std::ops::Deref) -> String { - format!("ayano_block__{}", **ident) -} - #[derive( Debug, derive_more::From, @@ -594,12 +592,14 @@ mod apply_ayano_tests { {$name:ident, $input:literal, $output:expr, $is_function:literal} => { #[test] fn $name() { + use rand::Rng; + use $crate::path_engine; // arrange let block = AyanoBlock { - ident: StaticDebug(rand::thread_rng().gen()), + ident: $crate::util::StaticDebug(rand::thread_rng().gen()), code: $input.into(), insert_path: None, - display_state: DisplayState::NotDisplayed, + display_state: $crate::data::DisplayState::NotDisplayed, is_static: false, spaces_before: 0, }; @@ -689,6 +689,7 @@ y = x + 2 #[test] fn insert_test() { + use rand::Rng; // arrange let insert_path: PathBuf = "src/gen/ayano/test_code.py" .parse() @@ -696,8 +697,8 @@ y = x + 2 let code = "@dev: x,y"; let block = AyanoBlock { - ident: StaticDebug(rand::thread_rng().gen()), - display_state: DisplayState::NotDisplayed, + ident: crate::util::StaticDebug(rand::thread_rng().gen()), + display_state: crate::data::DisplayState::NotDisplayed, code: code.into(), insert_path: Some(insert_path.into()), is_static: false, @@ -705,7 +706,7 @@ y = x + 2 }; // act - let transformed_code = apply_ayano(&block, &path_engine::primitive()) + let transformed_code = apply_ayano(&block, &crate::path_engine::primitive()) .expect("Should be able to apply ayano transform"); // assert @@ -927,17 +928,27 @@ impl<'l> TryFrom<&'l str> for SyntaxData<'l> { if let Some(s) = chip_literal!(s, "@fig:") { let mut src = None; - let mut ident = None; + let mut ident_inner = None; let mut caption = None; let mut width = None; - parse_args!(s, src, "src", ident, "ident", caption, "caption", width, "width"); + parse_args!( + s, + src, + "src", + ident_inner, + "ident", + caption, + "caption", + width, + "width" + ); needed!(src, "src"); return Ok(Self { indent, syntax: Syntax::Figure { src, caption, - ident, + ident: ident_inner, width, }, }); @@ -947,21 +958,25 @@ impl<'l> TryFrom<&'l str> for SyntaxData<'l> { let mut src = None; let mut rows = None; let mut columns = None; - let mut ident = None; + let mut ident_inner = None; let mut caption = None; parse_args!( - s, src, "src", rows, "rows", columns, "columns", ident, "ident", caption, "caption" + s, + src, + "src", + rows, + "rows", + columns, + "columns", + ident_inner, + "ident", + caption, + "caption" ); needed!(src, "src"); - let rows = rows - .map(|val| val.parse()) - .unwrap_or(Ok(GenericRange::Full))?; + let rows = rows.map_or(Ok(GenericRange::Full), str::parse)?; - let columns = if let Some(val) = columns.as_ref() { - Some(parse_columns(&val)) - } else { - None - }; + let columns = columns.as_ref().map(|val| parse_columns(val)); return Ok(Self { indent, @@ -969,7 +984,7 @@ impl<'l> TryFrom<&'l str> for SyntaxData<'l> { src, rows, columns, - ident, + ident: ident_inner, caption, }, }); @@ -981,9 +996,19 @@ impl<'l> TryFrom<&'l str> for SyntaxData<'l> { }; let mut rows = None; let mut columns = None; - let mut ident = None; + let mut ident_inner = None; let mut caption = None; - parse_args!(s, rows, "rows", columns, "columns", ident, "ident", caption, "caption"); + parse_args!( + s, + rows, + "rows", + columns, + "columns", + ident_inner, + "ident", + caption, + "caption" + ); needed!(rows, "rows"); needed!(columns, "columns"); return Ok(Self { @@ -992,7 +1017,7 @@ impl<'l> TryFrom<&'l str> for SyntaxData<'l> { generator, rows: rows.parse()?, columns: columns.parse()?, - ident, + ident: ident_inner, caption, }, }); @@ -1046,8 +1071,8 @@ mod syntax_data_tests { test! {fig4_width_expr, r#" @fig: caption = "Wow, such caption", width= WIDTH + 3 , ident="path" ,src = "path.png" "#, SyntaxData {indent: 4, syntax: Syntax::Figure{ src: "\"path.png\"", caption: Some("\"Wow, such caption\""), ident: Some("\"path\""), width: Some("WIDTH + 3")}}} test! {csv_table1, r#" @csv_table: src = path/to/table.csv, caption = "such caption, wow""#, SyntaxData {indent: 3, syntax: Syntax::CsvTable { src: "path/to/table.csv", rows: GenericRange::Full, columns: None, ident: None, caption: Some("\"such caption, wow\"") }}} - test! {csv_table2, r#" @csv_table: src = path/to/table.csv, rows = 1..1000"#, SyntaxData {indent: 3, syntax: Syntax::CsvTable { src: "path/to/table.csv", rows: GenericRange::DoubleBounded(1..1000), columns: None, ident: None, caption: None, }}} - test! {csv_table3, r#" @csv_table: src = path/to/table.csv, rows = 2.."#, SyntaxData {indent: 3, syntax: Syntax::CsvTable { src: "path/to/table.csv", rows: GenericRange::From(2..), columns: None, ident: None, caption: None }}} + test! {csv_table2, r" @csv_table: src = path/to/table.csv, rows = 1..1000", SyntaxData {indent: 3, syntax: Syntax::CsvTable { src: "path/to/table.csv", rows: GenericRange::DoubleBounded(1..1000), columns: None, ident: None, caption: None, }}} + test! {csv_table3, r" @csv_table: src = path/to/table.csv, rows = 2..", SyntaxData {indent: 3, syntax: Syntax::CsvTable { src: "path/to/table.csv", rows: GenericRange::From(2..), columns: None, ident: None, caption: None }}} test! {csv_table4, r#" @csv_table: src = path/to/table.csv, rows = 2.., columns = ["a", ("b", "b_err")]"#, SyntaxData {indent: 3, syntax: Syntax::CsvTable { src: "path/to/table.csv", rows: GenericRange::From(2..), columns: Some(vec![c!("a"),c!("b","b_err")]), caption: None, ident: None }}} test! {csv_table5, r#" @csv_table: src = path/to/table.csv, rows = 2.., columns = ["a", ("b", "b_err")], caption = "Lol, there's a bunch of random stuff going on, this should not be included into a syntax data""#, SyntaxData {indent: 4, syntax: Syntax::CsvTable { src: "path/to/table.csv", rows: GenericRange::From(2..), columns: Some(vec![c!("a"),c!("b","b_err")]), @@ -1081,7 +1106,7 @@ impl<'w, W: Write + ?Sized> CodeWriter<'w, W> { } fn write_str(&mut self, s: &str) -> std::fmt::Result { - self.inner.write_str(s); + self.inner.write_str(s)?; Ok(()) } @@ -1141,8 +1166,8 @@ impl Syntax<'_> { // try checking it out, and warning if it does not exist // TODO add option to opt this out let src_path = src.trim_matches(['\'', '\"']); - let src_path = Path::new(&*src_path); - let mut src_path = match path_engine.csv(src_path, insert_path.as_deref()) { + let src_path = Path::new(src_path); + let src_path = match path_engine.csv(src_path, insert_path.as_deref()) { Ok(existing_table) => existing_table, Err(error) => { // figure was not found, but this can be ok, if program generates it itself @@ -1172,7 +1197,7 @@ impl Syntax<'_> { } writer.write_str(", ")?; if let Some(width) = width { - writer.write_str(&width.to_string())?; + writer.write_str(width.as_ref())?; } else { writer.write_str("None")?; } @@ -1190,7 +1215,7 @@ impl Syntax<'_> { // try checking it out, and warning if it does not exist // TODO add option to opt this out let src_path = src.trim_matches(['\'', '\"']); - let src_path = Path::new(&*src_path); + let src_path = Path::new(src_path); let mut src_path = match path_engine.csv(src_path, insert_path.as_deref()) { Ok(existing_table) => existing_table, Err(error) => { @@ -1203,9 +1228,7 @@ impl Syntax<'_> { // the problem here - this type of syntax specifically relies on Python reading the file themselves // and if there is an insert path - Python will be executed at the insert directory // to check for that, we do - if let Some(insert_directory) = - insert_path.as_ref().map(|p| p.parent()).flatten() - { + if let Some(insert_directory) = insert_path.as_ref().and_then(|p| p.parent()) { // if there's no insert path - ignore that // if insert path has no parent - it's a file in current execution directory - no need to relocate the path // I could've implemented this thing myself, but why would I, was already done: @@ -1253,8 +1276,9 @@ impl Syntax<'_> { let headers = |writer: &mut CodeWriter<'_, W>| { if let Some(columns) = columns.as_ref() { writer.write_str("[")?; - for i in 0..columns.len() { - columns[i].write_to(writer)?; + + for col in columns { + col.write_to(writer)?; writer.write_str(", ")?; } writer.write_str("]")?; @@ -1267,8 +1291,8 @@ impl Syntax<'_> { let line_syntax = |writer: &mut CodeWriter<'_, W>| { if let Some(columns) = columns.as_ref() { writer.write_str("[")?; - for i in 0..columns.len() { - match columns[i] { + for col in columns { + match col { CsvTableColumn::Single(col) => { writer.write_str("line[\"")?; writer.write_str(col)?; @@ -1309,7 +1333,7 @@ impl Syntax<'_> { } else { writer.write_str("[")?; line_syntax(writer)?; - writer.write_str(" for line in ___reader___]")? + writer.write_str(" for line in ___reader___]")?; }; Ok::<_, std::fmt::Error>(()) }; @@ -1378,6 +1402,7 @@ mod syntax_display_tests { {$name:ident, $input:expr, $output:literal} => { #[test] fn $name() { + use $crate::path_engine; // arrange let expected_output = $output.to_string(); let mut actual_output = String::new(); @@ -1445,6 +1470,7 @@ mod last_line_tests { {$name:ident, $input:literal, $output:expr} => { #[test] fn $name() { + use $crate::path_engine; // arrange // act @@ -1475,9 +1501,9 @@ mod last_line_tests { "return \"fig\", \"./picture.jpg\", \"cool\", \"Some very cool picture\", None"} test! {fig4, "@fig: caption=\"Some very cool picture\", src=\"./picture.jpg\", ident=\"cool\"", "return \"fig\", \"./picture.jpg\", \"cool\", \"Some very cool picture\", None"} - test! {fig5, "@fig: caption=\"Some very cool picture\", ident=\"cool\"", -, SyntaxError::MissingArgument("src".into())} - test! {fig6, "@fig: caption=\"Some very cool picture\", ident=\"cool\", coolness=1000", -, SyntaxError::UnknownArgument("coolness".to_string().into())} - test! {fig7, "@fig: caption=\"Some very cool picture\", ident=\"cool\", coolness=1000, src=\"path.png\"", -, SyntaxError::UnknownArgument("coolness".to_string().into())} + test! {fig5, "@fig: caption=\"Some very cool picture\", ident=\"cool\"", -, SyntaxError::MissingArgument("src")} + test! {fig6, "@fig: caption=\"Some very cool picture\", ident=\"cool\", coolness=1000", -, SyntaxError::UnknownArgument("coolness".to_string())} + test! {fig7, "@fig: caption=\"Some very cool picture\", ident=\"cool\", coolness=1000, src=\"path.png\"", -, SyntaxError::UnknownArgument("coolness".to_string())} test! {csv_tab1e_simple, "@csv_table: src=\"some/path/idk.csv\"", r#" import csv @@ -1620,10 +1646,10 @@ with open("some/path/idk.csv") as ___csv_file___: return "tab", ["a", "b", ], list(___line_generator___()), None, None "#} - test! {gen_table1, r#"@gen_table: lambda row, col: 1; rows= 5, columns =2"#, + test! {gen_table1, r"@gen_table: lambda row, col: 1; rows= 5, columns =2", r#"___generator___ = lambda row, col: 1 return "tab", [___generator___(0, 0), ___generator___(0, 1), ], [[___generator___(1, 0), ___generator___(1, 1), ], [___generator___(2, 0), ___generator___(2, 1), ], [___generator___(3, 0), ___generator___(3, 1), ], [___generator___(4, 0), ___generator___(4, 1), ], ], None, None"#} - test! {gen_table2, r#"@gen_table: lambda row, col: -1;columns =2, rows= 5"#, + test! {gen_table2, r"@gen_table: lambda row, col: -1;columns =2, rows= 5", r#"___generator___ = lambda row, col: -1 return "tab", [___generator___(0, 0), ___generator___(0, 1), ], [[___generator___(1, 0), ___generator___(1, 1), ], [___generator___(2, 0), ___generator___(2, 1), ], [___generator___(3, 0), ___generator___(3, 1), ], [___generator___(4, 0), ___generator___(4, 1), ], ], None, None"#} } @@ -1703,7 +1729,7 @@ fn parse_ayano(python_output: &PyAny) -> Result, ParsingError> { .get_item(2) .and_then(|item| item.str()) .and_then(|s| s.to_str()))?; - return Ok(value_error_format(value, error)?); + return value_error_format(value, error); } "fig" => { // this is a figure syntax. @@ -1851,7 +1877,7 @@ mod parsing_tests { }; } - test! {text1, |py| PyString::new(py, "Some text".into()), Ok(Token::text("Some text"))} + test! {text1, |py| PyString::new(py, "Some text"), Ok(Token::text("Some text"))} test! {text2, |py| PyFloat::new(py, 0.0025), Ok(Token::text("0.0025"))} test! {text3, |py| PyList::new(py, [1, 2, 3]), Ok(Token::text("[1, 2, 3]"))} test! {text4, |py| PyTuple::new(py, ["lol", "some_info"]), Ok(Token::text("('lol', 'some_info')"))} @@ -1862,8 +1888,8 @@ mod parsing_tests { Ok(Token::Multiple{tokens: Tokens::new([Token::text("1.0"), Token::InlineMath{content:r"\pm".into(), space_before: false}, Token::text("0.5")])})} test! {value_error2, |py| PyTuple::new(py, ["err".into_py(py), "1.0".into_py(py), 0.5.into_py(py)]), Ok(Token::Multiple{tokens:Tokens::new([Token::text("1.0"), Token::InlineMath{content:r"\pm".into(), space_before: false}, Token::text("0.5")])})} - test! {value_error_format1, |py| PyTuple::new(py, ["err".into_py(py), "1.987654321".into_py(py), 0.00000123456.into_py(py)]), - Ok(value_error_token(1.9876543, 0.0000012))} + test! {value_error_format1, |py| PyTuple::new(py, ["err".into_py(py), "1.987654321".into_py(py), 0.000_001_234_56.into_py(py)]), + Ok(value_error_token(1.987_654_3, 0.000_001_2))} // TODO probably should add some more tests for value-error formatting test! {figure_nocaption1, |py| PyTuple::new(py, ["fig".into_py(py), "path/to/file.png".into_py(py), "circle".into_py(py), None::<()>.into_py(py), None::.into_py(py)]), @@ -1916,7 +1942,7 @@ mod parsing_tests { PyList::new(py, [ PyList::new(py, [PyTuple::new(py, [ "err".into_py(py), - 1.012345.into_py(py), + 1.012_345.into_py(py), "0.25".into_py(py) ]).into_py(py), 0.25.into_py(py)]), PyList::new(py, ["cell_21".into_py(py), (-2).into_py(py)]), @@ -1971,10 +1997,8 @@ impl FromStr for PrimitiveDecimal { let _: f64 = s.parse()?; let sign = if s.starts_with('+') { false - } else if s.starts_with('-') { - true } else { - false + s.starts_with('-') }; s = s.trim_start_matches(['+', '-']); let (s, exponent) = if let Some(split) = s.split_once(['e', 'E']) { @@ -1987,9 +2011,9 @@ impl FromStr for PrimitiveDecimal { } else { (s, "") }; - let zero = (isize::try_from(integer.len()).expect("Why.") - 1).try_into().expect("How in the world did you get 2B digits into the integer part? Things are certainly getting out of hand"); + let zero = isize::try_from(integer.len()).expect("How in the world did you get 2B digits into the integer part? Things are certainly getting out of hand") - 1; let digits = integer.chars().chain(fraction.chars()).collect_vec(); - let mut res = Self { zero, digits, sign }; + let mut res = Self { digits, zero, sign }; // Account for exponent let exponent: isize = exponent @@ -2000,8 +2024,8 @@ impl FromStr for PrimitiveDecimal { } } -impl ToString for PrimitiveDecimal { - fn to_string(&self) -> String { +impl Display for PrimitiveDecimal { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let len = self.digits.len(); let (integer_part, fraction_part) = if self.zero < 0 { let additinal: usize = (-self.zero - 1).try_into().expect("Why.?"); @@ -2013,7 +2037,7 @@ impl ToString for PrimitiveDecimal { .collect(), ) } else { - let zero = self.zero as usize; + let zero: usize = self.zero.try_into().expect("Why."); if zero + 1 >= len { // All the digits are in the integer part :) ( @@ -2025,14 +2049,12 @@ impl ToString for PrimitiveDecimal { ) } else { let (integer, fraction) = self.digits.split_at(zero + 1); - ( - integer.into_iter().collect(), - fraction.into_iter().collect(), - ) + (integer.iter().collect(), fraction.iter().collect()) } }; let sign = if self.sign { "-" } else { "" }; - format!( + write!( + f, "{sign}{}", match (integer_part.is_empty(), fraction_part.is_empty()) { (true, true) => String::from("0"), @@ -2049,14 +2071,17 @@ impl PrimitiveDecimal { self.digits .iter() .position(|ch| ch != &'0') - .map(|pos| self.zero.wrapping_sub_unsigned(pos)) - .unwrap_or(0) + .map_or(0, |pos| self.zero.wrapping_sub_unsigned(pos)) } pub fn get_digit(&self, digit: isize) -> char { let index = self.zero.wrapping_sub(digit); (index >= 0) - .then_some(self.digits.iter().nth(index as usize).cloned()) + .then_some( + self.digits + .get(index.try_into().unwrap_or(usize::MAX)) + .copied(), + ) .flatten() .unwrap_or('0') } @@ -2064,7 +2089,7 @@ impl PrimitiveDecimal { fn set_digit(&mut self, digit: isize, value: char) { #[cfg(test)] assert!( - matches!(value, '0'..='9'), + value.is_ascii_digit(), "New digit's value should be a valid decimal digit" ); @@ -2078,7 +2103,7 @@ impl PrimitiveDecimal { self.zero -= index; self.digits.insert(0, value); } else { - let index = index as usize; + let index: usize = index.try_into().expect("Why."); if index >= len { self.digits .extend(repeat_n('0', index - len).chain(std::iter::once(value))); @@ -2141,7 +2166,7 @@ impl PrimitiveDecimal { '8' => '9', _ => unreachable!(), }, - ) + ); } // Lastly, discard the fractional part @@ -2149,7 +2174,9 @@ impl PrimitiveDecimal { // fractional part is not the entire value if self.zero + 2 <= self.digits.len().try_into().expect("Wha...?") { // ... and is actually present in the chars array - let _ = self.digits.drain(((self.zero + 1) as usize)..); + let _ = self + .digits + .drain::>(((self.zero + 1).try_into().expect("Why."))..); } } else { // fractional part is the only thing left, discard the entire value @@ -2164,10 +2191,6 @@ impl PrimitiveDecimal { self.apply_exponent(digit); } - - pub fn unsign(&mut self) { - self.sign = false; - } } #[cfg(test)] @@ -2423,7 +2446,7 @@ mod primitive_decimal_tests { #[test] fn fuzz() { - const TOTAL_TESTS: usize = 1_000_000; + const TOTAL_TESTS: u32 = 1_000_000; const MAX_ABS: f64 = 1E6f64; const EXP_PROB: f64 = 0.2f64; const DIGIT_RANGE: RangeInclusive = -10..=10; @@ -2457,7 +2480,7 @@ mod primitive_decimal_tests { assert!(diff <= ok_diff, "Difference is larger than expected for number = {number} rounding to digit = {digit}. Rounding result: {rounded}, ok difference: {ok_diff}, actual difference: {diff}"); dev_sum += diff / ok_diff; } - println!("Average dev: {:.4}%", dev_sum / (TOTAL_TESTS as f64)); + println!("Average dev: {:.4}%", dev_sum / f64::from(TOTAL_TESTS)); } } } @@ -2490,10 +2513,7 @@ pub fn value_error_format<'v, 'e>( }) } -fn value_error_format_inner<'v, 'e>( - value: &'v str, - error: &'e str, -) -> Result<(String, String), ParsingError> { +fn value_error_format_inner(value: &str, error: &str) -> Result<(String, String), ParsingError> { // first, parse values into primitive decimals, for easier manipulation let mut value: PrimitiveDecimal = value.parse()?; let mut error: PrimitiveDecimal = error.parse()?; @@ -2546,7 +2566,7 @@ mod value_error_tests { test! {edge1, ["0.0", "0.0"] => ["0", "0"]} } -fn find_args<'l>(input: &'l str) -> impl IntoIterator { +fn find_args(input: &str) -> impl IntoIterator { // I sincerely tried to use regex here, but apparently, I'm too dumb to actually figure this out // my final guess: r#"\s*([0-9A-Za-z_]+)\s*=\s*((["'].*[^\\]["'])|([^["=,\s\[\]]][^[",\s\[\]]]*)|(\[.*\]))"# // Here is a state machine instead: @@ -2613,7 +2633,7 @@ fn find_args<'l>(input: &'l str) -> impl IntoIterator // (1) + ' ' -> (1) is done through finding first non-empty char let inner_rest = &rest[first_char..]; // (1) + [a-zA-z] -> (2) - if inner_rest.starts_with(|c| matches!(c, 'A'..='Z' | 'a'..='z')) { + if inner_rest.starts_with(|c: char| c.is_ascii_alphabetic()) { return inner(State::InsideName, inner_rest, result); } } @@ -2632,8 +2652,8 @@ fn find_args<'l>(input: &'l str) -> impl IntoIterator return inner(State::BeforeEquals { name }, inner_rest, result); } // (2) + '=' -> (4) - if inner_rest.starts_with('=') { - return inner(State::AfterEquals { name }, &inner_rest[1..], result); + if let Some(next_rest) = inner_rest.strip_prefix('=') { + return inner(State::AfterEquals { name }, next_rest, result); } } State::BeforeEquals { name } => { @@ -2645,8 +2665,7 @@ fn find_args<'l>(input: &'l str) -> impl IntoIterator let inner_rest = &rest[equals..]; // (3) + '=' -> (4) - if inner_rest.starts_with('=') { - let inner_after_equals = &inner_rest[1..]; + if let Some(inner_after_equals) = inner_rest.strip_prefix('=') { return inner(State::AfterEquals { name }, inner_after_equals, result); } } @@ -2680,10 +2699,17 @@ fn find_args<'l>(input: &'l str) -> impl IntoIterator // Fortunately, I assume one thing about values: they must have exited all of their quotes // This assumption is based on python syntax -- there would indeed be no way to insert something without quotes. // I HOPE that this is enough, and would appreciate any advice here. + fn beginning_quote(s: &str) -> Option { + match s.chars().next()? { + '\'' => Some(Quote::Single), + '\"' => Some(Quote::Double), + _ => None, + } + } // (5)(q) + "\"" -> (5)(q) // (5)(q) + "\'" -> (5)(q) - if rest.starts_with(r#"\""#) | rest.starts_with(r#"\'"#) { + if rest.starts_with(r#"\""#) | rest.starts_with(r"\'") { const OFFSET: usize = r#"\""#.len(); return inner( State::InsideValue { @@ -2697,15 +2723,6 @@ fn find_args<'l>(input: &'l str) -> impl IntoIterator ); } - fn beginning_quote(s: &str) -> Option { - match s.chars().next() { - Some('\'') => Some(Quote::Single), - Some('\"') => Some(Quote::Double), - Some(_) => None, - None => None, - } - } - if let Some(some_quote) = quote.as_ref() { if let Some(rest_quote) = beginning_quote(rest) { const OFFSET: usize = "\'".len(); @@ -2779,8 +2796,7 @@ fn find_args<'l>(input: &'l str) -> impl IntoIterator let next_char = rest .char_indices() .nth(1) // I need an index of a 2nd character here - .map(|(i, _)| i) - .unwrap_or(rest.len()); // If there's no 2nd character, inner_rest should be empty + .map_or(rest.len(), |(i, _)| i); // If there's no 2nd character, inner_rest should be empty let inner_rest = &rest[next_char..]; return inner( State::InsideValue { @@ -2802,8 +2818,8 @@ fn find_args<'l>(input: &'l str) -> impl IntoIterator }; let rest_inner = &rest[first_char..]; // (6) + ',' -> (1) - if rest_inner.starts_with(',') { - return inner(State::Initial, &rest_inner[1..], result); + if let Some(next_rest) = rest_inner.strip_prefix(',') { + return inner(State::Initial, next_rest, result); } } } @@ -2841,8 +2857,8 @@ mod find_args_tests { test! {empty3, "=", []} test! {empty4, r#"===""""#, []} test! {empty5, r#" src == "man""#, []} - test! {single1, r#"x=1"#, [("x", "1")]} - test! {single2, r#" x =1 "#, [("x", "1")]} + test! {single1, r"x=1", [("x", "1")]} + test! {single2, r" x =1 ", [("x", "1")]} test! {single3, r#" src = "path/to/th.png" "#, [("src", "\"path/to/th.png\"")]} test! {single4, r#" src = "path/to /th.png" "#, [("src", "\"path/to /th.png\"")]} test! {single5, r#"caption = "Some really cool text, expected to handle that comma too" "#, [("caption", "\"Some really cool text, expected to handle that comma too\"")]} @@ -2853,7 +2869,7 @@ mod find_args_tests { // These two are not even going to happen, lol test! {doc1, r#""very important info, that definitely couldn't be inserted into a document statically""#, []} - test! {doc2, r#"4.54535, 0.2342"#, []} + test! {doc2, r"4.54535, 0.2342", []} test! {doc3, r#"src="line.png", ident="line", caption="Some caption for a line drawing. This is parsed as a regular document text, so you may [@eq:line] and $f(x) = A \cdot x^{1} + B \cdot x^{0}$ here, for example""#, [("src", "\"line.png\""), ("ident", "\"line\""), ("caption", r#""Some caption for a line drawing. This is parsed as a regular document text, so you may [@eq:line] and $f(x) = A \cdot x^{1} + B \cdot x^{0}$ here, for example""#)]} test! {doc4, r#"src="file.csv", rows=1..100, columns=["type", ("a", "a_err")]"#, @@ -2878,7 +2894,7 @@ fn parse_columns<'l>(input: &'l str) -> Vec> { .captures_iter(input) .map( |cap: Captures<'l>| match (cap.get(4), cap.get(1), cap.get(2)) { - (Some(col), _, _) => CsvTableColumn::Single(col.as_str().into()), + (Some(col), _, _) => CsvTableColumn::Single(col.as_str()), (_, Some(value), Some(error)) => { CsvTableColumn::ValueError(value.as_str(), error.as_str()) } @@ -2917,7 +2933,7 @@ mod parse_columns_tests { }; } - test! {empty, r#""#, []} + test! {empty, r"", []} test! {single, r#"["a"]"#, [c!("a")]} test! {value_error, r#"["a", ("b", "c")]"#, [c!("a"), ve!("b", "c")]} test! {master, r#"["no", ("x", "x_err"), "y", ("z", "z_err")]"#, [c!("no"), ve!("x", "x_err"), c!("y"), ve!("z", "z_err")]} diff --git a/src/gen/ayano/python.rs b/src/gen/ayano/python.rs index 5f608fc..87e86b5 100644 --- a/src/gen/ayano/python.rs +++ b/src/gen/ayano/python.rs @@ -1,14 +1,12 @@ use std::{ - borrow::Cow, fmt::{Debug, Display}, - fs::read, path::Path, }; use pyo3::prelude::*; use rand::random; -static MODULE_NAME: &'static str = "ayano"; +static MODULE_NAME: &str = "ayano"; #[derive(Debug)] pub struct InitToken { @@ -55,7 +53,7 @@ pub fn append_static(target: &mut String, code: &str, execution_path: impl AsRef .as_str(), ); for line in code.lines() { - target.push_str(format!("\n{line}").as_str()) + target.push_str(format!("\n{line}").as_str()); } target.push_str("\nos.chdir(___cwd___)"); } @@ -64,7 +62,7 @@ pub fn append_function(target: &mut String, name: &str, code: &str) { // First, declare a function target.push_str(format!("\ndef {name}():").as_str()); for line in code.lines() { - target.push_str(format!("\n {line}").as_str()) + target.push_str(format!("\n {line}").as_str()); } } @@ -99,7 +97,7 @@ pub struct TracedPyErr { impl TracedPyErr { fn from_regular(err: PyErr, py: Python<'_>, code: &str) -> Self { - let trace_string = err.traceback(py).map(|trace| trace.format()); + let trace_string = err.traceback(py).map(pyo3::types::PyTraceback::format); Self { head: err, trace: trace_string, diff --git a/src/gen/comp/mod.rs b/src/gen/comp/mod.rs index 7863a89..c7dc577 100644 --- a/src/gen/comp/mod.rs +++ b/src/gen/comp/mod.rs @@ -6,17 +6,17 @@ use crate::{ data::{self, Formatting, Token}, path_engine::PathEngine, }; -use std::{borrow::Borrow, convert::Infallible, fmt::Write, marker::PhantomData}; +use std::{borrow::Borrow, fmt::Write, marker::PhantomData}; use super::{GenerationError, OutputGenerator, Res}; #[derive(Debug)] -pub struct CompLatex<'source, PE, PEErr> { +pub struct Generator<'source, PE, PEErr> { path_engine: PE, _phantom: PhantomData<(&'source (), PEErr)>, } -impl<'source, PE: PathEngine, PEErr> CompLatex<'source, PE, PEErr> { +impl<'source, PE: PathEngine, PEErr> Generator<'source, PE, PEErr> { pub fn new(path_engine: PE) -> Self { Self { path_engine, @@ -24,6 +24,7 @@ impl<'source, PE: PathEngine, PEErr> CompLatex<'source, PE, PEErr> { } } + #[allow(unused, clippy::unnecessary_wraps, clippy::unused_self)] // TODO: implement tables in compact format! fn write_table_row<'meta, 'context, 'token: 'context, W: Write + ?Sized>( &self, _output: &mut W, @@ -48,7 +49,7 @@ impl<'source, PE: PathEngine, PEErr> CompLatex<'source, PE, PEErr> { PEErr: std::error::Error + 'source, { let (_, tokens) = crate::parse_all(input)?; - let generator: CompLatex<'source, PE, PEErr> = Self::new(path_engine); + let generator: Generator<'source, PE, PEErr> = Self::new(path_engine); generator.write_preamble(output, &())?; let mut context = Context::default(); generator.write_to(output, &(), &mut context, &tokens)?; @@ -61,7 +62,7 @@ impl<'source, PE: PathEngine, PEErr> CompLatex<'source, PE, PEErr> { #[error("A dummy error, should be never thrown")] pub struct DummyMetaError; -impl<'source, PE, PEErr> OutputGenerator<'source, (), Context> for CompLatex<'source, PE, PEErr> +impl<'source, PE, PEErr> OutputGenerator<'source, (), Context> for Generator<'source, PE, PEErr> where PE: PathEngine, PEErr: std::error::Error + 'source, @@ -71,7 +72,7 @@ where fn write_to<'meta, 'context, 'token, W: Write + ?Sized>( &self, output: &mut W, - _: &(), + (): &(), context: &'context mut Context, token: &'token Token<'source>, ) -> Res<'source> @@ -93,15 +94,12 @@ where Token::DisplayMath { content, .. } => { write!(output, "\n$$\n{content}\n$$\n")?; } - Token::Table { header, cells, .. } => { - // ignoring tables - } Token::Figure { src_name, width, .. } => { output.write_str("\n\\begin{figure}[h!]\n\\centering\n\\includegraphics[width=")?; if let Some(width) = width.as_ref() { - output.write_str(&width.to_string())? + output.write_str(&width.to_string())?; } else { output.write_str("0.3")?; } @@ -113,9 +111,6 @@ where output.write_str(src_path.to_string_lossy().borrow())?; // FIXME non-utf paths *might* cause an issue here output.write_str("}\n\\end{figure}\n")?; } - Token::Ayano { .. } => { - // do nothing, lol - } Token::List { list_type, content } => { let start: &str = match list_type { crate::data::ListType::Bullet => "\\begin{itemize}", @@ -137,9 +132,9 @@ where }; let mut ident_producer = match list_type { data::ListType::Bullet | data::ListType::Num => None, - t @ data::ListType::Latin - | t @ data::ListType::Cyrillic - | t @ data::ListType::Roman => Some(crate::lexer::_list_item_generator(&t)), + t @ (data::ListType::Latin + | data::ListType::Cyrillic + | data::ListType::Roman) => Some(crate::lexer::_list_item_generator(t)), }; output.write_str("\n")?; output.write_str(start)?; @@ -165,14 +160,6 @@ where } write!(output, "${content}$")?; } - Token::Reference { .. } => { - // do nothing, lol - } - Token::FootnoteReference { .. } => { - // yeah, same here - do nothing - } - // might seem weird at first, but these are basically useless at this point, as their content was presumably collected into context - Token::FootnoteContent { .. } => {} Token::Error { message } => { eprintln!("{message}"); // TODO add representation in the document @@ -183,7 +170,7 @@ where content, space_before, } => { - let mut content = content.clone(); + let mut content_inner = content.clone(); if *is_newline { if context.allergic_to_newline { output.write_str("\n\\tb ")?; @@ -201,28 +188,28 @@ where Some(Formatting::MonoSpace) => ("\\texttt{", "}"), }; output.write_str(start)?; - if content.contains(['%', '_', '#', '[', ']']) { + if content_inner.contains(['%', '_', '#', '[', ']']) { // there are special symbols in the paragraph // most certainly they were not intended to be special latex symbols (because why?) // TODO add an option to opt this out // TODO add multiple replacements at once - let content_inner = content.replace('\\', "\\\\"); - let content_inner = content_inner.replace('%', "\\%"); - let content_inner = content_inner.replace('#', "\\#"); - content = content_inner.replace('_', "\\_").into(); + let mut content_str = content_inner.replace('\\', "\\\\"); + content_str = content_str.replace('%', "\\%"); + content_str = content_str.replace('#', "\\#"); + content_inner = content_str.replace('_', "\\_").into(); } if context.inside_caption { // We are generating text that was inside table/figure caption // meaning, we must un-escape double quotes let mut last_ind = 0; - for (ind, _) in content.match_indices("\\\"") { - output.write_str(&content[last_ind..ind])?; + for (ind, _) in content_inner.match_indices("\\\"") { + output.write_str(&content_inner[last_ind..ind])?; output.write_char('\"')?; last_ind = ind + 2; } - output.write_str(&content[last_ind..])?; + output.write_str(&content_inner[last_ind..])?; } else { - output.write_str(&content)?; + output.write_str(&content_inner)?; } output.write_str(end)?; } @@ -237,8 +224,13 @@ where write!(output, "\\href{{{url}}}{{{display}}}")?; } Token::Multiple { tokens } => self.write_tokens_to(output, &(), context, tokens)?, - Token::CodeBlock { code, language } => { - // ignore + Token::Table { .. } + | Token::Ayano { .. } + | Token::FootnoteReference { .. } + | Token::FootnoteContent { .. } + | Token::CodeBlock { .. } + | Token::Reference { .. } => { + // ignoring all of these (for now) } }; if let Some(new_value) = token.is_newline_allergic() { @@ -247,7 +239,7 @@ where Ok(()) } - fn write_preamble<'meta, W: Write + ?Sized>(&self, output: &mut W, _: &()) -> Res<'source> { + fn write_preamble<'meta, W: Write + ?Sized>(&self, output: &mut W, (): &()) -> Res<'source> { const PREAMBLE: &str = include_str!("preamble.tex"); output.write_str(PREAMBLE)?; Ok(()) @@ -256,7 +248,7 @@ where fn write_postamble<'meta, 'context, W: Write + ?Sized>( &self, output: &mut W, - _: &(), + (): &(), _context: &'context mut Context, ) -> Res<'source> { output.write_str("\n\n\\end{document}")?; diff --git a/src/gen/lab/meta.rs b/src/gen/lab/meta.rs index 9e4a491..eed3036 100644 --- a/src/gen/lab/meta.rs +++ b/src/gen/lab/meta.rs @@ -11,43 +11,43 @@ use crate::{ path_engine::PathEngine, }; -use super::super::ayano::{self, AyanoBuilder, AyanoExecutor}; +use super::super::ayano::{self, Builder}; #[derive(Debug, thiserror::Error)] -pub enum MetaError<'l> { +pub enum Error<'l> { #[error("Failed to recognize Ayano syntax: {}", .0)] AyanoSyntax(ayano::SyntaxError<'l>), } #[derive(Debug, Default)] -pub struct SourceMeta<'source, Ayano> { +pub struct SourceData<'source, Ayano> { pub footnotes: HashMap, Token<'source>>, pub refs: HashSet>, pub ayano: Ayano, pub title_info: TitleInfo<'source>, } -impl<'source, Ayano: Default> SourceMeta<'source, Ayano> { +impl<'source, Ayano: Default> SourceData<'source, Ayano> { fn empty() -> Self { Self::default() } } -impl<'source> SourceMeta<'source, AyanoBuilder> { +impl<'source> SourceData<'source, Builder> { pub fn collect<'meta, PEErr: std::error::Error + 'source, PE: PathEngine>( token: &'meta Token<'source>, path_engine: &PE, - ) -> Result> + ) -> Result> where 'source: 'meta, { - let mut res = SourceMeta::empty(); + let mut res = SourceData::empty(); collect_single(token, &mut res, path_engine)?; Ok(res) } - pub fn init_ayano(self) -> Result, TracedPyErr> { - Ok(SourceMeta { + pub fn init_ayano(self) -> Result, TracedPyErr> { + Ok(SourceData { ayano: self.ayano.initialize()?, footnotes: self.footnotes, refs: self.refs, @@ -58,9 +58,9 @@ impl<'source> SourceMeta<'source, AyanoBuilder> { fn collect_single<'meta, 'r, 'source, PEErr: std::error::Error + 'source, PE: PathEngine>( token: &'meta Token<'source>, - dest: &'r mut SourceMeta<'source, AyanoBuilder>, + dest: &'r mut SourceData<'source, Builder>, path_engine: &PE, -) -> Result<(), MetaError<'source>> +) -> Result<(), Error<'source>> where 'source: 'meta, 'meta: 'r, @@ -107,7 +107,7 @@ where Token::Ayano { data } => { dest.ayano .add_block(data, path_engine) - .map_err(MetaError::AyanoSyntax)?; + .map_err(Error::AyanoSyntax)?; } Token::PageDiv | Token::Header { .. } @@ -119,8 +119,8 @@ where | Token::FootnoteReference { .. } | Token::Figure { .. } | Token::DisplayMath { .. } - | Token::CodeBlock { .. } => {} - Token::Error { .. } => {} + | Token::CodeBlock { .. } + | Token::Error { .. } => {} } Ok(()) } @@ -134,9 +134,9 @@ fn collect_iterable< PE: PathEngine, >( tokens: I, - dest: &'r mut SourceMeta<'source, AyanoBuilder>, + dest: &'r mut SourceData<'source, Builder>, path_engine: &PE, -) -> Result<(), MetaError<'source>> +) -> Result<(), Error<'source>> where I: IntoIterator>, { diff --git a/src/gen/lab/mod.rs b/src/gen/lab/mod.rs index 7564922..7a7dac4 100644 --- a/src/gen/lab/mod.rs +++ b/src/gen/lab/mod.rs @@ -2,13 +2,12 @@ mod context; mod meta; pub use context::Context; -use meta::MetaError; -pub use meta::SourceMeta; +pub use meta::SourceData; use crate::{ - data::{self, AyanoBlock, Formatting, TitleInfo, Token}, + data::{self, Formatting, TitleInfo, Token}, gen::ayano::DisplayInfo, - path_engine::{self, PathEngine}, + path_engine::PathEngine, }; use itertools::Itertools; use std::{ @@ -17,7 +16,7 @@ use std::{ marker::PhantomData, }; -use super::{ayano::AyanoExecutor, GenerationError, OutputGenerator, Res}; +use super::{ayano::Executor, GenerationError, OutputGenerator, Res}; #[derive(Debug)] pub struct LabaLatex<'source, PE, PEErr> { @@ -36,7 +35,7 @@ impl<'source, PE: PathEngine, PEErr> LabaLatex<'source, PE, PEErr> { fn write_table_row<'meta, 'context, 'token: 'context, W: Write + ?Sized>( &self, output: &mut W, - meta: &'meta SourceMeta<'source, AyanoExecutor>, + meta: &'meta SourceData<'source, Executor>, context: &'context mut Context, cells: impl IntoIterator>, ) -> Res<'source> @@ -69,10 +68,9 @@ impl<'source, PE: PathEngine, PEErr> LabaLatex<'source, PE, PEErr> { PEErr: std::error::Error + 'source, { let (title_info, tokens) = crate::parse_all(input)?; - let mut meta: SourceMeta<'source, AyanoExecutor> = - SourceMeta::collect(&tokens, &path_engine) - .map_err(|err| GenerationError::Meta(Box::new(err)))? - .init_ayano()?; + let mut meta: SourceData<'source, Executor> = SourceData::collect(&tokens, &path_engine) + .map_err(|err| GenerationError::Meta(Box::new(err)))? + .init_ayano()?; let generator: LabaLatex<'source, PE, PEErr> = Self::new(path_engine); meta.title_info = title_info; generator.write_preamble(output, &meta)?; @@ -83,7 +81,7 @@ impl<'source, PE: PathEngine, PEErr> LabaLatex<'source, PE, PEErr> { } } -impl<'source, PE, PEErr> OutputGenerator<'source, SourceMeta<'source, AyanoExecutor>, Context> +impl<'source, PE, PEErr> OutputGenerator<'source, SourceData<'source, Executor>, Context> for LabaLatex<'source, PE, PEErr> where PE: PathEngine, @@ -95,7 +93,7 @@ where fn write_to<'meta, 'context, 'token, W: Write + ?Sized>( &self, output: &mut W, - meta: &'meta SourceMeta<'source, AyanoExecutor>, + meta: &'meta SourceData<'source, Executor>, context: &'context mut Context, token: &'token Token<'source>, ) -> Res<'source> @@ -122,8 +120,7 @@ where Token::DisplayMath { content, ident } => { let label = ident .as_ref() - .map(|ident| format!("\\label{{eq:{}}}\n", ident)) - .unwrap_or_else(String::new); + .map_or_else(String::new, |ident| format!("\\label{{eq:{}}}\n", ident)); write!( output, "\n\\begin{{equation}}\n{content}\n{label}\\end{{equation}}\n" @@ -151,7 +148,7 @@ where self.write_table_row(output, meta, context, header)?; output.write_str("\\hline\n")?; 'cells: { - let mut rows = cells.chunks(columns).into_iter(); + let mut rows = cells.chunks(columns); let Some(first_row) = rows.next() else { break 'cells; }; @@ -169,7 +166,7 @@ where output.write_str("}\n")?; } if let Some(ident) = ident.as_ref() { - write!(output, "\\label{{tab:{ident}}}\n")?; + writeln!(output, "\\label{{tab:{ident}}}")?; } output.write_str("\\end{center}\n\\end{table}\n")?; } @@ -181,7 +178,7 @@ where } => { output.write_str("\n\\begin{figure}[h!]\n\\centering\n\\includegraphics[width=")?; if let Some(width) = width.as_ref() { - output.write_str(&width.to_string())? + output.write_str(&width.to_string())?; } else { output.write_str("0.9")?; } @@ -200,7 +197,7 @@ where output.write_str("}\n")?; } if let Some(ident) = ident.as_ref() { - write!(output, "\\label{{fig:{ident}}}\n")?; + writeln!(output, "\\label{{fig:{ident}}}")?; } output.write_str("\\end{figure}\n")?; } @@ -236,9 +233,9 @@ where }; let mut ident_producer = match list_type { data::ListType::Bullet | data::ListType::Num => None, - t @ data::ListType::Latin - | t @ data::ListType::Cyrillic - | t @ data::ListType::Roman => Some(crate::lexer::_list_item_generator(&t)), + t @ (data::ListType::Latin + | data::ListType::Cyrillic + | data::ListType::Roman) => Some(crate::lexer::_list_item_generator(t)), }; output.write_str("\n")?; output.write_str(start)?; @@ -283,15 +280,14 @@ where let (encountered, i) = { // TODO probably should move this to separate function let encountered_map = &mut context.encountered_footnotes; - let encountered = encountered_map.get(ident.borrow() as &str).cloned(); - let i = match encountered { - Some(i) => i, - None => { - let i: usize = encountered_map.len(); - // let ident: &'context Cow<'source, str> = ident; - encountered_map.insert(ident.to_string(), i); - i - } + let encountered = encountered_map.get(ident.borrow() as &str).copied(); + let i = if let Some(i) = encountered { + i + } else { + let i: usize = encountered_map.len(); + // let ident: &'context Cow<'source, str> = ident; + encountered_map.insert(ident.to_string(), i); + i }; (encountered.is_some(), i) }; @@ -318,7 +314,7 @@ where content, space_before, } => { - let mut content = content.clone(); + let mut content_inner = content.clone(); if *is_newline { if context.allergic_to_newline { output.write_str("\n\\tb ")?; @@ -336,28 +332,28 @@ where Some(Formatting::MonoSpace) => ("\\texttt{", "}"), }; output.write_str(start)?; - if content.contains(['%', '_', '#', '[', ']']) { + if content_inner.contains(['%', '_', '#', '[', ']']) { // there are special symbols in the paragraph // most certainly they were not intended to be special latex symbols (because why?) // TODO add an option to opt this out // TODO add multiple replacements at once - let content_inner = content.replace('\\', "\\\\"); - let content_inner = content_inner.replace('%', "\\%"); - let content_inner = content_inner.replace('#', "\\#"); - content = content_inner.replace('_', "\\_").into(); + let mut content_str = content_inner.replace('\\', "\\\\"); + content_str = content_str.replace('%', "\\%"); + content_str = content_str.replace('#', "\\#"); + content_inner = content_str.replace('_', "\\_").into(); } if context.inside_caption { // We are generating text that was inside table/figure caption // meaning, we must un-escape double quotes let mut last_ind = 0; - for (ind, _) in content.match_indices("\\\"") { - output.write_str(&content[last_ind..ind])?; + for (ind, _) in content_inner.match_indices("\\\"") { + output.write_str(&content_inner[last_ind..ind])?; output.write_char('\"')?; last_ind = ind + 2; } - output.write_str(&content[last_ind..])?; + output.write_str(&content_inner[last_ind..])?; } else { - output.write_str(&content)?; + output.write_str(&content_inner)?; } output.write_str(end)?; } @@ -389,10 +385,9 @@ where fn write_preamble<'meta, W: Write + ?Sized>( &self, output: &mut W, - meta: &'meta SourceMeta<'source, AyanoExecutor>, + meta: &'meta SourceData<'source, Executor>, ) -> Res<'source> { - let mut preamble = include_str!("lab_preamble.tex").to_string(); - const PATTERNS: [&'static str; 12] = [ + const PATTERNS: [&str; 12] = [ "$header-line1$", "$header-line2$", "$document-type$", @@ -406,6 +401,7 @@ where "$date$", "$prof$", ]; + let mut preamble = include_str!("lab_preamble.tex").to_string(); let TitleInfo { header_line1, header_line2, @@ -440,7 +436,7 @@ where if let Some(ind) = preamble.find(pat) { preamble.replace_range( ind..ind + pat.len(), - &replacements[i].as_ref().unwrap_or(&"".into()), + replacements[i].as_ref().unwrap_or(&"".into()), ); } } @@ -451,7 +447,7 @@ where fn write_postamble<'meta, 'context, W: Write + ?Sized>( &self, output: &mut W, - meta: &'meta SourceMeta<'source, AyanoExecutor>, + meta: &'meta SourceData<'source, Executor>, context: &'context mut Context, ) -> Res<'source> { let mut display_ayano_blocks = meta.ayano.display_blocks().into_iter(); @@ -461,12 +457,12 @@ where 'meta, 'context, C, - G: OutputGenerator<'source, SourceMeta<'source, AyanoExecutor>, C>, + G: OutputGenerator<'source, SourceData<'source, Executor>, C>, W: Write + ?Sized, >( generator: &G, output: &mut W, - meta: &'meta SourceMeta<'source, AyanoExecutor>, + meta: &'meta SourceData<'source, Executor>, context: &'context mut C, block: &'meta DisplayInfo, ) -> Res<'source> @@ -505,11 +501,11 @@ where fn collect_context<'token>( &self, tokens: &'token Token<'source>, - ) -> Result, Self::MetaError> + ) -> Result, Self::MetaError> where 'source: 'token, { - Ok(SourceMeta::collect(tokens, &self.path_engine)?.init_ayano()?) + Ok(SourceData::collect(tokens, &self.path_engine)?.init_ayano()?) } fn default_context(&self) -> Context { @@ -518,6 +514,7 @@ where } #[cfg(test)] +#[allow(clippy::needless_raw_string_hashes)] mod tests { use std::path::Path; @@ -569,7 +566,7 @@ mod tests { use $crate::path_engine::primitive as primitive_engine; // arrange let token = $token; - let meta = SourceMeta::collect(&token, &path_engine::primitive()) + let meta = SourceData::collect(&token, &$crate::path_engine::primitive()) .expect("Should be able to collect") .init_ayano() .expect("Should be able to init Ayano"); diff --git a/src/gen/mod.rs b/src/gen/mod.rs index 5a8f8d8..4cd4137 100644 --- a/src/gen/mod.rs +++ b/src/gen/mod.rs @@ -15,7 +15,7 @@ pub enum GenerationError<'source> { #[error(transparent)] Write(std::fmt::Error), #[error(transparent)] - Ayano(ayano::AyanoError<'source>), + Ayano(ayano::Error<'source>), #[error(transparent)] Path(Box), #[error(transparent)] @@ -30,8 +30,8 @@ impl From for GenerationError<'_> { } } -impl<'source> From> for GenerationError<'source> { - fn from(value: ayano::AyanoError<'source>) -> Self { +impl<'source> From> for GenerationError<'source> { + fn from(value: ayano::Error<'source>) -> Self { Self::Ayano(value) } } @@ -76,9 +76,11 @@ pub trait OutputGenerator<'source, Meta, Context> { .map(|t| self.write_to(output, meta, context, t)) .try_collect() } + fn write_preamble<'meta, W: Write + ?Sized>(&self, _: &mut W, _: &'meta Meta) -> Res<'source> { Ok(()) } + fn write_postamble<'meta, 'context, W: Write + ?Sized>( &self, _: &mut W, diff --git a/src/lexer.rs b/src/lexer.rs index 67edaca..e9a8d3d 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -344,7 +344,7 @@ fn try_fold_many1( /// /// - Always starts from a newline followed by '|' char /// - First line is always interpreted as header -/// TODO - Next line is skipped for now; all tables are formatted with centered columns +/// - TODO: Next line is skipped for now; all tables are formatted with centered columns /// - Following lines starting with '|' are interpreted as table cells /// - Lastly, optional "ref" and "caption" arguments are parsed fn table< @@ -662,7 +662,7 @@ pub(crate) fn _list_item_generator<'scope>( ListType::Bullet => Box::new(std::iter::repeat("-")), ListType::Num => { static STRS: RwLock> = RwLock::new(Vec::new()); - Box::new((1..).into_iter().map(|i| { + Box::new((1..).map(|i| { // First, try get already generated string let read = STRS.read().unwrap(); if let Some(res) = read.get(i - 1) { @@ -688,7 +688,7 @@ pub(crate) fn _list_item_generator<'scope>( char_array!('a'..'z', 26, "latin") .map(|l| format!("{l}.").leak() as &'static str) }); - Box::new((*STRS).into_iter().map(|b| &*b)) + Box::new((*STRS).into_iter()) } ListType::Cyrillic => { static STRS: once_cell::sync::Lazy<[&'static str; 32]> = @@ -696,7 +696,7 @@ pub(crate) fn _list_item_generator<'scope>( char_array!('а'..'я', 32, "cyrillic") .map(|l| format!("{l}.").leak() as &'static str) }); - Box::new((*STRS).into_iter().map(|b| &*b)) + Box::new((*STRS).into_iter()) } ListType::Roman => unimplemented!("Roman numerated lists are not supported yet"), } @@ -1052,7 +1052,7 @@ fn paragraph_inner< }; (ind, parse_res) }) - .try_fold((), |_, (ind, parse_res)| match parse_res { + .try_fold((), |(), (ind, parse_res)| match parse_res { // if there is a match for the following token -- return this "Ok" wrapped into "Err", forcing iterator to stop Ok(ok) => Err(Ok((ind, ok))), // if there is not a match, but a FAUILURE -- return it as "Err" wrapped in "Err", forcing iterator to stop @@ -1071,14 +1071,10 @@ fn paragraph_inner< }; // Now, construct the output - let this_token_end = interruption - .as_ref() - .map(|(ind, _)| *ind) - .unwrap_or(interrupt_ind); + let this_token_end = interruption.as_ref().map_or(interrupt_ind, |(ind, _)| *ind); let rest_input = interruption .as_ref() - .map(|(_, (rest, _))| *rest) - .unwrap_or(&input[this_token_end..]); + .map_or(&input[this_token_end..], |(_, (rest, _))| *rest); let follower = interruption.map(|(_, (_, f))| f); Ok(( rest_input, @@ -1304,7 +1300,7 @@ fn tokens_many1< let Some((first_token, first_follower)) = (&mut iterator).next() else { // There is no first token! // Let's give iterator a chance to explain itself: - let (rest, _) = iterator.finish()?; + let (rest, ()) = iterator.finish()?; // Well, seems like there are no tokens and no error // (most likely input is just empty or th) // Return empty variant @@ -1321,12 +1317,12 @@ fn tokens_many1< let mut tokens = match (first_follower, (&mut iterator).next()) { // Meaning there are no more tokens (or iterator failed) - return single result (None, None) => { - let (rest, _) = iterator.finish()?; + let (rest, ()) = iterator.finish()?; return Ok((rest, InnerResult::Single(t1))); } // There are exactly two tokens (or iterator failed) - return exactly two tokens (Some(t2), None) => { - let (rest, _) = iterator.finish()?; + let (rest, ()) = iterator.finish()?; return Ok((rest, InnerResult::Multiple(vec![t1, t2]))); } // There are at least two tokens @@ -1351,7 +1347,7 @@ fn tokens_many1< } } // Iterator might have failed silently, so let them a chance to tell about it - let (rest, _) = iterator.finish()?; + let (rest, ()) = iterator.finish()?; // Return multiple variant Ok((rest, InnerResult::Multiple(tokens))) }, @@ -1542,7 +1538,7 @@ mod token_tests { result = $parser.parse($input); // assert - if !matches!(result, $expected) { + if !result.is_err() { panic!("{}: {:?}", "Parser did not produce complying result:", result); } } @@ -1649,41 +1645,41 @@ y = x^2 z = y $$;12344", e: Ok((";12344", Token::DisplayMath { content: "y = x^2\nz = y".into(), ident: None }))} // This is alright, though - test! {equation9, equation, r#" + test! {equation9, equation, r" $$ y = x^2 $$ -{ref = parabola}"#, e: Ok(("", Token::DisplayMath { content: "y = x^2".into(), ident: Some("parabola".into()) }))} - test! {equation9_5, equation, r#" +{ref = parabola}", e: Ok(("", Token::DisplayMath { content: "y = x^2".into(), ident: Some("parabola".into()) }))} + test! {equation9_5, equation, r" $$ y = x^2 $$ -{ref = parabola_ref}"#, e: Ok(("", Token::DisplayMath { content: "y = x^2".into(), ident: Some("parabola_ref".into()) }))} +{ref = parabola_ref}", e: Ok(("", Token::DisplayMath { content: "y = x^2".into(), ident: Some("parabola_ref".into()) }))} // Equation ident example - test! {equation10, equation, r#" + test! {equation10, equation, r" $$ y = x^2 $$ -{ref = parabola_1}"#, e: Ok(("", Token::DisplayMath { content: "y = x^2".into(), ident: Some("parabola_1".into()) }))} +{ref = parabola_1}", e: Ok(("", Token::DisplayMath { content: "y = x^2".into(), ident: Some("parabola_1".into()) }))} // This is not ok // TODO although that's subject to change - test! {equation11, equation, r#" + test! {equation11, equation, r" $$ y = x^2 $$ - {tel = +4204206969}"#, p: Err(_)} + {tel = +4204206969}", p: Err(_)} // Don't put random garbage there! - test! {equation12, equation, r#" + test! {equation12, equation, r" $$ y = x^2 $$ -{ref=11 } якийсь текст тому що чому ні"#, e: Ok((" якийсь текст тому що чому ні", Token::DisplayMath { content: "y = x^2 ".into(), ident: Some("11".into()) }))} +{ref=11 } якийсь текст тому що чому ні", e: Ok((" якийсь текст тому що чому ні", Token::DisplayMath { content: "y = x^2 ".into(), ident: Some("11".into()) }))} // Any sort of whitespaces are preserved - test! {equation13, equation, r#" + test! {equation13, equation, r" $$ y = x^2 $$ -{} якийсь текст тому що чому ні"#, e: Ok((" якийсь текст тому що чому ні", Token::DisplayMath { content: "y = x^2 ".into(), ident: None }))} +{} якийсь текст тому що чому ні", e: Ok((" якийсь текст тому що чому ні", Token::DisplayMath { content: "y = x^2 ".into(), ident: None }))} // You can explicitly provide no args - fun fact :idk: // `href` tests @@ -1847,11 +1843,6 @@ $$ // `inner_lex` tests // this is one of open interfaces, so extensive testing is required here // to achieve that, here are some macro to simplify token creation: - macro_rules! div { - () => { - Token::PageDiv - }; - } macro_rules! eq { ($eq:literal) => { eq!($eq, None) @@ -1894,20 +1885,6 @@ $$ } }; } - macro_rules! code { - ($code:literal, lang = $lang:literal) => { - code!($code, Some(Cow::Borrowed($lang))) - }; - ($code:literal) => { - code!($code, None) - }; - ($code:literal, $lang:expr) => { - Token::CodeBlock { - code: Cow::Borrowed($code), - language: $lang, - } - }; - } macro_rules! rf { (@ $ident:literal) => { Token::Reference { @@ -2106,10 +2083,10 @@ $$ [1 $($item:expr), +] => { ls![ListType::Num; $($item), +] }; - [a $($item:expr), +] => { + [lat $($item:expr), +] => { ls![ListType::Latin; $($item), +] }; - [а $($item:expr), +] => { + [cyr $($item:expr), +] => { ls![ListType::Cyrillic; $($item), +] }; [- $($item:expr), +] => { @@ -2142,13 +2119,13 @@ $$ tks![tx!("Explained in"), rf!(^^"source2")] ], "\n"} test_ok! {ok_latin, "\na. Item 1 \nb. Gamma-function def: $\\Gamma(\\alpha) = \\int\\limits_{0}^{+\\infinity} t^{\\alpha - 1} e^{-t} dt$.\nc. Here's some table ref: ([@tab:function ]) \nd. Explained in [^source2]\n", - ls![a tx!("Item 1"), + ls![lat tx!("Item 1"), tks![tx!("Gamma-function def:"), eq!(^!"\\Gamma(\\alpha) = \\int\\limits_{0}^{+\\infinity} t^{\\alpha - 1} e^{-t} dt"), tx!(".")], tks![tx!("Here's some table ref: ("), rf!(@"tab:function"), tx!(")")], tks![tx!("Explained in"), rf!(^^"source2")] ], "\n"} test_ok! {ok_cyrillic, "\nа. Item 1 \nб. Gamma-function def: $\\Gamma(\\alpha) = \\int\\limits_{0}^{+\\infinity} t^{\\alpha - 1} e^{-t} dt$.\nв. Here's some table ref: ([@tab:function ]) \nг. Explained in [^source2]\n", - ls![а tx!("Item 1"), + ls![cyr tx!("Item 1"), tks![tx!("Gamma-function def:"), eq!(^!"\\Gamma(\\alpha) = \\int\\limits_{0}^{+\\infinity} t^{\\alpha - 1} e^{-t} dt"), tx!(".")], tks![tx!("Here's some table ref: ("), rf!(@"tab:function"), tx!(")")], tks![tx!("Explained in"), rf!(^^"source2")] @@ -2277,7 +2254,7 @@ pub fn title_info<'source, Err: nom::error::ParseError<&'source str>>( let Ok((rest, _)) = tag::<_, _, Err>("---")(input) else { return Ok((input, TitleInfo::default())); }; - cut(|mut input: &'source str| { + cut(|input: &'source str| { macro_rules! match_field { ($name:literal) => { preceded::<_, _, _, Err, _, _>( @@ -2341,6 +2318,7 @@ pub fn title_info<'source, Err: nom::error::ParseError<&'source str>>( } #[cfg(test)] +#[allow(clippy::needless_raw_string_hashes)] mod title_info_tests { use super::*; macro_rules! title_info { diff --git a/src/lib.rs b/src/lib.rs index b7ce17e..eb59c27 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,7 @@ use lexer::KyomatoLexError; /// This module defines types that are used to represent parsed data mod data; mod gen; -pub use gen::comp::CompLatex; +pub use gen::comp::Generator; pub use gen::lab::LabaLatex; mod lexer; pub mod path_engine; @@ -50,7 +50,9 @@ pub fn gen_to_string<'token, 'source: 'token, Meta, Context>( where Meta: MetaExt<'source>, { - let mut meta = generator.collect_context(token).map_err(|b| b.into())?; + let mut meta = generator + .collect_context(token) + .map_err(std::convert::Into::into)?; if let Some(title_info) = title_info.into() { meta.set_title_info(title_info); // TODO make title declarations tokens too! } @@ -84,9 +86,7 @@ pub fn parse_tokens<'source>(input: &mut &'source str) -> Result, Ok(tokens) } -pub fn parse_all<'source>( - mut input: &'source str, -) -> Result<(TitleInfo<'source>, Token<'source>), KyomatoLexError> { +pub fn parse_all(mut input: &str) -> Result<(TitleInfo<'_>, Token<'_>), KyomatoLexError> { let title_info = parse_title_info(&mut input)?; let tokens = parse_tokens(&mut input)?; Ok((title_info, tokens)) @@ -106,7 +106,7 @@ pub trait MetaExt<'source> { 'title: 'source; } -impl<'source, Aynao> MetaExt<'source> for lab::SourceMeta<'source, Aynao> { +impl<'source, Aynao> MetaExt<'source> for lab::SourceData<'source, Aynao> { fn set_title_info<'title>(&mut self, title_info: TitleInfo<'title>) where 'title: 'source, diff --git a/src/path_engine.rs b/src/path_engine.rs index 4848c5e..109370c 100644 --- a/src/path_engine.rs +++ b/src/path_engine.rs @@ -14,7 +14,7 @@ pub enum ErrorContext { None, } -pub trait PathEngineError: Error { +pub trait EngineError: Error { fn from_generic(error: Box, context: ErrorContext) -> Self; fn append(&mut self, other: Box, context: ErrorContext); fn record_path<'l>(&mut self, path: impl Into>); @@ -25,14 +25,14 @@ pub trait PathEngineError: Error { #[error("Could not resolve a path")] pub struct MarkerError; -impl PathEngineError for MarkerError { +impl EngineError for MarkerError { fn append(&mut self, _other: Box, _context: ErrorContext) {} - fn from_generic(error: Box, context: ErrorContext) -> Self { + fn from_generic(_: Box, _: ErrorContext) -> Self { MarkerError } - fn record_path<'l>(&mut self, path: impl Into>) {} + fn record_path<'l>(&mut self, _: impl Into>) {} } /// An error holding rich information about reason resolution had failed @@ -47,7 +47,7 @@ pub struct InformativeError { path: Option, } -impl PathEngineError for InformativeError { +impl EngineError for InformativeError { fn append(&mut self, other: Box, context: ErrorContext) { match context { ErrorContext::Source => self.source.insert(other), @@ -66,7 +66,7 @@ impl PathEngineError for InformativeError { fn record_path<'l>(&mut self, path: impl Into>) { if self.path.is_none() { - self.path = Some(path.into().into_owned()) + self.path = Some(path.into().into_owned()); } } } @@ -100,7 +100,7 @@ pub trait PathEngine { fn get_source_folder(&self) -> Option<&Path>; } -pub trait PathEngineExt: PathEngine + Sized { +pub trait EngineExt: PathEngine + Sized { fn with_ayano(self) -> AyanoRespecting { AyanoRespecting(self) } @@ -122,7 +122,7 @@ pub trait PathEngineExt: PathEngine + Sized { self } - fn with_error_type(self) -> impl PathEngine + Sized + fn with_error_type(self) -> impl PathEngine + Sized where Self: PathEngine, { @@ -130,17 +130,17 @@ pub trait PathEngineExt: PathEngine + Sized { } } -impl PathEngineExt for PE where PE: PathEngine {} +impl EngineExt for PE where PE: PathEngine {} /// A primitive path engine, that returns, well, exact paths it's given. It literally does nothing. pub struct Engine(CheckType); -impl PathEngine for Engine { +impl PathEngine for Engine { fn image<'p>( &self, path: impl Into>, - ayano_insert_dir: Option<&Path>, + _: Option<&Path>, ) -> Result, E> { - let path = path.into(); + let path: Cow<'p, Path> = path.into(); if let Err(err) = (self.0)(&path) { let mut error = E::from_generic(err, ErrorContext::None); error.record_path(path); @@ -152,9 +152,9 @@ impl PathEngine for Engine { fn csv<'p>( &self, path: impl Into>, - ayano_insert_dir: Option<&Path>, + _: Option<&Path>, ) -> Result, E> { - let path = path.into(); + let path: Cow<'p, Path> = path.into(); if let Err(err) = (self.0)(&path) { let mut error = E::from_generic(err, ErrorContext::None); error.record_path(path); @@ -164,7 +164,7 @@ impl PathEngine for Engine { } fn ayano_insert<'p>(&self, path: impl Into>) -> Result, E> { - let path = path.into(); + let path: Cow<'p, Path> = path.into(); if let Err(err) = (self.0)(&path) { let mut error = E::from_generic(err, ErrorContext::None); error.record_path(path); @@ -198,7 +198,7 @@ pub enum VariantError { } /// A path-checking function always positive about path being correct -pub fn no_check(path: &Path) -> Result<(), Box> { +pub fn no_check(_: &Path) -> Result<(), Box> { Ok(()) } @@ -219,7 +219,7 @@ pub fn fs_check(path: &Path) -> Result<(), Box> { macro_rules! try_variant { ($this_path:expr, $this_variant:ident, $self:ident, $method:ident, $path:ident $(, $arg:tt)*) => {{ - let mut path = $path.into(); + let path = $path.into(); // first, try this path let this_error = if let Some(this_path) = $this_path.as_ref() { let this_guess = this_path.join(&path); @@ -257,15 +257,8 @@ macro_rules! impl_check_delegate { }; } -fn try_ayano<'p>( - path: impl Into>, - ayano_insert: &Path, -) -> Result, Box> { - todo!() -} - pub struct AyanoRespecting(PE); -impl> PathEngine for AyanoRespecting { +impl> PathEngine for AyanoRespecting { fn image<'p>( &self, path: impl Into>, @@ -346,7 +339,7 @@ impl> PathEngine for AyanoRespecting(PE, PathBuf); -impl> PathEngine for WithImageFolder { +impl> PathEngine for WithImageFolder { fn image<'p>( &self, path: impl Into>, @@ -375,7 +368,7 @@ impl> PathEngine for WithImageFolder(PE, PathBuf); -impl> PathEngine for WithSourceFolder { +impl> PathEngine for WithSourceFolder { fn image<'p>( &self, path: impl Into>, @@ -403,7 +396,7 @@ impl> PathEngine for WithSourceFolder

(PE, PathBuf); -impl> PathEngine for WithExecutionFolder { +impl> PathEngine for WithExecutionFolder { fn image<'p>( &self, path: impl Into>, @@ -434,7 +427,7 @@ impl> PathEngine for WithExecutionFolde /// /// Generic method. Rust will most likely complain about type inference if you use it directly; /// Make sure to add at least one `with_th_error`. -pub fn base_gen() -> impl PathEngine + Sized { +pub fn base_gen() -> impl PathEngine + Sized { Engine(no_check) } @@ -528,6 +521,7 @@ mod tests { let engine_result = $call_expr; // assert + #[allow(clippy::redundant_pattern_matching)] if !matches!(engine_result, $expected_res) { panic!("Path engine should produce matching result: {engine_result:?}"); } diff --git a/src/util.rs b/src/util.rs index 3eb91bf..a443136 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,14 +1,12 @@ use std::{ collections::VecDeque, - mem::{forget, MaybeUninit}, + mem::MaybeUninit, num::ParseIntError, ops::{Range, RangeFrom, RangeInclusive, RangeTo, RangeToInclusive}, - panic::{catch_unwind, UnwindSafe}, str::FromStr, }; -use itertools::Itertools; -use nom::{branch::Permutation, error::ParseError, IResult, Parser}; +use nom::{error::ParseError, IResult, Parser}; #[derive(Debug, Clone, PartialEq)] pub enum GenericRange { @@ -61,51 +59,37 @@ impl FromStr for GenericRange { let Some((from, to)) = s.split_once("..") else { return Err(GenericRangeParseError::Format); }; - let from: usize = from - .parse() - .map_err(|err| GenericRangeParseError::Object(err))?; - let to: usize = to - .parse() - .map_err(|err| GenericRangeParseError::Object(err))?; + let from: usize = from.parse().map_err(GenericRangeParseError::Object)?; + let to: usize = to.parse().map_err(GenericRangeParseError::Object)?; Ok(GenericRange::DoubleBounded(from..to)) } (false, false, true) => { let Some((from, to)) = s.split_once("..=") else { return Err(GenericRangeParseError::Format); }; - let from: usize = from - .parse() - .map_err(|err| GenericRangeParseError::Object(err))?; - let to: usize = to - .parse() - .map_err(|err| GenericRangeParseError::Object(err))?; + let from: usize = from.parse().map_err(GenericRangeParseError::Object)?; + let to: usize = to.parse().map_err(GenericRangeParseError::Object)?; Ok(GenericRange::DoubleBoundedInclusive(from..=to)) } (false, true, false) => { let Some((from, "")) = s.split_once("..") else { return Err(GenericRangeParseError::Format); }; - let from: usize = from - .parse() - .map_err(|err| GenericRangeParseError::Object(err))?; + let from: usize = from.parse().map_err(GenericRangeParseError::Object)?; Ok(GenericRange::From(from..)) } (true, false, false) => { let Some(("", to)) = s.split_once("..") else { return Err(GenericRangeParseError::Format); }; - let to: usize = to - .parse() - .map_err(|err| GenericRangeParseError::Object(err))?; + let to: usize = to.parse().map_err(GenericRangeParseError::Object)?; Ok(GenericRange::To(..to)) } (true, false, true) => { let Some(("", to)) = s.split_once("..=") else { return Err(GenericRangeParseError::Format); }; - let to: usize = to - .parse() - .map_err(|err| GenericRangeParseError::Object(err))?; + let to: usize = to.parse().map_err(GenericRangeParseError::Object)?; Ok(GenericRange::ToInclusive(..=to)) } _ => Err(GenericRangeParseError::Format), @@ -298,6 +282,7 @@ mod inline_stack_tests { } #[derive(Debug)] +#[allow(dead_code)] pub enum ArrayCollectError { NotEnough { expeected: usize, got: usize }, TooMuch(usize), @@ -314,8 +299,7 @@ impl> IteratorArrayCollectExt for I { // Create an array of uninitialized values. let mut array: [MaybeUninit; N] = unsafe { MaybeUninit::uninit().assume_init() }; - - for i in 0..N { + for (i, arr_i) in array.iter_mut().enumerate() { match self.next() { None => { return Err(ArrayCollectError::NotEnough { @@ -324,10 +308,11 @@ impl> IteratorArrayCollectExt for I { }); } Some(next) => { - array[i] = MaybeUninit::new(next); + *arr_i = MaybeUninit::new(next); } } } + if self.next().is_some() { // there are even more elements to get return Err(ArrayCollectError::TooMuch(N)); @@ -389,6 +374,9 @@ macro_rules! impl_optional_permutation { ), * { fn optional_permutation(&mut self, mut input: &'source str) -> IResult<&'source str, (Option<$out_ident0>, $(Option<$out_ident>), *), Err> { + // This is a max number of iterations we're gonna need + const COUNT: usize = $Count; + // for multiple variants, here comes chaos: // we can't create any sort of array or vector to hold the results - // they have different types, that's the point! @@ -403,8 +391,6 @@ macro_rules! impl_optional_permutation { // Now we can use `$type_ident` as corresponding parser, and `$out_ident` as a output variable! // Amazing! (not really; I imagine that would be a nightmare to read) - // This is a max number of iterations we're gonna need - const COUNT: usize = $Count; for _ in 0..COUNT { if $out_ident0.is_none() { match $type_ident0.parse(input) { diff --git a/tests/lex.rs b/tests/lex.rs index c1a570a..5ad85a7 100644 --- a/tests/lex.rs +++ b/tests/lex.rs @@ -1,13 +1,12 @@ use std::{ffi::OsString, path::PathBuf, str::FromStr}; -const CLEAR: &'static str = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" ; -const INPUT_DIR: &'static str = "./tests/input_files"; -const LEX_DIR: &'static str = "./tests/lex_files"; +const CLEAR: &str = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" ; +const INPUT_DIR: &str = "./tests/input_files"; +const LEX_DIR: &str = "./tests/lex_files"; fn input_files() -> impl Iterator { std::fs::read_dir(INPUT_DIR) .expect("Should be able to access input directory") - .into_iter() .map(|file| { let file_path = file .expect("Should be able to access files in input directory") diff --git a/tests/transform.rs b/tests/transform.rs index 7452592..55f7d88 100644 --- a/tests/transform.rs +++ b/tests/transform.rs @@ -1,15 +1,12 @@ use std::{ffi::OsString, path::PathBuf, str::FromStr}; -use kyomato_lib::path_engine::PathEngine; - -const CLEAR: &'static str = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" ; -const INPUT_DIR: &'static str = "./tests/input_files"; -const TRANSFORM_DIR: &'static str = "./tests/transformed_files"; +const CLEAR: &str = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" ; +const INPUT_DIR: &str = "./tests/input_files"; +const TRANSFORM_DIR: &str = "./tests/transformed_files"; fn input_files() -> impl Iterator { std::fs::read_dir(INPUT_DIR) .expect("Should be able to access input directory") - .into_iter() .map(|file| { let file_path = file .expect("Should be able to access files in input directory") From 4b09e171399da7005ccc6049a59673e782133a37 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Sat, 24 Aug 2024 11:58:44 +0300 Subject: [PATCH 79/98] Renamed binary crate to `kyomato-cli` because it makes sense. --- Cargo.toml | 2 +- {kyomato => kyomato-cli}/Cargo.toml | 0 {kyomato => kyomato-cli}/src/main.rs | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename {kyomato => kyomato-cli}/Cargo.toml (100%) rename {kyomato => kyomato-cli}/src/main.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 135fa5d..d09f234 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,4 @@ -workspace = { members = ["examples-util", "kyomato"] } +workspace = { members = ["examples-util", "kyomato-cli"] } [package] name = "kyomato-lib" version = "0.1.0" diff --git a/kyomato/Cargo.toml b/kyomato-cli/Cargo.toml similarity index 100% rename from kyomato/Cargo.toml rename to kyomato-cli/Cargo.toml diff --git a/kyomato/src/main.rs b/kyomato-cli/src/main.rs similarity index 100% rename from kyomato/src/main.rs rename to kyomato-cli/src/main.rs From ff751a30e40de98b14559c7a7794076ae827aca1 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Sat, 24 Aug 2024 13:43:53 +0300 Subject: [PATCH 80/98] It turns out, that file WAS important after all (one of the examples was using it) Revert "I have no idea what this file was for," This reverts commit dbc3f64912e2e86685918cfb192855337455dfc6. --- examples/advanced_ayano/2 | 1 + 1 file changed, 1 insertion(+) create mode 100644 examples/advanced_ayano/2 diff --git a/examples/advanced_ayano/2 b/examples/advanced_ayano/2 new file mode 100644 index 0000000..d8263ee --- /dev/null +++ b/examples/advanced_ayano/2 @@ -0,0 +1 @@ +2 \ No newline at end of file From b00956c91d0910527da2fa48850cbfa579e55407 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Sat, 24 Aug 2024 21:01:54 +0300 Subject: [PATCH 81/98] Started `Token` rework - `Token`s are called `Selci` (selshi) now - `Selci` do not own or statically refer to their data. Instead they only contain some sort of span - `Selci` are collected into `SelciTree` used to tranverse the AST - All base `Selci`-related stuff (like their definitions, `SelciTree` ddefinintion, convenience macro creation deifnition `SenciTree` traversal, etc) are now to be separated into a dedicated crate `kyomato-core` - For more details on rework, see temporary item in `kyomato-core/lib.rs` --- Cargo.toml | 2 +- kyomato-core/Cargo.toml | 13 ++ kyomato-core/src/lib.rs | 37 ++++++ kyomato-core/src/selci/mod.rs | 228 ++++++++++++++++++++++++++++++++++ 4 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 kyomato-core/Cargo.toml create mode 100644 kyomato-core/src/lib.rs create mode 100644 kyomato-core/src/selci/mod.rs diff --git a/Cargo.toml b/Cargo.toml index d09f234..8728fd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,4 @@ -workspace = { members = ["examples-util", "kyomato-cli"] } +workspace = { members = ["examples-util", "kyomato-cli", "kyomato-core"] } [package] name = "kyomato-lib" version = "0.1.0" diff --git a/kyomato-core/Cargo.toml b/kyomato-core/Cargo.toml new file mode 100644 index 0000000..d5d1903 --- /dev/null +++ b/kyomato-core/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "kyomato-core" +version = "0.1.0" +edition = "2021" + +[dependencies] +deranged = "0.3.11" +derive_more = "1.0.0" +url = "2.5.2" + +[features] +ayano = [] +ayano-python = [ "ayano" ] diff --git a/kyomato-core/src/lib.rs b/kyomato-core/src/lib.rs new file mode 100644 index 0000000..d736d7d --- /dev/null +++ b/kyomato-core/src/lib.rs @@ -0,0 +1,37 @@ +/// Here's a yet-another token redesign. +/// +/// Several months later, I decide to finally ditch the idea of tokens directly referring to or +/// owning underlying text in any way. This sure would make them reliable and stuff, but tokens +/// just noting the position in the source instead of actually owning it, opens a lot of +/// possibilities. +/// +/// I have no idea why tokens were essentially even called that. Instead, they are "objects", or +/// "generation points", etc. +/// +/// From now on, they are to be called `Selci` ("selshi", lojban for cell). +/// +/// All `Selci` are Collected into a `SelciTree` which represents a valid set of interrefering +/// `Selci`. `Selci` cannot be modified after insertion into the tree. +/// +/// `SelciTree` is essentially immutable, and can only be modified by offsetting contained `Selci`, +/// attaching another `SelciTree` in the end and a couple more that don't disrupt `SelciTree` +/// structure. +/// +/// `Selci` presented to the user code *appear* to have code references, but it's only as illution. +/// To create a `Selci` user code **must** use `SelciTree` methods that involve *seeding*. +/// +/// *seeding* is a process of providing a `SelciTree` with the actual data they are +/// supposed to refer to. This allows you to (for example) split the source into several parts and +/// generate `SelciTree`s separately, merging them afterwards. +/// +/// To support ASTs with recursive structure, actually stored `InternalSelci` fields +/// corresponding to child `Selci` contain instead +/// - a logic number of children `Selci` +/// - a physical offset to reach them +/// - a physical offset to skip the [Selci] +/// These two can be used to efficient access and iteration over `Selci`, while creating a +/// **single** heap allocation. +#[allow(unused)] +pub struct SomeExplanationOfWhatsGoingOn; + +mod selci; diff --git a/kyomato-core/src/selci/mod.rs b/kyomato-core/src/selci/mod.rs new file mode 100644 index 0000000..c9ba211 --- /dev/null +++ b/kyomato-core/src/selci/mod.rs @@ -0,0 +1,228 @@ +use deranged::RangedU8; + +const MAX_SECTION_ORDER: u8 = 6; +/// Section hierarchy order. +/// +/// Inspired by the fact that both markdown and html have 6 header insets, while latex have even +/// less (citation needed) +/// +/// If you need even more nested sections - please let me know. It would be easy to change as long +/// as your nesting is less than 255. +pub type SectionOrder = RangedU8<1, MAX_SECTION_ORDER>; + +/// A text that's guaranteed to be a valid ident +/// +/// Valid idents only use alphanumeric characters, underscores and dashes, so [A-Za-z0-9_\-] +pub type Idt = str; + +/// Type of the reference represented by [Selci::NumRef] +/// +/// Intended to expand into some sort of enumeration, with interactive element to bring reader to +/// referenced thing +pub enum RefKind { + /// Reference to [Selci::SectionStart] + Section, + /// Reference to [Selci::DisplayEquation] + Equation, + /// Reference to [Selci::Image] + Image, + /// Reference to [Selci::Diagram] + Diagram, + /// Reference to [Selci::Table] + Table, + /// Refernce to [Selci::Ayano] + #[cfg(ayano)] + Ayano, +} + +#[derive(Debug)] +/// Types of item lists and enumerations +pub enum ListKind { + /// A bullet (unordered list). Usually represented by `