Add source code
This commit is contained in:
commit
08dacdd263
|
@ -0,0 +1,4 @@
|
|||
target/
|
||||
scripts/
|
||||
*.log
|
||||
*.s
|
|
@ -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"
|
|
@ -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
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
<div align="center">
|
||||
|
||||
![SailFish](./resources/logo.png)
|
||||
|
||||
Simple, small, and extremely fast template engine for Rust
|
||||
|
||||
</div>
|
||||
|
||||
## ✨ 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
|
||||
<DOCTYPE! html>
|
||||
<html>
|
||||
<body>
|
||||
<%= content %>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
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)_
|
|
@ -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<Data: TemplateData, P: AsRef<Path>>(path: P) -> DynamicTemplate<Data>;
|
||||
```
|
||||
|
||||
#### 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<Data>` struct which contains the function pointer to call the template function.
|
||||
|
||||
## `DynamicTemplate::render` method API
|
||||
|
||||
#### Signature
|
||||
|
||||
```rust
|
||||
impl<Data: TemplateData> DynamicTemplate<Data> {
|
||||
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<String>` 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<u8>);
|
||||
}
|
||||
```
|
||||
|
||||
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\<T\> where T: TemplateData
|
||||
- Vec\<T\> 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
|
||||
<h1><%= msg %></h1>
|
||||
```
|
||||
|
||||
and Rust code
|
||||
|
||||
```rust
|
||||
struct Message {
|
||||
msg: String,
|
||||
}
|
||||
|
||||
let template = compile::<Message>("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, "<h1>");
|
||||
sfrt::render_escaped!(buf, msg);
|
||||
sfrt::render_text!(buf, "</h1>");
|
||||
}
|
||||
|
||||
SIZE_HINT.update(buf.len())
|
||||
Ok(buf.into_string())
|
||||
};
|
||||
|
||||
OutputData::from_result(inner())
|
||||
}
|
||||
```
|
||||
|
||||
## Example usage
|
||||
|
||||
Template:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<b><%= name %></b>: <%= score %>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
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<Team> = compile::<Team>("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);
|
||||
```
|
|
@ -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<T: Template> 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 { %><div><%= msg %></div><% } %>
|
||||
```
|
||||
|
||||
will be transformed into the Rust code like
|
||||
|
||||
```rust
|
||||
for msg in self.messages {
|
||||
render_text!(_ctx, "<div>");
|
||||
render!(_ctx, msg);
|
||||
render_text!(_ctx, "</div>");
|
||||
}
|
||||
```
|
||||
|
||||
which causes an compilation error because `self.messages` cannot be moved.
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "examples"
|
||||
version = "0.0.1"
|
||||
authors = ["Ryohei Machida <orcinus4627@gmail.com>"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
sailfish = { path = "../sailfish" }
|
||||
sailfish-macros = { path = "../sailfish-macros" }
|
||||
|
||||
[[bin]]
|
||||
name = "simple"
|
||||
path = "simple.rs"
|
||||
test = false
|
|
@ -0,0 +1,15 @@
|
|||
#[macro_use]
|
||||
extern crate sailfish_macros;
|
||||
|
||||
use sailfish::TemplateOnce;
|
||||
|
||||
#[derive(TemplateOnce)]
|
||||
#[template(path = "simple.stpl")]
|
||||
struct Simple {
|
||||
messages: Vec<String>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let messages = vec![String::from("Message 1"), String::from("<Message 2>")];
|
||||
println!("{}", Simple { messages }.render_once().unwrap());
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<%# This is a comment %>
|
||||
<% for (i, msg) in messages.iter().enumerate() { %>
|
||||
<% if i == 0 { %>
|
||||
<h1>Hello, world!</h1>
|
||||
<% } %>
|
||||
<div><%= *msg %></div>
|
||||
<% } %>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "integration-tests"
|
||||
version = "0.0.1"
|
||||
authors = ["Kogia-sima <orcinus4627@gmail.com>"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
sailfish = { path = "../sailfish" }
|
||||
sailfish-macros = { path = "../sailfish-macros" }
|
||||
|
||||
[dev-dependencies]
|
||||
trybuild = "1.0.28"
|
||||
pretty_assertions = "0.6.1"
|
|
@ -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)
|
||||
);
|
||||
};
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<div>i: 10</div>
|
|
@ -0,0 +1 @@
|
|||
<🍣 let i = 10; 🍣><div>i: <🍣= i 🍣></div>
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "Taro",
|
||||
"value": 16
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "<%= name %>",
|
||||
"value": <%= value %>
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
raw: <h1>Hello, World!</h1>
|
|
@ -0,0 +1 @@
|
|||
raw: <%- raw %>
|
|
@ -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<T: TemplateOnce>(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: "<h1>Hello, World!</h1>",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
|
@ -0,0 +1,6 @@
|
|||
max_width = 90
|
||||
hard_tabs = false
|
||||
use_field_init_shorthand = true
|
||||
edition = "2018"
|
||||
reorder_imports = true
|
||||
reorder_modules = true
|
|
@ -0,0 +1,38 @@
|
|||
[package]
|
||||
name = "sailfish-compiler"
|
||||
version = "0.0.1"
|
||||
authors = ["Ryohei Machida <orcinus4627@gmail.com>"]
|
||||
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"
|
|
@ -0,0 +1 @@
|
|||
fn main() {}
|
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -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<PathBuf>,
|
||||
pub(crate) source: Option<String>,
|
||||
pub(crate) offset: Option<usize>,
|
||||
pub(crate) chains: Vec<ErrorKind>,
|
||||
}
|
||||
|
||||
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<Item = &ErrorKind> {
|
||||
self.chains.iter().rev()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Error
|
||||
where
|
||||
ErrorKind: From<T>,
|
||||
{
|
||||
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, "{:<lpad$} |", "", lpad = lpad)?;
|
||||
writeln!(f, "{} | {}", lineno, line)?;
|
||||
writeln!(
|
||||
f,
|
||||
"{:<lpad$} | {:<rpad$}^",
|
||||
"",
|
||||
"",
|
||||
lpad = lpad,
|
||||
rpad = colno - 1
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
pub trait ResultExt<T> {
|
||||
fn chain_err<F, EK>(self, kind: F) -> Result<T, Error>
|
||||
where
|
||||
F: FnOnce() -> EK,
|
||||
EK: Into<ErrorKind>;
|
||||
}
|
||||
|
||||
impl<T> ResultExt<T> for Result<T, Error> {
|
||||
fn chain_err<F, EK>(self, kind: F) -> Result<T, Error>
|
||||
where
|
||||
F: FnOnce() -> EK,
|
||||
EK: Into<ErrorKind>,
|
||||
{
|
||||
self.map_err(|mut e| {
|
||||
e.chains.push(kind().into());
|
||||
e
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E: Into<ErrorKind>> ResultExt<T> for Result<T, E> {
|
||||
fn chain_err<F, EK>(self, kind: F) -> Result<T, Error>
|
||||
where
|
||||
F: FnOnce() -> EK,
|
||||
EK: Into<ErrorKind>,
|
||||
{
|
||||
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
|
||||
| ^
|
||||
"#
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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<Self> {
|
||||
let context = s.parse()?;
|
||||
s.parse::<Token![,]>()?;
|
||||
let arg = s.parse()?;
|
||||
|
||||
Ok(Self { context, arg })
|
||||
}
|
||||
}
|
||||
|
||||
fn get_rendertext_value(i: &ExprMacro) -> Option<String> {
|
||||
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::<RenderTextMacroArgument>(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);
|
||||
}
|
||||
}
|
|
@ -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<Vec<Token<'a>>, Error> {
|
||||
let mut vec = Vec::new();
|
||||
for token in self {
|
||||
vec.push(token?);
|
||||
}
|
||||
|
||||
Ok(vec)
|
||||
}
|
||||
|
||||
pub fn to_vec(&self) -> Result<Vec<Token<'a>>, 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<Token<'a>, 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<Token<'a>, 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<Token<'a>, Error>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
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<Vec<Token<'a>>> for ParseStream<'a> {
|
||||
type Error = crate::Error;
|
||||
|
||||
fn try_into(self) -> Result<Vec<Token<'a>>, Error> {
|
||||
self.into_vec()
|
||||
}
|
||||
}
|
||||
|
||||
fn find_block_end(haystack: &str, delimiter: &str) -> Option<usize> {
|
||||
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<usize> {
|
||||
debug_assert!(haystack.starts_with("//"));
|
||||
memchr(b'\n', haystack.as_bytes()).map(|p| p + 1)
|
||||
}
|
||||
|
||||
fn find_block_comment_end(haystack: &str) -> Option<usize> {
|
||||
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<usize> {
|
||||
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<usize> {
|
||||
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 }
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<LitStr>,
|
||||
delimiter: Option<LitChar>,
|
||||
escape: Option<LitBool>,
|
||||
type_: Option<LitStr>,
|
||||
}
|
||||
|
||||
impl Parse for DeriveTemplateOptions {
|
||||
fn parse(outer: ParseStream) -> ParseResult<Self> {
|
||||
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::<Ident>()?;
|
||||
s.parse::<Token![=]>()?;
|
||||
|
||||
// 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::<LitStr>()?);
|
||||
} else if key == "delimiter" {
|
||||
options.delimiter = Some(s.parse::<LitChar>()?);
|
||||
} else if key == "escape" {
|
||||
options.escape = Some(s.parse::<LitBool>()?);
|
||||
} else if key == "type" {
|
||||
options.type_ = Some(s.parse::<LitStr>()?);
|
||||
} 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::<Token![,]>()?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(options)
|
||||
}
|
||||
}
|
||||
|
||||
impl DeriveTemplateOptions {
|
||||
fn merge(&mut self, other: DeriveTemplateOptions) -> Result<(), syn::Error> {
|
||||
fn merge_single<T: ToTokens>(
|
||||
lhs: &mut Option<T>,
|
||||
rhs: Option<T>,
|
||||
) -> 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<TokenStream, syn::Error> {
|
||||
let strct = syn::parse2::<ItemStruct>(tokens)?;
|
||||
|
||||
let mut all_options = DeriveTemplateOptions::default();
|
||||
for attr in strct.attrs {
|
||||
let opt = syn::parse2::<DeriveTemplateOptions>(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<Ident, Token![,]> = 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())
|
||||
}
|
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -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<SourceMapEntry>,
|
||||
}
|
||||
|
||||
impl SourceMap {
|
||||
#[inline]
|
||||
pub fn entries(&self) -> &[SourceMapEntry] {
|
||||
&*self.entries
|
||||
}
|
||||
|
||||
pub fn reverse_mapping(&self, offset: usize) -> Option<usize> {
|
||||
// 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<TranslatedSource, Error> {
|
||||
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<TranslatedSource, Error> {
|
||||
self.source.push_str("\n}");
|
||||
match syn::parse_str::<Block>(&*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<usize> {
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -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<String> {
|
||||
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",
|
||||
))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
[package]
|
||||
name = "sailfish-macros"
|
||||
version = "0.0.1"
|
||||
authors = ["Ryohei Machida <orcinus4627@gmail.com>"]
|
||||
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"
|
|
@ -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)
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "sailfish"
|
||||
version = "0.0.1"
|
||||
authors = ["Ryohei Machida <orcinus4627@gmail.com>"]
|
||||
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"
|
|
@ -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;
|
||||
}
|
|
@ -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<String> 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()
|
||||
}
|
||||
}
|
|
@ -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<F: FnMut(&str)>(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);
|
||||
}
|
|
@ -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<F: FnMut(&str)>(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<F: FnMut(&str)>(
|
||||
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);
|
||||
}
|
|
@ -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<F: FnMut(&str)>(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("<html><body><h1>Hello, world</h1></body></html>"),
|
||||
"<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_*/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_""###),
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
use core::slice;
|
||||
|
||||
use super::{ESCAPED, ESCAPED_LEN, ESCAPE_LUT};
|
||||
|
||||
#[inline]
|
||||
pub(super) unsafe fn escape<F: FnMut(&str)>(
|
||||
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<F: FnMut(&str)>(
|
||||
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;
|
||||
}
|
|
@ -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<F: FnMut(&str)>(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<F: FnMut(&str)>(
|
||||
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);
|
||||
}
|
|
@ -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) => {};
|
||||
}
|
|
@ -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<fmt::Error> for RenderError {
|
||||
#[inline]
|
||||
fn from(other: fmt::Error) -> Self {
|
||||
Self { inner: other }
|
||||
}
|
||||
}
|
||||
|
||||
pub type RenderResult = Result<String, RenderError>;
|
||||
|
||||
pub struct Context {
|
||||
#[doc(hidden)]
|
||||
pub buf: Buffer,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
#[inline]
|
||||
pub fn into_result(self) -> RenderResult {
|
||||
Ok(self.buf.into_string())
|
||||
}
|
||||
}
|
|
@ -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<T: Display> 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<u8> {
|
||||
// #[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);
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
" Detect sailfish template files and set filetype
|
||||
" Maintainer: Ryohei Machida <orcinus4627@gmail.com>
|
||||
" URL: http://github.com/Kogia-sima/sailfish
|
||||
" License: MIT
|
||||
|
||||
autocmd BufNewFile,BufRead *.stpl set filetype=sailfish
|
|
@ -0,0 +1,12 @@
|
|||
" Vim indent file
|
||||
" Language: Sailfish template language
|
||||
" Maintainer: Ryohei Machida <orcinus4627@gmail.com>
|
||||
" 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
|
|
@ -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"
|
|
@ -0,0 +1 @@
|
|||
/test.stpl
|
|
@ -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}"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
.vscode/**
|
||||
.vscode-test/**
|
||||
.gitignore
|
||||
vsc-extension-quickstart.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
|
|
@ -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)
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"comments": {
|
||||
"blockComment": [ "<%#", "%>" ]
|
||||
},
|
||||
"brackets": [
|
||||
["<%#", "%>"],
|
||||
["<!--", "-->"],
|
||||
["<", ">"],
|
||||
["{", "}"],
|
||||
["(", ")"]
|
||||
],
|
||||
"autoClosingPairs": [
|
||||
{ "open": "<%", "close": "%>"}.
|
||||
{ "open": "{", "close": "}"},
|
||||
{ "open": "[", "close": "]"},
|
||||
{ "open": "(", "close": ")" },
|
||||
{ "open": "'", "close": "'" },
|
||||
{ "open": "\"", "close": "\"" },
|
||||
{ "open": "<!--", "close": "-->", "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.*%>"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <orcinus4627@gmail.com>",
|
||||
"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"
|
||||
}]
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 65 KiB |
|
@ -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"
|
||||
}
|
Loading…
Reference in New Issue