Skip to content

Commit 137aaa0

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

File tree

8 files changed

+269
-9
lines changed

8 files changed

+269
-9
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

+156-3
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ pub(crate) fn template_to_string(
2525
input: &TemplateInput<'_>,
2626
contexts: &HashMap<&Arc<Path>, Context<'_>, FxBuildHasher>,
2727
heritage: Option<&Heritage<'_, '_>>,
28-
tmpl_kind: TmplKind,
28+
tmpl_kind: TmplKind<'_>,
2929
) -> Result<usize, CompileError> {
3030
let generator = Generator::new(
3131
input,
@@ -50,11 +50,14 @@ pub(crate) fn template_to_string(
5050
}
5151

5252
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53-
pub(crate) enum TmplKind {
53+
pub(crate) enum TmplKind<'a> {
5454
/// [`rinja::Template`]
5555
Struct,
5656
/// [`rinja::helpers::EnumVariantTemplate`]
5757
Variant,
58+
/// Used in `blocks` implementation
59+
#[allow(unused)]
60+
Block(&'a str),
5861
}
5962

6063
struct Generator<'a, 'h> {
@@ -113,13 +116,14 @@ impl<'a, 'h> Generator<'a, 'h> {
113116
fn impl_template(
114117
mut self,
115118
buf: &mut Buffer,
116-
tmpl_kind: TmplKind,
119+
tmpl_kind: TmplKind<'a>,
117120
) -> Result<usize, CompileError> {
118121
let ctx = &self.contexts[&self.input.path];
119122

120123
let target = match tmpl_kind {
121124
TmplKind::Struct => "rinja::Template",
122125
TmplKind::Variant => "rinja::helpers::EnumVariantTemplate",
126+
TmplKind::Block(trait_name) => trait_name,
123127
};
124128
write_header(self.input.ast, buf, target);
125129
buf.write(
@@ -170,9 +174,158 @@ impl<'a, 'h> Generator<'a, 'h> {
170174
}
171175

172176
buf.write('}');
177+
178+
#[cfg(feature = "blocks")]
179+
for (block, span) in self.input.blocks {
180+
self.impl_block(buf, block, span)?;
181+
}
182+
173183
Ok(size_hint)
174184
}
175185

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

rinja_derive/src/input.rs

+65-2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use syn::{Attribute, Expr, ExprLit, ExprPath, Ident, Lit, LitBool, LitStr, Meta,
1717
use crate::config::{Config, SyntaxAndCache};
1818
use crate::{CompileError, FileInfo, MsgValidEscapers, OnceMap};
1919

20+
#[derive(Clone)]
2021
pub(crate) struct TemplateInput<'a> {
2122
pub(crate) ast: &'a syn::DeriveInput,
2223
pub(crate) enum_ast: Option<&'a syn::DeriveInput>,
@@ -25,10 +26,12 @@ pub(crate) struct TemplateInput<'a> {
2526
pub(crate) source: &'a Source,
2627
pub(crate) source_span: Option<Span>,
2728
pub(crate) block: Option<(&'a str, Span)>,
29+
#[cfg(feature = "blocks")]
30+
pub(crate) blocks: &'a [(String, Span)],
2831
pub(crate) print: Print,
2932
pub(crate) escaper: &'a str,
3033
pub(crate) path: Arc<Path>,
31-
pub(crate) fields: Vec<String>,
34+
pub(crate) fields: Arc<[String]>,
3235
}
3336

3437
impl TemplateInput<'_> {
@@ -44,6 +47,8 @@ impl TemplateInput<'_> {
4447
let TemplateArgs {
4548
source: (source, source_span),
4649
block,
50+
#[cfg(feature = "blocks")]
51+
blocks,
4752
print,
4853
escaping,
4954
ext,
@@ -134,10 +139,12 @@ impl TemplateInput<'_> {
134139
source,
135140
source_span: *source_span,
136141
block: block.as_ref().map(|(block, span)| (block.as_str(), *span)),
142+
#[cfg(feature = "blocks")]
143+
blocks: blocks.as_slice(),
137144
print: *print,
138145
escaper,
139146
path,
140-
fields,
147+
fields: fields.into(),
141148
})
142149
}
143150

@@ -347,6 +354,8 @@ impl AnyTemplateArgs {
347354
pub(crate) struct TemplateArgs {
348355
pub(crate) source: (Source, Option<Span>),
349356
block: Option<(String, Span)>,
357+
#[cfg(feature = "blocks")]
358+
blocks: Vec<(String, Span)>,
350359
print: Print,
351360
escaping: Option<String>,
352361
ext: Option<String>,
@@ -396,6 +405,13 @@ impl TemplateArgs {
396405
}
397406
},
398407
block: args.block.map(|value| (value.value(), value.span())),
408+
#[cfg(feature = "blocks")]
409+
blocks: args
410+
.blocks
411+
.unwrap_or_default()
412+
.into_iter()
413+
.map(|value| (value.value(), value.span()))
414+
.collect(),
399415
print: args.print.unwrap_or_default(),
400416
escaping: args.escape.map(|value| value.value()),
401417
ext: args.ext.as_ref().map(|value| value.value()),
@@ -413,6 +429,8 @@ impl TemplateArgs {
413429
Self {
414430
source: (Source::Source("".into()), None),
415431
block: None,
432+
#[cfg(feature = "blocks")]
433+
blocks: vec![],
416434
print: Print::default(),
417435
escaping: None,
418436
ext: Some("txt".to_string()),
@@ -692,6 +710,8 @@ pub(crate) struct PartialTemplateArgs {
692710
pub(crate) config: Option<LitStr>,
693711
pub(crate) whitespace: Option<Whitespace>,
694712
pub(crate) crate_name: Option<ExprPath>,
713+
#[cfg(feature = "blocks")]
714+
pub(crate) blocks: Option<Vec<LitStr>>,
695715
}
696716

697717
#[derive(Clone)]
@@ -754,6 +774,8 @@ const _: () = {
754774
config: None,
755775
whitespace: None,
756776
crate_name: None,
777+
#[cfg(feature = "blocks")]
778+
blocks: None,
757779
};
758780
let mut has_data = false;
759781

@@ -806,6 +828,31 @@ const _: () = {
806828
ensure_only_once(ident, &mut this.crate_name)?;
807829
this.crate_name = Some(get_exprpath(ident, pair.value)?);
808830
continue;
831+
} else if ident == "blocks" {
832+
if !cfg!(feature = "blocks") {
833+
return Err(CompileError::no_file_info(
834+
"enable feature `blocks` to use `blocks` argument",
835+
Some(ident.span()),
836+
));
837+
} else if is_enum_variant {
838+
return Err(CompileError::no_file_info(
839+
"template attribute `blocks` can only be used on the `enum`, \
840+
not its variants",
841+
Some(ident.span()),
842+
));
843+
}
844+
#[cfg(feature = "blocks")]
845+
{
846+
ensure_only_once(ident, &mut this.blocks)?;
847+
this.blocks = Some(
848+
get_exprarray(ident, pair.value)?
849+
.elems
850+
.into_iter()
851+
.map(|value| get_strlit(ident, get_lit(ident, value)?))
852+
.collect::<Result<_, _>>()?,
853+
);
854+
continue;
855+
}
809856
}
810857

811858
let value = get_lit(ident, pair.value)?;
@@ -965,6 +1012,22 @@ const _: () = {
9651012
}
9661013
}
9671014

1015+
#[cfg(feature = "blocks")]
1016+
fn get_exprarray(name: &Ident, mut expr: Expr) -> Result<syn::ExprArray, CompileError> {
1017+
loop {
1018+
match expr {
1019+
Expr::Array(array) => return Ok(array),
1020+
Expr::Group(group) => expr = *group.expr,
1021+
v => {
1022+
return Err(CompileError::no_file_info(
1023+
format_args!("template attribute `{name}` expects an array"),
1024+
Some(v.span()),
1025+
));
1026+
}
1027+
}
1028+
}
1029+
}
1030+
9681031
fn ensure_source_only_once(
9691032
name: &Ident,
9701033
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)