You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
medium [|](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_values_and_units/Value_definition_syntax#single_bar)
170
+
thick
171
+
```
172
+
173
+
Well and this Pest grammar is where all the permissible patterns are defined. E.g. here's a high-level example for a `{% ... %}` template tag (NOTE: outdated version):
174
+
175
+
```
176
+
// The full tag is a sequence of attributes
177
+
// E.g. `{% slot key=val key2=val2 %}`
178
+
tag_wrapper = { SOI ~ django_tag ~ EOI }
179
+
180
+
django_tag = { "{%" ~ tag_content ~ "%}" }
181
+
182
+
// The contents of a tag, without the delimiters
183
+
tag_content = ${
184
+
spacing* // Optional leading whitespace/comments
185
+
~ tag_name // The tag name must come first, MAY be preceded by whitespace
186
+
~ (spacing+ ~ attribute)* // Then zero or more attributes, MUST be separated by whitespace/comments
2. Parsing and handling of the matched grammar rules.
194
+
195
+
So each defined rule has its own name, e.g. `django_tag`.
196
+
197
+
When a text is parsed with Pest in Rust, we get a list of parsed rules (or a single rule?).
198
+
199
+
Since the grammar definition specifies the entire `{% .. %}` template tag, and we pass in a string starting and ending in `{% ... %}`, we should match exactly the top-level `tag_wrapper` rule.
200
+
201
+
If we match anything else in its place, we raise an error.
202
+
203
+
Once we have `tag_wrapper`, we walk down it, rule by rule, constructing the AST from the patterns we come across.
204
+
205
+
3. Constructing the AST.
206
+
207
+
The AST consists of these nodes - Tag, TagAttr, TagToken, TagValue, TagValueFilter
208
+
209
+
- `Tag` - the entire `{% ... %}`, e.g `{% my_tag x ...[1, 2, 3] key=val / %}`
210
+
211
+
- The first word inside a `Tag` is the `tag_name`, e.g. `my_tag`.
212
+
- After the tag name, there are zero or more `TagAttrs`. This is ALL inputs, both positional and keyword
213
+
- Tag attrs are `x`, `...[1, 2, 3]`, `key=val`
214
+
- If a tag attribute has a key, that's stored on `TagAttrs`.
215
+
- But ALL `TagAttrs` MUST have a value.
216
+
- TagValue holds a single value, may have a filter, e.g. `"cool"|upper`
217
+
- TagValue may be of different kinds, e.g. string, int, float, literal list, literal dict, variable, translation `_('mystr')`, etc. The specific kind is identified by what rules we parse, and the resulting TagValue nodes are distinguished by the `ValueKind`, an enum with values like `"string"`, `"float"`, etc.
218
+
- Since TagValue can be also e.g. literal lists, TagValues may contain other TagValues. This implies that:
219
+
1. Lists and dicts themselves can have filters applied to them, e.g. `[1, 2, 3]|append:4`
220
+
2. items inside lists and dicts can too have filters applied to them. e.g. `[1|add:1, 2|add:2]`
221
+
- Any TagValue can have 0 or more filters applied to it. Filters have a name and an optional argument, e.g. `3|add:2` - filter name `add`, arg `2`. These filters are held by `TagValueFilter`.
222
+
- While the filter name is a plain identifier, the argument can be yet another TagValue. so even using literal lists and dicts at the position of filter argument is permitted, e.g. `[1]|extend:[2, 3]`
223
+
224
+
- Lastly, `TagToken` is a secondary object used by the nodes above. It contains info about the original raw string, and the line / col where the string was found.
225
+
226
+
The final AST can look like this:
227
+
228
+
INPUT:
229
+
```django
230
+
{% my_tag value|lower %}
231
+
```
232
+
233
+
AST:
234
+
```rs
235
+
Tag {
236
+
name:TagToken {
237
+
token:"my_tag".to_string(),
238
+
start_index:3,
239
+
end_index:9,
240
+
line_col: (1, 4),
241
+
},
242
+
attrs:vec![TagAttr {
243
+
key:None,
244
+
value:TagValue {
245
+
token:TagToken {
246
+
token:"value".to_string(),
247
+
start_index:10,
248
+
end_index:15,
249
+
line_col: (1, 11),
250
+
},
251
+
children:vec![],
252
+
spread:None,
253
+
filters:vec![TagValueFilter {
254
+
arg:None,
255
+
token:TagToken {
256
+
token:"lower".to_string(),
257
+
start_index:16,
258
+
end_index:21,
259
+
line_col: (1, 17),
260
+
},
261
+
start_index:15,
262
+
end_index:21,
263
+
line_col: (1, 16),
264
+
}],
265
+
kind:ValueKind::Variable,
266
+
start_index:10,
267
+
end_index:21,
268
+
line_col: (1, 11),
269
+
},
270
+
is_flag:false,
271
+
start_index:10,
272
+
end_index:21,
273
+
line_col: (1, 11),
274
+
}],
275
+
is_self_closing:false,
276
+
syntax:TagSyntax::Django,
277
+
start_index:0,
278
+
end_index:24,
279
+
line_col: (1, 4),
280
+
}
281
+
```
282
+
283
+
284
+
## On template tag compilation
285
+
286
+
Another important part is the "tag compiler". This turns the parsed AST into an executable Python function. When this function is called with the `Context` object, it resolves the inputs to a tag into Python args and kwargs.
As you can see, the generated function accepts the definitions for the functions `variable()`, `filter()`, etc.
331
+
332
+
This means that the implementation for these is defined in Python. So we can still easily change how individual features are handled. These definitions of `variable()`, etc are NOT exposed to the users of django-components.
333
+
334
+
The implementation is defined in django-components, and it looks something like below.
335
+
336
+
There you can see e.g. that when the Rust compiler came across a variable `my_var`, it generated `variable(..)` call. And the implementation for `variable(...)` calls Django's `Variable(var).resolve(ctx)`.
337
+
338
+
So at the end of the day we're still using the same Django logic to actually resolve variables into actual values.
# The compiler gives us the variable stripped of `_(")` and `"),
366
+
# so we put it back for Django's Variable class to interpret it as a translation.
367
+
translation_var = "_('" + var + "')"
368
+
return Variable(translation_var).resolve(ctx)
369
+
370
+
args, kwargs = compiled_tag(
371
+
context=context,
372
+
template_string=template_string,
373
+
variable=resolve_variable,
374
+
translation=resolve_translation,
375
+
filter=resolve_filter,
376
+
)
377
+
```
378
+
379
+
5. Call the component with the args and kwargs
380
+
381
+
The compiled function returned a list of args and a dict of kwargs. We then simply pass these further to the implementation of the `{% component %}` node.
The template tag inputs respect Python's convetion of not allowing args after kwargs.
398
+
399
+
When compiling AST into a Python function, we're able to detect obvious cases and raise an error early, like:
400
+
401
+
```django
402
+
{% component key=val my_var / %} {# Error! #}
403
+
```
404
+
However, some cases can be figured out only at render time. Becasue the spread syntax `...my_var` can be used with both a list of args or a dict of kwargs.
405
+
406
+
So we need to wait for the Context object to figure out whether this:
407
+
```django
408
+
{% component ...items my_var / %}
409
+
```
410
+
Resolves to lists (OK):
411
+
```django
412
+
{% component ...[1, 2, 3] my_var / %}
413
+
```
414
+
Or to dict (Error):
415
+
```django
416
+
{% component ...{"key": "x"} my_var / %}
417
+
```
418
+
419
+
So when we detect that there is a spread within the template tag, we add a render-time function that checks whether the spread resolves to list or a dict, and raises if it's not permitted:
0 commit comments