helm_schema_syntax/cst.rs
1//! The `TemplatedDocument` CST: YAML layout nodes, template control regions,
2//! output holes, and comments, all carrying byte spans into the source.
3
4/// A half-open byte range `[start, end)` into the parsed source.
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub struct Span {
7 /// Inclusive byte offset where the range begins.
8 pub start: usize,
9 /// Exclusive byte offset where the range ends.
10 pub end: usize,
11}
12
13impl Span {
14 /// Creates a half-open byte range.
15 #[must_use]
16 pub fn new(start: usize, end: usize) -> Self {
17 Self { start, end }
18 }
19}
20
21/// A parsed templated-YAML source: the layout node forest with document
22/// boundaries.
23pub struct TemplatedDocument<'src> {
24 pub(crate) source: &'src str,
25 pub(crate) roots: Vec<Node>,
26 pub(crate) document_spans: Vec<Span>,
27}
28
29impl<'src> TemplatedDocument<'src> {
30 /// Parse a template source, running the Go-template parse internally.
31 /// Returns an empty document when tree-sitter fails entirely (layout
32 /// still parses; only action holes would be missing, so failure is
33 /// modeled as a document with no action tokens).
34 #[must_use]
35 pub fn parse(source: &'src str) -> Self {
36 match crate::actions::parse_go_template(source) {
37 Some(tree) => Self::parse_with_root(source, tree.root_node()),
38 None => crate::parse::parse_document(source, Vec::new()),
39 }
40 }
41
42 /// Parse a template source reusing an existing Go-template parse of the
43 /// same source (avoids a second tree-sitter pass).
44 #[must_use]
45 pub fn parse_with_root(source: &'src str, root: tree_sitter::Node<'_>) -> Self {
46 let tokens = crate::actions::collect_action_tokens(root);
47 crate::parse::parse_document(source, tokens)
48 }
49
50 /// Returns the source text parsed into this document.
51 #[must_use]
52 pub fn source(&self) -> &'src str {
53 self.source
54 }
55
56 /// Returns the top-level CST nodes in source order.
57 #[must_use]
58 pub fn roots(&self) -> &[Node] {
59 &self.roots
60 }
61
62 /// Top-level document spans, split at `---` separator lines (nonempty
63 /// spans only, mirroring the resource-identity document splitter).
64 #[must_use]
65 pub fn document_spans(&self) -> &[Span] {
66 &self.document_spans
67 }
68}
69
70/// A CST node. Containers own the nodes that structurally nest below them;
71/// control regions and action nodes are overlay nodes attached where they
72/// appear, without affecting container structure.
73#[derive(Debug)]
74pub enum Node {
75 /// A YAML mapping entry.
76 Mapping(MappingEntry),
77 /// A YAML sequence item.
78 Sequence(SequenceItem),
79 /// A structured Go-template control region.
80 Control(ControlRegion),
81 /// A standalone output action.
82 Output(OutputAction),
83 /// A YAML comment line.
84 Comment(CommentLine),
85 /// A scalar content line.
86 Scalar(ScalarLine),
87 /// Source retained without a more precise structural interpretation.
88 Opaque(OpaqueNode),
89}
90
91/// A scalar run split into literal text and template-action holes.
92#[derive(Debug)]
93pub struct ScalarParts {
94 /// Byte range covered by the complete scalar run.
95 pub span: Span,
96 /// Literal and templated pieces in source order.
97 pub parts: Vec<ScalarPart>,
98}
99
100/// One literal or templated piece of a scalar run.
101#[derive(Debug)]
102pub enum ScalarPart {
103 /// Literal source text.
104 Text(Span),
105 /// A Go-template output action embedded in the scalar.
106 Hole(Span),
107}
108
109/// One `key: …` mapping entry line and, when the entry opens a scope, the
110/// nodes nested below it.
111#[derive(Debug)]
112pub struct MappingEntry {
113 /// The entry's own line content (key start through line end).
114 pub span: Span,
115 /// Effective indent. Entries nested inline after a sequence dash use the
116 /// line model's `dash + 2` convention rather than the literal column.
117 pub indent: usize,
118 /// Parsed mapping key, including any embedded template holes.
119 pub key: ScalarParts,
120 /// Inline (non-block) value text, when present.
121 pub value: Option<ScalarParts>,
122 /// Block-scalar header and suppressed body, for `key: |`-style entries.
123 pub block: Option<BlockScalar>,
124 /// Whether the entry opened a container scope (empty, template, or
125 /// block-scalar value with a plain key). Closed or invalid-key entries
126 /// never adopt children.
127 pub opens_scope: bool,
128 /// Nodes structurally nested below the mapping entry.
129 pub children: Vec<Node>,
130}
131
132impl MappingEntry {
133 /// The sequence items nested below this entry in document order, looking
134 /// through control regions: an item closed while a branch was active is
135 /// owned by that branch, while an item still open when the region ended
136 /// escapes to the entry itself. Guard structure is dropped; use
137 /// `children` to keep it.
138 #[must_use]
139 pub fn sequence_items(&self) -> Vec<&SequenceItem> {
140 let mut items = Vec::new();
141 collect_sequence_items(&self.children, &mut items);
142 items.sort_by_key(|item| item.span.start);
143 items
144 }
145}
146
147fn collect_sequence_items<'nodes>(nodes: &'nodes [Node], items: &mut Vec<&'nodes SequenceItem>) {
148 for node in nodes {
149 match node {
150 Node::Sequence(item) => items.push(item),
151 Node::Control(region) => {
152 for branch in ®ion.branches {
153 collect_sequence_items(&branch.body, items);
154 }
155 }
156 _ => {}
157 }
158 }
159}
160
161/// One `- …` sequence item line and the nodes nested below it. An inline
162/// `- key: …` entry appears as the first child with effective indent
163/// `dash + 2`.
164#[derive(Debug)]
165pub struct SequenceItem {
166 /// The sequence item's own source line.
167 pub span: Span,
168 /// The dash column.
169 pub indent: usize,
170 /// Inline scalar item content (`- foo`), when present.
171 pub value: Option<ScalarParts>,
172 /// Block-scalar header and suppressed body, for `- |` items.
173 pub block: Option<BlockScalar>,
174 /// Nodes structurally nested below the sequence item.
175 pub children: Vec<Node>,
176}
177
178impl SequenceItem {
179 /// The item's content span: the first content after the dash through the
180 /// end of the deepest node nested below the item (a bare dash with no
181 /// content spans its own line).
182 #[must_use]
183 pub fn content_span(&self) -> Span {
184 let start = if let Some(value) = &self.value {
185 value.span.start
186 } else if let Some(block) = &self.block {
187 block.header.start
188 } else if let Some(first) = self.children.first() {
189 first.span_start()
190 } else {
191 self.span.start
192 };
193 let end = subtree_end(
194 self.span.end,
195 self.value.as_ref(),
196 self.block.as_ref(),
197 &self.children,
198 );
199 Span::new(start, end.max(start))
200 }
201}
202
203impl Node {
204 /// The byte where this node's own content starts.
205 #[must_use]
206 pub fn span_start(&self) -> usize {
207 match self {
208 Node::Mapping(entry) => entry.span.start,
209 Node::Sequence(item) => item.span.start,
210 Node::Control(region) => region.span.start,
211 Node::Output(action) => action.span.start,
212 Node::Comment(comment) => comment.span.start,
213 Node::Scalar(line) => line.span.start,
214 Node::Opaque(opaque) => opaque.span.start,
215 }
216 }
217
218 /// The end of the deepest content in this node's subtree: nested nodes,
219 /// block-scalar bodies, and inline-value holes that run past the line
220 /// end for multi-line actions.
221 #[must_use]
222 pub fn subtree_end(&self) -> usize {
223 match self {
224 Node::Mapping(entry) => subtree_end(
225 entry.span.end,
226 entry.value.as_ref(),
227 entry.block.as_ref(),
228 &entry.children,
229 ),
230 Node::Sequence(item) => subtree_end(
231 item.span.end,
232 item.value.as_ref(),
233 item.block.as_ref(),
234 &item.children,
235 ),
236 Node::Control(region) => region
237 .branches
238 .iter()
239 .flat_map(|branch| &branch.body)
240 .map(Node::subtree_end)
241 .fold(region.span.end, usize::max),
242 Node::Output(action) => action.span.end,
243 Node::Comment(comment) => comment.span.end,
244 Node::Scalar(line) => scalar_parts_end(&line.content).max(line.span.end),
245 Node::Opaque(opaque) => opaque.span.end,
246 }
247 }
248}
249
250fn subtree_end(
251 own_end: usize,
252 value: Option<&ScalarParts>,
253 block: Option<&BlockScalar>,
254 children: &[Node],
255) -> usize {
256 let mut end = own_end;
257 if let Some(value) = value {
258 end = end.max(scalar_parts_end(value));
259 }
260 if let Some(block) = block {
261 end = end.max(block.header.end).max(block.body.end);
262 }
263 for child in children {
264 end = end.max(child.subtree_end());
265 }
266 end
267}
268
269fn scalar_parts_end(parts: &ScalarParts) -> usize {
270 let mut end = parts.span.end;
271 for part in &parts.parts {
272 let (ScalarPart::Text(span) | ScalarPart::Hole(span)) = part;
273 end = end.max(span.end);
274 }
275 end
276}
277
278/// A literal block scalar (`|` / `>` families): its header token and the
279/// suppressed body span, with any template actions inside the body kept as
280/// suppressed holes.
281#[derive(Debug)]
282pub struct BlockScalar {
283 /// Block-scalar header token, including chomping and indentation markers.
284 pub header: Span,
285 /// Full body lines; empty (`start == end`) when the block has no body.
286 pub body: Span,
287 /// Template actions suppressed from normal YAML layout parsing.
288 pub holes: Vec<Span>,
289}
290
291/// Go-template action that owns a structured body.
292#[derive(Clone, Copy, Debug, PartialEq, Eq)]
293pub enum ControlKind {
294 /// Conditional `if` action.
295 If,
296 /// Context-selecting `with` action.
297 With,
298 /// Iterating `range` action.
299 Range,
300 /// Named-template `define` action.
301 Define,
302 /// Overridable named-template `block` action.
303 Block,
304}
305
306/// A template control region (`{{ if }}…{{ end }}` and friends) with its
307/// branch bodies. Container structure is decided by the visible YAML lines
308/// alone, so a region's branches hold exactly the nodes that opened and
309/// closed while the branch was active.
310#[derive(Debug)]
311pub struct ControlRegion {
312 /// Kind of action that opened the region.
313 pub kind: ControlKind,
314 /// Full source range from opener through closing action.
315 pub span: Span,
316 /// Conditional or fallback branches in source order.
317 pub branches: Vec<ControlBranch>,
318 /// `false` when the region provably violates the well-nested assumption:
319 /// a container opened inside a branch was still open when the branch
320 /// ended (its children escape the region), or a branch boundary sat
321 /// mid-line inside YAML content. Downstream must treat such regions
322 /// conservatively.
323 pub well_nested: bool,
324}
325
326/// One branch of a control region: its header action (`{{ if … }}`,
327/// `{{ else }}`, …) and the nodes emitted while the branch was active.
328#[derive(Debug)]
329pub struct ControlBranch {
330 /// Template action that opens this branch.
331 pub header: Span,
332 /// CST nodes emitted while this branch is active.
333 pub body: Vec<Node>,
334}
335
336/// A standalone-line output action (`{{ include "x" . }}` on its own line).
337/// Inline actions inside scalars are represented as [`ScalarPart::Hole`]s
338/// instead.
339#[derive(Debug)]
340pub struct OutputAction {
341 /// The full action span including delimiters.
342 pub span: Span,
343 /// The span of the expression inside the delimiters.
344 pub expr_span: Span,
345}
346
347/// A YAML `#` comment line (which may itself contain template actions).
348#[derive(Debug)]
349pub struct CommentLine {
350 /// Full source range of the comment line.
351 pub span: Span,
352 /// Comment content split around any template holes.
353 pub content: ScalarParts,
354}
355
356/// A plain scalar content line that is not a mapping entry or sequence item:
357/// flow-collection continuations, `---` markers, malformed keys, and other
358/// text the layout keeps only for its popping effect.
359#[derive(Debug)]
360pub struct ScalarLine {
361 /// Full source range of the scalar line.
362 pub span: Span,
363 /// YAML indentation column.
364 pub indent: usize,
365 /// Scalar content split around any template holes.
366 pub content: ScalarParts,
367}
368
369/// A span kept without further interpretation. Opaque nodes never guess:
370/// they preserve the raw span so downstream can attribute conservatively.
371#[derive(Debug)]
372pub struct OpaqueNode {
373 /// Source range retained for attribution.
374 pub span: Span,
375 /// Reason the source remains opaque.
376 pub kind: OpaqueKind,
377}
378
379/// Classification of source retained without deeper CST structure.
380#[derive(Clone, Copy, Debug, PartialEq, Eq)]
381pub enum OpaqueKind {
382 /// A `{{/* … */}}` template comment.
383 TemplateComment,
384 /// A `{{ $x := … }}` / `{{ $x = … }}` assignment action.
385 Assignment,
386 /// A `{{ break }}` atom.
387 Break,
388 /// A `{{ continue }}` atom.
389 Continue,
390 /// A control region that opened mid-line inside YAML content; the whole
391 /// region is preserved as one raw span.
392 InlineRegion,
393 /// Literal YAML text sharing a line with a standalone action.
394 ActionLineText,
395 /// Unparsable template content (tree-sitter `ERROR` output).
396 ParseError,
397}