From 08dacdd263f8b4bc56e5358801ba4979d596f912 Mon Sep 17 00:00:00 2001 From: Kogia-sima Date: Fri, 5 Jun 2020 05:39:33 +0900 Subject: [PATCH] Add source code --- .gitignore | 4 + Cargo.lock | 253 ++++++++++ Cargo.toml | 32 ++ LICENSE | 22 + README.md | 98 ++++ docs/rfcs/1-dynamic-loading.md | 210 ++++++++ docs/rfcs/3-template-trait.md | 45 ++ examples/Cargo.toml | 15 + examples/simple.rs | 15 + examples/templates/simple.stpl | 12 + integration-tests/Cargo.toml | 14 + integration-tests/src/lib.rs | 21 + .../templates/custom_delimiter.out | 1 + .../templates/custom_delimiter.stpl | 1 + integration-tests/templates/empty.out | 0 integration-tests/templates/empty.stpl | 0 integration-tests/templates/json.out | 4 + integration-tests/templates/json.stpl | 4 + integration-tests/templates/noescape.out | 1 + integration-tests/templates/noescape.stpl | 1 + integration-tests/tests/basic.rs | 75 +++ resources/logo.png | Bin 0 -> 25941 bytes rustfmt.toml | 6 + sailfish-compiler/Cargo.toml | 38 ++ sailfish-compiler/build.rs | 1 + sailfish-compiler/src/compiler.rs | 84 ++++ sailfish-compiler/src/config.rs | 0 sailfish-compiler/src/error.rs | 256 ++++++++++ sailfish-compiler/src/lib.rs | 18 + sailfish-compiler/src/optimizer.rs | 101 ++++ sailfish-compiler/src/parser.rs | 474 ++++++++++++++++++ sailfish-compiler/src/procmacro.rs | 238 +++++++++ sailfish-compiler/src/resolver.rs | 19 + sailfish-compiler/src/translator.rs | 223 ++++++++ sailfish-compiler/src/util.rs | 36 ++ sailfish-macros/Cargo.toml | 25 + sailfish-macros/src/lib.rs | 18 + sailfish/Cargo.toml | 17 + sailfish/src/lib.rs | 12 + sailfish/src/runtime/buffer.rs | 129 +++++ sailfish/src/runtime/escape/avx2.rs | 90 ++++ sailfish/src/runtime/escape/fallback.rs | 79 +++ sailfish/src/runtime/escape/mod.rs | 147 ++++++ sailfish/src/runtime/escape/naive.rs | 46 ++ sailfish/src/runtime/escape/sse2.rs | 132 +++++ sailfish/src/runtime/macros.rs | 29 ++ sailfish/src/runtime/mod.rs | 54 ++ sailfish/src/runtime/render.rs | 207 ++++++++ sailfish/src/runtime/size_hint.rs | 32 ++ syntax/vim/ftdetect/sailfish.vim | 6 + syntax/vim/indent/sailfish.vim | 12 + syntax/vim/syntax/sailfish.vim | 17 + syntax/vscode/.gitignore | 1 + syntax/vscode/.vscode/launch.json | 18 + syntax/vscode/.vscodeignore | 4 + syntax/vscode/CHANGELOG.md | 9 + syntax/vscode/README.md | 13 + syntax/vscode/language-configuration.json | 36 ++ syntax/vscode/package.json | 27 + syntax/vscode/screenshot.png | Bin 0 -> 66737 bytes .../vscode/syntaxes/sailfish.tmLanguage.json | 50 ++ 61 files changed, 3532 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docs/rfcs/1-dynamic-loading.md create mode 100644 docs/rfcs/3-template-trait.md create mode 100644 examples/Cargo.toml create mode 100644 examples/simple.rs create mode 100644 examples/templates/simple.stpl create mode 100644 integration-tests/Cargo.toml create mode 100644 integration-tests/src/lib.rs create mode 100644 integration-tests/templates/custom_delimiter.out create mode 100644 integration-tests/templates/custom_delimiter.stpl create mode 100644 integration-tests/templates/empty.out create mode 100644 integration-tests/templates/empty.stpl create mode 100644 integration-tests/templates/json.out create mode 100644 integration-tests/templates/json.stpl create mode 100644 integration-tests/templates/noescape.out create mode 100644 integration-tests/templates/noescape.stpl create mode 100644 integration-tests/tests/basic.rs create mode 100644 resources/logo.png create mode 100644 rustfmt.toml create mode 100644 sailfish-compiler/Cargo.toml create mode 100644 sailfish-compiler/build.rs create mode 100644 sailfish-compiler/src/compiler.rs create mode 100644 sailfish-compiler/src/config.rs create mode 100644 sailfish-compiler/src/error.rs create mode 100644 sailfish-compiler/src/lib.rs create mode 100644 sailfish-compiler/src/optimizer.rs create mode 100644 sailfish-compiler/src/parser.rs create mode 100644 sailfish-compiler/src/procmacro.rs create mode 100644 sailfish-compiler/src/resolver.rs create mode 100644 sailfish-compiler/src/translator.rs create mode 100644 sailfish-compiler/src/util.rs create mode 100644 sailfish-macros/Cargo.toml create mode 100644 sailfish-macros/src/lib.rs create mode 100644 sailfish/Cargo.toml create mode 100644 sailfish/src/lib.rs create mode 100644 sailfish/src/runtime/buffer.rs create mode 100644 sailfish/src/runtime/escape/avx2.rs create mode 100644 sailfish/src/runtime/escape/fallback.rs create mode 100644 sailfish/src/runtime/escape/mod.rs create mode 100644 sailfish/src/runtime/escape/naive.rs create mode 100644 sailfish/src/runtime/escape/sse2.rs create mode 100644 sailfish/src/runtime/macros.rs create mode 100644 sailfish/src/runtime/mod.rs create mode 100644 sailfish/src/runtime/render.rs create mode 100644 sailfish/src/runtime/size_hint.rs create mode 100644 syntax/vim/ftdetect/sailfish.vim create mode 100644 syntax/vim/indent/sailfish.vim create mode 100644 syntax/vim/syntax/sailfish.vim create mode 100644 syntax/vscode/.gitignore create mode 100644 syntax/vscode/.vscode/launch.json create mode 100644 syntax/vscode/.vscodeignore create mode 100644 syntax/vscode/CHANGELOG.md create mode 100644 syntax/vscode/README.md create mode 100644 syntax/vscode/language-configuration.json create mode 100644 syntax/vscode/package.json create mode 100644 syntax/vscode/screenshot.png create mode 100644 syntax/vscode/syntaxes/sailfish.tmLanguage.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e234ff5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +target/ +scripts/ +*.log +*.s diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..c9e1ba5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,253 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ctor" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.30 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "difference" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "examples" +version = "0.0.1" +dependencies = [ + "sailfish 0.0.1", + "sailfish-macros 0.0.1", +] + +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "integration-tests" +version = "0.0.1" +dependencies = [ + "pretty_assertions 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", + "sailfish 0.0.1", + "sailfish-macros 0.0.1", + "trybuild 1.0.28 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "itoa" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "memchr" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "output_vt100" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "pretty_assertions" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "ctor 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", + "difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "output_vt100 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "proc-macro2" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "quote" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "sailfish" +version = "0.0.1" +dependencies = [ + "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "ryu 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "sailfish-compiler" +version = "0.0.1" +dependencies = [ + "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "pretty_assertions 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.30 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "sailfish-macros" +version = "0.0.1" +dependencies = [ + "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)", + "sailfish-compiler 0.0.1", +] + +[[package]] +name = "serde" +version = "1.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde_derive 1.0.111 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_derive" +version = "1.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.30 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_json" +version = "1.0.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "ryu 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.111 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "syn" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "termcolor" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-util 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "toml" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde 1.0.111 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "trybuild" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.111 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)", + "termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "toml 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-xid" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[metadata] +"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +"checksum ctor 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "cf6b25ee9ac1995c54d7adb2eff8cfffb7260bc774fb63c601ec65467f43cd9d" +"checksum difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" +"checksum glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +"checksum itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" +"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +"checksum memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" +"checksum output_vt100 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9" +"checksum pretty_assertions 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3f81e1644e1b54f5a68959a29aa86cde704219254669da328ecfdf6a1f09d427" +"checksum proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)" = "beae6331a816b1f65d04c45b078fd8e6c93e8071771f41b8163255bbd8d7c8fa" +"checksum quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "54a21852a652ad6f610c9510194f398ff6f8692e334fd1145fed931f7fbe44ea" +"checksum ryu 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +"checksum serde 1.0.111 (registry+https://github.com/rust-lang/crates.io-index)" = "c9124df5b40cbd380080b2cc6ab894c040a3070d995f5c9dc77e18c34a8ae37d" +"checksum serde_derive 1.0.111 (registry+https://github.com/rust-lang/crates.io-index)" = "3f2c3ac8e6ca1e9c80b8be1023940162bf81ae3cffbb1809474152f2ce1eb250" +"checksum serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)" = "993948e75b189211a9b31a7528f950c6adc21f9720b6438ff80a7fa2f864cea2" +"checksum syn 1.0.30 (registry+https://github.com/rust-lang/crates.io-index)" = "93a56fabc59dce20fe48b6c832cc249c713e7ed88fa28b0ee0a3bfcaae5fe4e2" +"checksum termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" +"checksum toml 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a" +"checksum trybuild 1.0.28 (registry+https://github.com/rust-lang/crates.io-index)" = "39e3183158b2c8170db33b8b3a90ddc7b5f380d15b50794d22c1fa9c61b47249" +"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" +"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" +"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +"checksum winapi-util 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4ef3f48 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,32 @@ +[workspace] +members = [ + "sailfish", + "sailfish-compiler", + "sailfish-macros", + "examples", + "integration-tests" +] +exclude = [ + "benches" +] + +[profile.dev.package.syn] +opt-level = 0 +debug = false +debug-assertions = false +overflow-checks = false +incremental = true + +[profile.test.package.syn] +opt-level = 0 +debug = false +debug-assertions = false +overflow-checks = false +incremental = true + +[profile.release.package.syn] +opt-level = 0 +debug = false +debug-assertions = false +overflow-checks = false +incremental = true diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..52754f8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ + + The MIT License (MIT) + Copyright (c) 2020 Ryohei Machida + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..6e513c3 --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +
+ +![SailFish](./resources/logo.png) + +Simple, small, and extremely fast template engine for Rust + +
+ +## ✨ Features + +- Simple and intuitive syntax inspired by [EJS](https://ejs.co/) +- Relatively small number of dependencies (<15 crates in total) +- Extremely fast (See [benchmarks](./benches)) +- Better error message +- Template rendering is always type-safe because templates are statically compiled. +- Syntax highlighting support ([vscode](./syntax/vscode), [vim](./syntax/vim)) +- Automatically re-compile sources when template file is updated. + +:warning: Currentry sailfish is in early-stage development. You can use this library but be sure that there might be some bugs. Also API is still unstable, and thus may changes frequently. + +## 🐟 Example + +Dependencies: + +```toml +[dependencies] +sailfish = "0.0.1" +sailfish-macros = "0.0.1" +``` + +Template file (templates/hello.stpl): + +```html + + + + <%= content %> + + +``` + +Code: + +```rust +#[macro_use] +extern crate sailfish_macros; // enable derive macro + +use sailfish::TemplateOnce; + +#[derive(TemplateOnce)] +#[template(path = "hello.stpl")] +struct Hello { + content: String +} + +fn main() { + println!("{}", Hello { content: String::from("Hello, world!") }.render_once().unwrap()); +} +``` + +You can find more examples in [examples](./examples) directory. + +## 🐾 Roadmap + +- `Template` trait ([RFC](https://github.com/Kogia-sima/sailfish/blob/master/docs/rfcs/3-template-trait.md)) +- Template inheritance (block, partials, etc.) +- Include another templates without copy +- Whitespace suppressing +- HTML minification +- Filters +- Dynamic template compilation ([RFC](https://github.com/Kogia-sima/sailfish/blob/master/docs/rfcs/1-dynamic-loading.md)) +- `format_templates!(fmt, args..)` macro + +## 👤 Author + +:jp: **Ryohei Machida** + +* Github: [@Kogia-sima](https://github.com/Kogia-sima) + +## 🤝 Contributing + +Contributions, issues and feature requests are welcome! + +Feel free to check [issues page](https://github.com/Kogia-sima/sailfish/issues). + +## Show your support + +Give a ⭐️ if this project helped you! + + +## 📝 License + +Copyright © 2020 [Ryohei Machida](https://github.com/Kogia-sima). + +This project is [MIT](https://github.com/Kogia-sima/sailfish/blob/master/LICENSE) licensed. + +*** +_This README was generated with ❤️ by [readme-md-generator](https://github.com/kefranabg/readme-md-generator)_ diff --git a/docs/rfcs/1-dynamic-loading.md b/docs/rfcs/1-dynamic-loading.md new file mode 100644 index 0000000..c22bce0 --- /dev/null +++ b/docs/rfcs/1-dynamic-loading.md @@ -0,0 +1,210 @@ +# Dynamic template loading + +## Description + +Specify the path to template file at runtime, compile it, and then render with supplied data. + +This operation should be type-safe, and not raise any error after template compilation. + +## `sailfish::dynamic::compile` function API + +#### Signature + +```rust +fn compile>(path: P) -> DynamicTemplate; +``` + +#### Behaviour + +1. Generate Rust code to render templates +2. Compile it as a shared library by calling `cargo build` command. +3. Load the generated shared library. +4. returns the `DynamicTemplate` struct which contains the function pointer to call the template function. + +## `DynamicTemplate::render` method API + +#### Signature + +```rust +impl DynamicTemplate { + fn render(&self, data: &data) -> RenderResult; +} +``` + +#### Behaviour + +1. Serialize the `data` to byte array +2. Create the vtable for memory allocation (See the below section) +3. Pass the those objects to the template function pointer. +4. Retrieve the result from function pointer, deserialize it to `Result` and then return it. + +Trait bound makes this code type-safe. + +## Safety for memory allocation + +Since compiler used for compiling templates at runtime is different from the one used for compiling renderer, we must export allocator functions as vtable and share it. + +```rust +#[repr(C)] +pub struct AllocVtable { + pub alloc: unsafe fn(Layout) -> *mut u8, + pub realloc: unsafe fn(*mut u8, Layout, usize) -> *mut u8, +} + +struct VBuffer { + data: *mut u8, + len: usize, + capacity: usize, + vtable: AllocVTable, +} +``` + +AllocVtable is passed to template function, and then VBuffer is constructed inside template function. + +VBuffer should always use AllocVTable to allocate/reallocate a new memory. That cannot achieve with `std::string::String` struct only. We must re-implement the `RawVec` struct. + +## Rust standard library confliction problem + +Rarely, but not never, dynamically compiled templates may use different version of standard library. + +This causes an Undefined behaviour, so we should add `#![no_std]` attribute inside generate Rust code. + +However, since it is a corner case, It may be better if we provide `no_std=false` option to avoid this behaviour. + +## `TempalteData` trait + +We must ensure that all of the data passed to templates should satisfy the following restrictions. + +- completely immutable +- does not allocate/deallocate memory +- can be serialized to/deserialized from byte array (All data is serealized to byte array, and then decoded inside templates) +- can be defined inside `#![no_std]` crate + +Sailfish provide `TemplateData` trait which satisfies the above restrictions. + +```rust +pub unsafe trait TemplateData { + fn type_name() -> String; + fn definition() -> String; + fn fields() -> &'static [&'static str]; + fn deserialize() -> String; // rust code to deserialize struct + fn serialize(&self, v: &mut Vec); +} +``` + +This trait can be implemented to the following types + +- String, +- Primitive integers (bool, char, u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, isize, usize) +- [T; N] where T: TemplateData +- (T1, T2, T3, ...) where T1, T2, T3, ... : TemplateData +- Option\ where T: TemplateData +- Vec\ where T: TemplateData + +### `#[derive(TemplateData)]` attribute + +In order to pass the user-defined data, User must implement `TemplateData` manually. However, it is dangerous and should be avoided. + +We must export the `derive(TemplateData)` procedural macro to automatically implement this trait. + +This macro should cause error if any type of the fields does not implement `TemplateData`. + +### How template file is transformed (current idea) + +Template file contents is transformed into Rust code when `sailfish::dynamic::compile()` function is called. + +For example, if we have a template + +```html +

<%= msg %>

+``` + +and Rust code + +```rust +struct Message { + msg: String, +} + +let template = compile::("templates/message.stpl").unwrap(); +``` + +then, template will be transformed into the following code. + +```rust +#![no_std] +use sailfish::dynamic::runtime as sfrt; +use sfrt::{VBuffer, AllocVtable, OutputData, SizeHint, RenderResult}; + +struct Message { + msg: String, +} + +fn deserialize(data: &mut &[u8]) -> Message { + // Generated code from TemplateData::deserialize() + let msg = sfrt::deserialize_string(data); + + Message { msg } +} + +#[no_mangle] +pub extern fn sf_message(version: u64, data: *const [u8], data_len: usize, vtable: AllocVtable) -> OutputData { + let inner = move || -> RenderResult { + let mut data = unsafe { std::slice::from_raw_parts(data, data_len) }; + let Message { msg } = deserialize(&mut data); + + let mut buf = VBuffer::from_vtable(vtable); + + static SIZE_HINT = SizeHint::new(); + let size_hint = SIZE_HINT.get(); + buf.reserve(size_hint); + + { + sfrt::render_text!(buf, "

"); + sfrt::render_escaped!(buf, msg); + sfrt::render_text!(buf, "

"); + } + + SIZE_HINT.update(buf.len()) + Ok(buf.into_string()) + }; + + OutputData::from_result(inner()) +} +``` + +## Example usage + +Template: + +```html + + + + <%= name %>: <%= score %> + + +``` + +Rust code: + +```rust +use sailfish::dynamic::compile; +use sailfish_macros::TemplateData; + +#[derive(TemplateData)] +pub struct Team { + name: String, + score: u8 +} + +// compile the template as a callable shared library +let template: DynamicTemplate = compile::("templates/team.stpl").unwrap(); +let data = Team { + name: "Jiangsu".into(), + score: 43 +}; +// render templates with given data +let result: String = unsafe { template.render(data).unwrap() }; +println!("{}", result); +``` diff --git a/docs/rfcs/3-template-trait.md b/docs/rfcs/3-template-trait.md new file mode 100644 index 0000000..6ff4e52 --- /dev/null +++ b/docs/rfcs/3-template-trait.md @@ -0,0 +1,45 @@ +# Template trait + +## Description + +Currently `TemplateOnce::render_once` method consumes the object itself and not useful if you want to re-use the struct. + +`Template` trait helps those situation. `Template` trait has `render()` method, which does not consume the object itself. + +Like `TemplateOnce`, `Template` trait can be implemented using derive macro. + +## Definition + +```rust +pub trait Template { + fn render(&self) -> RenderResult; +} +``` + +Since `RenderError` can be converted into `fmt::Error`, we can now implement `Display` trait for those structs. + +```rust +impl Display for T { + ... +} +``` + +## Disadvantage + +If you derive this trait, you cannot move out the struct fields. For example, the following template + +```html +<% for msg in messages { %>
<%= msg %>
<% } %> +``` + +will be transformed into the Rust code like + +```rust +for msg in self.messages { + render_text!(_ctx, "
"); + render!(_ctx, msg); + render_text!(_ctx, "
"); +} +``` + +which causes an compilation error because `self.messages` cannot be moved. diff --git a/examples/Cargo.toml b/examples/Cargo.toml new file mode 100644 index 0000000..223c585 --- /dev/null +++ b/examples/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "examples" +version = "0.0.1" +authors = ["Ryohei Machida "] +edition = "2018" +publish = false + +[dependencies] +sailfish = { path = "../sailfish" } +sailfish-macros = { path = "../sailfish-macros" } + +[[bin]] +name = "simple" +path = "simple.rs" +test = false diff --git a/examples/simple.rs b/examples/simple.rs new file mode 100644 index 0000000..640b9ae --- /dev/null +++ b/examples/simple.rs @@ -0,0 +1,15 @@ +#[macro_use] +extern crate sailfish_macros; + +use sailfish::TemplateOnce; + +#[derive(TemplateOnce)] +#[template(path = "simple.stpl")] +struct Simple { + messages: Vec, +} + +fn main() { + let messages = vec![String::from("Message 1"), String::from("")]; + println!("{}", Simple { messages }.render_once().unwrap()); +} diff --git a/examples/templates/simple.stpl b/examples/templates/simple.stpl new file mode 100644 index 0000000..9ec4dd9 --- /dev/null +++ b/examples/templates/simple.stpl @@ -0,0 +1,12 @@ + + + + <%# This is a comment %> + <% for (i, msg) in messages.iter().enumerate() { %> + <% if i == 0 { %> +

Hello, world!

+ <% } %> +
<%= *msg %>
+ <% } %> + + diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml new file mode 100644 index 0000000..75693fc --- /dev/null +++ b/integration-tests/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "integration-tests" +version = "0.0.1" +authors = ["Kogia-sima "] +edition = "2018" +publish = false + +[dependencies] +sailfish = { path = "../sailfish" } +sailfish-macros = { path = "../sailfish-macros" } + +[dev-dependencies] +trybuild = "1.0.28" +pretty_assertions = "0.6.1" diff --git a/integration-tests/src/lib.rs b/integration-tests/src/lib.rs new file mode 100644 index 0000000..7dc2343 --- /dev/null +++ b/integration-tests/src/lib.rs @@ -0,0 +1,21 @@ +use std::fmt; + +#[derive(PartialEq, Eq)] +pub struct PrettyString<'a>(pub &'a str); + +/// Make diff to display string as multi-line string +impl<'a> fmt::Debug for PrettyString<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(self.0) + } +} + +#[macro_export] +macro_rules! assert_string_eq { + ($left:expr, $right:expr) => { + pretty_assertions::assert_eq!( + $crate::PrettyString($left), + $crate::PrettyString($right) + ); + }; +} diff --git a/integration-tests/templates/custom_delimiter.out b/integration-tests/templates/custom_delimiter.out new file mode 100644 index 0000000..f607375 --- /dev/null +++ b/integration-tests/templates/custom_delimiter.out @@ -0,0 +1 @@ +
i: 10
diff --git a/integration-tests/templates/custom_delimiter.stpl b/integration-tests/templates/custom_delimiter.stpl new file mode 100644 index 0000000..a5f83df --- /dev/null +++ b/integration-tests/templates/custom_delimiter.stpl @@ -0,0 +1 @@ +<🍣 let i = 10; 🍣>
i: <🍣= i 🍣>
diff --git a/integration-tests/templates/empty.out b/integration-tests/templates/empty.out new file mode 100644 index 0000000..e69de29 diff --git a/integration-tests/templates/empty.stpl b/integration-tests/templates/empty.stpl new file mode 100644 index 0000000..e69de29 diff --git a/integration-tests/templates/json.out b/integration-tests/templates/json.out new file mode 100644 index 0000000..3f4a244 --- /dev/null +++ b/integration-tests/templates/json.out @@ -0,0 +1,4 @@ +{ + "name": "Taro", + "value": 16 +} diff --git a/integration-tests/templates/json.stpl b/integration-tests/templates/json.stpl new file mode 100644 index 0000000..b7c6f5b --- /dev/null +++ b/integration-tests/templates/json.stpl @@ -0,0 +1,4 @@ +{ + "name": "<%= name %>", + "value": <%= value %> +} diff --git a/integration-tests/templates/noescape.out b/integration-tests/templates/noescape.out new file mode 100644 index 0000000..b7b0a10 --- /dev/null +++ b/integration-tests/templates/noescape.out @@ -0,0 +1 @@ +raw:

Hello, World!

diff --git a/integration-tests/templates/noescape.stpl b/integration-tests/templates/noescape.stpl new file mode 100644 index 0000000..2f6ca3f --- /dev/null +++ b/integration-tests/templates/noescape.stpl @@ -0,0 +1 @@ +raw: <%- raw %> diff --git a/integration-tests/tests/basic.rs b/integration-tests/tests/basic.rs new file mode 100644 index 0000000..983eb3d --- /dev/null +++ b/integration-tests/tests/basic.rs @@ -0,0 +1,75 @@ +#[macro_use] +extern crate sailfish_macros; + +use integration_tests::assert_string_eq; +use sailfish::runtime::RenderResult; +use sailfish::TemplateOnce; +use std::path::PathBuf; + +fn assert_render_result(name: &str, result: RenderResult) { + let mut output_file = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + output_file.push("templates"); + output_file.push(name); + output_file.set_extension("out"); + + let expected = std::fs::read_to_string(output_file).unwrap(); + assert_string_eq!(&*result.unwrap(), &*expected); +} + +#[inline] +fn assert_render(name: &str, template: T) { + assert_render_result(name, template.render_once()); +} + +#[derive(TemplateOnce)] +#[template(path = "empty.stpl")] +struct Empty {} + +#[test] +fn empty() { + assert_render("empty", Empty {}); +} + +#[derive(TemplateOnce)] +#[template(path = "noescape.stpl")] +struct Noescape<'a> { + raw: &'a str, +} + +#[test] +fn noescape() { + assert_render( + "noescape", + Noescape { + raw: "

Hello, World!

", + }, + ); +} + +#[derive(TemplateOnce)] +#[template(path = "json.stpl")] +struct Json { + name: String, + value: u16, +} + +#[test] +fn json() { + assert_render( + "json", + Json { + name: String::from("Taro"), + value: 16, + }, + ); +} + +#[derive(TemplateOnce)] +#[template(path = "custom_delimiter.stpl")] +#[template(delimiter = '🍣')] +struct CustomDelimiter; + +#[test] +fn custom_delimiter() { + assert_render("custom_delimiter", CustomDelimiter); +} diff --git a/resources/logo.png b/resources/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..fa470b3a605cb2df3942f3722ea58ddbad86bd00 GIT binary patch literal 25941 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 literal 0 HcmV?d00001 diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..de982c0 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,6 @@ +max_width = 90 +hard_tabs = false +use_field_init_shorthand = true +edition = "2018" +reorder_imports = true +reorder_modules = true diff --git a/sailfish-compiler/Cargo.toml b/sailfish-compiler/Cargo.toml new file mode 100644 index 0000000..61a1d72 --- /dev/null +++ b/sailfish-compiler/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "sailfish-compiler" +version = "0.0.1" +authors = ["Ryohei Machida "] +description = "Really fast, intuitive template engine for Rust" +homepage = "https://github.com/Kogia-sima/sailfish" +repository = "https://github.com/Kogia-sima/sailfish" +readme = "../README.md" +keywords = ["markup", "template", "html"] +categories = ["template-engine"] +license = "MIT" +workspace = ".." +edition = "2018" + +[lib] +name = "sailfish_compiler" +doctest = false + +[features] +default = [] +procmacro = [] + +[dependencies] +memchr = "2.3.3" +quote = { version = "1.0.6", default-features = false } + +[dependencies.syn] +version = "1.0.21" +default-features = false +features = ["parsing", "full", "visit-mut", "printing", "clone-impls"] + +[dependencies.proc-macro2] +version = "1.0.10" +default-features = false +features = ["span-locations"] + +[dev-dependencies] +pretty_assertions = "0.6.1" diff --git a/sailfish-compiler/build.rs b/sailfish-compiler/build.rs new file mode 100644 index 0000000..f328e4d --- /dev/null +++ b/sailfish-compiler/build.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/sailfish-compiler/src/compiler.rs b/sailfish-compiler/src/compiler.rs new file mode 100644 index 0000000..8862b21 --- /dev/null +++ b/sailfish-compiler/src/compiler.rs @@ -0,0 +1,84 @@ +use quote::ToTokens; +use std::fs; +use std::path::{Path, PathBuf}; + +use crate::error::*; +use crate::optimizer::Optimizer; +use crate::parser::Parser; +use crate::resolver::Resolver; +use crate::translator::Translator; +use crate::util::rustfmt_block; + +pub struct Compiler { + delimiter: char, + escape: bool, + cache_dir: PathBuf, +} + +impl Default for Compiler { + fn default() -> Self { + Self { + delimiter: '%', + escape: true, + cache_dir: Path::new(env!("OUT_DIR")).join("cache"), + } + } +} + +impl Compiler { + pub fn new() -> Self { + Self::default() + } + + pub fn delimiter(mut self, new: char) -> Self { + self.delimiter = new; + self + } + + pub fn escape(mut self, new: bool) -> Self { + self.escape = new; + self + } + + pub fn compile_file(&self, input: &Path, output: &Path) -> Result<(), Error> { + // TODO: introduce cache system + + let parser = Parser::new().delimiter(self.delimiter); + let translator = Translator::new().escape(self.escape); + let resolver = Resolver::new(); + let optimizer = Optimizer::new(); + + let compile_file = |input: &Path, output: &Path| -> Result<(), Error> { + let content = fs::read_to_string(&*input) + .chain_err(|| format!("Failed to open template file: {:?}", input))?; + + let stream = parser.parse(&*content); + let mut tsource = translator.translate(stream)?; + drop(content); + + resolver.resolve(&mut tsource.ast)?; + optimizer.optimize(&mut tsource.ast); + + if let Some(parent) = output.parent() { + fs::create_dir_all(parent)?; + } + if output.exists() { + fs::remove_file(output)?; + } + + let string = tsource.ast.into_token_stream().to_string(); + fs::write(output, rustfmt_block(&*string).unwrap_or(string))?; + Ok(()) + }; + + compile_file(&*input, &*output) + .chain_err(|| "Failed to compile template.") + .map_err(|mut e| { + e.source = fs::read_to_string(&*input).ok(); + e.source_file = Some(input.to_owned()); + e + })?; + + Ok(()) + } +} diff --git a/sailfish-compiler/src/config.rs b/sailfish-compiler/src/config.rs new file mode 100644 index 0000000..e69de29 diff --git a/sailfish-compiler/src/error.rs b/sailfish-compiler/src/error.rs new file mode 100644 index 0000000..b30730a --- /dev/null +++ b/sailfish-compiler/src/error.rs @@ -0,0 +1,256 @@ +use std::fmt; +use std::fs; +use std::io; +use std::path::PathBuf; + +#[non_exhaustive] +#[derive(Debug)] +pub enum ErrorKind { + FmtError(fmt::Error), + IoError(io::Error), + RustSyntaxError(syn::Error), + ParseError(String), + AnalyzeError(String), + Unimplemented(String), + Other(String), +} + +impl fmt::Display for ErrorKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + ErrorKind::FmtError(ref e) => e.fmt(f), + ErrorKind::IoError(ref e) => e.fmt(f), + ErrorKind::RustSyntaxError(ref e) => write!(f, "Rust Syntax Error: {}", e), + ErrorKind::ParseError(ref msg) => write!(f, "Parse error: {}", msg), + ErrorKind::AnalyzeError(ref msg) => write!(f, "Analyzation error: {}", msg), + ErrorKind::Unimplemented(ref msg) => f.write_str(&**msg), + ErrorKind::Other(ref msg) => f.write_str(&**msg), + } + } +} + +macro_rules! impl_errorkind_conversion { + ($source:ty, $kind:ident, $conv:expr, [ $($lifetimes:tt),* ]) => { + impl<$($lifetimes),*> From<$source> for ErrorKind { + #[inline] + fn from(other: $source) -> Self { + ErrorKind::$kind($conv(other)) + } + } + }; + ($source:ty, $kind:ident) => { + impl_errorkind_conversion!($source, $kind, std::convert::identity, []); + } +} + +impl_errorkind_conversion!(fmt::Error, FmtError); +impl_errorkind_conversion!(io::Error, IoError); +impl_errorkind_conversion!(syn::Error, RustSyntaxError); +impl_errorkind_conversion!(String, Other); +impl_errorkind_conversion!(&'a str, Other, |s: &str| s.to_owned(), ['a]); + +#[derive(Debug, Default)] +pub struct Error { + pub(crate) source_file: Option, + pub(crate) source: Option, + pub(crate) offset: Option, + pub(crate) chains: Vec, +} + +impl Error { + pub fn from_kind(kind: ErrorKind) -> Self { + Self { + chains: vec![kind], + ..Self::default() + } + } + + pub fn kind(&self) -> &ErrorKind { + self.chains.last().unwrap() + } + + pub fn iter(&self) -> impl Iterator { + self.chains.iter().rev() + } +} + +impl From for Error +where + ErrorKind: From, +{ + fn from(other: T) -> Self { + Self::from_kind(ErrorKind::from(other)) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let source = match (self.source.as_ref(), self.source_file.as_deref()) { + (Some(s), _) => Some(s.to_owned()), + (None, Some(f)) => fs::read_to_string(f).ok(), + (None, None) => None, + }; + + writeln!(f, "{}", self.chains.last().unwrap())?; + + for e in self.chains.iter().rev().skip(1) { + writeln!(f, "Caused by: {}", e)?; + } + + f.write_str("\n")?; + + if let Some(ref source_file) = self.source_file { + writeln!(f, "file: {}", source_file.display())?; + } + + if let (Some(ref source), Some(offset)) = (source, self.offset) { + let (lineno, colno) = into_line_column(source, offset); + writeln!(f, "position: line {}, column {}\n", lineno, colno)?; + + // TODO: display adjacent lines + let line = source.lines().nth(lineno - 1).unwrap(); + let lpad = count_digits(lineno); + + writeln!(f, "{: { + fn chain_err(self, kind: F) -> Result + where + F: FnOnce() -> EK, + EK: Into; +} + +impl ResultExt for Result { + fn chain_err(self, kind: F) -> Result + where + F: FnOnce() -> EK, + EK: Into, + { + self.map_err(|mut e| { + e.chains.push(kind().into()); + e + }) + } +} + +impl> ResultExt for Result { + fn chain_err(self, kind: F) -> Result + where + F: FnOnce() -> EK, + EK: Into, + { + self.map_err(|e| { + let mut e = Error::from(e.into()); + e.chains.push(kind().into()); + e + }) + } +} + +fn into_line_column(source: &str, offset: usize) -> (usize, usize) { + assert!( + offset <= source.len(), + "Internal error: error position offset overflow (error code: 56066)" + ); + let mut lineno = 1; + let mut colno = 1; + let mut current = 0; + + for line in source.lines() { + let end = current + line.len() + 1; + if offset < end { + colno = offset - current + 1; + break; + } + + lineno += 1; + current = end; + } + + (lineno, colno) +} + +fn count_digits(n: usize) -> usize { + let mut current = 10; + let mut digits = 1; + + while current <= n { + current *= 10; + digits += 1; + } + + digits +} + +macro_rules! make_error { + ($kind:expr) => { + $crate::Error::from_kind($kind) + }; + ($kind:expr, $($remain:tt)*) => {{ + #[allow(unused_mut)] + let mut err = $crate::Error::from_kind($kind); + make_error!(@opt err $($remain)*); + err + }}; + (@opt $var:ident $key:ident = $value:expr, $($remain:tt)*) => { + $var.$key = Some($value.into()); + make_error!(@opt $var $($remain)*); + }; + (@opt $var:ident $key:ident = $value:expr) => { + $var.$key = Some($value.into()); + }; + (@opt $var:ident $key:ident, $($remain:tt)*) => { + $var.$key = Some($key); + make_error!(@opt $var $($remain)*); + }; + (@opt $var:ident $key:ident) => { + $var.$key = Some($key); + }; + (@opt $var:ident) => {}; +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn display_error() { + let mut err = make_error!( + ErrorKind::AnalyzeError("mismatched types".to_owned()), + source_file = PathBuf::from("apple.rs"), + source = "fn func() {\n 1\n}".to_owned(), + offset = 16usize + ); + err.chains.push(ErrorKind::Other("some error".to_owned())); + assert_eq!( + err.to_string(), + r#"some error +Caused by: Analyzation error: mismatched types + +file: apple.rs +position: line 2, column 5 + + | +2 | 1 + | ^ +"# + ); + } +} diff --git a/sailfish-compiler/src/lib.rs b/sailfish-compiler/src/lib.rs new file mode 100644 index 0000000..145136f --- /dev/null +++ b/sailfish-compiler/src/lib.rs @@ -0,0 +1,18 @@ +#![allow(dead_code)] + +#[macro_use] +mod error; + +mod compiler; +mod optimizer; +mod parser; +mod resolver; +mod translator; +mod util; + +pub use compiler::Compiler; +pub use error::{Error, ErrorKind}; + +#[cfg(feature = "procmacro")] +#[doc(hidden)] +pub mod procmacro; diff --git a/sailfish-compiler/src/optimizer.rs b/sailfish-compiler/src/optimizer.rs new file mode 100644 index 0000000..ceadb46 --- /dev/null +++ b/sailfish-compiler/src/optimizer.rs @@ -0,0 +1,101 @@ +use quote::quote; +use syn::parse::{Parse, ParseStream, Result as ParseResult}; +use syn::visit_mut::VisitMut; +use syn::{Block, Expr, ExprMacro, Ident, LitStr, Stmt, Token}; + +struct RenderTextMacroArgument { + context: Ident, + arg: LitStr, +} + +impl Parse for RenderTextMacroArgument { + fn parse(s: ParseStream) -> ParseResult { + let context = s.parse()?; + s.parse::()?; + let arg = s.parse()?; + + Ok(Self { context, arg }) + } +} + +fn get_rendertext_value(i: &ExprMacro) -> Option { + let mut it = i.mac.path.segments.iter(); + + if it.next().map_or(false, |s| s.ident == "sfrt") + && it.next().map_or(false, |s| s.ident == "render_text") + && it.next().is_none() + { + let tokens = i.mac.tokens.clone(); + if let Ok(macro_arg) = syn::parse2::(tokens) { + return Some(macro_arg.arg.value()); + } + } + + None +} + +struct OptmizerImpl {} + +impl VisitMut for OptmizerImpl { + fn visit_expr_mut(&mut self, i: &mut Expr) { + let fl = if let Expr::ForLoop(ref mut fl) = *i { + fl + } else { + syn::visit_mut::visit_expr_mut(self, i); + return; + }; + + syn::visit_mut::visit_block_mut(self, &mut fl.body); + + let (mf, ml) = match (fl.body.stmts.first(), fl.body.stmts.last()) { + ( + Some(Stmt::Semi(Expr::Macro(ref mf), ..)), + Some(Stmt::Semi(Expr::Macro(ref ml), ..)), + ) => (mf, ml), + _ => { + syn::visit_mut::visit_expr_mut(self, i); + return; + } + }; + + let (sf, sl) = match (get_rendertext_value(mf), get_rendertext_value(ml)) { + (Some(sf), Some(sl)) => (sf, sl), + _ => { + syn::visit_mut::visit_expr_mut(self, i); + return; + } + }; + + let sf_len = sf.len(); + let concat = sl + &*sf; + + fl.body.stmts.remove(0); + *fl.body.stmts.last_mut().unwrap() = syn::parse2(quote! { + sfrt::render_text!(_ctx, #concat); + }) + .unwrap(); + + let new_expr = syn::parse2(quote! {{ + sfrt::render_text!(_ctx, #sf); + #fl; + unsafe { _ctx.buf.set_len(_ctx.buf.len() - #sf_len); } + }}) + .unwrap(); + + *i = new_expr; + } +} + +pub struct Optimizer {} + +impl Optimizer { + #[inline] + pub fn new() -> Self { + Self {} + } + + #[inline] + pub fn optimize(&self, i: &mut Block) { + OptmizerImpl {}.visit_block_mut(i); + } +} diff --git a/sailfish-compiler/src/parser.rs b/sailfish-compiler/src/parser.rs new file mode 100644 index 0000000..b4d4059 --- /dev/null +++ b/sailfish-compiler/src/parser.rs @@ -0,0 +1,474 @@ +// TODO: Better error message (unbalanced rust delimiter, etc.) + +use memchr::{memchr, memchr2, memchr3}; +use std::convert::TryInto; +use std::rc::Rc; + +use crate::{Error, ErrorKind}; + +macro_rules! unwrap_or_break { + ($val:expr) => { + match $val { + Some(t) => t, + None => break, + } + }; +} + +#[derive(Clone, Debug)] +pub struct Parser { + delimiter: char, +} + +impl Parser { + pub fn new() -> Self { + Self::default() + } + + /// change delimiter + pub fn delimiter(mut self, new: char) -> Self { + self.delimiter = new; + self + } + + /// parse source string + pub fn parse<'a>(&self, source: &'a str) -> ParseStream<'a> { + let block_delimiter = Rc::new(( + format!("<{}", self.delimiter), + format!("{}>", self.delimiter), + )); + + ParseStream { + block_delimiter, + original_source: source, + source, + delimiter: self.delimiter, + } + } +} + +impl Default for Parser { + fn default() -> Self { + Self { delimiter: '%' } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TokenKind { + BufferedCode { escape: bool }, + Code, + Comment, + Text, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Token<'a> { + content: &'a str, + offset: usize, + kind: TokenKind, +} + +impl<'a> Token<'a> { + #[inline] + pub fn new(content: &'a str, offset: usize, kind: TokenKind) -> Token<'a> { + Token { + content, + offset, + kind, + } + } + + #[inline] + pub fn as_str(&self) -> &'a str { + self.content + } + + #[inline] + pub fn offset(&self) -> usize { + self.offset + } + + #[inline] + pub fn kind(&self) -> TokenKind { + self.kind + } +} + +#[derive(Clone, Debug)] +pub struct ParseStream<'a> { + block_delimiter: Rc<(String, String)>, + pub(crate) original_source: &'a str, + source: &'a str, + delimiter: char, +} + +impl<'a> ParseStream<'a> { + /// Returns an empty `ParseStream` containing no tokens + pub fn new() -> Self { + Self::default() + } + + pub fn is_empty(&self) -> bool { + self.source.is_empty() + } + + pub fn into_vec(self) -> Result>, Error> { + let mut vec = Vec::new(); + for token in self { + vec.push(token?); + } + + Ok(vec) + } + + pub fn to_vec(&self) -> Result>, Error> { + self.clone().into_vec() + } + + fn error(&self, msg: &str) -> Error { + let offset = self.original_source.len() - self.source.len(); + make_error!( + ErrorKind::ParseError(msg.to_owned()), + source = self.original_source.to_owned(), + offset + ) + } + + fn offset(&self) -> usize { + self.original_source.len() - self.source.len() + } + + fn take_n(&mut self, n: usize) -> &'a str { + let (l, r) = self.source.split_at(n); + self.source = r; + l + } + + fn tokenize_code(&mut self) -> Result, Error> { + debug_assert!(self.source.starts_with(&*self.block_delimiter.0)); + + let mut start = self.block_delimiter.0.len(); + let mut token_kind = TokenKind::Code; + + // read flags + match self.source.as_bytes().get(start).copied() { + Some(b'#') => { + token_kind = TokenKind::Comment; + start += 1; + } + Some(b'=') => { + token_kind = TokenKind::BufferedCode { escape: true }; + start += 1; + } + Some(b'-') => { + token_kind = TokenKind::BufferedCode { escape: false }; + start += 1; + } + _ => {} + } + + // skip whitespaces + for ch in self.source.bytes().skip(start) { + match ch { + b' ' | b'\t' | b'\n'..=b'\r' => { + start += 1; + } + _ => break, + } + } + + if token_kind == TokenKind::Comment { + let pos = self.source[start..] + .find(&*self.block_delimiter.1) + .ok_or_else(|| self.error("Unterminated comment block"))?; + + self.take_n(start); + let token = Token { + content: self.source[..pos].trim_end(), + offset: self.offset(), + kind: token_kind, + }; + + self.take_n(pos + self.block_delimiter.1.len()); + return Ok(token); + } + + // find closing bracket + if let Some(pos) = find_block_end(&self.source[start..], &*self.block_delimiter.1) + { + // closing bracket was found + self.take_n(start); + let s = &self.source[..pos - self.block_delimiter.1.len()].trim_end_matches( + |c| matches!(c, ' ' | '\t' | '\r' | '\u{000B}' | '\u{000C}'), + ); + let token = Token { + content: s, + offset: self.offset(), + kind: token_kind, + }; + self.take_n(pos); + Ok(token) + } else { + Err(self.error("Unterminated code block")) + } + } + + fn tokenize_text(&mut self) -> Result, Error> { + // TODO: allow buffer block inside code block + let offset = self.offset(); + let end = self + .source + .find(&*self.block_delimiter.0) + .unwrap_or(self.source.len()); + let token = Token { + content: self.take_n(end), + offset, + kind: TokenKind::Text, + }; + Ok(token) + } +} + +impl<'a> Default for ParseStream<'a> { + fn default() -> Self { + Self { + block_delimiter: Rc::new(("<%".to_owned(), "%>".to_owned())), + original_source: "", + source: "", + delimiter: '%', + } + } +} + +impl<'a> Iterator for ParseStream<'a> { + type Item = Result, Error>; + + fn next(&mut self) -> Option { + if self.source.is_empty() { + return None; + } + + let token = if self.source.starts_with(&*self.block_delimiter.0) { + if !self.source[self.block_delimiter.0.len()..].starts_with(self.delimiter) { + self.tokenize_code() + } else { + debug_assert_eq!( + &self.source[..self.delimiter.len_utf8() * 2 + 1], + format!("<{0}{0}", self.delimiter) + ); + + // Escape '<%%' token + let token = Token { + content: &self.source[..self.block_delimiter.0.len()], + offset: self.offset(), + kind: TokenKind::Text, + }; + self.take_n(self.block_delimiter.0.len() * 2 - 1); + Ok(token) + } + } else { + self.tokenize_text() + }; + + Some(token) + } +} + +impl<'a> TryInto>> for ParseStream<'a> { + type Error = crate::Error; + + fn try_into(self) -> Result>, Error> { + self.into_vec() + } +} + +fn find_block_end(haystack: &str, delimiter: &str) -> Option { + let mut remain = haystack; + + 'outer: while let Some(pos) = + memchr3(b'/', b'\"', delimiter.as_bytes()[0], remain.as_bytes()) + { + let skip_num = match remain.as_bytes()[pos] { + b'/' => match remain.as_bytes().get(pos + 1).copied() { + Some(b'/') => unwrap_or_break!(find_comment_end(&remain[pos..])), + Some(b'*') => unwrap_or_break!(find_block_comment_end(&remain[pos..])), + _ => pos + 1, + }, + b'\"' => { + // check if the literal is a raw string + for (i, byte) in remain[..pos].as_bytes().iter().enumerate().rev() { + match byte { + b'#' => {} + b'r' => { + let skip_num = + unwrap_or_break!(find_raw_string_end(&remain[i..])); + remain = &remain[i + skip_num..]; + continue 'outer; + } + _ => break, + } + } + unwrap_or_break!(find_string_end(&remain[pos..])) + } + _ => { + if remain[pos..].starts_with(delimiter) { + return Some(haystack.len() - remain.len() + pos + delimiter.len()); + } else { + pos + 1 + } + } + }; + + remain = &remain[pos + skip_num..]; + } + + None +} + +fn find_comment_end(haystack: &str) -> Option { + debug_assert!(haystack.starts_with("//")); + memchr(b'\n', haystack.as_bytes()).map(|p| p + 1) +} + +fn find_block_comment_end(haystack: &str) -> Option { + debug_assert!(haystack.starts_with("/*")); + + let mut remain = &haystack[2..]; + let mut depth = 1; + + while let Some(p) = memchr2(b'*', b'/', remain.as_bytes()) { + let c = unsafe { *remain.as_bytes().get_unchecked(p) }; + let next = remain.as_bytes().get(p + 1); + + match (c, next) { + (b'*', Some(b'/')) => { + if depth == 1 { + let offset = haystack.len() - (remain.len() - (p + 2)); + return Some(offset); + } + depth -= 1; + remain = unsafe { remain.get_unchecked(p + 2..) }; + } + (b'/', Some(b'*')) => { + depth += 1; + remain = unsafe { remain.get_unchecked(p + 2..) }; + } + _ => { + remain = unsafe { remain.get_unchecked(p + 1..) }; + } + } + } + + None +} + +fn find_string_end(haystack: &str) -> Option { + debug_assert!(haystack.starts_with('\"')); + let mut bytes = &haystack.as_bytes()[1..]; + + while let Some(p) = memchr2(b'"', b'\\', bytes) { + unsafe { + if *bytes.get_unchecked(p) == b'\"' { + // string terminator found + return Some(haystack.len() - (bytes.len() - p) + 1); + } else if p + 2 < bytes.len() { + // skip escape + bytes = bytes.get_unchecked(p + 2..); + } else { + break; + } + } + } + + None +} + +fn find_raw_string_end(haystack: &str) -> Option { + debug_assert!(haystack.starts_with('r')); + let mut terminator = String::from("\""); + for ch in haystack[1..].bytes() { + match ch { + b'#' => terminator.push('#'), + b'"' => break, + _ => { + // is not a raw string literal + return Some(1); + } + } + } + + dbg!(&terminator); + haystack[terminator.len() + 1..] + .find(&terminator) + .map(|p| p + terminator.len() * 2 + 1) +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn non_ascii_delimiter() { + let src = r##"foo <🍣# This is a comment 🍣> bar <🍣= r"🍣>" 🍣> baz <🍣🍣"##; + let parser = Parser::new().delimiter('🍣'); + let tokens = parser.parse(src).into_vec().unwrap(); + assert_eq!( + &tokens, + &[ + Token { + content: "foo ", + offset: 0, + kind: TokenKind::Text + }, + Token { + content: "This is a comment", + offset: 11, + kind: TokenKind::Comment + }, + Token { + content: " bar ", + offset: 34, + kind: TokenKind::Text + }, + Token { + content: "r\"🍣>\"", + offset: 46, + kind: TokenKind::BufferedCode { escape: true } + }, + Token { + content: " baz ", + offset: 60, + kind: TokenKind::Text + }, + Token { + content: "<🍣", + offset: 65, + kind: TokenKind::Text + }, + ] + ); + } + + #[test] + fn comment_inside_block() { + let src = "<% // %>\n %><%= /* %%>*/ 1 %>"; + let parser = Parser::new(); + let tokens = parser.parse(src).into_vec().unwrap(); + assert_eq!( + &tokens, + &[ + Token { + content: "// %>\n", + offset: 3, + kind: TokenKind::Code + }, + Token { + content: "/* %%>*/ 1", + offset: 16, + kind: TokenKind::BufferedCode { escape: true } + }, + ] + ); + } +} diff --git a/sailfish-compiler/src/procmacro.rs b/sailfish-compiler/src/procmacro.rs new file mode 100644 index 0000000..3e51b9e --- /dev/null +++ b/sailfish-compiler/src/procmacro.rs @@ -0,0 +1,238 @@ +use proc_macro2::{Span, TokenStream}; +use quote::{quote, ToTokens}; +use std::fs; +use std::path::{Path, PathBuf}; +use syn::parse::{Parse, ParseStream, Result as ParseResult}; +use syn::punctuated::Punctuated; +use syn::{Fields, Ident, ItemStruct, Lifetime, LitBool, LitChar, LitStr, Token}; + +use crate::compiler::Compiler; +use crate::error::*; + +enum GenericParamName { + Ident(Ident), + LifeTime(Lifetime), +} + +impl ToTokens for GenericParamName { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + GenericParamName::Ident(ref i) => i.to_tokens(tokens), + GenericParamName::LifeTime(ref l) => l.to_tokens(tokens), + } + } +} + +// arguments for include_template* macros +#[derive(Default)] +struct DeriveTemplateOptions { + path: Option, + delimiter: Option, + escape: Option, + type_: Option, +} + +impl Parse for DeriveTemplateOptions { + fn parse(outer: ParseStream) -> ParseResult { + let s; + syn::parenthesized!(s in outer); + + let mut options = Self::default(); + let mut found_keys = Vec::new(); + + while !s.is_empty() { + let key = s.parse::()?; + s.parse::()?; + + // check if argument is repeated + if found_keys.iter().any(|e| *e == key) { + return Err(syn::Error::new( + key.span(), + format!("Argument `{}` was repeated.", key), + )); + } + + if key == "path" { + options.path = Some(s.parse::()?); + } else if key == "delimiter" { + options.delimiter = Some(s.parse::()?); + } else if key == "escape" { + options.escape = Some(s.parse::()?); + } else if key == "type" { + options.type_ = Some(s.parse::()?); + } else { + return Err(syn::Error::new( + key.span(), + format!("Unknown option: `{}`", key), + )); + } + + found_keys.push(key); + + // consume comma token + if s.is_empty() { + break; + } else { + s.parse::()?; + } + } + + Ok(options) + } +} + +impl DeriveTemplateOptions { + fn merge(&mut self, other: DeriveTemplateOptions) -> Result<(), syn::Error> { + fn merge_single( + lhs: &mut Option, + rhs: Option, + ) -> Result<(), syn::Error> { + if lhs.is_some() { + if let Some(rhs) = rhs { + Err(syn::Error::new_spanned(rhs, "keyword argument repeated.")) + } else { + Ok(()) + } + } else { + *lhs = rhs; + Ok(()) + } + } + + merge_single(&mut self.path, other.path)?; + merge_single(&mut self.delimiter, other.delimiter)?; + merge_single(&mut self.escape, other.escape)?; + merge_single(&mut self.type_, other.type_)?; + Ok(()) + } +} + +struct TemplateStruct { + options: DeriveTemplateOptions, +} + +fn compile( + input_file: &Path, + output_file: &Path, + options: &DeriveTemplateOptions, +) -> Result<(), Error> { + let mut compiler = Compiler::new(); + if let Some(ref delimiter) = options.delimiter { + compiler = compiler.delimiter(delimiter.value()); + } + if let Some(ref escape) = options.escape { + compiler = compiler.escape(escape.value); + } + + compiler.compile_file(input_file, &*output_file) +} + +fn derive_template_impl(tokens: TokenStream) -> Result { + let strct = syn::parse2::(tokens)?; + + let mut all_options = DeriveTemplateOptions::default(); + for attr in strct.attrs { + let opt = syn::parse2::(attr.tokens)?; + all_options.merge(opt)?; + } + + let input_file = match all_options.path { + Some(ref path) => { + let mut input = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect( + "Internal error: environmental variable `CARGO_MANIFEST_DIR` is not set.", + )); + input.push("templates"); + input.push(path.value()); + input + } + None => { + return Err(syn::Error::new( + Span::call_site(), + "`path` option must be specified.", + ) + .into()) + } + }; + + let filename = match input_file.file_name() { + Some(f) => f, + None => { + return Err(syn::Error::new( + Span::call_site(), + format!("Invalid file name: {:?}", input_file), + )) + } + }; + + let out_dir = match std::env::var("SAILFISH_OUTPUT_DIR") { + Ok(dir) => { + let p = PathBuf::from(dir); + fs::create_dir_all(&*p).unwrap(); + p.canonicalize().unwrap() + } + Err(_) => PathBuf::from(env!("OUT_DIR")), + }; + let mut output_file = out_dir.clone(); + output_file.push("templates"); + output_file.push(filename); + + compile(&*input_file, &*output_file, &all_options) + .map_err(|e| syn::Error::new(Span::call_site(), e))?; + + let input_file_string = input_file.to_string_lossy(); + let output_file_string = output_file.to_string_lossy(); + + // Generate tokens + + let name = strct.ident; + + let field_names: Punctuated = match strct.fields { + Fields::Named(fields) => fields + .named + .into_iter() + .map(|f| { + f.ident.expect( + "Internal error: Failed to get field name (error code: 73621)", + ) + }) + .collect(), + Fields::Unit => Punctuated::new(), + _ => { + return Err(syn::Error::new( + Span::call_site(), + "You cannot derive `Template` or `TemplateOnce` for tuple struct", + )); + } + }; + + let (impl_generics, ty_generics, where_clause) = strct.generics.split_for_impl(); + + let tokens = quote! { + impl #impl_generics sailfish::TemplateOnce for #name #ty_generics #where_clause { + fn render_once(self) -> sailfish::runtime::RenderResult { + include_bytes!(#input_file_string); + + use sailfish::runtime as sfrt; + use sfrt::Render as _; + + static SIZE_HINT: sfrt::SizeHint = sfrt::SizeHint::new(); + let _size_hint = SIZE_HINT.get(); + let mut _ctx = sfrt::Context { + buf: sfrt::Buffer::with_capacity(_size_hint) + }; + + let #name { #field_names } = self; + include!(#output_file_string); + + SIZE_HINT.update(_ctx.buf.len()); + _ctx.into_result() + } + } + }; + + Ok(tokens) +} + +pub fn derive_template(tokens: TokenStream) -> TokenStream { + derive_template_impl(tokens).unwrap_or_else(|e| e.to_compile_error()) +} diff --git a/sailfish-compiler/src/resolver.rs b/sailfish-compiler/src/resolver.rs new file mode 100644 index 0000000..b30c4b7 --- /dev/null +++ b/sailfish-compiler/src/resolver.rs @@ -0,0 +1,19 @@ +use syn::Block; + +use crate::error::*; + +#[derive(Clone, Debug, Default)] +pub struct Resolver {} + +impl Resolver { + #[inline] + pub fn new() -> Self { + Self {} + } + + #[inline] + pub fn resolve(&self, _ast: &mut Block) -> Result<(), Error> { + // not implemented yet + Ok(()) + } +} diff --git a/sailfish-compiler/src/translator.rs b/sailfish-compiler/src/translator.rs new file mode 100644 index 0000000..79291fd --- /dev/null +++ b/sailfish-compiler/src/translator.rs @@ -0,0 +1,223 @@ +use proc_macro2::Span; + +use crate::error::*; +use crate::parser::{ParseStream, Token, TokenKind}; + +use syn::Block; + +#[derive(Clone)] +pub struct SourceMapEntry { + pub original: usize, + pub new: usize, + pub length: usize, +} + +#[derive(Default)] +pub struct SourceMap { + entries: Vec, +} + +impl SourceMap { + #[inline] + pub fn entries(&self) -> &[SourceMapEntry] { + &*self.entries + } + + pub fn reverse_mapping(&self, offset: usize) -> Option { + // find entry which satisfies entry.new <= offset < entry.new + entry.length + let idx = self + .entries + .iter() + .position(|entry| offset < entry.new + entry.length && entry.new <= offset)?; + + let entry = &self.entries[idx]; + debug_assert!(entry.new <= offset); + debug_assert!(offset < entry.new + entry.length); + + Some(entry.original + offset - entry.new) + } +} + +pub struct TranslatedSource { + pub ast: Block, + pub source_map: SourceMap, +} + +// translate tokens into Rust code +#[derive(Clone, Debug, Default)] +pub struct Translator { + escape: bool, +} + +impl Translator { + #[inline] + pub fn new() -> Self { + Self { escape: true } + } + + #[inline] + pub fn escape(mut self, new: bool) -> Self { + self.escape = new; + self + } + + pub fn translate<'a>( + &self, + token_iter: ParseStream<'a>, + ) -> Result { + let original_source = token_iter.original_source; + + let mut source = String::with_capacity(original_source.len()); + source.push_str("{\n"); + let mut ps = SourceBuilder { + escape: self.escape, + source, + source_map: SourceMap::default(), + }; + ps.feed_tokens(&*token_iter.into_vec()?); + + Ok(ps.finalize()?) + } +} + +struct SourceBuilder { + escape: bool, + source: String, + source_map: SourceMap, +} + +impl SourceBuilder { + fn write_token<'a>(&mut self, token: &Token<'a>) { + let entry = SourceMapEntry { + original: token.offset(), + new: self.source.len(), + length: token.as_str().len(), + }; + self.source_map.entries.push(entry); + self.source.push_str(token.as_str()); + } + + fn write_code<'a>(&mut self, token: &Token<'a>) { + // TODO: automatically add missing tokens (e.g. ';', '{') + self.write_token(token); + self.source.push_str("\n"); + } + + fn write_text<'a>(&mut self, token: &Token<'a>) { + use std::fmt::Write; + + self.source.push_str("sfrt::render_text!(_ctx, "); + + // write text token with Debug::fmt + write!(self.source, "{:?}", token.as_str()).unwrap(); + + self.source.push_str(");\n"); + } + + fn write_buffered_code<'a>(&mut self, token: &Token<'a>, escape: bool) { + let method = if self.escape && escape { + "render_escaped" + } else { + "render" + }; + + self.source.push_str("sfrt::"); + self.source.push_str(method); + self.source.push_str("!(_ctx, "); + self.write_token(token); + self.source.push_str(");\n"); + } + + pub fn feed_tokens(&mut self, token_iter: &[Token]) { + let mut it = token_iter.iter().peekable(); + while let Some(token) = it.next() { + match token.kind() { + TokenKind::Code => self.write_code(&token), + TokenKind::Comment => {} + TokenKind::BufferedCode { escape } => { + self.write_buffered_code(&token, escape) + } + TokenKind::Text => { + // concatenate repeated text token + let offset = token.offset(); + let mut concatenated = String::new(); + concatenated.push_str(token.as_str()); + + while let Some(next_token) = it.peek() { + match next_token.kind() { + TokenKind::Text => { + concatenated.push_str(next_token.as_str()); + it.next(); + } + TokenKind::Comment => { + it.next(); + } + _ => break, + } + } + + let new_token = Token::new(&*concatenated, offset, TokenKind::Text); + self.write_text(&new_token); + } + } + } + } + + pub fn finalize(mut self) -> Result { + self.source.push_str("\n}"); + match syn::parse_str::(&*self.source) { + Ok(ast) => Ok(TranslatedSource { + ast, + source_map: self.source_map, + }), + Err(synerr) => { + let span = synerr.span(); + let original_offset = into_offset(&*self.source, span) + .and_then(|o| self.source_map.reverse_mapping(o)); + + let mut err = + make_error!(ErrorKind::RustSyntaxError(synerr), source = self.source); + + err.offset = original_offset; + + Err(err) + } + } + } +} + +fn into_offset(source: &str, span: Span) -> Option { + let lc = span.start(); + if lc.line > 0 { + Some( + source + .lines() + .take(lc.line - 1) + .fold(0, |s, e| s + e.len() + 1) + + lc.column, + ) + } else { + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::parser::Parser; + + #[test] + fn translate() { + let src = "<% pub fn sample() { %> <%% <%=//%>\n%><% } %>"; + let lexer = Parser::new(); + let token_iter = lexer.parse(src); + let mut ps = SourceBuilder { + escape: true, + source: String::with_capacity(token_iter.original_source.len()), + source_map: SourceMap::default(), + }; + ps.feed_tokens(&token_iter.clone().to_vec().unwrap()); + eprintln!("{}", ps.source); + Translator::new().translate(token_iter).unwrap(); + } +} diff --git a/sailfish-compiler/src/util.rs b/sailfish-compiler/src/util.rs new file mode 100644 index 0000000..329b66c --- /dev/null +++ b/sailfish-compiler/src/util.rs @@ -0,0 +1,36 @@ +use std::io::{self, Write}; +use std::process::{Command, Stdio}; + +/// Format block expression using `rustfmt` command +pub fn rustfmt_block(source: &str) -> io::Result { + let mut new_source = String::with_capacity(source.len() + 11); + new_source.push_str("fn render()"); + new_source.push_str(source); + + let mut child = Command::new("rustfmt") + .args(&["--emit", "stdout", "--color", "never", "--quiet"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn()?; + + let stdin = child + .stdin + .as_mut() + .ok_or_else(|| io::Error::from(io::ErrorKind::BrokenPipe))?; + stdin.write_all(new_source.as_bytes())?; + + let output = child.wait_with_output()?; + + if output.status.success() { + let mut s = unsafe { String::from_utf8_unchecked(output.stdout) }; + let brace_offset = s.find('{').unwrap(); + s.replace_range(..brace_offset, ""); + Ok(s) + } else { + Err(io::Error::new( + io::ErrorKind::Other, + "rustfmt command failed", + )) + } +} diff --git a/sailfish-macros/Cargo.toml b/sailfish-macros/Cargo.toml new file mode 100644 index 0000000..696ea8c --- /dev/null +++ b/sailfish-macros/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "sailfish-macros" +version = "0.0.1" +authors = ["Ryohei Machida "] +description = "Really fast, intuitive template engine for Rust" +homepage = "https://github.com/Kogia-sima/sailfish" +repository = "https://github.com/Kogia-sima/sailfish" +readme = "../README.md" +keywords = ["markup", "template", "html"] +categories = ["template-engine"] +license = "MIT" +workspace = ".." +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "sailfish_macros" +proc-macro = true +test = false +doctest = false + +[dependencies] +sailfish-compiler = { path = "../sailfish-compiler", version = "0.0.1", features = ["procmacro"] } +proc-macro2 = "1.0.17" diff --git a/sailfish-macros/src/lib.rs b/sailfish-macros/src/lib.rs new file mode 100644 index 0000000..b8625b1 --- /dev/null +++ b/sailfish-macros/src/lib.rs @@ -0,0 +1,18 @@ +extern crate proc_macro; + +use proc_macro::TokenStream; + +#[proc_macro_derive(TemplateOnce, attributes(template))] +pub fn derive_template_once(tokens: TokenStream) -> TokenStream { + let input = proc_macro2::TokenStream::from(tokens); + let output = sailfish_compiler::procmacro::derive_template(input); + TokenStream::from(output) +} + +/// WIP +#[proc_macro_derive(Template, attributes(template))] +pub fn derive_template(tokens: TokenStream) -> TokenStream { + let input = proc_macro2::TokenStream::from(tokens); + let output = sailfish_compiler::procmacro::derive_template(input); + TokenStream::from(output) +} diff --git a/sailfish/Cargo.toml b/sailfish/Cargo.toml new file mode 100644 index 0000000..048f477 --- /dev/null +++ b/sailfish/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "sailfish" +version = "0.0.1" +authors = ["Ryohei Machida "] +description = "Really fast, intuitive template engine for Rust" +homepage = "https://github.com/Kogia-sima/sailfish" +repository = "https://github.com/Kogia-sima/sailfish" +readme = "../README.md" +keywords = ["markup", "template", "html"] +categories = ["template-engine"] +license = "MIT" +workspace = ".." +edition = "2018" + +[dependencies] +itoa = "0.4.5" +ryu = "1.0.4" diff --git a/sailfish/src/lib.rs b/sailfish/src/lib.rs new file mode 100644 index 0000000..9f773aa --- /dev/null +++ b/sailfish/src/lib.rs @@ -0,0 +1,12 @@ +pub mod runtime; + +pub use runtime::{RenderError, RenderResult}; + +pub trait TemplateOnce { + fn render_once(self) -> runtime::RenderResult; +} + +/// WIP +pub trait Template { + fn render(self) -> runtime::RenderResult; +} diff --git a/sailfish/src/runtime/buffer.rs b/sailfish/src/runtime/buffer.rs new file mode 100644 index 0000000..bbeac60 --- /dev/null +++ b/sailfish/src/runtime/buffer.rs @@ -0,0 +1,129 @@ +use std::fmt; +use std::ops::{Add, AddAssign}; + +#[derive(Clone, Debug)] +pub struct Buffer { + inner: String, +} + +impl Buffer { + #[inline] + pub const fn new() -> Buffer { + Self { + inner: String::new(), + } + } + + #[inline] + pub fn with_capacity(n: usize) -> Buffer { + Self { + inner: String::with_capacity(n), + } + } + + #[inline] + pub fn as_str(&self) -> &str { + &*self.inner + } + + #[inline] + pub fn len(&self) -> usize { + self.inner.len() + } + + #[inline] + pub fn capacity(&self) -> usize { + self.inner.capacity() + } + + #[inline] + pub unsafe fn set_len(&mut self, new: usize) { + self.inner.as_mut_vec().set_len(new); + } + + #[inline] + pub fn reserve(&mut self, n: usize) { + if n > self.inner.capacity() - self.inner.len() { + self.inner.reserve(n); + } + } + + #[inline] + pub fn clear(&mut self) { + // unsafe { self.inner.set_len(0) }; + self.inner.clear(); + } + + #[inline] + pub fn into_string(self) -> String { + self.inner + } + + #[inline] + pub fn write_str(&mut self, data: &str) { + let inner_len = self.inner.len(); + let size = data.len(); + if size > self.inner.capacity() - self.inner.len() { + self.inner.reserve(size); + } + unsafe { + let p = self.inner.as_mut_ptr().add(self.inner.len()); + std::ptr::copy_nonoverlapping(data.as_ptr(), p, size); + self.inner.as_mut_vec().set_len(inner_len + size); + } + } + + #[inline] + pub fn write_char(&mut self, data: char) { + // TODO: do not use standard library + self.inner.push(data); + } +} + +impl fmt::Write for Buffer { + #[inline] + fn write_str(&mut self, s: &str) -> fmt::Result { + Buffer::write_str(self, s); + Ok(()) + } +} + +impl From for Buffer { + #[inline] + fn from(other: String) -> Buffer { + Buffer { inner: other } + } +} + +impl From<&str> for Buffer { + #[inline] + fn from(other: &str) -> Buffer { + Buffer { + inner: other.to_owned(), + } + } +} + +impl Add<&str> for Buffer { + type Output = Buffer; + + #[inline] + fn add(mut self, other: &str) -> Buffer { + self.write_str(other); + self + } +} + +impl AddAssign<&str> for Buffer { + #[inline] + fn add_assign(&mut self, other: &str) { + self.write_str(other) + } +} + +impl Default for Buffer { + #[inline] + fn default() -> Buffer { + Buffer::new() + } +} diff --git a/sailfish/src/runtime/escape/avx2.rs b/sailfish/src/runtime/escape/avx2.rs new file mode 100644 index 0000000..b714dc8 --- /dev/null +++ b/sailfish/src/runtime/escape/avx2.rs @@ -0,0 +1,90 @@ +#[cfg(target_arch = "x86")] +use std::arch::x86::*; +#[cfg(target_arch = "x86_64")] +use std::arch::x86_64::*; +use std::slice; + +use super::{naive, sse2}; +use super::{ESCAPED, ESCAPED_LEN, ESCAPE_LUT}; + +const VECTOR_BYTES: usize = std::mem::size_of::<__m256i>(); +const VECTOR_ALIGN: usize = VECTOR_BYTES - 1; + +#[target_feature(enable = "avx2")] +pub unsafe fn escape(writer: &mut F, bytes: &[u8]) { + let len = bytes.len(); + let mut start_ptr = bytes.as_ptr(); + let end_ptr = start_ptr.add(len); + + if len < VECTOR_BYTES { + if len < 16 { + naive::escape(writer, start_ptr, start_ptr, end_ptr); + } else { + sse2::escape(writer, bytes); + } + return; + } + + let v_independent1 = _mm256_set1_epi8(4); + let v_independent2 = _mm256_set1_epi8(2); + let v_key1 = _mm256_set1_epi8(0x26); + let v_key2 = _mm256_set1_epi8(0x3e); + + let maskgen = |x: __m256i| -> i32 { + _mm256_movemask_epi8(_mm256_or_si256( + _mm256_cmpeq_epi8(_mm256_or_si256(x, v_independent1), v_key1), + _mm256_cmpeq_epi8(_mm256_or_si256(x, v_independent2), v_key2), + )) + }; + + let mut ptr = start_ptr; + let aligned_ptr = ptr.add(VECTOR_BYTES - (start_ptr as usize & VECTOR_ALIGN)); + + { + let mut mask = maskgen(_mm256_loadu_si256(ptr as *const __m256i)); + loop { + let trailing_zeros = mask.trailing_zeros() as usize; + let ptr2 = ptr.add(trailing_zeros); + if ptr2 >= aligned_ptr { + break; + } + + let c = ESCAPE_LUT[*ptr2 as usize] as usize; + debug_assert!(c < ESCAPED_LEN); + if start_ptr < ptr2 { + let slc = + slice::from_raw_parts(start_ptr, ptr2 as usize - start_ptr as usize); + writer(std::str::from_utf8_unchecked(slc)); + } + writer(*ESCAPED.get_unchecked(c)); + start_ptr = ptr2.add(1); + mask ^= 1 << trailing_zeros; + } + } + + ptr = aligned_ptr; + let mut next_ptr = ptr.add(VECTOR_BYTES); + + while next_ptr <= end_ptr { + let mut mask = maskgen(_mm256_load_si256(ptr as *const __m256i)); + while mask != 0 { + let trailing_zeros = mask.trailing_zeros() as usize; + let ptr2 = ptr.add(trailing_zeros); + let c = ESCAPE_LUT[*ptr2 as usize] as usize; + debug_assert!(c < ESCAPED_LEN); + if start_ptr < ptr2 { + let slc = + slice::from_raw_parts(start_ptr, ptr2 as usize - start_ptr as usize); + writer(std::str::from_utf8_unchecked(slc)); + } + writer(*ESCAPED.get_unchecked(c)); + start_ptr = ptr2.add(1); + mask ^= 1 << trailing_zeros; + } + + ptr = next_ptr; + next_ptr = next_ptr.add(VECTOR_BYTES); + } + + sse2::escape_aligned(writer, start_ptr, ptr, end_ptr); +} diff --git a/sailfish/src/runtime/escape/fallback.rs b/sailfish/src/runtime/escape/fallback.rs new file mode 100644 index 0000000..9e75372 --- /dev/null +++ b/sailfish/src/runtime/escape/fallback.rs @@ -0,0 +1,79 @@ +use super::naive; + +#[cfg(target_pointer_width = "16")] +const USIZE_BYTES: usize = 2; + +#[cfg(target_pointer_width = "32")] +const USIZE_BYTES: usize = 4; + +#[cfg(target_pointer_width = "64")] +const USIZE_BYTES: usize = 8; + +const USIZE_ALIGN: usize = USIZE_BYTES - 1; + +#[inline(always)] +fn contains_zero_byte(x: usize) -> bool { + const LO_U64: u64 = 0x0101010101010101; + const HI_U64: u64 = 0x8080808080808080; + const LO_USIZE: usize = LO_U64 as usize; + const HI_USIZE: usize = HI_U64 as usize; + + x.wrapping_sub(LO_USIZE) & !x & HI_USIZE != 0 +} + +#[inline] +fn contains_key(x: usize) -> bool { + const INDEPENDENTS1: usize = 0x0404040404040404_u64 as usize; + const INDEPENDENTS2: usize = 0x0202020202020202_u64 as usize; + const KEY1: usize = 0x2626262626262626_u64 as usize; + const KEY2: usize = 0x3e3e3e3e3e3e3e3e_u64 as usize; + + let y1 = x | INDEPENDENTS1; + let y2 = x | INDEPENDENTS2; + let z1 = y1.wrapping_sub(KEY1); + let z2 = y2.wrapping_sub(KEY2); + contains_zero_byte(z1) || contains_zero_byte(z2) +} + +pub unsafe fn escape(writer: &mut F, bytes: &[u8]) { + let len = bytes.len(); + let mut start_ptr = bytes.as_ptr(); + let end_ptr = start_ptr.add(len); + + if bytes.len() < USIZE_BYTES { + naive::escape(writer, start_ptr, start_ptr, end_ptr); + return; + } + + let ptr = start_ptr; + let aligned_ptr = ptr.add(USIZE_BYTES - (start_ptr as usize & USIZE_ALIGN)); + debug_assert_eq!(aligned_ptr as usize % USIZE_BYTES, 0); + debug_assert!(aligned_ptr <= end_ptr); + + let chunk = (ptr as *const usize).read_unaligned(); + if contains_key(chunk) { + start_ptr = naive::proceed(writer, start_ptr, ptr, aligned_ptr); + } + + escape_aligned(writer, start_ptr, aligned_ptr, end_ptr); +} + +pub unsafe fn escape_aligned( + writer: &mut F, + mut start_ptr: *const u8, + mut ptr: *const u8, + end_ptr: *const u8, +) { + while ptr.add(USIZE_BYTES) <= end_ptr { + debug_assert_eq!((ptr as usize) % USIZE_BYTES, 0); + + let chunk = *(ptr as *const usize); + if contains_key(chunk) { + start_ptr = naive::proceed(writer, start_ptr, ptr, ptr.add(USIZE_BYTES)) + } + ptr = ptr.add(USIZE_BYTES); + } + debug_assert!(ptr <= end_ptr); + debug_assert!(start_ptr <= ptr); + naive::escape(writer, start_ptr, ptr, end_ptr); +} diff --git a/sailfish/src/runtime/escape/mod.rs b/sailfish/src/runtime/escape/mod.rs new file mode 100644 index 0000000..016835a --- /dev/null +++ b/sailfish/src/runtime/escape/mod.rs @@ -0,0 +1,147 @@ +mod avx2; +mod fallback; +mod naive; +mod sse2; + +use std::ptr; + +use super::buffer::Buffer; + +static ESCAPE_LUT: [u8; 256] = [ + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 0, 9, 9, 9, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 2, 9, 3, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, +]; + +const ESCAPED: [&'static str; 4] = [""", "&", "<", ">"]; +const ESCAPED_LEN: usize = 4; + +#[inline] +pub fn escape_with(mut writer: F, feed: &str) { + unsafe { + #[cfg(target_feature = "avx2")] + { + avx2::escape(&mut writer, feed.as_bytes()); + } + + #[cfg(not(target_feature = "avx2"))] + { + if is_x86_feature_detected!("avx2") { + avx2::escape(&mut writer, feed.as_bytes()); + } else if is_x86_feature_detected!("sse2") { + sse2::escape(&mut writer, feed.as_bytes()); + } else { + fallback::escape(&mut writer, feed.as_bytes()); + } + } + } +} + +#[doc(hidden)] +pub fn escape_to_buf(feed: &str, buf: &mut Buffer) { + escape_with(|e| buf.write_str(e), feed); +} + +#[inline] +pub fn escape_to_string(feed: &str, s: &mut String) { + unsafe { + let mut buf = Buffer::from(ptr::read(s)); + escape_to_buf(feed, &mut buf); + ptr::write(s, buf.into_string()); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn escape(feed: &str) -> String { + let mut buf = Buffer::new(); + escape_to_buf(feed, &mut buf); + buf.into_string() + } + + #[test] + fn noescape() { + assert_eq!(escape(""), ""); + assert_eq!( + escape("abcdefghijklmnopqrstrvwxyz"), + "abcdefghijklmnopqrstrvwxyz" + ); + assert_eq!(escape("!#$%()*+,-.:;=?_^"), "!#$%()*+,-.:;=?_^"); + assert_eq!( + escape("漢字はエスケープしないはずだよ"), + "漢字はエスケープしないはずだよ" + ); + } + + #[test] + fn escape_short() { + assert_eq!(escape("<"), "<"); + assert_eq!(escape("\"&<>"), ""&<>"); + assert_eq!( + escape("{\"title\": \"This is a JSON!\"}"), + "{"title": "This is a JSON!"}" + ); + assert_eq!( + escape("

Hello, world

"), + "<html><body><h1>Hello, world</h1>\ + </body></html>" + ); + } + + #[test] + #[rustfmt::skip] + fn escape_long() { + assert_eq!( + escape(r###"m{jml&,?6>\2~08g)\=3`,_`$1@0{i5j}.}2ki\^t}k"@p4$~?;!;pn_l8v."ki`%/&^=\[y+qcerr`@3*|?du.\0vd#40.>bcpf\u@m|c<2t7`hk)^?"0u{v%9}4y2hhv?%-f`<;rzwx`7}l(j2b:c\<|z&$x{+k;f`0+w3e0\m.wmdli>94e2hp\$}j0&m(*h$/lwlj#}99r;o.kj@1#}~v+;y~b[~m.eci}&l7fxt`\\{~#k*9z/d{}(.^j}[(,]:<\h]9k2+0*w60/|23~5;/!-h&ci*~e1h~+:1lhh\>y_*>:-\zzv+8uo],,a^k3_,uip]-/.-~\t51a*<{6!<(_|<#o6=\h1*`[2x_?#-/])x};};r@wqx|;/w&jrv~?\`t:^/dug3(g(ener?!t$}h4:57ptnm@71e=t>@o*"$]799r=+)t>co?rvgk%u0c@.9os;#t_*/gqve/t;o<*`~?3.jyx+h)+^cn^j4td|>)~rs)vm#]:"&\fi;54%+z~fhe|w~\q|ui={54[b9tg*?@]g+q!mq]3jg2?eoo"chyat3k#7pq1u=.l]c14twa4tg#5k_""###), + r###"m{jml&,?6>\2~08g)\=3`,_`$1@0{i5j}.}2ki\^t}k"@p4$~?;!;pn_l8v."ki`%/&^=\[y+qcerr`@3*|?du.\0vd#40.>bcpf\u@m|c<2t7`hk)^?"0u{v%9}4y2hhv?%-f`<;rzwx`7}l(j2b:c\<|z&$x{+k;f`0+w3e0\m.wmdli>94e2hp\$}j0&m(*h$/lwlj#}99r;o.kj@1#}~v+;y~b[~m.eci}&l7fxt`\\{~#k*9z/d{}(.^j}[(,]:<\h]9k2+0*w60/|23~5;/!-h&ci*~e1h~+:1lhh\>y_*>:-\zzv+8uo],,a^k3_,uip]-/.-~\t51a*<{6!<(_|<#o6=\h1*`[2x_?#-/])x};};r@wqx|;/w&jrv~?\`t:^/dug3(g(ener?!t$}h4:57ptnm@71e=t>@o*"$]799r=+)t>co?rvgk%u0c@.9os;#t_*/gqv<za&~r^]"{t4by2t`<q4bfo^&!so5/~(nxk:7l\;#0w41u~w3i$g|>e/t;o<*`~?3.jyx+h)+^cn^j4td|>)~rs)vm#]:"&\fi;54%+z~fhe|w~\q|ui={54[b9tg*?@]g+q!mq]3jg2?eoo"chyat3k#7pq1u=.l]c14twa4tg#5k_""### + ); + } + + #[test] + fn random() { + const ASCII_CHARS: &'static [u8] = br##"abcdefghijklmnopqrstuvwxyz0123456789-^\@[;:],./\!"#$%&'()~=~|`{+*}<>?_"##; + let mut state = 88172645463325252u64; + let mut data = Vec::with_capacity(100); + let mut buf1 = Buffer::new(); + let mut buf2 = Buffer::new(); + + for len in 0..100 { + data.clear(); + for _ in 0..len { + // xorshift + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + + let idx = state as usize % ASCII_CHARS.len(); + data.push(ASCII_CHARS[idx]); + } + + let s = unsafe { std::str::from_utf8_unchecked(&*data) }; + + buf1.clear(); + buf2.clear(); + + unsafe { + escape_to_buf(&*s, &mut buf1); + naive::escape( + &mut |s| buf2.write_str(s), + s.as_ptr(), + s.as_ptr(), + s.as_ptr().add(s.len()), + ); + } + + assert_eq!(buf1.as_str(), buf2.as_str()); + } + } +} diff --git a/sailfish/src/runtime/escape/naive.rs b/sailfish/src/runtime/escape/naive.rs new file mode 100644 index 0000000..a2b513f --- /dev/null +++ b/sailfish/src/runtime/escape/naive.rs @@ -0,0 +1,46 @@ +use core::slice; + +use super::{ESCAPED, ESCAPED_LEN, ESCAPE_LUT}; + +#[inline] +pub(super) unsafe fn escape( + writer: &mut F, + mut start_ptr: *const u8, + ptr: *const u8, + end_ptr: *const u8, +) { + start_ptr = proceed(writer, start_ptr, ptr, end_ptr); + + if end_ptr > start_ptr { + let slc = slice::from_raw_parts(start_ptr, end_ptr as usize - start_ptr as usize); + writer(std::str::from_utf8_unchecked(slc)); + } +} + +#[inline] +pub(super) unsafe fn proceed( + writer: &mut F, + mut start_ptr: *const u8, + mut ptr: *const u8, + end_ptr: *const u8, +) -> *const u8 { + while ptr < end_ptr { + debug_assert!(start_ptr <= ptr); + let idx = ESCAPE_LUT[*ptr as usize] as usize; + debug_assert!(idx <= 9); + if idx < ESCAPED_LEN { + if ptr > start_ptr { + let slc = + slice::from_raw_parts(start_ptr, ptr as usize - start_ptr as usize); + writer(std::str::from_utf8_unchecked(slc)); + } + writer(*ESCAPED.get_unchecked(idx)); + start_ptr = ptr.add(1); + } + ptr = ptr.add(1); + } + + debug_assert_eq!(ptr, end_ptr); + debug_assert!(start_ptr <= ptr); + return start_ptr; +} diff --git a/sailfish/src/runtime/escape/sse2.rs b/sailfish/src/runtime/escape/sse2.rs new file mode 100644 index 0000000..681fe7f --- /dev/null +++ b/sailfish/src/runtime/escape/sse2.rs @@ -0,0 +1,132 @@ +#[cfg(target_arch = "x86")] +use std::arch::x86::*; +#[cfg(target_arch = "x86_64")] +use std::arch::x86_64::*; +use std::slice; + +use super::naive; +use super::{ESCAPED, ESCAPED_LEN, ESCAPE_LUT}; + +const VECTOR_BYTES: usize = std::mem::size_of::<__m128i>(); +const VECTOR_ALIGN: usize = VECTOR_BYTES - 1; + +#[target_feature(enable = "sse2")] +#[inline] +pub unsafe fn escape(writer: &mut F, bytes: &[u8]) { + let len = bytes.len(); + let mut start_ptr = bytes.as_ptr(); + let end_ptr = start_ptr.add(len); + + if bytes.len() < VECTOR_BYTES { + naive::escape(writer, start_ptr, start_ptr, end_ptr); + return; + } + + let v_independent1 = _mm_set1_epi8(4); + let v_independent2 = _mm_set1_epi8(2); + let v_key1 = _mm_set1_epi8(0x26); + let v_key2 = _mm_set1_epi8(0x3e); + + let maskgen = |x: __m128i| -> i32 { + _mm_movemask_epi8(_mm_or_si128( + _mm_cmpeq_epi8(_mm_or_si128(x, v_independent1), v_key1), + _mm_cmpeq_epi8(_mm_or_si128(x, v_independent2), v_key2), + )) + }; + + let mut ptr = start_ptr; + let aligned_ptr = ptr.add(VECTOR_BYTES - (start_ptr as usize & VECTOR_ALIGN)); + + { + let mut mask = maskgen(_mm_loadu_si128(ptr as *const __m128i)); + loop { + let trailing_zeros = mask.trailing_zeros() as usize; + let ptr2 = ptr.add(trailing_zeros); + if ptr2 >= aligned_ptr { + break; + } + + let c = ESCAPE_LUT[*ptr2 as usize] as usize; + debug_assert!(c < ESCAPED_LEN); + if start_ptr < ptr2 { + let slc = + slice::from_raw_parts(start_ptr, ptr2 as usize - start_ptr as usize); + writer(std::str::from_utf8_unchecked(slc)); + } + writer(*ESCAPED.get_unchecked(c)); + start_ptr = ptr2.add(1); + mask ^= 1 << trailing_zeros; + } + } + + ptr = aligned_ptr; + escape_aligned(writer, start_ptr, ptr, end_ptr); +} + +pub unsafe fn escape_aligned( + writer: &mut F, + mut start_ptr: *const u8, + mut ptr: *const u8, + end_ptr: *const u8, +) { + let mut next_ptr = ptr.add(VECTOR_BYTES); + let v_independent1 = _mm_set1_epi8(4); + let v_independent2 = _mm_set1_epi8(2); + let v_key1 = _mm_set1_epi8(0x26); + let v_key2 = _mm_set1_epi8(0x3e); + + let maskgen = |x: __m128i| -> i32 { + _mm_movemask_epi8(_mm_or_si128( + _mm_cmpeq_epi8(_mm_or_si128(x, v_independent1), v_key1), + _mm_cmpeq_epi8(_mm_or_si128(x, v_independent2), v_key2), + )) + }; + + while next_ptr <= end_ptr { + debug_assert_eq!((ptr as usize) % VECTOR_BYTES, 0); + let mut mask = maskgen(_mm_load_si128(ptr as *const __m128i)); + while mask != 0 { + let trailing_zeros = mask.trailing_zeros() as usize; + let ptr2 = ptr.add(trailing_zeros); + let c = ESCAPE_LUT[*ptr2 as usize] as usize; + debug_assert!(c < ESCAPED_LEN); + if start_ptr < ptr2 { + let slc = + slice::from_raw_parts(start_ptr, ptr2 as usize - start_ptr as usize); + writer(std::str::from_utf8_unchecked(slc)); + } + writer(*ESCAPED.get_unchecked(c)); + start_ptr = ptr2.add(1); + mask ^= 1 << trailing_zeros; + } + + ptr = next_ptr; + next_ptr = next_ptr.add(VECTOR_BYTES); + } + + next_ptr = ptr.add(8); + if next_ptr <= end_ptr { + debug_assert_eq!((ptr as usize) % VECTOR_BYTES, 0); + let mut mask = maskgen(_mm_loadl_epi64(ptr as *const __m128i)); + while mask != 0 { + let trailing_zeros = mask.trailing_zeros() as usize; + let ptr2 = ptr.add(trailing_zeros); + let c = ESCAPE_LUT[*ptr2 as usize] as usize; + debug_assert!(c < ESCAPED_LEN); + if start_ptr < ptr2 { + let slc = + slice::from_raw_parts(start_ptr, ptr2 as usize - start_ptr as usize); + writer(std::str::from_utf8_unchecked(slc)); + } + writer(*ESCAPED.get_unchecked(c)); + start_ptr = ptr2.add(1); + mask ^= 1 << trailing_zeros; + } + + ptr = next_ptr; + } + + debug_assert!(ptr <= end_ptr); + debug_assert!(start_ptr <= ptr); + naive::escape(writer, start_ptr, ptr, end_ptr); +} diff --git a/sailfish/src/runtime/macros.rs b/sailfish/src/runtime/macros.rs new file mode 100644 index 0000000..9968585 --- /dev/null +++ b/sailfish/src/runtime/macros.rs @@ -0,0 +1,29 @@ +#[macro_export] +#[doc(hidden)] +macro_rules! render { + ($ctx:ident, $value:expr) => { + (&($value)).render(&mut $ctx.buf)? + }; +} + +#[macro_export] +#[doc(hidden)] +macro_rules! render_escaped { + ($ctx:ident, $value:expr) => { + (&($value)).render_escaped(&mut $ctx.buf)? + }; +} + +#[macro_export] +#[doc(hidden)] +macro_rules! render_text { + ($ctx:ident, $value:expr) => { + $ctx.buf.write_str($value) + }; +} + +#[macro_export] +#[doc(hidden)] +macro_rules! render_noop { + ($ctx:ident, $value:expr) => {}; +} diff --git a/sailfish/src/runtime/mod.rs b/sailfish/src/runtime/mod.rs new file mode 100644 index 0000000..ceaede0 --- /dev/null +++ b/sailfish/src/runtime/mod.rs @@ -0,0 +1,54 @@ +mod buffer; +pub mod escape; +mod macros; +mod render; +mod size_hint; + +pub use buffer::*; +pub use render::*; +pub use size_hint::*; + +use std::fmt; + +#[doc(hidden)] +pub use crate::{render, render_escaped, render_noop, render_text}; + +/// The error type which is returned from template function +#[derive(Clone, Debug)] +pub struct RenderError { + // currently RenderError simply wraps the fmt::Error + inner: fmt::Error, +} + +impl fmt::Display for RenderError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.inner.fmt(f) + } +} + +impl std::error::Error for RenderError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.inner) + } +} + +impl From for RenderError { + #[inline] + fn from(other: fmt::Error) -> Self { + Self { inner: other } + } +} + +pub type RenderResult = Result; + +pub struct Context { + #[doc(hidden)] + pub buf: Buffer, +} + +impl Context { + #[inline] + pub fn into_result(self) -> RenderResult { + Ok(self.buf.into_string()) + } +} diff --git a/sailfish/src/runtime/render.rs b/sailfish/src/runtime/render.rs new file mode 100644 index 0000000..b30ff47 --- /dev/null +++ b/sailfish/src/runtime/render.rs @@ -0,0 +1,207 @@ +use std::fmt::{self, Display}; +use std::path::{Path, PathBuf}; + +use super::buffer::Buffer; +use super::escape; + +pub trait Render { + fn render(&self, b: &mut Buffer) -> fmt::Result; + + #[inline] + fn render_escaped(&self, b: &mut Buffer) -> fmt::Result { + let mut tmp = Buffer::new(); + self.render(&mut tmp)?; + b.write_str(tmp.as_str()); + Ok(()) + } +} + +/// Autoref-based stable specialization +/// +/// Explanation can be found [here](https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md) +impl Render for &T { + fn render(&self, b: &mut Buffer) -> fmt::Result { + fmt::write(b, format_args!("{}", self)) + } + + fn render_escaped(&self, b: &mut Buffer) -> fmt::Result { + struct Wrapper<'a>(&'a mut Buffer); + + impl<'a> fmt::Write for Wrapper<'a> { + #[inline] + fn write_str(&mut self, s: &str) -> fmt::Result { + escape::escape_to_buf(s, self.0); + Ok(()) + } + } + + fmt::write(&mut Wrapper(b), format_args!("{}", self)) + } +} + +impl Render for str { + #[inline] + fn render(&self, b: &mut Buffer) -> fmt::Result { + b.write_str(self); + Ok(()) + } + + #[inline] + fn render_escaped(&self, b: &mut Buffer) -> fmt::Result { + escape::escape_to_buf(self, b); + Ok(()) + } +} + +impl<'a> Render for &'a str { + #[inline] + fn render(&self, b: &mut Buffer) -> fmt::Result { + b.write_str(self); + Ok(()) + } + + #[inline] + fn render_escaped(&self, b: &mut Buffer) -> fmt::Result { + // escape string + escape::escape_to_buf(self, b); + Ok(()) + } +} + +impl Render for String { + #[inline] + fn render(&self, b: &mut Buffer) -> fmt::Result { + b.write_str(self); + Ok(()) + } + + #[inline] + fn render_escaped(&self, b: &mut Buffer) -> fmt::Result { + // escape string + escape::escape_to_buf(self, b); + Ok(()) + } +} + +impl Render for char { + #[inline] + fn render(&self, b: &mut Buffer) -> fmt::Result { + b.write_char(*self); + Ok(()) + } + + #[inline] + fn render_escaped(&self, b: &mut Buffer) -> fmt::Result { + match *self { + '\"' => b.write_str("""), + '&' => b.write_str("&"), + '<' => b.write_str("<"), + '>' => b.write_str(">"), + _ => b.write_char(*self), + } + Ok(()) + } +} + +impl<'a> Render for &'a Path { + #[inline] + fn render(&self, b: &mut Buffer) -> fmt::Result { + // TODO: speed up on Windows using OsStrExt + b.write_str(&*self.to_string_lossy()); + Ok(()) + } + + #[inline] + fn render_escaped(&self, b: &mut Buffer) -> fmt::Result { + escape::escape_to_buf(&*self.to_string_lossy(), b); + Ok(()) + } +} + +impl Render for PathBuf { + #[inline] + fn render(&self, b: &mut Buffer) -> fmt::Result { + b.write_str(&*self.to_string_lossy()); + Ok(()) + } + + #[inline] + fn render_escaped(&self, b: &mut Buffer) -> fmt::Result { + // escape string + escape::escape_to_buf(&*self.to_string_lossy(), b); + + Ok(()) + } +} + +// impl Render for [u8] { +// #[inline] +// fn render(&self, b: &mut Buffer) -> fmt::Result { +// b.write_bytes(self); +// Ok(()) +// } +// } +// +// impl<'a> Render for &'a [u8] { +// #[inline] +// fn render(&self, b: &mut Buffer) -> fmt::Result { +// b.write_bytes(self); +// Ok(()) +// } +// } +// +// impl Render for Vec { +// #[inline] +// fn render(&self, b: &mut Buffer) -> fmt::Result { +// b.write_bytes(&**self); +// Ok(()) +// } +// } + +macro_rules! render_int { + ($($int:ty),*) => { + $( + impl Render for $int { + #[inline] + fn render(&self, b: &mut Buffer) -> fmt::Result { + let mut buffer = itoa::Buffer::new(); + let s = buffer.format(*self); + b.write_str(s); + Ok(()) + } + + #[inline] + fn render_escaped(&self, b: &mut Buffer) -> fmt::Result { + // write_str without escape + self.render(b) + } + } + )* + } +} + +render_int!(u8, u16, u32, u64, i8, i16, i32, i64, usize, isize); + +macro_rules! render_float { + ($($float:ty),*) => { + $( + impl Render for $float { + #[inline] + fn render(&self, b: &mut Buffer) -> fmt::Result { + let mut buffer = ryu::Buffer::new(); + let s = buffer.format(*self); + b.write_str(s); + Ok(()) + } + + #[inline] + fn render_escaped(&self, b: &mut Buffer) -> fmt::Result { + // escape string + self.render(b) + } + } + )* + } +} + +render_float!(f32, f64); diff --git a/sailfish/src/runtime/size_hint.rs b/sailfish/src/runtime/size_hint.rs new file mode 100644 index 0000000..d9e45d3 --- /dev/null +++ b/sailfish/src/runtime/size_hint.rs @@ -0,0 +1,32 @@ +use std::sync::atomic::{AtomicUsize, Ordering}; + +#[derive(Debug, Default)] +pub struct SizeHint { + value: AtomicUsize, +} + +impl SizeHint { + pub const fn new() -> SizeHint { + SizeHint { + value: AtomicUsize::new(0), + } + } + + /// Get the current value + #[inline] + pub fn get(&self) -> usize { + self.value.load(Ordering::Acquire) + } + + /// Update size hint based on given value. + /// + /// There is no guarantee that the value of get() after calling update() is same + /// as the value passed on update() + #[inline] + pub fn update(&self, mut value: usize) { + value = value + value / 8; + if self.get() < value { + self.value.store(value, Ordering::Release); + } + } +} diff --git a/syntax/vim/ftdetect/sailfish.vim b/syntax/vim/ftdetect/sailfish.vim new file mode 100644 index 0000000..04a0884 --- /dev/null +++ b/syntax/vim/ftdetect/sailfish.vim @@ -0,0 +1,6 @@ +" Detect sailfish template files and set filetype +" Maintainer: Ryohei Machida +" URL: http://github.com/Kogia-sima/sailfish +" License: MIT + +autocmd BufNewFile,BufRead *.stpl set filetype=sailfish diff --git a/syntax/vim/indent/sailfish.vim b/syntax/vim/indent/sailfish.vim new file mode 100644 index 0000000..5b44d88 --- /dev/null +++ b/syntax/vim/indent/sailfish.vim @@ -0,0 +1,12 @@ +" Vim indent file +" Language: Sailfish template language +" Maintainer: Ryohei Machida +" Last Change: 2020 May 29 + +" Only load this indent file when no other was loaded. +if exists("b:did_indent") + finish +endif + +" Use HTML formatting rules. +runtime! indent/html.vim diff --git a/syntax/vim/syntax/sailfish.vim b/syntax/vim/syntax/sailfish.vim new file mode 100644 index 0000000..f13dbf6 --- /dev/null +++ b/syntax/vim/syntax/sailfish.vim @@ -0,0 +1,17 @@ +runtime! syntax/html.vim +unlet b:current_syntax + +syn include @rustSyntax syntax/rust.vim + +syn region sailfishCodeBlock matchgroup=sailfishTag start=/<%/ keepend end=/%>/ contains=@rustSyntax +syn region sailfishBufferBlock matchgroup=sailfishTag start=/<%=/ keepend end=/%>/ contains=@rustSyntax +syn region sailfishCommentBlock start=/<%#/ end=/%>/ + +" Redefine htmlTag so that it can contain jspExpr +syn clear htmlTag +syn region htmlTag start=+<[^/%]+ end=+>+ fold contains=htmlTagN,htmlString,htmlArg,htmlValue,htmlTagError,htmlEvent,htmlCssDefinition,@htmlPreproc,@htmlArgCluster,sailfishBufferBlock + +hi default link sailfishTag htmlTag +hi default link sailfishCommentBlock htmlComment + +let b:current_syntax = "sailfish" diff --git a/syntax/vscode/.gitignore b/syntax/vscode/.gitignore new file mode 100644 index 0000000..e3a9959 --- /dev/null +++ b/syntax/vscode/.gitignore @@ -0,0 +1 @@ +/test.stpl diff --git a/syntax/vscode/.vscode/launch.json b/syntax/vscode/.vscode/launch.json new file mode 100644 index 0000000..7bc18a4 --- /dev/null +++ b/syntax/vscode/.vscode/launch.json @@ -0,0 +1,18 @@ +// A launch configuration that launches the extension inside a new window +// Use IntelliSense to learn about possible attributes. +// Hover to view descriptions of existing attributes. +// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ] + } + ] +} \ No newline at end of file diff --git a/syntax/vscode/.vscodeignore b/syntax/vscode/.vscodeignore new file mode 100644 index 0000000..f369b5e --- /dev/null +++ b/syntax/vscode/.vscodeignore @@ -0,0 +1,4 @@ +.vscode/** +.vscode-test/** +.gitignore +vsc-extension-quickstart.md diff --git a/syntax/vscode/CHANGELOG.md b/syntax/vscode/CHANGELOG.md new file mode 100644 index 0000000..6b5b8ad --- /dev/null +++ b/syntax/vscode/CHANGELOG.md @@ -0,0 +1,9 @@ +# Change Log + +All notable changes to the "vscode-sailfish" extension will be documented in this file. + +Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. + +## [Unreleased] + +- Initial release \ No newline at end of file diff --git a/syntax/vscode/README.md b/syntax/vscode/README.md new file mode 100644 index 0000000..1cf2bfb --- /dev/null +++ b/syntax/vscode/README.md @@ -0,0 +1,13 @@ +# Syntax Highlighting for Sailfish Templates in VSCode + +This directory contains Syntax Highlighting extension for sailfish templates in Visual Studio Code. + +## Features + +- Full Rust syntax highlighting rules inside code blocks +- Auto-closing brackets for code blocks +- Folding for comment blocks + +## Screenshots + +![screenshot](./screenshot.png) diff --git a/syntax/vscode/language-configuration.json b/syntax/vscode/language-configuration.json new file mode 100644 index 0000000..cca85c8 --- /dev/null +++ b/syntax/vscode/language-configuration.json @@ -0,0 +1,36 @@ +{ + "comments": { + "blockComment": [ "<%#", "%>" ] + }, + "brackets": [ + ["<%#", "%>"], + [""], + ["<", ">"], + ["{", "}"], + ["(", ")"] + ], + "autoClosingPairs": [ + { "open": "<%", "close": "%>"}. + { "open": "{", "close": "}"}, + { "open": "[", "close": "]"}, + { "open": "(", "close": ")" }, + { "open": "'", "close": "'" }, + { "open": "\"", "close": "\"" }, + { "open": "", "notIn": [ "comment", "string" ]} + ], + "surroundingPairs": [ + { "open": "<%", "close": "%>" }, + { "open": "'", "close": "'" }, + { "open": "\"", "close": "\"" }, + { "open": "{", "close": "}"}, + { "open": "[", "close": "]"}, + { "open": "(", "close": ")" }, + { "open": "<", "close": ">" } + ], + "folding": { + "markers": { + "start": "^\\s*<%#\\s*#region\\b.*%>", + "end": "^\\s*<%#\\s*#endregion\\b.*%>" + } + } +} diff --git a/syntax/vscode/package.json b/syntax/vscode/package.json new file mode 100644 index 0000000..3dbac8a --- /dev/null +++ b/syntax/vscode/package.json @@ -0,0 +1,27 @@ +{ + "name": "vscode-sailfish", + "displayName": "vscode-sailfish", + "description": "Syntax highlighting for sailfish templates in VSCode", + "version": "0.1.0", + "author": "Ryohei Machida ", + "license": "MIT", + "engines": { + "vscode": "^1.45.0" + }, + "categories": [ + "Programming Languages" + ], + "contributes": { + "languages": [{ + "id": "sailfish", + "aliases": ["sailfish"], + "extensions": [".stpl", ".html.stpl"], + "configuration": "./language-configuration.json" + }], + "grammars": [{ + "language": "sailfish", + "scopeName": "source.sailfish", + "path": "./syntaxes/sailfish.tmLanguage.json" + }] + } +} diff --git a/syntax/vscode/screenshot.png b/syntax/vscode/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..af25cf84e256274099bd719f06bb87f85481854e GIT binary patch literal 66737 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 z z!Grty8lW>sA1{6OSrFi8IbWYSd`J8zJL~fT-{*>`Yq3Rt_k<$Y(ol-qSTpEaRq>Ql zXB@}nCGz}86({;}`}dhy|3OUO6vv2D{qAuUv%Iu1bQV(@Q(naN>d_cVtemv6X^$~Ol%c04 zS60Ox@4bspLnf8uR+bNy7?3+ikY?FBn!9>xmxL$tbNXjfr?5T#Dq%hAdk8%JK^ z>cuXpmmOu^_>Kn4E79htT8te#154g1AAabJ2<)<+y60 z15D5zO3CqePjV{C0=W1>m`gmnF)#2(nj^|Aehg6RJ!6r2;ZS~O= zHhA=2`JAOH11Sesy?x}m{sw$~_`xPlshAMm{ zW2HZHn>R+AXsO++G`j2&g%S_KLxgTmcbCv<3w%_c@*UI#Ei85MoL6oDZS4)|J(`o~ zZQNqp>HG_!L96#8>IUH&T6c#TO#9}kCtIxxkujKHwKjPnq1llGG38)i-CjcI`0H1` zvH_dj$0RNJA|mUgao?-_m!`c4+{gRhkWE_*tsaIwA;2eMIo3%qf0hwUK}pHUZbzCp z5zyG!m~^5DvNiV|lhtrE*LU(`3+;)E`^o%KQ#ejhqp^iX3J2!!)Nl4JInsp_DB7=w zWVacQu#PRkubgk_^{gJ=Wj7r&2Wji+)n2W6O}%#?@%aZ5zM~0}uOEVQ6qti5t#O10 zw`ufpyKZ+JqRTtBA14uqH{*ff7AirE@Ss}!X_S_v?s-V#L4K-xJ>SvTa`YTo({Ri) zkHg_m$d`jp4sD@Di?cTOwGTfi(umSO@F|DA6fVd<6g zYdHyw6v@vy-qlwd5)M^{#9HIXL8TvUv>^h%MI$So60?|FydYSE?ZTrRg>M9sqKv__ z35kw@sNcN9q~5qPi>3z>!r!@w?wHLM~M+-zWbg-#XKO;JK?pG4B4)<|9P69)!wh7cp9D3hca`<7S zqxxmWl@*E$4O^(szP9i&IYMJK0lRGA?cSaIp$yOC?gYz3RqTD6j%VN<+~K0>i-Av{ zFcu;@Ygv(^NuTBr?s;wdkW7qvcUM#W1;*g(JmnA;N9YZsOO_-1$j70{>fH{*YQ(FI zY3S``is=hC=9M$m#uJ#s8vKC!l}zdzk$a7ij^R9q4KeZ3$hk(xP_~6zm`b{P_`5@f z!NbLH(^JI7L3qVy;dF-)YlIH$*Es`eI~xwv5l~p$me2TEo{J^O(ZBWpS3wo^i87#X z??$UN>3RHwY}YvS2xXb9GGR9=s*nH)EwjZP2cQ7)mO(QjkMh$w-RESW)t3-B2bG;F z2tI8J(h*85{l}v#AEDP1zb(d3WBhefu;s){jo0yxseOuDTY0J0?SNQRMWN)TYWNr$V@SU_FZ*o`CXn}rr?v>^0A~8#mCEh+Iy<2E(hjk^Ow|!1*TVcl^;$R zUpy1gY1}*A8cci5--zIyy(%#;q#Vd60MFDw>v$KFcSR!gVbjkk8QZUjv%fK$F5(GO zKQ8ISH=xNID)gmpu zIY2Mb*PrSWY;cHk$OUr)_UOzv?Nw&Z+qE+fVG(8#qUyoMj_9lPVWR=(J*mjQF7G)r z^j^=QV|CD;QWuAQRFjK1`A#4wYdjDmY zLM_DYh)LLjEC6ACisEMXpS~4>gQy8HKUD1COrWz2Htg3OG&sj3`b}BwQ<4HZ(hxQ_ zByKjF+Utq449QRZ$7|wmYPs-xiKY}a(}H{NGpEv>HWup5`Xhjguk|cQHD>G=4N=NH9$|XSn&3k_ulpI_Z36BsFj5wBIs5(|@GpodJ8-;vDXE z3a=2Y&W?b1ZkyR+5>=G*!oR&@Nksi3qoXA+2PFV3>+OzO8Lc_*WgWCn#!+DB<{H|3 zKc1-ebRD8s;|wBoy^AB4#9ktI;-J=I=%u`WA^V+};F8t*DAps0&*!!A;6jCLE%ovMpV+ zuI;><4#ki1u-d!eI-wKRMp=P3rz|!M{2s_8MV9=ZoLM)`Rg<_g-B~T%={oP&1;&HQ zbH%Vk1&Jzp!`IZigX`?$Kk@m41wFOPV(t$eXJ{y?xUDT4(=mFc!4@3?W#>b?bm+pZ z`L@C+Oe#Pz?)zqtowZ#rrK$iD?(DwIj#5tf0{SgwU$Y}^(m2i(rwux;*eAem0^r%Q zr`(2tFz4o?XA(J1-Iq8+1WASyT$sL$?ax1wm`~C}@d3-C`r$u(^3ZD9f_)w1rq(zI*lou1qRea;%rag3++saehBa+i6^n&5 zRAG55X4DvW#-)hXD%!-i8yKW63p%=8O*wPaaWF#CbYG+&FFR%&Ctd7NXIrC7S7auz zVp#Wim=#4XX|TE%qk3Si@L+J>Ra!fiLO8(+&1`>|HD}JxZbDp4K3zh8<9Nj~-e)By zcUNxVR6((=K6LM+aIn*^mXaKz ze=!634Ho=o>-*y_1o+|at08%Xg@K=C5+whk|Lx-FgoJ_1%O*yKKB1w1f8{BQGTMdHye~u#}{pUsf{~iy7$%;b?ED?ZDLU<1bIXl668lVtWzdu-r zdX3wVY+rP=7TI0gkbHPiW~Tppqt82L2wCJ$Q5)icfx17lgm1nU!V!=X zuqg=Lg|S@5W`SkZk~U;$01qwF<;?m zopj(ZvsX>YzR5AjQRsgrx38HC{jXdG_aPaz?;-<8T8Qq*E9m59k1#h%LE4L5$J3`J z1wF}0OMgS`DlJ8DtQLuJ$>*a-W7@tlUGbFD=9K(CqjEbaLv#a@`gK{s$OsmcS(p8h z?D`k$Q5aL@-XT5B> z0itvM%+Z*{pwf%2wl(^;*~4nt22R9IvgEf&b{e~@NQHL_cLR6&c^nW6K z`&F|xyuc8vnb?kwj+6b=w#F29LOy~yLsF}tNjZdvCnbVU#pUqEp;`|AJU26~*9w1E zcQHNB0N%-E5;z=iftQyJy_{MV>12Ic%JD6dXa)Vq+Wi^$)*|Pi=M0QOe){Yo%chMl zq9)xbMnvet7y!kOhR*!3Ks!|xpwMzMNM5Vc@Lk&aCT%exf_8E(c;pXQ_|1~?$Cdcx zE5iI#+Xym-{qz<^oqu|6ALVu}`H|YOI}CrdZ(H-{j`7UP3dlja&WrC^&=Ta9@f~E&8N2}0yp;bgwB#Ymygu8Dc}`#WuCKfDIE>w07tK?GP%eM4 zPZNxP+@?S(?tPx+S>+_3xAhb8?3I!hYwLCL3b476W8 zSaI4rGWNS{0$pP`e$_DeCV_BCtYYz=mv+J!imwLxCh<6QRnQj0Pb)JDnz-TVku6lV z4)%KoyB-t#s|c(pd{x<>^1l4x4_s0m^E>)5Z%UM8l!Wuj{FEb5900FnW5i?%QmW}A z?h*&|8`-S?9|oW4c8U9T8ZyJqWcACLjD)f}l>H0HwKX|Df4@{ShLNdhERM@xJ=Dm+ zQy%n0$aTbuZV5bFE*>ikLm2XfEm+n8KvyNBEjT;TWO6R2ryV)pyjDAZBs2ub%Km>D za_t?3mVki zKjr%a(%;)a2<)5b7Y6cYMBqGx2!S^qhn-m!8bMgt%?vp(_IgZJNEv}+pf305Vx}NfcUo!=dgqZcU z#m;teRYVjGQt1GQVx$6#)#%(514`;}rbe#(vwhByQb#)rMb(tNoErlI0uvBoA%shgSR5d&iw^ zOn}L?5_qEbdlt0R?7RH&Go>z$&SU*ss`^=jkjB+P?ivW9_G4Ye^fX$#=bOKj!v~z% z6(_#IRRf7S22A_e$I8$yq^K>UBvA&=Umc@rx6h0fE&()KmCh>RorK3R)uqtv%5%tnOdq6= zwifo&;Ng6v0_oV zEUadW{QEziKoh|3=b)>pKoB!$oM0p9=KgI5=iA5w(6rZs$tzi?>DNj*&o<509COBc z;x8(0Y(_IC_So)Fl$NnWPbUw$<$R}78vB^Bd6rXKQpmE#$ zrjLz<$*^NdU5ui>t=;5QS)I6PWUYd=_jK&VMZ8KQ|9y3RnIvpXhw7v>XMCoyU>bpN z{k|E%0E;eG=h8p0Ap^b*2n&lz{{yr?=hX;;`~4Ou3$Z3YQ5w`2$Y=d$96QdwwRsuc zb>69@u(BCh&aDiH3TkJI&?D~Oz#1(a=NQc=qf_g z;CP}%?4dtg{L0r{p!f|o)Bb&i>5(PV)}_cQcEE8a^D>#e{kzJd z&XBc}r%~PMCBEBn$x*z(#ruCGf-s{DRF&WJ4bQxdx#wr@QbLs>ErbZ<$YLSK^8(Fjl~+2(9c zAeQ2p0;?{Omy)f+>L|ce(7`KP>lw^e{dYpskubyqg=KJ3O_e|?ec-;5Y;bf;Es@Lx z<53&|dt^3H3~fR&(4z^_sRqD8Lu6Uq<^@y(pY|bxY?Iwp?~1BtTRq0i zg)%89H69gpnm3}P934g=HPX19zr%%HK5y>NnKNWF+=i^TJ9{SK>_Ed-8+7U&hc6Wp z46jp_#FBv^-Sl0a`z^uB5df z{_60Kc#|DxnEZhBMNmo1*(+Uh&7NOhcj2)2CpMgqEuEpD;7CqyyXO}IMjL}ZYY@;i zx_Ru}A`c$Gzg*P#uRkQ@ukyaGVQF@Dh=-l{_QC_DR=U%@Cq<+jSi1A=vW9gnj~N39 zRnNeUkyCp~${13_j9_liNdJEr^{yHJqwNb^$ivb3qg1os1%8)G_ss6~a~a+)m-WSl zBJ*15=?P<%me_=?usCHOv3TJ$)T`V*?+%VTWe=7lQljX;Wl{46>Hn(L-hxSCsB$p8 zJTkH*978P?9DIEI-s+Vgr6PJLG-PjILPA~geWe-=PCFNP#Nx^rg{pX$5fH~_*2l#^ zuul(w?OXtO;toK!n@3V^#5n1eMKK_a57tI|rubaPFgGN+35!*f#Q3auU(F^XR^$Y9 z-ZE7|{|ZxZuq_#1o583{cHIAEB2}EZ?wD*<12yyL&g0+>xM)6!C)Ael%H07u+1D1D zjI=T#wx+l+IL-^WnN_t>75g@W!ClVe6Ca}EE(*7Pm2C6P0qOypL)=xQ#s{WVu^DSZ z3#?lO$!vxa__;;?B1)I>R`@AR|80!`yD4K>bF!l(L`N6an40rEB7zpgmNjv$dOtoh zU(W)co(uZvW$I8{>2H5W%5yF^IC1q*zeSxY{~p@6YLycg6z+CvakcXxlE)$O>9fx_ zv)9fW2@V0e%o}8A*jcS!Rw!!>_{oe^$63fpRtkvHy`7d$1XB>QbnIrF7($o#wTI1# z+^pb1i3rnw1)&6hviCTyIBTbP-@!fvP#u)wQ5YTE_R3IEdSDyrGrgRe09pr}E7z*M zURiJP`Hqb^_tQ`kFY^yTpdn798ELFaf-E=%P?2sDvSjN0%B1MNr<{`;d<(NN-F0`Bu3 zpDaP6bZ~HRF67?@2?_N-$NB%tCHy%a7%~#sU)`ZRzD$`ieqrVGrSz?2Pj;iQi3g$_ z?;>U7=~@FtGrs0;*mK;A9M`sQ*Kz<9lbAYnPr~iRacgYHH&<{07uyekJWJM0>)^sw zRBzAfWuNX@gNa9Gqv?X6+!dHUWt!t7+zhAYjnh9Vx;v!gjFZC&Jr;%c;~n^wyXc=r zr}(?-oFMG84e(#nsz`ESN1G|idBE}dFhht*7&>>sh6U8Hir^5RS_h0m6-#9b1?2KQ zeNFL#Vn~A@Uen!&S$nVENSl!{qos#5biLoXo0o~i*C|i2J1RX_)`rRpSemTA>n5Hj zz;A;?9WSh?5VQJ3$03$?Tt;FVjA>3K)C{Fo5S-%VU9{OJD|bul{*l=y&HC%L%=K0( z#CO>w*uPXvFBYGKuAQaVmm2P+Svo#+?ndhEz_n^zysH#$`y2-JMsQ35L=W=gNgXIS zqW(dDb(|)$ZjeuP1E~CAHsr;+!aGC;r2O%=K?gccV}dBJB&0bF`7qdqc%#-cNP`|A zFAw%#F2Gclpwpja@yV+Uy9&$@MP>%kP=AfCbeT)X$KDTqLXjufbhC@JCA;ld3TnaT z>WeeeGLrgbb%$pFrF%1pq205A91_+mAx>3nJV}TyG!d@Tss{p#Q5!`ZCE^a;8>Onw z)o1o?be}2TpZ{oiDZIo@pJ^fLp%baN+CcaC%x?Av$4ghA(t%7B0NKPIFJRx2e(kQO(&XNJ#7v83|kYBt7Q_xw;f^%x5oQ0 zs;_BR&B8W?jd#5#&{_B4lXvFdvE2&bJ74qqR;4+(6=J|m#8C}X$!;7 zTyNteeqNz`9Q`s6bT$f0BpIEjnp*9OBE&=0sE#g$qiL-1YknR|!V%A{7|y9>^V6;Q zM(7h-|2fK~2pWc+FQ{>r2aM8E>ulm4>VP~wex#iOv`?x{byJugy?~R!6N0tMxD@r%gFBz_=AUn zSblOw_;>KMTB3Lrf+LSvn?;@_Q&DUlKZ+T9W#u_a7*vGVnilZmT|N38mkIVtRX}zE zFq|9$oP{(Z0Gt_lOWe;0cau04_y$5O1S!yHZwC>dj=plYqXV6ki6@f@p~$>P|Kdh2 zUA${e;^-i;+Dr3gnAdo8U>aNGsMd`l&0D>IJgWDvZt^zNHt2q2R3^6tM8OA1Tb(HqmUI9HJo0$vtIYNQO0atJ#wI+=G&!U`nTGIM*`B?^cvOc?c?Y zAD|3RLev3EW?`@S(=U!5t^ke zX-#NOD6ojFRLcrE; zra4|-Lw=SF1oNXds}Hv)ZnFwu)6>NDP{$h2iU@D2tD|mgc-mW|4=Kr>{xzNtRt2^; z`EL>V-vIs+aB<(&cVN{zUQtC`-MBq1!=tRDN?}#1xKyzqd`vA%Fj&TK(oqPY7C%y^ z;5-7$_YqAPRH_v2uC{Q*Q+`w|jN#bB5A^4|v=|La?8Flo>E_=KjEm8Y8akM22rX58 zFS!So1mvn%Stj;l>&LqnMRO|Szzl=FC>x;%5xw=>T9Zl(i9zSM&h{13h_7_~$RA1O zIgf_59=1`GjuLPjl-S5}Up^5)ixH~)tG>0%*=Eh}Mwh3pTZ7V*E38z?K?S&xTUhg$ zAF73Eq#6MM-PO8Cbzf&0{NfiNEEyw^+0JA|NqA@9e7w7~d!r3{Ra3jJIY+d!O(N4H zmW__m+32C85C#do1$UyzHIbu-HJWIL`}q> z-H*GWZbi7=y;-!QiXg`1bZ-rorx*J5xpX&R*^E792!J~)i+UsCx1UM|>@0=tn= zzCe)VM`l3M+FC%#lIUKx^m!pmo=|HJ&fTPpGLQM#FJp6r?Gril zyXT`PL&%Rs)d!Y{+-p_yLh+EWfV4XyW{o$;r}pIVn1Vte+cQe<2}cH;{a@Le7kk7x zWSE%j`rozI2|g&rc#Ht|dPR!TzlbS#Z_n7oLkLp(POg=djiiHd+eb4ML z0(!v3@hO0dPEQWq=A0EDxyV;Br6>HxZmsRV@UvgfkNkNVW#^)|>NcXRfKc+6S4ul4 z9$jhaZiKvg`c-2BKjgzjIE^8_4h4e)9H!s@PH)v81eEo4fQ)A&d4|rnw<2U>Nbtje`yI(hlCIry0%43LGlUomv42u@l2lNteoVueA^~R zC-=yVJS8ZiqlZ(?FJF}EUk*4l#K{Jwily9~7c)Yk2>MKD9lBXf2p=WTw{=H=@R(sY zaVqdVbut8O5)Z`vl!_LrGTy9FZXVEH3qvji*LT2Mh|-+xZzj^1{*3X3h62Ykb*taK zCNwsWdaw12sv5I2vBmkSGvdrX2LiQY^P6`NVTb60Q96^O%3sGT4vsQp2KipnKiKTv zxj*av4Fd3-i#o6H6q8Z-vZz?rJ4OcHGvCo9Pf0vV8Mr&mDi*4$dU^qwrB!$CpuSC? z-c8-dN7Q4pkfSE;QY1;Zi(&jedBb{s(SP~< zDRoBnGcP3yk%jdN8jS7iDWxvWVa_f`F!F>W#BErNd~r-VY*|9e%<>ziBwd3x&M*`J zwH0p3J^wPe6ycaI`AEMiCo7lEWLB83&WNCs8}U2&b;yhmBiLsH!8*mkF@M90L4tHI z_Rxnz;;Cd=;rQO1*R9&}7YuBuD}!(qjtXT_eqhibp;05(o2S7F+MmYUsp5W};taDt&!MQcsq0A)& z2wyE>Y10=)F=ddkm!g}&hVl{~x=`s2pU>}nG|zlA*!VJVgNPQDv~XYl6P+sxp^*~D zQ49yiaoA^4rYYaS!+?I&T<`pV+(U$V955q*BuZ9Lj?;1XOCvM5KhQ#{f>AVa&{Z%LDU+CwsD z?DXh~dm?h=)rF2LppUVLXra0kHQJ$IOzND>0S5@wlXU)X_GBXM@YrIcT0T*u1V{MD zpZi%;o-b0tu?93b<%Aty;BRv5_Ar8jo6soju{68exjIAP#>GDb3@s;)M?-8Mvg~p; z`AU-N)ON5s4yg?K1h5*G7G@f|eJQzyI5NT{DxK4+CDb7iA-q1%l2BuPO1D1AU1`cO9+BNt3x;z{oF?H70d+mB@U1*^+P&stv% zzhA?UYpk4lo(8tQ{t7LMwbTiEDRsUts2J}#Ce3wX0R$^tiNJ` z#FUxNZASC;u>^UtltFK-uC+l88_+;hbF6|I6{sRc-;U&z>G%?{q>r3y0{Wku!UyQ3 zufr5gR!EFwU3Yq&+ki1L%vhRKBbVuXtI7Rti*>C-mFc1ojVpNGPSk8Cs{AZV>yw%v zK~Cl5NrTP!=BU9>R6g4*RqDq)c^|V%eNbP#&{kCpcQ%t|xaKF`N@UKupru9)`P`Mc zA`Wf&8^b&`s7!va@YgzwG_{rB3FN*BhnN{cQlDq^Ds23Ac=_Vb zSCPVNaa`Cs$J0k>QhMcD7~!VktbGv!GquozV{fSBszTDxE0I-yC;6aXdSd-BZuLZk z{JH+iZT=)p{C=v}RccH>_68bnFuu~n-`L8W*7uI&G2jIqVEfK_3=a=KjNU5vNop8c zSacBHyA*}X{mXpZ4gJTs;5Zn;^X82-B{&$XT!K-cFwIxCH=~4zqWzbDy^R#y63dD} zLc#RV?o;y{ScGKmL}6!;@t4y7LNWB;tqXDh>G8_zOFOx=nw|1Z{`4i^wP}T~{ud6i z!F5PJ-dbFDSe)6!H6-n<(r2H8F%z8sFJZ&#%x@K0sbOSj-ND%C3ako+NwH>`y9oCG zoIv5P3d93VG2V7+WuJ{^8Y^F6aK{sp`pj{ifVd?KDeG9>FT2H_E5lFFpX z|A(`;463YIwnd?7pmAv28)@9#Y24l2-QBIR#@*fB-QC^Y-QDdi`rG@Q{q8+4?u{3* zfax>iBa?(Q^vC88_B>r8{05VW(yza1BEpK4OI8V>Z{P8|6sxSb!2n>p3d|tn@-)x z(cbPzPYyG%RQKVK{9EUn%VA9SH=8IVn~xm0Dz>@4drGxJ2e`?dxWDN&txN#2vu)m4 zUHv@;^c(3{aVGP8L@&6^fKL6YEI3jI7Cm4kD?914N*#Q^>7(4OL?Klxsjuo{KQWoh zdy63^7nXPjoWGWJ`SnHnBS17M)y<0HJa!Sx%BxJ3aI!dY4%ARHZMGy%=v3GIKz4=e z=ph!83uK@AU9vK+5A%O53R`YNCH<@7ZV(TbD)^+ zaSxL`klXvYWC}R8z;z)3<6q`ulYd#|OzW_ptbIv?v6moLY3uugHyH##4bffMW0Hm$ zM=Ax~oG+YKOrn<6#UK*uxSdmzXH_+wuT{Wgm2NtHBW^$*_&BOg@cY%nlK6jMVI@Rs zMQ4%&EAf`&aMCa_Ir1#Y2# zcJhcL;NTW#z!eh;8x-vpEYY$;LCw^Km{22TLc7Ul`e}2mgBrz}(e>8vn?N3e(oYSr zxDvBM6|=slQ7SA)Ypxp#_EeorAL%_voC+$whLcWEFk(!tr{x)Uz4uTF^7MUX^862v z{>0TMdCK+-EBVDCx}QAOk?rZeulqepg&8b%7tE{AUe4LwJs%!V$h;vJdt&~5`o!JJ zoqT8Q9XncXQr9w=T`YrKRDTMrO$T1R*+F$ygt*PW|SULtV&wp@eV z(ifQv*bb<@f(tB1*<9FXZSHBCnpZEbr7+_spVvRO;Zz=Mm3VND0%&~K`CbhdLT16n zKbEKqH3bx>LupobwmVBEstlA~Oa@8`jOGk+T8)tJR~bSJ3%RJvpU+w!Phpph)>wU! z;W&EVO2~)mFz|A-T~|yN@8~Alm#xWSkAjt3o^a+BmxrqIqYb}0G=eX*E4iHedo)Us z`%oDn!*Nz8DL9fpM<(2RP~QZ*qDrln9K;FXZ1>$K6)bdd!u8*$r8w$#P~V(Ffunt! zxO32`f&)K|6L@rdOFVF71d&vC1+TEw6h#0xX5ob2xMPVxYFcdfK&>0&ddd<2Sd_+U zyamC#1Z-s>X=x6~VU_RJf4+!=&bc@J>UvV7#DQouii@PpkpF^pKUUznLo{6Ted-I3 z7P!#5R|Y{k1nIQGk2+&yvy0-a{AS1!gxU18Q|yzhwXy=kHPc{>`)vsg35hs_3&)2d z>e6QU;oxXvE60q3ghjmjj)#valS!W=#rM;CwU*BW{nR+14m0QWYp$myX$Tj`##BKELzx!RQsx)ue%S*7`9b6dz6jxH2Wwr zDo2@YLIdqFMIu5|oZNduV||6?X|1otP_{~PH@@H^befA#r)JNn@8!e@49u5x>>c9o z2d~*m)vv(c)i$;>-e*Ae)t;;x*2bOY@2Bpb>siCYxE@@8vU+ybL{t#WDGih>HO!z) z%c*PhEDL4e6CcUoxb4hVd|?|P;J904yR~pzdgtX_k3=x5!Vb(yrh7Z3QNG`d;jn3l zOJ6=~y%dGRYm3u$Lk>4(FQVtOEcR_NCCgeL;xc0A)T~r2%iBL6aLy+Q62e*O>KuA0 z-<&GI(W|QpTs~7PyvdiXyXV2F`%>VvJ)u0ZGr=KR*B-d6L0wZv>^Ik*T<65yML%Eg zxE}K&$+hjSxv;7pI=9qw+vt(YW~v-D6>eAhC%H~wv&^6$9Y3e^TXG6ICX|jwThhS1 zh2U9{-p2iz`I-Yz1H;oNYNCX{3wCIJM5sT~ZTMVU0ajIpmlq=`fcN9>O`fO*Vp6*L z81|C6$}1r-a$WFgnk;@HaS~7g5P?)QgB%s>tET|tG*_Bk%mUMPoY#I2s+l--z3uJ@ zbw7Z+wWNd#%Ij>_aJhR=JFK|Y*F0xtgLHJornSFh$b7&oW0r{5*9Y&t2gdtraY4{y zLj~o4&MT=RK97F>?F;wXyXGlv>wxAS_0y9D`J?NT+AQ`F*NB~d6ta8xK)n~u8(QYU zCRRw^wz~n86&8@y99epyrO}C9MRkvo)cJ820FcfvB^8M{TK+Zqh?@|Ng7&En zSHZUum(6tDTZ@Xz-UzK!SUPs=;*Q-UH)@K#01%5=a3BeMvn@Mbs!flOf_tw{7Vqh? zk1^G5L}XZ$r1~k;M!&mhh3id%lS8Pk;Z?B-frlcNy!(@Z4=ZxLi_J&F}q`0uO zG`F}oV4z_&tRXADZ6#wLwBRK=qEBq7TJG1MoH1}Ie+BK_JC9_`vh93cc2c2cbk?0H z&*Sh>&(YN|S|*KDTCH!qdZ9%?T<&COa$$!)H1IfbGE*@p){#|C8zyVAQd0_wdd$2l zB`ZdCvvpP*LKAI$yPExC*(C)&SLocoUO!IPywp=3JTBfAAfxrf7*KjiST+}bf9(sO z-9-taED(?&I~K6`!d|piw_BCGMs8l<{LpiHmDt7!+kSy~mYS5-@|cQvwhnWoH@U8- zGG+_rKpdY<#G^uynkj@uTAzosiyJLL2o8Y?0S?jdfUAvJDPr!6ed?b6u_%l>?ch-^ zw(Y95fxD0t72(~Y(uQSuO154%2bF^ygA%aZ{4E!mgo?46_|lbEx}ZQD{B1^uCQcF< z1DXhXJbjWF%gy9F>93Lj^kPFBlLa-}n;4=WWDa()KS zU}|m-bV?nZ9~=!+_ZiRX(IhUKI{jJr4s%T6 zd1Tq1)E?s=ryEw8O|DC_fr-zmIL=s>6JtTbL;7FdJMKVg!U~y& zfiarAPO+}3Bg*KSSn3Q)XcC_5=sS!fa0AHruz`$bm371Vf*v#24nnK7=A zj~#~87RJ>y@VOirQTitrz0tos(D&}2^Oc3W|0Ef4oSbUgDMk)9WuRF@cwTNeaXnl- zx2`Pv_9MK)f|~Lj)DuS@jLfV-m+@Fyu+3RLjlqGWYJNetq(OTHqjPRv+|28A*bIV} zyWEkxd|Wo&a#12}Nmw8*={)@h-jm`d?ZVP=P(fCkPLFI*$6O!n8j{B2?j#gCB2rL7 zlLuC`9}UOn_ZLLN7}N-*-{we{xr3Qn6 z$Di>zuSup#GOx3O?9&~6_0a^!zMqsg688_%uwXoVwK%nikd;>vYHJHOE(7_9K5qk{}7^T#;;u8Sfi{Z++<+U(E497|IWIH^a z803^OQ5O~3WBXJ*Db#T)Vx1?F&q%QWe>YzwA`e}Ut+g1+dGYc`2^6Gsw4wR#SI>_Z zXyP~|4s*k^P!vd;yc^swx-L!Cf(^&BhPjz-{T@5QRU!z?Ci}fG7qq~L zJSfhdQl+F^b+iKU417(igeD`Dc50gHkwv6ck!S}@B}xcvT!%!KIF($IxE@GPrAf$3 zI4zw!2!H)jQN@1lV7^d?%1dJ)-r5FI-dxBS8jI zymalCe!VS~wZy(MSY1oDhqHXc6Gz9kLqcIv05OS$O|!E&`Ga>7*;1E;-HE8vBsMnI zGpWUh!_#RgQje<# zRu=>| zj=ENNa1ebXc!0rl(a$I*-A6aotB2ZbvQ@vj;y)!0#baNeFAJRHW-b}fjY7MNF+xeH z9XBM@8>TXDq`JahRZJKiSKb;W)m!}|5$2S+KQ5-^#EN5!uLYNj6&_#O9s}@mNqVJq z`l3^M7q*(z7h(e)58(SPR-APzlE>SDkmBSN%JGr{Fw?bIPjEzQK}&%Kn=|5oM$Qki z=4N&*i33?tUW`pf%~P&sjt>PoT7Iuls&bTbxla|TQ61e0ieuo0F}~zqbQO2bP73nr z07VQ){fGt%!wDG^m2v&^cAi8T>uQP>mYA){p^3ZomBhhd(cJY9n~laX26op8cD8Fu z92h&x%NN2qwoyfL0e;}I(m5E1`YEM$a4`@17JCin98>6eQE0*gb7e>;G4l9*u;OIZ zsk^%^2!0+lKS`rbefjO;iX2YcwiSmQOIfd^Sk#7!_~m8SFJ&Ht5_9Hr0yeC?(uX}y zy5N!t34g?MK9g_&65mYda~f9S;^4l}yzZj9=>0BB&{j^X=iv`r!aOc939@Oah8(YS z;uo966=UPVEI=bo+d;mNHJ^w4^xkm#%V({d9)89Ni^V}Zv-ICVHK`CEsTo+S+!LI( z-0Vl0P)?HpIx#RY3SrTNSH?gGA!oIBqo>g+c(d(+{qmf3af-)|%0X~0N0z2S8;jmY zf@aOOP+rfcD2ik5=6B3bAx0gncOSha4DYqRU3s>xcNaN2O5Vw)m_%wYD2#+V5ZMU0 z8rb9LG2aco-8EgVh@)A!QkXIh+oxJ*M#S$sHX9jqeZwh8=(gqDQybeYj?0aa;Z1*I zsIQwij>j@>s7Y`&EmbsiTofMYc~qBR2ry@HOfjr5&(7Fia}$|ckk+gNv11eMTpMoR zRXknUXE5tmUv%OxTGb32)6dE0g&ATAGX7>2h^TZcSbri&O(g!7BTaF?F1ES6z`r%_ zYMNqBvIDF>7ADuA4p}!trd9B1EhO&PiDHf#8cv2YINcQQH}R*A?EQGNR<<3i=9`0m zN*e6>hlAOBv^aZaXdT;Nl9XBKX({}UKL%?~*lyJgmN-3L^!+(E0u^@VoFhVqZld|= zYR4x7npnd}3{<#0Q40gg*%}1;8Iz{SWBgieeRl3Md(0zn9Tn21EsHD6mPYjS1r+Gbk zQyHQjXN@wJ`8B~vjNUFOcDM?%uXlGeE&(}Rxkw`F zMw1%Fz`uMQj0l**0-0KT6Z^aGmjxXKj~92E{b`kQ22{1g__`%hdg#;O9by;eW#!7d z7n;GnC~Vx$?EHhC`!Vd_neOWarIYJrTUw&{-(cAYGR}%AwH`=*vhJj^F$`|Ir-*#& z;e5`)%EYzqJSwH0?IQ`_K^|5{gh%>Fax~20jULvsL8RSGmUNp9_ye~Nf1V=U(-x4C zC#&_Gme$p*7+QzA{n5e3*Z1UxhX)M=Iwg;X0VGsd_}48cLPN&lqDk`?+Pt55o_9Um zM!O+k?eE60Q3OJ093JNZZipldd5L9A=FjlqZ3yD7mrU%3SB5~`COt$LV|gU%Zw*w< zgsZ0|H`z7w(qP3|4ff6Xlj@LKg0ifo#h<*Uh!jgxU6 z^LvN4h1r8qS2PvhnwXju(;S5iD&&jwBl7cUvZ-M=9{7#DN~{!BROwR4@^OR~H#n1aJd(htN)TwA84*nQFf_F-XQEDe11=_DtTjU{+M{& zXjk&N-kuoJT(^4PLe6qoLi8HR(VBsOyLw>pq@y>g`3Ml|$kNN*VTXpY>Mk`s2^MsCNYyya(TD zQ4!f~Yq8Fuf}I&croZr2pEqB}jk|dbPT?WzW0Jn!Hyk9z67#%r;+}|xmQI%lVdc^U8tf3H;&60Fb=q*X<}+kwTgSwarG#BqxxVJo z>+mA{tcQeR^TL%015J^Um}SjbspRD=k`y38<0&HL&w1W;5-rH?{m%S7LXNPTkNgr--FVLy$WBns6m_xDoNPv>#P!(E% zyX6;pzTN5y*~TI?u2;A4H+^ngkjskD^p^{?4#z$8cdH4|$2-;^VjcoqiAUYqxH&Nf z?lyerDnidxX_H1rl%=iYLx*nQ1vu=eyQm)wgKH0QnF}W>4eeMdHuQQu0}x-sX`IQ% zpI8G6@Hf2s40{Y2b0!p`!@(|3y$}j!{JEKsGn%m|4>3<{?3sW}veQvc$C2{(;^dV} z6YWK_*I}``kODNyPH8-CDRVjTZ z%_)NpjxFYeyBkD zMFTdOfM?msJV&nKvpsW}J0?uL#cY6wv$}>o`wMl0p%#~MQ-eMT=Iu!%wH4?RVh(f$ zpb2Vaf zg$7N18vp?ghfd#4RoD3!J4OKV7w8G;Txa41Nl&!RMX5)M+Hijurm|9Hv_Xs>bOu{6 zJN)Kj;1D?VOZ`;j+PofTL)nN?WM6hK6U!8&y)x}d0RDPXBkAT9j+ zrTx^Y{q#YHt!t)uA-G!@RM_~RE})yF4c8%E5%P`*+Lcq# zj+1&>_A95yEt}&ER`Qx`3gq3)**yyG#J6T2%x_ZlBjW0#?BsJquD>S>pq@EFQ>-azaY z6yfRn8}mH`>F3ote8*Ejkb*y?C+C`hN>)SWc(~m?CvHcY%WJQc&Fv;>IF7vSgU3(v zN$$>MI`Zdg8$n4p0OJB!a;Wxisezw*RVEt$)U&Q);W z6QD%6pmDZ)G9%nzw6;njkYyWrCPX%DWDPNq1u2gT1LNDq?7Y$d^*%g79PZ|38J8DPt|zBXJCTRrZ(5p2Gk%N1J54BL{MbKh+UBHBaFk1CND||r z<->>_9)WUYKSHCCFD>C1rlj}(GWR) zK~}?{KT`z$_I_$*TG&X2@ovRz(V`6h^sIa9aDR9$27h)VEN5frU4rq{vXIxRUS?`R z@Pif`)Bb5!Jp8g`VPkXi%5GsXO-Z8228O&p;--I7fe1y~Oh%v2kb&*sE;}^h%L9e> z1DLCb<&)g`dvw$W>(O3=@sxW}B6aA!?ftI163%ogyGN{Vf$!tFTWp2robdbGiOZ(a zg{>&q=_hak5L5U&szWALJT?r>rgAs5v6hPeIfn*&dVa(#qbWKRLKgRJ z)8r`E#nrqlyB0;LEy#F4^Hc|BKA8>UBxjcKZUS;4p*cxy^sr7eS_Zc*O|rQS{c&-i ztAVOghYtdd;|c48sh`E;Rq5-O0oFH&H_$Zw25FiTASd(EkuhVvhcq3o58t?&3M&4I94JDY^*KS8Vp1Wf z=F6CRqix?Qd_g+&t+kuYcELDLm{g{$lC>FZQ-A&FypppS2SF9w5zp3N8dm?ow)11E z+B_>`9ty-A=}*nE4itF;OlLwUJ<)EG&31=7vYF`ZA+9nXz}|_6M_9XFQo>@R@7|8i zm-%I+D{^y|p|-RwlT@tumQjf~clwU(_3lAL4+0@jm!sD}lZProi&BU0TRziI;!5ip zm-H21{Lo$#oGS<$QgA^*v!&0~4O!Am#X~qPAYPP$%Ool7T~`lmD#i+HaU&BGBH*9V z9$m=p<|eS-m&bv7ek|MK<+7=d@@cqal5^iHR6e^{?v=RjCk2 z3P-$joLc2*NO6gt>EcU;#nE*+XA@;QoJ!#8iig2O$7LP6`sLkuHDlLbK~=Cji<4M8l8vzbr@#of@u3nVGnfEtykUu}_$(Ci3j zd}7s?3P)sjKzhIg4$qJn{wn|1^;fU5b}97|7Q5$9%(30bWMAzoj}l_FrxiXHThyPE zLpeF{dV3`q`jepL^{SxgIn=EYNF~XUw^<%Y33XWP&id!=nUnfRh@zCN#Ji53csD7& zC2F`Brd0p;)Yi@|+*dRX_N+6()eQVG~}r2k9L+4ySc13(LVVT zmlo_0jOZ<3bxKP6rf2RuMc#v*fvXM|tdFjctxCUX;&Y>({%_}A&!E1j4R&z|dOS=y zT(|}M?Ak=`z1-q8)s?^16)pfJftTzLN|7z!QC#CLw!fMsQ+xQ{JRtQ5eRMA!j{ZE~ zA@At$2aoUS753Xy1BZZ4-%tHVdWg4Nng8j8bgHvAGXjyW*oO{IPgJ<@k zoKK6T?S^>$2c2}^Fg5oCpI4^A;~>0ohiF%iswarM&VGo#iHeLe%N^eMyu_PZLU_xC zl%xZ9dMJ$}8(RtawG}}dVKlNaR~E`OCwD;X+{cj1lGan(+T&@1mW+-mBD&mij7WMPuGgcrSq2V?e)OEmVlchokTs{5Gcg4h0*g7a*EdafJ_K1L-NH)U zA&Kpi%h5vw?4G6l?s@&3@*~jT=2ZZbX{{MT!2N2Tc=q6@&PsoI83Y?2Mrj>N?7HFW zjtIj!6Gs~r$Aa@p<`TF?G#ohiqqQ~A-r9pq_Qc+`0qf5b97;SR{O*%>spUixBd?_a zkBt)TwTJiMme!dD_+@3m`2i*Q_?GjToKZ08npdhHYCXZah+2uM&5gEP-+$2G(6q61 zCBMUwjt0z9yFEFN`^PPAvh#Z~4qi2?%m2$UO8F7O+oNiWc5pbQb42v;6xu?_jitZY zBX$*WweXH8MC@2;sRwG-F4MIe2$K4Aw2||EpApS|qjXWU?Q`*lGh)P5f4{;}d%q(0 z@o}hi`NmBdI^oKWYXl-wYoLmDr_PEu(ZmA=hN9P!Ph@9$H-2MZj2qY%77&1ekrGQ7 z$Wgn9?M&aWfg=vC(df$ronoF?Y8p!zBm}~uWnNHb(c!>pME+(E``<1M=W z@Yazlsii*~>92SfPJ}8AeY{=qbi%N-(+>-&YnNJ+W693LgC27q#3hDnxXsWS4okNm z5eUz?>_KTM4%!k@mGw#H0ca4TY5p%1U3^A+W}mVUjXiJSL;BBWXU*3}6T?Hpy8oTy zDsVvFp4qFp>vJ@XeJQFiJGf{65?A_ktXOKc-8#3#CygQeN$#r}oG(@3al*+drv>?>s1;wEeY^n{-9W= z1YFbbi$vlAmELYIME|~up~|`{bczI_*OmeQeGvS%WS&bhYBIU7%kw2zcCLaeg@OXw z46rI&o4(~p7~6o&2hf1Q#pk)bZHH)U7u$(O3?Rg8xx~wGrwVnc{uEup<^991Lid~d z>-@FpsDf$i5zE#uTxzDeX~d{&z$5_r@S=PsuzG7lGbG(W(*EHL{>l4p8@Idm@dNY* zwnBp5b{izX!2DmzBD{a{ux%@JzL0949djh{i>}7&8yb8*=o=oF5*~8|_`-f9*VY#5 z#RM~_Jn+6%GKogYz|eyMBC8}91uA`P)* zc;tJlf>zRiFDA3Da>~Ro^2lI17BK>po%tsG`__%Ey{Du<+v%yT*_292QBfg4>(anU ziWidL?+@yY{?;_Z^Wggsn73S0+N)inTf-#5;GF1d)UVTLDhiWlUcZ;abqSfi2ueC5 zn@3yeVZ_OlgJM7zx-b`mCYaMiA2D09Ffn%}1f`Mzo(WRGV&%1?SC%6#*9$rCO(@Ixp>$BvX0(a8Em&(@v_7(;_~UKEq52B8daxM@Di{%;8u z^d>B8WDHRUGTttLY*E1`KZ&TV8LI*qss-?*k}8R~&?0VIYiMyLk_DMCN!+iZl5ER} zu;fR0&4!qAD%AHxdy9XH@SThYB{D8ALmm!Nz?*<>m?d`Ad+74aZdjh)51hI7ZsD!k zlLzQZe1sZaaUvwSIikL8KOZooi4J!?CCMP`k~PSfkPj%QhR~t+9hl^d9d#p$M*_Eo zquhqwv9Mu!Gss9mIT{5|UyG7aD2lWFl(7O2*Y+C>D;FZ-qk|Bw17lphblwPUi1R2H zbYygp291QCoSO~SGTSyOK@L%gU_$O*nin737&|}EKuR92S~8Daah#M_5Sl#z#GC>R z#KOGS|Hz>=#sH3D4%F$K(D2?w7MtH$muS04Rqs)E?sH(Mu&rGXN!m~I(`bvTYQAHo zLM7tg?_CZTPkJ5Vv=boiv@2|~rqF58pwYsZFw~0wJagW%rWoG{qGXj=0a?+A_z(5p z+j5_+(*(_i5srRcp9iaV*p)M-(P6-#onzV&4!mmCs6DeJVle}e<8S4xb#*gNgMDRE zV^wIOG#3j%w|1Id-^M4@`D^`nDR@*$lq%jk$}Og_f^pQxs;9p3RoyVVB$?53ae2Nd zYQDkOZ@iEt50<>1s1c)~WB){8@0&FF*_RK)k%`ge{a>^F>B6P^UO@U?s13v6m+tHRyJ`p zO0VX8e8np()avK?mI7KT9T2k)Up!FezOzJ@zH*NZZ9`n%6;W~Ha7}Abw)gQEgW>7q zo2&)ChGqPs^=LcBaWhKKQPVg2OfAUb&eD%uk^02gA~vU`86GZHcxdJ8R8x6;0=wh2#1OX9qm9fa@E6Pz~-8Ql3wNB4jN zXmvk~VxoX*5c}y^P)@0j<>z=*kFk$8X^N;}m8lt@CM*2^|20;26SK{+| z;5E784T?m+8&-T8TJ~fcYnB|fD~buYcJnsoLT2WqNI>~p8x5iB70}AiVl}bG)^3e5 zxifnw6+;v*Ixjl9?qm*WF>UcCdF@)vfKfSQzpkC5b06qcAAWP1pE(W(()`W_GKPYf zoQ@Oy1V+I`t{8%zqbR|~6k@U1v@mQ?ipK zoU_i5B#e~!Ux9)A{eO(S=y8lPR+)tVIi!#9n8%bcuCM2>FMB|5C}Q|vGg|`;`umBv zHsYj&oOiT5`v?2!aL80A_8peQCBx}a*n?jt@n)vGmHy2I2=AjBoNDUL#ecJBe?D-j z{@?sK)L6tg67Lk7oHgNsG{ZAXQ;g)r`0#DJC6nWDU@BF<>WY61`2qfJ_~knf^$1gn za?8CO%Gjq>f0c?zyWHd?W&XYQO7qSPyO|)mijK#!7}i$qhM+0p!<(01xcXZXBFOlb zXm=IOQV7H{@Ra%Q>RmO#H2tzgyP)=@Qb61$qB}LuDZ)qfwx9LQAtw>|3rxXp=L3Mc zUJ2CW%9HqRsy^f zCLJFy8}0iBc|ZR#K>*JOo9{obf80uOxBV~I|1C86sj2X9zol1}K+l$2Yy^tNe0`9w z|dOz_3_2X|9)xG6DT1kMlX0q~vJZVLB#%sq>#( z@dS^uDIdmQsn0^0YR$TR8lgJnoRKP_(q}QmaF%(Z6X8tOOfC-FtFxmqCq;0`^LY*pz

#i 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 literal 0 HcmV?d00001 diff --git a/syntax/vscode/syntaxes/sailfish.tmLanguage.json b/syntax/vscode/syntaxes/sailfish.tmLanguage.json new file mode 100644 index 0000000..11e36f1 --- /dev/null +++ b/syntax/vscode/syntaxes/sailfish.tmLanguage.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", + "name": "sailfish", + "patterns": [ + { + "include": "#commentblock" + }, + { + "include": "#codeblock" + }, + { + "include": "text.html.basic" + } + ], + "repository": { + "commentblock": { + "patterns": [{ + "name": "comment.block.embedded.html", + "begin": "<(%|\\?)#", + "end": "(%|\\?)>", + "captures": { + "0": { + "name": "punctuation.definition.comment.html" + } + } + }] + }, + "codeblock": { + "patterns": [{ + "name": "source.rust.embedded.html", + "begin": "<(%|\\?)(=|-)?", + "beginCaptures": { + "0": { + "name": "punctuation.definition.tag.begin.html" + } + }, + "end": "(%|\\?)>", + "endCaptures": { + "0": { + "name": "punctuation.definition.tag.end.html" + } + }, + "patterns": [{ + "include": "source.rust" + }] + }] + } + }, + "scopeName": "source.sailfish" +}