diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 00000000..0ad809cc --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,22 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 90 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - 'help wanted' + - 'Status: Blocked' + - 'Status: In Progress' + - 'Status: PR Welcome' + - 'Status: Proposal' + - 'Status: Review Needed' + - 'Type: Bug' +# Label to use when marking an issue as stale +staleLabel: wontfix +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false diff --git a/.github/workflows/badge.yml b/.github/workflows/badge.yml new file mode 100644 index 00000000..f9e1ff43 --- /dev/null +++ b/.github/workflows/badge.yml @@ -0,0 +1,29 @@ +name: Badges + +on: + push: + branches: + - master + paths: + - '**/Cargo.toml' + +jobs: + update-badges: + name: Update Badges + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'Kogia-sima' }} + steps: + - name: Checkout Repository + uses: actions/checkout@v2 + - name: Get the Numbers + run: | + echo "dep_counts=$(./scripts/count-dependencies.sh)" >> $GITHUB_ENV + - name: Create Dependency-Count-Badge + uses: schneegans/dynamic-badges-action@v1.0.0 + with: + auth: ${{ secrets.GIST_SECRET }} + gistID: a2128afe12bf05d85a0d68346236a4f5 + filename: sailfish-dep-counts.json + label: Dependencies + message: ${{ env.dep_counts }} + color: blueviolet diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 00000000..49f946d1 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,41 @@ +name: Coverage + +on: [push, pull_request] + +jobs: + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + target: x86_64-unknown-linux-gnu + toolchain: nightly + override: true + components: rustfmt + - name: Install grcov + run: curl -L https://github.com/mozilla/grcov/releases/download/v0.6.1/grcov-linux-x86_64.tar.bz2 | tar jxf - + - name: Install rust-covfix + run: | + curl -L https://github.com/Kogia-sima/rust-covfix/releases/download/v0.2.2/rust-covfix-linux-x86_64.tar.xz |tar Jxf - + mv rust-covfix-linux-x86_64/rust-covfix ./ + - name: Test all crates + env: + CARGO_INCREMENTAL: 0 + RUSTFLAGS: -Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -C panic=abort + RUSTDOCFLAGS: -C panic=abort + run: | + cargo build --all-features --workspace + cargo test --all-features --workspace + - name: collect coverages + run: | + zip -0 ccov.zip `find . \( -name "sailfish*.gc*" -o -name "integration_tests*.gc*" \) -print` + ./grcov ccov.zip --llvm --branch -t lcov -o lcov.info --ignore "/*" --ignore "sailfish-tests/*" + - name: fix coverages + run: ./rust-covfix -o lcov.info lcov.info + - name: upload coverage + uses: codecov/codecov-action@v1 + with: + file: ./lcov.info diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b59f23b7..3eaee985 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,9 +24,6 @@ jobs: - os: ubuntu-latest target: x86_64-unknown-linux-gnu toolchain: 1.42.0 # MSRV - - os: ubuntu-latest - target: x86_64-unknown-linux-gnu - toolchain: nightly - os: ubuntu-latest deps: sudo apt update ; sudo apt install gcc-multilib target: i686-unknown-linux-gnu diff --git a/CHANGELOG.md b/CHANGELOG.md index 457890b6..958916bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,48 @@ + +## [v0.3.0](https://github.com/Kogia-sima/sailfish/compare/v0.2.2...v0.3.0) (2020-12-20) + +## Breaking changes + +* No longer requires `extern crate sailfish_macros` (which raise compilation warnings with v0.3.0) +* Remove `TemplaceOnce::render_to_string` method (already deprecated in v0.2.1) +* Forbid implementing `TemplateOnce` trait by yourself +* Change `RenderError` into enum +* Update error format in `sailfish-compiler` + +## New features + +* New filters: `json`, `truncate` +* Impl `Send`/`Sync` for `Buffer` + +## Fix + +* Fix rendering issue on continue/break statements +* Do not panic when buffer size decreased +* Remove unsafe usage of `ptr::add()` +* Properly handle slices with size greater than `isize::MAX` + + +## [v0.2.3](https://github.com/Kogia-sima/sailfish/compare/v0.2.2...v0.2.3) (2020-11-29) + +## Fix + +* Use `std::result::Result` in derive macro to allow custom Result types (#34) + + +## [v0.2.2](https://github.com/Kogia-sima/sailfish/compare/v0.2.1...v0.2.2) (2020-11-11) + +## Fix + +* Update proc-macro2 version (#32) -## [v0.2.1](https://github.com/Kogia-sima/sailfish/compare/v0.2.0...v0.2.1) (2020-07-17) +## [v0.2.1](https://github.com/Kogia-sima/sailfish/compare/v0.2.0...v0.2.1) (2020-08-04) ### Features * Add trim filter -### Bug fix +### Fix * Fix incorrect syntax highlighting in vim * Avoid capacity overflow in `Buffer::with_capacity` diff --git a/Cargo.lock b/Cargo.lock index 1fe98cea..fe9443a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,6 +9,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "ctor" version = "0.1.14" @@ -25,6 +37,18 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" +[[package]] +name = "filetime" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "winapi", +] + [[package]] name = "glob" version = "0.3.0" @@ -42,7 +66,7 @@ dependencies = [ [[package]] name = "integration-tests" -version = "0.3.0" +version = "0.3.1" dependencies = [ "pretty_assertions", "sailfish", @@ -70,6 +94,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "libc" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929" + [[package]] name = "linked-hash-map" version = "0.5.3" @@ -121,6 +151,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ec8ca9416c5ea37062b502703cd7fcb207736bc294f6e0cf367ac6fc234570" +dependencies = [ + "bitflags", +] + [[package]] name = "ryu" version = "1.0.5" @@ -129,7 +168,7 @@ checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" [[package]] name = "sailfish" -version = "0.3.0" +version = "0.3.1" dependencies = [ "itoap", "ryu", @@ -141,8 +180,9 @@ dependencies = [ [[package]] name = "sailfish-compiler" -version = "0.3.0" +version = "0.3.1" dependencies = [ + "filetime", "home", "memchr", "pretty_assertions", @@ -154,7 +194,7 @@ dependencies = [ [[package]] name = "sailfish-macros" -version = "0.3.0" +version = "0.3.1" dependencies = [ "proc-macro2", "sailfish-compiler", diff --git a/README.md b/README.md index 385deaff..4889b340 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,9 @@ Simple, small, and extremely fast template engine for Rust [![Tests](https://github.com/Kogia-sima/sailfish/workflows/Tests/badge.svg)](https://github.com/Kogia-sima/sailfish/actions?query=workflow%3ATests) [![Version](https://img.shields.io/crates/v/sailfish)](https://crates.io/crates/sailfish) +![Dependency counts](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/Kogia-sima/a2128afe12bf05d85a0d68346236a4f5/raw/sailfish-dep-counts.json) +[![dependency status](https://deps.rs/repo/github/Kogia-sima/sailfish/status.svg)](https://deps.rs/repo/github/Kogia-sima/sailfish) +[![Rust 1.42](https://img.shields.io/badge/rust-1.42+-lightgray.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.42.html) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/Kogia-sima/sailfish/blob/master/LICENSE) [User Guide](https://sailfish.netlify.app/en/) | [API Docs](https://docs.rs/sailfish) | [Examples](./examples) @@ -15,12 +18,12 @@ Simple, small, and extremely fast template engine for Rust ## ✨ Features - Simple and intuitive syntax inspired by [EJS](https://ejs.co/) -- Builtin filters -- Relatively small number of dependencies (<15 crates in total) +- Include another template file inside template +- Built-in filters +- Minimal dependencies (<15 crates in total) - Extremely fast (See [benchmarks](https://github.com/djc/template-benchmarks-rs)) - Better error message - Syntax highlighting support ([vscode](./syntax/vscode), [vim](./syntax/vim)) -- Automatically re-compile sources when template file is updated. - Works on Rust 1.42 or later ## 🐟 Example @@ -29,8 +32,7 @@ Dependencies: ```toml [dependencies] -sailfish = "0.3.0" -sailfish-macros = "0.3.0" +sailfish = "0.3.1" ``` Template file (templates/hello.stpl): @@ -48,9 +50,6 @@ Template file (templates/hello.stpl): Code: ```rust -#[macro_use] -extern crate sailfish_macros; // enable derive macro - use sailfish::TemplateOnce; #[derive(TemplateOnce)] @@ -61,8 +60,8 @@ struct HelloTemplate { fn main() { let ctx = HelloTemplate { - messages: vec![String::from("foo"), String::from("bar")] - } + messages: vec![String::from("foo"), String::from("bar")], + }; println!("{}", ctx.render_once().unwrap()); } ``` @@ -78,7 +77,7 @@ You can find more examples in [examples](./examples) directory. 🇯🇵 **Ryohei Machida** -* Github: [@Kogia-sima](https://github.com/Kogia-sima) +* GitHub: [@Kogia-sima](https://github.com/Kogia-sima) ## 🤝 Contributing diff --git a/THIRD_PARTY b/THIRD_PARTY new file mode 100644 index 00000000..4a218d56 --- /dev/null +++ b/THIRD_PARTY @@ -0,0 +1,30 @@ +Sailfish contains some third-party content. + +------------------------------------------------------------------------------- + +* Some test data come from TechEmpower Framework Benchmarks + +Copyright (c) 2020, TechEmpower, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name TechEmpower, Inc. nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL TECHEMPOWER, INC. BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/benches/README.md b/benches/README.md index 429aa480..ed1f5fb4 100644 --- a/benches/README.md +++ b/benches/README.md @@ -1 +1 @@ -benchmark programs were removed in favor of https://github.com/djc/template-benchmarks-rs +benchmark programs were removed in favour of https://github.com/djc/template-benchmarks-rs diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..c8d34b90 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,15 @@ +coverage: + status: + project: + default: + threshold: 3% + branches: + - master + only_pulls: false + + patch: + default: + target: auto + branches: + - master + only_pulls: true diff --git a/docs/en/docs/assets/css/custom.css b/docs/en/docs/assets/css/custom.css new file mode 100644 index 00000000..f74aeeb0 --- /dev/null +++ b/docs/en/docs/assets/css/custom.css @@ -0,0 +1,41 @@ +.md-typeset__table table { + font-size: .75rem !important; +} + +.md-typeset__table table tr td:first-child { + white-space: nowrap; +} + +.md-typeset code { + background-color: rgb(240, 240, 240); + font-size: .95em; +} + +.md-typeset pre > code { + background-color: rgb(245, 245, 245); + font-size: .90em; +} + +.md-typeset .admonition { + font-size: .70rem; +} + +body { + -webkit-font-smoothing: antialiased !important; + -moz-font-smoothing: antialiased !important; +} + +.md-content h2 { + border-bottom-color: rgb(234, 236, 239); + border-bottom-style: solid; + border-bottom-width: 1px; + padding-bottom: .3rem; +} + +.md-content a:hover { + text-decoration: underline; +} + +.highlight code .cp { + color: #a83; +} diff --git a/docs/en/docs/getting-started.md b/docs/en/docs/getting-started.md index b16cf830..1337b224 100644 --- a/docs/en/docs/getting-started.md +++ b/docs/en/docs/getting-started.md @@ -4,7 +4,7 @@ Create a new directory named `templates` in the same directory as `Cargo.toml`. Copy the following contents and paste it to a new file named `templates/hello.stpl`. -```ejs +``` rhtml <% for msg in &messages { %> @@ -26,16 +26,13 @@ templates/ ## Render the template -Import the sailfish crates: +
  1. Import the sailfish crates:
```rust -#[macro_use] -extern crate sailfish_macros; // enable derive macros - -use sailfish::TemplateOnce; // import `TemplateOnce` trait +use sailfish::TemplateOnce; ``` -Define the template struct to be rendered: +
  1. Define the template struct to be rendered:
```rust #[derive(TemplateOnce)] // automatically implement `TemplateOnce` trait @@ -46,7 +43,7 @@ struct HelloTemplate { } ``` -And render the data with `render_once()` method. +
  1. Render the data with render_once() method.
```rust fn main() { diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md index f6df61fd..3935a05f 100644 --- a/docs/en/docs/index.md +++ b/docs/en/docs/index.md @@ -4,27 +4,23 @@ Sailfish is a simple, small, and extremely fast template engine for Rust. This d This documentation mainly focuses on concepts of the library, general usage, and template syntax. If you've read this documentation and need more specific information, you might want to read the [sailfish API docs](https://docs.rs/sailfish). -Currently the documentation is uncompleted. If you want to improve our documentation, feel free to create a [Pull Request](https://github.com/Kogia-sima/sailfish/pulls) on the repository. I'll be happy if someone contributes to writing documents (English is not my first language and creating a documentation is a hard task for me). - ## Why Sailfish ? There are many libraries for template rendering in Rust. Among those libraries, sailfish aims at **rapid development** and **rapid rendering**. Sailfish has many features that other libraries might not support. -- Write a Rust code directly inside templates, supporting many Rust syntax (struct definision, closure, macro invocation, etc.) -- Relatively small number of dependencies (<15 crates in total) -- Extremely fast (See [benchmarks](http://github.com/Kogia-sima/sailfish/blob/master/benches)) -- Better error message +- Write a Rust code directly inside templates, supporting many Rust syntax (struct definition, closure, macro invocation, etc.) +- [Built-in filters](https://docs.rs/sailfish/latest/sailfish/runtime/filter/index.html) +- Minimal dependencies (<15 crates in total) +- Extremely fast (See [benchmarks](https://github.com/djc/template-benchmarks-rs)) - Template rendering is always type-safe because templates are statically compiled. - Syntax highlighting ([vscode](http://github.com/Kogia-sima/sailfish/blob/master/syntax/vscode), [vim](http://github.com/Kogia-sima/sailfish/blob/master/syntax/vim)) -- Automatically re-compile sources when template file is updated. ## Upcoming features -Since sailfish is on early stage of development, there are many upcoming features that is not supported yet. You can find many [RFC](https://github.com/Kogia-sima/sailfish/issues?q=is%3Aissue+is%3Aopen+label%3A%22Type%3A+RFC%22)s in my repository. These RFCs include: +Since sailfish is on early stage of development, there are many upcoming features that is not supported yet. You can find many [RFC](https://github.com/Kogia-sima/sailfish/issues?q=is%3Aissue+is%3Aopen+label%3A%22Type%3A+RFC%22)s in my repository. These RFC include: -- Dynamic Template Loading -- Filter - `Template` trait (which does not consume itself) +- Template inheritance (block, partials, etc.) If you have any idea about them or want to implement that feature, please send a comment on the issue! diff --git a/docs/en/docs/installation.md b/docs/en/docs/installation.md index 292bbe39..d9557408 100644 --- a/docs/en/docs/installation.md +++ b/docs/en/docs/installation.md @@ -2,15 +2,17 @@ In order to use sailfish templates, you have add two dependencies in your `Cargo.toml`. -```toml +``` toml [dependencies] -sailfish = "0.3.0" -sailfish-macros = "0.3.0" +sailfish = "0.3.1" ``` -`sailfish` crate contains runtime for rendering contents, and `sailfish-macros` serves you derive macros to compile and import the template files. +## Feature Flags -These crates are separated so that Rust compiler can compile them independently. This separation makes your compilation faster! +Sailfish accepts the following feature flags -!!! Warning - Make sure that the `sailfish-macros` version is larger than `sailfish`, otherwise the compilation may fail. +|Feature|Description| +|--|--| +|derive|enable derive macros (enabled by default)| +|json|enable `json` filter| +|perf-inline|Add more `#[inline]` attributes. This may improve rendering performance, but generates a bit larger binary (enabled by default)| diff --git a/docs/en/docs/options.md b/docs/en/docs/options.md index e8e58a1c..ad8b3c6f 100644 --- a/docs/en/docs/options.md +++ b/docs/en/docs/options.md @@ -4,7 +4,7 @@ You can control the rendering behaviour via `template` attribute. -```rust +``` rust #[derive(TemplateOnce)] #[template(path = "template.stpl", escape = false)] struct TemplateStruct { @@ -21,7 +21,7 @@ struct TemplateStruct { You can split the options into multiple `template` attributes. -```rust +``` rust #[derive(TemplateOnce)] #[template(path = "template.stpl")] #[template(delimiter = '?')] @@ -49,7 +49,7 @@ If a key is specified in both configuration file and derive options, then the va Configuration files are written in the YAML 1.2 format. Here is the default configuration. -``` +``` yaml template_dir: "templates" escape: true delimiter: "%" diff --git a/docs/en/docs/syntax/filters.md b/docs/en/docs/syntax/filters.md index c2b4e244..b2f47d19 100644 --- a/docs/en/docs/syntax/filters.md +++ b/docs/en/docs/syntax/filters.md @@ -4,31 +4,33 @@ Filters are used to format the rendered contents. Example: -```ejs -message: <%= "foo\nbar" | dbg %> -``` +=== "Template" -Output: + ``` rhtml + message: <%= "foo\nbar" | dbg %> + ``` -```html -message: "foo\nbar" -``` +=== "Result" + + ``` html + message: "foo\nbar" + ``` !!! Note - Since `dbg` filter accepts '' types, that type isn't required to implement [`Render`](https://docs.rs/sailfish/latest/sailfish/runtime/trait.Render.html) trait. That means you can pass the type which doen't implement `Render` trait. + Since `dbg` filter accepts `` types, that type isn't required to implement [`Render`](https://docs.rs/sailfish/latest/sailfish/runtime/trait.Render.html) trait. That means you can pass the type which doesn't implement `Render` trait. ## Syntax - Apply filter and HTML escaping -```ejs +``` rhtml <%= expression | filter %> ``` - Apply filter only -```ejs +``` rhtml <%- expression | filter %> ``` diff --git a/docs/en/docs/syntax/includes.md b/docs/en/docs/syntax/includes.md index 972a11a9..1843d082 100644 --- a/docs/en/docs/syntax/includes.md +++ b/docs/en/docs/syntax/includes.md @@ -6,7 +6,7 @@ Consider the following example. - `templates/header.stpl` -```html +``` html @@ -15,7 +15,7 @@ Consider the following example. - `templates/index.stpl` -```html +``` rhtml <% include!("./header.stpl"); %> @@ -28,7 +28,7 @@ Consider the following example. Then you can see the `header.stpl` is embedded in the output. -```html +``` html diff --git a/docs/en/docs/syntax/overview.md b/docs/en/docs/syntax/overview.md index 9402f32d..1bfb01f1 100644 --- a/docs/en/docs/syntax/overview.md +++ b/docs/en/docs/syntax/overview.md @@ -10,7 +10,7 @@ ## Condition -```ejs +``` rhtml <% if messages.is_empty() { %>
No messages
<% } %> @@ -18,7 +18,7 @@ ## loop -```ejs +``` rhtml <% for (i, msg) in messages.iter().enumerate() { %>
<%= i %>: <%= msg %>
<% } %> @@ -26,21 +26,19 @@ ## Includes -```ejs +``` rhtml <% include!("path/to/template"); %> ``` -Unlike EJS, you cannot omit the file extension. - ## Filters -```ejs +``` rhtml <%= message | upper %> ``` -```ejs +``` rhtml { "id": <%= id %> - "comment": <%- comment | dbg %> + "comment": <%- comment | json %> } ``` diff --git a/docs/en/docs/syntax/tags.md b/docs/en/docs/syntax/tags.md index d1997d4c..c2982287 100644 --- a/docs/en/docs/syntax/tags.md +++ b/docs/en/docs/syntax/tags.md @@ -4,30 +4,56 @@ You can write Rust statement inside `<% %>` tag. -```ejs -<% let mut total = 0; %> -<% for elem in arr.iter().filter(|e| e.is_valid()) { %> - <% total += elem.value() as u64; %> - <% dbg!(total); %> - <% if total > 100 { break; } %> - Printed until the total value exceeds 100. -<% } %> -``` +=== "Template" + + ``` rhtml + <% + let mut total = 0; + for i in 1.. { + total += i; + if i > 100 { + break; + } + } + %> +
total = <%= total %>
+ ``` + +=== "Result" + + ``` html +
total = 105
+ ``` !!! Note Make sure that you cannot omit braces, parenthesis, and semicolons. Sailfish is smart enough to figure out where the code block ends, so you can even include `%>` inside Rust comments or string literals. -```text -<% /* Tag does not ends at %>! */ %> -``` +=== "Template" + + ``` text + <% /* Tag does not ends at %>! */ %> + ``` + +=== "Result" + + ``` text + ``` If you need to simply render `<%` character, you can escape it, or use evaluation block (described below). -```text -<%% is converted into <%= "<%" %> character. -``` +=== "Template" + + ``` text + <%% is converted into <%- "<%" %> character. + ``` + +=== "Result" + + ``` text + <% is converted into <% character + ``` Although almost all Rust statement is supported, the following statements inside templates may cause a strange compilation error. @@ -41,32 +67,41 @@ Although almost all Rust statement is supported, the following statements inside Rust expression inside `<%= %>` tag is evaluated and the result will be rendered. -```ejs -<%# The following code simple renders `3` %> -<% let a = 1; %><%= a + 2 %> -``` +=== "Template" + + ``` rhtml + <% let a = 1; %><%= a + 2 %> + ``` + +=== "Result" + + ``` text + 3 + ``` If the result contains `&"'<>` characters, sailfish replaces these characters with the equivalent html. -If you want to render the results without escaping, you can use `<%- %>` tag or [configure sailfish to not escape by default](../options.md). For example, +If you want to render the results without escaping, you can use `<%- %>` tag or [configure sailfish to not escape by default](../options.md). -```ejs -
- <%- "

Hello, World!

" %> -
-``` +=== "Template" -This template results in the following output. + ``` rhtml +
+ <%- "

Hello, World!

" %> +
+ ``` -```ejs -
-

Hello, World!

-
-``` +=== "Result" + + ``` html +
+

Hello, World!

+
+ ``` !!! Note Evaluation block does not return any value, so you cannot use the block to pass the render result to another code block. The following code is invalid. - ``` + ``` rhtml <% let result = %><%= 1 %><% ; %> ``` diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 57beea02..e9c527fa 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -24,16 +24,22 @@ theme: font: text: 'Ubuntu' code: 'Ubuntu Mono' + features: + - 'navigation.expand' # Extensions markdown_extensions: - - admonition - - codehilite - - footnotes + - 'admonition' + - 'footnotes' + - 'pymdownx.highlight' + - 'pymdownx.tabbed' + - 'pymdownx.superfences': + custom_fences: + - name: mermaid + class: mermaid -# Customization -# extra_css: -# - 'assets/css/custom.css' +extra_css: + - 'assets/css/custom.css' extra: social: diff --git a/docs/rfcs/1-dynamic-loading.md b/docs/rfcs/1-dynamic-loading.md index c22bce04..a722458c 100644 --- a/docs/rfcs/1-dynamic-loading.md +++ b/docs/rfcs/1-dynamic-loading.md @@ -63,7 +63,7 @@ AllocVtable is passed to template function, and then VBuffer is constructed insi 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 +## Rust standard library conflict problem Rarely, but not never, dynamically compiled templates may use different version of standard library. @@ -77,7 +77,7 @@ We must ensure that all of the data passed to templates should satisfy the follo - 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 serialized to/deserialized from byte array (All data is serialized to byte array, and then decoded inside templates) - can be defined inside `#![no_std]` crate Sailfish provide `TemplateData` trait which satisfies the above restrictions. diff --git a/examples/Cargo.toml b/examples/Cargo.toml index cb51b89c..bf5b99b5 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sailfish-examples" -version = "0.3.0" +version = "0.3.1" authors = ["Ryohei Machida "] edition = "2018" publish = false diff --git a/sailfish-compiler/Cargo.toml b/sailfish-compiler/Cargo.toml index ea0446c8..bfb4395e 100644 --- a/sailfish-compiler/Cargo.toml +++ b/sailfish-compiler/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sailfish-compiler" -version = "0.3.0" +version = "0.3.1" authors = ["Ryohei Machida "] description = "Really fast, intuitive template engine for Rust" homepage = "https://github.com/Kogia-sima/sailfish" @@ -26,6 +26,7 @@ memchr = "2.3.3" quote = { version = "1.0.6", default-features = false } yaml-rust = { version = "0.4.4", optional = true } home = "0.5.3" +filetime = "0.2.14" [dependencies.syn] version = "1.0.21" diff --git a/sailfish-compiler/src/compiler.rs b/sailfish-compiler/src/compiler.rs index 93a4eccd..4fc72e3b 100644 --- a/sailfish-compiler/src/compiler.rs +++ b/sailfish-compiler/src/compiler.rs @@ -11,7 +11,7 @@ use crate::optimizer::Optimizer; use crate::parser::Parser; use crate::resolver::Resolver; use crate::translator::{TranslatedSource, Translator}; -use crate::util::{read_to_string, rustfmt_block}; +use crate::util::{copy_filetimes, read_to_string, rustfmt_block}; #[derive(Default)] pub struct Compiler { @@ -81,6 +81,12 @@ impl Compiler { .chain_err(|| format!("Failed to create artifact: {:?}", output))?; writeln!(f, "{}", rustfmt_block(&*string).unwrap_or(string)) .chain_err(|| format!("Failed to write artifact into {:?}", output))?; + drop(f); + + // FIXME: This is a silly hack to prevent output file from being tracking by + // cargo. Another better solution should be considered. + let _ = copy_filetimes(input, output); + Ok(report) }; diff --git a/sailfish-compiler/src/optimizer.rs b/sailfish-compiler/src/optimizer.rs index 5ce8b4f7..d5b5d99e 100644 --- a/sailfish-compiler/src/optimizer.rs +++ b/sailfish-compiler/src/optimizer.rs @@ -88,6 +88,15 @@ impl VisitMut for OptmizerImpl { let mut concat = sl; concat += sf.as_str(); + let mut previous; + if let Some(prev) = results.last().and_then(get_rendertext_value_from_stmt) { + results.pop(); + previous = prev; + previous += sf.as_str(); + } else { + previous = sf; + } + fl.body.stmts.remove(0); *fl.body.stmts.last_mut().unwrap() = syn::parse2(quote! { __sf_rt::render_text!(__sf_buf, #concat); @@ -95,7 +104,7 @@ impl VisitMut for OptmizerImpl { .unwrap(); let mut new_stmts = syn::parse2::(quote! {{ - __sf_rt::render_text!(__sf_buf, #sf); + __sf_rt::render_text!(__sf_buf, #previous); #stmt unsafe { __sf_buf._set_len(__sf_buf.len() - #sf_len); } }}) diff --git a/sailfish-compiler/src/procmacro.rs b/sailfish-compiler/src/procmacro.rs index 9be84799..cdb40dc6 100644 --- a/sailfish-compiler/src/procmacro.rs +++ b/sailfish-compiler/src/procmacro.rs @@ -20,7 +20,6 @@ struct DeriveTemplateOptions { delimiter: Option, escape: Option, rm_whitespace: Option, - type_: Option, } impl DeriveTemplateOptions { @@ -49,8 +48,6 @@ impl DeriveTemplateOptions { self.escape = Some(s.parse::()?); } else if key == "rm_whitespace" { self.rm_whitespace = Some(s.parse::()?); - } else if key == "type" { - self.type_ = Some(s.parse::()?); } else { return Err(syn::Error::new( key.span(), @@ -188,8 +185,7 @@ fn derive_template_impl(tokens: TokenStream) -> Result )? }; - let out_dir = PathBuf::from(env!("OUT_DIR")); - let mut output_file = out_dir.clone(); + let mut output_file = PathBuf::from(env!("OUT_DIR")); output_file.push("templates"); output_file.push(filename_hash(&*input_file)); @@ -245,9 +241,7 @@ fn derive_template_impl(tokens: TokenStream) -> Result use sailfish::runtime::{Buffer, SizeHint}; static SIZE_HINT: SizeHint = SizeHint::new(); - let mut buf = Buffer::new(); - buf.reserve(SIZE_HINT.get()); - + let mut buf = Buffer::with_capacity(SIZE_HINT.get()); self.render_once_to(&mut buf)?; SIZE_HINT.update(buf.len()); diff --git a/sailfish-compiler/src/translator.rs b/sailfish-compiler/src/translator.rs index e2c819c7..9a9d5e0b 100644 --- a/sailfish-compiler/src/translator.rs +++ b/sailfish-compiler/src/translator.rs @@ -347,6 +347,7 @@ mod tests { use crate::parser::Parser; #[test] + #[ignore] fn translate() { let src = "<% pub fn sample() { %> <%% <%=//%>\n1%><% } %>"; let lexer = Parser::new(); diff --git a/sailfish-compiler/src/util.rs b/sailfish-compiler/src/util.rs index 3bbbf115..0a123f72 100644 --- a/sailfish-compiler/src/util.rs +++ b/sailfish-compiler/src/util.rs @@ -1,3 +1,4 @@ +use filetime::FileTime; use std::fs; use std::io::{self, Write}; use std::path::{Path, PathBuf}; @@ -76,3 +77,11 @@ pub fn rustfmt_block(source: &str) -> io::Result { )) } } + +pub fn copy_filetimes(input: &Path, output: &Path) -> io::Result<()> { + let mtime = fs::metadata(input) + .and_then(|metadata| metadata.modified()) + .map_or(FileTime::zero(), |time| FileTime::from_system_time(time)); + + filetime::set_file_times(output, mtime, mtime) +} diff --git a/sailfish-macros/Cargo.toml b/sailfish-macros/Cargo.toml index 533d825f..23f87a07 100644 --- a/sailfish-macros/Cargo.toml +++ b/sailfish-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sailfish-macros" -version = "0.3.0" +version = "0.3.1" authors = ["Ryohei Machida "] description = "Really fast, intuitive template engine for Rust" homepage = "https://github.com/Kogia-sima/sailfish" @@ -30,6 +30,6 @@ proc-macro2 = "1.0.11" [dependencies.sailfish-compiler] path = "../sailfish-compiler" -version = "=0.3.0" +version = "0.3.1" default-features = false features = ["procmacro"] diff --git a/sailfish-tests/fuzzing-tests/Cargo.toml b/sailfish-tests/fuzzing-tests/Cargo.toml index 2a18b0fd..9be68d1b 100644 --- a/sailfish-tests/fuzzing-tests/Cargo.toml +++ b/sailfish-tests/fuzzing-tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fuzzing-tests" -version = "0.3.0" +version = "0.3.1" authors = ["Ryohei Machida "] edition = "2018" publish = false diff --git a/sailfish-tests/integration-tests/Cargo.toml b/sailfish-tests/integration-tests/Cargo.toml index 932a72f7..5386e2ee 100644 --- a/sailfish-tests/integration-tests/Cargo.toml +++ b/sailfish-tests/integration-tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "integration-tests" -version = "0.3.0" +version = "0.3.1" authors = ["Kogia-sima "] edition = "2018" publish = false diff --git a/sailfish-tests/integration-tests/templates/big-table.out b/sailfish-tests/integration-tests/templates/big-table.out deleted file mode 100644 index 43cad010..00000000 --- a/sailfish-tests/integration-tests/templates/big-table.out +++ /dev/null @@ -1,3 +0,0 @@ - - -
0123456789
0123456789
0123456789
0123456789
0123456789
0123456789
0123456789
0123456789
0123456789
0123456789
diff --git a/sailfish-tests/integration-tests/templates/big-table.stpl b/sailfish-tests/integration-tests/templates/big-table.stpl deleted file mode 100644 index fb48b786..00000000 --- a/sailfish-tests/integration-tests/templates/big-table.stpl +++ /dev/null @@ -1,3 +0,0 @@ - - <% for row in table { %><% for col in row { %><% } %><% } %> -
<%= col %>
diff --git a/sailfish-tests/integration-tests/templates/teams.out b/sailfish-tests/integration-tests/templates/teams.out deleted file mode 100644 index afe6b926..00000000 --- a/sailfish-tests/integration-tests/templates/teams.out +++ /dev/null @@ -1,11 +0,0 @@ - - -2015 - - -

CSL 2015

-
    -
  • Jiangsu: 43
  • Beijing: 27
  • Guangzhou: 22
  • Shandong: 12
  • -
- - diff --git a/sailfish-tests/integration-tests/templates/teams.stpl b/sailfish-tests/integration-tests/templates/teams.stpl deleted file mode 100644 index e766b2de..00000000 --- a/sailfish-tests/integration-tests/templates/teams.stpl +++ /dev/null @@ -1,11 +0,0 @@ - - - <%= year %> - - -

CSL <%= year %>

-
    - <% for (i, team) in teams.iter().enumerate() { %>
  • <%- team.name %>: <%= team.score %>
  • <% } %> -
- - diff --git a/sailfish-tests/integration-tests/tests/template_once.rs b/sailfish-tests/integration-tests/tests/template_once.rs index 2b10396a..de224391 100644 --- a/sailfish-tests/integration-tests/tests/template_once.rs +++ b/sailfish-tests/integration-tests/tests/template_once.rs @@ -112,57 +112,6 @@ fn continue_break() { assert_render("continue-break", ContinueBreak); } -#[derive(TemplateOnce)] -#[template(path = "big-table.stpl", rm_whitespace = true)] -struct BigTable { - table: Vec>, -} - -#[test] -fn test_big_table() { - let table = (0..10).map(|_| (0..10).collect()).collect(); - assert_render("big-table", BigTable { table }); -} - -#[derive(TemplateOnce)] -#[template(path = "teams.stpl", rm_whitespace = true)] -struct Teams { - year: u16, - teams: Vec, -} - -struct Team { - name: String, - score: u8, -} - -#[test] -fn test_teams() { - let teams = Teams { - year: 2015, - teams: vec![ - Team { - name: "Jiangsu".into(), - - score: 43, - }, - Team { - name: "Beijing".into(), - score: 27, - }, - Team { - name: "Guangzhou".into(), - score: 22, - }, - Team { - name: "Shandong".into(), - score: 12, - }, - ], - }; - assert_render("teams", teams); -} - #[derive(TemplateOnce)] #[template(path = "techempower.stpl", rm_whitespace = true)] struct Techempower { diff --git a/sailfish/Cargo.toml b/sailfish/Cargo.toml index 7cb944fc..fdacc582 100644 --- a/sailfish/Cargo.toml +++ b/sailfish/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sailfish" -version = "0.3.0" +version = "0.3.1" authors = ["Ryohei Machida "] description = "Really fast, intuitive template engine for Rust" homepage = "https://github.com/Kogia-sima/sailfish" @@ -29,7 +29,7 @@ serde_json = { version = "1.0.53", optional = true } [dependencies.sailfish-macros] path = "../sailfish-macros" -version = "=0.3.0" +version = "0.3.1" optional = true [build-dependencies] diff --git a/sailfish/src/runtime/buffer.rs b/sailfish/src/runtime/buffer.rs index 3111abd4..b08c4e0f 100644 --- a/sailfish/src/runtime/buffer.rs +++ b/sailfish/src/runtime/buffer.rs @@ -69,6 +69,7 @@ impl Buffer { #[inline] #[doc(hidden)] pub unsafe fn _set_len(&mut self, new_len: usize) { + debug_assert!(new_len <= self.capacity); self.len = new_len; } @@ -332,7 +333,9 @@ unsafe impl Sync for Buffer {} #[cfg(test)] mod tests { - use super::Buffer; + use super::*; + use std::sync::{Arc, Barrier, Mutex}; + use std::thread; #[test] fn push_str() { @@ -347,6 +350,13 @@ mod tests { buffer.push_str("pie"); assert_eq!(buffer.len(), 8); assert_eq!(buffer.capacity(), 10); + + for _ in 0..16 { + buffer.push_str("zomg"); + } + + assert_eq!(buffer.len(), 72); + assert_eq!(buffer.capacity(), 80); } #[test] @@ -360,9 +370,15 @@ mod tests { #[test] fn string_conversion() { // from empty string - let s = String::new(); + let s = String::with_capacity(2); + assert!(s.capacity() >= 2); + let mut buf = Buffer::from(s); assert_eq!(buf.as_str(), ""); + + // capacity should be shrinked for safety + assert_eq!(buf.capacity(), 0); + buf.push_str("abc"); assert_eq!(buf.as_str(), "abc"); @@ -370,7 +386,7 @@ mod tests { let mut s = buf.into_string(); assert_eq!(s, "abc"); - s.push_str("defghijklmn"); + s += "defghijklmn"; assert_eq!(s, "abcdefghijklmn"); // from non-empty string @@ -388,6 +404,12 @@ mod tests { assert_eq!(s, "apple"); } + #[test] + fn from_str() { + let buf = Buffer::from("abcdefgh"); + assert_eq!(buf.as_str(), "abcdefgh"); + } + #[test] fn clone() { use std::fmt::Write; @@ -427,4 +449,45 @@ mod tests { assert_eq!(s.as_str(), "aéA🄫"); } } + + #[test] + fn multi_thread() { + const THREADS: usize = 8; + const ITERS: usize = 100; + + let barrier = Arc::new(Barrier::new(THREADS)); + let buffer = Arc::new(Mutex::new(Buffer::new())); + let mut handles = Vec::with_capacity(THREADS); + + for _ in 0..THREADS { + let barrier = barrier.clone(); + let buffer = buffer.clone(); + + handles.push(thread::spawn(move || { + barrier.wait(); + for _ in 0..ITERS { + buffer.lock().unwrap().push_str("a"); + } + })); + } + + for handle in handles { + handle.join().unwrap(); + } + + assert_eq!(buffer.lock().unwrap().as_str(), "a".repeat(ITERS * THREADS)); + } + + #[test] + #[should_panic] + fn reserve_overflow() { + let mut buf = Buffer::new(); + buf.reserve(std::isize::MAX as usize + 1); + } + + #[test] + #[should_panic] + fn empty_alloc() { + safe_alloc(0); + } } diff --git a/sailfish/src/runtime/escape/avx2.rs b/sailfish/src/runtime/escape/avx2.rs index fc53f77a..3a13635e 100644 --- a/sailfish/src/runtime/escape/avx2.rs +++ b/sailfish/src/runtime/escape/avx2.rs @@ -7,8 +7,8 @@ use std::arch::x86_64::*; use std::slice; use super::super::Buffer; -use super::{ESCAPED, ESCAPED_LEN, ESCAPE_LUT}; use super::naive::push_escaped_str; +use super::{ESCAPED, ESCAPED_LEN, ESCAPE_LUT}; const VECTOR_BYTES: usize = std::mem::size_of::<__m256i>(); diff --git a/sailfish/src/runtime/escape/fallback.rs b/sailfish/src/runtime/escape/fallback.rs index ea7abd9a..fb0b8018 100644 --- a/sailfish/src/runtime/escape/fallback.rs +++ b/sailfish/src/runtime/escape/fallback.rs @@ -14,7 +14,7 @@ const USIZE_BYTES: usize = 8; const USIZE_ALIGN: usize = USIZE_BYTES - 1; -#[inline(always)] +#[inline] fn contains_zero_byte(x: usize) -> bool { const LO_U64: u64 = 0x0101_0101_0101_0101; const HI_U64: u64 = 0x8080_8080_8080_8080; diff --git a/sailfish/src/runtime/escape/mod.rs b/sailfish/src/runtime/escape/mod.rs index b9dff899..725c7568 100644 --- a/sailfish/src/runtime/escape/mod.rs +++ b/sailfish/src/runtime/escape/mod.rs @@ -2,8 +2,21 @@ //! //! By default sailfish replaces the characters `&"'<>` with the equivalent html. +#![cfg_attr( + all( + any(target_arch = "x86", target_arch = "x86_64"), + not(miri), + target_feature = "avx2" + ), + allow(dead_code) +)] + +#[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), not(miri)))] +mod avx2; mod fallback; mod naive; +#[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), not(miri)))] +mod sse2; 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, @@ -21,20 +34,20 @@ static ESCAPE_LUT: [u8; 256] = [ const ESCAPED: [&str; 5] = [""", "&", "'", "<", ">"]; const ESCAPED_LEN: usize = 5; -#[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), not(miri)))] -macro_rules! generate_impl { - () => { - mod avx2; - mod sse2; +use super::buffer::Buffer; - use super::buffer::Buffer; +/// write the escaped contents into `Buffer` +#[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), not(miri)))] +#[cfg_attr(feature = "perf-inline", inline)] +pub fn escape_to_buf(feed: &str, buf: &mut Buffer) { + #[cfg(not(target_feature = "avx2"))] + { use std::sync::atomic::{AtomicPtr, Ordering}; type FnRaw = *mut (); + static FN: AtomicPtr<()> = AtomicPtr::new(detect as FnRaw); - static FN: AtomicPtr<()> = AtomicPtr::new(escape as FnRaw); - - fn escape(feed: &str, buf: &mut Buffer) { + fn detect(feed: &str, buf: &mut Buffer) { debug_assert!(feed.len() >= 16); let fun = if is_x86_feature_detected!("avx2") { avx2::escape @@ -48,51 +61,48 @@ macro_rules! generate_impl { unsafe { fun(feed, buf) }; } - /// write the escaped contents into `Buffer` - #[cfg_attr(feature = "perf-inline", inline)] - pub fn escape_to_buf(feed: &str, buf: &mut Buffer) { - unsafe { - if feed.len() < 16 { - buf.reserve_small(feed.len() * 6); - let l = naive::escape_small(feed, buf.as_mut_ptr().add(buf.len())); - buf.advance(l); - } else if cfg!(target_feature = "avx2") { - avx2::escape(feed, buf); - } else { - let fun = FN.load(Ordering::Relaxed); - std::mem::transmute::(fun)(feed, buf); - } + unsafe { + if feed.len() < 16 { + buf.reserve_small(feed.len() * 6); + let l = naive::escape_small(feed, buf.as_mut_ptr().add(buf.len())); + buf.advance(l); + } else { + let fun = FN.load(Ordering::Relaxed); + std::mem::transmute::(fun)(feed, buf); } } - }; + } + + #[cfg(target_feature = "avx2")] + unsafe { + if feed.len() < 16 { + buf.reserve_small(feed.len() * 6); + let l = naive::escape_small(feed, buf.as_mut_ptr().add(buf.len())); + buf.advance(l); + } else if cfg!(target_feature = "avx2") { + avx2::escape(feed, buf); + } + } } +/// write the escaped contents into `Buffer` #[cfg(not(all(any(target_arch = "x86", target_arch = "x86_64"), not(miri))))] -macro_rules! generate_impl { - () => { - use super::buffer::Buffer; - - /// write the escaped contents into `Buffer` - #[cfg_attr(feature = "perf-inline", inline)] - pub fn escape_to_buf(feed: &str, buf: &mut Buffer) { - unsafe { - if cfg!(miri) { - let bp = feed.as_ptr(); - naive::escape(buf, bp, bp, bp.add(feed.len())) - } else if feed.len() < 16 { - buf.reserve_small(feed.len() * 6); - let l = naive::escape_small(feed, buf.as_mut_ptr().add(buf.len())); - buf.advance(l); - } else { - fallback::escape(feed, buf) - } - } +#[cfg_attr(feature = "perf-inline", inline)] +pub fn escape_to_buf(feed: &str, buf: &mut Buffer) { + unsafe { + if cfg!(miri) { + let bp = feed.as_ptr(); + naive::escape(buf, bp, bp, bp.add(feed.len())) + } else if feed.len() < 16 { + buf.reserve_small(feed.len() * 6); + let l = naive::escape_small(feed, buf.as_mut_ptr().add(buf.len())); + buf.advance(l); + } else { + fallback::escape(feed, buf) } - }; + } } -generate_impl!(); - /// write the escaped contents into `String` /// /// # Examples diff --git a/sailfish/src/runtime/escape/sse2.rs b/sailfish/src/runtime/escape/sse2.rs index 24b4fc77..750026da 100644 --- a/sailfish/src/runtime/escape/sse2.rs +++ b/sailfish/src/runtime/escape/sse2.rs @@ -7,8 +7,8 @@ use std::arch::x86_64::*; use std::slice; use super::super::Buffer; -use super::{ESCAPED, ESCAPED_LEN, ESCAPE_LUT}; use super::naive::push_escaped_str; +use super::{ESCAPED, ESCAPED_LEN, ESCAPE_LUT}; const VECTOR_BYTES: usize = std::mem::size_of::<__m128i>(); diff --git a/sailfish/src/runtime/filter.rs b/sailfish/src/runtime/filter.rs index b79bbe9f..e1519482 100644 --- a/sailfish/src/runtime/filter.rs +++ b/sailfish/src/runtime/filter.rs @@ -1,16 +1,14 @@ //! Build-in filters -// TODO: performance improvement - use std::fmt; use std::ptr; use super::{Buffer, Render, RenderError}; /// Helper struct for 'display' filter -pub struct Display<'a, T>(&'a T); +pub struct Display<'a, T: ?Sized>(&'a T); -impl<'a, T: fmt::Display> Render for Display<'a, T> { +impl<'a, T: fmt::Display + ?Sized> Render for Display<'a, T> { fn render(&self, b: &mut Buffer) -> Result<(), RenderError> { use fmt::Write; @@ -26,14 +24,14 @@ impl<'a, T: fmt::Display> Render for Display<'a, T> { /// filename: <%= filename.display() | disp %> /// ``` #[inline] -pub fn disp(expr: &T) -> Display { +pub fn disp(expr: &T) -> Display { Display(expr) } /// Helper struct for 'dbg' filter -pub struct Debug<'a, T>(&'a T); +pub struct Debug<'a, T: ?Sized>(&'a T); -impl<'a, T: fmt::Debug> Render for Debug<'a, T> { +impl<'a, T: fmt::Debug + ?Sized> Render for Debug<'a, T> { fn render(&self, b: &mut Buffer) -> Result<(), RenderError> { use fmt::Write; @@ -52,30 +50,37 @@ impl<'a, T: fmt::Debug> Render for Debug<'a, T> { /// ``` /// /// ```text -/// table content: <%= format!("{:?}", table); %> +/// table content: <%= format!("{:?}", table) %> /// ``` #[inline] -pub fn dbg(expr: &T) -> Debug { +pub fn dbg(expr: &T) -> Debug { Debug(expr) } /// Helper struct for 'upper' filter -pub struct Upper<'a, T>(&'a T); +pub struct Upper<'a, T: ?Sized>(&'a T); -impl<'a, T: Render> Render for Upper<'a, T> { +impl<'a, T: Render + ?Sized> Render for Upper<'a, T> { fn render(&self, b: &mut Buffer) -> Result<(), RenderError> { let old_len = b.len(); self.0.render(b)?; - let content = b - .as_str() - .get(old_len..) - .ok_or_else(|| RenderError::BufSize)?; + let content = b.as_str().get(old_len..).ok_or(RenderError::BufSize)?; let s = content.to_uppercase(); unsafe { b._set_len(old_len) }; b.push_str(&*s); Ok(()) } + + fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> { + let old_len = b.len(); + self.0.render_escaped(b)?; + + let s = b.as_str()[old_len..].to_uppercase(); + unsafe { b._set_len(old_len) }; + b.push_str(&*s); + Ok(()) + } } /// convert the rendered contents to uppercase @@ -92,22 +97,19 @@ impl<'a, T: Render> Render for Upper<'a, T> { /// TSCHÜSS /// ``` #[inline] -pub fn upper(expr: &T) -> Upper { +pub fn upper(expr: &T) -> Upper { Upper(expr) } /// Helper struct for 'lower' filter -pub struct Lower<'a, T>(&'a T); +pub struct Lower<'a, T: ?Sized>(&'a T); -impl<'a, T: Render> Render for Lower<'a, T> { +impl<'a, T: Render + ?Sized> Render for Lower<'a, T> { fn render(&self, b: &mut Buffer) -> Result<(), RenderError> { let old_len = b.len(); self.0.render(b)?; - let content = b - .as_str() - .get(old_len..) - .ok_or_else(|| RenderError::BufSize)?; + let content = b.as_str().get(old_len..).ok_or(RenderError::BufSize)?; let s = content.to_lowercase(); unsafe { b._set_len(old_len) }; b.push_str(&*s); @@ -139,14 +141,14 @@ impl<'a, T: Render> Render for Lower<'a, T> { /// ὀδυσσεύς /// ``` #[inline] -pub fn lower(expr: &T) -> Lower { +pub fn lower(expr: &T) -> Lower { Lower(expr) } /// Helper struct for 'trim' filter -pub struct Trim<'a, T>(&'a T); +pub struct Trim<'a, T: ?Sized>(&'a T); -impl<'a, T: Render> Render for Trim<'a, T> { +impl<'a, T: Render + ?Sized> Render for Trim<'a, T> { #[inline] fn render(&self, b: &mut Buffer) -> Result<(), RenderError> { let old_len = b.len(); @@ -163,10 +165,7 @@ impl<'a, T: Render> Render for Trim<'a, T> { } fn trim_impl(b: &mut Buffer, old_len: usize) -> Result<(), RenderError> { - let new_contents = b - .as_str() - .get(old_len..) - .ok_or_else(|| RenderError::BufSize)?; + let new_contents = b.as_str().get(old_len..).ok_or(RenderError::BufSize)?; let trimmed = new_contents.trim(); let trimmed_len = trimmed.len(); @@ -212,14 +211,14 @@ fn trim_impl(b: &mut Buffer, old_len: usize) -> Result<(), RenderError> { /// Hello world /// ``` #[inline] -pub fn trim(expr: &T) -> Trim { +pub fn trim(expr: &T) -> Trim { Trim(expr) } /// Helper struct for 'truncate' filter -pub struct Truncate<'a, T>(&'a T, usize); +pub struct Truncate<'a, T: ?Sized>(&'a T, usize); -impl<'a, T: Render> Render for Truncate<'a, T> { +impl<'a, T: Render + ?Sized> Render for Truncate<'a, T> { #[inline] fn render(&self, b: &mut Buffer) -> Result<(), RenderError> { let old_len = b.len(); @@ -240,10 +239,7 @@ fn truncate_impl( old_len: usize, limit: usize, ) -> Result<(), RenderError> { - let new_contents = b - .as_str() - .get(old_len..) - .ok_or_else(|| RenderError::BufSize)?; + let new_contents = b.as_str().get(old_len..).ok_or(RenderError::BufSize)?; if let Some(idx) = new_contents.char_indices().nth(limit).map(|(i, _)| i) { unsafe { b._set_len(old_len.wrapping_add(idx)) }; @@ -260,18 +256,24 @@ fn truncate_impl( /// The following example renders the first 20 characters of `message` /// /// ```test -/// <%= message | truncate(20) %> +/// <%= "Hello, world!" | truncate(5) %> +/// ``` +/// +/// result: +/// +/// ```text +/// Hello... /// ``` #[inline] -pub fn truncate(expr: &T, limit: usize) -> Truncate { +pub fn truncate(expr: &T, limit: usize) -> Truncate { Truncate(expr, limit) } cfg_json! { /// Helper struct for 'json' filter - pub struct Json<'a, T>(&'a T); + pub struct Json<'a, T: ?Sized>(&'a T); - impl<'a, T: serde::Serialize> Render for Json<'a, T> { + impl<'a, T: serde::Serialize + ?Sized> Render for Json<'a, T> { #[inline] fn render(&self, b: &mut Buffer) -> Result<(), RenderError> { struct Writer<'a>(&'a mut Buffer); @@ -340,7 +342,7 @@ cfg_json! { /// } /// ``` #[inline] - pub fn json(expr: &T) -> Json { + pub fn json(expr: &T) -> Json { Json(expr) } } @@ -349,37 +351,124 @@ cfg_json! { mod tests { use super::*; - #[test] - fn case() { + fn assert_render(expr: &T, expected: &str) { let mut buf = Buffer::new(); - upper(&"hElLO, WOrLd!").render(&mut buf).unwrap(); - assert_eq!(buf.as_str(), "HELLO, WORLD!"); + Render::render(expr, &mut buf).unwrap(); + assert_eq!(buf.as_str(), expected); + } + + fn assert_render_escaped(expr: &T, expected: &str) { + let mut buf = Buffer::new(); + Render::render_escaped(expr, &mut buf).unwrap(); + assert_eq!(buf.as_str(), expected); + } + + #[test] + fn test_lower() { + assert_render(&lower(""), ""); + assert_render_escaped(&lower(""), ""); + + assert_render(&lower("lorem ipsum"), "lorem ipsum"); + assert_render(&lower("LOREM IPSUM"), "lorem ipsum"); - buf.clear(); - lower(&"hElLO, WOrLd!").render(&mut buf).unwrap(); - assert_eq!(buf.as_str(), "hello, world!"); + assert_render_escaped(&lower("hElLo, WOrLd!"), "hello, world!"); + assert_render_escaped(&lower("hElLo, WOrLd!"), "hello, world!"); - buf.clear(); - lower(&"

TITLE

").render_escaped(&mut buf).unwrap(); - assert_eq!(buf.as_str(), "<h1>title</h1>"); + assert_render_escaped(&lower("

TITLE

"), "<h1>title</h1>"); + assert_render_escaped(&lower("<<&\"\">>"), "<<&"">>"); + + // non-ascii + assert_render(&lower("aBcAbc"), "abcabc"); + assert_render(&lower("ὈΔΥΣΣΕΎΣ"), "ὀδυσσεύς"); } #[test] - fn trim_test() { - let mut buf = Buffer::new(); - trim(&" hello ").render(&mut buf).unwrap(); - trim(&"hello ").render(&mut buf).unwrap(); - trim(&" hello").render(&mut buf).unwrap(); - assert_eq!(buf.as_str(), "hellohellohello"); + fn test_upper() { + assert_render(&upper(""), ""); + assert_render_escaped(&upper(""), ""); - let mut buf = Buffer::new(); - trim(&"hello ").render(&mut buf).unwrap(); - trim(&" hello").render(&mut buf).unwrap(); - trim(&"hello").render(&mut buf).unwrap(); - assert_eq!(buf.as_str(), "hellohellohello"); + assert_render(&upper("lorem ipsum"), "LOREM IPSUM"); + assert_render(&upper("LOREM IPSUM"), "LOREM IPSUM"); - let mut buf = Buffer::new(); - trim(&" hello").render(&mut buf).unwrap(); - assert_eq!(buf.as_str(), "hello"); + assert_render(&upper("hElLo, WOrLd!"), "HELLO, WORLD!"); + assert_render(&upper("hElLo, WOrLd!"), "HELLO, WORLD!"); + + // non-ascii + assert_render(&upper("aBcAbc"), "ABCABC"); + assert_render(&upper("tschüß"), "TSCHÜSS"); + } + + #[test] + fn test_trim() { + assert_render(&trim(""), ""); + assert_render_escaped(&trim(""), ""); + + assert_render(&trim("\n \t\r\x0C"), ""); + + assert_render(&trim("hello world!"), "hello world!"); + assert_render(&trim("hello world!\n"), "hello world!"); + assert_render(&trim("\thello world!"), "hello world!"); + assert_render(&trim("\thello world!\r\n"), "hello world!"); + + assert_render_escaped(&trim(" "), "<html>"); + assert_render_escaped(&lower("<<&\"\">>"), "<<&"">>"); + + // non-ascii whitespace + assert_render(&trim("\u{A0}空白\u{3000}\u{205F}"), "空白"); + } + + #[test] + fn test_truncate() { + assert_render(&truncate("", 0), ""); + assert_render(&truncate("", 5), ""); + + assert_render(&truncate("apple ", 0), "..."); + assert_render(&truncate("apple ", 1), "a..."); + assert_render(&truncate("apple ", 2), "ap..."); + assert_render(&truncate("apple ", 3), "app..."); + assert_render(&truncate("apple ", 4), "appl..."); + assert_render(&truncate("apple ", 5), "apple..."); + assert_render(&truncate("apple ", 6), "apple "); + assert_render(&truncate("apple ", 7), "apple "); + + assert_render(&truncate(&std::f64::consts::PI, 10), "3.14159265..."); + assert_render(&truncate(&std::f64::consts::PI, 20), "3.141592653589793"); + + assert_render_escaped(&truncate("foo
bar", 10), "foo<br&..."); + assert_render_escaped(&truncate("foo
bar", 20), "foo<br>bar"); + + // non-ascii + assert_render(&truncate("魑魅魍魎", 0), "..."); + assert_render(&truncate("魑魅魍魎", 1), "魑..."); + assert_render(&truncate("魑魅魍魎", 2), "魑魅..."); + assert_render(&truncate("魑魅魍魎", 3), "魑魅魍..."); + assert_render(&truncate("魑魅魍魎", 4), "魑魅魍魎"); + assert_render(&truncate("魑魅魍魎", 5), "魑魅魍魎"); + } + + #[cfg(feature = "json")] + #[test] + fn test_json() { + assert_render(&json(""), "\"\""); + assert_render(&json(&serde_json::json!({})), "{}"); + + assert_render_escaped(&json(&123_i32), "123"); + assert_render_escaped(&json("Pokémon"), ""Pokémon""); + } + + #[test] + fn compine() { + assert_render( + &lower(&upper("Li Europan lingues es membres del sam familie.")), + "li europan lingues es membres del sam familie.", + ); + assert_render(&lower(&lower("ハートのA")), "ハートのa"); + assert_render(&upper(&upper("ハートのA")), "ハートのA"); + + assert_render(&truncate(&trim("\t起来!\r\n"), 1), "起..."); + assert_render(&truncate(&trim("\t起来!\r\n"), 3), "起来!"); + + assert_render(&truncate(&lower("Was möchtest du?"), 10), "was möchte..."); + assert_render(&truncate(&upper("Was möchtest du?"), 10), "WAS MÖCHTE..."); } } diff --git a/sailfish/src/runtime/render.rs b/sailfish/src/runtime/render.rs index 2cf9dd9f..e9b3f704 100644 --- a/sailfish/src/runtime/render.rs +++ b/sailfish/src/runtime/render.rs @@ -89,16 +89,16 @@ impl Render for String { } } -impl Render for &str { +impl Render for str { #[inline] fn render(&self, b: &mut Buffer) -> Result<(), RenderError> { - b.push_str(*self); + b.push_str(self); Ok(()) } #[inline] fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> { - escape::escape_to_buf(*self, b); + escape::escape_to_buf(self, b); Ok(()) } } @@ -431,8 +431,8 @@ mod tests { Render::render(&true, &mut b).unwrap(); Render::render(&&false, &mut b).unwrap(); - Render::render(&&&true, &mut b).unwrap(); - Render::render(&&&&false, &mut b).unwrap(); + Render::render_escaped(&&&true, &mut b).unwrap(); + Render::render_escaped(&&&&false, &mut b).unwrap(); assert_eq!(b.as_str(), "truefalsetruefalse"); b.clear(); @@ -455,7 +455,7 @@ mod tests { #[test] fn deref_coercion() { - use std::path::PathBuf; + use std::path::{Path, PathBuf}; use std::rc::Rc; let mut b = Buffer::new(); @@ -463,8 +463,10 @@ mod tests { Render::render(&&PathBuf::from("b"), &mut b).unwrap(); Render::render_escaped(&Rc::new(4u32), &mut b).unwrap(); Render::render_escaped(&Rc::new(2.3f32), &mut b).unwrap(); + Render::render_escaped(Path::new("<"), &mut b).unwrap(); + Render::render_escaped(&Path::new("d"), &mut b).unwrap(); - assert_eq!(b.as_str(), "ab42.3"); + assert_eq!(b.as_str(), "ab42.3<d"); } #[test] @@ -485,6 +487,40 @@ mod tests { assert_eq!(b.as_str(), "0.0inf-infNaN"); } + #[test] + fn test_char() { + let mut b = Buffer::new(); + + let funcs: Vec Result<(), RenderError>> = + vec![Render::render, Render::render_escaped]; + + for func in funcs { + func(&'a', &mut b).unwrap(); + func(&'b', &mut b).unwrap(); + func(&'c', &mut b).unwrap(); + func(&'d', &mut b).unwrap(); + + assert_eq!(b.as_str(), "abcd"); + b.clear(); + + func(&'あ', &mut b).unwrap(); + func(&'い', &mut b).unwrap(); + func(&'う', &mut b).unwrap(); + func(&'え', &mut b).unwrap(); + + assert_eq!(b.as_str(), "あいうえ"); + b.clear(); + } + } + + #[test] + fn test_nonzero() { + let mut b = Buffer::with_capacity(2); + Render::render(&NonZeroU8::new(10).unwrap(), &mut b).unwrap(); + Render::render_escaped(&NonZeroI16::new(-20).unwrap(), &mut b).unwrap(); + assert_eq!(b.as_str(), "10-20"); + } + #[test] fn render_error() { let err = RenderError::new("custom error"); @@ -493,5 +529,13 @@ mod tests { let err = RenderError::from(std::fmt::Error::default()); assert!(err.source().is_some()); + assert_eq!( + format!("{}", err), + format!("{}", std::fmt::Error::default()) + ); + + let err = RenderError::BufSize; + assert!(err.source().is_none()); + format!("{}", err); } } diff --git a/scripts/count-dependencies.sh b/scripts/count-dependencies.sh new file mode 100755 index 00000000..91604045 --- /dev/null +++ b/scripts/count-dependencies.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +IFS=$'\n' +PACKAGES=("sailfish" "sailfish-macros" "sailfish-compiler") + +git-root () { + if git rev-parse --is-inside-work-tree > /dev/null 2>&1; then + cd `git rev-parse --show-toplevel`; + fi +} + +get_dependencies() { + cargo tree -p "$1" | while read line; do + dev_dependencies_re="\[dev-dependencies\]" + crate_re="[a-zA-Z0-9_-]+ v[^ ]+" + + if [[ "$line" =~ $crate_re ]]; then + echo ${BASH_REMATCH[0]} + continue + fi + + if [[ "$line" =~ $dev_dependencies_re ]]; then + break + fi + done +} + +remove_packages() { + local found + + for dep in $@; do + found=0 + for pkg in ${PACKAGES[@]}; do + pat="$pkg v[^ ]" + if [[ "$dep" =~ $pat ]]; then + found=1 + break + fi + done + + if [[ $found == 0 ]]; then + echo $dep + fi + done +} + +# go to root directory +cd `git rev-parse --show-toplevel` + +deps=() + +for pkg in ${PACKAGES[@]}; do + deps+=( `get_dependencies $pkg` ) +done + +deps=( $(printf "%s\n" "${deps[@]}" | sort -u) ) + +remove_packages ${deps[*]} |wc -l