1pub struct Tag {
4 pub name: &'static str,
5 pub summary: &'static str,
6 pub body: Body,
7 pub attributes: &'static [Attribute],
8}
9
10pub enum Body {
12 Empty,
14 Inline,
16 Block,
18}
19
20pub struct Attribute {
21 pub name: &'static str,
22 pub summary: &'static str,
23 pub required: bool,
24 pub values: &'static [&'static str],
26}
27
28const NONE: &[&str] = &[];
29const SCOPES: &[&str] = &["page", "request", "session"];
30const ENCODINGS: &[&str] = &["on", "off", "html", "xml", "wml", "jshtml", "jsonvalue"];
31
32pub fn tag(name: &str) -> Option<&'static Tag> {
33 TAGS.iter().find(|candidate| candidate.name == name)
34}
35
36pub fn tags() -> &'static [Tag] {
37 TAGS
38}
39
40pub const TEMPLATE_ATTRIBUTE: &str = "template";
41
42pub static TAGS: &[Tag] = &[
43 Tag {
44 name: "isif",
45 summary: "Renders its body when the condition holds.",
46 body: Body::Inline,
47 attributes: &[Attribute {
48 name: "condition",
49 summary: "Expression to evaluate.",
50 required: true,
51 values: NONE,
52 }],
53 },
54 Tag {
55 name: "iselseif",
56 summary: "Further branch of an <isif>.",
57 body: Body::Empty,
58 attributes: &[Attribute {
59 name: "condition",
60 summary: "Expression to evaluate.",
61 required: true,
62 values: NONE,
63 }],
64 },
65 Tag {
66 name: "iselse",
67 summary: "Fallback branch of an <isif>.",
68 body: Body::Empty,
69 attributes: NONE_ATTRIBUTES,
70 },
71 Tag {
72 name: "isloop",
73 summary: "Iterates over a collection or iterator.",
74 body: Body::Inline,
75 attributes: &[
76 Attribute {
77 name: "items",
78 summary: "Collection to iterate over.",
79 required: true,
80 values: NONE,
81 },
82 Attribute {
83 name: "var",
84 summary: "Variable bound to the current element.",
85 required: true,
86 values: NONE,
87 },
88 Attribute {
89 name: "status",
90 summary: "Variable holding count, index, first, last, odd, even.",
91 required: false,
92 values: NONE,
93 },
94 Attribute {
95 name: "begin",
96 summary: "Index to start at.",
97 required: false,
98 values: NONE,
99 },
100 Attribute {
101 name: "end",
102 summary: "Index to stop at.",
103 required: false,
104 values: NONE,
105 },
106 Attribute {
107 name: "step",
108 summary: "Increment between iterations.",
109 required: false,
110 values: NONE,
111 },
112 Attribute {
113 name: "iterator",
114 summary: "Deprecated alias of items.",
115 required: false,
116 values: NONE,
117 },
118 ],
119 },
120 Tag {
121 name: "isbreak",
122 summary: "Leaves the innermost loop.",
123 body: Body::Empty,
124 attributes: NONE_ATTRIBUTES,
125 },
126 Tag {
127 name: "iscontinue",
128 summary: "Skips to the next iteration.",
129 body: Body::Empty,
130 attributes: NONE_ATTRIBUTES,
131 },
132 Tag {
133 name: "isnext",
134 summary: "Advances the iterator by one position.",
135 body: Body::Empty,
136 attributes: NONE_ATTRIBUTES,
137 },
138 Tag {
139 name: "isinclude",
140 summary: "Includes another template, or the output of a URL.",
141 body: Body::Empty,
142 attributes: &[
143 Attribute {
144 name: "template",
145 summary: "Template path, without the .isml suffix.",
146 required: false,
147 values: NONE,
148 },
149 Attribute {
150 name: "url",
151 summary: "Remote include: a URL, usually from URLUtils.",
152 required: false,
153 values: NONE,
154 },
155 Attribute {
156 name: "sf-toolkit",
157 summary: "Storefront Toolkit decoration.",
158 required: false,
159 values: &["on", "off"],
160 },
161 ],
162 },
163 Tag {
164 name: "isdecorate",
165 summary: "Wraps the body in a decorator template.",
166 body: Body::Inline,
167 attributes: &[Attribute {
168 name: "template",
169 summary: "Decorator template path.",
170 required: true,
171 values: NONE,
172 }],
173 },
174 Tag {
175 name: "isreplace",
176 summary: "Marks where a decorator inserts the decorated content.",
177 body: Body::Empty,
178 attributes: &[Attribute {
179 name: "name",
180 summary: "Named replacement region.",
181 required: false,
182 values: NONE,
183 }],
184 },
185 Tag {
186 name: "ismodule",
187 summary: "Declares a custom tag backed by a template.",
188 body: Body::Empty,
189 attributes: &[
190 Attribute {
191 name: "template",
192 summary: "Template implementing the tag.",
193 required: true,
194 values: NONE,
195 },
196 Attribute {
197 name: "name",
198 summary: "Name of the custom tag.",
199 required: true,
200 values: NONE,
201 },
202 Attribute {
203 name: "attribute",
204 summary: "Attribute the custom tag accepts. Repeatable.",
205 required: false,
206 values: NONE,
207 },
208 ],
209 },
210 Tag {
211 name: "iscomponent",
212 summary: "Remote include of a controller or pipeline.",
213 body: Body::Empty,
214 attributes: &[
215 Attribute {
216 name: "pipeline",
217 summary: "Controller-Action or pipeline to call.",
218 required: true,
219 values: NONE,
220 },
221 Attribute {
222 name: "locale",
223 summary: "Locale to render the component in.",
224 required: false,
225 values: NONE,
226 },
227 ],
228 },
229 Tag {
230 name: "isslot",
231 summary: "Placeholder filled from Business Manager.",
232 body: Body::Empty,
233 attributes: &[
234 Attribute {
235 name: "id",
236 summary: "Slot id.",
237 required: true,
238 values: NONE,
239 },
240 Attribute {
241 name: "description",
242 summary: "What the slot is for, shown in Business Manager.",
243 required: true,
244 values: NONE,
245 },
246 Attribute {
247 name: "context",
248 summary: "Configuration context of the slot.",
249 required: true,
250 values: &["global", "category", "folder"],
251 },
252 Attribute {
253 name: "context-object",
254 summary: "Category or folder the context refers to.",
255 required: false,
256 values: NONE,
257 },
258 Attribute {
259 name: "preview-url",
260 summary: "URL used to preview the slot.",
261 required: false,
262 values: NONE,
263 },
264 ],
265 },
266 Tag {
267 name: "isprint",
268 summary: "Formats and encodes a value for output.",
269 body: Body::Empty,
270 attributes: &[
271 Attribute {
272 name: "value",
273 summary: "Expression to print.",
274 required: true,
275 values: NONE,
276 },
277 Attribute {
278 name: "encoding",
279 summary: "Output encoding. Only turn it off for trusted content.",
280 required: false,
281 values: ENCODINGS,
282 },
283 Attribute {
284 name: "style",
285 summary: "Named format for money, numbers and dates.",
286 required: false,
287 values: &[
288 "MONEY_SHORT",
289 "MONEY_LONG",
290 "INTEGER",
291 "DECIMAL",
292 "QUANTITY_SHORT",
293 "QUANTITY_LONG",
294 "DATE_SHORT",
295 "DATE_LONG",
296 "DATE_TIME",
297 ],
298 },
299 Attribute {
300 name: "formatter",
301 summary: "Explicit format pattern.",
302 required: false,
303 values: NONE,
304 },
305 Attribute {
306 name: "timezone",
307 summary: "Time zone a date is rendered in.",
308 required: false,
309 values: &["SITE", "INSTANCE", "utc"],
310 },
311 Attribute {
312 name: "padding",
313 summary: "Minimum width, padded with spaces.",
314 required: false,
315 values: NONE,
316 },
317 ],
318 },
319 Tag {
320 name: "isset",
321 summary: "Defines a variable in a scope.",
322 body: Body::Empty,
323 attributes: &[
324 Attribute {
325 name: "name",
326 summary: "Variable name.",
327 required: true,
328 values: NONE,
329 },
330 Attribute {
331 name: "value",
332 summary: "Value to assign.",
333 required: true,
334 values: NONE,
335 },
336 Attribute {
337 name: "scope",
338 summary: "Where the variable lives.",
339 required: false,
340 values: SCOPES,
341 },
342 ],
343 },
344 Tag {
345 name: "isremove",
346 summary: "Removes a variable from a scope.",
347 body: Body::Empty,
348 attributes: &[
349 Attribute {
350 name: "name",
351 summary: "Variable name.",
352 required: true,
353 values: NONE,
354 },
355 Attribute {
356 name: "scope",
357 summary: "Where the variable lives.",
358 required: false,
359 values: SCOPES,
360 },
361 ],
362 },
363 Tag {
364 name: "isscript",
365 summary: "Server-side JavaScript with the dw.* API available.",
366 body: Body::Block,
367 attributes: NONE_ATTRIBUTES,
368 },
369 Tag {
370 name: "iscomment",
371 summary: "Comment stripped before the response is sent.",
372 body: Body::Block,
373 attributes: NONE_ATTRIBUTES,
374 },
375 Tag {
376 name: "iscache",
377 summary: "Caching policy for the page.",
378 body: Body::Empty,
379 attributes: &[
380 Attribute {
381 name: "type",
382 summary: "Expiry strategy.",
383 required: true,
384 values: &["relative", "daily"],
385 },
386 Attribute {
387 name: "hour",
388 summary: "Hours to cache for, or hour of the day when daily.",
389 required: false,
390 values: NONE,
391 },
392 Attribute {
393 name: "minute",
394 summary: "Minutes to cache for.",
395 required: false,
396 values: NONE,
397 },
398 Attribute {
399 name: "varyby",
400 summary: "Adds price and promotion to the cache key.",
401 required: false,
402 values: &["price_promotion"],
403 },
404 Attribute {
405 name: "status",
406 summary: "Turns caching off for this template.",
407 required: false,
408 values: &["on", "off"],
409 },
410 ],
411 },
412 Tag {
413 name: "iscontent",
414 summary: "Sets the content type and encoding of the response.",
415 body: Body::Empty,
416 attributes: &[
417 Attribute {
418 name: "type",
419 summary: "MIME type.",
420 required: false,
421 values: &["text/html", "text/xml", "application/json", "text/plain"],
422 },
423 Attribute {
424 name: "encoding",
425 summary: "Output encoding for the whole template.",
426 required: false,
427 values: ENCODINGS,
428 },
429 Attribute {
430 name: "charset",
431 summary: "Character set.",
432 required: false,
433 values: &["UTF-8", "ISO-8859-1"],
434 },
435 Attribute {
436 name: "compact",
437 summary: "Collapses whitespace in the output.",
438 required: false,
439 values: &["true", "false"],
440 },
441 ],
442 },
443 Tag {
444 name: "isredirect",
445 summary: "Sends an HTTP redirect.",
446 body: Body::Empty,
447 attributes: &[
448 Attribute {
449 name: "location",
450 summary: "Target URL.",
451 required: true,
452 values: NONE,
453 },
454 Attribute {
455 name: "permanent",
456 summary: "301 instead of 302.",
457 required: false,
458 values: &["true", "false"],
459 },
460 ],
461 },
462 Tag {
463 name: "isstatus",
464 summary: "Sets the HTTP status code of the response.",
465 body: Body::Empty,
466 attributes: &[Attribute {
467 name: "value",
468 summary: "Status code.",
469 required: true,
470 values: NONE,
471 }],
472 },
473 Tag {
474 name: "iscookie",
475 summary: "Sets a cookie on the response.",
476 body: Body::Empty,
477 attributes: &[
478 Attribute {
479 name: "name",
480 summary: "Cookie name.",
481 required: true,
482 values: NONE,
483 },
484 Attribute {
485 name: "value",
486 summary: "Cookie value.",
487 required: true,
488 values: NONE,
489 },
490 Attribute {
491 name: "domain",
492 summary: "Domain the cookie is sent to.",
493 required: false,
494 values: NONE,
495 },
496 Attribute {
497 name: "path",
498 summary: "Path the cookie is sent for.",
499 required: false,
500 values: NONE,
501 },
502 Attribute {
503 name: "maxAge",
504 summary: "Lifetime in seconds.",
505 required: false,
506 values: NONE,
507 },
508 Attribute {
509 name: "secure",
510 summary: "Only send over HTTPS.",
511 required: false,
512 values: &["true", "false"],
513 },
514 Attribute {
515 name: "comment",
516 summary: "Cookie comment.",
517 required: false,
518 values: NONE,
519 },
520 Attribute {
521 name: "version",
522 summary: "Cookie version.",
523 required: false,
524 values: NONE,
525 },
526 ],
527 },
528 Tag {
529 name: "isobject",
530 summary: "Records a product impression for active data.",
531 body: Body::Inline,
532 attributes: &[
533 Attribute {
534 name: "object",
535 summary: "Product or content being viewed.",
536 required: true,
537 values: NONE,
538 },
539 Attribute {
540 name: "view",
541 summary: "Context the object is shown in.",
542 required: true,
543 values: &[
544 "none",
545 "searchhit",
546 "recommendation",
547 "setproduct",
548 "detail",
549 ],
550 },
551 ],
552 },
553 Tag {
554 name: "isselect",
555 summary: "Renders a select element from an iterator.",
556 body: Body::Empty,
557 attributes: &[
558 Attribute {
559 name: "name",
560 summary: "Form field name.",
561 required: true,
562 values: NONE,
563 },
564 Attribute {
565 name: "iterator",
566 summary: "Collection backing the options.",
567 required: true,
568 values: NONE,
569 },
570 Attribute {
571 name: "value",
572 summary: "Expression giving each option value.",
573 required: false,
574 values: NONE,
575 },
576 Attribute {
577 name: "description",
578 summary: "Expression giving each option label.",
579 required: false,
580 values: NONE,
581 },
582 Attribute {
583 name: "condition",
584 summary: "Expression selecting the preselected option.",
585 required: false,
586 values: NONE,
587 },
588 Attribute {
589 name: "encoding",
590 summary: "Output encoding.",
591 required: false,
592 values: ENCODINGS,
593 },
594 ],
595 },
596 Tag {
597 name: "isactivedatahead",
598 summary: "Loads the active data tracking libraries. Goes in <head>.",
599 body: Body::Empty,
600 attributes: NONE_ATTRIBUTES,
601 },
602 Tag {
603 name: "isactivedatacontext",
604 summary: "Reports the category context of the page.",
605 body: Body::Empty,
606 attributes: &[Attribute {
607 name: "category",
608 summary: "Deepest category being browsed.",
609 required: true,
610 values: NONE,
611 }],
612 },
613 Tag {
614 name: "isanalyticsoff",
615 summary: "Suppresses the analytics snippet on this page.",
616 body: Body::Empty,
617 attributes: NONE_ATTRIBUTES,
618 },
619 Tag {
620 name: "isapplepay",
621 summary: "Renders the Apple Pay button.",
622 body: Body::Empty,
623 attributes: NONE_ATTRIBUTES,
624 },
625 Tag {
626 name: "isbuynow",
627 summary: "Express checkout button for one product (B2C Commerce Payments).",
628 body: Body::Empty,
629 attributes: NONE_ATTRIBUTES,
630 },
631 Tag {
632 name: "ispayment",
633 summary: "Payment methods for the basket (B2C Commerce Payments).",
634 body: Body::Empty,
635 attributes: NONE_ATTRIBUTES,
636 },
637 Tag {
638 name: "ispaymentmessages",
639 summary: "Instalment and credit callouts (B2C Commerce Payments).",
640 body: Body::Empty,
641 attributes: NONE_ATTRIBUTES,
642 },
643];
644
645const NONE_ATTRIBUTES: &[Attribute] = &[];
646
647#[cfg(test)]
648mod tests {
649 use super::*;
650
651 #[test]
652 fn covers_the_tags_a_template_actually_uses() {
653 for name in ["isif", "isloop", "isinclude", "isprint", "isset", "iscache"] {
654 assert!(tag(name).is_some(), "{name} is missing");
655 }
656 }
657
658 #[test]
659 fn has_no_namespaced_html_lookalikes() {
660 assert!(tags().iter().all(|tag| !tag.name.contains(':')));
661 }
662
663 #[test]
664 fn marks_the_required_attributes_of_isslot() {
665 let required: Vec<&str> = tag("isslot")
666 .unwrap()
667 .attributes
668 .iter()
669 .filter(|attribute| attribute.required)
670 .map(|attribute| attribute.name)
671 .collect();
672 assert_eq!(required, ["id", "description", "context"]);
673 }
674}