diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 531c877..a23fb28 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,6 +27,6 @@ jobs: run: cargo check --no-default-features --features=x11 - name: Build wayland run: cargo check --no-default-features --features=wayland - - name: Build no-x11-wayland - run: cargo check --no-default-features --features=no-x11-wayland + - name: Build no-backend + run: cargo check --no-default-features diff --git a/.gitignore b/.gitignore index ea8c4bf..d03f68d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +/**/target diff --git a/Cargo.lock b/Cargo.lock index 8eb7f9b..41c8076 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,14 @@ version = 3 [[package]] name = "ahash" -version = "0.3.8" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217" +checksum = "43bb833f0bf979d8475d38fbf09ed3b8a55e1885fe93ad3f93239fc6a4f17b98" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] [[package]] name = "aho-corasick" @@ -37,41 +42,17 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.40" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b" +checksum = "28ae2b3dec75a406790005a200b1bd89785afc02517a00ca99ecfe093ee9e6cf" [[package]] -name = "anymap" -version = "0.12.1" +name = "ascii-canvas" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33954243bd79057c2de7338850b85983a44588021f8a5fee574a8888c6de4344" - -[[package]] -name = "arrayvec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" - -[[package]] -name = "async-stream" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a26cb53174ddd320edfff199a853f93d571f48eeb4dde75e67a9a3dbb7b7e5e" +checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" dependencies = [ - "async-stream-impl", - "futures-core", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db134ba52475c060f3329a8ef0f8786d6b872ed01515d4b79c162e5798da1340" -dependencies = [ - "proc-macro2", - "quote 1.0.9", - "syn 1.0.72", + "term", ] [[package]] @@ -125,9 +106,9 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "beef" -version = "0.4.4" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "474a626a67200bd107d44179bb3d4fc61891172d11696609264589be6a0e6a43" +checksum = "bed554bd50246729a1ec158d08aa3235d1b69d94ad120ebe187e28894787e736" [[package]] name = "bincode" @@ -138,24 +119,27 @@ dependencies = [ "serde", ] +[[package]] +name = "bit-set" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" -[[package]] -name = "bitvec" -version = "0.19.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "bytes" version = "1.0.1" @@ -190,9 +174,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.67" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" +checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" [[package]] name = "cfg-if" @@ -221,6 +205,29 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e769b5c8c8283982a987c6e948e540254f1058d5a74b8794914d4ef5fc2a24" +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "console" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3993e6445baa160675931ec041a5e03ca84b9c6e32a056150d3aa2bdda0a1f45" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "terminal_size", + "winapi", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -245,9 +252,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9" +checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" dependencies = [ "cfg-if", "crossbeam-epoch", @@ -256,9 +263,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52fb27eab85b17fbb9f6fd667089e07d6a2eb8743d02639ee7f6a7a7729c9c94" +checksum = "4ec02e091aa634e2c3ada4a392989e7c3116673ef0ac5b72232439094d73b7fd" dependencies = [ "cfg-if", "crossbeam-utils", @@ -269,15 +276,20 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4feb231f0d4d6af81aed15928e58ecf5816aa62a2393e2c82f46973e92a9a278" +checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" dependencies = [ - "autocfg", "cfg-if", "lazy_static", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "ctor" version = "0.1.20" @@ -285,7 +297,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e98e2ad1a782e33928b96fc3948e7c355e5af34ba4de7670fe8bac2a3b2006d" dependencies = [ "quote 1.0.9", - "syn 1.0.72", + "syn 1.0.74", ] [[package]] @@ -300,14 +312,15 @@ dependencies = [ [[package]] name = "derive_more" -version = "0.99.13" +version = "0.99.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f82b1b72f1263f214c0f823371768776c4f5841b942c9883aa8e5ec584fd0ba6" +checksum = "40eebddd2156ce1bb37b20bbe5151340a31828b1f2d22ba4141f3531710e38df" dependencies = [ "convert_case", "proc-macro2", "quote 1.0.9", - "syn 1.0.72", + "rustc_version", + "syn 1.0.74", ] [[package]] @@ -316,12 +329,39 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "doc-comment" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + [[package]] name = "dyn-clone" version = "1.0.4" @@ -334,6 +374,21 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "ena" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7402b94a93c24e742487327a7cd839dc9d36fec9de9fb25b09f2dae459f36c3" +dependencies = [ + "log", +] + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "env_logger" version = "0.7.1" @@ -349,16 +404,17 @@ dependencies = [ [[package]] name = "eww" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", - "async-stream", "base64", "bincode", "cairo-sys-rs", + "codespan-reporting", "debug_stub_derive", "derive_more", "dyn-clone", + "eww_shared_util", "extend", "futures-core", "futures-util", @@ -371,52 +427,56 @@ dependencies = [ "gtk", "gtk-layer-shell", "gtk-layer-shell-sys", - "itertools 0.10.0", - "lazy_static", + "itertools 0.10.1", "libc", "log", "maplit", "nix", - "nom", "notify", - "num", - "pretty_assertions", + "once_cell", "pretty_env_logger", "regex", - "roxmltree", "serde", "serde_json", "simple-signal", + "simplexpr", "smart-default", "structopt", - "strum 0.20.0", - "strum_macros 0.20.1", "sysinfo", "tokio", - "tokio-stream", "tokio-util", "unescape", + "unindent", "wait-timeout", "x11rb", + "yuck", +] + +[[package]] +name = "eww_shared_util" +version = "0.1.0" +dependencies = [ + "derive_more", + "serde", ] [[package]] name = "extend" -version = "1.0.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2feaa8e332f0db2f08788707dc550075aab8e6d20ffc85958e1174e22887d11" +checksum = "f5c89e2933a4ec753dc007a4d6a7f9b6dc8e89b8fe89cabc252ccddf39c08bb1" dependencies = [ "proc-macro-error", "proc-macro2", "quote 1.0.9", - "syn 1.0.72", + "syn 1.0.74", ] [[package]] name = "filetime" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8" +checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98" dependencies = [ "cfg-if", "libc", @@ -425,35 +485,25 @@ dependencies = [ ] [[package]] -name = "fsevent" -version = "2.0.2" +name = "fixedbitset" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97f347202c95c98805c216f9e1df210e8ebaec9fdb2365700a43c10797a35e63" -dependencies = [ - "bitflags", - "fsevent-sys", -] +checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d" [[package]] name = "fsevent-sys" -version = "3.0.2" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a29c77f1ca394c3e73a9a5d24cfcabb734682d9634fc398f2204a63c994120" +checksum = "5c0e564d24da983c053beff1bb7178e237501206840a3e6bf4e267b9e8ae734a" dependencies = [ "libc", ] -[[package]] -name = "funty" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" - [[package]] name = "futures" -version = "0.3.14" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d5813545e459ad3ca1bff9915e9ad7f1a47dc6a91b627ce321d5863b7dd253" +checksum = "1adc00f486adfc9ce99f77d717836f0c5aa84965eb0b4f051f4e83f7cab53f8b" dependencies = [ "futures-channel", "futures-core", @@ -466,9 +516,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.14" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce79c6a52a299137a6013061e0cf0e688fce5d7f1bc60125f520912fdb29ec25" +checksum = "74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9" dependencies = [ "futures-core", "futures-sink", @@ -476,15 +526,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.14" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "098cd1c6dda6ca01650f1a37a794245eb73181d0d4d4e955e2f3c37db7af1815" +checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99" [[package]] name = "futures-executor" -version = "0.3.14" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f6cb7042eda00f0049b1d2080aa4b93442997ee507eb3828e8bd7577f94c9d" +checksum = "4d0d535a57b87e1ae31437b892713aee90cd2d7b0ee48727cd11fc72ef54761c" dependencies = [ "futures-core", "futures-task", @@ -493,40 +543,42 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.14" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "365a1a1fb30ea1c03a830fdb2158f5236833ac81fa0ad12fe35b29cddc35cb04" +checksum = "0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582" [[package]] name = "futures-macro" -version = "0.3.14" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668c6733a182cd7deb4f1de7ba3bf2120823835b3bcfbeacf7d2c4a773c1bb8b" +checksum = "c54913bae956fb8df7f4dc6fc90362aa72e69148e3f39041fbe8742d21e0ac57" dependencies = [ + "autocfg", "proc-macro-hack", "proc-macro2", "quote 1.0.9", - "syn 1.0.72", + "syn 1.0.74", ] [[package]] name = "futures-sink" -version = "0.3.14" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5629433c555de3d82861a7a4e3794a4c40040390907cfbfd7143a92a426c23" +checksum = "c0f30aaa67363d119812743aa5f33c201a7a66329f97d1a887022971feea4b53" [[package]] name = "futures-task" -version = "0.3.14" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba7aa51095076f3ba6d9a1f702f74bd05ec65f555d70d2033d55ba8d69f581bc" +checksum = "bbe54a98670017f3be909561f6ad13e810d9a51f3f061b902062ca3da80799f2" [[package]] name = "futures-util" -version = "0.3.14" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c144ad54d60f23927f0a6b6d816e4271278b64f005ad65e4e35291d2de9c025" +checksum = "67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78" dependencies = [ + "autocfg", "futures-channel", "futures-core", "futures-io", @@ -657,9 +709,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.1.16" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" dependencies = [ "cfg-if", "libc", @@ -732,7 +784,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote 1.0.9", - "syn 1.0.72", + "syn 1.0.74", ] [[package]] @@ -758,20 +810,19 @@ dependencies = [ [[package]] name = "grass" -version = "0.10.4" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8209e6832a1f93e15adca845b43af12ead0cb8221fb2ef615ca84c75e54af1a" +checksum = "82317908bc4204532d098390f8e041693aaeab95cf7351f774bdacf253b1c8ed" dependencies = [ "beef", "clap", "codemap", "indexmap", "lasso", - "num-bigint 0.3.2", - "num-rational 0.3.2", + "num-bigint", + "num-rational", "num-traits", "once_cell", - "peekmore", "phf", "rand", ] @@ -852,34 +903,27 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.8.2" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b62f79061a0bc2e046024cb7ba44b08419ed238ecbd9adbd787434b9e8c25" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" dependencies = [ "ahash", - "autocfg", ] -[[package]] -name = "hashbrown" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" - [[package]] name = "heck" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" dependencies = [ "unicode-segmentation", ] [[package]] name = "hermit-abi" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] @@ -895,19 +939,19 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.6.2" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" dependencies = [ "autocfg", - "hashbrown 0.9.1", + "hashbrown", ] [[package]] name = "inotify" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19f57db1baad9d09e43a3cd76dcf82ebdafd37d75c9498b87762dba77c93f15" +checksum = "b031475cb1b103ee221afb806a23d35e0570bf7271d7588762ceba8127ed43b3" dependencies = [ "bitflags", "inotify-sys", @@ -924,10 +968,26 @@ dependencies = [ ] [[package]] -name = "instant" -version = "0.1.9" +name = "insta" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +checksum = "58019516c1403ac45b106c9fc4e8fcbd77a78e98b014c619d1506338902ccfa4" +dependencies = [ + "console", + "lazy_static", + "ron", + "serde", + "serde_json", + "serde_yaml", + "similar", + "uuid", +] + +[[package]] +name = "instant" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d" dependencies = [ "cfg-if", ] @@ -943,9 +1003,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d572918e350e82412fe766d24b15e6682fb2ed2bbe018280caa810397cb319" +checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" dependencies = [ "either", ] @@ -957,12 +1017,64 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" [[package]] -name = "lasso" -version = "0.3.1" +name = "kqueue" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf1a626ea51398f5acf36666c8046ff4bfd048aab88e92db676d2a6eac8805d0" +checksum = "058a107a784f8be94c7d35c1300f4facced2e93d2fbe5b1452b44e905ddca4a9" dependencies = [ - "hashbrown 0.8.2", + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "lalrpop" +version = "0.19.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15174f1c529af5bf1283c3bc0058266b483a67156f79589fab2a25e23cf8988" +dependencies = [ + "ascii-canvas", + "atty", + "bit-set", + "diff", + "ena", + "itertools 0.10.1", + "lalrpop-util", + "petgraph", + "pico-args", + "regex", + "regex-syntax", + "string_cache", + "term", + "tiny-keccak", + "unicode-xid 0.2.2", +] + +[[package]] +name = "lalrpop-util" +version = "0.19.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e58cce361efcc90ba8a0a5f982c741ff86b603495bb15a998412e957dcd278" +dependencies = [ + "regex", +] + +[[package]] +name = "lasso" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8647c8a01e5f7878eacb2c323c4c949fdb63773110f0686c7810769874b7e0a" +dependencies = [ + "hashbrown", ] [[package]] @@ -972,23 +1084,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] -name = "lexical-core" -version = "0.7.6" +name = "levenshtein" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" -dependencies = [ - "arrayvec", - "bitflags", - "cfg-if", - "ryu", - "static_assertions", -] +checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" [[package]] name = "libc" -version = "0.2.94" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e" +checksum = "a7f823d141fe0a24df1e23b4af4e3c7ba9e5966ec514ea068c93024aa7deb765" + +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" [[package]] name = "lock_api" @@ -1022,18 +1133,18 @@ checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" [[package]] name = "memoffset" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83fb6581e8ed1f85fd45c116db8405483899489e38406156c25eb743554361d" +checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" dependencies = [ "autocfg", ] [[package]] name = "mio" -version = "0.7.11" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf80d3e903b34e0bd7282b218398aec54e082c840d9baf8339e0080a0c542956" +checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16" dependencies = [ "libc", "log", @@ -1052,43 +1163,36 @@ dependencies = [ ] [[package]] -name = "nix" -version = "0.20.0" +name = "new_debug_unreachable" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "nix" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8e5e343312e7fbeb2a52139114e9e702991ef9c2aea6817ff2440b35647d56" dependencies = [ "bitflags", "cc", "cfg-if", "libc", -] - -[[package]] -name = "nom" -version = "6.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" -dependencies = [ - "bitvec", - "funty", - "lexical-core", - "memchr", - "version_check", + "memoffset", ] [[package]] name = "notify" -version = "5.0.0-pre.7" +version = "5.0.0-pre.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebe7699a0f8c5759450716ee03d231685c22b4fe8f406c42c22e0ad94d40ce7" +checksum = "20a629259bb2c87a884bb76f6086c8637919de6d074754341c12e5dd3aed6326" dependencies = [ - "anymap", "bitflags", "crossbeam-channel", "filetime", - "fsevent", "fsevent-sys", "inotify", + "kqueue", "libc", "mio", "walkdir", @@ -1104,31 +1208,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "num" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" -dependencies = [ - "num-bigint 0.4.0", - "num-complex", - "num-integer", - "num-iter", - "num-rational 0.4.0", - "num-traits", -] - -[[package]] -name = "num-bigint" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d0a3d5e207573f948a9e5376662aa743a2ea13f7c50a554d7af443a73fbfeba" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - [[package]] name = "num-bigint" version = "0.4.0" @@ -1140,15 +1219,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-complex" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26873667bbbb7c5182d4a37c1add32cdf09f841af72da53318fdb81543c15085" -dependencies = [ - "num-traits", -] - [[package]] name = "num-integer" version = "0.1.44" @@ -1159,29 +1229,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-iter" -version = "0.1.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" -dependencies = [ - "autocfg", - "num-bigint 0.3.2", - "num-integer", - "num-traits", -] - [[package]] name = "num-rational" version = "0.4.0" @@ -1189,7 +1236,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" dependencies = [ "autocfg", - "num-bigint 0.4.0", + "num-bigint", "num-integer", "num-traits", ] @@ -1215,9 +1262,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.7.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" [[package]] name = "output_vt100" @@ -1281,47 +1328,57 @@ dependencies = [ ] [[package]] -name = "peekmore" -version = "0.5.6" +name = "pest" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4089f831c514cbf080bd19bfce702f7a2de250492730d419204af6710ba19316" +checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" dependencies = [ - "smallvec", + "ucd-trie", +] + +[[package]] +name = "petgraph" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7" +dependencies = [ + "fixedbitset", + "indexmap", ] [[package]] name = "phf" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +checksum = "b2ac8b67553a7ca9457ce0e526948cad581819238f4a9d1ea74545851fa24f37" dependencies = [ "phf_macros", - "phf_shared", + "phf_shared 0.9.0", "proc-macro-hack", ] [[package]] name = "phf_generator" -version = "0.8.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +checksum = "d43f3220d96e0080cc9ea234978ccd80d904eafb17be31bb0f76daaea6493082" dependencies = [ - "phf_shared", + "phf_shared 0.9.0", "rand", ] [[package]] name = "phf_macros" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +checksum = "b706f5936eb50ed880ae3009395b43ed19db5bff2ebd459c95e7bf013a89ab86" dependencies = [ "phf_generator", - "phf_shared", + "phf_shared 0.9.0", "proc-macro-hack", "proc-macro2", "quote 1.0.9", - "syn 1.0.72", + "syn 1.0.74", ] [[package]] @@ -1334,10 +1391,25 @@ dependencies = [ ] [[package]] -name = "pin-project-lite" -version = "0.2.6" +name = "phf_shared" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905" +checksum = "a68318426de33640f02be62b4ae8eb1261be2efbc337b60c54d845bf4484e0d9" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468" + +[[package]] +name = "pin-project-lite" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" [[package]] name = "pin-utils" @@ -1357,6 +1429,12 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "pretty_assertions" version = "0.7.2" @@ -1397,7 +1475,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote 1.0.9", - "syn 1.0.72", + "syn 1.0.74", "version_check", ] @@ -1426,9 +1504,9 @@ checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" [[package]] name = "proc-macro2" -version = "1.0.26" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec" +checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612" dependencies = [ "unicode-xid 0.2.2", ] @@ -1454,31 +1532,23 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "radium" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" - [[package]] name = "rand" -version = "0.7.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" dependencies = [ - "getrandom", "libc", "rand_chacha", "rand_core", "rand_hc", - "rand_pcg", ] [[package]] name = "rand_chacha" -version = "0.2.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", @@ -1486,36 +1556,27 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.5.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ "getrandom", ] [[package]] name = "rand_hc" -version = "0.2.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core", -] - -[[package]] -name = "rand_pcg" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" dependencies = [ "rand_core", ] [[package]] name = "rayon" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b0d8e0819fadc20c74ea8373106ead0600e3a67ef1fe8da56e39b9ae7275674" +checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" dependencies = [ "autocfg", "crossbeam-deque", @@ -1525,9 +1586,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ab346ac5921dc62ffa9f89b7a773907511cdfa5490c572ae9be1be33e8afa4a" +checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" dependencies = [ "crossbeam-channel", "crossbeam-deque", @@ -1538,13 +1599,23 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.8" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom", + "redox_syscall", +] + [[package]] name = "regex" version = "1.5.4" @@ -1563,14 +1634,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" [[package]] -name = "roxmltree" -version = "0.14.1" +name = "ron" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "921904a62e410e37e215c40381b7117f830d9d89ba60ab5236170541dd25646b" +checksum = "064ea8613fb712a19faf920022ec8ddf134984f100090764a4e1d768f3827f1f" dependencies = [ - "xmlparser", + "base64", + "bitflags", + "serde", ] +[[package]] +name = "rustc_version" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088" + [[package]] name = "ryu" version = "1.0.5" @@ -1593,30 +1681,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] -name = "serde" -version = "1.0.125" +name = "semver" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" +dependencies = [ + "pest", +] + +[[package]] +name = "serde" +version = "1.0.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.125" +version = "1.0.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d" +checksum = "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc" dependencies = [ "proc-macro2", "quote 1.0.9", - "syn 1.0.72", + "syn 1.0.74", ] [[package]] name = "serde_json" -version = "1.0.64" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" +checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127" dependencies = [ "itoa", "ryu", @@ -1624,14 +1730,32 @@ dependencies = [ ] [[package]] -name = "signal-hook-registry" -version = "1.3.0" +name = "serde_yaml" +version = "0.8.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6" +checksum = "039ba818c784248423789eec090aab9fb566c7b94d6ebbfa1814a9fd52c8afb2" +dependencies = [ + "dtoa", + "linked-hash-map", + "serde", + "yaml-rust", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" dependencies = [ "libc", ] +[[package]] +name = "similar" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec" + [[package]] name = "simple-signal" version = "1.1.1" @@ -1642,17 +1766,35 @@ dependencies = [ "libc", ] +[[package]] +name = "simplexpr" +version = "0.1.0" +dependencies = [ + "eww_shared_util", + "insta", + "itertools 0.10.1", + "lalrpop", + "lalrpop-util", + "levenshtein", + "once_cell", + "regex", + "serde", + "serde_json", + "strum 0.21.0", + "thiserror", +] + [[package]] name = "siphasher" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbce6d4507c7e4a3962091436e56e95290cb71fa302d0d270e32130b75fbff27" +checksum = "729a25c17d72b06c68cb47955d44fda88ad2d3e7d77e025663fdd69b93dd71a1" [[package]] name = "slab" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527" +checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590" [[package]] name = "smallvec" @@ -1668,7 +1810,7 @@ checksum = "133659a15339456eeeb07572eb02a91c91e9815e9cbc89566944d2c8d3efdbf6" dependencies = [ "proc-macro2", "quote 1.0.9", - "syn 1.0.72", + "syn 1.0.74", ] [[package]] @@ -1677,6 +1819,18 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "string_cache" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb1139b5353f96e429e1a5e19fbaf663bddedaa06d1dbd49f82e352601209a" +dependencies = [ + "lazy_static", + "new_debug_unreachable", + "phf_shared 0.8.0", + "precomputed-hash", +] + [[package]] name = "strsim" version = "0.8.0" @@ -1685,9 +1839,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "structopt" -version = "0.3.21" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5277acd7ee46e63e5168a80734c9f6ee81b1367a7d8772a2d765df2a3705d28c" +checksum = "69b041cdcb67226aca307e6e7be44c8806423d83e018bd662360a93dabce4d71" dependencies = [ "clap", "lazy_static", @@ -1696,15 +1850,15 @@ dependencies = [ [[package]] name = "structopt-derive" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ba9cdfda491b814720b6b06e0cac513d922fc407582032e8706e9f137976f90" +checksum = "7813934aecf5f51a54775e00068c237de98489463968231a51746bbbc03f9c10" dependencies = [ "heck", "proc-macro-error", "proc-macro2", "quote 1.0.9", - "syn 1.0.72", + "syn 1.0.74", ] [[package]] @@ -1715,9 +1869,12 @@ checksum = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b" [[package]] name = "strum" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7318c509b5ba57f18533982607f24070a55d353e90d4cae30c467cdb2ad5ac5c" +checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2" +dependencies = [ + "strum_macros 0.21.1", +] [[package]] name = "strum_macros" @@ -1728,19 +1885,19 @@ dependencies = [ "heck", "proc-macro2", "quote 1.0.9", - "syn 1.0.72", + "syn 1.0.74", ] [[package]] name = "strum_macros" -version = "0.20.1" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee8bc6b87a5112aeeab1f4a9f7ab634fe6cbefc4850006df31267f4cfb9e3149" +checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec" dependencies = [ "heck", "proc-macro2", "quote 1.0.9", - "syn 1.0.72", + "syn 1.0.74", ] [[package]] @@ -1756,9 +1913,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.72" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" +checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c" dependencies = [ "proc-macro2", "quote 1.0.9", @@ -1806,10 +1963,15 @@ dependencies = [ ] [[package]] -name = "tap" -version = "1.0.1" +name = "term" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] [[package]] name = "termcolor" @@ -1820,6 +1982,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -1831,29 +2003,38 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.24" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" +checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.24" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" +checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745" dependencies = [ "proc-macro2", "quote 1.0.9", - "syn 1.0.72", + "syn 1.0.74", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", ] [[package]] name = "tokio" -version = "1.5.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83f0c8e7c0addab50b663055baf787d0af7f413a46e6e7fb9559a4e4db7137a5" +checksum = "01cf844b23c6131f624accf65ce0e4e9956a8bb329400ea5bcc26ae3a5c20b0b" dependencies = [ "autocfg", "bytes", @@ -1871,31 +2052,20 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf7b11a536f46a809a8a9f0bb4237020f70ecbf115b842360afb127ea2fda57" +checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110" dependencies = [ "proc-macro2", "quote 1.0.9", - "syn 1.0.72", -] - -[[package]] -name = "tokio-stream" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e177a5d8c3bf36de9ebe6d58537d8879e964332f93fb3339e43f618c81361af0" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", + "syn 1.0.74", ] [[package]] name = "tokio-util" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940a12c99365c31ea8dd9ba04ec1be183ffe4920102bb7122c2f515437601e8e" +checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592" dependencies = [ "bytes", "futures-core", @@ -1914,6 +2084,12 @@ dependencies = [ "serde", ] +[[package]] +name = "ucd-trie" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" + [[package]] name = "unescape" version = "0.1.0" @@ -1922,9 +2098,9 @@ checksum = "ccb97dac3243214f8d8507998906ca3e2e0b900bf9bf4870477f125b82e68f6e" [[package]] name = "unicode-segmentation" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" [[package]] name = "unicode-width" @@ -1944,6 +2120,18 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +[[package]] +name = "unindent" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7" + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" + [[package]] name = "vec_map" version = "0.8.2" @@ -1984,9 +2172,9 @@ dependencies = [ [[package]] name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" +version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] name = "winapi" @@ -2028,12 +2216,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "wyz" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" - [[package]] name = "x11" version = "2.18.2" @@ -2057,7 +2239,35 @@ dependencies = [ ] [[package]] -name = "xmlparser" -version = "0.13.3" +name = "yaml-rust" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "114ba2b24d2167ef6d67d7d04c8cc86522b87f490025f39f0303b7db5bf5e3d8" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yuck" +version = "0.1.0" +dependencies = [ + "anyhow", + "codespan-reporting", + "derive_more", + "eww_shared_util", + "insta", + "itertools 0.10.1", + "lalrpop", + "lalrpop-util", + "maplit", + "once_cell", + "pretty_assertions", + "regex", + "serde", + "serde_json", + "simplexpr", + "smart-default", + "static_assertions", + "strum 0.21.0", + "thiserror", +] diff --git a/Cargo.toml b/Cargo.toml index cbb85b4..66b1ea0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,73 +1,10 @@ -[package] -name = "eww" -version = "0.1.0" -authors = ["elkowar <5300871+elkowar@users.noreply.github.com>"] -edition = "2018" -description= "Widget system for everyone!" -license = "MIT" -repository = "https://github.com/elkowar/eww" -homepage = "https://github.com/elkowar/eww" +[workspace] +members = [ + "crates/eww", + "crates/simplexpr", + "crates/yuck", + "crates/eww_shared_util" +] +[profile.dev] +split-debuginfo = "unpacked" - -[features] -default = ["x11"] -x11 = ["gdkx11", "x11rb"] -wayland = ["gtk-layer-shell", "gtk-layer-shell-sys"] -no-x11-wayland = [] -[dependencies.cairo-sys-rs] -version = "0.10.0" - -[dependencies] -gtk = { version = "0.9", features = [ "v3_22" ] } -gdk = { version = "*", features = ["v3_22"] } -gio = { version = "*", features = ["v2_44"] } -glib = { version = "*", features = ["v2_44"] } - -gdk-pixbuf = "0.9" - -gtk-layer-shell = { version="0.2.0", optional=true } -gtk-layer-shell-sys = { version="0.2.0", optional=true } -gdkx11 = { version = "0.9", optional = true } -x11rb = { version = "0.8", features = ["randr"], optional = true } - -regex = "1" -bincode = "1.3" -anyhow = "1.0" -derive_more = "0.99" -maplit = "1" -structopt = "0.3" -serde = {version = "1.0", features = ["derive"]} -serde_json = "1.0" -extend = "1" -grass = "0.10" -num = "0.4" -roxmltree = "0.14" -itertools = "0.10" -debug_stub_derive = "0.3" -log = "0.4" -pretty_env_logger = "0.4" -lazy_static = "1.4.0" -libc = "0.2" -nix = "0.20" -smart-default = "0.6" -simple-signal = "1.1" -unescape = "0.1" - -tokio = { version = "1.0", features = ["full"] } -tokio-stream = "0.1" -async-stream = "0.3" -futures-core = "0.3" -futures-util = "0.3" -tokio-util = "0.6" - -sysinfo = "0.16.1" - -nom = "6.1" -dyn-clone = "1.0" -base64 = "0.13" -wait-timeout = "0.2" - -notify = "5.0.0-pre.7" - -[dev-dependencies] -pretty_assertions = "0.7.1" diff --git a/README.md b/README.md index 8c85a78..8cdd81b 100644 --- a/README.md +++ b/README.md @@ -4,18 +4,26 @@ -Elkowar’s Wacky Widgets is a standalone widget system made in Rust that allows you to implement +Elkowars Wacky Widgets is a standalone widget system made in Rust that allows you to implement your own, custom widgets in any window manager. Documentation **and instructions on how to install** can be found [here](https://elkowar.github.io/eww). -## New configuration language is being made! +## New configuration language! -A new configuration language for Eww is being made! `yuck` is the new name! (as discussed in the [discussion post](https://github.com/elkowar/eww/discussions/206).) -You can check out its development in [this PR](https://github.com/elkowar/eww/pull/221) and maybe contribewwte! +YUCK IS ALIVE! After months of waiting, the new configuration language has now been released! +This also means that XML is no longer supported from this point onwards. +If you want to keep using the latest releases of eww, you'll need to migrate your config over to yuck. + +The steps to migrate can be found in [the migration guide](YUCK_MIGRATION.md). + +Additionally, a couple _amazing_ people have started to work on an +[automatic converter](https://github.com/undefinedDarkness/ewwxml) that can turn your old eww.xml into the new yuck format! ## Examples +(note that some of these still make use of the old configuration syntax) + * A basic bar, see [examples](./examples/eww-bar) ![Example 1](./examples/eww-bar/eww-bar.png) diff --git a/YUCK_MIGRATION.md b/YUCK_MIGRATION.md new file mode 100644 index 0000000..8ba66f0 --- /dev/null +++ b/YUCK_MIGRATION.md @@ -0,0 +1,29 @@ +# Migrating to yuck + +Yuck is the new configuration syntax used by eww. +While the syntax has changed dramatically, the general structure of the configuration +has stayed mostly the same. + +Most notably, the top-level blocks are now gone. +This means that `defvar`, `defwidget`, etc blocks no longer need to be in separate +sections of the file, but instead can be put wherever you need them. + +Explaining the exact syntax of yuck would be significantly less effective than just +looking at an example, as the general syntax is very simple. + +Thus, to get a feel for yuck, read through the [example configuration](./examples/eww-bar/eww.yuck). + + +Additionally, a couple smaller things have been changed. +The fields and structure of the `defwindow` block as been adjusted to better reflect +the options provided by the displayserver that is being used. +The major changes are: +- The `screen` field is now called `monitor` +- `reserve` and `geometry` are now structured slightly differently (see [here](./docs/src/configuration.md#creating-your-first-window)) +To see how exactly the configuration now looks, check the [respective documentation](./docs/src/configuration.md#creating-your-first-window) + + +## Automatically converting your configuration + +A couple _amazing_ people have started to work on an [automatic converter](https://github.com/undefinedDarkness/ewwxml) that can turn your +old eww.xml into the new yuck format! diff --git a/crates/eww/Cargo.toml b/crates/eww/Cargo.toml new file mode 100644 index 0000000..e1329a8 --- /dev/null +++ b/crates/eww/Cargo.toml @@ -0,0 +1,71 @@ +[package] +name = "eww" +version = "0.2.0" +authors = ["elkowar <5300871+elkowar@users.noreply.github.com>"] +edition = "2018" +description = "Widget system for everyone!" +license = "MIT" +repository = "https://github.com/elkowar/eww" +homepage = "https://github.com/elkowar/eww" + + +[features] +default = ["x11"] +x11 = ["gdkx11", "x11rb"] +wayland = ["gtk-layer-shell", "gtk-layer-shell-sys"] +[dependencies.cairo-sys-rs] +version = "0.10.0" + +[dependencies] +gtk = { version = "0.9", features = [ "v3_22" ] } +gdk = { version = "*", features = ["v3_22"] } +gio = { version = "*", features = ["v2_44"] } +glib = { version = "*", features = ["v2_44"] } + +gdk-pixbuf = "0.9" + +gtk-layer-shell = { version="0.2.0", optional=true } +gtk-layer-shell-sys = { version="0.2.0", optional=true } +gdkx11 = { version = "0.9", optional = true } +x11rb = { version = "0.8", features = ["randr"], optional = true } + +regex = "1" +bincode = "1.3" +anyhow = "1.0" +derive_more = "0.99" +maplit = "1" +structopt = "0.3" +serde = {version = "1.0", features = ["derive"]} +serde_json = "1.0" +extend = "1" +grass = "0.10" +itertools = "0.10" +debug_stub_derive = "0.3" +log = "0.4" +pretty_env_logger = "0.4" +libc = "0.2" +once_cell = "1.8" +nix = "0.20" +smart-default = "0.6" +simple-signal = "1.1" +unescape = "0.1" +unindent = "0.1" + +tokio = { version = "1.0", features = ["full"] } +futures-core = "0.3" +futures-util = "0.3" +tokio-util = "0.6" + +sysinfo = "0.16.1" + +dyn-clone = "1.0" +base64 = "0.13" +wait-timeout = "0.2" + +notify = "5.0.0-pre.7" + +codespan-reporting = "0.11" + +simplexpr = { path = "../simplexpr" } +eww_shared_util = { path = "../eww_shared_util" } +yuck = { path = "../yuck", default-features = false} diff --git a/rustfmt.toml b/crates/eww/rustfmt.toml similarity index 100% rename from rustfmt.toml rename to crates/eww/rustfmt.toml diff --git a/src/app.rs b/crates/eww/src/app.rs similarity index 53% rename from src/app.rs rename to crates/eww/src/app.rs index 367d654..05b4f6d 100644 --- a/src/app.rs +++ b/crates/eww/src/app.rs @@ -1,61 +1,43 @@ use crate::{ - config, - config::{window_definition::WindowName, AnchorPoint}, - display_backend, eww_state, - script_var_handler::*, - value::{Coords, NumWithUnit, PrimVal, VarName}, + config, daemon_response::DaemonResponseSender, display_backend, error_handling_ctx, eww_state, script_var_handler::*, EwwPaths, }; use anyhow::*; use debug_stub_derive::*; +use eww_shared_util::VarName; use gdk::WindowExt; use gtk::{ContainerExt, CssProviderExt, GtkWindowExt, StyleContextExt, WidgetExt}; use itertools::Itertools; -use std::collections::HashMap; +use simplexpr::dynval::DynVal; +use std::collections::{HashMap, HashSet}; use tokio::sync::mpsc::UnboundedSender; - -/// Response that the app may send as a response to a event. -/// This is used in `DaemonCommand`s that contain a response sender. -#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, derive_more::Display)] -pub enum DaemonResponse { - Success(String), - Failure(String), -} - -impl DaemonResponse { - pub fn is_success(&self) -> bool { - matches!(self, DaemonResponse::Success(_)) - } - - pub fn is_failure(&self) -> bool { - !self.is_success() - } -} - -pub type DaemonResponseSender = tokio::sync::mpsc::UnboundedSender; -pub type DaemonResponseReceiver = tokio::sync::mpsc::UnboundedReceiver; +use yuck::{ + config::window_geometry::{AnchorPoint, WindowGeometry}, + value::Coords, +}; #[derive(Debug)] pub enum DaemonCommand { NoOp, - UpdateVars(Vec<(VarName, PrimVal)>), + UpdateVars(Vec<(VarName, DynVal)>), ReloadConfigAndCss(DaemonResponseSender), UpdateConfig(config::EwwConfig), UpdateCss(String), OpenMany { - windows: Vec, + windows: Vec, sender: DaemonResponseSender, }, OpenWindow { - window_name: WindowName, + window_name: String, pos: Option, size: Option, anchor: Option, - monitor: Option, + screen: Option, + should_toggle: bool, sender: DaemonResponseSender, }, CloseWindow { - window_name: WindowName, + window_name: String, sender: DaemonResponseSender, }, KillServer, @@ -70,7 +52,7 @@ pub enum DaemonCommand { #[derive(Debug, Clone)] pub struct EwwWindow { - pub name: WindowName, + pub name: String, pub definition: config::EwwWindowDefinition, pub gtk_window: gtk::Window, } @@ -85,7 +67,10 @@ impl EwwWindow { pub struct App { pub eww_state: eww_state::EwwState, pub eww_config: config::EwwConfig, - pub open_windows: HashMap, + pub open_windows: HashMap, + /// Window names that are supposed to be open, but failed. + /// When reloading the config, these should be opened again. + pub failed_windows: HashSet, pub css_provider: gtk::CssProvider, #[debug_stub = "ScriptVarHandler(...)"] @@ -111,24 +96,23 @@ impl App { DaemonCommand::ReloadConfigAndCss(sender) => { let mut errors = Vec::new(); - let config_result = config::RawEwwConfig::read_from_file(&self.paths.get_eww_xml_path()) - .and_then(config::EwwConfig::generate); - match config_result { - Ok(new_config) => self.handle_command(DaemonCommand::UpdateConfig(new_config)), + let config_result = config::read_from_file(&self.paths.get_yuck_path()); + match config_result.and_then(|new_config| self.load_config(new_config)) { + Ok(()) => {} Err(e) => errors.push(e), } let css_result = crate::util::parse_scss_from_file(&self.paths.get_eww_scss_path()); - match css_result { - Ok(new_css) => self.handle_command(DaemonCommand::UpdateCss(new_css)), + match css_result.and_then(|css| self.load_css(&css)) { + Ok(()) => {} Err(e) => errors.push(e), } - let errors = errors.into_iter().map(|e| format!("{:?}", e)).join("\n"); + let errors = errors.into_iter().map(|e| error_handling_ctx::format_error(&e)).join("\n"); if errors.is_empty() { - sender.send(DaemonResponse::Success(String::new()))?; + sender.send_success(String::new())?; } else { - sender.send(DaemonResponse::Failure(errors))?; + sender.send_failure(errors)?; } } DaemonCommand::UpdateConfig(config) => { @@ -150,15 +134,19 @@ impl App { } DaemonCommand::OpenMany { windows, sender } => { let result = windows.iter().try_for_each(|w| self.open_window(w, None, None, None, None)); - respond_with_error(sender, result)?; + respond_with_result(sender, result)?; } - DaemonCommand::OpenWindow { window_name, pos, size, anchor, monitor, sender } => { - let result = self.open_window(&window_name, pos, size, monitor, anchor); - respond_with_error(sender, result)?; + DaemonCommand::OpenWindow { window_name, pos, size, anchor, screen: monitor, should_toggle, sender } => { + let result = if should_toggle && self.open_windows.contains_key(&window_name) { + self.close_window(&window_name) + } else { + self.open_window(&window_name, pos, size, monitor, anchor) + }; + respond_with_result(sender, result)?; } DaemonCommand::CloseWindow { window_name, sender } => { let result = self.close_window(&window_name); - respond_with_error(sender, result)?; + respond_with_result(sender, result)?; } DaemonCommand::PrintState { all, sender } => { let vars = self.eww_state.get_variables().iter(); @@ -169,7 +157,7 @@ impl App { .map(|(key, value)| format!("{}: {}", key, value)) .join("\n") }; - sender.send(DaemonResponse::Success(output)).context("sending response from main thread")? + sender.send_success(output)? } DaemonCommand::PrintWindows(sender) => { let output = self @@ -181,38 +169,43 @@ impl App { format!("{}{}", if is_open { "*" } else { "" }, window_name) }) .join("\n"); - sender.send(DaemonResponse::Success(output)).context("sending response from main thread")? + sender.send_success(output)? } DaemonCommand::PrintDebug(sender) => { let output = format!("state: {:#?}\n\nconfig: {:#?}", &self.eww_state, &self.eww_config); - sender.send(DaemonResponse::Success(output)).context("sending response from main thread")? + sender.send_success(output)? } } }; - crate::print_result_err!("while handling event", &result); + if let Err(err) = result { + error_handling_ctx::print_error(err); + } } fn stop_application(&mut self) { self.script_var_handler.stop_all(); - self.open_windows.drain().for_each(|(_, w)| w.close()); + for (_, window) in self.open_windows.drain() { + window.close(); + } gtk::main_quit(); } - fn update_state(&mut self, fieldname: VarName, value: PrimVal) { + fn update_state(&mut self, fieldname: VarName, value: DynVal) { self.eww_state.update_variable(fieldname, value) } - fn close_window(&mut self, window_name: &WindowName) -> Result<()> { + fn close_window(&mut self, window_name: &String) -> Result<()> { for unused_var in self.variables_only_used_in(window_name) { - log::info!("stopping for {}", &unused_var); + log::debug!("stopping for {}", &unused_var); self.script_var_handler.stop_for_variable(unused_var.clone()); } - let window = - self.open_windows.remove(window_name).context(format!("No window with name '{}' is running.", window_name))?; + self.open_windows + .remove(window_name) + .with_context(|| format!("Tried to close window named '{}', but no such window was open", window_name))? + .close(); - window.close(); self.eww_state.clear_window_state(window_name); Ok(()) @@ -220,42 +213,55 @@ impl App { fn open_window( &mut self, - window_name: &WindowName, + window_name: &String, pos: Option, size: Option, monitor: Option, - anchor: Option, + anchor: Option, ) -> Result<()> { - // remove and close existing window with the same name - let _ = self.close_window(window_name); + self.failed_windows.remove(window_name); log::info!("Opening window {}", window_name); - let mut window_def = self.eww_config.get_window(window_name)?.clone(); - window_def.geometry = window_def.geometry.override_if_given(anchor, pos, size); + // if an instance of this is already running, close it + let _ = self.close_window(window_name); - let root_widget = - window_def.widget.render(&mut self.eww_state, window_name, &self.eww_config.get_widget_definitions())?; - root_widget.get_style_context().add_class(&window_name.to_string()); + let open_result: Result<_> = try { + let mut window_def = self.eww_config.get_window(window_name)?.clone(); + window_def.geometry = window_def.geometry.map(|x| x.override_if_given(anchor, pos, size)); - let monitor_geometry = - get_monitor_geometry(monitor.or(window_def.screen_number).unwrap_or_else(get_default_monitor_index)); - let eww_window = initialize_window(monitor_geometry, root_widget, window_def)?; + let root_widget = + window_def.widget.render(&mut self.eww_state, window_name, &self.eww_config.get_widget_definitions())?; - self.open_windows.insert(window_name.clone(), eww_window); + root_widget.get_style_context().add_class(&window_name.to_string()); - // initialize script var handlers for variables that where not used before opening this window. - // TODO somehow make this less shit - for newly_used_var in - self.variables_only_used_in(window_name).filter_map(|var| self.eww_config.get_script_var(var).ok()) - { - self.script_var_handler.add(newly_used_var.clone()); + let monitor_geometry = get_monitor_geometry(monitor.or(window_def.monitor_number))?; + + let eww_window = initialize_window(monitor_geometry, root_widget, window_def)?; + + self.open_windows.insert(window_name.clone(), eww_window); + + // initialize script var handlers for variables that where not used before opening this window. + // TODO somehow make this less shit + for newly_used_var in + self.variables_only_used_in(window_name).filter_map(|var| self.eww_config.get_script_var(var).ok()) + { + self.script_var_handler.add(newly_used_var.clone()); + } + }; + + if let Err(err) = open_result { + self.failed_windows.insert(window_name.to_string()); + Err(err).with_context(|| format!("failed to open window `{}`", window_name)) + } else { + Ok(()) } - - Ok(()) } - /// Load the given configuration, reloading all script-vars and reopening all windows that where opened. + /// Load the given configuration, reloading all script-vars and attempting to reopen all windows that where opened. pub fn load_config(&mut self, config: config::EwwConfig) -> Result<()> { + // TODO the reload procedure is kinda bad. + // It should probably instead prepare a new eww_state and everything, and then swap the instances once everything has worked. + log::info!("Reloading windows"); // refresh script-var poll stuff self.script_var_handler.stop_all(); @@ -263,9 +269,8 @@ impl App { self.eww_config = config; self.eww_state.clear_all_window_states(); - let windows = self.open_windows.clone(); - for (window_name, window) in windows { - window.close(); + let window_names: Vec = self.open_windows.keys().cloned().chain(self.failed_windows.iter().cloned()).dedup().collect(); + for window_name in &window_names { self.open_window(&window_name, None, None, None, None)?; } Ok(()) @@ -282,7 +287,7 @@ impl App { } /// Get all variables mapped to a list of windows they are being used in. - pub fn currently_used_variables<'a>(&'a self) -> HashMap<&'a VarName, Vec<&'a WindowName>> { + pub fn currently_used_variables<'a>(&'a self) -> HashMap<&'a VarName, Vec<&'a String>> { let mut vars: HashMap<&'a VarName, Vec<_>> = HashMap::new(); for window_name in self.open_windows.keys() { for var in self.eww_state.vars_referenced_in(window_name) { @@ -293,7 +298,7 @@ impl App { } /// Get all variables that are only used in the given window. - pub fn variables_only_used_in<'a>(&'a self, window: &'a WindowName) -> impl Iterator { + pub fn variables_only_used_in<'a>(&'a self, window: &'a String) -> impl Iterator { self.currently_used_variables() .into_iter() .filter(move |(_, wins)| wins.len() == 1 && wins.contains(&window)) @@ -306,55 +311,54 @@ fn initialize_window( root_widget: gtk::Widget, window_def: config::EwwWindowDefinition, ) -> Result { - let actual_window_rect = window_def.geometry.get_window_rectangle(monitor_geometry); - if let Some(window) = display_backend::initialize_window(&window_def, monitor_geometry) { - window.set_title(&format!("Eww - {}", window_def.name)); - let wm_class_name = format!("eww-{}", window_def.name); - window.set_wmclass(&wm_class_name, &wm_class_name); - window.set_position(gtk::WindowPosition::Center); + let window = display_backend::initialize_window(&window_def, monitor_geometry) + .with_context(|| format!("monitor {} is unavailable", window_def.monitor_number.unwrap()))?; + + window.set_title(&format!("Eww - {}", window_def.name)); + window.set_position(gtk::WindowPosition::None); + window.set_gravity(gdk::Gravity::Center); + + if let Some(geometry) = window_def.geometry { + let actual_window_rect = get_window_rectangle(geometry, monitor_geometry); window.set_size_request(actual_window_rect.width, actual_window_rect.height); window.set_default_size(actual_window_rect.width, actual_window_rect.height); - window.set_decorated(false); - // run on_screen_changed to set the visual correctly initially. - on_screen_changed(&window, None); - window.connect_screen_changed(on_screen_changed); - - window.add(&root_widget); - - window.show_all(); - - apply_window_position(window_def.clone(), monitor_geometry, &window)?; - let gdk_window = window.get_window().context("couldn't get gdk window from gtk window")?; - gdk_window.set_override_redirect(!window_def.focusable); - - #[cfg(feature = "x11")] - display_backend::set_xprops(&window, monitor_geometry, &window_def)?; - - // this should only be required on x11, as waylands layershell should manage the margins properly anways. - #[cfg(feature = "x11")] - window.connect_configure_event({ - let window_def = window_def.clone(); - move |window, _evt| { - let _ = apply_window_position(window_def.clone(), monitor_geometry, &window); - false - } - }); - Ok(EwwWindow { name: window_def.name.clone(), definition: window_def, gtk_window: window }) - } else { - Err(anyhow!("monitor {} is unavailable", window_def.screen_number.unwrap())) } + window.set_decorated(false); + window.set_skip_taskbar_hint(true); + window.set_skip_pager_hint(true); + + // run on_screen_changed to set the visual correctly initially. + on_screen_changed(&window, None); + window.connect_screen_changed(on_screen_changed); + + window.add(&root_widget); + + window.show_all(); + + #[cfg(feature = "x11")] + { + if let Some(geometry) = window_def.geometry { + let _ = apply_window_position(geometry, monitor_geometry, &window); + window.connect_configure_event(move |window, _| { + let _ = apply_window_position(geometry, monitor_geometry, &window); + false + }); + } + display_backend::set_xprops(&window, monitor_geometry, &window_def)?; + } + Ok(EwwWindow { name: window_def.name.clone(), definition: window_def, gtk_window: window }) } /// Apply the provided window-positioning rules to the window. +#[cfg(feature = "x11")] fn apply_window_position( - mut window_def: config::EwwWindowDefinition, + mut window_geometry: WindowGeometry, monitor_geometry: gdk::Rectangle, window: >k::Window, ) -> Result<()> { - let (gtk_window_width, gtk_window_height) = window.get_size(); - window_def.geometry.size = Coords { x: NumWithUnit::Pixels(gtk_window_width), y: NumWithUnit::Pixels(gtk_window_height) }; let gdk_window = window.get_window().context("Failed to get gdk window from gtk window")?; - let actual_window_rect = window_def.geometry.get_window_rectangle(monitor_geometry); + window_geometry.size = Coords::from_pixels(window.get_size()); + let actual_window_rect = get_window_rectangle(window_geometry, monitor_geometry); gdk_window.move_(actual_window_rect.x, actual_window_rect.y); Ok(()) } @@ -366,20 +370,34 @@ fn on_screen_changed(window: >k::Window, _old_screen: Option<&gdk::Screen>) { window.set_visual(visual.as_ref()); } -fn get_default_monitor_index() -> i32 { - gdk::Display::get_default().expect("could not get default display").get_default_screen().get_primary_monitor() -} - -/// Get the monitor geometry of a given monitor number -fn get_monitor_geometry(n: i32) -> gdk::Rectangle { - gdk::Display::get_default().expect("could not get default display").get_default_screen().get_monitor_geometry(n) +/// Get the monitor geometry of a given monitor number, or the default if none is given +fn get_monitor_geometry(n: Option) -> Result { + #[allow(deprecated)] + let display = gdk::Display::get_default().expect("could not get default display"); + let monitor = match n { + Some(n) => display.get_monitor(n).with_context(|| format!("Failed to get monitor with index {}", n))?, + None => display.get_primary_monitor().context("Failed to get primary monitor from GTK")?, + }; + Ok(monitor.get_geometry()) } /// In case of an Err, send the error message to a sender. -fn respond_with_error(sender: DaemonResponseSender, result: Result) -> Result<()> { +fn respond_with_result(sender: DaemonResponseSender, result: Result) -> Result<()> { match result { - Ok(_) => sender.send(DaemonResponse::Success(String::new())), - Err(e) => sender.send(DaemonResponse::Failure(format!("{:?}", e))), + Ok(_) => sender.send_success(String::new()), + Err(e) => { + let formatted = error_handling_ctx::format_error(&e); + println!("Action failed with error: {}", formatted); + sender.send_failure(formatted) + }, } .context("sending response from main thread") } + +pub fn get_window_rectangle(geometry: WindowGeometry, screen_rect: gdk::Rectangle) -> gdk::Rectangle { + let (offset_x, offset_y) = geometry.offset.relative_to(screen_rect.width, screen_rect.height); + let (width, height) = geometry.size.relative_to(screen_rect.width, screen_rect.height); + let x = screen_rect.x + offset_x + geometry.anchor_point.x.alignment_to_coordinate(width, screen_rect.width); + let y = screen_rect.y + offset_y + geometry.anchor_point.y.alignment_to_coordinate(height, screen_rect.height); + gdk::Rectangle { x, y, width, height } +} diff --git a/src/application_lifecycle.rs b/crates/eww/src/application_lifecycle.rs similarity index 90% rename from src/application_lifecycle.rs rename to crates/eww/src/application_lifecycle.rs index a44a050..8a5cd3c 100644 --- a/src/application_lifecycle.rs +++ b/crates/eww/src/application_lifecycle.rs @@ -3,11 +3,10 @@ //! `recv_exit()` function which can be awaited to receive an event in case of application termination. use anyhow::*; +use once_cell::sync::Lazy; use tokio::sync::broadcast; -lazy_static::lazy_static! { - static ref APPLICATION_EXIT_SENDER: broadcast::Sender<()> = broadcast::channel(2).0; -} +pub static APPLICATION_EXIT_SENDER: Lazy> = Lazy::new(|| broadcast::channel(2).0); /// Notify all listening tasks of the termination of the eww application process. pub fn send_exit() -> Result<()> { diff --git a/src/client.rs b/crates/eww/src/client.rs similarity index 86% rename from src/client.rs rename to crates/eww/src/client.rs index 3001357..e5d98ec 100644 --- a/src/client.rs +++ b/crates/eww/src/client.rs @@ -1,7 +1,7 @@ use std::process::Stdio; use crate::{ - app, + daemon_response::DaemonResponse, opts::{self, ActionClientOnly}, EwwPaths, }; @@ -24,8 +24,8 @@ pub fn handle_client_only_action(paths: &EwwPaths, action: ActionClientOnly) -> Ok(()) } -pub fn do_server_call(mut stream: UnixStream, action: opts::ActionWithServer) -> Result> { - log::info!("Forwarding options to server"); +pub fn do_server_call(stream: &mut UnixStream, action: &opts::ActionWithServer) -> Result> { + log::debug!("Forwarding options to server"); stream.set_nonblocking(false).context("Failed to set stream to non-blocking")?; let message_bytes = bincode::serialize(&action)?; diff --git a/crates/eww/src/config/eww_config.rs b/crates/eww/src/config/eww_config.rs new file mode 100644 index 0000000..efdf827 --- /dev/null +++ b/crates/eww/src/config/eww_config.rs @@ -0,0 +1,90 @@ +use anyhow::*; +use eww_shared_util::VarName; +use std::{collections::HashMap, path::Path}; +use yuck::config::{ + file_provider::YuckFiles, script_var_definition::ScriptVarDefinition, widget_definition::WidgetDefinition, Config, +}; + +use simplexpr::dynval::DynVal; + +use crate::error_handling_ctx; + +use super::{script_var, EwwWindowDefinition}; + +/// Load an [EwwConfig] from a given file, resetting and applying the global YuckFiles object in [error_handling_ctx]. +pub fn read_from_file(path: impl AsRef) -> Result { + error_handling_ctx::clear_files(); + EwwConfig::read_from_file(&mut error_handling_ctx::YUCK_FILES.write().unwrap(), path) +} + +/// Eww configuration structure. +#[derive(Debug, Clone)] +pub struct EwwConfig { + widgets: HashMap, + windows: HashMap, + initial_variables: HashMap, + script_vars: HashMap, +} + +impl Default for EwwConfig { + fn default() -> Self { + Self { widgets: HashMap::new(), windows: HashMap::new(), initial_variables: HashMap::new(), script_vars: HashMap::new() } + } +} + +impl EwwConfig { + pub fn read_from_file(files: &mut YuckFiles, path: impl AsRef) -> Result { + if !path.as_ref().exists() { + bail!("The configuration file `{}` does not exist", path.as_ref().display()); + } + let config = Config::generate_from_main_file(files, path)?; + + // run some validations on the configuration + yuck::config::validate::validate(&config, super::inbuilt::get_inbuilt_vars().keys().cloned().collect())?; + + let Config { widget_definitions, window_definitions, var_definitions, mut script_vars } = config; + script_vars.extend(crate::config::inbuilt::get_inbuilt_vars()); + Ok(EwwConfig { + windows: window_definitions + .into_iter() + .map(|(name, window)| Ok((name, EwwWindowDefinition::generate(&widget_definitions, window)?))) + .collect::>>()?, + widgets: widget_definitions, + initial_variables: var_definitions.into_iter().map(|(k, v)| (k, v.initial_value)).collect(), + script_vars, + }) + } + + // TODO this is kinda ugly + pub fn generate_initial_state(&self) -> Result> { + let mut vars = self + .script_vars + .iter() + .map(|(name, var)| Ok((name.clone(), script_var::initial_value(var)?))) + .collect::>>()?; + vars.extend(self.initial_variables.clone()); + Ok(vars) + } + + pub fn get_windows(&self) -> &HashMap { + &self.windows + } + + pub fn get_window(&self, name: &String) -> Result<&EwwWindowDefinition> { + self.windows.get(name).with_context(|| { + format!( + "No window named '{}' exists in config.\nThis may also be caused by your config failing to load properly, \ + please check for any other errors in that case.", + name + ) + }) + } + + pub fn get_script_var(&self, name: &VarName) -> Result<&ScriptVarDefinition> { + self.script_vars.get(name).with_context(|| format!("No script var named '{}' exists", name)) + } + + pub fn get_widget_definitions(&self) -> &HashMap { + &self.widgets + } +} diff --git a/src/config/inbuilt.rs b/crates/eww/src/config/inbuilt.rs similarity index 63% rename from src/config/inbuilt.rs rename to crates/eww/src/config/inbuilt.rs index cd9e117..54e5949 100644 --- a/src/config/inbuilt.rs +++ b/crates/eww/src/config/inbuilt.rs @@ -1,35 +1,38 @@ -use crate::{ - config::{system_stats::*, PollScriptVar, ScriptVar, VarSource}, - value::{PrimVal as PrimitiveValue, VarName}, -}; use std::{collections::HashMap, time::Duration}; +use simplexpr::dynval::DynVal; +use yuck::config::script_var_definition::{PollScriptVar, ScriptVarDefinition, VarSource}; + +use crate::config::system_stats::*; +use eww_shared_util::VarName; + macro_rules! builtin_vars { ($interval:expr, $($name:literal => $fun:expr),*$(,)?) => {{ maplit::hashmap! { $( - VarName::from($name) => ScriptVar::Poll(PollScriptVar { + VarName::from($name) => ScriptVarDefinition::Poll(PollScriptVar { name: VarName::from($name), command: VarSource::Function($fun), interval: $interval, + name_span: eww_shared_util::span::Span::DUMMY, }) ),* } }}} -pub fn get_inbuilt_vars() -> HashMap { +pub fn get_inbuilt_vars() -> HashMap { builtin_vars! {Duration::new(2, 0), // @desc EWW_TEMPS - Heat of the components in Celcius\nExample: `{{(CPU_TEMPS.core_1 + CPU_TEMPS.core_2) / 2}}` - "EWW_TEMPS" => || Ok(PrimitiveValue::from(cores())), + "EWW_TEMPS" => || Ok(DynVal::from(cores())), // @desc EWW_RAM - The current RAM + Swap usage - "EWW_RAM" => || Ok(PrimitiveValue::from(format!("{:.2}", ram()))), + "EWW_RAM" => || Ok(DynVal::from(format!("{:.2}", ram()))), // @desc EWW_DISK - Information on on all mounted partitions (Might report inaccurately on some filesystems, like btrfs)\nExample: `{{EWW_DISK["/"]}}` - "EWW_DISK" => || Ok(PrimitiveValue::from(disk())), + "EWW_DISK" => || Ok(DynVal::from(disk())), // @desc EWW_BATTERY - Battery capacity in procent of the main battery - "EWW_BATTERY" => || Ok(PrimitiveValue::from( + "EWW_BATTERY" => || Ok(DynVal::from( match get_battery_capacity() { Err(e) => { log::error!("Couldn't get the battery capacity: {:?}", e); @@ -40,9 +43,9 @@ pub fn get_inbuilt_vars() -> HashMap { )), // @desc EWW_CPU_USAGE - Average CPU usage (all cores) since the last update (No MacOS support) - "EWW_CPU_USAGE" => || Ok(PrimitiveValue::from(get_avg_cpu_usage())), + "EWW_CPU_USAGE" => || Ok(DynVal::from(get_avg_cpu_usage())), // @desc EWW_NET - Bytes up/down on all interfaces - "EWW_NET" => || Ok(PrimitiveValue::from(net())), + "EWW_NET" => || Ok(DynVal::from(net())), } } diff --git a/crates/eww/src/config/mod.rs b/crates/eww/src/config/mod.rs new file mode 100644 index 0000000..19ed762 --- /dev/null +++ b/crates/eww/src/config/mod.rs @@ -0,0 +1,8 @@ +pub mod eww_config; +pub mod inbuilt; +pub mod script_var; +pub mod system_stats; +pub mod window_definition; +pub use eww_config::*; +pub use script_var::*; +pub use window_definition::*; diff --git a/crates/eww/src/config/script_var.rs b/crates/eww/src/config/script_var.rs new file mode 100644 index 0000000..da44b43 --- /dev/null +++ b/crates/eww/src/config/script_var.rs @@ -0,0 +1,47 @@ +use std::process::Command; + +use anyhow::*; +use codespan_reporting::diagnostic::Severity; +use eww_shared_util::{Span, VarName}; +use simplexpr::dynval::DynVal; +use yuck::{ + config::script_var_definition::{ScriptVarDefinition, VarSource}, + gen_diagnostic, +}; + +use crate::error::DiagError; + +pub fn create_script_var_failed_warn(span: Span, var_name: &VarName, error_output: &str) -> DiagError { + DiagError::new(gen_diagnostic! { + kind = Severity::Warning, + msg = format!("The script for the `{}`-variable exited unsuccessfully", var_name), + label = span => "Defined here", + note = error_output, + }) +} + +pub fn initial_value(var: &ScriptVarDefinition) -> Result { + match var { + ScriptVarDefinition::Poll(x) => match &x.command { + VarSource::Function(f) => { + f().map_err(|err| anyhow!(err)).with_context(|| format!("Failed to compute initial value for {}", &var.name())) + } + VarSource::Shell(span, command) => { + run_command(command).map_err(|e| anyhow!(create_script_var_failed_warn(*span, var.name(), &e.to_string()))) + } + }, + ScriptVarDefinition::Listen(var) => Ok(var.initial_value.clone()), + } +} + +/// Run a command and get the output +pub fn run_command(cmd: &str) -> Result { + log::debug!("Running command: {}", cmd); + let command = Command::new("/bin/sh").arg("-c").arg(cmd).output()?; + if !command.status.success() { + bail!("Failed with output:\n{}", String::from_utf8(command.stderr)?); + } + let output = String::from_utf8(command.stdout)?; + let output = output.trim_matches('\n'); + Ok(DynVal::from(output)) +} diff --git a/src/config/system_stats.rs b/crates/eww/src/config/system_stats.rs similarity index 96% rename from src/config/system_stats.rs rename to crates/eww/src/config/system_stats.rs index 282c3a1..1ddb4e4 100644 --- a/src/config/system_stats.rs +++ b/crates/eww/src/config/system_stats.rs @@ -1,13 +1,11 @@ use crate::util::IterAverage; use anyhow::*; use itertools::Itertools; -use lazy_static::lazy_static; +use once_cell::sync::Lazy; use std::{fs::read_to_string, sync::Mutex}; use sysinfo::{ComponentExt, DiskExt, NetworkExt, NetworksExt, ProcessorExt, System, SystemExt}; -lazy_static! { - static ref SYSTEM: Mutex = Mutex::new(System::new()); -} +static SYSTEM: Lazy> = Lazy::new(|| Mutex::new(System::new())); pub fn disk() -> String { let mut c = SYSTEM.lock().unwrap(); @@ -63,7 +61,6 @@ pub fn get_avg_cpu_usage() -> String { #[cfg(target_os = "macos")] pub fn get_battery_capacity() -> Result { - use regex::Regex; let capacity = String::from_utf8( std::process::Command::new("pmset") .args(&["-g", "batt"]) @@ -75,7 +72,7 @@ pub fn get_battery_capacity() -> Result { // Example output of that command: // Now drawing from 'Battery Power' //-InternalBattery-0 (id=11403363) 100%; discharging; (no estimate) present: true - let regex = Regex::new(r"[0-9]*%")?; + let regex = regex!(r"[0-9]*%"); let mut number = regex.captures(&capacity).unwrap().get(0).unwrap().as_str().to_string(); // Removes the % at the end diff --git a/crates/eww/src/config/window_definition.rs b/crates/eww/src/config/window_definition.rs new file mode 100644 index 0000000..750e8fb --- /dev/null +++ b/crates/eww/src/config/window_definition.rs @@ -0,0 +1,39 @@ +use std::collections::HashMap; + +use anyhow::*; +use yuck::config::{ + backend_window_options::BackendWindowOptions, + widget_definition::WidgetDefinition, + window_definition::{WindowDefinition, WindowStacking}, + window_geometry::WindowGeometry, +}; + +use crate::widgets::widget_node; + +/// Full window-definition containing the fully expanded widget tree. +/// **Use this** rather than `[RawEwwWindowDefinition]`. +#[derive(Debug, Clone)] +pub struct EwwWindowDefinition { + pub name: String, + + pub geometry: Option, + pub stacking: WindowStacking, + pub monitor_number: Option, + pub widget: Box, + pub resizable: bool, + pub backend_options: BackendWindowOptions, +} + +impl EwwWindowDefinition { + pub fn generate(defs: &HashMap, window: WindowDefinition) -> Result { + Ok(EwwWindowDefinition { + name: window.name, + geometry: window.geometry, + stacking: window.stacking, + monitor_number: window.monitor_number, + resizable: window.resizable, + widget: widget_node::generate_generic_widget_node(defs, &HashMap::new(), window.widget)?, + backend_options: window.backend_options, + }) + } +} diff --git a/crates/eww/src/daemon_response.rs b/crates/eww/src/daemon_response.rs new file mode 100644 index 0000000..19e96e2 --- /dev/null +++ b/crates/eww/src/daemon_response.rs @@ -0,0 +1,39 @@ +use anyhow::*; + +/// Response that the app may send as a response to a event. +/// This is used in `DaemonCommand`s that contain a response sender. +#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, derive_more::Display)] +pub enum DaemonResponse { + Success(String), + Failure(String), +} + +impl DaemonResponse { + pub fn is_success(&self) -> bool { + matches!(self, DaemonResponse::Success(_)) + } + + pub fn is_failure(&self) -> bool { + !self.is_success() + } +} + +#[derive(Debug)] +pub struct DaemonResponseSender(tokio::sync::mpsc::UnboundedSender); + +pub fn create_pair() -> (DaemonResponseSender, tokio::sync::mpsc::UnboundedReceiver) { + let (sender, recv) = tokio::sync::mpsc::unbounded_channel(); + (DaemonResponseSender(sender), recv) +} + +impl DaemonResponseSender { + pub fn send_success(&self, s: String) -> Result<()> { + self.0.send(DaemonResponse::Success(s)).context("Failed to send success response from application thread") + } + + pub fn send_failure(&self, s: String) -> Result<()> { + self.0.send(DaemonResponse::Failure(s)).context("Failed to send failure response from application thread") + } +} + +pub type DaemonResponseReceiver = tokio::sync::mpsc::UnboundedReceiver; diff --git a/src/display_backend.rs b/crates/eww/src/display_backend.rs similarity index 66% rename from src/display_backend.rs rename to crates/eww/src/display_backend.rs index cac9484..5427f76 100644 --- a/src/display_backend.rs +++ b/crates/eww/src/display_backend.rs @@ -1,57 +1,38 @@ pub use platform::*; -#[cfg(feature = "no-x11-wayland")] +#[cfg(not(any(feature = "x11", feature = "wayland")))] mod platform { - use crate::config::{EwwWindowDefinition, StrutDefinition, WindowStacking}; - use anyhow::*; - use gtk::{self, prelude::*}; + use crate::config::EwwWindowDefinition; - pub fn initialize_window(window_def: &EwwWindowDefinition, _monitor: gdk::Rectangle) -> Option { - let window = if window_def.focusable { - gtk::Window::new(gtk::WindowType::Toplevel) - } else { - gtk::Window::new(gtk::WindowType::Popup) - }; - window.set_resizable(true); - if !window_def.focusable { - window.set_type_hint(gdk::WindowTypeHint::Dock); - } - if window_def.stacking == WindowStacking::Foreground { - window.set_keep_above(true); - } else { - window.set_keep_below(true); - } - Some(window) - } - - pub fn reserve_space_for(_window: >k::Window, _monitor: gdk::Rectangle, _strut_def: StrutDefinition) -> Result<()> { - Err(anyhow!("Cannot reserve space on non X11 or and wayland backends")) + pub fn initialize_window(_window_def: &EwwWindowDefinition, _monitor: gdk::Rectangle) -> Option { + Some(gtk::Window::new(gtk::WindowType::Toplevel)) } } #[cfg(feature = "wayland")] mod platform { - use crate::config::{AnchorAlignment, EwwWindowDefinition, Side, WindowStacking}; - use anyhow::*; use gdk; use gtk::prelude::*; + use yuck::config::{window_definition::WindowStacking, window_geometry::AnchorAlignment}; + + use crate::config::EwwWindowDefinition; pub fn initialize_window(window_def: &EwwWindowDefinition, monitor: gdk::Rectangle) -> Option { let window = gtk::Window::new(gtk::WindowType::Toplevel); // Initialising a layer shell surface gtk_layer_shell::init_for_window(&window); // Sets the monitor where the surface is shown - match window_def.screen_number { + match window_def.monitor_number { Some(index) => { if let Some(monitor) = gdk::Display::get_default().expect("could not get default display").get_monitor(index) { gtk_layer_shell::set_monitor(&window, &monitor); } else { - return None + return None; } } - None => {}, + None => {} }; - window.set_resizable(true); + window.set_resizable(window_def.resizable); // Sets the layer where the layer shell surface will spawn match window_def.stacking { @@ -62,44 +43,46 @@ mod platform { } // Sets the keyboard interactivity - gtk_layer_shell::set_keyboard_interactivity(&window, window_def.focusable); - // Positioning surface - let mut top = false; - let mut left = false; - let mut right = false; - let mut bottom = false; + gtk_layer_shell::set_keyboard_interactivity(&window, window_def.backend_options.focusable); - match window_def.geometry.anchor_point.x { - AnchorAlignment::START => left = true, - AnchorAlignment::CENTER => {} - AnchorAlignment::END => right = true, + if let Some(geometry) = window_def.geometry { + // Positioning surface + let mut top = false; + let mut left = false; + let mut right = false; + let mut bottom = false; + + match geometry.anchor_point.x { + AnchorAlignment::START => left = true, + AnchorAlignment::CENTER => {} + AnchorAlignment::END => right = true, + } + match geometry.anchor_point.y { + AnchorAlignment::START => top = true, + AnchorAlignment::CENTER => {} + AnchorAlignment::END => bottom = true, + } + + gtk_layer_shell::set_anchor(&window, gtk_layer_shell::Edge::Left, left); + gtk_layer_shell::set_anchor(&window, gtk_layer_shell::Edge::Right, right); + gtk_layer_shell::set_anchor(&window, gtk_layer_shell::Edge::Top, top); + gtk_layer_shell::set_anchor(&window, gtk_layer_shell::Edge::Bottom, bottom); + + let xoffset = geometry.offset.x.relative_to(monitor.width); + let yoffset = geometry.offset.y.relative_to(monitor.height); + + if left { + gtk_layer_shell::set_margin(&window, gtk_layer_shell::Edge::Left, xoffset); + } else { + gtk_layer_shell::set_margin(&window, gtk_layer_shell::Edge::Right, xoffset); + } + if bottom { + gtk_layer_shell::set_margin(&window, gtk_layer_shell::Edge::Bottom, yoffset); + } else { + gtk_layer_shell::set_margin(&window, gtk_layer_shell::Edge::Top, yoffset); + } } - match window_def.geometry.anchor_point.y { - AnchorAlignment::START => top = true, - AnchorAlignment::CENTER => {} - AnchorAlignment::END => bottom = true, - } - - gtk_layer_shell::set_anchor(&window, gtk_layer_shell::Edge::Left, left); - gtk_layer_shell::set_anchor(&window, gtk_layer_shell::Edge::Right, right); - gtk_layer_shell::set_anchor(&window, gtk_layer_shell::Edge::Top, top); - gtk_layer_shell::set_anchor(&window, gtk_layer_shell::Edge::Bottom, bottom); - - let xoffset = window_def.geometry.offset.x.relative_to(monitor.width); - let yoffset = window_def.geometry.offset.y.relative_to(monitor.height); - - if left { - gtk_layer_shell::set_margin(&window, gtk_layer_shell::Edge::Left, xoffset); - } else { - gtk_layer_shell::set_margin(&window, gtk_layer_shell::Edge::Right, xoffset); - } - if bottom { - gtk_layer_shell::set_margin(&window, gtk_layer_shell::Edge::Bottom, yoffset); - } else { - gtk_layer_shell::set_margin(&window, gtk_layer_shell::Edge::Top, yoffset); - } - - if window_def.exclusive { + if window_def.backend_options.exclusive { gtk_layer_shell::auto_exclusive_zone_enable(&window); } Some(window) @@ -108,7 +91,6 @@ mod platform { #[cfg(feature = "x11")] mod platform { - use crate::config::{EwwWindowDefinition, EwwWindowType, Side, WindowStacking}; use anyhow::*; use gdkx11; use gtk::{self, prelude::*}; @@ -120,21 +102,26 @@ mod platform { protocol::xproto::*, rust_connection::{DefaultStream, RustConnection}, }; + use yuck::config::{ + backend_window_options::{Side, WindowType}, + window_definition::WindowStacking, + }; + + use crate::config::EwwWindowDefinition; pub fn initialize_window(window_def: &EwwWindowDefinition, _monitor: gdk::Rectangle) -> Option { - let window = if window_def.focusable { - gtk::Window::new(gtk::WindowType::Toplevel) + let window_type = if window_def.backend_options.wm_ignore { gtk::WindowType::Popup } else { gtk::WindowType::Toplevel }; + let window = gtk::Window::new(window_type); + let wm_class_name = format!("eww-{}", window_def.name); + #[allow(deprecated)] + window.set_wmclass(&wm_class_name, &wm_class_name); + window.set_resizable(window_def.resizable); + window.set_keep_above(window_def.stacking == WindowStacking::Foreground); + window.set_keep_below(window_def.stacking == WindowStacking::Background); + if window_def.backend_options.sticky { + window.stick(); } else { - gtk::Window::new(gtk::WindowType::Popup) - }; - window.set_resizable(true); - if !window_def.focusable { - window.set_type_hint(gdk::WindowTypeHint::Dock); - } - if window_def.stacking == WindowStacking::Foreground { - window.set_keep_above(true); - } else { - window.set_keep_below(true); + window.unstick(); } Some(window) } @@ -172,7 +159,7 @@ mod platform { .ok() .context("Failed to get x11 window for gtk window")? .get_xid() as u32; - let strut_def = window_def.struts; + let strut_def = window_def.backend_options.struts; let root_window_geometry = self.conn.get_geometry(self.root_window)?.reply()?; let mon_end_x = (monitor_rect.x + monitor_rect.width) as u32 - 1u32; @@ -225,11 +212,12 @@ mod platform { win_id, self.atoms._NET_WM_WINDOW_TYPE, self.atoms.ATOM, - &[match window_def.window_type { - EwwWindowType::Dock => self.atoms._NET_WM_WINDOW_TYPE_DOCK, - EwwWindowType::Normal => self.atoms._NET_WM_WINDOW_TYPE_NORMAL, - EwwWindowType::Dialog => self.atoms._NET_WM_WINDOW_TYPE_DIALOG, - EwwWindowType::Toolbar => self.atoms._NET_WM_WINDOW_TYPE_TOOLBAR, + &[match window_def.backend_options.window_type { + WindowType::Dock => self.atoms._NET_WM_WINDOW_TYPE_DOCK, + WindowType::Normal => self.atoms._NET_WM_WINDOW_TYPE_NORMAL, + WindowType::Dialog => self.atoms._NET_WM_WINDOW_TYPE_DIALOG, + WindowType::Toolbar => self.atoms._NET_WM_WINDOW_TYPE_TOOLBAR, + WindowType::Utility => self.atoms._NET_WM_WINDOW_TYPE_UTILITY, }], )? .check()?; @@ -245,6 +233,7 @@ mod platform { _NET_WM_WINDOW_TYPE_DOCK, _NET_WM_WINDOW_TYPE_DIALOG, _NET_WM_WINDOW_TYPE_TOOLBAR, + _NET_WM_WINDOW_TYPE_UTILITY, _NET_WM_STATE, _NET_WM_STATE_STICKY, _NET_WM_STATE_ABOVE, diff --git a/crates/eww/src/error.rs b/crates/eww/src/error.rs new file mode 100644 index 0000000..b21348a --- /dev/null +++ b/crates/eww/src/error.rs @@ -0,0 +1,20 @@ +use codespan_reporting::diagnostic::Diagnostic; + +/// An error that contains a [Diagnostic] for ad-hoc creation of diagnostics. +#[derive(Debug)] +pub struct DiagError { + pub diag: Diagnostic, +} + +impl DiagError { + pub fn new(diag: Diagnostic) -> Self { + Self { diag } + } +} + +impl std::error::Error for DiagError {} +impl std::fmt::Display for DiagError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.diag.message) + } +} diff --git a/crates/eww/src/error_handling_ctx.rs b/crates/eww/src/error_handling_ctx.rs new file mode 100644 index 0000000..b965b4a --- /dev/null +++ b/crates/eww/src/error_handling_ctx.rs @@ -0,0 +1,88 @@ +//! Disgusting global state. +//! I hate this, but [buffet](https://github.com/buffet) told me that this is what I should do for peak maintainability! + +use std::sync::{Arc, RwLock}; + +use codespan_reporting::{ + diagnostic::Diagnostic, + term::{self, Chars}, +}; +use eww_shared_util::Span; +use once_cell::sync::Lazy; +use simplexpr::{dynval::ConversionError, eval::EvalError}; +use yuck::{ + config::{file_provider::YuckFiles, validate::ValidationError}, + error::AstError, + format_diagnostic::ToDiagnostic, +}; + +use crate::error::DiagError; + +pub static YUCK_FILES: Lazy>> = Lazy::new(|| Arc::new(RwLock::new(YuckFiles::new()))); + +pub fn clear_files() { + *YUCK_FILES.write().unwrap() = YuckFiles::new(); +} + +pub fn print_error(err: anyhow::Error) { + match anyhow_err_to_diagnostic(&err) { + Some(diag) => match stringify_diagnostic(diag) { + Ok(diag) => { + eprintln!("{}", diag); + } + Err(_) => { + log::error!("{:?}", err); + } + }, + None => { + log::error!("{:?}", err); + } + } +} + +pub fn format_error(err: &anyhow::Error) -> String { + for err in err.chain() { + format!("chain: {}", err); + } + anyhow_err_to_diagnostic(err).and_then(|diag| stringify_diagnostic(diag).ok()).unwrap_or_else(|| format!("{:?}", err)) +} + +pub fn anyhow_err_to_diagnostic(err: &anyhow::Error) -> Option> { + if let Some(err) = err.downcast_ref::() { + Some(err.diag.clone()) + } else if let Some(err) = err.downcast_ref::() { + Some(err.to_diagnostic()) + } else if let Some(err) = err.downcast_ref::() { + Some(err.to_diagnostic()) + } else if let Some(err) = err.downcast_ref::() { + Some(err.to_diagnostic()) + } else if let Some(err) = err.downcast_ref::() { + Some(err.to_diagnostic()) + } else { + None + } +} + +// pub fn print_diagnostic(diagnostic: codespan_reporting::diagnostic::Diagnostic) { +// match stringify_diagnostic(diagnostic.clone()) { +// Ok(diag) => { +// eprintln!("{}", diag); +//} +// Err(_) => { +// log::error!("{:?}", diagnostic); +//} + +pub fn stringify_diagnostic(mut diagnostic: codespan_reporting::diagnostic::Diagnostic) -> anyhow::Result { + diagnostic.labels.drain_filter(|label| Span(label.range.start, label.range.end, label.file_id).is_dummy()); + + let mut config = term::Config::default(); + let mut chars = Chars::box_drawing(); + chars.single_primary_caret = '─'; + config.chars = chars; + config.chars.note_bullet = '→'; + let mut buf = Vec::new(); + let mut writer = term::termcolor::Ansi::new(&mut buf); + let files = YUCK_FILES.read().unwrap(); + term::emit(&mut writer, &config, &*files, &diagnostic)?; + Ok(String::from_utf8(buf)?) +} diff --git a/src/eww_state.rs b/crates/eww/src/eww_state.rs similarity index 66% rename from src/eww_state.rs rename to crates/eww/src/eww_state.rs index 7f929e5..7bba210 100644 --- a/src/eww_state.rs +++ b/crates/eww/src/eww_state.rs @@ -1,39 +1,42 @@ -use crate::{ - config::window_definition::WindowName, - value::{AttrName, AttrValElement, VarName}, -}; use anyhow::*; +use eww_shared_util::{AttrName, VarName}; use std::{collections::HashMap, sync::Arc}; -use crate::value::{AttrVal, PrimVal}; +use simplexpr::{dynval::DynVal, SimplExpr}; + +use crate::error_handling_ctx; /// Handler that gets executed to apply the necessary parts of the eww state to /// a gtk widget. These are created and initialized in EwwState::resolve. pub struct StateChangeHandler { - func: Box) -> Result<()> + 'static>, - unresolved_values: HashMap, + func: Box) -> Result<()> + 'static>, + unresolved_values: HashMap, } impl StateChangeHandler { fn used_variables(&self) -> impl Iterator { - self.unresolved_values.iter().flat_map(|(_, value)| value.var_refs()) + self.unresolved_values.iter().flat_map(|(_, value)| value.var_refs()).map(|(_, value)| value) } /// Run the StateChangeHandler. /// [`state`] should be the global [EwwState::state]. - fn run_with_state(&self, state: &HashMap) { + fn run_with_state(&self, state: &HashMap) { let resolved_attrs = self .unresolved_values .clone() .into_iter() - .map(|(attr_name, value)| Ok((attr_name, value.resolve_fully(state)?))) + .map(|(attr_name, value)| Ok((attr_name, value.eval(state)?))) .collect::>(); match resolved_attrs { Ok(resolved_attrs) => { - crate::print_result_err!("while updating UI based after state change", &(self.func)(resolved_attrs)) + if let Err(err) = (self.func)(resolved_attrs).context("Error while updating UI after state change") { + error_handling_ctx::print_error(err); + } + } + Err(err) => { + error_handling_ctx::print_error(err); } - Err(err) => log::error!("Error while resolving attributes: {:?}", err), } } } @@ -60,8 +63,8 @@ impl EwwWindowState { /// window-specific state-change handlers. #[derive(Default)] pub struct EwwState { - windows: HashMap, - variables_state: HashMap, + windows: HashMap, + variables_state: HashMap, } impl std::fmt::Debug for EwwState { @@ -71,16 +74,16 @@ impl std::fmt::Debug for EwwState { } impl EwwState { - pub fn from_default_vars(defaults: HashMap) -> Self { + pub fn from_default_vars(defaults: HashMap) -> Self { EwwState { variables_state: defaults, ..EwwState::default() } } - pub fn get_variables(&self) -> &HashMap { + pub fn get_variables(&self) -> &HashMap { &self.variables_state } /// remove all state stored specific to one window - pub fn clear_window_state(&mut self, window_name: &WindowName) { + pub fn clear_window_state(&mut self, window_name: &str) { self.windows.remove(window_name); } @@ -91,7 +94,7 @@ impl EwwState { /// Update the value of a variable, running all registered /// [StateChangeHandler]s. - pub fn update_variable(&mut self, key: VarName, value: PrimVal) { + pub fn update_variable(&mut self, key: VarName, value: DynVal) { self.variables_state.insert(key.clone(), value); // run all of the handlers @@ -103,27 +106,21 @@ impl EwwState { } /// Look up a single variable in the eww state, returning an `Err` when the value is not found. - pub fn lookup(&self, var_name: &VarName) -> Result<&PrimVal> { + pub fn lookup(&self, var_name: &VarName) -> Result<&DynVal> { self.variables_state.get(var_name).with_context(|| format!("Unknown variable '{}' referenced", var_name)) } /// resolves a value if possible, using the current eww_state. - pub fn resolve_once<'a>(&'a self, value: &'a AttrVal) -> Result { - value - .iter() - .map(|element| match element { - AttrValElement::Primitive(primitive) => Ok(primitive.clone()), - AttrValElement::Expr(expr) => expr.clone().eval(&self.variables_state), - }) - .collect() + pub fn resolve_once<'a>(&'a self, value: &'a SimplExpr) -> Result { + Ok(value.clone().eval(&self.variables_state)?) } /// Resolve takes a function that applies a set of fully resolved attribute /// values to it's gtk widget. - pub fn resolve) -> Result<()> + 'static + Clone>( + pub fn resolve) -> Result<()> + 'static + Clone>( &mut self, - window_name: &WindowName, - required_attributes: HashMap, + window_name: &str, + required_attributes: HashMap, set_value: F, ) { let handler = StateChangeHandler { func: Box::new(set_value), unresolved_values: required_attributes }; @@ -132,7 +129,7 @@ impl EwwState { // only store the handler if at least one variable is being used if handler.used_variables().next().is_some() { - self.windows.entry(window_name.clone()).or_insert_with(EwwWindowState::default).put_handler(handler); + self.windows.entry(window_name.to_string()).or_insert_with(EwwWindowState::default).put_handler(handler); } } @@ -140,7 +137,7 @@ impl EwwState { self.windows.values().flat_map(|w| w.state_change_handlers.keys()) } - pub fn vars_referenced_in(&self, window_name: &WindowName) -> std::collections::HashSet<&VarName> { + pub fn vars_referenced_in(&self, window_name: &str) -> std::collections::HashSet<&VarName> { self.windows.get(window_name).map(|window| window.state_change_handlers.keys().collect()).unwrap_or_default() } } diff --git a/src/geometry.rs b/crates/eww/src/geometry.rs similarity index 100% rename from src/geometry.rs rename to crates/eww/src/geometry.rs diff --git a/src/ipc_server.rs b/crates/eww/src/ipc_server.rs similarity index 97% rename from src/ipc_server.rs rename to crates/eww/src/ipc_server.rs index ac1e116..2faadfe 100644 --- a/src/ipc_server.rs +++ b/crates/eww/src/ipc_server.rs @@ -38,7 +38,7 @@ async fn handle_connection(mut stream: tokio::net::UnixStream, evt_send: Unbound evt_send.send(command)?; if let Some(mut response_recv) = maybe_response_recv { - log::info!("Waiting for response for IPC client"); + log::debug!("Waiting for response for IPC client"); if let Ok(Some(response)) = tokio::time::timeout(Duration::from_millis(100), response_recv.recv()).await { let response = bincode::serialize(&response)?; let result = &stream_write.write_all(&response).await; diff --git a/crates/eww/src/main.rs b/crates/eww/src/main.rs new file mode 100644 index 0000000..41c53ad --- /dev/null +++ b/crates/eww/src/main.rs @@ -0,0 +1,247 @@ +#![feature(trace_macros)] +#![feature(drain_filter)] +#![feature(box_syntax)] +#![feature(box_patterns)] +#![feature(slice_concat_trait)] +#![feature(result_cloned)] +#![feature(try_blocks)] +#![feature(nll)] + +extern crate gio; +extern crate gtk; +#[cfg(feature = "wayland")] +extern crate gtk_layer_shell as gtk_layer_shell; + +use anyhow::*; +use daemon_response::DaemonResponseReceiver; +use opts::ActionWithServer; +use std::{ + os::unix::net, + path::{Path, PathBuf}, + time::Duration, +}; + +use crate::server::ForkResult; + +pub mod app; +pub mod application_lifecycle; +pub mod client; +pub mod config; +mod daemon_response; +pub mod display_backend; +pub mod error; +mod error_handling_ctx; +pub mod eww_state; +pub mod geometry; +pub mod ipc_server; +pub mod opts; +pub mod script_var_handler; +pub mod server; +pub mod util; +pub mod widgets; + +fn main() { + let eww_binary_name = std::env::args().next().unwrap(); + let opts: opts::Opt = opts::Opt::from_env(); + + let log_level_filter = if opts.log_debug { log::LevelFilter::Debug } else { log::LevelFilter::Info }; + if std::env::var("RUST_LOG").is_ok() { + pretty_env_logger::init_timed(); + } else { + pretty_env_logger::formatted_timed_builder().filter(Some("eww"), log_level_filter).init(); + } + + let result: Result<()> = try { + let paths = opts + .config_path + .map(EwwPaths::from_config_dir) + .unwrap_or_else(EwwPaths::default) + .context("Failed to initialize eww paths")?; + + let would_show_logs = match opts.action { + opts::Action::ClientOnly(action) => { + client::handle_client_only_action(&paths, action)?; + false + } + + // a running daemon is necessary for this command + opts::Action::WithServer(action) if action.can_start_daemon() => { + if opts.restart { + let _ = handle_server_command(&paths, &ActionWithServer::KillServer, 1); + std::thread::sleep(std::time::Duration::from_millis(200)); + } + + // attempt to just send the command to a running daemon + if let Err(err) = handle_server_command(&paths, &action, 5) { + // connecting to the daemon failed. Thus, start the daemon here! + log::warn!("Failed to connect to daemon: {}", err); + log::info!("Initializing eww server. ({})", paths.get_ipc_socket_file().display()); + let _ = std::fs::remove_file(paths.get_ipc_socket_file()); + if !opts.show_logs { + println!("Run `{} logs` to see any errors while editing your configuration.", eww_binary_name); + } + + let (command, response_recv) = action.into_daemon_command(); + // start the daemon and give it the command + let fork_result = server::initialize_server(paths.clone(), Some(command))?; + let is_parent = fork_result == ForkResult::Parent; + if let (Some(recv), true) = (response_recv, is_parent) { + listen_for_daemon_response(recv); + } + is_parent + } else { + true + } + } + opts::Action::WithServer(ActionWithServer::KillServer) => { + handle_server_command(&paths, &ActionWithServer::KillServer, 1)?; + false + } + + opts::Action::WithServer(action) => { + handle_server_command(&paths, &action, 5)?; + true + } + + // make sure that there isn't already a Eww daemon running. + opts::Action::Daemon if check_server_running(paths.get_ipc_socket_file()) => { + eprintln!("Eww server already running."); + true + } + opts::Action::Daemon => { + log::info!("Initializing Eww server. ({})", paths.get_ipc_socket_file().display()); + let _ = std::fs::remove_file(paths.get_ipc_socket_file()); + + if !opts.show_logs { + println!("Run `{} logs` to see any errors while editing your configuration.", eww_binary_name); + } + let fork_result = server::initialize_server(paths.clone(), None)?; + fork_result == ForkResult::Parent + } + }; + if would_show_logs && opts.show_logs { + client::handle_client_only_action(&paths, opts::ActionClientOnly::Logs)?; + } + }; + + if let Err(e) = result { + error_handling_ctx::print_error(e); + std::process::exit(1); + } +} + +fn listen_for_daemon_response(mut recv: DaemonResponseReceiver) { + let rt = tokio::runtime::Builder::new_current_thread().enable_time().build().expect("Failed to initialize tokio runtime"); + rt.block_on(async { + if let Ok(Some(response)) = tokio::time::timeout(Duration::from_millis(100), recv.recv()).await { + println!("{}", response); + } + }) +} + +fn handle_server_command(paths: &EwwPaths, action: &ActionWithServer, connect_attempts: usize) -> Result<()> { + log::debug!("Trying to find server process at socket {}", paths.get_ipc_socket_file().display()); + let mut stream = attempt_connect(&paths.get_ipc_socket_file(), connect_attempts).context("Failed to connect to daemon")?; + log::debug!("Connected to Eww server ({}).", &paths.get_ipc_socket_file().display()); + let response = client::do_server_call(&mut stream, action).context("Error while forwarding command to server")?; + if let Some(response) = response { + println!("{}", response); + } + Ok(()) +} + +fn attempt_connect(socket_path: impl AsRef, attempts: usize) -> Option { + for _ in 0..attempts { + if let Ok(mut con) = net::UnixStream::connect(&socket_path) { + if client::do_server_call(&mut con, &opts::ActionWithServer::Ping).is_ok() { + return net::UnixStream::connect(&socket_path).ok(); + } + } + std::thread::sleep(Duration::from_millis(200)); + } + None +} + +/// Check if a eww server is currently running by trying to send a ping message to it. +fn check_server_running(socket_path: impl AsRef) -> bool { + let response = net::UnixStream::connect(socket_path) + .ok() + .and_then(|mut stream| client::do_server_call(&mut stream, &opts::ActionWithServer::Ping).ok()); + response.is_some() +} + +#[derive(Debug, Clone)] +pub struct EwwPaths { + log_file: PathBuf, + ipc_socket_file: PathBuf, + config_dir: PathBuf, +} + +impl EwwPaths { + pub fn from_config_dir>(config_dir: P) -> Result { + let config_dir = config_dir.as_ref(); + if config_dir.is_file() { + bail!("Please provide the path to the config directory, not a file within it") + } + + if !config_dir.exists() { + bail!("Configuration directory {} does not exist", config_dir.display()); + } + + let config_dir = config_dir.canonicalize()?; + let daemon_id = base64::encode(format!("{}", config_dir.display())); + + Ok(EwwPaths { + config_dir, + log_file: std::env::var("XDG_CACHE_HOME") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from(std::env::var("HOME").unwrap()).join(".cache")) + .join(format!("eww_{}.log", daemon_id)), + ipc_socket_file: std::env::var("XDG_RUNTIME_DIR") + .map(std::path::PathBuf::from) + .unwrap_or_else(|_| std::path::PathBuf::from("/tmp")) + .join(format!("eww-server_{}", daemon_id)), + }) + } + + pub fn default() -> Result { + let config_dir = std::env::var("XDG_CONFIG_HOME") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from(std::env::var("HOME").unwrap()).join(".config")) + .join("eww"); + + Self::from_config_dir(config_dir) + } + + pub fn get_log_file(&self) -> &Path { + self.log_file.as_path() + } + + pub fn get_ipc_socket_file(&self) -> &Path { + self.ipc_socket_file.as_path() + } + + pub fn get_config_dir(&self) -> &Path { + self.config_dir.as_path() + } + + pub fn get_yuck_path(&self) -> PathBuf { + self.config_dir.join("eww.yuck") + } + + pub fn get_eww_scss_path(&self) -> PathBuf { + self.config_dir.join("eww.scss") + } +} + +impl std::fmt::Display for EwwPaths { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "config-dir: {}, ipc-socket: {}, log-file: {}", + self.config_dir.display(), + self.ipc_socket_file.display(), + self.log_file.display() + ) + } +} diff --git a/src/opts.rs b/crates/eww/src/opts.rs similarity index 75% rename from src/opts.rs rename to crates/eww/src/opts.rs index 5e84c99..3925ed4 100644 --- a/src/opts.rs +++ b/crates/eww/src/opts.rs @@ -1,17 +1,21 @@ use anyhow::*; +use eww_shared_util::VarName; use serde::{Deserialize, Serialize}; +use simplexpr::dynval::DynVal; use structopt::StructOpt; +use yuck::{config::window_geometry::AnchorPoint, value::Coords}; use crate::{ app, - config::{AnchorPoint, WindowName}, - value::{Coords, PrimVal, VarName}, + daemon_response::{self, DaemonResponse, DaemonResponseSender}, }; /// Struct that gets generated from `RawOpt`. #[derive(Debug, Serialize, Deserialize, PartialEq)] pub struct Opt { pub log_debug: bool, + pub show_logs: bool, + pub restart: bool, pub config_path: Option, pub action: Action, } @@ -22,10 +26,18 @@ struct RawOpt { #[structopt(long = "debug", global = true)] log_debug: bool, - /// override path to configuration directory (directory that contains eww.xml and eww.scss) + /// override path to configuration directory (directory that contains eww.yuck and eww.scss) #[structopt(short, long, global = true)] config: Option, + /// Watch the log output after executing the command + #[structopt(long = "logs", global = true)] + show_logs: bool, + + /// Restart the daemon completely before running the command + #[structopt(long = "restart", global = true)] + restart: bool, + #[structopt(subcommand)] action: Action, } @@ -61,18 +73,18 @@ pub enum ActionWithServer { Update { /// variable_name="new_value"-pairs that will be updated #[structopt(parse(try_from_str = parse_var_update_arg))] - mappings: Vec<(VarName, PrimVal)>, + mappings: Vec<(VarName, DynVal)>, }, /// open a window #[structopt(name = "open", alias = "o")] OpenWindow { /// Name of the window you want to open. - window_name: WindowName, + window_name: String, /// Monitor-index the window should open on - #[structopt(short, long)] - monitor: Option, + #[structopt(long)] + screen: Option, /// The position of the window, where it should open. #[structopt(short, long)] @@ -85,16 +97,20 @@ pub enum ActionWithServer { /// Sidepoint of the window, formatted like "top right" #[structopt(short, long)] anchor: Option, + + /// If the window is already open, close it instead + #[structopt(long = "toggle")] + should_toggle: bool, }, /// Open multiple windows at once. /// NOTE: This will in the future be part of eww open, and will then be removed. #[structopt(name = "open-many")] - OpenMany { windows: Vec }, + OpenMany { windows: Vec }, /// Close the window with the given name #[structopt(name = "close", alias = "c")] - CloseWindow { window_name: WindowName }, + CloseWindow { window_name: String }, /// Reload the configuration #[structopt(name = "reload", alias = "r")] @@ -137,40 +153,48 @@ impl Opt { impl From for Opt { fn from(other: RawOpt) -> Self { - let RawOpt { action, log_debug, config } = other; - Opt { action, log_debug, config_path: config } + let RawOpt { action, log_debug, show_logs, config, restart } = other; + Opt { action, log_debug, show_logs, config_path: config, restart } } } -fn parse_var_update_arg(s: &str) -> Result<(VarName, PrimVal)> { +fn parse_var_update_arg(s: &str) -> Result<(VarName, DynVal)> { let (name, value) = s .split_once('=') .with_context(|| format!("arguments must be in the shape `variable_name=\"new_value\"`, but got: {}", s))?; - Ok((name.into(), PrimVal::from_string(value.to_owned()))) + Ok((name.into(), DynVal::from_string(value.to_owned()))) } impl ActionWithServer { - pub fn into_daemon_command(self) -> (app::DaemonCommand, Option) { + pub fn can_start_daemon(&self) -> bool { + match self { + ActionWithServer::OpenWindow { .. } | ActionWithServer::OpenMany { .. } => true, + _ => false, + } + } + + pub fn into_daemon_command(self) -> (app::DaemonCommand, Option) { let command = match self { - ActionWithServer::Update { mappings } => app::DaemonCommand::UpdateVars(mappings.into_iter().collect()), + ActionWithServer::Update { mappings } => app::DaemonCommand::UpdateVars(mappings), ActionWithServer::KillServer => app::DaemonCommand::KillServer, ActionWithServer::CloseAll => app::DaemonCommand::CloseAll, ActionWithServer::Ping => { let (send, recv) = tokio::sync::mpsc::unbounded_channel(); - let _ = send.send(app::DaemonResponse::Success("pong".to_owned())); + let _ = send.send(DaemonResponse::Success("pong".to_owned())); return (app::DaemonCommand::NoOp, Some(recv)); } ActionWithServer::OpenMany { windows } => { return with_response_channel(|sender| app::DaemonCommand::OpenMany { windows, sender }); } - ActionWithServer::OpenWindow { window_name, pos, size, monitor, anchor } => { + ActionWithServer::OpenWindow { window_name, pos, size, screen, anchor, should_toggle } => { return with_response_channel(|sender| app::DaemonCommand::OpenWindow { window_name, pos, size, anchor, - monitor, + screen, + should_toggle, sender, }) } @@ -188,10 +212,10 @@ impl ActionWithServer { } } -fn with_response_channel(f: F) -> (O, Option>) +fn with_response_channel(f: F) -> (O, Option>) where - F: FnOnce(tokio::sync::mpsc::UnboundedSender) -> O, + F: FnOnce(DaemonResponseSender) -> O, { - let (sender, recv) = tokio::sync::mpsc::unbounded_channel(); + let (sender, recv) = daemon_response::create_pair(); (f(sender), Some(recv)) } diff --git a/src/script_var_handler.rs b/crates/eww/src/script_var_handler.rs similarity index 69% rename from src/script_var_handler.rs rename to crates/eww/src/script_var_handler.rs index d867a2b..0e9cb00 100644 --- a/src/script_var_handler.rs +++ b/crates/eww/src/script_var_handler.rs @@ -1,17 +1,20 @@ use std::collections::HashMap; use crate::{ - app, config, - value::{PrimVal, VarName}, + app, + config::{create_script_var_failed_warn, script_var}, }; use anyhow::*; use app::DaemonCommand; +use eww_shared_util::VarName; +use simplexpr::dynval::DynVal; use tokio::{ io::{AsyncBufReadExt, BufReader}, sync::mpsc::UnboundedSender, }; use tokio_util::sync::CancellationToken; +use yuck::config::script_var_definition::{ListenScriptVar, PollScriptVar, ScriptVarDefinition, VarSource}; /// Initialize the script var handler, and return a handle to that handler, which can be used to control /// the script var execution. @@ -23,7 +26,7 @@ pub fn init(evt_send: UnboundedSender) -> ScriptVarHandlerHandle rt.block_on(async { let _: Result<_> = try { let mut handler = ScriptVarHandler { - tail_handler: TailVarHandler::new(evt_send.clone())?, + listen_handler: ListenVarHandler::new(evt_send.clone())?, poll_handler: PollVarHandler::new(evt_send)?, }; crate::loop_select_exiting! { @@ -53,7 +56,7 @@ pub struct ScriptVarHandlerHandle { impl ScriptVarHandlerHandle { /// Add a new script-var that should be executed. - pub fn add(&self, script_var: config::ScriptVar) { + pub fn add(&self, script_var: ScriptVarDefinition) { crate::print_result_err!( "while forwarding instruction to script-var handler", self.msg_send.send(ScriptVarHandlerMsg::AddVar(script_var)) @@ -80,29 +83,29 @@ impl ScriptVarHandlerHandle { /// Message enum used by the ScriptVarHandlerHandle to communicate to the ScriptVarHandler #[derive(Debug, Eq, PartialEq)] enum ScriptVarHandlerMsg { - AddVar(config::ScriptVar), + AddVar(ScriptVarDefinition), Stop(VarName), StopAll, } -/// Handler that manages running and updating [ScriptVar]s +/// Handler that manages running and updating [ScriptVarDefinition]s struct ScriptVarHandler { - tail_handler: TailVarHandler, + listen_handler: ListenVarHandler, poll_handler: PollVarHandler, } impl ScriptVarHandler { - async fn add(&mut self, script_var: config::ScriptVar) { + async fn add(&mut self, script_var: ScriptVarDefinition) { match script_var { - config::ScriptVar::Poll(var) => self.poll_handler.start(var).await, - config::ScriptVar::Tail(var) => self.tail_handler.start(var).await, + ScriptVarDefinition::Poll(var) => self.poll_handler.start(var).await, + ScriptVarDefinition::Listen(var) => self.listen_handler.start(var).await, }; } /// Stop the handler that is responsible for a given variable. fn stop_for_variable(&mut self, name: &VarName) -> Result<()> { log::debug!("Stopping script var process for variable {}", name); - self.tail_handler.stop_for_variable(name); + self.listen_handler.stop_for_variable(name); self.poll_handler.stop_for_variable(name); Ok(()) } @@ -110,7 +113,7 @@ impl ScriptVarHandler { /// stop all running scripts and schedules fn stop_all(&mut self) { log::debug!("Stopping script-var-handlers"); - self.tail_handler.stop_all(); + self.listen_handler.stop_all(); self.poll_handler.stop_all(); } } @@ -126,24 +129,29 @@ impl PollVarHandler { Ok(handler) } - async fn start(&mut self, var: config::PollScriptVar) { + async fn start(&mut self, var: PollScriptVar) { log::debug!("starting poll var {}", &var.name); let cancellation_token = CancellationToken::new(); self.poll_handles.insert(var.name.clone(), cancellation_token.clone()); let evt_send = self.evt_send.clone(); tokio::spawn(async move { let result: Result<_> = try { - evt_send.send(app::DaemonCommand::UpdateVars(vec![(var.name.clone(), var.run_once()?)]))?; + evt_send.send(app::DaemonCommand::UpdateVars(vec![(var.name.clone(), run_poll_once(&var)?)]))?; }; - crate::print_result_err!("while running script-var command", &result); + if let Err(err) = result { + crate::error_handling_ctx::print_error(err); + } crate::loop_select_exiting! { _ = cancellation_token.cancelled() => break, _ = tokio::time::sleep(var.interval) => { let result: Result<_> = try { - evt_send.send(app::DaemonCommand::UpdateVars(vec![(var.name.clone(), var.run_once()?)]))?; + evt_send.send(app::DaemonCommand::UpdateVars(vec![(var.name.clone(), run_poll_once(&var)?)]))?; }; - crate::print_result_err!("while running script-var command", &result); + + if let Err(err) = result { + crate::error_handling_ctx::print_error(err); + } } } }); @@ -161,45 +169,58 @@ impl PollVarHandler { } } +fn run_poll_once(var: &PollScriptVar) -> Result { + match &var.command { + VarSource::Shell(span, command) => { + script_var::run_command(command).map_err(|e| anyhow!(create_script_var_failed_warn(*span, &var.name, &e.to_string()))) + } + VarSource::Function(x) => x().map_err(|e| anyhow!(e)), + } +} + impl Drop for PollVarHandler { fn drop(&mut self) { self.stop_all(); } } -struct TailVarHandler { +struct ListenVarHandler { evt_send: UnboundedSender, - tail_process_handles: HashMap, + listen_process_handles: HashMap, } -impl TailVarHandler { +impl ListenVarHandler { fn new(evt_send: UnboundedSender) -> Result { - let handler = TailVarHandler { evt_send, tail_process_handles: HashMap::new() }; + let handler = ListenVarHandler { evt_send, listen_process_handles: HashMap::new() }; Ok(handler) } - async fn start(&mut self, var: config::TailScriptVar) { - log::debug!("starting poll var {}", &var.name); + async fn start(&mut self, var: ListenScriptVar) { + log::debug!("starting listen-var {}", &var.name); let cancellation_token = CancellationToken::new(); - self.tail_process_handles.insert(var.name.clone(), cancellation_token.clone()); + self.listen_process_handles.insert(var.name.clone(), cancellation_token.clone()); let evt_send = self.evt_send.clone(); tokio::spawn(async move { - crate::try_logging_errors!(format!("Executing tail var command {}", &var.command) => { + crate::try_logging_errors!(format!("Executing listen var-command {}", &var.command) => { let mut handle = tokio::process::Command::new("sh") .args(&["-c", &var.command]) .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::inherit()) + .stderr(std::process::Stdio::piped()) .stdin(std::process::Stdio::null()) .spawn()?; let mut stdout_lines = BufReader::new(handle.stdout.take().unwrap()).lines(); + let mut stderr_lines = BufReader::new(handle.stderr.take().unwrap()).lines(); crate::loop_select_exiting! { _ = handle.wait() => break, _ = cancellation_token.cancelled() => break, Ok(Some(line)) = stdout_lines.next_line() => { - let new_value = PrimVal::from_string(line.to_owned()); + let new_value = DynVal::from_string(line.to_owned()); evt_send.send(DaemonCommand::UpdateVars(vec![(var.name.to_owned(), new_value)]))?; } + Ok(Some(line)) = stderr_lines.next_line() => { + log::warn!("stderr of `{}`: {}", var.name, line); + } else => break, } let _ = handle.kill().await; @@ -208,18 +229,18 @@ impl TailVarHandler { } fn stop_for_variable(&mut self, name: &VarName) { - if let Some(token) = self.tail_process_handles.remove(name) { - log::debug!("stopped tail var {}", name); + if let Some(token) = self.listen_process_handles.remove(name) { + log::debug!("stopped listen-var {}", name); token.cancel(); } } fn stop_all(&mut self) { - self.tail_process_handles.drain().for_each(|(_, token)| token.cancel()); + self.listen_process_handles.drain().for_each(|(_, token)| token.cancel()); } } -impl Drop for TailVarHandler { +impl Drop for ListenVarHandler { fn drop(&mut self) { self.stop_all(); } diff --git a/src/server.rs b/crates/eww/src/server.rs similarity index 61% rename from src/server.rs rename to crates/eww/src/server.rs index 3df39a6..81e3712 100644 --- a/src/server.rs +++ b/crates/eww/src/server.rs @@ -1,10 +1,42 @@ -use crate::{app, config, eww_state::*, ipc_server, script_var_handler, util, EwwPaths}; +use crate::{ + app::{self, DaemonCommand}, + config, daemon_response, error_handling_ctx, + eww_state::*, + ipc_server, script_var_handler, util, EwwPaths, +}; use anyhow::*; -use std::{collections::HashMap, os::unix::io::AsRawFd, path::Path}; + +use std::{ + collections::{HashMap, HashSet}, + os::unix::io::AsRawFd, + path::Path, + sync::{atomic::Ordering, Arc}, +}; use tokio::sync::mpsc::*; -pub fn initialize_server(paths: EwwPaths) -> Result<()> { - do_detach(&paths.get_log_file())?; +pub fn initialize_server(paths: EwwPaths, action: Option) -> Result { + let (ui_send, mut ui_recv) = tokio::sync::mpsc::unbounded_channel(); + + std::env::set_current_dir(&paths.get_config_dir()) + .with_context(|| format!("Failed to change working directory to {}", paths.get_config_dir().display()))?; + + log::info!("Loading paths: {}", &paths); + + let read_config = config::read_from_file(&paths.get_yuck_path()); + + let eww_config = match read_config { + Ok(config) => config, + Err(err) => { + error_handling_ctx::print_error(err); + config::EwwConfig::default() + } + }; + + let fork_result = do_detach(&paths.get_log_file())?; + + if fork_result == ForkResult::Parent { + return Ok(ForkResult::Parent); + } println!( r#" @@ -21,23 +53,17 @@ pub fn initialize_server(paths: EwwPaths) -> Result<()> { std::process::exit(1); } }); - let (ui_send, mut ui_recv) = tokio::sync::mpsc::unbounded_channel(); - - std::env::set_current_dir(&paths.get_config_dir()) - .with_context(|| format!("Failed to change working directory to {}", paths.get_config_dir().display()))?; - - log::info!("Loading paths: {}", &paths); - let eww_config = config::EwwConfig::read_from_file(&paths.get_eww_xml_path())?; gtk::init()?; - log::info!("Initializing script var handler"); + log::debug!("Initializing script var handler"); let script_var_handler = script_var_handler::init(ui_send.clone()); let mut app = app::App { eww_state: EwwState::from_default_vars(eww_config.generate_initial_state()?), eww_config, open_windows: HashMap::new(), + failed_windows: HashSet::new(), css_provider: gtk::CssProvider::new(), script_var_handler, app_evt_send: ui_send.clone(), @@ -56,6 +82,10 @@ pub fn initialize_server(paths: EwwPaths) -> Result<()> { init_async_part(app.paths.clone(), ui_send); glib::MainContext::default().spawn_local(async move { + // if an action was given to the daemon initially, execute it first. + if let Some(action) = action { + app.handle_command(action); + } while let Some(event) = ui_recv.recv().await { app.handle_command(event); } @@ -64,7 +94,7 @@ pub fn initialize_server(paths: EwwPaths) -> Result<()> { gtk::main(); log::info!("main application thread finished"); - Ok(()) + Ok(ForkResult::Child) } fn init_async_part(paths: EwwPaths, ui_send: UnboundedSender) { @@ -87,7 +117,7 @@ fn init_async_part(paths: EwwPaths, ui_send: UnboundedSender tokio::spawn(async move { // Wait for application exit event let _ = crate::application_lifecycle::recv_exit().await; - log::info!("Forward task received exit event"); + log::debug!("Forward task received exit event"); // Then forward that to the application let _ = ui_send.send(app::DaemonCommand::KillServer); }) @@ -104,34 +134,43 @@ fn init_async_part(paths: EwwPaths, ui_send: UnboundedSender /// Watch configuration files for changes, sending reload events to the eww app when the files change. async fn run_filewatch>(config_dir: P, evt_send: UnboundedSender) -> Result<()> { - use notify::Watcher; + use notify::{RecommendedWatcher, RecursiveMode, Watcher}; let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); - let mut watcher: notify::RecommendedWatcher = - notify::Watcher::new_immediate(move |res: notify::Result| match res { - Ok(event) => { - if let Err(err) = tx.send(event.paths) { + let mut watcher: RecommendedWatcher = Watcher::new(move |res: notify::Result| match res { + Ok(event) => { + let relevant_files_changed = event.paths.iter().any(|path| { + let ext = path.extension().unwrap_or_default(); + ext == "yuck" || ext == "scss" + }); + if !relevant_files_changed { + if let Err(err) = tx.send(()) { log::warn!("Error forwarding file update event: {:?}", err); } } - Err(e) => log::error!("Encountered Error While Watching Files: {}", e), - })?; - watcher.watch(&config_dir, notify::RecursiveMode::Recursive)?; + } + Err(e) => log::error!("Encountered Error While Watching Files: {}", e), + })?; + watcher.watch(&config_dir.as_ref(), RecursiveMode::Recursive)?; + + // make sure to not trigger reloads too much by only accepting one reload every 500ms. + let debounce_done = Arc::new(std::sync::atomic::AtomicBool::new(true)); crate::loop_select_exiting! { - Some(paths) = rx.recv() => { - for path in paths { - let extension = path.extension().unwrap_or_default(); - if extension != "xml" && extension != "scss" { - continue; - } + Some(()) = rx.recv() => { + let debounce_done = debounce_done.clone(); + if debounce_done.swap(false, Ordering::SeqCst) { + tokio::spawn(async move { + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + debounce_done.store(true, Ordering::SeqCst); + }); - let (daemon_resp_sender, mut daemon_resp_response) = tokio::sync::mpsc::unbounded_channel(); + let (daemon_resp_sender, mut daemon_resp_response) = daemon_response::create_pair(); evt_send.send(app::DaemonCommand::ReloadConfigAndCss(daemon_resp_sender))?; tokio::spawn(async move { match daemon_resp_response.recv().await { - Some(app::DaemonResponse::Success(_)) => log::info!("Reloaded config successfully"), - Some(app::DaemonResponse::Failure(e)) => log::error!("Failed to reload config: {}", e), + Some(daemon_response::DaemonResponse::Success(_)) => log::info!("Reloaded config successfully"), + Some(daemon_response::DaemonResponse::Failure(e)) => eprintln!("{}", e), None => log::error!("No response to reload configuration-reload request"), } }); @@ -142,14 +181,26 @@ async fn run_filewatch>(config_dir: P, evt_send: UnboundedSender< return Ok(()); } +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ForkResult { + Parent, + Child, +} + /// detach the process from the terminal, also redirecting stdout and stderr to LOG_FILE -fn do_detach(log_file_path: impl AsRef) -> Result<()> { +fn do_detach(log_file_path: impl AsRef) -> Result { // detach from terminal match unsafe { nix::unistd::fork()? } { - nix::unistd::ForkResult::Parent { .. } => { - std::process::exit(0); + nix::unistd::ForkResult::Child => { + nix::unistd::setsid()?; + match unsafe { nix::unistd::fork()? } { + nix::unistd::ForkResult::Parent { .. } => std::process::exit(0), + nix::unistd::ForkResult::Child => {} + } + } + nix::unistd::ForkResult::Parent { .. } => { + return Ok(ForkResult::Parent); } - nix::unistd::ForkResult::Child => {} } let file = std::fs::OpenOptions::new() @@ -166,5 +217,5 @@ fn do_detach(log_file_path: impl AsRef) -> Result<()> { nix::unistd::dup2(fd, std::io::stderr().as_raw_fd())?; } - Ok(()) + Ok(ForkResult::Child) } diff --git a/src/util.rs b/crates/eww/src/util.rs similarity index 82% rename from src/util.rs rename to crates/eww/src/util.rs index 2048961..afbfcc6 100644 --- a/src/util.rs +++ b/crates/eww/src/util.rs @@ -3,23 +3,6 @@ use extend::ext; use itertools::Itertools; use std::path::Path; -#[macro_export] -macro_rules! impl_try_from { - ($typ:ty { - $( - for $for:ty => |$arg:ident| $code:expr - );*; - }) => { - $(impl TryFrom<$typ> for $for { - type Error = anyhow::Error; - - fn try_from($arg: $typ) -> Result { - $code - } - })* - }; -} - #[macro_export] macro_rules! try_logging_errors { ($context:expr => $code:block) => {{ @@ -50,6 +33,14 @@ macro_rules! loop_select { } } +#[macro_export] +macro_rules! regex { + ($re:literal $(,)?) => {{ + static RE: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); + RE.get_or_init(|| regex::Regex::new($re).unwrap()) + }}; +} + /// Parse a string with a concrete set of options into some data-structure, /// and return a nicely formatted error message on invalid values. I.e.: /// ```rs @@ -62,8 +53,8 @@ macro_rules! loop_select { #[macro_export] macro_rules! enum_parse { ($name:literal, $input:expr, $($($s:literal)|* => $val:expr),* $(,)?) => { - let input = $input; - match input { + let input = $input.to_lowercase(); + match input.as_str() { $( $( $s )|* => Ok($val) ),*, _ => Err(anyhow!(concat!("Couldn't parse ", $name, ": '{}'. Possible values are ", $($($s),*),*), input)) } @@ -139,21 +130,6 @@ impl> T { } } -pub fn parse_duration(s: &str) -> Result { - use std::time::Duration; - if s.ends_with("ms") { - Ok(Duration::from_millis(s.trim_end_matches("ms").parse()?)) - } else if s.ends_with('s') { - Ok(Duration::from_secs(s.trim_end_matches('s').parse()?)) - } else if s.ends_with('m') { - Ok(Duration::from_secs(s.trim_end_matches('m').parse::()? * 60)) - } else if s.ends_with('h') { - Ok(Duration::from_secs(s.trim_end_matches('h').parse::()? * 60 * 60)) - } else { - Err(anyhow!("unrecognized time format: {}", s)) - } -} - pub trait IterAverage { fn avg(self) -> f32; } @@ -174,10 +150,7 @@ impl> IterAverage for I { /// by the actual env-variables. If the env-var isn't found, will replace the /// reference with an empty string. pub fn replace_env_var_references(input: String) -> String { - lazy_static::lazy_static! { - static ref ENV_VAR_PATTERN: regex::Regex = regex::Regex::new(r"\$\{([^\s]*)\}").unwrap(); - } - ENV_VAR_PATTERN + regex!(r"\$\{([^\s]*)\}") .replace_all(&input, |var_name: ®ex::Captures| std::env::var(var_name.get(1).unwrap().as_str()).unwrap_or_default()) .into_owned() } diff --git a/src/widgets/mod.rs b/crates/eww/src/widgets/mod.rs similarity index 75% rename from src/widgets/mod.rs rename to crates/eww/src/widgets/mod.rs index c6214ae..61d084c 100644 --- a/src/widgets/mod.rs +++ b/crates/eww/src/widgets/mod.rs @@ -1,12 +1,10 @@ -use crate::{ - config::{element::WidgetDefinition, window_definition::WindowName}, - eww_state::*, - value::AttrName, -}; +use crate::eww_state::*; use anyhow::*; +use eww_shared_util::AttrName; use gtk::prelude::*; use itertools::Itertools; use std::collections::HashMap; +use yuck::config::widget_definition::WidgetDefinition; use std::process::Command; use widget_definitions::*; @@ -18,7 +16,7 @@ const CMD_STRING_PLACEHODLER: &str = "{}"; /// Run a command that was provided as an attribute. This command may use a /// placeholder ('{}') which will be replaced by the value provided as [`arg`] -pub(self) fn run_command(cmd: &str, arg: T) { +pub(self) fn run_command(timeout: std::time::Duration, cmd: &str, arg: T) { use wait_timeout::ChildExt; let cmd = cmd.to_string(); std::thread::spawn(move || { @@ -26,7 +24,7 @@ pub(self) fn run_command(cmd: &str log::debug!("Running command from widget: {}", cmd); let child = Command::new("/bin/sh").arg("-c").arg(&cmd).spawn(); match child { - Ok(mut child) => match child.wait_timeout(std::time::Duration::from_millis(200)) { + Ok(mut child) => match child.wait_timeout(timeout) { // child timed out Ok(None) => { log::error!("WARNING: command {} timed out", &cmd); @@ -45,7 +43,7 @@ struct BuilderArgs<'a, 'b, 'c, 'd, 'e> { eww_state: &'a mut EwwState, widget: &'b widget_node::Generic, unhandled_attrs: Vec<&'c AttrName>, - window_name: &'d WindowName, + window_name: &'d str, widget_definitions: &'e HashMap, } @@ -60,40 +58,31 @@ struct BuilderArgs<'a, 'b, 'c, 'd, 'e> { /// widget name. fn build_builtin_gtk_widget( eww_state: &mut EwwState, - window_name: &WindowName, + window_name: &str, widget_definitions: &HashMap, widget: &widget_node::Generic, ) -> Result> { let mut bargs = BuilderArgs { eww_state, widget, window_name, unhandled_attrs: widget.attrs.keys().collect(), widget_definitions }; - let gtk_widget = match widget_to_gtk_widget(&mut bargs) { - Ok(Some(gtk_widget)) => gtk_widget, - result => { - return result.with_context(|| { - format!( - "{}Error building widget {}", - bargs.widget.text_pos.map(|x| format!("{} |", x)).unwrap_or_default(), - bargs.widget.name, - ) - }) - } - }; + let gtk_widget = widget_to_gtk_widget(&mut bargs)?; // run resolve functions for superclasses such as range, orientable, and widget if let Some(gtk_widget) = gtk_widget.dynamic_cast_ref::() { resolve_container_attrs(&mut bargs, gtk_widget); - for child in &widget.children { - let child_widget = child.render(bargs.eww_state, window_name, widget_definitions).with_context(|| { - format!( - "{}error while building child '{:#?}' of '{}'", - widget.text_pos.map(|x| format!("{} |", x)).unwrap_or_default(), - &child, - >k_widget.get_widget_name() - ) - })?; - gtk_widget.add(&child_widget); - child_widget.show(); + if gtk_widget.get_children().is_empty() { + for child in &widget.children { + let child_widget = child.render(bargs.eww_state, window_name, widget_definitions).with_context(|| { + format!( + "{}error while building child '{:#?}' of '{}'", + format!("{} | ", widget.span), + &child, + >k_widget.get_widget_name() + ) + })?; + gtk_widget.add(&child_widget); + child_widget.show(); + } } } @@ -108,7 +97,7 @@ fn build_builtin_gtk_widget( if !bargs.unhandled_attrs.is_empty() { log::error!( "{}: Unknown attribute used in {}: {}", - widget.text_pos.map(|x| format!("{} | ", x)).unwrap_or_default(), + format!("{} | ", widget.span), widget.name, bargs.unhandled_attrs.iter().map(|x| x.to_string()).join(", ") ) @@ -132,7 +121,7 @@ macro_rules! resolve_block { let attr_map: Result<_> = try { ::maplit::hashmap! { $( - crate::value::AttrName(::std::stringify!($attr_name).to_owned()) => + eww_shared_util::AttrName(::std::stringify!($attr_name).to_owned()) => resolve_block!(@get_value $args, &::std::stringify!($attr_name).replace('_', "-"), $(= $default)?) ),* } @@ -154,7 +143,7 @@ macro_rules! resolve_block { }; (@get_value $args:ident, $name:expr, = $default:expr) => { - $args.widget.get_attr($name).cloned().unwrap_or(AttrVal::from_primitive($default)) + $args.widget.get_attr($name).cloned().unwrap_or(simplexpr::SimplExpr::synth_literal($default)) }; (@get_value $args:ident, $name:expr,) => { diff --git a/src/widgets/widget_definitions.rs b/crates/eww/src/widgets/widget_definitions.rs similarity index 76% rename from src/widgets/widget_definitions.rs rename to crates/eww/src/widgets/widget_definitions.rs index d806092..50f9a64 100644 --- a/src/widgets/widget_definitions.rs +++ b/crates/eww/src/widgets/widget_definitions.rs @@ -1,25 +1,30 @@ #![allow(clippy::option_map_unit_fn)] use super::{run_command, BuilderArgs}; use crate::{ - config, enum_parse, eww_state, resolve_block, - util::{list_difference, parse_duration}, - value::AttrVal, - widgets::widget_node, + enum_parse, error::DiagError, error_handling_ctx, eww_state, resolve_block, util::list_difference, widgets::widget_node, }; use anyhow::*; use gdk::WindowExt; use glib; use gtk::{self, prelude::*, ImageExt}; -use std::{cell::RefCell, collections::HashMap, rc::Rc}; +use itertools::Itertools; +use std::{cell::RefCell, collections::HashMap, rc::Rc, time::Duration}; +use yuck::{ + config::validate::ValidationError, + error::{AstError, AstResult, AstResultExt}, + gen_diagnostic, + parser::from_ast::FromAst, +}; // TODO figure out how to // TODO https://developer.gnome.org/gtk3/stable/GtkFixed.html //// widget definitions -pub(super) fn widget_to_gtk_widget(bargs: &mut BuilderArgs) -> Result> { +pub(super) fn widget_to_gtk_widget(bargs: &mut BuilderArgs) -> Result { let gtk_widget = match bargs.widget.name.as_str() { "box" => build_gtk_box(bargs)?.upcast(), + "centerbox" => build_center_box(bargs)?.upcast(), "scale" => build_gtk_scale(bargs)?.upcast(), "progress" => build_gtk_progress(bargs)?.upcast(), "image" => build_gtk_image(bargs)?.upcast(), @@ -35,9 +40,11 @@ pub(super) fn widget_to_gtk_widget(bargs: &mut BuilderArgs) -> Result build_gtk_checkbox(bargs)?.upcast(), "revealer" => build_gtk_revealer(bargs)?.upcast(), "if-else" => build_if_else(bargs)?.upcast(), - _ => return Ok(None), + _ => { + Err(AstError::ValidationError(ValidationError::UnknownWidget(bargs.widget.name_span, bargs.widget.name.to_string())))? + } }; - Ok(Some(gtk_widget)) + Ok(gtk_widget) } /// attributes that apply to all widgets @@ -46,7 +53,9 @@ pub(super) fn widget_to_gtk_widget(bargs: &mut BuilderArgs) -> Result Result Result { // @prop reveal - sets if the child is revealed or not prop(reveal: as_bool) { gtk_widget.set_reveal_child(reveal); }, // @prop duration - the duration of the reveal transition - prop(duration: as_string = "500ms") { gtk_widget.set_transition_duration(parse_duration(&duration)?.as_millis() as u32); }, + prop(duration: as_duration = Duration::from_millis(500)) { gtk_widget.set_transition_duration(duration.as_millis() as u32); }, }); Ok(gtk_widget) } @@ -295,16 +307,13 @@ fn build_gtk_checkbox(bargs: &mut BuilderArgs) -> Result { let gtk_widget = gtk::CheckButton::new(); let on_change_handler_id: Rc>> = Rc::new(RefCell::new(None)); resolve_block!(bargs, gtk_widget, { - // @prop onchecked - action (command) to be executed when checked by the user - // @prop onunchecked - similar to onchecked but when the widget is unchecked - prop(onchecked: as_string = "", onunchecked: as_string = "") { + // @prop timeout - timeout of the command + // @prop onchecked - action (command) to be executed when checked by the user + // @prop onunchecked - similar to onchecked but when the widget is unchecked + prop(timeout: as_duration = Duration::from_millis(200), onchecked: as_string = "", onunchecked: as_string = "") { let old_id = on_change_handler_id.replace(Some( gtk_widget.connect_toggled(move |gtk_widget| { - if gtk_widget.get_active() { - run_command(&onchecked, ""); - } else { - run_command(&onunchecked, ""); - } + run_command(timeout, if gtk_widget.get_active() { &onchecked } else { &onunchecked }, ""); }) )); old_id.map(|id| gtk_widget.disconnect(id)); @@ -324,10 +333,11 @@ fn build_gtk_color_button(bargs: &mut BuilderArgs) -> Result { prop(use_alpha: as_bool) {gtk_widget.set_use_alpha(use_alpha);}, // @prop onchange - runs the code when the color was selected - prop(onchange: as_string) { + // @prop timeout - timeout of the command + prop(timeout: as_duration = Duration::from_millis(200), onchange: as_string) { let old_id = on_change_handler_id.replace(Some( gtk_widget.connect_color_set(move |gtk_widget| { - run_command(&onchange, gtk_widget.get_rgba()); + run_command(timeout, &onchange, gtk_widget.get_rgba()); }) )); old_id.map(|id| gtk_widget.disconnect(id)); @@ -347,10 +357,11 @@ fn build_gtk_color_chooser(bargs: &mut BuilderArgs) -> Result Result { }, // @prop onchange - Command to run when the text changes. The placeholder `{}` will be replaced by the value - prop(onchange: as_string) { + // @prop timeout - timeout of the command + prop(timeout: as_duration = Duration::from_millis(200), onchange: as_string) { let old_id = on_change_handler_id.replace(Some( gtk_widget.connect_changed(move |gtk_widget| { - run_command(&onchange, gtk_widget.get_text().to_string()); + run_command(timeout, &onchange, gtk_widget.get_text().to_string()); }) )); old_id.map(|id| gtk_widget.disconnect(id)); @@ -425,14 +437,20 @@ fn build_gtk_button(bargs: &mut BuilderArgs) -> Result { // @prop onclick - a command that get's run when the button is clicked // @prop onmiddleclick - a command that get's run when the button is middleclicked // @prop onrightclick - a command that get's run when the button is rightclicked - prop(onclick: as_string = "", onmiddleclick: as_string = "", onrightclick: as_string = "") { + // @prop timeout - timeout of the command + prop( + timeout: as_duration = Duration::from_millis(200), + onclick: as_string = "", + onmiddleclick: as_string = "", + onrightclick: as_string = "" + ) { gtk_widget.add_events(gdk::EventMask::ENTER_NOTIFY_MASK); let old_id = on_click_handler_id.replace(Some( gtk_widget.connect_button_press_event(move |_, evt| { match evt.get_button() { - 1 => run_command(&onclick, ""), - 2 => run_command(&onmiddleclick, ""), - 3 => run_command(&onrightclick, ""), + 1 => run_command(timeout, &onclick, ""), + 2 => run_command(timeout, &onmiddleclick, ""), + 3 => run_command(timeout, &onrightclick, ""), _ => {}, } gtk::Inhibit(false) @@ -481,6 +499,39 @@ fn build_gtk_box(bargs: &mut BuilderArgs) -> Result { Ok(gtk_widget) } +/// @widget centerbox extends container +/// @desc a box that must contain exactly three children, which will be layed out at the start, center and end of the container. +fn build_center_box(bargs: &mut BuilderArgs) -> Result { + let gtk_widget = gtk::Box::new(gtk::Orientation::Horizontal, 0); + resolve_block!(bargs, gtk_widget, { + // @prop orientation - orientation of the centerbox. possible values: $orientation + prop(orientation: as_string) { gtk_widget.set_orientation(parse_orientation(&orientation)?) }, + }); + + if bargs.widget.children.len() < 3 { + Err(DiagError::new(gen_diagnostic!("centerbox must contain exactly 3 elements", bargs.widget.span)))? + } else if bargs.widget.children.len() > 3 { + let (_, additional_children) = bargs.widget.children.split_at(3); + // we know that there is more than three children, so unwrapping on first and left here is fine. + let first_span = additional_children.first().unwrap().span(); + let last_span = additional_children.last().unwrap().span(); + Err(DiagError::new(gen_diagnostic!("centerbox must contain exactly 3 elements, but got more", first_span.to(last_span))))? + } + + let mut children = + bargs.widget.children.iter().map(|child| child.render(bargs.eww_state, bargs.window_name, bargs.widget_definitions)); + // we know that we have exactly three children here, so we can unwrap here. + let (first, center, end) = children.next_tuple().unwrap(); + let (first, center, end) = (first?, center?, end?); + gtk_widget.pack_start(&first, true, true, 0); + gtk_widget.set_center_widget(Some(¢er)); + gtk_widget.pack_end(&end, true, true, 0); + first.show(); + center.show(); + end.show(); + Ok(gtk_widget) +} + /// @widget label /// @desc A text widget giving you more control over how the text is displayed fn build_gtk_label(bargs: &mut BuilderArgs) -> Result { @@ -492,43 +543,59 @@ fn build_gtk_label(bargs: &mut BuilderArgs) -> Result { prop(text: as_string, limit_width: as_i32 = i32::MAX) { let text = text.chars().take(limit_width as usize).collect::(); let text = unescape::unescape(&text).context(format!("Failed to unescape label text {}", &text))?; + let text = unindent::unindent(&text); gtk_widget.set_text(&text); }, // @prop markup - Pango markup to display - prop(markup: as_string) { - gtk_widget.set_markup(&markup); - }, + prop(markup: as_string) { gtk_widget.set_markup(&markup); }, // @prop wrap - Wrap the text. This mainly makes sense if you set the width of this widget. - prop(wrap: as_bool) { - gtk_widget.set_line_wrap(wrap) - }, + prop(wrap: as_bool) { gtk_widget.set_line_wrap(wrap) }, // @prop angle - the angle of rotation for the label (between 0 - 360) - prop(angle: as_f64 = 0) { - gtk_widget.set_angle(angle) - } + prop(angle: as_f64 = 0) { gtk_widget.set_angle(angle) } }); Ok(gtk_widget) } /// @widget literal -/// @desc A widget that allows you to render arbitrary XML. +/// @desc A widget that allows you to render arbitrary yuck. fn build_gtk_literal(bargs: &mut BuilderArgs) -> Result { let gtk_widget = gtk::Box::new(gtk::Orientation::Vertical, 0); gtk_widget.set_widget_name("literal"); // TODO these clones here are dumdum - let window_name = bargs.window_name.clone(); + let window_name = bargs.window_name.to_string(); let widget_definitions = bargs.widget_definitions.clone(); + let literal_use_span = bargs.widget.span; + + // the file id the literal-content has been stored under, for error reporting. + let literal_file_id: Rc>> = Rc::new(RefCell::new(None)); + resolve_block!(bargs, gtk_widget, { - // @prop content - inline Eww XML that will be rendered as a widget. + // @prop content - inline yuck that will be rendered as a widget. prop(content: as_string) { gtk_widget.get_children().iter().for_each(|w| gtk_widget.remove(w)); if !content.is_empty() { - let document = roxmltree::Document::parse(&content).map_err(|e| anyhow!("Failed to parse eww xml literal: {:?}", e))?; - let content_widget_use = config::element::WidgetUse::from_xml_node(document.root_element().into())?; + let widget_node_result: AstResult<_> = try { + let ast = { + let mut yuck_files = error_handling_ctx::YUCK_FILES.write().unwrap(); + let (span, asts) = yuck_files.load_str("".to_string(), content)?; + if let Some(file_id) = literal_file_id.replace(Some(span.2)) { + yuck_files.unload(file_id); + } + yuck::parser::require_single_toplevel(span, asts)? + }; - let widget_node = &*widget_node::generate_generic_widget_node(&widget_definitions, &HashMap::new(), content_widget_use)?; - let child_widget = widget_node.render(&mut eww_state::EwwState::default(), &window_name, &widget_definitions)?; + let content_widget_use = yuck::config::widget_use::WidgetUse::from_ast(ast)?; + widget_node::generate_generic_widget_node(&widget_definitions, &HashMap::new(), content_widget_use)? + }; + + let widget_node = widget_node_result.context_label(literal_use_span, "Error in the literal used here")?; + let child_widget = widget_node.render(&mut eww_state::EwwState::default(), &window_name, &widget_definitions) + .map_err(|e| AstError::ErrorContext { + label_span: literal_use_span, + context: "Error in the literal used here".to_string(), + main_err: Box::new(error_handling_ctx::anyhow_err_to_diagnostic(&e).unwrap_or_else(|| gen_diagnostic!(e))) + })?; gtk_widget.add(&child_widget); child_widget.show(); } @@ -558,10 +625,12 @@ fn build_gtk_calendar(bargs: &mut BuilderArgs) -> Result { // @prop show-week-numbers - show week numbers prop(show_week_numbers: as_bool) { gtk_widget.set_property_show_week_numbers(show_week_numbers) }, // @prop onclick - command to run when the user selects a date. The `{}` placeholder will be replaced by the selected date. - prop(onclick: as_string) { + // @prop timeout - timeout of the command + prop(timeout: as_duration = Duration::from_millis(200), onclick: as_string) { let old_id = on_click_handler_id.replace(Some( gtk_widget.connect_day_selected(move |w| { run_command( + timeout, &onclick, format!("{}.{}.{}", w.get_property_day(), w.get_property_month(), w.get_property_year()) ) diff --git a/crates/eww/src/widgets/widget_node.rs b/crates/eww/src/widgets/widget_node.rs new file mode 100644 index 0000000..70d36a4 --- /dev/null +++ b/crates/eww/src/widgets/widget_node.rs @@ -0,0 +1,147 @@ +use crate::eww_state::EwwState; +use anyhow::*; +use dyn_clone; +use eww_shared_util::{AttrName, Span, Spanned, VarName}; +use simplexpr::SimplExpr; +use std::collections::HashMap; +use yuck::{ + config::{validate::ValidationError, widget_definition::WidgetDefinition, widget_use::WidgetUse}, + error::{AstError, AstResult}, +}; + +pub trait WidgetNode: Spanned + std::fmt::Debug + dyn_clone::DynClone + Send + Sync { + fn get_name(&self) -> &str; + + /// Generate a [gtk::Widget] from a [element::WidgetUse]. + /// + /// Also registers all the necessary state-change handlers in the eww_state. + /// + /// This may return `Err` in case there was an actual error while parsing + /// or when the widget_use did not match any widget name + fn render( + &self, + eww_state: &mut EwwState, + window_name: &str, + widget_definitions: &HashMap, + ) -> Result; +} + +dyn_clone::clone_trait_object!(WidgetNode); + +#[derive(Debug, Clone)] +pub struct UserDefined { + name: String, + span: Span, + content: Box, +} + +impl WidgetNode for UserDefined { + fn get_name(&self) -> &str { + &self.name + } + + fn render( + &self, + eww_state: &mut EwwState, + window_name: &str, + widget_definitions: &HashMap, + ) -> Result { + self.content.render(eww_state, window_name, widget_definitions) + } +} + +impl Spanned for UserDefined { + fn span(&self) -> Span { + self.span + } +} + +#[derive(Debug, Clone)] +pub struct Generic { + pub name: String, + pub name_span: Span, + pub span: Span, + pub children: Vec>, + pub attrs: HashMap, +} + +impl Generic { + pub fn get_attr(&self, key: &str) -> Result<&SimplExpr> { + Ok(self.attrs.get(key).ok_or_else(|| { + AstError::ValidationError(ValidationError::MissingAttr { + widget_name: self.name.to_string(), + arg_name: AttrName(key.to_string()), + use_span: self.span, + // TODO set this when available + arg_list_span: None, + }) + })?) + } + + /// returns all the variables that are referenced in this widget + pub fn referenced_vars(&self) -> impl Iterator { + self.attrs.iter().flat_map(|(_, value)| value.var_refs()).map(|(_, value)| value) + } +} + +impl WidgetNode for Generic { + fn get_name(&self) -> &str { + &self.name + } + + fn render( + &self, + eww_state: &mut EwwState, + window_name: &str, + widget_definitions: &HashMap, + ) -> Result { + Ok(crate::widgets::build_builtin_gtk_widget(eww_state, window_name, widget_definitions, self)?.ok_or_else(|| { + AstError::ValidationError(ValidationError::UnknownWidget(self.name_span, self.get_name().to_string())) + })?) + } +} +impl Spanned for Generic { + fn span(&self) -> Span { + self.span + } +} + +pub fn generate_generic_widget_node( + defs: &HashMap, + local_env: &HashMap, + w: WidgetUse, +) -> AstResult> { + if let Some(def) = defs.get(&w.name) { + if !w.children.is_empty() { + Err(AstError::TooManyNodes(w.children_span(), 0).note("User-defined widgets cannot be given children."))? + } + + let new_local_env = w + .attrs + .attrs + .into_iter() + .map(|(name, value)| Ok((VarName(name.0), value.value.as_simplexpr()?.resolve_one_level(local_env)))) + .collect::>>()?; + + let content = generate_generic_widget_node(defs, &new_local_env, def.widget.clone())?; + Ok(Box::new(UserDefined { name: w.name, span: w.span, content })) + } else { + Ok(Box::new(Generic { + name: w.name, + name_span: w.name_span, + span: w.span, + attrs: w + .attrs + .attrs + .into_iter() + .map(|(name, value)| Ok((name, value.value.as_simplexpr()?.resolve_one_level(local_env)))) + .collect::>>()?, + + children: w + .children + .into_iter() + .map(|child| generate_generic_widget_node(defs, local_env, child)) + .collect::>>()?, + })) + } +} diff --git a/crates/eww_shared_util/Cargo.toml b/crates/eww_shared_util/Cargo.toml new file mode 100644 index 0000000..3293833 --- /dev/null +++ b/crates/eww_shared_util/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "eww_shared_util" +version = "0.1.0" +edition = "2018" + +[dependencies] +serde = {version = "1.0", features = ["derive"]} +derive_more = "0.99" diff --git a/crates/eww_shared_util/src/lib.rs b/crates/eww_shared_util/src/lib.rs new file mode 100644 index 0000000..299f994 --- /dev/null +++ b/crates/eww_shared_util/src/lib.rs @@ -0,0 +1,38 @@ +pub mod span; +pub mod wrappers; + +pub use span::*; +pub use wrappers::*; + +#[macro_export] +macro_rules! snapshot_debug { + ( $($name:ident => $test:expr),* $(,)?) => { + $( + #[test] + fn $name() { ::insta::assert_debug_snapshot!($test); } + )* + }; +} +#[macro_export] +macro_rules! snapshot_string { + ( $($name:ident => $test:expr),* $(,)?) => { + $( + #[test] + fn $name() { ::insta::assert_snapshot!($test); } + )* + }; +} + +#[macro_export] +macro_rules! snapshot_ron { + ( $($name:ident => $test:expr),* $(,)?) => { + $( + #[test] + fn $name() { + ::insta::with_settings!({sort_maps => true}, { + ::insta::assert_ron_snapshot!($test); + }); + } + )* + }; +} diff --git a/crates/eww_shared_util/src/span.rs b/crates/eww_shared_util/src/span.rs new file mode 100644 index 0000000..94919b1 --- /dev/null +++ b/crates/eww_shared_util/src/span.rs @@ -0,0 +1,64 @@ +#[derive(Eq, PartialEq, Clone, Copy, serde::Serialize, serde::Deserialize)] +pub struct Span(pub usize, pub usize, pub usize); + +impl Span { + pub const DUMMY: Span = Span(usize::MAX, usize::MAX, usize::MAX); + + pub fn point(loc: usize, file_id: usize) -> Self { + Span(loc, loc, file_id) + } + + /// Get the span that includes this and the other span completely. + /// Will panic if the spans are from different file_ids. + pub fn to(mut self, other: Span) -> Self { + assert!(other.2 == self.2); + self.1 = other.1; + self + } + + pub fn ending_at(mut self, end: usize) -> Self { + self.1 = end; + self + } + + /// Turn this span into a span only highlighting the point it starts at, setting the length to 0. + pub fn point_span(mut self) -> Self { + self.1 = self.0; + self + } + + /// Turn this span into a span only highlighting the point it ends at, setting the length to 0. + pub fn point_span_at_end(mut self) -> Self { + self.0 = self.1; + self + } + pub fn shifted(mut self, n: isize) -> Self { + self.0 = isize::max(0, self.0 as isize + n) as usize; + self.1 = isize::max(0, self.0 as isize + n) as usize; + self + } + + pub fn is_dummy(&self) -> bool { + *self == Self::DUMMY + } +} + +impl std::fmt::Display for Span { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.is_dummy() { + write!(f, "DUMMY") + } else { + write!(f, "{}..{}", self.0, self.1) + } + } +} + +impl std::fmt::Debug for Span { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self) + } +} + +pub trait Spanned { + fn span(&self) -> Span; +} diff --git a/src/value/mod.rs b/crates/eww_shared_util/src/wrappers.rs similarity index 65% rename from src/value/mod.rs rename to crates/eww_shared_util/src/wrappers.rs index cd002af..dabdfe8 100644 --- a/src/value/mod.rs +++ b/crates/eww_shared_util/src/wrappers.rs @@ -1,17 +1,11 @@ use derive_more::*; use serde::{Deserialize, Serialize}; -pub mod attr_value; -pub mod coords; -pub mod primitive; -pub use attr_value::*; -pub use attr_value_expr::*; -pub use coords::*; -pub use primitive::*; - /// The name of a variable #[repr(transparent)] -#[derive(Clone, Hash, PartialEq, Eq, Serialize, Deserialize, AsRef, From, FromStr, Display, DebugCustom)] +#[derive( + Clone, Hash, PartialEq, Eq, Serialize, Deserialize, AsRef, From, FromStr, Display, DebugCustom, +)] #[debug(fmt = "VarName({})", .0)] pub struct VarName(pub String); @@ -29,7 +23,9 @@ impl From<&str> for VarName { /// The name of an attribute #[repr(transparent)] -#[derive(Clone, Hash, PartialEq, Eq, Serialize, Deserialize, AsRef, From, FromStr, Display, DebugCustom)] +#[derive( + Clone, Hash, PartialEq, Eq, Serialize, Deserialize, AsRef, From, FromStr, Display, DebugCustom, +)] #[debug(fmt="AttrName({})", .0)] pub struct AttrName(pub String); diff --git a/crates/simplexpr/Cargo.lock b/crates/simplexpr/Cargo.lock new file mode 100644 index 0000000..774d4b5 --- /dev/null +++ b/crates/simplexpr/Cargo.lock @@ -0,0 +1,656 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "ascii-canvas" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" +dependencies = [ + "term", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "beef" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6736e2428df2ca2848d846c43e88745121a6654696e349ce0054a420815a7409" + +[[package]] +name = "bit-set" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "console" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3993e6445baa160675931ec041a5e03ca84b9c6e32a056150d3aa2bdda0a1f45" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "terminal_size", + "winapi", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "diff" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "ena" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7402b94a93c24e742487327a7cd839dc9d36fec9de9fb25b09f2dae459f36c3" +dependencies = [ + "log", +] + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "fixedbitset" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "indexmap" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "insta" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1b21a2971cea49ca4613c0e9fe8225ecaf5de64090fddc6002284726e9244" +dependencies = [ + "console", + "lazy_static", + "serde", + "serde_json", + "serde_yaml", + "similar", + "uuid", +] + +[[package]] +name = "itertools" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + +[[package]] +name = "lalrpop" +version = "0.19.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15174f1c529af5bf1283c3bc0058266b483a67156f79589fab2a25e23cf8988" +dependencies = [ + "ascii-canvas", + "atty", + "bit-set", + "diff", + "ena", + "itertools", + "lalrpop-util", + "petgraph", + "pico-args", + "regex", + "regex-syntax", + "string_cache", + "term", + "tiny-keccak", + "unicode-xid", +] + +[[package]] +name = "lalrpop-util" +version = "0.19.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e58cce361efcc90ba8a0a5f982c741ff86b603495bb15a998412e957dcd278" +dependencies = [ + "regex", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" + +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "logos" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427e2abca5be13136da9afdbf874e6b34ad9001dd70f2b103b083a85daa7b345" +dependencies = [ + "logos-derive", +] + +[[package]] +name = "logos-derive" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56a7d287fd2ac3f75b11f19a1c8a874a7d55744bd91f7a1b3e7cf87d4343c36d" +dependencies = [ + "beef", + "fnv", + "proc-macro2", + "quote", + "regex-syntax", + "syn", + "utf8-ranges", +] + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "memchr" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" + +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "petgraph" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro2" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom", + "redox_syscall", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "rustversion" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088" + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "serde" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15654ed4ab61726bf918a39cb8d98a2e2995b002387807fa6ba58fdf7f59bb23" +dependencies = [ + "dtoa", + "linked-hash-map", + "serde", + "yaml-rust", +] + +[[package]] +name = "similar" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec" + +[[package]] +name = "simplexpr" +version = "0.1.0" +dependencies = [ + "insta", + "itertools", + "lalrpop", + "lalrpop-util", + "logos", + "maplit", + "regex", + "serde", + "serde_json", + "strum", + "thiserror", +] + +[[package]] +name = "siphasher" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbce6d4507c7e4a3962091436e56e95290cb71fa302d0d270e32130b75fbff27" + +[[package]] +name = "string_cache" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb1139b5353f96e429e1a5e19fbaf663bddedaa06d1dbd49f82e352601209a" +dependencies = [ + "lazy_static", + "new_debug_unreachable", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "strum" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "utf8-ranges" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ae116fef2b7fea257ed6440d3cfcff7f190865f170cdad00bb6465bf18ecba" + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/crates/simplexpr/Cargo.toml b/crates/simplexpr/Cargo.toml new file mode 100644 index 0000000..37a8338 --- /dev/null +++ b/crates/simplexpr/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "simplexpr" +version = "0.1.0" +edition = "2018" +authors = ["elkowar <5300871+elkowar@users.noreply.github.com>"] + + +build = "build.rs" + +[dependencies] +lalrpop-util = "0.19.5" +regex = "1" +itertools = "0.10" +thiserror = "1.0" + +once_cell = "1.8.0" +serde = {version = "1.0", features = ["derive"]} +serde_json = "1.0" +levenshtein = "1.0" + +strum = { version = "0.21", features = ["derive"] } + +eww_shared_util = { path = "../eww_shared_util" } + + +[build-dependencies] +lalrpop = "0.19.5" + +[dev-dependencies] +insta = "1.7" diff --git a/crates/simplexpr/README.md b/crates/simplexpr/README.md new file mode 100644 index 0000000..4a60e3d --- /dev/null +++ b/crates/simplexpr/README.md @@ -0,0 +1,6 @@ +# simplexpr + +simplexpr is a parser and interpreter for a simple expression syntax that can be embedded into other applications or crates. +It is being developed to be used in [eww](https://github.com/elkowar/eww), but may also other uses. + +For now, this is highly experimental, unstable, and ugly. You most definitely do not want to use this crate. diff --git a/crates/simplexpr/build.rs b/crates/simplexpr/build.rs new file mode 100644 index 0000000..cbc2c25 --- /dev/null +++ b/crates/simplexpr/build.rs @@ -0,0 +1,4 @@ +extern crate lalrpop; +fn main() { + lalrpop::Configuration::new().log_verbose().process_current_dir().unwrap(); +} diff --git a/crates/simplexpr/rust-toolchain b/crates/simplexpr/rust-toolchain new file mode 100644 index 0000000..bf867e0 --- /dev/null +++ b/crates/simplexpr/rust-toolchain @@ -0,0 +1 @@ +nightly diff --git a/crates/simplexpr/rustfmt.toml b/crates/simplexpr/rustfmt.toml new file mode 100644 index 0000000..edce9c8 --- /dev/null +++ b/crates/simplexpr/rustfmt.toml @@ -0,0 +1,14 @@ +unstable_features = true +fn_single_line = false +max_width = 130 +reorder_impl_items = true +merge_imports = true +normalize_comments = true +use_field_init_shorthand = true +#wrap_comments = true +combine_control_expr = false +condense_wildcard_suffixes = true +format_code_in_doc_comments = true +format_macro_matchers = true +format_strings = true +use_small_heuristics = "Max" diff --git a/crates/simplexpr/src/ast.rs b/crates/simplexpr/src/ast.rs new file mode 100644 index 0000000..57769f8 --- /dev/null +++ b/crates/simplexpr/src/ast.rs @@ -0,0 +1,103 @@ +use crate::dynval::DynVal; +use eww_shared_util::{Span, Spanned}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; + +use eww_shared_util::VarName; + +#[rustfmt::skip] +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug, strum::EnumString, strum::Display)] +pub enum BinOp { + #[strum(serialize = "+") ] Plus, + #[strum(serialize = "-") ] Minus, + #[strum(serialize = "*") ] Times, + #[strum(serialize = "/") ] Div, + #[strum(serialize = "%") ] Mod, + #[strum(serialize = "==")] Equals, + #[strum(serialize = "!=")] NotEquals, + #[strum(serialize = "&&")] And, + #[strum(serialize = "||")] Or, + #[strum(serialize = ">") ] GT, + #[strum(serialize = "<") ] LT, + #[strum(serialize = "?:")] Elvis, + #[strum(serialize = "=~")] RegexMatch, +} + +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug, strum::EnumString, strum::Display)] +pub enum UnaryOp { + #[strum(serialize = "!")] + Not, +} + +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum SimplExpr { + Literal(DynVal), + Concat(Span, Vec), + VarRef(Span, VarName), + BinOp(Span, Box, BinOp, Box), + UnaryOp(Span, UnaryOp, Box), + IfElse(Span, Box, Box, Box), + JsonAccess(Span, Box, Box), + FunctionCall(Span, String, Vec), +} + +impl std::fmt::Display for SimplExpr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SimplExpr::Literal(x) => write!(f, "\"{}\"", x), + SimplExpr::Concat(_, elems) => { + let text = elems + .iter() + .map(|x| match x { + SimplExpr::Literal(lit) => lit.to_string(), + other => format!("${{{}}}", other), + }) + .join(""); + write!(f, "\"{}\"", text) + } + SimplExpr::VarRef(_, x) => write!(f, "{}", x), + SimplExpr::BinOp(_, l, op, r) => write!(f, "({} {} {})", l, op, r), + SimplExpr::UnaryOp(_, op, x) => write!(f, "{}{}", op, x), + SimplExpr::IfElse(_, a, b, c) => write!(f, "({} ? {} : {})", a, b, c), + SimplExpr::JsonAccess(_, value, index) => write!(f, "{}[{}]", value, index), + SimplExpr::FunctionCall(_, function_name, args) => { + write!(f, "{}({})", function_name, args.iter().join(", ")) + } + } + } +} +impl SimplExpr { + pub fn literal(span: Span, s: String) -> Self { + Self::Literal(DynVal(s, span)) + } + + /// Construct a synthetic simplexpr from a literal string, without adding any relevant span information (uses [DUMMY_SPAN]) + pub fn synth_string(s: String) -> Self { + Self::Literal(DynVal(s, Span::DUMMY)) + } + + /// Construct a synthetic simplexpr from a literal dynval, without adding any relevant span information (uses [DUMMY_SPAN]) + pub fn synth_literal>(s: T) -> Self { + Self::Literal(s.into()) + } +} +impl Spanned for SimplExpr { + fn span(&self) -> Span { + match self { + SimplExpr::Literal(x) => x.span(), + SimplExpr::Concat(span, _) => *span, + SimplExpr::VarRef(span, _) => *span, + SimplExpr::BinOp(span, ..) => *span, + SimplExpr::UnaryOp(span, ..) => *span, + SimplExpr::IfElse(span, ..) => *span, + SimplExpr::JsonAccess(span, ..) => *span, + SimplExpr::FunctionCall(span, ..) => *span, + } + } +} + +impl std::fmt::Debug for SimplExpr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self) + } +} diff --git a/crates/simplexpr/src/dynval.rs b/crates/simplexpr/src/dynval.rs new file mode 100644 index 0000000..9ade8d8 --- /dev/null +++ b/crates/simplexpr/src/dynval.rs @@ -0,0 +1,225 @@ +use eww_shared_util::{Span, Spanned}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use std::{fmt, iter::FromIterator, str::FromStr}; + +pub type Result = std::result::Result; + +#[derive(Debug, thiserror::Error)] +#[error("Failed to turn `{value}` into a value of type {target_type}")] +pub struct ConversionError { + pub value: DynVal, + pub target_type: &'static str, + pub source: Option>, +} + +impl ConversionError { + fn new(value: DynVal, target_type: &'static str, source: impl std::error::Error + 'static + Sync + Send) -> Self { + ConversionError { value, target_type, source: Some(Box::new(source)) } + } +} +impl Spanned for ConversionError { + fn span(&self) -> Span { + self.value.1 + } +} + +#[derive(Clone, Deserialize, Serialize, Eq)] +pub struct DynVal(pub String, pub Span); + +impl From for DynVal { + fn from(s: String) -> Self { + DynVal(s, Span::DUMMY) + } +} + +impl fmt::Display for DynVal { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} +impl fmt::Debug for DynVal { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "\"{}\"", self.0) + } +} + +/// Manually implement equality, to allow for values in different formats (i.e. "1" and "1.0") to still be considered as equal. +impl std::cmp::PartialEq for DynVal { + fn eq(&self, other: &Self) -> bool { + if let (Ok(a), Ok(b)) = (self.as_f64(), other.as_f64()) { + a == b + } else { + self.0 == other.0 + } + } +} + +impl FromIterator for DynVal { + fn from_iter>(iter: T) -> Self { + DynVal(iter.into_iter().join(""), Span::DUMMY) + } +} + +impl std::str::FromStr for DynVal { + type Err = ConversionError; + + /// parses the value, trying to turn it into a number and a boolean first, + /// before deciding that it is a string. + fn from_str(s: &str) -> Result { + Ok(DynVal::from_string(s.to_string())) + } +} + +pub trait FromDynVal: Sized { + type Err; + fn from_dynval(x: &DynVal) -> std::result::Result; +} + +impl> FromDynVal for T { + type Err = E; + + fn from_dynval(x: &DynVal) -> std::result::Result { + x.0.parse() + } +} + +macro_rules! impl_dynval_from { + ($($t:ty),*) => { + $(impl From<$t> for DynVal { + fn from(x: $t) -> Self { DynVal(x.to_string(), Span::DUMMY) } + })* + }; +} + +impl_dynval_from!(bool, i32, u32, f32, u8, f64, &str); + +impl From for DynVal { + fn from(d: std::time::Duration) -> Self { + DynVal(format!("{}ms", d.as_millis()), Span::DUMMY) + } +} + +impl From<&serde_json::Value> for DynVal { + fn from(v: &serde_json::Value) -> Self { + DynVal( + v.as_str() + .map(|x| x.to_string()) + .or_else(|| serde_json::to_string(v).ok()) + .unwrap_or_else(|| "".to_string()), + Span::DUMMY, + ) + } +} + +impl Spanned for DynVal { + fn span(&self) -> Span { + self.1 + } +} + +impl DynVal { + pub fn at(mut self, span: Span) -> Self { + self.1 = span; + self + } + + pub fn from_string(s: String) -> Self { + DynVal(s, Span::DUMMY) + } + + pub fn read_as>(&self) -> std::result::Result { + T::from_dynval(self) + } + + pub fn into_inner(self) -> String { + self.0 + } + + /// This will never fail + pub fn as_string(&self) -> Result { + Ok(self.0.to_owned()) + } + + pub fn as_f64(&self) -> Result { + self.0.parse().map_err(|e| ConversionError::new(self.clone(), "f64", e)) + } + + pub fn as_i32(&self) -> Result { + self.0.parse().map_err(|e| ConversionError::new(self.clone(), "i32", e)) + } + + pub fn as_bool(&self) -> Result { + self.0.parse().map_err(|e| ConversionError::new(self.clone(), "bool", e)) + } + + pub fn as_duration(&self) -> Result { + use std::time::Duration; + let s = &self.0; + if s.ends_with("ms") { + Ok(Duration::from_millis( + s.trim_end_matches("ms").parse().map_err(|e| ConversionError::new(self.clone(), "integer", e))?, + )) + } else if s.ends_with('s') { + Ok(Duration::from_secs( + s.trim_end_matches('s').parse().map_err(|e| ConversionError::new(self.clone(), "integer", e))?, + )) + } else if s.ends_with('m') { + Ok(Duration::from_secs( + s.trim_end_matches('m').parse::().map_err(|e| ConversionError::new(self.clone(), "integer", e))? * 60, + )) + } else if s.ends_with('h') { + Ok(Duration::from_secs( + s.trim_end_matches('h').parse::().map_err(|e| ConversionError::new(self.clone(), "integer", e))? * 60 * 60, + )) + } else { + Err(ConversionError { value: self.clone(), target_type: "duration", source: None }) + } + } + + pub fn as_vec(&self) -> Result> { + if self.0.is_empty() { + Ok(Vec::new()) + } else { + match self.0.strip_prefix('[').and_then(|x| x.strip_suffix(']')) { + Some(content) => { + let mut items: Vec = content.split(',').map(|x: &str| x.to_string()).collect(); + let mut removed = 0; + for times_ran in 0..items.len() { + // escapes `,` if there's a `\` before em + if items[times_ran - removed].ends_with('\\') { + items[times_ran - removed].pop(); + let it = items.remove((times_ran + 1) - removed); + items[times_ran - removed] += ","; + items[times_ran - removed] += ⁢ + removed += 1; + } + } + Ok(items) + } + None => Err(ConversionError { value: self.clone(), target_type: "vec", source: None }), + } + } + } + + pub fn as_json_value(&self) -> Result { + serde_json::from_str::(&self.0) + .map_err(|e| ConversionError::new(self.clone(), "json-value", Box::new(e))) + } +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn test_parse_vec() { + insta::assert_debug_snapshot!(DynVal::from_string("[]".to_string()).as_vec()); + insta::assert_debug_snapshot!(DynVal::from_string("[hi]".to_string()).as_vec()); + insta::assert_debug_snapshot!(DynVal::from_string("[hi,ho,hu]".to_string()).as_vec()); + insta::assert_debug_snapshot!(DynVal::from_string("[hi\\,ho]".to_string()).as_vec()); + insta::assert_debug_snapshot!(DynVal::from_string("[hi\\,ho,hu]".to_string()).as_vec()); + insta::assert_debug_snapshot!(DynVal::from_string("".to_string()).as_vec()); + insta::assert_debug_snapshot!(DynVal::from_string("[a,b".to_string()).as_vec()); + insta::assert_debug_snapshot!(DynVal::from_string("a]".to_string()).as_vec()); + } +} diff --git a/crates/simplexpr/src/error.rs b/crates/simplexpr/src/error.rs new file mode 100644 index 0000000..0985468 --- /dev/null +++ b/crates/simplexpr/src/error.rs @@ -0,0 +1,65 @@ +use crate::{ + dynval, + parser::lexer::{self, LexicalError}, +}; +use eww_shared_util::{Span, Spanned}; + +pub type Result = std::result::Result; +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Error parsing expression: {source}")] + ParseError { file_id: usize, source: lalrpop_util::ParseError }, + + #[error(transparent)] + ConversionError(#[from] dynval::ConversionError), + + #[error("{1}")] + Spanned(Span, Box), + + #[error(transparent)] + Eval(#[from] crate::eval::EvalError), + + #[error(transparent)] + Other(#[from] Box), +} + +impl Error { + pub fn from_parse_error(file_id: usize, err: lalrpop_util::ParseError) -> Self { + Error::ParseError { file_id, source: err } + } + + pub fn at(self, span: Span) -> Self { + Self::Spanned(span, Box::new(self)) + } +} + +impl Spanned for Error { + fn span(&self) -> Span { + match self { + Self::ParseError { file_id, source } => get_parse_error_span(*file_id, source), + Self::Spanned(span, _) => *span, + Self::Eval(err) => err.span(), + Self::ConversionError(err) => err.span(), + _ => Span::DUMMY, + } + } +} + +fn get_parse_error_span(file_id: usize, err: &lalrpop_util::ParseError) -> Span { + match err { + lalrpop_util::ParseError::InvalidToken { location } => Span(*location, *location, file_id), + lalrpop_util::ParseError::UnrecognizedEOF { location, expected: _ } => Span(*location, *location, file_id), + lalrpop_util::ParseError::UnrecognizedToken { token, expected: _ } => Span(token.0, token.2, file_id), + lalrpop_util::ParseError::ExtraToken { token } => Span(token.0, token.2, file_id), + lalrpop_util::ParseError::User { error: LexicalError(span) } => *span, + } +} + +#[macro_export] +macro_rules! spanned { + ($err:ty, $span:expr, $block:expr) => {{ + let span = $span; + let result: Result<_, $err> = try { $block }; + result.at(span) + }}; +} diff --git a/crates/simplexpr/src/eval.rs b/crates/simplexpr/src/eval.rs new file mode 100644 index 0000000..1b278d2 --- /dev/null +++ b/crates/simplexpr/src/eval.rs @@ -0,0 +1,247 @@ +use itertools::Itertools; + +use crate::{ + ast::{BinOp, SimplExpr, UnaryOp}, + dynval::{ConversionError, DynVal}, +}; +use eww_shared_util::{Span, Spanned, VarName}; +use std::collections::HashMap; + +#[derive(Debug, thiserror::Error)] +pub enum EvalError { + #[error("Tried to reference variable `{0}`, but we cannot access variables here")] + NoVariablesAllowed(VarName), + + #[error("Invalid regex: {0}")] + InvalidRegex(#[from] regex::Error), + + #[error("Unknown variable {0}")] + UnknownVariable(VarName, Vec), + + #[error(transparent)] + ConversionError(#[from] ConversionError), + + #[error("Incorrect number of arguments given to function: {0}")] + WrongArgCount(String), + + #[error("Unknown function {0}")] + UnknownFunction(String), + + #[error("Unable to index into value {0}")] + CannotIndex(String), + + #[error("{1}")] + Spanned(Span, Box), +} + +impl EvalError { + pub fn at(self, span: Span) -> Self { + Self::Spanned(span, Box::new(self)) + } + + pub fn map_in_span(self, f: impl FnOnce(Self) -> Self) -> Self { + match self { + EvalError::Spanned(span, err) => EvalError::Spanned(span, Box::new(err.map_in_span(f))), + other => f(other), + } + } +} + +impl Spanned for EvalError { + fn span(&self) -> Span { + match self { + EvalError::Spanned(span, _) => *span, + EvalError::ConversionError(err) => err.span(), + _ => Span::DUMMY, + } + } +} + +impl SimplExpr { + /// map over all of the variable references, replacing them with whatever expression the provided function returns. + /// Returns [Err] when the provided function fails with an [Err] + pub fn try_map_var_refs Result + Copy>(self, f: F) -> Result { + use SimplExpr::*; + Ok(match self { + BinOp(span, box a, op, box b) => BinOp(span, box a.try_map_var_refs(f)?, op, box b.try_map_var_refs(f)?), + Concat(span, elems) => Concat(span, elems.into_iter().map(|x| x.try_map_var_refs(f)).collect::>()?), + UnaryOp(span, op, box a) => UnaryOp(span, op, box a.try_map_var_refs(f)?), + IfElse(span, box a, box b, box c) => { + IfElse(span, box a.try_map_var_refs(f)?, box b.try_map_var_refs(f)?, box c.try_map_var_refs(f)?) + } + JsonAccess(span, box a, box b) => JsonAccess(span, box a.try_map_var_refs(f)?, box b.try_map_var_refs(f)?), + FunctionCall(span, name, args) => { + FunctionCall(span, name, args.into_iter().map(|x| x.try_map_var_refs(f)).collect::>()?) + } + VarRef(span, name) => f(span, name)?, + x @ Literal(..) => x, + }) + } + + pub fn map_var_refs(self, f: impl Fn(Span, VarName) -> SimplExpr) -> Self { + self.try_map_var_refs(|span, var| Ok::<_, !>(f(span, var))).into_ok() + } + + /// resolve partially. + /// If a var-ref links to another var-ref, that other var-ref is used. + /// If a referenced variable is not found in the given hashmap, returns the var-ref unchanged. + pub fn resolve_one_level(self, variables: &HashMap) -> Self { + self.map_var_refs(|span, name| variables.get(&name).cloned().unwrap_or_else(|| Self::VarRef(span, name))) + } + + /// resolve variable references in the expression. Fails if a variable cannot be resolved. + pub fn resolve_refs(self, variables: &HashMap) -> Result { + use SimplExpr::*; + self.try_map_var_refs(|span, name| match variables.get(&name) { + Some(value) => Ok(Literal(value.clone())), + None => { + let similar_ish = + variables.keys().filter(|key| levenshtein::levenshtein(&key.0, &name.0) < 3).cloned().collect_vec(); + Err(EvalError::UnknownVariable(name.clone(), similar_ish).at(span)) + } + }) + } + + pub fn var_refs(&self) -> Vec<(Span, &VarName)> { + use SimplExpr::*; + match self { + Literal(..) => Vec::new(), + VarRef(span, name) => vec![(*span, name)], + Concat(_, elems) => elems.iter().flat_map(|x| x.var_refs().into_iter()).collect(), + BinOp(_, box a, _, box b) | JsonAccess(_, box a, box b) => { + let mut refs = a.var_refs(); + refs.extend(b.var_refs().iter()); + refs + } + UnaryOp(_, _, box x) => x.var_refs(), + IfElse(_, box a, box b, box c) => { + let mut refs = a.var_refs(); + refs.extend(b.var_refs().iter()); + refs.extend(c.var_refs().iter()); + refs + } + FunctionCall(_, _, args) => args.iter().flat_map(|a| a.var_refs()).collect(), + } + } + + pub fn eval_no_vars(&self) -> Result { + match self.eval(&HashMap::new()) { + Ok(x) => Ok(x), + Err(x) => Err(x.map_in_span(|err| match err { + EvalError::UnknownVariable(name, _) => EvalError::NoVariablesAllowed(name), + other => other, + })), + } + } + + pub fn eval(&self, values: &HashMap) -> Result { + let span = self.span(); + let value = match self { + SimplExpr::Literal(x) => Ok(x.clone()), + SimplExpr::Concat(span, elems) => { + let mut output = String::new(); + for elem in elems { + let result = elem.eval(values)?; + output.push_str(&result.0); + } + Ok(DynVal(output, *span)) + } + SimplExpr::VarRef(span, ref name) => { + let similar_ish = + values.keys().filter(|keys| levenshtein::levenshtein(&keys.0, &name.0) < 3).cloned().collect_vec(); + Ok(values + .get(name) + .cloned() + .ok_or_else(|| EvalError::UnknownVariable(name.clone(), similar_ish).at(*span))? + .at(*span)) + } + SimplExpr::BinOp(span, a, op, b) => { + let a = a.eval(values)?; + let b = b.eval(values)?; + let dynval = match op { + BinOp::Equals => DynVal::from(a == b), + BinOp::NotEquals => DynVal::from(a != b), + BinOp::And => DynVal::from(a.as_bool()? && b.as_bool()?), + BinOp::Or => DynVal::from(a.as_bool()? || b.as_bool()?), + BinOp::Plus => match (a.as_f64(), b.as_f64()) { + (Ok(a), Ok(b)) => DynVal::from(a + b), + _ => DynVal::from(format!("{}{}", a.as_string()?, b.as_string()?)), + }, + BinOp::Minus => DynVal::from(a.as_f64()? - b.as_f64()?), + BinOp::Times => DynVal::from(a.as_f64()? * b.as_f64()?), + BinOp::Div => DynVal::from(a.as_f64()? / b.as_f64()?), + BinOp::Mod => DynVal::from(a.as_f64()? % b.as_f64()?), + BinOp::GT => DynVal::from(a.as_f64()? > b.as_f64()?), + BinOp::LT => DynVal::from(a.as_f64()? < b.as_f64()?), + #[allow(clippy::useless_conversion)] + BinOp::Elvis => DynVal::from(if a.0.is_empty() { b } else { a }), + BinOp::RegexMatch => { + let regex = regex::Regex::new(&b.as_string()?)?; + DynVal::from(regex.is_match(&a.as_string()?)) + } + }; + Ok(dynval.at(*span)) + } + SimplExpr::UnaryOp(span, op, a) => { + let a = a.eval(values)?; + Ok(match op { + UnaryOp::Not => DynVal::from(!a.as_bool()?).at(*span), + }) + } + SimplExpr::IfElse(_, cond, yes, no) => { + if cond.eval(values)?.as_bool()? { + yes.eval(values) + } else { + no.eval(values) + } + } + SimplExpr::JsonAccess(span, val, index) => { + let val = val.eval(values)?; + let index = index.eval(values)?; + match val.as_json_value()? { + serde_json::Value::Array(val) => { + let index = index.as_i32()?; + let indexed_value = val.get(index as usize).unwrap_or(&serde_json::Value::Null); + Ok(DynVal::from(indexed_value).at(*span)) + } + serde_json::Value::Object(val) => { + let indexed_value = val + .get(&index.as_string()?) + .or_else(|| val.get(&index.as_i32().ok()?.to_string())) + .unwrap_or(&serde_json::Value::Null); + Ok(DynVal::from(indexed_value).at(*span)) + } + _ => Err(EvalError::CannotIndex(format!("{}", val)).at(*span)), + } + } + SimplExpr::FunctionCall(span, function_name, args) => { + let args = args.into_iter().map(|a| a.eval(values)).collect::>()?; + call_expr_function(&function_name, args).map(|x| x.at(*span)).map_err(|e| e.at(*span)) + } + }; + Ok(value?.at(span)) + } +} + +fn call_expr_function(name: &str, args: Vec) -> Result { + match name { + "round" => match args.as_slice() { + [num, digits] => { + let num = num.as_f64()?; + let digits = digits.as_i32()?; + Ok(DynVal::from(format!("{:.1$}", num, digits as usize))) + } + _ => Err(EvalError::WrongArgCount(name.to_string())), + }, + "replace" => match args.as_slice() { + [string, pattern, replacement] => { + let string = string.as_string()?; + let pattern = regex::Regex::new(&pattern.as_string()?)?; + let replacement = replacement.as_string()?; + Ok(DynVal::from(pattern.replace_all(&string, replacement.replace("$", "$$").replace("\\", "$")).into_owned())) + } + _ => Err(EvalError::WrongArgCount(name.to_string())), + }, + _ => Err(EvalError::UnknownFunction(name.to_string())), + } +} diff --git a/crates/simplexpr/src/lib.rs b/crates/simplexpr/src/lib.rs new file mode 100644 index 0000000..6aee794 --- /dev/null +++ b/crates/simplexpr/src/lib.rs @@ -0,0 +1,24 @@ +#![feature(box_patterns)] +#![feature(format_args_capture)] +#![feature(pattern)] +#![feature(box_syntax)] +#![feature(try_blocks)] +#![feature(unwrap_infallible)] +#![feature(never_type)] + +pub mod ast; +pub mod dynval; +pub mod error; +pub mod eval; +pub mod parser; + +pub use ast::SimplExpr; + +use lalrpop_util::lalrpop_mod; + +lalrpop_mod!( + #[allow(clippy::all)] + pub simplexpr_parser +); + +pub use parser::parse_string; diff --git a/crates/simplexpr/src/parser/lalrpop_helpers.rs b/crates/simplexpr/src/parser/lalrpop_helpers.rs new file mode 100644 index 0000000..fd7a1a2 --- /dev/null +++ b/crates/simplexpr/src/parser/lalrpop_helpers.rs @@ -0,0 +1,45 @@ +use eww_shared_util::Span; + +use crate::{dynval::DynVal, SimplExpr}; + +use super::lexer::{LexicalError, Sp, StrLitSegment, Token}; + +pub fn b(x: T) -> Box { + Box::new(x) +} + +pub fn parse_stringlit( + span: Span, + mut segs: Vec>, +) -> Result> { + let file_id = span.2; + let parser = crate::simplexpr_parser::ExprParser::new(); + + if segs.len() == 1 { + let (lo, seg, hi) = segs.remove(0); + let span = Span(lo, hi, file_id); + match seg { + StrLitSegment::Literal(lit) => Ok(SimplExpr::Literal(DynVal(lit, span))), + StrLitSegment::Interp(toks) => { + let token_stream = toks.into_iter().map(|x| Ok(x)); + parser.parse(file_id, token_stream) + } + } + } else { + let elems = segs + .into_iter() + .filter_map(|(lo, segment, hi)| { + let span = Span(lo, hi, file_id); + match segment { + StrLitSegment::Literal(lit) if lit.is_empty() => None, + StrLitSegment::Literal(lit) => Some(Ok(SimplExpr::Literal(DynVal(lit, span)))), + StrLitSegment::Interp(toks) => { + let token_stream = toks.into_iter().map(|x| Ok(x)); + Some(parser.parse(file_id, token_stream)) + } + } + }) + .collect::, _>>()?; + Ok(SimplExpr::Concat(span, elems)) + } +} diff --git a/crates/simplexpr/src/parser/lexer.rs b/crates/simplexpr/src/parser/lexer.rs new file mode 100644 index 0000000..4df1bde --- /dev/null +++ b/crates/simplexpr/src/parser/lexer.rs @@ -0,0 +1,290 @@ +use std::str::pattern::Pattern; + +use eww_shared_util::{Span, Spanned}; +use once_cell::sync::Lazy; +use regex::{escape, Regex, RegexSet}; + +pub type Sp = (usize, T, usize); + +#[derive(Debug, PartialEq, Eq, Clone, strum::Display, strum::EnumString)] +pub enum StrLitSegment { + Literal(String), + Interp(Vec>), +} + +#[derive(Debug, PartialEq, Eq, Clone, strum::Display, strum::EnumString)] +pub enum Token { + Plus, + Minus, + Times, + Div, + Mod, + Equals, + NotEquals, + And, + Or, + GT, + LT, + Elvis, + RegexMatch, + + Not, + + Comma, + Question, + Colon, + LPren, + RPren, + LBrack, + RBrack, + Dot, + True, + False, + + Ident(String), + NumLit(String), + + StringLit(Vec>), + + Comment, + Skip, +} + +macro_rules! regex_rules { + ($( $regex:expr => $token:expr),*) => { + static LEXER_REGEX_SET: Lazy = Lazy::new(|| { RegexSet::new(&[ + $(format!("^{}", $regex)),* + ]).unwrap()}); + static LEXER_REGEXES: Lazy> = Lazy::new(|| { vec![ + $(Regex::new(&format!("^{}", $regex)).unwrap()),* + ]}); + static LEXER_FNS: Lazy Token + Sync + Send>>> = Lazy::new(|| { vec![ + $(Box::new($token)),* + ]}); + } +} + +static ESCAPE_REPLACE_REGEX: Lazy = Lazy::new(|| Regex::new(r"\\(.)").unwrap()); +pub static STR_INTERPOLATION_START: &str = "${"; +pub static STR_INTERPOLATION_END: &str = "}"; + +regex_rules! { + escape(r"+") => |_| Token::Plus, + escape(r"-") => |_| Token::Minus, + escape(r"*") => |_| Token::Times, + escape(r"/") => |_| Token::Div, + escape(r"%") => |_| Token::Mod, + escape(r"==") => |_| Token::Equals, + escape(r"!=") => |_| Token::NotEquals, + escape(r"&&") => |_| Token::And, + escape(r"||") => |_| Token::Or, + escape(r">") => |_| Token::GT, + escape(r"<") => |_| Token::LT, + escape(r"?:") => |_| Token::Elvis, + escape(r"=~") => |_| Token::RegexMatch, + + escape(r"!" ) => |_| Token::Not, + + escape(r",") => |_| Token::Comma, + escape(r"?") => |_| Token::Question, + escape(r":") => |_| Token::Colon, + escape(r"(") => |_| Token::LPren, + escape(r")") => |_| Token::RPren, + escape(r"[") => |_| Token::LBrack, + escape(r"]") => |_| Token::RBrack, + escape(r".") => |_| Token::Dot, + escape(r"true") => |_| Token::True, + escape(r"false") => |_| Token::False, + + r"[ \n\n\f]+" => |_| Token::Skip, + r";.*"=> |_| Token::Comment, + + r"[a-zA-Z_][a-zA-Z0-9_-]*" => |x| Token::Ident(x.to_string()), + r"[+-]?(?:[0-9]+[.])?[0-9]+" => |x| Token::NumLit(x.to_string()) +} + +#[derive(Debug)] +pub struct Lexer<'s> { + file_id: usize, + source: &'s str, + pos: usize, + failed: bool, + offset: usize, +} + +impl<'s> Lexer<'s> { + pub fn new(file_id: usize, span_offset: usize, source: &'s str) -> Self { + Lexer { source, offset: span_offset, file_id, failed: false, pos: 0 } + } + + fn remaining(&self) -> &'s str { + &self.source[self.pos..] + } + + pub fn continues_with(&self, pat: impl Pattern<'s>) -> bool { + self.remaining().starts_with(pat) + } + + pub fn next_token(&mut self) -> Option, LexicalError>> { + loop { + if self.failed || self.pos >= self.source.len() { + return None; + } + let remaining = self.remaining(); + + if remaining.starts_with(&['"', '\'', '`'][..]) { + return self.string_lit().map(|x| x.map(|(lo, segs, hi)| (lo, Token::StringLit(segs), hi))); + } else { + let match_set = LEXER_REGEX_SET.matches(remaining); + let matched_token = match_set + .into_iter() + .map(|i: usize| { + let m = LEXER_REGEXES[i].find(remaining).unwrap(); + (m.end(), i) + }) + .min_by_key(|(_, x)| *x); + + let (len, i) = match matched_token { + Some(x) => x, + None => { + self.failed = true; + return Some(Err(LexicalError(Span(self.pos + self.offset, self.pos + self.offset, self.file_id)))); + } + }; + + let tok_str = &self.source[self.pos..self.pos + len]; + let old_pos = self.pos; + self.advance_by(len); + match LEXER_FNS[i](tok_str.to_string()) { + Token::Skip | Token::Comment => {} + token => { + return Some(Ok((old_pos + self.offset, token, self.pos + self.offset))); + } + } + } + } + } + + fn advance_by(&mut self, n: usize) { + self.pos += n; + while self.pos < self.source.len() && !self.source.is_char_boundary(self.pos) { + self.pos += 1; + } + } + + fn advance_until_one_of<'a>(&mut self, pat: &[&'a str]) -> Option<&'a str> { + loop { + let remaining = self.remaining(); + if remaining.is_empty() { + return None; + } else if let Some(matched) = pat.iter().find(|&&p| remaining.starts_with(p)) { + self.advance_by(matched.len()); + return Some(matched); + } else { + self.advance_by(1); + } + } + } + + fn advance_until_unescaped_one_of<'a>(&mut self, pat: &[&'a str]) -> Option<&'a str> { + let mut pattern = pat.to_vec(); + pattern.push("\\"); + match self.advance_until_one_of(pattern.as_slice()) { + Some("\\") => { + self.advance_by(1); + self.advance_until_unescaped_one_of(pat) + } + result => result, + } + } + + pub fn string_lit(&mut self) -> Option>>, LexicalError>> { + let quote = self.remaining().chars().next()?.to_string(); + let str_lit_start = self.pos; + self.advance_by(quote.len()); + + let mut elements = Vec::new(); + let mut in_string_lit = true; + loop { + if in_string_lit { + let segment_start = self.pos - quote.len(); + + let segment_ender = self.advance_until_unescaped_one_of(&[STR_INTERPOLATION_START, "e])?; + let lit_content = &self.source[segment_start + quote.len()..self.pos - segment_ender.len()]; + let lit_content = ESCAPE_REPLACE_REGEX.replace_all(lit_content, "$1").to_string(); + elements.push((segment_start + self.offset, StrLitSegment::Literal(lit_content), self.pos + self.offset)); + + if segment_ender == STR_INTERPOLATION_START { + in_string_lit = false; + } else if segment_ender == quote { + return Some(Ok((str_lit_start + self.offset, elements, self.pos + self.offset))); + } + } else { + let segment_start = self.pos; + let mut toks = Vec::new(); + while self.pos < self.source.len() && !self.remaining().starts_with(STR_INTERPOLATION_END) { + match self.next_token()? { + Ok(tok) => toks.push(tok), + Err(err) => return Some(Err(err)), + } + } + elements.push((segment_start + self.offset, StrLitSegment::Interp(toks), self.pos + self.offset)); + self.advance_by(STR_INTERPOLATION_END.len()); + in_string_lit = true; + } + } + } +} + +impl<'s> Iterator for Lexer<'s> { + type Item = Result, LexicalError>; + + fn next(&mut self) -> Option { + self.next_token() + } +} + +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +pub struct LexicalError(pub Span); + +impl Spanned for LexicalError { + fn span(&self) -> Span { + self.0 + } +} + +impl std::fmt::Display for LexicalError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Lexical error at {}", self.0) + } +} + +#[cfg(test)] +mod test { + use super::*; + use eww_shared_util::snapshot_string; + use itertools::Itertools; + + macro_rules! v { + ($x:literal) => { + Lexer::new(0, 0, $x) + .map(|x| match x { + Ok((l, x, r)) => format!("({}, {:?}, {})", l, x, r), + Err(err) => format!("{}", err), + }) + .join("\n") + }; + } + + snapshot_string! { + basic => v!(r#"bar "foo""#), + digit => v!(r#"12"#), + number_in_ident => v!(r#"foo_1_bar"#), + interpolation_1 => v!(r#" "foo ${2 * 2} bar" "#), + interpolation_nested => v!(r#" "foo ${(2 * 2) + "${5 + 5}"} bar" "#), + escaping => v!(r#" "a\"b\{}" "#), + comments => v!("foo ; bar"), + weird_char_boundaries => v!(r#"" " + music"#), + symbol_spam => v!(r#"(foo + - "()" "a\"b" true false [] 12.2)"#), + } +} diff --git a/crates/simplexpr/src/parser/mod.rs b/crates/simplexpr/src/parser/mod.rs new file mode 100644 index 0000000..f6a2d9b --- /dev/null +++ b/crates/simplexpr/src/parser/mod.rs @@ -0,0 +1,47 @@ +pub mod lalrpop_helpers; +pub mod lexer; + +use crate::{ + ast::SimplExpr, + error::{Error, Result}, +}; + +pub fn parse_string(byte_offset: usize, file_id: usize, s: &str) -> Result { + let lexer = lexer::Lexer::new(file_id, byte_offset, s); + let parser = crate::simplexpr_parser::ExprParser::new(); + parser.parse(file_id, lexer).map_err(|e| Error::from_parse_error(file_id, e)) +} + +#[cfg(test)] +mod tests { + macro_rules! test_parser { + ($($text:literal),* $(,)?) => {{ + let p = crate::simplexpr_parser::ExprParser::new(); + use crate::parser::lexer::Lexer; + ::insta::with_settings!({sort_maps => true}, { + $( + ::insta::assert_debug_snapshot!(p.parse(0, Lexer::new(0, 0, $text))); + )* + }); + }} + } + + #[test] + fn test() { + test_parser!( + "1", + "2 + 5", + "2 * 5 + 1 * 1 + 3", + "(1 + 2) * 2", + "1 + true ? 2 : 5", + "1 + true ? 2 : 5 + 2", + "1 + (true ? 2 : 5) + 2", + "foo(1, 2)", + "! false || ! true", + "\"foo\" + 12.4", + "hi[\"ho\"]", + "foo.bar.baz", + "foo.bar[2 + 2] * asdf[foo.bar]", + ); + } +} diff --git a/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__basic.snap b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__basic.snap new file mode 100644 index 0000000..a7382f1 --- /dev/null +++ b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__basic.snap @@ -0,0 +1,7 @@ +--- +source: crates/simplexpr/src/parser/lexer.rs +expression: "v!(r#\"bar \"foo\"\"#)" + +--- +(0, Ident("bar"), 3) +(4, StringLit([(4, Literal("foo"), 9)]), 9) diff --git a/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__comments.snap b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__comments.snap new file mode 100644 index 0000000..281e375 --- /dev/null +++ b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__comments.snap @@ -0,0 +1,6 @@ +--- +source: crates/simplexpr/src/parser/lexer.rs +expression: "v!(\"foo ; bar\")" + +--- +(0, Ident("foo"), 3) diff --git a/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__digit.snap b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__digit.snap new file mode 100644 index 0000000..d57d778 --- /dev/null +++ b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__digit.snap @@ -0,0 +1,6 @@ +--- +source: crates/simplexpr/src/parser/lexer.rs +expression: "v!(r#\"12\"#)" + +--- +(0, NumLit("12"), 2) diff --git a/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__escaping.snap b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__escaping.snap new file mode 100644 index 0000000..e0d89c0 --- /dev/null +++ b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__escaping.snap @@ -0,0 +1,6 @@ +--- +source: crates/simplexpr/src/parser/lexer.rs +expression: "v!(r#\" \"a\\\"b\\{}\" \"#)" + +--- +(1, StringLit([(1, Literal("a\"b{}"), 10)]), 10) diff --git a/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__interpolation_1.snap b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__interpolation_1.snap new file mode 100644 index 0000000..8c5b37b --- /dev/null +++ b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__interpolation_1.snap @@ -0,0 +1,6 @@ +--- +source: crates/simplexpr/src/parser/lexer.rs +expression: "v!(r#\" \"foo ${2 * 2} bar\" \"#)" + +--- +(1, StringLit([(1, Literal("foo "), 8), (8, Interp([(8, NumLit("2"), 9), (10, Times, 11), (12, NumLit("2"), 13)]), 13), (13, Literal(" bar"), 19)]), 19) diff --git a/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__interpolation_nested.snap b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__interpolation_nested.snap new file mode 100644 index 0000000..dd53372 --- /dev/null +++ b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__interpolation_nested.snap @@ -0,0 +1,6 @@ +--- +source: crates/simplexpr/src/parser/lexer.rs +expression: "v!(r#\" \"foo ${(2 * 2) + \"${5 + 5}\"} bar\" \"#)" + +--- +(1, StringLit([(1, Literal("foo "), 8), (8, Interp([(8, LPren, 9), (9, NumLit("2"), 10), (11, Times, 12), (13, NumLit("2"), 14), (14, RPren, 15), (16, Plus, 17), (18, StringLit([(18, Literal(""), 21), (21, Interp([(21, NumLit("5"), 22), (23, Plus, 24), (25, NumLit("5"), 26)]), 26), (26, Literal(""), 28)]), 28)]), 28), (28, Literal(" bar"), 34)]), 34) diff --git a/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__number_in_ident.snap b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__number_in_ident.snap new file mode 100644 index 0000000..3b7104f --- /dev/null +++ b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__number_in_ident.snap @@ -0,0 +1,6 @@ +--- +source: crates/simplexpr/src/parser/lexer.rs +expression: "v!(r#\"foo_1_bar\"#)" + +--- +(0, Ident("foo_1_bar"), 9) diff --git a/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__simplexpr_lexer_basic.snap b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__simplexpr_lexer_basic.snap new file mode 100644 index 0000000..063238f --- /dev/null +++ b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__simplexpr_lexer_basic.snap @@ -0,0 +1,33 @@ +--- +source: crates/simplexpr/src/parser/lexer.rs +expression: "Lexer::new(0, 0, r#\"bar \"foo\"\"#).collect_vec()" + +--- +[ + Ok( + ( + 0, + Ident( + "bar", + ), + 3, + ), + ), + Ok( + ( + 4, + StringLit( + [ + ( + 4, + Literal( + "foo", + ), + 9, + ), + ], + ), + 9, + ), + ), +] diff --git a/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__simplexpr_lexer_str_interpolate-2.snap b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__simplexpr_lexer_str_interpolate-2.snap new file mode 100644 index 0000000..30ddaf6 --- /dev/null +++ b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__simplexpr_lexer_str_interpolate-2.snap @@ -0,0 +1,122 @@ +--- +source: crates/simplexpr/src/parser/lexer.rs +expression: "Lexer::new(0, 0, r#\" \"foo {(2 * 2) + \"{5 + 5}\"} bar\" \"#).collect_vec()" + +--- +[ + Ok( + ( + 1, + StringLit( + [ + ( + 1, + Literal( + "foo ", + ), + 7, + ), + ( + 7, + Interp( + [ + ( + 7, + LPren, + 8, + ), + ( + 8, + NumLit( + "2", + ), + 9, + ), + ( + 10, + Times, + 11, + ), + ( + 12, + NumLit( + "2", + ), + 13, + ), + ( + 13, + RPren, + 14, + ), + ( + 15, + Plus, + 16, + ), + ( + 17, + StringLit( + [ + ( + 17, + Literal( + "", + ), + 19, + ), + ( + 19, + Interp( + [ + ( + 19, + NumLit( + "5", + ), + 20, + ), + ( + 21, + Plus, + 22, + ), + ( + 23, + NumLit( + "5", + ), + 24, + ), + ], + ), + 24, + ), + ( + 24, + Literal( + "", + ), + 26, + ), + ], + ), + 26, + ), + ], + ), + 26, + ), + ( + 26, + Literal( + " bar", + ), + 32, + ), + ], + ), + 32, + ), + ), +] diff --git a/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__simplexpr_lexer_str_interpolate-3.snap b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__simplexpr_lexer_str_interpolate-3.snap new file mode 100644 index 0000000..4114172 --- /dev/null +++ b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__simplexpr_lexer_str_interpolate-3.snap @@ -0,0 +1,24 @@ +--- +source: crates/simplexpr/src/parser/lexer.rs +expression: "Lexer::new(0, 0, r#\" \"a\\\"b\\{}\" \"#).collect_vec()" + +--- +[ + Ok( + ( + 1, + StringLit( + [ + ( + 1, + Literal( + "a\"b{}", + ), + 10, + ), + ], + ), + 10, + ), + ), +] diff --git a/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__simplexpr_lexer_str_interpolate.snap b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__simplexpr_lexer_str_interpolate.snap new file mode 100644 index 0000000..1d3ed82 --- /dev/null +++ b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__simplexpr_lexer_str_interpolate.snap @@ -0,0 +1,58 @@ +--- +source: crates/simplexpr/src/parser/lexer.rs +expression: "Lexer::new(0, 0, r#\" \"foo {2 * 2} bar\" \"#).collect_vec()" + +--- +[ + Ok( + ( + 1, + StringLit( + [ + ( + 1, + Literal( + "foo ", + ), + 7, + ), + ( + 7, + Interp( + [ + ( + 7, + NumLit( + "2", + ), + 8, + ), + ( + 9, + Times, + 10, + ), + ( + 11, + NumLit( + "2", + ), + 12, + ), + ], + ), + 12, + ), + ( + 12, + Literal( + " bar", + ), + 18, + ), + ], + ), + 18, + ), + ), +] diff --git a/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__symbol_spam.snap b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__symbol_spam.snap new file mode 100644 index 0000000..f44221e --- /dev/null +++ b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__symbol_spam.snap @@ -0,0 +1,17 @@ +--- +source: crates/simplexpr/src/parser/lexer.rs +expression: "v!(r#\"(foo + - \"()\" \"a\\\"b\" true false [] 12.2)\"#)" + +--- +(0, LPren, 1) +(1, Ident("foo"), 4) +(5, Plus, 6) +(7, Minus, 8) +(9, StringLit([(9, Literal("()"), 13)]), 13) +(14, StringLit([(14, Literal("a\"b"), 20)]), 20) +(21, True, 25) +(26, False, 31) +(32, LBrack, 33) +(33, RBrack, 34) +(35, NumLit("12.2"), 39) +(39, RPren, 40) diff --git a/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__weird_char_boundaries.snap b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__weird_char_boundaries.snap new file mode 100644 index 0000000..3ced076 --- /dev/null +++ b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__weird_char_boundaries.snap @@ -0,0 +1,8 @@ +--- +source: crates/simplexpr/src/parser/lexer.rs +expression: "v!(r#\"\" \" + music\"#)" + +--- +(0, StringLit([(0, Literal("\u{f001} "), 8)]), 8) +(9, Plus, 10) +(11, Ident("music"), 16) diff --git a/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-10.snap b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-10.snap new file mode 100644 index 0000000..ea50a76 --- /dev/null +++ b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-10.snap @@ -0,0 +1,8 @@ +--- +source: crates/simplexpr/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, 0, \"\\\"foo\\\" + 12.4\"))" + +--- +Ok( + ("foo" + "12.4"), +) diff --git a/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-11.snap b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-11.snap new file mode 100644 index 0000000..6c8c2db --- /dev/null +++ b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-11.snap @@ -0,0 +1,8 @@ +--- +source: crates/simplexpr/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, 0, \"hi[\\\"ho\\\"]\"))" + +--- +Ok( + hi["ho"], +) diff --git a/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-12.snap b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-12.snap new file mode 100644 index 0000000..0d6c286 --- /dev/null +++ b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-12.snap @@ -0,0 +1,8 @@ +--- +source: crates/simplexpr/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, 0, \"foo.bar.baz\"))" + +--- +Ok( + foo["bar"]["baz"], +) diff --git a/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-13.snap b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-13.snap new file mode 100644 index 0000000..0ee1103 --- /dev/null +++ b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-13.snap @@ -0,0 +1,8 @@ +--- +source: crates/simplexpr/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, 0, \"foo.bar[2 + 2] * asdf[foo.bar]\"))" + +--- +Ok( + (foo["bar"][("2" + "2")] * asdf[foo["bar"]]), +) diff --git a/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-2.snap b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-2.snap new file mode 100644 index 0000000..17d36f7 --- /dev/null +++ b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-2.snap @@ -0,0 +1,8 @@ +--- +source: crates/simplexpr/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, 0, \"2 + 5\"))" + +--- +Ok( + ("2" + "5"), +) diff --git a/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-3.snap b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-3.snap new file mode 100644 index 0000000..f05dc6e --- /dev/null +++ b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-3.snap @@ -0,0 +1,8 @@ +--- +source: crates/simplexpr/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, 0, \"2 * 5 + 1 * 1 + 3\"))" + +--- +Ok( + ((("2" * "5") + ("1" * "1")) + "3"), +) diff --git a/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-4.snap b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-4.snap new file mode 100644 index 0000000..f012005 --- /dev/null +++ b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-4.snap @@ -0,0 +1,8 @@ +--- +source: crates/simplexpr/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, 0, \"(1 + 2) * 2\"))" + +--- +Ok( + (("1" + "2") * "2"), +) diff --git a/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-5.snap b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-5.snap new file mode 100644 index 0000000..637cea6 --- /dev/null +++ b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-5.snap @@ -0,0 +1,8 @@ +--- +source: crates/simplexpr/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, 0, \"1 + true ? 2 : 5\"))" + +--- +Ok( + (("1" + "true") ? "2" : "5"), +) diff --git a/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-6.snap b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-6.snap new file mode 100644 index 0000000..b1f1eb3 --- /dev/null +++ b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-6.snap @@ -0,0 +1,8 @@ +--- +source: crates/simplexpr/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, 0, \"1 + true ? 2 : 5 + 2\"))" + +--- +Ok( + (("1" + "true") ? "2" : ("5" + "2")), +) diff --git a/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-7.snap b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-7.snap new file mode 100644 index 0000000..7824d4e --- /dev/null +++ b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-7.snap @@ -0,0 +1,8 @@ +--- +source: crates/simplexpr/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, 0, \"1 + (true ? 2 : 5) + 2\"))" + +--- +Ok( + (("1" + ("true" ? "2" : "5")) + "2"), +) diff --git a/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-8.snap b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-8.snap new file mode 100644 index 0000000..1bde215 --- /dev/null +++ b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-8.snap @@ -0,0 +1,8 @@ +--- +source: crates/simplexpr/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, 0, \"foo(1, 2)\"))" + +--- +Ok( + foo("1", "2"), +) diff --git a/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-9.snap b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-9.snap new file mode 100644 index 0000000..14b1288 --- /dev/null +++ b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-9.snap @@ -0,0 +1,8 @@ +--- +source: crates/simplexpr/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, 0, \"! false || ! true\"))" + +--- +Ok( + (!"false" || !"true"), +) diff --git a/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test.snap b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test.snap new file mode 100644 index 0000000..7e0726c --- /dev/null +++ b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test.snap @@ -0,0 +1,8 @@ +--- +source: crates/simplexpr/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, 0, \"1\"))" + +--- +Ok( + "1", +) diff --git a/crates/simplexpr/src/simplexpr_parser.lalrpop b/crates/simplexpr/src/simplexpr_parser.lalrpop new file mode 100644 index 0000000..10028b7 --- /dev/null +++ b/crates/simplexpr/src/simplexpr_parser.lalrpop @@ -0,0 +1,112 @@ +use crate::ast::{SimplExpr::{self, *}, BinOp::*, UnaryOp::*}; +use eww_shared_util::{Span, VarName}; +use crate::parser::lexer::{Token, LexicalError, StrLitSegment, Sp}; +use crate::parser::lalrpop_helpers::*; + + +grammar(fid: usize); + +extern { + type Location = usize; + type Error = LexicalError; + + enum Token { + "+" => Token::Plus, + "-" => Token::Minus, + "*" => Token::Times, + "/" => Token::Div, + "%" => Token::Mod, + "==" => Token::Equals, + "!=" => Token::NotEquals, + "&&" => Token::And, + "||" => Token::Or, + ">" => Token::GT, + "<" => Token::LT, + "?:" => Token::Elvis, + "=~" => Token::RegexMatch, + + "!" => Token::Not, + + "," => Token::Comma, + "?" => Token::Question, + ":" => Token::Colon, + "(" => Token::LPren, + ")" => Token::RPren, + "[" => Token::LBrack, + "]" => Token::RBrack, + "." => Token::Dot, + + "true" => Token::True, + "false" => Token::False, + + "identifier" => Token::Ident(), + "number" => Token::NumLit(), + "string" => Token::StringLit(>>), + + } +} + +Comma: Vec = { + ",")*> => match e { + None => v, + Some(e) => { + v.push(e); + v + } + } +}; + +pub Expr: SimplExpr = { + + #[precedence(level="0")] + // "lexer_error" =>? { + // Err(ParseError::User { error: LexicalError(l, r, fid) }) + //}, + + =>? parse_stringlit(Span(l, r, fid), x), + => SimplExpr::literal(Span(l, r, fid), x), + "true" => SimplExpr::literal(Span(l, r, fid), "true".into()), + "false" => SimplExpr::literal(Span(l, r, fid), "false".into()), + + => VarRef(Span(l, r, fid), VarName(ident.to_string())), + "(" ")", + + #[precedence(level="1")] #[assoc(side="right")] + "(" > ")" => FunctionCall(Span(l, r, fid), ident, args), + "[" "]" => JsonAccess(Span(l, r, fid), b(value), b(index)), + + "." => { + JsonAccess(Span(l, r, fid), b(value), b(Literal(index.into()))) + }, + + #[precedence(level="2")] #[assoc(side="right")] + "!" => UnaryOp(Span(l, r, fid), Not, b(e)), + + #[precedence(level="3")] #[assoc(side="left")] + "*" => BinOp(Span(l, r, fid), b(le), Times, b(re)), + "/" => BinOp(Span(l, r, fid), b(le), Div, b(re)), + "%" => BinOp(Span(l, r, fid), b(le), Mod, b(re)), + + #[precedence(level="4")] #[assoc(side="left")] + "+" => BinOp(Span(l, r, fid), b(le), Plus, b(re)), + "-" => BinOp(Span(l, r, fid), b(le), Minus, b(re)), + + #[precedence(level="5")] #[assoc(side="left")] + "==" => BinOp(Span(l, r, fid), b(le), Equals, b(re)), + "!=" => BinOp(Span(l, r, fid), b(le), NotEquals, b(re)), + "<" => BinOp(Span(l, r, fid), b(le), GT, b(re)), + ">" => BinOp(Span(l, r, fid), b(le), LT, b(re)), + "=~" => BinOp(Span(l, r, fid), b(le), RegexMatch, b(re)), + + #[precedence(level="6")] #[assoc(side="left")] + "&&" => BinOp(Span(l, r, fid), b(le), And, b(re)), + "||" => BinOp(Span(l, r, fid), b(le), Or, b(re)), + "?:" => BinOp(Span(l, r, fid), b(le), Elvis, b(re)), + + #[precedence(level="7")] #[assoc(side="right")] + "?" ":" => { + IfElse(Span(l, r, fid), b(cond), b(then), b(els)) + }, +}; + +ExprReset = ; diff --git a/crates/simplexpr/src/snapshots/simplexpr__dynval__test__parse_vec-2.snap b/crates/simplexpr/src/snapshots/simplexpr__dynval__test__parse_vec-2.snap new file mode 100644 index 0000000..bc9aa76 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__dynval__test__parse_vec-2.snap @@ -0,0 +1,10 @@ +--- +source: crates/simplexpr/src/dynval.rs +expression: "DynVal::from_string(\"[hi]\".to_string()).as_vec()" + +--- +Ok( + [ + "hi", + ], +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__dynval__test__parse_vec-3.snap b/crates/simplexpr/src/snapshots/simplexpr__dynval__test__parse_vec-3.snap new file mode 100644 index 0000000..62819a1 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__dynval__test__parse_vec-3.snap @@ -0,0 +1,12 @@ +--- +source: crates/simplexpr/src/dynval.rs +expression: "DynVal::from_string(\"[hi,ho,hu]\".to_string()).as_vec()" + +--- +Ok( + [ + "hi", + "ho", + "hu", + ], +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__dynval__test__parse_vec-4.snap b/crates/simplexpr/src/snapshots/simplexpr__dynval__test__parse_vec-4.snap new file mode 100644 index 0000000..485cc90 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__dynval__test__parse_vec-4.snap @@ -0,0 +1,10 @@ +--- +source: crates/simplexpr/src/dynval.rs +expression: "DynVal::from_string(\"[hi\\\\,ho]\".to_string()).as_vec()" + +--- +Ok( + [ + "hi,ho", + ], +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__dynval__test__parse_vec-5.snap b/crates/simplexpr/src/snapshots/simplexpr__dynval__test__parse_vec-5.snap new file mode 100644 index 0000000..8b754d4 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__dynval__test__parse_vec-5.snap @@ -0,0 +1,11 @@ +--- +source: crates/simplexpr/src/dynval.rs +expression: "DynVal::from_string(\"[hi\\\\,ho,hu]\".to_string()).as_vec()" + +--- +Ok( + [ + "hi,ho", + "hu", + ], +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__dynval__test__parse_vec-6.snap b/crates/simplexpr/src/snapshots/simplexpr__dynval__test__parse_vec-6.snap new file mode 100644 index 0000000..a458990 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__dynval__test__parse_vec-6.snap @@ -0,0 +1,8 @@ +--- +source: crates/simplexpr/src/dynval.rs +expression: "DynVal::from_string(\"\".to_string()).as_vec()" + +--- +Ok( + [], +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__dynval__test__parse_vec-7.snap b/crates/simplexpr/src/snapshots/simplexpr__dynval__test__parse_vec-7.snap new file mode 100644 index 0000000..c02e4e6 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__dynval__test__parse_vec-7.snap @@ -0,0 +1,12 @@ +--- +source: crates/simplexpr/src/dynval.rs +expression: "DynVal::from_string(\"[a,b\".to_string()).as_vec()" + +--- +Err( + ConversionError { + value: "[a,b", + target_type: "vec", + source: None, + }, +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__dynval__test__parse_vec-8.snap b/crates/simplexpr/src/snapshots/simplexpr__dynval__test__parse_vec-8.snap new file mode 100644 index 0000000..47e8caf --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__dynval__test__parse_vec-8.snap @@ -0,0 +1,12 @@ +--- +source: crates/simplexpr/src/dynval.rs +expression: "DynVal::from_string(\"a]\".to_string()).as_vec()" + +--- +Err( + ConversionError { + value: "a]", + target_type: "vec", + source: None, + }, +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__dynval__test__parse_vec.snap b/crates/simplexpr/src/snapshots/simplexpr__dynval__test__parse_vec.snap new file mode 100644 index 0000000..b5d5e2c --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__dynval__test__parse_vec.snap @@ -0,0 +1,10 @@ +--- +source: crates/simplexpr/src/dynval.rs +expression: "DynVal::from_string(\"[]\".to_string()).as_vec()" + +--- +Ok( + [ + "", + ], +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-10.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-10.snap new file mode 100644 index 0000000..1fa28f0 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-10.snap @@ -0,0 +1,8 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"\\\"foo\\\" + 12.4\"))" + +--- +Ok( + ("foo" + "12.4"), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-11.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-11.snap new file mode 100644 index 0000000..44b65b7 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-11.snap @@ -0,0 +1,8 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"hi[\\\"ho\\\"]\"))" + +--- +Ok( + hi["ho"], +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-12.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-12.snap new file mode 100644 index 0000000..e19354b --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-12.snap @@ -0,0 +1,8 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"foo.bar.baz\"))" + +--- +Ok( + foo["bar"]["baz"], +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-13.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-13.snap new file mode 100644 index 0000000..7494deb --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-13.snap @@ -0,0 +1,8 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"foo.bar[2 + 2] * asdf[foo.bar]\"))" + +--- +Ok( + (foo["bar"][("2" + "2")] * asdf[foo["bar"]]), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-14.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-14.snap new file mode 100644 index 0000000..692ac35 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-14.snap @@ -0,0 +1,30 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"1 + (true ? 2 : 5) + 2\"))" + +--- +Ok( + BinOp( + BinOp( + Literal( + "1", + ), + Plus, + IfElse( + Literal( + "true", + ), + Literal( + "2", + ), + Literal( + "5", + ), + ), + ), + Plus, + Literal( + "2", + ), + ), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-15.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-15.snap new file mode 100644 index 0000000..16b8e8a --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-15.snap @@ -0,0 +1,13 @@ +--- +source: src/lib.rs +expression: "Lexer::new(\"foo(1, 2)\").filter_map(|x|\n x.ok()).map(|(_, x, _)|\n match x {\n Token::Ident(x) |\n Token::NumLit(x) |\n Token::StrLit(x) =>\n format!(\"{}\", x),\n x =>\n format!(\"{}\", x),\n }).collect::>()" + +--- +[ + "foo", + "LPren", + "1", + "Comma", + "2", + "RPren", +] diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-16.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-16.snap new file mode 100644 index 0000000..e128f15 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-16.snap @@ -0,0 +1,18 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"foo(1, 2)\"))" + +--- +Ok( + FunctionCall( + "foo", + [ + Literal( + "1", + ), + Literal( + "2", + ), + ], + ), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-17.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-17.snap new file mode 100644 index 0000000..ae5f678 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-17.snap @@ -0,0 +1,12 @@ +--- +source: src/lib.rs +expression: "Lexer::new(\"! false || ! true\").filter_map(|x|\n x.ok()).map(|(_, x, _)|\n match x {\n Token::Ident(x)\n |\n Token::NumLit(x)\n |\n Token::StrLit(x)\n =>\n format!(\"{}\",\n x),\n x =>\n format!(\"{}\",\n x),\n }).collect::>()" + +--- +[ + "!", + "False", + "||", + "!", + "True", +] diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-18.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-18.snap new file mode 100644 index 0000000..9ffd512 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-18.snap @@ -0,0 +1,22 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"! false || ! true\"))" + +--- +Ok( + BinOp( + UnaryOp( + Not, + Literal( + "false", + ), + ), + Or, + UnaryOp( + Not, + Literal( + "true", + ), + ), + ), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-19.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-19.snap new file mode 100644 index 0000000..8d6b02b --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-19.snap @@ -0,0 +1,10 @@ +--- +source: src/lib.rs +expression: "Lexer::new(\"\\\"foo\\\" + 12.4\").filter_map(|x|\n x.ok()).map(|(_, x, _)|\n match x {\n Token::Ident(x)\n |\n Token::NumLit(x)\n |\n Token::StrLit(x)\n =>\n format!(\"{}\",\n x),\n x =>\n format!(\"{}\",\n x),\n }).collect::>()" + +--- +[ + "\"foo\"", + "+", + "12.4", +] diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-2.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-2.snap new file mode 100644 index 0000000..3b4e5e4 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-2.snap @@ -0,0 +1,8 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"2 + 5\"))" + +--- +Ok( + ("2" + "5"), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-20.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-20.snap new file mode 100644 index 0000000..9d42e5a --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-20.snap @@ -0,0 +1,16 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"\\\"foo\\\" + 12.4\"))" + +--- +Ok( + BinOp( + Literal( + "foo", + ), + Plus, + Literal( + "12.4", + ), + ), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-21.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-21.snap new file mode 100644 index 0000000..84a5eb7 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-21.snap @@ -0,0 +1,11 @@ +--- +source: src/lib.rs +expression: "Lexer::new(\"hi[\\\"ho\\\"]\").filter_map(|x|\n x.ok()).map(|(_, x, _)|\n match x {\n Token::Ident(x) |\n Token::NumLit(x) |\n Token::StrLit(x)\n =>\n format!(\"{}\", x),\n x =>\n format!(\"{}\", x),\n }).collect::>()" + +--- +[ + "hi", + "LBrack", + "\"ho\"", + "RBrack", +] diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-22.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-22.snap new file mode 100644 index 0000000..958cec3 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-22.snap @@ -0,0 +1,15 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"hi[\\\"ho\\\"]\"))" + +--- +Ok( + JsonAccess( + VarRef( + "hi", + ), + Literal( + "ho", + ), + ), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-23.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-23.snap new file mode 100644 index 0000000..6e91f79 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-23.snap @@ -0,0 +1,12 @@ +--- +source: src/lib.rs +expression: "Lexer::new(\"foo.bar.baz\").filter_map(|x|\n x.ok()).map(|(_, x, _)|\n match x {\n Token::Ident(x) |\n Token::NumLit(x)\n |\n Token::StrLit(x)\n =>\n format!(\"{}\", x),\n x =>\n format!(\"{}\", x),\n }).collect::>()" + +--- +[ + "foo", + "Dot", + "bar", + "Dot", + "baz", +] diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-24.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-24.snap new file mode 100644 index 0000000..be97009 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-24.snap @@ -0,0 +1,20 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"foo.bar.baz\"))" + +--- +Ok( + JsonAccess( + JsonAccess( + VarRef( + "foo", + ), + Literal( + "bar", + ), + ), + Literal( + "baz", + ), + ), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-25.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-25.snap new file mode 100644 index 0000000..3a21b7b --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-25.snap @@ -0,0 +1,22 @@ +--- +source: src/lib.rs +expression: "Lexer::new(\"foo.bar[2 + 2] * asdf[foo.bar]\").filter_map(|x|\n x.ok()).map(|(_,\n x,\n _)|\n match x\n {\n Token::Ident(x)\n |\n Token::NumLit(x)\n |\n Token::StrLit(x)\n =>\n format!(\"{}\",\n x),\n x\n =>\n format!(\"{}\",\n x),\n }).collect::>()" + +--- +[ + "foo", + "Dot", + "bar", + "LBrack", + "2", + "+", + "2", + "RBrack", + "*", + "asdf", + "LBrack", + "foo", + "Dot", + "bar", + "RBrack", +] diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-26.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-26.snap new file mode 100644 index 0000000..bdabe4e --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-26.snap @@ -0,0 +1,42 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"foo.bar[2 + 2] * asdf[foo.bar]\"))" + +--- +Ok( + BinOp( + JsonAccess( + JsonAccess( + VarRef( + "foo", + ), + Literal( + "bar", + ), + ), + BinOp( + Literal( + "2", + ), + Plus, + Literal( + "2", + ), + ), + ), + Times, + JsonAccess( + VarRef( + "asdf", + ), + JsonAccess( + VarRef( + "foo", + ), + Literal( + "bar", + ), + ), + ), + ), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-3.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-3.snap new file mode 100644 index 0000000..7a11e2b --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-3.snap @@ -0,0 +1,8 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"2 * 5 + 1 * 1 + 3\"))" + +--- +Ok( + ((("2" * "5") + ("1" * "1")) + "3"), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-4.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-4.snap new file mode 100644 index 0000000..9025ed3 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-4.snap @@ -0,0 +1,8 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"(1 + 2) * 2\"))" + +--- +Ok( + (("1" + "2") * "2"), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-5.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-5.snap new file mode 100644 index 0000000..683c97b --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-5.snap @@ -0,0 +1,8 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"1 + true ? 2 : 5\"))" + +--- +Ok( + (if ("1" + "true") then "2" else "5"), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-6.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-6.snap new file mode 100644 index 0000000..ca3c9ba --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-6.snap @@ -0,0 +1,8 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"1 + true ? 2 : 5 + 2\"))" + +--- +Ok( + (if ("1" + "true") then "2" else ("5" + "2")), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-7.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-7.snap new file mode 100644 index 0000000..bd2587c --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-7.snap @@ -0,0 +1,8 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"1 + (true ? 2 : 5) + 2\"))" + +--- +Ok( + (("1" + (if "true" then "2" else "5")) + "2"), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-8.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-8.snap new file mode 100644 index 0000000..cbd02d1 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-8.snap @@ -0,0 +1,8 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"foo(1, 2)\"))" + +--- +Ok( + foo("1", "2"), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-9.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-9.snap new file mode 100644 index 0000000..eba646a --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-9.snap @@ -0,0 +1,8 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"! false || ! true\"))" + +--- +Ok( + (!"false" || !"true"), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test.snap new file mode 100644 index 0000000..6c1e712 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test.snap @@ -0,0 +1,8 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"1\"))" + +--- +Ok( + "1", +) diff --git a/crates/yuck/Cargo.lock b/crates/yuck/Cargo.lock new file mode 100644 index 0000000..b720e0f --- /dev/null +++ b/crates/yuck/Cargo.lock @@ -0,0 +1,848 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anyhow" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595d3cfa7a60d4555cb5067b99f07142a08ea778de5cf993f7b75c7d8fabc486" + +[[package]] +name = "ascii-canvas" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" +dependencies = [ + "term", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "beef" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6736e2428df2ca2848d846c43e88745121a6654696e349ce0054a420815a7409" + +[[package]] +name = "bit-set" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "console" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3993e6445baa160675931ec041a5e03ca84b9c6e32a056150d3aa2bdda0a1f45" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "terminal_size", + "winapi", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "ctor" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e98e2ad1a782e33928b96fc3948e7c355e5af34ba4de7670fe8bac2a3b2006d" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "derive_more" +version = "0.99.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40eebddd2156ce1bb37b20bbe5151340a31828b1f2d22ba4141f3531710e38df" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "diff" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "ena" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7402b94a93c24e742487327a7cd839dc9d36fec9de9fb25b09f2dae459f36c3" +dependencies = [ + "log", +] + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "eww_config" +version = "0.1.0" +dependencies = [ + "anyhow", + "codespan-reporting", + "derive_more", + "insta", + "itertools", + "lalrpop", + "lalrpop-util", + "lazy_static", + "maplit", + "pretty_assertions", + "regex", + "serde", + "serde_json", + "simplexpr", + "smart-default", + "strum", + "thiserror", +] + +[[package]] +name = "fixedbitset" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "indexmap" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "insta" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1b21a2971cea49ca4613c0e9fe8225ecaf5de64090fddc6002284726e9244" +dependencies = [ + "console", + "lazy_static", + "ron", + "serde", + "serde_json", + "serde_yaml", + "similar", + "uuid", +] + +[[package]] +name = "itertools" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + +[[package]] +name = "lalrpop" +version = "0.19.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15174f1c529af5bf1283c3bc0058266b483a67156f79589fab2a25e23cf8988" +dependencies = [ + "ascii-canvas", + "atty", + "bit-set", + "diff", + "ena", + "itertools", + "lalrpop-util", + "petgraph", + "pico-args", + "regex", + "regex-syntax", + "string_cache", + "term", + "tiny-keccak", + "unicode-xid", +] + +[[package]] +name = "lalrpop-util" +version = "0.19.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e58cce361efcc90ba8a0a5f982c741ff86b603495bb15a998412e957dcd278" +dependencies = [ + "regex", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" + +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "logos" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427e2abca5be13136da9afdbf874e6b34ad9001dd70f2b103b083a85daa7b345" +dependencies = [ + "logos-derive", +] + +[[package]] +name = "logos-derive" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56a7d287fd2ac3f75b11f19a1c8a874a7d55744bd91f7a1b3e7cf87d4343c36d" +dependencies = [ + "beef", + "fnv", + "proc-macro2", + "quote", + "regex-syntax", + "syn", + "utf8-ranges", +] + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "memchr" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" + +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "output_vt100" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9" +dependencies = [ + "winapi", +] + +[[package]] +name = "pest" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +dependencies = [ + "ucd-trie", +] + +[[package]] +name = "petgraph" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "pretty_assertions" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cab0e7c02cf376875e9335e0ba1da535775beb5450d21e1dffca068818ed98b" +dependencies = [ + "ansi_term", + "ctor", + "diff", + "output_vt100", +] + +[[package]] +name = "proc-macro2" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom", + "redox_syscall", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "ron" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064ea8613fb712a19faf920022ec8ddf134984f100090764a4e1d768f3827f1f" +dependencies = [ + "base64", + "bitflags", + "serde", +] + +[[package]] +name = "rustc_version" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088" + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" +dependencies = [ + "pest", +] + +[[package]] +name = "serde" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15654ed4ab61726bf918a39cb8d98a2e2995b002387807fa6ba58fdf7f59bb23" +dependencies = [ + "dtoa", + "linked-hash-map", + "serde", + "yaml-rust", +] + +[[package]] +name = "similar" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec" + +[[package]] +name = "simplexpr" +version = "0.1.0" +dependencies = [ + "itertools", + "lalrpop", + "lalrpop-util", + "logos", + "maplit", + "regex", + "serde", + "serde_json", + "strum", + "thiserror", +] + +[[package]] +name = "siphasher" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbce6d4507c7e4a3962091436e56e95290cb71fa302d0d270e32130b75fbff27" + +[[package]] +name = "smart-default" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133659a15339456eeeb07572eb02a91c91e9815e9cbc89566944d2c8d3efdbf6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "string_cache" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb1139b5353f96e429e1a5e19fbaf663bddedaa06d1dbd49f82e352601209a" +dependencies = [ + "lazy_static", + "new_debug_unreachable", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "strum" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "ucd-trie" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" + +[[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "utf8-ranges" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ae116fef2b7fea257ed6440d3cfcff7f190865f170cdad00bb6465bf18ecba" + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/crates/yuck/Cargo.toml b/crates/yuck/Cargo.toml new file mode 100644 index 0000000..f918df5 --- /dev/null +++ b/crates/yuck/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "yuck" +version = "0.1.0" +authors = ["elkowar <5300871+elkowar@users.noreply.github.com>"] +edition = "2018" + +build = "build.rs" + +[features] +default = ["x11"] +x11 = [] +wayland = [] + +[dependencies] +lalrpop-util = "0.19.5" +regex = "1" +itertools = "0.10" +thiserror = "1.0" +maplit = "1.0" +codespan-reporting = "0.11" + +derive_more = "0.99" +smart-default = "0.6" +serde = {version = "1.0", features = ["derive"]} +serde_json = "1.0" +once_cell = "1.8" + +strum = { version = "0.21", features = ["derive"] } +anyhow = "1" +static_assertions = "1.1" + +simplexpr = { path = "../simplexpr" } +eww_shared_util = { path = "../eww_shared_util" } + + +[build-dependencies] +lalrpop = "0.19.5" + +[dev-dependencies] +insta = { version = "1.7", features = ["ron"]} +pretty_assertions = "0.7" diff --git a/crates/yuck/build.rs b/crates/yuck/build.rs new file mode 100644 index 0000000..57684be --- /dev/null +++ b/crates/yuck/build.rs @@ -0,0 +1,4 @@ +extern crate lalrpop; +fn main() { + lalrpop::process_root().unwrap(); +} diff --git a/crates/yuck/rust-toolchain b/crates/yuck/rust-toolchain new file mode 100644 index 0000000..bf867e0 --- /dev/null +++ b/crates/yuck/rust-toolchain @@ -0,0 +1 @@ +nightly diff --git a/crates/yuck/rustfmt.toml b/crates/yuck/rustfmt.toml new file mode 100644 index 0000000..edce9c8 --- /dev/null +++ b/crates/yuck/rustfmt.toml @@ -0,0 +1,14 @@ +unstable_features = true +fn_single_line = false +max_width = 130 +reorder_impl_items = true +merge_imports = true +normalize_comments = true +use_field_init_shorthand = true +#wrap_comments = true +combine_control_expr = false +condense_wildcard_suffixes = true +format_code_in_doc_comments = true +format_macro_matchers = true +format_strings = true +use_small_heuristics = "Max" diff --git a/crates/yuck/src/config/attributes.rs b/crates/yuck/src/config/attributes.rs new file mode 100644 index 0000000..eaa7d6a --- /dev/null +++ b/crates/yuck/src/config/attributes.rs @@ -0,0 +1,120 @@ +use std::{ + collections::HashMap, + convert::{TryFrom, TryInto}, +}; + +use simplexpr::{ + dynval::{DynVal, FromDynVal}, + eval::EvalError, + SimplExpr, +}; + +use crate::{ + error::AstError, + parser::{ast::Ast, from_ast::FromAst}, +}; +use eww_shared_util::{AttrName, Span, Spanned, VarName}; + +#[derive(Debug, thiserror::Error)] +pub enum AttrError { + #[error("Missing required attribute {1}")] + MissingRequiredAttr(Span, AttrName), + + #[error("{1}")] + EvaluationError(Span, EvalError), + + #[error("{1}")] + Other(Span, Box), +} + +impl Spanned for AttrError { + fn span(&self) -> Span { + match self { + AttrError::MissingRequiredAttr(span, _) => *span, + AttrError::EvaluationError(span, _) => *span, + AttrError::Other(span, _) => *span, + } + } +} + +#[derive(Debug)] +pub struct UnusedAttrs { + definition_span: Span, + attrs: Vec<(Span, AttrName)>, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] +pub struct AttrEntry { + pub key_span: Span, + pub value: Ast, +} + +impl AttrEntry { + pub fn new(key_span: Span, value: Ast) -> AttrEntry { + AttrEntry { key_span, value } + } +} + +// TODO maybe make this generic over the contained content +#[derive(Debug, Clone, Eq, PartialEq, serde::Serialize)] +pub struct Attributes { + pub span: Span, + pub attrs: HashMap, +} + +impl Attributes { + pub fn new(span: Span, attrs: HashMap) -> Self { + Attributes { span, attrs } + } + + pub fn ast_required(&mut self, key: &str) -> Result { + let key = AttrName(key.to_string()); + match self.attrs.remove(&key) { + Some(AttrEntry { key_span, value }) => T::from_ast(value), + None => Err(AttrError::MissingRequiredAttr(self.span, key.clone()).into()), + } + } + + pub fn ast_optional(&mut self, key: &str) -> Result, AstError> { + match self.attrs.remove(&AttrName(key.to_string())) { + Some(AttrEntry { key_span, value }) => T::from_ast(value).map(Some), + None => Ok(None), + } + } + + pub fn primitive_required(&mut self, key: &str) -> Result + where + E: std::error::Error + 'static + Sync + Send, + T: FromDynVal, + { + let ast: SimplExpr = self.ast_required(&key)?; + Ok(ast + .eval_no_vars() + .map_err(|err| AttrError::EvaluationError(ast.span(), err))? + .read_as() + .map_err(|e| AttrError::Other(ast.span().into(), Box::new(e)))?) + } + + pub fn primitive_optional(&mut self, key: &str) -> Result, AstError> + where + E: std::error::Error + 'static + Sync + Send, + T: FromDynVal, + { + let ast: SimplExpr = match self.ast_optional(key)? { + Some(ast) => ast, + None => return Ok(None), + }; + Ok(Some( + ast.eval_no_vars() + .map_err(|err| AttrError::EvaluationError(ast.span(), err))? + .read_as() + .map_err(|e| AttrError::Other(ast.span().into(), Box::new(e)))?, + )) + } + + /// Consumes the attributes to return a list of unused attributes which may be used to emit a warning. + /// TODO actually use this and implement warnings,... lol + pub fn get_unused(self, definition_span: Span) -> UnusedAttrs { + UnusedAttrs { definition_span, attrs: self.attrs.into_iter().map(|(k, v)| (v.key_span.to(v.value.span()), k)).collect() } + } +} diff --git a/crates/yuck/src/config/backend_window_options.rs b/crates/yuck/src/config/backend_window_options.rs new file mode 100644 index 0000000..46eedfa --- /dev/null +++ b/crates/yuck/src/config/backend_window_options.rs @@ -0,0 +1,133 @@ +use std::str::FromStr; + +use anyhow::*; + +use crate::{ + enum_parse, + error::AstResult, + parser::{ast::Ast, ast_iterator::AstIterator, from_ast::FromAstElementContent}, + value::NumWithUnit, +}; +use eww_shared_util::Span; + +use super::{attributes::Attributes, window_definition::EnumParseError}; + +pub use backend::*; + +#[cfg(feature = "x11")] +mod backend { + use super::*; + + #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] + pub struct BackendWindowOptions { + pub wm_ignore: bool, + pub sticky: bool, + pub window_type: WindowType, + pub struts: StrutDefinition, + } + + impl BackendWindowOptions { + pub fn from_attrs(attrs: &mut Attributes) -> AstResult { + let struts = attrs.ast_optional("reserve")?; + let window_type = attrs.primitive_optional("windowtype")?; + Ok(Self { + wm_ignore: attrs.primitive_optional("wm-ignore")?.unwrap_or(window_type.is_none() && struts.is_none()), + window_type: window_type.unwrap_or_default(), + sticky: attrs.primitive_optional("sticky")?.unwrap_or(true), + struts: struts.unwrap_or_default(), + }) + } + } + + #[derive(Debug, Clone, PartialEq, Eq, smart_default::SmartDefault, serde::Serialize)] + pub enum WindowType { + #[default] + Dock, + Dialog, + Toolbar, + Normal, + Utility, + } + impl FromStr for WindowType { + type Err = EnumParseError; + + fn from_str(s: &str) -> Result { + enum_parse! { "window type", s, + "dock" => Self::Dock, + "toolbar" => Self::Toolbar, + "dialog" => Self::Dialog, + "normal" => Self::Normal, + "utility" => Self::Utility, + } + } + } + + #[derive(Debug, Clone, Copy, Eq, PartialEq, smart_default::SmartDefault, serde::Serialize)] + pub enum Side { + #[default] + Top, + Left, + Right, + Bottom, + } + + impl std::str::FromStr for Side { + type Err = EnumParseError; + + fn from_str(s: &str) -> Result { + enum_parse! { "side", s, + "l" | "left" => Side::Left, + "r" | "right" => Side::Right, + "t" | "top" => Side::Top, + "b" | "bottom" => Side::Bottom, + } + } + } + + // Surface definition if the backend for X11 is enable + #[derive(Debug, Clone, Copy, Eq, PartialEq, Default, serde::Serialize)] + pub struct StrutDefinition { + pub side: Side, + pub dist: NumWithUnit, + } + + impl FromAstElementContent for StrutDefinition { + const ELEMENT_NAME: &'static str = "struts"; + + fn from_tail>(span: Span, mut iter: AstIterator) -> AstResult { + let mut attrs = iter.expect_key_values()?; + iter.expect_done().map_err(|e| e.note("Check if you are missing a colon in front of a key"))?; + Ok(StrutDefinition { side: attrs.primitive_required("side")?, dist: attrs.primitive_required("distance")? }) + } + } +} + +#[cfg(feature = "wayland")] +mod backend { + use super::*; + #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] + pub struct BackendWindowOptions { + pub exclusive: bool, + pub focusable: bool, + } + impl BackendWindowOptions { + pub fn from_attrs(attrs: &mut Attributes) -> AstResult { + Ok(Self { + exclusive: attrs.primitive_optional("exclusive")?.unwrap_or(false), + focusable: attrs.primitive_optional("focusable")?.unwrap_or(false), + }) + } + } +} + +#[cfg(not(any(feature = "x11", feature = "wayland")))] +mod backend { + use super::*; + #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] + pub struct BackendWindowOptions; + impl BackendWindowOptions { + pub fn from_attrs(attrs: &mut Attributes) -> AstResult { + Ok(Self) + } + } +} diff --git a/crates/yuck/src/config/config.rs b/crates/yuck/src/config/config.rs new file mode 100644 index 0000000..71d84bf --- /dev/null +++ b/crates/yuck/src/config/config.rs @@ -0,0 +1,138 @@ +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; + +use codespan_reporting::files::SimpleFiles; +use simplexpr::SimplExpr; + +use super::{ + file_provider::{FilesError, YuckFiles}, + script_var_definition::ScriptVarDefinition, + var_definition::VarDefinition, + widget_definition::WidgetDefinition, + widget_use::WidgetUse, + window_definition::WindowDefinition, +}; +use crate::{ + config::script_var_definition::{ListenScriptVar, PollScriptVar}, + error::{AstError, AstResult, OptionAstErrorExt}, + parser::{ + ast::Ast, + ast_iterator::AstIterator, + from_ast::{FromAst, FromAstElementContent}, + }, +}; +use eww_shared_util::{AttrName, Span, VarName}; + +pub static TOP_LEVEL_DEFINITION_NAMES: &[&str] = &[ + WidgetDefinition::ELEMENT_NAME, + WindowDefinition::ELEMENT_NAME, + VarDefinition::ELEMENT_NAME, + ListenScriptVar::ELEMENT_NAME, + PollScriptVar::ELEMENT_NAME, + Include::ELEMENT_NAME, +]; + +#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize)] +pub struct Include { + pub path: String, + pub path_span: Span, +} + +impl FromAstElementContent for Include { + const ELEMENT_NAME: &'static str = "include"; + + fn from_tail>(span: Span, mut iter: AstIterator) -> AstResult { + let (path_span, path) = iter.expect_literal()?; + iter.expect_done()?; + Ok(Include { path: path.to_string(), path_span }) + } +} + +pub enum TopLevel { + Include(Include), + VarDefinition(VarDefinition), + ScriptVarDefinition(ScriptVarDefinition), + WidgetDefinition(WidgetDefinition), + WindowDefinition(WindowDefinition), +} + +impl FromAst for TopLevel { + fn from_ast(e: Ast) -> AstResult { + let span = e.span(); + let mut iter = e.try_ast_iter()?; + let (sym_span, element_name) = iter.expect_symbol()?; + Ok(match element_name.as_str() { + x if x == Include::ELEMENT_NAME => Self::Include(Include::from_tail(span, iter)?), + x if x == WidgetDefinition::ELEMENT_NAME => Self::WidgetDefinition(WidgetDefinition::from_tail(span, iter)?), + x if x == VarDefinition::ELEMENT_NAME => Self::VarDefinition(VarDefinition::from_tail(span, iter)?), + x if x == PollScriptVar::ELEMENT_NAME => { + Self::ScriptVarDefinition(ScriptVarDefinition::Poll(PollScriptVar::from_tail(span, iter)?)) + } + x if x == ListenScriptVar::ELEMENT_NAME => { + Self::ScriptVarDefinition(ScriptVarDefinition::Listen(ListenScriptVar::from_tail(span, iter)?)) + } + x if x == WindowDefinition::ELEMENT_NAME => Self::WindowDefinition(WindowDefinition::from_tail(span, iter)?), + x => return Err(AstError::UnknownToplevel(sym_span, x.to_string())), + }) + } +} + +#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize)] +pub struct Config { + pub widget_definitions: HashMap, + pub window_definitions: HashMap, + pub var_definitions: HashMap, + pub script_vars: HashMap, +} + +impl Config { + fn append_toplevel(&mut self, files: &mut YuckFiles, toplevel: TopLevel) -> AstResult<()> { + match toplevel { + TopLevel::VarDefinition(x) => { + self.var_definitions.insert(x.name.clone(), x); + } + TopLevel::ScriptVarDefinition(x) => { + self.script_vars.insert(x.name().clone(), x); + } + TopLevel::WidgetDefinition(x) => { + self.widget_definitions.insert(x.name.clone(), x); + } + TopLevel::WindowDefinition(x) => { + self.window_definitions.insert(x.name.clone(), x); + } + TopLevel::Include(include) => { + let (file_id, toplevels) = files.load_file(PathBuf::from(&include.path)).map_err(|err| match err { + FilesError::IoError(_) => AstError::IncludedFileNotFound(include), + FilesError::AstError(x) => x, + })?; + for element in toplevels { + self.append_toplevel(files, TopLevel::from_ast(element)?)?; + } + } + } + Ok(()) + } + + pub fn generate(files: &mut YuckFiles, elements: Vec) -> AstResult { + let mut config = Self { + widget_definitions: HashMap::new(), + window_definitions: HashMap::new(), + var_definitions: HashMap::new(), + script_vars: HashMap::new(), + }; + for element in elements { + config.append_toplevel(files, TopLevel::from_ast(element)?)?; + } + Ok(config) + } + + pub fn generate_from_main_file(files: &mut YuckFiles, path: impl AsRef) -> AstResult { + let (span, top_levels) = files.load_file(path.as_ref().to_path_buf()).map_err(|err| match err { + FilesError::IoError(err) => AstError::Other(Span::DUMMY, Box::new(err)), + FilesError::AstError(x) => x, + })?; + Self::generate(files, top_levels) + } +} diff --git a/crates/yuck/src/config/file_provider.rs b/crates/yuck/src/config/file_provider.rs new file mode 100644 index 0000000..2db1e68 --- /dev/null +++ b/crates/yuck/src/config/file_provider.rs @@ -0,0 +1,136 @@ +use std::{collections::HashMap, path::PathBuf}; + +use codespan_reporting::files::{Files, SimpleFile, SimpleFiles}; +use eww_shared_util::Span; + +use crate::{ + error::{AstError, AstResult}, + parser::ast::Ast, +}; + +#[derive(thiserror::Error, Debug)] +pub enum FilesError { + #[error(transparent)] + IoError(#[from] std::io::Error), + + #[error(transparent)] + AstError(#[from] AstError), +} + +#[derive(Clone, Debug)] +pub enum YuckSource { + File(std::path::PathBuf), + Literal(String), +} + +impl YuckSource { + pub fn read_content(&self) -> std::io::Result { + match self { + YuckSource::File(path) => Ok(std::fs::read_to_string(path)?), + YuckSource::Literal(x) => Ok(x.to_string()), + } + } +} + +#[derive(Clone, Debug)] +pub struct YuckFile { + name: String, + line_starts: Vec, + source: YuckSource, + source_len_bytes: usize, +} + +impl YuckFile { + /// Return the starting byte index of the line with the specified line index. + /// Convenience method that already generates errors if necessary. + fn line_start(&self, line_index: usize) -> Result { + use std::cmp::Ordering; + + match line_index.cmp(&self.line_starts.len()) { + Ordering::Less => Ok(self.line_starts.get(line_index).cloned().expect("failed despite previous check")), + Ordering::Equal => Ok(self.source_len_bytes), + Ordering::Greater => { + Err(codespan_reporting::files::Error::LineTooLarge { given: line_index, max: self.line_starts.len() - 1 }) + } + } + } +} + +#[derive(Debug, Clone)] +pub struct YuckFiles { + files: HashMap, + latest_id: usize, +} + +impl YuckFiles { + pub fn new() -> Self { + Self { files: HashMap::new(), latest_id: 0 } + } +} + +impl YuckFiles { + pub fn get_file(&self, id: usize) -> Result<&YuckFile, codespan_reporting::files::Error> { + self.files.get(&id).ok_or(codespan_reporting::files::Error::FileMissing) + } + + fn insert_file(&mut self, file: YuckFile) -> usize { + let file_id = self.latest_id; + self.files.insert(file_id, file); + self.latest_id += 1; + file_id + } + + pub fn load_file(&mut self, path: std::path::PathBuf) -> Result<(Span, Vec), FilesError> { + let file_content = std::fs::read_to_string(&path)?; + let line_starts = codespan_reporting::files::line_starts(&file_content).collect(); + let yuck_file = YuckFile { + name: path.display().to_string(), + line_starts, + source_len_bytes: file_content.len(), + source: YuckSource::File(path), + }; + let file_id = self.insert_file(yuck_file); + Ok(crate::parser::parse_toplevel(file_id, file_content)?) + } + + pub fn load_str(&mut self, name: String, content: String) -> Result<(Span, Vec), AstError> { + let line_starts = codespan_reporting::files::line_starts(&content).collect(); + let yuck_file = + YuckFile { name, line_starts, source_len_bytes: content.len(), source: YuckSource::Literal(content.to_string()) }; + let file_id = self.insert_file(yuck_file); + Ok(crate::parser::parse_toplevel(file_id, content)?) + } + + pub fn unload(&mut self, id: usize) { + self.files.remove(&id); + } +} + +impl<'a> Files<'a> for YuckFiles { + type FileId = usize; + type Name = &'a str; + type Source = String; + + fn name(&'a self, id: Self::FileId) -> Result { + Ok(&self.get_file(id)?.name) + } + + fn source(&'a self, id: Self::FileId) -> Result { + Ok(self.get_file(id)?.source.read_content().map_err(codespan_reporting::files::Error::Io)?) + } + + fn line_index(&self, id: Self::FileId, byte_index: usize) -> Result { + Ok(self.get_file(id)?.line_starts.binary_search(&byte_index).unwrap_or_else(|next_line| next_line - 1)) + } + + fn line_range( + &self, + id: Self::FileId, + line_index: usize, + ) -> Result, codespan_reporting::files::Error> { + let file = self.get_file(id)?; + let line_start = file.line_start(line_index)?; + let next_line_start = file.line_start(line_index + 1)?; + Ok(line_start..next_line_start) + } +} diff --git a/crates/yuck/src/config/mod.rs b/crates/yuck/src/config/mod.rs new file mode 100644 index 0000000..e532f62 --- /dev/null +++ b/crates/yuck/src/config/mod.rs @@ -0,0 +1,15 @@ +pub mod attributes; +pub mod backend_window_options; +pub mod config; +pub mod file_provider; +pub mod script_var_definition; +#[cfg(test)] +mod test; +pub mod validate; +pub mod var_definition; +pub mod widget_definition; +pub mod widget_use; +pub mod window_definition; +pub mod window_geometry; + +pub use config::*; diff --git a/crates/yuck/src/config/script_var_definition.rs b/crates/yuck/src/config/script_var_definition.rs new file mode 100644 index 0000000..79b4fb8 --- /dev/null +++ b/crates/yuck/src/config/script_var_definition.rs @@ -0,0 +1,106 @@ +use std::collections::HashMap; + +use simplexpr::{dynval::DynVal, SimplExpr}; + +use crate::{ + error::{AstError, AstResult, AstResultExt}, + parser::{ + ast::Ast, + ast_iterator::AstIterator, + from_ast::{FromAst, FromAstElementContent}, + }, +}; +use eww_shared_util::{AttrName, Span, Spanned, VarName}; + +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] +pub enum ScriptVarDefinition { + Poll(PollScriptVar), + Listen(ListenScriptVar), +} + +impl ScriptVarDefinition { + pub fn name_span(&self) -> Span { + match self { + ScriptVarDefinition::Poll(x) => x.name_span, + ScriptVarDefinition::Listen(x) => x.name_span, + } + } + + pub fn name(&self) -> &VarName { + match self { + ScriptVarDefinition::Poll(x) => &x.name, + ScriptVarDefinition::Listen(x) => &x.name, + } + } + + pub fn command_span(&self) -> Option { + match self { + ScriptVarDefinition::Poll(x) => match x.command { + VarSource::Shell(span, _) => Some(span), + VarSource::Function(_) => None, + }, + ScriptVarDefinition::Listen(x) => Some(x.command_span), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] +pub enum VarSource { + // TODO allow for other executors? (python, etc) + Shell(Span, String), + #[serde(skip)] + Function(fn() -> Result>), +} + +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] +pub struct PollScriptVar { + pub name: VarName, + pub command: VarSource, + pub interval: std::time::Duration, + pub name_span: Span, +} + +impl FromAstElementContent for PollScriptVar { + const ELEMENT_NAME: &'static str = "defpoll"; + + fn from_tail>(span: Span, mut iter: AstIterator) -> AstResult { + let result: AstResult<_> = try { + let (name_span, name) = iter.expect_symbol()?; + let mut attrs = iter.expect_key_values()?; + let interval = attrs.primitive_required::("interval")?.as_duration()?; + let timeout = attrs + .primitive_optional::("timeout")? + .map(|x| x.as_duration()) + .transpose()? + .unwrap_or_else(|| std::time::Duration::from_millis(200)); + let (script_span, script) = iter.expect_literal()?; + iter.expect_done()?; + Self { name_span, name: VarName(name), command: VarSource::Shell(script_span, script.to_string()), interval } + }; + result.note(r#"Expected format: `(defpoll name :interval "10s" "echo 'a shell script'")`"#) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] +pub struct ListenScriptVar { + pub name: VarName, + pub command: String, + pub initial_value: DynVal, + pub command_span: Span, + pub name_span: Span, +} +impl FromAstElementContent for ListenScriptVar { + const ELEMENT_NAME: &'static str = "deflisten"; + + fn from_tail>(span: Span, mut iter: AstIterator) -> AstResult { + let result: AstResult<_> = try { + let (name_span, name) = iter.expect_symbol()?; + let mut attrs = iter.expect_key_values()?; + let initial_value = attrs.primitive_optional("initial")?.unwrap_or_else(|| DynVal::from_string(String::new())); + let (command_span, script) = iter.expect_literal()?; + iter.expect_done()?; + Self { name_span, name: VarName(name), command: script.to_string(), initial_value, command_span } + }; + result.note(r#"Expected format: `(deflisten name :initial "0" "tail -f /tmp/example")`"#) + } +} diff --git a/crates/yuck/src/config/snapshots/eww_config__config__test__config.snap b/crates/yuck/src/config/snapshots/eww_config__config__test__config.snap new file mode 100644 index 0000000..4316d36 --- /dev/null +++ b/crates/yuck/src/config/snapshots/eww_config__config__test__config.snap @@ -0,0 +1,112 @@ +--- +source: src/config/test.rs +expression: config.unwrap() + +--- +Config( + widget_definitions: { + "bar": WidgetDefinition( + name: "bar", + expected_args: [ + AttrName("arg"), + AttrName("arg2"), + ], + widget: WidgetUse( + name: "text", + attrs: Attributes( + span: Span(99, 104, 0), + attrs: { + AttrName("text"): AttrEntry( + key_span: Span(99, 104, 0), + value: Literal(Span(99, 104, 0), DynVal("bla", None)), + ), + }, + ), + children: [], + span: Span(99, 104, 0), + ), + span: Span(61, 105, 0), + args_span: Span(76, 86, 0), + ), + "foo": WidgetDefinition( + name: "foo", + expected_args: [ + AttrName("arg"), + ], + widget: WidgetUse( + name: "text", + attrs: Attributes( + span: Span(44, 51, 0), + attrs: { + AttrName("text"): AttrEntry( + key_span: Span(44, 51, 0), + value: Literal(Span(44, 51, 0), DynVal("heyho", None)), + ), + }, + ), + children: [], + span: Span(44, 51, 0), + ), + span: Span(11, 52, 0), + args_span: Span(26, 31, 0), + ), + }, + window_definitions: { + "some-window": WindowDefinition( + name: "some-window", + geometry: Some(WindowGeometry( + anchor_point: AnchorPoint( + x: START, + y: START, + ), + offset: Coords( + x: Pixels(0), + y: Pixels(0), + ), + size: Coords( + x: Percent(12), + y: Pixels(20), + ), + )), + stacking: Foreground, + monitor_number: Some(12), + widget: WidgetUse( + name: "foo", + attrs: Attributes( + span: Span(509, 509, 513), + attrs: { + AttrName("arg"): AttrEntry( + key_span: Span(514, 518, 0), + value: Literal(Span(519, 524, 0), DynVal("bla", None)), + ), + }, + ), + children: [], + span: Span(509, 525, 0), + ), + resizable: true, + backend_options: X11WindowOptions( + wm_ignore: false, + sticky: true, + window_type: Dock, + struts: StrutDefinition( + side: Left, + dist: Pixels(30), + ), + ), + ), + }, + var_definitions: { + VarName("some_var"): VarDefinition( + name: VarName("some_var"), + initial_value: DynVal("bla", None), + span: Span(114, 137, 0), + ), + }, + script_vars: { + VarName("stuff"): Tail(TailScriptVar( + name: VarName("stuff"), + command: "tail -f stuff", + )), + }, +) diff --git a/crates/yuck/src/config/snapshots/yuck__config__test__config.snap b/crates/yuck/src/config/snapshots/yuck__config__test__config.snap new file mode 100644 index 0000000..0c4fe00 --- /dev/null +++ b/crates/yuck/src/config/snapshots/yuck__config__test__config.snap @@ -0,0 +1,94 @@ +--- +source: crates/yuck/src/config/test.rs +expression: config.unwrap() + +--- +Config( + widget_definitions: { + "bar": WidgetDefinition( + name: "bar", + expected_args: [ + AttrName("arg"), + AttrName("arg2"), + ], + widget: WidgetUse( + name: "foo", + attrs: Attributes( + span: Span(51, 61, 0), + attrs: { + AttrName("arg"): AttrEntry( + key_span: Span(52, 56, 0), + value: SimplExpr(Span(57, 61, 0), Literal(DynVal("hi", Span(57, 61, 0)))), + ), + }, + ), + children: [], + span: Span(47, 62, 0), + name_span: Span(48, 51, 0), + ), + span: Span(9, 63, 0), + args_span: Span(24, 34, 0), + ), + }, + window_definitions: { + "some-window": WindowDefinition( + name: "some-window", + geometry: Some(WindowGeometry( + anchor_point: AnchorPoint( + x: START, + y: START, + ), + offset: Coords( + x: Pixels(0), + y: Pixels(0), + ), + size: Coords( + x: Percent(12), + y: Pixels(20), + ), + )), + stacking: Foreground, + monitor_number: Some(12), + widget: WidgetUse( + name: "bar", + attrs: Attributes( + span: Span(467, 478, 0), + attrs: { + AttrName("arg"): AttrEntry( + key_span: Span(468, 472, 0), + value: SimplExpr(Span(473, 478, 0), Literal(DynVal("bla", Span(473, 478, 0)))), + ), + }, + ), + children: [], + span: Span(463, 479, 0), + name_span: Span(464, 467, 0), + ), + resizable: true, + backend_options: BackendWindowOptions( + wm_ignore: false, + sticky: true, + window_type: Dock, + struts: StrutDefinition( + side: Left, + dist: Pixels(30), + ), + ), + ), + }, + var_definitions: { + VarName("some_var"): VarDefinition( + name: VarName("some_var"), + initial_value: DynVal("bla", Span(89, 94, 0)), + span: Span(72, 95, 0), + ), + }, + script_vars: { + VarName("stuff"): Listen(ListenScriptVar( + name: VarName("stuff"), + command: "tail -f stuff", + command_span: Span(168, 183, 0), + name_span: Span(162, 167, 0), + )), + }, +) diff --git a/crates/yuck/src/config/test.rs b/crates/yuck/src/config/test.rs new file mode 100644 index 0000000..1af2a61 --- /dev/null +++ b/crates/yuck/src/config/test.rs @@ -0,0 +1,30 @@ +use crate::{ + config::config::Config, + parser::{self, ast::Ast, from_ast::FromAst, lexer::Lexer}, +}; + +use super::file_provider::YuckFiles; + +#[test] +fn test_config() { + let input = r#" + (defwidget bar [arg arg2] + (foo :arg "hi")) + (defvar some_var "bla") + (defpoll stuff :interval "12s" "date") + (deflisten stuff "tail -f stuff") + (defwindow some-window + :stacking "fg" + :monitor 12 + :resizable true + :geometry (geometry :width "12%" :height "20px") + :reserve (struts :side "left" :distance "30px") + (bar :arg "bla")) + "#; + let mut files = YuckFiles::new(); + let (span, asts) = files.load_str("config.yuck".to_string(), input.to_string()).unwrap(); + let config = Config::generate(&mut files, asts); + insta::with_settings!({sort_maps => true}, { + insta::assert_ron_snapshot!(config.unwrap()); + }); +} diff --git a/crates/yuck/src/config/validate.rs b/crates/yuck/src/config/validate.rs new file mode 100644 index 0000000..309b806 --- /dev/null +++ b/crates/yuck/src/config/validate.rs @@ -0,0 +1,105 @@ +use std::collections::{HashMap, HashSet}; + +use simplexpr::SimplExpr; + +use crate::{ + error::AstResult, + parser::{ast::Ast, ast_iterator::AstIterator, from_ast::FromAst}, +}; + +use super::{widget_definition::WidgetDefinition, widget_use::WidgetUse, Config}; +use eww_shared_util::{AttrName, Span, Spanned, VarName}; + +#[derive(Debug, thiserror::Error)] +pub enum ValidationError { + #[error("Unknown widget `{1}` referenced")] + UnknownWidget(Span, String), + + #[error("Missing attribute `{arg_name}` in use of widget `{widget_name}`")] + MissingAttr { widget_name: String, arg_name: AttrName, arg_list_span: Option, use_span: Span }, + + #[error("No variable named `{name}` in scope")] + UnknownVariable { + span: Span, + name: VarName, + /// True if the error occurred inside a widget definition, false if it occurred in a window definition + in_definition: bool, + }, +} + +impl Spanned for ValidationError { + fn span(&self) -> Span { + match self { + ValidationError::UnknownWidget(span, _) => *span, + ValidationError::MissingAttr { use_span, .. } => *use_span, + ValidationError::UnknownVariable { span, .. } => *span, + } + } +} + +pub fn validate(config: &Config, additional_globals: Vec) -> Result<(), ValidationError> { + let var_names = std::iter::empty() + .chain(additional_globals.iter().cloned()) + .chain(config.script_vars.keys().cloned()) + .chain(config.var_definitions.keys().cloned()) + .collect(); + for window in config.window_definitions.values() { + validate_variables_in_widget_use(&config.widget_definitions, &var_names, &window.widget, false)?; + } + for def in config.widget_definitions.values() { + validate_widget_definition(&config.widget_definitions, &var_names, &def)?; + } + Ok(()) +} + +pub fn validate_widget_definition( + other_defs: &HashMap, + globals: &HashSet, + def: &WidgetDefinition, +) -> Result<(), ValidationError> { + let mut variables_in_scope = globals.clone(); + for arg in def.expected_args.iter() { + variables_in_scope.insert(VarName(arg.0.to_string())); + } + + validate_variables_in_widget_use(other_defs, &variables_in_scope, &def.widget, true) +} + +pub fn validate_variables_in_widget_use( + defs: &HashMap, + variables: &HashSet, + widget: &WidgetUse, + is_in_definition: bool, +) -> Result<(), ValidationError> { + let matching_definition = defs.get(&widget.name); + if let Some(matching_def) = matching_definition { + let missing_arg = matching_def.expected_args.iter().find(|expected| !widget.attrs.attrs.contains_key(*expected)); + if let Some(missing_arg) = missing_arg { + return Err(ValidationError::MissingAttr { + widget_name: widget.name.clone(), + arg_name: missing_arg.clone(), + arg_list_span: Some(matching_def.args_span), + use_span: widget.attrs.span, + }); + } + } + + let values = widget.attrs.attrs.values(); + let unknown_var = values.filter_map(|value| value.value.as_simplexpr().ok()).find_map(|expr: SimplExpr| { + let span = expr.span(); + expr.var_refs() + .iter() + .cloned() + .map(|(span, var_ref)| (span, var_ref.clone())) + .find(|(_, var_ref)| !variables.contains(var_ref)) + }); + if let Some((span, var)) = unknown_var { + return Err(ValidationError::UnknownVariable { span, name: var.clone(), in_definition: is_in_definition }); + } + + for child in widget.children.iter() { + let _ = validate_variables_in_widget_use(defs, variables, child, is_in_definition)?; + } + + Ok(()) +} diff --git a/crates/yuck/src/config/var_definition.rs b/crates/yuck/src/config/var_definition.rs new file mode 100644 index 0000000..77f6927 --- /dev/null +++ b/crates/yuck/src/config/var_definition.rs @@ -0,0 +1,34 @@ +use std::collections::HashMap; + +use simplexpr::{dynval::DynVal, SimplExpr}; + +use crate::{ + error::{AstResult, AstResultExt}, + parser::{ + ast::Ast, + ast_iterator::AstIterator, + from_ast::{FromAst, FromAstElementContent}, + }, +}; +use eww_shared_util::{AttrName, Span, VarName}; + +#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize)] +pub struct VarDefinition { + pub name: VarName, + pub initial_value: DynVal, + pub span: Span, +} + +impl FromAstElementContent for VarDefinition { + const ELEMENT_NAME: &'static str = "defvar"; + + fn from_tail>(span: Span, mut iter: AstIterator) -> AstResult { + let result: AstResult<_> = try { + let (_, name) = iter.expect_symbol()?; + let (_, initial_value) = iter.expect_literal()?; + iter.expect_done()?; + Self { name: VarName(name), initial_value, span } + }; + result.note(r#"Expected format: `(defvar name "initial-value")`"#) + } +} diff --git a/crates/yuck/src/config/widget_definition.rs b/crates/yuck/src/config/widget_definition.rs new file mode 100644 index 0000000..84ccb6e --- /dev/null +++ b/crates/yuck/src/config/widget_definition.rs @@ -0,0 +1,41 @@ +use std::collections::HashMap; + +use simplexpr::SimplExpr; + +use crate::{ + error::{AstError::WrongExprType, AstResult, AstResultExt, FormFormatError}, + parser::{ + ast::Ast, + ast_iterator::AstIterator, + from_ast::{FromAst, FromAstElementContent}, + }, +}; +use eww_shared_util::{AttrName, Span, Spanned, VarName}; + +use super::widget_use::WidgetUse; +#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize)] +pub struct WidgetDefinition { + pub name: String, + pub expected_args: Vec, + pub widget: WidgetUse, + pub span: Span, + pub args_span: Span, +} + +impl FromAstElementContent for WidgetDefinition { + const ELEMENT_NAME: &'static str = "defwidget"; + + fn from_tail>(span: Span, mut iter: AstIterator) -> AstResult { + let (name_span, name) = iter.expect_symbol().note(EXPECTED_WIDGET_DEF_FORMAT)?; + let (args_span, expected_args) = iter + .expect_array() + .wrong_expr_type_to(|_, _| Some(FormFormatError::WidgetDefArglistMissing(name_span.point_span_at_end()))) + .note(EXPECTED_WIDGET_DEF_FORMAT)?; + let expected_args = expected_args.into_iter().map(|x| x.as_symbol().map(AttrName)).collect::>()?; + let widget = iter.expect_any().note(EXPECTED_WIDGET_DEF_FORMAT).and_then(WidgetUse::from_ast)?; + iter.expect_done().map_err(|e| FormFormatError::WidgetDefMultipleChildren(e.span()))?; + Ok(Self { name, expected_args, widget, span, args_span }) + } +} + +static EXPECTED_WIDGET_DEF_FORMAT: &str = r#"Expected format: `(defwidget name [] (contained-widgets))`"#; diff --git a/crates/yuck/src/config/widget_use.rs b/crates/yuck/src/config/widget_use.rs new file mode 100644 index 0000000..775d53f --- /dev/null +++ b/crates/yuck/src/config/widget_use.rs @@ -0,0 +1,64 @@ +use std::collections::HashMap; + +use simplexpr::SimplExpr; + +use crate::{ + config::attributes::AttrEntry, + error::{AstError, AstResult}, + parser::{ast::Ast, ast_iterator::AstIterator, from_ast::FromAst}, +}; +use eww_shared_util::{AttrName, Span, VarName}; + +use super::attributes::Attributes; + +#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize)] +pub struct WidgetUse { + pub name: String, + pub attrs: Attributes, + pub children: Vec, + pub span: Span, + pub name_span: Span, +} + +impl WidgetUse { + pub fn children_span(&self) -> Span { + if self.children.is_empty() { + self.span.point_span_at_end().shifted(-1) + } else { + self.children.first().unwrap().span.to(self.children.last().unwrap().span) + } + } +} + +impl FromAst for WidgetUse { + fn from_ast(e: Ast) -> AstResult { + let span = e.span(); + if let Ok(value) = e.clone().as_simplexpr() { + Ok(label_from_simplexpr(value, span)) + } else { + let mut iter = e.try_ast_iter()?; + let (name_span, name) = iter.expect_symbol()?; + let attrs = iter.expect_key_values()?; + let children = iter.map(WidgetUse::from_ast).collect::>>()?; + Ok(Self { name, attrs, children, span, name_span }) + } + } +} + +fn label_from_simplexpr(value: SimplExpr, span: Span) -> WidgetUse { + WidgetUse { + name: "label".to_string(), + name_span: span.point_span(), + attrs: Attributes::new( + span, + maplit::hashmap! { + AttrName("text".to_string()) => AttrEntry::new( + span, + Ast::SimplExpr(span.into(), value.clone()) + ) + }, + ), + children: Vec::new(), + span, + } +} diff --git a/crates/yuck/src/config/window_definition.rs b/crates/yuck/src/config/window_definition.rs new file mode 100644 index 0000000..06b3578 --- /dev/null +++ b/crates/yuck/src/config/window_definition.rs @@ -0,0 +1,100 @@ +use std::{collections::HashMap, fmt::Display, str::FromStr}; + +use simplexpr::{dynval::DynVal, SimplExpr}; + +use crate::{ + error::{AstError, AstResult}, + parser::{ + ast::Ast, + ast_iterator::AstIterator, + from_ast::{FromAst, FromAstElementContent}, + }, + value::NumWithUnit, +}; +use eww_shared_util::{AttrName, Span, VarName}; + +use super::{backend_window_options::BackendWindowOptions, widget_use::WidgetUse, window_geometry::WindowGeometry}; + +#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)] +pub struct WindowDefinition { + pub name: String, + pub geometry: Option, + pub stacking: WindowStacking, + pub monitor_number: Option, + pub widget: WidgetUse, + pub resizable: bool, + pub backend_options: BackendWindowOptions, +} + +impl FromAstElementContent for WindowDefinition { + const ELEMENT_NAME: &'static str = "defwindow"; + + fn from_tail>(span: Span, mut iter: AstIterator) -> AstResult { + let (_, name) = iter.expect_symbol()?; + let mut attrs = iter.expect_key_values()?; + let monitor_number = attrs.primitive_optional("monitor")?; + let resizable = attrs.primitive_optional("resizable")?.unwrap_or(true); + let stacking = attrs.primitive_optional("stacking")?.unwrap_or(WindowStacking::Foreground); + let geometry = attrs.ast_optional("geometry")?; + let backend_options = BackendWindowOptions::from_attrs(&mut attrs)?; + let widget = iter.expect_any().and_then(WidgetUse::from_ast)?; + iter.expect_done()?; + Ok(Self { name, monitor_number, resizable, widget, stacking, geometry, backend_options }) + } +} + +#[derive(Debug, thiserror::Error)] +pub struct EnumParseError { + pub input: String, + pub expected: Vec<&'static str>, +} +impl Display for EnumParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Failed to parse `{}`, must be one of {}", self.input, self.expected.join(", ")) + } +} + +/// Parse a string with a concrete set of options into some data-structure, +/// and return an [EnumParseError] +/// ```rs +/// let input = "up"; +/// enum_parse { "direction", input, +/// "up" => Direction::Up, +/// "down" => Direction::Down, +/// } +/// ``` +#[macro_export] +macro_rules! enum_parse { + ($name:literal, $input:expr, $($($s:literal)|* => $val:expr),* $(,)?) => { + let input = $input.to_lowercase(); + match input.as_str() { + $( $( $s )|* => Ok($val) ),*, + _ => Err(EnumParseError { + input: input, + expected: vec![$($($s),*),*], + }) + } + }; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, derive_more::Display, smart_default::SmartDefault, serde::Serialize)] +pub enum WindowStacking { + #[default] + Foreground, + Background, + Bottom, + Overlay, +} + +impl std::str::FromStr for WindowStacking { + type Err = EnumParseError; + + fn from_str(s: &str) -> Result { + enum_parse! { "WindowStacking", s, + "foreground" | "fg" => WindowStacking::Foreground, + "background" | "bg" => WindowStacking::Background, + "bottom" | "bt" => WindowStacking::Bottom, + "overlay" | "ov" => WindowStacking::Overlay, + } + } +} diff --git a/crates/yuck/src/config/window_geometry.rs b/crates/yuck/src/config/window_geometry.rs new file mode 100644 index 0000000..2d06eb4 --- /dev/null +++ b/crates/yuck/src/config/window_geometry.rs @@ -0,0 +1,153 @@ +use std::collections::HashMap; + +use simplexpr::{dynval::DynVal, SimplExpr}; + +use crate::{ + enum_parse, + error::{AstError, AstResult}, + parser::{ + ast::Ast, + ast_iterator::AstIterator, + from_ast::{FromAst, FromAstElementContent}, + }, + value::Coords, +}; + +use super::{widget_use::WidgetUse, window_definition::EnumParseError}; +use eww_shared_util::{AttrName, Span, VarName}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, Eq, PartialEq, smart_default::SmartDefault, Serialize, Deserialize, strum::Display)] +pub enum AnchorAlignment { + #[strum(serialize = "start")] + #[default] + START, + #[strum(serialize = "center")] + CENTER, + #[strum(serialize = "end")] + END, +} + +impl AnchorAlignment { + pub fn from_x_alignment(s: &str) -> Result { + enum_parse! { "x-alignment", s, + "l" | "left" => AnchorAlignment::START, + "c" | "center" => AnchorAlignment::CENTER, + "r" | "right" => AnchorAlignment::END, + } + } + + pub fn from_y_alignment(s: &str) -> Result { + enum_parse! { "y-alignment", s, + "t" | "top" => AnchorAlignment::START, + "c" | "center" => AnchorAlignment::CENTER, + "b" | "bottom" => AnchorAlignment::END, + } + } + + pub fn alignment_to_coordinate(&self, size_inner: i32, size_container: i32) -> i32 { + match self { + AnchorAlignment::START => 0, + AnchorAlignment::CENTER => (size_container / 2) - (size_inner / 2), + AnchorAlignment::END => size_container - size_inner, + } + } +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Default, Serialize, Deserialize)] +pub struct AnchorPoint { + pub x: AnchorAlignment, + pub y: AnchorAlignment, +} + +impl std::fmt::Display for AnchorPoint { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use AnchorAlignment::*; + match (self.x, self.y) { + (CENTER, CENTER) => write!(f, "center"), + (x, y) => write!( + f, + "{} {}", + match x { + START => "left", + CENTER => "center", + END => "right", + }, + match y { + START => "top", + CENTER => "center", + END => "bottom", + } + ), + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum AnchorPointParseError { + #[error("Could not parse anchor: Must either be \"center\" or be formatted like \"top left\"")] + WrongFormat(String), + #[error(transparent)] + EnumParseError(#[from] EnumParseError), +} + +impl std::str::FromStr for AnchorPoint { + type Err = AnchorPointParseError; + + fn from_str(s: &str) -> Result { + if s == "center" { + Ok(AnchorPoint { x: AnchorAlignment::CENTER, y: AnchorAlignment::CENTER }) + } else { + let (first, second) = s.split_once(' ').ok_or_else(|| AnchorPointParseError::WrongFormat(s.to_string()))?; + let x_y_result: Result<_, EnumParseError> = try { + AnchorPoint { x: AnchorAlignment::from_x_alignment(first)?, y: AnchorAlignment::from_y_alignment(second)? } + }; + x_y_result.or_else(|_| { + Ok(AnchorPoint { x: AnchorAlignment::from_x_alignment(second)?, y: AnchorAlignment::from_y_alignment(first)? }) + }) + } + } +} + +#[derive(Default, Debug, Clone, Copy, Eq, PartialEq, Serialize)] +pub struct WindowGeometry { + pub anchor_point: AnchorPoint, + pub offset: Coords, + pub size: Coords, +} + +impl FromAstElementContent for WindowGeometry { + const ELEMENT_NAME: &'static str = "geometry"; + + fn from_tail>(span: Span, mut iter: AstIterator) -> AstResult { + let mut attrs = iter.expect_key_values()?; + iter.expect_done().map_err(|e| e.note("Check if you are missing a colon in front of a key"))?; + Ok(WindowGeometry { + anchor_point: attrs.primitive_optional("anchor")?.unwrap_or_default(), + size: Coords { + x: attrs.primitive_optional("width")?.unwrap_or_default(), + y: attrs.primitive_optional("height")?.unwrap_or_default(), + }, + offset: Coords { + x: attrs.primitive_optional("x")?.unwrap_or_default(), + y: attrs.primitive_optional("y")?.unwrap_or_default(), + }, + }) + } +} + +impl WindowGeometry { + pub fn override_if_given(&self, anchor_point: Option, offset: Option, size: Option) -> Self { + WindowGeometry { + anchor_point: anchor_point.unwrap_or(self.anchor_point), + offset: offset.unwrap_or(self.offset), + size: size.unwrap_or(self.size), + } + } +} + +impl std::fmt::Display for WindowGeometry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}-{} ({})", self.offset, self.size, self.anchor_point) + } +} diff --git a/crates/yuck/src/error.rs b/crates/yuck/src/error.rs new file mode 100644 index 0000000..c5f1980 --- /dev/null +++ b/crates/yuck/src/error.rs @@ -0,0 +1,182 @@ +use crate::{ + config::{attributes::AttrError, config::Include, validate::ValidationError}, + format_diagnostic::ToDiagnostic, + parser::{ + ast::{Ast, AstType}, + lexer, parse_error, + }, +}; +use codespan_reporting::{diagnostic, files}; +use eww_shared_util::{AttrName, Span, Spanned, VarName}; +use simplexpr::dynval; +use thiserror::Error; + +pub type AstResult = Result; + +#[derive(Debug, Error)] +pub enum AstError { + #[error("Unknown toplevel declaration `{1}`")] + UnknownToplevel(Span, String), + #[error("Expected another element, but got nothing")] + MissingNode(Span), + #[error("Too many elements, must be exactly {1}")] + TooManyNodes(Span, i32), + #[error("Did not expect any further elements here. Make sure your format is correct")] + NoMoreElementsExpected(Span), + + #[error(transparent)] + FormFormatError(#[from] FormFormatError), + + #[error("Wrong type of expression: Expected {1} but got {2}")] + WrongExprType(Span, AstType, AstType), + #[error("Expected to get a value, but got {1}")] + NotAValue(Span, AstType), + #[error("Expected element {1}, but read {2}")] + MismatchedElementName(Span, String, String), + + #[error("Keyword `{1}` is missing a value")] + DanglingKeyword(Span, String), + + #[error("Included file not found {}", .0.path)] + IncludedFileNotFound(Include), + + #[error("{}", .main_err.to_message())] + ErrorContext { label_span: Span, context: String, main_err: Box }, + #[error("{1}")] + ErrorNote(String, #[source] Box), + + #[error(transparent)] + SimplExpr(#[from] simplexpr::error::Error), + + #[error(transparent)] + ConversionError(#[from] dynval::ConversionError), + + #[error("{1}")] + Other(Span, Box), + + #[error(transparent)] + AttrError(#[from] AttrError), + + #[error(transparent)] + ValidationError(#[from] ValidationError), + + #[error("Parse error: {source}")] + ParseError { file_id: usize, source: lalrpop_util::ParseError }, +} + +static_assertions::assert_impl_all!(AstError: Send, Sync); +static_assertions::assert_impl_all!(dynval::ConversionError: Send, Sync); +static_assertions::assert_impl_all!(lalrpop_util::ParseError < usize, lexer::Token, parse_error::ParseError>: Send, Sync); + +impl AstError { + pub fn note(self, note: &str) -> Self { + AstError::ErrorNote(note.to_string(), Box::new(self)) + } + + pub fn context_label(self, label_span: Span, context: &str) -> Self { + AstError::ErrorContext { label_span, context: context.to_string(), main_err: Box::new(self) } + } + + pub fn from_parse_error( + file_id: usize, + err: lalrpop_util::ParseError, + ) -> AstError { + AstError::ParseError { file_id, source: err } + } + + pub fn wrong_expr_type_to>(self, f: impl FnOnce(Span, AstType) -> Option) -> AstError { + match self { + AstError::WrongExprType(span, expected, got) => { + f(span.point_span(), got).map(|x| x.into()).unwrap_or_else(|| AstError::WrongExprType(span, expected, got)) + } + AstError::ErrorNote(s, err) => AstError::ErrorNote(s, Box::new(err.wrong_expr_type_to(f))), + other => other, + } + } +} + +impl Spanned for AstError { + fn span(&self) -> Span { + match self { + AstError::UnknownToplevel(span, _) => *span, + AstError::MissingNode(span) => *span, + AstError::WrongExprType(span, ..) => *span, + AstError::NotAValue(span, ..) => *span, + AstError::MismatchedElementName(span, ..) => *span, + AstError::DanglingKeyword(span, _) => *span, + AstError::AttrError(err) => err.span(), + AstError::Other(span, ..) => *span, + AstError::ConversionError(err) => err.value.span(), + AstError::IncludedFileNotFound(include) => include.path_span, + AstError::TooManyNodes(span, ..) => *span, + AstError::ErrorContext { label_span, .. } => *label_span, + AstError::ValidationError(error) => error.span(), + AstError::ParseError { file_id, source } => get_parse_error_span(*file_id, source), + AstError::ErrorNote(_, err) => err.span(), + AstError::NoMoreElementsExpected(span) => *span, + AstError::SimplExpr(err) => err.span(), + AstError::FormFormatError(err) => err.span(), + } + } +} + +pub fn get_parse_error_span(file_id: usize, err: &lalrpop_util::ParseError) -> Span { + use lalrpop_util::ParseError::*; + match err { + InvalidToken { location } => Span(*location, *location, file_id), + UnrecognizedEOF { location, expected } => Span(*location, *location, file_id), + UnrecognizedToken { token, expected } => Span(token.0, token.2, file_id), + ExtraToken { token } => Span(token.0, token.2, file_id), + User { error } => error.span(), + } +} + +pub trait OptionAstErrorExt { + fn or_missing(self, span: Span) -> Result; +} +impl OptionAstErrorExt for Option { + fn or_missing(self, span: Span) -> Result { + self.ok_or(AstError::MissingNode(span)) + } +} + +pub trait AstResultExt { + fn context_label(self, label_span: Span, context: &str) -> AstResult; + fn note(self, note: &str) -> AstResult; + + /// Map any [AstError::WrongExprType]s error to any other Into (such as a [FormFormatError]) + /// If the provided closure returns `None`, the error will be kept unmodified + fn wrong_expr_type_to>(self, f: impl FnOnce(Span, AstType) -> Option) -> AstResult; +} + +impl AstResultExt for AstResult { + fn context_label(self, label_span: Span, context: &str) -> AstResult { + self.map_err(|e| AstError::ErrorContext { label_span, context: context.to_string(), main_err: Box::new(e) }) + } + + fn note(self, note: &str) -> AstResult { + self.map_err(|e| e.note(note)) + } + + fn wrong_expr_type_to>(self, f: impl FnOnce(Span, AstType) -> Option) -> AstResult { + self.map_err(|err| err.wrong_expr_type_to(f)) + } +} + +#[derive(Debug, Error)] +pub enum FormFormatError { + #[error("Widget definition missing argument list")] + WidgetDefArglistMissing(Span), + + #[error("Widget definition has more than one child widget")] + WidgetDefMultipleChildren(Span), +} + +impl Spanned for FormFormatError { + fn span(&self) -> Span { + match self { + FormFormatError::WidgetDefArglistMissing(span) => *span, + FormFormatError::WidgetDefMultipleChildren(span) => *span, + } + } +} diff --git a/crates/yuck/src/format_diagnostic.rs b/crates/yuck/src/format_diagnostic.rs new file mode 100644 index 0000000..d3b0ae4 --- /dev/null +++ b/crates/yuck/src/format_diagnostic.rs @@ -0,0 +1,293 @@ +use codespan_reporting::{diagnostic, files}; +use config::TOP_LEVEL_DEFINITION_NAMES; +use itertools::Itertools; +use simplexpr::dynval; + +use diagnostic::*; + +use crate::{ + config::{attributes::AttrError, config, validate::ValidationError}, + error::{get_parse_error_span, AstError, FormFormatError}, +}; + +use super::parser::parse_error; +use eww_shared_util::{AttrName, Span, Spanned, VarName}; + +fn span_to_primary_label(span: Span) -> Label { + Label::primary(span.2, span.0..span.1) +} +fn span_to_secondary_label(span: Span) -> Label { + Label::secondary(span.2, span.0..span.1) +} + +#[macro_export] +macro_rules! gen_diagnostic { + ( $(kind = $kind:expr,)? + $(msg = $msg:expr)? + $(, label = $span:expr $(=> $label:expr)?)? + $(, note = $note:expr)? $(,)? + ) => { + ::codespan_reporting::diagnostic::Diagnostic::new(gen_diagnostic! { + @macro_fallback $({$kind})? {::codespan_reporting::diagnostic::Severity::Error} + }) + $(.with_message($msg.to_string()))? + $(.with_labels(vec![ + ::codespan_reporting::diagnostic::Label::primary($span.2, $span.0..$span.1) + $(.with_message($label))? + ]))? + $(.with_notes(vec![$note.to_string()]))? + }; + ($msg:expr $(, $span:expr $(,)?)?) => {{ + ::codespan_reporting::diagnostic::Diagnostic::error() + .with_message($msg.to_string()) + $(.with_labels(vec![::codespan_reporting::diagnostic::Label::primary($span.2, $span.0..$span.1)]))? + }}; + + + (@macro_fallback { $value:expr } { $fallback:expr }) => { + $value + }; + (@macro_fallback { $fallback:expr }) => { + $fallback + }; +} + +pub trait DiagnosticExt: Sized { + fn with_label(self, label: Label) -> Self; +} + +impl DiagnosticExt for Diagnostic { + fn with_label(self, label: Label) -> Self { + self.with_labels(vec![label]) + } +} + +pub trait ToDiagnostic: std::fmt::Debug { + fn to_diagnostic(&self) -> Diagnostic; + fn to_message(&self) -> String { + self.to_diagnostic().message + } +} + +impl ToDiagnostic for Diagnostic { + fn to_diagnostic(&self) -> Diagnostic { + self.clone() + } +} +impl ToDiagnostic for AstError { + fn to_diagnostic(&self) -> Diagnostic { + match self { + AstError::UnknownToplevel(span, name) => gen_diagnostic! { + msg = self, + label = span, + note = format!("Must be one of: {}", TOP_LEVEL_DEFINITION_NAMES.iter().join(", ")) + }, + AstError::MissingNode(span) => gen_diagnostic! { + msg = "Expected another element", + label = span => "Expected another element here", + }, + AstError::WrongExprType(span, expected, actual) => gen_diagnostic! { + msg = "Wrong type of expression", + label = span => format!("Expected a `{}` here", expected), + note = format!("Expected: {}\n Got: {}", expected, actual), + }, + AstError::NotAValue(span, actual) => gen_diagnostic! { + msg = format!("Expected value, but got `{}`", actual), + label = span => "Expected some value here", + note = format!("Got: {}", actual), + }, + + AstError::ParseError { file_id, source } => lalrpop_error_to_diagnostic(source, *file_id), + AstError::MismatchedElementName(span, expected, got) => gen_diagnostic! { + msg = format!("Expected element `{}`, but found `{}`", expected, got), + label = span => format!("Expected `{}` here", expected), + note = format!("Expected: {}\n Got: {}", expected, got), + }, + AstError::ErrorContext { label_span, context, main_err } => { + main_err.to_diagnostic().with_label(span_to_secondary_label(*label_span).with_message(context)) + } + + AstError::ConversionError(source) => source.to_diagnostic(), + AstError::Other(span, source) => gen_diagnostic!(source, span), + AstError::AttrError(source) => source.to_diagnostic(), + AstError::IncludedFileNotFound(include) => gen_diagnostic!( + msg = format!("Included file `{}` not found", include.path), + label = include.path_span => "Included here", + ), + + AstError::TooManyNodes(extra_nodes_span, expected) => gen_diagnostic! { + msg = self, + label = extra_nodes_span => "these elements must not be here", + note = "Consider wrapping the elements in some container element", + }, + AstError::DanglingKeyword(span, keyword) => gen_diagnostic! { + msg = self, + label = span => "No value provided for this", + }, + AstError::ErrorNote(note, source) => source.to_diagnostic().with_notes(vec![note.to_string()]), + AstError::ValidationError(source) => source.to_diagnostic(), + AstError::NoMoreElementsExpected(span) => gen_diagnostic!(self, span), + AstError::SimplExpr(source) => source.to_diagnostic(), + AstError::FormFormatError(error) => error.to_diagnostic(), + } + } +} + +impl ToDiagnostic for parse_error::ParseError { + fn to_diagnostic(&self) -> Diagnostic { + match self { + parse_error::ParseError::SimplExpr(error) => error.to_diagnostic(), + parse_error::ParseError::LexicalError(span) => generate_lexical_error_diagnostic(*span), + } + } +} + +impl ToDiagnostic for AttrError { + fn to_diagnostic(&self) -> Diagnostic { + match self { + AttrError::MissingRequiredAttr(span, attr_name) => { + gen_diagnostic!(format!("Missing attribute `{}`", attr_name), span) + } + AttrError::EvaluationError(span, source) => source.to_diagnostic(), + AttrError::Other(span, source) => gen_diagnostic!(source, span), + } + } +} + +impl ToDiagnostic for ValidationError { + fn to_diagnostic(&self) -> Diagnostic { + match self { + ValidationError::UnknownWidget(span, name) => gen_diagnostic! { + msg = self, + label = span => "Used here", + }, + ValidationError::MissingAttr { widget_name, arg_name, arg_list_span, use_span } => { + let mut diag = Diagnostic::error() + .with_message(self.to_string()) + .with_label(span_to_secondary_label(*use_span).with_message("Argument missing here")) + .with_notes(vec![format!( + "Hint: pass the attribute like so: `({} :{} your-value ...`", + widget_name, arg_name + )]); + if let Some(arg_list_span) = arg_list_span { + diag = diag.with_label(span_to_secondary_label(*arg_list_span).with_message("But is required here")); + } + diag + } + ValidationError::UnknownVariable { span, name, in_definition } => { + let diag = gen_diagnostic! { + msg = self, + label = span => "Used here", + note = if *in_definition { + "Hint: Either define it as a global variable, or add it to the argument-list of your `defwidget` and pass it as an argument" + } else { + "Hint: Define it as a global variable" + } + }; + diag.with_notes(vec![format!( + "Hint: If you meant to use the literal value \"{}\", surround the value in quotes", + name + )]) + } + } + } +} + +fn lalrpop_error_to_diagnostic( + error: &lalrpop_util::ParseError, + file_id: usize, +) -> Diagnostic { + use lalrpop_util::ParseError::*; + match error { + InvalidToken { location } => gen_diagnostic!("Invalid token", Span::point(*location, file_id)), + UnrecognizedEOF { location, expected } => gen_diagnostic! { + msg = "Input ended unexpectedly. Check if you have any unclosed delimiters", + label = Span::point(*location, file_id), + }, + UnrecognizedToken { token, expected } => gen_diagnostic! { + msg = format!("Unexpected token `{}` encountered", token.1), + label = Span(token.0, token.2, file_id) => "Token unexpected", + }, + ExtraToken { token } => gen_diagnostic!(format!("Extra token encountered: `{}`", token.1)), + User { error } => error.to_diagnostic(), + } +} + +impl ToDiagnostic for simplexpr::error::Error { + fn to_diagnostic(&self) -> Diagnostic { + use simplexpr::error::Error::*; + match self { + ParseError { source, file_id } => lalrpop_error_to_diagnostic(source, *file_id), + ConversionError(error) => error.to_diagnostic(), + Eval(error) => error.to_diagnostic(), + Other(error) => gen_diagnostic!(error), + Spanned(span, error) => error.to_diagnostic().with_label(span_to_primary_label(*span)), + } + } +} + +impl ToDiagnostic for simplexpr::parser::lexer::LexicalError { + fn to_diagnostic(&self) -> Diagnostic { + generate_lexical_error_diagnostic(self.span()) + } +} + +impl ToDiagnostic for simplexpr::eval::EvalError { + fn to_diagnostic(&self) -> Diagnostic { + use simplexpr::eval::EvalError::*; + match self { + NoVariablesAllowed(name) => gen_diagnostic!(self), + UnknownVariable(name, similar) => { + let mut notes = Vec::new(); + if similar.len() == 1 { + notes.push(format!("Did you mean `{}`?", similar.first().unwrap())) + } else if similar.len() > 1 { + notes.push(format!("Did you mean one of: {}?", similar.iter().map(|x| format!("`{}`", x)).join(", "))) + } + // TODO the note here is confusing when it's an unknown variable being used _within_ a string literal / simplexpr + // it only really makes sense on top-level symbols + notes.push(format!("Hint: If you meant to use the literal value \"{}\", surround the value in quotes", name)); + gen_diagnostic!(self).with_notes(notes) + } + Spanned(span, error) => error.as_ref().to_diagnostic().with_label(span_to_primary_label(*span)), + _ => gen_diagnostic!(self, self.span()), + } + } +} + +impl ToDiagnostic for dynval::ConversionError { + fn to_diagnostic(&self) -> Diagnostic { + let diag = gen_diagnostic! { + msg = self, + label = self.value.span() => format!("`{}` is not of type `{}`", self.value, self.target_type), + }; + diag.with_notes(self.source.as_ref().map(|x| vec![x.to_string()]).unwrap_or_default()) + } +} + +fn generate_lexical_error_diagnostic(span: Span) -> Diagnostic { + gen_diagnostic! { + msg = "Invalid token", + label = span => "Invalid token" + } +} + +impl ToDiagnostic for FormFormatError { + fn to_diagnostic(&self) -> diagnostic::Diagnostic { + match self { + FormFormatError::WidgetDefArglistMissing(span) => gen_diagnostic! { + msg = self, + label = span => "Insert the argument list (e.g.: `[]`) here", + note = "This list will in the future need to declare all the non-global variables / attributes used in this widget.\n\ + This is not yet neccessary, but is still considered good style.", + }, + FormFormatError::WidgetDefMultipleChildren(span) => gen_diagnostic! { + msg = self, + label = span => "Found more than one child element here.", + note = "A widget-definition may only contain one child element.\n\ + To include multiple elements, wrap these elements in a single container widget such as `box`.\n\ + This is necessary as eww can't know how you want these elements to be layed out otherwise." + }, + } + } +} diff --git a/crates/yuck/src/lib.rs b/crates/yuck/src/lib.rs new file mode 100644 index 0000000..687c8f1 --- /dev/null +++ b/crates/yuck/src/lib.rs @@ -0,0 +1,9 @@ +#![allow(unused_imports)] +#![allow(unused)] +#![feature(try_blocks)] + +pub mod config; +pub mod error; +pub mod format_diagnostic; +pub mod parser; +pub mod value; diff --git a/crates/yuck/src/parser/ast.rs b/crates/yuck/src/parser/ast.rs new file mode 100644 index 0000000..59ad02a --- /dev/null +++ b/crates/yuck/src/parser/ast.rs @@ -0,0 +1,129 @@ +use itertools::Itertools; +use simplexpr::{ast::SimplExpr, dynval::DynVal}; +use std::collections::HashMap; + +use eww_shared_util::{Span, VarName}; +use std::fmt::Display; + +use super::{ast_iterator::AstIterator, from_ast::FromAst}; +use crate::{ + config::attributes::{AttrEntry, Attributes}, + error::{AstError, AstResult, OptionAstErrorExt}, +}; + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub enum AstType { + List, + Array, + Keyword, + Symbol, + // TODO this does no longer correspond to an actual literal ast type as that's replaced with SimplExpr + Literal, + SimplExpr, + Comment, + /// A value that could be used as a [SimplExpr] + IntoPrimitive, +} + +impl Display for AstType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AstType::IntoPrimitive => write!(f, "primitive"), + _ => write!(f, "{:?}", self), + } + } +} + +#[derive(PartialEq, Eq, Clone, serde::Serialize)] +pub enum Ast { + List(Span, Vec), + Array(Span, Vec), + Keyword(Span, String), + Symbol(Span, String), + SimplExpr(Span, SimplExpr), + Comment(Span), +} + +macro_rules! as_func { + ($exprtype:expr, $name:ident $nameref:ident < $t:ty > = $p:pat => $value:expr) => { + pub fn $name(self) -> Result<$t, AstError> { + match self { + $p => Ok($value), + x => Err(AstError::WrongExprType(x.span(), $exprtype, x.expr_type())), + } + } + + pub fn $nameref(&self) -> Result<&$t, AstError> { + match self { + $p => Ok($value), + x => Err(AstError::WrongExprType(x.span(), $exprtype, x.expr_type())), + } + } + }; +} + +impl Ast { + as_func!(AstType::Symbol, as_symbol as_symbol_ref = Ast::Symbol(_, x) => x); + + as_func!(AstType::Keyword, as_keyword as_keyword_ref = Ast::Keyword(_, x) => x); + + as_func!(AstType::List, as_list as_list_ref> = Ast::List(_, x) => x); + + pub fn expr_type(&self) -> AstType { + match self { + Ast::List(..) => AstType::List, + Ast::Array(..) => AstType::Array, + Ast::Keyword(..) => AstType::Keyword, + Ast::Symbol(..) => AstType::Symbol, + Ast::SimplExpr(..) => AstType::SimplExpr, + Ast::Comment(_) => AstType::Comment, + } + } + + pub fn span(&self) -> Span { + match self { + Ast::List(span, _) => *span, + Ast::Array(span, _) => *span, + Ast::Keyword(span, _) => *span, + Ast::Symbol(span, _) => *span, + Ast::SimplExpr(span, _) => *span, + Ast::Comment(span) => *span, + } + } + + pub fn as_simplexpr(&self) -> AstResult { + match self { + // TODO do I do this? + // Ast::Array(span, elements) => todo!() + Ast::Symbol(span, x) => Ok(SimplExpr::VarRef(*span, VarName(x.clone()))), + Ast::SimplExpr(span, x) => Ok(x.clone()), + _ => Err(AstError::WrongExprType(self.span(), AstType::IntoPrimitive, self.expr_type())), + } + } + + pub fn try_ast_iter(self) -> AstResult>> { + let span = self.span(); + let list = self.as_list()?; + Ok(AstIterator::new(span, list.into_iter())) + } +} + +impl std::fmt::Display for Ast { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use Ast::*; + match self { + List(_, x) => write!(f, "({})", x.iter().map(|e| format!("{}", e)).join(" ")), + Array(_, x) => write!(f, "({})", x.iter().map(|e| format!("{}", e)).join(" ")), + Keyword(_, x) => write!(f, ":{}", x), + Symbol(_, x) => write!(f, "{}", x), + SimplExpr(_, simplexpr::SimplExpr::Literal(value)) => write!(f, "\"{}\"", value), + SimplExpr(_, x) => write!(f, "{{{}}}", x), + Comment(_) => write!(f, ""), + } + } +} +impl std::fmt::Debug for Ast { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self) + } +} diff --git a/crates/yuck/src/parser/ast_iterator.rs b/crates/yuck/src/parser/ast_iterator.rs new file mode 100644 index 0000000..cee69dc --- /dev/null +++ b/crates/yuck/src/parser/ast_iterator.rs @@ -0,0 +1,131 @@ +use itertools::Itertools; +use simplexpr::{ast::SimplExpr, dynval::DynVal}; +use std::collections::HashMap; + +use std::fmt::Display; + +use super::{ + ast::{Ast, AstType}, + from_ast::FromAst, +}; +use crate::{ + config::attributes::{AttrEntry, Attributes}, + error::{AstError, AstResult, OptionAstErrorExt}, +}; +use eww_shared_util::{AttrName, Span, VarName}; + +pub struct AstIterator> { + remaining_span: Span, + iter: itertools::PutBack, +} + +macro_rules! return_or_put_back { + ($(fn $name:ident -> $expr_type:expr, $t:ty = $p:pat => $ret:expr)*) => { + $( + pub fn $name(&mut self) -> AstResult<$t> { + let expr_type = $expr_type; + match self.expect_any()? { + $p => Ok($ret), + other => { + let span = other.span(); + let actual_type = other.expr_type(); + self.put_back(other); + Err(AstError::WrongExprType(span, expr_type, actual_type)) + } + } + } + )* + }; +} + +impl> AstIterator { + return_or_put_back! { + fn expect_symbol -> AstType::Symbol, (Span, String) = Ast::Symbol(span, x) => (span, x) + fn expect_list -> AstType::List, (Span, Vec) = Ast::List(span, x) => (span, x) + fn expect_array -> AstType::Array, (Span, Vec) = Ast::Array(span, x) => (span, x) + } + + pub fn expect_literal(&mut self) -> AstResult<(Span, DynVal)> { + // TODO add some others + match self.expect_any()? { + // Ast::Array(_, _) => todo!(), + Ast::SimplExpr(span, expr) => Ok((span, expr.eval_no_vars().map_err(|e| AstError::SimplExpr(e.into()))?)), + other => { + let span = other.span(); + let actual_type = other.expr_type(); + self.put_back(other); + Err(AstError::WrongExprType(span, AstType::Literal, actual_type)) + } + } + } + + pub fn new(span: Span, iter: I) -> Self { + AstIterator { remaining_span: span, iter: itertools::put_back(iter) } + } + + pub fn expect_any(&mut self) -> AstResult { + self.next().or_missing(self.remaining_span.point_span()) + } + + pub fn expect_done(&mut self) -> AstResult<()> { + if let Some(next) = self.next() { + self.put_back(next); + Err(AstError::NoMoreElementsExpected(self.remaining_span)) + } else { + Ok(()) + } + } + + pub fn expect_key_values(&mut self) -> AstResult { + parse_key_values(self, true) + } + + pub fn put_back(&mut self, ast: Ast) { + self.remaining_span.0 = ast.span().0; + self.iter.put_back(ast) + } +} + +impl> Iterator for AstIterator { + type Item = Ast; + + fn next(&mut self) -> Option { + self.iter.next().map(|x| { + self.remaining_span.0 = x.span().1; + x + }) + } +} + +/// Parse consecutive `:keyword value` pairs from an expression iterator into an [Attributes]. +fn parse_key_values(iter: &mut AstIterator>, fail_on_dangling_kw: bool) -> AstResult { + let mut data = HashMap::new(); + let mut attrs_span = iter.remaining_span.point_span(); + loop { + match iter.next() { + Some(Ast::Keyword(key_span, kw)) => match iter.next() { + Some(value) => { + attrs_span.1 = iter.remaining_span.0; + let attr_value = AttrEntry { key_span, value }; + data.insert(AttrName(kw), attr_value); + } + None => { + if fail_on_dangling_kw { + return Err(AstError::DanglingKeyword(key_span, kw)); + } else { + iter.iter.put_back(Ast::Keyword(key_span, kw)); + attrs_span.1 = iter.remaining_span.0; + return Ok(Attributes::new(attrs_span, data)); + } + } + }, + next => { + if let Some(expr) = next { + iter.iter.put_back(expr); + } + attrs_span.1 = iter.remaining_span.0; + return Ok(Attributes::new(attrs_span, data)); + } + } + } +} diff --git a/crates/yuck/src/parser/from_ast.rs b/crates/yuck/src/parser/from_ast.rs new file mode 100644 index 0000000..09541ce --- /dev/null +++ b/crates/yuck/src/parser/from_ast.rs @@ -0,0 +1,58 @@ +use super::{ + ast::{Ast, AstType}, + ast_iterator::AstIterator, +}; +use crate::{error::*, parser}; +use eww_shared_util::{AttrName, Span, VarName}; +use itertools::Itertools; +use simplexpr::{ast::SimplExpr, dynval::DynVal}; +use std::{ + collections::{HashMap, LinkedList}, + iter::FromIterator, + str::FromStr, +}; + +pub trait FromAst: Sized { + fn from_ast(e: Ast) -> AstResult; +} + +impl FromAst for Ast { + fn from_ast(e: Ast) -> AstResult { + Ok(e) + } +} + +impl FromAst for String { + fn from_ast(e: Ast) -> AstResult { + Ok(e.as_simplexpr()?.eval_no_vars().map_err(simplexpr::error::Error::Eval)?.to_string()) + } +} + +/// A trait that allows creating a type from the tail of a list-node. +/// I.e. to parse (foo [a b] (c d)), [from_tail] would just get [a b] (c d). +pub trait FromAstElementContent: Sized { + const ELEMENT_NAME: &'static str; + fn from_tail>(span: Span, iter: AstIterator) -> AstResult; +} + +impl FromAst for T { + fn from_ast(e: Ast) -> AstResult { + let span = e.span(); + let mut iter = e.try_ast_iter()?; + let (element_name_span, element_name) = iter.expect_symbol()?; + if Self::ELEMENT_NAME != element_name { + return Err(AstError::MismatchedElementName(element_name_span, Self::ELEMENT_NAME.to_string(), element_name)); + } + Self::from_tail(span, iter) + } +} + +impl FromAst for SimplExpr { + fn from_ast(e: Ast) -> AstResult { + match e { + Ast::Symbol(span, x) => Ok(SimplExpr::VarRef(span.into(), VarName(x))), + Ast::SimplExpr(span, x) => Ok(x), + _ => Err(AstError::NotAValue(e.span(), e.expr_type())), + } + } +} diff --git a/crates/yuck/src/parser/lexer.rs b/crates/yuck/src/parser/lexer.rs new file mode 100644 index 0000000..8068c7f --- /dev/null +++ b/crates/yuck/src/parser/lexer.rs @@ -0,0 +1,201 @@ +use once_cell::sync::Lazy; +use regex::{escape, Regex, RegexSet}; +use simplexpr::parser::lexer::{STR_INTERPOLATION_END, STR_INTERPOLATION_START}; + +use super::parse_error; +use eww_shared_util::{AttrName, Span, Spanned, VarName}; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum Token { + LPren, + RPren, + LBrack, + RBrack, + True, + False, + NumLit(String), + Symbol(String), + Keyword(String), + SimplExpr(Vec<(usize, simplexpr::parser::lexer::Token, usize)>), + Comment, + Skip, +} + +impl std::fmt::Display for Token { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Token::LPren => write!(f, "'('"), + Token::RPren => write!(f, "')'"), + Token::LBrack => write!(f, "'['"), + Token::RBrack => write!(f, "']'"), + Token::True => write!(f, "true"), + Token::False => write!(f, "false"), + Token::NumLit(x) => write!(f, "{}", x), + Token::Symbol(x) => write!(f, "{}", x), + Token::Keyword(x) => write!(f, "{}", x), + Token::SimplExpr(x) => write!(f, "{{{:?}}}", x.iter().map(|x| &x.1)), + Token::Comment => write!(f, ""), + Token::Skip => write!(f, ""), + } + } +} + +macro_rules! regex_rules { + ($( $regex:expr => $token:expr),*) => { + static LEXER_REGEX_SET: Lazy = Lazy::new(|| { RegexSet::new(&[ + $(format!("^{}", $regex)),* + ]).unwrap()}); + static LEXER_REGEXES: Lazy> = Lazy::new(|| { vec![ + $(Regex::new(&format!("^{}", $regex)).unwrap()),* + ]}); + static LEXER_FNS: Lazy Token + Sync + Send>>> = Lazy::new(|| { vec![ + $(Box::new($token)),* + ]}); + } +} + +static ESCAPE_REPLACE_REGEX: Lazy = Lazy::new(|| Regex::new(r"\\(.)").unwrap()); + +regex_rules! { + escape("(") => |_| Token::LPren, + escape(")") => |_| Token::RPren, + escape("[") => |_| Token::LBrack, + escape("]") => |_| Token::RBrack, + escape("true") => |_| Token::True, + escape("false") => |_| Token::False, + r#"[+-]?(?:[0-9]+[.])?[0-9]+"# => |x| Token::NumLit(x), + r#":[^\s\)\]}]+"# => |x| Token::Keyword(x), + r#"[a-zA-Z_!\?<>/\.\*-\+\-][^\s{}\(\)\[\](){}]*"# => |x| Token::Symbol(x), + r#";.*"# => |_| Token::Comment, + r"[ \t\n\f]+" => |_| Token::Skip +} + +pub struct Lexer { + source: String, + file_id: usize, + failed: bool, + pos: usize, +} + +impl Lexer { + pub fn new(file_id: usize, source: String) -> Self { + Lexer { source, file_id, failed: false, pos: 0 } + } + + fn string_lit(&mut self) -> Option> { + let mut simplexpr_lexer = simplexpr::parser::lexer::Lexer::new(self.file_id, self.pos, &self.source[self.pos..]); + match simplexpr_lexer.string_lit() { + Some(Ok((lo, segments, hi))) => { + self.pos = hi; + self.advance_until_char_boundary(); + Some(Ok((lo, Token::SimplExpr(vec![(lo, simplexpr::parser::lexer::Token::StringLit(segments), hi)]), hi))) + } + Some(Err(e)) => Some(Err(parse_error::ParseError::LexicalError(e.0))), + None => None, + } + } + + fn simplexpr(&mut self) -> Option> { + self.pos += 1; + let mut simplexpr_lexer = simplexpr::parser::lexer::Lexer::new(self.file_id, self.pos, &self.source[self.pos..]); + let mut toks = Vec::new(); + let mut end = self.pos; + loop { + match simplexpr_lexer.next_token() { + Some(Ok((lo, tok, hi))) => { + end = hi; + toks.push((lo, tok, hi)); + } + Some(Err(err)) => { + if simplexpr_lexer.continues_with('}') { + let start = toks.first().map(|x| x.0).unwrap_or(end); + self.pos = end + 1; + self.advance_until_char_boundary(); + return Some(Ok((start, Token::SimplExpr(toks), end))); + } else { + return Some(Err(parse_error::ParseError::LexicalError(err.span()))); + } + } + None => return None, + } + } + } + + fn advance_until_char_boundary(&mut self) { + while self.pos < self.source.len() && !self.source.is_char_boundary(self.pos) { + self.pos += 1; + } + } +} + +impl Iterator for Lexer { + type Item = Result<(usize, Token, usize), parse_error::ParseError>; + + fn next(&mut self) -> Option { + loop { + if self.failed || self.pos >= self.source.len() { + return None; + } + let remaining = &self.source[self.pos..]; + if remaining.starts_with(&['"', '\'', '`'][..]) { + return self.string_lit(); + } else if remaining.starts_with('{') { + return self.simplexpr(); + } else { + let match_set = LEXER_REGEX_SET.matches(remaining); + let matched_token = match_set + .into_iter() + .map(|i: usize| { + let m = LEXER_REGEXES[i].find(remaining).unwrap(); + (m.end(), i) + }) + .min_by_key(|(_, x)| *x); + + let (len, i) = match matched_token { + Some(x) => x, + None => { + self.failed = true; + return Some(Err(parse_error::ParseError::LexicalError(Span(self.pos, self.pos, self.file_id)))); + } + }; + + let tok_str = &self.source[self.pos..self.pos + len]; + let old_pos = self.pos; + self.pos += len; + match LEXER_FNS[i](tok_str.to_string()) { + Token::Skip | Token::Comment => {} + token => { + return Some(Ok((old_pos, token, self.pos))); + } + } + } + } + } +} + +#[cfg(test)] +mod test { + + use super::*; + use eww_shared_util::snapshot_string; + use itertools::Itertools; + + macro_rules! v { + ($x:literal) => { + Lexer::new(0, 0, $x) + .map(|x| match x { + Ok((l, x, r)) => format!("({}, {:?}, {})", l, x, r), + Err(err) => format!("{}", err), + }) + .join("\n") + }; + } + + snapshot_string! { + basic => r#"(foo + - "text" )"#, + escaped_strings => r#"{ bla "} \" }" " \" "}"#, + escaped_quote => r#""< \" >""#, + char_boundary => r#"{ " " + music}"#, + quotes_in_quotes => r#"{ " } ' }" }"#, + } +} diff --git a/crates/yuck/src/parser/mod.rs b/crates/yuck/src/parser/mod.rs new file mode 100644 index 0000000..62c7291 --- /dev/null +++ b/crates/yuck/src/parser/mod.rs @@ -0,0 +1,80 @@ +use eww_shared_util::Span; +use lalrpop_util::lalrpop_mod; + +use super::error::{AstError, AstResult}; +use ast::Ast; + +use std::{fmt::Display, ops::Deref}; + +use itertools::Itertools; + +pub mod ast; +pub mod ast_iterator; +pub mod from_ast; +pub(crate) mod lexer; +pub(crate) mod parse_error; + +lalrpop_mod!( + #[allow(clippy::all)] + pub parser, + "/parser/parser.rs" +); + +pub fn parse_string(file_id: usize, s: &str) -> AstResult { + let lexer = lexer::Lexer::new(file_id, s.to_string()); + let parser = parser::AstParser::new(); + parser.parse(file_id, lexer).map_err(|e| AstError::from_parse_error(file_id, e)) +} + +/// Parse multiple toplevel nodes into a list of [Ast] +pub fn parse_toplevel(file_id: usize, s: String) -> AstResult<(Span, Vec)> { + let lexer = lexer::Lexer::new(file_id, s); + let parser = parser::ToplevelParser::new(); + parser.parse(file_id, lexer).map_err(|e| AstError::from_parse_error(file_id, e)) +} + +/// get a single ast node from a list of asts, returning an Err if the length is not exactly 1. +pub fn require_single_toplevel(span: Span, mut asts: Vec) -> AstResult { + match asts.len() { + 0 => Err(AstError::MissingNode(span)), + 1 => Ok(asts.remove(0)), + _ => Err(AstError::TooManyNodes(asts.get(1).unwrap().span().to(asts.last().unwrap().span()), 1)), + } +} + +macro_rules! test_parser { + ($($text:literal),*) => {{ + let p = parser::AstParser::new(); + use lexer::Lexer; + + ::insta::with_settings!({sort_maps => true}, { + $( + ::insta::assert_debug_snapshot!(p.parse(0, Lexer::new(0, $text.to_string()))); + )* + }); + }} +} + +#[test] +fn test() { + test_parser!( + "1", + "(12)", + "1.2", + "-1.2", + "(1 2)", + "(1 :foo 1)", + "(:foo 1)", + "(:foo->: 1)", + "(foo 1)", + "(lol😄 1)", + r#"(test "hi")"#, + r#"(test "h\"i")"#, + r#"(test " hi ")"#, + "(+ (1 2 (* 2 5)))", + r#"foo ; test"#, + r#"(f arg ; test + arg2)"#, + "\"h\\\"i\"" + ); +} diff --git a/crates/yuck/src/parser/parse_error.rs b/crates/yuck/src/parser/parse_error.rs new file mode 100644 index 0000000..3c57d0a --- /dev/null +++ b/crates/yuck/src/parser/parse_error.rs @@ -0,0 +1,18 @@ +use eww_shared_util::{AttrName, Span, Spanned, VarName}; + +#[derive(Debug, thiserror::Error)] +pub enum ParseError { + #[error("{0}")] + SimplExpr(simplexpr::error::Error), + #[error("Unknown token")] + LexicalError(Span), +} + +impl Spanned for ParseError { + fn span(&self) -> Span { + match self { + ParseError::SimplExpr(err) => err.span(), + ParseError::LexicalError(span) => *span, + } + } +} diff --git a/crates/yuck/src/parser/parser.lalrpop b/crates/yuck/src/parser/parser.lalrpop new file mode 100644 index 0000000..34740b4 --- /dev/null +++ b/crates/yuck/src/parser/parser.lalrpop @@ -0,0 +1,69 @@ +use std::str::FromStr; +use crate::parser::{lexer::Token, ast::Ast, parse_error}; +use eww_shared_util::Span; +use simplexpr::ast::SimplExpr; +use simplexpr; +use lalrpop_util::ParseError; + +grammar(file_id: usize); + +extern { + type Location = usize; + type Error = parse_error::ParseError; + + enum Token { + "(" => Token::LPren, + ")" => Token::RPren, + "[" => Token::LBrack, + "]" => Token::RBrack, + "true" => Token::True, + "false" => Token::False, + "number" => Token::NumLit(), + "symbol" => Token::Symbol(), + "keyword" => Token::Keyword(), + "simplexpr" => Token::SimplExpr(>), + "comment" => Token::Comment, + } +} + +pub Toplevel: (Span, Vec) = { + )*> => (Span(l, r, file_id), elems) +} + +pub Ast: Ast = { + "(" )*> ")" => Ast::List(Span(l, r, file_id), elems), + "[" )*> "]" => Ast::Array(Span(l, r, file_id), elems), + => Ast::SimplExpr(Span(l, r, file_id), expr), + => x, + => x, + => Ast::SimplExpr(Span(l, r, file_id), SimplExpr::literal(Span(l, r, file_id), x.into())), + "comment" => Ast::Comment(Span(l, r, file_id)), +}; + +Keyword: Ast = => Ast::Keyword(Span(l, r, file_id), x[1..].to_string()); +Symbol: Ast = => Ast::Symbol(Span(l, r, file_id), x.to_string()); + +Literal: String = { + => <>, + => <>, +}; + +SimplExpr: SimplExpr = { + =>? { + let parser = simplexpr::simplexpr_parser::ExprParser::new(); + parser.parse(file_id, x.into_iter().map(Ok)) + .map_err(|e| ParseError::User { + error: parse_error::ParseError::SimplExpr(simplexpr::error::Error::from_parse_error(file_id, e)) + }) + } +} + + +Num: String = <"number"> => <>.to_string(); +Bool: String = { + "true" => "true".to_string(), + "false" => "false".to_string(), +} + + +// vim:shiftwidth=4 diff --git a/crates/yuck/src/parser/snapshots/eww_config__parser__element__test__test.snap b/crates/yuck/src/parser/snapshots/eww_config__parser__element__test__test.snap new file mode 100644 index 0000000..f38b397 --- /dev/null +++ b/crates/yuck/src/parser/snapshots/eww_config__parser__element__test__test.snap @@ -0,0 +1,17 @@ +--- +source: src/parser/element.rs +expression: "Element::::from_ast(parser.parse(0, lexer).unwrap()).unwrap()" + +--- +Element { + name: "box", + attrs: { + "baz": "hi", + "bar": "12", + }, + children: [ + foo, + (bar), + ], + span: 0..33, +} diff --git a/crates/yuck/src/parser/snapshots/eww_config__parser__test-10.snap b/crates/yuck/src/parser/snapshots/eww_config__parser__test-10.snap new file mode 100644 index 0000000..86604ce --- /dev/null +++ b/crates/yuck/src/parser/snapshots/eww_config__parser__test-10.snap @@ -0,0 +1,8 @@ +--- +source: src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, \"(lol😄 1)\".to_string()))" + +--- +Ok( + (lol😄 "1"), +) diff --git a/crates/yuck/src/parser/snapshots/eww_config__parser__test-11.snap b/crates/yuck/src/parser/snapshots/eww_config__parser__test-11.snap new file mode 100644 index 0000000..ac7da83 --- /dev/null +++ b/crates/yuck/src/parser/snapshots/eww_config__parser__test-11.snap @@ -0,0 +1,8 @@ +--- +source: src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, r#\"(test \"hi\")\"#.to_string()))" + +--- +Ok( + (test "hi"), +) diff --git a/crates/yuck/src/parser/snapshots/eww_config__parser__test-12.snap b/crates/yuck/src/parser/snapshots/eww_config__parser__test-12.snap new file mode 100644 index 0000000..1906a01 --- /dev/null +++ b/crates/yuck/src/parser/snapshots/eww_config__parser__test-12.snap @@ -0,0 +1,8 @@ +--- +source: src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, r#\"(test \"h\\\"i\")\"#.to_string()))" + +--- +Ok( + (test "h\"i"), +) diff --git a/crates/yuck/src/parser/snapshots/eww_config__parser__test-13.snap b/crates/yuck/src/parser/snapshots/eww_config__parser__test-13.snap new file mode 100644 index 0000000..53ceea1 --- /dev/null +++ b/crates/yuck/src/parser/snapshots/eww_config__parser__test-13.snap @@ -0,0 +1,8 @@ +--- +source: src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, r#\"(test \" hi \")\"#.to_string()))" + +--- +Ok( + (test " hi "), +) diff --git a/crates/yuck/src/parser/snapshots/eww_config__parser__test-14.snap b/crates/yuck/src/parser/snapshots/eww_config__parser__test-14.snap new file mode 100644 index 0000000..de774e7 --- /dev/null +++ b/crates/yuck/src/parser/snapshots/eww_config__parser__test-14.snap @@ -0,0 +1,8 @@ +--- +source: src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, \"(+ (1 2 (* 2 5)))\".to_string()))" + +--- +Ok( + (+ ("1" "2" (* "2" "5"))), +) diff --git a/crates/yuck/src/parser/snapshots/eww_config__parser__test-15.snap b/crates/yuck/src/parser/snapshots/eww_config__parser__test-15.snap new file mode 100644 index 0000000..929bfab --- /dev/null +++ b/crates/yuck/src/parser/snapshots/eww_config__parser__test-15.snap @@ -0,0 +1,8 @@ +--- +source: src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, r#\"foo ; test\"#.to_string()))" + +--- +Ok( + foo, +) diff --git a/crates/yuck/src/parser/snapshots/eww_config__parser__test-16.snap b/crates/yuck/src/parser/snapshots/eww_config__parser__test-16.snap new file mode 100644 index 0000000..bd07751 --- /dev/null +++ b/crates/yuck/src/parser/snapshots/eww_config__parser__test-16.snap @@ -0,0 +1,8 @@ +--- +source: src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, r#\"(f arg ; test\n arg2)\"#.to_string()))" + +--- +Ok( + (f arg arg2), +) diff --git a/crates/yuck/src/parser/snapshots/eww_config__parser__test-17.snap b/crates/yuck/src/parser/snapshots/eww_config__parser__test-17.snap new file mode 100644 index 0000000..23356d7 --- /dev/null +++ b/crates/yuck/src/parser/snapshots/eww_config__parser__test-17.snap @@ -0,0 +1,8 @@ +--- +source: src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, \"\\\"h\\\\\\\"i\\\"\".to_string()))" + +--- +Ok( + "h\"i", +) diff --git a/crates/yuck/src/parser/snapshots/eww_config__parser__test-2.snap b/crates/yuck/src/parser/snapshots/eww_config__parser__test-2.snap new file mode 100644 index 0000000..43fb4a4 --- /dev/null +++ b/crates/yuck/src/parser/snapshots/eww_config__parser__test-2.snap @@ -0,0 +1,8 @@ +--- +source: src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, \"(12)\".to_string()))" + +--- +Ok( + ("12"), +) diff --git a/crates/yuck/src/parser/snapshots/eww_config__parser__test-3.snap b/crates/yuck/src/parser/snapshots/eww_config__parser__test-3.snap new file mode 100644 index 0000000..fb2160d --- /dev/null +++ b/crates/yuck/src/parser/snapshots/eww_config__parser__test-3.snap @@ -0,0 +1,8 @@ +--- +source: src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, \"1.2\".to_string()))" + +--- +Ok( + "1.2", +) diff --git a/crates/yuck/src/parser/snapshots/eww_config__parser__test-4.snap b/crates/yuck/src/parser/snapshots/eww_config__parser__test-4.snap new file mode 100644 index 0000000..464f39b --- /dev/null +++ b/crates/yuck/src/parser/snapshots/eww_config__parser__test-4.snap @@ -0,0 +1,8 @@ +--- +source: src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, \"-1.2\".to_string()))" + +--- +Ok( + "-1.2", +) diff --git a/crates/yuck/src/parser/snapshots/eww_config__parser__test-5.snap b/crates/yuck/src/parser/snapshots/eww_config__parser__test-5.snap new file mode 100644 index 0000000..d1331e7 --- /dev/null +++ b/crates/yuck/src/parser/snapshots/eww_config__parser__test-5.snap @@ -0,0 +1,8 @@ +--- +source: src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, \"(1 2)\".to_string()))" + +--- +Ok( + ("1" "2"), +) diff --git a/crates/yuck/src/parser/snapshots/eww_config__parser__test-6.snap b/crates/yuck/src/parser/snapshots/eww_config__parser__test-6.snap new file mode 100644 index 0000000..31b00ed --- /dev/null +++ b/crates/yuck/src/parser/snapshots/eww_config__parser__test-6.snap @@ -0,0 +1,8 @@ +--- +source: src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, \"(1 :foo 1)\".to_string()))" + +--- +Ok( + ("1" :foo "1"), +) diff --git a/crates/yuck/src/parser/snapshots/eww_config__parser__test-7.snap b/crates/yuck/src/parser/snapshots/eww_config__parser__test-7.snap new file mode 100644 index 0000000..e0454d1 --- /dev/null +++ b/crates/yuck/src/parser/snapshots/eww_config__parser__test-7.snap @@ -0,0 +1,8 @@ +--- +source: src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, \"(:foo 1)\".to_string()))" + +--- +Ok( + (:foo "1"), +) diff --git a/crates/yuck/src/parser/snapshots/eww_config__parser__test-8.snap b/crates/yuck/src/parser/snapshots/eww_config__parser__test-8.snap new file mode 100644 index 0000000..9025a0f --- /dev/null +++ b/crates/yuck/src/parser/snapshots/eww_config__parser__test-8.snap @@ -0,0 +1,8 @@ +--- +source: src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, \"(:foo->: 1)\".to_string()))" + +--- +Ok( + (:foo->: "1"), +) diff --git a/crates/yuck/src/parser/snapshots/eww_config__parser__test-9.snap b/crates/yuck/src/parser/snapshots/eww_config__parser__test-9.snap new file mode 100644 index 0000000..503bb62 --- /dev/null +++ b/crates/yuck/src/parser/snapshots/eww_config__parser__test-9.snap @@ -0,0 +1,8 @@ +--- +source: src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, \"(foo 1)\".to_string()))" + +--- +Ok( + (foo "1"), +) diff --git a/crates/yuck/src/parser/snapshots/eww_config__parser__test.snap b/crates/yuck/src/parser/snapshots/eww_config__parser__test.snap new file mode 100644 index 0000000..befaabc --- /dev/null +++ b/crates/yuck/src/parser/snapshots/eww_config__parser__test.snap @@ -0,0 +1,8 @@ +--- +source: src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, \"1\".to_string()))" + +--- +Ok( + "1", +) diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__basic.snap b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__basic.snap new file mode 100644 index 0000000..e8d564a --- /dev/null +++ b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__basic.snap @@ -0,0 +1,6 @@ +--- +source: crates/yuck/src/parser/lexer.rs +expression: "r#\"(foo + - \"text\" )\"#" + +--- +(foo + - "text" ) diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__char_boundary.snap b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__char_boundary.snap new file mode 100644 index 0000000..708318b --- /dev/null +++ b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__char_boundary.snap @@ -0,0 +1,6 @@ +--- +source: crates/yuck/src/parser/lexer.rs +expression: "r#\"{ \" \" + music}\"#" + +--- +{ " " + music} diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__escaped_quote.snap b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__escaped_quote.snap new file mode 100644 index 0000000..9ab4524 --- /dev/null +++ b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__escaped_quote.snap @@ -0,0 +1,6 @@ +--- +source: crates/yuck/src/parser/lexer.rs +expression: "r#\"\"< \\\" >\"\"#" + +--- +"< \" >" diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__escaped_strings.snap b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__escaped_strings.snap new file mode 100644 index 0000000..eb6ab16 --- /dev/null +++ b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__escaped_strings.snap @@ -0,0 +1,6 @@ +--- +source: crates/yuck/src/parser/lexer.rs +expression: "r#\"{ bla \"} \\\" }\" \" \\\" \"}\"#" + +--- +{ bla "} \" }" " \" "} diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__quotes_in_quotes.snap b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__quotes_in_quotes.snap new file mode 100644 index 0000000..26ead15 --- /dev/null +++ b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__quotes_in_quotes.snap @@ -0,0 +1,6 @@ +--- +source: crates/yuck/src/parser/lexer.rs +expression: "r#\"{ \" } ' }\" }\"#" + +--- +{ " } ' }" } diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__lexer__yuck_lexer-2.snap b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__yuck_lexer-2.snap new file mode 100644 index 0000000..846e42a --- /dev/null +++ b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__yuck_lexer-2.snap @@ -0,0 +1,54 @@ +--- +source: crates/yuck/src/parser/lexer.rs +expression: "Lexer::new(0, r#\"{ bla \"} \\\" }\" \" \\\" \"}\"#.to_string()).collect_vec()" + +--- +[ + Ok( + ( + 2, + SimplExpr( + [ + ( + 2, + Ident( + "bla", + ), + 5, + ), + ( + 6, + StringLit( + [ + ( + 6, + Literal( + "} \" }", + ), + 14, + ), + ], + ), + 14, + ), + ( + 15, + StringLit( + [ + ( + 15, + Literal( + " \" ", + ), + 21, + ), + ], + ), + 21, + ), + ], + ), + 21, + ), + ), +] diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__lexer__yuck_lexer-3.snap b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__yuck_lexer-3.snap new file mode 100644 index 0000000..cda3a96 --- /dev/null +++ b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__yuck_lexer-3.snap @@ -0,0 +1,32 @@ +--- +source: crates/yuck/src/parser/lexer.rs +expression: "Lexer::new(0, r#\"\"< \\\" >\"\"#.to_string()).collect_vec()" + +--- +[ + Ok( + ( + 0, + SimplExpr( + [ + ( + 0, + StringLit( + [ + ( + 0, + Literal( + "< \" >", + ), + 8, + ), + ], + ), + 8, + ), + ], + ), + 8, + ), + ), +] diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__lexer__yuck_lexer-4.snap b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__yuck_lexer-4.snap new file mode 100644 index 0000000..462cc2a --- /dev/null +++ b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__yuck_lexer-4.snap @@ -0,0 +1,44 @@ +--- +source: crates/yuck/src/parser/lexer.rs +expression: "Lexer::new(0, r#\"{ \" \" + music}\"#.to_string()).collect_vec()" + +--- +[ + Ok( + ( + 2, + SimplExpr( + [ + ( + 2, + StringLit( + [ + ( + 2, + Literal( + "\u{f001} ", + ), + 10, + ), + ], + ), + 10, + ), + ( + 11, + Plus, + 12, + ), + ( + 13, + Ident( + "music", + ), + 18, + ), + ], + ), + 18, + ), + ), +] diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__lexer__yuck_lexer.snap b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__yuck_lexer.snap new file mode 100644 index 0000000..813a740 --- /dev/null +++ b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__yuck_lexer.snap @@ -0,0 +1,73 @@ +--- +source: crates/yuck/src/parser/lexer.rs +expression: "Lexer::new(0, r#\"(foo + - \"text\" )\"#.to_string()).collect_vec()" + +--- +[ + Ok( + ( + 0, + LPren, + 1, + ), + ), + Ok( + ( + 1, + Symbol( + "foo", + ), + 4, + ), + ), + Ok( + ( + 5, + Symbol( + "+", + ), + 6, + ), + ), + Ok( + ( + 7, + Symbol( + "-", + ), + 8, + ), + ), + Ok( + ( + 9, + SimplExpr( + [ + ( + 9, + StringLit( + [ + ( + 9, + Literal( + "text", + ), + 15, + ), + ], + ), + 15, + ), + ], + ), + 15, + ), + ), + Ok( + ( + 16, + RPren, + 17, + ), + ), +] diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__test-10.snap b/crates/yuck/src/parser/snapshots/yuck__parser__test-10.snap new file mode 100644 index 0000000..d57421a --- /dev/null +++ b/crates/yuck/src/parser/snapshots/yuck__parser__test-10.snap @@ -0,0 +1,8 @@ +--- +source: crates/yuck/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, \"(lol😄 1)\".to_string()))" + +--- +Ok( + (lol😄 "1"), +) diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__test-11.snap b/crates/yuck/src/parser/snapshots/yuck__parser__test-11.snap new file mode 100644 index 0000000..4f80a03 --- /dev/null +++ b/crates/yuck/src/parser/snapshots/yuck__parser__test-11.snap @@ -0,0 +1,8 @@ +--- +source: crates/yuck/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, r#\"(test \"hi\")\"#.to_string()))" + +--- +Ok( + (test "hi"), +) diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__test-12.snap b/crates/yuck/src/parser/snapshots/yuck__parser__test-12.snap new file mode 100644 index 0000000..3fcc8ea --- /dev/null +++ b/crates/yuck/src/parser/snapshots/yuck__parser__test-12.snap @@ -0,0 +1,8 @@ +--- +source: crates/yuck/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, r#\"(test \"h\\\"i\")\"#.to_string()))" + +--- +Ok( + (test "h"i"), +) diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__test-13.snap b/crates/yuck/src/parser/snapshots/yuck__parser__test-13.snap new file mode 100644 index 0000000..8d4d533 --- /dev/null +++ b/crates/yuck/src/parser/snapshots/yuck__parser__test-13.snap @@ -0,0 +1,8 @@ +--- +source: crates/yuck/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, r#\"(test \" hi \")\"#.to_string()))" + +--- +Ok( + (test " hi "), +) diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__test-14.snap b/crates/yuck/src/parser/snapshots/yuck__parser__test-14.snap new file mode 100644 index 0000000..0975a96 --- /dev/null +++ b/crates/yuck/src/parser/snapshots/yuck__parser__test-14.snap @@ -0,0 +1,8 @@ +--- +source: crates/yuck/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, \"(+ (1 2 (* 2 5)))\".to_string()))" + +--- +Ok( + (+ ("1" "2" (* "2" "5"))), +) diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__test-15.snap b/crates/yuck/src/parser/snapshots/yuck__parser__test-15.snap new file mode 100644 index 0000000..fbc9f49 --- /dev/null +++ b/crates/yuck/src/parser/snapshots/yuck__parser__test-15.snap @@ -0,0 +1,8 @@ +--- +source: crates/yuck/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, r#\"foo ; test\"#.to_string()))" + +--- +Ok( + foo, +) diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__test-16.snap b/crates/yuck/src/parser/snapshots/yuck__parser__test-16.snap new file mode 100644 index 0000000..f226f40 --- /dev/null +++ b/crates/yuck/src/parser/snapshots/yuck__parser__test-16.snap @@ -0,0 +1,8 @@ +--- +source: crates/yuck/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, r#\"(f arg ; test\n arg2)\"#.to_string()))" + +--- +Ok( + (f arg arg2), +) diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__test-17.snap b/crates/yuck/src/parser/snapshots/yuck__parser__test-17.snap new file mode 100644 index 0000000..a2e848a --- /dev/null +++ b/crates/yuck/src/parser/snapshots/yuck__parser__test-17.snap @@ -0,0 +1,8 @@ +--- +source: crates/yuck/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, \"\\\"h\\\\\\\"i\\\"\".to_string()))" + +--- +Ok( + "h"i", +) diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__test-2.snap b/crates/yuck/src/parser/snapshots/yuck__parser__test-2.snap new file mode 100644 index 0000000..6fec876 --- /dev/null +++ b/crates/yuck/src/parser/snapshots/yuck__parser__test-2.snap @@ -0,0 +1,8 @@ +--- +source: crates/yuck/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, \"(12)\".to_string()))" + +--- +Ok( + ("12"), +) diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__test-3.snap b/crates/yuck/src/parser/snapshots/yuck__parser__test-3.snap new file mode 100644 index 0000000..6303fc0 --- /dev/null +++ b/crates/yuck/src/parser/snapshots/yuck__parser__test-3.snap @@ -0,0 +1,8 @@ +--- +source: crates/yuck/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, \"1.2\".to_string()))" + +--- +Ok( + "1.2", +) diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__test-4.snap b/crates/yuck/src/parser/snapshots/yuck__parser__test-4.snap new file mode 100644 index 0000000..d086147 --- /dev/null +++ b/crates/yuck/src/parser/snapshots/yuck__parser__test-4.snap @@ -0,0 +1,8 @@ +--- +source: crates/yuck/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, \"-1.2\".to_string()))" + +--- +Ok( + "-1.2", +) diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__test-5.snap b/crates/yuck/src/parser/snapshots/yuck__parser__test-5.snap new file mode 100644 index 0000000..6ec6487 --- /dev/null +++ b/crates/yuck/src/parser/snapshots/yuck__parser__test-5.snap @@ -0,0 +1,8 @@ +--- +source: crates/yuck/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, \"(1 2)\".to_string()))" + +--- +Ok( + ("1" "2"), +) diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__test-6.snap b/crates/yuck/src/parser/snapshots/yuck__parser__test-6.snap new file mode 100644 index 0000000..f6c3cbd --- /dev/null +++ b/crates/yuck/src/parser/snapshots/yuck__parser__test-6.snap @@ -0,0 +1,8 @@ +--- +source: crates/yuck/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, \"(1 :foo 1)\".to_string()))" + +--- +Ok( + ("1" :foo "1"), +) diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__test-7.snap b/crates/yuck/src/parser/snapshots/yuck__parser__test-7.snap new file mode 100644 index 0000000..e7fc34b --- /dev/null +++ b/crates/yuck/src/parser/snapshots/yuck__parser__test-7.snap @@ -0,0 +1,8 @@ +--- +source: crates/yuck/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, \"(:foo 1)\".to_string()))" + +--- +Ok( + (:foo "1"), +) diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__test-8.snap b/crates/yuck/src/parser/snapshots/yuck__parser__test-8.snap new file mode 100644 index 0000000..c5edd60 --- /dev/null +++ b/crates/yuck/src/parser/snapshots/yuck__parser__test-8.snap @@ -0,0 +1,8 @@ +--- +source: crates/yuck/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, \"(:foo->: 1)\".to_string()))" + +--- +Ok( + (:foo->: "1"), +) diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__test-9.snap b/crates/yuck/src/parser/snapshots/yuck__parser__test-9.snap new file mode 100644 index 0000000..d852703 --- /dev/null +++ b/crates/yuck/src/parser/snapshots/yuck__parser__test-9.snap @@ -0,0 +1,8 @@ +--- +source: crates/yuck/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, \"(foo 1)\".to_string()))" + +--- +Ok( + (foo "1"), +) diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__test.snap b/crates/yuck/src/parser/snapshots/yuck__parser__test.snap new file mode 100644 index 0000000..0d12c19 --- /dev/null +++ b/crates/yuck/src/parser/snapshots/yuck__parser__test.snap @@ -0,0 +1,8 @@ +--- +source: crates/yuck/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, \"1\".to_string()))" + +--- +Ok( + "1", +) diff --git a/src/value/coords.rs b/crates/yuck/src/value/coords.rs similarity index 69% rename from src/value/coords.rs rename to crates/yuck/src/value/coords.rs index cb50699..355c2e4 100644 --- a/src/value/coords.rs +++ b/crates/yuck/src/value/coords.rs @@ -1,9 +1,19 @@ -use anyhow::*; use derive_more::*; +use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; use smart_default::SmartDefault; use std::{fmt, str::FromStr}; +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Failed to parse \"{0}\" as a length value")] + NumParseFailed(String), + #[error("Inalid unit \"{0}\", must be either % or px")] + InvalidUnit(String), + #[error("Invalid format. Coordinates must be formated like 200x100")] + MalformedCoords, +} + #[derive(Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Display, DebugCustom, SmartDefault)] pub enum NumWithUnit { #[display(fmt = "{}%", .0)] @@ -25,21 +35,18 @@ impl NumWithUnit { } impl FromStr for NumWithUnit { - type Err = anyhow::Error; + type Err = Error; fn from_str(s: &str) -> Result { - lazy_static::lazy_static! { - static ref PATTERN: regex::Regex = regex::Regex::new("^(-?\\d+)(.*)$").unwrap(); - }; + static PATTERN: Lazy = Lazy::new(|| regex::Regex::new("^(-?\\d+)(.*)$").unwrap()); - let captures = PATTERN.captures(s).with_context(|| format!("could not parse '{}'", s))?; - let value = captures.get(1).unwrap().as_str().parse::()?; - let value = match captures.get(2).unwrap().as_str() { - "px" | "" => NumWithUnit::Pixels(value), - "%" => NumWithUnit::Percent(value), - _ => bail!("couldn't parse {}, unit must be either px or %", s), - }; - Ok(value) + let captures = PATTERN.captures(s).ok_or_else(|| Error::NumParseFailed(s.to_string()))?; + let value = captures.get(1).unwrap().as_str().parse::().map_err(|_| Error::NumParseFailed(s.to_string()))?; + match captures.get(2).unwrap().as_str() { + "px" | "" => Ok(NumWithUnit::Pixels(value)), + "%" => Ok(NumWithUnit::Percent(value)), + unit => Err(Error::InvalidUnit(unit.to_string())), + } } } @@ -51,12 +58,12 @@ pub struct Coords { } impl FromStr for Coords { - type Err = anyhow::Error; + type Err = Error; fn from_str(s: &str) -> Result { let (x, y) = s .split_once(|x: char| x.to_ascii_lowercase() == 'x' || x.to_ascii_lowercase() == '*') - .ok_or_else(|| anyhow!("must be formatted like 200x500"))?; + .ok_or(Error::MalformedCoords)?; Coords::from_strs(x, y) } } @@ -68,16 +75,13 @@ impl fmt::Debug for Coords { } impl Coords { - pub fn from_pixels(x: i32, y: i32) -> Self { + pub fn from_pixels((x, y): (i32, i32)) -> Self { Coords { x: NumWithUnit::Pixels(x), y: NumWithUnit::Pixels(y) } } /// parse a string for x and a string for y into a [`Coords`] object. - pub fn from_strs(x: &str, y: &str) -> Result { - Ok(Coords { - x: x.parse().with_context(|| format!("Failed to parse '{}'", x))?, - y: y.parse().with_context(|| format!("Failed to parse '{}'", y))?, - }) + pub fn from_strs(x: &str, y: &str) -> Result { + Ok(Coords { x: x.parse()?, y: y.parse()? }) } /// resolve the possibly relative coordinates relative to a given containers size diff --git a/crates/yuck/src/value/mod.rs b/crates/yuck/src/value/mod.rs new file mode 100644 index 0000000..cc26f2d --- /dev/null +++ b/crates/yuck/src/value/mod.rs @@ -0,0 +1,5 @@ +use derive_more::*; +use serde::{Deserialize, Serialize}; + +pub mod coords; +pub use coords::*; diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index d444c40..c0ee579 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -2,9 +2,9 @@ - [Eww - Widgets for everyone!](./eww.md) - [Configuration](./configuration.md) +- [Eww expressions](./expression_language.md) - [Theming with GTK](./working_with_gtk.md) - [Magic Variables](./magic-vars.md) - [Widgets](./widgets.md) - [Troubleshooting](./troubleshooting.md) -- [Eww expressions](./expression_language.md) - [Examples](./examples.md) diff --git a/docs/src/configuration.md b/docs/src/configuration.md index 8f2261c..f9c9867 100644 --- a/docs/src/configuration.md +++ b/docs/src/configuration.md @@ -1,256 +1,236 @@ -# Configuration +# Writing your eww configuration -For specific built in widgets `, , , etc` see [Widget Documentation](widgets.md) +(For a list of all built in widgets (i.e. `box`, `label`, `button`) see [Widget Documentation](widgets.md)) -## Placing the configuration file +Eww is configured using its own language called `yuck`. +using yuck, you declare the structure and content of your widgets, the geometry, position and behavior of any windows, +as well as any state and data that will be used in your widgets. +Yuck is based around S-expressions, which you may know from lisp-like languages. +If you're using vim, you can make use of [yuck.vim](https://github.com/elkowar/yuck.vim) for editor support. +It is also recommended to use [parinfer](https://shaunlebron.github.io/parinfer/), +which makes working with s-expressions delightfully easy! -Note: Example configuration files can be found in the `examples` directory of the repository and are showcased in [Examples](examples.md). +Additionally, any styles are defined in scss (which is mostly just slightly improved CSS syntax). +While eww supports a significant portion of the CSS you know from the web, +not everything is supported, as eww relies on GTKs own CSS engine. +Notably, some animation features are unsupported, +as well as most layout-related CSS properties such as flexbox, `float`, absolute position or `width`/`height`. -The configuration file and the scss file should lay in `$XDG_CONFIG_HOME/eww` (or, if unset, `$HOME/.config/eww`). The XML file should be named `eww.xml` and the scss should be named `eww.scss` -So the directory structure should look like this: -``` -$HOME -└──.config - ──eww - ├──eww.xml - └──eww.scss +To get started, you'll need to create two files: `eww.yuck` and `eww.scss`. +These files must be placed under `$XDG_CONFIG_HOME/eww` (this is most likely `~/.config/eww`). + +Now that those files are created, you can start writing your first widget! + +## Creating your first window + +Firstly, you will need to create a top-level window. Here, you configure things such as the name, position, geometry and content of your window. + +Let's look at an example window definition: + +```lisp +(defwindow example + :monitor 0 + :geometry (geometry :x "0%" + :y "20px" + :width "90%" + :height "30px" + :anchor "top center") + :stacking "fg" + :reserve (struts :distance "40px" :side "top") + :windowtype "dock" + :wm-ignore false + "example content") ``` -## Config structure +Here, we are defining a window named `example`, which we then set a set of properties for. Additionally, we set the content of the window to be the text `"example content"`. -Your config structure should look like this: -```xml - - - - +You can now open your first window by running `eww open example`! Glorious! - - - +### `defwindow`-properties - - - - - - - - -``` -See -[The `` block](#the-includes-block), -[The `` block](#the-definitions-block), -[Variables](#variables) and the -[The `` block](#the-windows-block). - -## Variables - -If you create a `` or a ``, you can reference them in your `` by doing `{{var}}`. Where `var` is your variable name. +| Property | Description | +| ---------: | ------------------------------------------------------------ | +| `monitor` | which monitor this window should be displayed on. | +| `geometry` | Geometry of the window. | -### The `` tag -Allows you to repeat the same text multiple times through without retyping it multiple times. +**Geometry-properties** -Example: This will define a variable named `banana`, with the default value "I like bananas." -```xml - - I like bananas. - -``` -You can then reference it in your widgets by doing: +| Property | Description | +| -----------------:| ------------------------------------------------------------ | +| `x`, `y` | Position of the window. Values may be provided in `px` or `%`. Will be relative to `anchor` | +| `width`, `height` | Width and height of the window. Values may be provided in `px` or `%`. | +| `anchor` | Anchor-point of the window. Either `center` or combinations of `top`, `center`, `bottom` and `left`, `center`, `right` | -```xml - - {{banana}} - + +Depending on if you are using X11 or wayland, some additional properties exist: + +#### x11 + +| Property | Description | +| -----------: | ------------------------------------------------------------ | +| `stacking` | Where the window should appear in the stack. Possible values: `fg`, `bg`. | +| `wm-ignore` | Whether the windowmanager should ignore this window. This is useful for dashboard-style widgets that don't need to interact with other windows at all. Note that this makes some of the other properties not have any effect. Either `true` or `false` | +| `reserve` | Specify how the window-manager should make space for your window. This is useful for bars, which should not overlap any other windows. | +| `windowtype` | Specify what type of window this is. This will be used by your window manager to determine how it should handle your window. Possible values: `normal`, `dock`, `toolbar`, `dialog`. Default: `dock` if `reserve` is specified, `normal` otherwise. | + +#### wayland + +| Property | Description | +| ----------: | ------------------------------------------------------------ | +| `stacking` | Where the window should appear in the stack. Possible values: `fg`, `bg`, `overlay`, `bottom`. | +| `exclusive` | Whether the compositor should reserve space for the window automatically. | +| `focusable` | Whether the window should be able to be focused. This is necessary for any widgets that use the keyboard to work. | + + + +## Your first widget + +While our bar is already looking great, it's a bit boring. Thus, let's add some actual content! + +```lisp +(defwidget greeter [text name] + (box :orientation "horizontal" + :halign "center" + text + (button :onclick "notify-send 'Hello' 'Hello, ${name}'" + "Greet"))) ``` -To change the value of the variable, and thus change the UI, you can run `eww update banana "I like apples"` +To show this, let's replace the text in our window definition with a call to this new widget: -### The `` tag - -Allows you to create a script that eww runs. -Useful for creating volume sliders or anything similar. - -Example: -```xml - - - date +%H:%M - - +```lisp +(defwindow example + ; ... values omitted + (greeter :text "Say hello!" + :name "Tim")) ``` -and then reference it by doing: -```xml - - {{date}} - +There is a lot going on here, so let's step through this. + +We are creating a widget named `greeter`. This widget takes two attributes, called `text` and `name`, which must be set when the widget is used. + +Now, we declare the body of our widget. We make use of a `box`, which we set a couple attributes of. + +We need this `box`, as a widget definition can only ever contain a single widget - otherwise, +eww would not know if it should align them vertically or horizontally, how it should space them, and so on. +Thus, we wrap multiple children in a `box.`. +This box then contains a reference to the provided attribute `text`, as well as a button. +In that buttons `onclick` attribute, we make refer to the provided `name` using string-interpolation syntax: `"${name}"`. +This allows us to easily refer to any variables within strings. +In fact, there is a lot more you can do withing `${...}` - more on that in the chapter about the [expression language](expression_language.md). + +To then use our widget, we call it just like we would use any other built-in widget, and provide the required attributes. + +As you may have noticed, we are using a couple predefined widgets here. These are all listed and explained in the [widgets chapter](widgets.md). + + + +## Adding dynamic content + +Now that you feel sufficiently greeted by your bar, you may realize that showing data like the time and date might be even more useful than having a button that greets you. + +To implement dynamic content in your widgets you make use of _variables_. + +These user-defined variables are globally available from all of your widgets. Whenever the variable changes, the value in the widget will update! + +There are four different types of variables: basic, polling, listening, and a set of builtin "magic" variables. + +**Basic variables (`defvar`)** + +```lisp +(defvar foo "initial value") ``` -The `interval="5s"` part says how long time it should take before Eww runs the command again. -Here are the available times you can set: +This is the simplest type of variable. +Basic variables don't ever change automatically. +Instead, you explicitly update them by calling eww like so: `eww update foo="new value"`. -| Shortened | Full name | -|-----------|-------------| -| ms | Miliseconds | -| s | Seconds | -| m | Minutes | -| h | Hours | +This is useful if you have values that change very rarely, or may change as a result of some external script you wrote. +They may also be useful to have buttons within eww change what is shown within your widget, by setting attributes like `onclick` to run `eww update`. +**Polling variables (`defpoll`)** -### Tail -If you don't want a set interval and instead want it to tail (run the script when it detects a change is present) you can simply remove the `interval="5s"` so it becomes: -```xml - - - date +%H:%M - - -``` -## The `` block -Here you can include other config files so that they are merged together at startup. Currently namespaced variables are not supported so be careful when reusing code. - -```xml - - - - +```lisp +(defpoll time :interval "1s" + :timeout "0.1s" ; setting timeout is optional + `date +%H:%M:%S`) ``` -If you define a variable/widget/window, in a config file, when it's defined somewhere else, you can see a warning in the eww logs (`eww logs`) +A polling variable is a variable which runs a provided shell-script repeatedly, in a given interval. -## The `` block -In here your whole widget will be made, and you can also create your own widgets. Check [Widget Documentation](widgets.md) for pre-defined widgets. +This may be the most commonly used type of variable. +They are useful to access any quickly retrieved value repeatedly, +and thus are the perfect choice for showing your time, date, as well as other bits of information such as your volume. -### Custom widgets +Optionally, you can specify a timeout, after which the provided script will be aborted. +This helps to avoid accidentally launching thousands of never-ending processes on your system. -Let's get a small config and break it down. +**Listening variables (`deflisten`)** -```xml - - - - The time is: {{my_time}} currently. - - - - - - - - - - - - date - - -``` -That's a long config just for a custom widget. But let's break it down and try to understand it. - -This part: -```xml - - - The time is: {{my_time}} currently. - - -``` -Is the custom widget. As we can see by the -```xml - -``` -the widget is called `clock.`Or referenced `` -The `{{my_time}}` is the value we assign to be well, our time. You can actually set to be anything, it doesn't have to be a time. You can compare it to `value=""` - -So if we look at: -```xml - - - - - -``` -we can see that we assign `{{my_time}}` to be `{{date}}` and if we look at -```xml - - date - -``` -we can see that `{{date}}` is simply running the `date` command. - -It doesn't have to be `{{my_time}}` either, it can be anything. -```xml - - - The time is: {{very_long_list_of_animals}} currently. - - -``` -is valid. - -To use that it would look like this: -```xml - - - - - -``` -## The `` block - -All different windows you might want to use are defined in the `` block. -The `` config should look something like this: - -```xml - - - - - -
- - - +```lisp +(deflisten foo :initial "whatever" + `tail -F /tmp/some_file`) ``` -For Wayland users the `` block is replaced by the exclusive field in ``. -The previous `` block would look like this. +Listening variables might be the most confusing of the bunch. +A listening variable runs a script once, and reads its output continously. +Whenever the script outputs a new line, the value will be updated to that new line. +In the example given above, the value of `foo` will start out as `"whatever"`, and will change whenever a new line is appended to `/tmp/some_file`. -```xml - - - -
- - +These are particularly useful if you have a script that can monitor some value on its own. +For example, the command `xprop -spy -root _NET_CURRENT_DESKTOP` writes the currently focused desktop whenever it changes. +This can be used to implement a workspace widget for a bar, for example. +Another example usecase is monitoring the currently playing song with playerctl: `playerctl --follow metadata --format {{title}}`. + +**Built-in "magic" variables** + +In addition to definition your own variables, eww provides some values for you to use out of the box. +These include values such as your CPU and RAM usage. +These mostly contain their data as JSON, which you can then use using the [json access syntax](expression_language.md). +All available magic variables are listed [here](magic-vars.md). + +## Dynamically generated widgets with `literal` + +In some cases, you want to not only change the text, +value or color of a widget dynamically, but instead want to generate an entire widget structure dynamically. +This is necessary if you want to display lists of things (for example notifications) +where the amount is not necessarily known, +or if you want to change the widget structure in some other more complex way. + +For this, you can make use of one of ewws most powerful features: the `literal` widget. + +```lisp +(defvar variable_containing_yuck + "(box (button 'foo') (button 'bar'))") + +; then, inside your widget, use: +(literal :content variable_containing_yuck) ``` -The window block contains multiple elements to configure the window. -- `` is used to specify the position and size of the window. -- `` is used to have eww reserve space at a given side of the screen the widget is on. -- `` will contain the widget that is shown in the window. +Here, you specify the content of your literal by providing it a string (most likely stored in a variable) which contains a single yuck widget tree. +Eww then reads the provided value and renders the resulting widget. Whenever it changes, the widget will be rerendered. + +Note that this is not all that efficient. Make sure to only use `literal` when necessary! + +## Splitting up your configuration + +As time passes, your configuration might grow larger and larger. Luckily, you can easily split up your configuration into multiple files! + +There are two options to achieve this: + +### Using `include` + +```lisp +(include "./path/to/your/file.yuck") +``` + +A single yuck-file may import the contents of any other yuck file. For this, make use of the `include` directive. + +### Using a separate eww configuration directory + +If you want to separate different widgets even further, you can create a new eww config folder anywhere else. +Then, you can tell eww to use that configuration directory by passing _every_ command the `--config /path/to/your/config/dir` flag. +Make sure to actually include this in all your `eww` calls, including `eww kill`, `eww logs`, etc. +This launches a separate instance of the eww daemon, that has separate logs and state from your main eww configuration. -There are a couple things you can optionally configure on the window itself: -- `stacking`: stacking describes on what "layer" of the screen the window is shown. - Possible values on the X11 backend: `foreground "fg"`, `background "bg"`. Default: `"fg"` - Possible values on the Wayland backend: `foreground "fg"`, `bottom "bt"`, `background "bg"`, `overlay "ov"`. Default: `"fg"` -- `focusable`: whether the window should be focusable by the windowmanager. - This is necessary for things like text-input-fields to work properly. - Possible values: `"true"`, `"false"`. Default: `"false"` -- `screen`: Specifies on which display to show the window in a multi-monitor setup. - This can be any number, representing the index of your monitor. -- `exclusive`: Specifies whether or not a surface can be occupied by another. - A surface can be a window, an Eww widget or any layershell surface. - The details on how it is actually implemented are left to the compositor. - This option is only valid on Wayland. - Possible values: `"true"`, `"false"`. Default: `"false"` -- `windowtype`: (X11 only) Can be used in determining the decoration, stacking position and other behavior of the window. - Possible values: - - `"normal"`: indicates that this is a normal, top-level window - - `"dock"`: indicates a dock or panel feature - - `"toolbar"`: toolbars "torn off" from the main application - - `"dialog"`: indicates that this is a dialog window - - Default: `"dock"` if reserve is set, else `"normal"` diff --git a/docs/src/eww.md b/docs/src/eww.md index 0974c33..44c3b9c 100644 --- a/docs/src/eww.md +++ b/docs/src/eww.md @@ -5,7 +5,7 @@ is a widget system made in [rust](https://www.rust-lang.org/), which let's you create your own widgets similarly to how you can in AwesomeWM. The key difference: It is independent of your window manager! -Configured in XML and themed using CSS, it is easy to customize and provides all the flexibility you need! +Configured in yuck and themed using CSS, it is easy to customize and provides all the flexibility you need! ## How to install Eww @@ -21,7 +21,7 @@ as this makes it easy to use the nightly toolchain necessary to build eww. ### Building -Once you have the Prerequisites ready, you're ready to install and build eww. +Once you have the prerequisites ready, you're ready to install and build eww. First clone the repo: ```bash @@ -55,4 +55,3 @@ and then to run it do ./eww daemon ./eww open ``` -`` is the name of the window, see [The windows block](configuration.md#windows-block). diff --git a/docs/src/expression_language.md b/docs/src/expression_language.md index bb72b90..b8010fe 100644 --- a/docs/src/expression_language.md +++ b/docs/src/expression_language.md @@ -1,31 +1,32 @@ -# The embedded Eww expression-language +# Simple expression language -Within variable references, you can make use of a small, built-in expression language. -This can be used whereever you can use variable-references (`{{varname}}`). +Yuck includes a small expression language that can be used to run several operations on your data. +This can be used to show different values depending on certain conditions, +do mathematic operations, and even to access values withing JSON-structures. + +These expressions can be placed anywhere within your configuration in between `{ ... }`, +as well as withing strings, inside string-interpolation blocks (`"foo ${ ... } bar"`). ## Example -```xml - - -Some math: {{12 + 2 * 10}} +```lisp +(box + "Some math: ${12 + foo * 10}" + (button :class {button_active ? "active" : "inactive"} + :onclick "toggle_thing" + {button_active ? "disable" : "enable"})) ``` -## Syntax +## Features -The expression language supports: +Supported currently are the following features: - simple mathematical operations (`+`, `-`, `*`, `/`, `%`) - comparisons (`==`, `!=`, `>`, `<`) - boolean operations (`||`, `&&`, `!`) - elvis operator (`?:`) - if the left side is `""`, then returns the right side, otherwise evaluates to the left side. -- conditionals (`if condition then 'value' else 'other value'`) +- conditionals (`condition ? 'value' : 'other value'`) - numbers, strings, booleans and variable references (`12`, `'hi'`, `true`, `some_variable`) - - strings can contain other expressions again: `'foo {{some_variable}} bar'` - json access (`object.field`, `array[12]`, `object["field"]`) - for this, the object/array value needs to refer to a variable that contains a valid json string. - some function calls: diff --git a/docs/src/troubleshooting.md b/docs/src/troubleshooting.md index fdd1d36..d765869 100644 --- a/docs/src/troubleshooting.md +++ b/docs/src/troubleshooting.md @@ -12,39 +12,24 @@ Here you will find help if something doesn't work, if the issue isn't listed her 1. Make sure you compiled eww with the `--no-default-features --features=wayland` flags. 2. Make sure that you're not trying to use X11-specific features (these are (hopefully) explicitly specified as such in the documentation). -## My scss isn't being loaded! +## My configuration is not loaded correctly -1. You have not created a scss file -2. The scss file isn't called correctly. (it should be called `eww.scss` in the `$HOME/.config/eww` folder) -3. The scss file isn't placed in the correct location (check above) - -If none of these fixed your problem [open an issue on the GitHub repo](https://github.com/elkowar/eww/issues), or check the [GTK-Debugger](working_with_gtk.md#gtk-debugger). - -## Eww can't find my configuration file! - -1. It's incorrectly named or it's in the wrong place (it should be called `eww.xml` in the `$HOME/.config/eww` folder) -2. You haven't started eww correctly or you started it wrong. +1. Make sure the `eww.yuck` and `eww.scss` files are in the correct places +2. Sometimes, eww might fail to load your configuration as a result of a configuration error. Make sure your configuration is valid. ## Something isn't styled correctly! -1. You have mistyped the CSS class. -2. Check the [GTK-Debugger](working_with_gtk.md#gtk-debugger) +Check the [GTK-Debugger](working_with_gtk.md#gtk-debugger) to get more insight into what styles GTK is applying to which elements. ## General issues You should try the following things, before opening a issue or doing more specialized troubleshooting: -- Kill the eww daemon by running `eww kill` and restart it with `eww --debug daemon` to get additional log output. +- Kill the eww daemon by running `eww kill` and re-open your window with the `--debug`-flag to get additional log output. - Now you can take a look at the logs by running `eww logs`. - use `eww state`, to see the state of all variables -- use `eww debug`, to see the xml of your widget and other information +- use `eww debug`, to see the structure of your widget and other information - update to the latest eww version -- sometimes hot reloading doesn't work. restart the widget in that case - -If you're experiencing issues printing variables, try to print them in quotes, so e.g. - -``` -onchange="notify-send '{}'" -``` +- sometimes hot reloading doesn't work. In that case, you can make use of `eww reload` manually. Remember, if your issue isn't listed here, [open an issue on the GitHub repo](https://github.com/elkowar/eww/issues). diff --git a/docs/src/working_with_gtk.md b/docs/src/working_with_gtk.md index 039d1bb..39dbdad 100644 --- a/docs/src/working_with_gtk.md +++ b/docs/src/working_with_gtk.md @@ -17,13 +17,13 @@ SCSS is _very_ close to CSS so if you know CSS you'll have no problem learning S The debugger can be used for **a lot** of things. Especially if something doesn't work or isn't styled right. to enable it launch your eww daemon with ```bash -GTK_DEBUG=interactive ./eww daemon +GTK_DEBUG=interactive eww daemon ``` or in fish ```bash -env GTK_DEBUG=interactive ./eww daemon +env GTK_DEBUG=interactive eww daemon ``` If a style or something similar doesn't work you can click on the icon in the top left icon to select the thing that isn't being styled or isn't being styled correctly. diff --git a/docs/theme/highlight.js b/docs/theme/highlight.js index 180385b..dd6fc1a 100644 --- a/docs/theme/highlight.js +++ b/docs/theme/highlight.js @@ -1,6 +1,668 @@ -/* - Highlight.js 10.1.1 (93fd0d73) +/*! + Highlight.js v11.0.1 (git: 1cf31f015d) + (c) 2006-2021 Ivan Sagalaev and other contributors License: BSD-3-Clause - Copyright (c) 2006-2020, Ivan Sagalaev -*/ -var hljs=function(){"use strict";function e(n){Object.freeze(n);var t="function"==typeof n;return Object.getOwnPropertyNames(n).forEach((function(r){!Object.hasOwnProperty.call(n,r)||null===n[r]||"object"!=typeof n[r]&&"function"!=typeof n[r]||t&&("caller"===r||"callee"===r||"arguments"===r)||Object.isFrozen(n[r])||e(n[r])})),n}class n{constructor(e){void 0===e.data&&(e.data={}),this.data=e.data}ignoreMatch(){this.ignore=!0}}function t(e){return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function r(e,...n){var t={};for(const n in e)t[n]=e[n];return n.forEach((function(e){for(const n in e)t[n]=e[n]})),t}function a(e){return e.nodeName.toLowerCase()}var i=Object.freeze({__proto__:null,escapeHTML:t,inherit:r,nodeStream:function(e){var n=[];return function e(t,r){for(var i=t.firstChild;i;i=i.nextSibling)3===i.nodeType?r+=i.nodeValue.length:1===i.nodeType&&(n.push({event:"start",offset:r,node:i}),r=e(i,r),a(i).match(/br|hr|img|input/)||n.push({event:"stop",offset:r,node:i}));return r}(e,0),n},mergeStreams:function(e,n,r){var i=0,s="",o=[];function l(){return e.length&&n.length?e[0].offset!==n[0].offset?e[0].offset"}function u(e){s+=""}function d(e){("start"===e.event?c:u)(e.node)}for(;e.length||n.length;){var g=l();if(s+=t(r.substring(i,g[0].offset)),i=g[0].offset,g===e){o.reverse().forEach(u);do{d(g.splice(0,1)[0]),g=l()}while(g===e&&g.length&&g[0].offset===i);o.reverse().forEach(c)}else"start"===g[0].event?o.push(g[0].node):o.pop(),d(g.splice(0,1)[0])}return s+t(r.substr(i))}});const s="",o=e=>!!e.kind;class l{constructor(e,n){this.buffer="",this.classPrefix=n.classPrefix,e.walk(this)}addText(e){this.buffer+=t(e)}openNode(e){if(!o(e))return;let n=e.kind;e.sublanguage||(n=`${this.classPrefix}${n}`),this.span(n)}closeNode(e){o(e)&&(this.buffer+=s)}value(){return this.buffer}span(e){this.buffer+=``}}class c{constructor(){this.rootNode={children:[]},this.stack=[this.rootNode]}get top(){return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){this.top.children.push(e)}openNode(e){const n={kind:e,children:[]};this.add(n),this.stack.push(n)}closeNode(){if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,n){return"string"==typeof n?e.addText(n):n.children&&(e.openNode(n),n.children.forEach(n=>this._walk(e,n)),e.closeNode(n)),e}static _collapse(e){"string"!=typeof e&&e.children&&(e.children.every(e=>"string"==typeof e)?e.children=[e.children.join("")]:e.children.forEach(e=>{c._collapse(e)}))}}class u extends c{constructor(e){super(),this.options=e}addKeyword(e,n){""!==e&&(this.openNode(n),this.addText(e),this.closeNode())}addText(e){""!==e&&this.add(e)}addSublanguage(e,n){const t=e.root;t.kind=n,t.sublanguage=!0,this.add(t)}toHTML(){return new l(this,this.options).value()}finalize(){return!0}}function d(e){return e?"string"==typeof e?e:e.source:null}const g="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",h={begin:"\\\\[\\s\\S]",relevance:0},f={className:"string",begin:"'",end:"'",illegal:"\\n",contains:[h]},p={className:"string",begin:'"',end:'"',illegal:"\\n",contains:[h]},b={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},m=function(e,n,t={}){var a=r({className:"comment",begin:e,end:n,contains:[]},t);return a.contains.push(b),a.contains.push({className:"doctag",begin:"(?:TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):",relevance:0}),a},v=m("//","$"),x=m("/\\*","\\*/"),E=m("#","$");var _=Object.freeze({__proto__:null,IDENT_RE:"[a-zA-Z]\\w*",UNDERSCORE_IDENT_RE:"[a-zA-Z_]\\w*",NUMBER_RE:"\\b\\d+(\\.\\d+)?",C_NUMBER_RE:g,BINARY_NUMBER_RE:"\\b(0b[01]+)",RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",SHEBANG:(e={})=>{const n=/^#![ ]*\//;return e.binary&&(e.begin=function(...e){return e.map(e=>d(e)).join("")}(n,/.*\b/,e.binary,/\b.*/)),r({className:"meta",begin:n,end:/$/,relevance:0,"on:begin":(e,n)=>{0!==e.index&&n.ignoreMatch()}},e)},BACKSLASH_ESCAPE:h,APOS_STRING_MODE:f,QUOTE_STRING_MODE:p,PHRASAL_WORDS_MODE:b,COMMENT:m,C_LINE_COMMENT_MODE:v,C_BLOCK_COMMENT_MODE:x,HASH_COMMENT_MODE:E,NUMBER_MODE:{className:"number",begin:"\\b\\d+(\\.\\d+)?",relevance:0},C_NUMBER_MODE:{className:"number",begin:g,relevance:0},BINARY_NUMBER_MODE:{className:"number",begin:"\\b(0b[01]+)",relevance:0},CSS_NUMBER_MODE:{className:"number",begin:"\\b\\d+(\\.\\d+)?(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0},REGEXP_MODE:{begin:/(?=\/[^/\n]*\/)/,contains:[{className:"regexp",begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[h,{begin:/\[/,end:/\]/,relevance:0,contains:[h]}]}]},TITLE_MODE:{className:"title",begin:"[a-zA-Z]\\w*",relevance:0},UNDERSCORE_TITLE_MODE:{className:"title",begin:"[a-zA-Z_]\\w*",relevance:0},METHOD_GUARD:{begin:"\\.\\s*[a-zA-Z_]\\w*",relevance:0},END_SAME_AS_BEGIN:function(e){return Object.assign(e,{"on:begin":(e,n)=>{n.data._beginMatch=e[1]},"on:end":(e,n)=>{n.data._beginMatch!==e[1]&&n.ignoreMatch()}})}}),N="of and for in not or if then".split(" ");function w(e,n){return n?+n:function(e){return N.includes(e.toLowerCase())}(e)?0:1}const R=t,y=r,{nodeStream:k,mergeStreams:O}=i,M=Symbol("nomatch");return function(t){var a=[],i={},s={},o=[],l=!0,c=/(^(<[^>]+>|\t|)+|\n)/gm,g="Could not find the language '{}', did you forget to load/include a language module?";const h={disableAutodetect:!0,name:"Plain text",contains:[]};var f={noHighlightRe:/^(no-?highlight)$/i,languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:null,__emitter:u};function p(e){return f.noHighlightRe.test(e)}function b(e,n,t,r){var a={code:n,language:e};S("before:highlight",a);var i=a.result?a.result:m(a.language,a.code,t,r);return i.code=a.code,S("after:highlight",i),i}function m(e,t,a,s){var o=t;function c(e,n){var t=E.case_insensitive?n[0].toLowerCase():n[0];return Object.prototype.hasOwnProperty.call(e.keywords,t)&&e.keywords[t]}function u(){null!=y.subLanguage?function(){if(""!==A){var e=null;if("string"==typeof y.subLanguage){if(!i[y.subLanguage])return void O.addText(A);e=m(y.subLanguage,A,!0,k[y.subLanguage]),k[y.subLanguage]=e.top}else e=v(A,y.subLanguage.length?y.subLanguage:null);y.relevance>0&&(I+=e.relevance),O.addSublanguage(e.emitter,e.language)}}():function(){if(!y.keywords)return void O.addText(A);let e=0;y.keywordPatternRe.lastIndex=0;let n=y.keywordPatternRe.exec(A),t="";for(;n;){t+=A.substring(e,n.index);const r=c(y,n);if(r){const[e,a]=r;O.addText(t),t="",I+=a,O.addKeyword(n[0],e)}else t+=n[0];e=y.keywordPatternRe.lastIndex,n=y.keywordPatternRe.exec(A)}t+=A.substr(e),O.addText(t)}(),A=""}function h(e){return e.className&&O.openNode(e.className),y=Object.create(e,{parent:{value:y}})}function p(e){return 0===y.matcher.regexIndex?(A+=e[0],1):(L=!0,0)}var b={};function x(t,r){var i=r&&r[0];if(A+=t,null==i)return u(),0;if("begin"===b.type&&"end"===r.type&&b.index===r.index&&""===i){if(A+=o.slice(r.index,r.index+1),!l){const n=Error("0 width match regex");throw n.languageName=e,n.badRule=b.rule,n}return 1}if(b=r,"begin"===r.type)return function(e){var t=e[0],r=e.rule;const a=new n(r),i=[r.__beforeBegin,r["on:begin"]];for(const n of i)if(n&&(n(e,a),a.ignore))return p(t);return r&&r.endSameAsBegin&&(r.endRe=RegExp(t.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&"),"m")),r.skip?A+=t:(r.excludeBegin&&(A+=t),u(),r.returnBegin||r.excludeBegin||(A=t)),h(r),r.returnBegin?0:t.length}(r);if("illegal"===r.type&&!a){const e=Error('Illegal lexeme "'+i+'" for mode "'+(y.className||"")+'"');throw e.mode=y,e}if("end"===r.type){var s=function(e){var t=e[0],r=o.substr(e.index),a=function e(t,r,a){let i=function(e,n){var t=e&&e.exec(n);return t&&0===t.index}(t.endRe,a);if(i){if(t["on:end"]){const e=new n(t);t["on:end"](r,e),e.ignore&&(i=!1)}if(i){for(;t.endsParent&&t.parent;)t=t.parent;return t}}if(t.endsWithParent)return e(t.parent,r,a)}(y,e,r);if(!a)return M;var i=y;i.skip?A+=t:(i.returnEnd||i.excludeEnd||(A+=t),u(),i.excludeEnd&&(A=t));do{y.className&&O.closeNode(),y.skip||y.subLanguage||(I+=y.relevance),y=y.parent}while(y!==a.parent);return a.starts&&(a.endSameAsBegin&&(a.starts.endRe=a.endRe),h(a.starts)),i.returnEnd?0:t.length}(r);if(s!==M)return s}if("illegal"===r.type&&""===i)return 1;if(B>1e5&&B>3*r.index)throw Error("potential infinite loop, way more iterations than matches");return A+=i,i.length}var E=T(e);if(!E)throw console.error(g.replace("{}",e)),Error('Unknown language: "'+e+'"');var _=function(e){function n(n,t){return RegExp(d(n),"m"+(e.case_insensitive?"i":"")+(t?"g":""))}class t{constructor(){this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}addRule(e,n){n.position=this.position++,this.matchIndexes[this.matchAt]=n,this.regexes.push([n,e]),this.matchAt+=function(e){return RegExp(e.toString()+"|").exec("").length-1}(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null);const e=this.regexes.map(e=>e[1]);this.matcherRe=n(function(e,n="|"){for(var t=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./,r=0,a="",i=0;i0&&(a+=n),a+="(";o.length>0;){var l=t.exec(o);if(null==l){a+=o;break}a+=o.substring(0,l.index),o=o.substring(l.index+l[0].length),"\\"===l[0][0]&&l[1]?a+="\\"+(+l[1]+s):(a+=l[0],"("===l[0]&&r++)}a+=")"}return a}(e),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex;const n=this.matcherRe.exec(e);if(!n)return null;const t=n.findIndex((e,n)=>n>0&&void 0!==e),r=this.matchIndexes[t];return n.splice(0,t),Object.assign(n,r)}}class a{constructor(){this.rules=[],this.multiRegexes=[],this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){if(this.multiRegexes[e])return this.multiRegexes[e];const n=new t;return this.rules.slice(e).forEach(([e,t])=>n.addRule(e,t)),n.compile(),this.multiRegexes[e]=n,n}considerAll(){this.regexIndex=0}addRule(e,n){this.rules.push([e,n]),"begin"===n.type&&this.count++}exec(e){const n=this.getMatcher(this.regexIndex);n.lastIndex=this.lastIndex;const t=n.exec(e);return t&&(this.regexIndex+=t.position+1,this.regexIndex===this.count&&(this.regexIndex=0)),t}}function i(e,n){const t=e.input[e.index-1],r=e.input[e.index+e[0].length];"."!==t&&"."!==r||n.ignoreMatch()}if(e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.");return function t(s,o){const l=s;if(s.compiled)return l;s.compiled=!0,s.__beforeBegin=null,s.keywords=s.keywords||s.beginKeywords;let c=null;if("object"==typeof s.keywords&&(c=s.keywords.$pattern,delete s.keywords.$pattern),s.keywords&&(s.keywords=function(e,n){var t={};return"string"==typeof e?r("keyword",e):Object.keys(e).forEach((function(n){r(n,e[n])})),t;function r(e,r){n&&(r=r.toLowerCase()),r.split(" ").forEach((function(n){var r=n.split("|");t[r[0]]=[e,w(r[0],r[1])]}))}}(s.keywords,e.case_insensitive)),s.lexemes&&c)throw Error("ERR: Prefer `keywords.$pattern` to `mode.lexemes`, BOTH are not allowed. (see mode reference) ");return l.keywordPatternRe=n(s.lexemes||c||/\w+/,!0),o&&(s.beginKeywords&&(s.begin="\\b("+s.beginKeywords.split(" ").join("|")+")(?=\\b|\\s)",s.__beforeBegin=i),s.begin||(s.begin=/\B|\b/),l.beginRe=n(s.begin),s.endSameAsBegin&&(s.end=s.begin),s.end||s.endsWithParent||(s.end=/\B|\b/),s.end&&(l.endRe=n(s.end)),l.terminator_end=d(s.end)||"",s.endsWithParent&&o.terminator_end&&(l.terminator_end+=(s.end?"|":"")+o.terminator_end)),s.illegal&&(l.illegalRe=n(s.illegal)),void 0===s.relevance&&(s.relevance=1),s.contains||(s.contains=[]),s.contains=[].concat(...s.contains.map((function(e){return function(e){return e.variants&&!e.cached_variants&&(e.cached_variants=e.variants.map((function(n){return r(e,{variants:null},n)}))),e.cached_variants?e.cached_variants:function e(n){return!!n&&(n.endsWithParent||e(n.starts))}(e)?r(e,{starts:e.starts?r(e.starts):null}):Object.isFrozen(e)?r(e):e}("self"===e?s:e)}))),s.contains.forEach((function(e){t(e,l)})),s.starts&&t(s.starts,o),l.matcher=function(e){const n=new a;return e.contains.forEach(e=>n.addRule(e.begin,{rule:e,type:"begin"})),e.terminator_end&&n.addRule(e.terminator_end,{type:"end"}),e.illegal&&n.addRule(e.illegal,{type:"illegal"}),n}(l),l}(e)}(E),N="",y=s||_,k={},O=new f.__emitter(f);!function(){for(var e=[],n=y;n!==E;n=n.parent)n.className&&e.unshift(n.className);e.forEach(e=>O.openNode(e))}();var A="",I=0,S=0,B=0,L=!1;try{for(y.matcher.considerAll();;){B++,L?L=!1:(y.matcher.lastIndex=S,y.matcher.considerAll());const e=y.matcher.exec(o);if(!e)break;const n=x(o.substring(S,e.index),e);S=e.index+n}return x(o.substr(S)),O.closeAllNodes(),O.finalize(),N=O.toHTML(),{relevance:I,value:N,language:e,illegal:!1,emitter:O,top:y}}catch(n){if(n.message&&n.message.includes("Illegal"))return{illegal:!0,illegalBy:{msg:n.message,context:o.slice(S-100,S+100),mode:n.mode},sofar:N,relevance:0,value:R(o),emitter:O};if(l)return{illegal:!1,relevance:0,value:R(o),emitter:O,language:e,top:y,errorRaised:n};throw n}}function v(e,n){n=n||f.languages||Object.keys(i);var t=function(e){const n={relevance:0,emitter:new f.__emitter(f),value:R(e),illegal:!1,top:h};return n.emitter.addText(e),n}(e),r=t;return n.filter(T).filter(I).forEach((function(n){var a=m(n,e,!1);a.language=n,a.relevance>r.relevance&&(r=a),a.relevance>t.relevance&&(r=t,t=a)})),r.language&&(t.second_best=r),t}function x(e){return f.tabReplace||f.useBR?e.replace(c,e=>"\n"===e?f.useBR?"
":e:f.tabReplace?e.replace(/\t/g,f.tabReplace):e):e}function E(e){let n=null;const t=function(e){var n=e.className+" ";n+=e.parentNode?e.parentNode.className:"";const t=f.languageDetectRe.exec(n);if(t){var r=T(t[1]);return r||(console.warn(g.replace("{}",t[1])),console.warn("Falling back to no-highlight mode for this block.",e)),r?t[1]:"no-highlight"}return n.split(/\s+/).find(e=>p(e)||T(e))}(e);if(p(t))return;S("before:highlightBlock",{block:e,language:t}),f.useBR?(n=document.createElement("div")).innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n"):n=e;const r=n.textContent,a=t?b(t,r,!0):v(r),i=k(n);if(i.length){const e=document.createElement("div");e.innerHTML=a.value,a.value=O(i,k(e),r)}a.value=x(a.value),S("after:highlightBlock",{block:e,result:a}),e.innerHTML=a.value,e.className=function(e,n,t){var r=n?s[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),e.includes(r)||a.push(r),a.join(" ").trim()}(e.className,t,a.language),e.result={language:a.language,re:a.relevance,relavance:a.relevance},a.second_best&&(e.second_best={language:a.second_best.language,re:a.second_best.relevance,relavance:a.second_best.relevance})}const N=()=>{if(!N.called){N.called=!0;var e=document.querySelectorAll("pre code");a.forEach.call(e,E)}};function T(e){return e=(e||"").toLowerCase(),i[e]||i[s[e]]}function A(e,{languageName:n}){"string"==typeof e&&(e=[e]),e.forEach(e=>{s[e]=n})}function I(e){var n=T(e);return n&&!n.disableAutodetect}function S(e,n){var t=e;o.forEach((function(e){e[t]&&e[t](n)}))}Object.assign(t,{highlight:b,highlightAuto:v,fixMarkup:x,highlightBlock:E,configure:function(e){f=y(f,e)},initHighlighting:N,initHighlightingOnLoad:function(){window.addEventListener("DOMContentLoaded",N,!1)},registerLanguage:function(e,n){var r=null;try{r=n(t)}catch(n){if(console.error("Language definition for '{}' could not be registered.".replace("{}",e)),!l)throw n;console.error(n),r=h}r.name||(r.name=e),i[e]=r,r.rawDefinition=n.bind(null,t),r.aliases&&A(r.aliases,{languageName:e})},listLanguages:function(){return Object.keys(i)},getLanguage:T,registerAliases:A,requireLanguage:function(e){var n=T(e);if(n)return n;throw Error("The '{}' language is required, but not loaded.".replace("{}",e))},autoDetection:I,inherit:y,addPlugin:function(e){o.push(e)}}),t.debugMode=function(){l=!1},t.safeMode=function(){l=!0},t.versionString="10.1.1";for(const n in _)"object"==typeof _[n]&&e(_[n]);return Object.assign(t,_),t}({})}();"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs);hljs.registerLanguage("php",function(){"use strict";return function(e){var r={begin:"\\$+[a-zA-Z_-ÿ][a-zA-Z0-9_-ÿ]*"},t={className:"meta",variants:[{begin:/<\?php/,relevance:10},{begin:/<\?[=]?/},{begin:/\?>/}]},a={className:"string",contains:[e.BACKSLASH_ESCAPE,t],variants:[{begin:'b"',end:'"'},{begin:"b'",end:"'"},e.inherit(e.APOS_STRING_MODE,{illegal:null}),e.inherit(e.QUOTE_STRING_MODE,{illegal:null})]},n={variants:[e.BINARY_NUMBER_MODE,e.C_NUMBER_MODE]},i={keyword:"__CLASS__ __DIR__ __FILE__ __FUNCTION__ __LINE__ __METHOD__ __NAMESPACE__ __TRAIT__ die echo exit include include_once print require require_once array abstract and as binary bool boolean break callable case catch class clone const continue declare default do double else elseif empty enddeclare endfor endforeach endif endswitch endwhile eval extends final finally float for foreach from global goto if implements instanceof insteadof int integer interface isset iterable list new object or private protected public real return string switch throw trait try unset use var void while xor yield",literal:"false null true",built_in:"Error|0 AppendIterator ArgumentCountError ArithmeticError ArrayIterator ArrayObject AssertionError BadFunctionCallException BadMethodCallException CachingIterator CallbackFilterIterator CompileError Countable DirectoryIterator DivisionByZeroError DomainException EmptyIterator ErrorException Exception FilesystemIterator FilterIterator GlobIterator InfiniteIterator InvalidArgumentException IteratorIterator LengthException LimitIterator LogicException MultipleIterator NoRewindIterator OutOfBoundsException OutOfRangeException OuterIterator OverflowException ParentIterator ParseError RangeException RecursiveArrayIterator RecursiveCachingIterator RecursiveCallbackFilterIterator RecursiveDirectoryIterator RecursiveFilterIterator RecursiveIterator RecursiveIteratorIterator RecursiveRegexIterator RecursiveTreeIterator RegexIterator RuntimeException SeekableIterator SplDoublyLinkedList SplFileInfo SplFileObject SplFixedArray SplHeap SplMaxHeap SplMinHeap SplObjectStorage SplObserver SplObserver SplPriorityQueue SplQueue SplStack SplSubject SplSubject SplTempFileObject TypeError UnderflowException UnexpectedValueException ArrayAccess Closure Generator Iterator IteratorAggregate Serializable Throwable Traversable WeakReference Directory __PHP_Incomplete_Class parent php_user_filter self static stdClass"};return{aliases:["php","php3","php4","php5","php6","php7"],case_insensitive:!0,keywords:i,contains:[e.HASH_COMMENT_MODE,e.COMMENT("//","$",{contains:[t]}),e.COMMENT("/\\*","\\*/",{contains:[{className:"doctag",begin:"@[A-Za-z]+"}]}),e.COMMENT("__halt_compiler.+?;",!1,{endsWithParent:!0,keywords:"__halt_compiler"}),{className:"string",begin:/<<<['"]?\w+['"]?$/,end:/^\w+;?$/,contains:[e.BACKSLASH_ESCAPE,{className:"subst",variants:[{begin:/\$\w+/},{begin:/\{\$/,end:/\}/}]}]},t,{className:"keyword",begin:/\$this\b/},r,{begin:/(::|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/},{className:"function",beginKeywords:"fn function",end:/[;{]/,excludeEnd:!0,illegal:"[$%\\[]",contains:[e.UNDERSCORE_TITLE_MODE,{className:"params",begin:"\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0,keywords:i,contains:["self",r,e.C_BLOCK_COMMENT_MODE,a,n]}]},{className:"class",beginKeywords:"class interface",end:"{",excludeEnd:!0,illegal:/[:\(\$"]/,contains:[{beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE]},{beginKeywords:"namespace",end:";",illegal:/[\.']/,contains:[e.UNDERSCORE_TITLE_MODE]},{beginKeywords:"use",end:";",contains:[e.UNDERSCORE_TITLE_MODE]},{begin:"=>"},a,n]}}}());hljs.registerLanguage("nginx",function(){"use strict";return function(e){var n={className:"variable",variants:[{begin:/\$\d+/},{begin:/\$\{/,end:/}/},{begin:"[\\$\\@]"+e.UNDERSCORE_IDENT_RE}]},a={endsWithParent:!0,keywords:{$pattern:"[a-z/_]+",literal:"on off yes no true false none blocked debug info notice warn error crit select break last permanent redirect kqueue rtsig epoll poll /dev/poll"},relevance:0,illegal:"=>",contains:[e.HASH_COMMENT_MODE,{className:"string",contains:[e.BACKSLASH_ESCAPE,n],variants:[{begin:/"/,end:/"/},{begin:/'/,end:/'/}]},{begin:"([a-z]+):/",end:"\\s",endsWithParent:!0,excludeEnd:!0,contains:[n]},{className:"regexp",contains:[e.BACKSLASH_ESCAPE,n],variants:[{begin:"\\s\\^",end:"\\s|{|;",returnEnd:!0},{begin:"~\\*?\\s+",end:"\\s|{|;",returnEnd:!0},{begin:"\\*(\\.[a-z\\-]+)+"},{begin:"([a-z\\-]+\\.)+\\*"}]},{className:"number",begin:"\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?\\b"},{className:"number",begin:"\\b\\d+[kKmMgGdshdwy]*\\b",relevance:0},n]};return{name:"Nginx config",aliases:["nginxconf"],contains:[e.HASH_COMMENT_MODE,{begin:e.UNDERSCORE_IDENT_RE+"\\s+{",returnBegin:!0,end:"{",contains:[{className:"section",begin:e.UNDERSCORE_IDENT_RE}],relevance:0},{begin:e.UNDERSCORE_IDENT_RE+"\\s",end:";|{",returnBegin:!0,contains:[{className:"attribute",begin:e.UNDERSCORE_IDENT_RE,starts:a}],relevance:0}],illegal:"[^\\s\\}]"}}}());hljs.registerLanguage("csharp",function(){"use strict";return function(e){var n={keyword:"abstract as base bool break byte case catch char checked const continue decimal default delegate do double enum event explicit extern finally fixed float for foreach goto if implicit in int interface internal is lock long object operator out override params private protected public readonly ref sbyte sealed short sizeof stackalloc static string struct switch this try typeof uint ulong unchecked unsafe ushort using virtual void volatile while add alias ascending async await by descending dynamic equals from get global group into join let nameof on orderby partial remove select set value var when where yield",literal:"null false true"},i=e.inherit(e.TITLE_MODE,{begin:"[a-zA-Z](\\.?\\w)*"}),a={className:"number",variants:[{begin:"\\b(0b[01']+)"},{begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],relevance:0},s={className:"string",begin:'@"',end:'"',contains:[{begin:'""'}]},t=e.inherit(s,{illegal:/\n/}),l={className:"subst",begin:"{",end:"}",keywords:n},r=e.inherit(l,{illegal:/\n/}),c={className:"string",begin:/\$"/,end:'"',illegal:/\n/,contains:[{begin:"{{"},{begin:"}}"},e.BACKSLASH_ESCAPE,r]},o={className:"string",begin:/\$@"/,end:'"',contains:[{begin:"{{"},{begin:"}}"},{begin:'""'},l]},g=e.inherit(o,{illegal:/\n/,contains:[{begin:"{{"},{begin:"}}"},{begin:'""'},r]});l.contains=[o,c,s,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.C_BLOCK_COMMENT_MODE],r.contains=[g,c,t,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.inherit(e.C_BLOCK_COMMENT_MODE,{illegal:/\n/})];var d={variants:[o,c,s,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},E={begin:"<",end:">",contains:[{beginKeywords:"in out"},i]},_=e.IDENT_RE+"(<"+e.IDENT_RE+"(\\s*,\\s*"+e.IDENT_RE+")*>)?(\\[\\])?",b={begin:"@"+e.IDENT_RE,relevance:0};return{name:"C#",aliases:["cs","c#"],keywords:n,illegal:/::/,contains:[e.COMMENT("///","$",{returnBegin:!0,contains:[{className:"doctag",variants:[{begin:"///",relevance:0},{begin:"\x3c!--|--\x3e"},{begin:""}]}]}),e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"meta",begin:"#",end:"$",keywords:{"meta-keyword":"if else elif endif define undef warning error line region endregion pragma checksum"}},d,a,{beginKeywords:"class interface",end:/[{;=]/,illegal:/[^\s:,]/,contains:[{beginKeywords:"where class"},i,E,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{beginKeywords:"namespace",end:/[{;=]/,illegal:/[^\s:]/,contains:[i,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"meta",begin:"^\\s*\\[",excludeBegin:!0,end:"\\]",excludeEnd:!0,contains:[{className:"meta-string",begin:/"/,end:/"/}]},{beginKeywords:"new return throw await else",relevance:0},{className:"function",begin:"("+_+"\\s+)+"+e.IDENT_RE+"\\s*(\\<.+\\>)?\\s*\\(",returnBegin:!0,end:/\s*[{;=]/,excludeEnd:!0,keywords:n,contains:[{begin:e.IDENT_RE+"\\s*(\\<.+\\>)?\\s*\\(",returnBegin:!0,contains:[e.TITLE_MODE,E],relevance:0},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:n,relevance:0,contains:[d,a,e.C_BLOCK_COMMENT_MODE]},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},b]}}}());hljs.registerLanguage("perl",function(){"use strict";return function(e){var n={$pattern:/[\w.]+/,keyword:"getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qq fileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmget sub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedir ioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when"},t={className:"subst",begin:"[$@]\\{",end:"\\}",keywords:n},s={begin:"->{",end:"}"},r={variants:[{begin:/\$\d/},{begin:/[\$%@](\^\w\b|#\w+(::\w+)*|{\w+}|\w+(::\w*)*)/},{begin:/[\$%@][^\s\w{]/,relevance:0}]},i=[e.BACKSLASH_ESCAPE,t,r],a=[r,e.HASH_COMMENT_MODE,e.COMMENT("^\\=\\w","\\=cut",{endsWithParent:!0}),s,{className:"string",contains:i,variants:[{begin:"q[qwxr]?\\s*\\(",end:"\\)",relevance:5},{begin:"q[qwxr]?\\s*\\[",end:"\\]",relevance:5},{begin:"q[qwxr]?\\s*\\{",end:"\\}",relevance:5},{begin:"q[qwxr]?\\s*\\|",end:"\\|",relevance:5},{begin:"q[qwxr]?\\s*\\<",end:"\\>",relevance:5},{begin:"qw\\s+q",end:"q",relevance:5},{begin:"'",end:"'",contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"'},{begin:"`",end:"`",contains:[e.BACKSLASH_ESCAPE]},{begin:"{\\w+}",contains:[],relevance:0},{begin:"-?\\w+\\s*\\=\\>",contains:[],relevance:0}]},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",relevance:0},{begin:"(\\/\\/|"+e.RE_STARTERS_RE+"|\\b(split|return|print|reverse|grep)\\b)\\s*",keywords:"split return print reverse grep",relevance:0,contains:[e.HASH_COMMENT_MODE,{className:"regexp",begin:"(s|tr|y)/(\\\\.|[^/])*/(\\\\.|[^/])*/[a-z]*",relevance:10},{className:"regexp",begin:"(m|qr)?/",end:"/[a-z]*",contains:[e.BACKSLASH_ESCAPE],relevance:0}]},{className:"function",beginKeywords:"sub",end:"(\\s*\\(.*?\\))?[;{]",excludeEnd:!0,relevance:5,contains:[e.TITLE_MODE]},{begin:"-\\w\\b",relevance:0},{begin:"^__DATA__$",end:"^__END__$",subLanguage:"mojolicious",contains:[{begin:"^@@.*",end:"$",className:"comment"}]}];return t.contains=a,s.contains=a,{name:"Perl",aliases:["pl","pm"],keywords:n,contains:a}}}());hljs.registerLanguage("swift",function(){"use strict";return function(e){var i={keyword:"#available #colorLiteral #column #else #elseif #endif #file #fileLiteral #function #if #imageLiteral #line #selector #sourceLocation _ __COLUMN__ __FILE__ __FUNCTION__ __LINE__ Any as as! as? associatedtype associativity break case catch class continue convenience default defer deinit didSet do dynamic dynamicType else enum extension fallthrough false fileprivate final for func get guard if import in indirect infix init inout internal is lazy left let mutating nil none nonmutating open operator optional override postfix precedence prefix private protocol Protocol public repeat required rethrows return right self Self set static struct subscript super switch throw throws true try try! try? Type typealias unowned var weak where while willSet",literal:"true false nil",built_in:"abs advance alignof alignofValue anyGenerator assert assertionFailure bridgeFromObjectiveC bridgeFromObjectiveCUnconditional bridgeToObjectiveC bridgeToObjectiveCUnconditional c compactMap contains count countElements countLeadingZeros debugPrint debugPrintln distance dropFirst dropLast dump encodeBitsAsWords enumerate equal fatalError filter find getBridgedObjectiveCType getVaList indices insertionSort isBridgedToObjectiveC isBridgedVerbatimToObjectiveC isUniquelyReferenced isUniquelyReferencedNonObjC join lazy lexicographicalCompare map max maxElement min minElement numericCast overlaps partition posix precondition preconditionFailure print println quickSort readLine reduce reflect reinterpretCast reverse roundUpToAlignment sizeof sizeofValue sort split startsWith stride strideof strideofValue swap toString transcode underestimateCount unsafeAddressOf unsafeBitCast unsafeDowncast unsafeUnwrap unsafeReflect withExtendedLifetime withObjectAtPlusZero withUnsafePointer withUnsafePointerToObject withUnsafeMutablePointer withUnsafeMutablePointers withUnsafePointer withUnsafePointers withVaList zip"},n=e.COMMENT("/\\*","\\*/",{contains:["self"]}),t={className:"subst",begin:/\\\(/,end:"\\)",keywords:i,contains:[]},a={className:"string",contains:[e.BACKSLASH_ESCAPE,t],variants:[{begin:/"""/,end:/"""/},{begin:/"/,end:/"/}]},r={className:"number",begin:"\\b([\\d_]+(\\.[\\deE_]+)?|0x[a-fA-F0-9_]+(\\.[a-fA-F0-9p_]+)?|0b[01_]+|0o[0-7_]+)\\b",relevance:0};return t.contains=[r],{name:"Swift",keywords:i,contains:[a,e.C_LINE_COMMENT_MODE,n,{className:"type",begin:"\\b[A-Z][\\wÀ-ʸ']*[!?]"},{className:"type",begin:"\\b[A-Z][\\wÀ-ʸ']*",relevance:0},r,{className:"function",beginKeywords:"func",end:"{",excludeEnd:!0,contains:[e.inherit(e.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/}),{begin://},{className:"params",begin:/\(/,end:/\)/,endsParent:!0,keywords:i,contains:["self",r,a,e.C_BLOCK_COMMENT_MODE,{begin:":"}],illegal:/["']/}],illegal:/\[|%/},{className:"class",beginKeywords:"struct protocol class extension enum",keywords:i,end:"\\{",excludeEnd:!0,contains:[e.inherit(e.TITLE_MODE,{begin:/[A-Za-z$_][\u00C0-\u02B80-9A-Za-z$_]*/})]},{className:"meta",begin:"(@discardableResult|@warn_unused_result|@exported|@lazy|@noescape|@NSCopying|@NSManaged|@objc|@objcMembers|@convention|@required|@noreturn|@IBAction|@IBDesignable|@IBInspectable|@IBOutlet|@infix|@prefix|@postfix|@autoclosure|@testable|@available|@nonobjc|@NSApplicationMain|@UIApplicationMain|@dynamicMemberLookup|@propertyWrapper)\\b"},{beginKeywords:"import",end:/$/,contains:[e.C_LINE_COMMENT_MODE,n]}]}}}());hljs.registerLanguage("makefile",function(){"use strict";return function(e){var i={className:"variable",variants:[{begin:"\\$\\("+e.UNDERSCORE_IDENT_RE+"\\)",contains:[e.BACKSLASH_ESCAPE]},{begin:/\$[@%`]+/}]}]}]};return{name:"HTML, XML",aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"],case_insensitive:!0,contains:[{className:"meta",begin:"",relevance:10,contains:[a,i,t,s,{begin:"\\[",end:"\\]",contains:[{className:"meta",begin:"",contains:[a,s,i,t]}]}]},e.COMMENT("\x3c!--","--\x3e",{relevance:10}),{begin:"<\\!\\[CDATA\\[",end:"\\]\\]>",relevance:10},n,{className:"meta",begin:/<\?xml/,end:/\?>/,relevance:10},{className:"tag",begin:")",end:">",keywords:{name:"style"},contains:[c],starts:{end:"",returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:")",end:">",keywords:{name:"script"},contains:[c],starts:{end:"<\/script>",returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{className:"tag",begin:"",contains:[{className:"name",begin:/[^\/><\s]+/,relevance:0},c]}]}}}());hljs.registerLanguage("bash",function(){"use strict";return function(e){const s={};Object.assign(s,{className:"variable",variants:[{begin:/\$[\w\d#@][\w\d_]*/},{begin:/\$\{/,end:/\}/,contains:[{begin:/:-/,contains:[s]}]}]});const t={className:"subst",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE]},n={className:"string",begin:/"/,end:/"/,contains:[e.BACKSLASH_ESCAPE,s,t]};t.contains.push(n);const a={begin:/\$\(\(/,end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},e.NUMBER_MODE,s]},i=e.SHEBANG({binary:"(fish|bash|zsh|sh|csh|ksh|tcsh|dash|scsh)",relevance:10}),c={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,contains:[e.inherit(e.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{name:"Bash",aliases:["sh","zsh"],keywords:{$pattern:/\b-?[a-z\._]+\b/,keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp",_:"-ne -eq -lt -gt -f -d -e -s -l -a"},contains:[i,e.SHEBANG(),c,a,e.HASH_COMMENT_MODE,n,{className:"",begin:/\\"/},{className:"string",begin:/'/,end:/'/},s]}}}());hljs.registerLanguage("c-like",function(){"use strict";return function(e){function t(e){return"(?:"+e+")?"}var n="(decltype\\(auto\\)|"+t("[a-zA-Z_]\\w*::")+"[a-zA-Z_]\\w*"+t("<.*?>")+")",r={className:"keyword",begin:"\\b[a-z\\d_]*_t\\b"},a={className:"string",variants:[{begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)",end:"'",illegal:"."},e.END_SAME_AS_BEGIN({begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},i={className:"number",variants:[{begin:"\\b(0b[01']+)"},{begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],relevance:0},s={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{"meta-keyword":"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include"},contains:[{begin:/\\\n/,relevance:0},e.inherit(a,{className:"meta-string"}),{className:"meta-string",begin:/<.*?>/,end:/$/,illegal:"\\n"},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},o={className:"title",begin:t("[a-zA-Z_]\\w*::")+e.IDENT_RE,relevance:0},c=t("[a-zA-Z_]\\w*::")+e.IDENT_RE+"\\s*\\(",l={keyword:"int float while private char char8_t char16_t char32_t catch import module export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using asm case typeid wchar_t short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignas alignof constexpr consteval constinit decltype concept co_await co_return co_yield requires noexcept static_assert thread_local restrict final override atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return and and_eq bitand bitor compl not not_eq or or_eq xor xor_eq",built_in:"std string wstring cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set pair bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap priority_queue make_pair array shared_ptr abort terminate abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf future isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr _Bool complex _Complex imaginary _Imaginary",literal:"true false nullptr NULL"},d=[r,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,i,a],_={variants:[{begin:/=/,end:/;/},{begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}],keywords:l,contains:d.concat([{begin:/\(/,end:/\)/,keywords:l,contains:d.concat(["self"]),relevance:0}]),relevance:0},u={className:"function",begin:"("+n+"[\\*&\\s]+)+"+c,returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:l,illegal:/[^\w\s\*&:<>]/,contains:[{begin:"decltype\\(auto\\)",keywords:l,relevance:0},{begin:c,returnBegin:!0,contains:[o],relevance:0},{className:"params",begin:/\(/,end:/\)/,keywords:l,relevance:0,contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,i,r,{begin:/\(/,end:/\)/,keywords:l,relevance:0,contains:["self",e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,i,r]}]},r,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,s]};return{aliases:["c","cc","h","c++","h++","hpp","hh","hxx","cxx"],keywords:l,disableAutodetect:!0,illegal:"",keywords:l,contains:["self",r]},{begin:e.IDENT_RE+"::",keywords:l},{className:"class",beginKeywords:"class struct",end:/[{;:]/,contains:[{begin://,contains:["self"]},e.TITLE_MODE]}]),exports:{preprocessor:s,strings:a,keywords:l}}}}());hljs.registerLanguage("coffeescript",function(){"use strict";const e=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],n=["true","false","null","undefined","NaN","Infinity"],a=[].concat(["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],["arguments","this","super","console","window","document","localStorage","module","global"],["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer"],["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"]);return function(r){var t={keyword:e.concat(["then","unless","until","loop","by","when","and","or","is","isnt","not"]).filter((e=>n=>!e.includes(n))(["var","const","let","function","static"])).join(" "),literal:n.concat(["yes","no","on","off"]).join(" "),built_in:a.concat(["npm","print"]).join(" ")},i="[A-Za-z$_][0-9A-Za-z$_]*",s={className:"subst",begin:/#\{/,end:/}/,keywords:t},o=[r.BINARY_NUMBER_MODE,r.inherit(r.C_NUMBER_MODE,{starts:{end:"(\\s*/)?",relevance:0}}),{className:"string",variants:[{begin:/'''/,end:/'''/,contains:[r.BACKSLASH_ESCAPE]},{begin:/'/,end:/'/,contains:[r.BACKSLASH_ESCAPE]},{begin:/"""/,end:/"""/,contains:[r.BACKSLASH_ESCAPE,s]},{begin:/"/,end:/"/,contains:[r.BACKSLASH_ESCAPE,s]}]},{className:"regexp",variants:[{begin:"///",end:"///",contains:[s,r.HASH_COMMENT_MODE]},{begin:"//[gim]{0,3}(?=\\W)",relevance:0},{begin:/\/(?![ *]).*?(?![\\]).\/[gim]{0,3}(?=\W)/}]},{begin:"@"+i},{subLanguage:"javascript",excludeBegin:!0,excludeEnd:!0,variants:[{begin:"```",end:"```"},{begin:"`",end:"`"}]}];s.contains=o;var c=r.inherit(r.TITLE_MODE,{begin:i}),l={className:"params",begin:"\\([^\\(]",returnBegin:!0,contains:[{begin:/\(/,end:/\)/,keywords:t,contains:["self"].concat(o)}]};return{name:"CoffeeScript",aliases:["coffee","cson","iced"],keywords:t,illegal:/\/\*/,contains:o.concat([r.COMMENT("###","###"),r.HASH_COMMENT_MODE,{className:"function",begin:"^\\s*"+i+"\\s*=\\s*(\\(.*\\))?\\s*\\B[-=]>",end:"[-=]>",returnBegin:!0,contains:[c,l]},{begin:/[:\(,=]\s*/,relevance:0,contains:[{className:"function",begin:"(\\(.*\\))?\\s*\\B[-=]>",end:"[-=]>",returnBegin:!0,contains:[l]}]},{className:"class",beginKeywords:"class",end:"$",illegal:/[:="\[\]]/,contains:[{beginKeywords:"extends",endsWithParent:!0,illegal:/[:="\[\]]/,contains:[c]},c]},{begin:i+":",end:":",returnBegin:!0,returnEnd:!0,relevance:0}])}}}());hljs.registerLanguage("ruby",function(){"use strict";return function(e){var n="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?",a={keyword:"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor",literal:"true false nil"},s={className:"doctag",begin:"@[A-Za-z]+"},i={begin:"#<",end:">"},r=[e.COMMENT("#","$",{contains:[s]}),e.COMMENT("^\\=begin","^\\=end",{contains:[s],relevance:10}),e.COMMENT("^__END__","\\n$")],c={className:"subst",begin:"#\\{",end:"}",keywords:a},t={className:"string",contains:[e.BACKSLASH_ESCAPE,c],variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{begin:"%[qQwWx]?\\(",end:"\\)"},{begin:"%[qQwWx]?\\[",end:"\\]"},{begin:"%[qQwWx]?{",end:"}"},{begin:"%[qQwWx]?<",end:">"},{begin:"%[qQwWx]?/",end:"/"},{begin:"%[qQwWx]?%",end:"%"},{begin:"%[qQwWx]?-",end:"-"},{begin:"%[qQwWx]?\\|",end:"\\|"},{begin:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/},{begin:/<<[-~]?'?(\w+)(?:.|\n)*?\n\s*\1\b/,returnBegin:!0,contains:[{begin:/<<[-~]?'?/},e.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/,contains:[e.BACKSLASH_ESCAPE,c]})]}]},b={className:"params",begin:"\\(",end:"\\)",endsParent:!0,keywords:a},d=[t,i,{className:"class",beginKeywords:"class module",end:"$|;",illegal:/=/,contains:[e.inherit(e.TITLE_MODE,{begin:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{begin:"<\\s*",contains:[{begin:"("+e.IDENT_RE+"::)?"+e.IDENT_RE}]}].concat(r)},{className:"function",beginKeywords:"def",end:"$|;",contains:[e.inherit(e.TITLE_MODE,{begin:n}),b].concat(r)},{begin:e.IDENT_RE+"::"},{className:"symbol",begin:e.UNDERSCORE_IDENT_RE+"(\\!|\\?)?:",relevance:0},{className:"symbol",begin:":(?!\\s)",contains:[t,{begin:n}],relevance:0},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",relevance:0},{begin:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{className:"params",begin:/\|/,end:/\|/,keywords:a},{begin:"("+e.RE_STARTERS_RE+"|unless)\\s*",keywords:"unless",contains:[i,{className:"regexp",contains:[e.BACKSLASH_ESCAPE,c],illegal:/\n/,variants:[{begin:"/",end:"/[a-z]*"},{begin:"%r{",end:"}[a-z]*"},{begin:"%r\\(",end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[",end:"\\][a-z]*"}]}].concat(r),relevance:0}].concat(r);c.contains=d,b.contains=d;var g=[{begin:/^\s*=>/,starts:{end:"$",contains:d}},{className:"meta",begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+>|(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>)",starts:{end:"$",contains:d}}];return{name:"Ruby",aliases:["rb","gemspec","podspec","thor","irb"],keywords:a,illegal:/\/\*/,contains:r.concat(g).concat(d)}}}());hljs.registerLanguage("yaml",function(){"use strict";return function(e){var n="true false yes no null",a="[\\w#;/?:@&=+$,.~*\\'()[\\]]+",s={className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/\S+/}],contains:[e.BACKSLASH_ESCAPE,{className:"template-variable",variants:[{begin:"{{",end:"}}"},{begin:"%{",end:"}"}]}]},i=e.inherit(s,{variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),l={end:",",endsWithParent:!0,excludeEnd:!0,contains:[],keywords:n,relevance:0},t={begin:"{",end:"}",contains:[l],illegal:"\\n",relevance:0},g={begin:"\\[",end:"\\]",contains:[l],illegal:"\\n",relevance:0},b=[{className:"attr",variants:[{begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---s*$",relevance:10},{className:"string",begin:"[\\|>]([0-9]?[+-])?[ ]*\\n( *)[\\S ]+\\n(\\2[\\S ]+\\n?)*"},{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:"!\\w+!"+a},{className:"type",begin:"!<"+a+">"},{className:"type",begin:"!"+a},{className:"type",begin:"!!"+a},{className:"meta",begin:"&"+e.UNDERSCORE_IDENT_RE+"$"},{className:"meta",begin:"\\*"+e.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"\\-(?=[ ]|$)",relevance:0},e.HASH_COMMENT_MODE,{beginKeywords:n,keywords:{literal:n}},{className:"number",begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b"},{className:"number",begin:e.C_NUMBER_RE+"\\b"},t,g,s],c=[...b];return c.pop(),c.push(i),l.contains=c,{name:"YAML",case_insensitive:!0,aliases:["yml","YAML"],contains:b}}}());hljs.registerLanguage("d",function(){"use strict";return function(e){var a={$pattern:e.UNDERSCORE_IDENT_RE,keyword:"abstract alias align asm assert auto body break byte case cast catch class const continue debug default delete deprecated do else enum export extern final finally for foreach foreach_reverse|10 goto if immutable import in inout int interface invariant is lazy macro mixin module new nothrow out override package pragma private protected public pure ref return scope shared static struct super switch synchronized template this throw try typedef typeid typeof union unittest version void volatile while with __FILE__ __LINE__ __gshared|10 __thread __traits __DATE__ __EOF__ __TIME__ __TIMESTAMP__ __VENDOR__ __VERSION__",built_in:"bool cdouble cent cfloat char creal dchar delegate double dstring float function idouble ifloat ireal long real short string ubyte ucent uint ulong ushort wchar wstring",literal:"false null true"},d="((0|[1-9][\\d_]*)|0[bB][01_]+|0[xX]([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*))",n="\\\\(['\"\\?\\\\abfnrtv]|u[\\dA-Fa-f]{4}|[0-7]{1,3}|x[\\dA-Fa-f]{2}|U[\\dA-Fa-f]{8})|&[a-zA-Z\\d]{2,};",t={className:"number",begin:"\\b"+d+"(L|u|U|Lu|LU|uL|UL)?",relevance:0},_={className:"number",begin:"\\b(((0[xX](([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*)\\.([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*)|\\.?([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*))[pP][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d))|((0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)(\\.\\d*|([eE][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)))|\\d+\\.(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)|\\.(0|[1-9][\\d_]*)([eE][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d))?))([fF]|L|i|[fF]i|Li)?|"+d+"(i|[fF]i|Li))",relevance:0},r={className:"string",begin:"'("+n+"|.)",end:"'",illegal:"."},i={className:"string",begin:'"',contains:[{begin:n,relevance:0}],end:'"[cwd]?'},s=e.COMMENT("\\/\\+","\\+\\/",{contains:["self"],relevance:10});return{name:"D",keywords:a,contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,s,{className:"string",begin:'x"[\\da-fA-F\\s\\n\\r]*"[cwd]?',relevance:10},i,{className:"string",begin:'[rq]"',end:'"[cwd]?',relevance:5},{className:"string",begin:"`",end:"`[cwd]?"},{className:"string",begin:'q"\\{',end:'\\}"'},_,t,r,{className:"meta",begin:"^#!",end:"$",relevance:5},{className:"meta",begin:"#(line)",end:"$",relevance:5},{className:"keyword",begin:"@[a-zA-Z_][a-zA-Z_\\d]*"}]}}}());hljs.registerLanguage("properties",function(){"use strict";return function(e){var n="[ \\t\\f]*",t="("+n+"[:=]"+n+"|[ \\t\\f]+)",a="([^\\\\:= \\t\\f\\n]|\\\\.)+",s={end:t,relevance:0,starts:{className:"string",end:/$/,relevance:0,contains:[{begin:"\\\\\\n"}]}};return{name:".properties",case_insensitive:!0,illegal:/\S/,contains:[e.COMMENT("^\\s*[!#]","$"),{begin:"([^\\\\\\W:= \\t\\f\\n]|\\\\.)+"+t,returnBegin:!0,contains:[{className:"attr",begin:"([^\\\\\\W:= \\t\\f\\n]|\\\\.)+",endsParent:!0,relevance:0}],starts:s},{begin:a+t,returnBegin:!0,relevance:0,contains:[{className:"meta",begin:a,endsParent:!0,relevance:0}],starts:s},{className:"attr",relevance:0,begin:a+n+"$"}]}}}());hljs.registerLanguage("http",function(){"use strict";return function(e){var n="HTTP/[0-9\\.]+";return{name:"HTTP",aliases:["https"],illegal:"\\S",contains:[{begin:"^"+n,end:"$",contains:[{className:"number",begin:"\\b\\d{3}\\b"}]},{begin:"^[A-Z]+ (.*?) "+n+"$",returnBegin:!0,end:"$",contains:[{className:"string",begin:" ",end:" ",excludeBegin:!0,excludeEnd:!0},{begin:n},{className:"keyword",begin:"[A-Z]+"}]},{className:"attribute",begin:"^\\w",end:": ",excludeEnd:!0,illegal:"\\n|\\s|=",starts:{end:"$",relevance:0}},{begin:"\\n\\n",starts:{subLanguage:[],endsWithParent:!0}}]}}}());hljs.registerLanguage("haskell",function(){"use strict";return function(e){var n={variants:[e.COMMENT("--","$"),e.COMMENT("{-","-}",{contains:["self"]})]},i={className:"meta",begin:"{-#",end:"#-}"},a={className:"meta",begin:"^#",end:"$"},s={className:"type",begin:"\\b[A-Z][\\w']*",relevance:0},l={begin:"\\(",end:"\\)",illegal:'"',contains:[i,a,{className:"type",begin:"\\b[A-Z][\\w]*(\\((\\.\\.|,|\\w+)\\))?"},e.inherit(e.TITLE_MODE,{begin:"[_a-z][\\w']*"}),n]};return{name:"Haskell",aliases:["hs"],keywords:"let in if then else case of where do module import hiding qualified type data newtype deriving class instance as default infix infixl infixr foreign export ccall stdcall cplusplus jvm dotnet safe unsafe family forall mdo proc rec",contains:[{beginKeywords:"module",end:"where",keywords:"module where",contains:[l,n],illegal:"\\W\\.|;"},{begin:"\\bimport\\b",end:"$",keywords:"import qualified as hiding",contains:[l,n],illegal:"\\W\\.|;"},{className:"class",begin:"^(\\s*)?(class|instance)\\b",end:"where",keywords:"class family instance where",contains:[s,l,n]},{className:"class",begin:"\\b(data|(new)?type)\\b",end:"$",keywords:"data family type newtype deriving",contains:[i,s,l,{begin:"{",end:"}",contains:l.contains},n]},{beginKeywords:"default",end:"$",contains:[s,l,n]},{beginKeywords:"infix infixl infixr",end:"$",contains:[e.C_NUMBER_MODE,n]},{begin:"\\bforeign\\b",end:"$",keywords:"foreign import export ccall stdcall cplusplus jvm dotnet safe unsafe",contains:[s,e.QUOTE_STRING_MODE,n]},{className:"meta",begin:"#!\\/usr\\/bin\\/env runhaskell",end:"$"},i,a,e.QUOTE_STRING_MODE,e.C_NUMBER_MODE,s,e.inherit(e.TITLE_MODE,{begin:"^[_a-z][\\w']*"}),n,{begin:"->|<-"}]}}}());hljs.registerLanguage("handlebars",function(){"use strict";function e(...e){return e.map(e=>(function(e){return e?"string"==typeof e?e:e.source:null})(e)).join("")}return function(n){const a={"builtin-name":"action bindattr collection component concat debugger each each-in get hash if in input link-to loc log lookup mut outlet partial query-params render template textarea unbound unless view with yield"},t=/\[.*?\]/,s=/[^\s!"#%&'()*+,.\/;<=>@\[\\\]^`{|}~]+/,i=e("(",/'.*?'/,"|",/".*?"/,"|",t,"|",s,"|",/\.|\//,")+"),r=e("(",t,"|",s,")(?==)"),l={begin:i,lexemes:/[\w.\/]+/},c=n.inherit(l,{keywords:{literal:"true false undefined null"}}),o={begin:/\(/,end:/\)/},m={className:"attr",begin:r,relevance:0,starts:{begin:/=/,end:/=/,starts:{contains:[n.NUMBER_MODE,n.QUOTE_STRING_MODE,n.APOS_STRING_MODE,c,o]}}},d={contains:[n.NUMBER_MODE,n.QUOTE_STRING_MODE,n.APOS_STRING_MODE,{begin:/as\s+\|/,keywords:{keyword:"as"},end:/\|/,contains:[{begin:/\w+/}]},m,c,o],returnEnd:!0},g=n.inherit(l,{className:"name",keywords:a,starts:n.inherit(d,{end:/\)/})});o.contains=[g];const u=n.inherit(l,{keywords:a,className:"name",starts:n.inherit(d,{end:/}}/})}),b=n.inherit(l,{keywords:a,className:"name"}),h=n.inherit(l,{className:"name",keywords:a,starts:n.inherit(d,{end:/}}/})});return{name:"Handlebars",aliases:["hbs","html.hbs","html.handlebars","htmlbars"],case_insensitive:!0,subLanguage:"xml",contains:[{begin:/\\\{\{/,skip:!0},{begin:/\\\\(?=\{\{)/,skip:!0},n.COMMENT(/\{\{!--/,/--\}\}/),n.COMMENT(/\{\{!/,/\}\}/),{className:"template-tag",begin:/\{\{\{\{(?!\/)/,end:/\}\}\}\}/,contains:[u],starts:{end:/\{\{\{\{\//,returnEnd:!0,subLanguage:"xml"}},{className:"template-tag",begin:/\{\{\{\{\//,end:/\}\}\}\}/,contains:[b]},{className:"template-tag",begin:/\{\{#/,end:/\}\}/,contains:[u]},{className:"template-tag",begin:/\{\{(?=else\}\})/,end:/\}\}/,keywords:"else"},{className:"template-tag",begin:/\{\{\//,end:/\}\}/,contains:[b]},{className:"template-variable",begin:/\{\{\{/,end:/\}\}\}/,contains:[h]},{className:"template-variable",begin:/\{\{/,end:/\}\}/,contains:[h]}]}}}());hljs.registerLanguage("rust",function(){"use strict";return function(e){var n="([ui](8|16|32|64|128|size)|f(32|64))?",t="drop i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize f32 f64 str char bool Box Option Result String Vec Copy Send Sized Sync Drop Fn FnMut FnOnce ToOwned Clone Debug PartialEq PartialOrd Eq Ord AsRef AsMut Into From Default Iterator Extend IntoIterator DoubleEndedIterator ExactSizeIterator SliceConcatExt ToString assert! assert_eq! bitflags! bytes! cfg! col! concat! concat_idents! debug_assert! debug_assert_eq! env! panic! file! format! format_args! include_bin! include_str! line! local_data_key! module_path! option_env! print! println! select! stringify! try! unimplemented! unreachable! vec! write! writeln! macro_rules! assert_ne! debug_assert_ne!";return{name:"Rust",aliases:["rs"],keywords:{$pattern:e.IDENT_RE+"!?",keyword:"abstract as async await become box break const continue crate do dyn else enum extern false final fn for if impl in let loop macro match mod move mut override priv pub ref return self Self static struct super trait true try type typeof unsafe unsized use virtual where while yield",literal:"true false Some None Ok Err",built_in:t},illegal:""}]}}}());hljs.registerLanguage("cpp",function(){"use strict";return function(e){var t=e.getLanguage("c-like").rawDefinition();return t.disableAutodetect=!1,t.name="C++",t.aliases=["cc","c++","h++","hpp","hh","hxx","cxx"],t}}());hljs.registerLanguage("ini",function(){"use strict";function e(e){return e?"string"==typeof e?e:e.source:null}function n(...n){return n.map(n=>e(n)).join("")}return function(a){var s={className:"number",relevance:0,variants:[{begin:/([\+\-]+)?[\d]+_[\d_]+/},{begin:a.NUMBER_RE}]},i=a.COMMENT();i.variants=[{begin:/;/,end:/$/},{begin:/#/,end:/$/}];var t={className:"variable",variants:[{begin:/\$[\w\d"][\w\d_]*/},{begin:/\$\{(.*?)}/}]},r={className:"literal",begin:/\bon|off|true|false|yes|no\b/},l={className:"string",contains:[a.BACKSLASH_ESCAPE],variants:[{begin:"'''",end:"'''",relevance:10},{begin:'"""',end:'"""',relevance:10},{begin:'"',end:'"'},{begin:"'",end:"'"}]},c={begin:/\[/,end:/\]/,contains:[i,r,t,l,s,"self"],relevance:0},g="("+[/[A-Za-z0-9_-]+/,/"(\\"|[^"])*"/,/'[^']*'/].map(n=>e(n)).join("|")+")";return{name:"TOML, also INI",aliases:["toml"],case_insensitive:!0,illegal:/\S/,contains:[i,{className:"section",begin:/\[+/,end:/\]+/},{begin:n(g,"(\\s*\\.\\s*",g,")*",n("(?=",/\s*=\s*[^#\s]/,")")),className:"attr",starts:{end:/$/,contains:[i,c,r,t,l,s]}}]}}}());hljs.registerLanguage("objectivec",function(){"use strict";return function(e){var n=/[a-zA-Z@][a-zA-Z0-9_]*/,_={$pattern:n,keyword:"@interface @class @protocol @implementation"};return{name:"Objective-C",aliases:["mm","objc","obj-c"],keywords:{$pattern:n,keyword:"int float while char export sizeof typedef const struct for union unsigned long volatile static bool mutable if do return goto void enum else break extern asm case short default double register explicit signed typename this switch continue wchar_t inline readonly assign readwrite self @synchronized id typeof nonatomic super unichar IBOutlet IBAction strong weak copy in out inout bycopy byref oneway __strong __weak __block __autoreleasing @private @protected @public @try @property @end @throw @catch @finally @autoreleasepool @synthesize @dynamic @selector @optional @required @encode @package @import @defs @compatibility_alias __bridge __bridge_transfer __bridge_retained __bridge_retain __covariant __contravariant __kindof _Nonnull _Nullable _Null_unspecified __FUNCTION__ __PRETTY_FUNCTION__ __attribute__ getter setter retain unsafe_unretained nonnull nullable null_unspecified null_resettable class instancetype NS_DESIGNATED_INITIALIZER NS_UNAVAILABLE NS_REQUIRES_SUPER NS_RETURNS_INNER_POINTER NS_INLINE NS_AVAILABLE NS_DEPRECATED NS_ENUM NS_OPTIONS NS_SWIFT_UNAVAILABLE NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END NS_REFINED_FOR_SWIFT NS_SWIFT_NAME NS_SWIFT_NOTHROW NS_DURING NS_HANDLER NS_ENDHANDLER NS_VALUERETURN NS_VOIDRETURN",literal:"false true FALSE TRUE nil YES NO NULL",built_in:"BOOL dispatch_once_t dispatch_queue_t dispatch_sync dispatch_async dispatch_once"},illegal:"/,end:/$/,illegal:"\\n"},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"class",begin:"("+_.keyword.split(" ").join("|")+")\\b",end:"({|$)",excludeEnd:!0,keywords:_,contains:[e.UNDERSCORE_TITLE_MODE]},{begin:"\\."+e.UNDERSCORE_IDENT_RE,relevance:0}]}}}());hljs.registerLanguage("apache",function(){"use strict";return function(e){var n={className:"number",begin:"\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?"};return{name:"Apache config",aliases:["apacheconf"],case_insensitive:!0,contains:[e.HASH_COMMENT_MODE,{className:"section",begin:"",contains:[n,{className:"number",begin:":\\d{1,5}"},e.inherit(e.QUOTE_STRING_MODE,{relevance:0})]},{className:"attribute",begin:/\w+/,relevance:0,keywords:{nomarkup:"order deny allow setenv rewriterule rewriteengine rewritecond documentroot sethandler errordocument loadmodule options header listen serverroot servername"},starts:{end:/$/,relevance:0,keywords:{literal:"on off all deny allow"},contains:[{className:"meta",begin:"\\s\\[",end:"\\]$"},{className:"variable",begin:"[\\$%]\\{",end:"\\}",contains:["self",{className:"number",begin:"[\\$%]\\d+"}]},n,{className:"number",begin:"\\d+"},e.QUOTE_STRING_MODE]}}],illegal:/\S/}}}());hljs.registerLanguage("java",function(){"use strict";function e(e){return e?"string"==typeof e?e:e.source:null}function n(e){return a("(",e,")?")}function a(...n){return n.map(n=>e(n)).join("")}function s(...n){return"("+n.map(n=>e(n)).join("|")+")"}return function(e){var t="false synchronized int abstract float private char boolean var static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do",i={className:"meta",begin:"@[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*",contains:[{begin:/\(/,end:/\)/,contains:["self"]}]},r=e=>a("[",e,"]+([",e,"_]*[",e,"]+)?"),c={className:"number",variants:[{begin:`\\b(0[bB]${r("01")})[lL]?`},{begin:`\\b(0${r("0-7")})[dDfFlL]?`},{begin:a(/\b0[xX]/,s(a(r("a-fA-F0-9"),/\./,r("a-fA-F0-9")),a(r("a-fA-F0-9"),/\.?/),a(/\./,r("a-fA-F0-9"))),/([pP][+-]?(\d+))?/,/[fFdDlL]?/)},{begin:a(/\b/,s(a(/\d*\./,r("\\d")),r("\\d")),/[eE][+-]?[\d]+[dDfF]?/)},{begin:a(/\b/,r(/\d/),n(/\.?/),n(r(/\d/)),/[dDfFlL]?/)}],relevance:0};return{name:"Java",aliases:["jsp"],keywords:t,illegal:/<\/|#/,contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/,relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{className:"class",beginKeywords:"class interface",end:/[{;=]/,excludeEnd:!0,keywords:"class interface",illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE]},{beginKeywords:"new throw return else",relevance:0},{className:"function",begin:"([À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*(<[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*(\\s*,\\s*[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*)*>)?\\s+)+"+e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:t,contains:[{begin:e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0,contains:[e.UNDERSCORE_TITLE_MODE]},{className:"params",begin:/\(/,end:/\)/,keywords:t,relevance:0,contains:[i,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,e.C_NUMBER_MODE,e.C_BLOCK_COMMENT_MODE]},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},c,i]}}}());hljs.registerLanguage("x86asm",function(){"use strict";return function(s){return{name:"Intel x86 Assembly",case_insensitive:!0,keywords:{$pattern:"[.%]?"+s.IDENT_RE,keyword:"lock rep repe repz repne repnz xaquire xrelease bnd nobnd aaa aad aam aas adc add and arpl bb0_reset bb1_reset bound bsf bsr bswap bt btc btr bts call cbw cdq cdqe clc cld cli clts cmc cmp cmpsb cmpsd cmpsq cmpsw cmpxchg cmpxchg486 cmpxchg8b cmpxchg16b cpuid cpu_read cpu_write cqo cwd cwde daa das dec div dmint emms enter equ f2xm1 fabs fadd faddp fbld fbstp fchs fclex fcmovb fcmovbe fcmove fcmovnb fcmovnbe fcmovne fcmovnu fcmovu fcom fcomi fcomip fcomp fcompp fcos fdecstp fdisi fdiv fdivp fdivr fdivrp femms feni ffree ffreep fiadd ficom ficomp fidiv fidivr fild fimul fincstp finit fist fistp fisttp fisub fisubr fld fld1 fldcw fldenv fldl2e fldl2t fldlg2 fldln2 fldpi fldz fmul fmulp fnclex fndisi fneni fninit fnop fnsave fnstcw fnstenv fnstsw fpatan fprem fprem1 fptan frndint frstor fsave fscale fsetpm fsin fsincos fsqrt fst fstcw fstenv fstp fstsw fsub fsubp fsubr fsubrp ftst fucom fucomi fucomip fucomp fucompp fxam fxch fxtract fyl2x fyl2xp1 hlt ibts icebp idiv imul in inc incbin insb insd insw int int01 int1 int03 int3 into invd invpcid invlpg invlpga iret iretd iretq iretw jcxz jecxz jrcxz jmp jmpe lahf lar lds lea leave les lfence lfs lgdt lgs lidt lldt lmsw loadall loadall286 lodsb lodsd lodsq lodsw loop loope loopne loopnz loopz lsl lss ltr mfence monitor mov movd movq movsb movsd movsq movsw movsx movsxd movzx mul mwait neg nop not or out outsb outsd outsw packssdw packsswb packuswb paddb paddd paddsb paddsiw paddsw paddusb paddusw paddw pand pandn pause paveb pavgusb pcmpeqb pcmpeqd pcmpeqw pcmpgtb pcmpgtd pcmpgtw pdistib pf2id pfacc pfadd pfcmpeq pfcmpge pfcmpgt pfmax pfmin pfmul pfrcp pfrcpit1 pfrcpit2 pfrsqit1 pfrsqrt pfsub pfsubr pi2fd pmachriw pmaddwd pmagw pmulhriw pmulhrwa pmulhrwc pmulhw pmullw pmvgezb pmvlzb pmvnzb pmvzb pop popa popad popaw popf popfd popfq popfw por prefetch prefetchw pslld psllq psllw psrad psraw psrld psrlq psrlw psubb psubd psubsb psubsiw psubsw psubusb psubusw psubw punpckhbw punpckhdq punpckhwd punpcklbw punpckldq punpcklwd push pusha pushad pushaw pushf pushfd pushfq pushfw pxor rcl rcr rdshr rdmsr rdpmc rdtsc rdtscp ret retf retn rol ror rdm rsdc rsldt rsm rsts sahf sal salc sar sbb scasb scasd scasq scasw sfence sgdt shl shld shr shrd sidt sldt skinit smi smint smintold smsw stc std sti stosb stosd stosq stosw str sub svdc svldt svts swapgs syscall sysenter sysexit sysret test ud0 ud1 ud2b ud2 ud2a umov verr verw fwait wbinvd wrshr wrmsr xadd xbts xchg xlatb xlat xor cmove cmovz cmovne cmovnz cmova cmovnbe cmovae cmovnb cmovb cmovnae cmovbe cmovna cmovg cmovnle cmovge cmovnl cmovl cmovnge cmovle cmovng cmovc cmovnc cmovo cmovno cmovs cmovns cmovp cmovpe cmovnp cmovpo je jz jne jnz ja jnbe jae jnb jb jnae jbe jna jg jnle jge jnl jl jnge jle jng jc jnc jo jno js jns jpo jnp jpe jp sete setz setne setnz seta setnbe setae setnb setnc setb setnae setcset setbe setna setg setnle setge setnl setl setnge setle setng sets setns seto setno setpe setp setpo setnp addps addss andnps andps cmpeqps cmpeqss cmpleps cmpless cmpltps cmpltss cmpneqps cmpneqss cmpnleps cmpnless cmpnltps cmpnltss cmpordps cmpordss cmpunordps cmpunordss cmpps cmpss comiss cvtpi2ps cvtps2pi cvtsi2ss cvtss2si cvttps2pi cvttss2si divps divss ldmxcsr maxps maxss minps minss movaps movhps movlhps movlps movhlps movmskps movntps movss movups mulps mulss orps rcpps rcpss rsqrtps rsqrtss shufps sqrtps sqrtss stmxcsr subps subss ucomiss unpckhps unpcklps xorps fxrstor fxrstor64 fxsave fxsave64 xgetbv xsetbv xsave xsave64 xsaveopt xsaveopt64 xrstor xrstor64 prefetchnta prefetcht0 prefetcht1 prefetcht2 maskmovq movntq pavgb pavgw pextrw pinsrw pmaxsw pmaxub pminsw pminub pmovmskb pmulhuw psadbw pshufw pf2iw pfnacc pfpnacc pi2fw pswapd maskmovdqu clflush movntdq movnti movntpd movdqa movdqu movdq2q movq2dq paddq pmuludq pshufd pshufhw pshuflw pslldq psrldq psubq punpckhqdq punpcklqdq addpd addsd andnpd andpd cmpeqpd cmpeqsd cmplepd cmplesd cmpltpd cmpltsd cmpneqpd cmpneqsd cmpnlepd cmpnlesd cmpnltpd cmpnltsd cmpordpd cmpordsd cmpunordpd cmpunordsd cmppd comisd cvtdq2pd cvtdq2ps cvtpd2dq cvtpd2pi cvtpd2ps cvtpi2pd cvtps2dq cvtps2pd cvtsd2si cvtsd2ss cvtsi2sd cvtss2sd cvttpd2pi cvttpd2dq cvttps2dq cvttsd2si divpd divsd maxpd maxsd minpd minsd movapd movhpd movlpd movmskpd movupd mulpd mulsd orpd shufpd sqrtpd sqrtsd subpd subsd ucomisd unpckhpd unpcklpd xorpd addsubpd addsubps haddpd haddps hsubpd hsubps lddqu movddup movshdup movsldup clgi stgi vmcall vmclear vmfunc vmlaunch vmload vmmcall vmptrld vmptrst vmread vmresume vmrun vmsave vmwrite vmxoff vmxon invept invvpid pabsb pabsw pabsd palignr phaddw phaddd phaddsw phsubw phsubd phsubsw pmaddubsw pmulhrsw pshufb psignb psignw psignd extrq insertq movntsd movntss lzcnt blendpd blendps blendvpd blendvps dppd dpps extractps insertps movntdqa mpsadbw packusdw pblendvb pblendw pcmpeqq pextrb pextrd pextrq phminposuw pinsrb pinsrd pinsrq pmaxsb pmaxsd pmaxud pmaxuw pminsb pminsd pminud pminuw pmovsxbw pmovsxbd pmovsxbq pmovsxwd pmovsxwq pmovsxdq pmovzxbw pmovzxbd pmovzxbq pmovzxwd pmovzxwq pmovzxdq pmuldq pmulld ptest roundpd roundps roundsd roundss crc32 pcmpestri pcmpestrm pcmpistri pcmpistrm pcmpgtq popcnt getsec pfrcpv pfrsqrtv movbe aesenc aesenclast aesdec aesdeclast aesimc aeskeygenassist vaesenc vaesenclast vaesdec vaesdeclast vaesimc vaeskeygenassist vaddpd vaddps vaddsd vaddss vaddsubpd vaddsubps vandpd vandps vandnpd vandnps vblendpd vblendps vblendvpd vblendvps vbroadcastss vbroadcastsd vbroadcastf128 vcmpeq_ospd vcmpeqpd vcmplt_ospd vcmpltpd vcmple_ospd vcmplepd vcmpunord_qpd vcmpunordpd vcmpneq_uqpd vcmpneqpd vcmpnlt_uspd vcmpnltpd vcmpnle_uspd vcmpnlepd vcmpord_qpd vcmpordpd vcmpeq_uqpd vcmpnge_uspd vcmpngepd vcmpngt_uspd vcmpngtpd vcmpfalse_oqpd vcmpfalsepd vcmpneq_oqpd vcmpge_ospd vcmpgepd vcmpgt_ospd vcmpgtpd vcmptrue_uqpd vcmptruepd vcmplt_oqpd vcmple_oqpd vcmpunord_spd vcmpneq_uspd vcmpnlt_uqpd vcmpnle_uqpd vcmpord_spd vcmpeq_uspd vcmpnge_uqpd vcmpngt_uqpd vcmpfalse_ospd vcmpneq_ospd vcmpge_oqpd vcmpgt_oqpd vcmptrue_uspd vcmppd vcmpeq_osps vcmpeqps vcmplt_osps vcmpltps vcmple_osps vcmpleps vcmpunord_qps vcmpunordps vcmpneq_uqps vcmpneqps vcmpnlt_usps vcmpnltps vcmpnle_usps vcmpnleps vcmpord_qps vcmpordps vcmpeq_uqps vcmpnge_usps vcmpngeps vcmpngt_usps vcmpngtps vcmpfalse_oqps vcmpfalseps vcmpneq_oqps vcmpge_osps vcmpgeps vcmpgt_osps vcmpgtps vcmptrue_uqps vcmptrueps vcmplt_oqps vcmple_oqps vcmpunord_sps vcmpneq_usps vcmpnlt_uqps vcmpnle_uqps vcmpord_sps vcmpeq_usps vcmpnge_uqps vcmpngt_uqps vcmpfalse_osps vcmpneq_osps vcmpge_oqps vcmpgt_oqps vcmptrue_usps vcmpps vcmpeq_ossd vcmpeqsd vcmplt_ossd vcmpltsd vcmple_ossd vcmplesd vcmpunord_qsd vcmpunordsd vcmpneq_uqsd vcmpneqsd vcmpnlt_ussd vcmpnltsd vcmpnle_ussd vcmpnlesd vcmpord_qsd vcmpordsd vcmpeq_uqsd vcmpnge_ussd vcmpngesd vcmpngt_ussd vcmpngtsd vcmpfalse_oqsd vcmpfalsesd vcmpneq_oqsd vcmpge_ossd vcmpgesd vcmpgt_ossd vcmpgtsd vcmptrue_uqsd vcmptruesd vcmplt_oqsd vcmple_oqsd vcmpunord_ssd vcmpneq_ussd vcmpnlt_uqsd vcmpnle_uqsd vcmpord_ssd vcmpeq_ussd vcmpnge_uqsd vcmpngt_uqsd vcmpfalse_ossd vcmpneq_ossd vcmpge_oqsd vcmpgt_oqsd vcmptrue_ussd vcmpsd vcmpeq_osss vcmpeqss vcmplt_osss vcmpltss vcmple_osss vcmpless vcmpunord_qss vcmpunordss vcmpneq_uqss vcmpneqss vcmpnlt_usss vcmpnltss vcmpnle_usss vcmpnless vcmpord_qss vcmpordss vcmpeq_uqss vcmpnge_usss vcmpngess vcmpngt_usss vcmpngtss vcmpfalse_oqss vcmpfalsess vcmpneq_oqss vcmpge_osss vcmpgess vcmpgt_osss vcmpgtss vcmptrue_uqss vcmptruess vcmplt_oqss vcmple_oqss vcmpunord_sss vcmpneq_usss vcmpnlt_uqss vcmpnle_uqss vcmpord_sss vcmpeq_usss vcmpnge_uqss vcmpngt_uqss vcmpfalse_osss vcmpneq_osss vcmpge_oqss vcmpgt_oqss vcmptrue_usss vcmpss vcomisd vcomiss vcvtdq2pd vcvtdq2ps vcvtpd2dq vcvtpd2ps vcvtps2dq vcvtps2pd vcvtsd2si vcvtsd2ss vcvtsi2sd vcvtsi2ss vcvtss2sd vcvtss2si vcvttpd2dq vcvttps2dq vcvttsd2si vcvttss2si vdivpd vdivps vdivsd vdivss vdppd vdpps vextractf128 vextractps vhaddpd vhaddps vhsubpd vhsubps vinsertf128 vinsertps vlddqu vldqqu vldmxcsr vmaskmovdqu vmaskmovps vmaskmovpd vmaxpd vmaxps vmaxsd vmaxss vminpd vminps vminsd vminss vmovapd vmovaps vmovd vmovq vmovddup vmovdqa vmovqqa vmovdqu vmovqqu vmovhlps vmovhpd vmovhps vmovlhps vmovlpd vmovlps vmovmskpd vmovmskps vmovntdq vmovntqq vmovntdqa vmovntpd vmovntps vmovsd vmovshdup vmovsldup vmovss vmovupd vmovups vmpsadbw vmulpd vmulps vmulsd vmulss vorpd vorps vpabsb vpabsw vpabsd vpacksswb vpackssdw vpackuswb vpackusdw vpaddb vpaddw vpaddd vpaddq vpaddsb vpaddsw vpaddusb vpaddusw vpalignr vpand vpandn vpavgb vpavgw vpblendvb vpblendw vpcmpestri vpcmpestrm vpcmpistri vpcmpistrm vpcmpeqb vpcmpeqw vpcmpeqd vpcmpeqq vpcmpgtb vpcmpgtw vpcmpgtd vpcmpgtq vpermilpd vpermilps vperm2f128 vpextrb vpextrw vpextrd vpextrq vphaddw vphaddd vphaddsw vphminposuw vphsubw vphsubd vphsubsw vpinsrb vpinsrw vpinsrd vpinsrq vpmaddwd vpmaddubsw vpmaxsb vpmaxsw vpmaxsd vpmaxub vpmaxuw vpmaxud vpminsb vpminsw vpminsd vpminub vpminuw vpminud vpmovmskb vpmovsxbw vpmovsxbd vpmovsxbq vpmovsxwd vpmovsxwq vpmovsxdq vpmovzxbw vpmovzxbd vpmovzxbq vpmovzxwd vpmovzxwq vpmovzxdq vpmulhuw vpmulhrsw vpmulhw vpmullw vpmulld vpmuludq vpmuldq vpor vpsadbw vpshufb vpshufd vpshufhw vpshuflw vpsignb vpsignw vpsignd vpslldq vpsrldq vpsllw vpslld vpsllq vpsraw vpsrad vpsrlw vpsrld vpsrlq vptest vpsubb vpsubw vpsubd vpsubq vpsubsb vpsubsw vpsubusb vpsubusw vpunpckhbw vpunpckhwd vpunpckhdq vpunpckhqdq vpunpcklbw vpunpcklwd vpunpckldq vpunpcklqdq vpxor vrcpps vrcpss vrsqrtps vrsqrtss vroundpd vroundps vroundsd vroundss vshufpd vshufps vsqrtpd vsqrtps vsqrtsd vsqrtss vstmxcsr vsubpd vsubps vsubsd vsubss vtestps vtestpd vucomisd vucomiss vunpckhpd vunpckhps vunpcklpd vunpcklps vxorpd vxorps vzeroall vzeroupper pclmullqlqdq pclmulhqlqdq pclmullqhqdq pclmulhqhqdq pclmulqdq vpclmullqlqdq vpclmulhqlqdq vpclmullqhqdq vpclmulhqhqdq vpclmulqdq vfmadd132ps vfmadd132pd vfmadd312ps vfmadd312pd vfmadd213ps vfmadd213pd vfmadd123ps vfmadd123pd vfmadd231ps vfmadd231pd vfmadd321ps vfmadd321pd vfmaddsub132ps vfmaddsub132pd vfmaddsub312ps vfmaddsub312pd vfmaddsub213ps vfmaddsub213pd vfmaddsub123ps vfmaddsub123pd vfmaddsub231ps vfmaddsub231pd vfmaddsub321ps vfmaddsub321pd vfmsub132ps vfmsub132pd vfmsub312ps vfmsub312pd vfmsub213ps vfmsub213pd vfmsub123ps vfmsub123pd vfmsub231ps vfmsub231pd vfmsub321ps vfmsub321pd vfmsubadd132ps vfmsubadd132pd vfmsubadd312ps vfmsubadd312pd vfmsubadd213ps vfmsubadd213pd vfmsubadd123ps vfmsubadd123pd vfmsubadd231ps vfmsubadd231pd vfmsubadd321ps vfmsubadd321pd vfnmadd132ps vfnmadd132pd vfnmadd312ps vfnmadd312pd vfnmadd213ps vfnmadd213pd vfnmadd123ps vfnmadd123pd vfnmadd231ps vfnmadd231pd vfnmadd321ps vfnmadd321pd vfnmsub132ps vfnmsub132pd vfnmsub312ps vfnmsub312pd vfnmsub213ps vfnmsub213pd vfnmsub123ps vfnmsub123pd vfnmsub231ps vfnmsub231pd vfnmsub321ps vfnmsub321pd vfmadd132ss vfmadd132sd vfmadd312ss vfmadd312sd vfmadd213ss vfmadd213sd vfmadd123ss vfmadd123sd vfmadd231ss vfmadd231sd vfmadd321ss vfmadd321sd vfmsub132ss vfmsub132sd vfmsub312ss vfmsub312sd vfmsub213ss vfmsub213sd vfmsub123ss vfmsub123sd vfmsub231ss vfmsub231sd vfmsub321ss vfmsub321sd vfnmadd132ss vfnmadd132sd vfnmadd312ss vfnmadd312sd vfnmadd213ss vfnmadd213sd vfnmadd123ss vfnmadd123sd vfnmadd231ss vfnmadd231sd vfnmadd321ss vfnmadd321sd vfnmsub132ss vfnmsub132sd vfnmsub312ss vfnmsub312sd vfnmsub213ss vfnmsub213sd vfnmsub123ss vfnmsub123sd vfnmsub231ss vfnmsub231sd vfnmsub321ss vfnmsub321sd rdfsbase rdgsbase rdrand wrfsbase wrgsbase vcvtph2ps vcvtps2ph adcx adox rdseed clac stac xstore xcryptecb xcryptcbc xcryptctr xcryptcfb xcryptofb montmul xsha1 xsha256 llwpcb slwpcb lwpval lwpins vfmaddpd vfmaddps vfmaddsd vfmaddss vfmaddsubpd vfmaddsubps vfmsubaddpd vfmsubaddps vfmsubpd vfmsubps vfmsubsd vfmsubss vfnmaddpd vfnmaddps vfnmaddsd vfnmaddss vfnmsubpd vfnmsubps vfnmsubsd vfnmsubss vfrczpd vfrczps vfrczsd vfrczss vpcmov vpcomb vpcomd vpcomq vpcomub vpcomud vpcomuq vpcomuw vpcomw vphaddbd vphaddbq vphaddbw vphadddq vphaddubd vphaddubq vphaddubw vphaddudq vphadduwd vphadduwq vphaddwd vphaddwq vphsubbw vphsubdq vphsubwd vpmacsdd vpmacsdqh vpmacsdql vpmacssdd vpmacssdqh vpmacssdql vpmacsswd vpmacssww vpmacswd vpmacsww vpmadcsswd vpmadcswd vpperm vprotb vprotd vprotq vprotw vpshab vpshad vpshaq vpshaw vpshlb vpshld vpshlq vpshlw vbroadcasti128 vpblendd vpbroadcastb vpbroadcastw vpbroadcastd vpbroadcastq vpermd vpermpd vpermps vpermq vperm2i128 vextracti128 vinserti128 vpmaskmovd vpmaskmovq vpsllvd vpsllvq vpsravd vpsrlvd vpsrlvq vgatherdpd vgatherqpd vgatherdps vgatherqps vpgatherdd vpgatherqd vpgatherdq vpgatherqq xabort xbegin xend xtest andn bextr blci blcic blsi blsic blcfill blsfill blcmsk blsmsk blsr blcs bzhi mulx pdep pext rorx sarx shlx shrx tzcnt tzmsk t1mskc valignd valignq vblendmpd vblendmps vbroadcastf32x4 vbroadcastf64x4 vbroadcasti32x4 vbroadcasti64x4 vcompresspd vcompressps vcvtpd2udq vcvtps2udq vcvtsd2usi vcvtss2usi vcvttpd2udq vcvttps2udq vcvttsd2usi vcvttss2usi vcvtudq2pd vcvtudq2ps vcvtusi2sd vcvtusi2ss vexpandpd vexpandps vextractf32x4 vextractf64x4 vextracti32x4 vextracti64x4 vfixupimmpd vfixupimmps vfixupimmsd vfixupimmss vgetexppd vgetexpps vgetexpsd vgetexpss vgetmantpd vgetmantps vgetmantsd vgetmantss vinsertf32x4 vinsertf64x4 vinserti32x4 vinserti64x4 vmovdqa32 vmovdqa64 vmovdqu32 vmovdqu64 vpabsq vpandd vpandnd vpandnq vpandq vpblendmd vpblendmq vpcmpltd vpcmpled vpcmpneqd vpcmpnltd vpcmpnled vpcmpd vpcmpltq vpcmpleq vpcmpneqq vpcmpnltq vpcmpnleq vpcmpq vpcmpequd vpcmpltud vpcmpleud vpcmpnequd vpcmpnltud vpcmpnleud vpcmpud vpcmpequq vpcmpltuq vpcmpleuq vpcmpnequq vpcmpnltuq vpcmpnleuq vpcmpuq vpcompressd vpcompressq vpermi2d vpermi2pd vpermi2ps vpermi2q vpermt2d vpermt2pd vpermt2ps vpermt2q vpexpandd vpexpandq vpmaxsq vpmaxuq vpminsq vpminuq vpmovdb vpmovdw vpmovqb vpmovqd vpmovqw vpmovsdb vpmovsdw vpmovsqb vpmovsqd vpmovsqw vpmovusdb vpmovusdw vpmovusqb vpmovusqd vpmovusqw vpord vporq vprold vprolq vprolvd vprolvq vprord vprorq vprorvd vprorvq vpscatterdd vpscatterdq vpscatterqd vpscatterqq vpsraq vpsravq vpternlogd vpternlogq vptestmd vptestmq vptestnmd vptestnmq vpxord vpxorq vrcp14pd vrcp14ps vrcp14sd vrcp14ss vrndscalepd vrndscaleps vrndscalesd vrndscaless vrsqrt14pd vrsqrt14ps vrsqrt14sd vrsqrt14ss vscalefpd vscalefps vscalefsd vscalefss vscatterdpd vscatterdps vscatterqpd vscatterqps vshuff32x4 vshuff64x2 vshufi32x4 vshufi64x2 kandnw kandw kmovw knotw kortestw korw kshiftlw kshiftrw kunpckbw kxnorw kxorw vpbroadcastmb2q vpbroadcastmw2d vpconflictd vpconflictq vplzcntd vplzcntq vexp2pd vexp2ps vrcp28pd vrcp28ps vrcp28sd vrcp28ss vrsqrt28pd vrsqrt28ps vrsqrt28sd vrsqrt28ss vgatherpf0dpd vgatherpf0dps vgatherpf0qpd vgatherpf0qps vgatherpf1dpd vgatherpf1dps vgatherpf1qpd vgatherpf1qps vscatterpf0dpd vscatterpf0dps vscatterpf0qpd vscatterpf0qps vscatterpf1dpd vscatterpf1dps vscatterpf1qpd vscatterpf1qps prefetchwt1 bndmk bndcl bndcu bndcn bndmov bndldx bndstx sha1rnds4 sha1nexte sha1msg1 sha1msg2 sha256rnds2 sha256msg1 sha256msg2 hint_nop0 hint_nop1 hint_nop2 hint_nop3 hint_nop4 hint_nop5 hint_nop6 hint_nop7 hint_nop8 hint_nop9 hint_nop10 hint_nop11 hint_nop12 hint_nop13 hint_nop14 hint_nop15 hint_nop16 hint_nop17 hint_nop18 hint_nop19 hint_nop20 hint_nop21 hint_nop22 hint_nop23 hint_nop24 hint_nop25 hint_nop26 hint_nop27 hint_nop28 hint_nop29 hint_nop30 hint_nop31 hint_nop32 hint_nop33 hint_nop34 hint_nop35 hint_nop36 hint_nop37 hint_nop38 hint_nop39 hint_nop40 hint_nop41 hint_nop42 hint_nop43 hint_nop44 hint_nop45 hint_nop46 hint_nop47 hint_nop48 hint_nop49 hint_nop50 hint_nop51 hint_nop52 hint_nop53 hint_nop54 hint_nop55 hint_nop56 hint_nop57 hint_nop58 hint_nop59 hint_nop60 hint_nop61 hint_nop62 hint_nop63",built_in:"ip eip rip al ah bl bh cl ch dl dh sil dil bpl spl r8b r9b r10b r11b r12b r13b r14b r15b ax bx cx dx si di bp sp r8w r9w r10w r11w r12w r13w r14w r15w eax ebx ecx edx esi edi ebp esp eip r8d r9d r10d r11d r12d r13d r14d r15d rax rbx rcx rdx rsi rdi rbp rsp r8 r9 r10 r11 r12 r13 r14 r15 cs ds es fs gs ss st st0 st1 st2 st3 st4 st5 st6 st7 mm0 mm1 mm2 mm3 mm4 mm5 mm6 mm7 xmm0 xmm1 xmm2 xmm3 xmm4 xmm5 xmm6 xmm7 xmm8 xmm9 xmm10 xmm11 xmm12 xmm13 xmm14 xmm15 xmm16 xmm17 xmm18 xmm19 xmm20 xmm21 xmm22 xmm23 xmm24 xmm25 xmm26 xmm27 xmm28 xmm29 xmm30 xmm31 ymm0 ymm1 ymm2 ymm3 ymm4 ymm5 ymm6 ymm7 ymm8 ymm9 ymm10 ymm11 ymm12 ymm13 ymm14 ymm15 ymm16 ymm17 ymm18 ymm19 ymm20 ymm21 ymm22 ymm23 ymm24 ymm25 ymm26 ymm27 ymm28 ymm29 ymm30 ymm31 zmm0 zmm1 zmm2 zmm3 zmm4 zmm5 zmm6 zmm7 zmm8 zmm9 zmm10 zmm11 zmm12 zmm13 zmm14 zmm15 zmm16 zmm17 zmm18 zmm19 zmm20 zmm21 zmm22 zmm23 zmm24 zmm25 zmm26 zmm27 zmm28 zmm29 zmm30 zmm31 k0 k1 k2 k3 k4 k5 k6 k7 bnd0 bnd1 bnd2 bnd3 cr0 cr1 cr2 cr3 cr4 cr8 dr0 dr1 dr2 dr3 dr8 tr3 tr4 tr5 tr6 tr7 r0 r1 r2 r3 r4 r5 r6 r7 r0b r1b r2b r3b r4b r5b r6b r7b r0w r1w r2w r3w r4w r5w r6w r7w r0d r1d r2d r3d r4d r5d r6d r7d r0h r1h r2h r3h r0l r1l r2l r3l r4l r5l r6l r7l r8l r9l r10l r11l r12l r13l r14l r15l db dw dd dq dt ddq do dy dz resb resw resd resq rest resdq reso resy resz incbin equ times byte word dword qword nosplit rel abs seg wrt strict near far a32 ptr",meta:"%define %xdefine %+ %undef %defstr %deftok %assign %strcat %strlen %substr %rotate %elif %else %endif %if %ifmacro %ifctx %ifidn %ifidni %ifid %ifnum %ifstr %iftoken %ifempty %ifenv %error %warning %fatal %rep %endrep %include %push %pop %repl %pathsearch %depend %use %arg %stacksize %local %line %comment %endcomment .nolist __FILE__ __LINE__ __SECT__ __BITS__ __OUTPUT_FORMAT__ __DATE__ __TIME__ __DATE_NUM__ __TIME_NUM__ __UTC_DATE__ __UTC_TIME__ __UTC_DATE_NUM__ __UTC_TIME_NUM__ __PASS__ struc endstruc istruc at iend align alignb sectalign daz nodaz up down zero default option assume public bits use16 use32 use64 default section segment absolute extern global common cpu float __utf16__ __utf16le__ __utf16be__ __utf32__ __utf32le__ __utf32be__ __float8__ __float16__ __float32__ __float64__ __float80m__ __float80e__ __float128l__ __float128h__ __Infinity__ __QNaN__ __SNaN__ Inf NaN QNaN SNaN float8 float16 float32 float64 float80m float80e float128l float128h __FLOAT_DAZ__ __FLOAT_ROUND__ __FLOAT__"},contains:[s.COMMENT(";","$",{relevance:0}),{className:"number",variants:[{begin:"\\b(?:([0-9][0-9_]*)?\\.[0-9_]*(?:[eE][+-]?[0-9_]+)?|(0[Xx])?[0-9][0-9_]*\\.?[0-9_]*(?:[pP](?:[+-]?[0-9_]+)?)?)\\b",relevance:0},{begin:"\\$[0-9][0-9A-Fa-f]*",relevance:0},{begin:"\\b(?:[0-9A-Fa-f][0-9A-Fa-f_]*[Hh]|[0-9][0-9_]*[DdTt]?|[0-7][0-7_]*[QqOo]|[0-1][0-1_]*[BbYy])\\b"},{begin:"\\b(?:0[Xx][0-9A-Fa-f_]+|0[DdTt][0-9_]+|0[QqOo][0-7_]+|0[BbYy][0-1_]+)\\b"}]},s.QUOTE_STRING_MODE,{className:"string",variants:[{begin:"'",end:"[^\\\\]'"},{begin:"`",end:"[^\\\\]`"}],relevance:0},{className:"symbol",variants:[{begin:"^\\s*[A-Za-z._?][A-Za-z0-9_$#@~.?]*(:|\\s+label)"},{begin:"^\\s*%%[A-Za-z0-9_$#@~.?]*:"}],relevance:0},{className:"subst",begin:"%[0-9]+",relevance:0},{className:"subst",begin:"%!S+",relevance:0},{className:"meta",begin:/^\s*\.[\w_-]+/}]}}}());hljs.registerLanguage("kotlin",function(){"use strict";return function(e){var n={keyword:"abstract as val var vararg get set class object open private protected public noinline crossinline dynamic final enum if else do while for when throw try catch finally import package is in fun override companion reified inline lateinit init interface annotation data sealed internal infix operator out by constructor super tailrec where const inner suspend typealias external expect actual trait volatile transient native default",built_in:"Byte Short Char Int Long Boolean Float Double Void Unit Nothing",literal:"true false null"},a={className:"symbol",begin:e.UNDERSCORE_IDENT_RE+"@"},i={className:"subst",begin:"\\${",end:"}",contains:[e.C_NUMBER_MODE]},s={className:"variable",begin:"\\$"+e.UNDERSCORE_IDENT_RE},t={className:"string",variants:[{begin:'"""',end:'"""(?=[^"])',contains:[s,i]},{begin:"'",end:"'",illegal:/\n/,contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"',illegal:/\n/,contains:[e.BACKSLASH_ESCAPE,s,i]}]};i.contains.push(t);var r={className:"meta",begin:"@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\s*:(?:\\s*"+e.UNDERSCORE_IDENT_RE+")?"},l={className:"meta",begin:"@"+e.UNDERSCORE_IDENT_RE,contains:[{begin:/\(/,end:/\)/,contains:[e.inherit(t,{className:"meta-string"})]}]},c=e.COMMENT("/\\*","\\*/",{contains:[e.C_BLOCK_COMMENT_MODE]}),o={variants:[{className:"type",begin:e.UNDERSCORE_IDENT_RE},{begin:/\(/,end:/\)/,contains:[]}]},d=o;return d.variants[1].contains=[o],o.variants[1].contains=[d],{name:"Kotlin",aliases:["kt"],keywords:n,contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag",begin:"@[A-Za-z]+"}]}),e.C_LINE_COMMENT_MODE,c,{className:"keyword",begin:/\b(break|continue|return|this)\b/,starts:{contains:[{className:"symbol",begin:/@\w+/}]}},a,r,l,{className:"function",beginKeywords:"fun",end:"[(]|$",returnBegin:!0,excludeEnd:!0,keywords:n,illegal:/fun\s+(<.*>)?[^\s\(]+(\s+[^\s\(]+)\s*=/,relevance:5,contains:[{begin:e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0,contains:[e.UNDERSCORE_TITLE_MODE]},{className:"type",begin://,keywords:"reified",relevance:0},{className:"params",begin:/\(/,end:/\)/,endsParent:!0,keywords:n,relevance:0,contains:[{begin:/:/,end:/[=,\/]/,endsWithParent:!0,contains:[o,e.C_LINE_COMMENT_MODE,c],relevance:0},e.C_LINE_COMMENT_MODE,c,r,l,t,e.C_NUMBER_MODE]},c]},{className:"class",beginKeywords:"class interface trait",end:/[:\{(]|$/,excludeEnd:!0,illegal:"extends implements",contains:[{beginKeywords:"public protected internal private constructor"},e.UNDERSCORE_TITLE_MODE,{className:"type",begin://,excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:/[,:]\s*/,end:/[<\(,]|$/,excludeBegin:!0,returnEnd:!0},r,l]},t,{className:"meta",begin:"^#!/usr/bin/env",end:"$",illegal:"\n"},{className:"number",begin:"\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?|\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))([eE][-+]?\\d+)?)[lLfF]?",relevance:0}]}}}());hljs.registerLanguage("armasm",function(){"use strict";return function(s){const e={variants:[s.COMMENT("^[ \\t]*(?=#)","$",{relevance:0,excludeBegin:!0}),s.COMMENT("[;@]","$",{relevance:0}),s.C_LINE_COMMENT_MODE,s.C_BLOCK_COMMENT_MODE]};return{name:"ARM Assembly",case_insensitive:!0,aliases:["arm"],keywords:{$pattern:"\\.?"+s.IDENT_RE,meta:".2byte .4byte .align .ascii .asciz .balign .byte .code .data .else .end .endif .endm .endr .equ .err .exitm .extern .global .hword .if .ifdef .ifndef .include .irp .long .macro .rept .req .section .set .skip .space .text .word .arm .thumb .code16 .code32 .force_thumb .thumb_func .ltorg ALIAS ALIGN ARM AREA ASSERT ATTR CN CODE CODE16 CODE32 COMMON CP DATA DCB DCD DCDU DCDO DCFD DCFDU DCI DCQ DCQU DCW DCWU DN ELIF ELSE END ENDFUNC ENDIF ENDP ENTRY EQU EXPORT EXPORTAS EXTERN FIELD FILL FUNCTION GBLA GBLL GBLS GET GLOBAL IF IMPORT INCBIN INCLUDE INFO KEEP LCLA LCLL LCLS LTORG MACRO MAP MEND MEXIT NOFP OPT PRESERVE8 PROC QN READONLY RELOC REQUIRE REQUIRE8 RLIST FN ROUT SETA SETL SETS SN SPACE SUBT THUMB THUMBX TTL WHILE WEND ",built_in:"r0 r1 r2 r3 r4 r5 r6 r7 r8 r9 r10 r11 r12 r13 r14 r15 pc lr sp ip sl sb fp a1 a2 a3 a4 v1 v2 v3 v4 v5 v6 v7 v8 f0 f1 f2 f3 f4 f5 f6 f7 p0 p1 p2 p3 p4 p5 p6 p7 p8 p9 p10 p11 p12 p13 p14 p15 c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 c11 c12 c13 c14 c15 q0 q1 q2 q3 q4 q5 q6 q7 q8 q9 q10 q11 q12 q13 q14 q15 cpsr_c cpsr_x cpsr_s cpsr_f cpsr_cx cpsr_cxs cpsr_xs cpsr_xsf cpsr_sf cpsr_cxsf spsr_c spsr_x spsr_s spsr_f spsr_cx spsr_cxs spsr_xs spsr_xsf spsr_sf spsr_cxsf s0 s1 s2 s3 s4 s5 s6 s7 s8 s9 s10 s11 s12 s13 s14 s15 s16 s17 s18 s19 s20 s21 s22 s23 s24 s25 s26 s27 s28 s29 s30 s31 d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 d10 d11 d12 d13 d14 d15 d16 d17 d18 d19 d20 d21 d22 d23 d24 d25 d26 d27 d28 d29 d30 d31 {PC} {VAR} {TRUE} {FALSE} {OPT} {CONFIG} {ENDIAN} {CODESIZE} {CPU} {FPU} {ARCHITECTURE} {PCSTOREOFFSET} {ARMASM_VERSION} {INTER} {ROPI} {RWPI} {SWST} {NOSWST} . @"},contains:[{className:"keyword",begin:"\\b(adc|(qd?|sh?|u[qh]?)?add(8|16)?|usada?8|(q|sh?|u[qh]?)?(as|sa)x|and|adrl?|sbc|rs[bc]|asr|b[lx]?|blx|bxj|cbn?z|tb[bh]|bic|bfc|bfi|[su]bfx|bkpt|cdp2?|clz|clrex|cmp|cmn|cpsi[ed]|cps|setend|dbg|dmb|dsb|eor|isb|it[te]{0,3}|lsl|lsr|ror|rrx|ldm(([id][ab])|f[ds])?|ldr((s|ex)?[bhd])?|movt?|mvn|mra|mar|mul|[us]mull|smul[bwt][bt]|smu[as]d|smmul|smmla|mla|umlaal|smlal?([wbt][bt]|d)|mls|smlsl?[ds]|smc|svc|sev|mia([bt]{2}|ph)?|mrr?c2?|mcrr2?|mrs|msr|orr|orn|pkh(tb|bt)|rbit|rev(16|sh)?|sel|[su]sat(16)?|nop|pop|push|rfe([id][ab])?|stm([id][ab])?|str(ex)?[bhd]?|(qd?)?sub|(sh?|q|u[qh]?)?sub(8|16)|[su]xt(a?h|a?b(16)?)|srs([id][ab])?|swpb?|swi|smi|tst|teq|wfe|wfi|yield)(eq|ne|cs|cc|mi|pl|vs|vc|hi|ls|ge|lt|gt|le|al|hs|lo)?[sptrx]?(?=\\s)"},e,s.QUOTE_STRING_MODE,{className:"string",begin:"'",end:"[^\\\\]'",relevance:0},{className:"title",begin:"\\|",end:"\\|",illegal:"\\n",relevance:0},{className:"number",variants:[{begin:"[#$=]?0x[0-9a-f]+"},{begin:"[#$=]?0b[01]+"},{begin:"[#$=]\\d+"},{begin:"\\b\\d+"}],relevance:0},{className:"symbol",variants:[{begin:"^[ \\t]*[a-z_\\.\\$][a-z0-9_\\.\\$]+:"},{begin:"^[a-z_\\.\\$][a-z0-9_\\.\\$]+"},{begin:"[=#]\\w+"}],relevance:0}]}}}());hljs.registerLanguage("go",function(){"use strict";return function(e){var n={keyword:"break default func interface select case map struct chan else goto package switch const fallthrough if range type continue for import return var go defer bool byte complex64 complex128 float32 float64 int8 int16 int32 int64 string uint8 uint16 uint32 uint64 int uint uintptr rune",literal:"true false iota nil",built_in:"append cap close complex copy imag len make new panic print println real recover delete"};return{name:"Go",aliases:["golang"],keywords:n,illegal:">>|\.\.\.) /},i={className:"subst",begin:/\{/,end:/\}/,keywords:n,illegal:/#/},s={begin:/\{\{/,relevance:0},r={className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{begin:/(u|b)?r?'''/,end:/'''/,contains:[e.BACKSLASH_ESCAPE,a],relevance:10},{begin:/(u|b)?r?"""/,end:/"""/,contains:[e.BACKSLASH_ESCAPE,a],relevance:10},{begin:/(fr|rf|f)'''/,end:/'''/,contains:[e.BACKSLASH_ESCAPE,a,s,i]},{begin:/(fr|rf|f)"""/,end:/"""/,contains:[e.BACKSLASH_ESCAPE,a,s,i]},{begin:/(u|r|ur)'/,end:/'/,relevance:10},{begin:/(u|r|ur)"/,end:/"/,relevance:10},{begin:/(b|br)'/,end:/'/},{begin:/(b|br)"/,end:/"/},{begin:/(fr|rf|f)'/,end:/'/,contains:[e.BACKSLASH_ESCAPE,s,i]},{begin:/(fr|rf|f)"/,end:/"/,contains:[e.BACKSLASH_ESCAPE,s,i]},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},l={className:"number",relevance:0,variants:[{begin:e.BINARY_NUMBER_RE+"[lLjJ]?"},{begin:"\\b(0o[0-7]+)[lLjJ]?"},{begin:e.C_NUMBER_RE+"[lLjJ]?"}]},t={className:"params",variants:[{begin:/\(\s*\)/,skip:!0,className:null},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:["self",a,l,r,e.HASH_COMMENT_MODE]}]};return i.contains=[r,l,a],{name:"Python",aliases:["py","gyp","ipython"],keywords:n,illegal:/(<\/|->|\?)|=>/,contains:[a,l,{beginKeywords:"if",relevance:0},r,e.HASH_COMMENT_MODE,{variants:[{className:"function",beginKeywords:"def"},{className:"class",beginKeywords:"class"}],end:/:/,illegal:/[${=;\n,]/,contains:[e.UNDERSCORE_TITLE_MODE,t,{begin:/->/,endsWithParent:!0,keywords:"None"}]},{className:"meta",begin:/^[\t ]*@/,end:/$/},{begin:/\b(print|exec)\(/}]}}}());hljs.registerLanguage("shell",function(){"use strict";return function(s){return{name:"Shell Session",aliases:["console"],contains:[{className:"meta",begin:"^\\s{0,3}[/\\w\\d\\[\\]()@-]*[>%$#]",starts:{end:"$",subLanguage:"bash"}}]}}}());hljs.registerLanguage("scala",function(){"use strict";return function(e){var n={className:"subst",variants:[{begin:"\\$[A-Za-z0-9_]+"},{begin:"\\${",end:"}"}]},a={className:"string",variants:[{begin:'"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{begin:'"""',end:'"""',relevance:10},{begin:'[a-z]+"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE,n]},{className:"string",begin:'[a-z]+"""',end:'"""',contains:[n],relevance:10}]},s={className:"type",begin:"\\b[A-Z][A-Za-z0-9_]*",relevance:0},t={className:"title",begin:/[^0-9\n\t "'(),.`{}\[\]:;][^\n\t "'(),.`{}\[\]:;]+|[^0-9\n\t "'(),.`{}\[\]:;=]/,relevance:0},i={className:"class",beginKeywords:"class object trait type",end:/[:={\[\n;]/,excludeEnd:!0,contains:[{beginKeywords:"extends with",relevance:10},{begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[s]},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[s]},t]},l={className:"function",beginKeywords:"def",end:/[:={\[(\n;]/,excludeEnd:!0,contains:[t]};return{name:"Scala",keywords:{literal:"true false null",keyword:"type yield lazy override def with val var sealed abstract private trait object if forSome for while throw finally protected extends import final return else break new catch super class case package default try this match continue throws implicit"},contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,{className:"symbol",begin:"'\\w[\\w\\d_]*(?!')"},s,l,i,e.C_NUMBER_MODE,{className:"meta",begin:"@[A-Za-z]+"}]}}}());hljs.registerLanguage("julia",function(){"use strict";return function(e){var r="[A-Za-z_\\u00A1-\\uFFFF][A-Za-z_0-9\\u00A1-\\uFFFF]*",t={$pattern:r,keyword:"in isa where baremodule begin break catch ccall const continue do else elseif end export false finally for function global if import importall let local macro module quote return true try using while type immutable abstract bitstype typealias ",literal:"true false ARGS C_NULL DevNull ENDIAN_BOM ENV I Inf Inf16 Inf32 Inf64 InsertionSort JULIA_HOME LOAD_PATH MergeSort NaN NaN16 NaN32 NaN64 PROGRAM_FILE QuickSort RoundDown RoundFromZero RoundNearest RoundNearestTiesAway RoundNearestTiesUp RoundToZero RoundUp STDERR STDIN STDOUT VERSION catalan e|0 eu|0 eulergamma golden im nothing pi γ π φ ",built_in:"ANY AbstractArray AbstractChannel AbstractFloat AbstractMatrix AbstractRNG AbstractSerializer AbstractSet AbstractSparseArray AbstractSparseMatrix AbstractSparseVector AbstractString AbstractUnitRange AbstractVecOrMat AbstractVector Any ArgumentError Array AssertionError Associative Base64DecodePipe Base64EncodePipe Bidiagonal BigFloat BigInt BitArray BitMatrix BitVector Bool BoundsError BufferStream CachingPool CapturedException CartesianIndex CartesianRange Cchar Cdouble Cfloat Channel Char Cint Cintmax_t Clong Clonglong ClusterManager Cmd CodeInfo Colon Complex Complex128 Complex32 Complex64 CompositeException Condition ConjArray ConjMatrix ConjVector Cptrdiff_t Cshort Csize_t Cssize_t Cstring Cuchar Cuint Cuintmax_t Culong Culonglong Cushort Cwchar_t Cwstring DataType Date DateFormat DateTime DenseArray DenseMatrix DenseVecOrMat DenseVector Diagonal Dict DimensionMismatch Dims DirectIndexString Display DivideError DomainError EOFError EachLine Enum Enumerate ErrorException Exception ExponentialBackOff Expr Factorization FileMonitor Float16 Float32 Float64 Function Future GlobalRef GotoNode HTML Hermitian IO IOBuffer IOContext IOStream IPAddr IPv4 IPv6 IndexCartesian IndexLinear IndexStyle InexactError InitError Int Int128 Int16 Int32 Int64 Int8 IntSet Integer InterruptException InvalidStateException Irrational KeyError LabelNode LinSpace LineNumberNode LoadError LowerTriangular MIME Matrix MersenneTwister Method MethodError MethodTable Module NTuple NewvarNode NullException Nullable Number ObjectIdDict OrdinalRange OutOfMemoryError OverflowError Pair ParseError PartialQuickSort PermutedDimsArray Pipe PollingFileWatcher ProcessExitedException Ptr QuoteNode RandomDevice Range RangeIndex Rational RawFD ReadOnlyMemoryError Real ReentrantLock Ref Regex RegexMatch RemoteChannel RemoteException RevString RoundingMode RowVector SSAValue SegmentationFault SerializationState Set SharedArray SharedMatrix SharedVector Signed SimpleVector Slot SlotNumber SparseMatrixCSC SparseVector StackFrame StackOverflowError StackTrace StepRange StepRangeLen StridedArray StridedMatrix StridedVecOrMat StridedVector String SubArray SubString SymTridiagonal Symbol Symmetric SystemError TCPSocket Task Text TextDisplay Timer Tridiagonal Tuple Type TypeError TypeMapEntry TypeMapLevel TypeName TypeVar TypedSlot UDPSocket UInt UInt128 UInt16 UInt32 UInt64 UInt8 UndefRefError UndefVarError UnicodeError UniformScaling Union UnionAll UnitRange Unsigned UpperTriangular Val Vararg VecElement VecOrMat Vector VersionNumber Void WeakKeyDict WeakRef WorkerConfig WorkerPool "},a={keywords:t,illegal:/<\//},n={className:"subst",begin:/\$\(/,end:/\)/,keywords:t},o={className:"variable",begin:"\\$"+r},i={className:"string",contains:[e.BACKSLASH_ESCAPE,n,o],variants:[{begin:/\w*"""/,end:/"""\w*/,relevance:10},{begin:/\w*"/,end:/"\w*/}]},l={className:"string",contains:[e.BACKSLASH_ESCAPE,n,o],begin:"`",end:"`"},s={className:"meta",begin:"@"+r};return a.name="Julia",a.contains=[{className:"number",begin:/(\b0x[\d_]*(\.[\d_]*)?|0x\.\d[\d_]*)p[-+]?\d+|\b0[box][a-fA-F0-9][a-fA-F0-9_]*|(\b\d[\d_]*(\.[\d_]*)?|\.\d[\d_]*)([eEfF][-+]?\d+)?/,relevance:0},{className:"string",begin:/'(.|\\[xXuU][a-zA-Z0-9]+)'/},i,l,s,{className:"comment",variants:[{begin:"#=",end:"=#",relevance:10},{begin:"#",end:"$"}]},e.HASH_COMMENT_MODE,{className:"keyword",begin:"\\b(((abstract|primitive)\\s+)type|(mutable\\s+)?struct)\\b"},{begin:/<:/}],n.contains=a.contains,a}}());hljs.registerLanguage("php-template",function(){"use strict";return function(n){return{name:"PHP template",subLanguage:"xml",contains:[{begin:/<\?(php|=)?/,end:/\?>/,subLanguage:"php",contains:[{begin:"/\\*",end:"\\*/",skip:!0},{begin:'b"',end:'"',skip:!0},{begin:"b'",end:"'",skip:!0},n.inherit(n.APOS_STRING_MODE,{illegal:null,className:null,contains:null,skip:!0}),n.inherit(n.QUOTE_STRING_MODE,{illegal:null,className:null,contains:null,skip:!0})]}]}}}());hljs.registerLanguage("scss",function(){"use strict";return function(e){var t={className:"variable",begin:"(\\$[a-zA-Z-][a-zA-Z0-9_-]*)\\b"},i={className:"number",begin:"#[0-9A-Fa-f]+"};return e.CSS_NUMBER_MODE,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,e.C_BLOCK_COMMENT_MODE,{name:"SCSS",case_insensitive:!0,illegal:"[=/|']",contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"selector-id",begin:"\\#[A-Za-z0-9_-]+",relevance:0},{className:"selector-class",begin:"\\.[A-Za-z0-9_-]+",relevance:0},{className:"selector-attr",begin:"\\[",end:"\\]",illegal:"$"},{className:"selector-tag",begin:"\\b(a|abbr|acronym|address|area|article|aside|audio|b|base|big|blockquote|body|br|button|canvas|caption|cite|code|col|colgroup|command|datalist|dd|del|details|dfn|div|dl|dt|em|embed|fieldset|figcaption|figure|footer|form|frame|frameset|(h[1-6])|head|header|hgroup|hr|html|i|iframe|img|input|ins|kbd|keygen|label|legend|li|link|map|mark|meta|meter|nav|noframes|noscript|object|ol|optgroup|option|output|p|param|pre|progress|q|rp|rt|ruby|samp|script|section|select|small|span|strike|strong|style|sub|sup|table|tbody|td|textarea|tfoot|th|thead|time|title|tr|tt|ul|var|video)\\b",relevance:0},{className:"selector-pseudo",begin:":(visited|valid|root|right|required|read-write|read-only|out-range|optional|only-of-type|only-child|nth-of-type|nth-last-of-type|nth-last-child|nth-child|not|link|left|last-of-type|last-child|lang|invalid|indeterminate|in-range|hover|focus|first-of-type|first-line|first-letter|first-child|first|enabled|empty|disabled|default|checked|before|after|active)"},{className:"selector-pseudo",begin:"::(after|before|choices|first-letter|first-line|repeat-index|repeat-item|selection|value)"},t,{className:"attribute",begin:"\\b(src|z-index|word-wrap|word-spacing|word-break|width|widows|white-space|visibility|vertical-align|unicode-bidi|transition-timing-function|transition-property|transition-duration|transition-delay|transition|transform-style|transform-origin|transform|top|text-underline-position|text-transform|text-shadow|text-rendering|text-overflow|text-indent|text-decoration-style|text-decoration-line|text-decoration-color|text-decoration|text-align-last|text-align|tab-size|table-layout|right|resize|quotes|position|pointer-events|perspective-origin|perspective|page-break-inside|page-break-before|page-break-after|padding-top|padding-right|padding-left|padding-bottom|padding|overflow-y|overflow-x|overflow-wrap|overflow|outline-width|outline-style|outline-offset|outline-color|outline|orphans|order|opacity|object-position|object-fit|normal|none|nav-up|nav-right|nav-left|nav-index|nav-down|min-width|min-height|max-width|max-height|mask|marks|margin-top|margin-right|margin-left|margin-bottom|margin|list-style-type|list-style-position|list-style-image|list-style|line-height|letter-spacing|left|justify-content|initial|inherit|ime-mode|image-orientation|image-resolution|image-rendering|icon|hyphens|height|font-weight|font-variant-ligatures|font-variant|font-style|font-stretch|font-size-adjust|font-size|font-language-override|font-kerning|font-feature-settings|font-family|font|float|flex-wrap|flex-shrink|flex-grow|flex-flow|flex-direction|flex-basis|flex|filter|empty-cells|display|direction|cursor|counter-reset|counter-increment|content|column-width|column-span|column-rule-width|column-rule-style|column-rule-color|column-rule|column-gap|column-fill|column-count|columns|color|clip-path|clip|clear|caption-side|break-inside|break-before|break-after|box-sizing|box-shadow|box-decoration-break|bottom|border-width|border-top-width|border-top-style|border-top-right-radius|border-top-left-radius|border-top-color|border-top|border-style|border-spacing|border-right-width|border-right-style|border-right-color|border-right|border-radius|border-left-width|border-left-style|border-left-color|border-left|border-image-width|border-image-source|border-image-slice|border-image-repeat|border-image-outset|border-image|border-color|border-collapse|border-bottom-width|border-bottom-style|border-bottom-right-radius|border-bottom-left-radius|border-bottom-color|border-bottom|border|background-size|background-repeat|background-position|background-origin|background-image|background-color|background-clip|background-attachment|background-blend-mode|background|backface-visibility|auto|animation-timing-function|animation-play-state|animation-name|animation-iteration-count|animation-fill-mode|animation-duration|animation-direction|animation-delay|animation|align-self|align-items|align-content)\\b",illegal:"[^\\s]"},{begin:"\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b"},{begin:":",end:";",contains:[t,i,e.CSS_NUMBER_MODE,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,{className:"meta",begin:"!important"}]},{begin:"@(page|font-face)",lexemes:"@[a-z-]+",keywords:"@page @font-face"},{begin:"@",end:"[{;]",returnBegin:!0,keywords:"and or not only",contains:[{begin:"@[a-z-]+",className:"keyword"},t,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,i,e.CSS_NUMBER_MODE]}]}}}());hljs.registerLanguage("r",function(){"use strict";return function(e){var n="([a-zA-Z]|\\.[a-zA-Z.])[a-zA-Z0-9._]*";return{name:"R",contains:[e.HASH_COMMENT_MODE,{begin:n,keywords:{$pattern:n,keyword:"function if in break next repeat else for return switch while try tryCatch stop warning require library attach detach source setMethod setGeneric setGroupGeneric setClass ...",literal:"NULL NA TRUE FALSE T F Inf NaN NA_integer_|10 NA_real_|10 NA_character_|10 NA_complex_|10"},relevance:0},{className:"number",begin:"0[xX][0-9a-fA-F]+[Li]?\\b",relevance:0},{className:"number",begin:"\\d+(?:[eE][+\\-]?\\d*)?L\\b",relevance:0},{className:"number",begin:"\\d+\\.(?!\\d)(?:i\\b)?",relevance:0},{className:"number",begin:"\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d*)?i?\\b",relevance:0},{className:"number",begin:"\\.\\d+(?:[eE][+\\-]?\\d*)?i?\\b",relevance:0},{begin:"`",end:"`",relevance:0},{className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{begin:'"',end:'"'},{begin:"'",end:"'"}]}]}}}());hljs.registerLanguage("sql",function(){"use strict";return function(e){var t=e.COMMENT("--","$");return{name:"SQL",case_insensitive:!0,illegal:/[<>{}*]/,contains:[{beginKeywords:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke comment values with",end:/;/,endsWithParent:!0,keywords:{$pattern:/[\w\.]+/,keyword:"as abort abs absolute acc acce accep accept access accessed accessible account acos action activate add addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias all allocate allow alter always analyze ancillary and anti any anydata anydataset anyschema anytype apply archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound bucket buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base char_length character_length characters characterset charindex charset charsetform charsetid check checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation collect colu colum column column_value columns columns_updated comment commit compact compatibility compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection consider consistent constant constraint constraints constructor container content contents context contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime customdatum cycle data database databases datafile datafiles datalength date_add date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor deterministic diagnostics difference dimension direct_load directory disable disable_all disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div do document domain dotnet double downgrade drop dumpfile duplicate duration each edition editionable editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding execu execut execute exempt exists exit exp expire explain explode export export_set extended extent external external_1 external_2 externally extract failed failed_login_attempts failover failure far fast feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final finish first first_value fixed flash_cache flashback floor flush following follows for forall force foreign form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ftp full function general generated get get_format get_lock getdate getutcdate global global_name globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex hierarchy high high_priority hosts hour hours http id ident_current ident_incr ident_seed identified identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile initial initialized initially initrans inmemory inner innodb input insert install instance instantiable instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists keep keep_duplicates key keys kill language large last last_day last_insert_id last_value lateral lax lcase lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call logoff logon logs long loop low low_priority lower lpad lrtrim ltrim main make_set makedate maketime managed management manual map mapping mask master master_pos_wait match matched materialized max maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans md5 measures median medium member memcompress memory merge microsecond mid migration min minextents minimum mining minus minute minutes minvalue missing mod mode model modification modify module monitoring month months mount move movement multiset mutex name name_const names nan national native natural nav nchar nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck noswitch not nothing notice notnull notrim novalidate now nowait nth_value nullif nulls num numb numbe nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary out outer outfile outline output over overflow overriding package pad parallel parallel_enable parameters parent parse partial partition partitions pascal passing password password_grace_time password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction prediction_cost prediction_details prediction_probability prediction_set prepare present preserve prior priority private private_sga privileges procedural procedure procedure_analyze processlist profiles project prompt protection public publishingservername purge quarter query quick quiesce quota quotename radians raise rand range rank raw read reads readsize rebuild record records recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename repair repeat replace replicate replication required reset resetlogs resize resource respect restore restricted result result_cache resumable resume retention return returning returns reuse reverse revoke right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll sdo_georaster sdo_topo_geometry search sec_to_time second seconds section securefile security seed segment select self semi sequence sequential serializable server servererror session session_user sessions_per_user set sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone standby start starting startup statement static statistics stats_binomial_test stats_crosstab stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime table tables tablespace tablesample tan tdo template temporary terminated tertiary_weights test than then thread through tier ties time time_format time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unnest unpivot unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear wellformed when whene whenev wheneve whenever where while whitespace window with within without work wrapped xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek",literal:"true false null unknown",built_in:"array bigint binary bit blob bool boolean char character date dec decimal float int int8 integer interval number numeric real record serial serial8 smallint text time timestamp tinyint varchar varchar2 varying void"},contains:[{className:"string",begin:"'",end:"'",contains:[{begin:"''"}]},{className:"string",begin:'"',end:'"',contains:[{begin:'""'}]},{className:"string",begin:"`",end:"`"},e.C_NUMBER_MODE,e.C_BLOCK_COMMENT_MODE,t,e.HASH_COMMENT_MODE]},e.C_BLOCK_COMMENT_MODE,t,e.HASH_COMMENT_MODE]}}}());hljs.registerLanguage("c",function(){"use strict";return function(e){var n=e.getLanguage("c-like").rawDefinition();return n.name="C",n.aliases=["c","h"],n}}());hljs.registerLanguage("json",function(){"use strict";return function(n){var e={literal:"true false null"},i=[n.C_LINE_COMMENT_MODE,n.C_BLOCK_COMMENT_MODE],t=[n.QUOTE_STRING_MODE,n.C_NUMBER_MODE],a={end:",",endsWithParent:!0,excludeEnd:!0,contains:t,keywords:e},l={begin:"{",end:"}",contains:[{className:"attr",begin:/"/,end:/"/,contains:[n.BACKSLASH_ESCAPE],illegal:"\\n"},n.inherit(a,{begin:/:/})].concat(i),illegal:"\\S"},s={begin:"\\[",end:"\\]",contains:[n.inherit(a)],illegal:"\\S"};return t.push(l,s),i.forEach((function(n){t.push(n)})),{name:"JSON",contains:t,keywords:e,illegal:"\\S"}}}());hljs.registerLanguage("python-repl",function(){"use strict";return function(n){return{aliases:["pycon"],contains:[{className:"meta",starts:{end:/ |$/,starts:{end:"$",subLanguage:"python"}},variants:[{begin:/^>>>(?=[ ]|$)/},{begin:/^\.\.\.(?=[ ]|$)/}]}]}}}());hljs.registerLanguage("markdown",function(){"use strict";return function(n){const e={begin:"<",end:">",subLanguage:"xml",relevance:0},a={begin:"\\[.+?\\][\\(\\[].*?[\\)\\]]",returnBegin:!0,contains:[{className:"string",begin:"\\[",end:"\\]",excludeBegin:!0,returnEnd:!0,relevance:0},{className:"link",begin:"\\]\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0},{className:"symbol",begin:"\\]\\[",end:"\\]",excludeBegin:!0,excludeEnd:!0}],relevance:10},i={className:"strong",contains:[],variants:[{begin:/_{2}/,end:/_{2}/},{begin:/\*{2}/,end:/\*{2}/}]},s={className:"emphasis",contains:[],variants:[{begin:/\*(?!\*)/,end:/\*/},{begin:/_(?!_)/,end:/_/,relevance:0}]};i.contains.push(s),s.contains.push(i);var c=[e,a];return i.contains=i.contains.concat(c),s.contains=s.contains.concat(c),{name:"Markdown",aliases:["md","mkdown","mkd"],contains:[{className:"section",variants:[{begin:"^#{1,6}",end:"$",contains:c=c.concat(i,s)},{begin:"(?=^.+?\\n[=-]{2,}$)",contains:[{begin:"^[=-]*$"},{begin:"^",end:"\\n",contains:c}]}]},e,{className:"bullet",begin:"^[ \t]*([*+-]|(\\d+\\.))(?=\\s+)",end:"\\s+",excludeEnd:!0},i,s,{className:"quote",begin:"^>\\s+",contains:c,end:"$"},{className:"code",variants:[{begin:"(`{3,})(.|\\n)*?\\1`*[ ]*"},{begin:"(~{3,})(.|\\n)*?\\1~*[ ]*"},{begin:"```",end:"```+[ ]*$"},{begin:"~~~",end:"~~~+[ ]*$"},{begin:"`.+?`"},{begin:"(?=^( {4}|\\t))",contains:[{begin:"^( {4}|\\t)",end:"(\\n)$"}],relevance:0}]},{begin:"^[-\\*]{3,}",end:"$"},a,{begin:/^\[[^\n]+\]:/,returnBegin:!0,contains:[{className:"symbol",begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0},{className:"link",begin:/:\s*/,end:/$/,excludeBegin:!0}]}]}}}());hljs.registerLanguage("javascript",function(){"use strict";const e=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],n=["true","false","null","undefined","NaN","Infinity"],a=[].concat(["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],["arguments","this","super","console","window","document","localStorage","module","global"],["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer"],["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"]);function s(e){return r("(?=",e,")")}function r(...e){return e.map(e=>(function(e){return e?"string"==typeof e?e:e.source:null})(e)).join("")}return function(t){var i="[A-Za-z$_][0-9A-Za-z$_]*",c={begin:/<[A-Za-z0-9\\._:-]+/,end:/\/[A-Za-z0-9\\._:-]+>|\/>/},o={$pattern:"[A-Za-z$_][0-9A-Za-z$_]*",keyword:e.join(" "),literal:n.join(" "),built_in:a.join(" ")},l={className:"number",variants:[{begin:"\\b(0[bB][01]+)n?"},{begin:"\\b(0[oO][0-7]+)n?"},{begin:t.C_NUMBER_RE+"n?"}],relevance:0},E={className:"subst",begin:"\\$\\{",end:"\\}",keywords:o,contains:[]},d={begin:"html`",end:"",starts:{end:"`",returnEnd:!1,contains:[t.BACKSLASH_ESCAPE,E],subLanguage:"xml"}},g={begin:"css`",end:"",starts:{end:"`",returnEnd:!1,contains:[t.BACKSLASH_ESCAPE,E],subLanguage:"css"}},u={className:"string",begin:"`",end:"`",contains:[t.BACKSLASH_ESCAPE,E]};E.contains=[t.APOS_STRING_MODE,t.QUOTE_STRING_MODE,d,g,u,l,t.REGEXP_MODE];var b=E.contains.concat([{begin:/\(/,end:/\)/,contains:["self"].concat(E.contains,[t.C_BLOCK_COMMENT_MODE,t.C_LINE_COMMENT_MODE])},t.C_BLOCK_COMMENT_MODE,t.C_LINE_COMMENT_MODE]),_={className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:b};return{name:"JavaScript",aliases:["js","jsx","mjs","cjs"],keywords:o,contains:[t.SHEBANG({binary:"node",relevance:5}),{className:"meta",relevance:10,begin:/^\s*['"]use (strict|asm)['"]/},t.APOS_STRING_MODE,t.QUOTE_STRING_MODE,d,g,u,t.C_LINE_COMMENT_MODE,t.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag",begin:"@[A-Za-z]+",contains:[{className:"type",begin:"\\{",end:"\\}",relevance:0},{className:"variable",begin:i+"(?=\\s*(-)|$)",endsParent:!0,relevance:0},{begin:/(?=[^\n])\s/,relevance:0}]}]}),t.C_BLOCK_COMMENT_MODE,l,{begin:r(/[{,\n]\s*/,s(r(/(((\/\/.*)|(\/\*(.|\n)*\*\/))\s*)*/,i+"\\s*:"))),relevance:0,contains:[{className:"attr",begin:i+s("\\s*:"),relevance:0}]},{begin:"("+t.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",contains:[t.C_LINE_COMMENT_MODE,t.C_BLOCK_COMMENT_MODE,t.REGEXP_MODE,{className:"function",begin:"(\\([^(]*(\\([^(]*(\\([^(]*\\))?\\))?\\)|"+t.UNDERSCORE_IDENT_RE+")\\s*=>",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:t.UNDERSCORE_IDENT_RE},{className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:o,contains:b}]}]},{begin:/,/,relevance:0},{className:"",begin:/\s/,end:/\s*/,skip:!0},{variants:[{begin:"<>",end:""},{begin:c.begin,end:c.end}],subLanguage:"xml",contains:[{begin:c.begin,end:c.end,skip:!0,contains:["self"]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/\{/,excludeEnd:!0,contains:[t.inherit(t.TITLE_MODE,{begin:i}),_],illegal:/\[|%/},{begin:/\$[(.]/},t.METHOD_GUARD,{className:"class",beginKeywords:"class",end:/[{;=]/,excludeEnd:!0,illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends"},t.UNDERSCORE_TITLE_MODE]},{beginKeywords:"constructor",end:/\{/,excludeEnd:!0},{begin:"(get|set)\\s+(?="+i+"\\()",end:/{/,keywords:"get set",contains:[t.inherit(t.TITLE_MODE,{begin:i}),{begin:/\(\)/},_]}],illegal:/#(?!!)/}}}());hljs.registerLanguage("typescript",function(){"use strict";const e=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],n=["true","false","null","undefined","NaN","Infinity"],a=[].concat(["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],["arguments","this","super","console","window","document","localStorage","module","global"],["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer"],["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"]);return function(r){var t={$pattern:"[A-Za-z$_][0-9A-Za-z$_]*",keyword:e.concat(["type","namespace","typedef","interface","public","private","protected","implements","declare","abstract","readonly"]).join(" "),literal:n.join(" "),built_in:a.concat(["any","void","number","boolean","string","object","never","enum"]).join(" ")},s={className:"meta",begin:"@[A-Za-z$_][0-9A-Za-z$_]*"},i={className:"number",variants:[{begin:"\\b(0[bB][01]+)n?"},{begin:"\\b(0[oO][0-7]+)n?"},{begin:r.C_NUMBER_RE+"n?"}],relevance:0},o={className:"subst",begin:"\\$\\{",end:"\\}",keywords:t,contains:[]},c={begin:"html`",end:"",starts:{end:"`",returnEnd:!1,contains:[r.BACKSLASH_ESCAPE,o],subLanguage:"xml"}},l={begin:"css`",end:"",starts:{end:"`",returnEnd:!1,contains:[r.BACKSLASH_ESCAPE,o],subLanguage:"css"}},E={className:"string",begin:"`",end:"`",contains:[r.BACKSLASH_ESCAPE,o]};o.contains=[r.APOS_STRING_MODE,r.QUOTE_STRING_MODE,c,l,E,i,r.REGEXP_MODE];var d={begin:"\\(",end:/\)/,keywords:t,contains:["self",r.QUOTE_STRING_MODE,r.APOS_STRING_MODE,r.NUMBER_MODE]},u={className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:t,contains:[r.C_LINE_COMMENT_MODE,r.C_BLOCK_COMMENT_MODE,s,d]};return{name:"TypeScript",aliases:["ts"],keywords:t,contains:[r.SHEBANG(),{className:"meta",begin:/^\s*['"]use strict['"]/},r.APOS_STRING_MODE,r.QUOTE_STRING_MODE,c,l,E,r.C_LINE_COMMENT_MODE,r.C_BLOCK_COMMENT_MODE,i,{begin:"("+r.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",contains:[r.C_LINE_COMMENT_MODE,r.C_BLOCK_COMMENT_MODE,r.REGEXP_MODE,{className:"function",begin:"(\\([^(]*(\\([^(]*(\\([^(]*\\))?\\))?\\)|"+r.UNDERSCORE_IDENT_RE+")\\s*=>",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:r.UNDERSCORE_IDENT_RE},{className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:t,contains:d.contains}]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/[\{;]/,excludeEnd:!0,keywords:t,contains:["self",r.inherit(r.TITLE_MODE,{begin:"[A-Za-z$_][0-9A-Za-z$_]*"}),u],illegal:/%/,relevance:0},{beginKeywords:"constructor",end:/[\{;]/,excludeEnd:!0,contains:["self",u]},{begin:/module\./,keywords:{built_in:"module"},relevance:0},{beginKeywords:"module",end:/\{/,excludeEnd:!0},{beginKeywords:"interface",end:/\{/,excludeEnd:!0,keywords:"interface extends"},{begin:/\$[(.]/},{begin:"\\."+r.IDENT_RE,relevance:0},s,d]}}}());hljs.registerLanguage("plaintext",function(){"use strict";return function(t){return{name:"Plain text",aliases:["text","txt"],disableAutodetect:!0}}}());hljs.registerLanguage("less",function(){"use strict";return function(e){var n="([\\w-]+|@{[\\w-]+})",a=[],s=[],t=function(e){return{className:"string",begin:"~?"+e+".*?"+e}},r=function(e,n,a){return{className:e,begin:n,relevance:a}},i={begin:"\\(",end:"\\)",contains:s,relevance:0};s.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,t("'"),t('"'),e.CSS_NUMBER_MODE,{begin:"(url|data-uri)\\(",starts:{className:"string",end:"[\\)\\n]",excludeEnd:!0}},r("number","#[0-9A-Fa-f]+\\b"),i,r("variable","@@?[\\w-]+",10),r("variable","@{[\\w-]+}"),r("built_in","~?`[^`]*?`"),{className:"attribute",begin:"[\\w-]+\\s*:",end:":",returnBegin:!0,excludeEnd:!0},{className:"meta",begin:"!important"});var c=s.concat({begin:"{",end:"}",contains:a}),l={beginKeywords:"when",endsWithParent:!0,contains:[{beginKeywords:"and not"}].concat(s)},o={begin:n+"\\s*:",returnBegin:!0,end:"[;}]",relevance:0,contains:[{className:"attribute",begin:n,end:":",excludeEnd:!0,starts:{endsWithParent:!0,illegal:"[<=$]",relevance:0,contains:s}}]},g={className:"keyword",begin:"@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b",starts:{end:"[;{}]",returnEnd:!0,contains:s,relevance:0}},d={className:"variable",variants:[{begin:"@[\\w-]+\\s*:",relevance:15},{begin:"@[\\w-]+"}],starts:{end:"[;}]",returnEnd:!0,contains:c}},b={variants:[{begin:"[\\.#:&\\[>]",end:"[;{}]"},{begin:n,end:"{"}],returnBegin:!0,returnEnd:!0,illegal:"[<='$\"]",relevance:0,contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,l,r("keyword","all\\b"),r("variable","@{[\\w-]+}"),r("selector-tag",n+"%?",0),r("selector-id","#"+n),r("selector-class","\\."+n,0),r("selector-tag","&",0),{className:"selector-attr",begin:"\\[",end:"\\]"},{className:"selector-pseudo",begin:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{begin:"\\(",end:"\\)",contains:c},{begin:"!important"}]};return a.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,g,d,o,b),{name:"Less",case_insensitive:!0,illegal:"[=>'/<($\"]",contains:a}}}());hljs.registerLanguage("lua",function(){"use strict";return function(e){var t={begin:"\\[=*\\[",end:"\\]=*\\]",contains:["self"]},a=[e.COMMENT("--(?!\\[=*\\[)","$"),e.COMMENT("--\\[=*\\[","\\]=*\\]",{contains:[t],relevance:10})];return{name:"Lua",keywords:{$pattern:e.UNDERSCORE_IDENT_RE,literal:"true false nil",keyword:"and break do else elseif end for goto if in local not or repeat return then until while",built_in:"_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall arg self coroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove"},contains:a.concat([{className:"function",beginKeywords:"function",end:"\\)",contains:[e.inherit(e.TITLE_MODE,{begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{className:"params",begin:"\\(",endsWithParent:!0,contains:a}].concat(a)},e.C_NUMBER_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{className:"string",begin:"\\[=*\\[",end:"\\]=*\\]",contains:[t],relevance:5}])}}}()); + */ +var hljs=function(){"use strict";var e={exports:{}};function t(e){ +return e instanceof Map?e.clear=e.delete=e.set=()=>{ +throw Error("map is read-only")}:e instanceof Set&&(e.add=e.clear=e.delete=()=>{ +throw Error("set is read-only") +}),Object.freeze(e),Object.getOwnPropertyNames(e).forEach((n=>{var i=e[n] +;"object"!=typeof i||Object.isFrozen(i)||t(i)})),e} +e.exports=t,e.exports.default=t;var n=e.exports;class i{constructor(e){ +void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1} +ignoreMatch(){this.isMatchIgnored=!0}}function r(e){ +return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'") +}function s(e,...t){const n=Object.create(null);for(const t in e)n[t]=e[t] +;return t.forEach((e=>{for(const t in e)n[t]=e[t]})),n}const o=e=>!!e.kind +;class a{constructor(e,t){ +this.buffer="",this.classPrefix=t.classPrefix,e.walk(this)}addText(e){ +this.buffer+=r(e)}openNode(e){if(!o(e))return;let t=e.kind +;t=e.sublanguage?"language-"+t:((e,{prefix:t})=>{if(e.includes(".")){ +const n=e.split(".") +;return[`${t}${n.shift()}`,...n.map(((e,t)=>`${e}${"_".repeat(t+1)}`))].join(" ") +}return`${t}${e}`})(t,{prefix:this.classPrefix}),this.span(t)}closeNode(e){ +o(e)&&(this.buffer+="
")}value(){return this.buffer}span(e){ +this.buffer+=``}}class l{constructor(){this.rootNode={ +children:[]},this.stack=[this.rootNode]}get top(){ +return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){ +this.top.children.push(e)}openNode(e){const t={kind:e,children:[]} +;this.add(t),this.stack.push(t)}closeNode(){ +if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){ +for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)} +walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,t){ +return"string"==typeof t?e.addText(t):t.children&&(e.openNode(t), +t.children.forEach((t=>this._walk(e,t))),e.closeNode(t)),e}static _collapse(e){ +"string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{ +l._collapse(e)})))}}class c extends l{constructor(e){super(),this.options=e} +addKeyword(e,t){""!==e&&(this.openNode(t),this.addText(e),this.closeNode())} +addText(e){""!==e&&this.add(e)}addSublanguage(e,t){const n=e.root +;n.kind=t,n.sublanguage=!0,this.add(n)}toHTML(){ +return new a(this,this.options).value()}finalize(){return!0}}function g(e){ +return e?"string"==typeof e?e:e.source:null}function d(...e){ +return e.map((e=>g(e))).join("")}function u(...e){return"("+((e=>{ +const t=e[e.length-1] +;return"object"==typeof t&&t.constructor===Object?(e.splice(e.length-1,1),t):{} +})(e).capture?"":"?:")+e.map((e=>g(e))).join("|")+")"}function h(e){ +return RegExp(e.toString()+"|").exec("").length-1} +const f=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./ +;function p(e,{joinWith:t}){let n=0;return e.map((e=>{n+=1;const t=n +;let i=g(e),r="";for(;i.length>0;){const e=f.exec(i);if(!e){r+=i;break} +r+=i.substring(0,e.index), +i=i.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?r+="\\"+(Number(e[1])+t):(r+=e[0], +"("===e[0]&&n++)}return r})).map((e=>`(${e})`)).join(t)} +const b="[a-zA-Z]\\w*",m="[a-zA-Z_]\\w*",E="\\b\\d+(\\.\\d+)?",x="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",y="\\b(0b[01]+)",w={ +begin:"\\\\[\\s\\S]",relevance:0},_={scope:"string",begin:"'",end:"'", +illegal:"\\n",contains:[w]},v={scope:"string",begin:'"',end:'"',illegal:"\\n", +contains:[w]},O=(e,t,n={})=>{const i=s({scope:"comment",begin:e,end:t, +contains:[]},n);i.contains.push({scope:"doctag", +begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)", +end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0}) +;const r=u("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/) +;return i.contains.push({begin:d(/[ ]+/,"(",r,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),i +},k=O("//","$"),N=O("/\\*","\\*/"),S=O("#","$");var M=Object.freeze({ +__proto__:null,MATCH_NOTHING_RE:/\b\B/,IDENT_RE:b,UNDERSCORE_IDENT_RE:m, +NUMBER_RE:E,C_NUMBER_RE:x,BINARY_NUMBER_RE:y, +RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~", +SHEBANG:(e={})=>{const t=/^#![ ]*\// +;return e.binary&&(e.begin=d(t,/.*\b/,e.binary,/\b.*/)),s({scope:"meta",begin:t, +end:/$/,relevance:0,"on:begin":(e,t)=>{0!==e.index&&t.ignoreMatch()}},e)}, +BACKSLASH_ESCAPE:w,APOS_STRING_MODE:_,QUOTE_STRING_MODE:v,PHRASAL_WORDS_MODE:{ +begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/ +},COMMENT:O,C_LINE_COMMENT_MODE:k,C_BLOCK_COMMENT_MODE:N,HASH_COMMENT_MODE:S, +NUMBER_MODE:{scope:"number",begin:E,relevance:0},C_NUMBER_MODE:{scope:"number", +begin:x,relevance:0},BINARY_NUMBER_MODE:{scope:"number",begin:y,relevance:0}, +REGEXP_MODE:{begin:/(?=\/[^/\n]*\/)/,contains:[{scope:"regexp",begin:/\//, +end:/\/[gimuy]*/,illegal:/\n/,contains:[w,{begin:/\[/,end:/\]/,relevance:0, +contains:[w]}]}]},TITLE_MODE:{scope:"title",begin:b,relevance:0}, +UNDERSCORE_TITLE_MODE:{scope:"title",begin:m,relevance:0},METHOD_GUARD:{ +begin:"\\.\\s*[a-zA-Z_]\\w*",relevance:0},END_SAME_AS_BEGIN:e=>Object.assign(e,{ +"on:begin":(e,t)=>{t.data._beginMatch=e[1]},"on:end":(e,t)=>{ +t.data._beginMatch!==e[1]&&t.ignoreMatch()}})});function R(e,t){ +"."===e.input[e.index-1]&&t.ignoreMatch()}function j(e,t){ +void 0!==e.className&&(e.scope=e.className,delete e.className)}function A(e,t){ +t&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)", +e.__beforeBegin=R,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords, +void 0===e.relevance&&(e.relevance=0))}function I(e,t){ +Array.isArray(e.illegal)&&(e.illegal=u(...e.illegal))}function B(e,t){ +if(e.match){ +if(e.begin||e.end)throw Error("begin & end are not supported with match") +;e.begin=e.match,delete e.match}}function T(e,t){ +void 0===e.relevance&&(e.relevance=1)}const L=(e,t)=>{if(!e.beforeMatch)return +;if(e.starts)throw Error("beforeMatch cannot be used with starts") +;const n=Object.assign({},e);Object.keys(e).forEach((t=>{delete e[t] +})),e.keywords=n.keywords, +e.begin=d(n.beforeMatch,d("(?=",n.begin,")")),e.starts={relevance:0, +contains:[Object.assign(n,{endsParent:!0})]},e.relevance=0,delete n.beforeMatch +},D=["of","and","for","in","not","or","if","then","parent","list","value"] +;function P(e,t,n="keyword"){const i=Object.create(null) +;return"string"==typeof e?r(n,e.split(" ")):Array.isArray(e)?r(n,e):Object.keys(e).forEach((n=>{ +Object.assign(i,P(e[n],t,n))})),i;function r(e,n){ +t&&(n=n.map((e=>e.toLowerCase()))),n.forEach((t=>{const n=t.split("|") +;i[n[0]]=[e,C(n[0],n[1])]}))}}function C(e,t){ +return t?Number(t):(e=>D.includes(e.toLowerCase()))(e)?0:1}const H={},$=e=>{ +console.error(e)},U=(e,...t)=>{console.log("WARN: "+e,...t)},z=(e,t)=>{ +H[`${e}/${t}`]||(console.log(`Deprecated as of ${e}. ${t}`),H[`${e}/${t}`]=!0) +},K=Error();function W(e,t,{key:n}){let i=0;const r=e[n],s={},o={} +;for(let e=1;e<=t.length;e++)o[e+i]=r[e],s[e+i]=!0,i+=h(t[e-1]) +;e[n]=o,e[n]._emit=s,e[n]._multi=!0}function X(e){(e=>{ +e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope, +delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={ +_wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope +}),(e=>{if(Array.isArray(e.begin)){ +if(e.skip||e.excludeBegin||e.returnBegin)throw $("skip, excludeBegin, returnBegin not compatible with beginScope: {}"), +K +;if("object"!=typeof e.beginScope||null===e.beginScope)throw $("beginScope must be object"), +K;W(e,e.begin,{key:"beginScope"}),e.begin=p(e.begin,{joinWith:""})}})(e),(e=>{ +if(Array.isArray(e.end)){ +if(e.skip||e.excludeEnd||e.returnEnd)throw $("skip, excludeEnd, returnEnd not compatible with endScope: {}"), +K +;if("object"!=typeof e.endScope||null===e.endScope)throw $("endScope must be object"), +K;W(e,e.end,{key:"endScope"}),e.end=p(e.end,{joinWith:""})}})(e)}function G(e){ +function t(t,n){return RegExp(g(t),"m"+(e.case_insensitive?"i":"")+(n?"g":""))} +class n{constructor(){ +this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0} +addRule(e,t){ +t.position=this.position++,this.matchIndexes[this.matchAt]=t,this.regexes.push([t,e]), +this.matchAt+=h(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null) +;const e=this.regexes.map((e=>e[1]));this.matcherRe=t(p(e,{joinWith:"|" +}),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex +;const t=this.matcherRe.exec(e);if(!t)return null +;const n=t.findIndex(((e,t)=>t>0&&void 0!==e)),i=this.matchIndexes[n] +;return t.splice(0,n),Object.assign(t,i)}}class i{constructor(){ +this.rules=[],this.multiRegexes=[], +this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){ +if(this.multiRegexes[e])return this.multiRegexes[e];const t=new n +;return this.rules.slice(e).forEach((([e,n])=>t.addRule(e,n))), +t.compile(),this.multiRegexes[e]=t,t}resumingScanAtSamePosition(){ +return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,t){ +this.rules.push([e,t]),"begin"===t.type&&this.count++}exec(e){ +const t=this.getMatcher(this.regexIndex);t.lastIndex=this.lastIndex +;let n=t.exec(e) +;if(this.resumingScanAtSamePosition())if(n&&n.index===this.lastIndex);else{ +const t=this.getMatcher(0);t.lastIndex=this.lastIndex+1,n=t.exec(e)} +return n&&(this.regexIndex+=n.position+1, +this.regexIndex===this.count&&this.considerAll()),n}} +if(e.compilerExtensions||(e.compilerExtensions=[]), +e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.") +;return e.classNameAliases=s(e.classNameAliases||{}),function n(r,o){const a=r +;if(r.isCompiled)return a +;[j,B,X,L].forEach((e=>e(r,o))),e.compilerExtensions.forEach((e=>e(r,o))), +r.__beforeBegin=null,[A,I,T].forEach((e=>e(r,o))),r.isCompiled=!0;let l=null +;return"object"==typeof r.keywords&&r.keywords.$pattern&&(r.keywords=Object.assign({},r.keywords), +l=r.keywords.$pattern, +delete r.keywords.$pattern),l=l||/\w+/,r.keywords&&(r.keywords=P(r.keywords,e.case_insensitive)), +a.keywordPatternRe=t(l,!0), +o&&(r.begin||(r.begin=/\B|\b/),a.beginRe=t(r.begin),r.end||r.endsWithParent||(r.end=/\B|\b/), +r.end&&(a.endRe=t(r.end)), +a.terminatorEnd=g(r.end)||"",r.endsWithParent&&o.terminatorEnd&&(a.terminatorEnd+=(r.end?"|":"")+o.terminatorEnd)), +r.illegal&&(a.illegalRe=t(r.illegal)), +r.contains||(r.contains=[]),r.contains=[].concat(...r.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((t=>s(e,{ +variants:null},t)))),e.cachedVariants?e.cachedVariants:Z(e)?s(e,{ +starts:e.starts?s(e.starts):null +}):Object.isFrozen(e)?s(e):e))("self"===e?r:e)))),r.contains.forEach((e=>{n(e,a) +})),r.starts&&n(r.starts,o),a.matcher=(e=>{const t=new i +;return e.contains.forEach((e=>t.addRule(e.begin,{rule:e,type:"begin" +}))),e.terminatorEnd&&t.addRule(e.terminatorEnd,{type:"end" +}),e.illegal&&t.addRule(e.illegal,{type:"illegal"}),t})(a),a}(e)}function Z(e){ +return!!e&&(e.endsWithParent||Z(e.starts))}const F=r,V=s,q=Symbol("nomatch") +;var J=(e=>{const t=Object.create(null),r=Object.create(null),s=[];let o=!0 +;const a="Could not find the language '{}', did you forget to load/include a language module?",l={ +disableAutodetect:!0,name:"Plain text",contains:[]};let g={ +ignoreUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i, +languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-", +cssSelector:"pre code",languages:null,__emitter:c};function d(e){ +return g.noHighlightRe.test(e)}function u(e,t,n,i){let r="",s="" +;"object"==typeof t?(r=e, +n=t.ignoreIllegals,s=t.language,i=void 0):(z("10.7.0","highlight(lang, code, ...args) has been deprecated."), +z("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"), +s=e,r=t),void 0===n&&(n=!0);const o={code:r,language:s};w("before:highlight",o) +;const a=o.result?o.result:h(o.language,o.code,n,i) +;return a.code=o.code,w("after:highlight",a),a}function h(e,n,r,s){ +const l=Object.create(null);function c(){if(!k.keywords)return void S.addText(M) +;let e=0;k.keywordPatternRe.lastIndex=0;let t=k.keywordPatternRe.exec(M),n="" +;for(;t;){n+=M.substring(e,t.index) +;const r=_.case_insensitive?t[0].toLowerCase():t[0],s=(i=r,k.keywords[i]);if(s){ +const[e,i]=s +;if(S.addText(n),n="",l[r]=(l[r]||0)+1,l[r]<=7&&(R+=i),e.startsWith("_"))n+=t[0];else{ +const n=_.classNameAliases[e]||e;S.addKeyword(t[0],n)}}else n+=t[0] +;e=k.keywordPatternRe.lastIndex,t=k.keywordPatternRe.exec(M)}var i +;n+=M.substr(e),S.addText(n)}function d(){null!=k.subLanguage?(()=>{ +if(""===M)return;let e=null;if("string"==typeof k.subLanguage){ +if(!t[k.subLanguage])return void S.addText(M) +;e=h(k.subLanguage,M,!0,N[k.subLanguage]),N[k.subLanguage]=e._top +}else e=f(M,k.subLanguage.length?k.subLanguage:null) +;k.relevance>0&&(R+=e.relevance),S.addSublanguage(e._emitter,e.language) +})():c(),M=""}function u(e,t){let n=1;for(;void 0!==t[n];){if(!e._emit[n]){n++ +;continue}const i=_.classNameAliases[e[n]]||e[n],r=t[n] +;i?S.addKeyword(r,i):(M=r,c(),M=""),n++}}function p(e,t){ +return e.scope&&"string"==typeof e.scope&&S.openNode(_.classNameAliases[e.scope]||e.scope), +e.beginScope&&(e.beginScope._wrap?(S.addKeyword(M,_.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap), +M=""):e.beginScope._multi&&(u(e.beginScope,t),M="")),k=Object.create(e,{parent:{ +value:k}}),k}function b(e,t,n){let r=((e,t)=>{const n=e&&e.exec(t) +;return n&&0===n.index})(e.endRe,n);if(r){if(e["on:end"]){const n=new i(e) +;e["on:end"](t,n),n.isMatchIgnored&&(r=!1)}if(r){ +for(;e.endsParent&&e.parent;)e=e.parent;return e}} +if(e.endsWithParent)return b(e.parent,t,n)}function m(e){ +return 0===k.matcher.regexIndex?(M+=e[0],1):(I=!0,0)}function x(e){ +const t=e[0],i=n.substr(e.index),r=b(k,e,i);if(!r)return q;const s=k +;k.endScope&&k.endScope._wrap?(d(), +S.addKeyword(t,k.endScope._wrap)):k.endScope&&k.endScope._multi?(d(), +u(k.endScope,e)):s.skip?M+=t:(s.returnEnd||s.excludeEnd||(M+=t), +d(),s.excludeEnd&&(M=t));do{ +k.scope&&!k.isMultiClass&&S.closeNode(),k.skip||k.subLanguage||(R+=k.relevance), +k=k.parent}while(k!==r.parent) +;return r.starts&&p(r.starts,e),s.returnEnd?0:t.length}let y={};function w(t,s){ +const a=s&&s[0];if(M+=t,null==a)return d(),0 +;if("begin"===y.type&&"end"===s.type&&y.index===s.index&&""===a){ +if(M+=n.slice(s.index,s.index+1),!o){const t=Error(`0 width match regex (${e})`) +;throw t.languageName=e,t.badRule=y.rule,t}return 1} +if(y=s,"begin"===s.type)return(e=>{ +const t=e[0],n=e.rule,r=new i(n),s=[n.__beforeBegin,n["on:begin"]] +;for(const n of s)if(n&&(n(e,r),r.isMatchIgnored))return m(t) +;return n.skip?M+=t:(n.excludeBegin&&(M+=t), +d(),n.returnBegin||n.excludeBegin||(M=t)),p(n,e),n.returnBegin?0:t.length})(s) +;if("illegal"===s.type&&!r){ +const e=Error('Illegal lexeme "'+a+'" for mode "'+(k.scope||"")+'"') +;throw e.mode=k,e}if("end"===s.type){const e=x(s);if(e!==q)return e} +if("illegal"===s.type&&""===a)return 1 +;if(A>1e5&&A>3*s.index)throw Error("potential infinite loop, way more iterations than matches") +;return M+=a,a.length}const _=E(e) +;if(!_)throw $(a.replace("{}",e)),Error('Unknown language: "'+e+'"') +;const v=G(_);let O="",k=s||v;const N={},S=new g.__emitter(g);(()=>{const e=[] +;for(let t=k;t!==_;t=t.parent)t.scope&&e.unshift(t.scope) +;e.forEach((e=>S.openNode(e)))})();let M="",R=0,j=0,A=0,I=!1;try{ +for(k.matcher.considerAll();;){ +A++,I?I=!1:k.matcher.considerAll(),k.matcher.lastIndex=j +;const e=k.matcher.exec(n);if(!e)break;const t=w(n.substring(j,e.index),e) +;j=e.index+t}return w(n.substr(j)),S.closeAllNodes(),S.finalize(),O=S.toHTML(),{ +language:e,value:O,relevance:R,illegal:!1,_emitter:S,_top:k}}catch(t){ +if(t.message&&t.message.includes("Illegal"))return{language:e,value:F(n), +illegal:!0,relevance:0,_illegalBy:{message:t.message,index:j, +context:n.slice(j-100,j+100),mode:t.mode,resultSoFar:O},_emitter:S};if(o)return{ +language:e,value:F(n),illegal:!1,relevance:0,errorRaised:t,_emitter:S,_top:k} +;throw t}}function f(e,n){n=n||g.languages||Object.keys(t);const i=(e=>{ +const t={value:F(e),illegal:!1,relevance:0,_top:l,_emitter:new g.__emitter(g)} +;return t._emitter.addText(e),t})(e),r=n.filter(E).filter(y).map((t=>h(t,e,!1))) +;r.unshift(i);const s=r.sort(((e,t)=>{ +if(e.relevance!==t.relevance)return t.relevance-e.relevance +;if(e.language&&t.language){if(E(e.language).supersetOf===t.language)return 1 +;if(E(t.language).supersetOf===e.language)return-1}return 0})),[o,a]=s,c=o +;return c.secondBest=a,c}function p(e){let t=null;const n=(e=>{ +let t=e.className+" ";t+=e.parentNode?e.parentNode.className:"" +;const n=g.languageDetectRe.exec(t);if(n){const t=E(n[1]) +;return t||(U(a.replace("{}",n[1])), +U("Falling back to no-highlight mode for this block.",e)),t?n[1]:"no-highlight"} +return t.split(/\s+/).find((e=>d(e)||E(e)))})(e);if(d(n))return +;w("before:highlightElement",{el:e,language:n +}),!g.ignoreUnescapedHTML&&e.children.length>0&&(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."), +console.warn("https://github.com/highlightjs/highlight.js/issues/2886"), +console.warn(e)),t=e;const i=t.textContent,s=n?u(i,{language:n,ignoreIllegals:!0 +}):f(i);e.innerHTML=s.value,((e,t,n)=>{const i=t&&r[t]||n +;e.classList.add("hljs"),e.classList.add("language-"+i) +})(e,n,s.language),e.result={language:s.language,re:s.relevance, +relevance:s.relevance},s.secondBest&&(e.secondBest={ +language:s.secondBest.language,relevance:s.secondBest.relevance +}),w("after:highlightElement",{el:e,result:s,text:i})}let b=!1;function m(){ +"loading"!==document.readyState?document.querySelectorAll(g.cssSelector).forEach(p):b=!0 +}function E(e){return e=(e||"").toLowerCase(),t[e]||t[r[e]]} +function x(e,{languageName:t}){"string"==typeof e&&(e=[e]),e.forEach((e=>{ +r[e.toLowerCase()]=t}))}function y(e){const t=E(e) +;return t&&!t.disableAutodetect}function w(e,t){const n=e;s.forEach((e=>{ +e[n]&&e[n](t)}))} +"undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(()=>{ +b&&m()}),!1),Object.assign(e,{highlight:u,highlightAuto:f,highlightAll:m, +highlightElement:p, +highlightBlock:e=>(z("10.7.0","highlightBlock will be removed entirely in v12.0"), +z("10.7.0","Please use highlightElement now."),p(e)),configure:e=>{g=V(g,e)}, +initHighlighting:()=>{ +m(),z("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")}, +initHighlightingOnLoad:()=>{ +m(),z("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.") +},registerLanguage:(n,i)=>{let r=null;try{r=i(e)}catch(e){ +if($("Language definition for '{}' could not be registered.".replace("{}",n)), +!o)throw e;$(e),r=l} +r.name||(r.name=n),t[n]=r,r.rawDefinition=i.bind(null,e),r.aliases&&x(r.aliases,{ +languageName:n})},unregisterLanguage:e=>{delete t[e] +;for(const t of Object.keys(r))r[t]===e&&delete r[t]}, +listLanguages:()=>Object.keys(t),getLanguage:E,registerAliases:x, +autoDetection:y,inherit:V,addPlugin:e=>{(e=>{ +e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=t=>{ +e["before:highlightBlock"](Object.assign({block:t.el},t)) +}),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=t=>{ +e["after:highlightBlock"](Object.assign({block:t.el},t))})})(e),s.push(e)} +}),e.debugMode=()=>{o=!1},e.safeMode=()=>{o=!0},e.versionString="11.0.1" +;for(const e in M)"object"==typeof M[e]&&n(M[e]);return Object.assign(e,M),e +})({}),Y=Object.freeze({__proto__:null});const Q=J +;for(const e of Object.keys(Y)){const t=e.replace("grmr_","") +;Q.registerLanguage(t,Y[e])}return Q}() +;"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs);hljs.registerLanguage("scheme",(()=>{"use strict";return e=>{ +const t="[^\\(\\)\\[\\]\\{\\}\",'`;#|\\\\\\s]+",n={$pattern:t, +built_in:"case-lambda call/cc class define-class exit-handler field import inherit init-field interface let*-values let-values let/ec mixin opt-lambda override protect provide public rename require require-for-syntax syntax syntax-case syntax-error unit/sig unless when with-syntax and begin call-with-current-continuation call-with-input-file call-with-output-file case cond define define-syntax delay do dynamic-wind else for-each if lambda let let* let-syntax letrec letrec-syntax map or syntax-rules ' * + , ,@ - ... / ; < <= = => > >= ` abs acos angle append apply asin assoc assq assv atan boolean? caar cadr call-with-input-file call-with-output-file call-with-values car cdddar cddddr cdr ceiling char->integer char-alphabetic? char-ci<=? char-ci=? char-ci>? char-downcase char-lower-case? char-numeric? char-ready? char-upcase char-upper-case? char-whitespace? char<=? char=? char>? char? close-input-port close-output-port complex? cons cos current-input-port current-output-port denominator display eof-object? eq? equal? eqv? eval even? exact->inexact exact? exp expt floor force gcd imag-part inexact->exact inexact? input-port? integer->char integer? interaction-environment lcm length list list->string list->vector list-ref list-tail list? load log magnitude make-polar make-rectangular make-string make-vector max member memq memv min modulo negative? newline not null-environment null? number->string number? numerator odd? open-input-file open-output-file output-port? pair? peek-char port? positive? procedure? quasiquote quote quotient rational? rationalize read read-char real-part real? remainder reverse round scheme-report-environment set! set-car! set-cdr! sin sqrt string string->list string->number string->symbol string-append string-ci<=? string-ci=? string-ci>? string-copy string-fill! string-length string-ref string-set! string<=? string=? string>? string? substring symbol->string symbol? tan transcript-off transcript-on truncate values vector vector->list vector-fill! vector-length vector-ref vector-set! with-input-from-file with-output-to-file write write-char zero?" +},r={className:"literal",begin:"(#t|#f|#\\\\"+t+"|#\\\\.)"},a={ +className:"number",variants:[{begin:"(-|\\+)?\\d+([./]\\d+)?",relevance:0},{ +begin:"(-|\\+)?\\d+([./]\\d+)?[+\\-](-|\\+)?\\d+([./]\\d+)?i",relevance:0},{ +begin:"#b[0-1]+(/[0-1]+)?"},{begin:"#o[0-7]+(/[0-7]+)?"},{ +begin:"#x[0-9a-f]+(/[0-9a-f]+)?"}]},i=e.QUOTE_STRING_MODE,c=[e.COMMENT(";","$",{ +relevance:0}),e.COMMENT("#\\|","\\|#")],s={begin:t,relevance:0},l={ +className:"symbol",begin:"'"+t},o={endsWithParent:!0,relevance:0},g={variants:[{ +begin:/'/},{begin:"`"}],contains:[{begin:"\\(",end:"\\)", +contains:["self",r,i,a,s,l]}]},u={className:"name",relevance:0,begin:t, +keywords:n},d={variants:[{begin:"\\(",end:"\\)"},{begin:"\\[",end:"\\]"}], +contains:[{begin:/lambda/,endsWithParent:!0,returnBegin:!0,contains:[u,{ +endsParent:!0,variants:[{begin:/\(/,end:/\)/},{begin:/\[/,end:/\]/}], +contains:[s]}]},u,o]};return o.contains=[r,a,i,s,l,g,d].concat(c),{ +name:"Scheme",illegal:/\S/,contains:[e.SHEBANG(),a,i,l,g,d].concat(c)}}})());hljs.registerLanguage("clojure",(()=>{"use strict";return e=>{ +const t="a-zA-Z_\\-!.?+*=<>&#'",n="["+t+"]["+t+"0-9/;:]*",r="def defonce defprotocol defstruct defmulti defmethod defn- defn defmacro deftype defrecord",a={ +$pattern:n, +built_in:r+" cond apply if-not if-let if not not= =|0 <|0 >|0 <=|0 >=|0 ==|0 +|0 /|0 *|0 -|0 rem quot neg? pos? delay? symbol? keyword? true? false? integer? empty? coll? list? set? ifn? fn? associative? sequential? sorted? counted? reversible? number? decimal? class? distinct? isa? float? rational? reduced? ratio? odd? even? char? seq? vector? string? map? nil? contains? zero? instance? not-every? not-any? libspec? -> ->> .. . inc compare do dotimes mapcat take remove take-while drop letfn drop-last take-last drop-while while intern condp case reduced cycle split-at split-with repeat replicate iterate range merge zipmap declare line-seq sort comparator sort-by dorun doall nthnext nthrest partition eval doseq await await-for let agent atom send send-off release-pending-sends add-watch mapv filterv remove-watch agent-error restart-agent set-error-handler error-handler set-error-mode! error-mode shutdown-agents quote var fn loop recur throw try monitor-enter monitor-exit macroexpand macroexpand-1 for dosync and or when when-not when-let comp juxt partial sequence memoize constantly complement identity assert peek pop doto proxy first rest cons cast coll last butlast sigs reify second ffirst fnext nfirst nnext meta with-meta ns in-ns create-ns import refer keys select-keys vals key val rseq name namespace promise into transient persistent! conj! assoc! dissoc! pop! disj! use class type num float double short byte boolean bigint biginteger bigdec print-method print-dup throw-if printf format load compile get-in update-in pr pr-on newline flush read slurp read-line subvec with-open memfn time re-find re-groups rand-int rand mod locking assert-valid-fdecl alias resolve ref deref refset swap! reset! set-validator! compare-and-set! alter-meta! reset-meta! commute get-validator alter ref-set ref-history-count ref-min-history ref-max-history ensure sync io! new next conj set! to-array future future-call into-array aset gen-class reduce map filter find empty hash-map hash-set sorted-map sorted-map-by sorted-set sorted-set-by vec vector seq flatten reverse assoc dissoc list disj get union difference intersection extend extend-type extend-protocol int nth delay count concat chunk chunk-buffer chunk-append chunk-first chunk-rest max min dec unchecked-inc-int unchecked-inc unchecked-dec-inc unchecked-dec unchecked-negate unchecked-add-int unchecked-add unchecked-subtract-int unchecked-subtract chunk-next chunk-cons chunked-seq? prn vary-meta lazy-seq spread list* str find-keyword keyword symbol gensym force rationalize" +},s={begin:n,relevance:0},o={className:"number",begin:"[-+]?\\d+(\\.\\d+)?", +relevance:0},i=e.inherit(e.QUOTE_STRING_MODE,{illegal:null +}),c=e.COMMENT(";","$",{relevance:0}),d={className:"literal", +begin:/\b(true|false|nil)\b/},l={begin:"[\\[\\{]",end:"[\\]\\}]",relevance:0 +},m={className:"comment",begin:"\\^"+n},p=e.COMMENT("\\^\\{","\\}"),u={ +className:"symbol",begin:"[:]{1,2}"+n},f={begin:"\\(",end:"\\)"},h={ +endsWithParent:!0,relevance:0},y={keywords:a,className:"name",begin:n, +relevance:0,starts:h},g=[f,i,m,p,c,u,l,o,d,s],b={beginKeywords:r,keywords:{ +$pattern:n,keyword:r},end:'(\\[|#|\\d|"|:|\\{|\\)|\\(|$)',contains:[{ +className:"title",begin:n,relevance:0,excludeEnd:!0,endsParent:!0}].concat(g)} +;return f.contains=[e.COMMENT("comment",""),b,y,h], +h.contains=g,l.contains=g,p.contains=[l],{name:"Clojure",aliases:["clj"], +illegal:/\S/,contains:[f,i,m,p,c,u,l,o,d]}}})());hljs.registerLanguage("bash",(()=>{"use strict";function e(...e){ +return e.map((e=>{return(s=e)?"string"==typeof s?s:s.source:null;var s +})).join("")}return s=>{const n={},t={begin:/\$\{/,end:/\}/,contains:["self",{ +begin:/:-/,contains:[n]}]};Object.assign(n,{className:"variable",variants:[{ +begin:e(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},t]});const a={ +className:"subst",begin:/\$\(/,end:/\)/,contains:[s.BACKSLASH_ESCAPE]},i={ +begin:/<<-?\s*(?=\w+)/,starts:{contains:[s.END_SAME_AS_BEGIN({begin:/(\w+)/, +end:/(\w+)/,className:"string"})]}},c={className:"string",begin:/"/,end:/"/, +contains:[s.BACKSLASH_ESCAPE,n,a]};a.contains.push(c);const o={begin:/\$\(\(/, +end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},s.NUMBER_MODE,n] +},r=s.SHEBANG({binary:"(fish|bash|zsh|sh|csh|ksh|tcsh|dash|scsh)",relevance:10 +}),l={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0, +contains:[s.inherit(s.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{ +name:"Bash",aliases:["sh"],keywords:{$pattern:/\b[a-z._-]+\b/, +keyword:["if","then","else","elif","fi","for","while","in","do","done","case","esac","function"], +literal:["true","false"], +built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp" +},contains:[r,s.SHEBANG(),l,o,s.HASH_COMMENT_MODE,i,c,{className:"",begin:/\\"/ +},{className:"string",begin:/'/,end:/'/},n]}}})());hljs.registerLanguage("lisp",(()=>{"use strict";return e=>{ +var n="[a-zA-Z_\\-+\\*\\/<=>&#][a-zA-Z0-9_\\-+*\\/<=>&#!]*",a="\\|[^]*?\\|",i="(-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|-)?\\d+)?",s={ +className:"literal",begin:"\\b(t{1}|nil)\\b"},l={className:"number",variants:[{ +begin:i,relevance:0},{begin:"#(b|B)[0-1]+(/[0-1]+)?"},{ +begin:"#(o|O)[0-7]+(/[0-7]+)?"},{begin:"#(x|X)[0-9a-fA-F]+(/[0-9a-fA-F]+)?"},{ +begin:"#(c|C)\\("+i+" +"+i,end:"\\)"}]},b=e.inherit(e.QUOTE_STRING_MODE,{ +illegal:null}),g=e.COMMENT(";","$",{relevance:0}),r={begin:"\\*",end:"\\*"},t={ +className:"symbol",begin:"[:&]"+n},c={begin:n,relevance:0},d={begin:a},o={ +contains:[l,b,r,t,{begin:"\\(",end:"\\)",contains:["self",s,b,l,c]},c], +variants:[{begin:"['`]\\(",end:"\\)"},{begin:"\\(quote ",end:"\\)",keywords:{ +name:"quote"}},{begin:"'"+a}]},v={variants:[{begin:"'"+n},{ +begin:"#'"+n+"(::"+n+")*"}]},m={begin:"\\(\\s*",end:"\\)"},u={endsWithParent:!0, +relevance:0};return m.contains=[{className:"name",variants:[{begin:n,relevance:0 +},{begin:a}]},u],u.contains=[o,v,m,s,l,b,g,r,t,d,c],{name:"Lisp",illegal:/\S/, +contains:[l,e.SHEBANG(),s,b,g,o,v,m,c]}}})());hljs.registerLanguage("css",(()=>{"use strict" +;const e=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","p","q","quote","samp","section","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],t=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"],i=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"],o=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"],r=["align-content","align-items","align-self","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","auto","backface-visibility","background","background-attachment","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","border","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","clear","clip","clip-path","color","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","content","counter-increment","counter-reset","cursor","direction","display","empty-cells","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","font-smoothing","font-stretch","font-style","font-variant","font-variant-ligatures","font-variation-settings","font-weight","height","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","inherit","initial","justify-content","left","letter-spacing","line-height","list-style","list-style-image","list-style-position","list-style-type","margin","margin-bottom","margin-left","margin-right","margin-top","marks","mask","max-height","max-width","min-height","min-width","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-wrap","overflow-x","overflow-y","padding","padding-bottom","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","perspective","perspective-origin","pointer-events","position","quotes","resize","right","src","tab-size","table-layout","text-align","text-align-last","text-decoration","text-decoration-color","text-decoration-line","text-decoration-style","text-indent","text-overflow","text-rendering","text-shadow","text-transform","text-underline-position","top","transform","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vertical-align","visibility","white-space","widows","width","word-break","word-spacing","word-wrap","z-index"].reverse() +;return n=>{const a=(e=>({IMPORTANT:{scope:"meta",begin:"!important"},HEXCOLOR:{ +scope:"number",begin:"#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})"}, +ATTRIBUTE_SELECTOR_MODE:{scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$", +contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{ +scope:"number", +begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?", +relevance:0}}))(n),l=[n.APOS_STRING_MODE,n.QUOTE_STRING_MODE];return{name:"CSS", +case_insensitive:!0,illegal:/[=|'\$]/,keywords:{keyframePosition:"from to"}, +classNameAliases:{keyframePosition:"selector-tag"}, +contains:[n.C_BLOCK_COMMENT_MODE,{begin:/-(webkit|moz|ms|o)-(?=[a-z])/ +},a.CSS_NUMBER_MODE,{className:"selector-id",begin:/#[A-Za-z0-9_-]+/,relevance:0 +},{className:"selector-class",begin:"\\.[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0 +},a.ATTRIBUTE_SELECTOR_MODE,{className:"selector-pseudo",variants:[{ +begin:":("+i.join("|")+")"},{begin:"::("+o.join("|")+")"}]},{ +className:"attribute",begin:"\\b("+r.join("|")+")\\b"},{begin:":",end:"[;}]", +contains:[a.HEXCOLOR,a.IMPORTANT,a.CSS_NUMBER_MODE,...l,{ +begin:/(url|data-uri)\(/,end:/\)/,relevance:0,keywords:{built_in:"url data-uri" +},contains:[{className:"string",begin:/[^)]/,endsWithParent:!0,excludeEnd:!0}] +},{className:"built_in",begin:/[\w-]+(?=\()/}]},{ +begin:(s=/@/,((...e)=>e.map((e=>(e=>e?"string"==typeof e?e:e.source:null)(e))).join(""))("(?=",s,")")), +end:"[{;]",relevance:0,illegal:/:/,contains:[{className:"keyword", +begin:/@-?\w[\w]*(-\w+)*/},{begin:/\s/,endsWithParent:!0,excludeEnd:!0, +relevance:0,keywords:{$pattern:/[a-z-]+/,keyword:"and or not only", +attribute:t.join(" ")},contains:[{begin:/[a-z-]+(?=:)/,className:"attribute" +},...l,a.CSS_NUMBER_MODE]}]},{className:"selector-tag", +begin:"\\b("+e.join("|")+")\\b"}]};var s}})());hljs.registerLanguage("vim",(()=>{"use strict";return e=>({name:"Vim Script", +keywords:{$pattern:/[!#@\w]+/, +keyword:"N|0 P|0 X|0 a|0 ab abc abo al am an|0 ar arga argd arge argdo argg argl argu as au aug aun b|0 bN ba bad bd be bel bf bl bm bn bo bp br brea breaka breakd breakl bro bufdo buffers bun bw c|0 cN cNf ca cabc caddb cad caddf cal cat cb cc ccl cd ce cex cf cfir cgetb cgete cg changes chd che checkt cl cla clo cm cmapc cme cn cnew cnf cno cnorea cnoreme co col colo com comc comp con conf cope cp cpf cq cr cs cst cu cuna cunme cw delm deb debugg delc delf dif diffg diffo diffp diffpu diffs diffthis dig di dl dell dj dli do doautoa dp dr ds dsp e|0 ea ec echoe echoh echom echon el elsei em en endfo endf endt endw ene ex exe exi exu f|0 files filet fin fina fini fir fix fo foldc foldd folddoc foldo for fu go gr grepa gu gv ha helpf helpg helpt hi hid his ia iabc if ij il im imapc ime ino inorea inoreme int is isp iu iuna iunme j|0 ju k|0 keepa kee keepj lN lNf l|0 lad laddb laddf la lan lat lb lc lch lcl lcs le lefta let lex lf lfir lgetb lgete lg lgr lgrepa lh ll lla lli lmak lm lmapc lne lnew lnf ln loadk lo loc lockv lol lope lp lpf lr ls lt lu lua luad luaf lv lvimgrepa lw m|0 ma mak map mapc marks mat me menut mes mk mks mksp mkv mkvie mod mz mzf nbc nb nbs new nm nmapc nme nn nnoreme noa no noh norea noreme norm nu nun nunme ol o|0 om omapc ome on ono onoreme opt ou ounme ow p|0 profd prof pro promptr pc ped pe perld po popu pp pre prev ps pt ptN ptf ptj ptl ptn ptp ptr pts pu pw py3 python3 py3d py3f py pyd pyf quita qa rec red redi redr redraws reg res ret retu rew ri rightb rub rubyd rubyf rund ru rv sN san sa sal sav sb sbN sba sbf sbl sbm sbn sbp sbr scrip scripte scs se setf setg setl sf sfir sh sim sig sil sl sla sm smap smapc sme sn sni sno snor snoreme sor so spelld spe spelli spellr spellu spellw sp spr sre st sta startg startr star stopi stj sts sun sunm sunme sus sv sw sy synti sync tN tabN tabc tabdo tabe tabf tabfir tabl tabm tabnew tabn tabo tabp tabr tabs tab ta tags tc tcld tclf te tf th tj tl tm tn to tp tr try ts tu u|0 undoj undol una unh unl unlo unm unme uns up ve verb vert vim vimgrepa vi viu vie vm vmapc vme vne vn vnoreme vs vu vunme windo w|0 wN wa wh wi winc winp wn wp wq wqa ws wu wv x|0 xa xmapc xm xme xn xnoreme xu xunme y|0 z|0 ~ Next Print append abbreviate abclear aboveleft all amenu anoremenu args argadd argdelete argedit argglobal arglocal argument ascii autocmd augroup aunmenu buffer bNext ball badd bdelete behave belowright bfirst blast bmodified bnext botright bprevious brewind break breakadd breakdel breaklist browse bunload bwipeout change cNext cNfile cabbrev cabclear caddbuffer caddexpr caddfile call catch cbuffer cclose center cexpr cfile cfirst cgetbuffer cgetexpr cgetfile chdir checkpath checktime clist clast close cmap cmapclear cmenu cnext cnewer cnfile cnoremap cnoreabbrev cnoremenu copy colder colorscheme command comclear compiler continue confirm copen cprevious cpfile cquit crewind cscope cstag cunmap cunabbrev cunmenu cwindow delete delmarks debug debuggreedy delcommand delfunction diffupdate diffget diffoff diffpatch diffput diffsplit digraphs display deletel djump dlist doautocmd doautoall deletep drop dsearch dsplit edit earlier echo echoerr echohl echomsg else elseif emenu endif endfor endfunction endtry endwhile enew execute exit exusage file filetype find finally finish first fixdel fold foldclose folddoopen folddoclosed foldopen function global goto grep grepadd gui gvim hardcopy help helpfind helpgrep helptags highlight hide history insert iabbrev iabclear ijump ilist imap imapclear imenu inoremap inoreabbrev inoremenu intro isearch isplit iunmap iunabbrev iunmenu join jumps keepalt keepmarks keepjumps lNext lNfile list laddexpr laddbuffer laddfile last language later lbuffer lcd lchdir lclose lcscope left leftabove lexpr lfile lfirst lgetbuffer lgetexpr lgetfile lgrep lgrepadd lhelpgrep llast llist lmake lmap lmapclear lnext lnewer lnfile lnoremap loadkeymap loadview lockmarks lockvar lolder lopen lprevious lpfile lrewind ltag lunmap luado luafile lvimgrep lvimgrepadd lwindow move mark make mapclear match menu menutranslate messages mkexrc mksession mkspell mkvimrc mkview mode mzscheme mzfile nbclose nbkey nbsart next nmap nmapclear nmenu nnoremap nnoremenu noautocmd noremap nohlsearch noreabbrev noremenu normal number nunmap nunmenu oldfiles open omap omapclear omenu only onoremap onoremenu options ounmap ounmenu ownsyntax print profdel profile promptfind promptrepl pclose pedit perl perldo pop popup ppop preserve previous psearch ptag ptNext ptfirst ptjump ptlast ptnext ptprevious ptrewind ptselect put pwd py3do py3file python pydo pyfile quit quitall qall read recover redo redir redraw redrawstatus registers resize retab return rewind right rightbelow ruby rubydo rubyfile rundo runtime rviminfo substitute sNext sandbox sargument sall saveas sbuffer sbNext sball sbfirst sblast sbmodified sbnext sbprevious sbrewind scriptnames scriptencoding scscope set setfiletype setglobal setlocal sfind sfirst shell simalt sign silent sleep slast smagic smapclear smenu snext sniff snomagic snoremap snoremenu sort source spelldump spellgood spellinfo spellrepall spellundo spellwrong split sprevious srewind stop stag startgreplace startreplace startinsert stopinsert stjump stselect sunhide sunmap sunmenu suspend sview swapname syntax syntime syncbind tNext tabNext tabclose tabedit tabfind tabfirst tablast tabmove tabnext tabonly tabprevious tabrewind tag tcl tcldo tclfile tearoff tfirst throw tjump tlast tmenu tnext topleft tprevious trewind tselect tunmenu undo undojoin undolist unabbreviate unhide unlet unlockvar unmap unmenu unsilent update vglobal version verbose vertical vimgrep vimgrepadd visual viusage view vmap vmapclear vmenu vnew vnoremap vnoremenu vsplit vunmap vunmenu write wNext wall while winsize wincmd winpos wnext wprevious wqall wsverb wundo wviminfo xit xall xmapclear xmap xmenu xnoremap xnoremenu xunmap xunmenu yank", +built_in:"synIDtrans atan2 range matcharg did_filetype asin feedkeys xor argv complete_check add getwinposx getqflist getwinposy screencol clearmatches empty extend getcmdpos mzeval garbagecollect setreg ceil sqrt diff_hlID inputsecret get getfperm getpid filewritable shiftwidth max sinh isdirectory synID system inputrestore winline atan visualmode inputlist tabpagewinnr round getregtype mapcheck hasmapto histdel argidx findfile sha256 exists toupper getcmdline taglist string getmatches bufnr strftime winwidth bufexists strtrans tabpagebuflist setcmdpos remote_read printf setloclist getpos getline bufwinnr float2nr len getcmdtype diff_filler luaeval resolve libcallnr foldclosedend reverse filter has_key bufname str2float strlen setline getcharmod setbufvar index searchpos shellescape undofile foldclosed setqflist buflisted strchars str2nr virtcol floor remove undotree remote_expr winheight gettabwinvar reltime cursor tabpagenr finddir localtime acos getloclist search tanh matchend rename gettabvar strdisplaywidth type abs py3eval setwinvar tolower wildmenumode log10 spellsuggest bufloaded synconcealed nextnonblank server2client complete settabwinvar executable input wincol setmatches getftype hlID inputsave searchpair or screenrow line settabvar histadd deepcopy strpart remote_peek and eval getftime submatch screenchar winsaveview matchadd mkdir screenattr getfontname libcall reltimestr getfsize winnr invert pow getbufline byte2line soundfold repeat fnameescape tagfiles sin strwidth spellbadword trunc maparg log lispindent hostname setpos globpath remote_foreground getchar synIDattr fnamemodify cscope_connection stridx winbufnr indent min complete_add nr2char searchpairpos inputdialog values matchlist items hlexists strridx browsedir expand fmod pathshorten line2byte argc count getwinvar glob foldtextresult getreg foreground cosh matchdelete has char2nr simplify histget searchdecl iconv winrestcmd pumvisible writefile foldlevel haslocaldir keys cos matchstr foldtext histnr tan tempname getcwd byteidx getbufvar islocked escape eventhandler remote_send serverlist winrestview synstack pyeval prevnonblank readfile cindent filereadable changenr exp" +},illegal:/;/,contains:[e.NUMBER_MODE,{className:"string",begin:"'",end:"'", +illegal:"\\n"},{className:"string",begin:/"(\\"|\n\\|[^"\n])*"/ +},e.COMMENT('"',"$"),{className:"variable",begin:/[bwtglsav]:[\w\d_]+/},{ +begin:[/\b(?:function|function!)/,/\s+/,e.IDENT_RE],className:{1:"keyword", +3:"title"},end:"$",relevance:0,contains:[{className:"params",begin:"\\(", +end:"\\)"}]},{className:"symbol",begin:/<[\w-]+>/}]})})());hljs.registerLanguage("xml",(()=>{"use strict";function e(e){ +return e?"string"==typeof e?e:e.source:null}function n(e){return a("(?=",e,")")} +function a(...n){return n.map((n=>e(n))).join("")}function s(...n){ +return"("+((e=>{const n=e[e.length-1] +;return"object"==typeof n&&n.constructor===Object?(e.splice(e.length-1,1),n):{} +})(n).capture?"":"?:")+n.map((n=>e(n))).join("|")+")"}return e=>{ +const t=a(/[A-Z_]/,a("(?:",/[A-Z0-9_.-]*:/,")?"),/[A-Z0-9_.-]*/),i={ +className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},c={begin:/\s/, +contains:[{className:"keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}] +},r=e.inherit(c,{begin:/\(/,end:/\)/}),l=e.inherit(e.APOS_STRING_MODE,{ +className:"string"}),g=e.inherit(e.QUOTE_STRING_MODE,{className:"string"}),m={ +endsWithParent:!0,illegal:/`]+/}]}]}]};return{ +name:"HTML, XML", +aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"], +case_insensitive:!0,contains:[{className:"meta",begin://, +relevance:10,contains:[c,g,l,r,{begin:/\[/,end:/\]/,contains:[{className:"meta", +begin://,contains:[c,r,g,l]}]}]},e.COMMENT(//,{ +relevance:10}),{begin://,relevance:10},i,{ +className:"meta",begin:/<\?xml/,end:/\?>/,relevance:10},{className:"tag", +begin:/)/,end:/>/,keywords:{name:"style"},contains:[m],starts:{ +end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag", +begin:/)/,end:/>/,keywords:{name:"script"},contains:[m],starts:{ +end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{ +className:"tag",begin:/<>|<\/>/},{className:"tag", +begin:a(//,/>/,/\s/)))),end:/\/?>/,contains:[{className:"name", +begin:t,relevance:0,starts:m}]},{className:"tag",begin:a(/<\//,n(a(t,/>/))), +contains:[{className:"name",begin:t,relevance:0},{begin:/>/,relevance:0, +endsParent:!0}]}]}}})());hljs.registerLanguage("markdown",(()=>{"use strict";function n(...n){ +return n.map((n=>{return(e=n)?"string"==typeof e?e:e.source:null;var e +})).join("")}return e=>{const a={begin:/<\/?[A-Za-z_]/,end:">", +subLanguage:"xml",relevance:0},i={variants:[{begin:/\[.+?\]\[.*?\]/,relevance:0 +},{begin:/\[.+?\]\(((data|javascript|mailto):|(?:http|ftp)s?:\/\/).*?\)/, +relevance:2},{begin:n(/\[.+?\]\(/,/[A-Za-z][A-Za-z0-9+.-]*/,/:\/\/.*?\)/), +relevance:2},{begin:/\[.+?\]\([./?&#].*?\)/,relevance:1},{ +begin:/\[.+?\]\(.*?\)/,relevance:0}],returnBegin:!0,contains:[{ +className:"string",relevance:0,begin:"\\[",end:"\\]",excludeBegin:!0, +returnEnd:!0},{className:"link",relevance:0,begin:"\\]\\(",end:"\\)", +excludeBegin:!0,excludeEnd:!0},{className:"symbol",relevance:0,begin:"\\]\\[", +end:"\\]",excludeBegin:!0,excludeEnd:!0}]},s={className:"strong",contains:[], +variants:[{begin:/_{2}/,end:/_{2}/},{begin:/\*{2}/,end:/\*{2}/}]},c={ +className:"emphasis",contains:[],variants:[{begin:/\*(?!\*)/,end:/\*/},{ +begin:/_(?!_)/,end:/_/,relevance:0}]};s.contains.push(c),c.contains.push(s) +;let t=[a,i] +;return s.contains=s.contains.concat(t),c.contains=c.contains.concat(t), +t=t.concat(s,c),{name:"Markdown",aliases:["md","mkdown","mkd"],contains:[{ +className:"section",variants:[{begin:"^#{1,6}",end:"$",contains:t},{ +begin:"(?=^.+?\\n[=-]{2,}$)",contains:[{begin:"^[=-]*$"},{begin:"^",end:"\\n", +contains:t}]}]},a,{className:"bullet",begin:"^[ \t]*([*+-]|(\\d+\\.))(?=\\s+)", +end:"\\s+",excludeEnd:!0},s,c,{className:"quote",begin:"^>\\s+",contains:t, +end:"$"},{className:"code",variants:[{begin:"(`{3,})[^`](.|\\n)*?\\1`*[ ]*"},{ +begin:"(~{3,})[^~](.|\\n)*?\\1~*[ ]*"},{begin:"```",end:"```+[ ]*$"},{ +begin:"~~~",end:"~~~+[ ]*$"},{begin:"`.+?`"},{begin:"(?=^( {4}|\\t))", +contains:[{begin:"^( {4}|\\t)",end:"(\\n)$"}],relevance:0}]},{ +begin:"^[-\\*]{3,}",end:"$"},i,{begin:/^\[[^\n]+\]:/,returnBegin:!0,contains:[{ +className:"symbol",begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0},{ +className:"link",begin:/:\s*/,end:/$/,excludeBegin:!0}]}]}}})());hljs.registerLanguage("scss",(()=>{"use strict" +;const e=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","p","q","quote","samp","section","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],t=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"],i=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"],r=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"],o=["align-content","align-items","align-self","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","auto","backface-visibility","background","background-attachment","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","border","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","clear","clip","clip-path","color","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","content","counter-increment","counter-reset","cursor","direction","display","empty-cells","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","font-smoothing","font-stretch","font-style","font-variant","font-variant-ligatures","font-variation-settings","font-weight","height","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","inherit","initial","justify-content","left","letter-spacing","line-height","list-style","list-style-image","list-style-position","list-style-type","margin","margin-bottom","margin-left","margin-right","margin-top","marks","mask","max-height","max-width","min-height","min-width","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-wrap","overflow-x","overflow-y","padding","padding-bottom","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","perspective","perspective-origin","pointer-events","position","quotes","resize","right","src","tab-size","table-layout","text-align","text-align-last","text-decoration","text-decoration-color","text-decoration-line","text-decoration-style","text-indent","text-overflow","text-rendering","text-shadow","text-transform","text-underline-position","top","transform","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vertical-align","visibility","white-space","widows","width","word-break","word-spacing","word-wrap","z-index"].reverse() +;return a=>{const n=(e=>({IMPORTANT:{scope:"meta",begin:"!important"},HEXCOLOR:{ +scope:"number",begin:"#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})"}, +ATTRIBUTE_SELECTOR_MODE:{scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$", +contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{ +scope:"number", +begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?", +relevance:0}}))(a),l=r,s=i,d="@[a-z-]+",c={className:"variable", +begin:"(\\$[a-zA-Z-][a-zA-Z0-9_-]*)\\b"};return{name:"SCSS",case_insensitive:!0, +illegal:"[=/|']",contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{ +className:"selector-id",begin:"#[A-Za-z0-9_-]+",relevance:0},{ +className:"selector-class",begin:"\\.[A-Za-z0-9_-]+",relevance:0 +},n.ATTRIBUTE_SELECTOR_MODE,{className:"selector-tag", +begin:"\\b("+e.join("|")+")\\b",relevance:0},{className:"selector-pseudo", +begin:":("+s.join("|")+")"},{className:"selector-pseudo", +begin:"::("+l.join("|")+")"},c,{begin:/\(/,end:/\)/,contains:[n.CSS_NUMBER_MODE] +},{className:"attribute",begin:"\\b("+o.join("|")+")\\b"},{ +begin:"\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b" +},{begin:":",end:";", +contains:[c,n.HEXCOLOR,n.CSS_NUMBER_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,n.IMPORTANT] +},{begin:"@(page|font-face)",keywords:{$pattern:d,keyword:"@page @font-face"}},{ +begin:"@",end:"[{;]",returnBegin:!0,keywords:{$pattern:/[a-z-]+/, +keyword:"and or not only",attribute:t.join(" ")},contains:[{begin:d, +className:"keyword"},{begin:/[a-z-]+(?=:)/,className:"attribute" +},c,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,n.HEXCOLOR,n.CSS_NUMBER_MODE]}]}} +})());hljs.registerLanguage("diff",(()=>{"use strict";function e(...e){ +return"("+((e=>{const n=e[e.length-1] +;return"object"==typeof n&&n.constructor===Object?(e.splice(e.length-1,1),n):{} +})(e).capture?"":"?:")+e.map((e=>{return(n=e)?"string"==typeof n?n:n.source:null +;var n})).join("|")+")"}return n=>({name:"Diff",aliases:["patch"],contains:[{ +className:"meta",relevance:10, +match:e(/^@@ +-\d+,\d+ +\+\d+,\d+ +@@/,/^\*\*\* +\d+,\d+ +\*\*\*\*$/,/^--- +\d+,\d+ +----$/) +},{className:"comment",variants:[{ +begin:e(/Index: /,/^index/,/={3,}/,/^-{3}/,/^\*{3} /,/^\+{3}/,/^diff --git/), +end:/$/},{match:/^\*{15}$/}]},{className:"addition",begin:/^\+/,end:/$/},{ +className:"deletion",begin:/^-/,end:/$/},{className:"addition",begin:/^!/, +end:/$/}]})})());hljs.registerLanguage("plaintext",(()=>{"use strict";return t=>({ +name:"Plain text",aliases:["text","txt"],disableAutodetect:!0})})());hljs.registerLanguage("ruby",(()=>{"use strict";function e(e){ +return n("(?=",e,")")}function n(...e){return e.map((e=>{ +return(n=e)?"string"==typeof n?n:n.source:null;var n})).join("")}return a=>{ +const i="([a-zA-Z_]\\w*[!?=]?|[-+~]@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?)",s={ +keyword:"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor __FILE__", +built_in:"proc lambda",literal:"true false nil"},r={className:"doctag", +begin:"@[A-Za-z]+"},b={begin:"#<",end:">"},c=[a.COMMENT("#","$",{contains:[r] +}),a.COMMENT("^=begin","^=end",{contains:[r],relevance:10 +}),a.COMMENT("^__END__","\\n$")],t={className:"subst",begin:/#\{/,end:/\}/, +keywords:s},g={className:"string",contains:[a.BACKSLASH_ESCAPE,t],variants:[{ +begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{begin:/%[qQwWx]?\(/, +end:/\)/},{begin:/%[qQwWx]?\[/,end:/\]/},{begin:/%[qQwWx]?\{/,end:/\}/},{ +begin:/%[qQwWx]?/},{begin:/%[qQwWx]?\//,end:/\//},{begin:/%[qQwWx]?%/, +end:/%/},{begin:/%[qQwWx]?-/,end:/-/},{begin:/%[qQwWx]?\|/,end:/\|/},{ +begin:/\B\?(\\\d{1,3})/},{begin:/\B\?(\\x[A-Fa-f0-9]{1,2})/},{ +begin:/\B\?(\\u\{?[A-Fa-f0-9]{1,6}\}?)/},{ +begin:/\B\?(\\M-\\C-|\\M-\\c|\\c\\M-|\\M-|\\C-\\M-)[\x20-\x7e]/},{ +begin:/\B\?\\(c|C-)[\x20-\x7e]/},{begin:/\B\?\\?\S/},{ +begin:n(/<<[-~]?'?/,e(/(\w+)(?=\W)[^\n]*\n(?:[^\n]*\n)*?\s*\1\b/)), +contains:[a.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/, +contains:[a.BACKSLASH_ESCAPE,t]})]}]},d="[0-9](_?[0-9])*",l={className:"number", +relevance:0,variants:[{ +begin:`\\b([1-9](_?[0-9])*|0)(\\.(${d}))?([eE][+-]?(${d})|r)?i?\\b`},{ +begin:"\\b0[dD][0-9](_?[0-9])*r?i?\\b"},{begin:"\\b0[bB][0-1](_?[0-1])*r?i?\\b" +},{begin:"\\b0[oO][0-7](_?[0-7])*r?i?\\b"},{ +begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*r?i?\\b"},{ +begin:"\\b0(_?[0-7])+r?i?\\b"}]},o={className:"params",begin:"\\(",end:"\\)", +endsParent:!0,keywords:s},_=[g,{className:"class",beginKeywords:"class module", +end:"$|;",illegal:/=/,contains:[a.inherit(a.TITLE_MODE,{ +begin:"[A-Za-z_]\\w*(::\\w+)*(\\?|!)?"}),{begin:"<\\s*",contains:[{ +begin:"("+a.IDENT_RE+"::)?"+a.IDENT_RE,relevance:0}]}].concat(c)},{ +className:"function",begin:n(/def\s+/,e(i+"\\s*(\\(|;|$)")),relevance:0, +keywords:"def",end:"$|;",contains:[a.inherit(a.TITLE_MODE,{begin:i +}),o].concat(c)},{begin:a.IDENT_RE+"::"},{className:"symbol", +begin:a.UNDERSCORE_IDENT_RE+"(!|\\?)?:",relevance:0},{className:"symbol", +begin:":(?!\\s)",contains:[g,{begin:i}],relevance:0},l,{className:"variable", +begin:"(\\$\\W)|((\\$|@@?)(\\w+))(?=[^@$?])(?![A-Za-z])(?![@$?'])"},{ +className:"params",begin:/\|/,end:/\|/,relevance:0,keywords:s},{ +begin:"("+a.RE_STARTERS_RE+"|unless)\\s*",keywords:"unless",contains:[{ +className:"regexp",contains:[a.BACKSLASH_ESCAPE,t],illegal:/\n/,variants:[{ +begin:"/",end:"/[a-z]*"},{begin:/%r\{/,end:/\}[a-z]*/},{begin:"%r\\(", +end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[",end:"\\][a-z]*"}] +}].concat(b,c),relevance:0}].concat(b,c);t.contains=_,o.contains=_;const E=[{ +begin:/^\s*=>/,starts:{end:"$",contains:_}},{className:"meta", +begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+>|(\\w+-)?\\d+\\.\\d+\\.\\d+(p\\d+)?[^\\d][^>]+>)(?=[ ])", +starts:{end:"$",contains:_}}];return c.unshift(b),{name:"Ruby", +aliases:["rb","gemspec","podspec","thor","irb"],keywords:s,illegal:/\/\*/, +contains:[a.SHEBANG({binary:"ruby"})].concat(E).concat(c).concat(_)}}})());hljs.registerLanguage("yaml",(()=>{"use strict";return e=>{ +const n="true false yes no null",a="[\\w#;/?:@&=+$,.~*'()[\\]]+",s={ +className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/ +},{begin:/\S+/}],contains:[e.BACKSLASH_ESCAPE,{className:"template-variable", +variants:[{begin:/\{\{/,end:/\}\}/},{begin:/%\{/,end:/\}/}]}]},i=e.inherit(s,{ +variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),l={ +end:",",endsWithParent:!0,excludeEnd:!0,keywords:n,relevance:0},t={begin:/\{/, +end:/\}/,contains:[l],illegal:"\\n",relevance:0},g={begin:"\\[",end:"\\]", +contains:[l],illegal:"\\n",relevance:0},b=[{className:"attr",variants:[{ +begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{ +begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---\\s*$", +relevance:10},{className:"string", +begin:"[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*"},{ +begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0, +relevance:0},{className:"type",begin:"!\\w+!"+a},{className:"type", +begin:"!<"+a+">"},{className:"type",begin:"!"+a},{className:"type",begin:"!!"+a +},{className:"meta",begin:"&"+e.UNDERSCORE_IDENT_RE+"$"},{className:"meta", +begin:"\\*"+e.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"-(?=[ ]|$)", +relevance:0},e.HASH_COMMENT_MODE,{beginKeywords:n,keywords:{literal:n}},{ +className:"number", +begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b" +},{className:"number",begin:e.C_NUMBER_RE+"\\b",relevance:0},t,g,s],c=[...b] +;return c.pop(),c.push(i),l.contains=c,{name:"YAML",case_insensitive:!0, +aliases:["yml"],contains:b}}})());hljs.registerLanguage("rust",(()=>{"use strict";function e(...e){ +return e.map((e=>{return(t=e)?"string"==typeof t?t:t.source:null;var t +})).join("")}return t=>{const n={className:"title.function.invoke",relevance:0, +begin:e(/\b/,/(?!let\b)/,t.IDENT_RE,(a=/\s*\(/,e("(?=",a,")")))};var a +;const r="([ui](8|16|32|64|128|size)|f(32|64))?",i=["drop ","Copy","Send","Sized","Sync","Drop","Fn","FnMut","FnOnce","ToOwned","Clone","Debug","PartialEq","PartialOrd","Eq","Ord","AsRef","AsMut","Into","From","Default","Iterator","Extend","IntoIterator","DoubleEndedIterator","ExactSizeIterator","SliceConcatExt","ToString","assert!","assert_eq!","bitflags!","bytes!","cfg!","col!","concat!","concat_idents!","debug_assert!","debug_assert_eq!","env!","panic!","file!","format!","format_args!","include_bin!","include_str!","line!","local_data_key!","module_path!","option_env!","print!","println!","select!","stringify!","try!","unimplemented!","unreachable!","vec!","write!","writeln!","macro_rules!","assert_ne!","debug_assert_ne!"] +;return{name:"Rust",aliases:["rs"],keywords:{$pattern:t.IDENT_RE+"!?", +type:["i8","i16","i32","i64","i128","isize","u8","u16","u32","u64","u128","usize","f32","f64","str","char","bool","Box","Option","Result","String","Vec"], +keyword:["abstract","as","async","await","become","box","break","const","continue","crate","do","dyn","else","enum","extern","false","final","fn","for","if","impl","in","let","loop","macro","match","mod","move","mut","override","priv","pub","ref","return","self","Self","static","struct","super","trait","true","try","type","typeof","unsafe","unsized","use","virtual","where","while","yield"], +literal:["true","false","Some","None","Ok","Err"],built_in:i},illegal:""},n]}}})());hljs.registerLanguage("javascript",(()=>{"use strict" +;const e="[A-Za-z$_][0-9A-Za-z$_]*",n=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],a=["true","false","null","undefined","NaN","Infinity"],t=["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer","BigInt64Array","BigUint64Array","BigInt"],s=["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],r=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],i=["arguments","this","super","console","window","document","localStorage","module","global"],c=[].concat(r,t,s) +;function o(e){return l("(?=",e,")")}function l(...e){return e.map((e=>{ +return(n=e)?"string"==typeof n?n:n.source:null;var n})).join("")}return b=>{ +const g=e,d={begin:/<[A-Za-z0-9\\._:-]+/,end:/\/[A-Za-z0-9\\._:-]+>|\/>/, +isTrulyOpeningTag:(e,n)=>{const a=e[0].length+e.index,t=e.input[a] +;"<"!==t?">"===t&&(((e,{after:n})=>{const a="",B={ +match:[/const|var|let/,/\s+/,g,/\s*/,/=\s*/,o(C)],className:{1:"keyword", +3:"title.function"},contains:[w]};return{name:"Javascript", +aliases:["js","jsx","mjs","cjs"],keywords:u,exports:{PARAMS_CONTAINS:S}, +illegal:/#(?![$_A-z])/,contains:[b.SHEBANG({label:"shebang",binary:"node", +relevance:5}),{label:"use_strict",className:"meta",relevance:10, +begin:/^\s*['"]use (strict|asm)['"]/ +},b.APOS_STRING_MODE,b.QUOTE_STRING_MODE,N,f,A,v,y,O,{className:"attr", +begin:g+o(":"),relevance:0},B,{ +begin:"("+b.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*", +keywords:"return throw case",relevance:0,contains:[v,b.REGEXP_MODE,{ +className:"function",begin:C,returnBegin:!0,end:"\\s*=>",contains:[{ +className:"params",variants:[{begin:b.UNDERSCORE_IDENT_RE,relevance:0},{ +className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0, +excludeEnd:!0,keywords:u,contains:S}]}]},{begin:/,/,relevance:0},{match:/\s+/, +relevance:0},{variants:[{begin:"<>",end:""},{begin:d.begin, +"on:begin":d.isTrulyOpeningTag,end:d.end}],subLanguage:"xml",contains:[{ +begin:d.begin,end:d.end,skip:!0,contains:["self"]}]}]},I,{ +beginKeywords:"while if switch catch for"},{ +begin:"\\b(?!function)"+b.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{", +returnBegin:!0,label:"func.def",contains:[w,b.inherit(b.TITLE_MODE,{begin:g, +className:"title.function"})]},{match:/\.\.\./,relevance:0},M,{match:"\\$"+g, +relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"}, +contains:[w]},T,{relevance:0,match:/\b[A-Z][A-Z_]+\b/, +className:"variable.constant"},R,k,{match:/\$[(.]/}]}}})());hljs.registerLanguage("json",(()=>{"use strict";return e=>({name:"JSON", +contains:[{className:"attr",begin:/"(\\.|[^\\"\r\n])*"(?=\s*:)/,relevance:1.01 +},{match:/[{}[\],:]/,className:"punctuation",relevance:0},e.QUOTE_STRING_MODE,{ +beginKeywords:"true false null" +},e.C_NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE],illegal:"\\S"}) +})()); \ No newline at end of file diff --git a/examples/eww-bar/eww.scss b/examples/eww-bar/eww.scss index 139c3ee..efa2a50 100644 --- a/examples/eww-bar/eww.scss +++ b/examples/eww-bar/eww.scss @@ -3,26 +3,26 @@ } //Global Styles -window { +.bar { background-color: #3a3a3a; color: #b0b4bc; - font-family: CascadiaCode; + padding: 10px; } -// Styles on classes (see eww.xml for more information) +// Styles on classes (see eww.yuck for more information) .sidestuff slider { all: unset; color: #ffd5cd; } -.slider-vol scale trough highlight { +.metric scale trough highlight { all: unset; background-color: #D35D6E; color: #000000; border-radius: 10px; } -.slider-vol scale trough { +.metric scale trough { all: unset; background-color: #4e4e4e; border-radius: 50px; @@ -31,13 +31,13 @@ window { margin-left: 10px; margin-right: 20px; } -.slider-ram scale trough highlight { +.metric scale trough highlight { all: unset; background-color: #D35D6E; color: #000000; border-radius: 10px; } -.slider-ram scale trough { +.metric scale trough { all: unset; background-color: #4e4e4e; border-radius: 50px; @@ -49,11 +49,7 @@ window { .label-ram { font-size: large; } -.time { - padding-right: 10px; -} .workspaces button:hover { color: #D35D6E; } - diff --git a/examples/eww-bar/eww.xml b/examples/eww-bar/eww.xml deleted file mode 100644 index 4c6e0af..0000000 --- a/examples/eww-bar/eww.xml +++ /dev/null @@ -1,104 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  {{music}} - - - - - - - - - - - - - - - - - - - {{hour}}:{{min}} {{month}} {{number_day}}, {{year_full}} - - - - - - - - - playerctl metadata --format '{{ artist }} - {{ title }}' - - - - - - ~/.config/eww/scripts/getvol - - - date "+%d" - date "+%b" - date "+%M" - date "+%H" - date "+%Y" - - - ~/.config/eww/scripts/getram - - - cat /sys/class/power_supply/BAT0/capacity - - - - - - - - - - - - - - diff --git a/examples/eww-bar/eww.yuck b/examples/eww-bar/eww.yuck new file mode 100644 index 0000000..7593ed6 --- /dev/null +++ b/examples/eww-bar/eww.yuck @@ -0,0 +1,75 @@ +(defwidget bar [] + (centerbox :orientation "h" + (workspaces) + (music) + (sidestuff))) + +(defwidget sidestuff [] + (box :class "sidestuff" :orientation "h" :space-evenly false :halign "end" + (metric :label "🔊" + :value volume + :onchange "amixer -D pulse sset Master {}%") + (metric :label "" + :value EWW_RAM + :onchange "") + (metric :label "💾" + :value {round((1 - (EWW_DISK["/"].free / EWW_DISK["/"].total)) * 100, 0)} + :onchange "") + time)) + +(defwidget workspaces [] + (box :class "workspaces" + :orientation "h" + :space-evenly true + :halign "start" + :spacing 10 + (button :onclick "wmctrl -s 0" 1) + (button :onclick "wmctrl -s 1" 2) + (button :onclick "wmctrl -s 2" 3) + (button :onclick "wmctrl -s 3" 4) + (button :onclick "wmctrl -s 4" 5) + (button :onclick "wmctrl -s 5" 6) + (button :onclick "wmctrl -s 6" 7) + (button :onclick "wmctrl -s 7" 8) + (button :onclick "wmctrl -s 8" 9))) + +(defwidget music [] + (box :class "music" + :orientation "h" + :space-evenly false + :halign "center" + {music != "" ? "🎵${music}" : ""})) + + +(defwidget metric [label value onchange] + (box :orientation "h" + :class "metric" + :space-evenly false + (box :class "label" label) + (scale :min 0 + :max 101 + :active {onchange != ""} + :value value + :onchange onchange))) + + + +(deflisten music :initial "" + "playerctl --follow metadata --format '{{ artist }} - {{ title }}' || true") + +(defpoll volume :interval "1s" + "scripts/getvol") + +(defpoll time :interval "10s" + "date '+%H:%M %b %d, %Y'") + +(defwindow bar + :monitor 0 + :windowtype "dock" + :geometry (geometry :x "0%" + :y "0%" + :width "90%" + :height "10px" + :anchor "top center") + :reserve (struts :side "top" :distance "4%") + (bar)) diff --git a/examples/eww-bar/scripts/getvol b/examples/eww-bar/scripts/getvol index 3c31607..6a95077 100755 --- a/examples/eww-bar/scripts/getvol +++ b/examples/eww-bar/scripts/getvol @@ -1,2 +1,2 @@ #!/bin/sh -amixer -D pulse sget Master | grep 'Left:' | awk -F'[][]' '{ print $2 }' | tr -d '%' +amixer -D pulse sget Master | grep 'Left:' | awk -F'[][]' '{ print $2 }' | tr -d '%' | head -1 diff --git a/rust-toolchain b/rust-toolchain index bf867e0..8b49edf 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -nightly +nightly-2021-08-10 diff --git a/src/config/element.rs b/src/config/element.rs deleted file mode 100644 index 51052fe..0000000 --- a/src/config/element.rs +++ /dev/null @@ -1,176 +0,0 @@ -use super::*; -use lazy_static::lazy_static; -use regex::Regex; -use std::ops::Range; - -use crate::{ - value::{AttrName, AttrVal}, - with_text_pos_context, -}; -use maplit::hashmap; -use std::collections::HashMap; - -#[derive(Debug, Clone, PartialEq)] -pub struct WidgetDefinition { - pub name: String, - pub structure: WidgetUse, - pub size: Option<(i32, i32)>, -} - -impl WidgetDefinition { - pub fn from_xml_element(xml: &XmlElement) -> Result { - with_text_pos_context! { xml => - if xml.tag_name() != "def" { - bail!( - "{} | Illegal element: only may be used in definition block, but found '{}'", - xml.text_pos(), - xml.as_tag_string() - ); - } - - WidgetDefinition { - name: xml.attr("name")?, - size: Option::zip(xml.parse_optional_attr("width")?, xml.parse_optional_attr("height")?), - structure: WidgetUse::from_xml_node(xml.only_child()?)?, - } - } - } -} - -#[derive(Debug, Clone, Default)] -pub struct WidgetUse { - pub name: String, - pub children: Vec, - pub attrs: HashMap, - pub text_pos: Option, -} - -#[derive(Debug, Clone)] -pub struct PositionData { - pub range: Range, -} - -impl PartialEq for WidgetUse { - fn eq(&self, other: &WidgetUse) -> bool { - self.name == other.name && self.children == other.children && self.attrs == other.attrs - } -} - -impl WidgetUse { - pub fn new(name: String, children: Vec) -> Self { - WidgetUse { name, children, attrs: HashMap::new(), ..WidgetUse::default() } - } - - pub fn from_xml_node(xml: XmlNode) -> Result { - lazy_static! { - static ref PATTERN: Regex = Regex::new("\\{\\{(.*)\\}\\}").unwrap(); - }; - let text_pos = xml.text_pos(); - let widget_use = match xml { - XmlNode::Text(text) => WidgetUse::simple_text(AttrVal::parse_string(&text.text())), - XmlNode::Element(elem) => WidgetUse { - name: elem.tag_name().to_owned(), - children: with_text_pos_context! { elem => elem.children().map(WidgetUse::from_xml_node).collect::>()?}?, - attrs: elem - .attributes() - .iter() - .map(|attr| { - ( - AttrName(attr.name().to_owned()), - AttrVal::parse_string(&xml_ext::resolve_escaped_symbols(attr.value())), - ) - }) - .collect::>(), - ..WidgetUse::default() - }, - XmlNode::Ignored(_) => bail!("{} | Failed to parse node {:?} as widget use", xml.text_pos(), xml), - }; - Ok(widget_use.at_pos(text_pos)) - } - - pub fn simple_text(text: AttrVal) -> Self { - WidgetUse { - name: "label".to_owned(), - children: vec![], - attrs: hashmap! { AttrName("text".to_owned()) => text }, // TODO this hardcoded "text" is dumdum - ..WidgetUse::default() - } - } - - pub fn at_pos(mut self, text_pos: TextPos) -> Self { - self.text_pos = Some(text_pos); - self - } -} - -#[cfg(test)] -mod test { - use super::*; - use maplit::hashmap; - use pretty_assertions::assert_eq; - - #[test] - fn test_simple_text() { - let expected_attr_value = AttrVal::from_primitive("my text"); - let widget = WidgetUse::simple_text(expected_attr_value.clone()); - assert_eq!( - widget, - WidgetUse { - name: "label".to_owned(), - children: Vec::new(), - attrs: hashmap! { AttrName("text".to_owned()) => expected_attr_value}, - ..WidgetUse::default() - }, - ); - } - - #[test] - fn test_parse_widget_use() { - let input = r#" - - - foo - - "#; - let document = roxmltree::Document::parse(input).unwrap(); - let xml = XmlNode::from(document.root_element().clone()); - - let expected = WidgetUse { - name: "widget_name".to_owned(), - attrs: hashmap! { - AttrName("attr1".to_owned()) => AttrVal::from_primitive("hi"), - AttrName("attr2".to_owned()) => AttrVal::from_primitive("12"), - }, - children: vec![ - WidgetUse::new("child_widget".to_owned(), Vec::new()), - WidgetUse::simple_text(AttrVal::from_primitive("foo".to_owned())), - ], - ..WidgetUse::default() - }; - assert_eq!(expected, WidgetUse::from_xml_node(xml).unwrap()); - } - - #[test] - fn test_parse_widget_definition() { - let input = r#" - - test - - "#; - let document = roxmltree::Document::parse(input).unwrap(); - let xml = XmlNode::from(document.root_element().clone()); - - let expected = WidgetDefinition { - name: "foo".to_owned(), - size: Some((12, 20)), - structure: WidgetUse { - name: "layout".to_owned(), - children: vec![WidgetUse::simple_text(AttrVal::from_primitive("test"))], - attrs: HashMap::new(), - ..WidgetUse::default() - }, - }; - - assert_eq!(expected, WidgetDefinition::from_xml_element(xml.as_element().unwrap()).unwrap()); - } -} diff --git a/src/config/eww_config.rs b/src/config/eww_config.rs deleted file mode 100644 index 88a2494..0000000 --- a/src/config/eww_config.rs +++ /dev/null @@ -1,289 +0,0 @@ -use anyhow::*; -use std::collections::HashMap; - -use crate::{ - util, - value::{PrimVal, VarName}, -}; - -use super::{ - element::WidgetDefinition, - xml_ext::{XmlElement, XmlNode}, - EwwWindowDefinition, RawEwwWindowDefinition, ScriptVar, WindowName, -}; -use std::path::PathBuf; - -/// Eww configuration structure. -#[derive(Debug, Clone)] -pub struct EwwConfig { - widgets: HashMap, - windows: HashMap, - initial_variables: HashMap, - script_vars: HashMap, - pub filepath: PathBuf, -} -impl EwwConfig { - pub fn read_from_file>(path: P) -> Result { - Self::generate(RawEwwConfig::read_from_file(path)?) - } - - pub fn generate(conf: RawEwwConfig) -> Result { - let RawEwwConfig { windows, initial_variables, script_vars, filepath, widgets } = conf; - Ok(EwwConfig { - windows: windows - .into_iter() - .map(|(name, window)| { - Ok((name, EwwWindowDefinition::generate(&widgets, window).context("Failed expand window definition")?)) - }) - .collect::>>()?, - widgets, - initial_variables, - script_vars, - filepath, - }) - } - - // TODO this is kinda ugly - pub fn generate_initial_state(&self) -> Result> { - let mut vars = - self.script_vars.iter().map(|var| Ok((var.0.clone(), var.1.initial_value()?))).collect::>>()?; - vars.extend(self.initial_variables.clone()); - Ok(vars) - } - - pub fn get_windows(&self) -> &HashMap { - &self.windows - } - - pub fn get_window(&self, name: &WindowName) -> Result<&EwwWindowDefinition> { - self.windows.get(name).with_context(|| format!("No window named '{}' exists", name)) - } - - pub fn get_script_var(&self, name: &VarName) -> Result<&ScriptVar> { - self.script_vars.get(name).with_context(|| format!("No script var named '{}' exists", name)) - } - - pub fn get_widget_definitions(&self) -> &HashMap { - &self.widgets - } -} - -/// Raw Eww configuration, before expanding widget usages. -#[derive(Debug, Clone)] -pub struct RawEwwConfig { - widgets: HashMap, - windows: HashMap, - initial_variables: HashMap, - script_vars: HashMap, - pub filepath: PathBuf, -} - -impl RawEwwConfig { - pub fn merge_includes(mut eww_config: RawEwwConfig, includes: Vec) -> Result { - let config_path = eww_config.filepath.clone(); - let log_conflict = |what: &str, conflict: &str, included_path: &std::path::PathBuf| { - log::error!( - "{} '{}' defined twice (defined in {} and in {})", - what, - conflict, - config_path.display(), - included_path.display() - ); - }; - for included_config in includes { - for conflict in util::extend_safe(&mut eww_config.widgets, included_config.widgets) { - log_conflict("widget", &conflict, &included_config.filepath) - } - for conflict in util::extend_safe(&mut eww_config.windows, included_config.windows) { - log_conflict("window", &conflict.to_string(), &included_config.filepath) - } - for conflict in util::extend_safe(&mut eww_config.script_vars, included_config.script_vars) { - log_conflict("script-var", &conflict.to_string(), &included_config.filepath) - } - for conflict in util::extend_safe(&mut eww_config.initial_variables, included_config.initial_variables) { - log_conflict("var", &conflict.to_string(), &included_config.filepath) - } - } - Ok(eww_config) - } - - pub fn read_from_file>(path: P) -> Result { - let result: Result<_> = try { - let content = util::replace_env_var_references(std::fs::read_to_string(path.as_ref())?); - let content = content.replace("&", "&"); - let document = roxmltree::Document::parse(&content).map_err(|e| anyhow!(e))?; - let root_node = XmlNode::from(document.root_element()); - let root_element = root_node.as_element()?; - - let (config, included_paths) = Self::from_xml_element(root_element.clone(), path.as_ref()) - .with_context(|| format!("Error parsing eww config file {}", path.as_ref().display()))?; - - let parsed_includes = included_paths - .into_iter() - .map(Self::read_from_file) - .collect::>>() - .with_context(|| format!("Included in {}", path.as_ref().display()))?; - - Self::merge_includes(config, parsed_includes) - .context("Failed to merge included files into parent configuration file")? - }; - result.with_context(|| format!("Failed to load eww config file {}", path.as_ref().display())) - } - - pub fn from_xml_element>(xml: XmlElement, path: P) -> Result<(Self, Vec)> { - let path = path.as_ref(); - - let included_paths = match xml.child("includes").ok() { - Some(tag) => tag - .child_elements() - .map(|child| { - crate::ensure_xml_tag_is!(child, "file"); - Ok(util::join_path_pretty(path, PathBuf::from(child.attr("path")?))) - }) - .collect::>>()?, - None => Default::default(), - }; - - let definitions = match xml.child("definitions").ok() { - Some(tag) => tag - .child_elements() - .map(|child| { - let def = WidgetDefinition::from_xml_element(&child).with_context(|| { - format!("Error parsing widget definition at {}:{}", path.display(), &child.text_pos()) - })?; - Ok((def.name.clone(), def)) - }) - .collect::>>()?, - None => Default::default(), - }; - - let windows = match xml.child("windows").ok() { - Some(tag) => tag - .child_elements() - .map(|child| { - let def = RawEwwWindowDefinition::from_xml_element(&child).with_context(|| { - format!("Error parsing window definition at {}:{}", path.display(), &child.text_pos()) - })?; - Ok((def.name.to_owned(), def)) - }) - .collect::>>()?, - None => Default::default(), - }; - - let (initial_variables, script_vars) = match xml.child("variables").ok() { - Some(tag) => parse_variables_block(tag)?, - None => Default::default(), - }; - - let config = RawEwwConfig { widgets: definitions, windows, initial_variables, script_vars, filepath: path.to_path_buf() }; - Ok((config, included_paths)) - } -} - -fn parse_variables_block(xml: XmlElement) -> Result<(HashMap, HashMap)> { - let mut normal_vars = HashMap::new(); - let mut script_vars = HashMap::new(); - for node in xml.child_elements() { - match node.tag_name() { - "var" => { - let value = node.only_child().map(|c| c.as_text_or_sourcecode()).unwrap_or_else(|_| String::new()); - normal_vars.insert(VarName(node.attr("name")?.to_owned()), PrimVal::from_string(value)); - } - "script-var" => { - let script_var = ScriptVar::from_xml_element(node)?; - script_vars.insert(script_var.name().clone(), script_var); - } - _ => bail!("Illegal element in variables block: {}", node.as_tag_string()), - } - } - - // Extends the variables with the predefined variables - let inbuilt = crate::config::inbuilt::get_inbuilt_vars(); - for i in util::extend_safe(&mut script_vars, inbuilt) { - log::error!( - "script-var '{}' defined twice (defined in your config and in the eww included variables)\nHint: don't define any \ - varible like any of these: https://elkowar.github.io/eww/main/magic-variables-documenation/", - i, - ); - } - - Ok((normal_vars, script_vars)) -} - -#[cfg(test)] -mod test { - use crate::config::{RawEwwConfig, XmlNode}; - use std::collections::HashMap; - - #[test] - fn test_merge_includes() { - let input1 = r#" - - - - - {{var1}} - - - - - - var1 - - - - - - - - - - - - "#; - let input2 = r#" - - - - - {{var2}} - - - - - var2 - - - - - - - - - - - - "#; - - let document1 = roxmltree::Document::parse(&input1).unwrap(); - let document2 = roxmltree::Document::parse(input2).unwrap(); - let config1 = - RawEwwConfig::from_xml_element(XmlNode::from(document1.root_element()).as_element().unwrap().clone(), "").unwrap().0; - let config2 = - RawEwwConfig::from_xml_element(XmlNode::from(document2.root_element()).as_element().unwrap().clone(), "").unwrap().0; - let base_config = RawEwwConfig { - widgets: HashMap::new(), - windows: HashMap::new(), - initial_variables: HashMap::new(), - script_vars: HashMap::new(), - filepath: "test_path".into(), - }; - - let merged_config = RawEwwConfig::merge_includes(base_config, vec![config1, config2]).unwrap(); - - assert_eq!(merged_config.widgets.len(), 2); - assert_eq!(merged_config.windows.len(), 2); - assert_eq!(merged_config.initial_variables.len(), 2); - assert_eq!(merged_config.script_vars.len(), 6); - } -} diff --git a/src/config/mod.rs b/src/config/mod.rs deleted file mode 100644 index 361c337..0000000 --- a/src/config/mod.rs +++ /dev/null @@ -1,32 +0,0 @@ -use crate::{ - util, - value::{PrimVal, VarName}, -}; - -use anyhow::*; - -use element::*; -use xml_ext::*; - -pub mod element; -pub mod eww_config; -pub mod inbuilt; -pub mod script_var; -pub mod system_stats; -pub mod window_definition; -pub mod window_geometry; -pub mod xml_ext; -pub use eww_config::*; -pub use script_var::*; -pub use window_definition::*; -pub use window_geometry::*; - -#[macro_export] -macro_rules! ensure_xml_tag_is { - ($element:ident, $name:literal) => { - ensure!( - $element.tag_name() == $name, - anyhow!("{} | Tag needed to be of type '{}', but was: {}", $element.text_pos(), $name, $element.as_tag_string()) - ) - }; -} diff --git a/src/config/script_var.rs b/src/config/script_var.rs deleted file mode 100644 index b3aeda8..0000000 --- a/src/config/script_var.rs +++ /dev/null @@ -1,82 +0,0 @@ -use std::process::Command; - -use anyhow::*; - -use crate::ensure_xml_tag_is; - -use super::*; - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum VarSource { - Shell(String), - Function(fn() -> Result), -} -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct PollScriptVar { - pub name: VarName, - pub command: VarSource, - pub interval: std::time::Duration, -} - -impl PollScriptVar { - pub fn run_once(&self) -> Result { - match &self.command { - VarSource::Shell(x) => run_command(x), - VarSource::Function(x) => x(), - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TailScriptVar { - pub name: VarName, - pub command: String, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum ScriptVar { - Poll(PollScriptVar), - Tail(TailScriptVar), -} - -impl ScriptVar { - pub fn name(&self) -> &VarName { - match self { - ScriptVar::Poll(x) => &x.name, - ScriptVar::Tail(x) => &x.name, - } - } - - pub fn initial_value(&self) -> Result { - match self { - ScriptVar::Poll(x) => match &x.command { - VarSource::Function(f) => f().with_context(|| format!("Failed to compute initial value for {}", &self.name())), - VarSource::Shell(f) => { - run_command(f).with_context(|| format!("Failed to compute initial value for {}", &self.name())) - } - }, - ScriptVar::Tail(_) => Ok(PrimVal::from_string(String::new())), - } - } - - pub fn from_xml_element(xml: XmlElement) -> Result { - ensure_xml_tag_is!(xml, "script-var"); - - let name = VarName(xml.attr("name")?); - let command = xml.only_child()?.as_text()?.text(); - if let Ok(interval) = xml.attr("interval") { - let interval = util::parse_duration(&interval)?; - Ok(ScriptVar::Poll(PollScriptVar { name, command: crate::config::VarSource::Shell(command), interval })) - } else { - Ok(ScriptVar::Tail(TailScriptVar { name, command })) - } - } -} - -/// Run a command and get the output -fn run_command(cmd: &str) -> Result { - log::debug!("Running command: {}", cmd); - let output = String::from_utf8(Command::new("/bin/sh").arg("-c").arg(cmd).output()?.stdout)?; - let output = output.trim_matches('\n'); - Ok(PrimVal::from(output)) -} diff --git a/src/config/window_definition.rs b/src/config/window_definition.rs deleted file mode 100644 index 1e28336..0000000 --- a/src/config/window_definition.rs +++ /dev/null @@ -1,219 +0,0 @@ -use super::*; -use crate::{ensure_xml_tag_is, value::NumWithUnit, widgets::widget_node}; -use derive_more::*; -use serde::{Deserialize, Serialize}; -use smart_default::SmartDefault; -use std::{collections::HashMap, str::FromStr}; - -#[derive(Debug, Clone, PartialEq)] -pub enum EwwWindowType { - Dock, - Dialog, - Toolbar, - Normal, -} -impl FromStr for EwwWindowType { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - match s { - "dock" => Ok(Self::Dock), - "toolbar" => Ok(Self::Toolbar), - "dialog" => Ok(Self::Dialog), - "normal" => Ok(Self::Normal), - x => Err(anyhow!("Unknown windowtype provided '{}'. Possible values are: dock, toolbar, dialog, normal", x)), - } - } -} - -impl Default for EwwWindowType { - fn default() -> Self { - Self::Normal - } -} - -/// Full window-definition containing the fully expanded widget tree. -/// **Use this** rather than `[RawEwwWindowDefinition]`. -#[derive(Debug, Clone)] -pub struct EwwWindowDefinition { - pub name: WindowName, - - pub geometry: EwwWindowGeometry, - pub stacking: WindowStacking, - pub screen_number: Option, - pub widget: Box, - pub focusable: bool, - - #[cfg(feature = "x11")] - pub window_type: EwwWindowType, - - #[cfg(feature = "x11")] - pub struts: StrutDefinition, - - #[cfg(feature = "wayland")] - pub exclusive: bool, -} - -impl EwwWindowDefinition { - pub fn generate(defs: &HashMap, window: RawEwwWindowDefinition) -> Result { - Ok(EwwWindowDefinition { - name: window.name, - geometry: window.geometry, - stacking: window.stacking, - screen_number: window.screen_number, - widget: widget_node::generate_generic_widget_node(defs, &HashMap::new(), window.widget)?, - focusable: window.focusable, - #[cfg(feature = "x11")] - window_type: window.window_type, - #[cfg(feature = "x11")] - struts: window.struts, - #[cfg(feature = "wayland")] - exclusive: window.exclusive, - }) - } -} - -/// Window-definition storing the raw WidgetUse, as received directly from parsing. -#[derive(Debug, Clone, PartialEq)] -pub struct RawEwwWindowDefinition { - pub name: WindowName, - pub geometry: EwwWindowGeometry, - pub stacking: WindowStacking, - pub screen_number: Option, - pub widget: WidgetUse, - pub focusable: bool, - - #[cfg(feature = "x11")] - pub window_type: EwwWindowType, - - #[cfg(feature = "x11")] - pub struts: StrutDefinition, - - #[cfg(feature = "wayland")] - pub exclusive: bool, -} - -impl RawEwwWindowDefinition { - pub fn from_xml_element(xml: &XmlElement) -> Result { - ensure_xml_tag_is!(xml, "window"); - let stacking: WindowStacking = xml.parse_optional_attr("stacking")?.unwrap_or_default(); - - // TODO maybe rename this to monitor? - let focusable = xml.parse_optional_attr("focusable")?; - let screen_number = xml.parse_optional_attr("screen")?; - - #[cfg(feature = "x11")] - let struts: Option = - xml.child("reserve").ok().map(StrutDefinition::from_xml_element).transpose().context("Failed to parse ")?; - - Ok(RawEwwWindowDefinition { - name: WindowName(xml.attr("name")?), - geometry: match xml.child("geometry") { - Ok(node) => EwwWindowGeometry::from_xml_element(node)?, - Err(_) => EwwWindowGeometry::default(), - }, - #[cfg(feature = "x11")] - window_type: match xml.attr("windowtype") { - Ok(v) => EwwWindowType::from_str(&v)?, - Err(_) => match struts { - Some(_) => EwwWindowType::Dock, - None => Default::default(), - }, - }, - widget: WidgetUse::from_xml_node(xml.child("widget")?.only_child()?)?, - stacking, - screen_number, - focusable: focusable.unwrap_or(false), - #[cfg(feature = "x11")] - struts: struts.unwrap_or_default(), - #[cfg(feature = "wayland")] - exclusive: xml.parse_optional_attr("exclusive")?.unwrap_or_default(), - }) - } -} - -#[derive(Debug, Clone, Copy, Eq, PartialEq, smart_default::SmartDefault)] -pub enum Side { - #[default] - Top, - Left, - Right, - Bottom, -} - -impl std::str::FromStr for Side { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - match s { - "l" | "left" => Ok(Side::Left), - "r" | "right" => Ok(Side::Right), - "t" | "top" => Ok(Side::Top), - "b" | "bottom" => Ok(Side::Bottom), - _ => Err(anyhow!("Failed to parse {} as valid side. Must be one of \"left\", \"right\", \"top\", \"bottom\"", s)), - } - } -} - -// Surface definition if the backend for X11 is enable -#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)] -pub struct StrutDefinition { - pub side: Side, - pub dist: NumWithUnit, -} - -impl StrutDefinition { - pub fn from_xml_element(xml: XmlElement) -> Result { - Ok(StrutDefinition { side: xml.attr("side")?.parse()?, dist: xml.attr("distance")?.parse()? }) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, derive_more::Display, SmartDefault)] -pub enum WindowStacking { - #[default] - Foreground, - Background, - Bottom, - Overlay, -} - -impl std::str::FromStr for WindowStacking { - type Err = anyhow::Error; - - #[cfg(not(feature = "wayland"))] - fn from_str(s: &str) -> Result { - let s = s.to_lowercase(); - match s.as_str() { - "foreground" | "fg" | "f" => Ok(WindowStacking::Foreground), - "background" | "bg" | "b" => Ok(WindowStacking::Background), - _ => Err(anyhow!("Couldn't parse '{}' as window stacking, must be either foreground, fg, background or bg", s)), - } - } - - #[cfg(feature = "wayland")] - fn from_str(s: &str) -> Result { - let s = s.to_lowercase(); - match s.as_str() { - "foreground" | "fg" => Ok(WindowStacking::Foreground), - "background" | "bg" => Ok(WindowStacking::Background), - "bottom" | "bt" => Ok(WindowStacking::Bottom), - "overlay" | "ov" => Ok(WindowStacking::Overlay), - _ => Err(anyhow!( - "Couldn't parse '{}' as window stacking, must be either foreground, fg, background, bg, bottom, bt, overlay or \ - ov", - s - )), - } - } -} - -#[repr(transparent)] -#[derive(Clone, Hash, PartialEq, Eq, AsRef, FromStr, Display, Serialize, Deserialize, Default, From, DebugCustom)] -#[debug(fmt = "WindowName(\".0\")")] -pub struct WindowName(String); - -impl std::borrow::Borrow for WindowName { - fn borrow(&self) -> &str { - &self.0 - } -} diff --git a/src/config/window_geometry.rs b/src/config/window_geometry.rs deleted file mode 100644 index 4b68c35..0000000 --- a/src/config/window_geometry.rs +++ /dev/null @@ -1,144 +0,0 @@ -use crate::value::Coords; -use anyhow::*; -use serde::{Deserialize, Serialize}; -use smart_default::SmartDefault; - -use std::fmt; - -use super::xml_ext::XmlElement; - -#[derive(Debug, derive_more::Display, Clone, Copy, Eq, PartialEq, SmartDefault, Serialize, Deserialize)] -pub enum AnchorAlignment { - #[display("start")] - #[default] - START, - #[display("center")] - CENTER, - #[display("end")] - END, -} - -impl AnchorAlignment { - pub fn from_x_alignment(s: &str) -> Result { - match s { - "l" | "left" => Ok(AnchorAlignment::START), - "c" | "center" => Ok(AnchorAlignment::CENTER), - "r" | "right" => Ok(AnchorAlignment::END), - _ => bail!(r#"couldn't parse '{}' as x-alignment. Must be one of "left", "center", "right""#, s), - } - } - - pub fn from_y_alignment(s: &str) -> Result { - match s { - "t" | "top" => Ok(AnchorAlignment::START), - "c" | "center" => Ok(AnchorAlignment::CENTER), - "b" | "bottom" => Ok(AnchorAlignment::END), - _ => bail!(r#"couldn't parse '{}' as y-alignment. Must be one of "top", "center", "bottom""#, s), - } - } - - pub fn alignment_to_coordinate(&self, size_inner: i32, size_container: i32) -> i32 { - match self { - AnchorAlignment::START => 0, - AnchorAlignment::CENTER => (size_container / 2) - (size_inner / 2), - AnchorAlignment::END => size_container - size_inner, - } - } -} - -#[derive(Debug, Clone, Copy, Eq, PartialEq, Default, Serialize, Deserialize)] -pub struct AnchorPoint { - pub x: AnchorAlignment, - pub y: AnchorAlignment, -} - -impl std::fmt::Display for AnchorPoint { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use AnchorAlignment::*; - match (self.x, self.y) { - (CENTER, CENTER) => write!(f, "center"), - (x, y) => write!( - f, - "{} {}", - match x { - START => "left", - CENTER => "center", - END => "right", - }, - match y { - START => "top", - CENTER => "center", - END => "bottom", - } - ), - } - } -} - -impl std::str::FromStr for AnchorPoint { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - if s == "center" { - Ok(AnchorPoint { x: AnchorAlignment::CENTER, y: AnchorAlignment::CENTER }) - } else { - let (first, second) = s - .split_once(' ') - .context("Failed to parse anchor: Must either be \"center\" or be formatted like \"top left\"")?; - let x_y_result: Result<_> = try { - AnchorPoint { x: AnchorAlignment::from_x_alignment(first)?, y: AnchorAlignment::from_y_alignment(second)? } - }; - x_y_result.or_else(|_| { - Ok(AnchorPoint { x: AnchorAlignment::from_x_alignment(second)?, y: AnchorAlignment::from_y_alignment(first)? }) - }) - } - } -} - -#[derive(Default, Debug, Clone, Copy, Eq, PartialEq)] -pub struct EwwWindowGeometry { - pub anchor_point: AnchorPoint, - pub offset: Coords, - pub size: Coords, -} - -impl EwwWindowGeometry { - pub fn from_xml_element(xml: XmlElement) -> Result { - Ok(EwwWindowGeometry { - anchor_point: xml.parse_optional_attr("anchor")?.unwrap_or_default(), - size: Coords { - x: xml.parse_optional_attr("width")?.unwrap_or_default(), - y: xml.parse_optional_attr("height")?.unwrap_or_default(), - }, - offset: Coords { - x: xml.parse_optional_attr("x")?.unwrap_or_default(), - y: xml.parse_optional_attr("y")?.unwrap_or_default(), - }, - }) - } - - pub fn override_if_given(&mut self, anchor_point: Option, offset: Option, size: Option) -> Self { - EwwWindowGeometry { - anchor_point: anchor_point.unwrap_or(self.anchor_point), - offset: offset.unwrap_or(self.offset), - size: size.unwrap_or(self.size), - } - } -} - -impl std::fmt::Display for EwwWindowGeometry { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}-{} ({})", self.offset, self.size, self.anchor_point) - } -} - -impl EwwWindowGeometry { - /// Calculate the window rectangle given the configured window geometry - pub fn get_window_rectangle(&self, screen_rect: gdk::Rectangle) -> gdk::Rectangle { - let (offset_x, offset_y) = self.offset.relative_to(screen_rect.width, screen_rect.height); - let (width, height) = self.size.relative_to(screen_rect.width, screen_rect.height); - let x = screen_rect.x + offset_x + self.anchor_point.x.alignment_to_coordinate(width, screen_rect.width); - let y = screen_rect.y + offset_y + self.anchor_point.y.alignment_to_coordinate(height, screen_rect.height); - gdk::Rectangle { x, y, width, height } - } -} diff --git a/src/config/xml_ext.rs b/src/config/xml_ext.rs deleted file mode 100644 index b8cdf9c..0000000 --- a/src/config/xml_ext.rs +++ /dev/null @@ -1,307 +0,0 @@ -use crate::util::StringExt; -use anyhow::*; -use itertools::Itertools; -use std::fmt; - -#[macro_export] -macro_rules! with_text_pos_context { - ($node:expr => $($code:tt)*) => {{ - let result: Result<_> = try { $($code)* }; - result.with_context(|| anyhow!("at: {}", $node.text_pos())) - }}; -} - -/// resolve symbols such as " to replace them with the actual " symbol -pub fn resolve_escaped_symbols(s: &str) -> String { - s.replace(""", "\"").replace("<", "<").replace(">", ">") -} - -#[derive(Debug, Clone)] -pub enum XmlNode<'a, 'b> { - Element(XmlElement<'a, 'b>), - Text(XmlText<'a, 'b>), - Ignored(roxmltree::Node<'a, 'b>), -} - -impl<'a, 'b> fmt::Display for XmlNode<'a, 'b> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - XmlNode::Text(text) => write!(f, "{}", text), - XmlNode::Element(elem) => write!(f, "{}", elem), - XmlNode::Ignored(node) => write!(f, "{:?}", node), - } - } -} - -#[derive(PartialEq, Eq, Clone, Copy, derive_more::Display)] -#[display(fmt = "{}:{}", row, col)] -pub struct TextPos { - pub row: u32, - pub col: u32, -} - -impl std::fmt::Debug for TextPos { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self) - } -} - -impl From for TextPos { - fn from(x: roxmltree::TextPos) -> Self { - TextPos { row: x.row, col: x.col } - } -} - -/// Get the part of a string that is selected by the start and end TextPos. -/// Will panic if the range is out of bounds in any way. -fn get_text_from_text_range(s: &str, (start_pos, end_pos): (TextPos, TextPos)) -> String { - let mut code_text = - s.lines().dropping(start_pos.row as usize - 1).take(end_pos.row as usize - (start_pos.row as usize - 1)).collect_vec(); - if let Some(first_line) = code_text.first_mut() { - *first_line = first_line.split_at(start_pos.col as usize - 1).1; - } - if let Some(last_line) = code_text.last_mut() { - *last_line = last_line.split_at(end_pos.col as usize - 1).0; - } - resolve_escaped_symbols(&code_text.join("\n")) -} - -impl<'a, 'b> XmlNode<'a, 'b> { - pub fn get_sourcecode(&self) -> String { - let input_text = self.node().document().input_text(); - let range = self.node().range(); - let start_pos = self.node().document().text_pos_at(range.start).into(); - let end_pos = self.node().document().text_pos_at(range.end).into(); - get_text_from_text_range(input_text, (start_pos, end_pos)) - } - - pub fn as_text_or_sourcecode(&self) -> String { - self.as_text().map(|c| resolve_escaped_symbols(&c.text())).unwrap_or_else(|_| self.get_sourcecode()) - } - - pub fn as_text(&self) -> Result<&XmlText<'a, 'b>> { - match self { - XmlNode::Text(text) => Ok(text), - _ => Err(anyhow!("'{}' is not a text node", self)), - } - } - - pub fn as_element(&self) -> Result<&XmlElement<'a, 'b>> { - match self { - XmlNode::Element(element) => Ok(element), - _ => Err(anyhow!("'{}' is not an element node", self)), - } - } - - pub fn text_range(&self) -> std::ops::Range { - self.node().range() - } - - pub fn text_pos(&self) -> TextPos { - let document = self.node().document(); - let range = self.node().range(); - document.text_pos_at(range.start).into() - } - - fn node(&self) -> roxmltree::Node<'a, 'b> { - match self { - XmlNode::Text(x) => x.0, - XmlNode::Element(x) => x.0, - XmlNode::Ignored(x) => *x, - } - } -} - -#[derive(Debug, Clone)] -pub struct XmlText<'a, 'b>(roxmltree::Node<'a, 'b>); - -impl<'a, 'b> fmt::Display for XmlText<'a, 'b> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Text(\"{}\")", self.text()) - } -} - -impl<'a, 'b> XmlText<'a, 'b> { - pub fn text(&self) -> String { - self.0.text().map(resolve_escaped_symbols).unwrap_or_default().trim_lines().trim_matches('\n').to_owned() - } - - pub fn text_pos(&self) -> TextPos { - let document = self.0.document(); - let range = self.0.range(); - document.text_pos_at(range.start).into() - } -} - -#[derive(Debug, Clone)] -pub struct XmlElement<'a, 'b>(roxmltree::Node<'a, 'b>); - -impl<'a, 'b> fmt::Display for XmlElement<'a, 'b> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let children = self - .children() - .map(|child| format!("{}", child)) - .map(|x| x.lines().map(|line| format!(" {}", line)).join("\n")) - .join("\n"); - - if children.is_empty() { - write!(f, "{}", self.as_tag_string(), self.tag_name()) - } else { - write!(f, "{}\n{}\n", self.as_tag_string(), children, self.tag_name()) - } - } -} - -impl<'a, 'b> XmlElement<'a, 'b> { - pub fn as_tag_string(&self) -> String { - let attrs = self.attributes().iter().map(|attr| format!("{}=\"{}\"", attr.name(), attr.value())).join(" "); - - format!("<{} {}>", self.tag_name(), attrs) - } - - pub fn tag_name(&self) -> &str { - self.0.tag_name().name() - } - - pub fn child(&self, tagname: &str) -> Result { - with_text_pos_context! { self => - self.child_elements() - .find(|child| child.tag_name() == tagname) - .with_context(|| anyhow!("child element '{}' missing from {}", tagname, self.as_tag_string()))? - } - } - - pub fn children(&self) -> impl Iterator { - self.0 - .children() - .filter(|child| child.is_element() || (child.is_text() && !child.text().unwrap_or_default().is_blank())) - .map(XmlNode::from) - } - - pub fn child_elements(&self) -> impl Iterator { - self.0.children().filter(|child| child.is_element()).map(XmlElement) - } - - pub fn attributes(&self) -> &[roxmltree::Attribute] { - self.0.attributes() - } - - pub fn attr(&self, key: &str) -> Result { - with_text_pos_context! { self => - self.0 - .attribute(key) - .map(resolve_escaped_symbols) - .with_context(|| anyhow!("'{}' missing attribute '{}'", self.as_tag_string(), key))? - } - } - - pub fn optional_attr Result>(&self, key: &str, parse: F) -> Result> { - match self.0.attribute(key) { - Some(value) => parse(value) - .with_context(|| format!("Parsing the value of {}=\"{}\" in <{}>", key, value, self.tag_name())) - .map(Some), - None => Ok(None), - } - } - - pub fn parse_optional_attr, O: std::str::FromStr>(&self, key: &str) -> Result> { - match self.0.attribute(key) { - Some(value) => value - .parse::() - .map_err(|e| anyhow!(e)) - .with_context(|| format!("Parsing the value of {}=\"{}\" in <{}>", key, value, self.tag_name())) - .map(Some), - None => Ok(None), - } - } - - pub fn only_child(&self) -> Result { - with_text_pos_context! { self => - let mut children_iter = self.children(); - let only_child = children_iter - .next() - .with_context(|| anyhow!("'{}' had no children", self.as_tag_string()))?; - if children_iter.next().is_some() { - bail!("'{}' had more than one child", &self); - } - only_child - } - } - - pub fn only_child_element(&self) -> Result { - with_text_pos_context! { self => - self.only_child()?.as_element()?.clone() - } - } - - pub fn text_pos(&self) -> TextPos { - let document = self.0.document(); - let range = self.0.range(); - document.text_pos_at(range.start).into() - } -} - -impl<'a, 'b> From> for XmlNode<'a, 'b> { - fn from(elem: XmlElement<'a, 'b>) -> Self { - XmlNode::Element(elem) - } -} - -impl<'a, 'b> From> for XmlNode<'a, 'b> { - fn from(elem: XmlText<'a, 'b>) -> Self { - XmlNode::Text(elem) - } -} - -impl<'a, 'b> From> for XmlNode<'a, 'b> { - fn from(node: roxmltree::Node<'a, 'b>) -> Self { - if node.is_text() { - XmlNode::Text(XmlText(node)) - } else if node.is_element() | node.is_root() { - XmlNode::Element(XmlElement(node)) - } else { - XmlNode::Ignored(node) - } - } -} - -#[cfg(test)] -mod test { - use super::*; - #[test] - pub fn test_parse_sourcecode_singleline() { - let input = "whatever"; - let document = roxmltree::Document::parse(&input).unwrap(); - let root_node = XmlNode::from(document.root_element()); - assert_eq!(root_node.as_element().unwrap().only_child().unwrap().as_text_or_sourcecode(), "whatever".to_string()); - } - - #[test] - pub fn test_parse_sourcecode_multiline() { - let input = r#" -this is -multiline - "#; - let document = roxmltree::Document::parse(&input).unwrap(); - let root_node = XmlNode::from(document.root_element()); - assert_eq!( - root_node.as_element().unwrap().only_child().unwrap().as_text_or_sourcecode(), - "this is\nmultiline".to_string() - ); - } - - #[test] - pub fn test_parse_sourcecode_code() { - let input = r#" -if [ "this" == '$that' ]; then - echo `hi` -fi - "#; - let document = roxmltree::Document::parse(&input).unwrap(); - let root_node = XmlNode::from(document.root_element()); - assert_eq!( - root_node.as_element().unwrap().only_child().unwrap().as_text_or_sourcecode(), - "if [ \"this\" == '$that' ]; then\necho `hi`\nfi".to_string() - ); - } -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 3fd10e7..0000000 --- a/src/main.rs +++ /dev/null @@ -1,185 +0,0 @@ -#![feature(trace_macros)] -#![feature(box_syntax)] -#![feature(box_patterns)] -#![feature(slice_concat_trait)] -#![feature(result_cloned)] -#![feature(try_blocks)] -#![feature(nll)] - -extern crate gio; -extern crate gtk; -#[cfg(feature = "wayland")] -extern crate gtk_layer_shell as gtk_layer_shell; - -use anyhow::*; -use std::{ - os::unix::net, - path::{Path, PathBuf}, -}; - -pub mod app; -pub mod application_lifecycle; -pub mod client; -pub mod config; -pub mod display_backend; -pub mod eww_state; -pub mod geometry; -pub mod ipc_server; -pub mod opts; -pub mod script_var_handler; -pub mod server; -pub mod util; -pub mod value; -pub mod widgets; - -fn main() { - let opts: opts::Opt = opts::Opt::from_env(); - - let log_level_filter = if opts.log_debug { log::LevelFilter::Debug } else { log::LevelFilter::Info }; - if std::env::var("RUST_LOG").is_ok() { - println!("hey"); - pretty_env_logger::init_timed(); - } else { - pretty_env_logger::formatted_timed_builder().filter(Some("eww"), log_level_filter).init(); - } - - let result: Result<()> = try { - let paths = opts - .config_path - .map(EwwPaths::from_config_dir) - .unwrap_or_else(EwwPaths::default) - .context("Failed to initialize eww paths")?; - - match opts.action { - opts::Action::ClientOnly(action) => { - client::handle_client_only_action(&paths, action)?; - } - opts::Action::WithServer(action) => { - log::info!("Trying to find server process at socket {}", paths.get_ipc_socket_file().display()); - match net::UnixStream::connect(&paths.get_ipc_socket_file()) { - Ok(stream) => { - log::info!("Connected to Eww server ({}).", &paths.get_ipc_socket_file().display()); - let response = - client::do_server_call(stream, action).context("Error while forwarding command to server")?; - if let Some(response) = response { - println!("{}", response); - if response.is_failure() { - std::process::exit(1); - } - } - } - Err(_) => { - eprintln!("Failed to connect to the eww daemon."); - eprintln!("Make sure to start the eww daemon process by running `eww daemon` first."); - std::process::exit(1); - } - } - } - - opts::Action::Daemon => { - // make sure that there isn't already a Eww daemon running. - if check_server_running(paths.get_ipc_socket_file()) { - eprintln!("Eww server already running."); - std::process::exit(1); - } else { - log::info!("Initializing Eww server. ({})", paths.get_ipc_socket_file().display()); - let _ = std::fs::remove_file(paths.get_ipc_socket_file()); - - println!("Run `eww logs` to see any errors, warnings or information while editing your configuration."); - server::initialize_server(paths)?; - } - } - } - }; - - if let Err(e) = result { - log::error!("{:?}", e); - std::process::exit(1); - } -} - -/// Check if a eww server is currently running by trying to send a ping message to it. -fn check_server_running(socket_path: impl AsRef) -> bool { - let response = net::UnixStream::connect(socket_path) - .ok() - .and_then(|stream| client::do_server_call(stream, opts::ActionWithServer::Ping).ok()); - response.is_some() -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct EwwPaths { - log_file: PathBuf, - ipc_socket_file: PathBuf, - config_dir: PathBuf, -} - -impl EwwPaths { - pub fn from_config_dir>(config_dir: P) -> Result { - let config_dir = config_dir.as_ref(); - let config_dir = if config_dir.is_file() { - config_dir.parent().context("Given config file did not have a parent directory")? - } else { - config_dir - }; - - if !config_dir.exists() { - bail!("Configuration directory {} does not exist", config_dir.display()); - } - - let config_dir = config_dir.canonicalize()?; - let daemon_id = base64::encode(format!("{}", config_dir.display())); - - Ok(EwwPaths { - config_dir, - log_file: std::env::var("XDG_CACHE_HOME") - .map(PathBuf::from) - .unwrap_or_else(|_| PathBuf::from(std::env::var("HOME").unwrap()).join(".cache")) - .join(format!("eww_{}.log", daemon_id)), - ipc_socket_file: std::env::var("XDG_RUNTIME_DIR") - .map(std::path::PathBuf::from) - .unwrap_or_else(|_| std::path::PathBuf::from("/tmp")) - .join(format!("eww-server_{}", daemon_id)), - }) - } - - pub fn default() -> Result { - let config_dir = std::env::var("XDG_CONFIG_HOME") - .map(PathBuf::from) - .unwrap_or_else(|_| PathBuf::from(std::env::var("HOME").unwrap()).join(".config")) - .join("eww"); - - Self::from_config_dir(config_dir) - } - - pub fn get_log_file(&self) -> &Path { - self.log_file.as_path() - } - - pub fn get_ipc_socket_file(&self) -> &Path { - self.ipc_socket_file.as_path() - } - - pub fn get_config_dir(&self) -> &Path { - self.config_dir.as_path() - } - - pub fn get_eww_xml_path(&self) -> PathBuf { - self.config_dir.join("eww.xml") - } - - pub fn get_eww_scss_path(&self) -> PathBuf { - self.config_dir.join("eww.scss") - } -} - -impl std::fmt::Display for EwwPaths { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "config-dir: {}, ipc-socket: {}, log-file: {}", - self.config_dir.display(), - self.ipc_socket_file.display(), - self.log_file.display() - ) - } -} diff --git a/src/value/attr_value/attr_value.rs b/src/value/attr_value/attr_value.rs deleted file mode 100644 index c7359ab..0000000 --- a/src/value/attr_value/attr_value.rs +++ /dev/null @@ -1,209 +0,0 @@ -use anyhow::*; -use itertools::Itertools; -use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fmt, iter::FromIterator}; - -use super::super::*; - -/// A value assigned to an attribute in a widget. -/// This can be a primitive String that contains any amount of variable -/// references, as would be generated by the string "foo {{var}} bar". -#[derive(Serialize, Deserialize, Clone, PartialEq, derive_more::Into, derive_more::From, Default)] -pub struct AttrVal(Vec); - -impl fmt::Display for AttrVal { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.iter().map(|x| format!("{}", x)).join("")) - } -} - -impl fmt::Debug for AttrVal { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "AttrValue({:?})", self.0) - } -} - -impl IntoIterator for AttrVal { - type IntoIter = std::vec::IntoIter; - type Item = AttrValElement; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} - -impl FromIterator for AttrVal { - fn from_iter>(iter: T) -> Self { - AttrVal(iter.into_iter().collect()) - } -} - -impl AttrVal { - pub fn from_primitive>(v: T) -> Self { - AttrVal(vec![AttrValElement::Primitive(v.into())]) - } - - pub fn from_var_ref>(v: T) -> Self { - AttrVal(vec![AttrValElement::Expr(AttrValExpr::VarRef(v.into()))]) - } - - pub fn iter(&self) -> std::slice::Iter { - self.0.iter() - } - - pub fn var_refs(&self) -> impl Iterator { - self.0.iter().filter_map(|x| Some(x.as_expr()?.var_refs())).flatten() - } - - /// resolve partially. - /// If a var-ref links to another var-ref, that other var-ref is used. - /// If a referenced variable is not found in the given hashmap, returns the var-ref unchanged. - pub fn resolve_one_level(self, variables: &HashMap) -> AttrVal { - self.into_iter() - .map(|entry| match entry { - AttrValElement::Expr(expr) => AttrValElement::Expr(expr.map_terminals_into(|child_expr| match child_expr { - AttrValExpr::VarRef(var_name) => match variables.get(&var_name) { - Some(value) => AttrValExpr::Literal(value.clone()), - None => AttrValExpr::VarRef(var_name), - }, - other => other, - })), - - _ => entry, - }) - .collect() - } - - /// resolve fully. - /// As the variables here have to be primitive values, - /// this enforces that var-refs are not linking to other variables. - pub fn resolve_fully(self, variables: &HashMap) -> Result { - self.into_iter() - .map(|element| match element { - AttrValElement::Primitive(x) => Ok(x), - AttrValElement::Expr(expr) => expr.eval(variables), - }) - .collect() - } - - // TODO this could be a fancy Iterator implementation, ig - pub fn parse_string(s: &str) -> AttrVal { - let mut elements = Vec::new(); - - let mut cur_word = "".to_owned(); - let mut cur_varref: Option = None; - let mut curly_count = 0; - for c in s.chars() { - if let Some(ref mut varref) = cur_varref { - if c == '}' { - curly_count -= 1; - if curly_count == 0 { - elements.push(AttrValElement::Expr(AttrValExpr::parse(varref).unwrap())); - cur_varref = None - } - } else { - curly_count = 2; - varref.push(c); - } - } else if c == '{' { - curly_count += 1; - if curly_count == 2 { - if !cur_word.is_empty() { - elements.push(AttrValElement::primitive(std::mem::take(&mut cur_word))); - } - cur_varref = Some(String::new()) - } - } else { - if curly_count == 1 { - cur_word.push('{'); - } - curly_count = 0; - cur_word.push(c); - } - } - if let Some(unfinished_varref) = cur_varref.take() { - elements.push(AttrValElement::primitive(unfinished_varref)); - } else if !cur_word.is_empty() { - elements.push(AttrValElement::primitive(cur_word)); - } - AttrVal(elements) - } -} - -#[derive(Clone, PartialEq, Serialize, Deserialize)] -pub enum AttrValElement { - Primitive(PrimVal), - Expr(AttrValExpr), -} -impl fmt::Display for AttrValElement { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - AttrValElement::Primitive(x) => write!(f, "{}", x), - AttrValElement::Expr(x) => write!(f, "{{{{{}}}}}", x), - } - } -} - -impl fmt::Debug for AttrValElement { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - AttrValElement::Primitive(x) => write!(f, "Primitive({:?})", x), - AttrValElement::Expr(x) => write!(f, "Expr({:?})", x), - } - } -} - -impl AttrValElement { - pub fn primitive(s: String) -> Self { - AttrValElement::Primitive(PrimVal::from_string(s)) - } - - pub fn as_expr(&self) -> Option<&AttrValExpr> { - match self { - AttrValElement::Expr(x) => Some(x), - _ => None, - } - } - - pub fn as_primitive(&self) -> Option<&PrimVal> { - match self { - AttrValElement::Primitive(x) => Some(x), - _ => None, - } - } -} - -#[cfg(test)] -mod test { - use super::*; - use pretty_assertions::assert_eq; - #[test] - fn test_parse_string_or_var_ref_list() { - let input = "{{foo}}{{bar}}b{}azb{a}z{{bat}}{}quok{{test}}"; - let output = AttrVal::parse_string(input); - assert_eq!( - output, - AttrVal(vec![ - AttrValElement::Expr(AttrValExpr::VarRef(VarName("foo".to_owned()))), - AttrValElement::Expr(AttrValExpr::VarRef(VarName("bar".to_owned()))), - AttrValElement::primitive("b{}azb{a}z".to_owned()), - AttrValElement::Expr(AttrValExpr::VarRef(VarName("bat".to_owned()))), - AttrValElement::primitive("{}quok".to_owned()), - AttrValElement::Expr(AttrValExpr::VarRef(VarName("test".to_owned()))), - ]), - ) - } - #[test] - fn test_parse_string_with_var_refs_attr_value() { - assert_eq!( - AttrVal( - vec![ - AttrValElement::Expr(AttrValExpr::VarRef(VarName("var".to_owned()))), - AttrValElement::primitive("something".to_owned()) - ] - .into() - ), - AttrVal::parse_string("{{var}}something") - ); - } -} diff --git a/src/value/attr_value/attr_value_expr.rs b/src/value/attr_value/attr_value_expr.rs deleted file mode 100644 index f565634..0000000 --- a/src/value/attr_value/attr_value_expr.rs +++ /dev/null @@ -1,244 +0,0 @@ -use super::super::*; -use anyhow::*; -use itertools::Itertools; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)] -pub enum BinOp { - Plus, - Minus, - Times, - Div, - Mod, - Equals, - NotEquals, - And, - Or, - GT, - LT, - Elvis, - RegexMatch, -} - -impl std::fmt::Display for BinOp { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - BinOp::Plus => write!(f, "+"), - BinOp::Minus => write!(f, "-"), - BinOp::Times => write!(f, "*"), - BinOp::Div => write!(f, "/"), - BinOp::Mod => write!(f, "%"), - BinOp::Equals => write!(f, "=="), - BinOp::NotEquals => write!(f, "!="), - BinOp::And => write!(f, "&&"), - BinOp::Or => write!(f, "||"), - BinOp::GT => write!(f, ">"), - BinOp::LT => write!(f, "<"), - BinOp::Elvis => write!(f, "?:"), - BinOp::RegexMatch => write!(f, "=~"), - } - } -} - -#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)] -pub enum UnaryOp { - Not, -} - -impl std::fmt::Display for UnaryOp { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - UnaryOp::Not => write!(f, "!"), - } - } -} - -#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)] -pub enum AttrValExpr { - Literal(AttrVal), - VarRef(VarName), - BinOp(Box, BinOp, Box), - UnaryOp(UnaryOp, Box), - IfElse(Box, Box, Box), - JsonAccess(Box, Box), - FunctionCall(String, Vec), -} - -impl std::fmt::Display for AttrValExpr { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - AttrValExpr::VarRef(x) => write!(f, "{}", x), - AttrValExpr::Literal(x) => write!(f, "\"{}\"", x), - AttrValExpr::BinOp(l, op, r) => write!(f, "({} {} {})", l, op, r), - AttrValExpr::UnaryOp(op, x) => write!(f, "{}{}", op, x), - AttrValExpr::IfElse(a, b, c) => write!(f, "(if {} then {} else {})", a, b, c), - AttrValExpr::JsonAccess(value, index) => write!(f, "{}[{}]", value, index), - AttrValExpr::FunctionCall(function_name, args) => write!(f, "{}({})", function_name, args.iter().join(", ")), - } - } -} - -// impl std::fmt::Debug for AttrValueExpr { -// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { -// write!(f, "{:?}", self) -//} - -impl AttrValExpr { - pub fn map_terminals_into(self, f: impl Fn(Self) -> Self) -> Self { - use AttrValExpr::*; - match self { - BinOp(box a, op, box b) => BinOp(box f(a), op, box f(b)), - IfElse(box a, box b, box c) => IfElse(box f(a), box f(b), box f(c)), - other => f(other), - } - } - - /// resolve variable references in the expression. Fails if a variable cannot be resolved. - pub fn resolve_refs(self, variables: &HashMap) -> Result { - use AttrValExpr::*; - match self { - // Literal(x) => Ok(Literal(AttrValue::from_primitive(x.resolve_fully(&variables)?))), - Literal(x) => Ok(Literal(x)), - VarRef(ref name) => Ok(Literal(AttrVal::from_primitive( - variables.get(name).with_context(|| format!("Unknown variable {} referenced in {:?}", &name, &self))?.clone(), - ))), - BinOp(box a, op, box b) => Ok(BinOp(box a.resolve_refs(variables)?, op, box b.resolve_refs(variables)?)), - UnaryOp(op, box x) => Ok(UnaryOp(op, box x.resolve_refs(variables)?)), - IfElse(box a, box b, box c) => { - Ok(IfElse(box a.resolve_refs(variables)?, box b.resolve_refs(variables)?, box c.resolve_refs(variables)?)) - } - JsonAccess(box a, box b) => Ok(JsonAccess(box a.resolve_refs(variables)?, box b.resolve_refs(variables)?)), - FunctionCall(function_name, args) => { - Ok(FunctionCall(function_name, args.into_iter().map(|a| a.resolve_refs(variables)).collect::>()?)) - } - } - } - - pub fn var_refs(&self) -> Vec<&VarName> { - use AttrValExpr::*; - match self { - Literal(s) => s.var_refs().collect(), - VarRef(name) => vec![name], - BinOp(box a, _, box b) => { - let mut refs = a.var_refs(); - refs.append(&mut b.var_refs()); - refs - } - UnaryOp(_, box x) => x.var_refs(), - IfElse(box a, box b, box c) => { - let mut refs = a.var_refs(); - refs.append(&mut b.var_refs()); - refs.append(&mut c.var_refs()); - refs - } - JsonAccess(box a, box b) => { - let mut refs = a.var_refs(); - refs.append(&mut b.var_refs()); - refs - } - FunctionCall(_, args) => args.iter().flat_map(|a| a.var_refs()).collect_vec(), - } - } - - pub fn eval(self, values: &HashMap) -> Result { - match self { - AttrValExpr::Literal(x) => x.resolve_fully(values), - AttrValExpr::VarRef(ref name) => values - .get(name) - .cloned() - .context(format!("Got unresolved variable {} while trying to evaluate expression {:?}", &name, &self)), - AttrValExpr::BinOp(a, op, b) => { - let a = a.eval(values)?; - let b = b.eval(values)?; - Ok(match op { - BinOp::Equals => PrimVal::from(a == b), - BinOp::NotEquals => PrimVal::from(a != b), - BinOp::And => PrimVal::from(a.as_bool()? && b.as_bool()?), - BinOp::Or => PrimVal::from(a.as_bool()? || b.as_bool()?), - - BinOp::Plus => PrimVal::from(a.as_f64()? + b.as_f64()?), - BinOp::Minus => PrimVal::from(a.as_f64()? - b.as_f64()?), - BinOp::Times => PrimVal::from(a.as_f64()? * b.as_f64()?), - BinOp::Div => PrimVal::from(a.as_f64()? / b.as_f64()?), - BinOp::Mod => PrimVal::from(a.as_f64()? % b.as_f64()?), - BinOp::GT => PrimVal::from(a.as_f64()? > b.as_f64()?), - BinOp::LT => PrimVal::from(a.as_f64()? < b.as_f64()?), - BinOp::Elvis => PrimVal::from(if a.0.is_empty() { b } else { a }), - BinOp::RegexMatch => { - let regex = regex::Regex::new(&b.as_string()?)?; - PrimVal::from(regex.is_match(&a.as_string()?)) - } - }) - } - AttrValExpr::UnaryOp(op, a) => { - let a = a.eval(values)?; - Ok(match op { - UnaryOp::Not => PrimVal::from(!a.as_bool()?), - }) - } - AttrValExpr::IfElse(cond, yes, no) => { - if cond.eval(values)?.as_bool()? { - yes.eval(values) - } else { - no.eval(values) - } - } - AttrValExpr::JsonAccess(val, index) => { - let val = val.eval(values)?; - let index = index.eval(values)?; - match val.as_json_value()? { - serde_json::Value::Array(val) => { - let index = index.as_i32()?; - let indexed_value = val.get(index as usize).unwrap_or(&serde_json::Value::Null); - Ok(PrimVal::from(indexed_value)) - } - serde_json::Value::Object(val) => { - let indexed_value = val - .get(&index.as_string()?) - .or_else(|| val.get(&index.as_i32().ok()?.to_string())) - .unwrap_or(&serde_json::Value::Null); - Ok(PrimVal::from(indexed_value)) - } - _ => bail!("Unable to index into value {}", val), - } - } - AttrValExpr::FunctionCall(function_name, args) => { - let args = args.into_iter().map(|a| a.eval(values)).collect::>()?; - call_expr_function(&function_name, args) - } - } - } - - pub fn parse(s: &str) -> Result { - let parsed = match parser::parse(s) { - Ok((_, x)) => Ok(x), - Err(nom::Err::Error(e) | nom::Err::Failure(e)) => Err(anyhow!(nom::error::convert_error(s, e))), - Err(nom::Err::Incomplete(_)) => Err(anyhow!("Parsing incomplete")), - }; - parsed.context("Failed to parse expression") - } -} - -fn call_expr_function(name: &str, args: Vec) -> Result { - match name { - "round" => match args.as_slice() { - [num, digits] => { - let num = num.as_f64()?; - let digits = digits.as_i32()?; - Ok(PrimVal::from(format!("{:.1$}", num, digits as usize))) - } - _ => Err(anyhow!("Incorrect number of arguments given to {}", name)), - }, - "replace" => match args.as_slice() { - [string, pattern, replacement] => { - let string = string.as_string()?; - let pattern = regex::Regex::new(&pattern.as_string()?)?; - let replacement = replacement.as_string()?; - Ok(PrimVal::from(pattern.replace_all(&string, replacement.replace("$", "$$").replace("\\", "$")).into_owned())) - } - _ => Err(anyhow!("Incorrect number of arguments given to {}", name)), - }, - _ => Err(anyhow!("Unknown function {}", name)), - } -} diff --git a/src/value/attr_value/mod.rs b/src/value/attr_value/mod.rs deleted file mode 100644 index 077a689..0000000 --- a/src/value/attr_value/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod attr_value; -pub mod attr_value_expr; -pub mod parser; -pub use attr_value::*; -pub use attr_value_expr::*; diff --git a/src/value/attr_value/parser.rs b/src/value/attr_value/parser.rs deleted file mode 100644 index 5f73d1d..0000000 --- a/src/value/attr_value/parser.rs +++ /dev/null @@ -1,224 +0,0 @@ -use super::*; - -use nom::{ - branch::*, - bytes::complete::{tag, take_while}, - character::complete::{multispace0 as multispace, *}, - combinator::{map, map_res, *}, - error::{context, ParseError, VerboseError}, - multi::{many0, separated_list0}, - sequence::{delimited, preceded, *}, - IResult, Parser, -}; - -use super::super::*; - -fn ws<'a, P, O, E: ParseError<&'a str>>(p: P) -> impl FnMut(&'a str) -> IResult<&'a str, O, E> -where - P: Parser<&'a str, O, E>, -{ - delimited(multispace, p, multispace) -} - -fn parse_num(i: &str) -> IResult<&str, f64, VerboseError<&str>> { - let (i, neg) = opt(tag("-"))(i)?; - let (i, num): (_, f64) = map_res(take_while(|c: char| c.is_numeric() || c == '.'), |n: &str| n.parse::())(i)?; - Ok((i, if neg.is_some() { -num } else { num })) -} - -fn parse_bool(i: &str) -> IResult<&str, &str, VerboseError<&str>> { - alt((tag("true"), tag("false")))(i) -} - -fn parse_literal(i: &str) -> IResult<&str, &str, VerboseError<&str>> { - alt((parse_bool, parse_stringlit, recognize(parse_num)))(i) -} - -fn parse_stringlit(i: &str) -> IResult<&str, &str, VerboseError<&str>> { - alt((delimited(tag("'"), take_while(|c| c != '\''), tag("'")), delimited(tag("\""), take_while(|c| c != '"'), tag("\""))))(i) -} - -fn parse_identifier(i: &str) -> IResult<&str, &str, VerboseError<&str>> { - verify(recognize(pair(alt((alpha1, tag("_"), tag("-"))), many0(alt((alphanumeric1, tag("_"), tag("-")))))), |x| { - !["if", "then", "else"].contains(x) - })(i) -} - -fn parse_unary_op(i: &str) -> IResult<&str, UnaryOp, VerboseError<&str>> { - value(UnaryOp::Not, tag("!"))(i) -} - -fn parse_function_call(i: &str) -> IResult<&str, AttrValExpr, VerboseError<&str>> { - let (i, name) = take_while(|c: char| c.is_ascii_alphanumeric() || c == '_')(i)?; - let (i, args) = delimited(tag("("), separated_list0(tag(","), ws(parse_expr)), tag(")"))(i)?; - Ok((i, AttrValExpr::FunctionCall(name.to_string(), args))) -} - -///////////////// -// actual tree // -///////////////// - -fn parse_factor(i: &str) -> IResult<&str, AttrValExpr, VerboseError<&str>> { - let (i, unary_op) = opt(parse_unary_op)(i)?; - let (i, factor) = alt(( - context("expression", ws(delimited(tag("("), parse_expr, tag(")")))), - context("if-expression", ws(parse_ifelse)), - context("function-call", ws(parse_function_call)), - context("literal", map(ws(parse_literal), |x| AttrValExpr::Literal(AttrVal::parse_string(x)))), - context("identifier", map(ws(parse_identifier), |x| AttrValExpr::VarRef(VarName(x.to_string())))), - ))(i)?; - Ok(( - i, - match unary_op { - Some(op) => AttrValExpr::UnaryOp(op, box factor), - None => factor, - }, - )) -} - -fn parse_object_index(i: &str) -> IResult<&str, AttrValExpr, VerboseError<&str>> { - let (i, initial) = parse_factor(i)?; - let (i, remainder) = many0(alt(( - delimited(tag("["), ws(parse_expr), tag("]")), - map(preceded(tag("."), parse_identifier), |x| AttrValExpr::Literal(AttrVal::from_primitive(x))), - )))(i)?; - let indexes = remainder.into_iter().fold(initial, |acc, index| AttrValExpr::JsonAccess(box acc, box index)); - Ok((i, indexes)) -} - -fn parse_term3(i: &str) -> IResult<&str, AttrValExpr, VerboseError<&str>> { - let (i, initial) = parse_object_index(i)?; - let (i, remainder) = many0(alt(( - map(preceded(tag("*"), parse_object_index), |x| (BinOp::Times, x)), - map(preceded(tag("/"), parse_object_index), |x| (BinOp::Div, x)), - map(preceded(tag("%"), parse_object_index), |x| (BinOp::Mod, x)), - )))(i)?; - - let exprs = remainder.into_iter().fold(initial, |acc, (op, expr)| AttrValExpr::BinOp(box acc, op, box expr)); - - Ok((i, exprs)) -} -fn parse_term2(i: &str) -> IResult<&str, AttrValExpr, VerboseError<&str>> { - let (i, initial) = parse_term3(i)?; - let (i, remainder) = many0(alt(( - map(preceded(tag("+"), parse_term3), |x| (BinOp::Plus, x)), - map(preceded(tag("-"), parse_term3), |x| (BinOp::Minus, x)), - )))(i)?; - - let exprs = remainder.into_iter().fold(initial, |acc, (op, expr)| AttrValExpr::BinOp(box acc, op, box expr)); - - Ok((i, exprs)) -} - -fn parse_term1(i: &str) -> IResult<&str, AttrValExpr, VerboseError<&str>> { - let (i, initial) = parse_term2(i)?; - let (i, remainder) = many0(alt(( - map(preceded(tag("=="), parse_term2), |x| (BinOp::Equals, x)), - map(preceded(tag("!="), parse_term2), |x| (BinOp::NotEquals, x)), - map(preceded(tag(">"), parse_term2), |x| (BinOp::GT, x)), - map(preceded(tag("<"), parse_term2), |x| (BinOp::LT, x)), - map(preceded(tag("=~"), parse_term2), |x| (BinOp::RegexMatch, x)), - )))(i)?; - - let exprs = remainder.into_iter().fold(initial, |acc, (op, expr)| AttrValExpr::BinOp(box acc, op, box expr)); - - Ok((i, exprs)) -} -pub fn parse_expr(i: &str) -> IResult<&str, AttrValExpr, VerboseError<&str>> { - let (i, initial) = parse_term1(i)?; - let (i, remainder) = many0(alt(( - map(preceded(tag("&&"), parse_term1), |x| (BinOp::And, x)), - map(preceded(tag("||"), parse_term1), |x| (BinOp::Or, x)), - map(preceded(tag("?:"), parse_term1), |x| (BinOp::Elvis, x)), - )))(i)?; - - let exprs = remainder.into_iter().fold(initial, |acc, (op, expr)| AttrValExpr::BinOp(box acc, op, box expr)); - - Ok((i, exprs)) -} - -fn parse_ifelse(i: &str) -> IResult<&str, AttrValExpr, VerboseError<&str>> { - let (i, _) = tag("if")(i)?; - let (i, a) = context("condition", ws(parse_expr))(i)?; - let (i, _) = tag("then")(i)?; - let (i, b) = context("true-case", ws(parse_expr))(i)?; - let (i, _) = tag("else")(i)?; - let (i, c) = context("false-case", ws(parse_expr))(i)?; - Ok((i, AttrValExpr::IfElse(box a, box b, box c))) -} - -pub fn parse(i: &str) -> IResult<&str, AttrValExpr, VerboseError<&str>> { - complete(parse_expr)(i) -} - -#[cfg(test)] -mod test { - use super::*; - use pretty_assertions::assert_eq; - #[test] - fn test_parser() { - use self::{BinOp::*, UnaryOp::*}; - use AttrValExpr::*; - - assert_eq!(("", 12.22f64), parse_num("12.22").unwrap()); - assert_eq!(Literal(AttrVal::from_primitive("12")), AttrValExpr::parse("12").unwrap()); - assert_eq!(UnaryOp(Not, box Literal(AttrVal::from_primitive("false"))), AttrValExpr::parse("!false").unwrap()); - assert_eq!( - BinOp(box Literal(AttrVal::from_primitive("12")), Plus, box Literal(AttrVal::from_primitive("2"))), - AttrValExpr::parse("12 + 2").unwrap() - ); - assert_eq!( - BinOp( - box FunctionCall( - "test".to_string(), - vec![ - JsonAccess(box VarRef(VarName("foo".to_string())), box Literal(AttrVal::from_primitive("hi"))), - Literal(AttrVal::from_primitive("ho")), - ] - ), - Times, - box Literal(AttrVal::from_primitive(2)) - ), - AttrValExpr::parse(r#"(test(foo["hi"], ("ho")) * 2)"#).unwrap() - ); - assert_eq!( - UnaryOp(Not, box BinOp(box Literal(AttrVal::from_primitive("1")), Equals, box Literal(AttrVal::from_primitive("2")))), - AttrValExpr::parse("!(1 == 2)").unwrap() - ); - assert_eq!( - IfElse( - box VarRef(VarName("a".to_string())), - box VarRef(VarName("b".to_string())), - box VarRef(VarName("c".to_string())), - ), - AttrValExpr::parse("if a then b else c").unwrap() - ); - assert_eq!( - JsonAccess( - box VarRef(VarName("array".to_string())), - box BinOp(box Literal(AttrVal::from_primitive("1")), Plus, box Literal(AttrVal::from_primitive("2"))) - ), - AttrValExpr::parse(r#"(array)[1+2]"#).unwrap() - ); - assert_eq!( - JsonAccess( - box JsonAccess( - box VarRef(VarName("object".to_string())), - box Literal(AttrVal::from_primitive("field".to_string())), - ), - box Literal(AttrVal::from_primitive("field2".to_string())), - ), - AttrValExpr::parse(r#"object.field.field2"#).unwrap() - ); - } - #[test] - fn test_complex() { - let parsed = - AttrValExpr::parse(r#"if hi > 12 + 2 * 2 && 12 == 15 then "foo" else if !true then 'hi' else "{{bruh}}""#).unwrap(); - - assert_eq!( - r#"(if ((hi > ("12" + ("2" * "2"))) && ("12" == "15")) then "foo" else (if !"true" then "hi" else "{{bruh}}"))"#, - format!("{}", parsed), - ) - } -} diff --git a/src/value/primitive.rs b/src/value/primitive.rs deleted file mode 100644 index 98a771c..0000000 --- a/src/value/primitive.rs +++ /dev/null @@ -1,188 +0,0 @@ -use anyhow::*; -use itertools::Itertools; -use serde::{Deserialize, Serialize}; -use std::{convert::TryFrom, fmt, iter::FromIterator}; - -use crate::impl_try_from; - -#[derive(Clone, Deserialize, Serialize, derive_more::From, Default)] -pub struct PrimVal(pub String); - -impl fmt::Display for PrimVal { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.0) - } -} -impl fmt::Debug for PrimVal { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "\"{}\"", self.0) - } -} - -/// Manually implement equality, to allow for values in different formats (i.e. "1" and "1.0") to still be considered as equal. -impl std::cmp::PartialEq for PrimVal { - fn eq(&self, other: &Self) -> bool { - if let (Ok(a), Ok(b)) = (self.as_f64(), other.as_f64()) { - a == b - } else { - self.0 == other.0 - } - } -} - -impl FromIterator for PrimVal { - fn from_iter>(iter: T) -> Self { - PrimVal(iter.into_iter().join("")) - } -} - -impl std::str::FromStr for PrimVal { - type Err = anyhow::Error; - - /// parses the value, trying to turn it into a number and a boolean first, - /// before deciding that it is a string. - fn from_str(s: &str) -> Result { - Ok(PrimVal::from_string(s.to_string())) - } -} - -impl_try_from!(PrimVal { - for String => |x| x.as_string(); - for f64 => |x| x.as_f64(); - for bool => |x| x.as_bool(); - for Vec => |x| x.as_vec(); -}); - -impl From for PrimVal { - fn from(x: bool) -> Self { - PrimVal(x.to_string()) - } -} - -impl From for PrimVal { - fn from(s: i32) -> Self { - PrimVal(s.to_string()) - } -} - -impl From for PrimVal { - fn from(s: u32) -> Self { - PrimVal(s.to_string()) - } -} - -impl From for PrimVal { - fn from(s: f32) -> Self { - PrimVal(s.to_string()) - } -} - -impl From for PrimVal { - fn from(s: u8) -> Self { - PrimVal(s.to_string()) - } -} -impl From for PrimVal { - fn from(s: f64) -> Self { - PrimVal(s.to_string()) - } -} - -impl From<&str> for PrimVal { - fn from(s: &str) -> Self { - PrimVal(s.to_string()) - } -} - -impl From<&serde_json::Value> for PrimVal { - fn from(v: &serde_json::Value) -> Self { - PrimVal( - v.as_str() - .map(|x| x.to_string()) - .or_else(|| serde_json::to_string(v).ok()) - .unwrap_or_else(|| "".to_string()), - ) - } -} - -impl PrimVal { - pub fn from_string(s: String) -> Self { - PrimVal(s) - } - - pub fn into_inner(self) -> String { - self.0 - } - - /// This will never fail - pub fn as_string(&self) -> Result { - Ok(self.0.to_owned()) - } - - pub fn as_f64(&self) -> Result { - self.0.parse().map_err(|e| anyhow!("couldn't convert {:?} to f64: {}", &self, e)) - } - - pub fn as_i32(&self) -> Result { - self.0.parse().map_err(|e| anyhow!("couldn't convert {:?} to i32: {}", &self, e)) - } - - pub fn as_bool(&self) -> Result { - self.0.parse().map_err(|e| anyhow!("couldn't convert {:?} to bool: {}", &self, e)) - } - - pub fn as_vec(&self) -> Result> { - parse_vec(self.0.to_owned()).map_err(|e| anyhow!("Couldn't convert {:#?} to a vec: {}", &self, e)) - } - - pub fn as_json_value(&self) -> Result { - serde_json::from_str::(&self.0) - .with_context(|| format!("Couldn't convert {:#?} to a json object", &self)) - } -} - -fn parse_vec(a: String) -> Result> { - match a.strip_prefix('[').and_then(|x| x.strip_suffix(']')) { - Some(content) => { - let mut items: Vec = content.split(',').map(|x: &str| x.to_string()).collect(); - let mut removed = 0; - for times_ran in 0..items.len() { - // escapes `,` if there's a `\` before em - if items[times_ran - removed].ends_with('\\') { - items[times_ran - removed].pop(); - let it = items.remove((times_ran + 1) - removed); - items[times_ran - removed] += ","; - items[times_ran - removed] += ⁢ - removed += 1; - } - } - Ok(items) - } - None => Err(anyhow!("Is your array built like this: '[these,are,items]'?")), - } -} - -#[cfg(test)] -mod test { - use super::*; - use pretty_assertions::assert_eq; - #[test] - fn test_parse_vec() { - assert_eq!(vec![""], parse_vec("[]".to_string()).unwrap(), "should be able to parse empty lists"); - assert_eq!(vec!["hi"], parse_vec("[hi]".to_string()).unwrap(), "should be able to parse single element list"); - assert_eq!( - vec!["hi", "ho", "hu"], - parse_vec("[hi,ho,hu]".to_string()).unwrap(), - "should be able to parse three element list" - ); - assert_eq!(vec!["hi,ho"], parse_vec("[hi\\,ho]".to_string()).unwrap(), "should be able to parse list with escaped comma"); - assert_eq!( - vec!["hi,ho", "hu"], - parse_vec("[hi\\,ho,hu]".to_string()).unwrap(), - "should be able to parse two element list with escaped comma" - ); - assert!(parse_vec("".to_string()).is_err(), "Should fail when parsing empty string"); - assert!(parse_vec("[a,b".to_string()).is_err(), "Should fail when parsing unclosed list"); - assert!(parse_vec("a]".to_string()).is_err(), "Should fail when parsing unopened list"); - } -} diff --git a/src/widgets/widget_node.rs b/src/widgets/widget_node.rs deleted file mode 100644 index bb7171b..0000000 --- a/src/widgets/widget_node.rs +++ /dev/null @@ -1,167 +0,0 @@ -use crate::{ - config::{ - element::{WidgetDefinition, WidgetUse}, - xml_ext::TextPos, - WindowName, - }, - eww_state::EwwState, - value::{AttrName, AttrVal, VarName}, -}; -use anyhow::*; -use dyn_clone; -use std::collections::HashMap; -pub trait WidgetNode: std::fmt::Debug + dyn_clone::DynClone + Send + Sync { - fn get_name(&self) -> &str; - fn get_text_pos(&self) -> Option; - - /// Generate a [gtk::Widget] from a [element::WidgetUse]. - /// - /// Also registers all the necessary state-change handlers in the eww_state. - /// - /// This may return `Err` in case there was an actual error while parsing or - /// resolving the widget, Or `Ok(None)` if the widget_use just didn't match any - /// widget name. - fn render( - &self, - eww_state: &mut EwwState, - window_name: &WindowName, - widget_definitions: &HashMap, - ) -> Result; -} - -dyn_clone::clone_trait_object!(WidgetNode); - -#[derive(Debug, Clone)] -pub struct UserDefined { - name: String, - text_pos: Option, - content: Box, -} - -impl WidgetNode for UserDefined { - fn get_name(&self) -> &str { - &self.name - } - - fn get_text_pos(&self) -> Option { - self.text_pos - } - - fn render( - &self, - eww_state: &mut EwwState, - window_name: &WindowName, - widget_definitions: &HashMap, - ) -> Result { - self.content.render(eww_state, window_name, widget_definitions) - } -} - -#[derive(Debug, Clone)] -pub struct Generic { - pub name: String, - pub text_pos: Option, - pub children: Vec>, - pub attrs: HashMap, -} - -impl Generic { - pub fn get_attr(&self, key: &str) -> Result<&AttrVal> { - self.attrs.get(key).context(format!("attribute '{}' missing from use of '{}'", key, &self.name)) - } - - /// returns all the variables that are referenced in this widget - pub fn referenced_vars(&self) -> impl Iterator { - self.attrs.iter().flat_map(|(_, value)| value.var_refs()) - } -} - -impl WidgetNode for Generic { - fn get_name(&self) -> &str { - &self.name - } - - fn get_text_pos(&self) -> Option { - self.text_pos - } - - fn render( - &self, - eww_state: &mut EwwState, - window_name: &WindowName, - widget_definitions: &HashMap, - ) -> Result { - crate::widgets::build_builtin_gtk_widget(eww_state, window_name, widget_definitions, self)? - .with_context(|| format!("Unknown widget '{}'", self.get_name())) - } -} - -pub fn generate_generic_widget_node( - defs: &HashMap, - local_env: &HashMap, - w: WidgetUse, -) -> Result> { - if let Some(def) = defs.get(&w.name) { - ensure!(w.children.is_empty(), "User-defined widgets cannot be given children."); - - let new_local_env = w - .attrs - .into_iter() - .map(|(name, value)| (VarName(name.0), value.resolve_one_level(local_env))) - .collect::>(); - - let content = generate_generic_widget_node(defs, &new_local_env, def.structure.clone())?; - Ok(Box::new(UserDefined { name: w.name, text_pos: w.text_pos, content })) - } else { - Ok(Box::new(Generic { - name: w.name, - text_pos: w.text_pos, - attrs: w.attrs.into_iter().map(|(name, value)| (name, value.resolve_one_level(local_env))).collect(), - children: w - .children - .into_iter() - .map(|child| generate_generic_widget_node(defs, local_env, child)) - .collect::>>()?, - })) - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::config::xml_ext::*; - use maplit::hashmap; - #[test] - fn test_generic_generate() { - let w_def1 = { - let input = r#"{{nested1}}{{raw1}}"#; - let document = roxmltree::Document::parse(input).unwrap(); - let xml = XmlNode::from(document.root_element().clone()); - WidgetDefinition::from_xml_element(&xml.as_element().unwrap()).unwrap() - }; - let w_def2 = { - let input = r#""#; - let document = roxmltree::Document::parse(input).unwrap(); - let xml = XmlNode::from(document.root_element().clone()); - WidgetDefinition::from_xml_element(&xml.as_element().unwrap()).unwrap() - }; - let w_use = { - let input = r#""#; - let document = roxmltree::Document::parse(input).unwrap(); - let xml = XmlNode::from(document.root_element().clone()); - WidgetUse::from_xml_node(xml).unwrap() - }; - - let generic = generate_generic_widget_node( - &hashmap! { "foo".to_string() => w_def1, "bar".to_string() => w_def2 }, - &HashMap::new(), - w_use, - ) - .unwrap(); - - // TODO actually implement this test ._. - - dbg!(&generic); - // panic!("REEEEEEEEEE") - } -}