Skip to main content

microsoft_fast_build/
error.rs

1use std::fmt;
2
3/// An error encountered while rendering a FAST HTML template.
4#[derive(Debug)]
5pub enum RenderError {
6    /// `{{expr` with no closing `}}`.
7    UnclosedBinding { expr: String, context: String },
8    /// `{{{expr` with no closing `}}}`.
9    UnclosedUnescapedBinding { expr: String, context: String },
10    /// `{{}}` — the expression between `{{` and `}}` is empty.
11    EmptyBinding { context: String },
12    /// `{{key}}` — `key` is not present in the provided state.
13    MissingState { binding: String, context: String },
14    /// A directive (`<f-when>` / `<f-repeat>`) has no matching closing tag.
15    UnclosedDirective { tag: String, context: String },
16    /// A directive is missing a valid `value="{{…}}"` attribute.
17    MissingValueAttribute { tag: String, context: String },
18    /// `<f-repeat value="{{expr}}">` — expression is not in `item in list` format.
19    InvalidRepeatExpression { expr: String, context: String },
20    /// `<f-repeat>` binding resolves to a value that is not a JSON array.
21    NotAnArray { binding: String, context: String },
22    /// The state string passed to `render_template` is not valid JSON.
23    JsonParse { message: String },
24    /// Two or more template files resolve to the same element name.
25    DuplicateTemplate { element: String, paths: Vec<String> },
26    /// A template file could not be read from the filesystem.
27    TemplateReadError { path: String, message: String },
28}
29
30impl fmt::Display for RenderError {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        match self {
33            Self::UnclosedBinding { expr, context } => write!(
34                f,
35                "unclosed binding '{{{{{}': \
36                 no closing '}}}}' found to end the expression \
37                 — template: \"{context}\"",
38                expr
39            ),
40            Self::UnclosedUnescapedBinding { expr, context } => write!(
41                f,
42                "unclosed unescaped binding '{{{{{{{expr}': \
43                 no closing '}}}}}}' found to end the expression \
44                 — template: \"{context}\""
45            ),
46            Self::EmptyBinding { context } => write!(
47                f,
48                "empty binding '{{{{}}}}': \
49                 the expression between '{{{{' and '}}}}' cannot be empty \
50                 — template: \"{context}\""
51            ),
52            Self::MissingState { binding, context } => write!(
53                f,
54                "missing state: '{{{{{}}}}}' has no matching key in the provided state \
55                 — template: \"{context}\"",
56                binding
57            ),
58            Self::UnclosedDirective { tag, context } => write!(
59                f,
60                "unclosed directive '<{tag}>': \
61                 no matching '</{tag}>' closing tag was found \
62                 — template: \"{context}\""
63            ),
64            Self::MissingValueAttribute { tag, context } => write!(
65                f,
66                "directive '<{tag}>' is missing a valid 'value=\"{{{{…}}}}\"' attribute \
67                 — template: \"{context}\""
68            ),
69            Self::InvalidRepeatExpression { expr, context } => write!(
70                f,
71                "invalid repeat expression '{{{{{}}}}}': \
72                 expected 'item in list' format \
73                 — template: \"{context}\"",
74                expr
75            ),
76            Self::NotAnArray { binding, context } => write!(
77                f,
78                "type error: '{{{{{}}}}}' must resolve to a JSON array for use in <f-repeat> \
79                 — template: \"{context}\"",
80                binding
81            ),
82            Self::JsonParse { message } => write!(
83                f,
84                "failed to parse state JSON: {message}"
85            ),
86            Self::DuplicateTemplate { element, paths } => write!(
87                f,
88                "duplicate template: element '<{element}>' is defined in multiple files: {}",
89                paths.join(", ")
90            ),
91            Self::TemplateReadError { path, message } => write!(
92                f,
93                "template read error: could not read '{path}': {message}"
94            ),
95        }
96    }
97}
98
99impl std::error::Error for RenderError {}
100
101/// Extract a short snippet of `template` around `at` for use in error messages.
102/// Includes up to 20 characters before `at` for surrounding context.
103pub fn template_context(template: &str, at: usize) -> String {
104    const PRE: usize = 20;
105    const POST: usize = 60;
106
107    // Walk start back to a UTF-8 boundary.
108    let mut start = at.saturating_sub(PRE);
109    while start > 0 && !template.is_char_boundary(start) {
110        start -= 1;
111    }
112    // Walk end forward to a UTF-8 boundary.
113    let mut end = (at + POST).min(template.len());
114    while end < template.len() && !template.is_char_boundary(end) {
115        end += 1;
116    }
117
118    let prefix = if start > 0 { "…" } else { "" };
119    let suffix = if end < template.len() { "…" } else { "" };
120    format!("{prefix}{}{suffix}", &template[start..end])
121}
122
123/// Truncate `s` to at most `max` chars, appending `…` if truncated.
124pub fn truncate(s: &str, max: usize) -> String {
125    let mut chars = s.chars();
126    let mut out: String = chars.by_ref().take(max).collect();
127    if chars.next().is_some() {
128        out.push('…');
129    }
130    out
131}