Skip to main content

isml_lsp/
isml.rs

1//! The ISML tag set, so editors stop offering HTML-mode guesses like `<is:include>`.
2
3pub struct Tag {
4    pub name: &'static str,
5    pub summary: &'static str,
6    pub body: Body,
7    pub attributes: &'static [Attribute],
8}
9
10/// Decides the snippet.
11pub enum Body {
12    /// `<isinclude ... />`
13    Empty,
14    /// `<isif ...> ... </isif>`
15    Inline,
16    /// `<isscript> ... </isscript>`, opened on its own line.
17    Block,
18}
19
20pub struct Attribute {
21    pub name: &'static str,
22    pub summary: &'static str,
23    pub required: bool,
24    /// Empty when any value is accepted.
25    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}