Use Ructe (#327)

All the template are now compiled at compile-time with the `ructe` crate.

I preferred to use it instead of askama because it allows more complex Rust expressions, where askama only supports a small subset of expressions and doesn't allow them everywhere (for instance, `{{ macro!() | filter }}` would result in a parsing error).

The diff is quite huge, but there is normally no changes in functionality.

Fixes #161 and unblocks #110 and #273
This commit is contained in:
Baptiste Gelez
2018-12-06 18:54:16 +01:00
committed by GitHub
parent 5f059c3e98
commit 70af57c6e1
121 changed files with 3132 additions and 3260 deletions
-152
View File
@@ -1,152 +0,0 @@
{% extends "base" %}
{% import "macros" as macros %}
{% block head %}
<meta property="og:title" content="{{ article.post.title }}"/>
<meta property="og:type" content="article"/>
{% if article.cover %}
<meta property="og:image" content="{{ article.cover.url | safe }}"/>
{% endif %}
<meta property="og:url" content="{{ article.url | safe }}"/>
{% if article.post.subtitle %}
<meta property="og:description" content="{{ article.post.subtitle }}"/>
{% endif %}
{% endblock head %}
{% block title %}
{{ article.post.title }}
{% endblock title %}
{% block header %}
<a href="/~/{{ blog.fqn }}">{{ blog.title }}</a>
{% endblock header %}
{% block content %}
<h1 class="article">{{ article.post.title }}</h1>
<h2 class="article">{{ article.post.subtitle }}</h2>
<div class="article-info">
<span class="author">{{ "Written by {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}{{ link_3 }}" | _(
link_1='<a href="/@/',
url=author.fqn,
link_2='/">',
name=author.name,
link_3="</a>"
)
}}</span>
&mdash;
<span class="date">{{ date | date(format="%B %e, %Y") }}</span>
{% if is_author %}
&mdash;
<a href="{{ article.url}}edit">{{ "Edit" | _ }}</a>
&mdash;
<form class="inline" method="post" action="{{ article.url}}delete">
<input onclick="return confirm('Are you sure you?')" type="submit" value="{{ 'Delete this article' | _ }}">
</form>
{% endif %}
{% if not article.post.published %}
<span class="badge">{{ "Draft" }}</span>
{% endif %}
</div>
{% if article.cover %}
<div class="cover" style="background-image: url('{{ article.cover.url }}')"></div>
{% endif %}
<article>
{{ article.post.content | safe }}
</article>
<div class="article-meta">
<p>{{ "This article is under the {{ license }} license." | _(license=article.post.license) }}</p>
<ul class="tags">
{% for tag in article.tags %}
{% if not tag.is_hashtag %}
<li><a href="/tag/{{ tag.tag }}">{{ tag.tag }}</a></li>
{% endif %}
{% endfor %}
</ul>
<div class="flex">
{{ macros::avatar(user=author, pad=true, size="medium") }}
<div class="grow">
<h2><a href="/@/{{ author.fqn }}">{{ author.name }}</a></h2>
<p>{{ author.summary | safe }}</h2>
</div>
<a href="/@/{{ author.fqn }}/follow" class="button">
{% if is_following %}
{{ "Unfollow" | _ }}
{% else %}
{{ "Follow" | _ }}
{% endif %}
</a>
</div>
{% if account %}
<div class="actions">
<form class="likes" action="{{ article.url }}like" method="POST">
<p aria-label="{{ "{{ count }} likes" | _n(singular="One like", count=n_likes) }}" title="{{ "{{ count }} likes" | _n(singular="One like", count=n_likes) }}">{{ n_likes }}</p>
{% if has_liked %}
<button type="submit" class="action liked">{{ macros::feather(name="heart") }}{{ "I don't like this anymore" | _ }}</button>
{% else %}
<button type="submit" class="action">{{ macros::feather(name="heart") }}{{ "Add yours" | _ }}</button>
{% endif %}
</form>
<form class="reshares" action="{{ article.url }}reshare" method="POST">
<p aria-label="{{ "{{ count }} Boosts" | _n(singular="One Boost", count=n_reshares) }}" title="{{ "{{ count }} Boosts" | _n(singular="One Boost", count=n_reshares) }}">{{ n_reshares }}</p>
{% if has_reshared %}
<button type="submit" class="action reshared"><i class="icon icon-repeat"></i>{{ "I don't want to boost this anymore" | _ }}</button>
{% else %}
<button type="submit" class="action"><i class="icon icon-repeat"></i>{{ "Boost" | _ }}</button>
{% endif %}
</form>
</div>
{% else %}
<p class="center">{{ "Login or use your Fediverse account to interact with this article" | _ }}</p>
<div class="actions">
<div class="likes">
<p aria-label="{{ "{{ count }} likes" | _n(singular="One like", count=n_likes) }}" title="{{ "{{ count }} likes" | _n(singular="One like", count=n_likes) }}">{{ n_likes }}</p>
<a href="/login?m=Login%20to%20like" class="action">{{ macros::feather(name="heart") }}{{ "Add yours" | _ }}</a>
</div>
<div class="reshares">
<p aria-label="{{ "{{ count }} Boosts" | _n(singular="One Boost", count=n_reshares) }}" title="{{ "{{ count }} Boosts" | _n(singular="One Boost", count=n_reshares) }}">{{ n_reshares }}</p>
<a href="/login?m=Login%20to%20boost" class="action"><i class="icon icon-repeat"></i>{{ "Boost" | _ }}</a>
</div>
</div>
{% endif %}
<div class="comments">
<h2>{{ "Comments" | _ }}</h2>
{% if account %}
<form method="post" action="{{ article.url }}comment">
{{ macros::input(
name="warning",
label="Content warning",
optional=true,
form=comment_form,
errors=comment_errors,
default=default)
}}
<label for="plume-editor">{{ "Your comment" | _ }}</label>
{% if previous %}
<input type="hidden" name="responding_to" value="{{ previous.id }}"/>
{% endif %}
{# Ugly, but we don't have the choice if we don't want weird paddings #}
<textarea id="plume-editor" name="content">{% filter trim %}{% if previous %}{% if previous.author.fqn != user_fqn %}@{{ previous.author.fqn }} {% endif %}{% for mention in previous.mentions %}{% if mention != user_fqn %}@{{ mention }} {% endif %}{% endfor %}{% endif %}{% endfilter %}</textarea>
<input type="submit" value="{{ "Submit comment" | _ }}" />
</form>
{% endif %}
{% if comments | length > 0 %}
<div class="list">
{% for comment in comments %}
{{ macros::comment(comm=comment) }}
{% endfor %}
</div>
{% else %}
<p class="center">{{ "No comments yet. Be the first to react!" | _ }}</p>
{% endif %}
</div>
</div>
{% endblock content %}
+146
View File
@@ -0,0 +1,146 @@
@use templates::{base, partials::comment};
@use template_utils::*;
@use plume_models::blogs::Blog;
@use plume_models::comments::Comment;
@use plume_models::posts::Post;
@use plume_models::tags::Tag;
@use plume_models::users::User;
@use validator::ValidationErrors;
@use routes::comments::NewCommentForm;
@(ctx: BaseContext, article: Post, blog: Blog, comment_form: &NewCommentForm, comment_errors: ValidationErrors, tags: Vec<Tag>, comments: Vec<Comment>, previous_comment: Option<Comment>, n_likes: usize, n_reshares: usize, has_liked: bool, has_reshared: bool, is_following: bool, author: User)
@:base(ctx, &article.title.clone(), {
<meta property="og:title" content="article.title"/>
<meta property="og:type" content="article"/>
@if article.cover_id.is_some() {
<meta property="og:image" content="@Html(article.cover_url(ctx.0).unwrap_or_default())"/>
}
<meta property="og:url" content="@Html(article.url(ctx.0))"/>
<meta property="og:description" content="@article.subtitle"/>
}, {
<a href="/~/@blog.get_fqn(ctx.0)">@blog.title</a>
}, {
<h1 class="article">@&article.title</h1>
<h2 class="article">@&article.subtitle</h2>
<div class="article-info">
<span class="author">
@Html(i18n!(ctx.1, "Written by {0}"; format!("<a href=\"/@/{}/\">{}</a>", escape(&author.get_fqn(ctx.0)), escape(&author.name(ctx.0)))))
</span>
&mdash;
<span class="date">@article.creation_date.format("%B %e, %Y")</span>
@if ctx.2.clone().map(|u| u.id == author.id).unwrap_or(false) {
&mdash;
<a href="@article.url(ctx.0)/edit">@i18n!(ctx.1, "Edit")</a>
&mdash;
<form class="inline" method="post" action="@article.url(ctx.0)/delete">
<input onclick="return confirm('Are you sure you?')" type="submit" value="@i18n!(ctx.1, "Delete this article")">
</form>
}
@if !article.published {
<span class="badge">@i18n!(ctx.1, "Draft")</span>
}
</div>
@if article.cover_id.is_some() {
<div class="cover" style="background-image: url('@Html(article.cover_url(ctx.0).unwrap_or_default())')"></div>
}
<article>
@Html(&article.content)
</article>
<div class="article-meta">
<p>@i18n!(ctx.1, "This article is under the {0} license."; &article.license)</p>
<ul class="tags">
@for tag in tags {
@if !tag.is_hashtag {
<li><a href="/tag/@tag.tag">@tag.tag</a></li>
}
}
</ul>
<div class="flex">
@avatar(ctx.0, &author, Size::Medium, true, ctx.1)
<div class="grow">
<h2><a href="/@@/@author.get_fqn(ctx.0)">@author.name(ctx.0)</a></h2>
<p>@Html(&author.summary)</h2>
</div>
<a href="/@@/@author.get_fqn(ctx.0)/follow" class="button">
@if is_following {
@i18n!(ctx.1, "Unfollow")
} else {
@i18n!(ctx.1, "Follow")
}
</a>
</div>
@if ctx.2.is_some() {
<div class="actions">
<form class="likes" action="@article.url(ctx.0)/like" method="POST">
<p aria-label="@i18n!(ctx.1, "One like", "{0} likes", &n_likes)" title="@i18n!(ctx.1, "One like", "{0} likes", n_likes)">
@n_likes
</p>
@if has_liked {
<button type="submit" class="action liked">@icon!("heart") @i18n!(ctx.1, "I don't like this anymore")</button>
} else {
<button type="submit" class="action">@icon!("heart") @i18n!(ctx.1, "Add yours")</button>
}
</form>
<form class="reshares" action="@article.url(ctx.0)/reshare" method="POST">
<p aria-label="@i18n!(ctx.1, "One boost", "{0} boost", &n_reshares)" title="@i18n!(ctx.1, "One boost", "{0} boosts", n_reshares)">
@n_reshares
</p>
@if has_reshared {
<button type="submit" class="action reshared">@icon!("repeat") @i18n!(ctx.1, "I don't want to boost this anymore")</button>
} else {
<button type="submit" class="action">@icon!("repeat") @i18n!(ctx.1, "Boost")</button>
}
</form>
</div>
} else {
<p class="center">@i18n!(ctx.1, "Login or use your Fediverse account to interact with this article")</p>
<div class="actions">
<div class="likes">
<p aria-label="@i18n!(ctx.1, "One like", "{0} likes", &n_likes)" title="@i18n!(ctx.1, "One like", "{0} likes", n_likes)">
@n_likes
</p>
<a href="/login?m=Login%20to%20like" class="action">@icon!("heart") @i18n!(ctx.1, "Add yours")</a>
</div>
<div class="reshares">
<p aria-label="@i18n!(ctx.1, "One boost", "{0} boost", &n_reshares)" title="@i18n!(ctx.1, "One boost", "{0} boosts", n_reshares)">
@n_reshares
</p>
<a href="/login?m=Login%20to%20boost" class="action">@icon!("repeat") @i18n!(ctx.1, "Boost")</a>
</div>
</div>
}
<div class="comments">
<h2>@i18n!(ctx.1, "Comments")</h2>
@if ctx.2.is_some() {
<form method="post" action="@article.url(ctx.0)/comment">
@input!(ctx.1, warning (optional text), "Content warning", comment_form, comment_errors, "")
<label for="plume-editor">@i18n!(ctx.1, "Your comment")</label>
@if let Some(ref prev) = previous_comment {
<input type="hidden" name="responding_to" value="@prev.id"/>
}
<textarea id="plume-editor" name="content">@comment_form.content</textarea>
<input type="submit" value="@i18n!(ctx.1, "Submit comment")" />
</form>
}
@if !comments.is_empty() {
<div class="list">
@for comm in comments {
@:comment(ctx, &comm, comm.get_author(ctx.0))
}
</div>
} else {
<p class="center">@i18n!(ctx.1, "No comments yet. Be the first to react!")</p>
}
</div>
</div>
})
-68
View File
@@ -1,68 +0,0 @@
{% extends "base" %}
{% import "macros" as macros %}
{% block title %}
{% if editing %}
{{ "Edit {{ post }}" | _(post=form.title) }}
{% else %}
{{ "New post" | _ }}
{% endif %}
{% endblock title %}
{% block content %}
<h1>
{% if editing %}
{{ "Edit {{ post }}" | _(post=form.title) }}
{% else %}
{{ "Create a post" | _ }}
{% endif %}
</h1>
<form class="new-post" method="post">
{{ macros::input(name="title", label="Title", errors=errors, form=form, props="required") }}
{{ macros::input(name="subtitle", label="Subtitle", errors=errors, form=form, optional=true) }}
{% if errors is defined and errors.content %}
{% for err in errors.content %}
<p class="error">{{ err.message | default(value="Unknown error") | _ }}</p>
{% endfor %}
{% endif %}
<label for="plume-editor">{{ "Content" | _ }}<small>{{ "Markdown is supported" | _ }}</small></label>
<textarea id="plume-editor" name="content" rows="20">{{ form.content | default(value="") }}</textarea>
{{ macros::input(name="tags", label="Tags, separated by commas", errors=errors, form=form, optional=true) }}
{% set license_infos = "Default license will be {{ instance.default_license }}" | _(instance=instance) %}
{{ macros::input(name="license", label="License", errors=errors, form=form, optional=true, details=license_infos) }}
<label for="cover">{{ "Illustration" | _ }}<small>{{ "Optional" | _ }}</small></label>
<select id="cover" name="cover">
<option value="none" {% if form is undefined or form.cover is undefined %}selected{% endif %}>{{ "None" | _ }}</option>
{% for media in medias %}
{% if media.category == "image" %}
<option value="{{ media.id }}" {% if form is defined and form.cover is defined and form.cover == media.id %}selected{% endif %}>
{{ media.alt_text | default(value=media.content_warning) }}
</option>
{% endif %}
{% endfor %}
</select>
{% if is_draft %}
<label for="draft">
<input type="checkbox" name="draft" id="draft" checked>
{{ "This is a draft, don't publish it yet." | _ }}
</label>
{% endif %}
{% if editing %}
<input type="submit" value="{{ "Update" | _ }}" />
{% else %}
{% if is_draft %}
<input type="submit" value="{{ "Update or publish" | _ }}" />
{% else %}
<input type="submit" value="{{ "Publish" | _ }}" />
{% endif %}
{% endif %}
</form>
<script src="/static/js/autoExpand.js"></script>
{% endblock content %}
+67
View File
@@ -0,0 +1,67 @@
@use templates::base;
@use template_utils::*;
@use validator::{ValidationErrors, ValidationErrorsKind};
@use std::borrow::Cow;
@use plume_models::medias::*;
@use routes::posts::NewPostForm;
@(ctx: BaseContext, editing: bool, form: &NewPostForm, errors: ValidationErrors, default_license: String, medias: Vec<Media>, is_draft: bool)
@:base(ctx, &i18n!(ctx.1, if editing { "Edit {0}" } else { "New post" }; &form.title), {}, {}, {
<h1>
@if editing {
@i18n!(ctx.1, "Edit {0}"; &form.title)
} else {
@i18n!(ctx.1, "Create a new post")
}
</h1>
<form class="new-post" method="post">
@input!(ctx.1, title (text), "Title", form, errors.clone(), "required")
@input!(ctx.1, subtitle (optional text), "Subtitle", form, errors.clone(), "")
@if let Some(ValidationErrorsKind::Field(errs)) = errors.clone().errors().get("content") {
@format!(r#"<p class="error">{}</p>"#, i18n!(ctx.1, &*errs[0].message.clone().unwrap_or(Cow::from("Unknown error"))))
}
<label for="plume-editor">@i18n!(ctx.1, "Content")<small>@i18n!(ctx.1, "Markdown is supported")</small></label>
<textarea id="plume-editor" name="content" rows="20">@form.content</textarea>
@input!(ctx.1, tags (optional text), "Tags, separated by commas", form, errors.clone(), "")
@input!(ctx.1, license (optional text), "License", &i18n!(ctx.1, "Default license will be {0}"; &default_license), form, errors, "")
<label for="cover">@i18n!(ctx.1, "Illustration")<small>@i18n!(ctx.1, "Optional")</small></label>
<select id="cover" name="cover">
<option value="none" @if form.cover.is_none() { selected }>@i18n!(ctx.1, "None")</option>
@for media in medias {
@if media.category() == MediaCategory::Image {
<option value="@media.id" @if form.cover.map(|c| c == media.id).unwrap_or(false) { selected }>
@if !media.alt_text.is_empty() {
@media.alt_text
} else {
@media.content_warning.unwrap_or_default()
}
</option>
}
}
</select>
@if is_draft {
<label for="draft">
<input type="checkbox" name="draft" id="draft" checked>
@i18n!(ctx.1, "This is a draft, don't publish it yet.")
</label>
}
@if editing {
<input type="submit" value="@i18n!(ctx.1, "Update")" />
} else {
@if is_draft {
<input type="submit" value="@i18n!(ctx.1, "Update or publish")" />
} else {
<input type="submit" value="@i18n!(ctx.1, "Publish")" />
}
}
</form>
<script src="/static/js/autoExpand.js"></script>
})