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:
@@ -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>
|
||||
—
|
||||
<span class="date">{{ date | date(format="%B %e, %Y") }}</span>
|
||||
{% if is_author %}
|
||||
—
|
||||
<a href="{{ article.url}}edit">{{ "Edit" | _ }}</a>
|
||||
—
|
||||
<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 %}
|
||||
@@ -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>
|
||||
—
|
||||
<span class="date">@article.creation_date.format("%B %e, %Y")</span>
|
||||
@if ctx.2.clone().map(|u| u.id == author.id).unwrap_or(false) {
|
||||
—
|
||||
<a href="@article.url(ctx.0)/edit">@i18n!(ctx.1, "Edit")</a>
|
||||
—
|
||||
<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>
|
||||
})
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
})
|
||||
Reference in New Issue
Block a user