Skip to content

Commit c3b0a11

Browse files
committed
Implement custom simple block tags
1 parent ff9da59 commit c3b0a11

File tree

4 files changed

+199
-8
lines changed

4 files changed

+199
-8
lines changed

src/parse.rs

Lines changed: 140 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,33 @@ impl PartialEq for SimpleTag {
456456
}
457457
}
458458

459+
#[derive(Clone, Debug)]
460+
pub struct SimpleBlockTag {
461+
pub func: Arc<Py<PyAny>>,
462+
pub nodes: Vec<TokenTree>,
463+
pub at: (usize, usize),
464+
pub takes_context: bool,
465+
pub args: Vec<TagElement>,
466+
pub kwargs: Vec<(String, TagElement)>,
467+
pub target_var: Option<String>,
468+
}
469+
470+
impl PartialEq for SimpleBlockTag {
471+
fn eq(&self, other: &Self) -> bool {
472+
// We use `Arc::ptr_eq` here to avoid needing the `py` token for true
473+
// equality comparison between two `Py` smart pointers.
474+
//
475+
// We only use `eq` in tests, so this concession is acceptable here.
476+
self.at == other.at
477+
&& self.takes_context == other.takes_context
478+
&& self.args == other.args
479+
&& self.kwargs == other.kwargs
480+
&& self.target_var == other.target_var
481+
&& self.nodes == other.nodes
482+
&& Arc::ptr_eq(&self.func, &other.func)
483+
}
484+
}
485+
459486
#[derive(Clone, Debug, PartialEq)]
460487
pub enum Tag {
461488
Autoescape {
@@ -470,6 +497,7 @@ pub enum Tag {
470497
For(For),
471498
Load,
472499
SimpleTag(SimpleTag),
500+
SimpleBlockTag(SimpleBlockTag),
473501
Url(Url),
474502
}
475503

@@ -482,6 +510,7 @@ enum EndTagType {
482510
Empty,
483511
EndFor,
484512
Verbatim,
513+
Custom(String),
485514
}
486515

487516
impl EndTagType {
@@ -494,6 +523,7 @@ impl EndTagType {
494523
Self::Empty => "empty",
495524
Self::EndFor => "endfor",
496525
Self::Verbatim => "endverbatim",
526+
Self::Custom(s) => return Cow::Owned(s.clone()),
497527
};
498528
Cow::Borrowed(end_tag)
499529
}
@@ -680,6 +710,12 @@ pub enum ParseError {
680710
#[label("here")]
681711
at: SourceSpan,
682712
},
713+
#[error("'{name}' must have a first argument of 'content'")]
714+
RequiresContent {
715+
name: String,
716+
#[label("loaded here")]
717+
at: SourceSpan,
718+
},
683719
#[error(
684720
"'{name}' is decorated with takes_context=True so it must have a first argument of 'context'"
685721
)]
@@ -809,7 +845,7 @@ impl LoadToken {
809845
}
810846
}
811847

812-
#[derive(Clone)]
848+
#[derive(Debug, Clone)]
813849
struct SimpleTagContext<'py> {
814850
func: Bound<'py, PyAny>,
815851
function_name: String,
@@ -825,6 +861,11 @@ struct SimpleTagContext<'py> {
825861
#[derive(Clone)]
826862
enum TagContext<'py> {
827863
Simple(SimpleTagContext<'py>),
864+
SimpleBlock {
865+
end_tag_name: String,
866+
context: SimpleTagContext<'py>,
867+
},
868+
EndSimpleBlock,
828869
}
829870

830871
pub struct Parser<'t, 'l, 'py> {
@@ -1091,6 +1132,21 @@ impl<'t, 'l, 'py> Parser<'t, 'l, 'py> {
10911132
Some(TagContext::Simple(context)) => {
10921133
Either::Left(self.parse_simple_tag(context, at, parts)?)
10931134
}
1135+
Some(TagContext::SimpleBlock {
1136+
context,
1137+
end_tag_name,
1138+
}) => Either::Left(self.parse_simple_block_tag(
1139+
context.clone(),
1140+
tag_name.to_string(),
1141+
end_tag_name.clone(),
1142+
at,
1143+
parts,
1144+
)?),
1145+
Some(TagContext::EndSimpleBlock) => Either::Right(EndTag {
1146+
end: EndTagType::Custom(tag_name.to_string()),
1147+
at,
1148+
parts,
1149+
}),
10941150
None => todo!("{tag_name}"),
10951151
},
10961152
})
@@ -1218,6 +1274,32 @@ impl<'t, 'l, 'py> Parser<'t, 'l, 'py> {
12181274
Ok(TokenTree::Tag(Tag::SimpleTag(tag)))
12191275
}
12201276

1277+
fn parse_simple_block_tag(
1278+
&mut self,
1279+
context: SimpleTagContext,
1280+
tag_name: String,
1281+
end_tag_name: String,
1282+
at: (usize, usize),
1283+
parts: TagParts,
1284+
) -> Result<TokenTree, PyParseError> {
1285+
let (args, kwargs, target_var) = self.parse_custom_tag_parts(parts, &context)?;
1286+
let (nodes, end_tag) = self.parse_until(
1287+
vec![EndTagType::Custom(end_tag_name)],
1288+
Cow::Owned(tag_name),
1289+
at,
1290+
)?;
1291+
let tag = SimpleBlockTag {
1292+
func: context.func.clone().unbind().into(),
1293+
nodes,
1294+
at,
1295+
takes_context: context.takes_context,
1296+
args,
1297+
kwargs,
1298+
target_var,
1299+
};
1300+
Ok(TokenTree::Tag(Tag::SimpleBlockTag(tag)))
1301+
}
1302+
12211303
fn parse_load(
12221304
&mut self,
12231305
at: (usize, usize),
@@ -1301,7 +1383,63 @@ impl<'t, 'l, 'py> Parser<'t, 'l, 'py> {
13011383
if closure_names.contains(&"filename".to_string()) {
13021384
todo!("Inclusion tag")
13031385
} else if closure_names.contains(&"end_name".to_string()) {
1304-
todo!("Simple block tag")
1386+
let defaults_count = get_defaults_count(&closure_values[0])?;
1387+
let end_tag_name: String = closure_values[1].extract()?;
1388+
let func = closure_values[2].clone();
1389+
let function_name = closure_values[3].extract()?;
1390+
let kwonly = closure_values[4].extract()?;
1391+
let kwonly_defaults = get_kwonly_defaults(&closure_values[5])?;
1392+
let params: Vec<String> = closure_values[6].extract()?;
1393+
let takes_context = closure_values[7].is_truthy()?;
1394+
let varargs = !closure_values[8].is_none();
1395+
let varkw = !closure_values[9].is_none();
1396+
1397+
let params = match takes_context {
1398+
false => {
1399+
if let Some(param) = params.first()
1400+
&& param == "content"
1401+
{
1402+
params.iter().skip(1).cloned().collect()
1403+
} else {
1404+
return Err(ParseError::RequiresContent {
1405+
name: function_name,
1406+
at: at.into(),
1407+
}
1408+
.into());
1409+
}
1410+
}
1411+
true => {
1412+
todo!("context and content");
1413+
if let Some(param) = params.first()
1414+
&& param == "context"
1415+
{
1416+
params.iter().skip(1).cloned().collect()
1417+
} else {
1418+
return Err(ParseError::RequiresContext {
1419+
name: function_name,
1420+
at: at.into(),
1421+
}
1422+
.into());
1423+
}
1424+
}
1425+
};
1426+
// TODO: `end_tag_name already present?
1427+
self.external_tags
1428+
.insert(end_tag_name.clone(), TagContext::EndSimpleBlock);
1429+
TagContext::SimpleBlock {
1430+
end_tag_name,
1431+
context: SimpleTagContext {
1432+
func,
1433+
function_name,
1434+
takes_context,
1435+
params,
1436+
defaults_count,
1437+
varargs,
1438+
kwonly,
1439+
kwonly_defaults,
1440+
varkw,
1441+
},
1442+
}
13051443
} else {
13061444
let defaults_count = get_defaults_count(&closure_values[0])?;
13071445
let func = closure_values[1].clone();

src/render/tags.rs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use pyo3::types::{PyBool, PyDict, PyList, PyNone, PyString, PyTuple};
1212
use super::types::{AsBorrowedContent, Content, Context, PyContext};
1313
use super::{Evaluate, Render, RenderResult, Resolve, ResolveFailures, ResolveResult};
1414
use crate::error::{AnnotatePyErr, PyRenderError, RenderError};
15-
use crate::parse::{For, IfCondition, SimpleTag, Tag, Url};
15+
use crate::parse::{For, IfCondition, SimpleBlockTag, SimpleTag, Tag, Url};
1616
use crate::template::django_rusty_templates::NoReverseMatch;
1717
use crate::types::TemplateString;
1818
use crate::utils::PyResultMethods;
@@ -653,6 +653,7 @@ impl Render for Tag {
653653
Self::For(for_tag) => for_tag.render(py, template, context)?,
654654
Self::Load => Cow::Borrowed(""),
655655
Self::SimpleTag(simple_tag) => simple_tag.render(py, template, context)?,
656+
Self::SimpleBlockTag(simple_tag) => simple_tag.render(py, template, context)?,
656657
Self::Url(url) => url.render(py, template, context)?,
657658
})
658659
}
@@ -861,3 +862,43 @@ impl Render for SimpleTag {
861862
}
862863
}
863864
}
865+
866+
impl Render for SimpleBlockTag {
867+
fn render<'t>(
868+
&self,
869+
py: Python<'_>,
870+
template: TemplateString<'t>,
871+
context: &mut Context,
872+
) -> RenderResult<'t> {
873+
let mut args = VecDeque::new();
874+
for arg in &self.args {
875+
match arg.resolve(py, template, context, ResolveFailures::Raise)? {
876+
None => return Ok(Cow::Borrowed("")),
877+
Some(arg) => args.push_back(arg.to_py(py)?),
878+
}
879+
}
880+
let kwargs = PyDict::new(py);
881+
for (key, value) in &self.kwargs {
882+
let value = value.resolve(py, template, context, ResolveFailures::Raise)?;
883+
kwargs.set_item(key, value)?;
884+
}
885+
886+
let content = self.nodes.render(py, template, context)?;
887+
let content = PyString::new(py, &content).into_any();
888+
args.push_front(content);
889+
890+
if self.takes_context {
891+
let py_context = add_context_to_args(py, &mut args, context)?;
892+
893+
// Actually call the tag
894+
let result = call_tag(py, &self.func, self.at, template, args, kwargs);
895+
896+
retrieve_context(py, py_context, context);
897+
898+
// Return the result of calling the tag
899+
result
900+
} else {
901+
call_tag(py, &self.func, self.at, template, args, kwargs)
902+
}
903+
}
904+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from django.template import engines
2+
3+
4+
def test_simple_block_tag_repeat():
5+
template = "{% load repeat from custom_tags %}{% repeat 5 %}foo{% endrepeat %}"
6+
7+
django_template = engines["django"].from_string(template)
8+
rust_template = engines["rusty"].from_string(template)
9+
10+
expected = "foofoofoofoofoo"
11+
assert django_template.render({}) == expected
12+
assert rust_template.render({}) == expected

tests/templatetags/custom_tags.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,11 @@ def counter(context):
7272
return ""
7373

7474

75-
# @register.simple_block_tag
76-
# def repeat(content, count):
77-
# return content * count
78-
#
79-
#
75+
@register.simple_block_tag
76+
def repeat(content, count):
77+
return content * count
78+
79+
8080
# @register.inclusion_tag("results.html")
8181
# def results(poll):
8282
# return {"choices": poll.choices}

0 commit comments

Comments
 (0)