Skip to content

Commit ecf1210

Browse files
committed
derive: implement template attribute blocks
1 parent 24c37c4 commit ecf1210

File tree

7 files changed

+241
-8
lines changed

7 files changed

+241
-8
lines changed

rinja/Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,15 @@ maintenance = { status = "actively-developed" }
4444

4545
[features]
4646
default = ["config", "std", "urlencode"]
47-
full = ["default", "code-in-doc", "serde_json"]
47+
full = ["default", "blocks", "code-in-doc", "serde_json"]
4848

4949
alloc = [
5050
"rinja_derive/alloc",
5151
"serde?/alloc",
5252
"serde_json?/alloc",
5353
"percent-encoding?/alloc"
5454
]
55+
blocks = ["rinja_derive/blocks"]
5556
code-in-doc = ["rinja_derive/code-in-doc"]
5657
config = ["rinja_derive/config"]
5758
serde_json = ["rinja_derive/serde_json", "dep:serde", "dep:serde_json"]

rinja_derive/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ syn = { version = "2.0.3", features = ["full"] }
3939

4040
[features]
4141
alloc = []
42+
blocks = ["syn/full"]
4243
code-in-doc = ["dep:pulldown-cmark"]
4344
config = ["dep:serde", "dep:basic-toml", "parser/config"]
4445
urlencode = []

rinja_derive/src/generator.rs

+147-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ use parser::node::{Macro, Whitespace};
1212
use parser::{
1313
CharLit, Expr, FloatKind, IntKind, MAX_RUST_KEYWORD_LEN, Num, RUST_KEYWORDS, StrLit, WithSpan,
1414
};
15+
use proc_macro2::Span;
16+
use quote::quote_spanned;
1517
use rustc_hash::FxBuildHasher;
18+
use syn::{GenericParam, Ident, Lifetime, LifetimeParam, Token};
1619

1720
use crate::heritage::{Context, Heritage};
1821
use crate::html::write_escaped_str;
@@ -25,7 +28,7 @@ pub(crate) fn template_to_string(
2528
input: &TemplateInput<'_>,
2629
contexts: &HashMap<&Arc<Path>, Context<'_>, FxBuildHasher>,
2730
heritage: Option<&Heritage<'_, '_>>,
28-
tmpl_kind: TmplKind,
31+
tmpl_kind: TmplKind<'_>,
2932
) -> Result<usize, CompileError> {
3033
let generator = Generator::new(
3134
input,
@@ -50,11 +53,13 @@ pub(crate) fn template_to_string(
5053
}
5154

5255
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53-
pub(crate) enum TmplKind {
56+
pub(crate) enum TmplKind<'a> {
5457
/// [`rinja::Template`]
5558
Struct,
5659
/// [`rinja::helpers::EnumVariantTemplate`]
5760
Variant,
61+
/// Used in `blocks` implementation
62+
Block(&'a str),
5863
}
5964

6065
struct Generator<'a, 'h> {
@@ -113,13 +118,14 @@ impl<'a, 'h> Generator<'a, 'h> {
113118
fn impl_template(
114119
mut self,
115120
buf: &mut Buffer,
116-
tmpl_kind: TmplKind,
121+
tmpl_kind: TmplKind<'a>,
117122
) -> Result<usize, CompileError> {
118123
let ctx = &self.contexts[&self.input.path];
119124

120125
let target = match tmpl_kind {
121126
TmplKind::Struct => "rinja::Template",
122127
TmplKind::Variant => "rinja::helpers::EnumVariantTemplate",
128+
TmplKind::Block(trait_name) => trait_name,
123129
};
124130
write_header(self.input.ast, buf, target);
125131
buf.write(
@@ -170,9 +176,147 @@ impl<'a, 'h> Generator<'a, 'h> {
170176
}
171177

172178
buf.write('}');
179+
180+
for (block, span) in self.input.blocks {
181+
self.impl_block(buf, block, span)?;
182+
}
183+
173184
Ok(size_hint)
174185
}
175186

187+
fn impl_block(&self, buf: &mut Buffer, block: &str, span: &Span) -> Result<(), CompileError> {
188+
// RATIONALE: `*self` must be the input type, implementation details should not leak:
189+
// - impl Self { fn as_block(self) } ->
190+
// - struct __Rinja__Self__as__block__Wrapper { this: self } ->
191+
// - impl Template for __Rinja__Self__as__block__Wrapper { fn render_into_with_values() } ->
192+
// - impl __Rinja__Self__as__block for Self { render_into_with_values() }
193+
194+
let span = *span;
195+
buf.write(
196+
"\
197+
#[allow(missing_docs, non_camel_case_types, non_snake_case, unreachable_pub)]\
198+
const _: () = {",
199+
);
200+
201+
let ident = &self.input.ast.ident;
202+
203+
let doc = format!("A sub-template that renders only the block `{block}` of [`{ident}`].");
204+
let method_name = format!("as_{block}");
205+
let trait_name = format!("__Rinja__{ident}__as__{block}");
206+
let wrapper_name = format!("__Rinja__{ident}__as__{block}__Wrapper");
207+
let self_lt_name = format!("'__Rinja__{ident}__as__{block}__self");
208+
209+
let method_id = Ident::new(&method_name, span);
210+
let trait_id = Ident::new(&trait_name, span);
211+
let wrapper_id = Ident::new(&wrapper_name, span);
212+
let self_lt = Lifetime::new(&self_lt_name, span);
213+
214+
// generics of the input with an additional lifetime to capture `self`
215+
let mut wrapper_generics = self.input.ast.generics.clone();
216+
if wrapper_generics.lt_token.is_none() {
217+
wrapper_generics.lt_token = Some(Token![<](span));
218+
wrapper_generics.gt_token = Some(Token![>](span));
219+
}
220+
wrapper_generics.params.insert(
221+
0,
222+
GenericParam::Lifetime(LifetimeParam::new(self_lt.clone())),
223+
);
224+
225+
let (impl_generics, ty_generics, where_clause) = self.input.ast.generics.split_for_impl();
226+
let (wrapper_impl_generics, wrapper_ty_generics, wrapper_where_clause) =
227+
wrapper_generics.split_for_impl();
228+
229+
let input = TemplateInput {
230+
block: Some((block, span)),
231+
blocks: &[],
232+
..self.input.clone()
233+
};
234+
let size_hint = template_to_string(
235+
buf,
236+
&input,
237+
self.contexts,
238+
self.heritage,
239+
TmplKind::Block(&trait_name),
240+
)?;
241+
242+
buf.write(quote_spanned! {
243+
span =>
244+
pub trait #trait_id {
245+
fn render_into_with_values<RinjaW>(
246+
&self,
247+
writer: &mut RinjaW,
248+
values: &dyn rinja::Values,
249+
) -> rinja::Result<()>
250+
where
251+
RinjaW:
252+
rinja::helpers::core::fmt::Write + ?rinja::helpers::core::marker::Sized;
253+
}
254+
255+
impl #impl_generics #ident #ty_generics #where_clause {
256+
#[inline]
257+
#[doc = #doc]
258+
pub fn #method_id(&self) -> impl rinja::Template + '_ {
259+
#wrapper_id {
260+
this: self,
261+
}
262+
}
263+
}
264+
265+
#[rinja::helpers::core::prelude::rust_2021::derive(
266+
rinja::helpers::core::prelude::rust_2021::Clone,
267+
rinja::helpers::core::prelude::rust_2021::Copy
268+
)]
269+
pub struct #wrapper_id #wrapper_generics #wrapper_where_clause {
270+
this: &#self_lt #ident #ty_generics,
271+
}
272+
273+
impl #wrapper_impl_generics rinja::Template
274+
for #wrapper_id #wrapper_ty_generics #wrapper_where_clause {
275+
#[inline]
276+
fn render_into_with_values<RinjaW>(
277+
&self,
278+
writer: &mut RinjaW,
279+
values: &dyn rinja::Values
280+
) -> rinja::Result<()>
281+
where
282+
RinjaW: rinja::helpers::core::fmt::Write + ?rinja::helpers::core::marker::Sized
283+
{
284+
<_ as #trait_id>::render_into_with_values(self.this, writer, values)
285+
}
286+
287+
const SIZE_HINT: rinja::helpers::core::primitive::usize = #size_hint;
288+
}
289+
290+
// cannot use `crate::integrations::impl_fast_writable()` w/o cloning the struct
291+
impl #wrapper_impl_generics rinja::filters::FastWritable
292+
for #wrapper_id #wrapper_ty_generics #wrapper_where_clause {
293+
#[inline]
294+
fn write_into<RinjaW>(&self, dest: &mut RinjaW) -> rinja::Result<()>
295+
where
296+
RinjaW: rinja::helpers::core::fmt::Write + ?rinja::helpers::core::marker::Sized
297+
{
298+
<_ as rinja::Template>::render_into(self, dest)
299+
}
300+
}
301+
302+
// cannot use `crate::integrations::impl_display()` w/o cloning the struct
303+
impl #wrapper_impl_generics rinja::helpers::core::fmt::Display
304+
for #wrapper_id #wrapper_ty_generics #wrapper_where_clause {
305+
#[inline]
306+
fn fmt(
307+
&self,
308+
f: &mut rinja::helpers::core::fmt::Formatter<'_>
309+
) -> rinja::helpers::core::fmt::Result {
310+
<_ as rinja::Template>::render_into(self, f)
311+
.map_err(|_| rinja::helpers::core::fmt::Error)
312+
}
313+
}
314+
});
315+
316+
buf.write("};");
317+
Ok(())
318+
}
319+
176320
fn is_var_defined(&self, var_name: &str) -> bool {
177321
self.locals.get(var_name).is_some() || self.input.fields.iter().any(|f| f == var_name)
178322
}

rinja_derive/src/input.rs

+51-3
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@ use proc_macro2::Span;
1212
use rustc_hash::FxBuildHasher;
1313
use syn::punctuated::Punctuated;
1414
use syn::spanned::Spanned;
15-
use syn::{Attribute, Expr, ExprLit, ExprPath, Ident, Lit, LitBool, LitStr, Meta, Token};
15+
use syn::{
16+
Attribute, Expr, ExprArray, ExprLit, ExprPath, Ident, Lit, LitBool, LitStr, Meta, Token,
17+
};
1618

1719
use crate::config::{Config, SyntaxAndCache};
1820
use crate::{CompileError, FileInfo, MsgValidEscapers, OnceMap};
1921

22+
#[derive(Clone)]
2023
pub(crate) struct TemplateInput<'a> {
2124
pub(crate) ast: &'a syn::DeriveInput,
2225
pub(crate) enum_ast: Option<&'a syn::DeriveInput>,
@@ -25,10 +28,11 @@ pub(crate) struct TemplateInput<'a> {
2528
pub(crate) source: &'a Source,
2629
pub(crate) source_span: Option<Span>,
2730
pub(crate) block: Option<(&'a str, Span)>,
31+
pub(crate) blocks: &'a [(String, Span)],
2832
pub(crate) print: Print,
2933
pub(crate) escaper: &'a str,
3034
pub(crate) path: Arc<Path>,
31-
pub(crate) fields: Vec<String>,
35+
pub(crate) fields: Arc<[String]>,
3236
}
3337

3438
impl TemplateInput<'_> {
@@ -44,6 +48,7 @@ impl TemplateInput<'_> {
4448
let TemplateArgs {
4549
source: (source, source_span),
4650
block,
51+
blocks,
4752
print,
4853
escaping,
4954
ext,
@@ -134,10 +139,11 @@ impl TemplateInput<'_> {
134139
source,
135140
source_span: *source_span,
136141
block: block.as_ref().map(|(block, span)| (block.as_str(), *span)),
142+
blocks: blocks.as_slice(),
137143
print: *print,
138144
escaper,
139145
path,
140-
fields,
146+
fields: fields.into(),
141147
})
142148
}
143149

@@ -347,6 +353,7 @@ impl AnyTemplateArgs {
347353
pub(crate) struct TemplateArgs {
348354
pub(crate) source: (Source, Option<Span>),
349355
block: Option<(String, Span)>,
356+
blocks: Vec<(String, Span)>,
350357
print: Print,
351358
escaping: Option<String>,
352359
ext: Option<String>,
@@ -396,6 +403,12 @@ impl TemplateArgs {
396403
}
397404
},
398405
block: args.block.map(|value| (value.value(), value.span())),
406+
blocks: args
407+
.blocks
408+
.unwrap_or_default()
409+
.into_iter()
410+
.map(|value| (value.value(), value.span()))
411+
.collect(),
399412
print: args.print.unwrap_or_default(),
400413
escaping: args.escape.map(|value| value.value()),
401414
ext: args.ext.as_ref().map(|value| value.value()),
@@ -413,6 +426,7 @@ impl TemplateArgs {
413426
Self {
414427
source: (Source::Source("".into()), None),
415428
block: None,
429+
blocks: vec![],
416430
print: Print::default(),
417431
escaping: None,
418432
ext: Some("txt".to_string()),
@@ -692,6 +706,7 @@ pub(crate) struct PartialTemplateArgs {
692706
pub(crate) config: Option<LitStr>,
693707
pub(crate) whitespace: Option<Whitespace>,
694708
pub(crate) crate_name: Option<ExprPath>,
709+
pub(crate) blocks: Option<Vec<LitStr>>,
695710
}
696711

697712
#[derive(Clone)]
@@ -754,6 +769,7 @@ const _: () = {
754769
config: None,
755770
whitespace: None,
756771
crate_name: None,
772+
blocks: None,
757773
};
758774
let mut has_data = false;
759775

@@ -806,6 +822,23 @@ const _: () = {
806822
ensure_only_once(ident, &mut this.crate_name)?;
807823
this.crate_name = Some(get_exprpath(ident, pair.value)?);
808824
continue;
825+
} else if ident == "blocks" {
826+
if is_enum_variant {
827+
return Err(CompileError::no_file_info(
828+
"template attribute `blocks` can only be used on the `enum`, \
829+
not its variants",
830+
Some(ident.span()),
831+
));
832+
}
833+
ensure_only_once(ident, &mut this.blocks)?;
834+
this.blocks = Some(
835+
get_exprarray(ident, pair.value)?
836+
.elems
837+
.into_iter()
838+
.map(|value| get_strlit(ident, get_lit(ident, value)?))
839+
.collect::<Result<_, _>>()?,
840+
);
841+
continue;
809842
}
810843

811844
let value = get_lit(ident, pair.value)?;
@@ -965,6 +998,21 @@ const _: () = {
965998
}
966999
}
9671000

1001+
fn get_exprarray(name: &Ident, mut expr: Expr) -> Result<ExprArray, CompileError> {
1002+
loop {
1003+
match expr {
1004+
Expr::Array(array) => return Ok(array),
1005+
Expr::Group(group) => expr = *group.expr,
1006+
v => {
1007+
return Err(CompileError::no_file_info(
1008+
format_args!("template attribute `{name}` expects an array"),
1009+
Some(v.span()),
1010+
));
1011+
}
1012+
}
1013+
}
1014+
}
1015+
9681016
fn ensure_source_only_once(
9691017
name: &Ident,
9701018
source: &Option<PartialTemplateArgsSource>,

rinja_derive/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ fn build_template_item(
262262
ast: &syn::DeriveInput,
263263
enum_ast: Option<&syn::DeriveInput>,
264264
template_args: &TemplateArgs,
265-
tmpl_kind: TmplKind,
265+
tmpl_kind: TmplKind<'_>,
266266
) -> Result<usize, CompileError> {
267267
let config_path = template_args.config_path();
268268
let s = read_config_file(config_path, template_args.config_span)?;

rinja_derive_standalone/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ syn = { version = "2.0.3", features = ["full"] }
4646
default = ["__standalone"]
4747
__standalone = []
4848

49+
blocks = ["syn/full"]
4950
code-in-doc = ["dep:pulldown-cmark"]
5051
config = ["dep:serde", "dep:basic-toml", "parser/config"]
5152
urlencode = []

0 commit comments

Comments
 (0)