1use super::html::{
26 self, AttrPart, AttrValue, EventModifier, HtmlAttr, HtmlErrorKind, HtmlToken, ModifierError,
27};
28use crate::token::Span;
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct TExpr {
33 pub text: String,
34 pub offset: usize,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct TFilter {
40 pub name: String,
41 pub args: Vec<TExpr>,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
46pub enum TAttr {
47 Static {
48 name: String,
49 value: Option<String>,
50 span: Span,
51 },
52 Dynamic {
53 name: String,
54 parts: Vec<AttrPart>,
55 span: Span,
56 },
57 On {
59 event: String,
60 modifiers: Vec<EventModifier>,
61 expr: TExpr,
62 span: Span,
63 },
64 Bind {
66 field: String,
67 at: usize,
68 span: Span,
69 },
70 Class {
71 name: String,
72 expr: TExpr,
73 span: Span,
74 },
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
79pub enum TNode {
80 Element {
81 tag: String,
82 attrs: Vec<TAttr>,
83 children: Vec<TNode>,
84 span: Span,
85 },
86 Text {
87 text: String,
88 span: Span,
89 },
90 Hole {
91 expr: TExpr,
92 filters: Vec<TFilter>,
93 raw: bool,
94 span: Span,
95 },
96 If {
97 arms: Vec<(TExpr, Vec<TNode>)>,
98 otherwise: Option<Vec<TNode>>,
99 span: Span,
100 },
101 For {
102 pattern: String,
103 iterable: TExpr,
104 key: Option<TExpr>,
105 index: Option<String>,
106 body: Vec<TNode>,
107 empty: Option<Vec<TNode>>,
108 span: Span,
109 },
110 Inspect {
111 scrutinee: TExpr,
112 arms: Vec<(String, Vec<TNode>)>,
113 span: Span,
114 },
115 Let {
116 name: String,
117 expr: TExpr,
118 span: Span,
119 },
120 Extends {
121 name: String,
122 span: Span,
123 },
124 Block {
125 name: String,
126 default: Option<Vec<TNode>>,
127 span: Span,
128 },
129 Children {
130 span: Span,
131 },
132 Component {
133 name: String,
134 props: Vec<TAttr>,
135 children: Vec<TNode>,
136 span: Span,
137 },
138}
139
140#[derive(Debug, Clone, PartialEq, Eq)]
141pub enum TemplateErrorKind {
142 Html(HtmlErrorKind),
143 UnexpectedEnd,
145 UnclosedTag(String),
148 MismatchedEndTag {
150 expected: String,
151 found: String,
152 },
153 UnclosedElement(String),
155 StrayEndTag(String),
157 UnexpectedEndTagOfVoid(String),
159 ExtendsNotFirst,
161 StrayElse,
163 DuplicateElse,
165 StrayWhen,
167 TagLikeHole(String),
169 MalformedTag(String),
171 EmptyHole,
173 MalformedDirective(String),
175 NonCanonicalTag {
178 written: String,
179 canonical: String,
180 },
181 EventModifier(ModifierError),
183 RetiredViewForm(RetiredForm),
185}
186
187#[derive(Debug, Clone, PartialEq, Eq)]
189pub enum RetiredForm {
190 Element(String),
193 Stylesheet,
195}
196
197pub fn retired_view_form(line: &str) -> Option<RetiredForm> {
202 let t = line.trim();
203 if let Some(rest) = t.strip_prefix("css") {
204 if rest.starts_with(char::is_whitespace) {
205 let rest = rest.trim_start();
206 if rest.starts_with('"') || rest.starts_with("r\"") {
207 return Some(RetiredForm::Stylesheet);
208 }
209 }
210 }
211 let tag_end = t
212 .find(|c: char| !(c.is_ascii_lowercase() || c.is_ascii_digit()))
213 .unwrap_or(t.len());
214 let tag = &t[..tag_end];
215 if tag.is_empty() || !html::is_html_element(tag) {
216 return None;
217 }
218 let mut rest = &t[tag_end..];
219 if let Some(attrs) = rest.trim_start().strip_prefix('[') {
220 rest = &attrs[attrs.find(']')? + 1..];
221 }
222 let value = rest.trim_start().strip_prefix(':')?.trim();
223 let is_call = value.ends_with(')')
224 && value.split_once('(').is_some_and(|(name, _)| {
225 !name.is_empty() && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
226 });
227 (value.is_empty() || value.starts_with('"') || is_call)
228 .then(|| RetiredForm::Element(tag.to_string()))
229}
230
231fn retired_line_in(src: &str, span: Span) -> Option<(RetiredForm, Span)> {
235 (span.start..span.end)
236 .filter(|&at| at == 0 || src.as_bytes()[at - 1] == b'\n')
237 .find_map(|start| {
238 let end = src[start..].find('\n').map_or(src.len(), |n| start + n);
239 let line = &src[start..end];
240 let form = retired_view_form(line)?;
241 let words = start + (line.len() - line.trim_start().len());
242 Some((form, Span::new(words, start + line.trim_end().len())))
243 })
244}
245
246#[derive(Debug, Clone, PartialEq, Eq)]
247pub struct TemplateError {
248 pub kind: TemplateErrorKind,
249 pub span: Span,
250}
251
252impl TemplateError {
253 pub fn message(&self) -> String {
255 use TemplateErrorKind::*;
256 match &self.kind {
257 Html(k) => html::HtmlError { kind: k.clone(), span: self.span }.message(),
258 UnexpectedEnd => "This `{end}` closes nothing — there is no open `{if …:}`, `{for each …:}`, `{inspect …:}` or `{block …:}` here.".into(),
259 UnclosedTag(t) => format!("This `{{{t} …:}}` is never closed. Every template tag that opens a body ends with `{{end}}` — and it must close before the element that contains it does."),
260 MismatchedEndTag { expected, found } => format!("`</{found}>` closes the wrong element: the innermost open element is `<{expected}>`. Close `<{expected}>` first, or fix the tag name."),
261 UnclosedElement(t) => format!("`<{t}>` is never closed. Add `</{t}>` (or write it as `<{t} …/>` if it has no children)."),
262 StrayEndTag(t) => format!("`</{t}>` closes nothing — no `<{t}>` is open here."),
263 UnexpectedEndTagOfVoid(t) => format!("`<{t}>` is a void element: it never has children or an end tag. Remove `</{t}>`."),
264 ExtendsNotFirst => "`{extends …}` must be the very first thing in a view: the layout is chosen before any content.".into(),
265 StrayElse => "This `{else…}` has no `{if …:}` (or `{for each …:}`) to belong to — it must sit between the opener and its `{end}`.".into(),
266 DuplicateElse => "An `{if …:}` or `{for each …:}` can have only one `{else:}` arm.".into(),
267 StrayWhen => "`{when …:}` only belongs inside `{inspect …:} … {end}`.".into(),
268 TagLikeHole(t) => format!("`{{{t}}}` ends in `:` like a template tag, but `{}` is not one. The tags are `if`, `else if`, `else`, `for each`, `inspect`, `when`, `let`, `extends`, `block`, `children`, `raw`, `end` — or drop the `:` to make it a hole.", t.split_whitespace().next().unwrap_or("")),
269 MalformedTag(t) => format!("This `{{{t} …}}` tag is missing a part. Shapes: `{{if condition:}}`, `{{else if condition:}}`, `{{else:}}`, `{{for each item in items[, keyed by key][, at index i]:}}`, `{{inspect value:}}`, `{{when Pattern:}}`, `{{let name be expression}}`, `{{extends Layout}}`, `{{block name:}}` / `{{block name}}`, `{{children}}`, `{{raw expression}}`, `{{end}}`."),
270 EmptyHole => "This `{}` is empty. A hole holds a LOGOS expression: `{name}`, `{length of todos}`, `{price | money}`.".into(),
271 MalformedDirective(d) => format!("`{d}` needs a `{{…}}` value: `on-click={{add}}`, `on-click={{remove(todo's id)}}`, `bind={{draft}}` (a state field), `class:done={{todo's done}}`."),
272 EventModifier(e) => e.message(),
273 NonCanonicalTag { written, canonical } => format!(
274 "`{written}` is written `{canonical}` in an LGX template. The tags are `{{if c:}}`, \
275 `{{else if c:}}`, `{{else:}}`, `{{for each x in xs:}}`, `{{inspect v:}}`, \
276 `{{when P:}}` and `{{end}}`, which closes any of them."
277 ),
278 RetiredViewForm(RetiredForm::Element(tag)) => format!(
279 "`{tag}: …` is the retired `Render:` way to write an element. In a `View:` an element \
280 is markup — `<{tag}>…</{tag}>` — and a component is placed by its name: \
281 `<Card title=\"Hello\"/>`."
282 ),
283 RetiredViewForm(RetiredForm::Stylesheet) => {
284 "`css \"…\"` is the retired `Render:` way to write a stylesheet. A component's CSS goes \
285 in its own `Style:` section, beside `View:`, scoped to the component."
286 .into()
287 }
288 }
289 }
290}
291
292const TAG_WORDS: &[&str] = &[
293 "if", "else", "end", "for", "inspect", "when", "let", "extends", "block", "children", "raw",
294];
295
296pub const TAG_ALIASES: &[(&str, &str)] = &[
299 ("elif", "else if"),
300 ("elsif", "else if"),
301 ("elseif", "else if"),
302 ("otherwise", "else"),
303 ("endif", "end"),
304 ("endfor", "end"),
305];
306
307enum Frame {
308 Element {
309 tag: String,
310 attrs: Vec<TAttr>,
311 children: Vec<TNode>,
312 start: usize,
313 verbatim: bool,
314 },
315 Component {
316 name: String,
317 props: Vec<TAttr>,
318 children: Vec<TNode>,
319 start: usize,
320 },
321 If {
322 arms: Vec<(TExpr, Vec<TNode>)>,
323 cond: TExpr,
324 current: Vec<TNode>,
325 otherwise: Option<Vec<TNode>>,
326 in_else: bool,
327 start: usize,
328 },
329 For {
330 pattern: String,
331 iterable: TExpr,
332 key: Option<TExpr>,
333 index: Option<String>,
334 body: Vec<TNode>,
335 current: Vec<TNode>,
336 in_else: bool,
337 start: usize,
338 },
339 Inspect {
340 scrutinee: TExpr,
341 arms: Vec<(String, Vec<TNode>)>,
342 current: Option<(String, Vec<TNode>)>,
343 start: usize,
344 },
345 Block {
346 name: String,
347 children: Vec<TNode>,
348 start: usize,
349 },
350}
351
352impl Frame {
353 fn tag_word(&self) -> &'static str {
354 match self {
355 Frame::If { .. } => "if",
356 Frame::For { .. } => "for each",
357 Frame::Inspect { .. } => "inspect",
358 Frame::Block { .. } => "block",
359 Frame::Element { .. } | Frame::Component { .. } => "",
360 }
361 }
362 fn start(&self) -> usize {
363 match self {
364 Frame::Element { start, .. }
365 | Frame::Component { start, .. }
366 | Frame::If { start, .. }
367 | Frame::For { start, .. }
368 | Frame::Inspect { start, .. }
369 | Frame::Block { start, .. } => *start,
370 }
371 }
372}
373
374struct P {
375 stack: Vec<Frame>,
376 root: Vec<TNode>,
377 saw_content: bool,
378}
379
380impl P {
381 fn push_node(&mut self, node: TNode, at: usize) -> Result<(), TemplateError> {
382 match self.stack.last_mut() {
383 None => {
384 if !matches!(node, TNode::Extends { .. }) {
385 self.saw_content = true;
386 }
387 self.root.push(node);
388 Ok(())
389 }
390 Some(Frame::Element { children, .. })
391 | Some(Frame::Component { children, .. })
392 | Some(Frame::Block { children, .. }) => {
393 children.push(node);
394 Ok(())
395 }
396 Some(Frame::If { current, .. }) | Some(Frame::For { current, .. }) => {
397 current.push(node);
398 Ok(())
399 }
400 Some(Frame::Inspect { current, .. }) => match current {
401 Some((_, body)) => {
402 body.push(node);
403 Ok(())
404 }
405 None => {
406 if let TNode::Text { text, .. } = &node {
408 if text.trim().is_empty() {
409 return Ok(());
410 }
411 }
412 Err(TemplateError {
413 kind: TemplateErrorKind::MalformedTag("inspect".into()),
414 span: Span::new(at, at),
415 })
416 }
417 },
418 }
419 }
420
421 fn verbatim(&self) -> bool {
422 self.stack
423 .iter()
424 .any(|f| matches!(f, Frame::Element { verbatim: true, .. }))
425 }
426}
427
428pub fn parse(src: &str) -> Result<Vec<TNode>, TemplateError> {
430 let tokens = html::tokenize(src).map_err(|e| TemplateError {
431 kind: TemplateErrorKind::Html(e.kind),
432 span: e.span,
433 })?;
434 let mut p = P {
435 stack: Vec::new(),
436 root: Vec::new(),
437 saw_content: false,
438 };
439 for tok in tokens {
440 match tok {
441 HtmlToken::Text { text, span } => {
442 if p.verbatim() {
443 p.push_node(TNode::Text { text, span }, span.start)?;
444 continue;
445 }
446 if let Some((form, at)) = retired_line_in(src, span) {
447 return Err(TemplateError {
448 kind: TemplateErrorKind::RetiredViewForm(form),
449 span: at,
450 });
451 }
452 if let Some(t) = collapse(&text) {
453 p.push_node(TNode::Text { text: t, span }, span.start)?;
454 }
455 }
456 HtmlToken::StartTag {
457 name,
458 attrs,
459 self_closing,
460 span,
461 } => {
462 let converted = convert_attrs(&attrs)?;
463 if html::is_component_name(&name) {
464 if self_closing {
465 p.push_node(
466 TNode::Component {
467 name,
468 props: converted,
469 children: Vec::new(),
470 span,
471 },
472 span.start,
473 )?;
474 } else {
475 p.stack.push(Frame::Component {
476 name,
477 props: converted,
478 children: Vec::new(),
479 start: span.start,
480 });
481 }
482 } else if self_closing {
483 p.push_node(
484 TNode::Element {
485 tag: name,
486 attrs: converted,
487 children: Vec::new(),
488 span,
489 },
490 span.start,
491 )?;
492 } else {
493 let verbatim =
494 name.eq_ignore_ascii_case("pre") || name.eq_ignore_ascii_case("textarea");
495 p.stack.push(Frame::Element {
496 tag: name,
497 attrs: converted,
498 children: Vec::new(),
499 start: span.start,
500 verbatim,
501 });
502 }
503 }
504 HtmlToken::EndTag { name, span } => match p.stack.pop() {
505 None => {
506 if html::is_void(&name) {
507 return Err(TemplateError {
508 kind: TemplateErrorKind::UnexpectedEndTagOfVoid(name),
509 span,
510 });
511 }
512 return Err(TemplateError {
513 kind: TemplateErrorKind::StrayEndTag(name),
514 span,
515 });
516 }
517 Some(Frame::Element {
518 tag,
519 attrs,
520 children,
521 start,
522 ..
523 }) => {
524 if !tag.eq_ignore_ascii_case(&name) {
525 if html::is_void(&name) {
526 return Err(TemplateError {
527 kind: TemplateErrorKind::UnexpectedEndTagOfVoid(name),
528 span,
529 });
530 }
531 return Err(TemplateError {
532 kind: TemplateErrorKind::MismatchedEndTag {
533 expected: tag,
534 found: name,
535 },
536 span,
537 });
538 }
539 p.push_node(
540 TNode::Element {
541 tag,
542 attrs,
543 children,
544 span: Span::new(start, span.end),
545 },
546 start,
547 )?;
548 }
549 Some(Frame::Component {
550 name: cname,
551 props,
552 children,
553 start,
554 }) => {
555 if cname != name {
556 return Err(TemplateError {
557 kind: TemplateErrorKind::MismatchedEndTag {
558 expected: cname,
559 found: name,
560 },
561 span,
562 });
563 }
564 p.push_node(
565 TNode::Component {
566 name: cname,
567 props,
568 children,
569 span: Span::new(start, span.end),
570 },
571 start,
572 )?;
573 }
574 Some(other) => {
575 return Err(TemplateError {
576 kind: TemplateErrorKind::UnclosedTag(other.tag_word().to_string()),
577 span: Span::new(other.start(), span.end),
578 });
579 }
580 },
581 HtmlToken::Brace { inner, span } => handle_brace(&mut p, &inner, span)?,
582 }
583 }
584 if let Some(top) = p.stack.pop() {
585 let start = top.start();
586 return Err(match top {
587 Frame::Element { tag, .. } => TemplateError {
588 kind: TemplateErrorKind::UnclosedElement(tag),
589 span: Span::new(start, src.len()),
590 },
591 Frame::Component { name, .. } => TemplateError {
592 kind: TemplateErrorKind::UnclosedElement(name),
593 span: Span::new(start, src.len()),
594 },
595 other => TemplateError {
596 kind: TemplateErrorKind::UnclosedTag(other.tag_word().to_string()),
597 span: Span::new(start, src.len()),
598 },
599 });
600 }
601 Ok(p.root)
602}
603
604fn collapse(text: &str) -> Option<String> {
607 if text.trim().is_empty() {
608 return if text.contains('\n') {
609 None
610 } else {
611 Some(" ".to_string())
612 };
613 }
614 let mut out = String::with_capacity(text.len());
615 let mut in_ws = false;
616 for c in text.chars() {
617 if c.is_whitespace() {
618 if !in_ws {
619 out.push(' ');
620 in_ws = true;
621 }
622 } else {
623 out.push(c);
624 in_ws = false;
625 }
626 }
627 Some(out)
628}
629
630fn convert_attrs(attrs: &[HtmlAttr]) -> Result<Vec<TAttr>, TemplateError> {
631 let mut out = Vec::with_capacity(attrs.len());
632 for a in attrs {
633 let single_brace = |a: &HtmlAttr| -> Option<TExpr> {
634 match &a.value {
635 Some(AttrValue::Parts(parts)) if parts.len() == 1 => match &parts[0] {
636 AttrPart::Brace { inner, span } => Some(texpr(inner, span.start + 1)),
637 _ => None,
638 },
639 _ => None,
640 }
641 };
642 if let Some(rest) = a.name.strip_prefix("on-") {
643 let mut segs = rest.split('.');
644 let event = segs.next().unwrap_or("").to_string();
645 let words: Vec<&str> = segs.collect();
646 let expr = single_brace(a).ok_or(TemplateError {
647 kind: TemplateErrorKind::MalformedDirective(a.name.clone()),
648 span: a.span,
649 })?;
650 if event.is_empty() || expr.text.trim().is_empty() {
651 return Err(TemplateError {
652 kind: TemplateErrorKind::MalformedDirective(a.name.clone()),
653 span: a.span,
654 });
655 }
656 let modifiers = html::classify_modifiers(&event, &words).map_err(|(at, e)| {
657 let start = a.span.start
659 + "on-".len()
660 + event.len()
661 + words[..at].iter().map(|w| w.len() + 1).sum::<usize>();
662 TemplateError {
663 kind: TemplateErrorKind::EventModifier(e),
664 span: Span::new(start, start + 1 + words[at].len()),
665 }
666 })?;
667 out.push(TAttr::On {
668 event,
669 modifiers,
670 expr,
671 span: a.span,
672 });
673 } else if a.name == "bind" {
674 let expr = single_brace(a).ok_or(TemplateError {
675 kind: TemplateErrorKind::MalformedDirective("bind".into()),
676 span: a.span,
677 })?;
678 let field = expr.text.trim().to_string();
679 if field.is_empty() || !field.chars().all(|c| c.is_alphanumeric() || c == '_') {
680 return Err(TemplateError {
681 kind: TemplateErrorKind::MalformedDirective("bind".into()),
682 span: a.span,
683 });
684 }
685 out.push(TAttr::Bind {
686 field,
687 at: expr.offset,
688 span: a.span,
689 });
690 } else if let Some(class) = a.name.strip_prefix("class:") {
691 let expr = single_brace(a).ok_or(TemplateError {
692 kind: TemplateErrorKind::MalformedDirective(a.name.clone()),
693 span: a.span,
694 })?;
695 if class.is_empty() {
696 return Err(TemplateError {
697 kind: TemplateErrorKind::MalformedDirective(a.name.clone()),
698 span: a.span,
699 });
700 }
701 out.push(TAttr::Class {
702 name: class.to_string(),
703 expr,
704 span: a.span,
705 });
706 } else {
707 match &a.value {
708 None => out.push(TAttr::Static {
709 name: a.name.clone(),
710 value: None,
711 span: a.span,
712 }),
713 Some(AttrValue::Static(v)) => out.push(TAttr::Static {
714 name: a.name.clone(),
715 value: Some(v.clone()),
716 span: a.span,
717 }),
718 Some(AttrValue::Parts(parts)) => out.push(TAttr::Dynamic {
719 name: a.name.clone(),
720 parts: parts.clone(),
721 span: a.span,
722 }),
723 }
724 }
725 }
726 Ok(out)
727}
728
729fn texpr(text: &str, base: usize) -> TExpr {
732 let trimmed_start = text.len() - text.trim_start().len();
733 TExpr {
734 text: text.trim().to_string(),
735 offset: base + trimmed_start,
736 }
737}
738
739fn handle_brace(p: &mut P, inner: &str, span: Span) -> Result<(), TemplateError> {
740 let body = inner.trim();
741 let base = span.start + 1; if body.is_empty() {
743 return Err(TemplateError {
744 kind: TemplateErrorKind::EmptyHole,
745 span,
746 });
747 }
748 if body.starts_with('#') && body.ends_with('#') && body.len() >= 2 {
749 return Ok(());
750 }
751 let first = body
752 .split(|c: char| c.is_whitespace() || c == ':')
753 .next()
754 .unwrap_or("");
755 if let Some((_, canonical)) = TAG_ALIASES.iter().find(|(alias, _)| *alias == first) {
756 let rest = body[first.len()..].trim_start();
757 let spelled = if rest.is_empty() || rest.starts_with(':') {
758 format!("{{{canonical}{rest}}}")
759 } else {
760 format!("{{{canonical} {rest}}}")
761 };
762 return Err(TemplateError {
763 kind: TemplateErrorKind::NonCanonicalTag {
764 written: format!("{{{body}}}"),
765 canonical: spelled,
766 },
767 span,
768 });
769 }
770 if !TAG_WORDS.contains(&first) {
771 if body.ends_with(':') {
772 return Err(TemplateError {
773 kind: TemplateErrorKind::TagLikeHole(body.to_string()),
774 span,
775 });
776 }
777 return hole(p, inner, base, span, false);
778 }
779 let rest_after_first = body[first.len()..].trim_start();
780 match first {
781 "end" => {
782 if !rest_after_first.is_empty() {
783 return Err(TemplateError {
784 kind: TemplateErrorKind::MalformedTag("end".into()),
785 span,
786 });
787 }
788 close_tag(p, span)
789 }
790 "if" => {
791 let cond = strip_colon(rest_after_first).ok_or(TemplateError {
792 kind: TemplateErrorKind::MalformedTag("if".into()),
793 span,
794 })?;
795 if cond.trim().is_empty() {
796 return Err(TemplateError {
797 kind: TemplateErrorKind::MalformedTag("if".into()),
798 span,
799 });
800 }
801 let cond = texpr(cond, base + offset_of(inner, cond));
802 p.stack.push(Frame::If {
803 arms: Vec::new(),
804 cond,
805 current: Vec::new(),
806 otherwise: None,
807 in_else: false,
808 start: span.start,
809 });
810 Ok(())
811 }
812 "else" => {
813 if let Some(rest) = rest_after_first.strip_prefix("if") {
814 let cond = strip_colon(rest.trim_start()).ok_or(TemplateError {
815 kind: TemplateErrorKind::MalformedTag("else if".into()),
816 span,
817 })?;
818 if cond.trim().is_empty() {
819 return Err(TemplateError {
820 kind: TemplateErrorKind::MalformedTag("else if".into()),
821 span,
822 });
823 }
824 let cond = texpr(cond, base + offset_of(inner, cond));
825 match p.stack.last_mut() {
826 Some(Frame::If {
827 arms,
828 cond: cur_cond,
829 current,
830 in_else: false,
831 ..
832 }) => {
833 let finished = std::mem::take(current);
834 let prev = std::mem::replace(cur_cond, cond);
835 arms.push((prev, finished));
836 Ok(())
837 }
838 Some(Frame::If { in_else: true, .. }) => Err(TemplateError {
839 kind: TemplateErrorKind::DuplicateElse,
840 span,
841 }),
842 _ => Err(TemplateError {
843 kind: TemplateErrorKind::StrayElse,
844 span,
845 }),
846 }
847 } else {
848 if strip_colon(rest_after_first).map(|s| s.trim().is_empty()) != Some(true) {
849 return Err(TemplateError {
850 kind: TemplateErrorKind::MalformedTag("else".into()),
851 span,
852 });
853 }
854 match p.stack.last_mut() {
855 Some(Frame::If {
856 arms,
857 cond,
858 current,
859 otherwise,
860 in_else,
861 ..
862 }) => {
863 if *in_else {
864 return Err(TemplateError {
865 kind: TemplateErrorKind::DuplicateElse,
866 span,
867 });
868 }
869 let finished = std::mem::take(current);
870 arms.push((cond.clone(), finished));
871 *otherwise = Some(Vec::new());
872 *in_else = true;
873 Ok(())
874 }
875 Some(Frame::For {
876 body,
877 current,
878 in_else,
879 ..
880 }) => {
881 if *in_else {
882 return Err(TemplateError {
883 kind: TemplateErrorKind::DuplicateElse,
884 span,
885 });
886 }
887 *body = std::mem::take(current);
888 *in_else = true;
889 Ok(())
890 }
891 _ => Err(TemplateError {
892 kind: TemplateErrorKind::StrayElse,
893 span,
894 }),
895 }
896 }
897 }
898 "for" => {
899 let rest = rest_after_first
900 .strip_prefix("each")
901 .map(str::trim_start)
902 .ok_or(TemplateError {
903 kind: TemplateErrorKind::MalformedTag("for each".into()),
904 span,
905 })?;
906 let rest = strip_colon(rest).ok_or(TemplateError {
907 kind: TemplateErrorKind::MalformedTag("for each".into()),
908 span,
909 })?;
910 let in_pos = find_top_level_word(rest, " in ").ok_or(TemplateError {
911 kind: TemplateErrorKind::MalformedTag("for each".into()),
912 span,
913 })?;
914 let pattern = rest[..in_pos].trim().to_string();
915 let after_in = &rest[in_pos + 4..];
916 let clauses = split_top_level(after_in, ',');
917 let iterable_text = clauses.first().copied().unwrap_or("").trim();
918 if pattern.is_empty() || iterable_text.is_empty() {
919 return Err(TemplateError {
920 kind: TemplateErrorKind::MalformedTag("for each".into()),
921 span,
922 });
923 }
924 let iterable = texpr(iterable_text, base + offset_of(inner, iterable_text));
925 let mut key = None;
926 let mut index = None;
927 for clause in clauses.iter().skip(1) {
928 let c = clause.trim();
929 if let Some(k) = c.strip_prefix("keyed by") {
930 let k = k.trim();
931 if k.is_empty() {
932 return Err(TemplateError {
933 kind: TemplateErrorKind::MalformedTag("for each".into()),
934 span,
935 });
936 }
937 key = Some(texpr(k, base + offset_of(inner, k)));
938 } else if let Some(i) = c.strip_prefix("at index") {
939 let i = i.trim();
940 if i.is_empty() {
941 return Err(TemplateError {
942 kind: TemplateErrorKind::MalformedTag("for each".into()),
943 span,
944 });
945 }
946 index = Some(i.to_string());
947 } else {
948 return Err(TemplateError {
949 kind: TemplateErrorKind::MalformedTag("for each".into()),
950 span,
951 });
952 }
953 }
954 p.stack.push(Frame::For {
955 pattern,
956 iterable,
957 key,
958 index,
959 body: Vec::new(),
960 current: Vec::new(),
961 in_else: false,
962 start: span.start,
963 });
964 Ok(())
965 }
966 "inspect" => {
967 let s = strip_colon(rest_after_first).ok_or(TemplateError {
968 kind: TemplateErrorKind::MalformedTag("inspect".into()),
969 span,
970 })?;
971 if s.trim().is_empty() {
972 return Err(TemplateError {
973 kind: TemplateErrorKind::MalformedTag("inspect".into()),
974 span,
975 });
976 }
977 let scrutinee = texpr(s, base + offset_of(inner, s));
978 p.stack.push(Frame::Inspect {
979 scrutinee,
980 arms: Vec::new(),
981 current: None,
982 start: span.start,
983 });
984 Ok(())
985 }
986 "when" => {
987 let pat = strip_colon(rest_after_first).ok_or(TemplateError {
988 kind: TemplateErrorKind::MalformedTag("when".into()),
989 span,
990 })?;
991 if pat.trim().is_empty() {
992 return Err(TemplateError {
993 kind: TemplateErrorKind::MalformedTag("when".into()),
994 span,
995 });
996 }
997 match p.stack.last_mut() {
998 Some(Frame::Inspect { arms, current, .. }) => {
999 if let Some(done) = current.take() {
1000 arms.push(done);
1001 }
1002 *current = Some((pat.trim().to_string(), Vec::new()));
1003 Ok(())
1004 }
1005 _ => Err(TemplateError {
1006 kind: TemplateErrorKind::StrayWhen,
1007 span,
1008 }),
1009 }
1010 }
1011 "let" => {
1012 let be = find_top_level_word(rest_after_first, " be ").ok_or(TemplateError {
1013 kind: TemplateErrorKind::MalformedTag("let".into()),
1014 span,
1015 })?;
1016 let name = rest_after_first[..be].trim().to_string();
1017 let e = rest_after_first[be + 4..].trim();
1018 if name.is_empty() || e.is_empty() || e.ends_with(':') {
1019 return Err(TemplateError {
1020 kind: TemplateErrorKind::MalformedTag("let".into()),
1021 span,
1022 });
1023 }
1024 let expr = texpr(e, base + offset_of(inner, e));
1025 p.push_node(TNode::Let { name, expr, span }, span.start)
1026 }
1027 "extends" => {
1028 let name = rest_after_first.trim();
1029 if name.is_empty() || name.contains(char::is_whitespace) || name.ends_with(':') {
1030 return Err(TemplateError {
1031 kind: TemplateErrorKind::MalformedTag("extends".into()),
1032 span,
1033 });
1034 }
1035 if !p.stack.is_empty() || p.saw_content {
1036 return Err(TemplateError {
1037 kind: TemplateErrorKind::ExtendsNotFirst,
1038 span,
1039 });
1040 }
1041 p.push_node(
1042 TNode::Extends {
1043 name: name.to_string(),
1044 span,
1045 },
1046 span.start,
1047 )
1048 }
1049 "block" => {
1050 let has_colon = rest_after_first.ends_with(':');
1051 let name = rest_after_first.trim_end_matches(':').trim();
1052 if name.is_empty() || name.contains(char::is_whitespace) {
1053 return Err(TemplateError {
1054 kind: TemplateErrorKind::MalformedTag("block".into()),
1055 span,
1056 });
1057 }
1058 if has_colon {
1059 p.stack.push(Frame::Block {
1060 name: name.to_string(),
1061 children: Vec::new(),
1062 start: span.start,
1063 });
1064 Ok(())
1065 } else {
1066 p.push_node(
1067 TNode::Block {
1068 name: name.to_string(),
1069 default: None,
1070 span,
1071 },
1072 span.start,
1073 )
1074 }
1075 }
1076 "children" => {
1077 if !rest_after_first.is_empty() {
1078 return Err(TemplateError {
1079 kind: TemplateErrorKind::MalformedTag("children".into()),
1080 span,
1081 });
1082 }
1083 p.push_node(TNode::Children { span }, span.start)
1084 }
1085 "raw" => {
1086 if rest_after_first.is_empty() || rest_after_first.ends_with(':') {
1087 return Err(TemplateError {
1088 kind: TemplateErrorKind::MalformedTag("raw".into()),
1089 span,
1090 });
1091 }
1092 hole(
1093 p,
1094 rest_after_first,
1095 base + offset_of(inner, rest_after_first),
1096 span,
1097 true,
1098 )
1099 }
1100 _ => unreachable!("every TAG_WORD is handled above"),
1101 }
1102}
1103
1104fn close_tag(p: &mut P, span: Span) -> Result<(), TemplateError> {
1105 let any_tag_open = p
1107 .stack
1108 .iter()
1109 .any(|f| !matches!(f, Frame::Element { .. } | Frame::Component { .. }));
1110 if !any_tag_open {
1111 return Err(TemplateError {
1112 kind: TemplateErrorKind::UnexpectedEnd,
1113 span,
1114 });
1115 }
1116 match p.stack.pop() {
1117 None => Err(TemplateError {
1118 kind: TemplateErrorKind::UnexpectedEnd,
1119 span,
1120 }),
1121 Some(Frame::If {
1122 mut arms,
1123 cond,
1124 current,
1125 otherwise,
1126 in_else,
1127 start,
1128 }) => {
1129 let otherwise = if in_else {
1132 Some(current)
1133 } else {
1134 arms.push((cond, current));
1135 otherwise
1136 };
1137 let node = TNode::If {
1138 arms,
1139 otherwise,
1140 span: Span::new(start, span.end),
1141 };
1142 p.push_node(node, start)
1143 }
1144 Some(Frame::For {
1145 pattern,
1146 iterable,
1147 key,
1148 index,
1149 body,
1150 current,
1151 in_else,
1152 start,
1153 }) => {
1154 let (body, empty) = if in_else {
1155 (body, Some(current))
1156 } else {
1157 (current, None)
1158 };
1159 p.push_node(
1160 TNode::For {
1161 pattern,
1162 iterable,
1163 key,
1164 index,
1165 body,
1166 empty,
1167 span: Span::new(start, span.end),
1168 },
1169 start,
1170 )
1171 }
1172 Some(Frame::Inspect {
1173 scrutinee,
1174 mut arms,
1175 current,
1176 start,
1177 }) => {
1178 if let Some(done) = current {
1179 arms.push(done);
1180 }
1181 p.push_node(
1182 TNode::Inspect {
1183 scrutinee,
1184 arms,
1185 span: Span::new(start, span.end),
1186 },
1187 start,
1188 )
1189 }
1190 Some(Frame::Block {
1191 name,
1192 children,
1193 start,
1194 }) => p.push_node(
1195 TNode::Block {
1196 name,
1197 default: Some(children),
1198 span: Span::new(start, span.end),
1199 },
1200 start,
1201 ),
1202 Some(Frame::Element { tag, start, .. }) => Err(TemplateError {
1203 kind: TemplateErrorKind::UnclosedElement(tag),
1204 span: Span::new(start, span.end),
1205 }),
1206 Some(Frame::Component { name, start, .. }) => Err(TemplateError {
1207 kind: TemplateErrorKind::UnclosedElement(name),
1208 span: Span::new(start, span.end),
1209 }),
1210 }
1211}
1212
1213fn hole(p: &mut P, text: &str, base: usize, span: Span, raw: bool) -> Result<(), TemplateError> {
1214 let segments = split_top_level(text, '|');
1215 let mut iter = segments.iter();
1216 let head = iter.next().copied().unwrap_or("");
1217 if head.trim().is_empty() {
1218 return Err(TemplateError {
1219 kind: TemplateErrorKind::EmptyHole,
1220 span,
1221 });
1222 }
1223 let expr = texpr(head, base + offset_of(text, head));
1224 let mut filters = Vec::new();
1225 for seg in iter {
1226 let seg_trim = seg.trim();
1227 if seg_trim.is_empty() {
1228 return Err(TemplateError {
1229 kind: TemplateErrorKind::EmptyHole,
1230 span,
1231 });
1232 }
1233 let name_end = seg_trim.find(char::is_whitespace).unwrap_or(seg_trim.len());
1234 let name = seg_trim[..name_end].to_string();
1235 let args_text = &seg_trim[name_end..];
1236 let args: Vec<TExpr> = split_top_level_ws(args_text)
1237 .into_iter()
1238 .map(|a| texpr(a, base + offset_of(text, a)))
1239 .collect();
1240 filters.push(TFilter { name, args });
1241 }
1242 p.push_node(
1243 TNode::Hole {
1244 expr,
1245 filters,
1246 raw,
1247 span,
1248 },
1249 span.start,
1250 )
1251}
1252
1253fn offset_of(whole: &str, part: &str) -> usize {
1255 let w = whole.as_ptr() as usize;
1256 let p = part.as_ptr() as usize;
1257 if p >= w && p <= w + whole.len() {
1258 p - w
1259 } else {
1260 0
1261 }
1262}
1263
1264fn strip_colon(s: &str) -> Option<&str> {
1265 s.trim_end().strip_suffix(':')
1266}
1267
1268fn find_top_level_word(s: &str, needle: &str) -> Option<usize> {
1270 let b = s.as_bytes();
1271 let mut depth = 0i32;
1272 let mut i = 0;
1273 while i < b.len() {
1274 match b[i] {
1275 b'"' => {
1276 i += 1;
1277 while i < b.len() && b[i] != b'"' {
1278 if b[i] == b'\\' {
1279 i += 1;
1280 }
1281 i += 1;
1282 }
1283 }
1284 b'(' | b'[' | b'{' => depth += 1,
1285 b')' | b']' | b'}' => depth -= 1,
1286 _ if depth == 0 && b[i..].starts_with(needle.as_bytes()) => return Some(i),
1293 _ => {}
1294 }
1295 i += 1;
1296 }
1297 None
1298}
1299
1300fn split_top_level(s: &str, delim: char) -> Vec<&str> {
1303 let b = s.as_bytes();
1304 let d = delim as u8;
1305 let mut out = Vec::new();
1306 let mut depth = 0i32;
1307 let mut start = 0usize;
1308 let mut i = 0;
1309 while i < b.len() {
1310 match b[i] {
1311 b'"' => {
1312 i += 1;
1313 while i < b.len() && b[i] != b'"' {
1314 if b[i] == b'\\' {
1315 i += 1;
1316 }
1317 i += 1;
1318 }
1319 }
1320 b'(' | b'[' | b'{' => depth += 1,
1321 b')' | b']' | b'}' => depth -= 1,
1322 c if c == d && depth == 0 => {
1323 out.push(&s[start..i]);
1324 start = i + 1;
1325 }
1326 _ => {}
1327 }
1328 i += 1;
1329 }
1330 out.push(&s[start.min(s.len())..]);
1331 out
1332}
1333
1334fn split_top_level_ws(s: &str) -> Vec<&str> {
1336 let b = s.as_bytes();
1337 let mut out = Vec::new();
1338 let mut depth = 0i32;
1339 let mut start: Option<usize> = None;
1340 let mut i = 0;
1341 while i < b.len() {
1342 let c = b[i];
1343 match c {
1344 b'"' => {
1345 if start.is_none() {
1346 start = Some(i);
1347 }
1348 i += 1;
1349 while i < b.len() && b[i] != b'"' {
1350 if b[i] == b'\\' {
1351 i += 1;
1352 }
1353 i += 1;
1354 }
1355 }
1356 b'(' | b'[' | b'{' => {
1357 if start.is_none() {
1358 start = Some(i);
1359 }
1360 depth += 1;
1361 }
1362 b')' | b']' | b'}' => depth -= 1,
1363 _ if c.is_ascii_whitespace() && depth == 0 => {
1364 if let Some(st) = start.take() {
1365 out.push(&s[st..i]);
1366 }
1367 }
1368 _ => {
1369 if start.is_none() {
1370 start = Some(i);
1371 }
1372 }
1373 }
1374 i += 1;
1375 }
1376 if let Some(st) = start {
1377 out.push(&s[st..]);
1378 }
1379 out
1380}
1381
1382pub fn print(nodes: &[TNode]) -> String {
1390 let mut out = String::new();
1391 print_nodes(nodes, &mut out);
1392 out
1393}
1394
1395fn print_nodes(nodes: &[TNode], out: &mut String) {
1396 for n in nodes {
1397 print_node(n, out);
1398 }
1399}
1400
1401fn escape_text(s: &str, out: &mut String) {
1402 for c in s.chars() {
1403 match c {
1404 '&' => out.push_str("&"),
1405 '<' => out.push_str("<"),
1406 '>' => out.push_str(">"),
1407 '{' => out.push_str("{"),
1408 '}' => out.push_str("}"),
1409 _ => out.push(c),
1410 }
1411 }
1412}
1413
1414fn escape_attr(s: &str, out: &mut String) {
1415 for c in s.chars() {
1416 match c {
1417 '&' => out.push_str("&"),
1418 '"' => out.push_str("""),
1419 '{' => out.push_str("{"),
1420 '}' => out.push_str("}"),
1421 _ => out.push(c),
1422 }
1423 }
1424}
1425
1426fn print_attrs(attrs: &[TAttr], out: &mut String) {
1427 for a in attrs {
1428 out.push(' ');
1429 match a {
1430 TAttr::Static {
1431 name, value: None, ..
1432 } => out.push_str(name),
1433 TAttr::Static {
1434 name,
1435 value: Some(v),
1436 ..
1437 } => {
1438 out.push_str(name);
1439 out.push_str("=\"");
1440 escape_attr(v, out);
1441 out.push('"');
1442 }
1443 TAttr::Dynamic { name, parts, .. } => {
1444 out.push_str(name);
1445 out.push('=');
1446 if let [AttrPart::Brace { inner, .. }] = parts.as_slice() {
1447 out.push('{');
1448 out.push_str(inner.trim());
1449 out.push('}');
1450 } else {
1451 out.push('"');
1452 for p in parts {
1453 match p {
1454 AttrPart::Text(t) => escape_attr(t, out),
1455 AttrPart::Brace { inner, .. } => {
1456 out.push('{');
1457 out.push_str(inner.trim());
1458 out.push('}');
1459 }
1460 }
1461 }
1462 out.push('"');
1463 }
1464 }
1465 TAttr::On {
1466 event,
1467 modifiers,
1468 expr,
1469 ..
1470 } => {
1471 out.push_str("on-");
1472 out.push_str(event);
1473 for m in modifiers {
1474 out.push('.');
1475 out.push_str(&m.word());
1476 }
1477 out.push_str("={");
1478 out.push_str(&expr.text);
1479 out.push('}');
1480 }
1481 TAttr::Bind { field, .. } => {
1482 out.push_str("bind={");
1483 out.push_str(field);
1484 out.push('}');
1485 }
1486 TAttr::Class { name, expr, .. } => {
1487 out.push_str("class:");
1488 out.push_str(name);
1489 out.push_str("={");
1490 out.push_str(&expr.text);
1491 out.push('}');
1492 }
1493 }
1494 }
1495}
1496
1497fn print_node(n: &TNode, out: &mut String) {
1498 match n {
1499 TNode::Element {
1500 tag,
1501 attrs,
1502 children,
1503 ..
1504 } => {
1505 out.push('<');
1506 out.push_str(tag);
1507 print_attrs(attrs, out);
1508 out.push('>');
1509 if html::is_void(tag) {
1510 return;
1511 }
1512 print_nodes(children, out);
1513 out.push_str("</");
1514 out.push_str(tag);
1515 out.push('>');
1516 }
1517 TNode::Text { text, .. } => escape_text(text, out),
1518 TNode::Hole {
1519 expr, filters, raw, ..
1520 } => {
1521 out.push('{');
1522 if *raw {
1523 out.push_str("raw ");
1524 }
1525 out.push_str(&expr.text);
1526 for f in filters {
1527 out.push_str(" | ");
1528 out.push_str(&f.name);
1529 for a in &f.args {
1530 out.push(' ');
1531 out.push_str(&a.text);
1532 }
1533 }
1534 out.push('}');
1535 }
1536 TNode::If {
1537 arms, otherwise, ..
1538 } => {
1539 for (i, (cond, body)) in arms.iter().enumerate() {
1540 out.push_str(if i == 0 { "{if " } else { "{else if " });
1541 out.push_str(&cond.text);
1542 out.push_str(":}");
1543 print_nodes(body, out);
1544 }
1545 if let Some(o) = otherwise {
1546 out.push_str("{else:}");
1547 print_nodes(o, out);
1548 }
1549 out.push_str("{end}");
1550 }
1551 TNode::For {
1552 pattern,
1553 iterable,
1554 key,
1555 index,
1556 body,
1557 empty,
1558 ..
1559 } => {
1560 out.push_str("{for each ");
1561 out.push_str(pattern);
1562 out.push_str(" in ");
1563 out.push_str(&iterable.text);
1564 if let Some(k) = key {
1565 out.push_str(", keyed by ");
1566 out.push_str(&k.text);
1567 }
1568 if let Some(i) = index {
1569 out.push_str(", at index ");
1570 out.push_str(i);
1571 }
1572 out.push_str(":}");
1573 print_nodes(body, out);
1574 if let Some(e) = empty {
1575 out.push_str("{else:}");
1576 print_nodes(e, out);
1577 }
1578 out.push_str("{end}");
1579 }
1580 TNode::Inspect {
1581 scrutinee, arms, ..
1582 } => {
1583 out.push_str("{inspect ");
1584 out.push_str(&scrutinee.text);
1585 out.push_str(":}");
1586 for (pat, body) in arms {
1587 out.push_str("{when ");
1588 out.push_str(pat);
1589 out.push_str(":}");
1590 print_nodes(body, out);
1591 }
1592 out.push_str("{end}");
1593 }
1594 TNode::Let { name, expr, .. } => {
1595 out.push_str("{let ");
1596 out.push_str(name);
1597 out.push_str(" be ");
1598 out.push_str(&expr.text);
1599 out.push('}');
1600 }
1601 TNode::Extends { name, .. } => {
1602 out.push_str("{extends ");
1603 out.push_str(name);
1604 out.push('}');
1605 }
1606 TNode::Block { name, default, .. } => {
1607 out.push_str("{block ");
1608 out.push_str(name);
1609 match default {
1610 None => out.push('}'),
1611 Some(d) => {
1612 out.push_str(":}");
1613 print_nodes(d, out);
1614 out.push_str("{end}");
1615 }
1616 }
1617 }
1618 TNode::Children { .. } => out.push_str("{children}"),
1619 TNode::Component {
1620 name,
1621 props,
1622 children,
1623 ..
1624 } => {
1625 out.push('<');
1626 out.push_str(name);
1627 print_attrs(props, out);
1628 if children.is_empty() {
1629 out.push_str("/>");
1630 } else {
1631 out.push('>');
1632 print_nodes(children, out);
1633 out.push_str("</");
1634 out.push_str(name);
1635 out.push('>');
1636 }
1637 }
1638 }
1639}