logicaffeine_lsp/
template.rs1pub const TAGS: &[(&str, &str)] = &[
14 ("else if", "{else if condition:}"),
15 ("else", "{else:}"),
16 ("if", "{if condition:} … {end}"),
17 (
18 "for each",
19 "{for each item in items[, keyed by key][, at index i]:} … {end}",
20 ),
21 ("inspect", "{inspect value:} {when Pattern:} … {end}"),
22 ("when", "{when Pattern:}"),
23 ("let", "{let name be expression}"),
24 ("extends", "{extends Layout}"),
25 ("block", "{block name:} … {end}"),
26 ("children", "{children} — the caller's markup"),
27 (
28 "raw",
29 "{raw expression} — unescaped, so only for markup you trust",
30 ),
31 ("end", "{end}"),
32];
33
34pub const FOR_EACH_CLAUSES: &[&str] = &["in", "keyed by", "at index"];
36
37pub fn tag_phrase(inner: &str) -> Option<(&'static str, usize)> {
40 let trimmed = inner.trim_start();
41 let lead = inner.len() - trimmed.len();
42 TAGS.iter().map(|(phrase, _)| *phrase).find_map(|phrase| {
43 let rest = trimmed.strip_prefix(phrase)?;
44 let ends_word = rest
45 .chars()
46 .next()
47 .is_none_or(|c| !c.is_alphanumeric() && c != '_');
48 ends_word.then_some((phrase, lead + phrase.len()))
49 })
50}
51
52pub fn is_comment(inner: &str) -> bool {
54 inner.trim_start().starts_with('#')
55}
56
57pub fn filter_pipes(text: &str) -> Vec<usize> {
60 let mut pipes = Vec::new();
61 let mut depth = 0usize;
62 let mut in_string = false;
63 let mut escaped = false;
64 for (i, c) in text.char_indices() {
65 if in_string {
66 match c {
67 _ if escaped => escaped = false,
68 '\\' => escaped = true,
69 '"' => in_string = false,
70 _ => {}
71 }
72 continue;
73 }
74 match c {
75 '"' => in_string = true,
76 '(' | '[' | '{' => depth += 1,
77 ')' | ']' | '}' => depth = depth.saturating_sub(1),
78 '|' if depth == 0 => pipes.push(i),
79 _ => {}
80 }
81 }
82 pipes
83}
84
85#[cfg(test)]
86#[path = "../../logicaffeine_tests/unit/logicaffeine_lsp/template/tests.rs"]
87mod tests;