Merge branch 'master' into stable

This commit is contained in:
Kogia-sima 2021-01-23 19:29:03 +09:00
commit c226f4bb7f
45 changed files with 823 additions and 332 deletions

22
.github/stale.yml vendored Normal file
View File

@ -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

29
.github/workflows/badge.yml vendored Normal file
View File

@ -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

41
.github/workflows/coverage.yml vendored Normal file
View File

@ -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

View File

@ -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

View File

@ -1,12 +1,48 @@
<a name="v0.3.0"></a>
## [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`
<a name="v0.2.3"></a>
## [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)
<a name="v0.2.2"></a>
## [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)
<a name="v0.2.1"></a>
## [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`

48
Cargo.lock generated
View File

@ -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",

View File

@ -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

30
THIRD_PARTY Normal file
View File

@ -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.

View File

@ -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

15
codecov.yml Normal file
View File

@ -0,0 +1,15 @@
coverage:
status:
project:
default:
threshold: 3%
branches:
- master
only_pulls: false
patch:
default:
target: auto
branches:
- master
only_pulls: true

View File

@ -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;
}

View File

@ -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
<html>
<body>
<% for msg in &messages { %>
@ -26,16 +26,13 @@ templates/
## Render the template
Import the sailfish crates:
<ol><li>Import the sailfish crates:</li></ol>
```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:
<ol start="2"><li>Define the template struct to be rendered:</li></ol>
```rust
#[derive(TemplateOnce)] // automatically implement `TemplateOnce` trait
@ -46,7 +43,7 @@ struct HelloTemplate {
}
```
And render the data with `render_once()` method.
<ol start="3"><li>Render the data with <code>render_once()</code> method.</li></ol>
```rust
fn main() {

View File

@ -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!

View File

@ -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)|

View File

@ -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: "%"

View File

@ -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: &quot;foo\nbar&quot;
```
=== "Result"
``` html
message: &quot;foo\nbar&quot;
```
!!! Note
Since `dbg` filter accepts '<T: std::fmt::Debug>' 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 `<T: std::fmt::Debug>` 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 %>
```

View File

@ -6,7 +6,7 @@ Consider the following example.
- `templates/header.stpl`
```html
``` html
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="format-detection" content="telephone=no">
@ -15,7 +15,7 @@ Consider the following example.
- `templates/index.stpl`
```html
``` rhtml
<html>
<head>
<% 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
<html>
<head>
<meta charset="UTF-8">

View File

@ -10,7 +10,7 @@
## Condition
```ejs
``` rhtml
<% if messages.is_empty() { %>
<div>No messages</div>
<% } %>
@ -18,7 +18,7 @@
## loop
```ejs
``` rhtml
<% for (i, msg) in messages.iter().enumerate() { %>
<div><%= i %>: <%= msg %></div>
<% } %>
@ -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 %>
}
```

View File

@ -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;
}
}
%>
<div>total = <%= total %></div>
```
=== "Result"
``` html
<div>total = 105</div>
```
!!! 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
<div>
<%- "<h1>Hello, World!</h1>" %>
</div>
```
=== "Template"
This template results in the following output.
``` rhtml
<div>
<%- "<h1>Hello, World!</h1>" %>
</div>
```
```ejs
<div>
<h1>Hello, World!</h1>
</div>
```
=== "Result"
``` html
<div>
<h1>Hello, World!</h1>
</div>
```
!!! 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 %><% ; %>
```

View File

@ -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:

View File

@ -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.

View File

@ -1,6 +1,6 @@
[package]
name = "sailfish-examples"
version = "0.3.0"
version = "0.3.1"
authors = ["Ryohei Machida <orcinus4627@gmail.com>"]
edition = "2018"
publish = false

View File

@ -1,6 +1,6 @@
[package]
name = "sailfish-compiler"
version = "0.3.0"
version = "0.3.1"
authors = ["Ryohei Machida <orcinus4627@gmail.com>"]
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"

View File

@ -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)
};

View File

@ -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::<Block>(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); }
}})

View File

@ -20,7 +20,6 @@ struct DeriveTemplateOptions {
delimiter: Option<LitChar>,
escape: Option<LitBool>,
rm_whitespace: Option<LitBool>,
type_: Option<LitStr>,
}
impl DeriveTemplateOptions {
@ -49,8 +48,6 @@ impl DeriveTemplateOptions {
self.escape = Some(s.parse::<LitBool>()?);
} else if key == "rm_whitespace" {
self.rm_whitespace = Some(s.parse::<LitBool>()?);
} else if key == "type" {
self.type_ = Some(s.parse::<LitStr>()?);
} else {
return Err(syn::Error::new(
key.span(),
@ -188,8 +185,7 @@ fn derive_template_impl(tokens: TokenStream) -> Result<TokenStream, syn::Error>
)?
};
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<TokenStream, syn::Error>
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());

View File

@ -347,6 +347,7 @@ mod tests {
use crate::parser::Parser;
#[test]
#[ignore]
fn translate() {
let src = "<% pub fn sample() { %> <%% <%=//%>\n1%><% } %>";
let lexer = Parser::new();

View File

@ -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<String> {
))
}
}
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)
}

View File

@ -1,6 +1,6 @@
[package]
name = "sailfish-macros"
version = "0.3.0"
version = "0.3.1"
authors = ["Ryohei Machida <orcinus4627@gmail.com>"]
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"]

View File

@ -1,6 +1,6 @@
[package]
name = "fuzzing-tests"
version = "0.3.0"
version = "0.3.1"
authors = ["Ryohei Machida <orcinus4627@gmail.com>"]
edition = "2018"
publish = false

View File

@ -1,6 +1,6 @@
[package]
name = "integration-tests"
version = "0.3.0"
version = "0.3.1"
authors = ["Kogia-sima <orcinus4627@gmail.com>"]
edition = "2018"
publish = false

View File

@ -1,3 +0,0 @@
<table>
<tr><td>0</td><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>6</td><td>7</td><td>8</td><td>9</td></tr><tr><td>0</td><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>6</td><td>7</td><td>8</td><td>9</td></tr><tr><td>0</td><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>6</td><td>7</td><td>8</td><td>9</td></tr><tr><td>0</td><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>6</td><td>7</td><td>8</td><td>9</td></tr><tr><td>0</td><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>6</td><td>7</td><td>8</td><td>9</td></tr><tr><td>0</td><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>6</td><td>7</td><td>8</td><td>9</td></tr><tr><td>0</td><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>6</td><td>7</td><td>8</td><td>9</td></tr><tr><td>0</td><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>6</td><td>7</td><td>8</td><td>9</td></tr><tr><td>0</td><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>6</td><td>7</td><td>8</td><td>9</td></tr><tr><td>0</td><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>6</td><td>7</td><td>8</td><td>9</td></tr>
</table>

View File

@ -1,3 +0,0 @@
<table>
<% for row in table { %><tr><% for col in row { %><td><%= col %></td><% } %></tr><% } %>
</table>

View File

@ -1,11 +0,0 @@
<html>
<head>
<title>2015</title>
</head>
<body>
<h1>CSL 2015</h1>
<ul>
<li class="champion"><b>Jiangsu</b>: 43</li><li class=""><b>Beijing</b>: 27</li><li class=""><b>Guangzhou</b>: 22</li><li class=""><b>Shandong</b>: 12</li>
</ul>
</body>
</html>

View File

@ -1,11 +0,0 @@
<html>
<head>
<title><%= year %></title>
</head>
<body>
<h1>CSL <%= year %></h1>
<ul>
<% for (i, team) in teams.iter().enumerate() { %><li class="<% if i == 0 { %>champion<% } %>"><b><%- team.name %></b>: <%= team.score %></li><% } %>
</ul>
</body>
</html>

View File

@ -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<Vec<usize>>,
}
#[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<Team>,
}
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 {

View File

@ -1,6 +1,6 @@
[package]
name = "sailfish"
version = "0.3.0"
version = "0.3.1"
authors = ["Ryohei Machida <orcinus4627@gmail.com>"]
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]

View File

@ -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(), "🄫");
}
}
#[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);
}
}

View File

@ -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>();

View File

@ -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;

View File

@ -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] = ["&quot;", "&amp;", "&#039;", "&lt;", "&gt;"];
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::<FnRaw, fn(&str, &mut Buffer)>(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::<FnRaw, fn(&str, &mut Buffer)>(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

View File

@ -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>();

View File

@ -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<T: fmt::Display>(expr: &T) -> Display<T> {
pub fn disp<T: fmt::Display + ?Sized>(expr: &T) -> Display<T> {
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<T: fmt::Debug>(expr: &T) -> Debug<T> {
pub fn dbg<T: fmt::Debug + ?Sized>(expr: &T) -> Debug<T> {
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<T: Render>(expr: &T) -> Upper<T> {
pub fn upper<T: Render + ?Sized>(expr: &T) -> Upper<T> {
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<T: Render>(expr: &T) -> Lower<T> {
pub fn lower<T: Render + ?Sized>(expr: &T) -> Lower<T> {
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<T: Render>(expr: &T) -> Trim<T> {
pub fn trim<T: Render + ?Sized>(expr: &T) -> Trim<T> {
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<T: Render>(expr: &T, limit: usize) -> Truncate<T> {
pub fn truncate<T: Render + ?Sized>(expr: &T, limit: usize) -> Truncate<T> {
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<T: serde::Serialize>(expr: &T) -> Json<T> {
pub fn json<T: serde::Serialize + ?Sized>(expr: &T) -> Json<T> {
Json(expr)
}
}
@ -349,37 +351,124 @@ cfg_json! {
mod tests {
use super::*;
#[test]
fn case() {
fn assert_render<T: 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);
}
buf.clear();
lower(&"hElLO, WOrLd!").render(&mut buf).unwrap();
assert_eq!(buf.as_str(), "hello, world!");
buf.clear();
lower(&"<h1>TITLE</h1>").render_escaped(&mut buf).unwrap();
assert_eq!(buf.as_str(), "&lt;h1&gt;title&lt;/h1&gt;");
fn assert_render_escaped<T: Render>(expr: &T, expected: &str) {
let mut buf = Buffer::new();
Render::render_escaped(expr, &mut buf).unwrap();
assert_eq!(buf.as_str(), expected);
}
#[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_lower() {
assert_render(&lower(""), "");
assert_render_escaped(&lower(""), "");
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(&lower("lorem ipsum"), "lorem ipsum");
assert_render(&lower("LOREM IPSUM"), "lorem ipsum");
let mut buf = Buffer::new();
trim(&" hello").render(&mut buf).unwrap();
assert_eq!(buf.as_str(), "hello");
assert_render_escaped(&lower("hElLo, WOrLd!"), "hello, world!");
assert_render_escaped(&lower("hElLo, WOrLd!"), "hello, world!");
assert_render_escaped(&lower("<h1>TITLE</h1>"), "&lt;h1&gt;title&lt;/h1&gt;");
assert_render_escaped(&lower("<<&\"\">>"), "&lt;&lt;&amp;&quot;&quot;&gt;&gt;");
// non-ascii
assert_render(&lower("aBc"), "abc");
assert_render(&lower("ὈΔΥΣΣΕΎΣ"), "ὀδυσσεύς");
}
#[test]
fn test_upper() {
assert_render(&upper(""), "");
assert_render_escaped(&upper(""), "");
assert_render(&upper("lorem ipsum"), "LOREM IPSUM");
assert_render(&upper("LOREM IPSUM"), "LOREM IPSUM");
assert_render(&upper("hElLo, WOrLd!"), "HELLO, WORLD!");
assert_render(&upper("hElLo, WOrLd!"), "HELLO, WORLD!");
// non-ascii
assert_render(&upper("aBc"), "ABC");
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> "), "&lt;html&gt;");
assert_render_escaped(&lower("<<&\"\">>"), "&lt;&lt;&amp;&quot;&quot;&gt;&gt;");
// 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<br>bar", 10), "foo&lt;br&...");
assert_render_escaped(&truncate("foo<br>bar", 20), "foo&lt;br&gt;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"), "&quot;Pokémon&quot;");
}
#[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...");
}
}

View File

@ -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&lt;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<fn(&char, &mut Buffer) -> 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);
}
}

58
scripts/count-dependencies.sh Executable file
View File

@ -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