Skip to content

Commit 45443c5

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

File tree

8 files changed

+271
-10
lines changed

8 files changed

+271
-10
lines changed

rinja/Cargo.toml

Lines changed: 2 additions & 1 deletion
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 156 additions & 3 deletions
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

Lines changed: 67 additions & 3 deletions
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,12 @@ 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+
#[cfg(feature = "blocks")]
32+
pub(crate) blocks: &'a [(String, Span)],
2833
pub(crate) print: Print,
2934
pub(crate) escaper: &'a str,
3035
pub(crate) path: Arc<Path>,
31-
pub(crate) fields: Vec<String>,
36+
pub(crate) fields: Arc<[String]>,
3237
}
3338

3439
impl TemplateInput<'_> {
@@ -44,6 +49,8 @@ impl TemplateInput<'_> {
4449
let TemplateArgs {
4550
source: (source, source_span),
4651
block,
52+
#[cfg(feature = "blocks")]
53+
blocks,
4754
print,
4855
escaping,
4956
ext,
@@ -134,10 +141,12 @@ impl TemplateInput<'_> {
134141
source,
135142
source_span: *source_span,
136143
block: block.as_ref().map(|(block, span)| (block.as_str(), *span)),
144+
#[cfg(feature = "blocks")]
145+
blocks: blocks.as_slice(),
137146
print: *print,
138147
escaper,
139148
path,
140-
fields,
149+
fields: fields.into(),
141150
})
142151
}
143152

@@ -347,6 +356,8 @@ impl AnyTemplateArgs {
347356
pub(crate) struct TemplateArgs {
348357
pub(crate) source: (Source, Option<Span>),
349358
block: Option<(String, Span)>,
359+
#[cfg(feature = "blocks")]
360+
blocks: Vec<(String, Span)>,
350361
print: Print,
351362
escaping: Option<String>,
352363
ext: Option<String>,
@@ -396,6 +407,13 @@ impl TemplateArgs {
396407
}
397408
},
398409
block: args.block.map(|value| (value.value(), value.span())),
410+
#[cfg(feature = "blocks")]
411+
blocks: args
412+
.blocks
413+
.unwrap_or_default()
414+
.into_iter()
415+
.map(|value| (value.value(), value.span()))
416+
.collect(),
399417
print: args.print.unwrap_or_default(),
400418
escaping: args.escape.map(|value| value.value()),
401419
ext: args.ext.as_ref().map(|value| value.value()),
@@ -413,6 +431,8 @@ impl TemplateArgs {
413431
Self {
414432
source: (Source::Source("".into()), None),
415433
block: None,
434+
#[cfg(feature = "blocks")]
435+
blocks: vec![],
416436
print: Print::default(),
417437
escaping: None,
418438
ext: Some("txt".to_string()),
@@ -692,6 +712,8 @@ pub(crate) struct PartialTemplateArgs {
692712
pub(crate) config: Option<LitStr>,
693713
pub(crate) whitespace: Option<Whitespace>,
694714
pub(crate) crate_name: Option<ExprPath>,
715+
#[cfg(feature = "blocks")]
716+
pub(crate) blocks: Option<Vec<LitStr>>,
695717
}
696718

697719
#[derive(Clone)]
@@ -754,6 +776,8 @@ const _: () = {
754776
config: None,
755777
whitespace: None,
756778
crate_name: None,
779+
#[cfg(feature = "blocks")]
780+
blocks: None,
757781
};
758782
let mut has_data = false;
759783

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

811860
let value = get_lit(ident, pair.value)?;
@@ -965,6 +1014,21 @@ const _: () = {
9651014
}
9661015
}
9671016

1017+
fn get_exprarray(name: &Ident, mut expr: Expr) -> Result<ExprArray, CompileError> {
1018+
loop {
1019+
match expr {
1020+
Expr::Array(array) => return Ok(array),
1021+
Expr::Group(group) => expr = *group.expr,
1022+
v => {
1023+
return Err(CompileError::no_file_info(
1024+
format_args!("template attribute `{name}` expects an array"),
1025+
Some(v.span()),
1026+
));
1027+
}
1028+
}
1029+
}
1030+
}
1031+
9681032
fn ensure_source_only_once(
9691033
name: &Ident,
9701034
source: &Option<PartialTemplateArgsSource>,

rinja_derive/src/lib.rs

Lines changed: 1 addition & 1 deletion
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

Lines changed: 1 addition & 0 deletions
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)