diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 522d961..6954a09 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,3 +7,5 @@ on: jobs: rust-ci: uses: UniverLab/workflows/.github/workflows/rust-ci.yml@main + with: + use-nextest: true diff --git a/Cargo.lock b/Cargo.lock index 4e0d0a5..f72a5cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "adler2" @@ -8,6 +8,26 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "adobe-cmap-parser" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8abfa9a4688de8fc9f42b3f013b6fffec18ed8a554f5f113577e0b9b3212a3" +dependencies = [ + "pom", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -17,6 +37,30 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "1.0.0" @@ -145,6 +189,45 @@ version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -156,6 +239,20 @@ name = "bytemuck" version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0e56a716f1e132ff6bf4bdac1c944a3fcdc1cae65f70a4a2a1ac3b401d2d1f" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] [[package]] name = "byteorder" @@ -175,6 +272,15 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.2.66" @@ -185,12 +291,50 @@ dependencies = [ "shlex", ] +[[package]] +name = "cff-parser" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5810ca1a2b5870df2aab1c03e11c40c361ba51d6e3e361e56310f1cb3b4e087" + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.6.1" @@ -222,7 +366,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -231,6 +375,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "color" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ec7c5eb7a16992b1904d76c517d170ab353b0e0b3d5a0c81a8a0cd1037893cf" +dependencies = [ + "bytemuck", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -278,6 +431,24 @@ dependencies = [ "libm", ] +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -287,6 +458,25 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.22" @@ -318,6 +508,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "d2-little" version = "0.7.1-1" @@ -346,6 +546,43 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + [[package]] name = "derive_arbitrary" version = "1.4.2" @@ -354,7 +591,17 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", ] [[package]] @@ -386,7 +633,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -395,6 +642,21 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "ecb" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a8bfa975b1aec2145850fcaa1c6fe269a16578c44705a532ae3edc92b8881c7" +dependencies = [ + "cipher", +] + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -420,6 +682,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "euclid" +version = "0.20.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bb7ef65b3777a325d1eeefefab5b6d4959da54747e33bd6258e789640f307ad" +dependencies = [ + "num-traits", +] + [[package]] name = "euclid" version = "0.22.14" @@ -455,6 +726,12 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fearless_simd" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97b65636e5b9ef369943878ac74335ba1c55c1cb6adbf1e2c293c624248d693" + [[package]] name = "filetime" version = "0.2.29" @@ -493,6 +770,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "font-types" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" +dependencies = [ + "bytemuck", +] + [[package]] name = "fontconfig-parser" version = "0.5.8" @@ -601,6 +893,16 @@ dependencies = [ "byteorder", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -619,8 +921,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", + "rand_core", + "wasm-bindgen", +] + +[[package]] +name = "gif" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +dependencies = [ + "color_quant", + "weezl", ] [[package]] @@ -633,6 +948,32 @@ dependencies = [ "weezl", ] +[[package]] +name = "glifo" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d99fc21d493812643aae86d53b7bbd02f376434a90317e8a790bc209fdd6605e" +dependencies = [ + "bytemuck", + "foldhash", + "hashbrown", + "log", + "peniko", + "png 0.18.1", + "skrifa", + "smallvec", + "vello_common 0.0.9", +] + +[[package]] +name = "guillotiere" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b17e70c989c36bad147b27a58d148c0741c51448aa5653436547323e524d0ab" +dependencies = [ + "euclid 0.22.14", +] + [[package]] name = "h2" version = "0.4.15" @@ -657,6 +998,99 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hayro" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4caa128ab87fd48ffb7490617cf93f77f606820dffbc9fd9ef6ab0ed077f56d" +dependencies = [ + "bytemuck", + "hayro-interpret", + "image", + "kurbo 0.13.1", + "pic-scale", + "vello_cpu", +] + +[[package]] +name = "hayro-ccitt" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f4d0e94ddd48749f06bbe4e5389fb9799a0c45bcaf00495042076ef05e3241a" + +[[package]] +name = "hayro-cmap" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d285dc30731c8485de5fa732fbdf2b3affdf01e4da7c2135022ca6fa664bf6" +dependencies = [ + "brotli", + "hayro-postscript", +] + +[[package]] +name = "hayro-interpret" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2613d0406898995042d0794c4245b2f2fba1246490c8ac593769bba6551129d" +dependencies = [ + "bitflags 2.13.0", + "hayro-cmap", + "hayro-syntax", + "kurbo 0.13.1", + "moxcms", + "phf", + "rustc-hash", + "siphasher", + "skrifa", + "smallvec", + "yoke", +] + +[[package]] +name = "hayro-jbig2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69374b3668dd45aeb3d3145cda68f2c7b4f223aaa2511e67d076f1c7d741388d" +dependencies = [ + "fearless_simd", + "hayro-ccitt", +] + +[[package]] +name = "hayro-jpeg2000" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ab947623ef4ccaa7acf0579edf7cbb5a73838e3839a7be73335e522f433a1" +dependencies = [ + "fearless_simd", +] + +[[package]] +name = "hayro-postscript" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "885c5ef0654933139a9b9546fc2c69e18d37f38aa2520f079092b1be18f1fcaa" + +[[package]] +name = "hayro-syntax" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0edeafd70aa2db743de8ede8637d07ec87db05efe69cde371d03f1b185fcef27" +dependencies = [ + "flate2", + "hayro-ccitt", + "hayro-jbig2", + "hayro-jpeg2000", + "memchr", + "rustc-hash", + "smallvec", + "zune-jpeg 0.5.15", +] [[package]] name = "heck" @@ -762,6 +1196,30 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.2.0" @@ -865,6 +1323,23 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "gif 0.14.2", + "moxcms", + "num-traits", + "png 0.18.1", + "zune-core 0.5.1", + "zune-jpeg 0.5.15", +] + [[package]] name = "image-webp" version = "0.2.4" @@ -875,6 +1350,12 @@ dependencies = [ "quick-error", ] +[[package]] +name = "imagesize" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" + [[package]] name = "imagesize" version = "0.14.0" @@ -911,6 +1392,16 @@ dependencies = [ "libc", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + [[package]] name = "inquire" version = "0.7.5" @@ -955,6 +1446,59 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jiff" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + [[package]] name = "jni" version = "0.22.4" @@ -982,7 +1526,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn", + "syn 2.0.118", ] [[package]] @@ -1001,7 +1545,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -1045,6 +1589,17 @@ dependencies = [ "libc", ] +[[package]] +name = "kurbo" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" +dependencies = [ + "arrayvec", + "euclid 0.22.14", + "smallvec", +] + [[package]] name = "kurbo" version = "0.13.1" @@ -1052,7 +1607,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2" dependencies = [ "arrayvec", - "euclid", + "euclid 0.22.14", "polycool", "smallvec", ] @@ -1084,6 +1639,12 @@ dependencies = [ "libc", ] +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1111,6 +1672,37 @@ version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +[[package]] +name = "lopdf" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25aab26d99567469098e64a02f42679f8965c6401263eefa31d8f2dcc37a221c" +dependencies = [ + "aes", + "bitflags 2.13.0", + "cbc", + "chrono", + "ecb", + "encoding_rs", + "flate2", + "getrandom 0.4.3", + "indexmap", + "itoa", + "jiff", + "log", + "md-5", + "nom", + "rand", + "rangemap", + "rayon", + "sha2", + "stringprep", + "thiserror", + "time", + "ttf-parser 0.25.1", + "weezl", +] + [[package]] name = "markdown" version = "1.0.0" @@ -1120,6 +1712,16 @@ dependencies = [ "unicode-id", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.8.3" @@ -1192,6 +1794,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "newline-converter" version = "0.3.0" @@ -1201,6 +1813,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "notify" version = "7.0.0" @@ -1229,6 +1850,12 @@ dependencies = [ "instant", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + [[package]] name = "num-traits" version = "0.2.19" @@ -1285,12 +1912,106 @@ dependencies = [ "windows-link", ] +[[package]] +name = "pdf-extract" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417e8fdc940f1d5bc62c5f89864c3a2255f74f69aa353c98509213d67df61e73" +dependencies = [ + "adobe-cmap-parser", + "cff-parser", + "encoding_rs", + "euclid 0.20.14", + "log", + "lopdf", + "postscript", + "type1-encoding-parser", + "unicode-normalization", +] + +[[package]] +name = "pdf-writer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df03c7d216de06f93f398ef06f1385a60f2c597bb96f8195c8d98e08a26b1d5" +dependencies = [ + "bitflags 2.13.0", + "itoa", + "memchr", + "ryu", +] + +[[package]] +name = "peniko" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "839c8299360d2e998bdb106dc0a6cd71dcc5f4df51df1b620361bf50e283cca6" +dependencies = [ + "bytemuck", + "color", + "kurbo 0.13.1", + "linebender_resource_handle", + "smallvec", +] + [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pic-scale" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c80d88d5c31215ceec2862abb2504860c715b8e2545a5ab15b62a3d097f30d" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "pico-args" version = "0.5.0" @@ -1316,15 +2037,55 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polycool" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" +checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "pom" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60f6ce597ecdcc9a098e7fddacb1065093a3d66446fa16c675e7e71d1b5c28e6" + +[[package]] +name = "portable-atomic" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ - "arrayvec", + "portable-atomic", ] +[[package]] +name = "postscript" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78451badbdaebaf17f053fd9152b3ffb33b516104eacb45e7864aaa9c712f306" + [[package]] name = "potential_utf" version = "0.1.5" @@ -1334,6 +2095,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" version = "1.0.106" @@ -1343,6 +2110,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + [[package]] name = "quick-error" version = "2.0.1" @@ -1364,6 +2137,59 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rangemap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "read-fonts" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4ed38b89c2c77ff968c524145ad65fb010f38af5c7a224b53b81d47ac2daa81" +dependencies = [ + "bytemuck", + "font-types", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1454,21 +2280,38 @@ dependencies = [ "web-sys", ] +[[package]] +name = "resvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43" +dependencies = [ + "gif 0.13.3", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes 0.15.3", + "tiny-skia", + "usvg 0.45.1", + "zune-jpeg 0.4.21", +] + [[package]] name = "resvg" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b563218631706d614e23059436526d005b50ab5f2d506b55a17eb65c5eb83419" dependencies = [ - "gif", + "gif 0.14.2", "image-webp", "log", "pico-args", "rgb", - "svgtypes", + "svgtypes 0.16.1", "tiny-skia", - "usvg", - "zune-jpeg", + "usvg 0.46.0", + "zune-jpeg 0.5.15", ] [[package]] @@ -1509,6 +2352,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -1628,6 +2477,12 @@ dependencies = [ "unicode-script", ] +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -1708,7 +2563,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -1733,6 +2588,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + [[package]] name = "shlex" version = "2.0.1" @@ -1807,6 +2673,16 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" +[[package]] +name = "skrifa" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c34617370ae968efb7161bb2beb517d9084659aae19e24b89e3db25b46e4564" +dependencies = [ + "bytemuck", + "read-fonts", +] + [[package]] name = "slab" version = "0.4.12" @@ -1838,6 +2714,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spellbook" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d204abcbdf8e88729306a8d0ca01a79d8a49969fe1f84696909d6dbc4321c1a" +dependencies = [ + "foldhash", + "hashbrown", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -1853,12 +2739,35 @@ dependencies = [ "float-cmp", ] +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subsetter" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38803281d1c23166c5ebcb455439a5d2afe711cc909cf88af72448c297756ad6" +dependencies = [ + "kurbo 0.13.1", + "rustc-hash", + "skrifa", + "write-fonts", +] + [[package]] name = "subtle" version = "2.6.1" @@ -1887,13 +2796,43 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "svg2pdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e50dc062439cc1a396181059c80932a6e6bd731b130e674c597c0c8874b6df22" +dependencies = [ + "fontdb", + "image", + "log", + "miniz_oxide", + "once_cell", + "pdf-writer", + "resvg 0.45.1", + "siphasher", + "subsetter", + "tiny-skia", + "ttf-parser 0.25.1", + "usvg 0.45.1", +] + +[[package]] +name = "svgtypes" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" +dependencies = [ + "kurbo 0.11.3", + "siphasher", +] + [[package]] name = "svgtypes" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "695b5790b3131dafa99b3bbfd25a216edb3d216dad9ca208d4657bfb8f2abc3d" dependencies = [ - "kurbo", + "kurbo 0.13.1", "siphasher", ] @@ -1908,6 +2847,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -1925,7 +2875,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -1954,21 +2904,28 @@ dependencies = [ [[package]] name = "texforge" -version = "0.7.0" +version = "0.8.0" dependencies = [ "anyhow", "clap", "d2-little", "dirs", "flate2", + "hayro", "inquire", "layout-rs", + "lopdf", "mermaid-rs-renderer", "notify", + "pdf-extract", + "png 0.17.16", "reqwest", - "resvg", + "resvg 0.46.0", "rustls", "serde", + "serde_json", + "spellbook", + "svg2pdf", "tar", "tempfile", "toml", @@ -1993,7 +2950,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -2005,6 +2962,36 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tiny-skia" version = "0.11.4" @@ -2016,7 +3003,7 @@ dependencies = [ "bytemuck", "cfg-if", "log", - "png", + "png 0.17.16", "tiny-skia-path", ] @@ -2217,6 +3204,21 @@ dependencies = [ "core_maths", ] +[[package]] +name = "type1-encoding-parser" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa10c302f5a53b7ad27fd42a3996e23d096ba39b5b8dd6d9e683a05b01bee749" +dependencies = [ + "pom", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + [[package]] name = "ucd-trie" version = "0.1.7" @@ -2253,6 +3255,15 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-properties" version = "0.1.4" @@ -2301,6 +3312,33 @@ dependencies = [ "serde", ] +[[package]] +name = "usvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" +dependencies = [ + "base64", + "data-url", + "flate2", + "fontdb", + "imagesize 0.13.0", + "kurbo 0.11.3", + "log", + "pico-args", + "roxmltree 0.20.0", + "rustybuzz", + "simplecss", + "siphasher", + "strict-num", + "svgtypes 0.15.3", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + [[package]] name = "usvg" version = "0.46.0" @@ -2311,8 +3349,8 @@ dependencies = [ "data-url", "flate2", "fontdb", - "imagesize", - "kurbo", + "imagesize 0.14.0", + "kurbo 0.13.1", "log", "pico-args", "roxmltree 0.21.1", @@ -2320,7 +3358,7 @@ dependencies = [ "simplecss", "siphasher", "strict-num", - "svgtypes", + "svgtypes 0.16.1", "tiny-skia-path", "unicode-bidi", "unicode-script", @@ -2340,6 +3378,53 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "vello_common" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3361bff7f7d82c0c496b92048db83846691f0e844cc28dee92b1c824291b55ee" +dependencies = [ + "bytemuck", + "fearless_simd", + "guillotiere", + "hashbrown", + "log", + "peniko", + "png 0.18.1", + "smallvec", + "thiserror", +] + +[[package]] +name = "vello_common" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d672facaa2d697285a786cd9d44d614cd2ce54cdc022504bf339f8fff3b750" +dependencies = [ + "bytemuck", + "fearless_simd", + "guillotiere", + "hashbrown", + "log", + "peniko", + "png 0.18.1", + "smallvec", + "thiserror", +] + +[[package]] +name = "vello_cpu" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d8ded630e8316bb94a55881256506d1f3b9947b5f66db8a7d32ca7ba02decd0" +dependencies = [ + "bytemuck", + "glifo", + "hashbrown", + "png 0.18.1", + "vello_common 0.0.8", +] + [[package]] name = "version_check" version = "0.9.5" @@ -2413,7 +3498,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.118", "wasm-bindgen-shared", ] @@ -2482,12 +3567,65 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -2642,6 +3780,19 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +[[package]] +name = "write-fonts" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb731d4c4d93eacc69a1ad2f270f905788a98e4a3438267bcafbe08d3431c8d8" +dependencies = [ + "font-types", + "indexmap", + "kurbo 0.13.1", + "log", + "read-fonts", +] + [[package]] name = "writeable" version = "0.6.3" @@ -2683,7 +3834,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", "synstructure", ] @@ -2704,7 +3855,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", "synstructure", ] @@ -2744,7 +3895,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -2782,17 +3933,32 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + [[package]] name = "zune-core" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core 0.4.12", +] + [[package]] name = "zune-jpeg" version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" dependencies = [ - "zune-core", + "zune-core 0.5.1", ] diff --git a/Cargo.toml b/Cargo.toml index 6ae7feb..7702e62 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,13 @@ [package] name = "texforge" -version = "0.7.0" +version = "0.8.0" edition = "2021" -rust-version = "1.75" +# Raised from 1.75: `hayro` (the pure-Rust PDF rasterizer) requires 1.92. +rust-version = "1.92" description = "Self-contained LaTeX to PDF compiler CLI" license = "MIT" repository = "https://github.com/UniverLab/texforge" +homepage = "https://univerlab.org/texforge" keywords = ["latex", "pdf", "compiler", "cli"] categories = ["command-line-utilities", "development-tools"] @@ -27,6 +29,9 @@ anyhow = "1.0" serde = { version = "1.0", features = ["derive"] } toml = "1.1" +# JSON output (texforge stats --json) +serde_json = "1.0" + # File system operations walkdir = "2.5" @@ -57,15 +62,37 @@ notify = { version = "7", features = ["macos_fsevent"] } mermaid-rs-renderer = { version = "0.2", default-features = false } resvg = "0.46" +# SVG -> PDF embedding, so diagrams stay vector in the built document instead +# of being rasterized. Pulls in its own usvg/resvg (^0.45, incompatible with +# our 0.46 above) — pass SVG as a string, never a usvg::Tree, across this +# boundary; see svg_to_pdf in src/diagrams/mod.rs. +svg2pdf = "0.13.0" + # Graphviz/DOT diagram rendering layout-rs = "0.1" # D2 diagram rendering (pure-Rust port of the d2lang pipeline, MPL-2.0) d2-little = "0.7.1-1" +# Pure-Rust PDF rasterization (no C toolchain, no -sys crates) +hayro = "0.7.1" + +# PNG encoding for `texforge preview` output (already in the lock via resvg) +png = "0.17" + # Temporary directories for build tempfile = "3.8" +# Pure-Rust PDF text extraction (no C toolchain; same constraint as hayro) +pdf-extract = "0.12" +lopdf = "0.42" + +# Hunspell-compatible spell-check backend (pure Rust, no_std; same +# musl-friendliness constraint as hayro/pdf-extract above). Pinned to the +# minor: spellbook's own README calls it alpha and warns any part of the API +# may break. +spellbook = "0.4" + [lints.clippy] needless_pass_by_value = "warn" redundant_closure = "warn" @@ -75,6 +102,9 @@ manual_let_else = "warn" semicolon_if_nothing_returned = "warn" inconsistent_struct_constructor = "warn" doc_markdown = "warn" +# Keep the established `% 2 == 0` parser expression until its owning spec +# changes it; Clippy's newer suggestion is deliberately out of scope for TF6. +manual_is_multiple_of = "allow" [profile.release] opt-level = 3 diff --git a/README.md b/README.md index cde52e3..ff9c1cf 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ Texforge is a unified LaTeX workspace — one tool for writing, rendering diagrams (Mermaid, Graphviz), and building PDFs. Set it up once and stay focused on your document. +**[univerlab.org/texforge](https://univerlab.org/texforge)** — the project page: what it does and why it exists. Command-level documentation lives in [`docs/`](docs/). + --- ## Features @@ -189,7 +191,18 @@ texforge init | `texforge clean` | Remove build artifacts | | `texforge fmt` | Format .tex files | | `texforge fmt --check` | Check formatting without modifying | -| `texforge check` | Lint without compiling | +| `texforge check` | Lint without compiling (includes spell-check) | +| `texforge spell add ` | Add word to personal dictionary (default: global) | +| `texforge spell list` | List personal dictionary words | +| `texforge spell remove ` | Remove word from personal dictionary | +| `texforge pdf text` | Extract text as seen by readers / accessibility tools | +| `texforge pdf info` | Report pages, fonts, embedding status, metadata | +| `texforge pdf pages` | List which section opens each page (diff-friendly) | +| `texforge pdf check` | Verify significant source words appear in PDF | +| `texforge outline` | Show document's section tree | +| `texforge stats` | Count words by section (or by file with `--by file`) | +| `texforge preview` | Rasterize PDF to PNG pages | +| `texforge doctor` | Diagnose Tectonic, cache, fonts, dictionaries, project | | `texforge config` | Interactive wizard to set user details (name, email, institution, language) | | `texforge config list` | Show all configured values | | `texforge config ` | Show value for key (name, email, institution, language) | @@ -338,6 +351,143 @@ texforge fmt --check # check without modifying (CI-friendly) --- +## Spell-Check + +`texforge check` now includes automatic spell-checking. The language is detected from the document itself: + +- `\usepackage[spanish]{babel}` — Spanish (Hunspell dictionary) +- `\usepackage[polyglossia]{...}` — Language extracted from polyglossia +- No language declaration — falls back to configured default (`texforge config language`) + +Dictionaries download automatically on first use into `~/.texforge/dicts/`. When a language is unsupported, checking is skipped with a message naming that language (never checks against the wrong dictionary). + +### Personal Dictionary + +Maintain a personal word list to skip custom terms, proper names, and project-specific jargon: + +```bash +# Add words to the global dictionary (applies to all projects) +texforge spell add "MyCompany" "LaTeX" "RGB" + +# Add to the current project only +texforge spell add "ProjectCodename" --local + +# List current dictionary +texforge spell list + +# Remove a word +texforge spell remove "MyCompany" +``` + +Scopes are unioned at check time — both global and project-local words are respected. + +--- + +## PDF Inspection + +`texforge pdf` extracts and inspects the compiled PDF without re-rendering: + +### Text Extraction + +```bash +# Print all text as a reader or accessibility tool would see it +texforge pdf text + +# Keep ligature codepoints (fi, fl, etc.) as separate characters +texforge pdf text --raw +``` + +### Metadata and Fonts + +```bash +# Report page count, fonts, whether fonts are embedded, and metadata +texforge pdf info +``` + +### Page Map + +```bash +# List which section opens each page (one line per page) +# Output designed for version control diffs +texforge pdf pages +``` + +### Fidelity Check + +```bash +# Verify that significant source words survived into the PDF text +# (catches missing fonts, encoding issues, rendering bugs) +texforge pdf check +``` + +--- + +## Outline + +Print the section tree with LaTeX markup resolved to readable text: + +```bash +texforge outline + +# JSON output for scripting +texforge outline --json +``` + +--- + +## Statistics + +Word count by section or by file: + +```bash +# Count words per section (default) +texforge stats + +# Count words per .tex file +texforge stats --by file + +# JSON output +texforge stats --json +``` + +--- + +## Preview + +Rasterize the compiled PDF to PNG pages: + +```bash +# Rasterize all pages (writes to ./preview/ by default) +texforge preview + +# Single page +texforge preview --page 1 + +# Custom output directory +texforge preview --out ./previews + +# Scale up for higher resolution (default: 1.0 px per PDF point) +texforge preview --scale 2.0 +``` + +--- + +## Doctor + +Diagnose the managed environment (Tectonic, cache, fonts, dictionaries, and project state): + +```bash +texforge doctor +``` + +Reports: +- Whether Tectonic is installed and functional +- Disk usage of the Tectonic cache and downloaded templates +- Available fonts and dictionaries +- Whether the current directory is a recognized texforge project + +--- + ## Runtime Directory ``` diff --git a/demo/demo.rec b/demo/demo.rec index 130c858..c511f9e 100644 --- a/demo/demo.rec +++ b/demo/demo.rec @@ -1,733 +1,681 @@ -{"demostage":{"captions":[],"demo":{"name":"demo","output_dir":"./dist","prompt":"\\[\\e[1;32m\\]texForge@univerlab\\[\\e[0m\\]:\\[\\e[1;34m\\]~\\[\\e[0m\\]$ "},"duration":38.578358061,"faithful":false,"focuses":[[0.120611476,"main"],[28.8673746,"pdf-r1"]],"layout":{"background":"#0b0f14","font_family":"IBM Plex Mono","fps":15,"height":1080,"line_height":1.2000000476837158,"panes":[{"font_family":"monospace","font_size":16,"height":1080,"id":"main","type":"terminal","width":1440,"x":0,"y":0},{"height":1080,"id":"pdf-r1","reveal_at":28.8673746,"type":"browser","url":"file:///home/jheisonmblivecom/Projects/UniverLab/texforge/examples/texforge-capabilites/texforge-capabilites.pdf","width":1440,"x":0,"y":0}],"width":1440},"timeline":[{"action":"focus","pane":"main"},{"action":"type","human_salt":true,"text":"texforge --help"},{"action":"wait","duration_ms":200},{"action":"keypress","key":"enter"},{"action":"wait","duration_ms":2500},{"action":"type","human_salt":true,"text":"opencode"},{"action":"wait","duration_ms":200},{"action":"keypress","key":"enter"},{"action":"wait_for_quiet","quiet_ms":1500},{"action":"type","human_salt":true,"text":"Create a texforge project \"texforge-capabilites\" demonstrating all capabilities: Mermaid sequence diagra, Graphviz architecture digraph, D2 pipeline diagram, math equations with cross-refs, BibTeX citations, tikz diagram, examples of code listings, two booktabs tables, table of contents, and lists. Compile with texforge build."},{"action":"wait","duration_ms":200},{"action":"keypress","key":"enter"},{"action":"wait_for_quiet","quiet_ms":5000},{"action":"type","human_salt":true,"text":"/exit"},{"action":"wait","duration_ms":200},{"action":"keypress","key":"enter"},{"action":"wait_for_quiet","quiet_ms":500},{"action":"focus","pane":"pdf-r1"},{"action":"scroll","direction":"down","duration_ms":8000,"pane":"pdf-r1","velocity":"constant"},{"action":"wait","duration_ms":8000},{"action":"terminate"}],"typing":{"base_ms":80,"salt_ms":15}},"env":{"TERM":"xterm-256color"},"height":54,"title":"demo","version":2,"width":144} -[0.000135769,"o","\r\n\u001b[?2004l\r"] -[0.000337255,"o","\u001b]697;OSCUnlock=6f3dc305774c4fb5ac9caff308f61ed2\u0007\u001b]697;Dir=/home/jheisonmblivecom\u0007"] -[0.000349953,"o","\u001b]697;Shell=bash\u0007"] -[0.000401504,"o","\u001b]697;ShellPath=/usr/bin/bash\u0007\u001b]697;WSLDistro=Ubuntu\u0007"] -[0.00047482,"o","\u001b]697;PID=16910\u0007\u001b]697;ExitCode=0\u0007"] -[0.000526347,"o","\u001b]697;TTY=/dev/pts/3\u0007\u001b]697;Log=\u0007"] -[0.000567266,"o","\u001b]697;User=jheisonmblivecom\u0007"] -[0.002317281,"o","\u001b]697;OSCLock=6f3dc305774c4fb5ac9caff308f61ed2\u0007"] -[0.002363538,"o","\u001b]697;PreExec\u0007"] -[0.089152788,"o","\u001b[?2004h\u001b]697;StartPrompt\u0007\u001b[1;32mtexForge@univerlab\u001b[0m:\u001b[1;34m~\u001b[0m$ \u001b]697;EndPrompt\u0007\u001b]697;NewCmd=6f3dc305774c4fb5ac9caff308f61ed2\u0007"] -[0.204858436,"o","t"] -[0.291980904,"o","e"] -[0.374100946,"o","x"] -[0.456189324,"o","f"] -[0.542274976,"o","o"] -[0.616330711,"o","r"] -[0.707403678,"o","g"] -[0.786497211,"o","e"] -[0.875573135,"o"," "] -[0.953618483,"o","-"] -[1.043708183,"o","-"] -[1.120829592,"o","h"] -[1.214921291,"o","e"] -[1.304021288,"o","l"] -[1.371120573,"o","p"] -[1.572695751,"o","\r\n\u001b[?2004l\r"] -[1.5747672769999999,"o","Self-contained LaTeX to PDF compiler CLI\r\n\r\n\u001b[1m\u001b[4mUsage:\u001b[0m \u001b[1mtexforge\u001b[0m \r\n\r\n\u001b[1m\u001b[4mCommands:\u001b[0m\r\n \u001b[1mclean\u001b[0m Remove build artifacts\r\n \u001b[1minit\u001b[0m Initialize a texforge project in the current directory\r\n \u001b[1mnew\u001b[0m Create a new project from a template\r\n \u001b[1mbuild\u001b[0m Compile project to PDF\r\n \u001b[1mfmt\u001b[0m Format .tex files\r\n \u001b[1mcheck\u001b[0m Lint project without compiling\r\n \u001b[1mtemplate\u001b[0m Manage templates\r\n \u001b[1mconfig\u001b[0m Manage global configuration\r\n \u001b[1mhelp\u001b[0m Print this message or the help of the given subcommand(s)\r\n\r\n\u001b[1m\u001b[4mOptions:\u001b[0m\r\n \u001b[1m-h\u001b[0m, \u001b[1m--help\u001b[0m Print help\r\n \u001b[1m-V\u001b[0m, \u001b[1m--version\u001b[0m Print version\r\n"] -[1.575336125,"o","\u001b]697;OSCUnlock=6f3dc305774c4fb5ac9caff308f61ed2\u0007\u001b]697;Dir=/home/jheisonmblivecom\u0007"] -[1.5753921979999999,"o","\u001b]697;Shell=bash\u0007\u001b]697;ShellPath=/usr/bin/bash\u0007"] -[1.575454122,"o","\u001b]697;WSLDistro=Ubuntu\u0007\u001b]697;PID=16910\u0007"] -[1.575515821,"o","\u001b]697;ExitCode=0\u0007\u001b]697;TTY=/dev/pts/3\u0007"] -[1.575567567,"o","\u001b]697;Log=\u0007\u001b]697;User=jheisonmblivecom\u0007"] -[1.576844584,"o","\u001b]697;OSCLock=6f3dc305774c4fb5ac9caff308f61ed2\u0007\u001b]697;PreExec\u0007"] -[1.6654565639999999,"o","\u001b[?2004h\u001b]697;StartPrompt\u0007\u001b[1;32mtexForge@univerlab\u001b[0m:\u001b[1;34m~\u001b[0m$ \u001b]697;EndPrompt\u0007\u001b]697;NewCmd=6f3dc305774c4fb5ac9caff308f61ed2\u0007"] -[4.221623371,"o","o"] -[4.2877478,"o","p"] -[4.352805733,"o","e"] -[4.433897023,"o","n"] -[4.506017184,"o","c"] -[4.585093582,"o","o"] -[4.656230257,"o","d"] -[4.740303969,"o","e"] -[4.941632268,"o","\r\n\u001b[?2004l\r"] -[5.658414806,"o","\u001b[?2031h\u001b]10;?\u0007\u001b]11;?\u0007\u001b[>0q\u001b[?25l\u001b[s\u001b[6n\u001b[?1016$p\u001b[?2027$p\u001b[?2031$p\u001b[?1004$p\u001b[?2004$p\u001b[?2026$p\u001b[?u\u001b]99;i=opentui-notifications:p=?;\u001b\\\u001b]1337;Capabilities\u001b\\\u001b[H\u001b]66;w=1; \u001b\\\u001b[6n\u001b[H\u001b]66;s=2; \u001b\\\u001b[6n\u001b[u\u001b[s\u001b[?1049h\u001b[>4;1m\u001b[?2027h\u001b[?2004h"] -[5.658749822,"o","\u001b[?1000h\u001b[?1002h\u001b[?1003h\u001b[?1006h"] -[5.658801952,"o","\u001b[14t"] -[5.668513847,"o","\u001b]4;0;?\u0007"] -[7.85971063,"o","\u001b[?2026h\u001b[?25l\u001b[1;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[2;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[3;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[4;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[5;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[6;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[7;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[8;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[9;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[10;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[11;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[12;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[13;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[14;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[15;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[16;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[17;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[18;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[19;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[20;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[21;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[22;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m "] -[7.859729269,"o"," \u001b[23;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[24;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[25;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[26;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[27;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[28;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[29;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[30;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[31;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[32;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[33;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[34;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[35;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[36;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[37;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[38;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[39;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[40;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[41;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[42;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[43;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[44;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b"] -[7.859754695,"o","[45;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[46;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[47;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[48;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[49;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[50;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[51;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[52;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[53;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[54;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[?2026l"] -[7.890234675,"o","\u001b]0;OpenCode\u0007"] -[7.9852611620000005,"o","\u001b[?2026h\u001b[?25l\u001b[20;54H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;56H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;57H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;58H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;59H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;60H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;61H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;62H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;64H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;65H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;67H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;68H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;69H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;70H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;71H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;72H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;74H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;75H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;77H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;78H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;79H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;80H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;83H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;84H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;85H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;86H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;87H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▄\u001b[0m\u001b[20;88H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;89H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;90H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;91H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;92H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[21;54H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[21;55H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[21;56H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[21;57H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[21;58H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[21;59H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[21;60H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀▀\u001b[0m\u001b[21;62H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[21;63H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[21;64H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[21;65H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[21;66H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[21;67H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[21;68H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[21;69H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[21;70H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[21;71H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀▄\u001b[0m\u001b[21;74H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[21;75H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀▀\u001b[0m\u001b[21;77H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀ \u001b[0m\u001b[21;79H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[21;80H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[21;81H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[21;82H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[21;83H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[21;84H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[21;85H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[21;86H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[21;87H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[21;88H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[21;89H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[21;90H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[21;91H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[21;92H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[22;54H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[22;55H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m \u001b[0m\u001b[22;56H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m \u001b[0m\u001b[22;57H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[22;58H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[22;59H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[22;60H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m \u001b[0m\u001b[22;62H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[22;63H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[22;64H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[22;65H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m▀▀▀\u001b[0m\u001b[22;68H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[22;69H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m"] -[7.98526746,"o","\u001b"] -[7.985274305,"o","[22;70H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m \u001b[0m\u001b[22;72H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[22;74H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[22;75H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m \u001b[0m\u001b[22;78H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[22;79H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[22;80H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m \u001b[0m\u001b[22;82H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[22;83H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[22;84H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[22;85H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m \u001b[0m\u001b[22;87H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[22;88H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[22;89H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[22;90H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m▀▀▀\u001b[0m\u001b[23;54H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀▀▀▀ \u001b[0m\u001b[23;59H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[23;60H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀▀▀ ▀▀▀▀ ▀\u001b[0m\u001b[23;70H\u001b[38;2;40;40;40m\u001b[48;2;10;10;10m▀▀\u001b[0m\u001b[23;72H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[23;74H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀▀▀▀ ▀▀▀▀ \u001b[0m\u001b[23;84H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[23;85H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[23;86H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[23;87H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[23;88H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[23;89H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[23;90H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[23;91H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀▀\u001b[0m\u001b[26;36H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[26;37H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[27;36H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[27;37H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[27;39H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mAsk anything... \"Fix broken tests\"\u001b[0m\u001b[27;73H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[28;36H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[28;37H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[29;36H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[29;37H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[29;39H\u001b[38;2;92;156;245m\u001b[48;2;30;30;30mBuild\u001b[0m\u001b[29;44H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[29;45H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30m·\u001b[0m\u001b[29;46H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[29;47H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mQwen3.7 Plus\u001b[0m\u001b[29;59H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[29;60H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mOpenCode Go\u001b[0m\u001b[29;71H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[30;36H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m╹\u001b[0m\u001b[30;37H\u001b[38;2;30;30;30m\u001b[48;2;10;10;10m▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\u001b[0m\u001b[31;84H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mtab \u001b[0m\u001b[31;88H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10magents\u001b[0m\u001b[31;96H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mctrl+p \u001b[0m\u001b[31;103H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mcommands\u001b[0m\u001b[35;36H\u001b[38;2;245;167;66m\u001b[48;2;10;10;10m● Tip \u001b[0m\u001b[35;42H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mRun \u001b[0m\u001b[35;46H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mdocker run -it --rm ghcr.io/anomalyco/opencode\u001b[0m\u001b[35;92H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m for containerized \u001b[0m\u001b[36;42H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10muse\u001b[0m\u001b[53;3H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m~\u001b[0m\u001b[53;136H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m1.15.13\u001b[0m\u001b[0m\u001b]12;#eeeeee\u0007\u001b[1 q\u001b[27;39H\u001b[?25h\u001b[?2026l"] -[8.03356446,"o","\u001b[?2026h\u001b[?25l\u001b[27;39H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mtes\" dem\u001b[0m\u001b[27;47H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[29;47H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mMiMo V2.5 Free\u001b[0m\u001b[29;61H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[29;62H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mOpenCode Zen\u001b[0m\u001b[0m\u001b[27;47H\u001b[?25h\u001b[?2026l"] -[8.047543303,"o","\u001b[?2026h\u001b[?25l\u001b[27;47H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mons\u001b[0m\u001b[0m\u001b[27;50H\u001b[?25h\u001b[?2026l"] -[8.063449681,"o","\u001b[?2026h\u001b[?25l\u001b[27;50H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mtr\u001b[0m\u001b[0m\u001b[27;52H\u001b[?25h\u001b[?2026l"] -[8.079271333,"o","\u001b[?2026h\u001b[?25l\u001b[27;52H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ma\u001b[0m\u001b[0m\u001b[27;53H\u001b[?25h\u001b[?2026l"] -[8.097240579,"o","\u001b[?2026h\u001b[?25l\u001b[27;53H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[27;54H\u001b[?25h\u001b[?2026l"] -[8.113261376,"o","\u001b[?2026h\u001b[?25l\u001b[27;54H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[27;55H\u001b[?25h\u001b[?2026l"] -[8.129084736,"o","\u001b[?2026h\u001b[?25l\u001b[27;55H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mn\u001b[0m\u001b[0m\u001b[27;56H\u001b[?25h\u001b[?2026l"] -[8.148726966,"o","\u001b[?2026h\u001b[?25l\u001b[27;56H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mg \u001b[0m\u001b[0m\u001b[27;58H\u001b[?25h\u001b[?2026l"] -[8.166859884,"o","\u001b[?2026h\u001b[?25l\u001b[27;58H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ma\u001b[0m\u001b[0m\u001b[27;59H\u001b[?25h\u001b[?2026l"] -[8.18287332,"o","\u001b[?2026h\u001b[?25l\u001b[27;59H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ml\u001b[0m\u001b[0m\u001b[27;60H\u001b[?25h\u001b[?2026l"] -[8.199655165,"o","\u001b[?2026h\u001b[?25l\u001b[27;60H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ml\u001b[0m\u001b[0m\u001b[27;61H\u001b[?25h\u001b[?2026l"] -[8.216095284,"o","\u001b[?2026h\u001b[?25l\u001b[27;61H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[27;62H\u001b[?25h\u001b[?2026l"] -[8.235712647,"o","\u001b[?2026h\u001b[?25l\u001b[27;62H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mca\u001b[0m\u001b[0m\u001b[27;64H\u001b[?25h\u001b[?2026l"] -[8.256083428,"o","\u001b[?2026h\u001b[?25l\u001b[27;64H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mp\u001b[0m\u001b[0m\u001b[27;65H\u001b[?25h\u001b[?2026l"] -[8.273031071,"o","\u001b[?2026h\u001b[?25l\u001b[27;65H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ma\u001b[0m\u001b[0m\u001b[27;66H\u001b[?25h\u001b[?2026l"] -[8.28926472,"o","\u001b[?2026h\u001b[?25l\u001b[27;66H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mb\u001b[0m\u001b[0m\u001b[27;67H\u001b[?25h\u001b[?2026l"] -[8.305519166,"o","\u001b[?2026h\u001b[?25l\u001b[27;67H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[27;68H\u001b[?25h\u001b[?2026l"] -[8.322005323,"o","\u001b[?2026h\u001b[?25l\u001b[27;68H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ml\u001b[0m\u001b[0m\u001b[27;69H\u001b[?25h\u001b[?2026l"] -[8.338115008,"o","\u001b[?2026h\u001b[?25l\u001b[27;69H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[27;70H\u001b[?25h\u001b[?2026l"] -[8.354736452,"o","\u001b[?2026h\u001b[?25l\u001b[27;70H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[27;71H\u001b[?25h\u001b[?2026l"] -[8.370591927,"o","\u001b[?2026h\u001b[?25l\u001b[27;71H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[27;72H\u001b[?25h\u001b[?2026l"] -[8.389016096,"o","\u001b[?2026h\u001b[?25l\u001b[27;72H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mes\u001b[0m\u001b[0m\u001b[27;74H\u001b[?25h\u001b[?2026l"] -[8.407057599,"o","\u001b[?2026h\u001b[?25l\u001b[27;74H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m:\u001b[0m\u001b[0m\u001b[27;75H\u001b[?25h\u001b[?2026l"] -[8.422767961,"o","\u001b[?2026h\u001b[?25l\u001b[27;75H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[27;76H\u001b[?25h\u001b[?2026l"] -[8.438975764,"o","\u001b[?2026h\u001b[?25l\u001b[27;76H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mM\u001b[0m\u001b[0m\u001b[27;77H\u001b[?25h\u001b[?2026l"] -[8.454879151,"o","\u001b[?2026h\u001b[?25l\u001b[27;77H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[27;78H\u001b[?25h\u001b[?2026l"] -[8.471218676,"o","\u001b[?2026h\u001b[?25l\u001b[27;78H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mr\u001b[0m\u001b[0m\u001b[27;79H\u001b[?25h\u001b[?2026l"] -[8.487997752,"o","\u001b[?2026h\u001b[?25l\u001b[27;79H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mm\u001b[0m\u001b[0m\u001b[27;80H\u001b[?25h\u001b[?2026l"] -[8.503985547,"o","\u001b[?2026h\u001b[?25l\u001b[27;80H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ma\u001b[0m\u001b[0m\u001b[27;81H\u001b[?25h\u001b[?2026l"] -[8.519627234,"o","\u001b[?2026h\u001b[?25l\u001b[27;81H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[27;82H\u001b[?25h\u001b[?2026l"] -[8.535530416,"o","\u001b[?2026h\u001b[?25l\u001b[27;82H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30md\u001b[0m\u001b[0m\u001b[27;83H\u001b[?25h\u001b[?2026l"] -[8.552071421,"o","\u001b[?2026h\u001b[?25l\u001b[27;83H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[27;84H\u001b[?25h\u001b[?2026l"] -[8.568294638,"o","\u001b[?2026h\u001b[?25l\u001b[27;84H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ms\u001b[0m\u001b[0m\u001b[27;85H\u001b[?25h\u001b[?2026l"] -[8.584099233,"o","\u001b[?2026h\u001b[?25l\u001b[27;85H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[27;86H\u001b[?25h\u001b[?2026l"] -[8.600271808,"o","\u001b[?2026h\u001b[?25l\u001b[27;86H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mq\u001b[0m\u001b[0m\u001b[27;87H\u001b[?25h\u001b[?2026l"] -[8.616276394,"o","\u001b[?2026h\u001b[?25l\u001b[27;87H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mu\u001b[0m\u001b[0m\u001b[27;88H\u001b[?25h\u001b[?2026l"] -[8.63205935,"o","\u001b[?2026h\u001b[?25l\u001b[27;88H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[27;89H\u001b[?25h\u001b[?2026l"] -[8.648534234,"o","\u001b[?2026h\u001b[?25l\u001b[27;89H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mn\u001b[0m\u001b[0m\u001b[27;90H\u001b[?25h\u001b[?2026l"] -[8.664893287,"o","\u001b[?2026h\u001b[?25l\u001b[27;90H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mc\u001b[0m\u001b[0m\u001b[27;91H\u001b[?25h\u001b[?2026l"] -[8.681244987,"o","\u001b[?2026h\u001b[?25l\u001b[27;91H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[27;92H\u001b[?25h\u001b[?2026l"] -[8.697702158,"o","\u001b[?2026h\u001b[?25l\u001b[27;92H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[27;93H\u001b[?25h\u001b[?2026l"] -[8.7141903,"o","\u001b[?2026h\u001b[?25l\u001b[27;93H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30md\u001b[0m\u001b[0m\u001b[27;94H\u001b[?25h\u001b[?2026l"] -[8.730293776,"o","\u001b[?2026h\u001b[?25l\u001b[27;94H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[27;95H\u001b[?25h\u001b[?2026l"] -[8.749865895,"o","\u001b[?2026h\u001b[?25l\u001b[27;95H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mag\u001b[0m\u001b[0m\u001b[27;97H\u001b[?25h\u001b[?2026l"] -[8.765722921,"o","\u001b[?2026h\u001b[?25l\u001b[27;97H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mr\u001b[0m\u001b[0m\u001b[27;98H\u001b[?25h\u001b[?2026l"] -[8.783756326,"o","\u001b[?2026h\u001b[?25l\u001b[27;98H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ma\u001b[0m\u001b[0m\u001b[27;99H\u001b[?25h\u001b[?2026l"] -[8.800320373,"o","\u001b[?2026h\u001b[?25l\u001b[27;99H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m,\u001b[0m\u001b[0m\u001b[27;100H\u001b[?25h\u001b[?2026l"] -[8.816438114,"o","\u001b[?2026h\u001b[?25l\u001b[27;100H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[27;101H\u001b[?25h\u001b[?2026l"] -[8.83288703,"o","\u001b[?2026h\u001b[?25l\u001b[27;101H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mG\u001b[0m\u001b[0m\u001b[27;102H\u001b[?25h\u001b[?2026l"] -[8.84953167,"o","\u001b[?2026h\u001b[?25l\u001b[27;102H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mra\u001b[0m\u001b[0m\u001b[27;104H\u001b[?25h\u001b[?2026l"] -[8.865342523,"o","\u001b[?2026h\u001b[?25l\u001b[27;104H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mp\u001b[0m\u001b[0m\u001b[27;105H\u001b[?25h\u001b[?2026l"] -[8.881426167,"o","\u001b[?2026h\u001b[?25l\u001b[27;105H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mh\u001b[0m\u001b[0m\u001b[27;106H\u001b[?25h\u001b[?2026l"] -[8.897569353,"o","\u001b[?2026h\u001b[?25l\u001b[27;106H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mv\u001b[0m\u001b[0m\u001b[27;107H\u001b[?25h\u001b[?2026l"] -[8.913642975,"o","\u001b[?2026h\u001b[?25l\u001b[27;107H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[27;108H\u001b[?25h\u001b[?2026l"] -[8.929717066,"o","\u001b[?2026h\u001b[?25l\u001b[27;108H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mz\u001b[0m\u001b[0m\u001b[27;109H\u001b[?25h\u001b[?2026l"] -[8.945686852,"o","\u001b[?2026h\u001b[?25l\u001b[27;101H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[28;39H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mGraphviz \u001b[0m\u001b[29;39H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[29;45H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[29;47H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[29;62H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[30;36H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[30;37H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[30;39H\u001b[38;2;92;156;245m\u001b[48;2;30;30;30mBuild\u001b[0m\u001b[30;44H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[30;45H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30m·\u001b[0m\u001b[30;46H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[30;47H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mMiMo V2.5 Free\u001b[0m\u001b[30;61H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[30;62H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mOpenCode Zen\u001b[0m\u001b[30;74H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[31;36H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m╹\u001b[0m\u001b[31;37H\u001b[38;2;30;30;30m\u001b[48;2;10;10;10m▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\u001b[0m\u001b[32;84H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mtab \u001b[0m\u001b[32;88H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10magents\u001b[0m\u001b[32;96H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mctrl+p \u001b[0m\u001b[32;103H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mcommands\u001b[0m\u001b[35;36H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[36;36H\u001b[38;2;245;167;66m\u001b[48;2;10;10;10m● Tip \u001b[0m\u001b[36;42H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mRun \u001b[0m\u001b[36;46H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mdocker run -it --rm ghcr.io/anomalyco/opencode\u001b[0m\u001b[36;92H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m for containerized \u001b[0m\u001b[37;42H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10muse\u001b[0m\u001b[0m\u001b[28;48H\u001b[?25h\u001b[?2026l"] -[8.962376118,"o","\u001b[?2026h\u001b[?25l\u001b[28;48H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ma\u001b[0m\u001b[0m\u001b[28;49H\u001b[?25h\u001b[?2026l"] -[8.978348926,"o","\u001b[?2026h\u001b[?25l\u001b[28;49H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mr\u001b[0m\u001b[0m\u001b[28;50H\u001b[?25h\u001b[?2026l"] -[8.996302793,"o","\u001b[?2026h\u001b[?25l\u001b[28;50H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mch\u001b[0m\u001b[0m\u001b[28;52H\u001b[?25h\u001b[?2026l"] -[9.012363502,"o","\u001b[?2026h\u001b[?25l\u001b[28;52H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[28;53H\u001b[?25h\u001b[?2026l"] -[9.028493446,"o","\u001b[?2026h\u001b[?25l\u001b[28;53H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[28;54H\u001b[?25h\u001b[?2026l"] -[9.044458414,"o","\u001b[?2026h\u001b[?25l\u001b[28;54H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[28;55H\u001b[?25h\u001b[?2026l"] -[9.060932337,"o","\u001b[?2026h\u001b[?25l\u001b[28;55H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mc\u001b[0m\u001b[53;6H\u001b[38;2;127;216;143m\u001b[48;2;10;10;10m⊙ \u001b[0m\u001b[53;8H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m3 MCP\u001b[0m\u001b[53;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m/status\u001b[0m\u001b[0m\u001b[28;56H\u001b[?25h\u001b[?2026l"] -[9.077659854,"o","\u001b[?2026h\u001b[?25l\u001b[28;56H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[28;57H\u001b[?25h\u001b[?2026l"] -[9.09358016,"o","\u001b[?2026h\u001b[?25l\u001b[28;57H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mur\u001b[0m\u001b[0m\u001b[28;59H\u001b[?25h\u001b[?2026l"] -[9.111542711,"o","\u001b[?2026h\u001b[?25l\u001b[28;59H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[28;60H\u001b[?25h\u001b[?2026l"] -[9.132142946,"o","\u001b[?2026h\u001b[?25l\u001b[28;60H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[28;61H\u001b[?25h\u001b[?2026l"] -[9.148048633,"o","\u001b[?2026h\u001b[?25l\u001b[28;61H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30md\u001b[0m\u001b[0m\u001b[28;62H\u001b[?25h\u001b[?2026l"] -[9.16523702,"o","\u001b[?2026h\u001b[?25l\u001b[28;62H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mig\u001b[0m\u001b[0m\u001b[28;64H\u001b[?25h\u001b[?2026l"] -[9.181153646,"o","\u001b[?2026h\u001b[?25l\u001b[28;64H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mr\u001b[0m\u001b[0m\u001b[28;65H\u001b[?25h\u001b[?2026l"] -[9.197459905,"o","\u001b[?2026h\u001b[?25l\u001b[28;65H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ma\u001b[0m\u001b[0m\u001b[28;66H\u001b[?25h\u001b[?2026l"] -[9.214135262,"o","\u001b[?2026h\u001b[?25l\u001b[28;66H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mp\u001b[0m\u001b[0m\u001b[28;67H\u001b[?25h\u001b[?2026l"] -[9.230552558,"o","\u001b[?2026h\u001b[?25l\u001b[28;67H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mh\u001b[0m\u001b[0m\u001b[28;68H\u001b[?25h\u001b[?2026l"] -[9.246684426,"o","\u001b[?2026h\u001b[?25l\u001b[28;68H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m,\u001b[0m\u001b[0m\u001b[28;69H\u001b[?25h\u001b[?2026l"] -[9.262897713,"o","\u001b[?2026h\u001b[?25l\u001b[28;69H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[28;70H\u001b[?25h\u001b[?2026l"] -[9.278955654,"o","\u001b[?2026h\u001b[?25l\u001b[28;70H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mD\u001b[0m\u001b[0m\u001b[28;71H\u001b[?25h\u001b[?2026l"] -[9.295717443000001,"o","\u001b[?2026h\u001b[?25l\u001b[28;71H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m2\u001b[0m\u001b[0m\u001b[28;72H\u001b[?25h\u001b[?2026l"] -[9.311282148,"o","\u001b[?2026h\u001b[?25l\u001b[28;72H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[28;73H\u001b[?25h\u001b[?2026l"] -[9.32754674,"o","\u001b[?2026h\u001b[?25l\u001b[28;73H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mp\u001b[0m\u001b[0m\u001b[28;74H\u001b[?25h\u001b[?2026l"] -[9.343662518,"o","\u001b[?2026h\u001b[?25l\u001b[28;74H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[28;75H\u001b[?25h\u001b[?2026l"] -[9.360496848,"o","\u001b[?2026h\u001b[?25l\u001b[28;75H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mp\u001b[0m\u001b[0m\u001b[28;76H\u001b[?25h\u001b[?2026l"] -[9.385056806,"o","\u001b[?2026h\u001b[?25l\u001b[28;76H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[28;77H\u001b[?25h\u001b[?2026l"] -[9.401905694,"o","\u001b[?2026h\u001b[?25l\u001b[28;77H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mli\u001b[0m\u001b[0m\u001b[28;79H\u001b[?25h\u001b[?2026l"] -[9.418235508,"o","\u001b[?2026h\u001b[?25l\u001b[28;79H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mn\u001b[0m\u001b[0m\u001b[28;80H\u001b[?25h\u001b[?2026l"] -[9.434156002,"o","\u001b[?2026h\u001b[?25l\u001b[28;80H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[28;81H\u001b[?25h\u001b[?2026l"] -[9.450988052,"o","\u001b[?2026h\u001b[?25l\u001b[28;81H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[28;82H\u001b[?25h\u001b[?2026l"] -[9.466834164,"o","\u001b[?2026h\u001b[?25l\u001b[28;82H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30md\u001b[0m\u001b[0m\u001b[28;83H\u001b[?25h\u001b[?2026l"] -[9.483849797,"o","\u001b[?2026h\u001b[?25l\u001b[28;83H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mia\u001b[0m\u001b[0m\u001b[28;85H\u001b[?25h\u001b[?2026l"] -[9.514971455,"o","\u001b[?2026h\u001b[?25l\u001b[28;85H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mg\u001b[0m\u001b[0m\u001b[28;86H\u001b[?25h\u001b[?2026l"] -[9.531170823,"o","\u001b[?2026h\u001b[?25l\u001b[28;86H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mra\u001b[0m\u001b[0m\u001b[28;88H\u001b[?25h\u001b[?2026l"] -[9.54979115,"o","\u001b[?2026h\u001b[?25l\u001b[28;88H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mm\u001b[0m\u001b[0m\u001b[28;89H\u001b[?25h\u001b[?2026l"] -[9.565724068,"o","\u001b[?2026h\u001b[?25l\u001b[28;89H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m,\u001b[0m\u001b[0m\u001b[28;90H\u001b[?25h\u001b[?2026l"] -[9.58161827,"o","\u001b[?2026h\u001b[?25l\u001b[28;90H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[28;91H\u001b[?25h\u001b[?2026l"] -[9.597912276,"o","\u001b[?2026h\u001b[?25l\u001b[28;91H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mm\u001b[0m\u001b[0m\u001b[28;92H\u001b[?25h\u001b[?2026l"] -[9.615446827,"o","\u001b[?2026h\u001b[?25l\u001b[28;92H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mat\u001b[0m\u001b[0m\u001b[28;94H\u001b[?25h\u001b[?2026l"] -[9.631165917,"o","\u001b[?2026h\u001b[?25l\u001b[28;94H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mh\u001b[0m\u001b[0m\u001b[28;95H\u001b[?25h\u001b[?2026l"] -[9.647299041,"o","\u001b[?2026h\u001b[?25l\u001b[28;95H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[28;96H\u001b[?25h\u001b[?2026l"] -[9.663407584,"o","\u001b[?2026h\u001b[?25l\u001b[28;96H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[28;97H\u001b[?25h\u001b[?2026l"] -[9.679418038,"o","\u001b[?2026h\u001b[?25l\u001b[28;97H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mq\u001b[0m\u001b[0m\u001b[28;98H\u001b[?25h\u001b[?2026l"] -[9.69575998,"o","\u001b[?2026h\u001b[?25l\u001b[28;98H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mu\u001b[0m\u001b[0m\u001b[28;99H\u001b[?25h\u001b[?2026l"] -[9.711662765,"o","\u001b[?2026h\u001b[?25l\u001b[28;99H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ma\u001b[0m\u001b[0m\u001b[28;100H\u001b[?25h\u001b[?2026l"] -[9.727571682,"o","\u001b[?2026h\u001b[?25l\u001b[28;100H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[28;101H\u001b[?25h\u001b[?2026l"] -[9.743930487,"o","\u001b[?2026h\u001b[?25l\u001b[28;101H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[28;102H\u001b[?25h\u001b[?2026l"] -[9.760443045,"o","\u001b[?2026h\u001b[?25l\u001b[28;102H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mo\u001b[0m\u001b[0m\u001b[28;103H\u001b[?25h\u001b[?2026l"] -[9.776790737,"o","\u001b[?2026h\u001b[?25l\u001b[28;103H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mn\u001b[0m\u001b[0m\u001b[28;104H\u001b[?25h\u001b[?2026l"] -[9.793408737,"o","\u001b[?2026h\u001b[?25l\u001b[28;104H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ms\u001b[0m\u001b[0m\u001b[28;105H\u001b[?25h\u001b[?2026l"] -[9.809440027,"o","\u001b[?2026h\u001b[?25l\u001b[28;105H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m w\u001b[0m\u001b[0m\u001b[28;107H\u001b[?25h\u001b[?2026l"] -[9.826094353,"o","\u001b[?2026h\u001b[?25l\u001b[28;107H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[28;108H\u001b[?25h\u001b[?2026l"] -[9.843194759,"o","\u001b[?2026h\u001b[?25l\u001b[28;108H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[28;109H\u001b[?25h\u001b[?2026l"] -[9.859626202,"o","\u001b[?2026h\u001b[?25l\u001b[19;54H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;56H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;57H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;58H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;59H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;60H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;61H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;62H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;64H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;65H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;67H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;68H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;69H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;70H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;71H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;72H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;74H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[19;75H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[19;77H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[19;78H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[19;79H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[19;80H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[19;83H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[19;84H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[19;85H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[19;86H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[19;87H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▄\u001b[0m\u001b[19;88H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[19;89H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[19;90H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[19;91H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[19;92H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;54H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[20;55H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[20;56H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[20;57H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[20;58H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;59H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[20;60H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀▀\u001b[0m\u001b[20;62H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[20;63H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;64H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[20;65H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[20;66H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[20;67H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[20;68H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;69H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[20;70H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[20;71H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀▄\u001b[0m\u001b[20;74H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[20;75H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀▀\u001b[0m\u001b[20;77H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀ \u001b[0m\u001b[20;79H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[20;80H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[20;81H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[20;82H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[20;83H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;84H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[20;85H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[20;86H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[20;87H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[20;88H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;89H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[20;90H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[20;91H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[20;92H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[21;54H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[21;55H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m \u001b[0m\u001b[21;56H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m \u001b[0m\u001b[21;57H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[21;58H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[21;59H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[21;60H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m \u001b[0m\u001b[21;62H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[21;63H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[21;64H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[21;65H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m▀▀▀\u001b[0m\u001b[21;69H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[21;70H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m \u001b[0"] -[9.859638036,"o","m\u001b[21;72H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[21;74H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[21;75H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m \u001b[0m\u001b[21;78H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[21;79H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[21;80H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m \u001b[0m\u001b[21;82H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[21;83H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[21;84H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[21;85H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m \u001b[0m\u001b[21;87H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[21;88H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[21;89H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[21;90H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m▀▀▀\u001b[0m\u001b[22;54H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀▀▀▀\u001b[0m\u001b[22;60H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀▀▀\u001b[0m\u001b[22;64H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀▀▀▀\u001b[0m\u001b[22;69H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[22;70H\u001b[38;2;40;40;40m\u001b[48;2;10;10;10m▀▀\u001b[0m\u001b[22;72H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[22;74H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀▀▀▀\u001b[0m\u001b[22;79H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀▀▀▀\u001b[0m\u001b[22;84H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[22;85H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[22;86H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[22;87H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[22;88H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[22;89H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[22;90H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[22;91H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀▀\u001b[0m\u001b[23;54H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[23;74H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[25;36H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[25;37H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[26;39H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mtes\" demonstrating all capabilities: Mermaid sequence diagra, \u001b[0m\u001b[27;39H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mGraphviz archi\u001b[0m\u001b[27;54H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mecture digr\u001b[0m\u001b[27;66H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mph, D2 pipeline \u001b[0m\u001b[27;83H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30miagram, math equations \u001b[0m\u001b[28;39H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mwith\u001b[0m\u001b[28;43H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[28;43H\u001b[?25h\u001b[?2026l"] -[9.876286918,"o","\u001b[?2026h\u001b[?25l\u001b[28;43H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[28;44H\u001b[?25h\u001b[?2026l"] -[9.892532699,"o","\u001b[?2026h\u001b[?25l\u001b[28;44H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mc\u001b[0m\u001b[0m\u001b[28;45H\u001b[?25h\u001b[?2026l"] -[9.908733199,"o","\u001b[?2026h\u001b[?25l\u001b[28;45H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mr\u001b[0m\u001b[0m\u001b[28;46H\u001b[?25h\u001b[?2026l"] -[9.925490057,"o","\u001b[?2026h\u001b[?25l\u001b[28;46H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mo\u001b[0m\u001b[0m\u001b[28;47H\u001b[?25h\u001b[?2026l"] -[9.941976675,"o","\u001b[?2026h\u001b[?25l\u001b[28;47H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ms\u001b[0m\u001b[0m\u001b[28;48H\u001b[?25h\u001b[?2026l"] -[9.958709344,"o","\u001b[?2026h\u001b[?25l\u001b[28;48H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ms\u001b[0m\u001b[0m\u001b[28;49H\u001b[?25h\u001b[?2026l"] -[9.976395824,"o","\u001b[?2026h\u001b[?25l\u001b[28;49H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m-r\u001b[0m\u001b[0m\u001b[28;51H\u001b[?25h\u001b[?2026l"] -[9.992217523,"o","\u001b[?2026h\u001b[?25l\u001b[28;51H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[28;52H\u001b[?25h\u001b[?2026l"] -[10.008416487,"o","\u001b[?2026h\u001b[?25l\u001b[28;52H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mf\u001b[0m\u001b[0m\u001b[28;53H\u001b[?25h\u001b[?2026l"] -[10.024487218,"o","\u001b[?2026h\u001b[?25l\u001b[28;53H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ms\u001b[0m\u001b[0m\u001b[28;54H\u001b[?25h\u001b[?2026l"] -[10.040496778,"o","\u001b[?2026h\u001b[?25l\u001b[28;54H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m,\u001b[0m\u001b[0m\u001b[28;55H\u001b[?25h\u001b[?2026l"] -[10.056507578,"o","\u001b[?2026h\u001b[?25l\u001b[28;55H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[28;56H\u001b[?25h\u001b[?2026l"] -[10.072710369,"o","\u001b[?2026h\u001b[?25l\u001b[28;56H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mB\u001b[0m\u001b[0m\u001b[28;57H\u001b[?25h\u001b[?2026l"] -[10.088767337,"o","\u001b[?2026h\u001b[?25l\u001b[28;57H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[28;58H\u001b[?25h\u001b[?2026l"] -[10.105044328,"o","\u001b[?2026h\u001b[?25l\u001b[28;58H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mb\u001b[0m\u001b[0m\u001b[28;59H\u001b[?25h\u001b[?2026l"] -[10.121379806,"o","\u001b[?2026h\u001b[?25l\u001b[28;59H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mTe\u001b[0m\u001b[0m\u001b[28;61H\u001b[?25h\u001b[?2026l"] -[10.137211597,"o","\u001b[?2026h\u001b[?25l\u001b[28;61H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mX\u001b[0m\u001b[0m\u001b[28;62H\u001b[?25h\u001b[?2026l"] -[10.153149588,"o","\u001b[?2026h\u001b[?25l\u001b[28;62H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[28;63H\u001b[?25h\u001b[?2026l"] -[10.169193917,"o","\u001b[?2026h\u001b[?25l\u001b[28;63H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mc\u001b[0m\u001b[0m\u001b[28;64H\u001b[?25h\u001b[?2026l"] -[10.185449247,"o","\u001b[?2026h\u001b[?25l\u001b[28;64H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[28;65H\u001b[?25h\u001b[?2026l"] -[10.202150347,"o","\u001b[?2026h\u001b[?25l\u001b[28;65H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[28;66H\u001b[?25h\u001b[?2026l"] -[10.217917179,"o","\u001b[?2026h\u001b[?25l\u001b[28;66H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ma\u001b[0m\u001b[0m\u001b[28;67H\u001b[?25h\u001b[?2026l"] -[10.234625941000001,"o","\u001b[?2026h\u001b[?25l\u001b[28;67H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[28;68H\u001b[?25h\u001b[?2026l"] -[10.250888869,"o","\u001b[?2026h\u001b[?25l\u001b[28;68H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[28;69H\u001b[?25h\u001b[?2026l"] -[10.266897825,"o","\u001b[?2026h\u001b[?25l\u001b[28;69H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mo\u001b[0m\u001b[0m\u001b[28;70H\u001b[?25h\u001b[?2026l"] -[10.283268565,"o","\u001b[?2026h\u001b[?25l\u001b[28;70H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mn\u001b[0m\u001b[0m\u001b[28;71H\u001b[?25h\u001b[?2026l"] -[10.299543127,"o","\u001b[?2026h\u001b[?25l\u001b[28;71H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ms\u001b[0m\u001b[0m\u001b[28;72H\u001b[?25h\u001b[?2026l"] -[10.315465123,"o","\u001b[?2026h\u001b[?25l\u001b[28;72H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m,\u001b[0m\u001b[0m\u001b[28;73H\u001b[?25h\u001b[?2026l"] -[10.331412099,"o","\u001b[?2026h\u001b[?25l\u001b[28;73H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m t\u001b[0m\u001b[0m\u001b[28;75H\u001b[?25h\u001b[?2026l"] -[10.349124717,"o","\u001b[?2026h\u001b[?25l\u001b[28;75H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[28;76H\u001b[?25h\u001b[?2026l"] -[10.368064211,"o","\u001b[?2026h\u001b[?25l\u001b[28;76H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mk\u001b[0m\u001b[0m\u001b[28;77H\u001b[?25h\u001b[?2026l"] -[10.38843236,"o","\u001b[?2026h\u001b[?25l\u001b[28;77H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mz\u001b[0m\u001b[0m\u001b[28;78H\u001b[?25h\u001b[?2026l"] -[10.407435441,"o","\u001b[?2026h\u001b[?25l\u001b[28;78H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m d\u001b[0m\u001b[0m\u001b[28;80H\u001b[?25h\u001b[?2026l"] -[10.42397839,"o","\u001b[?2026h\u001b[?25l\u001b[28;80H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[28;81H\u001b[?25h\u001b[?2026l"] -[10.439985639,"o","\u001b[?2026h\u001b[?25l\u001b[28;81H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ma\u001b[0m\u001b[0m\u001b[28;82H\u001b[?25h\u001b[?2026l"] -[10.456676889,"o","\u001b[?2026h\u001b[?25l\u001b[28;82H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mg\u001b[0m\u001b[0m\u001b[28;83H\u001b[?25h\u001b[?2026l"] -[10.473048347,"o","\u001b[?2026h\u001b[?25l\u001b[28;83H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mr\u001b[0m\u001b[0m\u001b[28;84H\u001b[?25h\u001b[?2026l"] -[10.489325363,"o","\u001b[?2026h\u001b[?25l\u001b[28;84H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ma\u001b[0m\u001b[0m\u001b[28;85H\u001b[?25h\u001b[?2026l"] -[10.505440452,"o","\u001b[?2026h\u001b[?25l\u001b[28;85H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mm\u001b[0m\u001b[0m\u001b[28;86H\u001b[?25h\u001b[?2026l"] -[10.522205121,"o","\u001b[?2026h\u001b[?25l\u001b[28;86H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m,\u001b[0m\u001b[0m\u001b[28;87H\u001b[?25h\u001b[?2026l"] -[10.538913067,"o","\u001b[?2026h\u001b[?25l\u001b[28;87H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[28;88H\u001b[?25h\u001b[?2026l"] -[10.555933122999999,"o","\u001b[?2026h\u001b[?25l\u001b[28;88H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[28;89H\u001b[?25h\u001b[?2026l"] -[10.571886849,"o","\u001b[?2026h\u001b[?25l\u001b[28;89H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mx\u001b[0m\u001b[0m\u001b[28;90H\u001b[?25h\u001b[?2026l"] -[10.58798463,"o","\u001b[?2026h\u001b[?25l\u001b[28;90H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ma\u001b[0m\u001b[0m\u001b[28;91H\u001b[?25h\u001b[?2026l"] -[10.603799892,"o","\u001b[?2026h\u001b[?25l\u001b[28;91H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mm\u001b[0m\u001b[0m\u001b[28;92H\u001b[?25h\u001b[?2026l"] -[10.619625981,"o","\u001b[?2026h\u001b[?25l\u001b[28;92H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mp\u001b[0m\u001b[0m\u001b[28;93H\u001b[?25h\u001b[?2026l"] -[10.636151222,"o","\u001b[?2026h\u001b[?25l\u001b[28;93H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ml\u001b[0m\u001b[0m\u001b[28;94H\u001b[?25h\u001b[?2026l"] -[10.65286565,"o","\u001b[?2026h\u001b[?25l\u001b[28;94H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[28;95H\u001b[?25h\u001b[?2026l"] -[10.670690837,"o","\u001b[?2026h\u001b[?25l\u001b[28;95H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ms \u001b[0m\u001b[0m\u001b[28;97H\u001b[?25h\u001b[?2026l"] -[10.68831802,"o","\u001b[?2026h\u001b[?25l\u001b[28;97H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mo\u001b[0m\u001b[0m\u001b[28;98H\u001b[?25h\u001b[?2026l"] -[10.704516371,"o","\u001b[?2026h\u001b[?25l\u001b[28;98H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mf\u001b[0m\u001b[0m\u001b[28;99H\u001b[?25h\u001b[?2026l"] -[10.720675614,"o","\u001b[?2026h\u001b[?25l\u001b[28;99H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[28;100H\u001b[?25h\u001b[?2026l"] -[10.736899302,"o","\u001b[?2026h\u001b[?25l\u001b[28;100H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mc\u001b[0m\u001b[0m\u001b[28;101H\u001b[?25h\u001b[?2026l"] -[10.752878541,"o","\u001b[?2026h\u001b[?25l\u001b[28;101H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mo\u001b[0m\u001b[0m\u001b[28;102H\u001b[?25h\u001b[?2026l"] -[10.769433025,"o","\u001b[?2026h\u001b[?25l\u001b[28;102H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30md\u001b[0m\u001b[0m\u001b[28;103H\u001b[?25h\u001b[?2026l"] -[10.789516113,"o","\u001b[?2026h\u001b[?25l\u001b[28;103H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[28;104H\u001b[?25h\u001b[?2026l"] -[10.808530166,"o","\u001b[?2026h\u001b[?25l\u001b[28;104H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[28;105H\u001b[?25h\u001b[?2026l"] -[10.828210689,"o","\u001b[?2026h\u001b[?25l\u001b[28;105H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ml\u001b[0m\u001b[0m\u001b[28;106H\u001b[?25h\u001b[?2026l"] -[10.847114714,"o","\u001b[?2026h\u001b[?25l\u001b[28;106H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mis\u001b[0m\u001b[0m\u001b[28;108H\u001b[?25h\u001b[?2026l"] -[10.864883831,"o","\u001b[?2026h\u001b[?25l\u001b[28;108H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[28;109H\u001b[?25h\u001b[?2026l"] -[10.880692931,"o","\u001b[?2026h\u001b[?25l\u001b[28;105H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[29;39H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mlisti\u001b[0m\u001b[30;39H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[30;45H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[30;47H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[30;62H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[31;36H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[31;37H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[31;39H\u001b[38;2;92;156;245m\u001b[48;2;30;30;30mBuild\u001b[0m\u001b[31;44H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[31;45H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30m·\u001b[0m\u001b[31;46H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[31;47H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mMiMo V2.5 Free\u001b[0m\u001b[31;61H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[31;62H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mOpenCode Zen\u001b[0m\u001b[31;74H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[32;36H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m╹\u001b[0m\u001b[32;37H\u001b[38;2;30;30;30m\u001b[48;2;10;10;10m▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\u001b[0m\u001b[33;84H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mtab \u001b[0m\u001b[33;88H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10magents\u001b[0m\u001b[33;96H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mctrl+p \u001b[0m\u001b[33;103H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mcommands\u001b[0m\u001b[36;36H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[37;36H\u001b[38;2;245;167;66m\u001b[48;2;10;10;10m● Tip \u001b[0m\u001b[37;42H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mRun \u001b[0m\u001b[37;46H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mdocker run -it --rm ghcr.io/anomalyco/opencode\u001b[0m\u001b[37;92H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m for containerized \u001b[0m\u001b[38;42H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10muse\u001b[0m\u001b[0m\u001b[29;44H\u001b[?25h\u001b[?2026l"] -[10.897307728,"o","\u001b[?2026h\u001b[?25l\u001b[29;44H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mn\u001b[0m\u001b[0m\u001b[29;45H\u001b[?25h\u001b[?2026l"] -[10.913763263,"o","\u001b[?2026h\u001b[?25l\u001b[29;45H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mg\u001b[0m\u001b[0m\u001b[29;46H\u001b[?25h\u001b[?2026l"] -[10.929739509000001,"o","\u001b[?2026h\u001b[?25l\u001b[29;46H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ms\u001b[0m\u001b[0m\u001b[29;47H\u001b[?25h\u001b[?2026l"] -[10.949735068,"o","\u001b[?2026h\u001b[?25l\u001b[29;47H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m,\u001b[0m\u001b[0m\u001b[29;48H\u001b[?25h\u001b[?2026l"] -[10.966137405,"o","\u001b[?2026h\u001b[?25l\u001b[29;48H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[29;49H\u001b[?25h\u001b[?2026l"] -[10.983666547,"o","\u001b[?2026h\u001b[?25l\u001b[29;49H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mtw\u001b[0m\u001b[0m\u001b[29;51H\u001b[?25h\u001b[?2026l"] -[11.000621029,"o","\u001b[?2026h\u001b[?25l\u001b[29;51H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mo\u001b[0m\u001b[0m\u001b[29;52H\u001b[?25h\u001b[?2026l"] -[11.01782169,"o","\u001b[?2026h\u001b[?25l\u001b[29;52H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[29;53H\u001b[?25h\u001b[?2026l"] -[11.038098722,"o","\u001b[?2026h\u001b[?25l\u001b[29;53H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mb\u001b[0m\u001b[0m\u001b[29;54H\u001b[?25h\u001b[?2026l"] -[11.058257095,"o","\u001b[?2026h\u001b[?25l\u001b[29;54H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mo\u001b[0m\u001b[0m\u001b[29;55H\u001b[?25h\u001b[?2026l"] -[11.074664046,"o","\u001b[?2026h\u001b[?25l\u001b[29;55H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mok\u001b[0m\u001b[0m\u001b[29;57H\u001b[?25h\u001b[?2026l"] -[11.090757796,"o","\u001b[?2026h\u001b[?25l\u001b[29;57H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[29;58H\u001b[?25h\u001b[?2026l"] -[11.106908656,"o","\u001b[?2026h\u001b[?25l\u001b[29;58H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ma\u001b[0m\u001b[0m\u001b[29;59H\u001b[?25h\u001b[?2026l"] -[11.125646054,"o","\u001b[?2026h\u001b[?25l\u001b[29;59H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mb\u001b[0m\u001b[0m\u001b[29;60H\u001b[?25h\u001b[?2026l"] -[11.141357046,"o","\u001b[?2026h\u001b[?25l\u001b[29;60H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ms\u001b[0m\u001b[0m\u001b[29;61H\u001b[?25h\u001b[?2026l"] -[11.157149151,"o","\u001b[?2026h\u001b[?25l\u001b[29;61H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[29;62H\u001b[?25h\u001b[?2026l"] -[11.173395655,"o","\u001b[?2026h\u001b[?25l\u001b[29;62H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[29;63H\u001b[?25h\u001b[?2026l"] -[11.190052763,"o","\u001b[?2026h\u001b[?25l\u001b[29;63H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ma\u001b[0m\u001b[0m\u001b[29;64H\u001b[?25h\u001b[?2026l"] -[11.212095644,"o","\u001b[?2026h\u001b[?25l\u001b[29;64H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mb\u001b[0m\u001b[0m\u001b[29;65H\u001b[?25h\u001b[?2026l"] -[11.223640344,"o","\u001b[?2026h\u001b[?25l\u001b[29;65H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mle\u001b[0m\u001b[0m\u001b[29;67H\u001b[?25h\u001b[?2026l"] -[11.23962341,"o","\u001b[?2026h\u001b[?25l\u001b[29;67H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ms\u001b[0m\u001b[0m\u001b[29;68H\u001b[?25h\u001b[?2026l"] -[11.260236274,"o","\u001b[?2026h\u001b[?25l\u001b[29;68H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m,\u001b[0m\u001b[0m\u001b[29;69H\u001b[?25h\u001b[?2026l"] -[11.276768334,"o","\u001b[?2026h\u001b[?25l\u001b[29;69H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[29;70H\u001b[?25h\u001b[?2026l"] -[11.292845481,"o","\u001b[?2026h\u001b[?25l\u001b[29;70H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[29;71H\u001b[?25h\u001b[?2026l"] -[11.309504524,"o","\u001b[?2026h\u001b[?25l\u001b[29;71H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ma\u001b[0m\u001b[0m\u001b[29;72H\u001b[?25h\u001b[?2026l"] -[11.328733477,"o","\u001b[?2026h\u001b[?25l\u001b[29;72H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mbl\u001b[0m\u001b[0m\u001b[29;74H\u001b[?25h\u001b[?2026l"] -[11.345188197,"o","\u001b[?2026h\u001b[?25l\u001b[29;74H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[29;75H\u001b[?25h\u001b[?2026l"] -[11.361086551,"o","\u001b[?2026h\u001b[?25l\u001b[29;75H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[29;76H\u001b[?25h\u001b[?2026l"] -[11.377140474,"o","\u001b[?2026h\u001b[?25l\u001b[29;76H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mo\u001b[0m\u001b[0m\u001b[29;77H\u001b[?25h\u001b[?2026l"] -[11.393709628,"o","\u001b[?2026h\u001b[?25l\u001b[29;77H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mf\u001b[0m\u001b[0m\u001b[29;78H\u001b[?25h\u001b[?2026l"] -[11.409462902,"o","\u001b[?2026h\u001b[?25l\u001b[29;78H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[29;79H\u001b[?25h\u001b[?2026l"] -[11.425625074,"o","\u001b[?2026h\u001b[?25l\u001b[29;79H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mc\u001b[0m\u001b[0m\u001b[29;80H\u001b[?25h\u001b[?2026l"] -[11.448146372,"o","\u001b[?2026h\u001b[?25l\u001b[29;80H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mon\u001b[0m\u001b[0m\u001b[29;82H\u001b[?25h\u001b[?2026l"] -[11.468360302,"o","\u001b[?2026h\u001b[?25l\u001b[29;82H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[29;83H\u001b[?25h\u001b[?2026l"] -[11.488756024,"o","\u001b[?2026h\u001b[?25l\u001b[29;83H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30men\u001b[0m\u001b[0m\u001b[29;85H\u001b[?25h\u001b[?2026l"] -[11.505334171,"o","\u001b[?2026h\u001b[?25l\u001b[29;85H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[29;86H\u001b[?25h\u001b[?2026l"] -[11.521325766,"o","\u001b[?2026h\u001b[?25l\u001b[29;86H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ms\u001b[0m\u001b[0m\u001b[29;87H\u001b[?25h\u001b[?2026l"] -[11.537354381,"o","\u001b[?2026h\u001b[?25l\u001b[29;87H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m,\u001b[0m\u001b[0m\u001b[29;88H\u001b[?25h\u001b[?2026l"] -[11.553481928,"o","\u001b[?2026h\u001b[?25l\u001b[29;88H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[29;89H\u001b[?25h\u001b[?2026l"] -[11.569550087,"o","\u001b[?2026h\u001b[?25l\u001b[29;89H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ma\u001b[0m\u001b[0m\u001b[29;90H\u001b[?25h\u001b[?2026l"] -[11.585598366,"o","\u001b[?2026h\u001b[?25l\u001b[29;90H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mn\u001b[0m\u001b[0m\u001b[29;91H\u001b[?25h\u001b[?2026l"] -[11.601652177,"o","\u001b[?2026h\u001b[?25l\u001b[29;91H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30md\u001b[0m\u001b[0m\u001b[29;92H\u001b[?25h\u001b[?2026l"] -[11.617799273,"o","\u001b[?2026h\u001b[?25l\u001b[29;92H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[29;93H\u001b[?25h\u001b[?2026l"] -[11.633531559,"o","\u001b[?2026h\u001b[?25l\u001b[29;93H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ml\u001b[0m\u001b[0m\u001b[29;94H\u001b[?25h\u001b[?2026l"] -[11.651685148,"o","\u001b[?2026h\u001b[?25l\u001b[29;94H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mis\u001b[0m\u001b[0m\u001b[29;96H\u001b[?25h\u001b[?2026l"] -[11.669696378,"o","\u001b[?2026h\u001b[?25l\u001b[29;96H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[29;97H\u001b[?25h\u001b[?2026l"] -[11.686143903,"o","\u001b[?2026h\u001b[?25l\u001b[29;97H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ms\u001b[0m\u001b[0m\u001b[29;98H\u001b[?25h\u001b[?2026l"] -[11.70222538,"o","\u001b[?2026h\u001b[?25l\u001b[29;98H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m.\u001b[0m\u001b[0m\u001b[29;99H\u001b[?25h\u001b[?2026l"] -[11.71838617,"o","\u001b[?2026h\u001b[?25l\u001b[29;99H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[29;100H\u001b[?25h\u001b[?2026l"] -[11.734510281,"o","\u001b[?2026h\u001b[?25l\u001b[29;100H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mC\u001b[0m\u001b[0m\u001b[29;101H\u001b[?25h\u001b[?2026l"] -[11.750544012,"o","\u001b[?2026h\u001b[?25l\u001b[29;101H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mo\u001b[0m\u001b[0m\u001b[29;102H\u001b[?25h\u001b[?2026l"] -[11.770725414,"o","\u001b[?2026h\u001b[?25l\u001b[29;102H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mm\u001b[0m\u001b[0m\u001b[29;103H\u001b[?25h\u001b[?2026l"] -[11.790511114,"o","\u001b[?2026h\u001b[?25l\u001b[29;103H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mp\u001b[0m\u001b[0m\u001b[29;104H\u001b[?25h\u001b[?2026l"] -[11.806982446,"o","\u001b[?2026h\u001b[?25l\u001b[29;104H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mil\u001b[0m\u001b[0m\u001b[29;106H\u001b[?25h\u001b[?2026l"] -[11.823587574,"o","\u001b[?2026h\u001b[?25l\u001b[29;106H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[29;107H\u001b[?25h\u001b[?2026l"] -[11.839906764,"o","\u001b[?2026h\u001b[?25l\u001b[29;107H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[29;108H\u001b[?25h\u001b[?2026l"] -[11.855821649,"o","\u001b[?2026h\u001b[?25l\u001b[29;108H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mw\u001b[0m\u001b[0m\u001b[29;109H\u001b[?25h\u001b[?2026l"] -[11.871745495,"o","\u001b[?2026h\u001b[?25l\u001b[18;54H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[18;56H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[18;57H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[18;58H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[18;59H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[18;60H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[18;61H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[18;62H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[18;64H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[18;65H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[18;67H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[18;68H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[18;69H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[18;70H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[18;71H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[18;72H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[18;74H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[18;75H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[18;77H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[18;78H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[18;79H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[18;80H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[18;83H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[18;84H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[18;85H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[18;86H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[18;87H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▄\u001b[0m\u001b[18;88H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[18;89H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[18;90H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[18;91H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[18;92H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[19;54H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[19;55H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[19;56H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[19;57H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[19;58H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;59H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[19;60H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀▀\u001b[0m\u001b[19;62H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[19;63H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;64H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[19;65H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[19;66H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[19;67H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[19;68H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;69H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[19;70H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[19;71H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀▄\u001b[0m\u001b[19;74H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[19;75H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀▀\u001b[0m\u001b[19;77H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀ \u001b[0m\u001b[19;79H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[19;80H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[19;81H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[19;82H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[19;83H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[19;84H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[19;85H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[19;86H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[19;87H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[19;88H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[19;89H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[19;90H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[19;91H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[19;92H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[20;54H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[20;55H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m \u001b[0m\u001b[20;56H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m \u001b[0m\u001b[20;57H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[20;58H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;59H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[20;60H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m \u001b[0m\u001b[20;62H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[20;63H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;64H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[20;65H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m▀▀▀\u001b[0m\u001b[20;69H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[20;70H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m \u001b[0"] -[11.87175852,"o","m\u001b[20;72H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[20;74H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[20;75H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m \u001b[0m\u001b[20;78H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;79H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[20;80H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m \u001b[0m\u001b[20;82H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[20;83H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;84H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[20;85H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m \u001b[0m\u001b[20;87H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[20;88H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;89H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[20;90H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m▀▀▀\u001b[0m\u001b[21;54H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀▀▀▀\u001b[0m\u001b[21;60H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀▀▀\u001b[0m\u001b[21;64H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀▀▀▀\u001b[0m\u001b[21;69H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[21;70H\u001b[38;2;40;40;40m\u001b[48;2;10;10;10m▀▀\u001b[0m\u001b[21;72H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[21;74H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀▀▀▀\u001b[0m\u001b[21;79H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀▀▀▀\u001b[0m\u001b[21;84H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[21;85H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[21;86H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[21;87H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[21;88H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[21;89H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[21;90H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[21;91H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀▀\u001b[0m\u001b[22;54H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[22;74H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[24;36H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[24;37H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[25;39H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mtes\" demonstrating all capabilities: Mermaid sequence diagra, \u001b[0m\u001b[26;39H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mGraphviz archi\u001b[0m\u001b[26;54H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mecture digr\u001b[0m\u001b[26;66H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mph, D2 pipeline \u001b[0m\u001b[26;83H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30miagram, math equations \u001b[0m\u001b[27;39H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mwith cross-refs, BibTeX citations, tikz diagr\u001b[0m\u001b[27;85H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mm, exa\u001b[0m\u001b[27;92H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mples of code \u001b[0m\u001b[27;105H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[28;39H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ml\u001b[0m\u001b[28;41H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mstings, two booktabs tables, table of\u001b[0m\u001b[28;79H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mcontents, and \u001b[0m\u001b[28;94H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[28;96H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mts.\u001b[0m\u001b[28;100H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mC\u001b[0m\u001b[28;102H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mmpile \u001b[0m\u001b[29;39H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mw\u001b[0m\u001b[29;41H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[29;41H\u001b[?25h\u001b[?2026l"] -[11.888336044,"o","\u001b[?2026h\u001b[?25l\u001b[29;41H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[29;42H\u001b[?25h\u001b[?2026l"] -[11.910531944,"o","\u001b[?2026h\u001b[?25l\u001b[29;42H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mh \u001b[0m\u001b[0m\u001b[29;44H\u001b[?25h\u001b[?2026l"] -[11.926859619,"o","\u001b[?2026h\u001b[?25l\u001b[29;44H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[29;45H\u001b[?25h\u001b[?2026l"] -[11.942819826000001,"o","\u001b[?2026h\u001b[?25l\u001b[29;45H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[29;46H\u001b[?25h\u001b[?2026l"] -[11.958869358,"o","\u001b[?2026h\u001b[?25l\u001b[29;46H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mx\u001b[0m\u001b[0m\u001b[29;47H\u001b[?25h\u001b[?2026l"] -[11.975064322,"o","\u001b[?2026h\u001b[?25l\u001b[29;47H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mf\u001b[0m\u001b[0m\u001b[29;48H\u001b[?25h\u001b[?2026l"] -[11.990887615,"o","\u001b[?2026h\u001b[?25l\u001b[29;48H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mo\u001b[0m\u001b[0m\u001b[29;49H\u001b[?25h\u001b[?2026l"] -[12.007348368,"o","\u001b[?2026h\u001b[?25l\u001b[29;49H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mr\u001b[0m\u001b[0m\u001b[29;50H\u001b[?25h\u001b[?2026l"] -[12.023249827,"o","\u001b[?2026h\u001b[?25l\u001b[29;50H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mg\u001b[0m\u001b[0m\u001b[29;51H\u001b[?25h\u001b[?2026l"] -[12.039271768,"o","\u001b[?2026h\u001b[?25l\u001b[29;51H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[29;52H\u001b[?25h\u001b[?2026l"] -[12.05541549,"o","\u001b[?2026h\u001b[?25l\u001b[29;52H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[29;53H\u001b[?25h\u001b[?2026l"] -[12.071979261,"o","\u001b[?2026h\u001b[?25l\u001b[29;53H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mb\u001b[0m\u001b[0m\u001b[29;54H\u001b[?25h\u001b[?2026l"] -[12.093140625,"o","\u001b[?2026h\u001b[?25l\u001b[29;54H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mui\u001b[0m\u001b[0m\u001b[29;56H\u001b[?25h\u001b[?2026l"] -[12.108898637,"o","\u001b[?2026h\u001b[?25l\u001b[29;56H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ml\u001b[0m\u001b[0m\u001b[29;57H\u001b[?25h\u001b[?2026l"] -[12.124901081,"o","\u001b[?2026h\u001b[?25l\u001b[29;57H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30md\u001b[0m\u001b[0m\u001b[29;58H\u001b[?25h\u001b[?2026l"] -[12.14090112,"o","\u001b[?2026h\u001b[?25l\u001b[29;58H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m.\u001b[0m\u001b[0m\u001b[29;59H\u001b[?25h\u001b[?2026l"] -[12.352289975,"o","\u001b[?2026h\u001b[?25l\u001b[18;54H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[18;74H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;54H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;74H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;54H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;56H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;57H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;58H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;59H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;60H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;61H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;62H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;64H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;65H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;67H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;68H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;69H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;70H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;71H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;72H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;74H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;75H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;77H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;78H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;79H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;80H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;83H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;84H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;85H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;86H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;87H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▄\u001b[0m\u001b[20;88H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;89H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;90H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;91H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[20;92H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[21;54H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[21;55H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[21;56H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[21;57H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[21;58H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[21;59H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[21;60H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀▀\u001b[0m\u001b[21;62H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[21;63H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[21;64H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[21;65H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[21;66H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[21;67H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[21;69H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[21;70H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[21;71H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀▄\u001b[0m\u001b[21;74H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[21;75H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀▀\u001b[0m\u001b[21;77H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀ \u001b[0m\u001b[21;79H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[21;81H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[21;82H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[21;83H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[21;84H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[21;85H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[21;86H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[21;87H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[21;88H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[21;89H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[21;90H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[21;91H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[21;92H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[22;54H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[22;55H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m \u001b[0m\u001b[22;56H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m \u001b[0m\u001b[22;57H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[22;58H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[22;59H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[22;60H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m \u001b[0m\u001b[22;62H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[22;63H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[22;64H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m�"] -[12.352354868,"o","�\u001b[0m\u001b[22;65H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m▀▀▀\u001b[0m\u001b[22;68H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[22;69H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[22;70H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m \u001b[0m\u001b[22;72H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[22;74H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[22;75H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m \u001b[0m\u001b[22;78H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[22;79H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[22;80H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m \u001b[0m\u001b[22;82H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[22;83H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[22;84H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[22;85H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m \u001b[0m\u001b[22;87H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[22;88H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[22;89H\u001b[38;2;238;238;238m\u001b[48;2;238;238;238m\u001b[1m▀\u001b[0m\u001b[22;90H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m▀▀▀\u001b[0m\u001b[23;54H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀▀▀▀ \u001b[0m\u001b[23;59H\u001b[38;2;128;128;128m\u001b[48;2;128;128;128m▀\u001b[0m\u001b[23;60H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀▀▀ ▀▀▀▀ ▀\u001b[0m\u001b[23;70H\u001b[38;2;40;40;40m\u001b[48;2;10;10;10m▀▀\u001b[0m\u001b[23;72H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[23;74H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀▀▀▀ ▀▀▀▀ \u001b[0m\u001b[23;84H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[23;85H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[23;86H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[23;87H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[23;88H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[23;89H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[23;90H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀\u001b[0m\u001b[23;91H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀▀\u001b[0m\u001b[24;36H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[25;36H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[26;39H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[27;39H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mAsk anything... \"Fix broken tests\"\u001b[0m\u001b[27;73H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[28;39H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[29;39H\u001b[38;2;92;156;245m\u001b[48;2;30;30;30mBuild\u001b[0m\u001b[29;44H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[29;45H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30m·\u001b[0m\u001b[29;46H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[29;47H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mMiMo V2.5 Free\u001b[0m\u001b[29;62H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mOpenCode Zen\u001b[0m\u001b[30;36H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m╹\u001b[0m\u001b[30;37H\u001b[38;2;30;30;30m\u001b[48;2;10;10;10m▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\u001b[0m\u001b[31;36H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[31;84H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mtab \u001b[0m\u001b[31;88H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10magents\u001b[0m\u001b[31;94H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[31;96H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mctrl+p \u001b[0m\u001b[31;103H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mcommands\u001b[0m\u001b[32;36H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[33;84H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[33;96H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[35;36H\u001b[38;2;245;167;66m\u001b[48;2;10;10;10m● Tip \u001b[0m\u001b[35;42H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mRun \u001b[0m\u001b[35;46H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mdocker run -it --rm ghcr.io/anomalyco/opencode\u001b[0m\u001b[35;92H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m for containerized \u001b[0m\u001b[36;42H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10muse\u001b[0m\u001b[37;36H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[38;42H\u001b[38;2;25"] -[12.352392383,"o","5;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[0m\u001b[27;39H\u001b[?25h\u001b[?2026l"] -[12.426528682,"o","\u001b]0;OpenCode\u0007"] -[12.4428589,"o","\u001b[?2026h\u001b[?25l\u001b[1;3H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[1;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[1;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[2;3H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[2;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[2;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mtes\" demonstrating all capabilities: Mermaid sequence diagra, Graphviz architecture digraph, \u001b[0m\u001b[2;99H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[2;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[2;105H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m\u001b[1mNew session - 2026-07-07T13:55:09.\u001b[0m\u001b[2;139H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[2;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[2;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[3;3H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[3;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[3;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mD2 pipeline diagram, math equations with cross-refs, BibTeX citations, tikz diagram, examples \u001b[0m\u001b[3;100H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[3;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[3;105H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m\u001b[1m012Z\u001b[0m\u001b[3;109H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[3;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[3;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[4;3H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[4;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[4;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mof code listings, two booktabs tables, table of contents, and lists. Compile with texforge \u001b[0m\u001b[4;97H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[4;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[4;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[4;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[5;3H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[5;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[5;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mbuild.\u001b[0m\u001b[5;12H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[5;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[5;105H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m\u001b[1mContext\u001b[0m\u001b[5;112H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[5;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[5;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[6;3H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[6;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[6;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[6;105H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m0 tokens\u001b[0m\u001b[6;113H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[6;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[6;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[7;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[7;105H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m0% used\u001b[0m\u001b[7;112H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[7;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[7;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[8;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[8;8H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[8;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[8;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[8;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[8;105H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m$0.00 spent\u001b[0m\u001b[8;116H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[8;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[8;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[9;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[9;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[9;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[10;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[10;105H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m▼\u001b[0m\u001b[10;106H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[10;107H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m\u001b[1mMCP\u001b[0m\u001b[10;110H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[10"] -[12.442885127,"o",";142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[10;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[11;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[11;105H\u001b[38;2;127;216;143m\u001b[48;2;20;20;20m•\u001b[0m\u001b[11;106H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[11;107H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mcanopy \u001b[0m\u001b[11;114H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mConnected\u001b[0m\u001b[11;123H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[11;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[11;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[12;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[12;105H\u001b[38;2;127;216;143m\u001b[48;2;20;20;20m•\u001b[0m\u001b[12;106H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[12;107H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mfetch \u001b[0m\u001b[12;113H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mConnected\u001b[0m\u001b[12;122H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[12;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[12;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[13;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[13;105H\u001b[38;2;127;216;143m\u001b[48;2;20;20;20m•\u001b[0m\u001b[13;106H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[13;107H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mfilesystem \u001b[0m\u001b[13;118H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mConnected\u001b[0m\u001b[13;127H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[13;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[13;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[14;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[14;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[14;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[15;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[15;105H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m\u001b[1mLSP\u001b[0m\u001b[15;108H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[15;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[15;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[16;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[16;105H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mLSPs are disabled\u001b[0m\u001b[16;122H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[16;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[16;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[17;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[17;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[17;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[18;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[18;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[18;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[19;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[19;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[19;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[20;54H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;74H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[20;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[20;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[21;54H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[21;74H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[21;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[21;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[21;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[22;54H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[22;74H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[22;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[22;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[22;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[23;54H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[23;74H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[23;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[23;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[23;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[24;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[24;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[24;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[25;103H\u001b[38;2;255;25"] -[12.442891435,"o","5;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[25;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[25;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[26;36H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[26;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[26;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[27;36H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[27;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[27;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[28;36H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[28;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[28;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[29;36H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[29;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[29;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[30;36H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[30;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[30;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[31;84H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[31;96H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[31;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[31;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[32;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[32;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[33;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[33;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[34;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[34;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[35;36H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[35;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[35;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[36;42H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[36;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[36;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[37;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[37;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[38;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[38;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[39;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[39;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[40;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[40;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[41;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[41;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[42;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[42;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[43;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[43;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[44;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[44;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[45;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[45;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[46;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[46;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[47;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[47;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[48;3H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[48;4H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m "] -[12.442901199,"o"," \u001b[0m\u001b[48;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[48;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[49;3H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[49;4H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[49;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[49;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[50;3H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[50;4H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[50;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[51;3H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[51;4H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[51;6H\u001b[38;2;92;156;245m\u001b[48;2;30;30;30mBuild\u001b[0m\u001b[51;11H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[51;12H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30m·\u001b[0m\u001b[51;13H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[51;14H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mMiMo V2.5 Free\u001b[0m\u001b[51;28H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[51;29H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mOpenCode Zen\u001b[0m\u001b[51;41H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[51;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[51;105H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m/\u001b[0m\u001b[51;106H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m~\u001b[0m\u001b[51;107H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[52;3H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m╹\u001b[0m\u001b[52;4H\u001b[38;2;30;30;30m\u001b[48;2;10;10;10m▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\u001b[0m\u001b[52;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[53;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[53;4H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;12H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[53;14H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mesc \u001b[0m\u001b[53;18H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10minterrupt\u001b[0m\u001b[53;74H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mtab \u001b[0m\u001b[53;78H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10magents\u001b[0m\u001b[53;86H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mctrl+p \u001b[0m\u001b[53;93H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mcommands\u001b[0m\u001b[53;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[53;105H\u001b[38;2;127;216;143m\u001b[48;2;20;20;20m•\u001b[0m\u001b[53;106H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m \u001b[0m\u001b[53;107H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m\u001b[1mOpen\u001b[0m\u001b[53;111H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m\u001b[1mCode\u001b[0m\u001b[53;115H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m 1.15.13\u001b[0m\u001b[53;123H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[54;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[12.457242478,"o","\u001b[?2026h\u001b[?25l\u001b[1;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[2;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[2;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[3;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mtes\" d\u001b[0m\u001b[3;13H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mmo\u001b[0m\u001b[3;16H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mstrating all capabilitie\u001b[0m\u001b[3;41H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m: Mermaid \u001b[0m\u001b[3;52H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mequence diagra, Graphviz\u001b[0m\u001b[3;77H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20marchitecture digra\u001b[0m\u001b[3;96H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mh, \u001b[0m\u001b[3;99H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[3;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[4;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mD2\u001b[0m\u001b[4;9H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mpip\u001b[0m\u001b[4;13H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mline diagram, math equations with cross-refs, BibTeX citations, tikz diagram, examples \u001b[0m\u001b[4;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[5;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mof code listings, two booktabs tables, table of contents, and lists. Compile with texforge \u001b[0m\u001b[5;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[6;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mbuild.\u001b[0m\u001b[6;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[7;3H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[7;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[7;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[8;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[8;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[9;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[9;8H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[9;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[9;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[9;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[10;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[11;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[12;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[13;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[14;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[15;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[16;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[17;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[18;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[19;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[20;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[21;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[22;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[23;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[24;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[25;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[26;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[27;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[28;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[29;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[30;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[31;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[32;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[33;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[34;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[35;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[36;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[37;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[38;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[39;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[40;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[41;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[42;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[43;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[44;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[45;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[46;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[47;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[48;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[49;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[12.473241113,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;30;45;66m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[12.48725357,"o","\u001b]0;OpenCode\u0007"] -[12.490250037,"o","\u001b[?2026h\u001b[?25l\u001b[9;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[9;8H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[9;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[9;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[12.507061996000001,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;35;54;81m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[12.542465366,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;40;63;95m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[12.585375627,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;44;71;109m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[12.628192228,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;49;80;122m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[12.671211911,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;54;89;137m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;54;89;137m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[12.71395133,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;59;98;151m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[53;6H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[12.799934853,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;55;91;140m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[53;7H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[12.843582272999999,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;51;84;129m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[53;8H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[12.887037093,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[53;9H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[12.929417312,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;44;70;107m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;10H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[12.972444272,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;40;64;97m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;11H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[13.016362246,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[13.059513435,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[13.103005031,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;29;43;63m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[13.145809795,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;10H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[13.186759852,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;31;47;69m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[53;9H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[13.226713469,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[53;8H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[13.270196825,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;42;67;101m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[53;7H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[13.312494332,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[53;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[13.356273159,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[53;5H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[13.401049402,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;59;98;151m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[13.488239676,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;58;95;147m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[13.532239821,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;57;94;145m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[13.575963576,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;56;91;141m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[13.619855821,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;55;90;138m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[13.663964949,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[13.707053613,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;52;86;132m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[13.750591383,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;51;83;128m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[13.793760667,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;50;81;124m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[13.837075681,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;49;79;122m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[13.881146837,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[13.924924337,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;47;75;115m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[13.967561241,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;73;111m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[14.010819926,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;44;71;109m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[14.053797086,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;43;69;105m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[14.096848591,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;42;67;101m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[14.139511595,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;41;65;98m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[14.182589011,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;40;63;95m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[14.22558942,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;39;61;92m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[14.269059676,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;37;59;88m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[14.311701263,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[14.354915362,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;35;55;82m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[14.397988468,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;34;52;78m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[14.437942611,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;51;75m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[14.480944758,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;32;48;72m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[14.524101404,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;31;47;69m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[14.567071273,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;29;44;65m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[14.609579826000001,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;28;43;63m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[14.652791241,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;27;40;59m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[14.695532608,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;26;38;55m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[14.738537088,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[14.7819413,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;30;45;66m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[14.825415693,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;35;54;81m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[14.868243142,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;40;63;95m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[14.911912682,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;44;71;109m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[14.956206795,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;49;80;122m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[15.000181961,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;54;89;137m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;54;89;137m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[15.044478881,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;59;98;151m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[53;6H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[15.131367818,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;55;91;140m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[53;7H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[15.174494309,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;51;84;129m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[53;8H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[15.218194811,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[53;9H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[15.261446149,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;44;70;107m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;10H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[15.304304845,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;40;64;97m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;11H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[15.347664057,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[15.391049706,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[15.434447691,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;29;43;63m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[15.478163423,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;10H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[15.521028798,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;31;47;69m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[53;9H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[15.564682233,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[53;8H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[15.6081406,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;42;67;101m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[53;7H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[15.651449313,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[53;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[15.694760269,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[53;5H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[15.737926627,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;59;98;151m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[15.82475647,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;58;95;147m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[15.867761273,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;57;94;145m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[15.910880274,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;56;91;141m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[15.953605806,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;55;90;138m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.00067844,"o","\u001b[?2026h\u001b[?25l\u001b[9;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠋\u001b[0m\u001b[9;7H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[9;8H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThinking\u001b[0m\u001b[9;16H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[13;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[13;8H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[13;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[13;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.022637747,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.056684595,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;52;86;132m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.07817464,"o","\u001b[?2026h\u001b[?25l\u001b[9;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠙\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.100068677,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;51;83;128m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.120890694,"o","\u001b[?2026h\u001b[?25l\u001b[11;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mThe user wants a LaTeX document demonstrating various\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.137131215,"o","\u001b[?2026h\u001b[?25l\u001b[11;59H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m capabilities.\u001b[0m\u001b[53;4H\u001b[38;2;50;81;124m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.153223488,"o","\u001b[?2026h\u001b[?25l\u001b[11;73H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m Let me first load\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.169792535,"o","\u001b[?2026h\u001b[?25l\u001b[9;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠹\u001b[0m\u001b[53;4H\u001b[38;2;49;79;122m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.186022877,"o","\u001b[?2026h\u001b[?25l\u001b[11;91H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m the \u001b[0m\u001b[12;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mtexforge skill\u001b[0m\u001b[13;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[14;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[14;8H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[14;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[14;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.203195054,"o","\u001b[?2026h\u001b[?25l\u001b[12;20H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m to understand how to\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.218910401,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.241503858,"o","\u001b[?2026h\u001b[?25l\u001b[9;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠸\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.262272319,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;47;75;115m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.29915741,"o","\u001b[?2026h\u001b[?25l\u001b[12;41H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m use texforge properly.\u001b[0m\u001b[53;4H\u001b[38;2;45;73;111m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.324855853,"o","\u001b[?2026h\u001b[?25l\u001b[9;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠼\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.340637828,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;44;71;109m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.365355689,"o","\u001b[?2026h\u001b[?25l\u001b[9;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 385ms\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.382124128,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;43;69;105m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.400951983,"o","\u001b[?2026h\u001b[?25l\u001b[13;6H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m~ Loading skill...\u001b[0m\u001b[14;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[15;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[15;8H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[15;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[15;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.417813623,"o","\u001b[?2026h\u001b[?25l\u001b[13;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[14;6H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m~ Loading skill...\u001b[0m\u001b[15;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[16;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[16;8H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[16;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[16;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.433285035,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;42;67;101m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.449291178,"o","\u001b[?2026h\u001b[?25l\u001b[14;6H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m→\u001b[0m\u001b[14;7H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[14;8H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mSkill \"texforge\"\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.466795586,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;41;65;98m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.510038633,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;40;63;95m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.553748119,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;39;61;92m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.581435399,"o","\u001b[?2026h\u001b[?25l\u001b[6;105H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m26,590 tokens\u001b[0m\u001b[7;105H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m13% used\u001b[0m\u001b[16;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[16;8H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[16;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[16;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[53;73H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m26.6K (13%)\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.598344168,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;37;59;88m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.634661586,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.676968898,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;35;55;82m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.72139269,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;34;52;78m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.763423985,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;51;75m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.807489451,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;32;48;72m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.850619514,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;31;47;69m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.893551097,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;29;44;65m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.936025338,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;28;43;63m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[16.97929939,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;27;40;59m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[17.022834511,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;26;38;55m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[17.066344224,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[17.109695021,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;30;45;66m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[17.153176092,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;35;54;81m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[17.19592304,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;40;63;95m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[17.239203684,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;44;71;109m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[17.282417628,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;49;80;122m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[17.326176067,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;54;89;137m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;54;89;137m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[17.369300512,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;59;98;151m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[53;6H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[17.458649135,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;55;91;140m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[53;7H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[17.501760344,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;51;84;129m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[53;8H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[17.54506395,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[53;9H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[17.588373627,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;44;70;107m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;10H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[17.632098399,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;40;64;97m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;11H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[17.67528966,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[17.71826991,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[17.761867585,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;29;43;63m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[17.804987612,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;10H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[17.848344399,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;31;47;69m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[53;9H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[17.89177466,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[53;8H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[17.935073564,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;42;67;101m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[53;7H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[17.978201173,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[53;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[18.021762378,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[53;5H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[18.064546557,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;59;98;151m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[18.15113521,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;58;95;147m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[18.193179707,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;57;94;145m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[18.236237291,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;56;91;141m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[18.279723928,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;55;90;138m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[18.322269604,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[18.365882907,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;52;86;132m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[18.408261616,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;51;83;128m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[18.448356237,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;50;81;124m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[18.490796144,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;49;79;122m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[18.534378044,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[18.577761385,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;47;75;115m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[18.621464608,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;73;111m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[18.663938595,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;44;71;109m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[18.706964927,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;43;69;105m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[18.750281797,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;42;67;101m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[18.792736255,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;41;65;98m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[18.83609189,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;40;63;95m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[18.878998602,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;39;61;92m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[18.923165617,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;37;59;88m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[18.966711371,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[19.013397206,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;35;55;82m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[19.057524887,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;34;52;78m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[19.10122393,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;51;75m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[19.145183376,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;32;48;72m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[19.188700677,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;31;47;69m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[19.232749925,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;29;44;65m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[19.276360925,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;28;43;63m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[19.320218882,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;27;40;59m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[19.363818681,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;26;38;55m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[19.407849056,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[19.451403585,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;30;45;66m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[19.495265093,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;35;54;81m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[19.538284859,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;40;63;95m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[19.58164763,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;44;71;109m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[19.624326013,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;49;80;122m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[19.66759953,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;54;89;137m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;54;89;137m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[19.710830933,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;59;98;151m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[53;6H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[19.795403154,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;55;91;140m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[53;7H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[19.838871702,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;51;84;129m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[53;8H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[19.881260628,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[53;9H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[19.923995252,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;44;70;107m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;10H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[19.967322626,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;40;64;97m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;11H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[20.00967514,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[20.051812792,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[20.094499522,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;29;43;63m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[20.137572553,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;10H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[20.180671721,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;31;47;69m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[53;9H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[20.225147036,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[53;8H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[20.268099802,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;42;67;101m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[53;7H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[20.310895919,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[53;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[20.353884183,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[53;5H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[20.39705081,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;59;98;151m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[20.483748817,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;58;95;147m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[20.52786247,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;57;94;145m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[20.571985194,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;56;91;141m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[20.616144499,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;55;90;138m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[20.659517086,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[20.703306326,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;52;86;132m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[20.745761221,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;51;83;128m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[20.788962926,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;50;81;124m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[20.832394066,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;49;79;122m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[20.875197744,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[20.918518467,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;47;75;115m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[20.961101978,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;73;111m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.00476656,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;44;71;109m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.047616703,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;43;69;105m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.06988877,"o","\u001b[?2026h\u001b[?25l\u001b[16;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠋\u001b[0m\u001b[16;7H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[16;8H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThinking\u001b[0m\u001b[16;16H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[20;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[20;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.091337406,"o","\u001b[?2026h\u001b[?25l\u001b[18;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mNow I understand tex\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.107423681,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;42;67;101m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.12389502,"o","\u001b[?2026h\u001b[?25l\u001b[18;26H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mforge.\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.141104558,"o","\u001b[?2026h\u001b[?25l\u001b[18;32H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m The\u001b[0m\u001b[53;4H\u001b[38;2;41;65;98m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.156612785,"o","\u001b[?2026h\u001b[?25l\u001b[16;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠙\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.176475057,"o","\u001b[?2026h\u001b[?25l\u001b[18;36H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m user wants a comprehensive\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.192275495,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;40;63;95m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.209444948,"o","\u001b[?2026h\u001b[?25l\u001b[18;63H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m LaTeX document demonstrating\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.225253261,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;39;61;92m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.24165003,"o","\u001b[?2026h\u001b[?25l\u001b[16;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠹\u001b[0m\u001b[18;92H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m many\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.25756951,"o","\u001b[?2026h\u001b[?25l\u001b[18;97H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mcapabilities. Let me\u001b[0m\u001b[20;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;9H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[21;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[21;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[21;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[53;4H\u001b[38;2;37;59;88m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.290714433,"o","\u001b[?2026h\u001b[?25l\u001b[19;26H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m create a project\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.307486924,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.323852331,"o","\u001b[?2026h\u001b[?25l\u001b[16;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠸\u001b[0m\u001b[19;43H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m and\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.340483741,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;35;55;82m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.383529255,"o","\u001b[?2026h\u001b[?25l\u001b[19;47H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m write\u001b[0m\u001b[53;4H\u001b[38;2;34;52;78m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.399521359,"o","\u001b[?2026h\u001b[?25l\u001b[16;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠼\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.417531435,"o","\u001b[?2026h\u001b[?25l\u001b[19;53H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m the document.\u001b[0m\u001b[21;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mLet me create\u001b[0m\u001b[21;19H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[23;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[23;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[23;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.433348811,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;51;75m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.466574218,"o","\u001b[?2026h\u001b[?25l\u001b[21;19H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m a\u001b[0m\u001b[53;4H\u001b[38;2;32;48;72m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.482333043,"o","\u001b[?2026h\u001b[?25l\u001b[16;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠴\u001b[0m\u001b[21;21H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m new tex\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.515367185,"o","\u001b[?2026h\u001b[?25l\u001b[21;29H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mforge project,\u001b[0m\u001b[53;4H\u001b[38;2;31;47;69m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.531142903,"o","\u001b[?2026h\u001b[?25l\u001b[21;43H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m then write the main\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.547229203,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;29;44;65m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.563761302,"o","\u001b[?2026h\u001b[?25l\u001b[16;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠦\u001b[0m\u001b[21;63H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m.tex with all the\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.587589483,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;28;43;63m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.639944073,"o","\u001b[?2026h\u001b[?25l\u001b[16;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠧\u001b[0m\u001b[21;80H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m requested features.\u001b[0m\u001b[53;4H\u001b[38;2;27;40;59m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.677736091,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;26;38;55m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.721739663,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.737654007,"o","\u001b[?2026h\u001b[?25l\u001b[16;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 672ms\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.75501103,"o","\u001b[?2026h\u001b[?25l\u001b[22;6H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m~ Updating todos...\u001b[0m\u001b[23;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[23;9H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[24;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[24;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[24;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.771894774,"o","\u001b[?2026h\u001b[?25l\u001b[22;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[23;6H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m~ Updating todos...\u001b[0m\u001b[24;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[24;9H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[25;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[25;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[25;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[53;4H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;30;45;66m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.807007761,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;35;54;81m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.850813982,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;40;63;95m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.89519037,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;44;71;109m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.938909831,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;49;80;122m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[21.983696112,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;54;89;137m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;54;89;137m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[22.026233171,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;59;98;151m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[53;6H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[22.11396016,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;55;91;140m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[53;7H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[22.157700792,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;51;84;129m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[53;8H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[22.201432861,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[53;9H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[22.245097566,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;44;70;107m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;10H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[22.288854094,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;40;64;97m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;11H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[22.331494133,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[22.375167493,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[22.418976636,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;29;43;63m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[22.459043652,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;10H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[22.502488147,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;31;47;69m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[53;9H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[22.545534321,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[53;8H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[22.59008395,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;42;67;101m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[53;7H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[22.633795659,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[53;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[22.677495639,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[53;5H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[22.720604562,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;59;98;151m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[22.737587908,"o","\u001b[?2026h\u001b[?25l\u001b[18;105H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m▼\u001b[0m\u001b[18;107H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m\u001b[1mTodo\u001b[0m\u001b[19;105H\u001b[38;2;245;167;66m\u001b[48;2;20;20;20m[•] \u001b[0m\u001b[19;109H\u001b[38;2;245;167;66m\u001b[48;2;20;20;20mCreate texforge project\u001b[0m\u001b[20;105H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] \u001b[0m\u001b[20;109H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mWrite main.tex with all \u001b[0m\u001b[21;109H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mrequested features\u001b[0m\u001b[22;105H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] \u001b[0m\u001b[22;109H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mCreate bib file with example \u001b[0m\u001b[23;109H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mentries\u001b[0m\u001b[24;105H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] Create sections/body.tex\u001b[0m\u001b[25;105H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] Run texforge build\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[22.754001082,"o","\u001b[?2026h\u001b[?25l\u001b[23;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[23;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[24;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[24;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[24;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m# Todos\u001b[0m\u001b[24;13H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[25;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[25;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[26;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[26;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[26;6H\u001b[38;2;245;167;66m\u001b[48;2;20;20;20m[•] \u001b[0m\u001b[26;10H\u001b[38;2;245;167;66m\u001b[48;2;20;20;20mCreate texforge project\u001b[0m\u001b[26;33H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[27;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[27;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[27;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] \u001b[0m\u001b[27;10H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mWrite main.tex with all requested features\u001b[0m\u001b[27;52H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[28;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[28;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[28;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] Create bib file with example entries\u001b[0m\u001b[28;46H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[29;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[29;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[29;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] \u001b[0m\u001b[29;10H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mCreate sections/body.tex\u001b[0m\u001b[29;34H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[30;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[30;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[30;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] \u001b[0m\u001b[30;10H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mRun texforge build\u001b[0m\u001b[30;28H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[31;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[31;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[33;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[33;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[33;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[22.797093494,"o","\u001b[?2026h\u001b[?25l\u001b[6;106H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m8\u001b[0m\u001b[6;109H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m46\u001b[0m\u001b[7;106H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m4\u001b[0m\u001b[53;74H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m8\u001b[0m\u001b[53;76H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m5\u001b[0m\u001b[53;81H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m4\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[22.820293731,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;58;95;147m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[22.845162496,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;57;94;145m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[22.889506016,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;56;91;141m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[22.931921647,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;55;90;138m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[22.974304609,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[23.018926654,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;52;86;132m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[23.062068743,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;51;83;128m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[23.10567553,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;50;81;124m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[23.149509748,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;49;79;122m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[23.193405264,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[23.236980491,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;47;75;115m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[23.280367816,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;73;111m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[23.323317493,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;44;71;109m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[23.366930977,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;43;69;105m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[23.40953621,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;42;67;101m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[23.453390345,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;41;65;98m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[23.497313996,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;40;63;95m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[23.540408747,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;39;61;92m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[23.583237436,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;37;59;88m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[23.626645833,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[23.670155815,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;35;55;82m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[23.713207666,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;34;52;78m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[23.75618647,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;51;75m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[23.801287455,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;32;48;72m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[23.844062437,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;31;47;69m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[23.887420021,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;29;44;65m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[23.930472038,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;28;43;63m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[23.973019928,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;27;40;59m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[24.016289417,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;26;38;55m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[24.058782834,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[24.102770539,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;30;45;66m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[24.145561212,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;35;54;81m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[24.189155963,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;40;63;95m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[24.232520123,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;44;71;109m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[24.27678901,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;49;80;122m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[24.319473294,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;54;89;137m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;54;89;137m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[24.36283215,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;59;98;151m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[53;6H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[24.450136946,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;55;91;140m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[53;7H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[24.49350428,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;51;84;129m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[53;8H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[24.536782222,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[53;9H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[24.579622307,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;44;70;107m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;10H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[24.623708628,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;40;64;97m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;11H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[24.667806727,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[24.710902068,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[24.754104248,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;29;43;63m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[24.796949846,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;10H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[24.841436306,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;31;47;69m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[53;9H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[24.886067363,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[53;8H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[24.929419505,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;42;67;101m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[53;7H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[24.971628347,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[53;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.011740726,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[53;5H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.031589792,"o","\u001b[?2026h\u001b[?25l\u001b[33;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠋\u001b[0m\u001b[33;7H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[33;8H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThinking\u001b[0m\u001b[33;16H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[37;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[37;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[37;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.052267025,"o","\u001b[?2026h\u001b[?25l\u001b[35;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mThe\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.072768058,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;59;98;151m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.105008541,"o","\u001b[?2026h\u001b[?25l\u001b[35;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m user wants a comprehensive LaTeX document demonstrating\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.121076673,"o","\u001b[?2026h\u001b[?25l\u001b[33;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠙\u001b[0m\u001b[35;65H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m various\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.137235218,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;58;95;147m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.153467785,"o","\u001b[?2026h\u001b[?25l\u001b[35;73H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m capabilities. Let me\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.185466817,"o","\u001b[?2026h\u001b[?25l\u001b[35;94H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m \u001b[0m\u001b[36;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mcreate the\u001b[0m\u001b[37;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[37;9H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[38;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[38;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[38;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[53;4H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;57;94;145m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.202681867,"o","\u001b[?2026h\u001b[?25l\u001b[33;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠹\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.219221532,"o","\u001b[?2026h\u001b[?25l\u001b[36;16H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m tex\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.235234414,"o","\u001b[?2026h\u001b[?25l\u001b[36;20H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mforge project first.\u001b[0m\u001b[53;4H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;56;91;141m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.261491572,"o","\u001b[?2026h\u001b[?25l\u001b[33;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 1.1s\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.27834678,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;55;90;138m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.297023766,"o","\u001b[?2026h\u001b[?25l\u001b[37;6H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m~ Writing command...\u001b[0m\u001b[38;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[38;9H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[39;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[39;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[39;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.313889591,"o","\u001b[?2026h\u001b[?25l\u001b[37;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[38;6H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m~ Writing command...\u001b[0m\u001b[39;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[39;9H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[40;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[40;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[40;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.357082846,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;52;86;132m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.400957407,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;51;83;128m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.444654523,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;50;81;124m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.490317048,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;49;79;122m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.533223276,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.576621447,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;47;75;115m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.599721796,"o","\u001b[?2026h\u001b[?25l\u001b[38;6H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m$\u001b[0m\u001b[38;7H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[38;8H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mtexforge new tes-demo\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.641582888,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;73;111m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.658451871,"o","\u001b[?2026h\u001b[?25l\u001b[38;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[38;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[39;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[39;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[39;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m⠋\u001b[0m\u001b[39;7H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[39;8H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mCreate new texforge project\u001b[0m\u001b[39;35H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[40;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[40;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[41;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[41;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[41;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m$ texforge new tes-demo\u001b[0m\u001b[41;29H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[42;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[42;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[44;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[44;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[44;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.675902552,"o","\u001b[?2026h\u001b[?25l\u001b[1;3H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[1;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[1;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mof code listings, two booktabs tables, table of contents, and lists. Compile with texforge \u001b[0m\u001b[1;97H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[2;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mbuild.\u001b[0m\u001b[3;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[4;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[5;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[5;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 385ms\u001b[0m\u001b[5;20H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[6;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[7;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[7;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mThe user wants a LaTeX document demonstrating various capabilities. Let me first load the \u001b[0m\u001b[7;96H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[8;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mtexforge skill to understand how to use texforge properly.\u001b[0m\u001b[9;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[10;6H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m→\u001b[0m\u001b[10;8H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mSkill \"texforge\"\u001b[0m\u001b[11;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[12;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 672ms\u001b[0m\u001b[12;20H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[14;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mNow I understand texforge. The user wants a comprehensive LaTeX document demonstrating many \u001b[0m\u001b[15;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mcapabilities. Let me create a project and write the document.\u001b[0m\u001b[16;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[17;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mLet me create a new texforge project, then write the main.tex with all the requested features.\u001b[0m\u001b[18;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[19;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[20;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[20;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[20;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m# Todos\u001b[0m\u001b[20;13H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[21;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[21;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[22;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[22;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[22;6H\u001b[38;2;245;167;66m\u001b[48;2;20;20;20m[•] \u001b[0m\u001b[22;10H\u001b[38;2;245;167;66m\u001b[48;2;20;20;20mCreate texforge project\u001b[0m\u001b[22;33H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[23;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] \u001b[0m\u001b[23;10H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mWrite main.tex with all requested features\u001b[0m\u001b[24;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[\u001b[0m\u001b[24;8H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m] Create bib file with example entries\u001b[0m\u001b[25;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] \u001b[0m\u001b[25;10H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mCreate sections/body.tex\u001b[0m\u001b[26;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] \u001b[0m\u001b[26;10H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mRun texforge build\u001b[0m\u001b[26;28H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[27;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[28;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m "] -[25.675919815,"o"," \u001b[0m\u001b[29;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[29;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 1.1s\u001b[0m\u001b[29;19H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[30;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[31;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[31;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mThe user wants a comprehensive LaTeX document demonstrating various capabilities. Let me \u001b[0m\u001b[31;95H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[32;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mcreate the texforge project first.\u001b[0m\u001b[33;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[34;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[34;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[35;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[35;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[35;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m# Create new texforge project\u001b[0m\u001b[35;35H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[36;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[36;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[37;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[37;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[37;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m$ texforge new tes-demo\u001b[0m\u001b[37;29H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[39;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mCreating project 'tes-demo' with template 'general'...\u001b[0m\u001b[40;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m ◇ Project 'tes-demo' created successfully\u001b[0m\u001b[41;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[42;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m cd tes-demo\u001b[0m\u001b[43;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[43;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[43;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m texforge build\u001b[0m\u001b[43;22H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[44;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[44;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[46;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[46;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[46;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[53;4H\u001b[38;2;44;71;109m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.693056607,"o","\u001b[?2026h\u001b[?25l\u001b[6;108H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m764\u001b[0m\u001b[53;76H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m8\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.71203792,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;43;69;105m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.755568079,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;42;67;101m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.800594346,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;41;65;98m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.843918447,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;40;63;95m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.887342733,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;39;61;92m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.930964077,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;37;59;88m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[25.974225518,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[26.017195533,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;35;55;82m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[26.060332452,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;34;52;78m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[26.10363996,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;51;75m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[26.147509326,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;32;48;72m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[26.190281432,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;31;47;69m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[26.233551366,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;29;44;65m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[26.277704008,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;28;43;63m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[26.321301465,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;27;40;59m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[26.364990384,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;26;38;55m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[26.40912582,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[26.453085261,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;30;45;66m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[26.495325846,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;35;54;81m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[26.53893023,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;40;63;95m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[26.582332742,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;44;71;109m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[26.625338037,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;49;80;122m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[26.668673486,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;54;89;137m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;54;89;137m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[26.712149629,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;59;98;151m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[53;6H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[26.799653695,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;55;91;140m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[53;7H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[26.842658951,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;51;84;129m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[53;8H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[26.885574297,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[53;9H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[26.928735055,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;44;70;107m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;10H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[26.971954717,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;40;64;97m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;11H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[27.015173672,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[27.058605398,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[27.102217988,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;29;43;63m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[27.145960916,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;10H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[27.18949666,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;31;47;69m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[53;9H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[27.23288056,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[53;8H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[27.27611958,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;42;67;101m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[53;7H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[27.319563828,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[53;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[27.363063222,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[53;5H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[27.406090421,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;59;98;151m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] -[27.496891064,"o","\u001b[?2026h\u001b[?25l\u001b[38;3H\u001b[38;2;72;72;72m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[38;4H\u001b[38;2;255;255;255m\u001b[48;2;250;178;131m \u001b[0m\u001b[38;5H\u001b[38;2;10;10;10m\u001b[48;2;250;178;131m/agents \u001b[0m\u001b[38;23H\u001b[38;2;10;10;10m\u001b[48;2;250;178;131mSwitch agent\u001b[0m\u001b[38;35H\u001b[38;2;255;255;255m\u001b[48;2;250;178;131m \u001b[0m\u001b[38;100H\u001b[38;2;72;72;72m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[39;3H\u001b[38;2;72;72;72m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[39;4H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[39;5H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m/compact \u001b[0m\u001b[39;23H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mCompact session\u001b[0m\u001b[39;38H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[39;100H\u001b[38;2;72;72;72m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[40;3H\u001b[38;2;72;72;72m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[40;4H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[40;5H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m/connect \u001b[0m\u001b[40;23H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mConnect provider\u001b[0m\u001b[40;39H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[40;100H\u001b[38;2;72;72;72m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[41;3H\u001b[38;2;72;72;72m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[41;4H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[41;5H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m/copy \u001b[0m\u001b[41;23H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mCopy session transcript\u001b[0m\u001b[41;46H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[41;100H\u001b[38;2;72;72;72m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[42;3H\u001b[38;2;72;72;72m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[42;4H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[42;5H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m/diff \u001b[0m\u001b[42;23H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mOpen diff viewer\u001b[0m\u001b[42;39H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[42;100H\u001b[38;2;72;72;72m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[43;3H\u001b[38;2;72;72;72m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[43;4H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[43;5H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m/editor \u001b[0m\u001b[43;23H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mOpen editor\u001b[0m\u001b[43;34H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[43;100H\u001b[38;2;72;72;72m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[44;3H\u001b[38;2;72;72;72m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[44;4H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[44;5H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m/exit \u001b[0m\u001b[44;23H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mExit the app\u001b[0m\u001b[44;35H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[44;100H\u001b[38;2;72;72;72m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[45;3H\u001b[38;2;72;72;72m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[45;4H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[45;5H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m/export \u001b[0m\u001b[45;23H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mExport session transcript\u001b[0m\u001b[45;48H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[45;100H\u001b[38;2;72;72;72m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[46;3H\u001b[38;2;72;72;72m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[46;4H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[46;5H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m/fetch:fetch:mcp \u001b[0m\u001b[46;23H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mFetch a URL and extract its contents as markdown\u001b[0m\u001b[46;71H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[46;100H\u001b[38;2;72;72;72m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[47;3H\u001b[38;2;72;72;72m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[47;4H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[47;5H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m/fork \u001b[0m\u001b[47;23H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mFork session\u001b[0m\u001b[47;35H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[47;100H\u001b[38;2;72;72;72m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[49;6H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m/\u001b[0m\u001b[0m\u001b[49;7H\u001b[?25h\u001b[?2026l"] -[27.512319111,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;58;95;147m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;7H\u001b[?25h\u001b[?2026l"] -[27.541596243,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;57;94;145m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;7H\u001b[?25h\u001b[?2026l"] -[27.563606404,"o","\u001b[?2026h\u001b[?25l\u001b[1;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[1;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 385ms\u001b[0m\u001b[1;20H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[2;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[3;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[3;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mThe user wants a LaTeX document demonstrating various capabilities. Let me first load the \u001b[0m\u001b[3;96H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[4;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mtexforge skill to understand how to use texforge properly.\u001b[0m\u001b[5;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[6;6H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m→\u001b[0m\u001b[6;8H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mSkill \"texforge\"\u001b[0m\u001b[7;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[8;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 672ms\u001b[0m\u001b[8;20H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[10;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mNow I understand texforge. The user wants a comprehensive LaTeX document demonstrating many \u001b[0m\u001b[11;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mcapabilities. Let me create a project and write the document.\u001b[0m\u001b[12;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[13;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mLet me create a new texforge project, then write the main.tex with all the requested features.\u001b[0m\u001b[14;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[15;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[15;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[16;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[16;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[16;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m# Todos\u001b[0m\u001b[16;13H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[17;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[17;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[18;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[18;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[18;6H\u001b[38;2;245;167;66m\u001b[48;2;20;20;20m[•] \u001b[0m\u001b[18;10H\u001b[38;2;245;167;66m\u001b[48;2;20;20;20mCreate texforge project\u001b[0m\u001b[18;33H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[19;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] \u001b[0m\u001b[19;10H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mWrite main.tex with all requested features\u001b[0m\u001b[20;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[\u001b[0m\u001b[20;8H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m] Create bib file with example entries\u001b[0m\u001b[21;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] \u001b[0m\u001b[21;10H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mCreate sections/body.tex\u001b[0m\u001b[22;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] \u001b[0m\u001b[22;10H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mRun texforge build\u001b[0m\u001b[22;28H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[23;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[24;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[25;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[25;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 1.1s\u001b[0m\u001b[25;19H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[26;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[27;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[27;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mThe user wants a comprehensive LaTeX document demonstrating various capabilities"] -[27.563620299,"o",". Let me \u001b[0m\u001b[27;95H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[28;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mcreate the texforge project first.\u001b[0m\u001b[29;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[30;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[30;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[31;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[31;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[31;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m# Create new texforge project\u001b[0m\u001b[31;35H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[32;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[32;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[33;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[33;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[33;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m$ texforge new tes-demo\u001b[0m\u001b[33;29H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[35;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mCreating project 'tes-demo' with template 'general'...\u001b[0m\u001b[36;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m ◇ Project 'tes-demo' created successfully\u001b[0m\u001b[37;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[41;3H\u001b[38;2;72;72;72m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[41;100H\u001b[38;2;72;72;72m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[42;3H\u001b[38;2;72;72;72m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[42;100H\u001b[38;2;72;72;72m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[43;3H\u001b[38;2;72;72;72m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[43;100H\u001b[38;2;72;72;72m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[44;3H\u001b[38;2;72;72;72m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[44;100H\u001b[38;2;72;72;72m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[0m\u001b[49;7H\u001b[?25h\u001b[?2026l"] -[27.580630854,"o","\u001b[?2026h\u001b[?25l\u001b[38;6H\u001b[38;2;10;10;10m\u001b[48;2;250;178;131mexit \u001b[0m\u001b[38;23H\u001b[38;2;10;10;10m\u001b[48;2;250;178;131mEx\u001b[0m\u001b[38;27H\u001b[38;2;10;10;10m\u001b[48;2;250;178;131m the app\u001b[0m\u001b[39;6H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30meditor \u001b[0m\u001b[39;23H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mOpen editor\u001b[0m\u001b[39;34H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[40;6H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mexport \u001b[0m\u001b[40;23H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mExport session transcript\u001b[0m\u001b[41;6H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mfetch:fetch:mcp\u001b[0m\u001b[41;23H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mFetch a URL and ext\u001b[0m\u001b[41;43H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mac\u001b[0m\u001b[41;46H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30m its contents as markdown\u001b[0m\u001b[42;6H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mhelp\u001b[0m\u001b[42;23H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mHelp\u001b[0m\u001b[42;27H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[43;6H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mnew \u001b[0m\u001b[43;23H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mNew s\u001b[0m\u001b[43;29H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mssi\u001b[0m\u001b[43;33H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mn\u001b[0m\u001b[44;6H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mrename\u001b[0m\u001b[44;23H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mRename session\u001b[0m\u001b[45;6H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mreview\u001b[0m\u001b[45;23H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mreview\u001b[0m\u001b[45;30H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mchanges\u001b[0m\u001b[45;38H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30m[commit|branch|pr], defaults to uncommitted\u001b[0m\u001b[46;6H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ms\u001b[0m\u001b[46;8H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mssions \u001b[0m\u001b[46;23H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mSwitch sessio\u001b[0m\u001b[46;37H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[47;6H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mstatus\u001b[0m\u001b[47;23H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mView\u001b[0m\u001b[47;29H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mtatus\u001b[0m\u001b[47;34H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[49;7H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[49;8H\u001b[?25h\u001b[?2026l"] -[27.59927693,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;56;91;141m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;8H\u001b[?25h\u001b[?2026l"] -[27.631750599,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;55;90;138m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;8H\u001b[?25h\u001b[?2026l"] -[27.664731618,"o","\u001b[?2026h\u001b[?25l\u001b[38;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[38;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[38;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m cd tes-demo\u001b[0m\u001b[38;19H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[39;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[39;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[39;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m texforge build\u001b[0m\u001b[39;22H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[40;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[40;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[41;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[42;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[42;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠙\u001b[0m\u001b[42;7H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[42;8H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThinking\u001b[0m\u001b[42;16H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[43;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[44;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[44;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mNow I need to create a comprehensive LaTeX document with all the requested features\u001b[0m\u001b[44;89H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[45;4H\u001b[38;2;255;255;255m\u001b[48;2;250;178;131m \u001b[0m\u001b[45;5H\u001b[38;2;10;10;10m\u001b[48;2;250;178;131m/exit \u001b[0m\u001b[45;23H\u001b[38;2;10;10;10m\u001b[48;2;250;178;131mExit the app\u001b[0m\u001b[45;35H\u001b[38;2;255;255;255m\u001b[48;2;250;178;131m \u001b[0m\u001b[46;5H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m/export \u001b[0m\u001b[46;23H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mExport session transcript\u001b[0m\u001b[47;5H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m/fetch:fetch:mcp \u001b[0m\u001b[47;23H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mFetch a URL and extract its contents as markdown\u001b[0m\u001b[49;8H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mx\u001b[0m\u001b[0m\u001b[49;9H\u001b[?25h\u001b[?2026l"] -[27.682378337,"o","\u001b[?2026h\u001b[?25l\u001b[1;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[2;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mThe user wants a LaTeX document demonstrating various capabilities. Let me first load the \u001b[0m\u001b[3;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mtexforge\u001b[0m\u001b[3;15H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mskill\u001b[0m\u001b[3;21H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mto understand how to u\u001b[0m\u001b[3;44H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10me texforge properly.\u001b[0m\u001b[3;64H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[4;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[5;6H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m→\u001b[0m\u001b[5;8H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mSkill \"texforge\"\u001b[0m\u001b[6;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[6;8H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[7;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 672ms\u001b[0m\u001b[8;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[9;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mNow I understand texforge. The user wants a comprehensive LaTeX document demonstrating many \u001b[0m\u001b[10;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mcapabilitie\u001b[0m\u001b[10;18H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m. Let me c\u001b[0m\u001b[10;29H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10meate a project and\u001b[0m\u001b[10;48H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mwrite the document.\u001b[0m\u001b[10;67H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[11;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[12;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mLet me create a new texforge project, then write the main.tex with all the requested features.\u001b[0m\u001b[13;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[14;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[14;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[15;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m# Todos\u001b[0m\u001b[16;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[17;6H\u001b[38;2;245;167;66m\u001b[48;2;20;20;20m[•] \u001b[0m\u001b[17;10H\u001b[38;2;245;167;66m\u001b[48;2;20;20;20mCreate texforge project\u001b[0m\u001b[18;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] \u001b[0m\u001b[18;10H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mWrite main.tex with all requested features\u001b[0m\u001b[19;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] Create bib file with example entries\u001b[0m\u001b[19;46H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[20;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] \u001b[0m\u001b[20;17H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20msections/body.tex\u001b[0m\u001b[20;34H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[21;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] \u001b[0m\u001b[21;10H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mRun texforge build\u001b[0m\u001b[21;28H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[22;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[23;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[24;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 1.1s\u001b[0m\u001b[25;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[26;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mThe user wants a comprehensive LaTeX document demonstrating various capabilities. Let me \u001b[0m\u001b[27;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mcr\u001b[0m\u001b[27;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mate the texforge \u001b[0m\u001b[27;28H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10moj\u001b[0m\u001b[27;31H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mct first.\u001b[0m\u001b[27;40H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[28;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[29;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[29;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[30;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m# Create new texforge project\u001b[0m\u001b[31;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[32;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m$ texforge "] -[27.68238732,"o","new tes-demo\u001b[0m\u001b[33;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[34;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mCreating project 'tes-demo' with template 'general'...\u001b[0m\u001b[35;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m ◇ Project 'tes-demo' created successfully\u001b[0m\u001b[35;49H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[36;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[37;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m cd tes-demo\u001b[0m\u001b[38;8H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mtexforge build\u001b[0m\u001b[39;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[40;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[41;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠙\u001b[0m\u001b[41;8H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThinking\u001b[0m\u001b[42;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[42;8H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[43;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mNow I need to create a comprehensive LaTeX document with all the requested features. Let me \u001b[0m\u001b[44;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mwrite\u001b[0m\u001b[44;11H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;9H\u001b[?25h\u001b[?2026l"] -[27.69839801,"o","\u001b[?2026h\u001b[?25l\u001b[44;11H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m the main.tex file\u001b[0m\u001b[0m\u001b[49;9H\u001b[?25h\u001b[?2026l"] -[27.714371393,"o","\u001b[?2026h\u001b[?25l\u001b[41;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠹\u001b[0m\u001b[53;4H\u001b[38;2;52;86;132m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;9H\u001b[?25h\u001b[?2026l"] -[27.730572737,"o","\u001b[?2026h\u001b[?25l\u001b[44;29H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m with:\u001b[0m\u001b[46;6H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mfetch:fetch:mcp\u001b[0m\u001b[46;23H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mFetch a URL a\u001b[0m\u001b[46;37H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30md extract its contents as markdown\u001b[0m\u001b[47;6H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mexport \u001b[0m\u001b[47;23H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mExport sessio\u001b[0m\u001b[47;37H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30m transcript\u001b[0m\u001b[47;48H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[49;9H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[49;10H\u001b[?25h\u001b[?2026l"] -[27.763530251,"o","\u001b[?2026h\u001b[?25l\u001b[1;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mThe user wants a LaTeX document demonstrating various capabilities. Let me first load the \u001b[0m\u001b[2;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mtexforge\u001b[0m\u001b[2;15H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mskill\u001b[0m\u001b[2;21H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mto understand how to u\u001b[0m\u001b[2;44H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10me texforge properly.\u001b[0m\u001b[2;64H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[3;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[4;6H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m→\u001b[0m\u001b[4;8H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mSkill \"texforge\"\u001b[0m\u001b[5;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[5;8H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[6;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 672ms\u001b[0m\u001b[7;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[8;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mNow I understand texforge. The user wants a comprehensive LaTeX document demonstrating many \u001b[0m\u001b[9;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mcapabilitie\u001b[0m\u001b[9;18H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m. Let me c\u001b[0m\u001b[9;29H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10meate a project and\u001b[0m\u001b[9;48H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mwrite the document.\u001b[0m\u001b[9;67H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[10;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[11;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mLet me create a new texforge project, then write the main.tex with all the requested features.\u001b[0m\u001b[12;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[13;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[13;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[14;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m# Todos\u001b[0m\u001b[15;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[16;6H\u001b[38;2;245;167;66m\u001b[48;2;20;20;20m[•] \u001b[0m\u001b[16;10H\u001b[38;2;245;167;66m\u001b[48;2;20;20;20mCreate texforge project\u001b[0m\u001b[17;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] \u001b[0m\u001b[17;10H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mWrite main.tex with all requested features\u001b[0m\u001b[18;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] Create bib file with example entries\u001b[0m\u001b[18;46H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[19;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] \u001b[0m\u001b[19;17H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20msections/body.tex\u001b[0m\u001b[19;34H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[20;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] \u001b[0m\u001b[20;10H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mRun texforge build\u001b[0m\u001b[20;28H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[21;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[22;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[23;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 1.1s\u001b[0m\u001b[24;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[25;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mThe user wants a comprehensive LaTeX document demonstrating various capabilities. Let me \u001b[0m\u001b[26;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mcr\u001b[0m\u001b[26;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mate the texforge \u001b[0m\u001b[26;28H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10moj\u001b[0m\u001b[26;31H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mct first.\u001b[0m\u001b[26;40H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[27;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[28;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[28;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[29;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m# Create new texforge project\u001b[0m\u001b[30;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[31;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m$ texforge new tes-demo\u001b[0m\u001b[32;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m "] -[27.763573768,"o"," \u001b[0m\u001b[33;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mCreating project 'tes-demo' with template 'general'...\u001b[0m\u001b[34;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m ◇ Project 'tes-demo' created successfully\u001b[0m\u001b[34;49H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[35;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[36;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m cd tes-demo\u001b[0m\u001b[37;8H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mtexforge build\u001b[0m\u001b[38;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[39;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[40;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠹\u001b[0m\u001b[40;8H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThinking\u001b[0m\u001b[41;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[41;8H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[42;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mNow I need to create a comprehensive LaTeX document with all the requested features. Let me \u001b[0m\u001b[43;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mwrite\u001b[0m\u001b[43;12H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mth\u001b[0m\u001b[43;15H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m main.t\u001b[0m\u001b[43;23H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mx file with:\u001b[0m\u001b[43;35H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[44;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m1. \u001b[0m\u001b[44;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mM\u001b[0m\u001b[44;11H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mrmaid\u001b[0m\u001b[44;16H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[53;4H\u001b[38;2;51;83;128m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;10H\u001b[?25h\u001b[?2026l"] -[27.795963652,"o","\u001b[?2026h\u001b[?25l\u001b[1;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mtexforge\u001b[0m\u001b[1;15H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mskill\u001b[0m\u001b[1;21H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mto understand how to u\u001b[0m\u001b[1;44H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10me texforge properly.\u001b[0m\u001b[1;64H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[2;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[3;6H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m→\u001b[0m\u001b[3;8H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mSkill \"texforge\"\u001b[0m\u001b[4;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[4;8H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[5;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 672ms\u001b[0m\u001b[6;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[7;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mNow I understand texforge. The user wants a comprehensive LaTeX document demonstrating many \u001b[0m\u001b[8;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mcapabilitie\u001b[0m\u001b[8;18H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m. Let me c\u001b[0m\u001b[8;29H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10meate a project and\u001b[0m\u001b[8;48H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mwrite the document.\u001b[0m\u001b[8;67H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[9;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[10;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mLet me create a new texforge project, then write the main.tex with all the requested features.\u001b[0m\u001b[11;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[12;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[12;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[13;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m# Todos\u001b[0m\u001b[14;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[15;6H\u001b[38;2;245;167;66m\u001b[48;2;20;20;20m[•] \u001b[0m\u001b[15;10H\u001b[38;2;245;167;66m\u001b[48;2;20;20;20mCreate texforge project\u001b[0m\u001b[16;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] \u001b[0m\u001b[16;10H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mWrite main.tex with all requested features\u001b[0m\u001b[17;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] Create bib file with example entries\u001b[0m\u001b[17;46H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[18;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] \u001b[0m\u001b[18;17H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20msections/body.tex\u001b[0m\u001b[18;34H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[19;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] \u001b[0m\u001b[19;10H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mRun texforge build\u001b[0m\u001b[19;28H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[20;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[21;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[22;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 1.1s\u001b[0m\u001b[23;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[24;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mThe user wants a comprehensive LaTeX document demonstrating various capabilities. Let me \u001b[0m\u001b[25;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mcr\u001b[0m\u001b[25;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mate the texforge \u001b[0m\u001b[25;28H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10moj\u001b[0m\u001b[25;31H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mct first.\u001b[0m\u001b[25;40H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[26;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[27;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[27;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[28;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m# Create new texforge project\u001b[0m\u001b[29;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[30;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m$ texforge new tes-demo\u001b[0m\u001b[31;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[32;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mCreating project 'tes-demo' with template 'general'...\u001b[0m\u001b[33;6H\u001b[38;2;2"] -[27.79601931,"o","38;238;238m\u001b[48;2;20;20;20m ◇ Project 'tes-demo' created successfully\u001b[0m\u001b[33;49H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[34;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[35;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m cd tes-demo\u001b[0m\u001b[36;8H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mtexforge build\u001b[0m\u001b[37;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[38;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[39;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠸\u001b[0m\u001b[39;8H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThinking\u001b[0m\u001b[40;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[40;8H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[41;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mNow I need to create a comprehensive LaTeX document with all the requested features. Let me \u001b[0m\u001b[42;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mwrite\u001b[0m\u001b[42;12H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mth\u001b[0m\u001b[42;15H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m main.t\u001b[0m\u001b[42;23H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mx file with:\u001b[0m\u001b[42;35H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[43;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m1. \u001b[0m\u001b[43;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mM\u001b[0m\u001b[43;11H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mrmaid sequence diagram\u001b[0m\u001b[43;33H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[44;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m2\u001b[0m\u001b[44;7H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[53;4H\u001b[38;2;50;81;124m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;10H\u001b[?25h\u001b[?2026l"] -[27.827991232,"o","\u001b[?2026h\u001b[?25l\u001b[44;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m2. \u001b[0m\u001b[44;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mGraphviz architecture\u001b[0m\u001b[49;10H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[49;11H\u001b[?25h\u001b[?2026l"] -[27.844696911,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;49;79;122m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;11H\u001b[?25h\u001b[?2026l"] -[27.874642276,"o","\u001b[?2026h\u001b[?25l\u001b[39;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠼\u001b[0m\u001b[0m\u001b[49;11H\u001b[?25h\u001b[?2026l"] -[27.890218861,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;11H\u001b[?25h\u001b[?2026l"] -[27.919507439,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;47;75;115m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;11H\u001b[?25h\u001b[?2026l"] -[27.953164347,"o","\u001b[?2026h\u001b[?25l\u001b[1;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[2;6H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m→\u001b[0m\u001b[2;8H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mSkill \"texforge\"\u001b[0m\u001b[3;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[3;8H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[4;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 672ms\u001b[0m\u001b[5;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[6;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mNow I understand texforge. The user wants a comprehensive LaTeX document demonstrating many \u001b[0m\u001b[7;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mcapabilitie\u001b[0m\u001b[7;18H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m. Let me c\u001b[0m\u001b[7;29H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10meate a project and\u001b[0m\u001b[7;48H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mwrite the document.\u001b[0m\u001b[7;67H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[8;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[9;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mLet me create a new texforge project, then write the main.tex with all the requested features.\u001b[0m\u001b[10;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[11;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[11;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[12;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m# Todos\u001b[0m\u001b[13;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[14;6H\u001b[38;2;245;167;66m\u001b[48;2;20;20;20m[•] \u001b[0m\u001b[14;10H\u001b[38;2;245;167;66m\u001b[48;2;20;20;20mCreate texforge project\u001b[0m\u001b[15;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] \u001b[0m\u001b[15;10H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mWrite main.tex with all requested features\u001b[0m\u001b[16;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] Create bib file with example entries\u001b[0m\u001b[16;46H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[17;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] \u001b[0m\u001b[17;17H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20msections/body.tex\u001b[0m\u001b[17;34H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[18;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] \u001b[0m\u001b[18;10H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mRun texforge build\u001b[0m\u001b[18;28H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[19;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[20;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[21;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 1.1s\u001b[0m\u001b[22;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[23;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mThe user wants a comprehensive LaTeX document demonstrating various capabilities. Let me \u001b[0m\u001b[24;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mcr\u001b[0m\u001b[24;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mate the texforge \u001b[0m\u001b[24;28H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10moj\u001b[0m\u001b[24;31H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mct first.\u001b[0m\u001b[24;40H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[25;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[26;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[26;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[27;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m# Create new texforge project\u001b[0m\u001b[28;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[29;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m$ texforge new tes-demo\u001b[0m\u001b[30;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[31;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mCreating project 'tes-demo' with template 'general'...\u001b[0m\u001b[32;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m ◇ Project 'tes-demo' created successfully\u001b[0m\u001b[32;49H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[33;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[34;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m cd tes-demo\u001b[0m\u001b[35;8H\u001b[38;2;238;238;238m\u001b[48;2;20"] -[27.953175432,"o",";20;20mtexforge build\u001b[0m\u001b[36;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[37;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[38;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠼\u001b[0m\u001b[38;8H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThinking\u001b[0m\u001b[39;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[39;8H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[40;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mNow I need to create a comprehensive LaTeX document with all the requested features. Let me \u001b[0m\u001b[41;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mwrite\u001b[0m\u001b[41;12H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mth\u001b[0m\u001b[41;15H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m main.t\u001b[0m\u001b[41;23H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mx file with:\u001b[0m\u001b[41;35H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[42;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m1. \u001b[0m\u001b[42;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mM\u001b[0m\u001b[42;11H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mrmaid sequence diagram\u001b[0m\u001b[42;33H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[43;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m2\u001b[0m\u001b[43;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mGraphviz archit\u001b[0m\u001b[43;25H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mcture digraph\u001b[0m\u001b[44;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m3\u001b[0m\u001b[44;7H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[0m\u001b[49;11H\u001b[?25h\u001b[?2026l"] -[27.969632915,"o","\u001b[?2026h\u001b[?25l\u001b[1;6H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m→\u001b[0m\u001b[1;8H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mSkill \"texforge\"\u001b[0m\u001b[2;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[2;8H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[3;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 672ms\u001b[0m\u001b[4;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[5;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mNow I understand texforge. The user wants a comprehensive LaTeX document demonstrating many \u001b[0m\u001b[6;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mcapabilitie\u001b[0m\u001b[6;18H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m. Let me c\u001b[0m\u001b[6;29H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10meate a project and\u001b[0m\u001b[6;48H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mwrite the document.\u001b[0m\u001b[6;67H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[7;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[8;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mLet me create a new texforge project, then write the main.tex with all the requested features.\u001b[0m\u001b[9;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[10;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[10;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[11;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m# Todos\u001b[0m\u001b[12;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[13;6H\u001b[38;2;245;167;66m\u001b[48;2;20;20;20m[•] \u001b[0m\u001b[13;10H\u001b[38;2;245;167;66m\u001b[48;2;20;20;20mCreate texforge project\u001b[0m\u001b[14;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] \u001b[0m\u001b[14;10H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mWrite main.tex with all requested features\u001b[0m\u001b[15;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] Create bib file with example entries\u001b[0m\u001b[15;46H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[16;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] \u001b[0m\u001b[16;17H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20msections/body.tex\u001b[0m\u001b[16;34H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[17;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] \u001b[0m\u001b[17;10H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mRun texforge build\u001b[0m\u001b[17;28H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[18;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[19;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 1.1s\u001b[0m\u001b[21;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[22;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mThe user wants a comprehensive LaTeX document demonstrating various capabilities. Let me \u001b[0m\u001b[23;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mcr\u001b[0m\u001b[23;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mate the texforge \u001b[0m\u001b[23;28H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10moj\u001b[0m\u001b[23;31H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mct first.\u001b[0m\u001b[23;40H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[24;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[25;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[25;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[26;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m# Create new texforge project\u001b[0m\u001b[27;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[28;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m$ texforge new tes-demo\u001b[0m\u001b[29;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[30;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mCreating project 'tes-demo' with template 'general'...\u001b[0m\u001b[31;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m ◇ Project 'tes-demo' created successfully\u001b[0m\u001b[31;49H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[32;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[33;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m cd tes-demo\u001b[0m\u001b[34;8H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mtexforge build\u001b[0m\u001b[35;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[36;3H\u001b[38;2;255"] -[27.969681345,"o",";255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[37;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠴\u001b[0m\u001b[37;8H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThinking\u001b[0m\u001b[38;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[38;8H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[39;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mNow I need to create a comprehensive LaTeX document with all the requested features. Let me \u001b[0m\u001b[40;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mwrite\u001b[0m\u001b[40;12H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mth\u001b[0m\u001b[40;15H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m main.t\u001b[0m\u001b[40;23H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mx file with:\u001b[0m\u001b[40;35H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[41;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m1. \u001b[0m\u001b[41;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mM\u001b[0m\u001b[41;11H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mrmaid sequence diagram\u001b[0m\u001b[41;33H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[42;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m2\u001b[0m\u001b[42;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mGraphviz archit\u001b[0m\u001b[42;25H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mcture digraph\u001b[0m\u001b[43;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m3\u001b[0m\u001b[43;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mD2 \u001b[0m\u001b[43;13H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mipeline d\u001b[0m\u001b[43;23H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10magram\u001b[0m\u001b[43;28H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[44;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m4. \u001b[0m\u001b[44;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mMath equations with cross\u001b[0m\u001b[53;4H\u001b[38;2;45;73;111m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;11H\u001b[?25h\u001b[?2026l"] -[28.002413648,"o","\u001b[?2026h\u001b[?25l\u001b[44;34H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m-re\u001b[0m\u001b[53;4H\u001b[38;2;44;71;109m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;11H\u001b[?25h\u001b[?2026l"] -[28.019206557,"o","\u001b[?2026h\u001b[?25l\u001b[1;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[1;8H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[2;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 672ms\u001b[0m\u001b[3;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[4;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mNow I understand texforge. The user wants a comprehensive LaTeX document demonstrating many \u001b[0m\u001b[5;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mcapabilitie\u001b[0m\u001b[5;18H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m. Let me c\u001b[0m\u001b[5;29H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10meate a project and\u001b[0m\u001b[5;48H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mwrite the document.\u001b[0m\u001b[5;67H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[6;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[7;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mLet me create a new texforge project, then write the main.tex with all the requested features.\u001b[0m\u001b[8;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[9;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[9;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[10;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m# Todos\u001b[0m\u001b[11;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[12;6H\u001b[38;2;245;167;66m\u001b[48;2;20;20;20m[•] \u001b[0m\u001b[12;10H\u001b[38;2;245;167;66m\u001b[48;2;20;20;20mCreate texforge project\u001b[0m\u001b[13;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] \u001b[0m\u001b[13;10H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mWrite main.tex with all requested features\u001b[0m\u001b[14;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] Create bib file with example entries\u001b[0m\u001b[14;46H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[15;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] \u001b[0m\u001b[15;17H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20msections/body.tex\u001b[0m\u001b[15;34H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[16;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] \u001b[0m\u001b[16;10H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mRun texforge build\u001b[0m\u001b[16;28H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[17;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[18;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 1.1s\u001b[0m\u001b[20;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[21;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mThe user wants a comprehensive LaTeX document demonstrating various capabilities. Let me \u001b[0m\u001b[22;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mcr\u001b[0m\u001b[22;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mate the texforge \u001b[0m\u001b[22;28H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10moj\u001b[0m\u001b[22;31H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mct first.\u001b[0m\u001b[22;40H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[23;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[24;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[24;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[25;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m# Create new texforge project\u001b[0m\u001b[26;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[27;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m$ texforge new tes-demo\u001b[0m\u001b[28;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[29;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mCreating project 'tes-demo' with template 'general'...\u001b[0m\u001b[30;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m ◇ Project 'tes-demo' created successfully\u001b[0m\u001b[30;49H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[31;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[32;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m cd tes-demo\u001b[0m\u001b[33;8H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mtexforge build\u001b[0m\u001b[34;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[35;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m "] -[28.019244483,"o"," \u001b[0m\u001b[36;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠴\u001b[0m\u001b[36;8H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThinking\u001b[0m\u001b[37;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[37;8H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[38;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mNow I need to create a comprehensive LaTeX document with all the requested features. Let me \u001b[0m\u001b[39;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mwrite\u001b[0m\u001b[39;12H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mth\u001b[0m\u001b[39;15H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m main.t\u001b[0m\u001b[39;23H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mx file with:\u001b[0m\u001b[39;35H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[40;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m1. \u001b[0m\u001b[40;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mM\u001b[0m\u001b[40;11H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mrmaid sequence diagram\u001b[0m\u001b[40;33H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[41;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m2\u001b[0m\u001b[41;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mGraphviz archit\u001b[0m\u001b[41;25H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mcture digraph\u001b[0m\u001b[42;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m3\u001b[0m\u001b[42;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mD2 \u001b[0m\u001b[42;13H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mipeline d\u001b[0m\u001b[42;23H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10magram\u001b[0m\u001b[42;28H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[43;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m4\u001b[0m\u001b[43;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mMath equations with cross-references\u001b[0m\u001b[44;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m5.\u001b[0m\u001b[44;8H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[0m\u001b[49;11H\u001b[?25h\u001b[?2026l"] -[28.031438905,"o","\u001b]0;\u0007"] -[28.034929181,"o","\u001b[?25h\u001b[0m\u001b]22;\u0007\u001b[>4;0m\u001b[?1003l\u001b[?1002l\u001b[?1000l\u001b[?1006l\u001b[?2004l\u001b[?1049l\u001b[?2031l\u001b]0;\u0007\u001b]12;default\u0007\u001b]112\u0007\u001b[0 q\u001b[?25h"] -[28.044983217,"o","\u001b[?25h"] -[28.05579063,"o"," \u001b[0m▄\u001b[0m \r\n \u001b[90m█\u001b[0m\u001b[90m▀\u001b[0m\u001b[90m▀\u001b[0m\u001b[90m█\u001b[0m \u001b[90m█\u001b[0m\u001b[90m▀\u001b[0m\u001b[90m▀\u001b[0m\u001b[90m█\u001b[0m \u001b[90m█\u001b[0m\u001b[90m▀\u001b[0m\u001b[90m▀\u001b[0m\u001b[90m█\u001b[0m \u001b[90m█\u001b[0m\u001b[90m▀\u001b[0m\u001b[90m▀\u001b[0m\u001b[90m▄\u001b[0m \u001b[0m█\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m \u001b[0m█\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m█\u001b[0m \u001b[0m█\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m█\u001b[0m \u001b[0m█\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m█\u001b[0m\r\n \u001b[90m█\u001b[0m\u001b[48;5;235m \u001b[0m\u001b[48;5;235m \u001b[0m\u001b[90m█\u001b[0m \u001b[90m█\u001b[0m\u001b[48;5;235m \u001b[0m\u001b[48;5;235m \u001b[0m\u001b[90m█\u001b[0m \u001b[90m█\u001b[0m\u001b[90m\u001b[48;5;235m▀\u001b[0m\u001b[90m\u001b[48;5;235m▀\u001b[0m\u001b[90m\u001b[48;5;235m▀\u001b[0m \u001b[90m█\u001b[0m\u001b[48;5;235m \u001b[0m\u001b[48;5;235m \u001b[0m\u001b[90m█\u001b[0m \u001b[0m█\u001b[0m\u001b[48;5;238m \u001b[0m\u001b[48;5;238m \u001b[0m\u001b[48;5;238m \u001b[0m \u001b[0m█\u001b[0m\u001b[48;5;238m \u001b[0m\u001b[48;5;238m \u001b[0m\u001b[0m█\u001b[0m \u001b[0m█\u001b[0m\u001b[48;5;238m \u001b[0m\u001b[48;5;238m \u001b[0m\u001b[0m█\u001b[0m \u001b[0m█\u001b[0m\u001b[0m\u001b[48;5;238m▀\u001b[0m\u001b[0m\u001b[48;5;238m▀\u001b[0m\u001b[0m\u001b[48;5;238m▀\u001b[0m\r\n \u001b[90m▀\u001b[0m\u001b[90m▀\u001b[0m\u001b[90m▀\u001b[0m\u001b[90m▀\u001b[0m \u001b[90m█\u001b[0m\u001b[90m▀\u001b[0m\u001b[90m▀\u001b[0m\u001b[90m▀\u001b[0m \u001b[90m▀\u001b[0m\u001b[90m▀\u001b[0m\u001b[90m▀\u001b[0m\u001b[90m▀\u001b[0m \u001b[90m▀\u001b[0m\u001b[38;5;235m▀\u001b[0m\u001b[38;5;235m▀\u001b[0m\u001b[90m▀\u001b[0m \u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m \u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m \u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m \u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m\r\n\r\n \u001b[90mSession \u001b[0m\u001b[1mNew session - 2026-07-07T13:55:09.012Z\u001b[0m\r\n \u001b[90mContinue \u001b[0m\u001b[1mopencode -s ses_0c32389abffewyL790AFRcnI60\u001b[0m\r\n\r\n"] -[28.179515255,"o","\u001b]697;OSCUnlock=6f3dc305774c4fb5ac9caff308f61ed2\u0007\u001b]697;Dir=/home/jheisonmblivecom\u0007"] -[28.179562802,"o","\u001b]697;Shell=bash\u0007\u001b]697;ShellPath=/usr/bin/bash\u0007"] -[28.179604986,"o","\u001b]697;WSLDistro=Ubuntu\u0007\u001b]697;PID=16910\u0007"] -[28.179667202,"o","\u001b]697;ExitCode=0\u0007\u001b]697;TTY=/dev/pts/3\u0007"] -[28.179731596,"o","\u001b]697;Log=\u0007\u001b]697;User=jheisonmblivecom\u0007"] -[28.181307235,"o","\u001b]697;OSCLock=6f3dc305774c4fb5ac9caff308f61ed2\u0007\u001b]697;PreExec\u0007"] -[28.349678092,"o","\u001b[?2004h\u001b]697;StartPrompt\u0007\u001b[1;32mtexForge@univerlab\u001b[0m:\u001b[1;34m~\u001b[0m$ \u001b]697;EndPrompt\u0007\u001b]697;NewCmd=6f3dc305774c4fb5ac9caff308f61ed2\u0007"] +{"demostage":{"captions":[],"demo":{"name":"demo","output_dir":"./dist","prompt":"\\[\\e[1;32m\\]texForge@univerlab\\[\\e[0m\\]:\\[\\e[1;34m\\]~\\[\\e[0m\\]$ "},"duration":38.99586896,"faithful":false,"focuses":[[0.121938098,"main"],[29.271204528,"pdf-r1"]],"layout":{"background":"#0b0f14","font_family":"IBM Plex Mono","fps":15,"height":1080,"line_height":1.2000000476837158,"panes":[{"font_family":"monospace","font_size":16,"height":1080,"id":"main","type":"terminal","width":1440,"x":0,"y":0},{"height":1080,"id":"pdf-r1","reveal_at":29.271204528,"type":"browser","url":"file:///home/jheisonmblivecom/Projects/UniverLab/texforge/examples/texforge-capabilites/texforge-capabilites.pdf","width":1440,"x":0,"y":0}],"width":1440},"timeline":[{"action":"focus","pane":"main"},{"action":"type","human_salt":true,"text":"texforge --help"},{"action":"wait","duration_ms":200},{"action":"keypress","key":"enter"},{"action":"wait","duration_ms":2500},{"action":"type","human_salt":true,"text":"opencode"},{"action":"wait","duration_ms":200},{"action":"keypress","key":"enter"},{"action":"wait_for_quiet","quiet_ms":1500},{"action":"type","human_salt":true,"text":"Create a texforge project \"texforge-capabilites\" demonstrating all capabilities: Mermaid sequence diagra, Graphviz architecture digraph, D2 pipeline diagram, math equations with cross-refs, BibTeX citations, tikz diagram, examples of code listings, two booktabs tables, table of contents, and lists. Compile with texforge build."},{"action":"wait","duration_ms":200},{"action":"keypress","key":"enter"},{"action":"wait_for_quiet","quiet_ms":5000},{"action":"type","human_salt":true,"text":"/exit"},{"action":"wait","duration_ms":200},{"action":"keypress","key":"enter"},{"action":"wait_for_quiet","quiet_ms":500},{"action":"focus","pane":"pdf-r1"},{"action":"scroll","direction":"down","duration_ms":8000,"pane":"pdf-r1","velocity":"constant"},{"action":"wait","duration_ms":8000},{"action":"terminate"}],"typing":{"base_ms":80,"salt_ms":15}},"env":{"TERM":"xterm-256color"},"height":54,"title":"demo","version":2,"width":144} +[0.000167838,"o","\r\n\u001b[?2004l\r"] +[0.000450978,"o","\u001b]697;OSCUnlock=f83785b047c648f6bd12bde086bcbd50\u0007\u001b]697;Dir=/home/jheisonmblivecom\u0007"] +[0.000537025,"o","\u001b]697;Shell=bash\u0007\u001b]697;ShellPath=/usr/bin/bash\u0007"] +[0.000626467,"o","\u001b]697;WSLDistro=Ubuntu\u0007\u001b]697;PID=736321\u0007"] +[0.000709791,"o","\u001b]697;ExitCode=0\u0007\u001b]697;TTY=/dev/pts/0\u0007"] +[0.000788593,"o","\u001b]697;Log=\u0007\u001b]697;User=jheisonmblivecom\u0007"] +[0.002606482,"o","\u001b]697;OSCLock=f83785b047c648f6bd12bde086bcbd50\u0007"] +[0.002645218,"o","\u001b]697;PreExec\u0007"] +[0.097222219,"o","\u001b[?2004h\u001b]697;StartPrompt\u0007\u001b[1;32mtexForge@univerlab\u001b[0m:\u001b[1;34m~\u001b[0m$ \u001b]697;EndPrompt\u0007\u001b]697;NewCmd=f83785b047c648f6bd12bde086bcbd50\u0007"] +[0.2062778,"o","t"] +[0.293471998,"o","e"] +[0.375761599,"o","x"] +[0.457997601,"o","f"] +[0.544116501,"o","o"] +[0.6182664,"o","r"] +[0.709487873,"o","g"] +[0.788626296,"o","e"] +[0.877910727,"o"," "] +[0.956132852,"o","-"] +[1.046261921,"o","-"] +[1.123368522,"o","h"] +[1.217511678,"o","e"] +[1.306642289,"o","l"] +[1.373907527,"o","p"] +[1.577673795,"o","\r\n\u001b[?2004l\r"] +[1.591132153,"o","Self-contained LaTeX to PDF compiler CLI\r\n\r\n\u001b[1m\u001b[4mUsage:\u001b[0m \u001b[1mtexforge\u001b[0m \r\n\r\n\u001b[1m\u001b[4mCommands:\u001b[0m\r\n \u001b[1mclean\u001b[0m Remove build artifacts\r\n \u001b[1minit\u001b[0m Initialize a texforge project in the current directory\r\n \u001b[1mnew\u001b[0m Create a new project from a template\r\n \u001b[1mbuild\u001b[0m Compile project to PDF\r\n \u001b[1mfmt\u001b[0m Format .tex files\r\n \u001b[1mcheck\u001b[0m Lint project without compiling\r\n \u001b[1mstats\u001b[0m Count words per section or file\r\n \u001b[1moutline\u001b[0m Print the document's section tree\r\n \u001b[1mpreview\u001b[0m Rasterize the compiled PDF to PNG pages\r\n \u001b[1mpdf\u001b[0m Inspect the compiled PDF (text, info, pages, fidelity)\r\n \u001b[1mtemplate\u001b[0m Manage templates\r\n \u001b[1mspell\u001b[0m Manage the personal spell-check whitelist (dictionary)\r\n \u001b[1mdoctor\u001b[0m Diagnose the managed environment (Tectonic, cache, fonts, dictionaries, project)\r\n \u001b[1mconfig\u001b[0m Manage global configuration\r\n \u001b[1mhelp\u001b[0m Print this message or the help of the given subcommand(s)\r\n\r\n\u001b[1m\u001b[4mOptions:\u001b[0m\r\n \u001b[1m-h\u001b[0m, \u001b[1m--help\u001b[0m Print help\r\n \u001b[1m-V\u001b[0m, \u001b[1m--version\u001b[0m Print version\r\n"] +[1.591874186,"o","\u001b]697;OSCUnlock=f83785b047c648f6bd12bde086bcbd50\u0007\u001b]697;Dir=/home/jheisonmblivecom\u0007"] +[1.591928846,"o","\u001b]697;Shell=bash\u0007\u001b]697;ShellPath=/usr/bin/bash\u0007"] +[1.592005361,"o","\u001b]697;WSLDistro=Ubuntu\u0007\u001b]697;PID=736321\u0007"] +[1.592061848,"o","\u001b]697;ExitCode=0\u0007\u001b]697;TTY=/dev/pts/0\u0007\u001b]697;Log=\u0007"] +[1.592126506,"o","\u001b]697;User=jheisonmblivecom\u0007"] +[1.593478557,"o","\u001b]697;OSCLock=f83785b047c648f6bd12bde086bcbd50\u0007\u001b]697;PreExec\u0007"] +[1.690253753,"o","\u001b[?2004h\u001b]697;StartPrompt\u0007\u001b[1;32mtexForge@univerlab\u001b[0m:\u001b[1;34m~\u001b[0m$ \u001b]697;EndPrompt\u0007\u001b]697;NewCmd=f83785b047c648f6bd12bde086bcbd50\u0007"] +[4.22844371,"o","o"] +[4.294660108,"o","p"] +[4.3604601689999996,"o","e"] +[4.441689855,"o","n"] +[4.514313938,"o","c"] +[4.593683797,"o","o"] +[4.664840732,"o","d"] +[4.749870259,"o","e"] +[4.954451493,"o","\r\n\u001b[?2004l\r"] +[5.838894564,"o","\u001b[?2031h\u001b]10;?\u0007\u001b]11;?\u0007\u001b[>0q\u001b[?25l\u001b[s\u001b[6n\u001bP+q4d73\u001b\\\u001b[?1016$p\u001b[?2027$p\u001b[?2031$p\u001b[?1004$p\u001b[?2004$p\u001b[?2026$p\u001b[?u\u001b]99;i=opentui-notifications:p=?;\u001b\\\u001b]1337;Capabilities\u001b\\\u001b[H\u001b]66;w=1; \u001b\\\u001b[6n\u001b[H\u001b]66;s=2; \u001b\\\u001b[6n\u001b[u"] +[5.83992059,"o","\u001b[s\u001b[?1049h\u001b[>4;1m\u001b[?2027h\u001b[?2004h"] +[5.840307896,"o","\u001b[?1000h\u001b[?1002h\u001b[?1003h\u001b[?1006h"] +[5.840375845,"o","\u001b[14t"] +[5.850244146,"o","\u001b]4;0;?\u0007"] +[8.4179517,"o","\u001b[?2026h\u001b[?25l\u001b[1;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[2;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[3;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[4;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[5;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[6;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[7;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[8;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[9;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[10;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[11;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[12;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[13;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[14;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[15;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[16;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[17;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[18;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[19;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[20;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[21;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[22;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m "] +[8.418004673,"o"," \u001b[23;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[24;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[25;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[26;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[27;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[28;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[29;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[30;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[31;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[32;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[33;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[34;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[35;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[36;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[37;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[38;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[39;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[40;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[41;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[42;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[43;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[44;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b"] +[8.4180308,"o","[45;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[46;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[47;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[48;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[49;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[50;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[51;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[52;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[53;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[54;1H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[?2026l"] +[8.461773761,"o","\u001b]0;OpenCode\u0007"] +[8.55094566,"o","\u001b[?2026h\u001b[?25l\u001b[21;54H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[21;74H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m ▄ \u001b[0m\u001b[22;54H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m█▀▀█ █▀▀█ █▀▀█ █▀▀▄\u001b[0m\u001b[22;74H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m█▀▀▀ █▀▀█ █▀▀█ █▀▀█\u001b[0m\u001b[23;54H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m█\u001b[0m\u001b[23;55H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m \u001b[0m\u001b[23;57H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m█ █\u001b[0m\u001b[23;60H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m \u001b[0m\u001b[23;62H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m█ █\u001b[0m\u001b[23;65H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m▀▀▀\u001b[0m\u001b[23;68H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m █\u001b[0m\u001b[23;70H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m \u001b[0m\u001b[23;72H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m█\u001b[0m\u001b[23;74H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m█\u001b[0m\u001b[23;75H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m \u001b[0m\u001b[23;78H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m █\u001b[0m\u001b[23;80H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m \u001b[0m\u001b[23;82H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m█ █\u001b[0m\u001b[23;85H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m \u001b[0m\u001b[23;87H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m█ █\u001b[0m\u001b[23;90H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m▀▀▀\u001b[0m\u001b[24;54H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀\u001b[0m\u001b[24;70H\u001b[38;2;40;40;40m\u001b[48;2;10;10;10m▀▀\u001b[0m\u001b[24;72H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[24;74H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀\u001b[0m\u001b[27;36H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[27;37H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[28;36H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[28;37H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[28;39H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mAsk anything... \"Fix broken tests\"\u001b[0m\u001b[28;73H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[29;36H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[29;37H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[30;36H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[30;37H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[30;39H\u001b[38;2;92;156;245m\u001b[48;2;30;30;30mBuild\u001b[0m\u001b[30;44H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[30;45H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30m·\u001b[0m\u001b[30;46H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[30;47H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mGPT-5.6 Luna (2x usage)\u001b[0m\u001b[30;70H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[30;71H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mOpenCode Go\u001b[0m\u001b[30;82H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[31;36H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m╹\u001b[0m\u001b[31;37H\u001b[38;2;30;30;30m\u001b[48;2;10;10;10m▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\u001b[0m\u001b[32;36H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mtab \u001b[0m\u001b[32;40H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10magents\u001b[0m\u001b[32;48H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mctrl+p \u001b[0m\u001b[32;55H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mcommands\u001b[0m\u001b[36;46H\u001b[38;2;245;167;66m\u001b[48;2;10;10;10m● Tip \u001b[0m\u001b[36;52H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mThemes support dark/light variants for both modes\u001b[0m\u001b[53;3H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m~\u001b[0m\u001b[53;136H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m1.18.18\u001b[0m\u001b[0m\u001b]12;#eeeeee\u0007\u001b[1 q\u001b[28;39H\u001b[?25h\u001b[?2026l"] +[8.583136683,"o","\u001b[?2026h\u001b[?25l\u001b[28;39H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mpabiliti\u001b[0m\u001b[28;47H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[30;47H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mMiMo V2.5 Free\u001b[0m\u001b[30;61H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[30;62H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mOpenCode Zen\u001b[0m\u001b[30;74H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[28;47H\u001b[?25h\u001b[?2026l"] +[8.602499246,"o","\u001b[?2026h\u001b[?25l\u001b[28;47H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mes\u001b[0m\u001b[0m\u001b[28;49H\u001b[?25h\u001b[?2026l"] +[8.631878927,"o","\u001b[?2026h\u001b[?25l\u001b[28;49H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m: \u001b[0m\u001b[0m\u001b[28;51H\u001b[?25h\u001b[?2026l"] +[8.648039921,"o","\u001b[?2026h\u001b[?25l\u001b[28;51H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mM\u001b[0m\u001b[0m\u001b[28;52H\u001b[?25h\u001b[?2026l"] +[8.667366397,"o","\u001b[?2026h\u001b[?25l\u001b[28;52H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mer\u001b[0m\u001b[0m\u001b[28;54H\u001b[?25h\u001b[?2026l"] +[8.683606345,"o","\u001b[?2026h\u001b[?25l\u001b[28;54H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mm\u001b[0m\u001b[0m\u001b[28;55H\u001b[?25h\u001b[?2026l"] +[8.700796868,"o","\u001b[?2026h\u001b[?25l\u001b[28;55H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ma\u001b[0m\u001b[0m\u001b[28;56H\u001b[?25h\u001b[?2026l"] +[8.717611925,"o","\u001b[?2026h\u001b[?25l\u001b[28;56H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[28;57H\u001b[?25h\u001b[?2026l"] +[8.733897295,"o","\u001b[?2026h\u001b[?25l\u001b[28;57H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30md\u001b[0m\u001b[0m\u001b[28;58H\u001b[?25h\u001b[?2026l"] +[8.750768685,"o","\u001b[?2026h\u001b[?25l\u001b[28;58H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[28;59H\u001b[?25h\u001b[?2026l"] +[8.76723208,"o","\u001b[?2026h\u001b[?25l\u001b[28;59H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ms\u001b[0m\u001b[0m\u001b[28;60H\u001b[?25h\u001b[?2026l"] +[8.783171367,"o","\u001b[?2026h\u001b[?25l\u001b[28;60H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[28;61H\u001b[?25h\u001b[?2026l"] +[8.803886146,"o","\u001b[?2026h\u001b[?25l\u001b[28;61H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mqu\u001b[0m\u001b[0m\u001b[28;63H\u001b[?25h\u001b[?2026l"] +[8.821942496,"o","\u001b[?2026h\u001b[?25l\u001b[28;63H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[28;64H\u001b[?25h\u001b[?2026l"] +[8.838520192,"o","\u001b[?2026h\u001b[?25l\u001b[28;64H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mn\u001b[0m\u001b[0m\u001b[28;65H\u001b[?25h\u001b[?2026l"] +[8.861010287,"o","\u001b[?2026h\u001b[?25l\u001b[28;65H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mc\u001b[0m\u001b[0m\u001b[28;66H\u001b[?25h\u001b[?2026l"] +[8.877630661,"o","\u001b[?2026h\u001b[?25l\u001b[28;66H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[28;67H\u001b[?25h\u001b[?2026l"] +[8.894261987,"o","\u001b[?2026h\u001b[?25l\u001b[28;67H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[28;68H\u001b[?25h\u001b[?2026l"] +[8.910452756,"o","\u001b[?2026h\u001b[?25l\u001b[28;68H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30md\u001b[0m\u001b[0m\u001b[28;69H\u001b[?25h\u001b[?2026l"] +[8.9349258,"o","\u001b[?2026h\u001b[?25l\u001b[28;69H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[28;70H\u001b[?25h\u001b[?2026l"] +[8.950490393,"o","\u001b[?2026h\u001b[?25l\u001b[28;70H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mag\u001b[0m\u001b[0m\u001b[28;72H\u001b[?25h\u001b[?2026l"] +[8.971402171,"o","\u001b[?2026h\u001b[?25l\u001b[28;72H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mr\u001b[0m\u001b[0m\u001b[28;73H\u001b[?25h\u001b[?2026l"] +[8.990136846,"o","\u001b[?2026h\u001b[?25l\u001b[28;73H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ma\u001b[0m\u001b[0m\u001b[28;74H\u001b[?25h\u001b[?2026l"] +[9.007053202,"o","\u001b[?2026h\u001b[?25l\u001b[28;74H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m, \u001b[0m\u001b[0m\u001b[28;76H\u001b[?25h\u001b[?2026l"] +[9.02938872,"o","\u001b[?2026h\u001b[?25l\u001b[28;76H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mG\u001b[0m\u001b[0m\u001b[28;77H\u001b[?25h\u001b[?2026l"] +[9.04603495,"o","\u001b[?2026h\u001b[?25l\u001b[28;77H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mr\u001b[0m\u001b[0m\u001b[28;78H\u001b[?25h\u001b[?2026l"] +[9.064029851,"o","\u001b[?2026h\u001b[?25l\u001b[28;78H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30map\u001b[0m\u001b[0m\u001b[28;80H\u001b[?25h\u001b[?2026l"] +[9.085494871,"o","\u001b[?2026h\u001b[?25l\u001b[28;80H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mh\u001b[0m\u001b[0m\u001b[28;81H\u001b[?25h\u001b[?2026l"] +[9.101627666,"o","\u001b[?2026h\u001b[?25l\u001b[28;81H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mv\u001b[0m\u001b[0m\u001b[28;82H\u001b[?25h\u001b[?2026l"] +[9.122101389000001,"o","\u001b[?2026h\u001b[?25l\u001b[28;82H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30miz\u001b[0m\u001b[0m\u001b[28;84H\u001b[?25h\u001b[?2026l"] +[9.138250769,"o","\u001b[?2026h\u001b[?25l\u001b[28;84H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[28;85H\u001b[?25h\u001b[?2026l"] +[9.156324677,"o","\u001b[?2026h\u001b[?25l\u001b[28;85H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ma\u001b[0m\u001b[0m\u001b[28;86H\u001b[?25h\u001b[?2026l"] +[9.185442201,"o","\u001b[?2026h\u001b[?25l\u001b[28;86H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mr\u001b[0m\u001b[0m\u001b[28;87H\u001b[?25h\u001b[?2026l"] +[9.201799074,"o","\u001b[?2026h\u001b[?25l\u001b[28;87H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mch\u001b[0m\u001b[0m\u001b[28;89H\u001b[?25h\u001b[?2026l"] +[9.218374673,"o","\u001b[?2026h\u001b[?25l\u001b[28;89H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[28;90H\u001b[?25h\u001b[?2026l"] +[9.241428508,"o","\u001b[?2026h\u001b[?25l\u001b[28;90H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mte\u001b[0m\u001b[0m\u001b[28;92H\u001b[?25h\u001b[?2026l"] +[9.25784102,"o","\u001b[?2026h\u001b[?25l\u001b[28;92H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mc\u001b[0m\u001b[0m\u001b[28;93H\u001b[?25h\u001b[?2026l"] +[9.273791467,"o","\u001b[?2026h\u001b[?25l\u001b[28;93H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[28;94H\u001b[?25h\u001b[?2026l"] +[9.289612717,"o","\u001b[?2026h\u001b[?25l\u001b[28;94H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mu\u001b[0m\u001b[0m\u001b[28;95H\u001b[?25h\u001b[?2026l"] +[9.305664305,"o","\u001b[?2026h\u001b[?25l\u001b[28;95H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mr\u001b[0m\u001b[0m\u001b[28;96H\u001b[?25h\u001b[?2026l"] +[9.333226634,"o","\u001b[?2026h\u001b[?25l\u001b[28;96H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me \u001b[0m\u001b[0m\u001b[28;98H\u001b[?25h\u001b[?2026l"] +[9.34885756,"o","\u001b[?2026h\u001b[?25l\u001b[28;98H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30md\u001b[0m\u001b[0m\u001b[28;99H\u001b[?25h\u001b[?2026l"] +[9.369854019,"o","\u001b[?2026h\u001b[?25l\u001b[28;99H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mig\u001b[0m\u001b[0m\u001b[28;101H\u001b[?25h\u001b[?2026l"] +[9.389989622,"o","\u001b[?2026h\u001b[?25l\u001b[28;101H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mr\u001b[0m\u001b[0m\u001b[28;102H\u001b[?25h\u001b[?2026l"] +[9.405829969,"o","\u001b[?2026h\u001b[?25l\u001b[28;102H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ma\u001b[0m\u001b[0m\u001b[28;103H\u001b[?25h\u001b[?2026l"] +[9.422114979,"o","\u001b[?2026h\u001b[?25l\u001b[28;103H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mp\u001b[0m\u001b[0m\u001b[28;104H\u001b[?25h\u001b[?2026l"] +[9.437651842,"o","\u001b[?2026h\u001b[?25l\u001b[28;104H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mh\u001b[0m\u001b[0m\u001b[28;105H\u001b[?25h\u001b[?2026l"] +[9.454202656,"o","\u001b[?2026h\u001b[?25l\u001b[28;105H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m,\u001b[0m\u001b[0m\u001b[28;106H\u001b[?25h\u001b[?2026l"] +[9.470199236,"o","\u001b[?2026h\u001b[?25l\u001b[28;106H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[28;107H\u001b[?25h\u001b[?2026l"] +[9.489993968,"o","\u001b[?2026h\u001b[?25l\u001b[28;107H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mD2\u001b[0m\u001b[0m\u001b[28;109H\u001b[?25h\u001b[?2026l"] +[9.51574959,"o","\u001b[?2026h\u001b[?25l\u001b[20;54H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;74H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m ▄ \u001b[0m\u001b[21;54H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m█▀▀█\u001b[0m\u001b[21;59H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m█▀▀█\u001b[0m\u001b[21;64H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m█▀▀█\u001b[0m\u001b[21;69H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m█▀▀▄\u001b[0m\u001b[21;74H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m█▀▀▀\u001b[0m\u001b[21;79H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m█▀▀█\u001b[0m\u001b[21;84H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m█▀▀█\u001b[0m\u001b[21;89H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m█▀▀█\u001b[0m\u001b[22;55H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m \u001b[0m\u001b[22;60H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m \u001b[0m\u001b[22;65H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m▀▀▀\u001b[0m\u001b[22;70H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m \u001b[0m\u001b[22;72H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m█\u001b[0m\u001b[22;75H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m \u001b[0m\u001b[22;80H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m \u001b[0m\u001b[22;85H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m \u001b[0m\u001b[22;90H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m▀▀▀\u001b[0m\u001b[23;54H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀▀▀▀\u001b[0m\u001b[23;60H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀▀▀\u001b[0m\u001b[23;64H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀▀▀▀\u001b[0m\u001b[23;69H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[23;70H\u001b[38;2;40;40;40m\u001b[48;2;10;10;10m▀▀\u001b[0m\u001b[23;72H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[23;74H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀▀▀▀\u001b[0m\u001b[23;79H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀▀▀▀\u001b[0m\u001b[23;84H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀▀▀▀\u001b[0m\u001b[23;89H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀▀▀▀\u001b[0m\u001b[24;54H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[24;74H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[26;36H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[26;37H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[27;39H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mpabilities: Mermaid sequence diagra, Graphviz architecture digraph, \u001b[0m\u001b[28;39H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mD2 \u001b[0m\u001b[28;42H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[28;42H\u001b[?25h\u001b[?2026l"] +[9.53344092,"o","\u001b[?2026h\u001b[?25l\u001b[28;42H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mpi\u001b[0m\u001b[0m\u001b[28;44H\u001b[?25h\u001b[?2026l"] +[9.555979923,"o","\u001b[?2026h\u001b[?25l\u001b[28;44H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mp\u001b[0m\u001b[0m\u001b[28;45H\u001b[?25h\u001b[?2026l"] +[9.572433632,"o","\u001b[?2026h\u001b[?25l\u001b[28;45H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[28;46H\u001b[?25h\u001b[?2026l"] +[9.588578131,"o","\u001b[?2026h\u001b[?25l\u001b[28;46H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ml\u001b[0m\u001b[0m\u001b[28;47H\u001b[?25h\u001b[?2026l"] +[9.605178765,"o","\u001b[?2026h\u001b[?25l\u001b[28;47H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[28;48H\u001b[?25h\u001b[?2026l"] +[9.621225475,"o","\u001b[?2026h\u001b[?25l\u001b[28;48H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mn\u001b[0m\u001b[0m\u001b[28;49H\u001b[?25h\u001b[?2026l"] +[9.63754306,"o","\u001b[?2026h\u001b[?25l\u001b[28;49H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[28;50H\u001b[?25h\u001b[?2026l"] +[9.761889894,"o","\u001b[?2026h\u001b[?25l\u001b[28;50H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m diagra\u001b[0m\u001b[0m\u001b[28;57H\u001b[?25h\u001b[?2026l"] +[9.778466795,"o","\u001b[?2026h\u001b[?25l\u001b[28;57H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mm, \u001b[0m\u001b[0m\u001b[28;60H\u001b[?25h\u001b[?2026l"] +[9.794894477,"o","\u001b[?2026h\u001b[?25l\u001b[28;60H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mm\u001b[0m\u001b[0m\u001b[28;61H\u001b[?25h\u001b[?2026l"] +[9.811161511,"o","\u001b[?2026h\u001b[?25l\u001b[28;61H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ma\u001b[0m\u001b[0m\u001b[28;62H\u001b[?25h\u001b[?2026l"] +[9.82702393,"o","\u001b[?2026h\u001b[?25l\u001b[28;62H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[28;63H\u001b[?25h\u001b[?2026l"] +[9.853739783,"o","\u001b[?2026h\u001b[?25l\u001b[28;63H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mh \u001b[0m\u001b[0m\u001b[28;65H\u001b[?25h\u001b[?2026l"] +[9.870143998,"o","\u001b[?2026h\u001b[?25l\u001b[28;65H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[28;66H\u001b[?25h\u001b[?2026l"] +[9.886211889,"o","\u001b[?2026h\u001b[?25l\u001b[28;66H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mq\u001b[0m\u001b[0m\u001b[28;67H\u001b[?25h\u001b[?2026l"] +[9.902260731,"o","\u001b[?2026h\u001b[?25l\u001b[28;67H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mu\u001b[0m\u001b[0m\u001b[28;68H\u001b[?25h\u001b[?2026l"] +[9.918357787,"o","\u001b[?2026h\u001b[?25l\u001b[28;68H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ma\u001b[0m\u001b[0m\u001b[28;69H\u001b[?25h\u001b[?2026l"] +[9.934754842,"o","\u001b[?2026h\u001b[?25l\u001b[28;69H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[28;70H\u001b[?25h\u001b[?2026l"] +[9.962658548,"o","\u001b[?2026h\u001b[?25l\u001b[28;70H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mio\u001b[0m\u001b[0m\u001b[28;72H\u001b[?25h\u001b[?2026l"] +[9.979241368,"o","\u001b[?2026h\u001b[?25l\u001b[28;72H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mn\u001b[0m\u001b[0m\u001b[28;73H\u001b[?25h\u001b[?2026l"] +[9.997783879,"o","\u001b[?2026h\u001b[?25l\u001b[28;73H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ms \u001b[0m\u001b[0m\u001b[28;75H\u001b[?25h\u001b[?2026l"] +[10.018544736,"o","\u001b[?2026h\u001b[?25l\u001b[28;75H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mw\u001b[0m\u001b[0m\u001b[28;76H\u001b[?25h\u001b[?2026l"] +[10.035063533,"o","\u001b[?2026h\u001b[?25l\u001b[28;76H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[28;77H\u001b[?25h\u001b[?2026l"] +[10.050724685,"o","\u001b[?2026h\u001b[?25l\u001b[28;77H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[28;78H\u001b[?25h\u001b[?2026l"] +[10.067527273,"o","\u001b[?2026h\u001b[?25l\u001b[28;78H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mh\u001b[0m\u001b[0m\u001b[28;79H\u001b[?25h\u001b[?2026l"] +[10.083505382,"o","\u001b[?2026h\u001b[?25l\u001b[28;79H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[28;80H\u001b[?25h\u001b[?2026l"] +[10.100438028,"o","\u001b[?2026h\u001b[?25l\u001b[28;80H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mc\u001b[0m\u001b[0m\u001b[28;81H\u001b[?25h\u001b[?2026l"] +[10.116912707000001,"o","\u001b[?2026h\u001b[?25l\u001b[28;81H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mro\u001b[0m\u001b[0m\u001b[28;83H\u001b[?25h\u001b[?2026l"] +[10.135026604,"o","\u001b[?2026h\u001b[?25l\u001b[28;83H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ms\u001b[0m\u001b[0m\u001b[28;84H\u001b[?25h\u001b[?2026l"] +[10.151268812,"o","\u001b[?2026h\u001b[?25l\u001b[28;84H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ms\u001b[0m\u001b[0m\u001b[28;85H\u001b[?25h\u001b[?2026l"] +[10.166933727,"o","\u001b[?2026h\u001b[?25l\u001b[28;85H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m-\u001b[0m\u001b[0m\u001b[28;86H\u001b[?25h\u001b[?2026l"] +[10.183139677,"o","\u001b[?2026h\u001b[?25l\u001b[28;86H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mr\u001b[0m\u001b[0m\u001b[28;87H\u001b[?25h\u001b[?2026l"] +[10.202477098,"o","\u001b[?2026h\u001b[?25l\u001b[28;87H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[28;88H\u001b[?25h\u001b[?2026l"] +[10.21850469,"o","\u001b[?2026h\u001b[?25l\u001b[28;88H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mf\u001b[0m\u001b[0m\u001b[28;89H\u001b[?25h\u001b[?2026l"] +[10.235024936,"o","\u001b[?2026h\u001b[?25l\u001b[28;89H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ms\u001b[0m\u001b[0m\u001b[28;90H\u001b[?25h\u001b[?2026l"] +[10.252164208,"o","\u001b[?2026h\u001b[?25l\u001b[28;90H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m,\u001b[0m\u001b[0m\u001b[28;91H\u001b[?25h\u001b[?2026l"] +[10.268609425,"o","\u001b[?2026h\u001b[?25l\u001b[28;91H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m B\u001b[0m\u001b[0m\u001b[28;93H\u001b[?25h\u001b[?2026l"] +[10.288753678,"o","\u001b[?2026h\u001b[?25l\u001b[28;93H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[28;94H\u001b[?25h\u001b[?2026l"] +[10.304794475,"o","\u001b[?2026h\u001b[?25l\u001b[28;94H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mb\u001b[0m\u001b[0m\u001b[28;95H\u001b[?25h\u001b[?2026l"] +[10.320574793,"o","\u001b[?2026h\u001b[?25l\u001b[28;95H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mT\u001b[0m\u001b[0m\u001b[28;96H\u001b[?25h\u001b[?2026l"] +[10.337059798,"o","\u001b[?2026h\u001b[?25l\u001b[28;96H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[28;97H\u001b[?25h\u001b[?2026l"] +[10.360911943,"o","\u001b[?2026h\u001b[?25l\u001b[28;97H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mX \u001b[0m\u001b[0m\u001b[28;99H\u001b[?25h\u001b[?2026l"] +[10.37752458,"o","\u001b[?2026h\u001b[?25l\u001b[28;99H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mc\u001b[0m\u001b[0m\u001b[28;100H\u001b[?25h\u001b[?2026l"] +[10.396068143,"o","\u001b[?2026h\u001b[?25l\u001b[28;100H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mit\u001b[0m\u001b[0m\u001b[28;102H\u001b[?25h\u001b[?2026l"] +[10.413541683,"o","\u001b[?2026h\u001b[?25l\u001b[28;102H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ma\u001b[0m\u001b[0m\u001b[28;103H\u001b[?25h\u001b[?2026l"] +[10.433745689,"o","\u001b[?2026h\u001b[?25l\u001b[28;103H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[28;104H\u001b[?25h\u001b[?2026l"] +[10.449915347,"o","\u001b[?2026h\u001b[?25l\u001b[28;104H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[28;105H\u001b[?25h\u001b[?2026l"] +[10.467323845,"o","\u001b[?2026h\u001b[?25l\u001b[28;105H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mo\u001b[0m\u001b[0m\u001b[28;106H\u001b[?25h\u001b[?2026l"] +[10.48341549,"o","\u001b[?2026h\u001b[?25l\u001b[28;106H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mn\u001b[0m\u001b[0m\u001b[28;107H\u001b[?25h\u001b[?2026l"] +[10.499572144,"o","\u001b[?2026h\u001b[?25l\u001b[28;107H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ms\u001b[0m\u001b[0m\u001b[28;108H\u001b[?25h\u001b[?2026l"] +[10.517468126,"o","\u001b[?2026h\u001b[?25l\u001b[28;108H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m,\u001b[0m\u001b[0m\u001b[28;109H\u001b[?25h\u001b[?2026l"] +[10.535329805,"o","\u001b[?2026h\u001b[?25l\u001b[29;39H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[30;39H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[30;45H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[30;47H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[30;62H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[31;36H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[31;37H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[31;39H\u001b[38;2;92;156;245m\u001b[48;2;30;30;30mBuild\u001b[0m\u001b[31;44H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[31;45H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30m·\u001b[0m\u001b[31;46H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[31;47H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mMiMo V2.5 Free\u001b[0m\u001b[31;61H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[31;62H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mOpenCode Zen\u001b[0m\u001b[31;74H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[32;36H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m╹\u001b[0m\u001b[32;37H\u001b[38;2;30;30;30m\u001b[48;2;10;10;10m▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\u001b[0m\u001b[33;36H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mtab \u001b[0m\u001b[33;40H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10magents\u001b[0m\u001b[33;48H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mctrl+p \u001b[0m\u001b[33;55H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mcommands\u001b[0m\u001b[36;46H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[37;46H\u001b[38;2;245;167;66m\u001b[48;2;10;10;10m● Tip \u001b[0m\u001b[37;52H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mThemes support dark/light variants for both modes\u001b[0m\u001b[53;6H\u001b[38;2;224;108;117m\u001b[48;2;10;10;10m⊙ \u001b[0m\u001b[53;8H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m2 MCP\u001b[0m\u001b[53;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m/status\u001b[0m\u001b[0m\u001b[29;40H\u001b[?25h\u001b[?2026l"] +[10.560716557,"o","\u001b[?2026h\u001b[?25l\u001b[29;40H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mti\u001b[0m\u001b[0m\u001b[29;42H\u001b[?25h\u001b[?2026l"] +[10.570526114,"o","\u001b[?2026h\u001b[?25l\u001b[29;42H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mk\u001b[0m\u001b[0m\u001b[29;43H\u001b[?25h\u001b[?2026l"] +[10.590777249,"o","\u001b[?2026h\u001b[?25l\u001b[29;43H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mz\u001b[0m\u001b[0m\u001b[29;44H\u001b[?25h\u001b[?2026l"] +[10.613711641,"o","\u001b[?2026h\u001b[?25l\u001b[29;44H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[29;45H\u001b[?25h\u001b[?2026l"] +[10.6276666,"o","\u001b[?2026h\u001b[?25l\u001b[29;45H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mdi\u001b[0m\u001b[0m\u001b[29;47H\u001b[?25h\u001b[?2026l"] +[10.644291343999999,"o","\u001b[?2026h\u001b[?25l\u001b[29;47H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ma\u001b[0m\u001b[0m\u001b[29;48H\u001b[?25h\u001b[?2026l"] +[10.660873744,"o","\u001b[?2026h\u001b[?25l\u001b[29;48H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mg\u001b[0m\u001b[0m\u001b[29;49H\u001b[?25h\u001b[?2026l"] +[10.680826941,"o","\u001b[?2026h\u001b[?25l\u001b[29;49H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mr\u001b[0m\u001b[0m\u001b[29;50H\u001b[?25h\u001b[?2026l"] +[10.697898957,"o","\u001b[?2026h\u001b[?25l\u001b[29;50H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ma\u001b[0m\u001b[0m\u001b[29;51H\u001b[?25h\u001b[?2026l"] +[10.714560507,"o","\u001b[?2026h\u001b[?25l\u001b[29;51H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mm\u001b[0m\u001b[0m\u001b[29;52H\u001b[?25h\u001b[?2026l"] +[10.730472906,"o","\u001b[?2026h\u001b[?25l\u001b[29;52H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m,\u001b[0m\u001b[0m\u001b[29;53H\u001b[?25h\u001b[?2026l"] +[10.74713676,"o","\u001b[?2026h\u001b[?25l\u001b[29;53H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[29;54H\u001b[?25h\u001b[?2026l"] +[10.763953132,"o","\u001b[?2026h\u001b[?25l\u001b[29;54H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[29;55H\u001b[?25h\u001b[?2026l"] +[10.779748344,"o","\u001b[?2026h\u001b[?25l\u001b[29;55H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mx\u001b[0m\u001b[0m\u001b[29;56H\u001b[?25h\u001b[?2026l"] +[10.796524084,"o","\u001b[?2026h\u001b[?25l\u001b[29;56H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ma\u001b[0m\u001b[0m\u001b[29;57H\u001b[?25h\u001b[?2026l"] +[10.815053069,"o","\u001b[?2026h\u001b[?25l\u001b[29;57H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mm\u001b[0m\u001b[0m\u001b[29;58H\u001b[?25h\u001b[?2026l"] +[10.829766872,"o","\u001b[?2026h\u001b[?25l\u001b[29;58H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mp\u001b[0m\u001b[0m\u001b[29;59H\u001b[?25h\u001b[?2026l"] +[10.847454809,"o","\u001b[?2026h\u001b[?25l\u001b[29;59H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ml\u001b[0m\u001b[0m\u001b[29;60H\u001b[?25h\u001b[?2026l"] +[10.868605493,"o","\u001b[?2026h\u001b[?25l\u001b[29;60H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mes\u001b[0m\u001b[0m\u001b[29;62H\u001b[?25h\u001b[?2026l"] +[10.884629752,"o","\u001b[?2026h\u001b[?25l\u001b[29;62H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[29;63H\u001b[?25h\u001b[?2026l"] +[10.9009092,"o","\u001b[?2026h\u001b[?25l\u001b[29;63H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mo\u001b[0m\u001b[0m\u001b[29;64H\u001b[?25h\u001b[?2026l"] +[10.918877412,"o","\u001b[?2026h\u001b[?25l\u001b[29;64H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mf\u001b[0m\u001b[0m\u001b[29;65H\u001b[?25h\u001b[?2026l"] +[10.945117509,"o","\u001b[?2026h\u001b[?25l\u001b[29;65H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m c\u001b[0m\u001b[0m\u001b[29;67H\u001b[?25h\u001b[?2026l"] +[10.971137424,"o","\u001b[?2026h\u001b[?25l\u001b[29;67H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mo\u001b[0m\u001b[0m\u001b[29;68H\u001b[?25h\u001b[?2026l"] +[10.994688237,"o","\u001b[?2026h\u001b[?25l\u001b[29;68H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30md\u001b[0m\u001b[0m\u001b[29;69H\u001b[?25h\u001b[?2026l"] +[11.004302701,"o","\u001b[?2026h\u001b[?25l\u001b[29;69H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[29;70H\u001b[?25h\u001b[?2026l"] +[11.023150125,"o","\u001b[?2026h\u001b[?25l\u001b[29;70H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m l\u001b[0m\u001b[0m\u001b[29;72H\u001b[?25h\u001b[?2026l"] +[11.039386719,"o","\u001b[?2026h\u001b[?25l\u001b[29;72H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[29;73H\u001b[?25h\u001b[?2026l"] +[11.055759191,"o","\u001b[?2026h\u001b[?25l\u001b[29;73H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ms\u001b[0m\u001b[0m\u001b[29;74H\u001b[?25h\u001b[?2026l"] +[11.072155426,"o","\u001b[?2026h\u001b[?25l\u001b[29;74H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[29;75H\u001b[?25h\u001b[?2026l"] +[11.08818182,"o","\u001b[?2026h\u001b[?25l\u001b[29;75H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[29;76H\u001b[?25h\u001b[?2026l"] +[11.104346794,"o","\u001b[?2026h\u001b[?25l\u001b[29;76H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mn\u001b[0m\u001b[0m\u001b[29;77H\u001b[?25h\u001b[?2026l"] +[11.120613421,"o","\u001b[?2026h\u001b[?25l\u001b[29;77H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mg\u001b[0m\u001b[0m\u001b[29;78H\u001b[?25h\u001b[?2026l"] +[11.136867315,"o","\u001b[?2026h\u001b[?25l\u001b[29;78H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ms\u001b[0m\u001b[0m\u001b[29;79H\u001b[?25h\u001b[?2026l"] +[11.15280531,"o","\u001b[?2026h\u001b[?25l\u001b[29;79H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m,\u001b[0m\u001b[0m\u001b[29;80H\u001b[?25h\u001b[?2026l"] +[11.168899422,"o","\u001b[?2026h\u001b[?25l\u001b[29;80H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[29;81H\u001b[?25h\u001b[?2026l"] +[11.185376525,"o","\u001b[?2026h\u001b[?25l\u001b[29;81H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[29;82H\u001b[?25h\u001b[?2026l"] +[11.201386279,"o","\u001b[?2026h\u001b[?25l\u001b[29;82H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mw\u001b[0m\u001b[0m\u001b[29;83H\u001b[?25h\u001b[?2026l"] +[11.217697889,"o","\u001b[?2026h\u001b[?25l\u001b[29;83H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mo\u001b[0m\u001b[0m\u001b[29;84H\u001b[?25h\u001b[?2026l"] +[11.233772202,"o","\u001b[?2026h\u001b[?25l\u001b[29;84H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[29;85H\u001b[?25h\u001b[?2026l"] +[11.249743752,"o","\u001b[?2026h\u001b[?25l\u001b[29;85H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mb\u001b[0m\u001b[0m\u001b[29;86H\u001b[?25h\u001b[?2026l"] +[11.266343067,"o","\u001b[?2026h\u001b[?25l\u001b[29;86H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mo\u001b[0m\u001b[0m\u001b[29;87H\u001b[?25h\u001b[?2026l"] +[11.283065316,"o","\u001b[?2026h\u001b[?25l\u001b[29;87H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mok\u001b[0m\u001b[0m\u001b[29;89H\u001b[?25h\u001b[?2026l"] +[11.299797069,"o","\u001b[?2026h\u001b[?25l\u001b[29;89H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[29;90H\u001b[?25h\u001b[?2026l"] +[11.316023112,"o","\u001b[?2026h\u001b[?25l\u001b[29;90H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ma\u001b[0m\u001b[0m\u001b[29;91H\u001b[?25h\u001b[?2026l"] +[11.335517348,"o","\u001b[?2026h\u001b[?25l\u001b[29;91H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mb\u001b[0m\u001b[0m\u001b[29;92H\u001b[?25h\u001b[?2026l"] +[11.351452007,"o","\u001b[?2026h\u001b[?25l\u001b[29;92H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ms\u001b[0m\u001b[0m\u001b[29;93H\u001b[?25h\u001b[?2026l"] +[11.36782299,"o","\u001b[?2026h\u001b[?25l\u001b[29;93H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[29;94H\u001b[?25h\u001b[?2026l"] +[11.384109153,"o","\u001b[?2026h\u001b[?25l\u001b[29;94H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[29;95H\u001b[?25h\u001b[?2026l"] +[11.400118607,"o","\u001b[?2026h\u001b[?25l\u001b[29;95H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ma\u001b[0m\u001b[0m\u001b[29;96H\u001b[?25h\u001b[?2026l"] +[11.416765518,"o","\u001b[?2026h\u001b[?25l\u001b[29;96H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mb\u001b[0m\u001b[0m\u001b[29;97H\u001b[?25h\u001b[?2026l"] +[11.434273614,"o","\u001b[?2026h\u001b[?25l\u001b[29;97H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mle\u001b[0m\u001b[0m\u001b[29;99H\u001b[?25h\u001b[?2026l"] +[11.450709107,"o","\u001b[?2026h\u001b[?25l\u001b[29;99H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ms\u001b[0m\u001b[0m\u001b[29;100H\u001b[?25h\u001b[?2026l"] +[11.472549446,"o","\u001b[?2026h\u001b[?25l\u001b[29;100H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m,\u001b[0m\u001b[0m\u001b[29;101H\u001b[?25h\u001b[?2026l"] +[11.489074118,"o","\u001b[?2026h\u001b[?25l\u001b[29;101H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[29;102H\u001b[?25h\u001b[?2026l"] +[11.506504543,"o","\u001b[?2026h\u001b[?25l\u001b[29;102H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[29;103H\u001b[?25h\u001b[?2026l"] +[11.521974275,"o","\u001b[?2026h\u001b[?25l\u001b[29;103H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mab\u001b[0m\u001b[0m\u001b[29;105H\u001b[?25h\u001b[?2026l"] +[11.538829584,"o","\u001b[?2026h\u001b[?25l\u001b[29;105H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ml\u001b[0m\u001b[0m\u001b[29;106H\u001b[?25h\u001b[?2026l"] +[11.555082746,"o","\u001b[?2026h\u001b[?25l\u001b[29;106H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[29;107H\u001b[?25h\u001b[?2026l"] +[11.571089781,"o","\u001b[?2026h\u001b[?25l\u001b[29;107H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[29;108H\u001b[?25h\u001b[?2026l"] +[11.587294325,"o","\u001b[?2026h\u001b[?25l\u001b[29;108H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mo\u001b[0m\u001b[0m\u001b[29;109H\u001b[?25h\u001b[?2026l"] +[11.603585841,"o","\u001b[?2026h\u001b[?25l\u001b[19;54H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;74H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m ▄ \u001b[0m\u001b[20;54H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m█▀▀█\u001b[0m\u001b[20;59H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m█▀▀█\u001b[0m\u001b[20;64H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m█▀▀█\u001b[0m\u001b[20;69H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m█▀▀▄\u001b[0m\u001b[20;74H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m█▀▀▀\u001b[0m\u001b[20;79H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m█▀▀█\u001b[0m\u001b[20;84H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m█▀▀█\u001b[0m\u001b[20;89H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m█▀▀█\u001b[0m\u001b[21;55H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m \u001b[0m\u001b[21;60H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m \u001b[0m\u001b[21;65H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m▀▀▀\u001b[0m\u001b[21;70H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m \u001b[0m\u001b[21;72H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m█\u001b[0m\u001b[21;75H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m \u001b[0m\u001b[21;80H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m \u001b[0m\u001b[21;85H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m \u001b[0m\u001b[21;90H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m▀▀▀\u001b[0m\u001b[22;54H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀▀▀▀\u001b[0m\u001b[22;60H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀▀▀\u001b[0m\u001b[22;64H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀▀▀▀\u001b[0m\u001b[22;69H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[22;70H\u001b[38;2;40;40;40m\u001b[48;2;10;10;10m▀▀\u001b[0m\u001b[22;72H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[22;74H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀▀▀▀\u001b[0m\u001b[22;79H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀▀▀▀\u001b[0m\u001b[22;84H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀▀▀▀\u001b[0m\u001b[22;89H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀▀▀▀\u001b[0m\u001b[23;54H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[23;74H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[25;36H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[25;37H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[26;39H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mpabilities: Mermaid sequence diagra, Graphviz architecture digraph, \u001b[0m\u001b[27;39H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mD2 pipeline\u001b[0m\u001b[27;51H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mdiagram, math equations with cross-\u001b[0m\u001b[27;87H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mefs, BibT\u001b[0m\u001b[27;97H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mX cit\u001b[0m\u001b[27;103H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mtions,\u001b[0m\u001b[28;39H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m tikz diagram, ex\u001b[0m\u001b[28;58H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mples of code li\u001b[0m\u001b[28;74H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mtings, two booktabs tables, table \u001b[0m\u001b[28;108H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[29;39H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mof\u001b[0m\u001b[29;41H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[29;41H\u001b[?25h\u001b[?2026l"] +[11.621681124,"o","\u001b[?2026h\u001b[?25l\u001b[29;41H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[29;42H\u001b[?25h\u001b[?2026l"] +[11.637640257,"o","\u001b[?2026h\u001b[?25l\u001b[29;42H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mc\u001b[0m\u001b[0m\u001b[29;43H\u001b[?25h\u001b[?2026l"] +[11.655630238,"o","\u001b[?2026h\u001b[?25l\u001b[29;43H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mon\u001b[0m\u001b[0m\u001b[29;45H\u001b[?25h\u001b[?2026l"] +[11.67218443,"o","\u001b[?2026h\u001b[?25l\u001b[29;45H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[29;46H\u001b[?25h\u001b[?2026l"] +[11.68829032,"o","\u001b[?2026h\u001b[?25l\u001b[29;46H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[29;47H\u001b[?25h\u001b[?2026l"] +[11.704641794,"o","\u001b[?2026h\u001b[?25l\u001b[29;47H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mn\u001b[0m\u001b[0m\u001b[29;48H\u001b[?25h\u001b[?2026l"] +[11.720725123,"o","\u001b[?2026h\u001b[?25l\u001b[29;48H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[29;49H\u001b[?25h\u001b[?2026l"] +[11.737112264,"o","\u001b[?2026h\u001b[?25l\u001b[29;49H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ms\u001b[0m\u001b[0m\u001b[29;50H\u001b[?25h\u001b[?2026l"] +[11.753698162,"o","\u001b[?2026h\u001b[?25l\u001b[29;50H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m,\u001b[0m\u001b[0m\u001b[29;51H\u001b[?25h\u001b[?2026l"] +[11.769805249000001,"o","\u001b[?2026h\u001b[?25l\u001b[29;51H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[29;52H\u001b[?25h\u001b[?2026l"] +[11.786282361,"o","\u001b[?2026h\u001b[?25l\u001b[29;52H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ma\u001b[0m\u001b[0m\u001b[29;53H\u001b[?25h\u001b[?2026l"] +[11.802605197,"o","\u001b[?2026h\u001b[?25l\u001b[29;53H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mn\u001b[0m\u001b[0m\u001b[29;54H\u001b[?25h\u001b[?2026l"] +[11.819838798,"o","\u001b[?2026h\u001b[?25l\u001b[29;54H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30md \u001b[0m\u001b[0m\u001b[29;56H\u001b[?25h\u001b[?2026l"] +[11.836702041,"o","\u001b[?2026h\u001b[?25l\u001b[29;56H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ml\u001b[0m\u001b[0m\u001b[29;57H\u001b[?25h\u001b[?2026l"] +[11.855008812,"o","\u001b[?2026h\u001b[?25l\u001b[29;57H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[29;58H\u001b[?25h\u001b[?2026l"] +[11.878429929,"o","\u001b[?2026h\u001b[?25l\u001b[29;58H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ms\u001b[0m\u001b[0m\u001b[29;59H\u001b[?25h\u001b[?2026l"] +[11.895888507,"o","\u001b[?2026h\u001b[?25l\u001b[29;59H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mts\u001b[0m\u001b[0m\u001b[29;61H\u001b[?25h\u001b[?2026l"] +[11.912207489,"o","\u001b[?2026h\u001b[?25l\u001b[29;61H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m.\u001b[0m\u001b[0m\u001b[29;62H\u001b[?25h\u001b[?2026l"] +[11.928273075,"o","\u001b[?2026h\u001b[?25l\u001b[29;62H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[29;63H\u001b[?25h\u001b[?2026l"] +[11.944618494,"o","\u001b[?2026h\u001b[?25l\u001b[29;63H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mC\u001b[0m\u001b[0m\u001b[29;64H\u001b[?25h\u001b[?2026l"] +[11.960587553,"o","\u001b[?2026h\u001b[?25l\u001b[29;64H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mo\u001b[0m\u001b[0m\u001b[29;65H\u001b[?25h\u001b[?2026l"] +[11.97694595,"o","\u001b[?2026h\u001b[?25l\u001b[29;65H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mm\u001b[0m\u001b[0m\u001b[29;66H\u001b[?25h\u001b[?2026l"] +[11.993104272,"o","\u001b[?2026h\u001b[?25l\u001b[29;66H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mp\u001b[0m\u001b[0m\u001b[29;67H\u001b[?25h\u001b[?2026l"] +[12.009943025,"o","\u001b[?2026h\u001b[?25l\u001b[29;67H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[29;68H\u001b[?25h\u001b[?2026l"] +[12.030859267,"o","\u001b[?2026h\u001b[?25l\u001b[29;68H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ml\u001b[0m\u001b[0m\u001b[29;69H\u001b[?25h\u001b[?2026l"] +[12.04914661,"o","\u001b[?2026h\u001b[?25l\u001b[29;69H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me \u001b[0m\u001b[0m\u001b[29;71H\u001b[?25h\u001b[?2026l"] +[12.065004103,"o","\u001b[?2026h\u001b[?25l\u001b[29;71H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mw\u001b[0m\u001b[0m\u001b[29;72H\u001b[?25h\u001b[?2026l"] +[12.081153646,"o","\u001b[?2026h\u001b[?25l\u001b[29;72H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[29;73H\u001b[?25h\u001b[?2026l"] +[12.097019747,"o","\u001b[?2026h\u001b[?25l\u001b[29;73H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[29;74H\u001b[?25h\u001b[?2026l"] +[12.113415971,"o","\u001b[?2026h\u001b[?25l\u001b[29;74H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mh\u001b[0m\u001b[0m\u001b[29;75H\u001b[?25h\u001b[?2026l"] +[12.129460891,"o","\u001b[?2026h\u001b[?25l\u001b[29;75H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[29;76H\u001b[?25h\u001b[?2026l"] +[12.146062434,"o","\u001b[?2026h\u001b[?25l\u001b[29;76H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[29;77H\u001b[?25h\u001b[?2026l"] +[12.162210463,"o","\u001b[?2026h\u001b[?25l\u001b[29;77H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[29;78H\u001b[?25h\u001b[?2026l"] +[12.178776689,"o","\u001b[?2026h\u001b[?25l\u001b[29;78H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mx\u001b[0m\u001b[0m\u001b[29;79H\u001b[?25h\u001b[?2026l"] +[12.194913988,"o","\u001b[?2026h\u001b[?25l\u001b[29;79H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mf\u001b[0m\u001b[0m\u001b[29;80H\u001b[?25h\u001b[?2026l"] +[12.211611846,"o","\u001b[?2026h\u001b[?25l\u001b[29;80H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mo\u001b[0m\u001b[0m\u001b[29;81H\u001b[?25h\u001b[?2026l"] +[12.227526829,"o","\u001b[?2026h\u001b[?25l\u001b[29;81H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mr\u001b[0m\u001b[0m\u001b[29;82H\u001b[?25h\u001b[?2026l"] +[12.244254748,"o","\u001b[?2026h\u001b[?25l\u001b[29;82H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mg\u001b[0m\u001b[0m\u001b[29;83H\u001b[?25h\u001b[?2026l"] +[12.260624224,"o","\u001b[?2026h\u001b[?25l\u001b[29;83H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[29;84H\u001b[?25h\u001b[?2026l"] +[12.276539551,"o","\u001b[?2026h\u001b[?25l\u001b[29;84H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m \u001b[0m\u001b[0m\u001b[29;85H\u001b[?25h\u001b[?2026l"] +[12.293062613,"o","\u001b[?2026h\u001b[?25l\u001b[29;85H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mbu\u001b[0m\u001b[0m\u001b[29;87H\u001b[?25h\u001b[?2026l"] +[12.309362583,"o","\u001b[?2026h\u001b[?25l\u001b[29;87H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[29;88H\u001b[?25h\u001b[?2026l"] +[12.325299365,"o","\u001b[?2026h\u001b[?25l\u001b[29;88H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30ml\u001b[0m\u001b[0m\u001b[29;89H\u001b[?25h\u001b[?2026l"] +[12.341538464,"o","\u001b[?2026h\u001b[?25l\u001b[29;89H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30md\u001b[0m\u001b[0m\u001b[29;90H\u001b[?25h\u001b[?2026l"] +[12.357658495999999,"o","\u001b[?2026h\u001b[?25l\u001b[29;90H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m.\u001b[0m\u001b[0m\u001b[29;91H\u001b[?25h\u001b[?2026l"] +[12.57114898,"o","\u001b[?2026h\u001b[?25l\u001b[19;54H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;74H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;54H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;74H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[21;54H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[21;59H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[21;64H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[21;69H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m \u001b[0m\u001b[21;74H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[21;79H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[21;84H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m ▄\u001b[0m\u001b[21;89H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m \u001b[0m\u001b[22;54H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m█\u001b[0m\u001b[22;57H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m█\u001b[0m\u001b[22;62H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m█\u001b[0m\u001b[22;64H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m█\u001b[0m\u001b[22;67H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m█\u001b[0m\u001b[22;69H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m█▀▀▄\u001b[0m\u001b[22;74H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m█\u001b[0m\u001b[22;79H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m█\u001b[0m\u001b[22;82H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m█\u001b[0m\u001b[22;84H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m█\u001b[0m\u001b[22;87H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m█\u001b[0m\u001b[22;89H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m█\u001b[0m\u001b[22;92H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m█\u001b[0m\u001b[23;54H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m█\u001b[0m\u001b[23;55H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m \u001b[0m\u001b[23;57H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m█ █\u001b[0m\u001b[23;60H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m \u001b[0m\u001b[23;62H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m█ █\u001b[0m\u001b[23;65H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m▀▀▀\u001b[0m\u001b[23;68H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m █\u001b[0m\u001b[23;70H\u001b[38;2;128;128;128m\u001b[48;2;40;40;40m \u001b[0m\u001b[23;72H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m█\u001b[0m\u001b[23;74H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m█\u001b[0m\u001b[23;75H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m \u001b[0m\u001b[23;78H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m █\u001b[0m\u001b[23;80H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m \u001b[0m\u001b[23;82H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m█ █\u001b[0m\u001b[23;85H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m \u001b[0m\u001b[23;87H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m█ █\u001b[0m\u001b[23;90H\u001b[38;2;238;238;238m\u001b[48;2;67;67;67m\u001b[1m▀▀▀\u001b[0m\u001b[24;54H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀\u001b[0m\u001b[24;70H\u001b[38;2;40;40;40m\u001b[48;2;10;10;10m▀▀\u001b[0m\u001b[24;72H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m▀\u001b[0m\u001b[24;74H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m\u001b[1m▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀\u001b[0m\u001b[25;36H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[26;36H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[27;39H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[28;39H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mAsk anything... \"Fix broken tests\"\u001b[0m\u001b[28;73H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[29;39H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[30;39H\u001b[38;2;92;156;245m\u001b[48;2;30;30;30mBuild\u001b[0m\u001b[30;45H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30m·\u001b[0m\u001b[30;47H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mMiMo V2.5 Free\u001b[0m\u001b[30;62H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mOpenCode Zen\u001b[0m\u001b[31;36H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m╹\u001b[0m\u001b[31;37H\u001b[38;2;30;30;30m\u001b[48;2;10;10;10m▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\u001b[0m\u001b[32;36H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mtab \u001b[0m\u001b[32;40H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10magents\u001b[0m\u001b[32;46H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[32;48H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mctrl+p \u001b[0m\u001b[32;55H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mcommands\u001b[0m\u001b[32;63H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m "] +[12.571167836,"o"," \u001b[0m\u001b[33;36H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[33;48H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[36;46H\u001b[38;2;245;167;66m\u001b[48;2;10;10;10m● Tip \u001b[0m\u001b[36;52H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mThemes support dark/light variants for both modes\u001b[0m\u001b[37;46H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[0m\u001b[28;39H\u001b[?25h\u001b[?2026l"] +[12.640946248,"o","\u001b]0;OpenCode\u0007"] +[12.653438507,"o","\u001b[?2026h\u001b[?25l\u001b[1;3H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[1;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[1;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[2;3H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[2;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[2;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mpabilities: Mermaid sequence diagra, Graphviz architecture digraph, D2 pipeline diagram, math \u001b[0m\u001b[2;100H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[2;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[2;105H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m\u001b[1mNew session - 2026-08-13T21:59:15.\u001b[0m\u001b[2;139H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[2;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[2;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[3;3H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[3;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[3;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mequations with cross-refs, BibTeX citations, tikz diagram, examples of code listings, two \u001b[0m\u001b[3;96H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[3;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[3;105H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m\u001b[1m663Z\u001b[0m\u001b[3;109H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[3;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[3;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[4;3H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[4;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[4;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mbooktabs tables, table of contents, and lists. Compile with texforge build.\u001b[0m\u001b[4;81H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[4;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[4;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[4;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[5;3H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[5;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[5;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[5;105H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m\u001b[1mContext\u001b[0m\u001b[5;112H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[5;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[5;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[6;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[6;105H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m0 tokens\u001b[0m\u001b[6;113H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[6;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[6;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[7;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[7;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[7;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[7;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[7;105H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m0% used\u001b[0m\u001b[7;112H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[7;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[7;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[8;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[8;105H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m$0.00 spent\u001b[0m\u001b[8;116H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[8;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[8;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[9;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[9;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[9;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[10;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[10;105H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m▼\u001b[0m\u001b[10;106H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[10;107H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m\u001b[1mMCP\u001b[0m\u001b[10;110H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[10;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[10;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[11;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[11;105H\u001b[38;2;127;216;143m\u001b[48;2;20;20;20m•\u001b[0m\u001b[11;106H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[11;107H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mcanopy \u001b[0m\u001b[11;114H\u001b[38;2;128;128;128m\u001b[48"] +[12.653479784,"o",";2;20;20;20mConnected\u001b[0m\u001b[11;123H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[11;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[11;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[12;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[12;105H\u001b[38;2;224;108;117m\u001b[48;2;20;20;20m•\u001b[0m\u001b[12;106H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[12;107H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mfetch \u001b[0m\u001b[12;113H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m\u001b[3mMCP error -32000: Connection \u001b[0m\u001b[12;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[12;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[13;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[13;107H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m\u001b[3mclosed\u001b[0m\u001b[13;113H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[13;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[13;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[14;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[14;105H\u001b[38;2;127;216;143m\u001b[48;2;20;20;20m•\u001b[0m\u001b[14;106H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[14;107H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mfilesystem \u001b[0m\u001b[14;118H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mConnected\u001b[0m\u001b[14;127H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[14;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[14;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[15;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[15;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[15;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[16;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[16;105H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m\u001b[1mLSP\u001b[0m\u001b[16;108H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[16;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[16;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[17;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[17;105H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mLSPs are disabled\u001b[0m\u001b[17;122H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[17;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[17;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[18;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[18;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[18;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[19;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[19;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[19;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[20;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[20;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[20;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[21;54H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[21;74H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[21;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[21;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[21;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[22;54H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[22;74H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[22;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[22;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[22;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[23;54H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[23;74H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[23;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[23;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[23;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[24;54H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[24;74H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[24;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[24;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[24;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[25;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[25;142H\u001b[38;2;96;96;96m\u001b[48;2;10;10;10m█\u001b[0m\u001b[25;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[26;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[26;143H\u001b[38;2;255;255;"] +[12.653493516,"o","255m\u001b[48;2;20;20;20m \u001b[27;36H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[27;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[27;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[28;36H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[28;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[28;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[29;36H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[29;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[29;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[30;36H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[30;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[30;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[31;36H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[31;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[31;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[32;36H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[32;48H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[32;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[32;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[33;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[33;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[34;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[34;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[35;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[35;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[36;46H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[36;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[36;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[37;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[37;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[38;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[38;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[39;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[39;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[40;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[40;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[41;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[41;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[42;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[42;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[43;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[43;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[44;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[44;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[45;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[45;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[46;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[46;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[47;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[47;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[48;3H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[48;4H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[48;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m"] +[12.653542155,"o"," \u001b[0m\u001b[48;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[49;3H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[49;4H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[49;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[49;143H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[50;3H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[50;4H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[50;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[51;3H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[51;4H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[51;6H\u001b[38;2;92;156;245m\u001b[48;2;30;30;30mBuild\u001b[0m\u001b[51;11H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[51;12H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30m·\u001b[0m\u001b[51;13H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[51;14H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mMiMo V2.5 Free\u001b[0m\u001b[51;28H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[51;29H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mOpenCode Zen\u001b[0m\u001b[51;41H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[51;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[51;105H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m/\u001b[0m\u001b[51;106H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m~\u001b[0m\u001b[51;107H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[52;3H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m╹\u001b[0m\u001b[52;4H\u001b[38;2;30;30;30m\u001b[48;2;10;10;10m▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\u001b[0m\u001b[52;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[53;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[53;4H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;12H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[53;14H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mesc \u001b[0m\u001b[53;18H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10minterrupt\u001b[0m\u001b[53;74H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mtab \u001b[0m\u001b[53;78H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10magents\u001b[0m\u001b[53;86H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mctrl+p \u001b[0m\u001b[53;93H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mcommands\u001b[0m\u001b[53;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[53;105H\u001b[38;2;127;216;143m\u001b[48;2;20;20;20m•\u001b[0m\u001b[53;106H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m \u001b[0m\u001b[53;107H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m\u001b[1mOpen\u001b[0m\u001b[53;111H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m\u001b[1mCode\u001b[0m\u001b[53;115H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m 1.18.18\u001b[0m\u001b[53;123H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[54;103H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[12.663751619,"o","\u001b[?2026h\u001b[?25l\u001b[1;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[2;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[2;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[3;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mpabil\u001b[0m\u001b[3;12H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mties: Mermaid sequence diagra, Graphviz architecture\u001b[0m\u001b[3;65H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mdigraph,\u001b[0m\u001b[3;74H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mD2\u001b[0m\u001b[3;77H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mpip\u001b[0m\u001b[3;81H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mline diagram, math \u001b[0m\u001b[3;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[4;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mequa\u001b[0m\u001b[4;11H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mions with cross-refs, BibTeX citation\u001b[0m\u001b[4;49H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m, tikz d\u001b[0m\u001b[4;58H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20magram, examples of co\u001b[0m\u001b[4;80H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20me listings, two \u001b[0m\u001b[4;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[5;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mbooktabs tables, table of contents, and lists. Compile with texforge build.\u001b[0m\u001b[5;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[6;3H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[6;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[6;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[7;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[7;9H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[7;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[8;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[8;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[8;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[8;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[9;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[10;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[11;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[12;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[13;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[14;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[15;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[16;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[17;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[18;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[19;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[20;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[21;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[22;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[23;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[24;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[25;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[26;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[27;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[28;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[29;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[30;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[31;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[32;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[33;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[34;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[35;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[36;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[37;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[38;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[39;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[40;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[41;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[42;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[43;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[44;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[45;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[46;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[47;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[48;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[49;142H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[12.679530044,"o","\u001b]0;OpenCode\u0007"] +[12.681040858,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;30;45;66m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[12.715778384,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;35;54;81m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[12.755877004,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;40;63;95m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[12.79871148,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;44;71;109m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[12.84176602,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;49;80;122m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[12.885077477,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;54;89;137m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;54;89;137m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[12.927625777,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;59;98;151m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[53;6H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[13.014532789,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;55;91;140m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[53;7H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[13.058499359,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;51;84;129m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[53;8H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[13.100496331,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[53;9H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[13.146259012,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;44;70;107m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;10H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[13.184415115,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;40;64;97m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;11H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[13.227772946,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[13.271003198,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[13.314140921,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;29;43;63m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[13.354442489,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;10H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[13.398144833,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;31;47;69m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[53;9H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[13.441617061,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[53;8H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[13.484259052,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;42;67;101m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[53;7H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[13.527395092999999,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[53;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[13.570634758,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[53;5H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[13.613000003,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;59;98;151m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[13.703124263,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;58;95;147m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[13.746543373,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;57;94;145m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[13.789552583999999,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;56;91;141m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[13.832060715,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;55;90;138m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[13.875832719,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[13.918892624,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;52;86;132m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[13.961624432,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;51;83;128m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[14.004836044,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;50;81;124m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[14.047090042,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;49;79;122m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[14.088908374,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[14.130767552,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;47;75;115m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[14.172971072,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;73;111m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[14.215446585,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;44;71;109m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[14.258258716,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;43;69;105m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[14.300660273,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;42;67;101m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[14.343077464,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;41;65;98m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[14.386222625,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;40;63;95m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[14.428604131,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;39;61;92m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[14.470491877,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;37;59;88m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[14.513635893,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[14.556073343,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;35;55;82m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[14.59842888,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;34;52;78m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[14.641090341,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;51;75m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[14.683728685,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;32;48;72m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[14.725833119,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;31;47;69m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[14.768079494,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;29;44;65m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[14.811288195,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;28;43;63m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[14.854365351,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;27;40;59m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[14.897353212,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;26;38;55m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[14.940560049,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[14.986280115,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;30;45;66m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[15.024570446,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;35;54;81m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[15.066972512,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;40;63;95m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[15.110058487,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;44;71;109m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[15.152349979,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;49;80;122m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[15.194793708,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;54;89;137m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;54;89;137m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[15.237035551,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;59;98;151m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[53;6H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[15.322269989,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;55;91;140m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[53;7H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[15.364754276,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;51;84;129m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[53;8H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[15.410338153,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[53;9H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[15.452059732,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;44;70;107m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;10H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[15.494183744,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;40;64;97m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;11H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[15.536079997,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[15.578896823000001,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[15.621613387,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;29;43;63m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[15.661831441,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;10H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[15.704453007,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;31;47;69m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[53;9H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[15.747254796,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[53;8H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[15.789433396,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;42;67;101m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[53;7H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[15.831928038000001,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[53;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[15.874313473,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[53;5H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[15.916687713,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;59;98;151m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[16.000692722,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;58;95;147m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[16.04330314,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;57;94;145m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[16.085760739,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;56;91;141m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[16.129028304,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;55;90;138m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[16.170865574,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[16.21398956,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;52;86;132m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[16.257158341,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;51;83;128m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[16.299150543,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;50;81;124m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[16.34124726,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;49;79;122m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[16.383826631,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[16.426439237,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;47;75;115m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[16.468931342,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;73;111m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[16.511487136,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;44;71;109m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[16.5534508,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;43;69;105m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[16.595060377,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;42;67;101m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[16.637249925,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;41;65;98m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[16.679955146,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;40;63;95m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[16.722723674,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;39;61;92m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[16.765968532,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;37;59;88m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[16.806154826,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[16.849903752,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;35;55;82m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[16.892520778,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;34;52;78m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[16.935978385,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;51;75m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[16.978806796,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;32;48;72m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.021644968,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;31;47;69m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.065370453,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;29;44;65m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.108757252,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;28;43;63m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.15415931,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;27;40;59m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.197507137,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;26;38;55m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.24107079,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.285456786,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;30;45;66m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.329627358,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;35;54;81m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.372460491,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;40;63;95m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.411675244,"o","\u001b[?2026h\u001b[?25l\u001b[8;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠋\u001b[0m\u001b[8;7H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[8;8H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThinking\u001b[0m\u001b[8;16H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[12;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[12;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[12;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.421488091,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;44;71;109m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.461459019,"o","\u001b[?2026h\u001b[?25l\u001b[8;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠙\u001b[0m\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;49;80;122m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.51252381,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;54;89;137m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;54;89;137m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.558219076,"o","\u001b[?2026h\u001b[?25l\u001b[8;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠹\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.578080679,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;59;98;151m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[53;6H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.616395001,"o","\u001b[?2026h\u001b[?25l\u001b[10;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mThe user wants me to create a LaTeX document that demonstrates various capabilities including \u001b[0m\u001b[11;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mMermaid sequence diagrams, Graphviz architecture diagrams, D2 pipeline diagrams, math equations\u001b[0m\u001b[12;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[12;9H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[13;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[13;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[13;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.634220412,"o","\u001b[?2026h\u001b[?25l\u001b[8;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠸\u001b[0m\u001b[11;92H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[12;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mequations with cross-references\u001b[0m\u001b[13;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[13;9H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[14;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[14;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[14;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.65087158,"o","\u001b[?2026h\u001b[?25l\u001b[12;37H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m, BibTeX citations\u001b[0m\u001b[53;4H\u001b[38;2;55;91;140m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[53;7H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.68374038,"o","\u001b[?2026h\u001b[?25l\u001b[12;55H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m, tikz diagrams\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.698949935999998,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;51;84;129m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[53;8H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.717159509,"o","\u001b[?2026h\u001b[?25l\u001b[8;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠼\u001b[0m\u001b[12;70H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m, code listings,\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.750270592,"o","\u001b[?2026h\u001b[?25l\u001b[12;86H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m booktabs \u001b[0m\u001b[13;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mtables,\u001b[0m\u001b[14;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[14;9H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[15;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[15;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[15;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[53;9H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.783103034,"o","\u001b[?2026h\u001b[?25l\u001b[13;13H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m table of contents,\u001b[0m\u001b[53;4H\u001b[38;2;44;70;107m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;10H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.798986656,"o","\u001b[?2026h\u001b[?25l\u001b[8;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠴\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.821778207,"o","\u001b[?2026h\u001b[?25l\u001b[13;32H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m and lists. They\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.845151694,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;40;64;97m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;11H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.855744905,"o","\u001b[?2026h\u001b[?25l\u001b[13;48H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m want me to compile\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.871021875,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.889112549,"o","\u001b[?2026h\u001b[?25l\u001b[8;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠦\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.925829195,"o","\u001b[?2026h\u001b[?25l\u001b[13;67H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m it with texforge\u001b[0m\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.942348312,"o","\u001b[?2026h\u001b[?25l\u001b[13;84H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m.\u001b[0m\u001b[15;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mLet me first load\u001b[0m\u001b[15;23H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[17;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[17;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[17;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.960526658,"o","\u001b[?2026h\u001b[?25l\u001b[15;23H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m the texforge skill\u001b[0m\u001b[53;4H\u001b[38;2;29;43;63m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[17.977269774,"o","\u001b[?2026h\u001b[?25l\u001b[8;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠧\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[18.009160621,"o","\u001b[?2026h\u001b[?25l\u001b[15;42H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m to understand how to\u001b[0m\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;10H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[18.043111436,"o","\u001b[?2026h\u001b[?25l\u001b[15;63H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m use it properly\u001b[0m\u001b[53;4H\u001b[38;2;31;47;69m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[53;9H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[18.059283971,"o","\u001b[?2026h\u001b[?25l\u001b[8;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠇\u001b[0m\u001b[15;79H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m.\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[18.076220345,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[53;8H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[18.09922922,"o","\u001b[?2026h\u001b[?25l\u001b[8;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 737ms\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[18.148141596,"o","\u001b[?2026h\u001b[?25l\u001b[17;6H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m~ Loading skill...\u001b[0m\u001b[17;24H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[19;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[19;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[53;4H\u001b[38;2;42;67;101m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[53;7H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[18.176131638,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[53;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[18.216098301,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[53;5H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[18.260942314,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;59;98;151m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[18.343389173,"o","\u001b[?2026h\u001b[?25l\u001b[17;6H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m→\u001b[0m\u001b[17;7H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[17;8H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mSkill \"texforge\"\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[18.365789736,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;58;95;147m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[18.386586432,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;57;94;145m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[18.426078857,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;56;91;141m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[18.446600258,"o","\u001b[?2026h\u001b[?25l\u001b[6;105H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m41,477 tokens\u001b[0m\u001b[7;105H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m21% used\u001b[0m\u001b[53;73H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m41.5K (21%)\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[18.491157062,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;55;90;138m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[18.524520274,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[18.566895401,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;52;86;132m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[18.612509933,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;51;83;128m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[18.655445095,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;50;81;124m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[18.699604487,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;49;79;122m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[18.744763758,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[18.787510183,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;47;75;115m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[18.831007321,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;73;111m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[18.882404089,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;44;71;109m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[18.926109819,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;43;69;105m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[18.969466952,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;42;67;101m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[19.013690328,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;41;65;98m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[19.05593713,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;40;63;95m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[19.100584716,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;39;61;92m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[19.144669286,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;37;59;88m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[19.188006854,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[19.232033317,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;35;55;82m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[19.275304428,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;34;52;78m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[19.318593616,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;51;75m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[19.362014897999998,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;32;48;72m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[19.405891254,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;31;47;69m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[19.449517906,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;29;44;65m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[19.49379061,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;28;43;63m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[19.537174564,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;27;40;59m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[19.580672092,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;26;38;55m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[19.623783548,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[19.664292906,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;30;45;66m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[19.705262575,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;35;54;81m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[19.74785472,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;40;63;95m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[19.790934133,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;44;71;109m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[19.83502587,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;49;80;122m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[19.877884695,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;54;89;137m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;54;89;137m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[19.922524474,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;59;98;151m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[53;6H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[20.010020856,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;55;91;140m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[53;7H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[20.053971032,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;51;84;129m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[53;8H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[20.098629881,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[53;9H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[20.14135069,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;44;70;107m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;10H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[20.184269581,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;40;64;97m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;11H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[20.227529609,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[20.270639205,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[20.312850939,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;29;43;63m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[20.354887885,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;10H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[20.397946844,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;31;47;69m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[53;9H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[20.441153098,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[53;8H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[20.485317957,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;42;67;101m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[53;7H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[20.529507896,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[53;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[20.572799712,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[53;5H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[20.620541483,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;59;98;151m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[20.708160302,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;58;95;147m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[20.75100902,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;57;94;145m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[20.794663842,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;56;91;141m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[20.838767613,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;55;90;138m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[20.882179195,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[20.92662392,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;52;86;132m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[20.970963748,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;51;83;128m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.013221305,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;50;81;124m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.057216808,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;49;79;122m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.099477396,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.130481346,"o","\u001b[?2026h\u001b[?25l\u001b[19;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠋\u001b[0m\u001b[19;7H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;8H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThinking\u001b[0m\u001b[19;16H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[23;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[23;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[23;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.147518744,"o","\u001b[?2026h\u001b[?25l\u001b[21;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mNow I understand the\u001b[0m\u001b[53;4H\u001b[38;2;47;75;115m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.181376961,"o","\u001b[?2026h\u001b[?25l\u001b[21;26H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m texforge capabilities. The\u001b[0m\u001b[53;4H\u001b[38;2;45;73;111m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.198390088,"o","\u001b[?2026h\u001b[?25l\u001b[21;53H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m user wants a document\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.214463827,"o","\u001b[?2026h\u001b[?25l\u001b[19;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠙\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.230938409,"o","\u001b[?2026h\u001b[?25l\u001b[21;75H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m showcasing\u001b[0m\u001b[53;4H\u001b[38;2;44;71;109m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.247343293,"o","\u001b[?2026h\u001b[?25l\u001b[21;86H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m:\u001b[0m\u001b[22;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m1. \u001b[0m\u001b[22;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mMer\u001b[0m\u001b[23;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[23;9H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[24;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[24;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[24;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.28125737,"o","\u001b[?2026h\u001b[?25l\u001b[22;12H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mmaid sequence diagrams\u001b[0m\u001b[53;4H\u001b[38;2;43;69;105m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.296936582,"o","\u001b[?2026h\u001b[?25l\u001b[19;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠹\u001b[0m\u001b[23;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m2. \u001b[0m\u001b[23;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mGraphviz\u001b[0m\u001b[24;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[24;9H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[25;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[25;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[25;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.31505131,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;42;67;101m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.358248302,"o","\u001b[?2026h\u001b[?25l\u001b[23;17H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m architecture diagrams\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.379603868,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;41;65;98m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.397002948,"o","\u001b[?2026h\u001b[?25l\u001b[19;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠸\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.412779453,"o","\u001b[?2026h\u001b[?25l\u001b[24;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m3. \u001b[0m\u001b[24;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mD2 pipeline diagrams\u001b[0m\u001b[25;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[25;9H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[26;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[26;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[26;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[53;4H\u001b[38;2;40;63;95m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.452317741,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;39;61;92m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.473800534,"o","\u001b[?2026h\u001b[?25l\u001b[19;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠼\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.491251552,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;37;59;88m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.532398423,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.555031518,"o","\u001b[?2026h\u001b[?25l\u001b[19;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠴\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.576040116,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;35;55;82m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.619332084,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;34;52;78m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.641177894,"o","\u001b[?2026h\u001b[?25l\u001b[19;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠦\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.675476267,"o","\u001b[?2026h\u001b[?25l\u001b[25;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m4. \u001b[0m\u001b[25;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mMath equations\u001b[0m\u001b[26;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[26;9H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[27;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[27;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[27;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[53;4H\u001b[38;2;33;51;75m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.695043816,"o","\u001b[?2026h\u001b[?25l\u001b[25;23H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m with cross-refs\u001b[0m\u001b[26;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m5. \u001b[0m\u001b[26;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mBibTeX citations\u001b[0m\u001b[27;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m6. \u001b[0m\u001b[27;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mtikz diagrams\u001b[0m\u001b[27;22H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[28;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m7. \u001b[0m\u001b[28;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mCode listings\u001b[0m\u001b[29;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m8. \u001b[0m\u001b[29;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mTwo booktabs tables\u001b[0m\u001b[30;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m9.\u001b[0m\u001b[32;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[32;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[32;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.711809048,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;32;48;72m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.728559082,"o","\u001b[?2026h\u001b[?25l\u001b[19;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠧\u001b[0m\u001b[30;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m9. \u001b[0m\u001b[30;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mTable of contents\u001b[0m\u001b[31;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m10. \u001b[0m\u001b[31;10H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mLists\u001b[0m\u001b[32;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[32;9H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[33;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mLet me create\u001b[0m\u001b[35;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[35;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[35;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.744765805,"o","\u001b[?2026h\u001b[?25l\u001b[33;19H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m a new\u001b[0m\u001b[53;4H\u001b[38;2;31;47;69m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.781474865,"o","\u001b[?2026h\u001b[?25l\u001b[33;25H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m project and\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.798449297,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;29;44;65m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.815341945,"o","\u001b[?2026h\u001b[?25l\u001b[19;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠇\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.831236152,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;28;43;63m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.870457352,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;27;40;59m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.886369815,"o","\u001b[?2026h\u001b[?25l\u001b[19;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠏\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.903037321,"o","\u001b[?2026h\u001b[?25l\u001b[33;37H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m build the document.\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.920450787,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;26;38;55m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.957921283,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[21.979987507,"o","\u001b[?2026h\u001b[?25l\u001b[19;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠋\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[22.001848835,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;30;45;66m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[22.045564378,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;35;54;81m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[22.067995152,"o","\u001b[?2026h\u001b[?25l\u001b[19;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠙\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[22.08613718,"o","\u001b[?2026h\u001b[?25l\u001b[19;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 971ms\u001b[0m\u001b[53;4H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;40;63;95m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[22.111045337,"o","\u001b[?2026h\u001b[?25l\u001b[35;6H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m~ Updating todos...\u001b[0m\u001b[35;25H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[37;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[37;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[37;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[22.142989347,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;44;71;109m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[22.16976528,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;49;80;122m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[22.212989313,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;54;89;137m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;54;89;137m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[22.255614848,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;59;98;151m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[53;6H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[22.341790913,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;55;91;140m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[53;7H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[22.383805246,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;51;84;129m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[53;8H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[22.426627654,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[53;9H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[22.470805532,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;44;70;107m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;10H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[22.513537218,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;40;64;97m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;11H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[22.55692698,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[22.60151171,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[22.645261962,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;29;43;63m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[22.689441097,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;10H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[22.733220713,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;31;47;69m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[53;9H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[22.777071327,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[53;8H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[22.819079226,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;42;67;101m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[53;7H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[22.863745384,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[53;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[22.907341041,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[53;5H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[22.95016255,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;59;98;151m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[23.036893782,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;58;95;147m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[23.079776651,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;57;94;145m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[23.096481956,"o","\u001b[?2026h\u001b[?25l\u001b[19;105H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m▼\u001b[0m\u001b[19;107H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m\u001b[1mTodo\u001b[0m\u001b[20;105H\u001b[38;2;245;167;66m\u001b[48;2;20;20;20m[•] Create texforge project\u001b[0m\u001b[21;105H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] Write main.tex with all \u001b[0m\u001b[22;109H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mrequested capabilities\u001b[0m\u001b[23;105H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] Write bib/references.bib\u001b[0m\u001b[24;105H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] Run texforge build\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[23.114149934,"o","\u001b[?2026h\u001b[?25l\u001b[35;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[35;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[36;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[36;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[36;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m# Todos\u001b[0m\u001b[36;13H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[37;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[37;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[38;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[38;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[38;6H\u001b[38;2;245;167;66m\u001b[48;2;20;20;20m[•] Create texforge project\u001b[0m\u001b[38;33H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[39;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[39;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[39;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] Write main.tex with all requested capabilities\u001b[0m\u001b[39;56H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[40;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[40;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[40;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] Write bib/references.bib\u001b[0m\u001b[40;34H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[41;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[41;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[41;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] Run texforge build\u001b[0m\u001b[41;28H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[42;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[42;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[44;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[44;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[44;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[23.130419928,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;56;91;141m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[23.147048262,"o","\u001b[?2026h\u001b[?25l\u001b[6;106H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m3\u001b[0m\u001b[6;108H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m535\u001b[0m\u001b[7;106H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m2\u001b[0m\u001b[53;74H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m3\u001b[0m\u001b[53;81H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m2\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[23.164344134,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;55;90;138m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[23.208658656,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[23.252737037,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;52;86;132m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[23.296814705,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;51;83;128m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[23.33994785,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;50;81;124m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[23.384992372,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;49;79;122m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[23.429489822,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[23.472697107,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;47;75;115m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[23.515224395,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;73;111m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[23.558194963,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;44;71;109m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[23.601456454,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;43;69;105m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[23.644665717,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;42;67;101m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[23.687862408,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;41;65;98m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[23.730332274,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;40;63;95m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[23.774156162,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;39;61;92m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[23.817546442,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;37;59;88m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[23.860868655,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[23.90481352,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;35;55;82m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[23.94850693,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;34;52;78m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[23.991061456,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;51;75m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[24.035447926,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;32;48;72m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[24.081188446,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;31;47;69m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[24.125248161,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;29;44;65m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[24.168063786,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;28;43;63m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[24.212644056,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;27;40;59m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[24.255939963,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;26;38;55m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[24.299046375,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[24.342070285,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;30;45;66m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[24.392803978,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;35;54;81m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[24.437373706,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;40;63;95m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[24.481554761,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;44;71;109m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[24.525125471,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;49;80;122m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[24.569557474,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;54;89;137m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;54;89;137m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[24.613597582,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;59;98;151m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[53;6H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[24.699309699,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;55;91;140m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[53;7H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[24.739831139,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;51;84;129m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[53;8H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[24.783215117,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[53;9H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[24.826421117,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;44;70;107m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;10H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[24.869697875,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;40;64;97m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;11H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[24.914148306,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[24.95755808,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[25.000567669,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;29;43;63m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[25.044373169,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;10H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[25.086512984,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;31;47;69m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[53;9H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[25.129465714,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[53;8H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[25.174253713,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;42;67;101m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[53;7H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[25.217249476,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[53;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[25.261033562,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[53;5H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[25.304579989,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;59;98;151m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[25.390862045,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;58;95;147m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[25.434415717,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;57;94;145m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[25.477970345,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;56;91;141m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[25.521657975,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;55;90;138m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[25.565126499,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[25.608538753,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;52;86;132m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[25.650443045,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;51;83;128m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[25.695699598,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;50;81;124m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[25.738765734,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;49;79;122m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[25.781946664,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[25.824653546,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;47;75;115m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[25.864757353,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;73;111m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[25.906109217,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;44;71;109m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[25.95038233,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;43;69;105m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[25.9927883,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;42;67;101m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[26.035707924,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;41;65;98m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[26.079354742,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;40;63;95m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[26.123337173,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;39;61;92m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[26.166653165,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;37;59;88m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[26.210300999,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[26.252020818,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;35;55;82m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[26.295730615,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;34;52;78m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[26.338386338,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;51;75m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[26.382944679,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;32;48;72m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[26.40940918,"o","\u001b[?2026h\u001b[?25l\u001b[1;3H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[1;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[1;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mpabilities: Mermaid sequence diagra, Graphviz architecture digraph, D2 pipeline diagram, math \u001b[0m\u001b[1;100H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[2;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mequations with cross-refs, BibTeX citations, tikz diagram, examples of code listings, two \u001b[0m\u001b[3;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mbooktabs tabl\u001b[0m\u001b[3;20H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20ms, tabl\u001b[0m\u001b[3;28H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m of \u001b[0m\u001b[3;33H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20montents, and lists. Compile with texforge build.\u001b[0m\u001b[3;81H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[4;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[5;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[6;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[6;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 737ms\u001b[0m\u001b[6;20H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[8;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mThe user wants me to create a LaTeX document that demonstrates various capabilities including \u001b[0m\u001b[9;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mMermaid sequence diagrams, Graphviz architecture diagrams, D2 pipeline diagrams, math \u001b[0m\u001b[10;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mequations wi\u001b[0m\u001b[10;19H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mh\u001b[0m\u001b[10;21H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mcross-refer\u001b[0m\u001b[10;33H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mnces, BibTeX citations, tikz di\u001b[0m\u001b[10;65H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mgrams, c\u001b[0m\u001b[10;74H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mde\u001b[0m\u001b[10;77H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mlistings, booktabs \u001b[0m\u001b[10;96H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[11;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mtables,\u001b[0m\u001b[11;14H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mtabl\u001b[0m\u001b[11;19H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m of\u001b[0m\u001b[11;23H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mcontent\u001b[0m\u001b[11;33H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mand lists. T\u001b[0m\u001b[11;46H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mey want me to compile\u001b[0m\u001b[11;68H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mit with texfo\u001b[0m\u001b[11;82H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mge.\u001b[0m\u001b[11;85H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[12;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[13;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mLet me first l\u001b[0m\u001b[13;21H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mad the texforge\u001b[0m\u001b[13;37H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mskill to unders\u001b[0m\u001b[13;53H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mand\u001b[0m\u001b[13;57H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mh\u001b[0m\u001b[13;59H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mw to us\u001b[0m\u001b[13;71H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mproperly.\u001b[0m\u001b[13;80H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[15;6H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m→\u001b[0m\u001b[15;7H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[15;8H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mSkill \"texforge\"\u001b[0m\u001b[15;24H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[17;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 971ms\u001b[0m\u001b[17;20H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mNow I understand the texforge capabilities. The user wants a document showcasing:\u001b[0m\u001b[20;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m1. \u001b[0m\u001b[20;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mMermaid sequence diagrams\u001b[0m\u001b[21;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m2. \u001b[0m\u001b[21;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mGraphviz archi\u001b[0m\u001b[21;24H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mecture diagrams\u001b[0m\u001b[21;39H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[22;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m3\u001b[0m\u001b[22;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mD2 pipeline diagrams\u001b[0m\u001b[22;29H\u001b[38;2;255;255;255m\u001b"] +[26.409442079,"o","[48;2;10;10;10m \u001b[0m\u001b[23;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m4\u001b[0m\u001b[23;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mMath equations wi\u001b[0m\u001b[23;27H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mh cross-\u001b[0m\u001b[23;36H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mef\u001b[0m\u001b[24;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m5\u001b[0m\u001b[24;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mBibTeX c\u001b[0m\u001b[24;18H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mtations\u001b[0m\u001b[24;25H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[25;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m6\u001b[0m\u001b[25;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mtikz\u001b[0m\u001b[25;14H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mdiagrams\u001b[0m\u001b[25;22H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[26;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m7\u001b[0m\u001b[26;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mCode listings\u001b[0m\u001b[26;22H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[27;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m8\u001b[0m\u001b[27;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mTwo booktabs tables\u001b[0m\u001b[28;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m9\u001b[0m\u001b[28;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mTable of contents\u001b[0m\u001b[29;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m10. \u001b[0m\u001b[29;10H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mLists\u001b[0m\u001b[29;15H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[30;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[31;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mLet me create a new project and build the document.\u001b[0m\u001b[33;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[33;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[34;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[34;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[34;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m# Todos\u001b[0m\u001b[34;13H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[36;6H\u001b[38;2;245;167;66m\u001b[48;2;20;20;20m[•] Create texforge project\u001b[0m\u001b[37;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] Write main.tex with all requested capabilities\u001b[0m\u001b[38;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] Write bib/references.bib\u001b[0m\u001b[39;10H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mRun texforge build\u001b[0m\u001b[39;28H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[40;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[41;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[42;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[42;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10m⠋\u001b[0m\u001b[42;7H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[42;8H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThinking\u001b[0m\u001b[42;16H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[44;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[44;9H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[46;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m▣ \u001b[0m\u001b[46;9H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10mBuild\u001b[0m\u001b[46;14H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m · MiMo V2.5 Free\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[26.4386085,"o","\u001b[?2026h\u001b[?25l\u001b[44;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mLet me create the project first.\u001b[0m\u001b[53;4H\u001b[38;2;31;47;69m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[26.454239727,"o","\u001b[?2026h\u001b[?25l\u001b[42;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 75ms\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[26.470623807,"o","\u001b[?2026h\u001b[?25l\u001b[1;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mbooktabs tabl\u001b[0m\u001b[1;20H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20ms, tabl\u001b[0m\u001b[1;28H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m of \u001b[0m\u001b[1;33H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20montents, and lists. Compile with texforge build.\u001b[0m\u001b[1;81H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[2;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[3;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[4;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[4;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 737ms\u001b[0m\u001b[4;20H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[6;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mThe user wants me to create a LaTeX document that demonstrates various capabilities including \u001b[0m\u001b[7;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mMermaid sequence diagrams, Graphviz architecture diagrams, D2 pipeline diagrams, math \u001b[0m\u001b[8;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mequations wi\u001b[0m\u001b[8;19H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mh\u001b[0m\u001b[8;21H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mcross-refer\u001b[0m\u001b[8;33H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mnces, BibTeX citations, tikz di\u001b[0m\u001b[8;65H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mgrams, c\u001b[0m\u001b[8;74H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mde\u001b[0m\u001b[8;77H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mlistings, booktabs \u001b[0m\u001b[8;96H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[9;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mtables,\u001b[0m\u001b[9;14H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mtabl\u001b[0m\u001b[9;19H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m of\u001b[0m\u001b[9;23H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mcontent\u001b[0m\u001b[9;33H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mand lists. T\u001b[0m\u001b[9;46H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mey want me to compile\u001b[0m\u001b[9;68H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mit with texfo\u001b[0m\u001b[9;82H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mge.\u001b[0m\u001b[9;85H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[10;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[11;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mLet me first l\u001b[0m\u001b[11;21H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mad the texforge\u001b[0m\u001b[11;37H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mskill to unders\u001b[0m\u001b[11;53H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mand\u001b[0m\u001b[11;57H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mh\u001b[0m\u001b[11;59H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mw to us\u001b[0m\u001b[11;71H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mproperly.\u001b[0m\u001b[11;80H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[13;6H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m→\u001b[0m\u001b[13;7H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[13;8H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mSkill \"texforge\"\u001b[0m\u001b[13;24H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[15;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 971ms\u001b[0m\u001b[15;20H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[17;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mNow I understand the texforge capabilities. The user wants a document showcasing:\u001b[0m\u001b[18;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m1. \u001b[0m\u001b[18;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mMermaid sequence diagrams\u001b[0m\u001b[19;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m2. \u001b[0m\u001b[19;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mGraphviz archi\u001b[0m\u001b[19;24H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mecture diagrams\u001b[0m\u001b[19;39H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m3\u001b[0m\u001b[20;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mD2 pipeline diagrams\u001b[0m\u001b[20;29H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[21;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m4\u001b[0m\u001b[21;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mMath equations wi\u001b[0m\u001b[21;27H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mh cross-\u001b[0m\u001b[21;36H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mef\u001b[0m\u001b[22;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m5\u001b[0m\u001b[22;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mBibTeX c\u001b[0m\u001b[22;18H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mtations\u001b[0m\u001b[22;25H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10"] +[26.47063477,"o","m \u001b[0m\u001b[23;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m6\u001b[0m\u001b[23;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mtikz\u001b[0m\u001b[23;14H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mdiagrams\u001b[0m\u001b[23;22H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[24;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m7\u001b[0m\u001b[24;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mCode listings\u001b[0m\u001b[24;22H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[25;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m8\u001b[0m\u001b[25;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mTwo booktabs tables\u001b[0m\u001b[26;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m9\u001b[0m\u001b[26;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mTable of contents\u001b[0m\u001b[27;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m10. \u001b[0m\u001b[27;10H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mLists\u001b[0m\u001b[27;15H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[28;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[29;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mLet me create a new project and build the document.\u001b[0m\u001b[31;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[31;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[32;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[32;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[32;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m# Todos\u001b[0m\u001b[32;13H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[34;6H\u001b[38;2;245;167;66m\u001b[48;2;20;20;20m[•] Create texforge project\u001b[0m\u001b[35;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] Write main.tex with all requested capabilities\u001b[0m\u001b[36;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] Write bib/references.bib\u001b[0m\u001b[37;10H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mRun texforge build\u001b[0m\u001b[37;28H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[38;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[39;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[40;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[40;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 75ms\u001b[0m\u001b[40;19H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[42;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mLet me create the project first.\u001b[0m\u001b[44;6H\u001b[38;2;238;238;238m\u001b[48;2;10;10;10m~ Writing command...\u001b[0m\u001b[44;26H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[53;4H\u001b[38;2;29;44;65m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[26.507774775,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;28;43;63m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[26.55000341,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;27;40;59m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[26.594086413,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;26;38;55m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[26.637248464,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[26.661636597,"o","\u001b[?2026h\u001b[?25l\u001b[44;6H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m$\u001b[0m\u001b[44;7H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[44;8H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mtexforge new capabilities-demo\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[26.685637752,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;30;45;66m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[26.734234421,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;35;54;81m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[26.750393398,"o","\u001b[?2026h\u001b[?25l\u001b[1;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[2;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[2;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 737ms\u001b[0m\u001b[2;20H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[4;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mThe user wants me to create a LaTeX document that demonstrates various capabilities including \u001b[0m\u001b[5;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mMermaid sequence diagrams, Graphviz architecture diagrams, D2 pipeline diagrams, math \u001b[0m\u001b[6;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mequations wi\u001b[0m\u001b[6;19H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mh\u001b[0m\u001b[6;21H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mcross-refer\u001b[0m\u001b[6;33H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mnces, BibTeX citations, tikz di\u001b[0m\u001b[6;65H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mgrams, c\u001b[0m\u001b[6;74H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mde\u001b[0m\u001b[6;77H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mlistings, booktabs \u001b[0m\u001b[6;96H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[7;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mtables,\u001b[0m\u001b[7;14H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mtabl\u001b[0m\u001b[7;19H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10m of\u001b[0m\u001b[7;23H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mcontent\u001b[0m\u001b[7;33H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mand lists. T\u001b[0m\u001b[7;46H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mey want me to compile\u001b[0m\u001b[7;68H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mit with texfo\u001b[0m\u001b[7;82H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mge.\u001b[0m\u001b[7;85H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[8;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[9;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mLet me first l\u001b[0m\u001b[9;21H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mad the texforge\u001b[0m\u001b[9;37H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mskill to unders\u001b[0m\u001b[9;53H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mand\u001b[0m\u001b[9;57H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mh\u001b[0m\u001b[9;59H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mw to us\u001b[0m\u001b[9;71H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mproperly.\u001b[0m\u001b[9;80H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[11;6H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m→\u001b[0m\u001b[11;7H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[11;8H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mSkill \"texforge\"\u001b[0m\u001b[11;24H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[13;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 971ms\u001b[0m\u001b[13;20H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[15;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mNow I understand the texforge capabilities. The user wants a document showcasing:\u001b[0m\u001b[16;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m1. \u001b[0m\u001b[16;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mMermaid sequence diagrams\u001b[0m\u001b[17;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m2. \u001b[0m\u001b[17;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mGraphviz archi\u001b[0m\u001b[17;24H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mecture diagrams\u001b[0m\u001b[17;39H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[18;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m3\u001b[0m\u001b[18;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mD2 pipeline diagrams\u001b[0m\u001b[18;29H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m4\u001b[0m\u001b[19;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mMath equations wi\u001b[0m\u001b[19;27H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mh cross-\u001b[0m\u001b[19;36H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mef\u001b[0m\u001b[20;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m5\u001b[0m\u001b[20;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mBibTeX c\u001b[0m\u001b[20;18H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mtations\u001b[0m\u001b[20;25H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[21;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m6\u001b[0m\u001b[21;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mtikz\u001b[0m\u001b[21;14H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mdiagrams\u001b[0m\u001b[21;22H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[22;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m7\u001b[0m\u001b[22;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mCode listings\u001b[0m\u001b[22;22H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[23;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m8\u001b[0m\u001b[23;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;"] +[26.750404391,"o","10mTwo booktabs tables\u001b[0m\u001b[24;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m9\u001b[0m\u001b[24;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mTable of contents\u001b[0m\u001b[25;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m10. \u001b[0m\u001b[25;10H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mLists\u001b[0m\u001b[25;15H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[26;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[27;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mLet me create a new project and build the document.\u001b[0m\u001b[29;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[29;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[30;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[30;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[30;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m# Todos\u001b[0m\u001b[30;13H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[32;6H\u001b[38;2;245;167;66m\u001b[48;2;20;20;20m[•] Create texforge project\u001b[0m\u001b[33;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] Write main.tex with all requested capabilities\u001b[0m\u001b[34;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] Write bib/references.bib\u001b[0m\u001b[35;10H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20mRun texforge build\u001b[0m\u001b[35;28H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[36;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[37;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[38;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[38;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 75ms\u001b[0m\u001b[38;19H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[40;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mLet me create the project first.\u001b[0m\u001b[42;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[42;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[43;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[43;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[43;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m⠋\u001b[0m\u001b[43;7H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[43;8H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mtexforge new capabilities-demo\u001b[0m\u001b[43;38H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[44;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[44;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[26.783628347,"o","\u001b[?2026h\u001b[?25l\u001b[1;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mtables, table of contents, and lists. They want me to compile it with texforge.\u001b[0m\u001b[2;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[3;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mLet me first load the texforge skill to understand how to use it properly.\u001b[0m\u001b[4;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[5;6H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m→\u001b[0m\u001b[5;7H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[5;8H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10mSkill \"texforge\"\u001b[0m\u001b[5;24H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[6;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[6;108H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m717\u001b[0m\u001b[7;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 971ms\u001b[0m\u001b[7;20H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[9;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mNow\u001b[0m\u001b[9;10H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mI understand the texforge capabilities. The user \u001b[0m\u001b[9;60H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mants a document showcasing:\u001b[0m\u001b[10;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m1. \u001b[0m\u001b[10;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mMermaid sequence diagrams\u001b[0m\u001b[11;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m2. \u001b[0m\u001b[11;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mGraphviz architecture diagrams\u001b[0m\u001b[12;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m3. \u001b[0m\u001b[12;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mD2 pipeline diagrams\u001b[0m\u001b[13;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m4. \u001b[0m\u001b[13;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mMath equations with cross-refs\u001b[0m\u001b[14;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m5. \u001b[0m\u001b[14;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mBibTeX citations\u001b[0m\u001b[15;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m6. \u001b[0m\u001b[15;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mtikz \u001b[0m\u001b[15;15H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10miagr\u001b[0m\u001b[15;20H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mms\u001b[0m\u001b[15;22H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[16;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m7\u001b[0m\u001b[16;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mCode listings\u001b[0m\u001b[16;22H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[17;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m8\u001b[0m\u001b[17;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mTwo bookt\u001b[0m\u001b[17;19H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mbs tables\u001b[0m\u001b[17;28H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[18;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m9\u001b[0m\u001b[18;9H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mTable of contents\u001b[0m\u001b[18;26H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[19;6H\u001b[38;2;154;111;83m\u001b[48;2;10;10;10m10. \u001b[0m\u001b[19;10H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mLists\u001b[0m\u001b[19;15H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[20;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[21;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mLet me cre\u001b[0m\u001b[21;17H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mte a new project and build the document.\u001b[0m\u001b[22;6H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[23;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[23;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[24;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[24;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[24;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m# Todos\u001b[0m\u001b[24;13H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[25;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[25;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[26;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[26;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[26;6H\u001b[38;2;245;167;66m\u001b[48;2;20;20;20m[•] Create texforge project\u001b[0m\u001b[26;33H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m "] +[26.78364175,"o"," \u001b[0m\u001b[27;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[27;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[27;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] Write main.tex with all requested capabilities\u001b[0m\u001b[27;56H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[28;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[28;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[28;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] Write bib/references.bib\u001b[0m\u001b[28;34H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[29;6H\u001b[38;2;128;128;128m\u001b[48;2;20;20;20m[ ] Run texforge build\u001b[0m\u001b[30;6H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[31;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[32;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[32;6H\u001b[38;2;151;104;44m\u001b[48;2;10;10;10mThought: 75ms\u001b[0m\u001b[32;19H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[33;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[34;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[34;6H\u001b[38;2;147;147;147m\u001b[48;2;10;10;10mLet me create the project first.\u001b[0m\u001b[34;38H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[35;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[37;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[37;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[37;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m$ texforge new capabilities-demo\u001b[0m\u001b[37;38H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[38;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[38;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[39;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[39;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[39;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mCreating project 'capabilities-demo' with template 'general'...\u001b[0m\u001b[39;69H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[40;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[40;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[40;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m ◇ Project 'capabilities-demo' created successfully\u001b[0m\u001b[40;58H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[41;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[41;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[42;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m cd capabilities-demo\u001b[0m\u001b[43;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m \u001b[0m\u001b[43;17H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mbuild\u001b[0m\u001b[43;22H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[53;4H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;40;63;95m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[53;76H\u001b[38;2;128;128;128m\u001b[48;2;10;10;10m7\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[26.825364652,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;44;71;109m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[26.866837935,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;49;80;122m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[26.909949794,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;54;89;137m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;54;89;137m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[26.95313319,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;59;98;151m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[53;6H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[27.039655829,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;55;91;140m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[53;7H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[27.083090524,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;51;84;129m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[53;8H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[27.126595015,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[53;9H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[27.169903964,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;44;70;107m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;10H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[27.213290908,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;40;64;97m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;11H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[27.256386505,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[27.299859695,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[27.345328393,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;29;43;63m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[27.388924879,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[53;10H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[27.432404579,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;31;47;69m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[53;9H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[27.475063348,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;36;57;86m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[53;8H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[27.518457873,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;42;67;101m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[53;7H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[27.561692273,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[53;6H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[27.604111218,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[53;5H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;11H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[27.646878175,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;92;156;245m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;10H\u001b[38;2;59;98;151m\u001b[48;2;10;10;10m⬝⬝\u001b[0m\u001b[0m\u001b[49;6H\u001b[?25h\u001b[?2026l"] +[27.708851284,"o","\u001b[?2026h\u001b[?25l\u001b[38;3H\u001b[38;2;72;72;72m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[38;4H\u001b[38;2;255;255;255m\u001b[48;2;250;178;131m \u001b[0m\u001b[38;5H\u001b[38;2;10;10;10m\u001b[48;2;250;178;131m/agents Switch agent\u001b[0m\u001b[38;31H\u001b[38;2;255;255;255m\u001b[48;2;250;178;131m \u001b[0m\u001b[38;100H\u001b[38;2;72;72;72m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[39;3H\u001b[38;2;72;72;72m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[39;4H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[39;5H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m/compact \u001b[0m\u001b[39;18H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30m Compact session\u001b[0m\u001b[39;34H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[39;100H\u001b[38;2;72;72;72m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[40;3H\u001b[38;2;72;72;72m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[40;4H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[40;5H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m/connect \u001b[0m\u001b[40;18H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30m Connect provider\u001b[0m\u001b[40;35H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[40;100H\u001b[38;2;72;72;72m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[41;3H\u001b[38;2;72;72;72m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[41;4H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[41;5H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m/copy \u001b[0m\u001b[41;18H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30m Copy session transcript\u001b[0m\u001b[41;42H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[41;100H\u001b[38;2;72;72;72m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[42;3H\u001b[38;2;72;72;72m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[42;4H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[42;5H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m/debug \u001b[0m\u001b[42;18H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30m View debug info\u001b[0m\u001b[42;34H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[42;100H\u001b[38;2;72;72;72m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[43;3H\u001b[38;2;72;72;72m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[43;4H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[43;5H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m/diff \u001b[0m\u001b[43;18H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30m Open diff viewer\u001b[0m\u001b[43;35H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[43;100H\u001b[38;2;72;72;72m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[44;3H\u001b[38;2;72;72;72m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[44;4H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[44;5H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m/editor \u001b[0m\u001b[44;18H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30m Open editor\u001b[0m\u001b[44;30H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[44;100H\u001b[38;2;72;72;72m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[45;3H\u001b[38;2;72;72;72m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[45;4H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[45;5H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m/exit \u001b[0m\u001b[45;18H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30m Exit the app\u001b[0m\u001b[45;31H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[45;100H\u001b[38;2;72;72;72m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[46;3H\u001b[38;2;72;72;72m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[46;4H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[46;5H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m/export \u001b[0m\u001b[46;18H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30m Export session transcript\u001b[0m\u001b[46;44H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[46;100H\u001b[38;2;72;72;72m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[47;3H\u001b[38;2;72;72;72m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[47;4H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[47;5H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m/fork \u001b[0m\u001b[47;18H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30m Fork session\u001b[0m\u001b[47;31H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[47;100H\u001b[38;2;72;72;72m\u001b[48;2;10;10;10m┃\u001b[0m\u001b[49;6H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30m/\u001b[0m\u001b[0m\u001b[49;7H\u001b[?25h\u001b[?2026l"] +[27.742429234,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;97;162;231m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;9H\u001b[38;2;58;95;147m\u001b[48;2;10;10;10m⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;7H\u001b[?25h\u001b[?2026l"] +[27.783943037,"o","\u001b[?2026h\u001b[?25l\u001b[38;6H\u001b[38;2;10;10;10m\u001b[48;2;250;178;131mexit \u001b[0m\u001b[38;19H\u001b[38;2;10;10;10m\u001b[48;2;250;178;131mEx\u001b[0m\u001b[38;23H\u001b[38;2;10;10;10m\u001b[48;2;250;178;131m the app\u001b[0m\u001b[39;6H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30meditor \u001b[0m\u001b[39;19H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mOpen editor\u001b[0m\u001b[39;30H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[40;6H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mexport \u001b[0m\u001b[40;19H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mExport session transcript\u001b[0m\u001b[41;6H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mhelp\u001b[0m\u001b[41;19H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mHelp\u001b[0m\u001b[41;23H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[42;6H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mn\u001b[0m\u001b[42;8H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mw \u001b[0m\u001b[42;19H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mNew session\u001b[0m\u001b[42;30H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[43;6H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mrename\u001b[0m\u001b[43;19H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mRename sess\u001b[0m\u001b[43;31H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mon\u001b[0m\u001b[43;33H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[44;6H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mreview\u001b[0m\u001b[44;19H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mreview changes [commit|branch|pr], defaults to uncommitted\u001b[0m\u001b[45;6H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mdebug\u001b[0m\u001b[45;19H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mView\u001b[0m\u001b[45;24H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mdebug info\u001b[0m\u001b[46;6H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30msessions\u001b[0m\u001b[46;19H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mSwitch\u001b[0m\u001b[46;33H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[47;6H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mstatus\u001b[0m\u001b[47;19H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mView\u001b[0m\u001b[47;25H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mtatus\u001b[0m\u001b[47;30H\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m \u001b[0m\u001b[49;7H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30me\u001b[0m\u001b[0m\u001b[49;8H\u001b[?25h\u001b[?2026l"] +[27.80876477,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;63;105;163m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;8H\u001b[38;2;57;94;145m\u001b[48;2;10;10;10m⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;8H\u001b[?25h\u001b[?2026l"] +[27.825967127,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;72;110m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;7H\u001b[38;2;56;91;141m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;8H\u001b[?25h\u001b[?2026l"] +[27.869539854,"o","\u001b[?2026h\u001b[?25l\u001b[38;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[38;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[39;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[39;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[39;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20mCreating project 'capabilities-demo' with template 'general'...\u001b[0m\u001b[39;69H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[40;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[40;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[40;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m ◇ Project 'capabilities-demo' created successfully\u001b[0m\u001b[40;58H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[41;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[41;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[42;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[42;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[42;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m cd capabilities-demo\u001b[0m\u001b[42;28H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[43;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[43;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[43;6H\u001b[38;2;238;238;238m\u001b[48;2;20;20;20m texforge build\u001b[0m\u001b[43;22H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[44;3H\u001b[38;2;10;10;10m\u001b[48;2;20;20;20m┃\u001b[0m\u001b[44;4H\u001b[38;2;255;255;255m\u001b[48;2;20;20;20m \u001b[0m\u001b[45;3H\u001b[38;2;255;255;255m\u001b[48;2;10;10;10m \u001b[0m\u001b[46;4H\u001b[38;2;255;255;255m\u001b[48;2;250;178;131m \u001b[0m\u001b[46;5H\u001b[38;2;10;10;10m\u001b[48;2;250;178;131m/exit Exit the app\u001b[0m\u001b[46;31H\u001b[38;2;255;255;255m\u001b[48;2;250;178;131m \u001b[0m\u001b[47;6H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mexport\u001b[0m\u001b[47;19H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mExport ses\u001b[0m\u001b[47;30H\u001b[38;2;128;128;128m\u001b[48;2;30;30;30mion transcript\u001b[0m\u001b[49;8H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mx\u001b[0m\u001b[0m\u001b[49;9H\u001b[?25h\u001b[?2026l"] +[27.885423398,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;33;50;75m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;6H\u001b[38;2;55;90;138m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;9H\u001b[?25h\u001b[?2026l"] +[27.913343439,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;25;36;52m\u001b[48;2;10;10;10m■\u001b[0m\u001b[53;5H\u001b[38;2;53;87;134m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;9H\u001b[?25h\u001b[?2026l"] +[27.936728311,"o","\u001b[?2026h\u001b[?25l\u001b[49;9H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mi\u001b[0m\u001b[0m\u001b[49;10H\u001b[?25h\u001b[?2026l"] +[27.964154656,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;52;86;132m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;10H\u001b[?25h\u001b[?2026l"] +[28.007080016,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;51;83;128m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;10H\u001b[?25h\u001b[?2026l"] +[28.031625426,"o","\u001b[?2026h\u001b[?25l\u001b[49;10H\u001b[38;2;238;238;238m\u001b[48;2;30;30;30mt\u001b[0m\u001b[0m\u001b[49;11H\u001b[?25h\u001b[?2026l"] +[28.05382409,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;50;81;124m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;11H\u001b[?25h\u001b[?2026l"] +[28.096782137,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;49;79;122m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;11H\u001b[?25h\u001b[?2026l"] +[28.14010949,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;48;77;118m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;11H\u001b[?25h\u001b[?2026l"] +[28.182158168,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;47;75;115m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;11H\u001b[?25h\u001b[?2026l"] +[28.225458223,"o","\u001b[?2026h\u001b[?25l\u001b[53;4H\u001b[38;2;45;73;111m\u001b[48;2;10;10;10m⬝⬝⬝⬝⬝⬝⬝⬝\u001b[0m\u001b[0m\u001b[49;11H\u001b[?25h\u001b[?2026l"] +[28.233842554,"o","\u001b]0;\u0007"] +[28.245355,"o","\u001b[?25h\u001b[0m\u001b]22;\u0007\u001b[>4;0m\u001b[?1003l\u001b[?1002l\u001b[?1000l\u001b[?1006l\u001b[?2004l\u001b[?1049l\u001b[?2031l\u001b]0;\u0007\u001b]12;default\u0007\u001b]112\u0007\u001b[0 q\u001b[?25h"] +[28.255462803,"o","\u001b[?25h"] +[28.269351853,"o"," \u001b[0m▄\u001b[0m \r\n \u001b[90m█\u001b[0m\u001b[90m▀\u001b[0m\u001b[90m▀\u001b[0m\u001b[90m█\u001b[0m \u001b[90m█\u001b[0m\u001b[90m▀\u001b[0m\u001b[90m▀\u001b[0m\u001b[90m█\u001b[0m \u001b[90m█\u001b[0m\u001b[90m▀\u001b[0m\u001b[90m▀\u001b[0m\u001b[90m█\u001b[0m \u001b[90m█\u001b[0m\u001b[90m▀\u001b[0m\u001b[90m▀\u001b[0m\u001b[90m▄\u001b[0m \u001b[0m█\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m \u001b[0m█\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m█\u001b[0m \u001b[0m█\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m█\u001b[0m \u001b[0m█\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m█\u001b[0m\r\n \u001b[90m█\u001b[0m\u001b[48;5;235m \u001b[0m\u001b[48;5;235m \u001b[0m\u001b[90m█\u001b[0m \u001b[90m█\u001b[0m\u001b[48;5;235m \u001b[0m\u001b[48;5;235m \u001b[0m\u001b[90m█\u001b[0m \u001b[90m█\u001b[0m\u001b[90m\u001b[48;5;235m▀\u001b[0m\u001b[90m\u001b[48;5;235m▀\u001b[0m\u001b[90m\u001b[48;5;235m▀\u001b[0m \u001b[90m█\u001b[0m\u001b[48;5;235m \u001b[0m\u001b[48;5;235m \u001b[0m\u001b[90m█\u001b[0m \u001b[0m█\u001b[0m\u001b[48;5;238m \u001b[0m\u001b[48;5;238m \u001b[0m\u001b[48;5;238m \u001b[0m \u001b[0m█\u001b[0m\u001b[48;5;238m \u001b[0m\u001b[48;5;238m \u001b[0m\u001b[0m█\u001b[0m \u001b[0m█\u001b[0m\u001b[48;5;238m \u001b[0m\u001b[48;5;238m \u001b[0m\u001b[0m█\u001b[0m \u001b[0m█\u001b[0m\u001b[0m\u001b[48;5;238m▀\u001b[0m\u001b[0m\u001b[48;5;238m▀\u001b[0m\u001b[0m\u001b[48;5;238m▀\u001b[0m\r\n \u001b[90m▀\u001b[0m\u001b[90m▀\u001b[0m\u001b[90m▀\u001b[0m\u001b[90m▀\u001b[0m \u001b[90m█\u001b[0m\u001b[90m▀\u001b[0m\u001b[90m▀\u001b[0m\u001b[90m▀\u001b[0m \u001b[90m▀\u001b[0m\u001b[90m▀\u001b[0m\u001b[90m▀\u001b[0m\u001b[90m▀\u001b[0m \u001b[90m▀\u001b[0m\u001b[38;5;235m▀\u001b[0m\u001b[38;5;235m▀\u001b[0m\u001b[90m▀\u001b[0m \u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m \u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m \u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m \u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m\u001b[0m▀\u001b[0m\r\n\r\n \u001b[90mSession \u001b[0m\u001b[1mNew session - 2026-08-13T21:59:15.663Z\u001b[0m\r\n \u001b[90mContinue \u001b[0m\u001b[1mopencode -s ses_002dd0630ffe8DizT1CYciGZ6G\u001b[0m\r\n\r\n"] +[28.503887986,"o","\u001b]697;OSCUnlock=f83785b047c648f6bd12bde086bcbd50\u0007\u001b]697;Dir=/home/jheisonmblivecom\u0007"] +[28.503900948,"o","\u001b]697;Shell=bash\u0007"] +[28.503984551,"o","\u001b]697;ShellPath=/usr/bin/bash\u0007\u001b]697;WSLDistro=Ubuntu\u0007"] +[28.504057201,"o","\u001b]697;PID=736321\u0007\u001b]697;ExitCode=0\u0007"] +[28.504149127,"o","\u001b]697;TTY=/dev/pts/0\u0007\u001b]697;Log=\u0007"] +[28.504689143,"o","\u001b]697;User=jheisonmblivecom\u0007"] +[28.506471618,"o","\u001b]697;OSCLock=f83785b047c648f6bd12bde086bcbd50\u0007\u001b]697;PreExec\u0007"] +[28.748802341,"o","\u001b[?2004h\u001b]697;StartPrompt\u0007\u001b[1;32mtexForge@univerlab\u001b[0m:\u001b[1;34m~\u001b[0m$ \u001b]697;EndPrompt\u0007\u001b]697;NewCmd=f83785b047c648f6bd12bde086bcbd50\u0007"] diff --git a/demo/dist/demo.gif b/demo/dist/demo.gif index f42ede8..39311ae 100644 Binary files a/demo/dist/demo.gif and b/demo/dist/demo.gif differ diff --git a/docs/building.md b/docs/building.md index e9ac53e..d7b7567 100644 --- a/docs/building.md +++ b/docs/building.md @@ -20,6 +20,32 @@ Compiles the project to `.pdf` in the project root: On the first run texforge downloads the Tectonic binary into `~/.texforge/bin/` automatically. +## Reproducible builds + +By default a build embeds a current timestamp, so the same source produces a +different PDF on each run. For anything that compares outputs — visual +regression, build caching, meaningful diffs — texforge can pin the build time: + +```bash +texforge build --reproducible +``` + +This sets `SOURCE_DATE_EPOCH` for the Tectonic invocation. With no explicit +value a fixed epoch is used (never "now"); a release can pin its own: + +```bash +texforge build --reproducible=1700000000 +``` + +The same behaviour can be made the default for a project in `project.toml` +(see [Configuration](configuration.md)); the `--reproducible` flag wins when +both are present. + +**The guarantee, and its limit:** identical source plus an identical Tectonic +version yields an identical PDF. A different Tectonic version (or engine +updates within it) can still change the output, and the setting does not alter +the visible content of a document — it only pins the embedded time. + ## Watch mode `texforge build --watch` watches `.tex` files and rebuilds automatically: @@ -38,6 +64,43 @@ last build. Press `Ctrl+C` to stop. texforge clean # remove build artifacts ``` +## Preview + +`texforge preview` rasterizes the compiled PDF to PNG pages for quick visual review or for embedding in documentation: + +```bash +# Rasterize all pages (writes to ./preview/) +texforge preview + +# Single page +texforge preview --page 1 + +# Custom output directory +texforge preview --out ./previews + +# Scale up for higher resolution (default: 1.0 pixel per PDF point) +texforge preview --scale 2.0 +``` + +## PDF Inspection + +Once built, the PDF can be inspected without re-compiling: + +```bash +# Extract text as readers and accessibility tools see it +texforge pdf text + +# Report pages, fonts, whether fonts are embedded, and metadata +texforge pdf info + +# List which section opens each page (for diffing in version control) +texforge pdf pages + +# Verify significant source words survived into the PDF +# (catches missing fonts, encoding issues, rendering bugs) +texforge pdf check +``` + ## Runtime directory Texforge keeps its engine and template cache under `~/.texforge/`: diff --git a/docs/cli-reference.md b/docs/cli-reference.md index a6a816e..bdd2124 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -20,13 +20,16 @@ texforge <command> [options] | `texforge build` | Compile to PDF | | `texforge build --watch` | Watch for changes and rebuild automatically | | `texforge build --watch --delay <s>` | Custom debounce delay (default: 2s) | +| `texforge build --reproducible` | Pin `SOURCE_DATE_EPOCH` to a fixed epoch so identical source yields an identical PDF | +| `texforge build --reproducible=<epoch>` | Reproducible build with an explicit epoch (seconds since the Unix epoch) | | `texforge clean` | Remove build artifacts | ## Quality | Command | Description | |---|---| -| `texforge check` | Lint without compiling | +| `texforge check` | Lint without compiling (includes spell-check) | +| `texforge check --deny-warnings` | Treat warnings as errors | | `texforge fmt` | Format `.tex` files in place | | `texforge fmt --check` | Check formatting without modifying (CI-friendly) | @@ -40,6 +43,54 @@ texforge <command> [options] | `texforge template remove <name>` | Remove an installed template | | `texforge template validate <name>` | Verify template compatibility | +## Spell-Check + +| Command | Description | +|---|---| +| `texforge spell add <words>...` | Add word(s) to personal dictionary | +| `texforge spell add <words>... --local` | Add to project-local dictionary instead of global | +| `texforge spell list` | List all words in personal dictionary | +| `texforge spell list --local` | List project-local dictionary | +| `texforge spell remove <words>...` | Remove word(s) from personal dictionary | +| `texforge spell remove <words>... --local` | Remove from project-local dictionary | + +Default scope is global (`~/.texforge/spell-words`). Both scopes are unioned at check time. + +## PDF Inspection + +| Command | Description | +|---|---| +| `texforge pdf text` | Extract text as seen by readers and accessibility tools | +| `texforge pdf text --raw` | Keep ligature codepoints as separate characters | +| `texforge pdf info` | Report pages, fonts, embedding status, metadata | +| `texforge pdf pages` | List which section opens each page (diff-friendly) | +| `texforge pdf check` | Verify significant source words appear in the PDF text | + +## Document Analysis + +| Command | Description | +|---|---| +| `texforge outline` | Print the section tree | +| `texforge outline --json` | Output as JSON | +| `texforge stats` | Count words by section (default) | +| `texforge stats --by file` | Count words by `.tex` file | +| `texforge stats --json` | Output as JSON | + +## Preview + +| Command | Description | +|---|---| +| `texforge preview` | Rasterize all PDF pages to PNG (writes to `./preview/`) | +| `texforge preview --page <N>` | Rasterize page N only (1-based) | +| `texforge preview --scale <SCALE>` | Scale factor for rasterization (default: 1.0) | +| `texforge preview --out <DIR>` | Output directory (default: `./preview/`) | + +## Diagnostics + +| Command | Description | +|---|---| +| `texforge doctor` | Diagnose Tectonic, cache, fonts, dictionaries, and project | + ## Configuration | Command | Description | diff --git a/docs/configuration.md b/docs/configuration.md index f7ccdb8..12523b3 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -26,7 +26,7 @@ The wizard asks for: - **Name** — your full name - **Email** — your email address - **Institution** — your institution/organization -- **Language** — document language (default: `english`) +- **Language** — fallback language for documents without an explicit `\usepackage[lang]{babel}` or `polyglossia` declaration (default: `english`). If a document declares its language in the preamble, that takes precedence. **Command-line interface:** @@ -53,6 +53,10 @@ template = "general" [build] entry = "main.tex" # bibliography = "references.bib" # optional +# reproducible = true # optional: reproducible builds by default + +# [diagrams] +# style = "editorial" # optional: document-wide diagram style default ``` | Key | Description | @@ -62,3 +66,5 @@ entry = "main.tex" | `document.template` | Template the project was created from | | `build.entry` | Entry `.tex` file passed to the engine | | `build.bibliography` | Optional `.bib` file used by the linter | +| `build.reproducible` | Optional: pin `SOURCE_DATE_EPOCH` so identical source plus the same Tectonic version yields an identical PDF. `true` uses a fixed default epoch; a number pins an explicit epoch (`reproducible = 1700000000`); `false` or absent keeps the default behaviour. Overridden by `texforge build --reproducible` when that flag is present. | +| `diagrams.style` | Optional: document-wide default diagram style preset (`default`, `editorial`, `monochrome`, `technical`; see [Diagrams](diagrams.md)). A `style=` on the diagram environment itself overrides this. | diff --git a/docs/diagrams.md b/docs/diagrams.md index 4d5cde4..222421b 100644 --- a/docs/diagrams.md +++ b/docs/diagrams.md @@ -10,9 +10,15 @@ order: 5 compilation and replaces them with rendered figures. Your original `.tex` files are never modified — rendering happens in the `build/` copies. -Both renderers are pure Rust: no browser, no Node.js, no `dot` binary +All three renderers are pure Rust: no browser, no Node.js, no `dot` binary required. +Diagrams are embedded as vector PDF, so they stay sharp at any zoom level +and their label text remains selectable and searchable in the final PDF. +If a diagram's SVG can't be converted to PDF, texforge falls back to a +rasterized PNG for that one diagram, prints a warning naming it, and the +build still succeeds. + ## Mermaid ```latex @@ -48,6 +54,84 @@ digraph G { | `width` | `\linewidth` | Image width | | `pos` | `H` | Figure placement (`H`, `t`, `b`, `h`, `p`) | | `caption` | _(none)_ | Figure caption | +| `style` | `default` | Editorial style preset — see below | When a `caption` is given the diagram is wrapped in a `figure` environment at the requested position; without it the image is embedded inline. + +If an option value contains a comma, wrap it in braces — the same +convention LaTeX packages already use for this: + +```latex +\begin{mermaid}[caption={Preset \texttt{editorial}: paleta restringida, un solo acento}] +flowchart LR + A --> B +\end{mermaid} +``` + +Without the braces, the diagram intercepts commas as option separators, +so `un solo acento` would be parsed as a second, unrecognized option and +the caption would end at `restringida`. An unrecognized option prints a +warning naming it and the environment, but the build continues; an +unterminated `{` fails the build. + +## Style presets + +By default, each renderer draws diagrams with whatever it ships as its own +look — which means a Mermaid figure and a D2 figure in the same document can +look like they came from different tools. `style=` applies one of four +named presets consistently across all three renderers: + +```latex +\begin{mermaid}[style=editorial, caption=System flow] +flowchart LR + A --> B --> C +\end{mermaid} +``` + +| Preset | Effect | +|---|---| +| `default` | Each renderer's own untouched default. Omitting `style=` always renders this — no existing document changes appearance. | +| `editorial` | Restrained palette, one accent colour, no drop shadows, thin strokes, generous whitespace. | +| `monochrome` | Greyscale only — for documents printed in black and white. | +| `technical` | Drafting register: uniform stroke weight, no fills, labels in a monospaced face where the renderer allows it. | + +An unrecognised style name (e.g. `style=editoral`) fails the build with a +message naming the value and listing the valid ones — it never silently +falls back to `default`. + +Presets are named, not parameterised: there is no `style=editorial,accent=#ff0000`. +Opening them to arbitrary colours would make them themes, a different and +larger feature. + +### Project-wide default + +Set a document-wide default in `project.toml`: + +```toml +[diagrams] +style = "editorial" +``` + +The environment's own `style=` attribute overrides this; omitting both +falls back to `default`. + +### Fidelity varies by renderer + +The three renderers expose very different styling surfaces, so the same +preset degrades differently depending on which one draws it — a diagram +still renders and the build still succeeds even where a renderer can't +express part of a preset. + +- **Mermaid** has the richest surface (a full theme: colours, font, layout + spacing), so every preset is expressed almost exactly. +- **D2** has native theming (a named colour palette), so presets map onto + it with good fidelity. `technical`'s monospaced labels come from D2's own + `mono` theme rule, which also adds minor drafting-style ornamentation + (double borders, container dots) to nested/grouped diagrams as a side + effect — not a colour or fidelity gap, just a quirk of the one lever D2 + exposes for a monospaced face. +- **Graphviz** has no theme concept at all — styling is per-node and + per-edge only. Node fill/border colour and edge colour carry over, but a + document-wide background is not expressible, and neither is a + monospaced label face (`technical`'s font rule has no effect here). diff --git a/docs/index.md b/docs/index.md index 4bc6484..3c2e38b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -31,7 +31,7 @@ collapses that into a single workflow: built-in offline fallback. - **Build and live edit** — compile once or use watch mode. - **Smart linting** — catch missing files, broken references, bibliography - keys and unclosed environments before compiling. + keys, unclosed environments, and spelling errors before compiling. - **Opinionated formatter** — one canonical `.tex` style, clean git diffs. ## How the documentation is organized diff --git a/docs/installation.md b/docs/installation.md index c0330af..ffb289a 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -46,7 +46,25 @@ cargo install texforge Available on [crates.io](https://crates.io/crates/texforge). -## From source +## Updating texforge + +Texforge updates itself in place, **replacing the binary that is running**. + +The check runs when you create or migrate a project with `texforge init` — not on every command, and never during a build. If a newer release exists, you are asked once, there and then. To update outside that flow, reach for the installer or `cargo` as below. + +**If you used the quick installer or downloaded a binary directly:** + +Accept the update prompt, and it will overwrite the binary at its current location (typically `~/.local/bin/texforge`). + +**If you installed via `cargo install`:** + +Self-update is deliberately disabled — cargo owns that installation path and tracks its own versions. To update, run: + +```bash +cargo install --force texforge +``` + +**Why it matters:** Mixing install methods leaves two binaries on your system. The one on your PATH may not be the one that updated, leading to confusing version mismatches. Choose one method and stick with it. ```bash git clone https://github.com/UniverLab/texforge.git diff --git a/docs/linting-and-formatting.md b/docs/linting-and-formatting.md index d8e5316..830266e 100644 --- a/docs/linting-and-formatting.md +++ b/docs/linting-and-formatting.md @@ -8,7 +8,7 @@ order: 8 ## Linter — `texforge check` -Runs static analysis without compiling: +Runs static analysis without compiling, including spell-check: | Check | What it verifies | |---|---| @@ -17,6 +17,7 @@ Runs static analysis without compiling: | `\cite{key}` | key exists in the `.bib` file | | `\ref{label}` / `\label{label}` | cross-reference consistency | | `\begin{env}` / `\end{env}` | no unclosed environments | +| Spelling | prose against language-specific dictionaries | Errors come with file, line and a suggestion: @@ -30,8 +31,37 @@ ERROR [main.tex:12] ERROR [main.tex:23] \begin{figure} never closed suggestion: Add \end{figure} + +ERROR [main.tex:18] + mispeled — not in dictionary +``` + +### Spell-Check + +Language is detected from the document: + +- `\usepackage[spanish]{babel}` — uses Spanish Hunspell dictionary +- `\usepackage[polyglossia]{...}` — language extracted from polyglossia declaration +- No declaration — falls back to `texforge config language` (default: `english`) + +Dictionaries download automatically into `~/.texforge/dicts/` on first use. + +Manage custom words with `texforge spell`: + +```bash +# Add to global dictionary (all projects) +texforge spell add "LaTeX" "UUID" + +# Add to current project only +texforge spell add "ProjectCodename" --local + +# List and remove +texforge spell list [--local] +texforge spell remove "LaTeX" [--local] ``` +Both scopes are unioned at check time — project-local and global words are both respected. + ## Formatter — `texforge fmt` Applies opinionated formatting inspired by `rustfmt`: diff --git a/docs/quickstart.md b/docs/quickstart.md index 804245b..26e4166 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -47,6 +47,8 @@ texforge build # compile to <title>.pdf in the project root texforge build --watch # rebuild automatically while you edit ``` +`texforge check` also validates spelling using language-specific dictionaries (detected from your document or configured as a fallback). + ## Project layout A texforge project is a regular LaTeX project plus a `project.toml` diff --git a/examples/texforge-capabilites/main.tex b/examples/texforge-capabilites/main.tex index 4239fc7..53540d1 100644 --- a/examples/texforge-capabilites/main.tex +++ b/examples/texforge-capabilites/main.tex @@ -151,6 +151,105 @@ engines -> pdf: "tectonic" \end{d2} + \clearpage + \subsection{Estilos de Diagrama (\texttt{style})}\label{sec:estilos} + + Los tres entornos aceptan también un atributo \texttt{style}, combinable + con \texttt{width}, \texttt{pos} y \texttt{caption}. Un diagrama de tres + cajas apenas ejercita un relleno y un color de borde, así que en vez de + eso el mismo diagrama de secuencia --- con participantes, una nota, un + bloque \texttt{alt} y una barra de activación --- se repite a + continuación una vez por preset, para que la diferencia se vea en mucha + más superficie: + + \begin{mermaid}[style=editorial, width=0.85\linewidth, caption={Preset \texttt{editorial}: participantes y nota en tono papel con tinta oscura; el acento vermellón vive solo en las flechas de mensaje, nunca en el relleno de una caja}, pos=H] + sequenceDiagram + participant U as Usuario + participant CLI as texforge + participant V as Validador + participant M as Motor + + U->>CLI: build + CLI->>V: valida sintaxis + alt sintaxis valida + V-->>CLI: ok + Note over CLI,M: cache de tectonic reutilizada + CLI->>M: compila + activate M + M-->>CLI: PDF generado + deactivate M + else error de sintaxis + V-->>CLI: linea N invalida + end + CLI-->>U: resultado + \end{mermaid} + + \begin{mermaid}[style=monochrome, width=0.85\linewidth, caption={Preset \texttt{monochrome}: el mismo diagrama sin un solo color, apto para impresión en blanco y negro}, pos=H] + sequenceDiagram + participant U as Usuario + participant CLI as texforge + participant V as Validador + participant M as Motor + + U->>CLI: build + CLI->>V: valida sintaxis + alt sintaxis valida + V-->>CLI: ok + Note over CLI,M: cache de tectonic reutilizada + CLI->>M: compila + activate M + M-->>CLI: PDF generado + deactivate M + else error de sintaxis + V-->>CLI: linea N invalida + end + CLI-->>U: resultado + \end{mermaid} + + \begin{mermaid}[style=technical, width=0.85\linewidth, caption={Preset \texttt{technical}: cajas sin relleno, trazo uniforme y etiquetas monoespaciadas, incluida la etiqueta del bloque \texttt{alt}}, pos=H] + sequenceDiagram + participant U as Usuario + participant CLI as texforge + participant V as Validador + participant M as Motor + + U->>CLI: build + CLI->>V: valida sintaxis + alt sintaxis valida + V-->>CLI: ok + Note over CLI,M: cache de tectonic reutilizada + CLI->>M: compila + activate M + M-->>CLI: PDF generado + deactivate M + else error de sintaxis + V-->>CLI: linea N invalida + end + CLI-->>U: resultado + \end{mermaid} + + El preset \texttt{editorial} sirve para documentos que se leen en pantalla + o se imprimen a color y buscan un acabado sobrio: las cajas de los + participantes, la nota y la barra de activación comparten un neutro + cálido, y el acento vermellón se reserva para las flechas de mensaje, sin + gastarse nunca en un relleno. El preset \texttt{technical} conviene a + diagramas que funcionan como planos o esquemas de referencia: sin + relleno, con peso de trazo uniforme y etiquetas monoespaciadas hasta en + el rótulo del bloque \texttt{alt}. El preset \texttt{monochrome} tiene una + razón menos obvia: existe para documentos que se imprimen en blanco y + negro, donde un diagrama a color puede perder significado, porque dos + colores que se distinguen en pantalla pueden reducirse al mismo gris al + imprimirse sin color; \texttt{monochrome} evita ese problema por + construcción, ya que nunca depende del color para distinguir nada --- ni + siquiera una llamada de una respuesta. Omitir \texttt{style} equivale a + \texttt{style=default}, el aspecto propio de cada motor, como en los + diagramas de las secciones anteriores. + + También puede fijarse un preset por defecto para todo el documento en + \texttt{project.toml}, bajo \texttt{[diagrams]} con \texttt{style = "..."}; + el atributo \texttt{style} de un entorno concreto, cuando está presente, + sobrescribe ese valor por defecto. + % ------------------------------------------------------- \section{Matemáticas}\label{sec:matematicas} diff --git a/examples/texforge-capabilites/texforge-capabilites.pdf b/examples/texforge-capabilites/texforge-capabilites.pdf index ffa94c3..3a07e87 100644 Binary files a/examples/texforge-capabilites/texforge-capabilites.pdf and b/examples/texforge-capabilites/texforge-capabilites.pdf differ diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 10cac5b..61c3729 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,5 +1,7 @@ //! CLI argument parsing and command dispatch. +use std::path::PathBuf; + use anyhow::Result; use clap::{Parser, Subcommand}; @@ -35,6 +37,25 @@ enum Commands { /// Debounce delay in seconds before rebuilding (default: 10) #[arg(long, default_value = "2")] delay: u64, + /// Print every engine warning instead of a per-file summary + #[arg(long)] + verbose: bool, + /// Build reproducibly: pin `SOURCE_DATE_EPOCH` so identical source plus + /// the same Tectonic version yields an identical PDF. An optional epoch + /// (seconds since the Unix epoch) overrides the fixed default. + #[arg(long, num_args = 0..=1)] + reproducible: Option<Option<u64>>, + /// After each successful rebuild, write a PNG of one page to a fixed + /// path so any file-watching image viewer becomes a live preview + /// (requires `--watch`) + #[arg(long, requires = "watch")] + preview: bool, + /// Page to rasterize for `--preview` (1-based; default: 1) + #[arg(long, default_value_t = 1, requires = "preview")] + preview_page: usize, + /// Fixed path for the `--preview` PNG (default: preview.png) + #[arg(long, value_name = "PATH", requires = "preview")] + preview_out: Option<PathBuf>, }, /// Format .tex files Fmt { @@ -43,12 +64,55 @@ enum Commands { check: bool, }, /// Lint project without compiling - Check, + Check { + /// Treat warnings as errors (exit non-zero if any warning is present) + #[arg(long)] + deny_warnings: bool, + }, + /// Count words per section or file + Stats { + /// Output JSON instead of a human-readable breakdown + #[arg(long)] + json: bool, + /// Break down by .tex file instead of by section + #[arg(long, value_enum, default_value = "section")] + by: crate::commands::stats::ByMode, + }, + /// Print the document's section tree + Outline { + /// Output JSON instead of a human-readable tree + #[arg(long)] + json: bool, + }, + /// Rasterize the compiled PDF to PNG pages + Preview { + /// Page number to rasterize (1-based; default: all pages) + #[arg(long, value_name = "N")] + page: Option<usize>, + /// Rasterization scale in pixels per PDF point (default: 1.0) + #[arg(long, default_value_t = 1.0)] + scale: f32, + /// Directory to write PNGs to (default: <project>/preview) + #[arg(long, value_name = "DIR")] + out: Option<PathBuf>, + }, + /// Inspect the compiled PDF (text, info, pages, fidelity) + Pdf { + #[command(subcommand)] + action: PdfAction, + }, /// Manage templates Template { #[command(subcommand)] action: TemplateAction, }, + /// Manage the personal spell-check whitelist (dictionary) + Spell { + #[command(subcommand)] + action: SpellAction, + }, + /// Diagnose the managed environment (Tectonic, cache, fonts, dictionaries, project) + Doctor, /// Manage global configuration Config { /// Key to get/set (name, email, institution, language) @@ -58,6 +122,59 @@ enum Commands { }, } +#[derive(Subcommand)] +enum PdfAction { + /// Print text as a reader or ATS would see it + Text { + /// Keep ligature codepoints unnormalized + #[arg(long)] + raw: bool, + }, + /// Report pages, fonts (embedded?), and metadata + Info, + /// Report which section opens each page (diff-friendly) + Pages, + /// Check significant source words appear in the PDF text + Check, +} + +#[derive(Subcommand)] +enum SpellAction { + /// Add one or more words to the whitelist (default: global personal dictionary) + Add { + /// Word(s) to add + #[arg(required = true)] + words: Vec<String>, + /// Target the project's whitelist instead of the global personal dictionary + #[arg(long, conflicts_with = "global")] + local: bool, + /// Target the global personal dictionary (~/.texforge/spell-words); this is the default + #[arg(long, conflicts_with = "local")] + global: bool, + }, + /// List the effective whitelist words for the scope (default: global personal dictionary) + List { + /// List the project's whitelist instead of the global personal dictionary + #[arg(long, conflicts_with = "global")] + local: bool, + /// List the global personal dictionary (~/.texforge/spell-words); this is the default + #[arg(long, conflicts_with = "local")] + global: bool, + }, + /// Remove one or more words from the whitelist (default: global personal dictionary) + Remove { + /// Word(s) to remove + #[arg(required = true)] + words: Vec<String>, + /// Target the project's whitelist instead of the global personal dictionary + #[arg(long, conflicts_with = "global")] + local: bool, + /// Target the global personal dictionary (~/.texforge/spell-words); this is the default + #[arg(long, conflicts_with = "local")] + global: bool, + }, +} + #[derive(Subcommand)] enum TemplateAction { /// List available templates (installed + remote registry by default) @@ -80,21 +197,70 @@ impl Cli { Commands::Clean => commands::clean::execute(), Commands::Init => commands::init::execute(), Commands::New { name, template } => commands::new::execute(&name, template.as_deref()), - Commands::Build { watch, delay } => { + Commands::Build { + watch, + delay, + verbose, + reproducible, + preview, + preview_page, + preview_out, + } => { if watch { - commands::build::watch(delay) + let live_preview = preview.then_some(commands::build::LivePreview { + page: preview_page, + out: preview_out, + }); + commands::build::watch(delay, verbose, reproducible, live_preview.as_ref()) } else { - commands::build::execute() + commands::build::execute(verbose, reproducible) } } Commands::Fmt { check } => commands::fmt::execute(check), - Commands::Check => commands::check::execute(), + Commands::Check { deny_warnings } => commands::check::execute(deny_warnings), + Commands::Stats { json, by } => commands::stats::execute(json, by), + Commands::Outline { json } => commands::outline::execute(json), + Commands::Preview { page, scale, out } => commands::preview::execute(page, scale, out), + Commands::Pdf { action } => { + let action = match action { + PdfAction::Text { raw } => commands::pdf::PdfAction::Text { raw }, + PdfAction::Info => commands::pdf::PdfAction::Info, + PdfAction::Pages => commands::pdf::PdfAction::Pages, + PdfAction::Check => commands::pdf::PdfAction::Check, + }; + commands::pdf::execute(action) + } Commands::Template { action } => match action { TemplateAction::List { local } => commands::template::list(!local), TemplateAction::Add { source } => commands::template::add(&source), TemplateAction::Remove { name } => commands::template::remove(&name), TemplateAction::Validate { name } => commands::template::validate(&name), }, + Commands::Spell { action } => { + let action = match action { + SpellAction::Add { + words, + local, + global, + } => commands::spell::SpellAction::Add { + words, + scope: commands::spell::Scope::from_flags(local, global), + }, + SpellAction::List { local, global } => commands::spell::SpellAction::List { + scope: commands::spell::Scope::from_flags(local, global), + }, + SpellAction::Remove { + words, + local, + global, + } => commands::spell::SpellAction::Remove { + words, + scope: commands::spell::Scope::from_flags(local, global), + }, + }; + commands::spell::execute(action) + } + Commands::Doctor => commands::doctor::execute(), Commands::Config { key, value } => match (key, value) { (None, None) => commands::config::wizard(), (Some(k), None) if k == "list" => commands::config::list(), @@ -105,3 +271,46 @@ impl Cli { } } } + +#[cfg(test)] +mod tests { + use super::*; + + /// TE14 requirement 6: `--local --global` together must be rejected by + /// clap, not silently resolved to one or the other. + #[test] + fn spell_add_rejects_local_and_global_together() { + let result = + Cli::try_parse_from(["texforge", "spell", "add", "docker", "--local", "--global"]); + assert!( + result.is_err(), + "`--local --global` together must be a usage error" + ); + } + + #[test] + fn spell_list_rejects_local_and_global_together() { + let result = Cli::try_parse_from(["texforge", "spell", "list", "--local", "--global"]); + assert!( + result.is_err(), + "`--local --global` together must be a usage error" + ); + } + + #[test] + fn spell_remove_rejects_local_and_global_together() { + let result = Cli::try_parse_from([ + "texforge", "spell", "remove", "docker", "--local", "--global", + ]); + assert!( + result.is_err(), + "`--local --global` together must be a usage error" + ); + } + + #[test] + fn spell_add_accepts_local_alone() { + let result = Cli::try_parse_from(["texforge", "spell", "add", "docker", "--local"]); + assert!(result.is_ok()); + } +} diff --git a/src/commands/build.rs b/src/commands/build.rs index c992874..b09548c 100644 --- a/src/commands/build.rs +++ b/src/commands/build.rs @@ -1,34 +1,88 @@ //! `texforge build` command implementation. -use std::path::Path; +use std::io::BufWriter; +use std::path::{Path, PathBuf}; use std::sync::mpsc; use std::time::Duration; -use anyhow::Result; +use anyhow::{Context, Result}; use notify::{RecursiveMode, Watcher}; use crate::commands::init::BANNER; use crate::compiler; use crate::diagrams; -use crate::domain::project::Project; +use crate::domain::project::{Project, Reproducible}; +use crate::raster::PdfDocument; use crate::utils::sanitize_filename; +/// Options for writing a stable live-preview PNG after each successful watch rebuild. +pub struct LivePreview { + /// 1-based page number to rasterize. + pub page: usize, + /// Fixed output path; relative paths resolve against the project root. + /// `None` means `preview.png` in the project root. + pub out: Option<PathBuf>, +} + +/// Resolve the `SOURCE_DATE_EPOCH` value for a build. The CLI flag wins over +/// `project.toml`; a flag with no value pins the fixed default epoch; a config +/// value pins its own epoch or the default when enabled without one. +fn resolve_epoch(cli: Option<Option<u64>>, config: Option<Reproducible>) -> Option<u64> { + match cli { + Some(Some(epoch)) => Some(epoch), + Some(None) => Some(compiler::DEFAULT_EPOCH), + None => match config { + Some(Reproducible::Enabled(true)) => Some(compiler::DEFAULT_EPOCH), + Some(Reproducible::Epoch(epoch)) => Some(epoch), + Some(Reproducible::Enabled(false)) | None => None, + }, + } +} + +/// Resolve the document-wide default diagram style from `project.toml`'s +/// `[diagrams] style` key. A `style=` on the environment itself overrides +/// this. An unrecognised name fails the build rather than silently falling +/// back to `default`. +fn resolve_default_style(project: &Project) -> Result<diagrams::style::DiagramStyle> { + match project + .config + .diagrams + .as_ref() + .and_then(|d| d.style.as_deref()) + { + Some(name) => diagrams::style::DiagramStyle::parse(name), + None => Ok(diagrams::style::DiagramStyle::Default), + } +} + /// Compile project to PDF using a temp directory, output named after the document title. -pub fn execute() -> Result<()> { +pub fn execute(verbose: bool, reproducible: Option<Option<u64>>) -> Result<()> { let project = Project::load()?; let titulo = &project.config.document.title; println!("Building project: {titulo}"); + let epoch = resolve_epoch(reproducible, project.config.build.reproducible); + if epoch.is_some() { + println!(" ◇ reproducible build (SOURCE_DATE_EPOCH pinned)"); + } + + let default_style = resolve_default_style(&project)?; + let temp_dir = tempfile::tempdir()?; let build_dir = temp_dir.path(); println!(" ◇ temp: {}", build_dir.display()); - diagrams::process(&project.root, &project.config.build.entry, build_dir)?; + diagrams::process( + &project.root, + &project.config.build.entry, + build_dir, + default_style, + )?; let entry_filename = Path::new(&project.config.build.entry) .file_name() .map(|n| n.to_string_lossy().to_string()) .unwrap_or_else(|| project.config.build.entry.clone()); - compiler::compile(build_dir, &entry_filename)?; + compiler::compile(build_dir, &entry_filename, verbose, epoch)?; let pdf_name = format!("{}.pdf", sanitize_filename(titulo)); let pdf_dest = project.root.join(&pdf_name); @@ -45,18 +99,30 @@ pub fn execute() -> Result<()> { } /// Watch for .tex file changes and rebuild with debounce. -pub fn watch(delay_secs: u64) -> Result<()> { +pub fn watch( + delay_secs: u64, + verbose: bool, + reproducible: Option<Option<u64>>, + live_preview: Option<&LivePreview>, +) -> Result<()> { let project = Project::load()?; + let epoch = resolve_epoch(reproducible, project.config.build.reproducible); let debounce = Duration::from_secs(delay_secs); let cooldown = Duration::from_secs(2); + let preview_path = + live_preview.map(|opts| resolve_preview_path(&project.root, opts.out.as_deref())); - print_watch_header(&project.config.document.title, delay_secs); + print_watch_header( + &project.config.document.title, + delay_secs, + preview_path.as_deref(), + ); let temp_dir = tempfile::tempdir()?; let build_dir = temp_dir.path().to_path_buf(); let started = std::time::Instant::now(); - let result = run_build(&project, &build_dir); + let result = run_build_with_preview(&project, &build_dir, verbose, epoch, live_preview); redraw_status(&result, 1, started); let (tx, rx) = mpsc::channel(); @@ -99,7 +165,8 @@ pub fn watch(delay_secs: u64) -> Result<()> { if pending && last_event.elapsed() >= debounce { pending = false; build_count += 1; - last_result = run_build(&project, &build_dir); + last_result = + run_build_with_preview(&project, &build_dir, verbose, epoch, live_preview); last_build = std::time::Instant::now(); redraw_status(&last_result, build_count, started); } @@ -108,10 +175,13 @@ pub fn watch(delay_secs: u64) -> Result<()> { Ok(()) } -fn print_watch_header(title: &str, delay_secs: u64) { +fn print_watch_header(title: &str, delay_secs: u64, preview: Option<&Path>) { print!("\x1B[2J\x1B[H"); println!("{BANNER}"); println!(" {title} — watching ({delay_secs}s debounce Ctrl+C to stop)"); + if let Some(path) = preview { + println!(" live preview → {}", path.display()); + } } fn redraw_status(result: &WatchResult, build_count: u32, started: std::time::Instant) { @@ -139,16 +209,30 @@ enum WatchResult { Err(String), } -fn run_build(project: &Project, build_dir: &Path) -> WatchResult { +fn run_build( + project: &Project, + build_dir: &Path, + verbose: bool, + epoch: Option<u64>, +) -> WatchResult { let _ = std::fs::create_dir_all(build_dir); - if let Err(e) = diagrams::process(&project.root, &project.config.build.entry, build_dir) { + let default_style = match resolve_default_style(project) { + Ok(style) => style, + Err(e) => return WatchResult::Err(e.to_string()), + }; + if let Err(e) = diagrams::process( + &project.root, + &project.config.build.entry, + build_dir, + default_style, + ) { return WatchResult::Err(e.to_string()); } let entry_filename = Path::new(&project.config.build.entry) .file_name() .map(|n| n.to_string_lossy().to_string()) .unwrap_or_else(|| project.config.build.entry.clone()); - match compiler::compile(build_dir, &entry_filename) { + match compiler::compile(build_dir, &entry_filename, verbose, epoch) { Ok(()) => { let pdf_name = format!("{}.pdf", sanitize_filename(&project.config.document.title)); let pdf_dest = project.root.join(&pdf_name); @@ -166,3 +250,360 @@ fn run_build(project: &Project, build_dir: &Path) -> WatchResult { Err(e) => WatchResult::Err(e.to_string()), } } + +/// Rebuild, then on success write the live-preview PNG. A failed rebuild +/// leaves any previous preview image untouched. +fn run_build_with_preview( + project: &Project, + build_dir: &Path, + verbose: bool, + epoch: Option<u64>, + live_preview: Option<&LivePreview>, +) -> WatchResult { + let result = run_build(project, build_dir, verbose, epoch); + if let (WatchResult::Ok(ref pdf_name), Some(opts)) = (&result, live_preview) { + let pdf_path = project.root.join(pdf_name); + let out = resolve_preview_path(&project.root, opts.out.as_deref()); + // Rasterize failures must not erase the last good frame. + let _ = write_live_preview(&pdf_path, opts.page, &out); + } + result +} + +fn resolve_preview_path(project_root: &Path, out: Option<&Path>) -> PathBuf { + match out { + Some(path) if path.is_absolute() => path.to_path_buf(), + Some(path) => project_root.join(path), + None => project_root.join("preview.png"), + } +} + +/// Sidecar path used while encoding; renamed onto `target` only when complete. +fn preview_temp_path(target: &Path) -> PathBuf { + let mut tmp = target.as_os_str().to_owned(); + tmp.push(".tmp"); + PathBuf::from(tmp) +} + +/// Rasterize one PDF page to `out` via temp file + rename (atomic on the same FS). +fn write_live_preview(pdf_path: &Path, page: usize, out: &Path) -> Result<()> { + if page == 0 { + anyhow::bail!("--preview-page must be at least 1"); + } + let document = PdfDocument::open(pdf_path)?; + let pages = document.page_count(); + if page > pages { + anyhow::bail!("--preview-page {page} is out of range (document has {pages} pages)"); + } + let rendered = document.render_page(page - 1, 1.0)?; + write_png_atomic(out, rendered.width, rendered.height, &rendered.rgba) +} + +/// Encode a PNG to a sibling `.tmp` file, then rename over `path` so viewers +/// never observe a half-written image. +fn write_png_atomic(path: &Path, width: usize, height: usize, rgba: &[u8]) -> Result<()> { + if let Some(parent) = path.parent() { + if !parent.as_os_str().is_empty() { + std::fs::create_dir_all(parent) + .with_context(|| format!("failed to create {}", parent.display()))?; + } + } + let tmp = preview_temp_path(path); + // Drop any leftover temp from a previous interrupted write before starting. + let _ = std::fs::remove_file(&tmp); + write_png(&tmp, width, height, rgba).inspect_err(|_| { + let _ = std::fs::remove_file(&tmp); + })?; + if let Err(e) = std::fs::rename(&tmp, path) { + let _ = std::fs::remove_file(&tmp); + return Err(e).with_context(|| format!("failed to publish {}", path.display())); + } + Ok(()) +} + +fn write_png(path: &Path, width: usize, height: usize, rgba: &[u8]) -> Result<()> { + let file = std::fs::File::create(path) + .with_context(|| format!("failed to create {}", path.display()))?; + let mut encoder = png::Encoder::new(BufWriter::new(file), width as u32, height as u32); + encoder.set_color(png::ColorType::Rgba); + encoder.set_depth(png::BitDepth::Eight); + let mut writer = encoder + .write_header() + .with_context(|| format!("failed to encode {}", path.display()))?; + writer + .write_image_data(rgba) + .with_context(|| format!("failed to write {}", path.display()))?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn ensure_rustls() { + let _ = rustls::crypto::ring::default_provider().install_default(); + } + + #[test] + fn flag_without_value_wins_with_default_epoch() { + assert_eq!( + resolve_epoch(Some(None), Some(Reproducible::Epoch(123))), + Some(compiler::DEFAULT_EPOCH) + ); + } + + #[test] + fn flag_with_value_wins_over_config() { + assert_eq!( + resolve_epoch(Some(Some(123)), Some(Reproducible::Enabled(true))), + Some(123) + ); + } + + #[test] + fn config_enabled_uses_default_epoch() { + assert_eq!( + resolve_epoch(None, Some(Reproducible::Enabled(true))), + Some(compiler::DEFAULT_EPOCH) + ); + } + + #[test] + fn config_explicit_epoch_used_when_no_flag() { + assert_eq!( + resolve_epoch(None, Some(Reproducible::Epoch(1700000000))), + Some(1700000000) + ); + } + + #[test] + fn config_disabled_or_absent_is_off() { + assert_eq!( + resolve_epoch(None, Some(Reproducible::Enabled(false))), + None + ); + assert_eq!(resolve_epoch(None, None), None); + } + + fn project_with_diagrams_style(style: Option<&str>) -> Project { + Project { + root: PathBuf::from("."), + config: crate::domain::project::ProjectConfig { + document: crate::domain::project::DocumentConfig { + title: "T".to_string(), + author: "A".to_string(), + template: "general".to_string(), + }, + build: crate::domain::project::BuildConfig { + entry: "main.tex".to_string(), + bibliography: None, + reproducible: None, + }, + diagrams: style.map(|s| crate::domain::project::DiagramsConfig { + style: Some(s.to_string()), + }), + }, + } + } + + #[test] + fn default_style_is_used_when_project_toml_has_none() { + let project = project_with_diagrams_style(None); + assert_eq!( + resolve_default_style(&project).unwrap(), + diagrams::style::DiagramStyle::Default + ); + } + + #[test] + fn project_toml_style_becomes_the_default() { + let project = project_with_diagrams_style(Some("editorial")); + assert_eq!( + resolve_default_style(&project).unwrap(), + diagrams::style::DiagramStyle::Editorial + ); + } + + #[test] + fn invalid_project_toml_style_fails() { + let project = project_with_diagrams_style(Some("editoral")); + let err = resolve_default_style(&project).unwrap_err(); + assert!(err.to_string().contains("editoral")); + } + + fn tectonic_available() -> bool { + crate::compiler::locate_tectonic().is_some() + } + + /// A minimal project fixture: a single self-contained .tex file. + fn fixture() -> tempfile::TempDir { + let dir = tempfile::tempdir().unwrap(); + std::fs::write( + dir.path().join("main.tex"), + "\\documentclass{article}\n\\begin{document}\nReproducible world.\n\\end{document}\n", + ) + .unwrap(); + dir + } + + #[test] + fn reproducible_builds_are_byte_identical() { + if !tectonic_available() { + eprintln!("skipping: tectonic not available in environment"); + return; + } + let dir = fixture(); + compiler::compile(dir.path(), "main.tex", false, Some(compiler::DEFAULT_EPOCH)).unwrap(); + let first = std::fs::read(dir.path().join("main.pdf")).unwrap(); + compiler::compile(dir.path(), "main.tex", false, Some(compiler::DEFAULT_EPOCH)).unwrap(); + let second = std::fs::read(dir.path().join("main.pdf")).unwrap(); + assert_eq!(first, second); + } + + #[test] + fn explicit_epoch_builds_are_byte_identical() { + if !tectonic_available() { + eprintln!("skipping: tectonic not available in environment"); + return; + } + let dir = fixture(); + compiler::compile(dir.path(), "main.tex", false, Some(1700000000)).unwrap(); + let first = std::fs::read(dir.path().join("main.pdf")).unwrap(); + compiler::compile(dir.path(), "main.tex", false, Some(1700000000)).unwrap(); + let second = std::fs::read(dir.path().join("main.pdf")).unwrap(); + assert_eq!(first, second); + } + + #[test] + fn non_reproducible_build_still_succeeds() { + if !tectonic_available() { + eprintln!("skipping: tectonic not available in environment"); + return; + } + let dir = fixture(); + compiler::compile(dir.path(), "main.tex", false, None).unwrap(); + assert!(dir.path().join("main.pdf").exists()); + } + + const FIXTURE_PDF: &[u8] = include_bytes!("../../tests/fixtures/two-page.pdf"); + + #[test] + fn write_live_preview_is_atomic_and_complete() { + let dir = tempfile::tempdir().unwrap(); + let pdf = dir.path().join("doc.pdf"); + std::fs::write(&pdf, FIXTURE_PDF).unwrap(); + let out = dir.path().join("preview.png"); + + write_live_preview(&pdf, 1, &out).unwrap(); + + let tmp = preview_temp_path(&out); + assert!(!tmp.exists(), "temp sidecar must be gone after publish"); + assert!(out.exists()); + + let file = std::fs::File::open(&out).unwrap(); + let decoder = png::Decoder::new(file); + let mut reader = decoder.read_info().unwrap(); + let mut buf = vec![0u8; reader.output_buffer_size()]; + let info = reader.next_frame(&mut buf).unwrap(); + assert_eq!((info.width, info.height), (612, 842)); + assert_eq!(buf.len(), (info.width * info.height * 4) as usize); + } + + #[test] + fn write_live_preview_replaces_previous_frame_atomically() { + let dir = tempfile::tempdir().unwrap(); + let pdf = dir.path().join("doc.pdf"); + std::fs::write(&pdf, FIXTURE_PDF).unwrap(); + let out = dir.path().join("live.png"); + + write_live_preview(&pdf, 1, &out).unwrap(); + let first = std::fs::read(&out).unwrap(); + write_live_preview(&pdf, 2, &out).unwrap(); + let second = std::fs::read(&out).unwrap(); + + assert!(!preview_temp_path(&out).exists()); + assert_ne!(first, second, "page 2 should replace page 1 bytes"); + // Target remains a complete PNG after the second cycle. + let file = std::fs::File::open(&out).unwrap(); + let decoder = png::Decoder::new(file); + let mut reader = decoder.read_info().unwrap(); + let mut buf = vec![0u8; reader.output_buffer_size()]; + reader.next_frame(&mut buf).unwrap(); + } + + #[test] + fn failed_rebuild_leaves_previous_preview_untouched() { + ensure_rustls(); + if !tectonic_available() { + eprintln!("skipping: tectonic not available in environment"); + return; + } + let dir = tempfile::tempdir().unwrap(); + std::fs::write( + dir.path().join("project.toml"), + "[document]\ntitle = \"Live Preview\"\nauthor = \"A\"\ntemplate = \"general\"\n\n[build]\nentry = \"main.tex\"\n", + ) + .unwrap(); + // Intentionally broken so the rebuild fails before any PNG write. + std::fs::write(dir.path().join("main.tex"), "\\documentclass{article\n").unwrap(); + let out = dir.path().join("preview.png"); + std::fs::write(&out, b"previous-good-frame").unwrap(); + + let project = Project { + root: dir.path().to_path_buf(), + config: crate::domain::project::ProjectConfig { + document: crate::domain::project::DocumentConfig { + title: "Live Preview".to_string(), + author: "A".to_string(), + template: "general".to_string(), + }, + build: crate::domain::project::BuildConfig { + entry: "main.tex".to_string(), + bibliography: None, + reproducible: None, + }, + diagrams: None, + }, + }; + let build_dir = dir.path().join(".texforge-build"); + let opts = LivePreview { + page: 1, + out: Some(out.clone()), + }; + let result = run_build_with_preview(&project, &build_dir, false, None, Some(&opts)); + assert!(matches!(result, WatchResult::Err(_))); + assert_eq!(std::fs::read(&out).unwrap(), b"previous-good-frame"); + assert!(!preview_temp_path(&out).exists()); + } + + #[test] + fn resolve_preview_path_defaults_and_joins_relative() { + let root = Path::new("/proj"); + assert_eq!( + resolve_preview_path(root, None), + PathBuf::from("/proj/preview.png") + ); + assert_eq!( + resolve_preview_path(root, Some(Path::new("out/live.png"))), + PathBuf::from("/proj/out/live.png") + ); + assert_eq!( + resolve_preview_path(root, Some(Path::new("/abs/preview.png"))), + PathBuf::from("/abs/preview.png") + ); + } + + #[test] + fn out_of_range_preview_page_errors_without_touching_target() { + let dir = tempfile::tempdir().unwrap(); + let pdf = dir.path().join("doc.pdf"); + std::fs::write(&pdf, FIXTURE_PDF).unwrap(); + let out = dir.path().join("preview.png"); + std::fs::write(&out, b"keep-me").unwrap(); + + let err = write_live_preview(&pdf, 9, &out).unwrap_err(); + assert!(err.to_string().contains("out of range")); + assert_eq!(std::fs::read(&out).unwrap(), b"keep-me"); + assert!(!preview_temp_path(&out).exists()); + } +} diff --git a/src/commands/check.rs b/src/commands/check.rs index 044b54e..9c1f190 100644 --- a/src/commands/check.rs +++ b/src/commands/check.rs @@ -3,33 +3,73 @@ use anyhow::Result; use crate::domain::project::Project; -use crate::linter; +use crate::linter::{self, Severity}; /// Lint project without compiling. -pub fn execute() -> Result<()> { +/// +/// Exits non-zero when any `Error`-severity finding is present, or when +/// `deny_warnings` is `true` and any finding (including `Warning`) is present. +pub fn execute(deny_warnings: bool) -> Result<()> { let project = Project::load()?; println!("Checking project: {}", project.config.document.title); - let errors = linter::lint( + let findings = linter::lint( &project.root, &project.config.build.entry, project.config.build.bibliography.as_deref(), )?; - if errors.is_empty() { + if findings.is_empty() { println!(" ◇ No issues found"); - } else { - println!(); - for e in &errors { - println!("ERROR [{}:{}]", e.file, e.line); - println!(" {}", e.message); - if let Some(ref s) = e.suggestion { - println!(" suggestion: {}", s); + return Ok(()); + } + + // Partition by severity; errors first. + let errors: Vec<_> = findings + .iter() + .filter(|f| f.severity == Severity::Error) + .collect(); + let warnings: Vec<_> = findings + .iter() + .filter(|f| f.severity == Severity::Warning) + .collect(); + + println!(); + + if !errors.is_empty() { + println!("ERRORS ({})", errors.len()); + for f in &errors { + println!(" ERROR [{}:{}]", f.file, f.line); + println!(" {}", f.message); + if let Some(ref s) = f.suggestion { + println!(" suggestion: {}", s); + } + println!(); + } + } + + if !warnings.is_empty() { + println!("WARNINGS ({})", warnings.len()); + for f in &warnings { + println!(" WARNING [{}:{}]", f.file, f.line); + println!(" {}", f.message); + if let Some(ref s) = f.suggestion { + println!(" suggestion: {}", s); } println!(); } - anyhow::bail!("{} issue(s) found", errors.len()); + } + + let should_fail = !errors.is_empty() || (deny_warnings && !warnings.is_empty()); + + if should_fail { + let total = if deny_warnings { + findings.len() + } else { + errors.len() + }; + anyhow::bail!("{} issue(s) found", total); } Ok(()) diff --git a/src/commands/doctor.rs b/src/commands/doctor.rs new file mode 100644 index 0000000..03b8cfb --- /dev/null +++ b/src/commands/doctor.rs @@ -0,0 +1,482 @@ +//! `texforge doctor` command implementation. +//! +//! Reports the verified state of everything texforge manages: the Tectonic +//! engine, its resource cache, fonts available to it, installed spell-check +//! dictionaries, and whether the current directory is a texforge project. +//! Read-only: it diagnoses, it never installs, repairs, or cleans anything. + +use std::collections::BTreeSet; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use anyhow::Result; + +use crate::compiler; +use crate::domain::project::Project; +use crate::linter; + +/// Run every check and print a human-readable report. +/// +/// Every other section is purely informational; Tectonic is the one +/// component a build cannot proceed without. +pub fn execute() -> Result<()> { + println!("texforge doctor"); + + let tectonic = compiler::locate_tectonic(); + let tectonic_version = tectonic.as_deref().and_then(query_tectonic_version); + println!(); + println!("Tectonic"); + print!( + "{}", + format_tectonic_status( + tectonic + .as_deref() + .map(|p| (p, tectonic_version.as_deref())) + ) + ); + + let cache_dir = tectonic_cache_dir(); + println!(); + report_cache(cache_dir.as_deref()); + + println!(); + report_fonts(cache_dir.as_deref()); + + println!(); + report_dictionaries(); + + println!(); + report_project(); + + if tectonic.is_none() { + anyhow::bail!("Tectonic is not present — builds cannot run"); + } + Ok(()) +} + +/// Format the Tectonic status line(s). Separated from resolution so the +/// missing-binary case is testable without touching `PATH`. +fn format_tectonic_status(found: Option<(&Path, Option<&str>)>) -> String { + match found { + Some((path, version)) => format!( + " \u{2713} present: {}\n version: {}\n", + path.display(), + version.unwrap_or("(could not determine)") + ), + None => " \u{2717} not found\n".to_string(), + } +} + +/// Run `tectonic --version` and pull the version number out of its output. +fn query_tectonic_version(path: &Path) -> Option<String> { + let output = Command::new(path).arg("--version").output().ok()?; + if !output.status.success() { + return None; + } + let raw = format!( + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + parse_version(&raw) +} + +/// Pull the first `X.Y.Z`-shaped token out of arbitrary CLI output. +fn parse_version(raw: &str) -> Option<String> { + raw.split(|c: char| !(c.is_ascii_digit() || c == '.')) + .find(|tok| tok.matches('.').count() >= 2 && tok.starts_with(|c: char| c.is_ascii_digit())) + .map(str::to_string) +} + +/// Resolve Tectonic's resource cache directory: `$TECTONIC_CACHE_DIR` when +/// set (Tectonic itself honors this override), otherwise the platform cache +/// directory joined with `Tectonic` — matching Tectonic's own default. +fn tectonic_cache_dir() -> Option<PathBuf> { + std::env::var_os("TECTONIC_CACHE_DIR") + .map(PathBuf::from) + .or_else(|| dirs::cache_dir().map(|d| d.join("Tectonic"))) +} + +/// Recursively total the size and file count of everything under `dir`. +/// A missing or empty directory reports `(0, 0)`. +fn dir_stats(dir: &Path) -> (u64, usize) { + let mut size = 0u64; + let mut count = 0usize; + for entry in walkdir::WalkDir::new(dir) + .into_iter() + .filter_map(std::result::Result::ok) + { + if entry.file_type().is_file() { + count += 1; + if let Ok(meta) = entry.metadata() { + size += meta.len(); + } + } + } + (size, count) +} + +/// Render a byte count as a human-readable size (e.g. `65.2 MB`). +fn format_size(bytes: u64) -> String { + const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"]; + if bytes < 1024 { + return format!("{bytes} B"); + } + let mut size = bytes as f64; + let mut unit = 0; + while size >= 1024.0 && unit < UNITS.len() - 1 { + size /= 1024.0; + unit += 1; + } + format!("{size:.1} {}", UNITS[unit]) +} + +fn report_cache(cache_dir: Option<&Path>) { + println!("Cache"); + let Some(dir) = cache_dir else { + println!(" \u{2717} could not determine cache location (no home directory)"); + return; + }; + println!(" location: {}", dir.display()); + if !dir.exists() { + println!(" size: 0 B"); + println!(" entries: 0 (not created yet — run a build to populate it)"); + return; + } + let (size, count) = dir_stats(dir); + println!(" size: {}", format_size(size)); + println!(" entries: {count}"); +} + +/// Font-program extensions Tectonic embeds glyph data from, as opposed to +/// `.tfm`/`.vf` files, which describe spacing but carry no glyph outlines. +const FONT_EXTENSIONS: &[&str] = &["otf", "ttf", "ttc", "otc", "pfb"]; + +/// Pull font filenames out of Tectonic bundle manifest text. Each manifest +/// line is `filename size hash`; this reports what the cache actually +/// recorded, not what a bundle might theoretically contain. +fn extract_font_names(manifest_content: &str) -> BTreeSet<String> { + let mut fonts = BTreeSet::new(); + for line in manifest_content.lines() { + let Some(name) = line.split_whitespace().next() else { + continue; + }; + let Some(ext) = name.rsplit('.').next() else { + continue; + }; + if FONT_EXTENSIONS.contains(&ext.to_ascii_lowercase().as_str()) { + fonts.insert(name.to_string()); + } + } + fonts +} + +fn fonts_from_cache(cache_dir: &Path) -> BTreeSet<String> { + let manifests_dir = cache_dir.join("manifests"); + let mut fonts = BTreeSet::new(); + let Ok(entries) = std::fs::read_dir(&manifests_dir) else { + return fonts; + }; + for entry in entries.filter_map(std::result::Result::ok) { + if let Ok(content) = std::fs::read_to_string(entry.path()) { + fonts.extend(extract_font_names(&content)); + } + } + fonts +} + +fn report_fonts(cache_dir: Option<&Path>) { + println!("Fonts"); + let Some(dir) = cache_dir else { + println!(" \u{2717} could not determine cache location (no home directory)"); + return; + }; + let fonts = fonts_from_cache(dir); + if fonts.is_empty() { + println!(" 0 fonts available (no cached bundle manifest — build once to populate)"); + return; + } + println!(" {} font file(s) available", fonts.len()); + const SHOWN: usize = 20; + for font in fonts.iter().take(SHOWN) { + println!(" {font}"); + } + if fonts.len() > SHOWN { + println!(" ... and {} more", fonts.len() - SHOWN); + } +} + +fn report_dictionaries() { + println!("Dictionaries"); + let dicts = linter::installed_dictionaries(); + if dicts.is_empty() { + println!(" none installed"); + return; + } + for dict in &dicts { + match dict { + linter::InstalledDictionary::Wordlist { lang, path } => { + match std::fs::read_to_string(path) { + Ok(content) => { + let words = content.lines().filter(|l| !l.trim().is_empty()).count(); + println!(" {lang}: {} ({words} words)", path.display()); + } + Err(e) => println!(" {lang}: {} (unreadable: {e})", path.display()), + } + } + linter::InstalledDictionary::Hunspell { + lang, + dic_path, + aff_path, + } => { + println!( + " {lang}: hunspell dictionary ({}, {})", + dic_path.display(), + aff_path.display() + ); + } + } + } +} + +fn report_project() { + println!("Project"); + let cwd = match std::env::current_dir() { + Ok(d) => d, + Err(e) => { + println!(" \u{2717} could not determine current directory: {e}"); + return; + } + }; + let config_path = cwd.join("project.toml"); + if !config_path.exists() { + println!( + " \u{2717} not a texforge project (no project.toml in {})", + cwd.display() + ); + return; + } + match Project::load() { + Ok(project) => { + println!(" \u{2713} {}", config_path.display()); + println!(" title: {}", project.config.document.title); + println!(" entry: {}", project.config.build.entry); + } + Err(e) => { + println!( + " \u{2717} {} exists but failed to parse: {e}", + config_path.display() + ); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + + // --- Tectonic status formatting (covers the missing-binary case) --- + + #[test] + fn format_tectonic_status_missing_binary() { + let out = format_tectonic_status(None); + assert!(out.contains("not found")); + assert!(!out.contains("present")); + } + + #[test] + fn format_tectonic_status_present_with_version() { + let path = PathBuf::from("/usr/local/bin/tectonic"); + let out = format_tectonic_status(Some((&path, Some("0.15.0")))); + assert!(out.contains("present: /usr/local/bin/tectonic")); + assert!(out.contains("version: 0.15.0")); + } + + #[test] + fn format_tectonic_status_present_unknown_version() { + let path = PathBuf::from("/usr/local/bin/tectonic"); + let out = format_tectonic_status(Some((&path, None))); + assert!(out.contains("could not determine")); + } + + // --- Version parsing --- + + #[test] + fn parse_version_extracts_simple_semver() { + assert_eq!(parse_version("tectonic 0.15.0\n"), Some("0.15.0".into())); + } + + #[test] + fn parse_version_handles_concatenated_banner_output() { + // Tectonic's actual `--version` output has no separator between the + // clap-generated line and its own banner. + assert_eq!( + parse_version("tectonic 0.15.0Tectonic 0.15.0"), + Some("0.15.0".into()) + ); + } + + #[test] + fn parse_version_none_when_absent() { + assert_eq!(parse_version("no version info here"), None); + } + + // --- Cache stats (covers the empty-cache case) --- + + #[test] + fn dir_stats_missing_dir_is_zero() { + let tmp = tempfile::tempdir().unwrap(); + let missing = tmp.path().join("does-not-exist"); + assert_eq!(dir_stats(&missing), (0, 0)); + } + + #[test] + fn dir_stats_empty_dir_is_zero() { + let tmp = tempfile::tempdir().unwrap(); + assert_eq!(dir_stats(tmp.path()), (0, 0)); + } + + #[test] + fn dir_stats_counts_nested_files() { + let tmp = tempfile::tempdir().unwrap(); + std::fs::write(tmp.path().join("a"), b"1234").unwrap(); + let sub = tmp.path().join("sub"); + std::fs::create_dir_all(&sub).unwrap(); + std::fs::write(sub.join("b"), b"12345678").unwrap(); + assert_eq!(dir_stats(tmp.path()), (12, 2)); + } + + #[test] + fn report_cache_empty_cache_dir_does_not_panic() { + let tmp = tempfile::tempdir().unwrap(); + report_cache(Some(tmp.path())); + } + + #[test] + fn report_cache_missing_dir_does_not_panic() { + let tmp = tempfile::tempdir().unwrap(); + let missing = tmp.path().join("not-created-yet"); + report_cache(Some(&missing)); + } + + #[test] + fn report_cache_none_does_not_panic() { + report_cache(None); + } + + #[test] + fn format_size_bytes() { + assert_eq!(format_size(0), "0 B"); + assert_eq!(format_size(512), "512 B"); + } + + #[test] + fn format_size_kilobytes() { + assert_eq!(format_size(2048), "2.0 KB"); + } + + #[test] + fn format_size_megabytes() { + assert_eq!(format_size(65 * 1024 * 1024), "65.0 MB"); + } + + // --- Font extraction from bundle manifests --- + + #[test] + fn extract_font_names_filters_by_extension() { + let manifest = "article.cls 20144 abc\nlmroman12-regular.otf 110400 def\nec-lmr12.tfm 12092 ghi\ncmr10.pfb 5000 jkl\n"; + let fonts = extract_font_names(manifest); + assert_eq!(fonts.len(), 2); + assert!(fonts.contains("lmroman12-regular.otf")); + assert!(fonts.contains("cmr10.pfb")); + assert!(!fonts.contains("article.cls")); + assert!(!fonts.contains("ec-lmr12.tfm")); + } + + #[test] + fn extract_font_names_empty_manifest() { + assert!(extract_font_names("").is_empty()); + } + + #[test] + fn extract_font_names_deduplicates() { + let manifest = "font.otf 100 aaa\nfont.otf 100 bbb\n"; + assert_eq!(extract_font_names(manifest).len(), 1); + } + + #[test] + fn fonts_from_cache_missing_manifests_dir_is_empty() { + let tmp = tempfile::tempdir().unwrap(); + assert!(fonts_from_cache(tmp.path()).is_empty()); + } + + #[test] + fn fonts_from_cache_reads_all_manifest_files() { + let tmp = tempfile::tempdir().unwrap(); + let manifests = tmp.path().join("manifests"); + std::fs::create_dir_all(&manifests).unwrap(); + std::fs::write(manifests.join("a.txt"), "one.otf 1 x\n").unwrap(); + std::fs::write(manifests.join("b.txt"), "two.pfb 1 y\n").unwrap(); + let fonts = fonts_from_cache(tmp.path()); + assert_eq!(fonts.len(), 2); + assert!(fonts.contains("one.otf")); + assert!(fonts.contains("two.pfb")); + } + + // --- Dictionary reporting --- + + #[test] + fn report_dictionaries_runs_without_panicking() { + report_dictionaries(); + } + + // --- Project detection --- + + #[test] + fn report_project_no_project_toml() { + let tmp = tempfile::tempdir().unwrap(); + let orig = std::env::current_dir().unwrap(); + std::env::set_current_dir(tmp.path()).unwrap(); + report_project(); + std::env::set_current_dir(&orig).unwrap(); + } + + #[test] + fn report_project_valid_project_toml() { + let tmp = tempfile::tempdir().unwrap(); + std::fs::write( + tmp.path().join("project.toml"), + "[document]\ntitle = \"T\"\nauthor = \"A\"\ntemplate = \"general\"\n\n[build]\nentry = \"main.tex\"\n", + ) + .unwrap(); + let orig = std::env::current_dir().unwrap(); + std::env::set_current_dir(tmp.path()).unwrap(); + report_project(); + std::env::set_current_dir(&orig).unwrap(); + } + + #[test] + fn report_project_invalid_project_toml() { + let tmp = tempfile::tempdir().unwrap(); + std::fs::write(tmp.path().join("project.toml"), "not valid {{{ toml").unwrap(); + let orig = std::env::current_dir().unwrap(); + std::env::set_current_dir(tmp.path()).unwrap(); + report_project(); + std::env::set_current_dir(&orig).unwrap(); + } + + // --- Cache directory resolution respects TECTONIC_CACHE_DIR --- + + #[test] + fn tectonic_cache_dir_honors_env_override() { + let orig = std::env::var_os("TECTONIC_CACHE_DIR"); + std::env::set_var("TECTONIC_CACHE_DIR", "/tmp/custom-tectonic-cache"); + let dir = tectonic_cache_dir(); + match orig { + Some(v) => std::env::set_var("TECTONIC_CACHE_DIR", v), + None => std::env::remove_var("TECTONIC_CACHE_DIR"), + } + assert_eq!(dir, Some(PathBuf::from("/tmp/custom-tectonic-cache"))); + } +} diff --git a/src/commands/init.rs b/src/commands/init.rs index 5b958dc..c5b89a5 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -2,7 +2,7 @@ use std::path::Path; -use anyhow::Result; +use anyhow::{Context, Result}; use inquire::{Confirm, Select, Text}; use crate::commands::new as new_cmd; @@ -205,11 +205,20 @@ fn check_for_updates() -> Result<()> { Ok(()) } -/// Download and install a new binary (placeholder for Phase 1) +/// Download and install a new binary by replacing the file currently +/// running (`std::env::current_exe()`) — never a hardcoded install +/// directory. If that file is a `cargo install`, refuse to touch it: cargo +/// keeps its own metadata about what it manages there, and this is a clean +/// exit, not a failure. fn download_and_install(version: &crate::version::SemVer) -> Result<()> { - // Phase 1: Show the download URL and instructions - // Phase 2+: Implement automatic download/install - let url = version_checker::get_release_download_url("UniverLab", "texforge", version); - println!(" Download: {}", url); - anyhow::bail!("Automatic installation not yet implemented. Please download from the URL above.") + let current_exe = + std::env::current_exe().context("failed to resolve the running binary's path")?; + + if version_checker::current_exe_is_cargo_managed(¤t_exe) { + println!("\n texforge was installed with cargo."); + println!(" Run: cargo install --force texforge\n"); + std::process::exit(0); + } + + version_checker::download_and_replace("UniverLab", "texforge", version, ¤t_exe) } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index d2f7934..2bbca49 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -4,7 +4,13 @@ pub mod build; pub mod check; pub mod clean; pub mod config; +pub mod doctor; pub mod fmt; pub mod init; pub mod new; +pub mod outline; +pub mod pdf; +pub mod preview; +pub mod spell; +pub mod stats; pub mod template; diff --git a/src/commands/outline.rs b/src/commands/outline.rs new file mode 100644 index 0000000..1ea4860 --- /dev/null +++ b/src/commands/outline.rs @@ -0,0 +1,398 @@ +//! `texforge outline` command implementation. +//! +//! Prints the document's section tree, or emits it as JSON for agents that +//! want the skeleton of a large document without reading the source. + +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +use anyhow::Result; +use serde::Serialize; + +use crate::domain::project::Project; +use crate::texparse::{self, SectionTracker, SpannedToken, Token}; +use crate::texutil; + +/// Deepest level shown in the outline: `part` through `subsubsection`. +const MAX_LEVEL: u8 = 4; + +/// One entry in the document outline. +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +pub struct OutlineSection { + /// Section level (`0` = part, ..., `4` = subsubsection). + pub level: u8, + /// Dotted section number (`1`, `1.1`, `2.1.1`). + pub number: String, + /// Resolved section title: wrapping macros (`\textit`, `\href`, + /// `\textcolor`, ...) are stripped down to their text. + pub title: String, + /// `.tex` file containing the section, relative to the project root. + pub file: String, + /// 1-based line of the section command within `file`. + pub line: usize, +} + +/// The document's section tree. +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +pub struct DocumentOutline { + /// Document name (the project title). + pub document: String, + /// Sections in `\input` traversal order. + pub sections: Vec<OutlineSection>, +} + +/// Build the outline for the document rooted at `root` with entry `entry`. +/// +/// Reads source only — the document does not need to compile. Sections from +/// `\input`-ed files appear at the position their `\input` occupies, not +/// grouped by file. +pub fn build_outline(document: &str, root: &Path, entry: &str) -> DocumentOutline { + let collection = texutil::collect_tex_files(root, entry); + + let mut sources: HashMap<PathBuf, String> = HashMap::new(); + let mut tokenized: HashMap<PathBuf, Vec<SpannedToken>> = HashMap::new(); + for path in &collection.files { + if let Ok(source) = std::fs::read_to_string(path) { + let tokens = texparse::tokenize_with_spans(&source).tokens; + sources.insert(path.clone(), source); + tokenized.insert(path.clone(), tokens); + } + } + + let entry_path = texutil::resolve_tex_path(root, entry); + let mut tracker = SectionTracker::new(6); + let mut sections = Vec::new(); + let mut stack = Vec::new(); + walk( + &entry_path, + root, + &tokenized, + &sources, + &mut tracker, + &mut sections, + &mut stack, + ); + + DocumentOutline { + document: document.to_string(), + sections, + } +} + +/// Walk one file's token stream, recursing into `\input` targets in place. +fn walk( + file: &Path, + root: &Path, + tokenized: &HashMap<PathBuf, Vec<SpannedToken>>, + sources: &HashMap<PathBuf, String>, + tracker: &mut SectionTracker, + sections: &mut Vec<OutlineSection>, + stack: &mut Vec<PathBuf>, +) { + if stack.iter().any(|path| path == file) { + return; + } + let Some(tokens) = tokenized.get(file) else { + return; + }; + stack.push(file.to_path_buf()); + let source = &sources[file]; + + for spanned in tokens { + match &spanned.token { + Token::Section { level, title, .. } => { + let number = tracker.enter(*level); + if *level <= MAX_LEVEL { + sections.push(OutlineSection { + level: *level, + number, + title: title.clone(), + file: relative_file(file, root), + line: line_at(source, spanned.start), + }); + } + } + Token::Command { name, args } if name == "input" => { + if let Some(input) = args.last() { + let child = texutil::resolve_tex_path(root, input); + if tokenized.contains_key(&child) { + walk(&child, root, tokenized, sources, tracker, sections, stack); + } + } + } + _ => {} + } + } + + stack.pop(); +} + +/// The section command's line number, computed from its byte offset. +fn line_at(source: &str, offset: usize) -> usize { + source[..offset] + .bytes() + .filter(|byte| *byte == b'\n') + .count() + + 1 +} + +/// `path` relative to `root`, falling back to the file name outside `root`. +fn relative_file(path: &Path, root: &Path) -> String { + path.strip_prefix(root) + .map(|rel| rel.to_string_lossy().into_owned()) + .unwrap_or_else(|_| { + path.file_name() + .map(|name| name.to_string_lossy().into_owned()) + .unwrap_or_default() + }) +} + +/// Emit the outline for the current project. +pub fn execute(json: bool) -> Result<()> { + let project = Project::load()?; + + let entry = project.root.join(&project.config.build.entry); + if !entry.exists() { + anyhow::bail!("Entry point file does not exist: {}", entry.display()); + } + + let outline = build_outline( + &project.config.document.title, + &project.root, + &project.config.build.entry, + ); + + if json { + println!("{}", serde_json::to_string_pretty(&outline)?); + } else { + print_human(&outline); + } + + Ok(()) +} + +/// Human-readable tree, indented by level. +fn print_human(outline: &DocumentOutline) { + println!("{}", outline.document); + for section in &outline.sections { + let indent = " ".repeat(section.level as usize); + println!("{}{} {}", indent, section.number, section.title); + } +} + +#[cfg(test)] +mod tests { + use std::fs; + + use super::*; + + fn write(files: &[(&str, &str)]) -> tempfile::TempDir { + let dir = tempfile::tempdir().unwrap(); + for (name, source) in files { + let path = dir.path().join(name); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).unwrap(); + } + fs::write(path, source).unwrap(); + } + dir + } + + fn numbers(outline: &DocumentOutline) -> Vec<String> { + outline + .sections + .iter() + .map(|section| section.number.clone()) + .collect() + } + + fn titles(outline: &DocumentOutline) -> Vec<String> { + outline + .sections + .iter() + .map(|section| section.title.clone()) + .collect() + } + + #[test] + fn numbering_follows_the_hierarchy() { + let dir = write(&[( + "main.tex", + "\\documentclass{book}\n\\begin{document}\n\\part{One}\n\\chapter{Intro}\n\\section{First}\n\\subsection{Sub}\n\\subsubsection{Subsub}\n\\section{Second}\n\\end{document}", + )]); + let outline = build_outline("Doc", dir.path(), "main.tex"); + assert_eq!( + numbers(&outline), + vec!["1", "1.1", "1.1.1", "1.1.1.1", "1.1.1.1.1", "1.1.2"] + ); + } + + #[test] + fn sections_from_input_files_appear_at_their_input_position() { + let dir = write(&[ + ( + "main.tex", + "\\begin{document}\n\\part{Part}\n\\input{ch1}\n\\section{After Ch1}\n\\input{chapters/ch2}\n\\section{After Ch2}\n\\end{document}", + ), + ("ch1.tex", "\\chapter{Chapter One}\n\\input{ch1_sec}\n"), + ("ch1_sec.tex", "\\section{Ch1 Section}\n"), + ( + "chapters/ch2.tex", + "\\chapter{Chapter Two}\n\\section{Ch2 Section}\n", + ), + ]); + let outline = build_outline("Doc", dir.path(), "main.tex"); + assert_eq!( + numbers(&outline), + vec!["1", "1.1", "1.1.1", "1.1.2", "1.2", "1.2.1", "1.2.2"] + ); + assert_eq!( + titles(&outline), + vec![ + "Part", + "Chapter One", + "Ch1 Section", + "After Ch1", + "Chapter Two", + "Ch2 Section", + "After Ch2" + ] + ); + } + + #[test] + fn paragraphs_are_not_included() { + let dir = write(&[( + "main.tex", + "\\begin{document}\n\\section{One}\n\\paragraph{Note}\n\\subparagraph{Detail}\n\\section{Two}\n\\end{document}", + )]); + let outline = build_outline("Doc", dir.path(), "main.tex"); + assert_eq!(numbers(&outline), vec!["1", "2"]); + assert!(outline.sections.iter().all(|section| section.level <= 4)); + } + + #[test] + fn reports_source_file_and_line() { + let dir = write(&[( + "main.tex", + "\\begin{document}\n\n\\section{Intro}\n\\end{document}", + )]); + let outline = build_outline("Doc", dir.path(), "main.tex"); + let section = &outline.sections[0]; + assert_eq!(section.file, "main.tex"); + assert_eq!(section.line, 3); + } + + #[test] + fn file_paths_are_relative_to_root() { + let dir = write(&[("chapters/intro.tex", "\\section{Intro}\n")]); + let outline = build_outline("Doc", dir.path(), "chapters/intro.tex"); + assert_eq!(outline.sections[0].file, "chapters/intro.tex"); + } + + #[test] + fn circular_inputs_do_not_duplicate_sections() { + let dir = write(&[ + ( + "main.tex", + "\\begin{document}\n\\section{A}\n\\input{loop}\n\\end{document}", + ), + ("loop.tex", "\\input{main}\n\\section{B}\n"), + ]); + let outline = build_outline("Doc", dir.path(), "main.tex"); + assert_eq!(titles(&outline), vec!["A", "B"]); + } + + #[test] + fn macro_wrapped_titles_resolve_to_plain_text() { + let dir = write(&[( + "main.tex", + "\\begin{document}\n\\section{\\href{https://univerlab.org}{UniverLab.org}}\n\\subsection{AI Engineer en Accenture\n\\textcolor{lightgray}{\\leaders\\hbox{.}\\hfill}\n\\textit{Julio 2026 -- Actual}}\n\\end{document}", + )]); + let outline = build_outline("Doc", dir.path(), "main.tex"); + assert_eq!( + titles(&outline), + vec![ + "UniverLab.org", + "AI Engineer en Accenture Julio 2026 -- Actual" + ] + ); + for title in titles(&outline) { + assert!(!title.contains('\\'), "title leaks markup: {title:?}"); + assert!( + !title.contains('\n'), + "title spans multiple lines: {title:?}" + ); + } + } + + #[test] + fn evidence_escaped_ampersand_survives_in_outline() { + let dir = write(&[( + "main.tex", + "\\begin{document}\n\\section{Team}\n\\subsection*{\\textit{Fundador \\& Lead Engineer}}\n\\end{document}", + )]); + let outline = build_outline("Doc", dir.path(), "main.tex"); + assert_eq!(titles(&outline), vec!["Team", "Fundador & Lead Engineer"]); + + let value = serde_json::to_value(&outline).unwrap(); + let json_title = value["sections"][1]["title"].as_str().unwrap(); + assert_eq!(json_title, "Fundador & Lead Engineer"); + } + + #[test] + fn json_contract_keys_are_stable() { + let dir = write(&[( + "main.tex", + "\\begin{document}\n\\section{S}\n\\end{document}", + )]); + let outline = build_outline("Doc", dir.path(), "main.tex"); + let value = serde_json::to_value(&outline).unwrap(); + let object = value.as_object().unwrap(); + let mut top_keys: Vec<&str> = object.keys().map(String::as_str).collect(); + top_keys.sort_unstable(); + assert_eq!(top_keys, vec!["document", "sections"]); + let section = object["sections"][0].as_object().unwrap(); + let mut section_keys: Vec<&str> = section.keys().map(String::as_str).collect(); + section_keys.sort_unstable(); + assert_eq!( + section_keys, + vec!["file", "level", "line", "number", "title"] + ); + } + + #[test] + fn execute_json_on_small_project() { + let tmp = tempfile::tempdir().unwrap(); + std::fs::write( + tmp.path().join("project.toml"), + "[document]\ntitle = \"T\"\nauthor = \"A\"\ntemplate = \"general\"\n\n[build]\nentry = \"main.tex\"\n", + ) + .unwrap(); + std::fs::write( + tmp.path().join("main.tex"), + "\\begin{document}\n\\section{Intro}\ntext\n\\end{document}", + ) + .unwrap(); + let orig = std::env::current_dir().unwrap(); + std::env::set_current_dir(tmp.path()).unwrap(); + let result = execute(true); + std::env::set_current_dir(&orig).unwrap(); + result.unwrap(); + } + + #[test] + fn execute_missing_entry_errors() { + let tmp = tempfile::tempdir().unwrap(); + std::fs::write( + tmp.path().join("project.toml"), + "[document]\ntitle = \"T\"\nauthor = \"A\"\ntemplate = \"general\"\n\n[build]\nentry = \"missing.tex\"\n", + ) + .unwrap(); + let orig = std::env::current_dir().unwrap(); + std::env::set_current_dir(tmp.path()).unwrap(); + let result = execute(false); + std::env::set_current_dir(&orig).unwrap(); + assert!(result.is_err()); + } +} diff --git a/src/commands/pdf.rs b/src/commands/pdf.rs new file mode 100644 index 0000000..c15fdd9 --- /dev/null +++ b/src/commands/pdf.rs @@ -0,0 +1,375 @@ +//! `texforge pdf` — text extraction, info, page breaks, and fidelity check. + +use std::path::{Path, PathBuf}; + +use anyhow::{bail, Result}; + +use crate::commands::outline; +use crate::domain::project::Project; +use crate::linter::Severity; +use crate::pdftext::{self, PdfInfo}; +use crate::utils::sanitize_filename; + +/// Subcommands of `texforge pdf`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PdfAction { + /// Print extracted text (`--raw` keeps ligature codepoints). + Text { raw: bool }, + /// Pages, fonts (embedded?), metadata. + Info, + /// Diff-friendly page → section map. + Pages, + /// Source-to-PDF fidelity: missing significant words as warnings. + Check, +} + +/// Dispatch a `texforge pdf` action for the project in the current directory. +pub fn execute(action: PdfAction) -> Result<()> { + let project = Project::load()?; + execute_for_project(&project, action) +} + +fn execute_for_project(project: &Project, action: PdfAction) -> Result<()> { + let pdf_path = compiled_pdf_path(project); + if !pdf_path.exists() { + bail!( + "No compiled PDF at {} — run `texforge build` first", + pdf_path.display() + ); + } + + match action { + PdfAction::Text { raw } => cmd_text(&pdf_path, raw), + PdfAction::Info => cmd_info(&pdf_path), + PdfAction::Pages => cmd_pages(project, &pdf_path), + PdfAction::Check => cmd_check(project, &pdf_path), + } +} + +fn compiled_pdf_path(project: &Project) -> PathBuf { + project.root.join(format!( + "{}.pdf", + sanitize_filename(&project.config.document.title) + )) +} + +fn cmd_text(pdf_path: &Path, raw: bool) -> Result<()> { + let extracted = pdftext::extract_text(pdf_path)?; + let text = if raw { + extracted + } else { + pdftext::normalize_pdf_text(&extracted) + }; + print!("{text}"); + if !text.ends_with('\n') { + println!(); + } + Ok(()) +} + +fn cmd_info(pdf_path: &Path) -> Result<()> { + let info = pdftext::pdf_info(pdf_path)?; + print_info(&info); + Ok(()) +} + +fn print_info(info: &PdfInfo) { + println!("pages: {}", info.pages); + println!("fonts: {}", info.fonts.len()); + for font in &info.fonts { + println!( + " name={} subtype={} embedded={}", + font.name, font.subtype, font.embedded + ); + } + let m = &info.metadata; + println!("metadata:"); + print_meta("title", m.title.as_deref()); + print_meta("author", m.author.as_deref()); + print_meta("subject", m.subject.as_deref()); + print_meta("keywords", m.keywords.as_deref()); + print_meta("creator", m.creator.as_deref()); + print_meta("producer", m.producer.as_deref()); + print_meta("creation_date", m.creation_date.as_deref()); + print_meta("mod_date", m.mod_date.as_deref()); +} + +fn print_meta(key: &str, value: Option<&str>) { + match value { + Some(v) => println!(" {key}={v}"), + None => println!(" {key}="), + } +} + +fn cmd_pages(project: &Project, pdf_path: &Path) -> Result<()> { + let page_texts = pdftext::extract_text_by_pages(pdf_path)?; + let outline = outline::build_outline( + &project.config.document.title, + &project.root, + &project.config.build.entry, + ); + let sections: Vec<(String, String)> = outline + .sections + .iter() + .map(|s| (s.number.clone(), s.title.clone())) + .collect(); + let breaks = pdftext::page_breaks(&page_texts, §ions); + println!("{}", pdftext::format_page_breaks(&breaks)); + Ok(()) +} + +fn cmd_check(project: &Project, pdf_path: &Path) -> Result<()> { + let mut findings = + pdftext::check_fidelity(&project.root, &project.config.build.entry, pdf_path)?; + findings.extend(pdftext::check_quality(pdf_path)?); + + if findings.is_empty() { + println!(" ◇ PDF check: fidelity, fonts, and metadata look good"); + return Ok(()); + } + + let warnings: Vec<_> = findings + .iter() + .filter(|f| f.severity == Severity::Warning) + .collect(); + + println!("WARNINGS ({})", warnings.len()); + for f in &warnings { + println!(" WARNING [{}]", f.file); + println!(" {}", f.message); + if let Some(ref s) = f.suggestion { + println!(" suggestion: {s}"); + } + println!(); + } + + // Findings are warnings only — exit zero unless the caller + // pipes through `check --deny-warnings`. Distinct words, not flood. + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::domain::project::{BuildConfig, DocumentConfig, ProjectConfig}; + use std::fs; + + const LIGATURES: &[u8] = include_bytes!("../../tests/fixtures/ligatures.pdf"); + const PAGES: &[u8] = include_bytes!("../../tests/fixtures/pages-ligatures.pdf"); + + fn project_with_pdf( + pdf_bytes: &[u8], + title: &str, + main_tex: &str, + ) -> (tempfile::TempDir, Project) { + let dir = tempfile::tempdir().unwrap(); + let root = dir.path().to_path_buf(); + fs::write( + root.join("project.toml"), + format!( + r#"[document] +title = "{title}" +author = "Test" +template = "general" + +[build] +entry = "main.tex" +"# + ), + ) + .unwrap(); + fs::write(root.join("main.tex"), main_tex).unwrap(); + let pdf_name = format!("{}.pdf", sanitize_filename(title)); + fs::write(root.join(pdf_name), pdf_bytes).unwrap(); + let project = Project { + root, + config: ProjectConfig { + document: DocumentConfig { + title: title.into(), + author: "Test".into(), + template: "general".into(), + }, + build: BuildConfig { + entry: "main.tex".into(), + bibliography: None, + reproducible: None, + }, + diagrams: None, + }, + }; + (dir, project) + } + + #[test] + fn text_normalizes_ligatures_by_default() { + let (_dir, project) = project_with_pdf( + LIGATURES, + "Lig Doc", + r"\documentclass{article}\begin{document}Artificial\end{document}", + ); + execute_for_project(&project, PdfAction::Text { raw: false }).unwrap(); + } + + #[test] + fn text_raw_keeps_ligature_codepoints() { + let (_dir, project) = project_with_pdf( + LIGATURES, + "Lig Doc", + r"\documentclass{article}\begin{document}Artificial\end{document}", + ); + let pdf = compiled_pdf_path(&project); + let raw = pdftext::extract_text(&pdf).unwrap(); + assert!(raw.contains('\u{FB01}')); + execute_for_project(&project, PdfAction::Text { raw: true }).unwrap(); + } + + #[test] + fn info_and_pages_and_check_run() { + let tex = r#"\documentclass{article} +\begin{document} +\section{Introduction} +Artificial Intelligence and MLflow workflows for local-first systems. +Deep Learning appears here with enough filler text to eventually force a page break if we add more. +\newpage +\section{Methods} +More Artificial Intelligence content on page two about workflows. +\end{document} +"#; + let (_dir, project) = project_with_pdf(PAGES, "Pages Doc", tex); + execute_for_project(&project, PdfAction::Info).unwrap(); + execute_for_project(&project, PdfAction::Pages).unwrap(); + execute_for_project(&project, PdfAction::Check).unwrap(); + } + + #[test] + fn pages_attributes_macro_wrapped_heading_to_its_own_page() { + // Same fixture PDF as `info_and_pages_and_check_run`, but the + // heading opening page 2 is wrapped in `\textit`. Before titles + // were resolved at the tokenizer, the raw markup title never + // matched the plain page text, so page 2 stayed attributed to + // section 1 instead of section 2. + let tex = r#"\documentclass{article} +\begin{document} +\section{Introduction} +Artificial Intelligence and MLflow workflows for local-first systems. +Deep Learning appears here with enough filler text to eventually force a page break if we add more. +\newpage +\section{\textit{Methods}} +More Artificial Intelligence content on page two about workflows. +\end{document} +"#; + let (_dir, project) = project_with_pdf(PAGES, "Pages Doc Macro", tex); + let pdf_path = compiled_pdf_path(&project); + let page_texts = pdftext::extract_text_by_pages(&pdf_path).unwrap(); + let outline = outline::build_outline( + &project.config.document.title, + &project.root, + &project.config.build.entry, + ); + let sections: Vec<(String, String)> = outline + .sections + .iter() + .map(|s| (s.number.clone(), s.title.clone())) + .collect(); + assert_eq!( + sections[1].1, "Methods", + "title should be resolved, not raw markup" + ); + + let breaks = pdftext::page_breaks(&page_texts, §ions); + assert_eq!( + pdftext::format_page_breaks(&breaks), + "page=1 section=1 title=Introduction\npage=2 section=2 title=Methods" + ); + } + + #[test] + fn pages_does_not_let_an_unmatchable_heading_hide_a_later_section() { + // TE5: page 2 opens with a macro-wrapped `\section` (an `\href`), + // exactly the shape that survived the 2c16831 title-resolution fix + // because that fix targeted `outline`, not this mapper. A + // dot-leader subsection sits between "Introduction" and the `\href` + // section — its resolved title never appears verbatim in the PDF + // text (the PDF renders a literal row of dots; the resolved title + // strips it to a space) — and previously that unmatched title + // permanently blocked every section after it, including the one + // that truly opens page 2. + let tex = r#"\documentclass{article} +\begin{document} +\section{Introduction} +Artificial Intelligence and MLflow workflows for local-first systems. +Deep Learning appears here with enough filler text to eventually force a page break if we add more. +\subsection{AI Engineer en Accenture +\textcolor{lightgray}{\leaders\hbox{.}\hfill} +\textit{Julio 2026 -- Actual}} +\newpage +\section{\href{https://example.com}{Methods}} +More Artificial Intelligence content on page two about workflows. +\end{document} +"#; + let (_dir, project) = project_with_pdf(PAGES, "Pages Doc Href", tex); + let pdf_path = compiled_pdf_path(&project); + let page_texts = pdftext::extract_text_by_pages(&pdf_path).unwrap(); + let outline = outline::build_outline( + &project.config.document.title, + &project.root, + &project.config.build.entry, + ); + let sections: Vec<(String, String)> = outline + .sections + .iter() + .map(|s| (s.number.clone(), s.title.clone())) + .collect(); + assert_eq!( + sections[2].1, "Methods", + "title should be resolved, not raw markup" + ); + + let breaks = pdftext::page_breaks(&page_texts, §ions); + assert_eq!( + pdftext::format_page_breaks(&breaks), + "page=1 section=1 title=Introduction\npage=2 section=2 title=Methods" + ); + } + + #[test] + fn missing_pdf_is_helpful() { + let dir = tempfile::tempdir().unwrap(); + let root = dir.path().to_path_buf(); + fs::write( + root.join("project.toml"), + r#"[document] +title = "No Pdf" +author = "Test" +template = "general" + +[build] +entry = "main.tex" +"#, + ) + .unwrap(); + fs::write( + root.join("main.tex"), + r"\documentclass{article}\begin{document}Hi\end{document}", + ) + .unwrap(); + let project = Project { + root, + config: ProjectConfig { + document: DocumentConfig { + title: "No Pdf".into(), + author: "Test".into(), + template: "general".into(), + }, + build: BuildConfig { + entry: "main.tex".into(), + bibliography: None, + reproducible: None, + }, + diagrams: None, + }, + }; + let err = execute_for_project(&project, PdfAction::Info).unwrap_err(); + assert!(err.to_string().contains("texforge build")); + } +} diff --git a/src/commands/preview.rs b/src/commands/preview.rs new file mode 100644 index 0000000..9249532 --- /dev/null +++ b/src/commands/preview.rs @@ -0,0 +1,244 @@ +//! `texforge preview` command implementation. +//! +//! Rasterizes the compiled PDF into PNG pages with the pure-Rust rasterizer +//! and prints per-page ink coverage, so blank, nearly-blank and overfull pages +//! surface without opening the document. + +use std::io::BufWriter; +use std::path::{Path, PathBuf}; + +use anyhow::{bail, Context, Result}; + +use crate::domain::project::Project; +use crate::raster::PdfDocument; +use crate::utils::sanitize_filename; + +/// Rasterize the compiled PDF to PNG pages. +pub fn execute(page: Option<usize>, scale: f32, out: Option<PathBuf>) -> Result<()> { + let project = Project::load()?; + execute_for_project(&project, page, scale, out) +} + +/// The work behind [`execute`], taking a project directly so tests can run it +/// without touching the process-global current directory. +fn execute_for_project( + project: &Project, + page: Option<usize>, + scale: f32, + out: Option<PathBuf>, +) -> Result<()> { + if !scale.is_finite() || scale <= 0.0 { + bail!("--scale must be a finite positive number"); + } + + let pdf_path = project.root.join(format!( + "{}.pdf", + sanitize_filename(&project.config.document.title) + )); + if !pdf_path.exists() { + bail!( + "No compiled PDF at {} — run `texforge build` first", + pdf_path.display() + ); + } + + let out_dir = match out { + Some(dir) if dir.is_absolute() => dir, + Some(dir) => project.root.join(dir), + None => project.root.join("preview"), + }; + std::fs::create_dir_all(&out_dir) + .with_context(|| format!("failed to create {}", out_dir.display()))?; + + let document = PdfDocument::open(&pdf_path)?; + let pages = document.page_count(); + if pages == 0 { + bail!("{} has no pages", pdf_path.display()); + } + + let page_indexes = match page { + Some(n) if n == 0 || n > pages => { + bail!("--page {n} is out of range (document has {pages} pages)"); + } + Some(n) => vec![n - 1], + None => (0..pages).collect(), + }; + + println!( + "Rasterizing {} page(s) at {scale:.2}x -> {}", + page_indexes.len(), + out_dir.display() + ); + let width_digits = pages.to_string().len(); + for index in page_indexes { + let rendered = document.render_page(index, scale)?; + let name = format!( + "page-{:0width$}.png", + rendered.index + 1, + width = width_digits + ); + let path = out_dir.join(&name); + write_png(&path, rendered.width, rendered.height, &rendered.rgba)?; + println!( + " {name} {}x{} ink {:.1}%", + rendered.width, + rendered.height, + rendered.ink_coverage * 100.0 + ); + } + + Ok(()) +} + +/// Write `rgba` to `path` as a PNG. +fn write_png(path: &Path, width: usize, height: usize, rgba: &[u8]) -> Result<()> { + let file = std::fs::File::create(path) + .with_context(|| format!("failed to create {}", path.display()))?; + let mut encoder = png::Encoder::new(BufWriter::new(file), width as u32, height as u32); + encoder.set_color(png::ColorType::Rgba); + encoder.set_depth(png::BitDepth::Eight); + let mut writer = encoder + .write_header() + .with_context(|| format!("failed to encode {}", path.display()))?; + writer + .write_image_data(rgba) + .with_context(|| format!("failed to write {}", path.display()))?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::fs; + + use super::*; + use crate::domain::project::{BuildConfig, DocumentConfig, ProjectConfig}; + + const FIXTURE: &[u8] = include_bytes!("../../tests/fixtures/two-page.pdf"); + + /// A project whose compiled PDF is the committed two-page fixture. The + /// title sanitizes to `preview-doc`, so the PDF must be named accordingly. + fn project() -> (tempfile::TempDir, Project) { + let dir = tempfile::tempdir().unwrap(); + fs::write( + dir.path().join("project.toml"), + "[document]\ntitle = \"Preview Doc\"\nauthor = \"A\"\ntemplate = \"general\"\n\n[build]\nentry = \"main.tex\"\n", + ) + .unwrap(); + fs::write(dir.path().join("preview-doc.pdf"), FIXTURE).unwrap(); + let project = Project { + root: dir.path().to_path_buf(), + config: ProjectConfig { + document: DocumentConfig { + title: "Preview Doc".to_string(), + author: "A".to_string(), + template: "general".to_string(), + }, + build: BuildConfig { + entry: "main.tex".to_string(), + bibliography: None, + reproducible: None, + }, + diagrams: None, + }, + }; + (dir, project) + } + + /// Run the command with an injected project, avoiding process-global + /// current-directory state in parallel tests. + fn run(project: &Project, page: Option<usize>, scale: f32, out: Option<PathBuf>) -> Result<()> { + execute_for_project(project, page, scale, out) + } + + /// Decode a PNG and return `(width, height, rgba)`. + fn decode(path: &Path) -> (u32, u32, Vec<u8>) { + let file = fs::File::open(path).unwrap(); + let decoder = png::Decoder::new(file); + let mut reader = decoder.read_info().unwrap(); + let mut buf = vec![0u8; reader.output_buffer_size()]; + let info = reader.next_frame(&mut buf).unwrap(); + let (w, h) = (info.width, info.height); + buf.truncate((w * h * 4) as usize); + (w, h, buf) + } + + #[test] + fn writes_pngs_for_all_pages() { + let (dir, project) = project(); + run(&project, None, 1.0, None).unwrap(); + for name in ["page-1.png", "page-2.png"] { + let path = dir.path().join("preview").join(name); + let (w, h, rgba) = decode(&path); + assert_eq!((w, h), (612, 842), "{name}"); + assert!(rgba.chunks_exact(4).all(|px| px[3] == 255)); + } + } + + #[test] + fn single_page_flag_limits_output() { + let (dir, project) = project(); + run(&project, Some(1), 1.0, None).unwrap(); + let preview = dir.path().join("preview"); + assert!(preview.join("page-1.png").exists()); + assert!(!preview.join("page-2.png").exists()); + } + + #[test] + fn custom_out_dir_is_used() { + let (dir, project) = project(); + let out = dir.path().join("renders"); + run(&project, None, 1.0, Some(out.clone())).unwrap(); + assert!(out.join("page-1.png").exists()); + assert!(!dir.path().join("preview").exists()); + } + + #[test] + fn scale_flag_resizes_the_output() { + let (dir, project) = project(); + run(&project, Some(1), 0.5, None).unwrap(); + let (w, h, _) = decode(&dir.path().join("preview/page-1.png")); + assert_eq!((w, h), (306, 421)); + } + + #[test] + fn missing_pdf_is_a_helpful_error() { + let dir = tempfile::tempdir().unwrap(); + fs::write( + dir.path().join("project.toml"), + "[document]\ntitle = \"Preview Doc\"\nauthor = \"A\"\ntemplate = \"general\"\n\n[build]\nentry = \"main.tex\"\n", + ) + .unwrap(); + let project = Project { + root: dir.path().to_path_buf(), + config: ProjectConfig { + document: DocumentConfig { + title: "Preview Doc".to_string(), + author: "A".to_string(), + template: "general".to_string(), + }, + build: BuildConfig { + entry: "main.tex".to_string(), + bibliography: None, + reproducible: None, + }, + diagrams: None, + }, + }; + let err = run(&project, None, 1.0, None).unwrap_err(); + assert!(err.to_string().contains("run `texforge build` first")); + } + + #[test] + fn out_of_range_page_errors() { + let (_dir, project) = project(); + let err = run(&project, Some(9), 1.0, None).unwrap_err(); + assert!(err.to_string().contains("out of range")); + } + + #[test] + fn non_positive_scale_errors() { + let (_dir, project) = project(); + let err = run(&project, Some(1), 0.0, None).unwrap_err(); + assert!(err.to_string().contains("finite positive")); + } +} diff --git a/src/commands/spell.rs b/src/commands/spell.rs new file mode 100644 index 0000000..26dc9d2 --- /dev/null +++ b/src/commands/spell.rs @@ -0,0 +1,633 @@ +//! `texforge spell` — manage the personal spell-check whitelist (dictionary) +//! from the CLI, reusing the whitelist mechanism the linter already reads. +//! +//! Two scopes: the global personal dictionary (`~/.texforge/spell-words`, +//! shared by every project and the default, since the words in this list are +//! overwhelmingly true of the person rather than of one document) and the +//! project (the first of `PROJECT_WHITELIST_FILES` that already exists, or +//! `.texforge/spell-words` if none do), selected explicitly with `--local`. +//! Adding is append-only and never rewrites existing bytes; removing rewrites +//! the file with only the matching lines dropped. + +use std::collections::HashSet; +use std::fs; +use std::io::Write; +use std::path::{Path, PathBuf}; + +use anyhow::{Context, Result}; + +use crate::domain::project::Project; +use crate::linter::{global_whitelist_path, parse_whitelist_words, PROJECT_WHITELIST_FILES}; + +/// Which whitelist a `spell` action targets. Global is the default; `--local` +/// is the explicit opt-in to the project's whitelist (decisions 1-3). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Scope { + Global, + Local, +} + +impl Scope { + /// `--local` and `--global` are mutually exclusive at the clap level, so + /// at most one of these is ever true here; global wins when neither is + /// set, since global is the default (decision 1). + pub fn from_flags(local: bool, global: bool) -> Self { + debug_assert!(!(local && global)); + if local { + Scope::Local + } else { + let _ = global; + Scope::Global + } + } +} + +/// Subcommands of `texforge spell`. +#[derive(Debug, Clone)] +pub enum SpellAction { + /// Add one or more words to the whitelist. + Add { words: Vec<String>, scope: Scope }, + /// List the effective whitelist words for the scope. + List { scope: Scope }, + /// Remove one or more words from the whitelist. + Remove { words: Vec<String>, scope: Scope }, +} + +/// Dispatch a `texforge spell` action. +pub fn execute(action: SpellAction) -> Result<()> { + match action { + SpellAction::Add { words, scope } => add(&words, scope), + SpellAction::List { scope } => list(scope), + SpellAction::Remove { words, scope } => remove(&words, scope), + } +} + +/// Resolve the file `add`/`remove`/`list` should target for the given scope. +/// Global always resolves to `global_whitelist_path()`. Local resolves to +/// the first of `PROJECT_WHITELIST_FILES` that already exists, or +/// `.texforge/spell-words` if none do (decision 4) — this does not require +/// the file to exist yet, only its parent project. `--local` outside a +/// texforge project is an error, not a silent fallback to global +/// (decision 5): `Project::load()` already fails with a message naming what +/// was expected when no project is found. +fn target_file(scope: Scope) -> Result<PathBuf> { + match scope { + Scope::Global => global_whitelist_path() + .context("Could not determine home directory for the global personal dictionary"), + Scope::Local => { + let project = Project::load().context( + "`--local` requires a texforge project (project.toml not found in this \ + directory or any parent)", + )?; + for name in PROJECT_WHITELIST_FILES { + let p = project.root.join(name); + if p.exists() { + return Ok(p); + } + } + Ok(project.root.join(".texforge").join("spell-words")) + } + } +} + +fn add(words: &[String], scope: Scope) -> Result<()> { + if words.is_empty() { + anyhow::bail!("No words given; usage: texforge spell add <WORD>..."); + } + + let path = target_file(scope)?; + if let Some(parent) = path.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("Failed to create {}", parent.display()))?; + } + + // Single read to find out what's already there; the write below only + // appends and never rewrites existing bytes. + let existing = fs::read_to_string(&path).unwrap_or_default(); + let mut known = parse_whitelist_words(&existing); + + let mut to_append: Vec<&str> = Vec::new(); + for w in words { + let wl = w.to_lowercase(); + if known.contains(&wl) { + println!(" ◇ '{}' already present in {}", w, path.display()); + continue; + } + known.insert(wl); + to_append.push(w); + } + + if to_append.is_empty() { + return Ok(()); + } + + let mut file = fs::OpenOptions::new() + .create(true) + .append(true) + .open(&path) + .with_context(|| format!("Failed to open {}", path.display()))?; + + let mut buf = String::new(); + if !existing.is_empty() && !existing.ends_with('\n') { + buf.push('\n'); + } + for w in &to_append { + buf.push_str(w); + buf.push('\n'); + } + file.write_all(buf.as_bytes()) + .with_context(|| format!("Failed to write to {}", path.display()))?; + + for w in &to_append { + println!(" ◇ Added '{}' to {}", w, path.display()); + } + + Ok(()) +} + +fn remove(words: &[String], scope: Scope) -> Result<()> { + if words.is_empty() { + anyhow::bail!("No words given; usage: texforge spell remove <WORD>..."); + } + + let path = target_file(scope)?; + + let Ok(content) = fs::read_to_string(&path) else { + for w in words { + println!( + " ◇ '{}' was not in {} ({} does not exist)", + w, + path.display(), + path.display() + ); + } + return Ok(()); + }; + + let targets: HashSet<String> = words.iter().map(|w| w.to_lowercase()).collect(); + let mut removed: HashSet<String> = HashSet::new(); + let mut kept_lines: Vec<&str> = Vec::new(); + + for line in content.lines() { + let trimmed = line.trim(); + let is_word_line = !trimmed.is_empty() && !trimmed.starts_with('#'); + if is_word_line && targets.contains(&trimmed.to_lowercase()) { + removed.insert(trimmed.to_lowercase()); + continue; + } + kept_lines.push(line); + } + + for w in words { + if removed.contains(&w.to_lowercase()) { + println!(" ◇ Removed '{}' from {}", w, path.display()); + } else { + println!(" ◇ '{}' was not in {}", w, path.display()); + } + } + + // Nothing matched: leave the file byte-for-byte untouched rather than + // rewriting it to the same content. + if removed.is_empty() { + return Ok(()); + } + + let mut new_content = kept_lines.join("\n"); + if content.ends_with('\n') && !new_content.is_empty() { + new_content.push('\n'); + } + fs::write(&path, new_content).with_context(|| format!("Failed to write {}", path.display()))?; + + Ok(()) +} + +fn list(scope: Scope) -> Result<()> { + if scope == Scope::Global { + let path = global_whitelist_path() + .context("Could not determine home directory for the global personal dictionary")?; + print_whitelist_file(&path); + return Ok(()); + } + + let project = Project::load().context( + "`--local` requires a texforge project (project.toml not found in this directory or \ + any parent)", + )?; + let mut any = false; + for name in PROJECT_WHITELIST_FILES { + let p = project.root.join(name); + if p.exists() { + any = true; + print_whitelist_file(&p); + } + } + if !any { + println!( + " ◇ No project whitelist file exists yet (checked: {})", + PROJECT_WHITELIST_FILES.join(", ") + ); + } + Ok(()) +} + +fn print_whitelist_file(path: &Path) { + match fs::read_to_string(path) { + Ok(content) => { + let words = parse_whitelist_words(&content); + let mut sorted: Vec<&String> = words.iter().collect(); + sorted.sort(); + println!("{} ({} word(s)):", path.display(), sorted.len()); + for w in sorted { + println!(" {}", w); + } + } + Err(_) => println!("{} (not found)", path.display()), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::linter::spell as linter_spell; + use tempfile::TempDir; + + /// A minimal project.toml so `Project::load()` succeeds from `cwd`. + fn write_project_toml(root: &Path) { + fs::write( + root.join("project.toml"), + r#"[document] +title = "Test" +author = "Test" +template = "general" + +[build] +entry = "main.tex" +"#, + ) + .unwrap(); + } + + /// Runs `body` with `cwd` set to `root` for the duration, then restores + /// the original working directory. `Project::load()` reads + /// `std::env::current_dir()`, so scope commands need a real cwd change. + fn with_cwd<T>(root: &Path, body: impl FnOnce() -> T) -> T { + let orig = std::env::current_dir().unwrap(); + std::env::set_current_dir(root).unwrap(); + let result = body(); + std::env::set_current_dir(orig).unwrap(); + result + } + + fn with_home<T>(home: &Path, body: impl FnOnce() -> T) -> T { + let orig_home = std::env::var("HOME").ok(); + std::env::set_var("HOME", home); + let result = body(); + match orig_home { + Some(v) => std::env::set_var("HOME", v), + None => std::env::remove_var("HOME"), + } + result + } + + #[test] + fn add_creates_texforge_spell_words_when_no_whitelist_file_exists() { + let project = TempDir::new().unwrap(); + write_project_toml(project.path()); + + with_cwd(project.path(), || { + add(&["docker".to_string()], Scope::Local).unwrap(); + }); + + let target = project.path().join(".texforge").join("spell-words"); + assert!(target.exists()); + assert_eq!(fs::read_to_string(&target).unwrap(), "docker\n"); + assert!(!project.path().join("spell-whitelist.txt").exists()); + } + + #[test] + fn add_appends_to_spell_whitelist_txt_when_it_already_exists_and_does_not_create_texforge_dir() + { + let project = TempDir::new().unwrap(); + write_project_toml(project.path()); + fs::write(project.path().join("spell-whitelist.txt"), "acme\n").unwrap(); + + with_cwd(project.path(), || { + add(&["docker".to_string()], Scope::Local).unwrap(); + }); + + assert_eq!( + fs::read_to_string(project.path().join("spell-whitelist.txt")).unwrap(), + "acme\ndocker\n" + ); + assert!(!project + .path() + .join(".texforge") + .join("spell-words") + .exists()); + } + + #[test] + fn add_appends_to_texforge_spell_words_when_only_that_one_exists() { + let project = TempDir::new().unwrap(); + write_project_toml(project.path()); + fs::create_dir_all(project.path().join(".texforge")).unwrap(); + fs::write( + project.path().join(".texforge").join("spell-words"), + "acme\n", + ) + .unwrap(); + + with_cwd(project.path(), || { + add(&["docker".to_string()], Scope::Local).unwrap(); + }); + + assert_eq!( + fs::read_to_string(project.path().join(".texforge").join("spell-words")).unwrap(), + "acme\ndocker\n" + ); + assert!(!project.path().join("spell-whitelist.txt").exists()); + } + + #[test] + fn appending_preserves_prior_content_exactly_including_comment_and_ordering() { + let project = TempDir::new().unwrap(); + write_project_toml(project.path()); + fs::write( + project.path().join("spell-whitelist.txt"), + "zeta\n# a note about zeta\nalpha\n", + ) + .unwrap(); + + with_cwd(project.path(), || { + add(&["docker".to_string()], Scope::Local).unwrap(); + }); + + assert_eq!( + fs::read_to_string(project.path().join("spell-whitelist.txt")).unwrap(), + "zeta\n# a note about zeta\nalpha\ndocker\n" + ); + } + + #[test] + fn adding_an_already_present_word_adds_no_line_case_insensitively() { + let project = TempDir::new().unwrap(); + write_project_toml(project.path()); + fs::write(project.path().join("spell-whitelist.txt"), "docker\n").unwrap(); + + with_cwd(project.path(), || { + add(&["docker".to_string()], Scope::Local).unwrap(); + add(&["Docker".to_string()], Scope::Local).unwrap(); + add(&["DOCKER".to_string()], Scope::Local).unwrap(); + }); + + assert_eq!( + fs::read_to_string(project.path().join("spell-whitelist.txt")).unwrap(), + "docker\n" + ); + } + + #[test] + fn remove_deletes_only_the_matching_line_and_leaves_comments_and_others_intact() { + let project = TempDir::new().unwrap(); + write_project_toml(project.path()); + fs::write( + project.path().join("spell-whitelist.txt"), + "zeta\n# a note about docker\ndocker\nalpha\n", + ) + .unwrap(); + + with_cwd(project.path(), || { + remove(&["docker".to_string()], Scope::Local).unwrap(); + }); + + assert_eq!( + fs::read_to_string(project.path().join("spell-whitelist.txt")).unwrap(), + "zeta\n# a note about docker\nalpha\n" + ); + } + + #[test] + fn remove_of_an_absent_word_changes_the_file_not_at_all() { + let project = TempDir::new().unwrap(); + write_project_toml(project.path()); + let original = "zeta\nalpha\n"; + fs::write(project.path().join("spell-whitelist.txt"), original).unwrap(); + + with_cwd(project.path(), || { + remove(&["nonexistent".to_string()], Scope::Local).unwrap(); + }); + + assert_eq!( + fs::read_to_string(project.path().join("spell-whitelist.txt")).unwrap(), + original + ); + } + + #[test] + fn add_and_remove_target_global_file_under_home() { + let home = TempDir::new().unwrap(); + with_home(home.path(), || { + add(&["docker".to_string()], Scope::Global).unwrap(); + }); + + let global = home.path().join(".texforge").join("spell-words"); + assert_eq!(fs::read_to_string(&global).unwrap(), "docker\n"); + + with_home(home.path(), || { + remove(&["docker".to_string()], Scope::Global).unwrap(); + }); + assert_eq!(fs::read_to_string(&global).unwrap(), ""); + } + + #[test] + fn list_output_names_the_file_it_read() { + let project = TempDir::new().unwrap(); + write_project_toml(project.path()); + fs::write(project.path().join("spell-whitelist.txt"), "docker\nacme\n").unwrap(); + + // `list` only prints; assert indirectly via the underlying data it + // would report, since stdout isn't captured here. + with_cwd(project.path(), || { + list(Scope::Local).unwrap(); + }); + + let words = parse_whitelist_words( + &fs::read_to_string(project.path().join("spell-whitelist.txt")).unwrap(), + ); + assert!(words.contains("docker")); + assert!(words.contains("acme")); + } + + /// End-to-end: a word added globally is then accepted by the linter in a + /// project that has no whitelist file of its own (requirement 6). + #[test] + fn word_added_globally_is_accepted_by_the_linter_in_a_bare_project() { + let home = TempDir::new().unwrap(); + fs::create_dir_all(home.path().join(".texforge").join("dicts")).unwrap(); + fs::write( + home.path() + .join(".texforge") + .join("dicts") + .join("english.txt"), + "hello\nworld\n", + ) + .unwrap(); + + with_home(home.path(), || { + add(&["docker".to_string()], Scope::Global).unwrap(); + }); + + let src = "\\begin{document}\nHello docker world\n\\end{document}"; + let files = vec![("main.tex".to_string(), src.to_string())]; + let project_root = TempDir::new().unwrap(); + + let findings = with_home(home.path(), || { + linter_spell::lint_files(&files, project_root.path(), Some("english")) + }) + .unwrap(); + + assert!( + findings.is_empty(), + "word added via `texforge spell add --global` must be accepted: {:?}", + findings + ); + } + + /// TE14 requirement 1: no scope flag means global, even when a project + /// whitelist file already exists in the current directory. + #[test] + fn add_with_no_flag_writes_global_and_leaves_existing_project_file_untouched() { + let project = TempDir::new().unwrap(); + write_project_toml(project.path()); + fs::write(project.path().join("spell-whitelist.txt"), "acme\n").unwrap(); + + let home = TempDir::new().unwrap(); + with_home(home.path(), || { + with_cwd(project.path(), || { + add(&["docker".to_string()], Scope::Global).unwrap(); + }); + }); + + let global = home.path().join(".texforge").join("spell-words"); + assert_eq!(fs::read_to_string(&global).unwrap(), "docker\n"); + assert_eq!( + fs::read_to_string(project.path().join("spell-whitelist.txt")).unwrap(), + "acme\n", + "project whitelist must be untouched by a default-scope add" + ); + } + + /// TE14 requirement 5: `--global` behaves identically to no flag, since + /// global is both the default and the explicit name for it. + #[test] + fn add_global_flag_behaves_identically_to_no_flag() { + let home = TempDir::new().unwrap(); + with_home(home.path(), || { + add(&["docker".to_string()], Scope::Global).unwrap(); + }); + + let global = home.path().join(".texforge").join("spell-words"); + assert_eq!(fs::read_to_string(&global).unwrap(), "docker\n"); + } + + /// TE14 requirement 4: `list --local` reads the project file rather than + /// the global default. + #[test] + fn list_local_reads_the_project_file_not_global() { + let project = TempDir::new().unwrap(); + write_project_toml(project.path()); + fs::write(project.path().join("spell-whitelist.txt"), "docker\n").unwrap(); + + let home = TempDir::new().unwrap(); + fs::create_dir_all(home.path().join(".texforge")).unwrap(); + fs::write( + home.path().join(".texforge").join("spell-words"), + "globalword\n", + ) + .unwrap(); + + with_home(home.path(), || { + with_cwd(project.path(), || { + list(Scope::Local).unwrap(); + }); + }); + + let words = parse_whitelist_words( + &fs::read_to_string(project.path().join("spell-whitelist.txt")).unwrap(), + ); + assert!(words.contains("docker")); + assert!(!words.contains("globalword")); + } + + /// TE14 requirement: `remove` with no flag removes from global only, + /// leaving an identically-named word in the project file untouched. + #[test] + fn remove_with_no_flag_removes_from_global_only() { + let project = TempDir::new().unwrap(); + write_project_toml(project.path()); + fs::write(project.path().join("spell-whitelist.txt"), "docker\n").unwrap(); + + let home = TempDir::new().unwrap(); + fs::create_dir_all(home.path().join(".texforge")).unwrap(); + fs::write( + home.path().join(".texforge").join("spell-words"), + "docker\n", + ) + .unwrap(); + + with_home(home.path(), || { + with_cwd(project.path(), || { + remove(&["docker".to_string()], Scope::Global).unwrap(); + }); + }); + + assert_eq!( + fs::read_to_string(home.path().join(".texforge").join("spell-words")).unwrap(), + "" + ); + assert_eq!( + fs::read_to_string(project.path().join("spell-whitelist.txt")).unwrap(), + "docker\n", + "project whitelist must be untouched by a default-scope remove" + ); + } + + /// TE14 decision 5: `--local` outside a texforge project must fail + /// loudly rather than silently falling back to the global list. + #[test] + fn local_outside_a_project_errors_and_writes_nothing() { + let outside = TempDir::new().unwrap(); + let home = TempDir::new().unwrap(); + + let result = with_home(home.path(), || { + with_cwd(outside.path(), || { + add(&["docker".to_string()], Scope::Local) + }) + }); + + assert!(result.is_err(), "add --local with no project must fail"); + assert!( + !home.path().join(".texforge").join("spell-words").exists(), + "a failed --local add must not fall back to writing the global list" + ); + assert!( + !outside + .path() + .join(".texforge") + .join("spell-words") + .exists(), + "a failed --local add must not create a project file out of nowhere either" + ); + } + + /// TE14 requirement 6: `--local --global` together is a clap usage + /// error, checked at the `Scope::from_flags` boundary this command + /// module relies on — the CLI layer enforces `conflicts_with` before + /// either flag reaches here. + #[test] + fn from_flags_defaults_to_global_when_neither_flag_is_set() { + assert_eq!(Scope::from_flags(false, false), Scope::Global); + assert_eq!(Scope::from_flags(true, false), Scope::Local); + assert_eq!(Scope::from_flags(false, true), Scope::Global); + } +} diff --git a/src/commands/stats.rs b/src/commands/stats.rs new file mode 100644 index 0000000..4ed0bd4 --- /dev/null +++ b/src/commands/stats.rs @@ -0,0 +1,129 @@ +//! `texforge stats` command implementation. + +use anyhow::Result; +use clap::ValueEnum; + +use crate::domain::project::Project; +use crate::texparse; +use crate::wordcount::{self, DocumentStats}; + +/// Breakdown mode for `texforge stats`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] +pub enum ByMode { + /// Report per section. + Section, + /// Report per `.tex` file. + File, +} + +/// Count words and report document statistics. +pub fn execute(json: bool, by: ByMode) -> Result<()> { + let project = Project::load()?; + + let entry = project.root.join(&project.config.build.entry); + if !entry.exists() { + anyhow::bail!("Entry point file does not exist: {}", entry.display()); + } + + let files = texparse::tokenize_document(&project.root, &project.config.build.entry); + let stats = match by { + ByMode::Section => wordcount::count_document(&project.config.document.title, &files), + ByMode::File => wordcount::count_by_file(&project.config.document.title, &files), + }; + + if json { + println!("{}", serde_json::to_string_pretty(&stats)?); + } else { + print_human(&stats, by); + } + + Ok(()) +} + +/// Human-readable breakdown with dotted leaders and a total. +fn print_human(stats: &DocumentStats, by: ByMode) { + println!("{}", stats.document); + + if stats.preamble_words > 0 { + println!( + " {}", + dotted_line("preamble", &stats.preamble_words.to_string()) + ); + } + for section in &stats.sections { + let left = match by { + ByMode::Section => format!( + "{} {}", + section.path, + section.title.as_deref().unwrap_or("") + ), + ByMode::File => section.path.clone(), + }; + println!(" {}", dotted_line(&left, §ion.words.to_string())); + } + if stats.preamble_words > 0 || !stats.sections.is_empty() { + println!(" {}", dotted_line("Total", &stats.total_words.to_string())); + } +} + +/// Join `left` and `right` with a run of dots, padded to `WIDTH`. +fn dotted_line(left: &str, right: &str) -> String { + const WIDTH: usize = 60; + let dots = WIDTH.saturating_sub(left.chars().count() + right.chars().count()); + format!("{left}{dots}{right}", dots = ".".repeat(dots)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn dotted_line_pads_with_dots() { + let line = dotted_line("Total", "12"); + assert!(line.starts_with("Total")); + assert!(line.ends_with("12")); + assert_eq!(line.chars().count(), 60); + assert!(line.contains("....")); + } + + #[test] + fn dotted_line_tolerates_overlong_left() { + let line = dotted_line(&"x".repeat(70), "1"); + assert!(line.ends_with("1")); + } + + #[test] + fn execute_json_on_small_project() { + let tmp = tempfile::tempdir().unwrap(); + std::fs::write( + tmp.path().join("project.toml"), + "[document]\ntitle = \"T\"\nauthor = \"A\"\ntemplate = \"general\"\n\n[build]\nentry = \"main.tex\"\n", + ) + .unwrap(); + std::fs::write( + tmp.path().join("main.tex"), + "\\begin{document}\nhello world\n\\section{S}\ntext\n\\end{document}", + ) + .unwrap(); + let orig = std::env::current_dir().unwrap(); + std::env::set_current_dir(tmp.path()).unwrap(); + let result = execute(true, ByMode::Section); + std::env::set_current_dir(&orig).unwrap(); + result.unwrap(); + } + + #[test] + fn execute_missing_entry_errors() { + let tmp = tempfile::tempdir().unwrap(); + std::fs::write( + tmp.path().join("project.toml"), + "[document]\ntitle = \"T\"\nauthor = \"A\"\ntemplate = \"general\"\n\n[build]\nentry = \"main.tex\"\n", + ) + .unwrap(); + let orig = std::env::current_dir().unwrap(); + std::env::set_current_dir(tmp.path()).unwrap(); + let result = execute(false, ByMode::Section); + std::env::set_current_dir(&orig).unwrap(); + assert!(result.is_err()); + } +} diff --git a/src/commands/template.rs b/src/commands/template.rs index 6804761..77c878d 100644 --- a/src/commands/template.rs +++ b/src/commands/template.rs @@ -81,6 +81,7 @@ mod tests { #[test] fn validate_general_template() { + ensure_rustls(); // "general" falls back to embedded, which has template.toml validate("general").unwrap(); } diff --git a/src/compiler/mod.rs b/src/compiler/mod.rs index 9b67867..28eadeb 100644 --- a/src/compiler/mod.rs +++ b/src/compiler/mod.rs @@ -1,33 +1,46 @@ //! LaTeX compilation engine — wraps Tectonic. +use std::collections::BTreeMap; use std::path::Path; use std::process::Command; use anyhow::{Context, Result}; +use crate::linter::Severity; + +/// Fixed `SOURCE_DATE_EPOCH` value used when reproducible mode is enabled +/// without an explicit epoch. Deliberately not "now": a fixed value is what +/// makes the same source compile to the same PDF. +pub const DEFAULT_EPOCH: u64 = 1700000000; + /// Compile a LaTeX project to PDF using Tectonic. /// `root` is the working directory; output PDF goes into `root/` itself. -pub fn compile(root: &Path, entry: &str) -> Result<()> { +/// +/// When `epoch` is `Some`, the `SOURCE_DATE_EPOCH` environment variable is set +/// for the Tectonic invocation so that identical source plus an identical +/// Tectonic version yields an identical PDF. `None` leaves the build as-is. +/// +/// On a successful build, warnings emitted by the engine are parsed and +/// printed to the console — summarized per file, or every occurrence with +/// `verbose` — without changing the success exit code. +pub fn compile(root: &Path, entry: &str, verbose: bool, epoch: Option<u64>) -> Result<()> { let tectonic = find_tectonic()?; - let entry_path = root.join(entry); - let output = Command::new(&tectonic) - .arg(&entry_path) - .arg("--outdir") - .arg(root) - .arg("--keep-logs") - .current_dir(root) + let output = tectonic_command(&tectonic, root, entry, epoch) .output() .with_context(|| format!("Failed to run tectonic at {}", tectonic.display()))?; - if output.status.success() { - return Ok(()); - } - let stderr = String::from_utf8_lossy(&output.stderr); let stdout = String::from_utf8_lossy(&output.stdout); let raw = format!("{}{}", stdout, stderr); + let warnings = parse_warnings(&raw); + + if output.status.success() { + print_warnings(&warnings, verbose); + return Ok(()); + } + let errors = parse_errors(&raw); if errors.is_empty() { anyhow::bail!("Compilation failed:\n{}", raw.trim()); @@ -43,6 +56,26 @@ pub fn compile(root: &Path, entry: &str) -> Result<()> { anyhow::bail!("{}", msg.trim()); } +/// Build the Tectonic invocation. Reproducible builds set `SOURCE_DATE_EPOCH`; +/// everything else in the invocation is identical either way. +fn tectonic_command( + tectonic: &std::path::Path, + root: &Path, + entry: &str, + epoch: Option<u64>, +) -> Command { + let mut cmd = Command::new(tectonic); + cmd.arg(root.join(entry)) + .arg("--outdir") + .arg(root) + .arg("--keep-logs") + .current_dir(root); + if let Some(epoch) = epoch { + cmd.env("SOURCE_DATE_EPOCH", epoch.to_string()); + } + cmd +} + struct CompileError { file: String, line: usize, @@ -101,6 +134,149 @@ fn parse_tectonic_error(rest: &str, errors: &mut Vec<CompileError>) { }); } +/// A single TeX/Tectonic warning parsed from compiler output. +#[derive(Debug, PartialEq, Eq)] +struct CompileWarning { + file: String, + line: usize, + severity: Severity, + kind: &'static str, + message: String, +} + +/// Recognized warning shapes: (substring fragment, canonical kind name). +/// Keep this list in one place so new shapes are a one-line extension. +const WARNING_PATTERNS: &[(&str, &str)] = &[ + ("Underfull \\hbox (badness", "underfull hbox"), + ("Underfull \\vbox (badness", "underfull vbox"), + ("Overfull \\hbox", "overfull hbox"), + ("Overfull \\vbox", "overfull vbox"), + ("LaTeX Warning:", "latex warning"), +]; + +/// Classify a single line into a warning kind, or `None` when it does not +/// match any known shape. Unrecognized lines are ignored silently. +fn warning_kind(line: &str) -> Option<&'static str> { + for (fragment, kind) in WARNING_PATTERNS { + if line.contains(fragment) { + return Some(kind); + } + } + if line.starts_with("Package ") && line.contains(" Warning:") { + return Some("package warning"); + } + None +} + +/// Pull a line number out of TeX warning markers: `at lines 41--42` and +/// `on input line 42`. Returns 0 when none is reported. +fn extract_line(line: &str) -> usize { + for marker in ["at lines ", "on input line "] { + if let Some(idx) = line.find(marker) { + let rest = &line[idx + marker.len()..]; + let num: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect(); + if let Ok(n) = num.parse() { + return n; + } + } + } + 0 +} + +/// Heuristic: is a `(path` token an engine file-open marker (as opposed to +/// other parenthesized chatter)? Files end in a known TeX extension. +fn is_source_path(path: &str) -> bool { + let lower = path.to_ascii_lowercase(); + [".tex", ".sty", ".cls", ".def", ".cfg", ".bib"] + .iter() + .any(|ext| lower.ends_with(ext)) +} + +/// Parse TeX/Tectonic stdout/stderr into structured warnings. +/// +/// Attribution: the engine prints file opens as `(path` and closes as `)`; +/// warnings seen while a file is open are credited to that file. Line numbers +/// come from `at lines N--M` / `on input line N` markers when the engine +/// reports them. +fn parse_warnings(raw: &str) -> Vec<CompileWarning> { + let mut warnings = Vec::new(); + let mut file_stack: Vec<String> = Vec::new(); + + for line in raw.lines() { + let trimmed = line.trim(); + if trimmed == ")" { + file_stack.pop(); + continue; + } + if let Some(path) = trimmed.strip_prefix('(') { + if is_source_path(path) { + file_stack.push(path.to_string()); + continue; + } + } + let Some(kind) = warning_kind(trimmed) else { + continue; + }; + warnings.push(CompileWarning { + file: file_stack.last().cloned().unwrap_or_default(), + line: extract_line(trimmed), + severity: Severity::Warning, + kind, + message: trimmed.to_string(), + }); + } + + warnings +} + +/// Render warnings for the console: one line per file with counts by kind, +/// or every occurrence when `verbose` is set. +fn format_warnings(warnings: &[CompileWarning], verbose: bool) -> String { + if warnings.is_empty() { + return String::new(); + } + let mut out = format!("WARNINGS ({})\n", warnings.len()); + if verbose { + for w in warnings { + let loc = if w.line == 0 { + format!("[{}]", w.file) + } else { + format!("[{}:{}]", w.file, w.line) + }; + out.push_str(&format!( + " {} {loc} {}: {}\n", + w.severity, w.kind, w.message + )); + } + } else { + let mut by_file: BTreeMap<&str, BTreeMap<&str, usize>> = BTreeMap::new(); + for w in warnings { + let file = if w.file.is_empty() { + "(unknown)" + } else { + w.file.as_str() + }; + *by_file.entry(file).or_default().entry(w.kind).or_default() += 1; + } + for (file, kinds) in by_file { + let counts: Vec<String> = kinds + .into_iter() + .map(|(kind, n)| format!("{n} {kind}")) + .collect(); + out.push_str(&format!(" {file}: {}\n", counts.join(", "))); + } + } + out +} + +/// Print warnings to the console on a successful build. +fn print_warnings(warnings: &[CompileWarning], verbose: bool) { + let formatted = format_warnings(warnings, verbose); + if !formatted.is_empty() { + println!("\n{}", formatted.trim_end()); + } +} + /// Find the tectonic binary in PATH or known locations, auto-installing if needed. fn find_tectonic() -> Result<std::path::PathBuf> { if let Some(path) = locate_tectonic() { @@ -113,7 +289,7 @@ fn find_tectonic() -> Result<std::path::PathBuf> { } /// Locate tectonic in PATH or known install locations without installing. -fn locate_tectonic() -> Option<std::path::PathBuf> { +pub(crate) fn locate_tectonic() -> Option<std::path::PathBuf> { // Check PATH using platform-appropriate which/where #[cfg(unix)] let which_cmd = "which"; @@ -263,6 +439,53 @@ fn current_target() -> Result<&'static str> { mod tests { use super::*; + fn ensure_rustls() { + let _ = rustls::crypto::ring::default_provider().install_default(); + } + + #[test] + fn command_sets_source_date_epoch_when_pinned() { + let cmd = tectonic_command( + Path::new("tectonic"), + Path::new("/tmp/root"), + "main.tex", + Some(DEFAULT_EPOCH), + ); + let epochs: Vec<_> = cmd + .get_envs() + .filter_map(|(key, value)| { + if key == "SOURCE_DATE_EPOCH" { + value.map(|b| b.to_os_string()) + } else { + None + } + }) + .collect(); + assert_eq!( + epochs, + vec![std::ffi::OsString::from(DEFAULT_EPOCH.to_string())] + ); + } + + #[test] + fn command_omits_source_date_epoch_when_not_pinned() { + let cmd = tectonic_command( + Path::new("tectonic"), + Path::new("/tmp/root"), + "main.tex", + None, + ); + let has_epoch = cmd.get_envs().any(|(key, _)| key == "SOURCE_DATE_EPOCH"); + assert!(!has_epoch); + } + + #[test] + fn default_epoch_is_fixed_and_not_zero() { + // Reproducibility needs a fixed value, not "now" — and not the + // pre-1970-style sentinel that would be indistinguishable from unset. + assert_eq!(DEFAULT_EPOCH, 1700000000); + } + #[test] fn parse_errors_tectonic_style() { let raw = "error: main.tex:42: undefined control sequence \\foo"; @@ -361,6 +584,7 @@ mod tests { #[test] fn find_tectonic_returns_path() { + ensure_rustls(); let result = find_tectonic(); // This test just verifies the function doesn't panic; // tectonic may or may not be installed. @@ -446,4 +670,132 @@ mod tests { assert_eq!(errors[0].file, "/some/long/path/to/file.tex"); assert_eq!(errors[0].line, 42); } + + #[test] + fn parse_warnings_underfull_hbox() { + let raw = "(main.tex\nUnderfull \\hbox (badness 1856) in paragraph at lines 58--59\n)"; + let warnings = parse_warnings(raw); + assert_eq!(warnings.len(), 1); + assert_eq!(warnings[0].file, "main.tex"); + assert_eq!(warnings[0].line, 58); + assert_eq!(warnings[0].severity, Severity::Warning); + assert_eq!(warnings[0].kind, "underfull hbox"); + assert!(warnings[0].message.contains("badness 1856")); + } + + #[test] + fn parse_warnings_overfull_hbox() { + let raw = "Overfull \\hbox (12.0pt too wide) in paragraph at lines 41--42"; + let warnings = parse_warnings(raw); + assert_eq!(warnings.len(), 1); + assert_eq!(warnings[0].kind, "overfull hbox"); + assert_eq!(warnings[0].line, 41); + assert_eq!(warnings[0].file, ""); + } + + #[test] + fn parse_warnings_package_warning() { + let raw = + "Package hyperref Warning: Token not allowed in a PDF string (Unicode) on input line 12."; + let warnings = parse_warnings(raw); + assert_eq!(warnings.len(), 1); + assert_eq!(warnings[0].kind, "package warning"); + assert_eq!(warnings[0].line, 12); + assert!(warnings[0].message.contains("hyperref")); + } + + #[test] + fn parse_warnings_latex_warning() { + let raw = "LaTeX Warning: Reference `fig1' on page 1 undefined on input line 22."; + let warnings = parse_warnings(raw); + assert_eq!(warnings.len(), 1); + assert_eq!(warnings[0].kind, "latex warning"); + assert_eq!(warnings[0].line, 22); + } + + #[test] + fn parse_warnings_ignores_unrecognized_lines() { + let raw = "some random output\n[]\\OT1/cmr/m/n/10 foo\nnot a warning"; + let warnings = parse_warnings(raw); + assert!(warnings.is_empty()); + } + + #[test] + fn parse_warnings_empty_input() { + assert!(parse_warnings("").is_empty()); + } + + #[test] + fn parse_warnings_file_tracking() { + let raw = "(main.tex\n(chapter.tex\nUnderfull \\hbox (badness 1000) in paragraph at lines 1--2\n)\nUnderfull \\hbox (badness 2000) in paragraph at lines 3--4\n)"; + let warnings = parse_warnings(raw); + assert_eq!(warnings.len(), 2); + assert_eq!(warnings[0].file, "chapter.tex"); + assert_eq!(warnings[1].file, "main.tex"); + } + + #[test] + fn parse_warnings_badness_variant_ignored_when_not_badness() { + let raw = "Underfull \\hbox (2.0pt too wide) in paragraph at lines 5--6"; + let warnings = parse_warnings(raw); + assert!(warnings.is_empty()); + } + + #[test] + fn extract_line_from_markers() { + assert_eq!(extract_line("in paragraph at lines 41--42"), 41); + assert_eq!(extract_line("on input line 7."), 7); + assert_eq!(extract_line("no marker here"), 0); + } + + #[test] + fn format_warnings_summary_groups_by_file() { + let warnings = vec![ + CompileWarning { + file: "main.tex".into(), + line: 58, + severity: Severity::Warning, + kind: "underfull hbox", + message: "Underfull \\hbox (badness 1856)".into(), + }, + CompileWarning { + file: "main.tex".into(), + line: 90, + severity: Severity::Warning, + kind: "underfull hbox", + message: "Underfull \\hbox (badness 2000)".into(), + }, + CompileWarning { + file: "skills.tex".into(), + line: 12, + severity: Severity::Warning, + kind: "package warning", + message: "Package hyperref Warning: ...".into(), + }, + ]; + let out = format_warnings(&warnings, false); + assert!(out.starts_with("WARNINGS (3)\n")); + assert!(out.contains(" main.tex: 2 underfull hbox")); + assert!(out.contains(" skills.tex: 1 package warning")); + } + + #[test] + fn format_warnings_verbose_lists_every_occurrence() { + let warnings = vec![CompileWarning { + file: "main.tex".into(), + line: 58, + severity: Severity::Warning, + kind: "underfull hbox", + message: "Underfull \\hbox (badness 1856) in paragraph at lines 58--59".into(), + }]; + let out = format_warnings(&warnings, true); + assert!(out.contains("WARNING [main.tex:58] underfull hbox:")); + assert!(out.contains("badness 1856")); + } + + #[test] + fn format_warnings_empty_is_empty() { + assert!(format_warnings(&[], false).is_empty()); + assert!(format_warnings(&[], true).is_empty()); + } } diff --git a/src/diagrams/mod.rs b/src/diagrams/mod.rs index ed5aa1b..7b4f1c8 100644 --- a/src/diagrams/mod.rs +++ b/src/diagrams/mod.rs @@ -1,7 +1,8 @@ //! Pre-processor for embedded diagram environments. //! //! Intercepts `\begin{mermaid}[opts]...\end{mermaid}` blocks, renders them -//! to PNG, and replaces each block with a proper `figure` environment. +//! to vector PDF (falling back to PNG if the PDF conversion fails), and +//! replaces each block with a proper `figure` environment. //! //! Works on copies in `build/` — the original .tex files are never modified. @@ -12,10 +13,20 @@ use std::sync::{Arc, OnceLock}; use anyhow::{Context, Result}; +pub mod style; +use style::DiagramStyle; + /// Copy all .tex files to `build_dir`, rendering embedded diagrams in the copies. /// Also mirrors non-.tex assets so tectonic can resolve relative paths. -/// Returns the path to the build copy of `entry`. -pub fn process(root: &Path, entry: &str, build_dir: &Path) -> Result<PathBuf> { +/// `default_style` is the document-wide style (`project.toml`'s `[diagrams] +/// style`, or `DiagramStyle::Default`); a `style=` on the environment itself +/// overrides it. Returns the path to the build copy of `entry`. +pub fn process( + root: &Path, + entry: &str, + build_dir: &Path, + default_style: DiagramStyle, +) -> Result<PathBuf> { std::fs::create_dir_all(build_dir)?; let diagrams_dir = build_dir.join("diagrams"); @@ -30,7 +41,7 @@ pub fn process(root: &Path, entry: &str, build_dir: &Path) -> Result<PathBuf> { std::fs::create_dir_all(parent)?; } let content = std::fs::read_to_string(src)?; - let processed = render_diagrams(&content, &diagrams_dir) + let processed = render_diagrams(&content, &diagrams_dir, default_style) .with_context(|| format!("Failed to render diagrams in {}", src.display()))?; std::fs::write(&dest, processed)?; } @@ -42,44 +53,56 @@ pub fn process(root: &Path, entry: &str, build_dir: &Path) -> Result<PathBuf> { } /// Replace all `\begin{mermaid}[opts]...\end{mermaid}` with figure environments. -fn render_diagrams(content: &str, diagrams_dir: &Path) -> Result<String> { - let content = render_env(content, "mermaid", diagrams_dir, |src| { - let svg = render_mermaid_with_config(src) - .map_err(|e| anyhow::anyhow!("Mermaid render error: {}", e))?; - svg_to_png(&svg).context("Failed to convert mermaid SVG to PNG") - })?; - let content = render_env(&content, "graphviz", diagrams_dir, |src| { - let svg = render_graphviz(src)?; - svg_to_png(&svg).context("Failed to convert graphviz SVG to PNG") - })?; - let content = render_env(&content, "d2", diagrams_dir, |src| { - let svg = render_d2(src)?; - svg_to_png(&svg).context("Failed to convert D2 SVG to PNG") +fn render_diagrams( + content: &str, + diagrams_dir: &Path, + default_style: DiagramStyle, +) -> Result<String> { + let content = render_env( + content, + "mermaid", + diagrams_dir, + default_style, + |src, sty| { + let svg = render_mermaid_with_config(src, sty)?; + convert_svg_or_fallback("mermaid", &svg) + }, + )?; + let content = render_env( + &content, + "graphviz", + diagrams_dir, + default_style, + |src, sty| { + let svg = render_graphviz(src, sty)?; + convert_svg_or_fallback("graphviz", &svg) + }, + )?; + let content = render_env(&content, "d2", diagrams_dir, default_style, |src, sty| { + let svg = render_d2(src, sty)?; + convert_svg_or_fallback("d2", &svg) })?; Ok(content) } -/// Render Mermaid diagram with improved configuration for better layout. -fn render_mermaid_with_config(src: &str) -> Result<String> { - // Try with default configuration first - mermaid_rs_renderer::render(src).map_err(|e| { - // If default fails, try with explicit configuration - anyhow::anyhow!( - "Mermaid render error: {}. Consider checking diagram syntax.", - e - ) - }) +/// Render a Mermaid diagram, applying `style`'s theme and layout spacing. +fn render_mermaid_with_config(src: &str, sty: DiagramStyle) -> Result<String> { + mermaid_rs_renderer::render_with_options(src, style::mermaid_options(sty)) + .map_err(|e| anyhow::anyhow!("Mermaid render error: {}", e)) } /// Generic environment renderer: replaces `\begin{env}[opts]...\end{env}` with figure. /// -/// Rendered PNGs are named after a hash of the diagram source, so unchanged -/// diagrams are reused across rebuilds (watch mode) instead of re-rendered. +/// Rendered artefacts are named after a hash of the diagram source, so +/// unchanged diagrams are reused across rebuilds (watch mode) instead of +/// re-rendered. `render_fn` returns the encoded bytes and the extension +/// (`"pdf"` on the vector path, `"png"` when it fell back to rasterizing). pub(crate) fn render_env( content: &str, env: &str, diagrams_dir: &Path, - render_fn: impl Fn(&str) -> Result<Vec<u8>>, + default_style: DiagramStyle, + render_fn: impl Fn(&str, DiagramStyle) -> Result<(Vec<u8>, &'static str)>, ) -> Result<String> { let begin_tag = format!("\\begin{{{}}}", env); let end_tag = format!("\\end{{{}}}", env); @@ -91,18 +114,24 @@ pub(crate) fn render_env( result.push_str(&remaining[..start]); let after_begin = &remaining[start + begin_tag.len()..]; - let (opts, after_opts) = parse_opts(after_begin); + let (opts, after_opts) = parse_opts(after_begin, env)?; let end = find_end_tag(after_opts, &end_tag, env)?; let diagram_src = after_opts[..end].trim(); validate_pos_option(&opts, env)?; - - let filename = format!("{}-{:016x}.png", env, content_hash(diagram_src)); - if !diagrams_dir.join(&filename).exists() { - let png = render_fn(diagram_src)?; - std::fs::write(diagrams_dir.join(&filename), png)?; - } + let diagram_style = resolve_style(&opts, env, default_style)?; + + let base = format!("{}-{:016x}", env, content_hash(diagram_src, diagram_style)); + let filename = match cached_filename(diagrams_dir, &base) { + Some(filename) => filename, + None => { + let (bytes, ext) = render_fn(diagram_src, diagram_style)?; + let filename = format!("{base}.{ext}"); + std::fs::write(diagrams_dir.join(&filename), bytes)?; + filename + } + }; let fig_env = build_figure_environment(&opts, env, &filename)?; result.push_str(&fig_env); @@ -113,13 +142,45 @@ pub(crate) fn render_env( Ok(result) } -/// Stable-enough 64-bit hash of diagram source for cache filenames. -fn content_hash(src: &str) -> u64 { +/// Look for an already-rendered artefact for `base`, vector form first. +fn cached_filename(diagrams_dir: &Path, base: &str) -> Option<String> { + ["pdf", "png"].into_iter().find_map(|ext| { + let filename = format!("{base}.{ext}"); + diagrams_dir.join(&filename).exists().then_some(filename) + }) +} + +/// Stable-enough 64-bit hash of diagram source and style for cache filenames. +/// Folding the style in means changing `style=` re-renders instead of +/// serving a stale cached artefact. +fn content_hash(src: &str, style: DiagramStyle) -> u64 { let mut hasher = std::collections::hash_map::DefaultHasher::new(); src.hash(&mut hasher); + (style as u8).hash(&mut hasher); hasher.finish() } +/// Resolve the style for one diagram: the environment's `style=` option +/// wins, then the `project.toml` default. An unknown name is an error +/// naming the offending value and the valid alternatives. +fn resolve_style( + opts: &HashMap<String, String>, + env: &str, + default_style: DiagramStyle, +) -> Result<DiagramStyle> { + match opts.get("style") { + None => Ok(default_style), + Some(name) => DiagramStyle::parse(name).map_err(|_| { + anyhow::anyhow!( + "Invalid {} option style='{}' — valid values are: {}", + env, + name, + style::VALID_STYLE_NAMES.join(", ") + ) + }), + } +} + /// Find the end tag position and validate it exists. fn find_end_tag(after_opts: &str, end_tag: &str, env: &str) -> Result<usize> { after_opts @@ -197,13 +258,18 @@ fn add_caption_if_present(opts: &HashMap<String, String>, fig: &mut String) -> R } /// Render a DOT/Graphviz diagram to SVG using layout-rs (pure Rust). -fn render_graphviz(src: &str) -> Result<String> { +/// +/// `layout-rs` has no theme concept: `style` is applied by injecting +/// `node [...]; edge [...];` default-attribute statements into the DOT +/// source before parsing (see [`style::graphviz_inject`]). +fn render_graphviz(src: &str, sty: DiagramStyle) -> Result<String> { use layout::backends::svg::SVGWriter; use layout::gv::DotParser; use layout::gv::GraphBuilder; use layout::topo::layout::VisualGraph; - let mut parser = DotParser::new(src); + let styled_src = style::graphviz_inject(src, sty); + let mut parser = DotParser::new(&styled_src); let graph = parser.process().map_err(|e| { parser.print_error(); anyhow::anyhow!("Graphviz parse error: {}", e) @@ -219,31 +285,107 @@ fn render_graphviz(src: &str) -> Result<String> { } /// Render a D2 diagram to SVG using d2-little (pure Rust port of the d2lang pipeline). -fn render_d2(src: &str) -> Result<String> { - let svg = d2_little::d2_to_svg(src).map_err(|e| anyhow::anyhow!("D2 render error: {}", e))?; +/// +/// `style` is applied by prepending a `vars: { d2-config: ... }` block (see +/// [`style::d2_prefix`]) — the only way to reach `theme-overrides`, which +/// `CompileOptions` has no field for. +fn render_d2(src: &str, sty: DiagramStyle) -> Result<String> { + let styled_src = format!("{}{}", style::d2_prefix(sty), src); + let svg = + d2_little::d2_to_svg(&styled_src).map_err(|e| anyhow::anyhow!("D2 render error: {}", e))?; String::from_utf8(svg).context("D2 produced non-UTF8 SVG") } +/// Option keys read anywhere in this module. An option outside this list is +/// almost always a typo, so [`parse_opts`] warns instead of dropping it. +const KNOWN_OPTION_KEYS: &[&str] = &[ + "style", + "pos", + "width", + "height", + "scale", + "keepaspectratio", + "label", + "caption", +]; + /// Parse `[key=val, key2=val2]` into a map. Returns `(map, rest_of_str)`. -pub(crate) fn parse_opts(s: &str) -> (HashMap<String, String>, &str) { +/// +/// A value may be wrapped in `{...}` — the LaTeX convention for "this may +/// contain a comma" — in which case the comma inside no longer separates +/// options; the outer braces are stripped from the stored value but nested +/// braces are preserved. Brace depth is tracked in a single pass over the +/// string, so an option is only split on a comma seen at depth zero. +/// +/// An unrecognised key emits a warning (naming `env`) and is dropped rather +/// than aborting the build; an unterminated `{` is a hard error, since there +/// is no reasonable place to guess the value ended. +pub(crate) fn parse_opts<'a>(s: &'a str, env: &str) -> Result<(HashMap<String, String>, &'a str)> { let s = s.trim_start_matches('\n').trim_start_matches('\r'); if !s.starts_with('[') { - return (HashMap::new(), s); + return Ok((HashMap::new(), s)); } - let Some(end) = s.find(']') else { - return (HashMap::new(), s); + let after = &s[1..]; + + let mut depth = 0i32; + let mut part_start = 0usize; + let mut parts: Vec<&str> = Vec::new(); + let mut end_idx = None; + for (i, c) in after.char_indices() { + match c { + '{' => depth += 1, + '}' => depth -= 1, + ',' if depth == 0 => { + parts.push(&after[part_start..i]); + part_start = i + 1; + } + ']' if depth == 0 => { + parts.push(&after[part_start..i]); + end_idx = Some(i); + break; + } + _ => {} + } + } + + let Some(end_idx) = end_idx else { + if depth > 0 { + let unterminated = &after[part_start..]; + let option = unterminated + .split_once('=') + .map_or(unterminated, |(k, _)| k) + .trim(); + anyhow::bail!( + "{env} diagram: unterminated '{{' in option '{option}' — every {{ needs a matching }}" + ); + } + return Ok((HashMap::new(), s)); }; - let inner = &s[1..end]; - let rest = &s[end + 1..]; + let rest = &after[end_idx + 1..]; let mut map = HashMap::new(); - for part in inner.split(',') { + for part in parts { let part = part.trim(); - if let Some((k, v)) = part.split_once('=') { - map.insert(k.trim().to_string(), v.trim().to_string()); + if part.is_empty() { + continue; } + let Some((k, v)) = part.split_once('=') else { + eprintln!("warning: {env} diagram: unknown option '{part}' ignored"); + continue; + }; + let k = k.trim(); + let v = v.trim(); + if !KNOWN_OPTION_KEYS.contains(&k) { + eprintln!("warning: {env} diagram: unknown option '{k}' ignored"); + continue; + } + let value = v + .strip_prefix('{') + .and_then(|v| v.strip_suffix('}')) + .unwrap_or(v); + map.insert(k.to_string(), value.to_string()); } - (map, rest) + Ok((map, rest)) } /// Collect .tex files reachable from entry via \input. @@ -405,6 +547,50 @@ fn configure_monospace_family( /// which is print quality. const RASTER_SCALE: f32 = 3.0; +/// Convert SVG string to PDF bytes, embedding it as vector art (with +/// selectable text) rather than rasterizing it. +/// +/// Takes the SVG as a string rather than a pre-parsed `usvg::Tree`: `svg2pdf` +/// depends on `usvg ^0.45` while the rest of texforge is on `usvg 0.46`, and +/// those are distinct incompatible types. Parsing here, with svg2pdf's own +/// bundled usvg, avoids needing to bridge the two. +fn svg_to_pdf(svg: &str) -> Result<Vec<u8>> { + let options = svg2pdf::usvg::Options { + fontdb: shared_fontdb(), + shape_rendering: svg2pdf::usvg::ShapeRendering::GeometricPrecision, + text_rendering: svg2pdf::usvg::TextRendering::OptimizeLegibility, + ..Default::default() + }; + + let tree = + svg2pdf::usvg::Tree::from_str(svg, &options).context("Failed to parse SVG for PDF")?; + + svg2pdf::to_pdf( + &tree, + svg2pdf::ConversionOptions::default(), + svg2pdf::PageOptions::default(), + ) + .map_err(|e| anyhow::anyhow!("SVG to PDF conversion failed: {}", e)) +} + +/// Convert a diagram's SVG to a vector PDF; fall back to a rasterized PNG, +/// with a warning naming the diagram, if the PDF conversion fails. +/// +/// Returns the encoded bytes alongside the extension actually produced, so +/// callers can name the cached artefact accordingly. +fn convert_svg_or_fallback(env: &str, svg: &str) -> Result<(Vec<u8>, &'static str)> { + match svg_to_pdf(svg) { + Ok(pdf) => Ok((pdf, "pdf")), + Err(e) => { + eprintln!( + "warning: {env} diagram: SVG to PDF conversion failed ({e}), falling back to PNG" + ); + let png = svg_to_png(svg).context("Failed to convert diagram SVG to PNG (fallback)")?; + Ok((png, "png")) + } + } +} + /// Convert SVG string to PNG bytes at print resolution. fn svg_to_png(svg: &str) -> Result<Vec<u8>> { let options = resvg::usvg::Options { @@ -436,33 +622,188 @@ fn svg_to_png(svg: &str) -> Result<Vec<u8>> { mod tests { use super::*; + /// Builds an SVG whose group nesting is deep enough to exceed svg2pdf's + /// PDF content-stream nesting guard (`state_nesting_depth() > 28`), while + /// remaining well within what resvg's rasterizer renders without issue. + /// Each level carries a decoy sibling rect so usvg can't flatten the + /// single-child transform chain into one group. + fn deeply_nested_svg() -> String { + let mut svg = + String::from(r#"<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80">"#); + let depth = 35; + for i in 0..depth { + svg.push_str(&format!( + r#"<g transform="translate(0.1,0.1)"><rect x="{i}" y="0" width="1" height="1" fill="blue"/>"# + )); + } + svg.push_str(r#"<rect width="10" height="10" fill="red"/>"#); + for _ in 0..depth { + svg.push_str("</g>"); + } + svg.push_str("</svg>"); + svg + } + + #[test] + fn svg_to_pdf_produces_pdf_magic_bytes() { + let svg = r#"<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><rect width="10" height="10" fill="red"/></svg>"#; + let pdf = svg_to_pdf(svg).unwrap(); + assert!( + pdf.starts_with(b"%PDF-"), + "expected PDF magic bytes, got: {:?}", + &pdf[..pdf.len().min(20)] + ); + } + + #[test] + fn svg_to_pdf_fails_on_excessive_nesting_that_png_still_handles() { + let svg = deeply_nested_svg(); + assert!( + svg_to_pdf(&svg).is_err(), + "expected the deeply nested SVG to exceed svg2pdf's nesting guard" + ); + assert!( + svg_to_png(&svg).is_ok(), + "rasterization should not be affected by the PDF nesting guard" + ); + } + + #[test] + fn convert_svg_or_fallback_falls_back_to_png_on_pdf_failure() { + let svg = deeply_nested_svg(); + let (bytes, ext) = convert_svg_or_fallback("graphviz", &svg).unwrap(); + assert_eq!(ext, "png"); + assert!(!bytes.is_empty()); + } + + #[test] + fn content_hash_stable_across_calls() { + let src = "digraph G { A -> B }"; + assert_eq!( + content_hash(src, DiagramStyle::Default), + content_hash(src, DiagramStyle::Default) + ); + } + + #[test] + fn content_hash_differs_by_style() { + let src = "digraph G { A -> B }"; + assert_ne!( + content_hash(src, DiagramStyle::Default), + content_hash(src, DiagramStyle::Editorial) + ); + } + #[test] fn parse_opts_no_brackets_returns_empty_map() { - let (map, rest) = parse_opts("hello"); + let (map, rest) = parse_opts("hello", "mermaid").unwrap(); assert!(map.is_empty()); assert_eq!(rest, "hello"); } #[test] fn parse_opts_width_and_pos() { - let (map, _) = parse_opts("[width=0.5\\linewidth, pos=t]"); + let (map, _) = parse_opts("[width=0.5\\linewidth, pos=t]", "mermaid").unwrap(); assert_eq!(map.get("width").map(String::as_str), Some("0.5\\linewidth")); assert_eq!(map.get("pos").map(String::as_str), Some("t")); } #[test] fn parse_opts_caption() { - let (map, _) = parse_opts("[caption=My diagram]"); + let (map, _) = parse_opts("[caption=My diagram]", "mermaid").unwrap(); assert_eq!(map.get("caption").map(String::as_str), Some("My diagram")); } #[test] fn parse_opts_label_and_height() { - let (map, _) = parse_opts("[label=fig:my-diagram, height=5cm]"); + let (map, _) = parse_opts("[label=fig:my-diagram, height=5cm]", "mermaid").unwrap(); assert_eq!(map.get("label").map(String::as_str), Some("fig:my-diagram")); assert_eq!(map.get("height").map(String::as_str), Some("5cm")); } + #[test] + fn parse_opts_style_alongside_others_any_order() { + let (map, _) = + parse_opts("[pos=t, style=editorial, width=0.5\\linewidth]", "mermaid").unwrap(); + assert_eq!(map.get("style").map(String::as_str), Some("editorial")); + assert_eq!(map.get("pos").map(String::as_str), Some("t")); + assert_eq!(map.get("width").map(String::as_str), Some("0.5\\linewidth")); + + let (map, _) = parse_opts("[style=monochrome, caption=A diagram]", "mermaid").unwrap(); + assert_eq!(map.get("style").map(String::as_str), Some("monochrome")); + assert_eq!(map.get("caption").map(String::as_str), Some("A diagram")); + } + + #[test] + fn parse_opts_braced_caption_keeps_commas() { + let (map, _) = parse_opts("[caption={A, with commas}]", "mermaid").unwrap(); + assert_eq!( + map.get("caption").map(String::as_str), + Some("A, with commas") + ); + } + + #[test] + fn parse_opts_braced_caption_preserves_nested_braces() { + let (map, _) = parse_opts("[caption={\\texttt{a, b}}]", "mermaid").unwrap(); + assert_eq!( + map.get("caption").map(String::as_str), + Some("\\texttt{a, b}") + ); + } + + #[test] + fn parse_opts_unbraced_caption_unchanged() { + let (map, _) = parse_opts("[caption=simple]", "mermaid").unwrap(); + assert_eq!(map.get("caption").map(String::as_str), Some("simple")); + } + + #[test] + fn parse_opts_braced_value_mixed_with_others_any_order() { + let (map, _) = parse_opts( + "[style=editorial, width=0.5\\linewidth, caption={x, y}, pos=H]", + "mermaid", + ) + .unwrap(); + assert_eq!(map.get("style").map(String::as_str), Some("editorial")); + assert_eq!(map.get("width").map(String::as_str), Some("0.5\\linewidth")); + assert_eq!(map.get("caption").map(String::as_str), Some("x, y")); + assert_eq!(map.get("pos").map(String::as_str), Some("H")); + } + + #[test] + fn parse_opts_unknown_key_warns_but_does_not_abort() { + let (map, _) = parse_opts("[style=default, frobnicate=yes, pos=t]", "mermaid").unwrap(); + assert_eq!(map.get("style").map(String::as_str), Some("default")); + assert_eq!(map.get("pos").map(String::as_str), Some("t")); + assert!(!map.contains_key("frobnicate")); + } + + #[test] + fn parse_opts_unterminated_brace_fails_naming_environment() { + let err = parse_opts("[caption={unterminated]", "mermaid").unwrap_err(); + let message = err.to_string(); + assert!(message.contains("mermaid"), "message was: {message}"); + assert!(message.contains("caption"), "message was: {message}"); + } + + /// TF-C: a caption with commas used to be silently truncated at the first + /// one — this is the exact broken line from the capabilities example + /// (commit 2b848dc), used verbatim to guard against regressing it. + #[test] + fn parse_opts_defect_tf_c_caption_with_commas_not_truncated() { + let (map, _) = parse_opts( + "[style=editorial, width=0.55\\linewidth, caption={Preset \\texttt{editorial}: paleta restringida, un solo acento, sin sombras}, pos=H]", + "mermaid", + ) + .unwrap(); + assert_eq!( + map.get("caption").map(String::as_str), + Some("Preset \\texttt{editorial}: paleta restringida, un solo acento, sin sombras") + ); + assert_eq!(map.get("pos").map(String::as_str), Some("H")); + } + #[test] fn build_figure_with_label() { let mut opts = HashMap::new(); @@ -522,7 +863,7 @@ mod tests { #[test] fn render_graphviz_produces_svg() { let dot = "digraph G { A -> B }"; - let svg = render_graphviz(dot).unwrap(); + let svg = render_graphviz(dot, DiagramStyle::Default).unwrap(); assert!( svg.contains("<svg"), "expected SVG output, got: {}", @@ -532,7 +873,7 @@ mod tests { #[test] fn render_d2_produces_svg() { - let svg = render_d2("a -> b -> c").unwrap(); + let svg = render_d2("a -> b -> c", DiagramStyle::Default).unwrap(); assert!( svg.contains("<svg"), "expected SVG output, got: {}", @@ -541,22 +882,93 @@ mod tests { } #[test] - fn render_d2_to_png_via_pipeline() { + fn render_d2_to_pdf_via_pipeline() { let dir = tempfile::tempdir().unwrap(); let content = "\\begin{d2}[caption=Flow]\nx -> y: go\n\\end{d2}"; - let out = render_diagrams(content, dir.path()).unwrap(); + let out = render_diagrams(content, dir.path(), DiagramStyle::Default).unwrap(); assert!(out.contains("\\includegraphics")); + assert!(out.contains(".pdf")); assert!(out.contains("\\caption{Flow}")); - // exactly one cached PNG written - let pngs = std::fs::read_dir(dir.path()).unwrap().count(); - assert_eq!(pngs, 1); + // exactly one cached artefact written, and it's the vector form + let mut entries = std::fs::read_dir(dir.path()).unwrap(); + let entry = entries.next().unwrap().unwrap(); + assert!(entries.next().is_none()); + assert_eq!(entry.path().extension().unwrap(), "pdf"); + } + + #[test] + fn render_mermaid_to_pdf_via_pipeline() { + let dir = tempfile::tempdir().unwrap(); + let content = "\\begin{mermaid}\nflowchart LR\n A --> B\n\\end{mermaid}"; + let out = render_diagrams(content, dir.path(), DiagramStyle::Default).unwrap(); + assert!(out.contains(".pdf")); + } + + #[test] + fn render_graphviz_to_pdf_via_pipeline() { + let dir = tempfile::tempdir().unwrap(); + let content = "\\begin{graphviz}\ndigraph G { A -> B }\n\\end{graphviz}"; + let out = render_diagrams(content, dir.path(), DiagramStyle::Default).unwrap(); + assert!(out.contains(".pdf")); + } + + #[test] + fn render_diagrams_with_editorial_style_attribute_succeeds() { + let dir = tempfile::tempdir().unwrap(); + let content = "\\begin{mermaid}[style=editorial]\nflowchart LR\n A --> B\n\\end{mermaid}"; + let out = render_diagrams(content, dir.path(), DiagramStyle::Default).unwrap(); + assert!(out.contains(".pdf")); + } + + #[test] + fn render_diagrams_unknown_style_attribute_fails_build() { + let dir = tempfile::tempdir().unwrap(); + let content = "\\begin{mermaid}[style=editoral]\nflowchart LR\n A --> B\n\\end{mermaid}"; + let err = render_diagrams(content, dir.path(), DiagramStyle::Default).unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("editoral"), "message: {msg}"); + } + + #[test] + fn render_env_build_succeeds_when_pdf_conversion_fails() { + // Exercises the same fallback a real diagram render would take when + // its SVG trips svg2pdf's nesting guard: the build must still + // succeed, emitting a PNG artefact rather than failing outright. + let content = "\\begin{graphviz}\ndigraph G { A -> B }\n\\end{graphviz}"; + let dir = tempfile::tempdir().unwrap(); + let svg = deeply_nested_svg(); + + let result = render_env( + content, + "graphviz", + dir.path(), + DiagramStyle::Default, + |_, _| convert_svg_or_fallback("graphviz", &svg), + ); + + assert!(result.is_ok(), "build must succeed via the PNG fallback"); + let out = result.unwrap(); + assert!(out.contains(".png")); + let entry = std::fs::read_dir(dir.path()) + .unwrap() + .next() + .unwrap() + .unwrap(); + assert_eq!(entry.path().extension().unwrap(), "png"); } #[test] fn render_env_no_blocks_unchanged() { let content = "hello world"; let dir = tempfile::tempdir().unwrap(); - let result = render_env(content, "graphviz", dir.path(), |_| Ok(vec![])).unwrap(); + let result = render_env( + content, + "graphviz", + dir.path(), + DiagramStyle::Default, + |_, _| Ok((vec![], "pdf")), + ) + .unwrap(); assert_eq!(result, content); assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), 0); } @@ -565,10 +977,36 @@ mod tests { fn render_env_invalid_pos_returns_error() { let content = "\\begin{graphviz}[pos=Z]\ndigraph G{}\n\\end{graphviz}"; let dir = tempfile::tempdir().unwrap(); - let err = render_env(content, "graphviz", dir.path(), |_| Ok(vec![1, 2, 3])).unwrap_err(); + let err = render_env( + content, + "graphviz", + dir.path(), + DiagramStyle::Default, + |_, _| Ok((vec![1, 2, 3], "pdf")), + ) + .unwrap_err(); assert!(err.to_string().contains("pos='Z'")); } + #[test] + fn render_env_invalid_style_returns_error_listing_valid_names() { + let content = "\\begin{graphviz}[style=editoral]\ndigraph G{}\n\\end{graphviz}"; + let dir = tempfile::tempdir().unwrap(); + let err = render_env( + content, + "graphviz", + dir.path(), + DiagramStyle::Default, + |_, _| Ok((vec![1, 2, 3], "pdf")), + ) + .unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("style='editoral'"), "message: {msg}"); + for name in style::VALID_STYLE_NAMES { + assert!(msg.contains(name), "message missing '{name}': {msg}"); + } + } + #[test] fn render_env_reuses_cached_diagram() { let content = "\\begin{graphviz}\ndigraph G{ A -> B }\n\\end{graphviz}"; @@ -576,16 +1014,260 @@ mod tests { let calls = std::cell::Cell::new(0u32); // render twice into the same dir — second pass must hit the cache for _ in 0..2 { - render_env(content, "graphviz", dir.path(), |_| { - calls.set(calls.get() + 1); - Ok(vec![1, 2, 3]) - }) + render_env( + content, + "graphviz", + dir.path(), + DiagramStyle::Default, + |_, _| { + calls.set(calls.get() + 1); + Ok((vec![1, 2, 3], "pdf")) + }, + ) .unwrap(); } assert_eq!(calls.get(), 1); assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), 1); } + #[test] + fn render_env_falls_back_to_png_extension() { + let content = "\\begin{graphviz}\ndigraph G{ A -> B }\n\\end{graphviz}"; + let dir = tempfile::tempdir().unwrap(); + let out = render_env( + content, + "graphviz", + dir.path(), + DiagramStyle::Default, + |_, _| Ok((vec![1, 2, 3], "png")), + ) + .unwrap(); + assert!(out.contains(".png")); + let mut entries = std::fs::read_dir(dir.path()).unwrap(); + let entry = entries.next().unwrap().unwrap(); + assert_eq!(entry.path().extension().unwrap(), "png"); + } + + #[test] + fn render_env_reuses_cached_png_without_recomputing() { + let content = "\\begin{graphviz}\ndigraph G{ A -> B }\n\\end{graphviz}"; + let dir = tempfile::tempdir().unwrap(); + // A prior run fell back to PNG for this diagram. + render_env( + content, + "graphviz", + dir.path(), + DiagramStyle::Default, + |_, _| Ok((vec![1, 2, 3], "png")), + ) + .unwrap(); + + let calls = std::cell::Cell::new(0u32); + render_env( + content, + "graphviz", + dir.path(), + DiagramStyle::Default, + |_, _| { + calls.set(calls.get() + 1); + Ok((vec![9, 9, 9], "pdf")) + }, + ) + .unwrap(); + assert_eq!( + calls.get(), + 0, + "cached .png should be reused, not re-rendered" + ); + } + + #[test] + fn render_env_omitted_style_matches_explicit_default_style() { + // Omitting `style=` must render exactly as `style=default`: both + // resolve to `DiagramStyle::Default`, so they hash to the same + // cache entry and only one artefact is written. + let dir = tempfile::tempdir().unwrap(); + let content_omitted = "\\begin{graphviz}\ndigraph G{ A -> B }\n\\end{graphviz}"; + let content_explicit = + "\\begin{graphviz}[style=default]\ndigraph G{ A -> B }\n\\end{graphviz}"; + + render_env( + content_omitted, + "graphviz", + dir.path(), + DiagramStyle::Default, + |_, sty| { + assert_eq!(sty, DiagramStyle::Default); + Ok((vec![1, 2, 3], "pdf")) + }, + ) + .unwrap(); + render_env( + content_explicit, + "graphviz", + dir.path(), + DiagramStyle::Default, + |_, sty| { + assert_eq!(sty, DiagramStyle::Default); + Ok((vec![1, 2, 3], "pdf")) + }, + ) + .unwrap(); + + assert_eq!( + std::fs::read_dir(dir.path()).unwrap().count(), + 1, + "omitted style= and explicit style=default must share one cache entry" + ); + } + + #[test] + fn render_env_project_default_style_used_when_attribute_absent() { + let dir = tempfile::tempdir().unwrap(); + let content = "\\begin{graphviz}\ndigraph G{ A -> B }\n\\end{graphviz}"; + render_env( + content, + "graphviz", + dir.path(), + DiagramStyle::Editorial, + |_, sty| { + assert_eq!(sty, DiagramStyle::Editorial); + Ok((vec![1, 2, 3], "pdf")) + }, + ) + .unwrap(); + } + + #[test] + fn render_env_attribute_style_overrides_project_default() { + let dir = tempfile::tempdir().unwrap(); + let content = "\\begin{graphviz}[style=monochrome]\ndigraph G{ A -> B }\n\\end{graphviz}"; + render_env( + content, + "graphviz", + dir.path(), + DiagramStyle::Editorial, + |_, sty| { + assert_eq!(sty, DiagramStyle::Monochrome); + Ok((vec![1, 2, 3], "pdf")) + }, + ) + .unwrap(); + } + + #[test] + fn render_env_different_styles_produce_distinct_cache_entries() { + let dir = tempfile::tempdir().unwrap(); + let content_default = "\\begin{graphviz}\ndigraph G{ A -> B }\n\\end{graphviz}"; + let content_editorial = + "\\begin{graphviz}[style=editorial]\ndigraph G{ A -> B }\n\\end{graphviz}"; + render_env( + content_default, + "graphviz", + dir.path(), + DiagramStyle::Default, + |_, _| Ok((vec![1], "pdf")), + ) + .unwrap(); + render_env( + content_editorial, + "graphviz", + dir.path(), + DiagramStyle::Default, + |_, _| Ok((vec![2], "pdf")), + ) + .unwrap(); + assert_eq!( + std::fs::read_dir(dir.path()).unwrap().count(), + 2, + "different styles must produce distinct cached artefacts" + ); + } + + fn hex_colors_for_attr(svg: &str, attr: &str) -> Vec<(u8, u8, u8)> { + let pattern = format!("{attr}=\"#"); + let mut out = Vec::new(); + let mut rest = svg; + while let Some(idx) = rest.find(&pattern) { + let after = &rest[idx + pattern.len()..]; + if after.len() >= 6 { + let hex = &after[..6]; + if let (Ok(r), Ok(g), Ok(b)) = ( + u8::from_str_radix(&hex[0..2], 16), + u8::from_str_radix(&hex[2..4], 16), + u8::from_str_radix(&hex[4..6], 16), + ) { + out.push((r, g, b)); + } + } + rest = &after[6.min(after.len())..]; + } + out + } + + /// Grayscale means R == G == B for every `fill`/`stroke` hex color — + /// the requirement `monochrome` exists to guarantee. + fn assert_grayscale_svg(svg: &str, label: &str) { + for attr in ["fill", "stroke"] { + for (r, g, b) in hex_colors_for_attr(svg, attr) { + assert!( + r == g && g == b, + "{label}: non-grayscale {attr} color #{r:02X}{g:02X}{b:02X} under monochrome style" + ); + } + } + } + + #[test] + fn monochrome_style_mermaid_svg_is_grayscale() { + let svg = + render_mermaid_with_config("flowchart LR\n A --> B --> C", DiagramStyle::Monochrome) + .unwrap(); + assert_grayscale_svg(&svg, "mermaid"); + } + + #[test] + fn monochrome_style_graphviz_svg_is_grayscale() { + let svg = render_graphviz("digraph G { A -> B }", DiagramStyle::Monochrome).unwrap(); + assert_grayscale_svg(&svg, "graphviz"); + } + + #[test] + fn monochrome_style_d2_svg_is_grayscale() { + let svg = render_d2("a -> b -> c", DiagramStyle::Monochrome).unwrap(); + assert_grayscale_svg(&svg, "d2"); + } + + #[test] + fn technical_style_renders_across_all_three_renderers() { + // technical is the trickiest preset: D2 reaches its monospaced-label + // rule via a base theme-id (301) injected alongside theme-overrides. + let svg = + render_mermaid_with_config("flowchart LR\n A --> B", DiagramStyle::Technical).unwrap(); + assert!(svg.contains("<svg"), "mermaid: {svg}"); + + let svg = render_graphviz("digraph G { A -> B }", DiagramStyle::Technical).unwrap(); + assert!(svg.contains("<svg"), "graphviz: {svg}"); + + let svg = render_d2("a -> b -> c", DiagramStyle::Technical).unwrap(); + assert!(svg.contains("<svg"), "d2: {svg}"); + } + + #[test] + fn graphviz_renders_despite_unsupported_background_override() { + // layout-rs has no document-wide theme or background — StyleAttr is + // per-node/per-edge only — so a preset it can't fully express must + // still render successfully rather than fail the build. + for sty in [ + DiagramStyle::Editorial, + DiagramStyle::Monochrome, + DiagramStyle::Technical, + ] { + let svg = render_graphviz("digraph G { A -> B }", sty).unwrap(); + assert!(svg.contains("<svg")); + } + } + #[test] fn build_figure_default_uses_linewidth() { let opts = HashMap::new(); diff --git a/src/diagrams/style.rs b/src/diagrams/style.rs new file mode 100644 index 0000000..2bd443a --- /dev/null +++ b/src/diagrams/style.rs @@ -0,0 +1,388 @@ +//! Named diagram style presets: `default`, `editorial`, `monochrome`, `technical`. +//! +//! Presets are declared here as data — one [`Palette`] per preset — and +//! mapped onto each renderer's own theming vocabulary by the functions +//! below. `default` carries no palette: renderers keep producing exactly +//! what they do today, so omitting `style=` never changes an existing +//! document's appearance. +//! +//! Fidelity differs by renderer. Mermaid exposes a rich `Theme`; D2 exposes +//! a named colour palette via `theme-overrides`; Graphviz (`layout-rs`) has +//! no theme concept at all, only per-node/per-edge attributes, so a +//! document-wide background is not expressible there — that gap is +//! documented in `docs/diagrams.md` rather than hidden. + +use anyhow::{bail, Result}; + +/// The four supported style presets. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DiagramStyle { + /// Each renderer's own untouched default. Never changes. + Default, + /// Restrained palette, one accent colour, no shadows, thin strokes, + /// generous whitespace. + Editorial, + /// Greyscale only — for black-and-white printing. + Monochrome, + /// Drafting register: uniform stroke weight, no fills, monospaced + /// labels where the renderer allows it. + Technical, +} + +/// The valid style names, in the order errors should list them. +pub const VALID_STYLE_NAMES: [&str; 4] = ["default", "editorial", "monochrome", "technical"]; + +impl DiagramStyle { + /// Parse a style name from a `style=` attribute or `project.toml`. + /// + /// An unknown name is an error naming the offending value and the valid + /// alternatives — silently falling back to `default` would hide a typo + /// until the document is printed. + pub fn parse(name: &str) -> Result<Self> { + match name { + "default" => Ok(Self::Default), + "editorial" => Ok(Self::Editorial), + "monochrome" => Ok(Self::Monochrome), + "technical" => Ok(Self::Technical), + other => bail!( + "Unknown diagram style '{other}' — valid styles are: {}", + VALID_STYLE_NAMES.join(", ") + ), + } + } + + /// The palette this preset expresses, or `None` for `Default`. + fn palette(self) -> Option<Palette> { + match self { + Self::Default => None, + // texforge's own editorial palette, the one its page on + // univerlab.org is typeset in (`data-surface='paper'`). The accent + // is the proofreader's vermilion change-bar, and the surface + // comment there states the rule this preset follows: it is used + // once. `background` stays pure white rather than taking the + // page's warm tint, because a diagram sits *on* the printed page — + // tinting its canvas would paint a visible grey panel on white + // paper. The tint becomes the node fill instead, so boxes read as + // paper stock raised off the sheet. + Self::Editorial => Some(Palette { + background: "#FFFFFF", + foreground: "#1B1B1A", + neutral: "#54514C", + neutral_light: "#F5F5F3", + accent: Some("#B2362C"), + monospace: false, + }), + Self::Monochrome => Some(Palette { + background: "#FFFFFF", + foreground: "#1A1A1A", + neutral: "#808080", + neutral_light: "#E8E8E8", + accent: None, + monospace: false, + }), + Self::Technical => Some(Palette { + background: "#FFFFFF", + foreground: "#1A1A1A", + neutral: "#555555", + // Equal to background: shapes read as unfilled outlines. + neutral_light: "#FFFFFF", + accent: None, + monospace: true, + }), + } + } +} + +/// A restrained palette one style preset expresses, in vocabulary generic +/// enough to map onto Mermaid's `Theme`, D2's `theme-overrides`, and +/// Graphviz's per-node/per-edge `StyleAttr`. +struct Palette { + background: &'static str, + foreground: &'static str, + neutral: &'static str, + neutral_light: &'static str, + /// The one accent colour a preset uses; `None` for the greyscale-only + /// presets (`monochrome`, `technical`), so every field below falls back + /// to a neutral and the output stays saturation-free. + accent: Option<&'static str>, + /// Whether labels should render in a monospaced face. + monospace: bool, +} + +impl Palette { + fn accent_or_neutral(&self) -> &'static str { + self.accent.unwrap_or(self.neutral) + } +} + +// --------------------------------------------------------------------------- +// Mermaid — the richest surface: a full `Theme` plus layout spacing. +// --------------------------------------------------------------------------- + +/// Build Mermaid `RenderOptions` for `style`. `Default` returns exactly +/// `RenderOptions::default()`, so an unstyled diagram renders byte-identical +/// to before this feature existed. +pub fn mermaid_options(style: DiagramStyle) -> mermaid_rs_renderer::RenderOptions { + let Some(p) = style.palette() else { + return mermaid_rs_renderer::RenderOptions::default(); + }; + + let accent = p.accent_or_neutral(); + let font_family = if p.monospace { + "'Courier New', 'DejaVu Sans Mono', ui-monospace, monospace".to_string() + } else { + "'Helvetica Neue', Helvetica, Arial, sans-serif".to_string() + }; + let pie_colors: [String; 12] = { + let cycle = [ + p.background, + accent, + p.neutral, + p.neutral_light, + p.foreground, + ]; + std::array::from_fn(|i| cycle[i % cycle.len()].to_string()) + }; + + let theme = mermaid_rs_renderer::Theme { + font_family, + font_size: 14.0, + primary_color: p.neutral_light.to_string(), + primary_text_color: p.foreground.to_string(), + // Shapes are drawn in ink; the accent is reserved for what connects + // them. Spending it on every node border as well turns "one accent" + // into "coloured everywhere", which is what the restrained presets + // exist to avoid — and a saturated accent on every outline reads as a + // warning rather than as a finish. + primary_border_color: p.neutral.to_string(), + line_color: accent.to_string(), + secondary_color: p.neutral_light.to_string(), + tertiary_color: p.background.to_string(), + edge_label_background: p.background.to_string(), + cluster_background: p.neutral_light.to_string(), + cluster_border: p.neutral.to_string(), + background: p.background.to_string(), + sequence_actor_fill: p.neutral_light.to_string(), + sequence_actor_border: p.neutral.to_string(), + sequence_actor_line: p.neutral.to_string(), + sequence_note_fill: p.neutral_light.to_string(), + sequence_note_border: p.neutral.to_string(), + sequence_activation_fill: p.neutral_light.to_string(), + sequence_activation_border: p.neutral.to_string(), + text_color: p.foreground.to_string(), + git_colors: [ + p.foreground, + accent, + p.neutral, + p.neutral_light, + p.background, + p.foreground, + accent, + p.neutral, + ] + .map(|s| s.to_string()), + git_inv_colors: [p.background; 8].map(|s| s.to_string()), + git_branch_label_colors: [p.foreground; 8].map(|s| s.to_string()), + git_commit_label_color: p.foreground.to_string(), + git_commit_label_background: p.background.to_string(), + git_tag_label_color: p.foreground.to_string(), + git_tag_label_background: p.neutral_light.to_string(), + git_tag_label_border: p.neutral.to_string(), + pie_colors, + pie_title_text_size: 20.0, + pie_title_text_color: p.foreground.to_string(), + pie_section_text_size: 14.0, + pie_section_text_color: p.foreground.to_string(), + pie_legend_text_size: 14.0, + pie_legend_text_color: p.foreground.to_string(), + pie_stroke_color: p.foreground.to_string(), + pie_stroke_width: 1.0, + pie_outer_stroke_width: 1.0, + pie_outer_stroke_color: p.neutral.to_string(), + pie_opacity: 1.0, + }; + + let mut layout = mermaid_rs_renderer::LayoutConfig::default(); + if matches!(style, DiagramStyle::Editorial) { + // "Generous whitespace" is one of the editorial rules. + layout.node_spacing *= 1.4; + layout.rank_spacing *= 1.4; + } + + mermaid_rs_renderer::RenderOptions { theme, layout } +} + +// --------------------------------------------------------------------------- +// D2 — native theming via a `d2-config` block prepended to the source. +// --------------------------------------------------------------------------- + +/// A `vars: { d2-config: ... }` block to prepend to D2 source, or an empty +/// string for `Default` (no override — d2-little's own default theme). +/// +/// `theme-overrides` in D2's own config vocabulary only takes effect when +/// parsed from source text; the compile-options struct has no field for it. +/// Prepending valid D2 syntax is the supported route, not a workaround. +/// +/// `technical` bases on theme 301 (Terminal Grayscale), whose `mono` +/// special rule switches labels to a monospaced face — the one lever D2 +/// exposes for that requirement. Its other special rules (double borders, +/// container dots) only affect nested/grouped diagrams and are a documented +/// side effect, not a colour or fidelity gap. +pub fn d2_prefix(style: DiagramStyle) -> String { + let Some(p) = style.palette() else { + return String::new(); + }; + let accent = p.accent_or_neutral(); + let theme_id = if p.monospace { 301 } else { 0 }; + format!( + "vars: {{\n d2-config: {{\n theme-id: {theme_id}\n theme-overrides: {{\n N1: \"{fg}\"\n N2: \"{neu}\"\n N3: \"{neu}\"\n N4: \"{neul}\"\n N5: \"{neul}\"\n N6: \"{neul}\"\n N7: \"{bg}\"\n B1: \"{fg}\"\n B2: \"{acc}\"\n B3: \"{neu}\"\n B4: \"{neul}\"\n B5: \"{neul}\"\n B6: \"{bg}\"\n AA2: \"{acc}\"\n AA4: \"{neul}\"\n AA5: \"{bg}\"\n AB4: \"{neul}\"\n AB5: \"{bg}\"\n }}\n }}\n}}\n", + fg = p.foreground, + neu = p.neutral, + neul = p.neutral_light, + bg = p.background, + acc = accent, + ) +} + +// --------------------------------------------------------------------------- +// Graphviz — no theme concept; per-node/per-edge default attributes only. +// --------------------------------------------------------------------------- + +/// `node [...]; edge [...];` default-attribute statements to inject right +/// after the graph's opening `{`, or an empty string for `Default`. +/// +/// `layout-rs` has no document-wide theme or background: `StyleAttr` is +/// applied per node and per edge as the graph is built, and the parser +/// never reads a graph-level `bgcolor`. A page background is therefore not +/// expressible here — an accepted, documented gap, not a build failure. +/// Font family (`fontname`) is likewise never read by the DOT builder, so +/// `technical`'s monospaced-label rule cannot be expressed for Graphviz +/// either; only the colour and fill rules carry over. +fn graphviz_attr_statements(style: DiagramStyle) -> String { + let Some(p) = style.palette() else { + return String::new(); + }; + let accent = p.accent_or_neutral(); + format!( + "node [style=filled, fillcolor=\"{}\", color=\"{}\", fontsize=12];\nedge [color=\"{}\"];\n", + p.neutral_light, accent, accent + ) +} + +/// Inject `style`'s default-attribute statements into DOT source, right +/// after the first `{` (the body of `digraph G { ... }` / `graph G { ... }`). +pub fn graphviz_inject(src: &str, style: DiagramStyle) -> String { + let stmt = graphviz_attr_statements(style); + if stmt.is_empty() { + return src.to_string(); + } + match src.find('{') { + Some(idx) => { + let mut out = String::with_capacity(src.len() + stmt.len() + 1); + out.push_str(&src[..=idx]); + out.push('\n'); + out.push_str(&stmt); + out.push_str(&src[idx + 1..]); + out + } + None => src.to_string(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_all_four_names() { + assert_eq!( + DiagramStyle::parse("default").unwrap(), + DiagramStyle::Default + ); + assert_eq!( + DiagramStyle::parse("editorial").unwrap(), + DiagramStyle::Editorial + ); + assert_eq!( + DiagramStyle::parse("monochrome").unwrap(), + DiagramStyle::Monochrome + ); + assert_eq!( + DiagramStyle::parse("technical").unwrap(), + DiagramStyle::Technical + ); + } + + #[test] + fn parse_unknown_name_lists_valid_names() { + let err = DiagramStyle::parse("editoral").unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("editoral"), "message: {msg}"); + for name in VALID_STYLE_NAMES { + assert!(msg.contains(name), "message missing '{name}': {msg}"); + } + } + + #[test] + fn parse_rejects_old_mono_name() { + // `mono` was renamed to `monochrome`; the abbreviation must not be + // a silent alias, and the error should point at the new name. + let err = DiagramStyle::parse("mono").unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("monochrome"), "message: {msg}"); + } + + #[test] + fn parse_rejects_blueprint_name() { + // `technical` was named on purpose to avoid promising a blue-on-white + // colour scheme; `blueprint` must not be a silent alias for it. + assert!(DiagramStyle::parse("blueprint").is_err()); + } + + #[test] + fn mermaid_default_style_is_untouched_default_options() { + let opts = mermaid_options(DiagramStyle::Default); + let base = mermaid_rs_renderer::RenderOptions::default(); + assert_eq!(opts.theme.background, base.theme.background); + assert_eq!(opts.theme.primary_color, base.theme.primary_color); + assert_eq!(opts.layout.node_spacing, base.layout.node_spacing); + } + + #[test] + fn mermaid_monochrome_style_has_no_accent() { + let opts = mermaid_options(DiagramStyle::Monochrome); + // No accent color: border and line colors fall back to the neutral. + assert_eq!(opts.theme.primary_border_color, opts.theme.line_color); + } + + #[test] + fn d2_default_style_has_no_prefix() { + assert_eq!(d2_prefix(DiagramStyle::Default), ""); + } + + #[test] + fn d2_editorial_style_prefix_contains_theme_overrides() { + let prefix = d2_prefix(DiagramStyle::Editorial); + assert!(prefix.contains("theme-overrides")); + // Derived from the palette rather than hardcoded: this asserts the + // accent reaches the prefix, which is what can break. Retuning the + // palette is not a regression and should not fail here. + let accent = DiagramStyle::Editorial.palette().unwrap().accent.unwrap(); + assert!(prefix.contains(accent)); + } + + #[test] + fn graphviz_default_style_leaves_source_untouched() { + let src = "digraph G { A -> B }"; + assert_eq!(graphviz_inject(src, DiagramStyle::Default), src); + } + + #[test] + fn graphviz_styled_source_injects_after_opening_brace() { + let src = "digraph G { A -> B }"; + let injected = graphviz_inject(src, DiagramStyle::Editorial); + assert!(injected.contains("node [style=filled")); + assert!(injected.contains("edge [color=")); + assert!(injected.trim_end().ends_with("}")); + } +} diff --git a/src/domain/project.rs b/src/domain/project.rs index a98d6ad..dd545d1 100644 --- a/src/domain/project.rs +++ b/src/domain/project.rs @@ -10,6 +10,18 @@ use serde::{Deserialize, Serialize}; pub struct ProjectConfig { pub document: DocumentConfig, pub build: BuildConfig, + #[serde(default)] + pub diagrams: Option<DiagramsConfig>, +} + +/// `[diagrams]` section of `project.toml`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DiagramsConfig { + /// Document-wide default style preset (`default`, `editorial`, + /// `monochrome`, `technical`). A `style=` on the environment itself + /// overrides this. + #[serde(default)] + pub style: Option<String>, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -24,6 +36,20 @@ pub struct BuildConfig { pub entry: String, #[serde(default)] pub bibliography: Option<String>, + #[serde(default)] + pub reproducible: Option<Reproducible>, +} + +/// Reproducible-build setting from `project.toml` (`[build] reproducible`). +/// +/// Accepts `true` to pin a fixed default epoch, `false` to disable, or a +/// number to pin an explicit `SOURCE_DATE_EPOCH` value. Absent means the build +/// is not reproducible. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum Reproducible { + Enabled(bool), + Epoch(u64), } /// Represents a `TexForge` project. @@ -100,7 +126,9 @@ entry = "main.tex" build: BuildConfig { entry: "main.tex".to_string(), bibliography: Some("refs.bib".to_string()), + reproducible: None, }, + diagrams: None, }; let toml_str = toml::to_string_pretty(&config).unwrap(); let parsed: ProjectConfig = toml::from_str(&toml_str).unwrap(); @@ -119,7 +147,9 @@ entry = "main.tex" build: BuildConfig { entry: "main.tex".to_string(), bibliography: None, + reproducible: None, }, + diagrams: None, }; let cloned = config.clone(); let debug_str = format!("{:?}", config); @@ -165,4 +195,132 @@ entry = "main.tex" std::env::set_current_dir(&orig).unwrap(); assert!(result.is_err()); } + + #[test] + fn project_config_reproducible_true() { + let toml_str = r#" +[document] +title = "T" +author = "A" +template = "general" + +[build] +entry = "main.tex" +reproducible = true +"#; + let config: ProjectConfig = toml::from_str(toml_str).unwrap(); + assert_eq!(config.build.reproducible, Some(Reproducible::Enabled(true))); + } + + #[test] + fn project_config_reproducible_false() { + let toml_str = r#" +[document] +title = "T" +author = "A" +template = "general" + +[build] +entry = "main.tex" +reproducible = false +"#; + let config: ProjectConfig = toml::from_str(toml_str).unwrap(); + assert_eq!( + config.build.reproducible, + Some(Reproducible::Enabled(false)) + ); + } + + #[test] + fn project_config_reproducible_explicit_epoch() { + let toml_str = r#" +[document] +title = "T" +author = "A" +template = "general" + +[build] +entry = "main.tex" +reproducible = 1700000000 +"#; + let config: ProjectConfig = toml::from_str(toml_str).unwrap(); + assert_eq!( + config.build.reproducible, + Some(Reproducible::Epoch(1700000000)) + ); + } + + #[test] + fn project_config_reproducible_absent_is_off() { + let toml_str = r#" +[document] +title = "T" +author = "A" +template = "general" + +[build] +entry = "main.tex" +"#; + let config: ProjectConfig = toml::from_str(toml_str).unwrap(); + assert_eq!(config.build.reproducible, None); + } + + #[test] + fn project_config_reproducible_roundtrip() { + let config = ProjectConfig { + document: DocumentConfig { + title: "T".to_string(), + author: "A".to_string(), + template: "general".to_string(), + }, + build: BuildConfig { + entry: "main.tex".to_string(), + bibliography: None, + reproducible: Some(Reproducible::Epoch(1700000000)), + }, + diagrams: None, + }; + let serialized = toml::to_string_pretty(&config).unwrap(); + let parsed: ProjectConfig = toml::from_str(&serialized).unwrap(); + assert_eq!( + parsed.build.reproducible, + Some(Reproducible::Epoch(1700000000)) + ); + } + + #[test] + fn project_config_diagrams_style_present() { + let toml_str = r#" +[document] +title = "T" +author = "A" +template = "general" + +[build] +entry = "main.tex" + +[diagrams] +style = "editorial" +"#; + let config: ProjectConfig = toml::from_str(toml_str).unwrap(); + assert_eq!( + config.diagrams.and_then(|d| d.style), + Some("editorial".to_string()) + ); + } + + #[test] + fn project_config_diagrams_absent_is_none() { + let toml_str = r#" +[document] +title = "T" +author = "A" +template = "general" + +[build] +entry = "main.tex" +"#; + let config: ProjectConfig = toml::from_str(toml_str).unwrap(); + assert!(config.diagrams.is_none()); + } } diff --git a/src/linter/engine.rs b/src/linter/engine.rs new file mode 100644 index 0000000..4975cac --- /dev/null +++ b/src/linter/engine.rs @@ -0,0 +1,393 @@ +//! Engine-compatibility lint rules (TF7). +//! +//! texforge always compiles with Tectonic, which is a `XeTeX` engine. A generic +//! LaTeX linter cannot warn about constructs that behave badly specifically +//! under `XeTeX`, because it does not know the engine; texforge does. These +//! rules flag the packages and commands that Tectonic ignores, and the ones +//! that break the build outright. +//! +//! | Rule | Fires when | Severity | +//! |---|---|---| +//! | `inputenc` loaded | `\usepackage[utf8]{inputenc}` | [`Severity::Warning`] — ignored | +//! | `epstopdf` loaded | `\usepackage{epstopdf}` | [`Severity::Warning`] — no EPS conversion | +//! | `\DisableLigatures` with microtype | `\usepackage{microtype}` + `\DisableLigatures` | [`Severity::Error`] — build fails | +//! | `\setmainfont{Latin Modern Roman}` with fontspec | `\usepackage{fontspec}` + `\setmainfont{Latin Modern Roman}` | [`Severity::Error`] — build fails | +//! +//! The rules are data, not code branches: each entry in [`ENGINE_RULES`] +//! lists the [`TriggerKind`]s that must all be observed, a severity, and a +//! message that names the engine and the consequence. Detection runs over the +//! token stream of every file the `\input` traversal reaches, restricted to +//! the preamble, so comments and verbatim text can never fire a rule. + +use crate::texparse::{tokenize_with_spans, Token}; + +use super::{LintFinding, Severity}; + +/// An observable LaTeX construct a rule can require. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TriggerKind { + /// `\usepackage{name}`, with any options. + Package(&'static str), + /// The command `\name`, with or without arguments. + Command(&'static str), + /// `\command{arg}` whose last braced argument equals `arg` exactly. + CommandWithArg(&'static str, &'static str), +} + +/// One engine-compatibility rule. +#[derive(Debug)] +pub struct EngineRule { + /// Severity split: ignored constructs are [`Severity::Warning`]; + /// build-breaking constructs are [`Severity::Error`]. + pub severity: Severity, + /// Constructs that must ALL be observed for the rule to fire. The last + /// trigger is the actionable one and owns the finding's location. + pub triggers: &'static [TriggerKind], + /// Message naming the engine and stating the consequence in plain terms. + pub message: &'static str, + pub suggestion: &'static str, +} + +/// The data table of engine-compatibility rules. Adding a construct is one +/// entry here; the scanning and reporting logic never grows. +pub const ENGINE_RULES: &[EngineRule] = &[ + EngineRule { + severity: Severity::Warning, + triggers: &[TriggerKind::Package("inputenc")], + message: "\\usepackage{inputenc} is ignored under Tectonic (a XeTeX engine): it always \ + reads UTF-8 source, so the package does nothing", + suggestion: "Remove the \\usepackage{inputenc} line", + }, + EngineRule { + severity: Severity::Warning, + triggers: &[TriggerKind::Package("epstopdf")], + message: "\\usepackage{epstopdf} will not work under Tectonic (a XeTeX engine): epstopdf \ + supports only the pdfTeX and LuaTeX drivers, so included .eps graphics are not \ + converted to a renderable format", + suggestion: "Convert the .eps files to PDF or PNG and \\includegraphics those instead", + }, + EngineRule { + severity: Severity::Error, + triggers: &[ + TriggerKind::Package("microtype"), + TriggerKind::Command("DisableLigatures"), + ], + message: "\\DisableLigatures breaks the build under Tectonic (a XeTeX engine): microtype \ + cannot disable a font's ligatures there — XeTeX aborts with 'Disabling \ + ligatures of a font is only possible'", + suggestion: "Remove \\DisableLigatures from the preamble", + }, + EngineRule { + severity: Severity::Error, + triggers: &[ + TriggerKind::Package("fontspec"), + TriggerKind::CommandWithArg("setmainfont", "Latin Modern Roman"), + ], + message: "\\setmainfont{Latin Modern Roman} breaks the build under Tectonic (a XeTeX \ + engine): Tectonic ships no system fonts, so XeTeX aborts with 'The font ... \ + cannot be found'", + suggestion: "Remove the \\setmainfont line and use the engine's bundled Latin Modern \ + instead", + }, +]; + +/// Lint every file reachable from the entry point against the engine rules. +/// +/// Each entry is a `(project-relative path, source)` pair; the caller already +/// resolved `\input` traversal ([`crate::texutil::collect_tex_files`]), so +/// packages loaded from included files are seen here. Compound rules +/// accumulate their triggers across files, and each rule fires at most once +/// per project, at the location of its actionable (last) trigger. +pub fn lint_files(files: &[(String, String)]) -> Vec<LintFinding> { + let mut fired: Vec<Vec<Option<(String, usize)>>> = ENGINE_RULES + .iter() + .map(|rule| vec![None; rule.triggers.len()]) + .collect(); + + for (rel, source) in files { + let tokenized = tokenize_with_spans(source); + for spanned in &tokenized.tokens { + match &spanned.token { + Token::BeginDocument => break, + Token::Command { name, args } => { + let line = line_of(source, spanned.start); + for (rule_idx, rule) in ENGINE_RULES.iter().enumerate() { + for (trigger_idx, trigger) in rule.triggers.iter().enumerate() { + if fired[rule_idx][trigger_idx].is_none() + && trigger_matches(trigger, name, args) + { + fired[rule_idx][trigger_idx] = Some((rel.clone(), line)); + } + } + } + } + _ => {} + } + } + } + + let mut findings = Vec::new(); + for (rule_idx, rule) in ENGINE_RULES.iter().enumerate() { + if fired[rule_idx].iter().all(Option::is_some) { + let (file, line) = fired[rule_idx].last().unwrap().as_ref().unwrap(); + findings.push(LintFinding { + file: file.clone(), + line: *line, + severity: rule.severity, + message: rule.message.to_string(), + suggestion: Some(rule.suggestion.to_string()), + }); + } + } + findings +} + +/// Whether one `\command` token satisfies a trigger. +fn trigger_matches(trigger: &TriggerKind, name: &str, args: &[String]) -> bool { + match trigger { + TriggerKind::Package(pkg) => { + name == "usepackage" && args.last().is_some_and(|a| a.as_str() == *pkg) + } + TriggerKind::Command(cmd) => name == *cmd, + TriggerKind::CommandWithArg(cmd, arg) => { + name == *cmd && args.last().is_some_and(|a| a.as_str() == *arg) + } + } +} + +/// 1-based line number of a byte offset. +fn line_of(source: &str, offset: usize) -> usize { + let offset = offset.min(source.len()); + 1 + source[..offset].matches('\n').count() +} + +#[cfg(test)] +mod tests { + use super::*; + + fn lint(source: &str) -> Vec<LintFinding> { + lint_files(&[("main.tex".to_string(), source.to_string())]) + } + + fn has_severity_with(findings: &[LintFinding], fragment: &str, sev: Severity) -> bool { + findings + .iter() + .any(|f| f.severity == sev && f.message.contains(fragment)) + } + + // --- the four constructs, severity + consequence naming --- + + #[test] + fn inputenc_is_a_warning_naming_the_consequence() { + let findings = lint( + r"\documentclass{article} +\usepackage[utf8]{inputenc} +\begin{document} +\end{document}", + ); + assert!(has_severity_with( + &findings, + "does nothing", + Severity::Warning + )); + assert!(has_severity_with(&findings, "XeTeX", Severity::Warning)); + } + + #[test] + fn epstopdf_is_a_warning_naming_the_consequence() { + let findings = lint( + r"\documentclass{article} +\usepackage{epstopdf} +\begin{document} +\end{document}", + ); + assert!(has_severity_with(&findings, ".eps", Severity::Warning)); + assert!(has_severity_with(&findings, "XeTeX", Severity::Warning)); + } + + #[test] + fn disable_ligatures_with_microtype_is_an_error_naming_the_consequence() { + let findings = lint( + r"\documentclass{article} +\usepackage{microtype} +\DisableLigatures +\begin{document} +\end{document}", + ); + assert!(has_severity_with( + &findings, + "breaks the build", + Severity::Error + )); + assert!(has_severity_with(&findings, "XeTeX", Severity::Error)); + } + + #[test] + fn latin_modern_roman_setmainfont_with_fontspec_is_an_error_naming_the_consequence() { + let findings = lint( + r"\documentclass{article} +\usepackage{fontspec} +\setmainfont{Latin Modern Roman} +\begin{document} +\end{document}", + ); + assert!(has_severity_with( + &findings, + "breaks the build", + Severity::Error + )); + assert!(has_severity_with( + &findings, + "cannot be found", + Severity::Error + )); + } + + // --- no false positives --- + + #[test] + fn microtype_without_disable_ligatures_is_clean() { + let findings = lint( + r"\documentclass{article} +\usepackage{microtype} +\begin{document} +\end{document}", + ); + assert!(!has_severity_with(&findings, "ligature", Severity::Error)); + } + + #[test] + fn disable_ligatures_without_microtype_is_clean() { + let findings = lint( + r"\documentclass{article} +\DisableLigatures +\begin{document} +\end{document}", + ); + assert!(!has_severity_with(&findings, "ligature", Severity::Error)); + } + + #[test] + fn setmainfont_of_another_font_is_clean() { + let findings = lint( + r"\documentclass{article} +\usepackage{fontspec} +\setmainfont{TeX Gyre Termes} +\begin{document} +\end{document}", + ); + assert!(!has_severity_with( + &findings, + "cannot be found", + Severity::Error + )); + } + + #[test] + fn setmainfont_without_fontspec_is_clean() { + let findings = lint( + r"\documentclass{article} +\setmainfont{Latin Modern Roman} +\begin{document} +\end{document}", + ); + assert!(!has_severity_with( + &findings, + "cannot be found", + Severity::Error + )); + } + + #[test] + fn a_clean_document_has_no_engine_findings() { + let findings = lint( + r"\documentclass{article} +\begin{document} +Hello +\end{document}", + ); + assert!(findings.is_empty()); + } + + #[test] + fn constructs_after_begin_document_are_not_reported() { + let findings = lint( + r"\documentclass{article} +\begin{document} +\usepackage[utf8]{inputenc} +\setmainfont{Latin Modern Roman} +\end{document}", + ); + assert!(findings.is_empty()); + } + + #[test] + fn constructs_in_comments_are_not_reported() { + let findings = lint( + r"\documentclass{article} +% \usepackage[utf8]{inputenc} +% \usepackage{epstopdf} +\begin{document} +\end{document}", + ); + assert!(findings.is_empty()); + } + + #[test] + fn compound_rule_reports_the_command_location() { + let findings = lint( + "\\documentclass{article}\n\\usepackage{microtype}\n\\DisableLigatures\n\\begin{document}", + ); + let finding = findings + .iter() + .find(|f| f.severity == Severity::Error && f.message.contains("DisableLigatures")) + .unwrap(); + assert_eq!(finding.line, 3); + assert_eq!(finding.file, "main.tex"); + } + + // --- wiring through \input traversal --- + + #[test] + fn packages_loaded_from_input_files_are_seen() { + let dir = tempfile::TempDir::new().unwrap(); + std::fs::write( + dir.path().join("main.tex"), + "\\documentclass{article}\n\\input{preamble}\n\\begin{document}\n\\end{document}", + ) + .unwrap(); + std::fs::write( + dir.path().join("preamble.tex"), + "\\usepackage[utf8]{inputenc}\n\\usepackage{epstopdf}", + ) + .unwrap(); + let findings = super::super::lint(dir.path(), "main.tex", None).unwrap(); + assert!(has_severity_with( + &findings, + "does nothing", + Severity::Warning + )); + assert!(has_severity_with(&findings, ".eps", Severity::Warning)); + } + + #[test] + fn compound_rule_fires_across_input_files() { + let dir = tempfile::TempDir::new().unwrap(); + std::fs::write( + dir.path().join("main.tex"), + "\\documentclass{article}\n\\usepackage{fontspec}\n\\input{preamble}\n\\begin{document}\n\\end{document}", + ) + .unwrap(); + std::fs::write( + dir.path().join("preamble.tex"), + "\\setmainfont{Latin Modern Roman}", + ) + .unwrap(); + let findings = super::super::lint(dir.path(), "main.tex", None).unwrap(); + assert!(has_severity_with( + &findings, + "cannot be found", + Severity::Error + )); + } +} diff --git a/src/linter/glyphs.rs b/src/linter/glyphs.rs new file mode 100644 index 0000000..5d18117 --- /dev/null +++ b/src/linter/glyphs.rs @@ -0,0 +1,549 @@ +//! Silent-divergence glyph rules (TF9). +//! +//! The most expensive class of LaTeX defect is the construct that compiles +//! cleanly and produces something other than what the author meant: TeX emits +//! no warning because, to TeX, nothing is wrong. The archetype is `~60M COP` +//! in text mode, where `~` is a non-breaking space — not "approximately" — +//! and the figure silently goes from approximate to exact. +//! +//! The six rules below are deliberately narrow. The discriminating heuristics +//! are the whole point: almost every glyph in the table has a legitimate use, +//! so a rule fires only on the pattern that cannot be that legitimate use, and +//! the tokenizer ([`crate::texparse`]) has already stripped math, verbatim, +//! comments and non-prose command arguments from the stream before these rules +//! see any text. +//! +//! | Rule | Fires on | Does NOT fire on | Severity | +//! |---|---|---|---| +//! | `~` as "approximately" | `~60M`, `~5 anos` (adjacent to a digit) | `Figura~\ref{}`, `Dr.~Smith` | [`Severity::Warning`] | +//! | unescaped `%` | `50%` (digit before, unescaped) | `% a real comment` | [`Severity::Warning`] | +//! | literal `...` | three dots in prose | `\ldots`, `\dots` | [`Severity::Warning`] | +//! | straight quotes | `"text"` | `` `` `` / `''` | [`Severity::Warning`] | +//! | hyphen as range | `2020-2024` | `well-written` | [`Severity::Warning`] | +//! | unescaped `&`, `#`, `_`, `$` | outside math and tables | inside math or tables | [`Severity::Error`] | + +use crate::texparse::{tokenize_with_spans, Token}; + +use super::{LintFinding, Severity}; + +/// Alignment environments where `&` is the column separator rather than prose. +const TABLE_ENVIRONMENTS: &[&str] = &[ + "tabular", + "tabular*", + "tabularx", + "longtable", + "supertabular", + "array", +]; + +/// Run the six glyph rules over one `.tex` file's source. +/// +/// `rel` is the project-relative path used in every finding, matching the rest +/// of the linter's output. +pub fn lint_file(rel: &str, source: &str) -> Vec<LintFinding> { + let tokenized = tokenize_with_spans(source); + let mut findings = Vec::new(); + let mut in_math = 0usize; + let mut table_stack: Vec<&str> = Vec::new(); + let mut in_quote = false; + + for spanned in &tokenized.tokens { + match &spanned.token { + Token::BeginMath => in_math += 1, + Token::EndMath => in_math = in_math.saturating_sub(1), + Token::Environment { name } => { + if table_stack.last() == Some(&name.as_str()) { + table_stack.pop(); + } else if TABLE_ENVIRONMENTS.contains(&name.as_str()) { + table_stack.push(name); + } + } + Token::Text(text) => { + let in_prose = in_math == 0 && table_stack.is_empty(); + check_prose( + rel, + source, + text, + spanned.start, + in_prose, + &mut in_quote, + &mut findings, + ); + } + Token::Section { raw_title, .. } => { + check_section_title(rel, source, raw_title, spanned.start, &mut findings); + } + Token::Comment(_) => check_percent(rel, source, spanned.start, &mut findings), + _ => {} + } + } + + for offset in &tokenized.unclosed_math { + findings.push(LintFinding { + file: rel.to_string(), + line: line_of(source, *offset), + severity: Severity::Error, + message: "Unescaped `$` opens a math region that is never closed — \ + everything after it is typeset as math, not as prose" + .into(), + suggestion: Some("Escape the dollar sign (\\$) for a literal one, or close the math region with a matching $".into()), + }); + } + + findings +} + +/// Scan one prose text run for the rules that live entirely inside a single +/// `Token::Text` (`~`, `...`, straight quotes, hyphen ranges) and, when the +/// run is outside math and tables, the unescaped special characters. +/// +/// `in_quote` toggles per straight-quote pair, so `"text"` is one construct +/// (one finding) rather than two. +fn check_prose( + rel: &str, + source: &str, + text: &str, + start: usize, + in_prose: bool, + in_quote: &mut bool, + findings: &mut Vec<LintFinding>, +) { + let bytes = text.as_bytes(); + let mut i = 0usize; + while i < bytes.len() { + let c = bytes[i] as char; + match c { + '~' => { + if bytes.get(i + 1).is_some_and(|b| b.is_ascii_digit()) { + let value = &text[i + 1..].split_whitespace().next().unwrap_or(""); + findings.push(LintFinding { + file: rel.to_string(), + line: line_of(source, start + i), + severity: Severity::Warning, + message: format!( + "`~{value}` — `~` in text mode is a non-breaking space, not \"approximately\"; \ + the PDF will show a plain space and the figure becomes exact" + ), + suggestion: Some("Use \\textasciitilde{} (e.g. \\textasciitilde{}60M) or spell the word out".into()), + }); + } + i += 1; + } + '.' => { + if bytes.get(i + 1) == Some(&b'.') && bytes.get(i + 2) == Some(&b'.') { + findings.push(LintFinding { + file: rel.to_string(), + line: line_of(source, start + i), + severity: Severity::Warning, + message: "Literal \"...\" renders as three periods, not an ellipsis".into(), + suggestion: Some("Use \\ldots or \\dots".into()), + }); + i += 3; + } else { + i += 1; + } + } + '"' => { + if !*in_quote { + findings.push(LintFinding { + file: rel.to_string(), + line: line_of(source, start + i), + severity: Severity::Warning, + message: "Straight double quote renders as a plain \" glyph, not LaTeX quotation marks".into(), + suggestion: Some("Use `` (open) and '' (close) around quoted text".into()), + }); + } + *in_quote = !*in_quote; + i += 1; + } + '-' => { + if i > 0 + && bytes.get(i - 1).is_some_and(|b| b.is_ascii_digit()) + && bytes.get(i + 1).is_some_and(|b| b.is_ascii_digit()) + { + let mut left = i - 1; + while left > 0 && bytes[left - 1].is_ascii_digit() { + left -= 1; + } + let mut right = i + 1; + while right < bytes.len() && bytes[right].is_ascii_digit() { + right += 1; + } + let construct = &text[left..right]; + findings.push(LintFinding { + file: rel.to_string(), + line: line_of(source, start + i), + severity: Severity::Warning, + message: format!( + "\"{construct}\" renders as a plain hyphen, not a range dash" + ), + suggestion: Some("Use an en-dash for numeric ranges: 2020--2024".into()), + }); + } + i += 1; + } + '&' | '#' | '_' if in_prose => { + let (meaning, fix) = match c { + '&' => ("an alignment tab character, not a literal ampersand", "\\&"), + '#' => ("a macro parameter character, not a literal hash", "\\#"), + _ => ( + "the math subscript operator, not a literal underscore", + "\\_", + ), + }; + findings.push(LintFinding { + file: rel.to_string(), + line: line_of(source, start + i), + severity: Severity::Error, + message: format!( + "Unescaped `{c}` is {meaning} and will not typeset as a literal {c}" + ), + suggestion: Some(format!("Escape it as {fix}")), + }); + i += 1; + } + _ => i += 1, + } + } +} + +/// Lint a section title, which is prose but carries commands and math of its +/// own; re-tokenize it so `\ldots`, math and the like stay inert. +/// +/// Takes the raw (unresolved) title so escaped specials like `\&` are still +/// visible as escaped — the resolved `title` field has already turned them +/// into their literal character and would make every escape look like a +/// mistake. +fn check_section_title( + rel: &str, + source: &str, + raw_title: &str, + command_start: usize, + findings: &mut Vec<LintFinding>, +) { + let tokenized = tokenize_with_spans(raw_title); + let mut in_math = 0usize; + let mut in_quote = false; + for spanned in &tokenized.tokens { + match &spanned.token { + Token::BeginMath => in_math += 1, + Token::EndMath => in_math = in_math.saturating_sub(1), + Token::Text(text) => { + check_prose( + rel, + source, + text, + command_start + spanned.start, + in_math == 0, + &mut in_quote, + findings, + ); + } + Token::Comment(_) => { + check_percent(rel, source, command_start + spanned.start, findings); + } + _ => {} + } + } +} + +/// The `%` rule: a `%` that starts a comment immediately after a digit is the +/// gravest miss — `mejora del 20% en latencia` silently loses `en latencia`. +/// A `%` preceded by anything else (line start, space) is a real comment. +fn check_percent(rel: &str, source: &str, start: usize, findings: &mut Vec<LintFinding>) { + let before = source[..start].chars().next_back(); + if before.is_some_and(|c| c.is_ascii_digit()) { + let digits: String = source[..start] + .chars() + .rev() + .take_while(|c| c.is_ascii_digit()) + .collect::<String>() + .chars() + .rev() + .collect(); + findings.push(LintFinding { + file: rel.to_string(), + line: line_of(source, start), + severity: Severity::Warning, + message: format!( + "\"{digits}%\" — unescaped `%` after a digit starts a comment and silently \ + deletes the rest of the line" + ), + suggestion: Some("Escape the percent sign as \\%".into()), + }); + } +} + +/// 1-based line number of a byte offset. +fn line_of(source: &str, offset: usize) -> usize { + let offset = offset.min(source.len()); + 1 + source[..offset].matches('\n').count() +} + +#[cfg(test)] +mod tests { + use super::*; + + fn lint(source: &str) -> Vec<LintFinding> { + lint_file("main.tex", source) + } + + fn warning(source: &str, fragment: &str) -> bool { + lint(source) + .iter() + .any(|f| f.severity == Severity::Warning && f.message.contains(fragment)) + } + + fn error(source: &str, fragment: &str) -> bool { + lint(source) + .iter() + .any(|f| f.severity == Severity::Error && f.message.contains(fragment)) + } + + fn silent(source: &str, fragment: &str) -> bool { + lint(source).iter().any(|f| f.message.contains(fragment)) + } + + // --- `~` as "approximately" --- + + #[test] + fn tilde_before_digit_is_approximately() { + assert!(warning( + r"negociacion comercial, ~60M COP de ahorro.", + "non-breaking space" + )); + assert!(warning(r"~5 anos de experiencia", "non-breaking space")); + } + + #[test] + fn tilde_before_command_or_letter_is_a_tie() { + assert!(!silent(r"ver Figura~\ref{fig:x}", "non-breaking space")); + assert!(!silent(r"Dr.~Smith", "non-breaking space")); + } + + // --- unescaped `%` --- + + #[test] + fn percent_after_digit_eats_the_rest_of_the_line() { + assert!(warning(r"mejora del 20% en latencia", "silently deletes")); + assert!(warning(r"descuento 50% aplicado", "silently deletes")); + } + + #[test] + fn percent_at_line_start_is_a_real_comment() { + assert!(!silent("% a real comment", "silently deletes")); + assert!(!silent("texto\n% a real comment", "silently deletes")); + } + + #[test] + fn escaped_percent_does_not_fire() { + assert!(!silent(r"50\% de descuento", "silently deletes")); + } + + #[test] + fn percent_after_digit_inside_prose_command_fires() { + assert!(warning( + r"\textit{mejora del 20% en latencia}", + "silently deletes" + )); + } + + // --- literal `...` --- + + #[test] + fn three_dots_in_prose_are_not_an_ellipsis() { + assert!(warning(r"y entonces ... sucede algo", "three periods")); + } + + #[test] + fn latex_ellipsis_commands_do_not_fire() { + assert!(!silent(r"\ldots y luego \dots", "three periods")); + } + + // --- straight quotes --- + + #[test] + fn straight_double_quote_fires() { + assert!(warning( + r#"dijo "hola mundo" y se fue"#, + "Straight double quote" + )); + } + + #[test] + fn tex_quotes_do_not_fire() { + assert!(!silent( + "dijo ``hola mundo'' y se fue", + "Straight double quote" + )); + } + + #[test] + fn a_quote_pair_is_one_finding() { + let findings = lint(r#"dijo "hola mundo" y se fue"#); + let quote_findings: Vec<&LintFinding> = findings + .iter() + .filter(|f| f.message.contains("Straight double quote")) + .collect(); + assert_eq!(quote_findings.len(), 1); + } + + // --- hyphen as range --- + + #[test] + fn hyphen_between_digits_is_a_range_dash() { + assert!(warning(r"periodo 2020-2024", "not a range dash")); + } + + #[test] + fn hyphenated_word_does_not_fire() { + assert!(!silent("well-written code", "not a range dash")); + } + + // --- unescaped `&`, `#`, `_`, `$` --- + + #[test] + fn unescaped_special_characters_are_errors() { + assert!(error(r"a & b", "literal ampersand")); + assert!(error(r"referencia #1", "literal hash")); + assert!(error(r"foo_bar", "literal underscore")); + assert!(error(r"costo $5 dolares", "never closed")); + } + + #[test] + fn math_and_tables_do_not_fire() { + assert!(!silent(r"El valor es $x_{i}^2$ aqui", "literal underscore")); + assert!(!silent( + r"\begin{tabular}{cc} a & b \\ 1 & 2 \end{tabular}", + "literal ampersand" + )); + } + + #[test] + fn starred_math_does_not_fire() { + assert!(!silent( + r"\begin{align*}a &= b_0\end{align*}", + "literal ampersand" + )); + } + + #[test] + fn escaped_special_characters_do_not_fire() { + assert!(!silent(r"a \& b \# 1 x\_y", "literal")); + } + + #[test] + fn verbatim_content_does_not_fire() { + assert!(!silent(r"\verb|a_b & c|", "literal")); + assert!(!silent(r"\lstinline|50% ...|", "silently deletes")); + } + + #[test] + fn url_arguments_do_not_fire() { + assert!(!silent( + r"\url{https://example.com/~user_a}", + "non-breaking space" + )); + assert!(!silent( + r"\url{https://example.com/a_b}", + "literal underscore" + )); + } + + // --- locations --- + + #[test] + fn findings_report_the_right_line() { + let source = "primera linea\nsegunda con ~60M aqui\ntercera"; + let findings = lint(source); + let tilde = findings + .iter() + .find(|f| f.message.contains("non-breaking space")) + .unwrap(); + assert_eq!(tilde.line, 2); + assert_eq!(tilde.file, "main.tex"); + } + + #[test] + fn percent_finding_is_on_the_comment_line() { + let source = "linea uno\nahorro 20% perdido\nlinea tres"; + let findings = lint(source); + let finding = findings + .iter() + .find(|f| f.message.contains("silently deletes")) + .unwrap(); + assert_eq!(finding.line, 2); + } + + #[test] + fn section_titles_are_prose() { + assert!(warning( + r"\section{Resultados 2020-2024}", + "not a range dash" + )); + } + + #[test] + fn section_title_latex_is_inert() { + assert!(!silent( + r"\section{Introduccion \ldots y \dots}", + "three periods" + )); + } + + // --- regression: TE8, escaped specials in section titles --- + // + // 9ab9d86 made `Token::Section::title` the resolved (unescaped) form for + // `outline`. `check_section_title` kept reading `title`, so it saw a + // bare `&` where the source had `\&` and reported a false Error. It must + // read `raw_title` instead. + + #[test] + fn regression_escaped_ampersand_in_section_title_is_silent() { + assert!(lint(r"\subsection*{\textit{Fundador \& Lead Engineer}}").is_empty()); + } + + #[test] + fn unescaped_ampersand_in_section_title_still_fires() { + assert!(error(r"\section{A & B}", "literal ampersand")); + } + + #[test] + fn escaped_percent_in_section_title_does_not_fire() { + assert!(!silent( + r"\section{Progreso 20\% completo}", + "silently deletes" + )); + } + + #[test] + fn escaped_dollar_in_section_title_does_not_fire() { + assert!(lint(r"\section{Costo \$5}").is_empty()); + } + + #[test] + fn escaped_hash_in_section_title_does_not_fire() { + assert!(!silent(r"\section{Ref \#1}", "literal hash")); + } + + #[test] + fn escaped_underscore_in_section_title_does_not_fire() { + assert!(!silent(r"\section{foo\_bar}", "literal underscore")); + } + + // --- wiring through the whole linter --- + + #[test] + fn lint_entry_point_includes_glyph_findings() { + let dir = tempfile::TempDir::new().unwrap(); + std::fs::write( + dir.path().join("main.tex"), + "\\documentclass{article}\n\\begin{document}\n~60M de ahorro\n\\end{document}", + ) + .unwrap(); + let findings = super::super::lint(dir.path(), "main.tex", None).unwrap(); + assert!(findings + .iter() + .any(|f| f.severity == Severity::Warning && f.message.contains("non-breaking space"))); + } +} diff --git a/src/linter/mod.rs b/src/linter/mod.rs index 28444ba..a3b5736 100644 --- a/src/linter/mod.rs +++ b/src/linter/mod.rs @@ -1,20 +1,48 @@ //! Static linting rules. +mod engine; +mod glyphs; +pub(crate) mod spell; + +pub use spell::{ + global_whitelist_path, installed_dictionaries, parse_whitelist_words, InstalledDictionary, + PROJECT_WHITELIST_FILES, +}; + use std::collections::HashSet; use std::path::Path; use anyhow::Result; -/// A linting error with location and suggestion. +use crate::texutil; + +/// Severity of a lint finding. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Severity { + Error, + Warning, +} + +impl std::fmt::Display for Severity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Severity::Error => write!(f, "ERROR"), + Severity::Warning => write!(f, "WARNING"), + } + } +} + +/// A lint finding with location, severity, and suggestion. #[derive(Debug)] -pub struct LintError { +pub struct LintFinding { pub file: String, pub line: usize, + pub severity: Severity, pub message: String, pub suggestion: Option<String>, } -impl std::fmt::Display for LintError { +impl std::fmt::Display for LintFinding { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, " {}:{} — {}", self.file, self.line, self.message)?; if let Some(ref s) = self.suggestion { @@ -25,14 +53,15 @@ impl std::fmt::Display for LintError { } /// Run all lint rules on a project directory. -pub fn lint(root: &Path, entry: &str, bib_file: Option<&str>) -> Result<Vec<LintError>> { +pub fn lint(root: &Path, entry: &str, bib_file: Option<&str>) -> Result<Vec<LintFinding>> { let mut errors = Vec::new(); let entry_path = root.join(entry); if !entry_path.exists() { - errors.push(LintError { + errors.push(LintFinding { file: entry.to_string(), line: 0, + severity: Severity::Error, message: "Entry point file does not exist".into(), suggestion: Some(format!("Create {}", entry)), }); @@ -40,8 +69,17 @@ pub fn lint(root: &Path, entry: &str, bib_file: Option<&str>) -> Result<Vec<Lint } // Collect all .tex files reachable from entry - let mut tex_files = Vec::new(); - collect_tex_files(root, entry, &mut tex_files, &mut errors); + let collected = texutil::collect_tex_files(root, entry); + for (entry, path) in &collected.circular { + errors.push(LintFinding { + severity: Severity::Error, + file: entry.clone(), + line: 0, + message: format!("Circular \\input detected: {}", path.display()), + suggestion: Some("Remove the circular reference".into()), + }); + } + let tex_files = collected.files; // Parse .bib keys if bibliography exists let bib_keys = match bib_file { @@ -54,14 +92,15 @@ pub fn lint(root: &Path, entry: &str, bib_file: Option<&str>) -> Result<Vec<Lint for file in &tex_files { let content = std::fs::read_to_string(file)?; for line in content.lines() { - let line = strip_comment(line); - for label in extract_commands(&line, "label") { + let line = texutil::strip_comment(line); + for label in texutil::extract_commands(&line, "label") { all_labels.insert(label.to_string()); } } } // Run checks on each file + let mut file_contents: Vec<(String, String)> = Vec::new(); for file in &tex_files { let rel = file .strip_prefix(root) @@ -83,8 +122,66 @@ pub fn lint(root: &Path, entry: &str, bib_file: Option<&str>) -> Result<Vec<Lint check_diagram_blocks(&rel, &content, "mermaid", &mut errors); check_diagram_blocks(&rel, &content, "graphviz", &mut errors); check_diagram_blocks(&rel, &content, "d2", &mut errors); + errors.extend(glyphs::lint_file(&rel, &content)); + file_contents.push((rel, content)); + } + + // TF12: report .bib keys that are never cited. `\nocite{*}` cites every key. + let mut cited_keys = HashSet::new(); + let mut nocite_star = false; + collect_cited_keys(&file_contents, &mut cited_keys, &mut nocite_star); + if !nocite_star { + let mut unused: Vec<&str> = bib_keys + .difference(&cited_keys) + .map(String::as_str) + .collect(); + if !unused.is_empty() { + unused.sort(); + errors.push(LintFinding { + severity: Severity::Warning, + file: bib_file.unwrap_or("").to_string(), + line: 0, + message: format!("Unused .bib entries (never cited): {}", unused.join(", ")), + suggestion: Some( + "Cite them with \\cite or remove them from the bibliography".into(), + ), + }); + } + } + + // Spell-checking: attempt to load user-level default language and run spell + // checks over the tokenized file contents. Fail-open: if spell-check cannot + // obtain a dictionary, it prints a clear message and yields no findings. + // To keep the test suite offline and deterministic, do not use the user's + // config during unit tests — tests must explicitly opt-in by calling the + // spell API with a local fixture language (see tests that pass Some("english")). + let is_test_harness = std::env::var("RUST_TEST_THREADS").is_ok() + || std::env::var("NEXTEST_CURRENT_RUN_ID").is_ok() + || std::env::var("NEXTEST_RUN_ID").is_ok() + || std::env::var("CI").is_ok(); + + // When running under a test harness, ignore user config to avoid network + // or environment leakage from the developer machine. Tests that want + // spell-checking should pass a language and provide a local dictionary. + let default_lang = if is_test_harness { + None + } else { + crate::config::load() + .ok() + .and_then(|cfg| cfg.defaults.language) + }; + + if default_lang.is_some() || !is_test_harness { + match spell::lint_files(&file_contents, root, default_lang.as_deref()) { + Ok(mut fs) => errors.append(&mut fs), + Err(e) => eprintln!("Spell-check skipped: {}", e), + } + } else { + eprintln!("Spell-check skipped: test harness detected and no default language configured"); } + errors.extend(engine::lint_files(&file_contents)); + Ok(errors) } @@ -96,11 +193,11 @@ fn check_references( bib_file: Option<&str>, bib_keys: &HashSet<String>, all_labels: &HashSet<String>, - errors: &mut Vec<LintError>, + errors: &mut Vec<LintFinding>, ) { for (i, line) in content.lines().enumerate() { let line_num = i + 1; - let line = strip_comment(line); + let line = texutil::strip_comment(line); check_input_references(root, rel, line_num, &line, errors); check_includegraphics_references(root, rel, line_num, &line, errors); @@ -117,12 +214,13 @@ fn check_input_references( rel: &str, line_num: usize, line: &str, - errors: &mut Vec<LintError>, + errors: &mut Vec<LintFinding>, ) { - for arg in extract_commands(line, "input") { - let input_path = resolve_tex_path(root, arg); + for arg in texutil::extract_commands(line, "input") { + let input_path = texutil::resolve_tex_path(root, arg); if !input_path.exists() { - errors.push(LintError { + errors.push(LintFinding { + severity: Severity::Error, file: rel.to_string(), line: line_num, message: format!("\\input{{{}}} — file not found", arg), @@ -138,12 +236,13 @@ fn check_includegraphics_references( rel: &str, line_num: usize, line: &str, - errors: &mut Vec<LintError>, + errors: &mut Vec<LintFinding>, ) { - for arg in extract_commands(line, "includegraphics") { + for arg in texutil::extract_commands(line, "includegraphics") { let img_path = root.join(arg); if !img_path.exists() { - errors.push(LintError { + errors.push(LintFinding { + severity: Severity::Error, file: rel.to_string(), line: line_num, message: format!("\\includegraphics{{{}}} — file not found", arg), @@ -160,17 +259,18 @@ fn check_cite_references( line: &str, bib_file: Option<&str>, bib_keys: &HashSet<String>, - errors: &mut Vec<LintError>, + errors: &mut Vec<LintFinding>, ) { if bib_file.is_none() { return; } - for arg in extract_commands(line, "cite") { + for arg in texutil::extract_commands(line, "cite") { for key in arg.split(',') { let key = key.trim(); if !key.is_empty() && !bib_keys.contains(key) { - errors.push(LintError { + errors.push(LintFinding { + severity: Severity::Error, file: rel.to_string(), line: line_num, message: format!("\\cite{{{}}} — key not found in .bib", key), @@ -181,17 +281,54 @@ fn check_cite_references( } } +/// Collect every cited key from in-memory file contents for the unused-bib check. +/// +/// `\cite{...}` and `\nocite{...}` mark their keys as cited; `\nocite{*}` cites +/// every key in the bibliography, so it sets `nocite_star` instead. +fn collect_cited_keys( + file_contents: &[(String, String)], + cited_keys: &mut HashSet<String>, + nocite_star: &mut bool, +) { + for (_, content) in file_contents { + for line in content.lines() { + let line = texutil::strip_comment(line); + for arg in texutil::extract_commands(&line, "cite") { + for key in arg.split(',') { + let key = key.trim(); + if !key.is_empty() { + cited_keys.insert(key.to_string()); + } + } + } + for arg in texutil::extract_commands(&line, "nocite") { + if arg == "*" { + *nocite_star = true; + } else { + for key in arg.split(',') { + let key = key.trim(); + if !key.is_empty() { + cited_keys.insert(key.to_string()); + } + } + } + } + } + } +} + /// Check \ref references against defined labels. fn check_ref_references( rel: &str, line_num: usize, line: &str, all_labels: &HashSet<String>, - errors: &mut Vec<LintError>, + errors: &mut Vec<LintFinding>, ) { - for arg in extract_commands(line, "ref") { + for arg in texutil::extract_commands(line, "ref") { if !all_labels.contains(arg) { - errors.push(LintError { + errors.push(LintFinding { + severity: Severity::Error, file: rel.to_string(), line: line_num, message: format!("\\ref{{{}}} — no matching \\label found", arg), @@ -207,11 +344,12 @@ fn check_lstinputlisting_references( rel: &str, line_num: usize, line: &str, - errors: &mut Vec<LintError>, + errors: &mut Vec<LintFinding>, ) { - for arg in extract_commands(line, "lstinputlisting") { + for arg in texutil::extract_commands(line, "lstinputlisting") { if !root.join(arg).exists() { - errors.push(LintError { + errors.push(LintFinding { + severity: Severity::Error, file: rel.to_string(), line: line_num, message: format!("\\lstinputlisting{{{}}} — file not found", arg), @@ -227,11 +365,12 @@ fn check_inputminted_references( rel: &str, line_num: usize, line: &str, - errors: &mut Vec<LintError>, + errors: &mut Vec<LintFinding>, ) { for arg in extract_inputminted_files(line) { if !root.join(arg).exists() { - errors.push(LintError { + errors.push(LintFinding { + severity: Severity::Error, file: rel.to_string(), line: line_num, message: format!("\\inputminted{{{}}} — file not found", arg), @@ -242,7 +381,7 @@ fn check_inputminted_references( } /// Check for unclosed \begin{env} environments. -fn check_environments(rel: &str, content: &str, errors: &mut Vec<LintError>) { +fn check_environments(rel: &str, content: &str, errors: &mut Vec<LintFinding>) { // Stack of (env_name, line_number) let mut stack: Vec<(&str, usize)> = Vec::new(); @@ -255,16 +394,17 @@ fn check_environments(rel: &str, content: &str, errors: &mut Vec<LintError>) { continue; } - for env in extract_commands(trimmed, "begin") { + for env in texutil::extract_commands(trimmed, "begin") { stack.push((env, line_num)); } - for env in extract_commands(trimmed, "end") { + for env in texutil::extract_commands(trimmed, "end") { if let Some((open_env, _)) = stack.last() { if *open_env == env { stack.pop(); } else { - errors.push(LintError { + errors.push(LintFinding { + severity: Severity::Error, file: rel.to_string(), line: line_num, message: format!("\\end{{{}}} does not match \\begin{{{}}}", env, open_env), @@ -272,7 +412,8 @@ fn check_environments(rel: &str, content: &str, errors: &mut Vec<LintError>) { }); } } else { - errors.push(LintError { + errors.push(LintFinding { + severity: Severity::Error, file: rel.to_string(), line: line_num, message: format!("\\end{{{}}} without matching \\begin", env), @@ -284,7 +425,8 @@ fn check_environments(rel: &str, content: &str, errors: &mut Vec<LintError>) { // Report unclosed environments for (env, line_num) in stack { - errors.push(LintError { + errors.push(LintFinding { + severity: Severity::Error, file: rel.to_string(), line: line_num, message: format!("\\begin{{{}}} never closed", env), @@ -293,99 +435,8 @@ fn check_environments(rel: &str, content: &str, errors: &mut Vec<LintError>) { } } -/// Extract arguments from `\command{arg}` and `\command[opts]{arg}` occurrences in a line. -fn extract_commands<'a>(line: &'a str, cmd: &str) -> Vec<&'a str> { - let mut results = Vec::new(); - let pattern = format!("\\{}", cmd); - let mut search = line; - - while let Some(pos) = search.find(&pattern) { - let after = &search[pos + pattern.len()..]; - // Skip optional args [...] - let after = if after.starts_with('[') { - match after.find(']') { - Some(end) => &after[end + 1..], - None => break, - } - } else { - after - }; - if after.starts_with('{') { - if let Some(end) = after.find('}') { - let arg = after[1..end].trim(); - if !arg.is_empty() { - results.push(arg); - } - search = &after[end + 1..]; - continue; - } - } - search = after; - } - - results -} - -/// Resolve a tex input path, adding .tex extension if missing. -fn resolve_tex_path(root: &Path, input: &str) -> std::path::PathBuf { - let p = root.join(input); - if p.extension().is_some() { - p - } else { - p.with_extension("tex") - } -} - -/// Recursively collect .tex files referenced by `\input{}`. -fn collect_tex_files( - root: &Path, - entry: &str, - files: &mut Vec<std::path::PathBuf>, - errors: &mut Vec<LintError>, -) { - let path = resolve_tex_path(root, entry); - if !path.exists() { - return; - } - if files.contains(&path) { - errors.push(LintError { - file: entry.to_string(), - line: 0, - message: format!("Circular \\input detected: {}", path.display()), - suggestion: Some("Remove the circular reference".into()), - }); - return; - } - files.push(path.clone()); - - if let Ok(content) = std::fs::read_to_string(&path) { - for line in content.lines() { - let line = strip_comment(line); - for input in extract_commands(&line, "input") { - collect_tex_files(root, input, files, errors); - } - } - } -} - -/// Strip LaTeX comment from a line (everything after unescaped %). -fn strip_comment(line: &str) -> String { - let mut result = String::with_capacity(line.len()); - let mut prev_backslash = false; - - for c in line.chars() { - if c == '%' && !prev_backslash { - break; - } - prev_backslash = c == '\\'; - result.push(c); - } - - result -} - /// Check mermaid/graphviz blocks: unclosed and invalid pos option. -fn check_diagram_blocks(rel: &str, content: &str, env: &str, errors: &mut Vec<LintError>) { +fn check_diagram_blocks(rel: &str, content: &str, env: &str, errors: &mut Vec<LintFinding>) { for (i, line) in content.lines().enumerate() { let line_num = i + 1; let trimmed = line.trim(); @@ -406,7 +457,7 @@ fn check_unclosed_diagram_block( env: &str, line_num: usize, line_index: usize, - errors: &mut Vec<LintError>, + errors: &mut Vec<LintFinding>, ) { let end_tag = format!("\\end{{{}}}", env); let rest = &content[content @@ -415,7 +466,8 @@ fn check_unclosed_diagram_block( .map(|l| l.len() + 1) .sum::<usize>()..]; if !rest.contains(&*end_tag) { - errors.push(LintError { + errors.push(LintFinding { + severity: Severity::Error, file: rel.to_string(), line: line_num, message: format!("\\begin{{{}}} without matching \\end{{{}}}", env, env), @@ -430,7 +482,7 @@ fn check_diagram_pos_option( line: &str, env: &str, line_num: usize, - errors: &mut Vec<LintError>, + errors: &mut Vec<LintFinding>, ) { const VALID_POS: &[&str] = &["H", "t", "b", "h", "p"]; @@ -452,7 +504,8 @@ fn check_diagram_pos_option( if VALID_POS.contains(&pos) { continue; } - errors.push(LintError { + errors.push(LintFinding { + severity: Severity::Error, file: rel.to_string(), line: line_num, message: format!( @@ -542,10 +595,16 @@ mod tests { (dir, "main.tex".to_string()) } - fn has_error(errors: &[LintError], fragment: &str) -> bool { + fn has_error(errors: &[LintFinding], fragment: &str) -> bool { errors.iter().any(|e| e.message.contains(fragment)) } + fn has_finding_with_severity(errors: &[LintFinding], fragment: &str, sev: Severity) -> bool { + errors + .iter() + .any(|e| e.message.contains(fragment) && e.severity == sev) + } + #[test] fn includegraphics_missing_file_is_error() { let (dir, entry) = setup("\\includegraphics{missing.png}"); @@ -810,4 +869,132 @@ mod tests { let errors = lint(dir.path(), &entry, None).unwrap(); assert!(errors.is_empty()); } + + // --- Severity tests --- + + /// All current rules produce Error-severity findings. + #[test] + fn existing_findings_have_error_severity() { + let (dir, entry) = setup("\\includegraphics{missing.png}"); + let findings = lint(dir.path(), &entry, None).unwrap(); + assert!(!findings.is_empty()); + assert!(has_finding_with_severity( + &findings, + "missing.png", + Severity::Error + )); + } + + /// A clean project with no errors contains no findings. + #[test] + fn clean_project_has_no_findings() { + let (dir, entry) = + setup("\\documentclass{article}\n\\begin{document}\nHello\n\\end{document}"); + let findings = lint(dir.path(), &entry, None).unwrap(); + // No errors → check command would exit 0 regardless of --deny-warnings + assert!( + findings + .iter() + .filter(|f| f.severity == Severity::Error) + .count() + == 0 + ); + } + + /// Error-severity findings are detected independently of any deny-warnings flag + /// (flag logic lives in the command layer; the linter just tags severity). + #[test] + fn error_severity_finding_present_when_error_rule_fires() { + let (dir, entry) = setup("\\cite{ghost}"); + std::fs::write(dir.path().join("refs.bib"), "@article{real,}").unwrap(); + let findings = lint(dir.path(), &entry, Some("refs.bib")).unwrap(); + assert!(findings.iter().any(|f| f.severity == Severity::Error)); + } + + // --- Unused .bib entries (TF12) --- + + /// Unused bib keys are reported once, as a Warning naming the .bib file. + #[test] + fn unused_bib_entries_warn_as_single_finding() { + let (dir, entry) = setup("\\cite{key1}\n\\cite{key2}"); + fs::write( + dir.path().join("refs.bib"), + "@article{key1,}\n@article{key2,}\n@article{unused3,}", + ) + .unwrap(); + let findings = lint(dir.path(), &entry, Some("refs.bib")).unwrap(); + let unused: Vec<_> = findings + .iter() + .filter(|f| f.message.starts_with("Unused")) + .collect(); + assert_eq!(unused.len(), 1, "grouped into one finding, not one per key"); + let finding = unused[0]; + assert_eq!(finding.severity, Severity::Warning); + assert_eq!(finding.file, "refs.bib"); + assert!(finding.message.contains("unused3")); + assert!(!finding.message.contains("key1")); + assert!(!finding.message.contains("key2")); + } + + /// `\nocite{*}` cites every key, so nothing is reported as unused. + #[test] + fn nocite_star_suppresses_unused_warning() { + let (dir, entry) = setup("\\nocite{*}"); + fs::write( + dir.path().join("refs.bib"), + "@article{key1,}\n@article{key2,}", + ) + .unwrap(); + let findings = lint(dir.path(), &entry, Some("refs.bib")).unwrap(); + assert!( + !findings.iter().any(|f| f.message.starts_with("Unused")), + "\\nocite{{*}} reaches every key; none are unused" + ); + } + + /// Keys listed in `\nocite{...}` count as cited for the unused check. + #[test] + fn nocite_key_counts_as_cited() { + let (dir, entry) = setup("\\nocite{key2}"); + fs::write( + dir.path().join("refs.bib"), + "@article{key1,}\n@article{key2,}", + ) + .unwrap(); + let findings = lint(dir.path(), &entry, Some("refs.bib")).unwrap(); + let unused: Vec<_> = findings + .iter() + .filter(|f| f.message.starts_with("Unused")) + .collect(); + assert_eq!(unused.len(), 1); + assert!(unused[0].message.contains("key1")); + assert!(!unused[0].message.contains("key2")); + } + + /// A bibliography whose keys are all cited produces no unused warning. + #[test] + fn all_bib_entries_cited_no_unused_warning() { + let (dir, entry) = setup("\\cite{key1,key2}"); + fs::write( + dir.path().join("refs.bib"), + "@article{key1,}\n@article{key2,}", + ) + .unwrap(); + let findings = lint(dir.path(), &entry, Some("refs.bib")).unwrap(); + assert!( + !findings.iter().any(|f| f.message.starts_with("Unused")), + "all keys are cited" + ); + } + + /// No bibliography means no unused-key warning. + #[test] + fn no_bib_file_no_unused_warning() { + let (dir, entry) = setup("\\cite{key1}"); + let findings = lint(dir.path(), &entry, None).unwrap(); + assert!( + !findings.iter().any(|f| f.message.starts_with("Unused")), + "no .bib, nothing to check" + ); + } } diff --git a/src/linter/spell.rs b/src/linter/spell.rs new file mode 100644 index 0000000..a8c99a0 --- /dev/null +++ b/src/linter/spell.rs @@ -0,0 +1,1848 @@ +use anyhow::{Context, Result}; +use std::collections::{HashMap, HashSet}; +use std::fs; +use std::io::Write; +use std::path::{Path, PathBuf}; + +use super::{LintFinding, Severity}; + +/// 1-based line number of a byte offset. +fn line_of(source: &str, offset: usize) -> usize { + let offset = offset.min(source.len()); + 1 + source[..offset].matches('\n').count() +} +use crate::texparse::{tokenize_with_spans, Token}; +use crate::texutil::strip_empty_groups; + +/// Project-local whitelist filenames to check, in order. Also the order +/// `texforge spell add` picks a target file in: the first that already +/// exists wins (decision 4). +pub const PROJECT_WHITELIST_FILES: &[&str] = &["spell-whitelist.txt", ".texforge/spell-words"]; + +/// The personal dictionary shared by every project: `~/.texforge/spell-words`. +/// `None` when the home directory cannot be determined. +pub fn global_whitelist_path() -> Option<PathBuf> { + dirs::home_dir().map(|h| h.join(".texforge").join("spell-words")) +} + +/// Parse one whitelist file's contents into a lowercase word set. Blank +/// lines and `#`-prefixed comment lines are skipped. Comparison elsewhere is +/// case-insensitive because entries are lowercased here on load. +pub fn parse_whitelist_words(content: &str) -> HashSet<String> { + let mut set = HashSet::new(); + for line in content.lines() { + let w = line.trim(); + if w.is_empty() || w.starts_with('#') { + continue; + } + set.insert(w.to_lowercase()); + } + set +} + +/// Managed dictionary directory under the user's home (`~/.texforge/dicts`). +fn dictionaries_dir() -> Option<PathBuf> { + dirs::home_dir().map(|h| h.join(".texforge").join("dicts")) +} + +fn dictionary_path_for(lang: &str) -> Option<PathBuf> { + dictionaries_dir().map(|d| d.join(format!("{}.txt", lang))) +} + +fn dictionary_dic_path_for(lang: &str) -> Option<PathBuf> { + dictionaries_dir().map(|d| d.join(format!("{}.dic", lang))) +} + +fn dictionary_aff_path_for(lang: &str) -> Option<PathBuf> { + dictionaries_dir().map(|d| d.join(format!("{}.aff", lang))) +} + +/// Where a language's dictionary source lives remotely: a single wordlist +/// file, or a Hunspell `.dic` + `.aff` pair. English keeps the wordlist it +/// has always used; Spanish gets a Hunspell pair because a plain wordlist +/// cannot represent Hunspell's affix-generated forms (see spec rationale). +enum RemoteSource { + Wordlist(&'static str), + Hunspell { + dic_url: &'static str, + aff_url: &'static str, + }, +} + +/// Language -> remote dictionary source mapping (best-effort). Missing +/// entries mean the language is not supported remotely and spell-check will +/// be skipped with a clear message. +fn remote_for_language(lang: &str) -> Option<RemoteSource> { + match lang { + "english" | "en" => Some(RemoteSource::Wordlist( + "https://raw.githubusercontent.com/dwyl/english-words/master/words.txt", + )), + "spanish" | "es" => Some(RemoteSource::Hunspell { + dic_url: "https://raw.githubusercontent.com/wooorm/dictionaries/main/dictionaries/es/index.dic", + aff_url: "https://raw.githubusercontent.com/wooorm/dictionaries/main/dictionaries/es/index.aff", + }), + _ => None, + } +} + +/// Where a language's dictionary lives on disk once `ensure_dictionary` has +/// confirmed or fetched it. Two shapes because a plain wordlist is one file +/// and a Hunspell dictionary is a `.dic`/`.aff` pair. +enum DictionaryLocation { + Wordlist(PathBuf), + Hunspell { dic: PathBuf, aff: PathBuf }, +} + +/// Ensure a dictionary for `lang` is present, downloading and caching it on +/// first use. Returns its on-disk location on success. +fn ensure_dictionary(lang: &str) -> Result<DictionaryLocation> { + let dic_path = dictionary_dic_path_for(lang); + let aff_path = dictionary_aff_path_for(lang); + let txt_path = dictionary_path_for(lang); + + // The Hunspell pair wins when both backends are already present on disk + // for this language — it is the better checker. Both files must exist: + // a lone `.dic` (or lone `.aff`) is not usable and falls through below. + if let (Some(dic), Some(aff)) = (dic_path.as_ref(), aff_path.as_ref()) { + if dic.exists() && aff.exists() { + return Ok(DictionaryLocation::Hunspell { + dic: dic.clone(), + aff: aff.clone(), + }); + } + } + + if let Some(txt) = txt_path.as_ref() { + if txt.exists() { + return Ok(DictionaryLocation::Wordlist(txt.clone())); + } + } + + let dicts_dir = dictionaries_dir().ok_or_else(|| { + anyhow::anyhow!("Could not determine home directory for dictionary cache") + })?; + + let Some(source) = remote_for_language(lang) else { + anyhow::bail!( + "no {} dictionary is available from the configured source", + lang + ) + }; + + // During tests (and when running under a test harness such as nextest) + // avoid any network activity — fail open with a clear message so the + // test suite remains offline-friendly and deterministic. Detect at + // runtime because cfg!(test) is not reliable for code compiled into + // non-test binaries that run under test harnesses. + let is_test_harness = std::env::var("RUST_TEST_THREADS").is_ok() + || std::env::var("NEXTEST_CURRENT_RUN_ID").is_ok() + || std::env::var("NEXTEST_RUN_ID").is_ok() + || std::env::var("CI").is_ok(); + + if is_test_harness { + anyhow::bail!( + "Dictionary for '{}' not present and network disabled during tests", + lang + ); + } + + fs::create_dir_all(&dicts_dir).context("Failed to create dictionary cache directory")?; + + // Prefer the system 'curl' or 'wget' binary to avoid pulling in reqwest/ + // rustls at runtime (which has previously caused panics in some test + // environments). If neither tool is available, degrade to a clear message + // and do not attempt network activity. + match source { + RemoteSource::Wordlist(url) => { + eprintln!( + "Dictionary for '{}' not found locally. Downloading...", + lang + ); + let bytes = match download_with_tools(url, "curl", "wget") { + Ok(bytes) => bytes, + Err(failure) => anyhow::bail!(failure.describe(lang, url)), + }; + + let path = txt_path.expect("dictionaries_dir() succeeded above"); + let mut f = fs::File::create(&path) + .with_context(|| format!("Failed to create dictionary file: {}", path.display()))?; + f.write_all(&bytes)?; + eprintln!(" ◇ Dictionary cached to {}", path.display()); + + Ok(DictionaryLocation::Wordlist(path)) + } + RemoteSource::Hunspell { dic_url, aff_url } => { + eprintln!( + "Dictionary for '{}' not found locally. Downloading Hunspell dictionary...", + lang + ); + + // Fetch both files before writing anything to their final + // location: a partially downloaded language must never be left + // on disk (a stray `<lang>.dic` with no `.aff`, or vice versa, + // would make every later run fail with nothing obviously wrong). + let dic_bytes = match download_with_tools(dic_url, "curl", "wget") { + Ok(bytes) => bytes, + Err(failure) => anyhow::bail!(failure.describe(lang, dic_url)), + }; + let aff_bytes = match download_with_tools(aff_url, "curl", "wget") { + Ok(bytes) => bytes, + Err(failure) => anyhow::bail!(failure.describe(lang, aff_url)), + }; + + let dic_final = dic_path.expect("dictionaries_dir() succeeded above"); + let aff_final = aff_path.expect("dictionaries_dir() succeeded above"); + let dic_tmp = dicts_dir.join(format!("{}.dic.part", lang)); + let aff_tmp = dicts_dir.join(format!("{}.aff.part", lang)); + + fs::write(&dic_tmp, &dic_bytes) + .with_context(|| format!("Failed to write {}", dic_tmp.display()))?; + fs::write(&aff_tmp, &aff_bytes) + .with_context(|| format!("Failed to write {}", aff_tmp.display()))?; + + fs::rename(&dic_tmp, &dic_final) + .with_context(|| format!("Failed to install {}", dic_final.display()))?; + if let Err(e) = fs::rename(&aff_tmp, &aff_final) { + // Undo the first half of the move rather than leave a `.dic` + // with no matching `.aff` on disk. + let _ = fs::remove_file(&dic_final); + return Err(e) + .with_context(|| format!("Failed to install {}", aff_final.display())); + } + + eprintln!( + " ◇ Dictionary cached to {} and {}", + dic_final.display(), + aff_final.display() + ); + + Ok(DictionaryLocation::Hunspell { + dic: dic_final, + aff: aff_final, + }) + } + } +} + +/// Outcome of running a single download tool (curl or wget). +enum ToolOutcome { + Success(Vec<u8>), + /// The binary does not exist in PATH (spawn failed with `NotFound`). + NotFound, + /// The binary exists and ran, but did not produce the dictionary. + Failed(ToolFailure), +} + +/// Detail of a download tool that ran but failed, kept separate from "tool +/// not installed" so the two situations never collapse into one message. +struct ToolFailure { + tool: &'static str, + detail: String, +} + +/// Reason a download could not be completed by any available tool. +enum DownloadFailure { + /// Neither tool exists in PATH. + NoToolFound, + /// A tool ran but the transfer itself failed (bad exit status, HTTP + /// error, network error, ...). Carries that tool's own error output so + /// it can be reported verbatim instead of behind a generic message. + ToolError(ToolFailure), +} + +impl DownloadFailure { + /// `lang` and `url` are folded in here (rather than left to the caller) + /// so every branch names the specific language and source attempted, + /// even though the caller also wraps this in its own "could not obtain + /// dictionary for '{lang}'" context. + fn describe(&self, lang: &str, url: &str) -> String { + match self { + DownloadFailure::NoToolFound => { + "neither 'curl' nor 'wget' was found in PATH".to_string() + } + DownloadFailure::ToolError(f) => { + let mut msg = format!("{} exited fetching {}: {}", f.tool, url, f.detail); + if f.detail.contains("404") { + msg.push_str(&format!( + "; this looks like a missing resource (HTTP 404) — the source configured \ + for '{}' may not have this dictionary, see remote_for_language()", + lang + )); + } + msg + } + } + } +} + +/// Last non-empty lines of tool output, trimmed, for embedding in an error +/// message without dumping an entire progress bar or stack trace. +fn tail_lines(text: &str, max_lines: usize) -> String { + let lines: Vec<&str> = text + .lines() + .map(str::trim) + .filter(|l| !l.is_empty()) + .collect(); + let start = lines.len().saturating_sub(max_lines); + lines[start..].join(" | ") +} + +/// Run one download tool binary and classify what happened. Kept separate +/// from `download_with_tools` so both curl and wget go through identical +/// classification logic. +fn run_tool(bin: &str, args: &[&str], tool_label: &'static str) -> ToolOutcome { + match std::process::Command::new(bin).args(args).output() { + Ok(output) if output.status.success() => ToolOutcome::Success(output.stdout), + Ok(output) => { + let tail = tail_lines(&String::from_utf8_lossy(&output.stderr), 5); + let detail = if tail.is_empty() { + format!("exited with {}", output.status) + } else { + format!("exited with {}: {}", output.status, tail) + }; + ToolOutcome::Failed(ToolFailure { + tool: tool_label, + detail, + }) + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => ToolOutcome::NotFound, + Err(e) => ToolOutcome::Failed(ToolFailure { + tool: tool_label, + detail: format!("could not be run: {}", e), + }), + } +} + +/// Try `curl_bin` then `wget_bin` to fetch `url`. Binary names are +/// parameterized (rather than hardcoded to "curl"/"wget") so tests can +/// exercise the "no download tool" and "tool ran but failed" branches +/// deterministically — e.g. a nonexistent binary name for "not found", or +/// `false`, which always exits 1, for "ran but failed" — without depending +/// on what happens to be installed on the machine running the tests. +fn download_with_tools( + url: &str, + curl_bin: &str, + wget_bin: &str, +) -> std::result::Result<Vec<u8>, DownloadFailure> { + let curl = match run_tool(curl_bin, &["-fsSL", url], "curl") { + ToolOutcome::Success(bytes) => return Ok(bytes), + other => other, + }; + let wget = match run_tool(wget_bin, &["--no-verbose", "-O", "-", url], "wget") { + ToolOutcome::Success(bytes) => return Ok(bytes), + other => other, + }; + + match (curl, wget) { + (ToolOutcome::Failed(f), _) => Err(DownloadFailure::ToolError(f)), + (ToolOutcome::NotFound, ToolOutcome::Failed(f)) => Err(DownloadFailure::ToolError(f)), + (ToolOutcome::NotFound, ToolOutcome::NotFound) => Err(DownloadFailure::NoToolFound), + _ => unreachable!("success cases already returned above"), + } +} + +/// Map a babel/polyglossia language option (e.g. `spanish`, `es`, `main=spanish`) +/// to the canonical language name used for dictionary filenames. +fn normalize_babel_option(opt: &str) -> Option<&'static str> { + // `main=spanish` / `variant=es-MX` style keyed options: use the value. + let value = opt.rsplit('=').next().unwrap_or(opt).trim(); + match value { + "spanish" | "es" | "spanish-mexico" | "es-MX" => Some("spanish"), + "english" | "en" | "USenglish" | "UKenglish" => Some("english"), + "french" | "fr" | "francais" => Some("french"), + _ => None, + } +} + +/// If `args` is a `\usepackage` invocation loading `babel` or `polyglossia`, +/// return the language it declares (e.g. `\usepackage[spanish]{babel}` -> `Some("spanish")`). +/// +/// The tokenizer appends bracket and brace groups to `args` in the order they +/// appear, so for `[spanish]{babel}` the package name (`babel`) is `args.last()` +/// and the language option (`spanish`) is an *earlier* element — not the last one. +fn babel_language_from_usepackage(args: &[String]) -> Option<&'static str> { + let package = args.last()?; + if package != "babel" && package != "polyglossia" { + return None; + } + for opt_group in &args[..args.len() - 1] { + for opt in opt_group.split(',') { + if let Some(lang) = normalize_babel_option(opt.trim()) { + return Some(lang); + } + } + } + None +} + +/// Outcome of `resolve_language`: the language spell-check will actually use, +/// plus the document's own `babel`/`polyglossia` declaration (if any) and +/// where it was found. Kept separate from finding-construction so +/// `resolve_language` stays free of `LintFinding` concerns; callers decide +/// whether the declaration and the configured default disagree. +struct LanguageResolution { + /// The language spell-check will use. + language: String, + /// `(language, file, line)` of the `\usepackage[...]{babel}` (or + /// `polyglossia`) declaration found in the preamble, if any. + declared: Option<(String, String, usize)>, +} + +/// Scan a single file's preamble for a `babel`/`polyglossia` language +/// declaration, stopping at `\begin{document}` so the body is never +/// tokenized for this. Returns the language and the 1-based line of the +/// `\usepackage` that declared it. +fn find_babel_declaration(source: &str) -> Option<(&'static str, usize)> { + let tokenized = tokenize_with_spans(source); + for sp in &tokenized.tokens { + match &sp.token { + Token::Command { name, args } if name == "usepackage" => { + if let Some(lang) = babel_language_from_usepackage(args) { + return Some((lang, line_of(source, sp.start))); + } + } + Token::BeginDocument => break, + _ => {} + } + } + None +} + +/// Resolve the language to spell-check against. Highest priority first: +/// (1) a `babel`/`polyglossia` language declared in the document's own +/// preamble — a declaration inside the file is evidence about *this* +/// document, while a global default is only a guess; (2) the user-configured +/// default; (3) `english`. The declaration (if any) is reported alongside the +/// resolved language so the caller can warn when it disagrees with the +/// configured default rather than silently overriding it. +fn resolve_language(files: &[(String, String)], default_lang: Option<&str>) -> LanguageResolution { + let declared = files.iter().find_map(|(rel, source)| { + find_babel_declaration(source).map(|(lang, line)| (lang.to_string(), rel.clone(), line)) + }); + + let language = match &declared { + Some((lang, _, _)) => lang.clone(), + None => default_lang + .map(str::to_string) + .unwrap_or_else(|| "english".to_string()), + }; + + LanguageResolution { language, declared } +} + +/// Message for the `Severity::Warning` finding emitted when the document's +/// own declaration overrides a configured default that names a different +/// language. Names both languages explicitly and states which one won, so +/// the override is never silent. +fn language_disagreement_message(configured: &str, declared: &str) -> String { + format!( + "Configured default language is '{}', but this document declares '{}' via babel/polyglossia; using '{}'", + configured, declared, declared + ) +} + +/// Best-effort path to name in the skip message before it's known which +/// backend (if any) would have served `lang` — the `.dic` half of a +/// Hunspell pair, or the wordlist path otherwise. +fn expected_dictionary_hint(lang: &str) -> Option<PathBuf> { + match remote_for_language(lang) { + Some(RemoteSource::Hunspell { .. }) => dictionary_dic_path_for(lang), + _ => dictionary_path_for(lang), + } +} + +/// Single-line message printed when spell-check must be skipped because the +/// dictionary for the resolved language is unavailable. Names the language, +/// the dictionary path that was expected, and why it could not be obtained — +/// so a wrong-language (or no-language) run is never silent. +fn skip_message(lang: &str, expected_path: Option<&Path>, reason: &str) -> String { + let expected = expected_path + .map(|p| p.display().to_string()) + .unwrap_or_else(|| "<unknown: could not determine home directory>".to_string()); + format!( + "Spell-check skipped: resolved language '{}', but its dictionary ({}) is unavailable: {}", + lang, expected, reason + ) +} + +/// Single-line message printed when spell-check runs, naming the language +/// and what it is being checked against, for either backend. +fn using_message(lang: &str, loc: &DictionaryLocation) -> String { + match loc { + DictionaryLocation::Wordlist(path) => format!( + "Spell-check: checking '{}' prose against {}", + lang, + path.display() + ), + DictionaryLocation::Hunspell { dic, aff } => format!( + "Spell-check: checking '{}' prose against Hunspell dictionary {} + {}", + lang, + dic.display(), + aff.display() + ), + } +} + +/// A dictionary asked exactly one question by every caller: "is this word +/// known?" Two backends answer it — a plain wordlist and a Hunspell +/// `.dic`/`.aff` pair — and nothing upstream learns which one did. +enum WordDictionary { + Wordlist(HashSet<String>), + Hunspell(Box<spellbook::Dictionary>), +} + +impl WordDictionary { + fn contains(&self, word: &str) -> bool { + match self { + WordDictionary::Wordlist(set) => set.contains(word), + WordDictionary::Hunspell(dict) => dict.check(word), + } + } +} + +fn load_dictionary(loc: &DictionaryLocation) -> Result<WordDictionary> { + match loc { + DictionaryLocation::Wordlist(path) => { + let content = fs::read_to_string(path).context("Failed to read dictionary file")?; + let mut set = HashSet::new(); + for line in content.lines() { + let w = line.trim(); + if w.is_empty() { + continue; + } + set.insert(w.to_lowercase()); + } + Ok(WordDictionary::Wordlist(set)) + } + DictionaryLocation::Hunspell { dic, aff } => { + let aff_content = fs::read_to_string(aff).with_context(|| { + format!("Failed to read Hunspell affix file: {}", aff.display()) + })?; + let dic_content = fs::read_to_string(dic).with_context(|| { + format!("Failed to read Hunspell dictionary file: {}", dic.display()) + })?; + // A parse error here means a corrupt or truncated download, not + // a bug in the caller — surface it through the existing skip + // path with the language named, never as a panic. + let dict = spellbook::Dictionary::new(&aff_content, &dic_content).map_err(|e| { + anyhow::anyhow!( + "Failed to parse Hunspell dictionary ({}, {}): {}", + dic.display(), + aff.display(), + e + ) + })?; + Ok(WordDictionary::Hunspell(Box::new(dict))) + } + } +} + +/// A dictionary found under the managed `~/.texforge/dicts` directory, +/// naming which backend it is so callers (e.g. `texforge doctor`) can report +/// on it without assuming a single-file wordlist. +pub enum InstalledDictionary { + Wordlist { + lang: String, + path: PathBuf, + }, + Hunspell { + lang: String, + dic_path: PathBuf, + aff_path: PathBuf, + }, +} + +impl InstalledDictionary { + pub fn lang(&self) -> &str { + match self { + InstalledDictionary::Wordlist { lang, .. } => lang, + InstalledDictionary::Hunspell { lang, .. } => lang, + } + } +} + +/// List installed dictionaries, sorted by language. +/// +/// Reports only what is actually present under `dir` — verified state, not +/// the set of languages texforge merely knows how to fetch remotely. A lone +/// `.dic` with no matching `.aff` is not usable and is not reported. +fn installed_dictionaries_in(dir: &Path) -> Vec<InstalledDictionary> { + let Ok(entries) = fs::read_dir(dir) else { + return Vec::new(); + }; + + let mut dicts: Vec<InstalledDictionary> = Vec::new(); + let mut hunspell_langs: Vec<String> = Vec::new(); + + for path in entries.filter_map(Result::ok).map(|e| e.path()) { + match path.extension().and_then(|s| s.to_str()) { + Some("txt") => { + if let Some(lang) = path.file_stem().and_then(|s| s.to_str()) { + dicts.push(InstalledDictionary::Wordlist { + lang: lang.to_string(), + path: path.clone(), + }); + } + } + Some("dic") => { + if let Some(lang) = path.file_stem().and_then(|s| s.to_str()) { + hunspell_langs.push(lang.to_string()); + } + } + _ => {} + } + } + + for lang in hunspell_langs { + let aff_path = dir.join(format!("{}.aff", lang)); + if aff_path.exists() { + let dic_path = dir.join(format!("{}.dic", lang)); + dicts.push(InstalledDictionary::Hunspell { + lang, + dic_path, + aff_path, + }); + } + } + + dicts.sort_by(|a, b| a.lang().cmp(b.lang())); + dicts +} + +/// List dictionaries installed under the managed `~/.texforge/dicts` directory. +pub fn installed_dictionaries() -> Vec<InstalledDictionary> { + match dictionaries_dir() { + Some(dir) => installed_dictionaries_in(&dir), + None => Vec::new(), + } +} + +/// Words accepted for this project: the union of whichever +/// `PROJECT_WHITELIST_FILES` exist under `root`, plus the user's global +/// personal dictionary (`global_whitelist_path`). Neither scope shadows the +/// other — a word accepted anywhere is accepted (decision 3). Reading either +/// source is best-effort: a missing or unreadable file (including a global +/// dictionary that was never created, or a home directory that can't be +/// determined) is the normal case, not an error. +fn load_project_whitelist(root: &Path) -> HashSet<String> { + let mut set = HashSet::new(); + for name in PROJECT_WHITELIST_FILES { + if let Ok(text) = fs::read_to_string(root.join(name)) { + set.extend(parse_whitelist_words(&text)); + } + } + if let Some(global) = global_whitelist_path() { + if let Ok(text) = fs::read_to_string(&global) { + set.extend(parse_whitelist_words(&text)); + } + } + set +} + +/// Lint files for spelling mistakes. Returns warnings (never errors). +/// If a dictionary cannot be obtained, returns Ok(vec![]) after printing a +/// clear message (per spec: don't fail the build for missing dictionaries). +pub fn lint_files( + files: &[(String, String)], + root: &Path, + default_lang: Option<&str>, +) -> Result<Vec<LintFinding>> { + // Determine language: the document's own babel/polyglossia declaration + // wins over the configured default, which wins over the `english` + // fallback. See `resolve_language` for the rationale. + let resolution = resolve_language(files, default_lang); + let lang = resolution.language; + + // The document's declaration silently overriding the user's global + // default would just replace one confusing behaviour with another: warn, + // naming both languages, whenever they disagree. Fires at most once per + // run and never when either is absent or they agree. + let mut findings = Vec::new(); + if let (Some(configured), Some((declared, file, line))) = + (default_lang, resolution.declared.as_ref()) + { + if declared != configured { + findings.push(LintFinding { + file: file.clone(), + line: *line, + severity: Severity::Warning, + message: language_disagreement_message(configured, declared), + suggestion: None, + }); + } + } + + // Ensure a dictionary exists and load it. Never fall back to a dictionary + // for a different language: a missing dictionary means spell-check is + // skipped for this run, not silently degraded. + let dict_loc = match ensure_dictionary(&lang) { + Ok(loc) => loc, + Err(e) => { + eprintln!( + "{}", + skip_message( + &lang, + expected_dictionary_hint(&lang).as_deref(), + &e.to_string() + ) + ); + return Ok(findings); + } + }; + + let dict_hint = match &dict_loc { + DictionaryLocation::Wordlist(path) => path.clone(), + DictionaryLocation::Hunspell { dic, .. } => dic.clone(), + }; + let dict = match load_dictionary(&dict_loc) { + Ok(d) => d, + Err(e) => { + eprintln!("{}", skip_message(&lang, Some(&dict_hint), &e.to_string())); + return Ok(findings); + } + }; + + eprintln!("{}", using_message(&lang, &dict_loc)); + + let whitelist = load_project_whitelist(root); + + // Map unknown word -> first (file, line) occurrence + let mut unknowns: HashMap<String, (String, usize)> = HashMap::new(); + + for (rel, source) in files { + let tokenized = tokenize_with_spans(source); + for sp in &tokenized.tokens { + if let Token::Text(text) = &sp.token { + // Empty LaTeX groups ({}) are a ligature workaround that + // produces no glyph (e.g. `workf{}lows` renders as + // `workflows`); strip them before splitting so the joined + // word is checked, not its fragments. + let text = strip_empty_groups(text); + // Extract candidate words by splitting on non-alpha characters + for word in text.split(|c: char| !c.is_alphabetic()) { + let w = word.trim(); + if w.is_empty() { + continue; + } + let wl = w.to_lowercase(); + if wl.len() <= 1 { + // skip short tokens to avoid noisy single-letter misses + continue; + } + if !dict.contains(&wl) && !whitelist.contains(&wl) { + // record first occurrence only + unknowns + .entry(wl) + .or_insert_with(|| (rel.clone(), line_of(source, sp.start))); + } + } + } + } + } + + for (word, (file, line)) in unknowns { + findings.push(LintFinding { + file, + line, + severity: Severity::Warning, + message: format!("Unknown word: '{}'", word), + suggestion: Some( + "Add to your personal dictionary with `texforge spell add <word>` \ + (add --local instead to accept it only in this project) to accept this word" + .into(), + ), + }); + } + + Ok(findings) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + #[test] + fn tokenizer_integration_does_not_flag_commands_or_labels() { + let src = r#"\documentclass{article} +\begin{document} +Hello world. This is some text. \label{sec:intro} More text. +\end{document}"#; + // Create a tiny english dictionary that contains common words + let tmp = TempDir::new().unwrap(); + let dict_dir = tmp.path().join(".texforge").join("dicts"); + fs::create_dir_all(&dict_dir).unwrap(); + fs::write( + dict_dir.join("english.txt"), + "hello\nworld\nthis\nis\nsome\ntext\nmore\n", + ) + .unwrap(); + + // Run lint_files against a single file + let files = vec![("main.tex".to_string(), src.to_string())]; + let findings = lint_files(&files, tmp.path(), Some("english")).unwrap(); + // Should be empty: the words used are in the tiny dictionary and commands/labels not emitted + assert!( + findings.is_empty(), + "Expected no findings, got: {:?}", + findings + ); + } + + #[test] + fn installed_dictionaries_in_empty_dir_returns_empty() { + let tmp = TempDir::new().unwrap(); + let dicts = installed_dictionaries_in(tmp.path()); + assert!(dicts.is_empty()); + } + + #[test] + fn installed_dictionaries_in_missing_dir_returns_empty() { + let tmp = TempDir::new().unwrap(); + let missing = tmp.path().join("does-not-exist"); + let dicts = installed_dictionaries_in(&missing); + assert!(dicts.is_empty()); + } + + #[test] + fn installed_dictionaries_in_lists_txt_files_sorted() { + let tmp = TempDir::new().unwrap(); + fs::write(tmp.path().join("spanish.txt"), "hola\nmundo\n").unwrap(); + fs::write(tmp.path().join("english.txt"), "hello\nworld\n").unwrap(); + fs::write(tmp.path().join("notes.md"), "ignored").unwrap(); + let dicts = installed_dictionaries_in(tmp.path()); + let langs: Vec<&str> = dicts.iter().map(InstalledDictionary::lang).collect(); + assert_eq!(langs, vec!["english", "spanish"]); + } + + /// A Hunspell pair (both `.dic` and `.aff` present) is reported as an + /// installed dictionary in its own right (requirement 8) — `texforge + /// doctor` must not go blind to a language once its Hunspell pair lands. + #[test] + fn installed_dictionaries_in_reports_hunspell_pair() { + let tmp = TempDir::new().unwrap(); + fs::write(tmp.path().join("spanish.dic"), "1\nsol/S\n").unwrap(); + fs::write(tmp.path().join("spanish.aff"), "SFX S Y 1\nSFX S 0 es .\n").unwrap(); + let dicts = installed_dictionaries_in(tmp.path()); + assert_eq!(dicts.len(), 1); + match &dicts[0] { + InstalledDictionary::Hunspell { + lang, + dic_path, + aff_path, + } => { + assert_eq!(lang, "spanish"); + assert!(dic_path.ends_with("spanish.dic")); + assert!(aff_path.ends_with("spanish.aff")); + } + InstalledDictionary::Wordlist { path, .. } => { + panic!( + "expected a Hunspell entry, got a Wordlist entry: {}", + path.display() + ) + } + } + } + + /// A lone `.dic` with no matching `.aff` is not a usable dictionary and + /// must not be reported as installed. + #[test] + fn installed_dictionaries_in_ignores_dic_without_aff() { + let tmp = TempDir::new().unwrap(); + fs::write(tmp.path().join("spanish.dic"), "1\nsol/S\n").unwrap(); + let dicts = installed_dictionaries_in(tmp.path()); + assert!( + dicts.is_empty(), + "a .dic with no .aff must not be reported as installed: got entries for {:?}", + dicts + .iter() + .map(InstalledDictionary::lang) + .collect::<Vec<_>>() + ); + } + + #[test] + fn ensure_dictionary_bails_in_test_harness_environment() { + // Simulate being run under a test harness like nextest by setting a + // recognized environment variable. ensure_dictionary must not attempt + // network activity in this case and should return an Err. + let home = TempDir::new().unwrap(); + let orig_home = std::env::var("HOME").ok(); + std::env::set_var("HOME", home.path()); + std::env::set_var("NEXTEST_RUN_ID", "1"); + let res = ensure_dictionary("spanish"); + assert!( + res.is_err(), + "Expected ensure_dictionary to error when under test harness" + ); + std::env::remove_var("NEXTEST_RUN_ID"); + match orig_home { + Some(v) => std::env::set_var("HOME", v), + None => std::env::remove_var("HOME"), + } + } + + /// Neither binary exists in PATH: must report that plainly, naming both + /// tools, and must NOT claim a transfer failure that never happened. + #[test] + fn download_with_tools_reports_missing_tools_by_name() { + let bogus_a = "definitely-not-a-real-binary-abc123"; + let bogus_b = "definitely-not-a-real-binary-xyz789"; + let err = download_with_tools("https://example.invalid/dict.txt", bogus_a, bogus_b) + .expect_err("expected failure when neither tool exists"); + assert!(matches!(err, DownloadFailure::NoToolFound)); + let msg = err.describe("spanish", "https://example.invalid/dict.txt"); + assert!(msg.contains("curl"), "message should name curl: {}", msg); + assert!(msg.contains("wget"), "message should name wget: {}", msg); + } + + /// A tool that exists and runs but fails must have ITS failure reported + /// (exit status), not the generic "no download tool" message — that + /// message is reserved for the tool genuinely being absent. + #[cfg(unix)] + #[test] + fn download_with_tools_reports_real_exit_status_when_tool_runs_but_fails() { + // `false` always exists on Unix and always exits 1 without touching + // the network, so this is deterministic and offline. + let bogus_wget = "definitely-not-a-real-binary-xyz789"; + let err = download_with_tools("https://example.invalid/dict.txt", "false", bogus_wget) + .expect_err("expected failure when curl exits non-zero"); + match &err { + DownloadFailure::ToolError(f) => assert_eq!(f.tool, "curl"), + DownloadFailure::NoToolFound => { + panic!("curl exists and ran; must not report NoToolFound") + } + } + let msg = err.describe("spanish", "https://example.invalid/dict.txt"); + assert!(msg.contains("curl"), "message should name curl: {}", msg); + assert!( + msg.contains("exit"), + "message should carry the tool's exit status: {}", + msg + ); + } + + /// A 404 from a tool that ran must be called out explicitly as a + /// missing-resource problem, not folded into a generic error. + #[cfg(unix)] + #[test] + fn download_with_tools_flags_http_404_as_missing_source() { + use std::io::Write as _; + use std::os::unix::fs::PermissionsExt; + + let tmp = TempDir::new().unwrap(); + let fake_curl = tmp.path().join("curl"); + { + let mut f = fs::File::create(&fake_curl).unwrap(); + writeln!(f, "#!/bin/sh").unwrap(); + writeln!( + f, + "echo 'curl: (22) The requested URL returned error: 404' 1>&2" + ) + .unwrap(); + writeln!(f, "exit 22").unwrap(); + } + let mut perms = fs::metadata(&fake_curl).unwrap().permissions(); + perms.set_mode(0o755); + fs::set_permissions(&fake_curl, perms).unwrap(); + + let bogus_wget = "definitely-not-a-real-binary-xyz789"; + let err = download_with_tools( + "https://example.invalid/spanish.txt", + fake_curl.to_str().unwrap(), + bogus_wget, + ) + .expect_err("expected failure on HTTP 404"); + let msg = err.describe("spanish", "https://example.invalid/spanish.txt"); + assert!( + msg.contains("404"), + "message should surface the 404 the tool reported: {}", + msg + ); + assert!( + msg.to_lowercase().contains("source") || msg.to_lowercase().contains("exist"), + "message should call out that the dictionary may not exist at the source: {}", + msg + ); + } + + // --- TE6: language resolution must not silently default to English --- + + /// `\usepackage[spanish]{babel}` tokenizes with the package name + /// (`babel`) last and the language option (`spanish`) *before* it — the + /// bug was checking `args.last()` for the language, which is always the + /// package name, so this never matched. + #[test] + fn babel_language_reads_the_option_not_the_package_name() { + let args = vec!["spanish".to_string(), "babel".to_string()]; + assert_eq!(babel_language_from_usepackage(&args), Some("spanish")); + } + + #[test] + fn babel_language_ignores_unrelated_packages() { + let args = vec!["amsmath".to_string()]; + assert_eq!(babel_language_from_usepackage(&args), None); + + let args = vec!["utf8".to_string(), "inputenc".to_string()]; + assert_eq!(babel_language_from_usepackage(&args), None); + } + + #[test] + fn babel_language_handles_babel_with_no_option() { + let args = vec!["babel".to_string()]; + assert_eq!(babel_language_from_usepackage(&args), None); + } + + #[test] + fn babel_language_handles_keyed_options_and_polyglossia() { + let args = vec!["main=spanish".to_string(), "polyglossia".to_string()]; + assert_eq!(babel_language_from_usepackage(&args), Some("spanish")); + } + + #[test] + fn resolve_language_infers_spanish_from_babel_preamble() { + let src = "\\usepackage[spanish]{babel}\n\\begin{document}\nHola\n\\end{document}"; + let files = vec![("main.tex".to_string(), src.to_string())]; + assert_eq!(resolve_language(&files, None).language, "spanish"); + } + + #[test] + fn resolve_language_defaults_to_english_without_babel() { + let src = "\\documentclass{article}\n\\begin{document}\nHello\n\\end{document}"; + let files = vec![("main.tex".to_string(), src.to_string())]; + assert_eq!(resolve_language(&files, None).language, "english"); + } + + /// TE10 regression: the document's own declaration must win over a + /// configured default that names a different language. + #[test] + fn resolve_language_prefers_document_declaration_over_configured_default() { + let src = "\\usepackage[spanish]{babel}\n\\begin{document}\nHola\n\\end{document}"; + let files = vec![("main.tex".to_string(), src.to_string())]; + assert_eq!( + resolve_language(&files, Some("english")).language, + "spanish" + ); + } + + /// The precedence rule is not spanish-specific: any declared language + /// overrides the configured default. + #[test] + fn resolve_language_prefers_document_declaration_for_other_languages_too() { + let src = "\\usepackage[french]{babel}\n\\begin{document}\nSalut\n\\end{document}"; + let files = vec![("main.tex".to_string(), src.to_string())]; + assert_eq!(resolve_language(&files, Some("english")).language, "french"); + } + + #[test] + fn resolve_language_uses_configured_default_without_babel() { + let src = "\\documentclass{article}\n\\begin{document}\nHello\n\\end{document}"; + let files = vec![("main.tex".to_string(), src.to_string())]; + assert_eq!( + resolve_language(&files, Some("english")).language, + "english" + ); + } + + /// TE11: Spanish gets a Hunspell `.dic`+`.aff` pair (not a plain + /// wordlist, and not the "no source" state TE10 left it in), for both + /// spellings of the language. + #[test] + fn remote_for_language_returns_hunspell_pair_for_spanish() { + assert!(matches!( + remote_for_language("spanish"), + Some(RemoteSource::Hunspell { .. }) + )); + assert!(matches!( + remote_for_language("es"), + Some(RemoteSource::Hunspell { .. }) + )); + } + + /// English keeps its single-URL wordlist source, unmigrated (decision 3). + #[test] + fn remote_for_language_returns_single_url_for_english() { + assert!(matches!( + remote_for_language("english"), + Some(RemoteSource::Wordlist(_)) + )); + } + + #[test] + fn skip_message_is_one_line_and_names_language_path_and_reason() { + let msg = skip_message( + "spanish", + Some(Path::new("/home/user/.texforge/dicts/spanish.txt")), + "network disabled during tests", + ); + assert_eq!(msg.lines().count(), 1, "must be exactly one line: {}", msg); + assert!(msg.contains("spanish"), "must name the language: {}", msg); + assert!( + msg.contains("/home/user/.texforge/dicts/spanish.txt"), + "must name the expected dictionary path: {}", + msg + ); + assert!( + msg.contains("network disabled during tests"), + "must carry the reason: {}", + msg + ); + } + + #[test] + fn using_message_is_one_line_and_names_language_and_path_for_wordlist() { + let msg = using_message( + "spanish", + &DictionaryLocation::Wordlist(PathBuf::from("/home/user/.texforge/dicts/spanish.txt")), + ); + assert_eq!(msg.lines().count(), 1, "must be exactly one line: {}", msg); + assert!(msg.contains("spanish")); + assert!(msg.contains("/home/user/.texforge/dicts/spanish.txt")); + } + + /// Requirement 7: `using_message` names the language and what it is + /// checking against for the Hunspell backend too — both files, not just + /// one, since the pair together is what "checking against" means here. + #[test] + fn using_message_names_both_files_for_hunspell() { + let msg = using_message( + "spanish", + &DictionaryLocation::Hunspell { + dic: PathBuf::from("/home/user/.texforge/dicts/spanish.dic"), + aff: PathBuf::from("/home/user/.texforge/dicts/spanish.aff"), + }, + ); + assert_eq!(msg.lines().count(), 1, "must be exactly one line: {}", msg); + assert!(msg.contains("spanish")); + assert!(msg.contains("/home/user/.texforge/dicts/spanish.dic")); + assert!(msg.contains("/home/user/.texforge/dicts/spanish.aff")); + } + + /// Reproduces the reported defect end-to-end: a Spanish document, no + /// configured default language, and only an English dictionary present. + /// Must emit ZERO `Unknown word` findings — not 214 false positives from + /// checking Spanish prose against English words. + #[test] + fn spanish_document_with_only_english_dictionary_emits_no_unknown_word_warnings() { + let home = TempDir::new().unwrap(); + fs::create_dir_all(home.path().join(".texforge").join("dicts")).unwrap(); + fs::write( + home.path() + .join(".texforge") + .join("dicts") + .join("english.txt"), + "hello\nworld\n", + ) + .unwrap(); + + let orig_home = std::env::var("HOME").ok(); + std::env::set_var("HOME", home.path()); + // Force the offline bail deterministically (see + // ensure_dictionary_bails_in_test_harness_environment) instead of + // depending on real network access being unavailable. + std::env::set_var("NEXTEST_RUN_ID", "te6-spanish-only-english-dict"); + + let src = "\\usepackage[spanish]{babel}\n\\begin{document}\n\ + mentoría ahí universidad liderazgo soluciones\n\\end{document}"; + let files = vec![("main.tex".to_string(), src.to_string())]; + let project_root = TempDir::new().unwrap(); + let findings = lint_files(&files, project_root.path(), None); + + std::env::remove_var("NEXTEST_RUN_ID"); + match orig_home { + Some(v) => std::env::set_var("HOME", v), + None => std::env::remove_var("HOME"), + } + + let findings = findings.unwrap(); + assert!( + findings.is_empty(), + "expected zero findings when the resolved language's dictionary is missing, got: {:?}", + findings + ); + } + + /// When a dictionary for the resolved language IS present, spell-check + /// must run against THAT dictionary, never a different language's — + /// proven by a Spanish word passing and an English-only word failing. + #[test] + fn spanish_document_checks_against_spanish_dictionary_not_english() { + let home = TempDir::new().unwrap(); + let dicts_dir = home.path().join(".texforge").join("dicts"); + fs::create_dir_all(&dicts_dir).unwrap(); + fs::write(dicts_dir.join("spanish.txt"), "mentoría\nahí\n").unwrap(); + fs::write(dicts_dir.join("english.txt"), "hello\nworld\n").unwrap(); + + let orig_home = std::env::var("HOME").ok(); + std::env::set_var("HOME", home.path()); + + let src = "\\usepackage[spanish]{babel}\n\\begin{document}\n\ + mentoría hello\n\\end{document}"; + let files = vec![("main.tex".to_string(), src.to_string())]; + let project_root = TempDir::new().unwrap(); + let findings = lint_files(&files, project_root.path(), None); + + match orig_home { + Some(v) => std::env::set_var("HOME", v), + None => std::env::remove_var("HOME"), + } + + let findings = findings.unwrap(); + assert!( + !findings.iter().any(|f| f.message.contains("mentoría")), + "'mentoría' is in the spanish dictionary and must not be flagged: {:?}", + findings + ); + assert!( + findings.iter().any(|f| f.message.contains("hello")), + "'hello' is English-only and must be flagged when checking against spanish, \ + proving no fallback to the english dictionary occurred: {:?}", + findings + ); + } + + // --- TE10: a global default must not silently override the document's + // own language declaration --- + + /// The disagreement warning must be a `Warning`, name both languages, and + /// point at the line of the `\usepackage` that declared the language + /// which won. Also proves the skip path still runs (zero `Unknown word` + /// findings) so the disagreement warning is the only finding produced. + #[test] + fn disagreement_warning_names_both_languages_and_points_at_declaration() { + let home = TempDir::new().unwrap(); + fs::create_dir_all(home.path().join(".texforge").join("dicts")).unwrap(); + fs::write( + home.path() + .join(".texforge") + .join("dicts") + .join("english.txt"), + "hello\nworld\n", + ) + .unwrap(); + + let orig_home = std::env::var("HOME").ok(); + std::env::set_var("HOME", home.path()); + std::env::set_var("NEXTEST_RUN_ID", "te10-disagreement-warning"); + + let src = "\\usepackage[spanish]{babel}\n\\begin{document}\nHola\n\\end{document}"; + let files = vec![("main.tex".to_string(), src.to_string())]; + let project_root = TempDir::new().unwrap(); + let findings = lint_files(&files, project_root.path(), Some("english")); + + std::env::remove_var("NEXTEST_RUN_ID"); + match orig_home { + Some(v) => std::env::set_var("HOME", v), + None => std::env::remove_var("HOME"), + } + + let findings = findings.unwrap(); + assert_eq!( + findings.len(), + 1, + "expected exactly one finding (the disagreement warning; spell-check itself is \ + skipped since no spanish dictionary is obtainable): {:?}", + findings + ); + let warning = &findings[0]; + assert!(matches!(warning.severity, Severity::Warning)); + assert!( + warning.message.contains("spanish") && warning.message.contains("english"), + "message must name both languages: {}", + warning.message + ); + assert_eq!(warning.file, "main.tex"); + assert_eq!(warning.line, 1, "must point at the \\usepackage line"); + } + + #[test] + fn no_disagreement_warning_when_declared_matches_configured_default() { + let home = TempDir::new().unwrap(); + fs::create_dir_all(home.path().join(".texforge").join("dicts")).unwrap(); + + let orig_home = std::env::var("HOME").ok(); + std::env::set_var("HOME", home.path()); + std::env::set_var("NEXTEST_RUN_ID", "te10-no-disagreement-same-lang"); + + let src = "\\usepackage[spanish]{babel}\n\\begin{document}\nHola\n\\end{document}"; + let files = vec![("main.tex".to_string(), src.to_string())]; + let project_root = TempDir::new().unwrap(); + let findings = lint_files(&files, project_root.path(), Some("spanish")); + + std::env::remove_var("NEXTEST_RUN_ID"); + match orig_home { + Some(v) => std::env::set_var("HOME", v), + None => std::env::remove_var("HOME"), + } + + let findings = findings.unwrap(); + assert!( + findings.is_empty(), + "declared and configured language agree; expected no findings: {:?}", + findings + ); + } + + #[test] + fn no_disagreement_warning_without_babel_declaration() { + let home = TempDir::new().unwrap(); + fs::create_dir_all(home.path().join(".texforge").join("dicts")).unwrap(); + fs::write( + home.path() + .join(".texforge") + .join("dicts") + .join("english.txt"), + "hello\nworld\n", + ) + .unwrap(); + + let orig_home = std::env::var("HOME").ok(); + std::env::set_var("HOME", home.path()); + + let src = "\\documentclass{article}\n\\begin{document}\nhello world\n\\end{document}"; + let files = vec![("main.tex".to_string(), src.to_string())]; + let project_root = TempDir::new().unwrap(); + let findings = lint_files(&files, project_root.path(), Some("english")); + + match orig_home { + Some(v) => std::env::set_var("HOME", v), + None => std::env::remove_var("HOME"), + } + + let findings = findings.unwrap(); + assert!( + findings.is_empty(), + "no babel declaration means no disagreement is possible: {:?}", + findings + ); + } + + /// A multi-file project where two files declare the same language must + /// produce exactly one warning, not one per file. + #[test] + fn multi_file_project_with_matching_declarations_produces_one_warning() { + let home = TempDir::new().unwrap(); + fs::create_dir_all(home.path().join(".texforge").join("dicts")).unwrap(); + fs::write( + home.path() + .join(".texforge") + .join("dicts") + .join("english.txt"), + "hello\nworld\n", + ) + .unwrap(); + + let orig_home = std::env::var("HOME").ok(); + std::env::set_var("HOME", home.path()); + std::env::set_var("NEXTEST_RUN_ID", "te10-multi-file-one-warning"); + + let src_a = "\\usepackage[spanish]{babel}\n\\begin{document}\nHola\n\\end{document}"; + let src_b = "\\usepackage[spanish]{babel}\n\\begin{document}\nAdios\n\\end{document}"; + let files = vec![ + ("a.tex".to_string(), src_a.to_string()), + ("b.tex".to_string(), src_b.to_string()), + ]; + let project_root = TempDir::new().unwrap(); + let findings = lint_files(&files, project_root.path(), Some("english")); + + std::env::remove_var("NEXTEST_RUN_ID"); + match orig_home { + Some(v) => std::env::set_var("HOME", v), + None => std::env::remove_var("HOME"), + } + + let findings = findings.unwrap(); + let warnings: Vec<_> = findings + .iter() + .filter(|f| f.message.contains("Configured default language")) + .collect(); + assert_eq!( + warnings.len(), + 1, + "two files declaring the same language must produce one warning, not two: {:?}", + findings + ); + } + + // --- TE11: Hunspell backend via `spellbook`, hand-written fixture pair --- + + /// Absolute paths to the minimal, hand-written fixture pair committed + /// under `tests/fixtures/hunspell/`. Deliberately not a copy of a real + /// dictionary: a handful of stems and exactly one affix rule. + fn hunspell_fixture_paths() -> (PathBuf, PathBuf) { + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + ( + root.join("tests/fixtures/hunspell/mini.dic"), + root.join("tests/fixtures/hunspell/mini.aff"), + ) + } + + #[test] + fn hunspell_backend_accepts_a_stem() { + let (dic, aff) = hunspell_fixture_paths(); + let dict = load_dictionary(&DictionaryLocation::Hunspell { dic, aff }).unwrap(); + assert!( + dict.contains("gato"), + "'gato' is a bare stem in the fixture .dic" + ); + } + + /// The whole point of the change: a form that is NOT itself a line in + /// the fixture `.dic`, but IS generated by the fixture `.aff`'s suffix + /// rule (`sol/S` plus `SFX S 0 es .` yields "soles"), must be accepted. + /// A test that only checked stems would pass against the old + /// plain-wordlist backend too and would prove nothing about this change. + #[test] + fn hunspell_backend_accepts_affix_generated_form() { + let (dic, aff) = hunspell_fixture_paths(); + let dict = load_dictionary(&DictionaryLocation::Hunspell { dic, aff }).unwrap(); + assert!( + dict.contains("soles"), + "'soles' is generated from stem 'sol' by the SFX S rule; it is not present verbatim in mini.dic" + ); + } + + #[test] + fn hunspell_backend_rejects_word_not_in_stems_or_generated_forms() { + let (dic, aff) = hunspell_fixture_paths(); + let dict = load_dictionary(&DictionaryLocation::Hunspell { dic, aff }).unwrap(); + assert!(!dict.contains("xylophone")); + } + + #[test] + fn wordlist_backend_still_accepts_and_rejects_exactly_as_before() { + let tmp = TempDir::new().unwrap(); + let path = tmp.path().join("english.txt"); + fs::write(&path, "hello\nworld\n").unwrap(); + let dict = load_dictionary(&DictionaryLocation::Wordlist(path)).unwrap(); + assert!(dict.contains("hello")); + assert!(dict.contains("world")); + assert!(!dict.contains("goodbye")); + } + + /// Decision 4: when both a `.txt` and a `.dic`/`.aff` exist on disk for + /// one language, `ensure_dictionary` must choose the Hunspell pair. + #[test] + fn ensure_dictionary_prefers_hunspell_pair_when_both_present() { + let home = TempDir::new().unwrap(); + let dicts_dir = home.path().join(".texforge").join("dicts"); + fs::create_dir_all(&dicts_dir).unwrap(); + fs::write(dicts_dir.join("spanish.txt"), "hola\n").unwrap(); + let (fixture_dic, fixture_aff) = hunspell_fixture_paths(); + fs::copy(&fixture_dic, dicts_dir.join("spanish.dic")).unwrap(); + fs::copy(&fixture_aff, dicts_dir.join("spanish.aff")).unwrap(); + + let orig_home = std::env::var("HOME").ok(); + std::env::set_var("HOME", home.path()); + let loc = ensure_dictionary("spanish"); + match orig_home { + Some(v) => std::env::set_var("HOME", v), + None => std::env::remove_var("HOME"), + } + + match loc.unwrap() { + DictionaryLocation::Hunspell { .. } => {} + DictionaryLocation::Wordlist(p) => panic!( + "expected the Hunspell pair to win over the wordlist, got wordlist path: {}", + p.display() + ), + } + } + + /// A `.dic` present with no `.aff` is not usable — it must be treated + /// the same as "no dictionary available" (falling through to the normal + /// missing-dictionary path and its skip message), never a panic. + #[test] + fn lint_files_treats_dic_without_aff_as_no_dictionary_available() { + let home = TempDir::new().unwrap(); + let dicts_dir = home.path().join(".texforge").join("dicts"); + fs::create_dir_all(&dicts_dir).unwrap(); + let (fixture_dic, _fixture_aff) = hunspell_fixture_paths(); + fs::copy(&fixture_dic, dicts_dir.join("spanish.dic")).unwrap(); + + let orig_home = std::env::var("HOME").ok(); + std::env::set_var("HOME", home.path()); + std::env::set_var("NEXTEST_RUN_ID", "te11-dic-without-aff"); + + let src = "\\usepackage[spanish]{babel}\n\\begin{document}\nHola\n\\end{document}"; + let files = vec![("main.tex".to_string(), src.to_string())]; + let project_root = TempDir::new().unwrap(); + let findings = lint_files(&files, project_root.path(), None); + + std::env::remove_var("NEXTEST_RUN_ID"); + match orig_home { + Some(v) => std::env::set_var("HOME", v), + None => std::env::remove_var("HOME"), + } + + let findings = findings.unwrap(); + assert!( + findings.is_empty(), + "a lone .dic with no .aff must be treated as no dictionary available, not crash or \ + check against it: {:?}", + findings + ); + } + + /// End-to-end: `lint_files` on a Spanish document with a Hunspell pair + /// installed checks against it (not a fallback wordlist), accepting both + /// a bare stem and an affix-generated form while still flagging a + /// genuine misspelling (requirement 10). + #[test] + fn spanish_document_checks_against_installed_hunspell_pair() { + let home = TempDir::new().unwrap(); + let dicts_dir = home.path().join(".texforge").join("dicts"); + fs::create_dir_all(&dicts_dir).unwrap(); + let (fixture_dic, fixture_aff) = hunspell_fixture_paths(); + fs::copy(&fixture_dic, dicts_dir.join("spanish.dic")).unwrap(); + fs::copy(&fixture_aff, dicts_dir.join("spanish.aff")).unwrap(); + + let orig_home = std::env::var("HOME").ok(); + std::env::set_var("HOME", home.path()); + + let src = "\\usepackage[spanish]{babel}\n\\begin{document}\n\ + sol soles perro xilofonoinventado\n\\end{document}"; + let files = vec![("main.tex".to_string(), src.to_string())]; + let project_root = TempDir::new().unwrap(); + let findings = lint_files(&files, project_root.path(), None); + + match orig_home { + Some(v) => std::env::set_var("HOME", v), + None => std::env::remove_var("HOME"), + } + + let findings = findings.unwrap(); + assert!( + !findings.iter().any(|f| f.message.contains("'sol'")), + "stem 'sol' must be accepted: {:?}", + findings + ); + assert!( + !findings.iter().any(|f| f.message.contains("'soles'")), + "affix-generated form 'soles' must be accepted: {:?}", + findings + ); + assert!( + findings + .iter() + .any(|f| f.message.contains("xilofonoinventado")), + "a genuine misspelling must still be flagged: {:?}", + findings + ); + } + + // --- TE12: ligature-workaround empty groups must not split words --- + + /// The four real tokens from the reported document must each be checked + /// as their joined form, not fragmented on the `{}` empty-group ligature + /// workaround. + #[test] + fn ligature_workaround_empty_groups_are_checked_as_joined_words() { + let home = TempDir::new().unwrap(); + let dicts_dir = home.path().join(".texforge").join("dicts"); + fs::create_dir_all(&dicts_dir).unwrap(); + fs::write( + dicts_dir.join("english.txt"), + "artificial\nworkflows\nmlflow\nlocal\nfirst\n", + ) + .unwrap(); + + let orig_home = std::env::var("HOME").ok(); + std::env::set_var("HOME", home.path()); + + let src = "\\begin{document}\nArtif{}icial workf{}lows MLf{}low local-f{}irst\n\ + \\end{document}"; + let files = vec![("main.tex".to_string(), src.to_string())]; + let project_root = TempDir::new().unwrap(); + let findings = lint_files(&files, project_root.path(), Some("english")); + + match orig_home { + Some(v) => std::env::set_var("HOME", v), + None => std::env::remove_var("HOME"), + } + + let findings = findings.unwrap(); + assert!( + findings.is_empty(), + "empty-group ligature workarounds must be checked as joined words, not fragments: {:?}", + findings + ); + } + + /// A genuine misspelling written with the empty-group idiom must still + /// be reported, exactly once, as the joined word — never as fragments, + /// since a fragment is not something the author can search for. + #[test] + fn misspelled_word_with_empty_group_is_reported_as_joined_word() { + let home = TempDir::new().unwrap(); + let dicts_dir = home.path().join(".texforge").join("dicts"); + fs::create_dir_all(&dicts_dir).unwrap(); + fs::write(dicts_dir.join("english.txt"), "hello\nworld\n").unwrap(); + + let orig_home = std::env::var("HOME").ok(); + std::env::set_var("HOME", home.path()); + + let src = "\\begin{document}\nHello Wrongwo{}rd world\n\\end{document}"; + let files = vec![("main.tex".to_string(), src.to_string())]; + let project_root = TempDir::new().unwrap(); + let findings = lint_files(&files, project_root.path(), Some("english")); + + match orig_home { + Some(v) => std::env::set_var("HOME", v), + None => std::env::remove_var("HOME"), + } + + let findings = findings.unwrap(); + assert_eq!( + findings.len(), + 1, + "expected exactly one finding for the joined misspelling: {:?}", + findings + ); + assert!( + findings[0].message.contains("wrongword"), + "must report the joined word: {:?}", + findings + ); + assert!( + !findings + .iter() + .any(|f| f.message.contains("wrongwo'") || f.message.contains("'rd")), + "must not report fragments of the joined word: {:?}", + findings + ); + } + + /// A `{}` at the start or end of a word, and two empty groups inside one + /// word, must all be stripped correctly rather than merely the common + /// mid-word case. + #[test] + fn empty_group_at_start_end_and_doubled_behave_sanely() { + let home = TempDir::new().unwrap(); + let dicts_dir = home.path().join(".texforge").join("dicts"); + fs::create_dir_all(&dicts_dir).unwrap(); + fs::write(dicts_dir.join("english.txt"), "begin\nend\nmiddlepoint\n").unwrap(); + + let orig_home = std::env::var("HOME").ok(); + std::env::set_var("HOME", home.path()); + + let src = "\\begin{document}\n{}Begin End{} Middle{}Po{}int\n\\end{document}"; + let files = vec![("main.tex".to_string(), src.to_string())]; + let project_root = TempDir::new().unwrap(); + let findings = lint_files(&files, project_root.path(), Some("english")); + + match orig_home { + Some(v) => std::env::set_var("HOME", v), + None => std::env::remove_var("HOME"), + } + + let findings = findings.unwrap(); + assert!( + findings.is_empty(), + "leading/trailing/doubled empty groups must all be stripped: {:?}", + findings + ); + } + + // --- TE13: global personal dictionary unions with the project whitelist --- + + /// `global_whitelist_path` must be `~/.texforge/spell-words` — the exact + /// path a user already tried before this feature existed (decision 2). + #[test] + fn global_whitelist_path_is_home_texforge_spell_words() { + let home = TempDir::new().unwrap(); + let orig_home = std::env::var("HOME").ok(); + std::env::set_var("HOME", home.path()); + + let path = global_whitelist_path().unwrap(); + + match orig_home { + Some(v) => std::env::set_var("HOME", v), + None => std::env::remove_var("HOME"), + } + + assert_eq!(path, home.path().join(".texforge").join("spell-words")); + } + + #[test] + fn parse_whitelist_words_skips_blank_and_comment_lines_and_lowercases() { + let content = "Docker\n# a comment\n\nAcme\n"; + let words = parse_whitelist_words(content); + assert_eq!(words.len(), 2); + assert!(words.contains("docker")); + assert!(words.contains("acme")); + } + + /// A word present only in the global personal dictionary must be + /// accepted in a project that has no whitelist file at all + /// (requirement 6). + #[test] + fn a_global_only_word_is_accepted_in_a_project_with_no_whitelist_file() { + let home = TempDir::new().unwrap(); + fs::create_dir_all(home.path().join(".texforge").join("dicts")).unwrap(); + fs::write( + home.path() + .join(".texforge") + .join("dicts") + .join("english.txt"), + "hello\nworld\n", + ) + .unwrap(); + fs::write( + home.path().join(".texforge").join("spell-words"), + "docker\n", + ) + .unwrap(); + + let orig_home = std::env::var("HOME").ok(); + std::env::set_var("HOME", home.path()); + + let src = "\\begin{document}\nHello docker world\n\\end{document}"; + let files = vec![("main.tex".to_string(), src.to_string())]; + // No whitelist file at all under the project root. + let project_root = TempDir::new().unwrap(); + let findings = lint_files(&files, project_root.path(), Some("english")); + + match orig_home { + Some(v) => std::env::set_var("HOME", v), + None => std::env::remove_var("HOME"), + } + + let findings = findings.unwrap(); + assert!( + findings.is_empty(), + "'docker' is in the global personal dictionary and must be accepted: {:?}", + findings + ); + } + + /// A missing (or unreadable) global personal dictionary must not fail + /// `check`, and must not change which findings are produced — reading it + /// is best-effort, same as the project-local files. + #[test] + fn missing_global_whitelist_yields_no_error_and_no_findings_change() { + let home = TempDir::new().unwrap(); + fs::create_dir_all(home.path().join(".texforge").join("dicts")).unwrap(); + fs::write( + home.path() + .join(".texforge") + .join("dicts") + .join("english.txt"), + "hello\nworld\n", + ) + .unwrap(); + // Deliberately do NOT create ~/.texforge/spell-words. + + let orig_home = std::env::var("HOME").ok(); + std::env::set_var("HOME", home.path()); + + let src = "\\begin{document}\nHello docker world\n\\end{document}"; + let files = vec![("main.tex".to_string(), src.to_string())]; + let project_root = TempDir::new().unwrap(); + let findings = lint_files(&files, project_root.path(), Some("english")); + + match orig_home { + Some(v) => std::env::set_var("HOME", v), + None => std::env::remove_var("HOME"), + } + + let findings = findings.unwrap(); + assert_eq!( + findings.len(), + 1, + "no global dictionary present: 'docker' should still be flagged, and lint_files \ + must not error: {:?}", + findings + ); + assert!(findings[0].message.contains("docker")); + } + + /// Both scopes union rather than either shadowing the other: a word only + /// in the project file, and a different word only in the global file, + /// are both accepted together (decision 3). + #[test] + fn project_and_global_whitelists_union_rather_than_override() { + let home = TempDir::new().unwrap(); + fs::create_dir_all(home.path().join(".texforge").join("dicts")).unwrap(); + fs::write( + home.path() + .join(".texforge") + .join("dicts") + .join("english.txt"), + "hello\nworld\n", + ) + .unwrap(); + fs::write(home.path().join(".texforge").join("spell-words"), "acme\n").unwrap(); + + let orig_home = std::env::var("HOME").ok(); + std::env::set_var("HOME", home.path()); + + let project_root = TempDir::new().unwrap(); + fs::write(project_root.path().join("spell-whitelist.txt"), "docker\n").unwrap(); + + let src = "\\begin{document}\nHello docker acme world\n\\end{document}"; + let files = vec![("main.tex".to_string(), src.to_string())]; + let findings = lint_files(&files, project_root.path(), Some("english")); + + match orig_home { + Some(v) => std::env::set_var("HOME", v), + None => std::env::remove_var("HOME"), + } + + let findings = findings.unwrap(); + assert!( + findings.is_empty(), + "words from both the project and global lists must be accepted together: {:?}", + findings + ); + } + + /// The `Unknown word` finding's suggestion must point at the new command, + /// not at hand-editing files, and must now name `--local` rather than + /// `--global` since global became the default (requirement 9). + #[test] + fn unknown_word_suggestion_names_spell_add_and_local_flag() { + let home = TempDir::new().unwrap(); + fs::create_dir_all(home.path().join(".texforge").join("dicts")).unwrap(); + fs::write( + home.path() + .join(".texforge") + .join("dicts") + .join("english.txt"), + "hello\nworld\n", + ) + .unwrap(); + + let orig_home = std::env::var("HOME").ok(); + std::env::set_var("HOME", home.path()); + + let src = "\\begin{document}\nHello zzzznotaword world\n\\end{document}"; + let files = vec![("main.tex".to_string(), src.to_string())]; + let project_root = TempDir::new().unwrap(); + let findings = lint_files(&files, project_root.path(), Some("english")); + + match orig_home { + Some(v) => std::env::set_var("HOME", v), + None => std::env::remove_var("HOME"), + } + + let findings = findings.unwrap(); + assert_eq!(findings.len(), 1); + let suggestion = findings[0].suggestion.as_deref().unwrap_or(""); + assert!( + suggestion.contains("texforge spell add"), + "suggestion must name the new command: {}", + suggestion + ); + assert!( + suggestion.contains("--local"), + "suggestion must mention --local, since global is now the default: {}", + suggestion + ); + assert!( + !suggestion.contains("--global"), + "suggestion should not point at --global now that it is the default: {}", + suggestion + ); + } +} diff --git a/src/main.rs b/src/main.rs index 8f4bf9b..55dc0f6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,11 +12,16 @@ mod domain; mod formatter; mod linter; mod manifest; +mod pdftext; mod placeholders; +mod raster; mod templates; +mod texparse; +mod texutil; mod utils; mod version; mod version_checker; +mod wordcount; use anyhow::Result; use clap::Parser; diff --git a/src/pdftext.rs b/src/pdftext.rs new file mode 100644 index 0000000..d3a75b9 --- /dev/null +++ b/src/pdftext.rs @@ -0,0 +1,1543 @@ +//! PDF text extraction and source-to-PDF fidelity (TF8). +//! +//! Extracts text the way a reader or ATS sees it, normalizes typographic +//! ligatures and hyphenated line breaks for comparison, and checks that every +//! significant source word (from the TF3 tokenizer) still appears in the PDF. +//! Missing words are reported as warnings; the source is never auto-edited. +//! +//! Also verifies font embedding and Info-dictionary date shape (TF15). + +use std::collections::BTreeMap; +use std::path::Path; + +use anyhow::{Context, Result}; +use lopdf::{Dictionary, Document, Object, ObjectId}; + +use crate::linter::{LintFinding, Severity}; +use crate::texparse::{tokenize, tokenize_document, Token, TokenizedFile}; +use crate::texutil::strip_empty_groups; + +/// PDF Info date shape required by ISO 32000 (`D:YYYYMMDDHHmmSS` plus optional TZ). +pub const PDF_DATE_EXPECTED: &str = "D:YYYYMMDDHHmmSS"; + +/// Common alphabetic ligature codepoints → their ASCII letter expansions. +const LIGATURES: &[(char, &str)] = &[ + ('\u{FB00}', "ff"), // ff + ('\u{FB01}', "fi"), // fi + ('\u{FB02}', "fl"), // fl + ('\u{FB03}', "ffi"), // ffi + ('\u{FB04}', "ffl"), // ffl + ('\u{FB05}', "st"), // ſt + ('\u{FB06}', "st"), // st +]; + +/// One font used by the PDF. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PdfFontInfo { + /// PostScript / `BaseFont` name. + pub name: String, + /// `/Subtype` (Type1, TrueType, Type0, …). + pub subtype: String, + /// Whether a `FontFile` / `FontFile2` / `FontFile3` stream is present. + pub embedded: bool, + /// 1-based page numbers that reference this font (sorted, unique). + pub pages: Vec<usize>, +} + +/// Document-level metadata from the Info dictionary. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct PdfMetadata { + pub title: Option<String>, + pub author: Option<String>, + pub subject: Option<String>, + pub keywords: Option<String>, + pub creator: Option<String>, + pub producer: Option<String>, + pub creation_date: Option<String>, + pub mod_date: Option<String>, +} + +/// Summary returned by [`pdf_info`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PdfInfo { + pub pages: usize, + pub fonts: Vec<PdfFontInfo>, + pub metadata: PdfMetadata, +} + +/// One page in the machine-readable pages report. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PdfPageBreak { + /// 1-based page number. + pub page: usize, + /// Dotted section number that opens this page, when known. + pub section: Option<String>, + /// Section title that opens this page, when known. + pub title: Option<String>, +} + +/// A distinct source word missing from the extracted PDF text. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MissingWord { + pub word: String, + pub count: usize, +} + +/// Extract raw PDF text (ligatures left as `ToUnicode` mapped them). +pub fn extract_text(path: &Path) -> Result<String> { + pdf_extract::extract_text(path) + .map_err(|e| anyhow::anyhow!("failed to extract text from {}: {e}", path.display())) +} + +/// Extract raw PDF text from bytes. +#[allow(dead_code)] +pub fn extract_text_from_bytes(data: &[u8]) -> Result<String> { + pdf_extract::extract_text_from_mem(data) + .map_err(|e| anyhow::anyhow!("failed to extract text from PDF bytes: {e}")) +} + +/// Extract raw text one page at a time (1-based order). +pub fn extract_text_by_pages(path: &Path) -> Result<Vec<String>> { + pdf_extract::extract_text_by_pages(path).map_err(|e| { + anyhow::anyhow!( + "failed to extract per-page text from {}: {e}", + path.display() + ) + }) +} + +/// Extract raw text one page at a time from bytes. +/// +/// Used by the fixture tests and by callers that already hold the PDF in memory. +#[allow(dead_code)] +pub fn extract_text_by_pages_from_bytes(data: &[u8]) -> Result<Vec<String>> { + pdf_extract::extract_text_from_mem_by_pages(data) + .map_err(|e| anyhow::anyhow!("failed to extract per-page text from PDF bytes: {e}")) +} + +/// Map common ligature codepoints back to letters and rejoin hyphenated +/// line-breaks (`Deep Learn-\ning` → `Deep Learning`). +pub fn normalize_pdf_text(raw: &str) -> String { + let expanded = expand_ligatures(raw); + rejoin_hyphenated_linebreaks(&expanded) +} + +/// Expand U+FB00..U+FB06 ligatures to their ASCII letter sequences. +pub fn expand_ligatures(text: &str) -> String { + let mut out = String::with_capacity(text.len()); + for c in text.chars() { + if let Some((_, repl)) = LIGATURES.iter().find(|(lig, _)| *lig == c) { + out.push_str(repl); + } else { + out.push(c); + } + } + out +} + +/// Rejoin words split by a hyphen at a line break. +/// +/// Matches `letter - newline letter` (with optional CR) and also strips soft +/// hyphens (U+00AD). +pub fn rejoin_hyphenated_linebreaks(text: &str) -> String { + let without_soft = text.replace('\u{00AD}', ""); + let chars: Vec<char> = without_soft.chars().collect(); + let mut out = String::with_capacity(without_soft.len()); + let mut i = 0; + while i < chars.len() { + if chars[i] == '-' && i > 0 && chars[i - 1].is_alphabetic() && i + 1 < chars.len() { + let mut j = i + 1; + while j < chars.len() && (chars[j] == '\n' || chars[j] == '\r') { + j += 1; + } + if j < chars.len() && chars[j].is_alphabetic() && j > i + 1 { + // Skip the hyphen and the line break(s); keep the next letter. + i += 1; + while i < chars.len() && (chars[i] == '\n' || chars[i] == '\r') { + i += 1; + } + continue; + } + } + out.push(chars[i]); + i += 1; + } + out +} + +/// Significant prose words from a tokenized document (TF3), excluding labels, +/// refs and math — the tokenizer already dropped those from [`Token::Text`]. +pub fn significant_words(files: &[TokenizedFile]) -> BTreeMap<String, usize> { + let mut counts: BTreeMap<String, usize> = BTreeMap::new(); + let mut in_document = false; + + for file in files { + for token in &file.tokens { + match token { + Token::BeginDocument => in_document = true, + Token::EndDocument => in_document = false, + Token::Section { title, .. } if in_document => { + // Re-tokenize so math/commands inside the title are excluded. + for t in tokenize(title) { + if let Token::Text(text) = t { + for word in words_in_text(&text) { + *counts.entry(word).or_insert(0) += 1; + } + } + } + } + Token::Text(text) if in_document => { + for word in words_in_text(text) { + *counts.entry(word).or_insert(0) += 1; + } + } + _ => {} + } + } + } + counts +} + +/// Words from a prose run: non-space spans with ≥1 alphabetic character, +/// with leading/trailing punctuation stripped and empty groups removed for +/// PDF matching. +fn words_in_text(text: &str) -> impl Iterator<Item = String> + '_ { + text.split_whitespace().filter_map(|raw| { + let trimmed = strip_empty_groups(trim_punct(raw)); + if trimmed.chars().any(char::is_alphabetic) { + Some(trimmed) + } else { + None + } + }) +} + +fn trim_punct(word: &str) -> &str { + word.trim_matches(|c: char| !c.is_alphanumeric() && c != '-' && c != '\'') +} + +/// Compare significant source words against normalized PDF text. +/// +/// Returns the distinct missing words with how often they appear in the source. +pub fn fidelity_missing_words( + source_words: &BTreeMap<String, usize>, + pdf_text_normalized: &str, +) -> Vec<MissingWord> { + // Build a searchable haystack of PDF words (normalized, punct-trimmed). + let pdf_words: std::collections::HashSet<String> = pdf_text_normalized + .split_whitespace() + .filter_map(|w| { + let t = trim_punct(w); + if t.is_empty() { + None + } else { + Some(t.to_string()) + } + }) + .collect(); + + // Also keep the full text for substring fallback (handles hyphenated + // compounds already rejoined into a single token). + let mut missing = Vec::new(); + for (word, count) in source_words { + if pdf_words.contains(word) || pdf_text_normalized.contains(word.as_str()) { + continue; + } + missing.push(MissingWord { + word: word.clone(), + count: *count, + }); + } + missing +} + +/// Turn missing words into Warning findings. Suggestion breaks the first +/// ligature pair with an empty group (`Artif{}icial`) — the portable fix on +/// the Tectonic stack. +pub fn fidelity_findings(missing: &[MissingWord]) -> Vec<LintFinding> { + missing + .iter() + .map(|m| { + let suggestion = ligature_break_suggestion(&m.word); + let message = if m.count == 1 { + format!( + "source word `{}` not found in PDF text (ligature, hyphenation, or encoding)", + m.word + ) + } else { + format!( + "source word `{}` not found in PDF text ({} occurrences in source; ligature, hyphenation, or encoding)", + m.word, m.count + ) + }; + LintFinding { + file: "pdf".into(), + line: 0, + severity: Severity::Warning, + message, + suggestion, + } + }) + .collect() +} + +/// Suggest breaking the first `fi`/`fl`/`ff`/`ffi`/`ffl` pair with `{}`. +fn ligature_break_suggestion(word: &str) -> Option<String> { + // Longest pairs first so `ffi` wins over `fi`. + const PAIRS: &[&str] = &["ffi", "ffl", "ff", "fi", "fl"]; + let lower = word.to_ascii_lowercase(); + for pair in PAIRS { + if let Some(idx) = lower.find(pair) { + // Split after the first letter of the pair: `Artif{}icial`. + let split_at = idx + 1; + let (before, after) = word.split_at(split_at); + return Some(format!( + "Break the ligature in the source with an empty group: `{before}{{}}{after}` \ + (microtype/\\DisableLigatures and fontspec Ligatures=NoCommon do not work under Tectonic)" + )); + } + } + Some( + "Ensure the word survives compilation; if a ligature is involved, break it with \ + an empty group (e.g. `Artif{}icial`)" + .into(), + ) +} + +/// Run the fidelity check for a project: tokenize source, extract+normalize +/// PDF text, report distinct missing words as warnings. +pub fn check_fidelity(root: &Path, entry: &str, pdf_path: &Path) -> Result<Vec<LintFinding>> { + let files = tokenize_document(root, entry); + let source_words = significant_words(&files); + let raw = extract_text(pdf_path)?; + let normalized = normalize_pdf_text(&raw); + let missing = fidelity_missing_words(&source_words, &normalized); + Ok(fidelity_findings(&missing)) +} + +/// Font embedding + Info date checks (TF15) over an already-parsed [`PdfInfo`]. +/// +/// Silent when every referenced font is embedded and present dates match +/// [`PDF_DATE_EXPECTED`]. Findings are always [`Severity::Warning`]. +pub fn quality_findings(info: &PdfInfo) -> Vec<LintFinding> { + let mut findings = Vec::new(); + findings.extend(font_embedding_findings(&info.fonts)); + findings.extend(metadata_date_findings(&info.metadata)); + findings +} + +/// Run TF15 quality checks on a PDF path. +pub fn check_quality(pdf_path: &Path) -> Result<Vec<LintFinding>> { + let info = pdf_info(pdf_path)?; + Ok(quality_findings(&info)) +} + +/// Warn for every font referenced by the PDF that is not embedded. +fn font_embedding_findings(fonts: &[PdfFontInfo]) -> Vec<LintFinding> { + fonts + .iter() + // Only page-referenced fonts (Type0/simple faces in Resources). + // Descendant CID entries have empty `pages` and are covered by the parent. + .filter(|f| !f.embedded && !f.pages.is_empty()) + .map(|f| { + let pages = format_page_list(&f.pages); + LintFinding { + file: "pdf".into(), + line: 0, + severity: Severity::Warning, + message: format!( + "font `{}` is referenced but not embedded (pages: {})", + f.name, pages + ), + suggestion: Some( + "Embed the font at compile time so the PDF travels; viewers on other \ + machines may substitute a different face" + .into(), + ), + } + }) + .collect() +} + +fn format_page_list(pages: &[usize]) -> String { + if pages.is_empty() { + return "unknown".into(); + } + pages + .iter() + .map(|p| p.to_string()) + .collect::<Vec<_>>() + .join(", ") +} + +/// Warn when `/CreationDate` or `/ModDate` is present but not PDF-shaped. +fn metadata_date_findings(meta: &PdfMetadata) -> Vec<LintFinding> { + let mut findings = Vec::new(); + for (field, value) in [ + ("CreationDate", meta.creation_date.as_deref()), + ("ModDate", meta.mod_date.as_deref()), + ] { + let Some(observed) = value else { + continue; + }; + if is_valid_pdf_date(observed) { + continue; + } + findings.push(LintFinding { + file: "pdf".into(), + line: 0, + severity: Severity::Warning, + message: format!( + "/{field} value `{observed}` is not a valid PDF date (expected {PDF_DATE_EXPECTED})" + ), + suggestion: Some(format!( + "Set pdf{field} to a PDF date string like `{PDF_DATE_EXPECTED}` \ + (e.g. via hyperref); avoid `\\today` which expands to a human date" + )), + }); + } + findings +} + +/// True when `value` matches `D:YYYYMMDDHHmmSS` with an optional timezone suffix. +/// +/// Accepts trailing `Z` / `z` or `±HH'mm'` as allowed by ISO 32000. +pub fn is_valid_pdf_date(value: &str) -> bool { + let bytes = value.as_bytes(); + if bytes.len() < 16 { + return false; + } + if &bytes[..2] != b"D:" { + return false; + } + if !bytes[2..16].iter().all(u8::is_ascii_digit) { + return false; + } + match &bytes[16..] { + [] | [b'Z'] | [b'z'] => true, + [b'+' | b'-', h1, h2, b'\'', m1, m2, b'\''] + if h1.is_ascii_digit() + && h2.is_ascii_digit() + && m1.is_ascii_digit() + && m2.is_ascii_digit() => + { + true + } + _ => false, + } +} + +/// Pages, fonts (with embedding), and Info metadata. +pub fn pdf_info(path: &Path) -> Result<PdfInfo> { + let doc = + Document::load(path).with_context(|| format!("failed to open PDF {}", path.display()))?; + Ok(info_from_document(&doc)) +} + +/// Pages, fonts, and metadata from an already-loaded document / bytes. +#[allow(dead_code)] +pub fn pdf_info_from_bytes(data: &[u8]) -> Result<PdfInfo> { + let doc = Document::load_mem(data).context("failed to parse PDF bytes")?; + Ok(info_from_document(&doc)) +} + +fn info_from_document(doc: &Document) -> PdfInfo { + let pages = doc.get_pages().len(); + let fonts = collect_fonts(doc); + let metadata = read_metadata(doc); + PdfInfo { + pages, + fonts, + metadata, + } +} + +fn read_metadata(doc: &Document) -> PdfMetadata { + let Ok(info_obj) = doc.trailer.get(b"Info") else { + return PdfMetadata::default(); + }; + let dict = match info_obj { + Object::Reference(id) => match doc.get_object(*id) { + Ok(Object::Dictionary(d)) => d, + _ => return PdfMetadata::default(), + }, + Object::Dictionary(d) => d, + _ => return PdfMetadata::default(), + }; + PdfMetadata { + title: dict_text(dict, b"Title"), + author: dict_text(dict, b"Author"), + subject: dict_text(dict, b"Subject"), + keywords: dict_text(dict, b"Keywords"), + creator: dict_text(dict, b"Creator"), + producer: dict_text(dict, b"Producer"), + creation_date: dict_text(dict, b"CreationDate"), + mod_date: dict_text(dict, b"ModDate"), + } +} + +fn dict_text(dict: &Dictionary, key: &[u8]) -> Option<String> { + let obj = dict.get(key).ok()?; + object_to_string(obj) +} + +fn object_to_string(obj: &Object) -> Option<String> { + match obj { + Object::String(bytes, _) => Some(decode_pdf_string(bytes)), + Object::Name(n) => Some(String::from_utf8_lossy(n).into_owned()), + _ => None, + } +} + +fn decode_pdf_string(bytes: &[u8]) -> String { + // UTF-16BE with BOM + if bytes.starts_with(&[0xFE, 0xFF]) && bytes.len() >= 2 { + let u16s: Vec<u16> = bytes[2..] + .chunks(2) + .filter_map(|c| { + if c.len() == 2 { + Some(u16::from_be_bytes([c[0], c[1]])) + } else { + None + } + }) + .collect(); + return String::from_utf16_lossy(&u16s); + } + String::from_utf8_lossy(bytes).into_owned() +} + +fn collect_fonts(doc: &Document) -> Vec<PdfFontInfo> { + let mut fonts: BTreeMap<String, PdfFontInfo> = BTreeMap::new(); + for (page_num, page_id) in doc.get_pages() { + collect_fonts_from_page(doc, page_num as usize, page_id, &mut fonts); + } + fonts.into_values().collect() +} + +fn collect_fonts_from_page( + doc: &Document, + page_num: usize, + page_id: ObjectId, + fonts: &mut BTreeMap<String, PdfFontInfo>, +) { + let Ok(Object::Dictionary(page)) = doc.get_object(page_id) else { + return; + }; + let Some(resources) = resolve_dict(doc, page.get(b"Resources").ok()) else { + return; + }; + let Some(font_dict) = resolve_dict(doc, resources.get(b"Font").ok()) else { + return; + }; + for (_, obj) in font_dict.iter() { + let font_obj = match obj { + Object::Reference(id) => doc.get_object(*id).ok(), + other => Some(other), + }; + let Some(Object::Dictionary(font)) = font_obj else { + continue; + }; + push_font(doc, font, Some(page_num), fonts); + } +} + +fn push_font( + doc: &Document, + font: &Dictionary, + page_num: Option<usize>, + fonts: &mut BTreeMap<String, PdfFontInfo>, +) { + let subtype = font + .get(b"Subtype") + .ok() + .and_then(object_to_string) + .unwrap_or_else(|| "Unknown".into()); + + // Type0: report the parent BaseFont; embedding lives on the descendant. + let name = font + .get(b"BaseFont") + .ok() + .and_then(object_to_string) + .unwrap_or_else(|| "(unnamed)".into()); + + let embedded = font_is_embedded(doc, font); + fonts + .entry(name.clone()) + .and_modify(|f| { + if embedded { + f.embedded = true; + } + if let Some(p) = page_num { + if !f.pages.contains(&p) { + f.pages.push(p); + f.pages.sort_unstable(); + } + } + }) + .or_insert_with(|| { + let pages = match page_num { + Some(p) => vec![p], + None => Vec::new(), + }; + PdfFontInfo { + name, + subtype, + embedded, + pages, + } + }); + + // Also walk DescendantFonts for CID fonts (no page attribution — the + // parent Type0 already recorded the pages that reference the face). + if let Ok(obj) = font.get(b"DescendantFonts") { + let arr = match obj { + Object::Array(a) => Some(a.as_slice()), + Object::Reference(id) => match doc.get_object(*id) { + Ok(Object::Array(a)) => Some(a.as_slice()), + _ => None, + }, + _ => None, + }; + if let Some(items) = arr { + for item in items { + let desc = match item { + Object::Reference(id) => doc.get_object(*id).ok(), + other => Some(other), + }; + if let Some(Object::Dictionary(d)) = desc { + push_font(doc, d, None, fonts); + } + } + } + } +} + +fn font_is_embedded(doc: &Document, font: &Dictionary) -> bool { + if let Some(desc) = resolve_dict(doc, font.get(b"FontDescriptor").ok()) { + for key in [b"FontFile".as_slice(), b"FontFile2", b"FontFile3"] { + if desc.get(key).is_ok() { + return true; + } + } + } + + // Type0 has no descriptor; embedding lives on DescendantFonts. + let Ok(obj) = font.get(b"DescendantFonts") else { + return false; + }; + let arr = match obj { + Object::Array(a) => Some(a.as_slice()), + Object::Reference(id) => match doc.get_object(*id) { + Ok(Object::Array(a)) => Some(a.as_slice()), + _ => None, + }, + _ => None, + }; + let Some(items) = arr else { + return false; + }; + items.iter().any(|item| { + let desc = match item { + Object::Reference(id) => doc.get_object(*id).ok(), + other => Some(other), + }; + match desc { + Some(Object::Dictionary(d)) => font_is_embedded(doc, d), + _ => false, + } + }) +} + +fn resolve_dict<'a>(doc: &'a Document, obj: Option<&'a Object>) -> Option<&'a Dictionary> { + match obj? { + Object::Dictionary(d) => Some(d), + Object::Reference(id) => match doc.get_object(*id).ok()? { + Object::Dictionary(d) => Some(d), + _ => None, + }, + _ => None, + } +} + +/// Report which section opens each page. +/// +/// `sections` is `(number, title)` in document order (e.g. from the outline). +/// A section title matches a page only when it appears as its own line in +/// that page's extracted text — either the bare title, or the title with a +/// leading numbering prefix (`"1 "`, `"2.4. "`, `"2.4.1 "`), since a numbered +/// `\section` renders that way (see `section_title_in_page`). A mention +/// inside a sentence does not count, and neither does a table-of-contents +/// entry, whose line carries dot leaders and a page number after the title +/// instead of ending there. For each section, in order, we search forward +/// from the page where the previous *matched* section was found for the +/// first page whose text contains this section's title as a line. +/// +/// A page is opened by the *first* matched heading that appears on it, in +/// document order — never the last. Whatever precedes that heading's line +/// (a diagram's rendered vector text spilling over the top of the page from +/// a floated figure, a caption, blank lines) is not itself a section, so it +/// cannot be what the page opens with instead: the heading is still the +/// first piece of section content the page reaches, however much non-section +/// filler sits above it. A page with no matched heading of its own carries +/// the previous page's section forward unchanged; pages before the first +/// match anywhere stay unattributed. +/// +/// Matching keys on page position, not on a strict in-order text-equality +/// chain: a title that fails to match anywhere (for example a heading whose +/// rendered PDF form diverges from its resolved source text — dot-leader +/// alignment, a substituted dash, ...) is skipped rather than permanently +/// blocking every section that follows it. Skipping a title does not move +/// the search position, so later sections still search from the last page +/// that *did* match. +pub fn page_breaks(page_texts: &[String], sections: &[(String, String)]) -> Vec<PdfPageBreak> { + let normalized_pages: Vec<String> = page_texts.iter().map(|p| normalize_pdf_text(p)).collect(); + + // (page index, number, title) for each section that could be located. + let mut matches: Vec<(usize, String, String)> = Vec::new(); + let mut search_from = 0usize; + for (num, title) in sections { + let found = normalized_pages + .iter() + .enumerate() + .skip(search_from) + .find(|(_, text)| section_title_in_page(text, title)) + .map(|(idx, _)| idx); + if let Some(page_idx) = found { + matches.push((page_idx, num.clone(), title.clone())); + search_from = page_idx; + } + } + + let mut out = Vec::with_capacity(page_texts.len()); + let mut current: Option<(String, String)> = None; + let mut match_idx = 0usize; + for i in 0..normalized_pages.len() { + // `matches` is grouped by page in document order, so the entry at + // `match_idx` — if it belongs to this page at all — is the first + // (not last) section matched on it. + if match_idx < matches.len() && matches[match_idx].0 == i { + let (_, num, title) = &matches[match_idx]; + current = Some((num.clone(), title.clone())); + while match_idx < matches.len() && matches[match_idx].0 == i { + match_idx += 1; + } + } + out.push(PdfPageBreak { + page: i + 1, + section: current.as_ref().map(|(n, _)| n.clone()), + title: current.as_ref().map(|(_, t)| t.clone()), + }); + } + out +} + +/// Strip a leading numbering prefix from `line`: one or more digit groups +/// separated by dots (`2`, `2.4`, `2.4.1`), optionally followed by a +/// trailing dot, then at least one whitespace character — the shape a +/// numbered `\section`'s auto-numbering renders as (`"2.4. Estilos..."`, +/// `"2.4.1 Something"`). Returns the remainder with leading whitespace +/// trimmed, or `None` if `line` does not start with such a prefix. +/// +/// A character walk, not a regex: the prefix grammar is small and fixed. +fn strip_numbering_prefix(line: &str) -> Option<&str> { + let chars: Vec<char> = line.chars().collect(); + let mut i = 0; + + if i >= chars.len() || !chars[i].is_ascii_digit() { + return None; + } + while i < chars.len() && chars[i].is_ascii_digit() { + i += 1; + } + while i < chars.len() && chars[i] == '.' { + if i + 1 < chars.len() && chars[i + 1].is_ascii_digit() { + i += 1; + while i < chars.len() && chars[i].is_ascii_digit() { + i += 1; + } + } else { + // Trailing dot with nothing numeric after it: consume it and + // stop — the next character must be whitespace. + i += 1; + break; + } + } + if i >= chars.len() || !chars[i].is_whitespace() { + return None; + } + let byte_offset: usize = chars[..i].iter().map(|c| c.len_utf8()).sum(); + Some(line[byte_offset..].trim_start()) +} + +/// True when a trimmed page line is the heading line for `title`: either the +/// bare title, or the title with a leading numbering prefix stripped (see +/// [`strip_numbering_prefix`]) — the two forms a `\section` and a numbered +/// `\section` render as in extracted PDF text. Equality is required after +/// stripping, so a table-of-contents line — title followed by dot leaders +/// and a page number — does not match, and neither does a mention inside a +/// sentence: neither form leaves the line ending exactly at the title. +fn line_is_section_heading(line: &str, needle: &str) -> bool { + if line == needle { + return true; + } + match strip_numbering_prefix(line) { + Some(rest) => rest == needle, + None => false, + } +} + +/// True when `title` appears as its own line (trimmed, normalized) anywhere +/// in `page_text`. A mention inside a sentence does not count. +fn section_title_in_page(page_text: &str, title: &str) -> bool { + let needle = normalize_pdf_text(title); + let needle = needle.trim(); + if needle.is_empty() { + return false; + } + page_text + .lines() + .any(|line| line_is_section_heading(line.trim(), needle)) +} + +/// Format page breaks for diff-friendly output: one line per page. +pub fn format_page_breaks(breaks: &[PdfPageBreak]) -> String { + let mut lines = Vec::with_capacity(breaks.len()); + for b in breaks { + match (&b.section, &b.title) { + (Some(num), Some(title)) => { + lines.push(format!("page={} section={} title={}", b.page, num, title)); + } + _ => lines.push(format!("page={} section= title=", b.page)), + } + } + lines.join("\n") +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::texparse::TokenizedFile; + use std::path::PathBuf; + + const LIGATURES_PDF: &[u8] = include_bytes!("../tests/fixtures/ligatures.pdf"); + const PAGES_PDF: &[u8] = include_bytes!("../tests/fixtures/pages-ligatures.pdf"); + const MALFORMED_DATE_PDF: &[u8] = include_bytes!("../tests/fixtures/malformed-date.pdf"); + + #[test] + fn expand_ligatures_maps_common_codepoints() { + assert_eq!(expand_ligatures("Arti\u{FB01}cial"), "Artificial"); + assert_eq!(expand_ligatures("ML\u{FB02}ow"), "MLflow"); + assert_eq!(expand_ligatures("work\u{FB02}ows"), "workflows"); + assert_eq!(expand_ligatures("local-\u{FB01}rst"), "local-first"); + assert_eq!(expand_ligatures("\u{FB00}\u{FB03}\u{FB04}"), "ffffiffl"); + } + + #[test] + fn rejoin_hyphenated_linebreaks_merges_split_words() { + assert_eq!( + rejoin_hyphenated_linebreaks("Deep Learn-\ning"), + "Deep Learning" + ); + assert_eq!( + rejoin_hyphenated_linebreaks("Deep Learn-\r\ning"), + "Deep Learning" + ); + // Real hyphen in a compound must stay. + assert_eq!(rejoin_hyphenated_linebreaks("local-first"), "local-first"); + } + + #[test] + fn normalize_handles_ligatures_and_hyphenation_together() { + let raw = "Arti\u{FB01}cial Deep Learn-\ning work\u{FB02}ows"; + assert_eq!( + normalize_pdf_text(raw), + "Artificial Deep Learning workflows" + ); + } + + #[test] + fn fixture_extracts_raw_ligature_codepoints() { + let raw = extract_text_from_bytes(LIGATURES_PDF).unwrap(); + assert!( + raw.contains('\u{FB01}') || raw.contains('\u{FB02}'), + "fixture must contain ligature codepoints; got {raw:?}" + ); + assert!(!raw.contains("Artificial"), "raw should keep fi, not fi"); + } + + #[test] + fn fixture_normalized_text_is_searchable() { + let raw = extract_text_from_bytes(LIGATURES_PDF).unwrap(); + let norm = normalize_pdf_text(&raw); + for word in [ + "Artificial", + "MLflow", + "workflows", + "local-first", + "Deep", + "Learning", + ] { + assert!( + norm.contains(word), + "normalized text missing {word}: {norm:?}" + ); + } + } + + #[test] + fn pages_fixture_has_hyphenation_and_sections() { + let pages = extract_text_by_pages_from_bytes(PAGES_PDF).unwrap(); + assert_eq!(pages.len(), 2); + assert!(pages[0].contains('\u{FB01}') || pages[0].contains("Learn-")); + let norm = normalize_pdf_text(&pages[0]); + assert!(norm.contains("Deep Learning"), "got {norm:?}"); + assert!(pages[0].contains("Introduction")); + assert!(pages[1].contains("Methods")); + } + + #[test] + fn page_breaks_are_machine_readable() { + let pages = extract_text_by_pages_from_bytes(PAGES_PDF).unwrap(); + let sections = vec![ + ("1".into(), "Introduction".into()), + ("2".into(), "Methods".into()), + ]; + let breaks = page_breaks(&pages, §ions); + let formatted = format_page_breaks(&breaks); + assert_eq!( + formatted, + "page=1 section=1 title=Introduction\npage=2 section=2 title=Methods" + ); + } + + #[test] + fn page_breaks_skip_a_permanently_unmatched_section_between_matches() { + // TE5: a heading between two matchable sections whose resolved title + // never appears verbatim in any page's extracted text (e.g. a résumé + // job-date line built with a dot leader, which renders as a row of + // dots in the PDF but collapses to plain text once resolved) must + // not permanently block later sections from being found. + let pages = extract_text_by_pages_from_bytes(PAGES_PDF).unwrap(); + let sections = vec![ + ("1".into(), "Introduction".into()), + ( + "1.1".into(), + "AI Engineer en Accenture Julio 2026 -- Actual".into(), + ), + ("2".into(), "Methods".into()), + ]; + let breaks = page_breaks(&pages, §ions); + let formatted = format_page_breaks(&breaks); + assert_eq!( + formatted, + "page=1 section=1 title=Introduction\npage=2 section=2 title=Methods" + ); + } + + #[test] + fn page_breaks_ignore_a_title_mentioned_inside_prose_te9() { + // TE9: the reporter's two-page document. "UniverLab.org" is a + // section title, but it also occurs verbatim inside a sentence in + // the "Perfil Profesional" prose on page 1. A substring match would + // false-positive there (as the buggy code did); a whole-line match + // must not. + let pages = vec![ + "Jane Doe\ncontacto@example.com\nFundador de UniverLab.org y contribuidor activo en open source\nPerfil Profesional\nExperiencia Laboral\nSix positions of professional experience follow.".to_string(), + "UniverLab.org\nFormación Académica\nHabilidades Técnicas\nRust, Python, and more.".to_string(), + ]; + let sections = vec![ + ("1".into(), "Perfil Profesional".into()), + ("2".into(), "Experiencia Laboral".into()), + ("3".into(), "UniverLab.org".into()), + ("4".into(), "Formación Académica".into()), + ("5".into(), "Habilidades Técnicas".into()), + ]; + let breaks = page_breaks(&pages, §ions); + let formatted = format_page_breaks(&breaks); + assert_eq!( + formatted, + "page=1 section=1 title=Perfil Profesional\npage=2 section=3 title=UniverLab.org" + ); + } + + #[test] + fn page_breaks_title_only_in_prose_never_matches() { + let pages = vec![ + "This report inline-mentions Special Report but never as a heading.".to_string(), + "More filler text on the second page, still no heading line.".to_string(), + ]; + let sections = vec![("1".into(), "Special Report".into())]; + let breaks = page_breaks(&pages, §ions); + let formatted = format_page_breaks(&breaks); + assert_eq!(formatted, "page=1 section= title=\npage=2 section= title="); + } + + #[test] + fn page_breaks_a_single_match_wins_even_when_preceded_by_other_text() { + // A page's only matched heading is what opens it, regardless of what + // precedes that heading's line — text before it (spillover, a + // caption, a diagram) is not itself a section, so it cannot be what + // the page opens with instead. See `page_breaks_defect_2_...` below + // for the real-document shape of this: a heading preceded by a + // diagram's garbled rendered text, not real prose spillover. + let pages = vec![ + "Alpha\nBody text under Alpha continues here.".to_string(), + "Trailing body text from Alpha spills onto this page.\nBeta\nMore Beta content." + .to_string(), + ]; + let sections = vec![("1".into(), "Alpha".into()), ("2".into(), "Beta".into())]; + let breaks = page_breaks(&pages, §ions); + let formatted = format_page_breaks(&breaks); + assert_eq!( + formatted, + "page=1 section=1 title=Alpha\npage=2 section=2 title=Beta" + ); + } + + #[test] + fn page_breaks_several_headings_on_one_page_first_wins() { + let pages = vec![ + "Cover page with no headings at all.".to_string(), + "Gamma\nDelta\nBody text under Delta.".to_string(), + "Body continues, no new heading here.".to_string(), + ]; + let sections = vec![("1".into(), "Gamma".into()), ("2".into(), "Delta".into())]; + let breaks = page_breaks(&pages, §ions); + let formatted = format_page_breaks(&breaks); + assert_eq!( + formatted, + "page=1 section= title=\npage=2 section=1 title=Gamma\npage=3 section=1 title=Gamma" + ); + } + + #[test] + fn page_breaks_several_headings_on_a_page_reports_the_first_not_the_last() { + // Defect 1: a page with multiple matched headings must report the + // *first* one, not the last — and this must hold even once a + // section has already opened on an earlier page (`current` is + // already `Some`), which is the actual shape of the bug: a + // first-page-with-any-match short-circuit made this look fixed + // while the real failure (pages 4-6 of examples/texforge-capabilites, + // each carrying a section already open from before) went uncaught. + let pages = vec![ + "Alpha\nBody under Alpha.".to_string(), + "Leftover Alpha text spills here.\nBeta\nGamma\nMore body.".to_string(), + ]; + let sections = vec![ + ("1".into(), "Alpha".into()), + ("2".into(), "Beta".into()), + ("3".into(), "Gamma".into()), + ]; + let breaks = page_breaks(&pages, §ions); + let formatted = format_page_breaks(&breaks); + assert_eq!( + formatted, + "page=1 section=1 title=Alpha\npage=2 section=2 title=Beta" + ); + } + + #[test] + fn page_breaks_pages_before_first_heading_are_unattributed() { + let pages = vec![ + "Cover page with no headings at all.".to_string(), + "Epsilon\nBody text under Epsilon.".to_string(), + ]; + let sections = vec![("1".into(), "Epsilon".into())]; + let breaks = page_breaks(&pages, §ions); + let formatted = format_page_breaks(&breaks); + assert_eq!( + formatted, + "page=1 section= title=\npage=2 section=1 title=Epsilon" + ); + } + + #[test] + fn line_is_section_heading_matches_dotted_numbering() { + // The real heading line from examples/texforge-capabilites. + assert!(line_is_section_heading( + "2.4. Estilos de Diagrama (style)", + "Estilos de Diagrama (style)" + )); + } + + #[test] + fn line_is_section_heading_matches_single_level_numbering() { + assert!(line_is_section_heading( + "2. Diagramas Embebidos", + "Diagramas Embebidos" + )); + } + + #[test] + fn line_is_section_heading_matches_three_level_numbering() { + assert!(line_is_section_heading("2.4.1 Something", "Something")); + } + + #[test] + fn line_is_section_heading_rejects_table_of_contents_line_te_d() { + // TF-D: the table-of-contents entry from examples/texforge-capabilites + // carries the same numbering prefix and title as the real heading, + // but with dot leaders and a page number trailing it. That must not + // match — a false match here is the same class of lie TE9 removed. + assert!(!line_is_section_heading( + "2.4. Estilos de Diagrama (style) . . . . . . . . . . . . . . . . . 3", + "Estilos de Diagrama (style)" + )); + } + + #[test] + fn line_is_section_heading_matches_unnumbered_heading() { + assert!(line_is_section_heading( + "Estilos de Diagrama (style)", + "Estilos de Diagrama (style)" + )); + } + + #[test] + fn page_breaks_numbered_heading_matches_and_toc_does_not_te_d() { + // TF-D: reproduces the bug with the real lines from + // examples/texforge-capabilites — a table of contents listing the + // heading (with dot leaders and a page number) must not be mistaken + // for the heading itself, which appears later as its own line. + let pages = vec![ + "Table of Contents\n2.4. Estilos de Diagrama (style) . . . . . . . . . . . . . . . . . 3" + .to_string(), + "Some other page with no heading.".to_string(), + "2.4. Estilos de Diagrama (style)\nBody text about diagram styles follows." + .to_string(), + ]; + let sections = vec![("2.4".into(), "Estilos de Diagrama (style)".into())]; + let breaks = page_breaks(&pages, §ions); + let formatted = format_page_breaks(&breaks); + assert_eq!( + formatted, + "page=1 section= title=\npage=2 section= title=\npage=3 section=2.4 title=Estilos de Diagrama (style)" + ); + } + + #[test] + fn line_is_section_heading_rejects_leaderless_table_of_contents_line_tf_f() { + // TF-F: examples/texforge-capabilites' table of contents has a + // *second* shape for top-level entries — no dot leaders at all, + // just the title then a bare page number. `d9af352` only ever + // tested the dot-leader shape; this must be rejected too. + assert!(!line_is_section_heading("3. Matemáticas 4", "Matemáticas")); + } + + #[test] + fn line_is_section_heading_matches_title_ending_in_a_number() { + // A heading can legitimately end in a digit (`Capítulo 2`); the + // leaderless-TOC rejection must key on what follows the title, not + // merely on the line ending in a number. + assert!(line_is_section_heading("Capítulo 2", "Capítulo 2")); + assert!(line_is_section_heading("5. Capítulo 2", "Capítulo 2")); + } + + #[test] + fn page_breaks_leaderless_toc_entry_never_matches_tf_f() { + // TF-F: reproduces the second bug shape with the real lines from + // examples/texforge-capabilites — a leaderless top-level TOC entry + // must not be mistaken for the heading itself. + let pages = vec![ + "Table of Contents\n3. Matemáticas 4".to_string(), + "Some other page with no heading.".to_string(), + "3. Matemáticas\nBody text about equations follows.".to_string(), + ]; + let sections = vec![("3".into(), "Matemáticas".into())]; + let breaks = page_breaks(&pages, §ions); + let formatted = format_page_breaks(&breaks); + assert_eq!( + formatted, + "page=1 section= title=\npage=2 section= title=\npage=3 section=3 title=Matemáticas" + ); + } + + #[test] + fn page_breaks_full_example_structure_tf_g() { + // TF-G: a seven-page fixture reproducing examples/texforge-capabilites' + // actual structure — TOC page mixing both TOC shapes (leaderless + // top-level entries, dot-leader subsection entries), then the real + // per-page content including the garbled letter-spaced text that + // `pdf-extract` produces for the document's embedded diagrams. That + // filler is what a prior attempt's fixture omitted: without it every + // heading happens to be the first line of its page, so a page's only + // match always "opens" the page and both defects stay hidden. Pages + // 4-6 additionally carry more than one matched heading, so this + // fixture alone would have caught both defect 1 (last match wins, + // should be first) and defect 2 (a single non-opening match was + // dropped in favor of carrying the previous section forward). + let pages = vec![ + "1. Introducción 2\n\ + 1.1. Antecedentes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2\n\ + 2. Diagramas Embebidos 2\n\ + 2.4. Estilos de Diagrama (style) . . . . . . . . . . . . . . . . . . . . . . . . . . 3\n\ + 3. Matemáticas 4\n\ + 3.1. Ecuaciones en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4\n\ + 3.3. Matrices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4\n\ + 4. Citas Bibliográficas 5\n\ + 5. Listados de Código 5\n\ + 5.2. LaTeX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6\n\ + 6. Tablas 6\n\ + 7. Comandos de Texforge 7" + .to_string(), + "1. Introducción\nTexforge simplifica el flujo de trabajo académico.\n\ + 1.1. Antecedentes\nLa edición de documentos académicos en LATEX requiere ...\n\ + 2. Diagramas Embebidos\nTexforge renderiza diagramas directamente." + .to_string(), + // Real garbled text from the Mermaid diagram (Figura 1) spills + // onto the top of the page before the 2.4 heading — this is the + // exact shape of defect 2. + "l o o p [ D i a g r a m a s e m b e b i d o s ]\n\ + Figura 1: Secuencia de compilación de Texforge\n\ + 2.4. Estilos de Diagrama (style)\n\ + Los tres entornos aceptan también un atributo style." + .to_string(), + // Real garbled preset-diagram text before three headings — the + // shape of defect 1: must report the first (3), not the last + // (3.3). + "l o p [ D i D a [ g r m s g D e b i D\n\ + El preset editorial sirve para documentos que se leen en pantalla.\n\ + 3. Matemáticas\n\ + 3.1. Ecuaciones en línea\n\ + La fórmula cuadrática se expresa como sigue.\n\ + 3.3. Matrices\n\ + A =" + .to_string(), + "C ó d i g o L a T e X\n\ + Figura 3: Pipeline de renderizado\n\ + 4. Citas Bibliográficas\n\ + Texforge gestiona automáticamente las referencias.\n\ + 5. Listados de Código\n\ + 5.1. Python" + .to_string(), + "5.2. LaTeX\n\ + Listing 2: Estructura básica de Texforge\n\ + 6. Tablas\n\ + Comparación de motores de renderizado." + .to_string(), + "7. Comandos de Texforge\n\ + Cuadro 2: Resumen de comandos principales.\n\ + 8. Conclusión\n\ + Texforge demuestra ser una herramienta completa." + .to_string(), + ]; + let sections = vec![ + ("1".into(), "Introducción".into()), + ("1.1".into(), "Antecedentes".into()), + ("2".into(), "Diagramas Embebidos".into()), + ("2.4".into(), "Estilos de Diagrama (style)".into()), + ("3".into(), "Matemáticas".into()), + ("3.1".into(), "Ecuaciones en línea".into()), + ("3.3".into(), "Matrices".into()), + ("4".into(), "Citas Bibliográficas".into()), + ("5".into(), "Listados de Código".into()), + ("5.2".into(), "LaTeX".into()), + ("6".into(), "Tablas".into()), + ("7".into(), "Comandos de Texforge".into()), + ]; + let breaks = page_breaks(&pages, §ions); + let formatted = format_page_breaks(&breaks); + assert_eq!( + formatted, + "page=1 section= title=\n\ + page=2 section=1 title=Introducción\n\ + page=3 section=2.4 title=Estilos de Diagrama (style)\n\ + page=4 section=3 title=Matemáticas\n\ + page=5 section=4 title=Citas Bibliográficas\n\ + page=6 section=5.2 title=LaTeX\n\ + page=7 section=7 title=Comandos de Texforge" + ); + } + + #[test] + fn page_breaks_a_later_match_on_a_page_does_not_override_the_first() { + // A page can carry more than one matched heading; the page is still + // reported as opened by the first one, and that is what carries + // forward into a following page with no heading of its own — never + // the later heading. + let pages = vec![ + "Alpha\nBody under Alpha.".to_string(), + "Trailing Alpha content spills onto this page.\nBeta\nMore text.\nGamma\nBody." + .to_string(), + "No new heading here, still reads as later content.".to_string(), + ]; + let sections = vec![ + ("1".into(), "Alpha".into()), + ("2".into(), "Beta".into()), + ("3".into(), "Gamma".into()), + ]; + let breaks = page_breaks(&pages, §ions); + let formatted = format_page_breaks(&breaks); + assert_eq!( + formatted, + "page=1 section=1 title=Alpha\npage=2 section=2 title=Beta\npage=3 section=2 title=Beta" + ); + } + + #[test] + fn page_breaks_defect_2_regression_heading_preceded_by_diagram_junk() { + // Regression for defect 2: `2.4. Estilos de Diagrama (style)` is a + // real heading line from examples/texforge-capabilites (page 3 of + // the compiled PDF), but it is preceded by the garbled letter-spaced + // text `pdf-extract` produces for the Mermaid diagram rendered at + // the top of that page ("l o o p [ D i a g r a m a s ... ]", the + // literal extracted text of Figura 1). The buggy code required a + // page's first matched heading to be the very first non-blank line + // on the page or fall back to carrying the previous section forward; + // since nothing else on the page matched, it kept reporting the + // prior section ("1 Introducción") instead of "2.4". The fix: a + // page's first matched heading opens it regardless of what + // (non-section) text precedes it. + let pages = vec![ + "1. Introducción\nTexforge simplifica el flujo de trabajo académico.".to_string(), + "l o o p [ D i a g r a m a s e m b e b i d o s ]\n\ + Figura 1: Secuencia de compilación de Texforge\n\ + 2.4. Estilos de Diagrama (style)\n\ + Los tres entornos aceptan también un atributo style." + .to_string(), + ]; + let sections = vec![ + ("1".into(), "Introducción".into()), + ("2.4".into(), "Estilos de Diagrama (style)".into()), + ]; + let breaks = page_breaks(&pages, §ions); + let formatted = format_page_breaks(&breaks); + assert_eq!( + formatted, + "page=1 section=1 title=Introducción\npage=2 section=2.4 title=Estilos de Diagrama (style)" + ); + } + + #[test] + fn pdf_info_reports_pages_fonts_and_metadata() { + let info = pdf_info_from_bytes(LIGATURES_PDF).unwrap(); + assert_eq!(info.pages, 1); + assert!(!info.fonts.is_empty()); + assert!( + info.fonts.iter().all(|f| f.embedded), + "fixture fonts should all be embedded: {:?}", + info.fonts + ); + assert!( + info.fonts.iter().any(|f| f.pages == vec![1]), + "page-level fonts should record page 1: {:?}", + info.fonts + ); + assert_eq!(info.metadata.creator.as_deref(), Some("tectonic")); + } + + #[test] + fn embedded_fixture_is_silent_for_quality_checks() { + let info = pdf_info_from_bytes(LIGATURES_PDF).unwrap(); + let findings = quality_findings(&info); + assert!( + findings.is_empty(), + "embedded fonts + well-formed dates must be silent: {findings:?}" + ); + } + + #[test] + fn malformed_date_fixture_warns_with_expected_shape() { + let info = pdf_info_from_bytes(MALFORMED_DATE_PDF).unwrap(); + assert_eq!( + info.metadata.creation_date.as_deref(), + Some("July 28, 2026") + ); + assert_eq!(info.metadata.mod_date.as_deref(), Some("August 7, 2026")); + + let findings = quality_findings(&info); + let date_findings: Vec<_> = findings + .iter() + .filter(|f| f.message.contains("CreationDate") || f.message.contains("ModDate")) + .collect(); + assert_eq!(date_findings.len(), 2, "{findings:?}"); + for f in &date_findings { + assert_eq!(f.severity, Severity::Warning); + assert!( + f.message.contains(PDF_DATE_EXPECTED), + "must name expected shape: {}", + f.message + ); + } + assert!(date_findings + .iter() + .any(|f| f.message.contains("July 28, 2026"))); + assert!(date_findings + .iter() + .any(|f| f.message.contains("August 7, 2026"))); + } + + #[test] + fn non_embedded_font_warns_with_page_list() { + let info = pdf_info_from_bytes(MALFORMED_DATE_PDF).unwrap(); + let helvetica = info + .fonts + .iter() + .find(|f| f.name == "Helvetica") + .expect("Helvetica present"); + assert!(!helvetica.embedded); + assert_eq!(helvetica.pages, vec![1, 2]); + + let findings = quality_findings(&info); + let font_finding = findings + .iter() + .find(|f| f.message.contains("Helvetica") && f.message.contains("not embedded")) + .expect("non-embedded font warning"); + assert_eq!(font_finding.severity, Severity::Warning); + assert!( + font_finding.message.contains("pages: 1, 2"), + "{}", + font_finding.message + ); + } + + #[test] + fn pdf_date_accepts_spec_shape_and_timezone() { + assert!(is_valid_pdf_date("D:20260807144421")); + assert!(is_valid_pdf_date("D:20260807144421Z")); + assert!(is_valid_pdf_date("D:20260807144421-00'00'")); + assert!(is_valid_pdf_date("D:20260807144421+05'30'")); + assert!(!is_valid_pdf_date("July 28, 2026")); + assert!(!is_valid_pdf_date("D:20260807")); + assert!(!is_valid_pdf_date("")); + } + + #[test] + fn fidelity_flags_missing_words_without_flooding() { + let mut source = BTreeMap::new(); + source.insert("Artificial".into(), 3); + source.insert("MLflow".into(), 1); + source.insert("present".into(), 1); + // Raw ligature text — without normalize, words are missing. + let raw = "Arti\u{FB01}cial ML\u{FB02}ow present"; + let missing_raw = fidelity_missing_words(&source, raw); + assert!( + missing_raw + .iter() + .any(|m| m.word == "Artificial" && m.count == 3), + "{missing_raw:?}" + ); + // After normalize, only nothing missing for these. + let missing_norm = fidelity_missing_words(&source, &normalize_pdf_text(raw)); + assert!(missing_norm.is_empty(), "{missing_norm:?}"); + } + + #[test] + fn fidelity_findings_are_warnings_with_empty_group_suggestion() { + let missing = vec![MissingWord { + word: "Artificial".into(), + count: 3, + }]; + let findings = fidelity_findings(&missing); + assert_eq!(findings.len(), 1); + assert_eq!(findings[0].severity, Severity::Warning); + let suggestion = findings[0].suggestion.as_deref().unwrap(); + assert!( + suggestion.contains("Artif{}icial"), + "suggestion was {suggestion}" + ); + assert!( + !suggestion.to_lowercase().contains("disableligatures") + || suggestion.contains("do not work") + ); + } + + #[test] + fn significant_words_skip_math_and_preamble() { + let files = vec![TokenizedFile { + path: PathBuf::from("main.tex"), + tokens: vec![ + Token::Command { + name: "documentclass".into(), + args: vec!["article".into()], + }, + Token::Text("IgnorePreamble".into()), + Token::BeginDocument, + Token::Text("Hello $x$ world Artificial".into()), + Token::BeginMath, + Token::EndMath, + Token::Text(" and MLflow.".into()), + ], + }]; + // Note: tokenize would strip math; here we simulate post-tokenizer stream. + let words = significant_words(&files); + assert!(words.contains_key("Hello")); + assert!(words.contains_key("world")); + assert!(words.contains_key("Artificial")); + assert!(words.contains_key("MLflow")); + assert!(!words.contains_key("IgnorePreamble")); + } + + #[test] + fn tabular_column_spec_is_excluded_from_significant_words() { + let source = "\\begin{document}\n\\begin{tabular}{@{}>{\\bfseries}p{3cm}>{\\raggedright\\arraybackslash}p{5.5cm}@{}}\nName & Alice \\\\\n\\end{tabular}\n\\end{document}\n"; + let files = vec![TokenizedFile { + path: PathBuf::from("main.tex"), + tokens: crate::texparse::tokenize(source), + }]; + let words = significant_words(&files); + assert!(!words.contains_key("p{3cm"), "{words:?}"); + assert!(!words.contains_key("p{5.5cm"), "{words:?}"); + assert!(words.contains_key("Name"), "{words:?}"); + assert!(words.contains_key("Alice"), "{words:?}"); + } + + #[test] + fn ligature_workaround_empty_group_matches_rendered_word() { + let source = + "\\begin{document}\nWe streamlined the workf{}lows for the team.\n\\end{document}\n"; + let files = vec![TokenizedFile { + path: PathBuf::from("main.tex"), + tokens: crate::texparse::tokenize(source), + }]; + let words = significant_words(&files); + assert!(words.contains_key("workflows"), "{words:?}"); + assert!(!words.contains_key("workf{}lows"), "{words:?}"); + + let missing = fidelity_missing_words(&words, "We streamlined the workflows for the team."); + assert!(missing.is_empty(), "{missing:?}"); + } + + #[test] + fn genuinely_missing_word_still_warns() { + let mut source = BTreeMap::new(); + source.insert("Nonexistent".into(), 1); + let missing = fidelity_missing_words(&source, "some other text entirely"); + assert_eq!(missing.len(), 1); + assert_eq!(missing[0].word, "Nonexistent"); + } + + #[test] + fn package_options_and_hypersetup_keys_are_excluded_from_significant_words() { + let source = "\\usepackage[hyphens]{url}\n\\hypersetup{pdfcreationdate={\\today}, colorlinks=true}\n\\setlength{\\parindent}{0pt}\n\\begin{document}\nHello world.\n\\end{document}\n"; + let files = vec![TokenizedFile { + path: PathBuf::from("main.tex"), + tokens: crate::texparse::tokenize(source), + }]; + let words = significant_words(&files); + assert!(words.contains_key("Hello"), "{words:?}"); + assert!(words.contains_key("world"), "{words:?}"); + assert!(!words.contains_key("hyphens"), "{words:?}"); + assert!(!words.contains_key("pdfcreationdate"), "{words:?}"); + assert!(!words.contains_key("colorlinks"), "{words:?}"); + assert!(!words.contains_key("parindent"), "{words:?}"); + } + + #[test] + fn fixture_fidelity_passes_after_normalize() { + let raw = extract_text_from_bytes(LIGATURES_PDF).unwrap(); + let mut source = BTreeMap::new(); + for w in [ + "Artificial", + "MLflow", + "workflows", + "local-first", + "Learning", + ] { + source.insert(w.into(), 1); + } + let missing = fidelity_missing_words(&source, &normalize_pdf_text(&raw)); + assert!(missing.is_empty(), "unexpected missing: {missing:?}"); + } +} diff --git a/src/raster.rs b/src/raster.rs new file mode 100644 index 0000000..b5188d5 --- /dev/null +++ b/src/raster.rs @@ -0,0 +1,249 @@ +//! Pure-Rust PDF rasterization. +//! +//! Turns a compiled PDF into RGBA8 bitmaps with `hayro` — no C toolchain, no +//! `-sys` crates. One [`RenderCache`] is created per document and shared across +//! every page. Each rendered buffer also reports ink coverage, so blank, +//! nearly-blank and overfull pages are cheap to detect from the same pixels. + +use std::path::Path; + +use anyhow::{bail, Context, Result}; +use hayro::hayro_interpret::InterpreterSettings; +use hayro::hayro_syntax::Pdf; +use hayro::vello_cpu::color::palette::css::WHITE; +use hayro::{render, RenderCache, RenderSettings}; + +/// A rasterized page: RGBA8 pixels plus ink coverage. +#[derive(Debug, Clone)] +pub struct RenderedPage { + /// 0-based page index within the document. + pub index: usize, + /// Rendered width in pixels. + pub width: usize, + /// Rendered height in pixels. + pub height: usize, + /// RGBA8 pixels, row-major, straight alpha with alpha forced to 255. + pub rgba: Vec<u8>, + /// Fraction of the page covered by ink, computed from the same buffer. + pub ink_coverage: f64, +} + +/// A parsed PDF document with one render cache shared across all its pages. +/// +/// The lifetime parameter is internal plumbing: the cache is invariant over it, +/// so rendering takes `&'a self` to unify the page and cache borrows. +pub struct PdfDocument<'a> { + pdf: Pdf, + cache: RenderCache<'a>, + settings: InterpreterSettings, +} + +impl<'a> PdfDocument<'a> { + /// Parse the PDF at `path`. + pub fn open(path: &Path) -> Result<Self> { + let data = + std::fs::read(path).with_context(|| format!("failed to read {}", path.display()))?; + Self::from_bytes(data).with_context(|| format!("{} is not a readable PDF", path.display())) + } + + /// Parse a PDF from raw bytes. + pub fn from_bytes(data: Vec<u8>) -> Result<Self> { + let pdf = Pdf::new(data).map_err(|_| anyhow::anyhow!("invalid or encrypted PDF"))?; + Ok(Self { + pdf, + cache: RenderCache::new(), + settings: InterpreterSettings::default(), + }) + } + + /// Number of pages in the document. + pub fn page_count(&self) -> usize { + self.pdf.pages().len() + } + + /// Logical size of page `index` in PDF points. + /// + /// Exercised by the module tests and consumed by the visual-regression and + /// page-break specs; the bin only renders, so it looks dead here. + #[allow(dead_code)] + pub fn page_dimensions(&self, index: usize) -> Result<(usize, usize)> { + let page = self + .pdf + .pages() + .get(index) + .with_context(|| self.out_of_range(index))?; + let (w, h) = page.render_dimensions(); + Ok((w.round() as usize, h.round() as usize)) + } + + /// The scale that maps `page_width` PDF points onto `target_width` pixels. + /// + /// Exercised by the module tests and consumed by the visual-regression and + /// page-break specs; the bin only renders, so it looks dead here. + #[allow(dead_code)] + pub fn scale_for_width(page_width: f32, target_width: usize) -> f32 { + target_width as f32 / page_width.max(1.0) + } + + /// Rasterize page `index` at `scale` (pixels per PDF point). + pub fn render_page(&'a self, index: usize, scale: f32) -> Result<RenderedPage> { + let page = self + .pdf + .pages() + .get(index) + .with_context(|| self.out_of_range(index))?; + let (w, h) = page.render_dimensions(); + if w <= 0.0 || h <= 0.0 { + bail!("page {} has invalid dimensions", index + 1); + } + let pixmap = render( + page, + &self.cache, + &self.settings, + &RenderSettings { + x_scale: scale, + y_scale: scale, + bg_color: WHITE, + ..Default::default() + }, + ); + let (width, height) = (pixmap.width() as usize, pixmap.height() as usize); + let mut rgba = pixmap.data_as_u8_slice().to_vec(); + // Pages render on an opaque white base, so premultiplied equals direct; + // force alpha anyway so compositors see a solid channel. + for px in rgba.chunks_exact_mut(4) { + px[3] = 255; + } + let ink_coverage = ink_coverage(&rgba); + Ok(RenderedPage { + index, + width, + height, + rgba, + ink_coverage, + }) + } + + /// Rasterize page `index` at whatever scale fits it to `target_width` pixels. + /// + /// Exercised by the module tests and consumed by the visual-regression and + /// page-break specs; the bin only renders, so it looks dead here. + #[allow(dead_code)] + pub fn render_page_to_width( + &'a self, + index: usize, + target_width: usize, + ) -> Result<RenderedPage> { + let (page_width, _) = self.page_dimensions(index)?; + let scale = Self::scale_for_width(page_width as f32, target_width); + self.render_page(index, scale) + } + + /// Rasterize every page at `scale`, sharing this document's render cache. + /// + /// Exercised by the module tests and consumed by the visual-regression and + /// page-break specs; the bin only renders, so it looks dead here. + #[allow(dead_code)] + pub fn render_all(&'a self, scale: f32) -> Result<Vec<RenderedPage>> { + (0..self.page_count()) + .map(|index| self.render_page(index, scale)) + .collect() + } + + fn out_of_range(&self, index: usize) -> String { + format!( + "page {} does not exist (document has {} pages)", + index + 1, + self.page_count() + ) + } +} + +/// Fraction of a rendered RGBA8 buffer covered by ink: the average of +/// `1 - luminance` across pixels. Pure white reads `0.0`, solid black `1.0`, +/// and antialiased edges count proportionally. +pub fn ink_coverage(rgba: &[u8]) -> f64 { + let mut ink = 0.0; + for px in rgba.chunks_exact(4) { + let luminance = (px[0] as u32 + px[1] as u32 + px[2] as u32) / 3; + ink += 1.0 - luminance as f64 / 255.0; + } + ink / (rgba.len() / 4) as f64 +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Committed fixture: two US-Letter pages; page 1 paints a black rectangle + /// over the left half of the page, page 2 is blank. + const FIXTURE: &[u8] = include_bytes!("../tests/fixtures/two-page.pdf"); + + fn document<'a>() -> PdfDocument<'a> { + PdfDocument::from_bytes(FIXTURE.to_vec()).expect("fixture parses") + } + + #[test] + fn fixture_renders_two_pages_with_opaque_alpha() { + let doc = document(); + assert_eq!(doc.page_count(), 2); + let pages = doc.render_all(1.0).unwrap(); + assert_eq!(pages.len(), 2); + for page in &pages { + assert_eq!((page.width, page.height), (612, 842)); + assert_eq!(page.rgba.len(), page.width * page.height * 4); + assert!( + page.rgba.chunks_exact(4).all(|px| px[3] == 255), + "alpha must be forced to 255" + ); + } + } + + #[test] + fn ink_coverage_distinguishes_blank_from_inked() { + let doc = document(); + let pages = doc.render_all(1.0).unwrap(); + let half_page = &pages[0]; + let blank = &pages[1]; + assert!( + (half_page.ink_coverage - 0.5).abs() < 0.02, + "left-half fill should be ~50%, got {}", + half_page.ink_coverage + ); + assert!( + blank.ink_coverage < 0.001, + "blank page should be ~0%, got {}", + blank.ink_coverage + ); + } + + #[test] + fn scale_derives_from_target_width_over_page_width() { + assert_eq!(PdfDocument::scale_for_width(612.0, 1224), 2.0); + assert_eq!(PdfDocument::scale_for_width(612.0, 306), 0.5); + assert_eq!(PdfDocument::scale_for_width(0.0, 100), 100.0); + } + + #[test] + fn render_page_to_width_fits_the_target() { + let doc = document(); + let page = doc.render_page_to_width(0, 306).unwrap(); + assert_eq!(page.width, 306); + assert_eq!(page.height, 421); + } + + #[test] + fn out_of_range_page_errors() { + let doc = document(); + let err = doc.render_page(5, 1.0).unwrap_err(); + assert!(err.to_string().contains("page 6 does not exist")); + } + + #[test] + fn invalid_bytes_error() { + let err = PdfDocument::from_bytes(b"not a pdf".to_vec()) + .err() + .expect("invalid bytes must fail"); + assert!(!err.to_string().is_empty()); + } +} diff --git a/src/texparse.rs b/src/texparse.rs new file mode 100644 index 0000000..4fd4058 --- /dev/null +++ b/src/texparse.rs @@ -0,0 +1,1830 @@ +//! A context-aware, single-pass LaTeX tokenizer. +//! +//! [`tokenize`] turns a LaTeX source buffer into a flat [`Token`] stream that +//! answers the one question every later text feature shares: *what is real +//! prose and what is not*. Word counting, spell checking and the glyph linter +//! consume this stream instead of each growing their own parser. +//! +//! [`tokenize_with_spans`] is the position-aware sibling: it pairs every token +//! with the byte range it covers in the source, and reports the byte offsets of +//! `$`-delimited math regions that were never closed. The glyph linter needs +//! both to map findings back to a `file:line`. +//! +//! # Contract +//! +//! * Prose appears as [`Token::Text`]. Text inside math regions, verbatim +//! regions and comments is never emitted. +//! * [`Token::Command`] carries only the *non-prose* arguments. Arguments that +//! are prose — `\textit`, `\textbf`, `\emph`, `\footnote`, `\caption`, and +//! the link text of `\href` — are emitted as [`Token::Text`] immediately +//! after their command token. +//! * Everything before [`Token::BeginDocument`] is the preamble. +//! * The tokenizer never panics: unbalanced braces, an unterminated +//! environment or a stray backslash yield tokens and scanning continues. +//! * Comments are recognized at the top level of the stream; a `%` inside a +//! command argument is kept as literal text. +//! +//! # Limitations +//! +//! * A `%` comment inside a command argument is not stripped. +//! * Math content between `BeginMath`/`EndMath` and verbatim content between +//! `BeginVerbatim`/`EndVerbatim` is not emitted as tokens. + +// This module is the shared parsing contract consumed by word counting +// ([`crate::wordcount`]) and the glyph linter ([`crate::linter::glyphs`]). + +use std::path::{Path, PathBuf}; + +use crate::texutil; + +/// A single token produced by the tokenizer. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Token { + /// Literal prose text outside math, verbatim, and comment regions. + /// + /// This is what consumers count and spell-check. The prose argument of a + /// prose command follows that command's [`Token::Command`] as [`Token::Text`]. + Text(String), + + /// A command and its non-prose arguments. + /// + /// `name` is the command name without the leading backslash; control + /// symbols (`\$`, `\%`, `\{`, `\\`) use the raw character as their name. + /// `args` holds the raw text of the optional/braced arguments that are not + /// prose. Prose arguments are emitted as [`Token::Text`] instead. + Command { name: String, args: Vec<String> }, + + /// A sectioning command. + /// + /// `level` follows the standard hierarchy: part = 0, chapter = 1, section + /// = 2, subsection = 3, subsubsection = 4, paragraph = 5, subparagraph = + /// 6. + /// + /// Two consumers need opposite forms of the title, so it is carried + /// twice: + /// + /// * `title` is the first braced argument with any wrapping macros + /// (`\textit`, `\href`, `\textcolor`, dot-leader constructs, ...) + /// resolved to plain text and escaped specials (`\&`, `\%`, ...) + /// unescaped to their literal character — see + /// [`resolve_section_title`]. This is what `outline`, `wordcount` and + /// `pdftext` display: human-readable prose. + /// * `raw_title` is the verbatim source text of that same braced + /// argument, escapes and macros intact. This is what the glyph linter + /// reads: it needs to see `\&` as still escaped to judge whether a + /// special character is protected. + Section { + level: u8, + title: String, + raw_title: String, + }, + + /// Beginning of a math region: `$...$`, `$$...$$`, `\(...\)`, `\[...\]`, + /// or a math environment (`equation`, `align`, `gather`, `multline`, + /// `flalign`, `eqnarray`, `displaymath`). + BeginMath, + + /// End of a math region. + EndMath, + + /// Beginning of a verbatim region (`verbatim`, `Verbatim`, `lstlisting`, + /// `minted`). + BeginVerbatim { env: String }, + + /// End of a verbatim region. + EndVerbatim { env: String }, + + /// A comment: `%` through end of line, including the leading `%`. + Comment(String), + + /// `\begin{document}`. Everything before this token is the preamble. + BeginDocument, + + /// `\end{document}`. + EndDocument, + + /// Any other environment, emitted for both `\begin{env}` and `\end{env}` + /// in nesting order. + Environment { name: String }, +} + +/// Math environments handled by the tokenizer (including the `*`-variants, +/// which are the same environments in unnumbered form). +const MATH_ENVIRONMENTS: &[&str] = &[ + "equation", + "equation*", + "align", + "align*", + "alignat", + "alignat*", + "gather", + "gather*", + "multline", + "multline*", + "flalign", + "flalign*", + "eqnarray", + "eqnarray*", + "displaymath", +]; + +/// Verbatim environments handled by the tokenizer. +const VERBATIM_ENVIRONMENTS: &[&str] = &["verbatim", "Verbatim", "lstlisting", "minted"]; + +/// Table environments whose `\begin{...}` is followed by a column +/// specification (and, for the starred/extended forms, a width) rather than +/// content. These arguments are typography, not prose — e.g. +/// `\begin{tabular}{@{}>{\bfseries}p{3cm}...}` — and must never reach the +/// significant-word stream that the fidelity check draws from. +const TABLE_PREAMBLE_ENVIRONMENTS: &[&str] = &[ + "tabular", + "tabular*", + "longtable", + "tabularx", + "tabulary", + "array", +]; + +/// Commands whose braced arguments are prose. +const PROSE_COMMANDS: &[&str] = &["textit", "textbf", "emph", "footnote", "caption"]; + +/// One tokenized file, paired with the file it came from. +#[derive(Debug)] +pub struct TokenizedFile { + /// Absolute path of the tokenized `.tex` file. + pub path: PathBuf, + /// The token stream for that file. + pub tokens: Vec<Token>, +} + +/// A token paired with the byte span it covers in the source. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SpannedToken { + /// The token itself. + pub token: Token, + /// Byte offset of the first character of the construct, inclusive. + pub start: usize, + /// Byte offset just past the last character of the construct, exclusive. + pub end: usize, +} + +/// The outcome of a position-aware tokenization. +#[derive(Debug)] +pub struct TokenizedSource { + /// The flat token stream, each with its source span. + pub tokens: Vec<SpannedToken>, + /// Byte offset of every `$`/`$$` delimiter that opened a math region with + /// no matching close in the source (the tokenizer synthesized the closing + /// delimiter at end of input). + pub unclosed_math: Vec<usize>, +} + +/// Tokenize a single LaTeX source buffer. +pub fn tokenize(source: &str) -> Vec<Token> { + tokenize_with_spans(source) + .tokens + .into_iter() + .map(|spanned| spanned.token) + .collect() +} + +/// Tokenize a single LaTeX source buffer, pairing each token with its span. +/// +/// The spans of prose-command arguments (`\textit{...}` and friends) and of +/// `\href` link text are absolute offsets into `source`, so consumers can map +/// any finding back to a `file:line` without re-scanning the buffer. +pub fn tokenize_with_spans(source: &str) -> TokenizedSource { + Parser::new(source).run() +} + +/// Shift a tokenized buffer by `offset` bytes, as used when prose-command +/// arguments are re-tokenized inside the enclosing source. +fn offset_tokens(tokens: Vec<SpannedToken>, offset: usize) -> Vec<SpannedToken> { + tokens + .into_iter() + .map(|mut spanned| { + spanned.start += offset; + spanned.end += offset; + spanned + }) + .collect() +} + +/// Tokenize every `.tex` file reachable from `entry` via `\input{}`. +/// +/// Traversal reuses [`texutil::collect_tex_files`], so the file set matches +/// what the linter inspects. +pub fn tokenize_document(root: &Path, entry: &str) -> Vec<TokenizedFile> { + texutil::collect_tex_files(root, entry) + .files + .into_iter() + .filter_map(|path| { + let source = std::fs::read_to_string(&path).ok()?; + Some(TokenizedFile { + path, + tokens: tokenize(&source), + }) + }) + .collect() +} + +/// Command-name characters beyond plain ASCII letters (internal commands use `@`). +fn is_command_char(c: char) -> bool { + c.is_ascii_alphabetic() || c == '@' +} + +/// Nesting level for a sectioning command name (a trailing `*` is ignored). +fn section_level(name: &str) -> Option<u8> { + match name.trim_end_matches('*') { + "part" => Some(0), + "chapter" => Some(1), + "section" => Some(2), + "subsection" => Some(3), + "subsubsection" => Some(4), + "paragraph" => Some(5), + "subparagraph" => Some(6), + _ => None, + } +} + +/// Macros that contribute no text to a resolved section title: pure spacing +/// or rule constructs, typically used to build dot leaders. +const TITLE_VISUAL_MACROS: &[&str] = &["leaders", "hfill", "hbox"]; + +/// Macros whose braced argument is the title text itself: everything but the +/// last argument is dropped (a URL, a color name, ...), and the kept argument +/// is resolved recursively so nesting works. +const TITLE_TEXT_MACROS: &[&str] = &["textit", "textbf", "emph"]; + +/// The standard LaTeX escaped specials: `\&`, `\%`, `\$`, `\#`, `\_`, `\{`, +/// `\}` each resolve to their literal character in a title. A backslash +/// followed by anything else still introduces a command. +const ESCAPED_SPECIALS: &[char] = &['&', '%', '$', '#', '_', '{', '}']; + +/// Resolves a raw section title — the literal source text of a `\section`-like +/// command's braced argument — into human-readable prose. +/// +/// This is the single place that understands macro-wrapped titles (`\href`, +/// `\textit`, `\textcolor`, dot-leader constructs, ...), so `outline` and the +/// PDF page mapper both see the same resolved string instead of drifting +/// title cleaners. See the module's [DECISIONS] in the originating spec for +/// the resolution rule per macro class. +/// +/// The resolver never panics: unbalanced braces yield a best-effort string +/// built from whatever was scanned before the input ran out. +pub(crate) fn resolve_section_title(raw: &str) -> String { + let chars: Vec<char> = raw.chars().collect(); + let resolved = TitleResolver::new(&chars).resolve(); + resolved.split_whitespace().collect::<Vec<_>>().join(" ") +} + +/// Recursive-descent scanner over a title's raw source, mirroring the +/// tokenizer's own brace handling but standalone: it only needs to tell +/// prose apart from the handful of macros a title can be wrapped in. +struct TitleResolver<'a> { + chars: &'a [char], + pos: usize, +} + +impl<'a> TitleResolver<'a> { + fn new(chars: &'a [char]) -> Self { + Self { chars, pos: 0 } + } + + fn peek(&self) -> Option<char> { + self.chars.get(self.pos).copied() + } + + fn bump(&mut self) -> Option<char> { + let c = self.peek(); + if c.is_some() { + self.pos += 1; + } + c + } + + /// Reads a balanced `{...}` group's inner content. On unbalanced input, + /// returns everything scanned up to the end of input (best effort). + fn read_group(&mut self) -> Option<Vec<char>> { + if self.peek() != Some('{') { + return None; + } + self.bump(); + let mut depth = 1usize; + let mut out = Vec::new(); + while let Some(c) = self.peek() { + match c { + '\\' => { + out.push(c); + self.bump(); + if let Some(next) = self.bump() { + out.push(next); + } + } + '{' => { + depth += 1; + out.push(c); + self.bump(); + } + '}' => { + self.bump(); + depth -= 1; + if depth == 0 { + return Some(out); + } + out.push(c); + } + _ => { + out.push(c); + self.bump(); + } + } + } + Some(out) + } + + fn read_command_name(&mut self) -> String { + let mut name = String::new(); + match self.peek() { + Some(c) if c.is_ascii_alphabetic() => { + while let Some(c) = self.peek() { + if c.is_ascii_alphabetic() { + name.push(c); + self.bump(); + } else { + break; + } + } + } + Some(c) => { + name.push(c); + self.bump(); + } + None => {} + } + name + } + + fn resolve(&mut self) -> String { + let mut out = String::new(); + while let Some(c) = self.peek() { + match c { + '\\' => { + self.bump(); + if let Some(special) = self.peek().filter(|c| ESCAPED_SPECIALS.contains(c)) { + // Escaped special: the backslash just protects the + // character, it does not introduce a command. + self.bump(); + out.push(special); + continue; + } + let name = self.read_command_name(); + if TITLE_VISUAL_MACROS.contains(&name.as_str()) { + // Purely visual: drop the macro and, if present, the + // one group it dresses (e.g. `\hbox{.}`). + let _ = self.read_group(); + } else if TITLE_TEXT_MACROS.contains(&name.as_str()) { + if let Some(group) = self.read_group() { + out.push_str(&TitleResolver::new(&group).resolve()); + } + } else if name == "textcolor" || name == "href" { + // `\textcolor{color}{text}` and `\href{url}{text}`: + // drop the first (non-textual) group, keep the second. + let _dropped = self.read_group(); + if let Some(text) = self.read_group() { + out.push_str(&TitleResolver::new(&text).resolve()); + } + } else if let Some(group) = self.read_group() { + // Unknown macro: keep its textual content rather than + // dropping the title or emitting raw source. + out.push_str(&TitleResolver::new(&group).resolve()); + } + // Unknown macro with no braced argument: drop silently. + } + '{' => { + // A bare group not attached to a command still groups + // prose (`{Emphasis}`); keep its resolved content. + if let Some(group) = self.read_group() { + out.push_str(&TitleResolver::new(&group).resolve()); + } + } + '}' => { + // Stray closing brace from malformed input: skip it. + self.bump(); + } + _ => { + out.push(c); + self.bump(); + } + } + } + out + } +} + +/// Produces dotted section numbers such as `1`, `1.1` and `2.1.1`. +/// +/// One counter per level, mirroring the tokenizer's section hierarchy +/// ([`section_level`]: `part` = 0, `chapter` = 1, `section` = 2, ...). Entering +/// a level bumps its counter and resets every deeper one. Leading zero counters +/// are dropped from the printed number, so a document that only uses +/// `\section` numbers its sections `1`, `2`, ... rather than `0.0.1`. +pub struct SectionTracker { + counters: Vec<usize>, +} + +impl SectionTracker { + /// Create a tracker covering levels `0..=max_level`. + pub fn new(max_level: u8) -> Self { + Self { + counters: vec![0; max_level as usize + 1], + } + } + + /// Enter a section at `level`, returning its dotted number. + pub fn enter(&mut self, level: u8) -> String { + let level = level as usize; + self.counters[level] += 1; + for counter in &mut self.counters[level + 1..] { + *counter = 0; + } + let parts: Vec<String> = self.counters[..=level] + .iter() + .map(|counter| counter.to_string()) + .collect(); + let start = parts + .iter() + .position(|part| part != "0") + .unwrap_or(parts.len() - 1); + parts[start..].join(".") + } +} + +/// Single-pass state machine over the character stream. +struct Parser<'a> { + src: &'a str, + pos: usize, + /// Byte offsets of `$`/`$$` math delimiters that were never closed. + unclosed_math: Vec<usize>, +} + +impl<'a> Parser<'a> { + fn new(src: &'a str) -> Self { + Parser { + src, + pos: 0, + unclosed_math: Vec::new(), + } + } + + fn peek(&self) -> Option<char> { + self.src[self.pos..].chars().next() + } + + fn bump(&mut self) -> Option<char> { + let c = self.peek()?; + self.pos += c.len_utf8(); + Some(c) + } + + fn eat(&mut self, expected: char) -> bool { + if self.peek() == Some(expected) { + self.bump(); + true + } else { + false + } + } + + fn push(&self, tokens: &mut Vec<SpannedToken>, token: Token, start: usize, end: usize) { + tokens.push(SpannedToken { token, start, end }); + } + + fn run(mut self) -> TokenizedSource { + let mut tokens = Vec::new(); + let mut text = String::new(); + let mut text_start = 0usize; + + while let Some(c) = self.peek() { + match c { + '%' => { + self.flush_text(&mut tokens, &mut text, text_start); + let start = self.pos; + let comment = self.read_comment(); + self.push(&mut tokens, Token::Comment(comment), start, self.pos); + } + '\\' => { + self.flush_text(&mut tokens, &mut text, text_start); + self.handle_backslash(&mut tokens); + } + '$' => { + self.flush_text(&mut tokens, &mut text, text_start); + self.read_dollar_math(&mut tokens); + } + _ => { + if text.is_empty() { + text_start = self.pos; + } + text.push(c); + self.bump(); + } + } + } + self.flush_text(&mut tokens, &mut text, text_start); + TokenizedSource { + tokens, + unclosed_math: self.unclosed_math, + } + } + + fn flush_text(&self, tokens: &mut Vec<SpannedToken>, text: &mut String, start: usize) { + if !text.is_empty() { + let end = self.pos; + self.push(tokens, Token::Text(std::mem::take(text)), start, end); + } + } + + /// Read a comment: `%` through end of line (the newline is not consumed). + fn read_comment(&mut self) -> String { + let start = self.pos; + while let Some(c) = self.peek() { + if c == '\n' { + break; + } + self.bump(); + } + self.src[start..self.pos].to_string() + } + + fn handle_backslash(&mut self, tokens: &mut Vec<SpannedToken>) { + let start = self.pos; + self.bump(); // consume the backslash + let Some(c) = self.peek() else { + self.push( + tokens, + Token::Command { + name: "\\".to_string(), + args: Vec::new(), + }, + start, + self.pos, + ); + return; + }; + + match c { + '[' => { + self.bump(); + self.push(tokens, Token::BeginMath, start, start + 2); + let close_start = self.pos; + self.skip_to_control_close(']'); + self.push(tokens, Token::EndMath, close_start, close_start + 2); + } + ']' => { + self.bump(); + self.push(tokens, Token::EndMath, start, start + 2); + } + '(' => { + self.bump(); + self.push(tokens, Token::BeginMath, start, start + 2); + let close_start = self.pos; + self.skip_to_control_close(')'); + self.push(tokens, Token::EndMath, close_start, close_start + 2); + } + ')' => { + self.bump(); + self.push(tokens, Token::EndMath, start, start + 2); + } + _ if is_command_char(c) => { + let name = self.read_command_name(); + self.handle_command(&name, start, tokens); + } + _ => { + // Control symbol: `\\`, `\$`, `\%`, `\{`, `\&`, ... + self.bump(); + self.push( + tokens, + Token::Command { + name: c.to_string(), + args: Vec::new(), + }, + start, + self.pos, + ); + } + } + } + + fn read_command_name(&mut self) -> String { + let start = self.pos; + while let Some(c) = self.peek() { + if is_command_char(c) { + self.bump(); + } else { + break; + } + } + self.src[start..self.pos].to_string() + } + + fn handle_command(&mut self, name: &str, start: usize, tokens: &mut Vec<SpannedToken>) { + match name { + "begin" => self.handle_begin(start, tokens), + "end" => self.handle_end(start, tokens), + "verb" | "lstinline" => self.handle_verb_command(name, start, tokens), + _ => { + if let Some(level) = section_level(name) { + self.eat('*'); + let _ = self.read_bracket_group(); + let raw_title = self.read_braced_group().unwrap_or_default(); + let title = resolve_section_title(&raw_title); + self.push( + tokens, + Token::Section { + level, + title, + raw_title, + }, + start, + self.pos, + ); + } else if name == "href" { + self.handle_href(start, tokens); + } else if PROSE_COMMANDS.contains(&name) { + self.handle_prose_command(name, start, tokens); + } else { + self.handle_plain_command(name, start, tokens); + } + } + } + } + + /// `\verb[char][...]` and `\lstinline[opts][char][...]`: the delimiter is + /// the first non-letter after the name (and any options); everything up to + /// the next occurrence of that delimiter is verbatim and never prose. + fn handle_verb_command(&mut self, name: &str, start: usize, tokens: &mut Vec<SpannedToken>) { + let mut args = Vec::new(); + if name == "lstinline" { + while let Some(optional) = self.read_bracket_group() { + args.push(optional); + } + } + if self.peek() == Some('*') { + self.bump(); + } + let Some(delim) = self.bump() else { + self.push( + tokens, + Token::Command { + name: name.to_string(), + args, + }, + start, + self.pos, + ); + return; + }; + let mut content = String::new(); + while let Some(c) = self.peek() { + if c == delim || c == '\n' { + break; + } + content.push(c); + self.bump(); + } + args.push(content); + self.push( + tokens, + Token::Command { + name: name.to_string(), + args, + }, + start, + self.pos, + ); + } + + fn handle_begin(&mut self, start: usize, tokens: &mut Vec<SpannedToken>) { + let env = self.read_braced_group().unwrap_or_default(); + // Discard float placement etc.: `\begin{figure}[htbp]`. + let _ = self.read_bracket_group(); + let mid = self.pos; + + if env == "document" { + self.push(tokens, Token::BeginDocument, start, mid); + return; + } + if MATH_ENVIRONMENTS.contains(&env.as_str()) { + self.push(tokens, Token::BeginMath, start, mid); + self.skip_to_env_end(&env); + self.push(tokens, Token::EndMath, mid, self.pos); + return; + } + if VERBATIM_ENVIRONMENTS.contains(&env.as_str()) { + self.push( + tokens, + Token::BeginVerbatim { env: env.clone() }, + start, + mid, + ); + self.skip_to_env_end(&env); + self.push(tokens, Token::EndVerbatim { env }, mid, self.pos); + return; + } + if TABLE_PREAMBLE_ENVIRONMENTS.contains(&env.as_str()) { + self.consume_table_preamble(&env); + } + self.push(tokens, Token::Environment { name: env }, start, self.pos); + } + + /// Consume the non-prose arguments that follow `\begin{tabular}` and its + /// relatives. The `[pos]` shared by all of them was already discarded by + /// the caller as "float placement"; this reads what plain discard leaves + /// behind: `tabular*`/`tabularx`/`tabulary` take `{width}` before the + /// column spec, all of them take a mandatory `{cols}`. + fn consume_table_preamble(&mut self, env: &str) { + if matches!(env, "tabular*" | "tabularx" | "tabulary") { + let _ = self.read_braced_group(); // {width} + let _ = self.read_bracket_group(); // [pos] (tabular* only) + } + let _ = self.read_braced_group(); // {cols} + } + + fn handle_end(&mut self, start: usize, tokens: &mut Vec<SpannedToken>) { + let env = self.read_braced_group().unwrap_or_default(); + if env == "document" { + self.push(tokens, Token::EndDocument, start, self.pos); + } else if MATH_ENVIRONMENTS.contains(&env.as_str()) { + self.push(tokens, Token::EndMath, start, self.pos); + } else if VERBATIM_ENVIRONMENTS.contains(&env.as_str()) { + self.push(tokens, Token::EndVerbatim { env }, start, self.pos); + } else { + self.push(tokens, Token::Environment { name: env }, start, self.pos); + } + } + + /// `\href{url}{text}`: the URL is a non-prose argument, the link text is prose. + fn handle_href(&mut self, start: usize, tokens: &mut Vec<SpannedToken>) { + let mut args = Vec::new(); + while let Some(optional) = self.read_bracket_group() { + args.push(optional); + } + if let Some(url) = self.read_braced_group() { + args.push(url); + } + self.push( + tokens, + Token::Command { + name: "href".to_string(), + args, + }, + start, + self.pos, + ); + if let Some((text, text_start, _text_end)) = self.read_braced_group_spanned() { + let sub = tokenize_with_spans(&text); + self.unclosed_math + .extend(sub.unclosed_math.iter().map(|offset| offset + text_start)); + tokens.extend(offset_tokens(sub.tokens, text_start)); + } + } + + /// A command whose braced argument is prose, emitted as [`Token::Text`]. + fn handle_prose_command(&mut self, name: &str, start: usize, tokens: &mut Vec<SpannedToken>) { + let mut args = Vec::new(); + while let Some(optional) = self.read_bracket_group() { + args.push(optional); + } + self.push( + tokens, + Token::Command { + name: name.to_string(), + args, + }, + start, + self.pos, + ); + if let Some((prose, prose_start, _prose_end)) = self.read_braced_group_spanned() { + let sub = tokenize_with_spans(&prose); + self.unclosed_math + .extend(sub.unclosed_math.iter().map(|offset| offset + prose_start)); + tokens.extend(offset_tokens(sub.tokens, prose_start)); + } + } + + /// Any other command: greedily collect optional and braced arguments. + fn handle_plain_command(&mut self, name: &str, start: usize, tokens: &mut Vec<SpannedToken>) { + let mut args = Vec::new(); + loop { + if let Some(optional) = self.read_bracket_group() { + args.push(optional); + continue; + } + if let Some(braced) = self.read_braced_group() { + args.push(braced); + continue; + } + break; + } + self.push( + tokens, + Token::Command { + name: name.to_string(), + args, + }, + start, + self.pos, + ); + } + + /// Read a balanced `{...}` group, returning its raw content (escaped + /// braces `\{`/`\}` do not affect nesting). + fn read_braced_group(&mut self) -> Option<String> { + self.read_braced_group_spanned().map(|(text, _, _)| text) + } + + /// Like [`Parser::read_braced_group`], also returning the byte span of the + /// inner content so consumers can map re-tokenized prose back to the source. + fn read_braced_group_spanned(&mut self) -> Option<(String, usize, usize)> { + if !self.eat('{') { + return None; + } + let content_start = self.pos; + let mut depth = 1usize; + let mut out = String::new(); + while let Some(c) = self.peek() { + match c { + '\\' => { + out.push('\\'); + self.bump(); + if let Some(next) = self.bump() { + out.push(next); + } + } + '{' => { + depth += 1; + out.push('{'); + self.bump(); + } + '}' => { + depth -= 1; + self.bump(); + if depth == 0 { + return Some((out, content_start, self.pos - 1)); + } + out.push('}'); + } + _ => { + out.push(c); + self.bump(); + } + } + } + Some((out, content_start, self.pos)) + } + + /// Read a balanced `[...]` optional-argument group. + fn read_bracket_group(&mut self) -> Option<String> { + if !self.eat('[') { + return None; + } + let mut depth = 1usize; + let mut out = String::new(); + while let Some(c) = self.peek() { + match c { + '\\' => { + out.push('\\'); + self.bump(); + if let Some(next) = self.bump() { + out.push(next); + } + } + '[' => { + depth += 1; + out.push('['); + self.bump(); + } + ']' => { + depth -= 1; + self.bump(); + if depth == 0 { + break; + } + out.push(']'); + } + _ => { + out.push(c); + self.bump(); + } + } + } + Some(out) + } + + fn read_dollar_math(&mut self, tokens: &mut Vec<SpannedToken>) { + let start = self.pos; // at the opening `$` + self.bump(); // consume the opening `$` + let display = self.eat('$'); + let delim_len = if display { 2 } else { 1 }; + self.push(tokens, Token::BeginMath, start, start + delim_len); + let closed = if display { + self.skip_to_display_close() + } else { + self.skip_to_inline_dollar() + }; + if !closed { + self.unclosed_math.push(start); + } + let end = self.pos; + self.push(tokens, Token::EndMath, end.saturating_sub(delim_len), end); + } + + /// Advance past a `$$` close (or to end of input). Returns whether a close + /// was actually found. + fn skip_to_display_close(&mut self) -> bool { + if let Some(idx) = self.src[self.pos..].find("$$") { + self.pos += idx + 2; + true + } else { + self.pos = self.src.len(); + false + } + } + + /// Advance past a `\X` close such as `\]` or `\)` (or to end of input). + fn skip_to_control_close(&mut self, close: char) { + let needle = format!("\\{}", close); + if let Some(idx) = self.src[self.pos..].find(&needle) { + self.pos += idx + needle.len(); + } else { + self.pos = self.src.len(); + } + } + + /// Advance past the closing inline `$`, honoring escaped `\$`. Returns + /// whether a close was actually found. + fn skip_to_inline_dollar(&mut self) -> bool { + let bytes = self.src.as_bytes(); + let mut i = self.pos; + let mut backslashes = 0usize; + while i < bytes.len() { + match bytes[i] { + b'\\' => { + backslashes += 1; + i += 1; + } + b'$' => { + if backslashes % 2 == 0 { + self.pos = i + 1; + return true; + } + backslashes = 0; + i += 1; + } + _ => { + backslashes = 0; + i += 1; + } + } + } + self.pos = self.src.len(); + false + } + + /// Advance past the matching `\end{env}` terminator (or to end of input, + /// closing the region synthetically at EOF). + fn skip_to_env_end(&mut self, env: &str) { + let terminator = format!("\\end{{{}}}", env); + if let Some(idx) = self.src[self.pos..].find(&terminator) { + self.pos += idx + terminator.len(); + } else { + self.pos = self.src.len(); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn plain_prose_is_text() { + let tokens = tokenize("Hello world, this is prose."); + assert_eq!( + tokens, + vec![Token::Text("Hello world, this is prose.".to_string())] + ); + } + + #[test] + fn command_names_distinct_from_arguments() { + let tokens = tokenize(r"\label{sec:intro} and \ref{fig:x}"); + assert_eq!( + tokens, + vec![ + Token::Command { + name: "label".to_string(), + args: vec!["sec:intro".to_string()], + }, + Token::Text(" and ".to_string()), + Token::Command { + name: "ref".to_string(), + args: vec!["fig:x".to_string()], + }, + ] + ); + } + + #[test] + fn cite_is_command() { + let tokens = tokenize(r"\cite{knuth1984}"); + assert_eq!( + tokens, + vec![Token::Command { + name: "cite".to_string(), + args: vec!["knuth1984".to_string()], + }] + ); + } + + #[test] + fn input_is_command() { + let tokens = tokenize(r"\input{chapter1}"); + assert_eq!( + tokens, + vec![Token::Command { + name: "input".to_string(), + args: vec!["chapter1".to_string()], + }] + ); + } + + #[test] + fn includegraphics_with_options_is_command() { + let tokens = tokenize(r"\includegraphics[width=0.5\textwidth]{img.png}"); + assert_eq!( + tokens, + vec![Token::Command { + name: "includegraphics".to_string(), + args: vec![r"width=0.5\textwidth".to_string(), "img.png".to_string()], + }] + ); + } + + #[test] + fn index_is_command() { + let tokens = tokenize(r"\index{LaTeX}"); + assert_eq!( + tokens, + vec![Token::Command { + name: "index".to_string(), + args: vec!["LaTeX".to_string()], + }] + ); + } + + #[test] + fn inline_math_is_marked() { + let tokens = tokenize("The value is $x+1$ here."); + assert_eq!( + tokens, + vec![ + Token::Text("The value is ".to_string()), + Token::BeginMath, + Token::EndMath, + Token::Text(" here.".to_string()), + ] + ); + } + + #[test] + fn display_math_dollars_is_marked() { + let tokens = tokenize("Before $$\\int f dx$$ after."); + assert_eq!( + tokens, + vec![ + Token::Text("Before ".to_string()), + Token::BeginMath, + Token::EndMath, + Token::Text(" after.".to_string()), + ] + ); + } + + #[test] + fn bracket_display_math_is_marked() { + let tokens = tokenize(r"Before \[a+b\] after."); + assert_eq!( + tokens, + vec![ + Token::Text("Before ".to_string()), + Token::BeginMath, + Token::EndMath, + Token::Text(" after.".to_string()), + ] + ); + } + + #[test] + fn paren_inline_math_is_marked() { + let tokens = tokenize(r"Before \(x\) after."); + assert_eq!( + tokens, + vec![ + Token::Text("Before ".to_string()), + Token::BeginMath, + Token::EndMath, + Token::Text(" after.".to_string()), + ] + ); + } + + #[test] + fn math_environments_are_marked() { + for env in MATH_ENVIRONMENTS { + let src = format!("Before \\begin{{{env}}}a+b\\end{{{env}}} after."); + let tokens = tokenize(&src); + assert_eq!( + tokens, + vec![ + Token::Text("Before ".to_string()), + Token::BeginMath, + Token::EndMath, + Token::Text(" after.".to_string()), + ], + "math env: {env}" + ); + } + } + + #[test] + fn math_content_is_not_prose() { + let tokens = tokenize("$\\text{alpha} + \\beta$"); + assert_eq!(tokens, vec![Token::BeginMath, Token::EndMath]); + } + + #[test] + fn verbatim_environments_are_marked() { + for env in VERBATIM_ENVIRONMENTS { + let src = format!("\\begin{{{env}}}% $nonsense$ 100% \\end{{{env}}} after."); + let tokens = tokenize(&src); + assert_eq!( + tokens, + vec![ + Token::BeginVerbatim { + env: env.to_string(), + }, + Token::EndVerbatim { + env: env.to_string(), + }, + Token::Text(" after.".to_string()), + ], + "verbatim env: {env}" + ); + } + } + + #[test] + fn comments_are_tokens() { + let tokens = tokenize("Text % a comment\nmore"); + assert_eq!( + tokens, + vec![ + Token::Text("Text ".to_string()), + Token::Comment("% a comment".to_string()), + Token::Text("\nmore".to_string()), + ] + ); + } + + #[test] + fn escaped_percent_is_not_a_comment() { + let tokens = tokenize(r"50\% off"); + assert_eq!( + tokens, + vec![ + Token::Text("50".to_string()), + Token::Command { + name: "%".to_string(), + args: Vec::new(), + }, + Token::Text(" off".to_string()), + ] + ); + } + + #[test] + fn urls_are_not_prose() { + let tokens = tokenize(r"\url{https://example.com/x}"); + assert_eq!( + tokens, + vec![Token::Command { + name: "url".to_string(), + args: vec!["https://example.com/x".to_string()], + }] + ); + } + + #[test] + fn href_splits_url_from_link_text() { + let tokens = tokenize(r"\href{https://example.com}{Example}"); + assert_eq!( + tokens, + vec![ + Token::Command { + name: "href".to_string(), + args: vec!["https://example.com".to_string()], + }, + Token::Text("Example".to_string()), + ] + ); + } + + #[test] + fn sectioning_commands_are_marked() { + let tokens = tokenize( + r"\part{One}\chapter{Two}\section{Three}\subsection{Four}\subsubsection{Five}\paragraph{Six}\subparagraph{Seven}", + ); + let levels: Vec<u8> = tokens + .iter() + .filter_map(|t| match t { + Token::Section { level, .. } => Some(*level), + _ => None, + }) + .collect(); + assert_eq!(levels, vec![0, 1, 2, 3, 4, 5, 6]); + assert_eq!( + tokens[2], + Token::Section { + level: 2, + title: "Three".to_string(), + raw_title: "Three".to_string(), + } + ); + } + + #[test] + fn starred_section_is_marked() { + let tokens = tokenize(r"\section*{Intro}"); + assert_eq!( + tokens, + vec![Token::Section { + level: 2, + title: "Intro".to_string(), + raw_title: "Intro".to_string(), + }] + ); + } + + #[test] + fn section_with_optional_toc_title_uses_required_title() { + let tokens = tokenize(r"\section[Short]{Full title}"); + assert_eq!( + tokens, + vec![Token::Section { + level: 2, + title: "Full title".to_string(), + raw_title: "Full title".to_string(), + }] + ); + } + + #[test] + fn prose_commands_emit_text() { + for cmd in PROSE_COMMANDS { + let src = format!("\\{cmd}{{Hello world}}"); + let tokens = tokenize(&src); + assert_eq!( + tokens, + vec![ + Token::Command { + name: cmd.to_string(), + args: Vec::new(), + }, + Token::Text("Hello world".to_string()), + ], + "prose cmd: {cmd}" + ); + } + } + + #[test] + fn caption_optional_argument_is_non_prose() { + let tokens = tokenize(r"\caption[Short]{Long caption}"); + assert_eq!( + tokens, + vec![ + Token::Command { + name: "caption".to_string(), + args: vec!["Short".to_string()], + }, + Token::Text("Long caption".to_string()), + ] + ); + } + + #[test] + fn math_inside_caption_is_handled() { + let tokens = tokenize(r"\caption{The value is $x$}"); + assert_eq!( + tokens, + vec![ + Token::Command { + name: "caption".to_string(), + args: Vec::new(), + }, + Token::Text("The value is ".to_string()), + Token::BeginMath, + Token::EndMath, + ] + ); + } + + #[test] + fn nested_prose_command_in_braces_is_handled() { + let tokens = tokenize(r"\textbf{see \ref{fig:1}}"); + assert_eq!( + tokens, + vec![ + Token::Command { + name: "textbf".to_string(), + args: Vec::new(), + }, + Token::Text("see ".to_string()), + Token::Command { + name: "ref".to_string(), + args: vec!["fig:1".to_string()], + }, + ] + ); + } + + #[test] + fn verbatim_inside_figure_is_handled() { + let tokens = tokenize( + "\\begin{figure}\n\\begin{verbatim}\nx = $y$ 100%\n\\end{verbatim}\n\\end{figure}", + ); + assert_eq!( + tokens, + vec![ + Token::Environment { + name: "figure".to_string(), + }, + Token::Text("\n".to_string()), + Token::BeginVerbatim { + env: "verbatim".to_string(), + }, + Token::EndVerbatim { + env: "verbatim".to_string(), + }, + Token::Text("\n".to_string()), + Token::Environment { + name: "figure".to_string(), + }, + ] + ); + } + + #[test] + fn tabular_column_spec_is_not_prose() { + let tokens = tokenize( + r"\begin{tabular}{@{}>{\bfseries}p{3cm}>{\raggedright\arraybackslash}p{5.5cm}@{}}Name & Alice \\\end{tabular}", + ); + for t in &tokens { + if let Token::Text(text) = t { + assert!(!text.contains("p{3cm"), "column spec leaked: {text:?}"); + assert!(!text.contains("p{5.5cm"), "column spec leaked: {text:?}"); + } + } + assert!(tokens.iter().any( + |t| matches!(t, Token::Text(text) if text.contains("Name") || text.contains("Alice")) + )); + } + + #[test] + fn tabular_star_and_x_and_y_column_specs_are_not_prose() { + for source in [ + r"\begin{tabular*}{\textwidth}[t]{lcr}x\end{tabular*}", + r"\begin{tabularx}{\textwidth}{lX}x\end{tabularx}", + r"\begin{tabulary}{\textwidth}{lC}x\end{tabulary}", + r"\begin{longtable}[c]{ll}x\end{longtable}", + r"\begin{array}{cc}x\end{array}", + ] { + let tokens = tokenize(source); + for t in &tokens { + if let Token::Text(text) = t { + assert!( + !text.contains('{') && !text.contains('}'), + "preamble leaked from {source:?}: {text:?}" + ); + } + } + } + } + + #[test] + fn generic_environments_preserve_order() { + let tokens = tokenize("\\begin{figure}\\begin{tabular}c\\end{tabular}\\end{figure}"); + assert_eq!( + tokens, + vec![ + Token::Environment { + name: "figure".to_string(), + }, + Token::Environment { + name: "tabular".to_string(), + }, + Token::Text("c".to_string()), + Token::Environment { + name: "tabular".to_string(), + }, + Token::Environment { + name: "figure".to_string(), + }, + ] + ); + } + + #[test] + fn unterminated_environment_does_not_panic() { + let tokens = tokenize("\\begin{verbatim}\nraw $ content %"); + assert_eq!( + tokens, + vec![ + Token::BeginVerbatim { + env: "verbatim".to_string(), + }, + Token::EndVerbatim { + env: "verbatim".to_string(), + }, + ] + ); + } + + #[test] + fn unbalanced_braces_do_not_panic() { + let tokens = tokenize(r"\textbf{unclosed and }}}} stray"); + assert!(tokens.iter().any(|t| matches!( + t, + Token::Command { name, .. } if name == "textbf" + ))); + } + + #[test] + fn stray_backslash_does_not_panic() { + let tokens = tokenize("trailing \\"); + assert_eq!( + tokens, + vec![ + Token::Text("trailing ".to_string()), + Token::Command { + name: "\\".to_string(), + args: Vec::new(), + }, + ] + ); + } + + #[test] + fn escaped_dollar_is_not_math() { + let tokens = tokenize(r"cost: \$5"); + assert_eq!( + tokens, + vec![ + Token::Text("cost: ".to_string()), + Token::Command { + name: "$".to_string(), + args: Vec::new(), + }, + Token::Text("5".to_string()), + ] + ); + } + + #[test] + fn preamble_precedes_begin_document() { + let tokens = tokenize( + "\\documentclass{article}\n\\usepackage{amsmath}\n\\begin{document}\nHello world\n\\end{document}", + ); + let doc_pos = tokens + .iter() + .position(|t| matches!(t, Token::BeginDocument)) + .unwrap(); + assert!(tokens[..doc_pos] + .iter() + .all(|t| matches!(t, Token::Command { .. } | Token::Text(_)))); + assert!(tokens[doc_pos..] + .iter() + .any(|t| matches!(t, Token::Text(s) if s.contains("Hello")))); + assert!(tokens.iter().any(|t| matches!(t, Token::EndDocument))); + } + + #[test] + fn trailing_empty_group_is_an_argument() { + let tokens = tokenize(r"\LaTeX{}"); + assert_eq!( + tokens, + vec![Token::Command { + name: "LaTeX".to_string(), + args: vec!["".to_string()], + }] + ); + } + + #[test] + fn tokenize_document_traverses_input_files() { + let dir = tempfile::TempDir::new().unwrap(); + std::fs::write( + dir.path().join("main.tex"), + "\\input{ch1}\n\\begin{document}\nMain\\end{document}", + ) + .unwrap(); + std::fs::write(dir.path().join("ch1.tex"), "% ch1\nText in chapter").unwrap(); + + let files = tokenize_document(dir.path(), "main.tex"); + assert_eq!(files.len(), 2); + + let ch1 = files.iter().find(|f| f.path.ends_with("ch1.tex")).unwrap(); + assert!(ch1 + .tokens + .iter() + .any(|t| matches!(t, Token::Comment(c) if c == "% ch1"))); + + let main = files.iter().find(|f| f.path.ends_with("main.tex")).unwrap(); + assert!(main + .tokens + .iter() + .any(|t| matches!(t, Token::BeginDocument))); + } + + #[test] + fn tokenize_agrees_with_tokenize_with_spans() { + let src = r"before $x+1$ after \textit{emph} 50% done"; + let plain = tokenize(src); + let spanned = tokenize_with_spans(src); + let projected: Vec<Token> = spanned.tokens.iter().map(|s| s.token.clone()).collect(); + assert_eq!(plain, projected); + } + + #[test] + fn spans_cover_text_comments_and_commands() { + let src = "hola % c\n\\ref{fig:1}"; + let spanned = tokenize_with_spans(src).tokens; + assert_eq!( + spanned, + vec![ + SpannedToken { + token: Token::Text("hola ".to_string()), + start: 0, + end: 5, + }, + SpannedToken { + token: Token::Comment("% c".to_string()), + start: 5, + end: 8, + }, + SpannedToken { + token: Token::Text("\n".to_string()), + start: 8, + end: 9, + }, + SpannedToken { + token: Token::Command { + name: "ref".to_string(), + args: vec!["fig:1".to_string()], + }, + start: 9, + end: 20, + }, + ] + ); + } + + #[test] + fn prose_command_args_get_absolute_spans() { + let src = "\\textit{hello world}"; + let spanned = tokenize_with_spans(src).tokens; + let text = spanned + .iter() + .find(|s| matches!(s.token, Token::Text(_))) + .unwrap(); + assert_eq!( + *text, + SpannedToken { + token: Token::Text("hello world".to_string()), + start: 8, + end: 19, + } + ); + assert_eq!(&src[text.start..text.end], "hello world"); + } + + #[test] + fn href_link_text_gets_absolute_spans() { + let src = r"\href{https://example.com}{Example}"; + let spanned = tokenize_with_spans(src).tokens; + let text = spanned + .iter() + .find(|s| matches!(s.token, Token::Text(_))) + .unwrap(); + assert_eq!(&src[text.start..text.end], "Example"); + } + + #[test] + fn verb_commands_are_not_prose() { + let src = r"a \verb|b_c & d| e \verb*#$x$# f \lstinline|l_m| g"; + let tokens = tokenize(src); + let has_prose = tokens + .iter() + .any(|t| matches!(t, Token::Text(s) if s.contains('_'))); + assert!(!has_prose); + assert!(tokens.iter().any(|t| matches!( + t, + Token::Command { name, args } if name == "verb" && args.contains(&"b_c & d".to_string()) + ))); + assert!(tokens.iter().any(|t| matches!( + t, + Token::Command { name, args } if name == "verb" && args.contains(&"$x$".to_string()) + ))); + assert!(tokens.iter().any(|t| matches!( + t, + Token::Command { name, args } if name == "lstinline" && args.contains(&"l_m".to_string()) + ))); + } + + #[test] + fn starred_math_environments_are_stripped() { + for env in [ + "equation*", + "align*", + "alignat*", + "gather*", + "multline*", + "flalign*", + ] { + let src = format!("before \\begin{{{env}}}a &= b_{{0}}\\end{{{env}}} after"); + let tokens = tokenize(&src); + assert_eq!( + tokens, + vec![ + Token::Text("before ".to_string()), + Token::BeginMath, + Token::EndMath, + Token::Text(" after".to_string()), + ], + "starred math env: {env}" + ); + } + } + + #[test] + fn unclosed_dollar_math_is_reported() { + let src = "costo: $5 dolares\nfin"; + let tokenized = tokenize_with_spans(src); + assert_eq!(tokenized.unclosed_math, vec![7]); + assert_eq!(&src[7..8], "$"); + } + + #[test] + fn closed_dollar_math_is_not_reported() { + let tokenized = tokenize_with_spans(r"El valor es $x^2$"); + assert!(tokenized.unclosed_math.is_empty()); + } + + #[test] + fn unclosed_dollar_in_prose_command_is_reported_absolutely() { + let src = r"\textit{costo $5}"; + let tokenized = tokenize_with_spans(src); + assert_eq!(tokenized.unclosed_math, vec![14]); + assert_eq!(&src[14..15], "$"); + } + + #[test] + fn plain_title_is_unchanged() { + assert_eq!(resolve_section_title("Introduction"), "Introduction"); + } + + #[test] + fn href_title_keeps_link_text_not_url() { + assert_eq!( + resolve_section_title(r"\href{https://univerlab.org}{UniverLab.org}"), + "UniverLab.org" + ); + } + + #[test] + fn textit_and_textbf_keep_their_argument() { + assert_eq!(resolve_section_title(r"\textit{Hello}"), "Hello"); + assert_eq!(resolve_section_title(r"\textbf{Hello}"), "Hello"); + } + + #[test] + fn textcolor_drops_color_keeps_text() { + assert_eq!( + resolve_section_title(r"\textcolor{lightgray}{Hello}"), + "Hello" + ); + } + + #[test] + fn visual_macros_are_dropped_entirely() { + assert_eq!(resolve_section_title(r"\leaders\hbox{.}\hfill"), ""); + assert_eq!( + resolve_section_title(r"Before \leaders\hbox{.}\hfill{}After"), + "Before After" + ); + } + + #[test] + fn unknown_macro_keeps_textual_content() { + assert_eq!(resolve_section_title(r"\foo{bar}"), "bar"); + } + + #[test] + fn nested_macros_resolve_to_innermost_text() { + assert_eq!(resolve_section_title(r"\textbf{\href{u}{X}}"), "X"); + } + + #[test] + fn whitespace_and_newlines_collapse_to_single_spaces() { + assert_eq!( + resolve_section_title("AI Engineer en Accenture\n\\textcolor{lightgray}{\\leaders\\hbox{.}\\hfill}\n\\textit{Julio 2026 -- Actual}"), + "AI Engineer en Accenture Julio 2026 -- Actual" + ); + } + + #[test] + fn title_resolution_survives_unbalanced_braces() { + assert_eq!(resolve_section_title(r"\textit{Hello"), "Hello"); + assert_eq!(resolve_section_title("Hello}"), "Hello"); + } + + #[test] + fn evidence_heading_resolves_via_tokenize() { + let tokens = tokenize(r"\section{\href{https://univerlab.org}{UniverLab.org}}"); + assert_eq!( + tokens, + vec![Token::Section { + level: 2, + title: "UniverLab.org".to_string(), + raw_title: r"\href{https://univerlab.org}{UniverLab.org}".to_string(), + }] + ); + } + + #[test] + fn escaped_ampersand_resolves_to_literal_character() { + assert_eq!(resolve_section_title(r"\&"), "&"); + } + + #[test] + fn escaped_percent_resolves_to_literal_character() { + assert_eq!(resolve_section_title(r"\%"), "%"); + } + + #[test] + fn escaped_dollar_resolves_to_literal_character() { + assert_eq!(resolve_section_title(r"\$"), "$"); + } + + #[test] + fn escaped_hash_resolves_to_literal_character() { + assert_eq!(resolve_section_title(r"\#"), "#"); + } + + #[test] + fn escaped_underscore_resolves_to_literal_character() { + assert_eq!(resolve_section_title(r"\_"), "_"); + } + + #[test] + fn escaped_open_brace_resolves_to_literal_character() { + assert_eq!(resolve_section_title(r"\{"), "{"); + } + + #[test] + fn escaped_close_brace_resolves_to_literal_character() { + assert_eq!(resolve_section_title(r"\}"), "}"); + } + + #[test] + fn escaped_specials_survive_inside_wrapping_macros() { + assert_eq!( + resolve_section_title(r"\textit{Fundador \& Lead Engineer}"), + "Fundador & Lead Engineer" + ); + } + + #[test] + fn evidence_escaped_ampersand_resolves_via_tokenize() { + let tokens = tokenize(r"\subsection*{\textit{Fundador \& Lead Engineer}}"); + assert_eq!( + tokens, + vec![Token::Section { + level: 3, + title: "Fundador & Lead Engineer".to_string(), + raw_title: r"\textit{Fundador \& Lead Engineer}".to_string(), + }] + ); + } +} diff --git a/src/texutil.rs b/src/texutil.rs new file mode 100644 index 0000000..d4a82f6 --- /dev/null +++ b/src/texutil.rs @@ -0,0 +1,138 @@ +//! Shared helpers for scanning LaTeX text. +//! +//! These were extracted from [`crate::linter`] so that the linter and the +//! tokenizer ([`crate::texparse`]) agree on comment stripping and `\input` +//! traversal instead of maintaining two independent copies. + +use std::path::{Path, PathBuf}; + +/// Result of a recursive `\input` traversal. +#[derive(Debug, Default)] +pub struct TexFileCollection { + /// All `.tex` files reachable from the entry point, in traversal order. + pub files: Vec<PathBuf>, + /// `\input` targets that referenced an already-visited file. Each entry is + /// the `(entry, resolved_path)` pair that triggered the cycle. + pub circular: Vec<(String, PathBuf)>, +} + +/// Resolve a tex input path, adding `.tex` extension if missing. +pub fn resolve_tex_path(root: &Path, input: &str) -> PathBuf { + let p = root.join(input); + if p.extension().is_some() { + p + } else { + p.with_extension("tex") + } +} + +/// Remove empty LaTeX groups (`{}`) from a source token. They produce no +/// glyph — `workf{}lows` is the recommended fix for the ligature `workflows`, +/// so it must be searched for as `workflows`, not penalized for following +/// the tool's own suggestion. Shared by the PDF fidelity check and the spell +/// checker so both treat the idiom the same way. +pub fn strip_empty_groups(word: &str) -> String { + word.replace("{}", "") +} + +/// Strip a LaTeX comment from a line: everything after an unescaped `%`. +pub fn strip_comment(line: &str) -> String { + let mut result = String::with_capacity(line.len()); + let mut prev_backslash = false; + + for c in line.chars() { + if c == '%' && !prev_backslash { + break; + } + prev_backslash = c == '\\'; + result.push(c); + } + + result +} + +/// Extract arguments from `\command{arg}` and `\command[opts]{arg}` occurrences in a line. +pub fn extract_commands<'a>(line: &'a str, cmd: &str) -> Vec<&'a str> { + let mut results = Vec::new(); + let pattern = format!("\\{}", cmd); + let mut search = line; + + while let Some(pos) = search.find(&pattern) { + let after = &search[pos + pattern.len()..]; + // Skip optional args [...] + let after = if after.starts_with('[') { + match after.find(']') { + Some(end) => &after[end + 1..], + None => break, + } + } else { + after + }; + if after.starts_with('{') { + if let Some(end) = after.find('}') { + let arg = after[1..end].trim(); + if !arg.is_empty() { + results.push(arg); + } + search = &after[end + 1..]; + continue; + } + } + search = after; + } + + results +} + +/// Recursively collect `.tex` files referenced by `\input{}` from `entry`. +pub fn collect_tex_files(root: &Path, entry: &str) -> TexFileCollection { + let mut collection = TexFileCollection::default(); + collect_tex_files_inner(root, entry, &mut collection); + collection +} + +fn collect_tex_files_inner(root: &Path, entry: &str, collection: &mut TexFileCollection) { + let path = resolve_tex_path(root, entry); + if !path.exists() { + return; + } + if collection.files.contains(&path) { + collection.circular.push((entry.to_string(), path)); + return; + } + collection.files.push(path.clone()); + + if let Ok(content) = std::fs::read_to_string(&path) { + for line in content.lines() { + let line = strip_comment(line); + for input in extract_commands(&line, "input") { + collect_tex_files_inner(root, input, collection); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn strip_empty_groups_joins_the_ligature_workaround() { + assert_eq!(strip_empty_groups("workf{}lows"), "workflows"); + assert_eq!(strip_empty_groups("Artif{}icial"), "Artificial"); + assert_eq!(strip_empty_groups("MLf{}low"), "MLflow"); + } + + #[test] + fn strip_empty_groups_handles_leading_trailing_and_doubled_groups() { + assert_eq!(strip_empty_groups("{}Word"), "Word"); + assert_eq!(strip_empty_groups("Word{}"), "Word"); + assert_eq!(strip_empty_groups("Mid{}dle{}Point"), "MiddlePoint"); + } + + #[test] + fn strip_empty_groups_leaves_non_empty_groups_alone() { + assert_eq!(strip_empty_groups("\\textit{foo}"), "\\textit{foo}"); + assert_eq!(strip_empty_groups("plain"), "plain"); + } +} diff --git a/src/version_checker.rs b/src/version_checker.rs index 2c25423..e26956a 100644 --- a/src/version_checker.rs +++ b/src/version_checker.rs @@ -7,6 +7,8 @@ use crate::version::SemVer; use anyhow::{anyhow, Context, Result}; use serde::Deserialize; +use std::io::Read; +use std::path::{Path, PathBuf}; /// GitHub API release response (minimal fields) #[derive(Debug, Deserialize)] @@ -91,9 +93,13 @@ fn fetch_latest_stable_release(owner: &str, repo: &str) -> Result<Option<SemVer> /// Get the download URL for a specific release pub fn get_release_download_url(owner: &str, repo: &str, version: &SemVer) -> String { - let arch = get_architecture(); - let _os = get_os(); - let filename = format!("{}-{}-{}", repo, version, arch); + // The release workflow names its assets `<repo>-v<version>-<target>.<ext>`, + // e.g. `texforge-v0.7.0-x86_64-unknown-linux-musl.tar.gz`. Three things + // have to line up or the URL 404s: the `v` before the version, the FULL + // target triple (not the bare architecture), and the archive extension. + // Verified against the published assets of v0.7.0 on 2026-08-10. + let (target, ext) = release_target(); + let filename = format!("{repo}-v{version}-{target}.{ext}"); format!( "https://github.com/{}/{}/releases/download/v{}/{}", @@ -101,6 +107,31 @@ pub fn get_release_download_url(owner: &str, repo: &str, version: &SemVer) -> St ) } +/// The rust target triple the release workflow builds for this platform, and +/// the archive extension it uses. Kept next to `get_architecture`/`get_os` +/// because those two answer a different question — what machine we are on — +/// and neither is enough to name a release asset on its own. +fn release_target() -> (&'static str, &'static str) { + #[cfg(all(target_os = "linux", target_arch = "x86_64"))] + return ("x86_64-unknown-linux-musl", "tar.gz"); + #[cfg(all(target_os = "linux", target_arch = "aarch64"))] + return ("aarch64-unknown-linux-musl", "tar.gz"); + #[cfg(all(target_os = "macos", target_arch = "x86_64"))] + return ("x86_64-apple-darwin", "tar.gz"); + #[cfg(all(target_os = "macos", target_arch = "aarch64"))] + return ("aarch64-apple-darwin", "tar.gz"); + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + return ("x86_64-pc-windows-msvc", "zip"); + #[cfg(not(any( + all(target_os = "linux", target_arch = "x86_64"), + all(target_os = "linux", target_arch = "aarch64"), + all(target_os = "macos", target_arch = "x86_64"), + all(target_os = "macos", target_arch = "aarch64"), + all(target_os = "windows", target_arch = "x86_64"), + )))] + return ("unknown", "tar.gz"); +} + fn get_architecture() -> &'static str { #[cfg(target_arch = "x86_64")] return "x86_64"; @@ -129,6 +160,170 @@ fn get_os() -> &'static str { } } +// ── Cargo-managed install detection ──────────────────────────────── +// +// A self-updater that writes to a hardcoded directory creates a second +// binary whenever the user installed with `cargo install` instead — and +// which copy actually runs then depends on PATH order. Refusing to touch a +// cargo-managed binary avoids that: cargo keeps its own metadata about what +// it manages at that path, and overwriting the file behind its back leaves +// cargo believing it still owns a binary it no longer produced. + +/// The environment inputs used to resolve where `cargo install` places +/// binaries. Kept as a struct (rather than reading `std::env` directly) +/// so the precedence rules can be tested without mutating process-global +/// environment variables. +struct CargoRootEnv { + install_root: Option<String>, + cargo_home: Option<String>, + home: Option<PathBuf>, +} + +/// `CARGO_INSTALL_ROOT` wins over `CARGO_HOME`, which wins over `~/.cargo` — +/// the same precedence cargo itself uses to decide where `cargo install` +/// places binaries. +fn resolve_cargo_bin_dir(env: &CargoRootEnv) -> Option<PathBuf> { + if let Some(root) = &env.install_root { + return Some(PathBuf::from(root).join("bin")); + } + if let Some(home) = &env.cargo_home { + return Some(PathBuf::from(home).join("bin")); + } + env.home.as_ref().map(|h| h.join(".cargo").join("bin")) +} + +/// True if `exe_path` resolves inside the cargo install root. +/// +/// Both sides are canonicalised before comparing, since `exe_path` (from +/// `std::env::current_exe`) can be a symlink. A path that fails to +/// canonicalise — missing, broken symlink, permission denied — is treated +/// as "not cargo": a false positive here blocks a legitimate update, which +/// is worse than the false negative of overwriting the same file the user +/// was already running. +fn is_cargo_managed(exe_path: &Path, env: &CargoRootEnv) -> bool { + let Some(cargo_bin) = resolve_cargo_bin_dir(env) else { + return false; + }; + let Ok(canon_exe) = exe_path.canonicalize() else { + return false; + }; + let Ok(canon_cargo_bin) = cargo_bin.canonicalize() else { + return false; + }; + canon_exe.starts_with(&canon_cargo_bin) +} + +/// True if the currently running binary was installed with `cargo install`. +pub fn current_exe_is_cargo_managed(exe_path: &Path) -> bool { + let env = CargoRootEnv { + install_root: std::env::var("CARGO_INSTALL_ROOT") + .ok() + .filter(|s| !s.is_empty()), + cargo_home: std::env::var("CARGO_HOME").ok().filter(|s| !s.is_empty()), + home: dirs::home_dir(), + }; + is_cargo_managed(exe_path, &env) +} + +// ── Download + install ────────────────────────────────────────────── + +/// Extract the binary named `bin_name` from a `.tar.gz` archive read from +/// `reader`, writing it to `dest`. Rejects a missing or empty entry so a +/// truncated or corrupt download never reaches the replace step. +fn extract_binary_from_tar_gz<R: Read>(reader: R, bin_name: &str, dest: &Path) -> Result<()> { + let decoder = flate2::read::GzDecoder::new(reader); + let mut archive = tar::Archive::new(decoder); + + for entry in archive.entries().context("failed to read archive")? { + let mut entry = entry.context("failed to read archive entry")?; + let path = entry.path().context("failed to read archive entry path")?; + if path.file_name().is_some_and(|n| n == bin_name) { + entry + .unpack(dest) + .context("failed to extract binary from archive")?; + let size = std::fs::metadata(dest) + .context("failed to read extracted binary metadata")? + .len(); + if size == 0 { + anyhow::bail!("extracted binary '{bin_name}' is empty"); + } + return Ok(()); + } + } + + anyhow::bail!("binary '{bin_name}' not found in downloaded archive") +} + +/// Move `new_bin` into `current_exe`'s place. Renames when possible (fast, +/// same-filesystem, and keeps the source's file mode); falls back to copy +/// for the cross-filesystem case. +fn replace_binary(new_bin: &Path, current_exe: &Path) -> Result<()> { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(new_bin, std::fs::Permissions::from_mode(0o755)) + .context("failed to mark downloaded binary as executable")?; + } + + if std::fs::rename(new_bin, current_exe).is_err() { + std::fs::copy(new_bin, current_exe).with_context(|| { + format!( + "failed to replace binary at {} (check that you have write permission to this location)", + current_exe.display() + ) + })?; + } + + Ok(()) +} + +/// Extract the binary named `bin_name` from the archive read from `reader` +/// and atomically replace `current_exe` with it. +/// +/// The archive is extracted to a temporary file beside `current_exe` (same +/// directory, so the common case is a same-filesystem rename) and verified +/// before anything is replaced. On any failure the temporary file is +/// removed and `current_exe` is left untouched. +fn replace_from_reader<R: Read>(reader: R, bin_name: &str, current_exe: &Path) -> Result<()> { + let parent = current_exe.parent().unwrap_or_else(|| Path::new(".")); + let tmp = tempfile::Builder::new() + .prefix(".texforge-update-") + .tempfile_in(parent) + .context("failed to create temporary file next to the running binary")? + .into_temp_path(); + + extract_binary_from_tar_gz(reader, bin_name, &tmp)?; + replace_binary(&tmp, current_exe)?; + + Ok(()) +} + +/// Download the release archive for the running platform, verify it, and +/// atomically replace `current_exe` with it. +/// +/// Callers are expected to check `current_exe_is_cargo_managed` first — +/// this function always downloads and writes when called. +pub fn download_and_replace( + owner: &str, + repo: &str, + version: &SemVer, + current_exe: &Path, +) -> Result<()> { + let url = get_release_download_url(owner, repo, version); + + let response = reqwest::blocking::Client::new() + .get(&url) + .header("User-Agent", repo) + .send() + .context("failed to download update")?; + + if !response.status().is_success() { + anyhow::bail!("download failed: HTTP {}", response.status()); + } + + replace_from_reader(response, repo, current_exe) +} + #[cfg(test)] mod tests { use super::*; @@ -209,6 +404,24 @@ mod tests { assert!(url.contains(arch)); } + /// The asset name has to match what the release workflow publishes, + /// byte for byte, or the printed URL 404s. It did, until 2026-08-10. + #[test] + fn download_url_matches_the_published_asset_name() { + let version = SemVer::parse("0.7.0").unwrap(); + let url = get_release_download_url("UniverLab", "texforge", &version); + let (target, ext) = release_target(); + + assert!( + url.ends_with(&format!("/texforge-v0.7.0-{target}.{ext}")), + "asset name drifted from the release workflow: {url}" + ); + assert!( + url.contains("/releases/download/v0.7.0/"), + "tag path lost its v prefix: {url}" + ); + } + #[test] fn test_get_release_download_url_format() { let version = SemVer::parse("2.5.1").unwrap(); @@ -261,4 +474,202 @@ mod tests { assert_eq!(result.local_version, cloned.local_version); assert_eq!(result.update_available, cloned.update_available); } + + // ── Cargo-managed install detection ────────────────────────── + + #[test] + fn install_root_env_wins_over_cargo_home_and_default_home() { + let env = CargoRootEnv { + install_root: Some("/install-root".to_string()), + cargo_home: Some("/cargo-home".to_string()), + home: Some(PathBuf::from("/home/user")), + }; + assert_eq!( + resolve_cargo_bin_dir(&env), + Some(PathBuf::from("/install-root/bin")) + ); + } + + #[test] + fn cargo_home_env_wins_over_default_home() { + let env = CargoRootEnv { + install_root: None, + cargo_home: Some("/cargo-home".to_string()), + home: Some(PathBuf::from("/home/user")), + }; + assert_eq!( + resolve_cargo_bin_dir(&env), + Some(PathBuf::from("/cargo-home/bin")) + ); + } + + #[test] + fn default_home_cargo_dir_used_when_no_env_vars_set() { + let env = CargoRootEnv { + install_root: None, + cargo_home: None, + home: Some(PathBuf::from("/home/user")), + }; + assert_eq!( + resolve_cargo_bin_dir(&env), + Some(PathBuf::from("/home/user/.cargo/bin")) + ); + } + + #[test] + fn path_inside_cargo_root_is_detected_as_cargo_managed() { + let tmp = tempfile::tempdir().unwrap(); + let cargo_root = tmp.path().join("cargo-home"); + let bin_dir = cargo_root.join("bin"); + std::fs::create_dir_all(&bin_dir).unwrap(); + let exe = bin_dir.join("texforge"); + std::fs::write(&exe, b"fake").unwrap(); + + let env = CargoRootEnv { + install_root: None, + cargo_home: Some(cargo_root.to_string_lossy().to_string()), + home: None, + }; + assert!(is_cargo_managed(&exe, &env)); + } + + #[test] + fn path_outside_cargo_root_is_not_cargo_managed() { + let tmp = tempfile::tempdir().unwrap(); + let cargo_root = tmp.path().join("cargo-home"); + std::fs::create_dir_all(cargo_root.join("bin")).unwrap(); + let elsewhere = tmp.path().join("elsewhere"); + std::fs::create_dir_all(&elsewhere).unwrap(); + let exe = elsewhere.join("texforge"); + std::fs::write(&exe, b"fake").unwrap(); + + let env = CargoRootEnv { + install_root: None, + cargo_home: Some(cargo_root.to_string_lossy().to_string()), + home: None, + }; + assert!(!is_cargo_managed(&exe, &env)); + } + + #[test] + fn uncanonicalizable_path_is_treated_as_not_cargo_managed() { + let env = CargoRootEnv { + install_root: None, + cargo_home: Some("/nonexistent-cargo-home-xyz-texforge-test".to_string()), + home: None, + }; + let missing_exe = Path::new("/nonexistent-path-xyz-texforge-test/texforge"); + assert!(!is_cargo_managed(missing_exe, &env)); + } + + // ── Download + install ─────────────────────────────────────── + + /// Build a `.tar.gz` archive in memory containing the given entries. + fn build_tar_gz(entries: &[(&str, &[u8])]) -> Vec<u8> { + use std::io::Write; + + let mut tar_data = Vec::new(); + { + let mut builder = tar::Builder::new(&mut tar_data); + for (name, content) in entries { + let mut header = tar::Header::new_gnu(); + header.set_size(content.len() as u64); + header.set_mode(0o755); + header.set_cksum(); + builder.append_data(&mut header, name, *content).unwrap(); + } + builder.finish().unwrap(); + } + + let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); + encoder.write_all(&tar_data).unwrap(); + encoder.finish().unwrap() + } + + #[test] + fn valid_archive_replaces_binary_and_sets_executable() { + let tmp = tempfile::tempdir().unwrap(); + let current = tmp.path().join("texforge"); + std::fs::write(¤t, b"old binary content").unwrap(); + + let archive = build_tar_gz(&[("texforge", b"new binary content")]); + replace_from_reader(std::io::Cursor::new(archive), "texforge", ¤t).unwrap(); + + assert_eq!(std::fs::read(¤t).unwrap(), b"new binary content"); + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mode = std::fs::metadata(¤t).unwrap().permissions().mode(); + assert_eq!(mode & 0o777, 0o755); + } + } + + #[test] + fn missing_binary_entry_leaves_original_untouched() { + let tmp = tempfile::tempdir().unwrap(); + let current = tmp.path().join("texforge"); + std::fs::write(¤t, b"original content").unwrap(); + + let archive = build_tar_gz(&[("readme.txt", b"not a binary")]); + let result = replace_from_reader(std::io::Cursor::new(archive), "texforge", ¤t); + + assert!(result.is_err()); + assert_eq!(std::fs::read(¤t).unwrap(), b"original content"); + } + + #[test] + fn empty_binary_entry_is_rejected_and_original_untouched() { + let tmp = tempfile::tempdir().unwrap(); + let current = tmp.path().join("texforge"); + std::fs::write(¤t, b"original content").unwrap(); + + let archive = build_tar_gz(&[("texforge", b"")]); + let result = replace_from_reader(std::io::Cursor::new(archive), "texforge", ¤t); + + assert!(result.is_err()); + assert_eq!(std::fs::read(¤t).unwrap(), b"original content"); + } + + #[test] + fn temp_file_does_not_survive_a_failed_install() { + let tmp = tempfile::tempdir().unwrap(); + let current = tmp.path().join("texforge"); + std::fs::write(¤t, b"original content").unwrap(); + + let archive = build_tar_gz(&[("readme.txt", b"not a binary")]); + let result = replace_from_reader(std::io::Cursor::new(archive), "texforge", ¤t); + assert!(result.is_err()); + + let leftovers: Vec<_> = std::fs::read_dir(tmp.path()) + .unwrap() + .filter_map(|e| e.ok()) + .filter(|e| { + e.file_name() + .to_string_lossy() + .starts_with(".texforge-update-") + }) + .collect(); + assert!(leftovers.is_empty(), "leftover temp files: {leftovers:?}"); + } + + #[test] + fn replace_binary_in_tmp_dir_is_executable() { + let tmp = tempfile::tempdir().unwrap(); + let new_bin = tmp.path().join("new-bin"); + std::fs::write(&new_bin, b"new content").unwrap(); + let current = tmp.path().join("current-bin"); + std::fs::write(¤t, b"old content").unwrap(); + + replace_binary(&new_bin, ¤t).unwrap(); + + assert_eq!(std::fs::read(¤t).unwrap(), b"new content"); + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mode = std::fs::metadata(¤t).unwrap().permissions().mode(); + assert_eq!(mode & 0o777, 0o755); + } + } } diff --git a/src/wordcount.rs b/src/wordcount.rs new file mode 100644 index 0000000..1eb8a68 --- /dev/null +++ b/src/wordcount.rs @@ -0,0 +1,433 @@ +//! Document word counts built on the [`crate::texparse`] tokenizer. +//! +//! This is the first production consumer of the tokenizer. It turns a flat +//! [`Token`] stream into per-section and per-file word counts in a single pass. +//! +//! # Word definition +//! +//! A word is a maximal run of non-space characters containing at least one +//! alphabetic character ([`char::is_alphabetic`]). `Introduccion` counts, +//! `50` does not, and `fig:cap` does (a mild false positive, accepted for v1). +//! +//! # What is counted +//! +//! The tokenizer has already stripped math, verbatim regions and comments from +//! the stream and demoted non-prose command arguments to [`Token::Command`], so +//! the counter only ever sees prose: body text, section titles, prose-command +//! arguments (`\textit`, `\textbf`, `\emph`, `\footnote`, `\caption`, and the +//! link text of `\href`), and text inside environments such as `figure`, +//! `table`, `tabular` and `thebibliography`. +//! +//! The preamble (everything before the first [`Token::BeginDocument`]) is not +//! counted. Text between `\begin{document}` and the first section is reported +//! as [`DocumentStats::preamble_words`] and attributed to no section. +//! +//! # Limitations +//! +//! * A document's files are consumed in `\input` traversal order as one logical +//! stream, so `\begin{document}` is a one-way switch: text in a file whose +//! `\input` appears in the preamble is still counted. A mild inflation, in +//! the same accepted family as `\tableofcontents` title duplication. +//! * Words in a section title are counted after re-tokenizing the title, so +//! math inside a title is excluded. +//! * `--by file` mode reports one entry per `.tex` file, whether or not it +//! contains counted text. + +use std::path::Path; + +use serde::Serialize; + +use crate::texparse::{tokenize, SectionTracker, Token, TokenizedFile}; + +/// One counted section, or one `.tex` file in `--by file` mode. +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +pub struct SectionStat { + /// Dotted section number (`1`, `1.1`, `2.1.1`) in section mode; the + /// `.tex` file name in `--by file` mode. + pub path: String, + /// Section level (`0` = part, ..., `6` = subparagraph) in section mode. + #[serde(skip_serializing_if = "Option::is_none")] + pub level: Option<u8>, + /// Raw section title, kept out of the JSON contract. + #[serde(skip)] + pub title: Option<String>, + /// Word count for this section or file. + pub words: usize, +} + +/// Complete statistics for a document. +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +pub struct DocumentStats { + /// Document name (the project title). + pub document: String, + /// Total counted words across all files. + pub total_words: usize, + /// Number of `.tex` files scanned. + pub file_count: usize, + /// Words between `\begin{document}` and the first section. + pub preamble_words: usize, + /// Per-section or per-file breakdown. + pub sections: Vec<SectionStat>, +} + +/// Count the words in a text run. +/// +/// A word is a maximal run of non-space characters containing at least one +/// alphabetic character. +pub fn count_words(text: &str) -> usize { + text.split_whitespace() + .filter(|word| word.chars().any(char::is_alphabetic)) + .count() +} + +/// Count the words of a section title after re-tokenizing it, so math and +/// commands inside the title do not count. +fn count_title_words(title: &str) -> usize { + tokenize(title) + .into_iter() + .filter_map(|token| match token { + Token::Text(text) => Some(count_words(&text)), + _ => None, + }) + .sum() +} + +/// Produces dotted section numbers such as `1`, `1.1` and `2.1.1`. +/// +/// Numbering lives in [`crate::texparse::SectionTracker`], shared with the +/// `outline` command so both JSON contracts number sections identically. +type Section = (u8, String, String, usize); + +/// Word counts collected in one pass over every token stream. +struct RawCounts { + preamble_words: usize, + sections: Vec<Section>, + file_words: Vec<usize>, +} + +/// Single-pass counting over all token streams, in `\input` traversal order. +fn count_files(files: &[TokenizedFile]) -> RawCounts { + let mut tracker = SectionTracker::new(6); + let mut preamble_words = 0usize; + let mut sections: Vec<Section> = Vec::new(); + let mut file_words = vec![0usize; files.len()]; + + let mut in_document = false; + let mut current: Option<Section> = None; + + for (file_idx, file) in files.iter().enumerate() { + for token in &file.tokens { + match token { + Token::BeginDocument => in_document = true, + Token::Section { level, title, .. } => { + if !in_document { + continue; + } + if let Some(finished) = current.take() { + sections.push(finished); + } + let number = tracker.enter(*level); + let title_words = count_title_words(title); + file_words[file_idx] += title_words; + current = Some((*level, number, title.clone(), title_words)); + } + Token::Text(text) => { + if !in_document { + continue; + } + let words = count_words(text); + if words == 0 { + continue; + } + file_words[file_idx] += words; + match &mut current { + Some((_, _, _, words_here)) => *words_here += words, + None => preamble_words += words, + } + } + _ => {} + } + } + } + if let Some(finished) = current { + sections.push(finished); + } + + RawCounts { + preamble_words, + sections, + file_words, + } +} + +/// Count words per section for a document. +pub fn count_document(document: &str, files: &[TokenizedFile]) -> DocumentStats { + let raw = count_files(files); + let sections: Vec<SectionStat> = raw + .sections + .into_iter() + .map(|(level, number, title, words)| SectionStat { + path: number, + level: Some(level), + title: Some(title), + words, + }) + .collect(); + let total_words = raw.preamble_words + sections.iter().map(|s| s.words).sum::<usize>(); + DocumentStats { + document: document.to_string(), + total_words, + file_count: files.len(), + preamble_words: raw.preamble_words, + sections, + } +} + +/// Count words per `.tex` file for a document. +pub fn count_by_file(document: &str, files: &[TokenizedFile]) -> DocumentStats { + let raw = count_files(files); + let sections: Vec<SectionStat> = raw + .file_words + .iter() + .enumerate() + .map(|(file_idx, words)| SectionStat { + path: file_name(&files[file_idx].path), + level: None, + title: None, + words: *words, + }) + .collect(); + let total_words = raw.file_words.iter().sum(); + DocumentStats { + document: document.to_string(), + total_words, + file_count: files.len(), + preamble_words: raw.preamble_words, + sections, + } +} + +/// The file name of a tokenized file. +fn file_name(path: &Path) -> String { + path.file_name() + .map(|name| name.to_string_lossy().into_owned()) + .unwrap_or_default() +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::*; + + fn files_from(fixtures: &[(&str, &str)]) -> Vec<TokenizedFile> { + fixtures + .iter() + .map(|(name, source)| TokenizedFile { + path: PathBuf::from(name), + tokens: tokenize(source), + }) + .collect() + } + + fn paths_and_words(stats: &DocumentStats) -> Vec<(String, usize)> { + stats + .sections + .iter() + .map(|section| (section.path.clone(), section.words)) + .collect() + } + + #[test] + fn word_is_a_run_of_non_space_containing_an_alphabetic_character() { + assert_eq!(count_words("Introduccion"), 1); + assert_eq!(count_words("50"), 0); + assert_eq!(count_words("fig:cap"), 1); + assert_eq!(count_words("Hello world, this is prose."), 5); + assert_eq!(count_words(""), 0); + assert_eq!(count_words(" \t\n"), 0); + } + + #[test] + fn comments_are_not_counted() { + let files = files_from(&[( + "main.tex", + "\\begin{document}\nbody words\n% hidden words here\nmore words\n\\end{document}", + )]); + let stats = count_document("Doc", &files); + assert_eq!(stats.preamble_words, 4); + assert_eq!(stats.total_words, 4); + assert!(stats.sections.is_empty()); + } + + #[test] + fn math_is_not_counted_in_any_form() { + let src = "\\begin{document}\nbefore $x + y$ after\n$$\\int f dx$$\n\\[a+b\\]\n\\(c+d\\)\n\\begin{equation}e=mc^2\\end{equation}\nmore\n\\end{document}"; + let stats = count_document("Doc", &files_from(&[("main.tex", src)])); + assert_eq!(stats.preamble_words, 3); + assert_eq!(stats.total_words, 3); + } + + #[test] + fn verbatim_content_is_not_counted() { + let src = "\\begin{document}\nwords \\begin{verbatim}not words here $math$\\end{verbatim} after\n\\end{document}"; + let stats = count_document("Doc", &files_from(&[("main.tex", src)])); + assert_eq!(stats.preamble_words, 2); + } + + #[test] + fn non_prose_command_arguments_are_not_counted() { + let src = r"\begin{document} +\label{sec:intro} \ref{fig:x} \cite{key} \input{ch1} \includegraphics{img.png} \url{https://example.com/x} \href{https://example.com}{} \index{LaTeX} +body +\end{document}"; + let stats = count_document("Doc", &files_from(&[("main.tex", src)])); + assert_eq!(stats.preamble_words, 1); + assert_eq!(stats.total_words, 1); + } + + #[test] + fn prose_command_text_is_counted() { + let src = r"\begin{document}\section{S}\textit{italic words}\textbf{bold words}\emph{emph words}\footnote{foot words}\caption{cap words}\end{document}"; + let stats = count_document("Doc", &files_from(&[("main.tex", src)])); + assert_eq!(stats.sections[0].words, 11); + } + + #[test] + fn figure_table_and_tabular_text_is_counted() { + let src = r"\begin{document}\section{S} +\begin{figure}\caption{A cat}\includegraphics{cat.png}\end{figure} +\begin{table}\caption{Timeline}\end{table} +\begin{tabular}{cc}cell one & cell two\end{tabular} +\end{document}"; + let stats = count_document("Doc", &files_from(&[("main.tex", src)])); + assert_eq!(stats.sections[0].words, 1 + 2 + 1 + 4); + } + + #[test] + fn bibliography_text_is_counted() { + let src = r"\begin{document}\section{S}body +\begin{thebibliography}{9} +\bibitem{knuth} Knuth, The Art of Computer Programming. +\end{thebibliography} +\end{document}"; + let stats = count_document("Doc", &files_from(&[("main.tex", src)])); + assert_eq!(stats.sections[0].words, 1 + 1 + 6); + } + + #[test] + fn preamble_is_not_counted_but_front_matter_is() { + let src = "\\documentclass{article}\n% preamble prose not counted\n\\begin{document}\nfront matter words\n\\section{S}\nbody\n\\end{document}"; + let stats = count_document("Doc", &files_from(&[("main.tex", src)])); + assert_eq!(stats.file_count, 1); + assert_eq!(stats.preamble_words, 3); + assert_eq!(paths_and_words(&stats), vec![("1".to_string(), 2)]); + assert_eq!(stats.total_words, 5); + } + + #[test] + fn section_numbers_follow_the_hierarchy() { + let src = r"\begin{document} +\section{One} +\subsection{One One} +\subsection{One Two} +\section{Two} +\subsection{Two One} +\subsubsection{Two One One} +\end{document}"; + let stats = count_document("Doc", &files_from(&[("main.tex", src)])); + let paths: Vec<&str> = stats.sections.iter().map(|s| s.path.as_str()).collect(); + assert_eq!(paths, vec!["1", "1.1", "1.2", "2", "2.1", "2.1.1"]); + } + + #[test] + fn part_and_chapter_levels_number_correctly() { + let src = "\\begin{document}\n\\part{One}\\chapter{Ch}\\section{Sec}\\section{Sec2}\\chapter{Ch2}\\section{Sec3}\n\\end{document}"; + let stats = count_document("Doc", &files_from(&[("main.tex", src)])); + let paths: Vec<&str> = stats.sections.iter().map(|s| s.path.as_str()).collect(); + assert_eq!(paths, vec!["1", "1.1", "1.1.1", "1.1.2", "1.2", "1.2.1"]); + } + + #[test] + fn section_title_words_are_attributed_to_their_section() { + let src = r"\begin{document} +\section{Introduction and Background} +text here +\section{Results} +more text +\end{document}"; + let stats = count_document("Doc", &files_from(&[("main.tex", src)])); + assert_eq!( + paths_and_words(&stats), + vec![("1".to_string(), 5), ("2".to_string(), 3)] + ); + } + + #[test] + fn math_inside_a_section_title_is_not_counted() { + let src = r"\begin{document}\section{Energy $E=mc^2$}body\end{document}"; + let stats = count_document("Doc", &files_from(&[("main.tex", src)])); + assert_eq!(stats.sections[0].words, 2); + } + + #[test] + fn sections_from_input_files_are_attributed_to_the_document() { + let files = files_from(&[ + ( + "main.tex", + "\\begin{document}\nfront matter\n\\input{ch1}\n\\end{document}", + ), + ("ch1.tex", "\\section{One}\nbody"), + ]); + let stats = count_document("Doc", &files); + assert_eq!(stats.preamble_words, 2); + assert_eq!(paths_and_words(&stats), vec![("1".to_string(), 2)]); + assert_eq!(stats.total_words, 4); + } + + #[test] + fn by_file_breaks_down_per_tex_file() { + let files = files_from(&[ + ( + "main.tex", + "\\begin{document}\nfront matter\n\\input{ch1}\n\\end{document}", + ), + ("ch1.tex", "\\section{One}\nbody"), + ]); + let stats = count_by_file("Doc", &files); + assert_eq!(stats.total_words, 4); + assert_eq!(stats.preamble_words, 2); + assert_eq!(stats.file_count, 2); + assert_eq!( + paths_and_words(&stats), + vec![("main.tex".to_string(), 2), ("ch1.tex".to_string(), 2)] + ); + } + + #[test] + fn json_contract_keys_are_stable() { + let files = files_from(&[( + "main.tex", + "\\begin{document}\n\\section{S}\ntext\n\\end{document}", + )]); + let stats = count_document("Doc", &files); + let value = serde_json::to_value(&stats).unwrap(); + let object = value.as_object().unwrap(); + let mut top_keys: Vec<&str> = object.keys().map(String::as_str).collect(); + top_keys.sort_unstable(); + assert_eq!( + top_keys, + vec![ + "document", + "file_count", + "preamble_words", + "sections", + "total_words" + ] + ); + let section = object["sections"][0].as_object().unwrap(); + let mut section_keys: Vec<&str> = section.keys().map(String::as_str).collect(); + section_keys.sort_unstable(); + assert_eq!(section_keys, vec!["level", "path", "words"]); + } +} diff --git a/tests/fixtures/hunspell/mini.aff b/tests/fixtures/hunspell/mini.aff new file mode 100644 index 0000000..0bc9352 --- /dev/null +++ b/tests/fixtures/hunspell/mini.aff @@ -0,0 +1,5 @@ +SET UTF-8 +TRY esianrtolcdugmphbyfvkwz + +SFX S Y 1 +SFX S 0 es . diff --git a/tests/fixtures/hunspell/mini.dic b/tests/fixtures/hunspell/mini.dic new file mode 100644 index 0000000..683bacc --- /dev/null +++ b/tests/fixtures/hunspell/mini.dic @@ -0,0 +1,4 @@ +3 +sol/S +gato +perro diff --git a/tests/fixtures/ligatures.pdf b/tests/fixtures/ligatures.pdf new file mode 100644 index 0000000..5da79a5 Binary files /dev/null and b/tests/fixtures/ligatures.pdf differ diff --git a/tests/fixtures/malformed-date.pdf b/tests/fixtures/malformed-date.pdf new file mode 100644 index 0000000..df44d21 Binary files /dev/null and b/tests/fixtures/malformed-date.pdf differ diff --git a/tests/fixtures/pages-ligatures.pdf b/tests/fixtures/pages-ligatures.pdf new file mode 100644 index 0000000..0a0af39 Binary files /dev/null and b/tests/fixtures/pages-ligatures.pdf differ diff --git a/tests/fixtures/two-page.pdf b/tests/fixtures/two-page.pdf new file mode 100644 index 0000000..c2a34ac Binary files /dev/null and b/tests/fixtures/two-page.pdf differ