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
[](https://github.com/Kogia-sima/sailfish/actions?query=workflow%3ATests)
[](https://crates.io/crates/sailfish)
+
+[](https://deps.rs/repo/github/Kogia-sima/sailfish)
+[](https://blog.rust-lang.org/2020/03/12/Rust-1.42.html)
[](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:
+- 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:
+- 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.
+- 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 @@
-
-0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
-
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