helm_schema_syntax/
yaml_scan.rs1#[must_use]
7pub fn parse_yaml_key(after: &str) -> Option<String> {
8 fn finalize_yaml_key(key: String) -> Option<String> {
9 if key.is_empty() {
10 return None;
11 }
12 Some(key)
13 }
14
15 let after = after.trim_end();
16 if after.starts_with("{{") {
17 return None;
18 }
19
20 if let Some(quote) = after.chars().next().filter(|c| matches!(c, '"' | '\'')) {
21 let quoted = &after[quote.len_utf8()..];
22 let mut chars = quoted.char_indices().peekable();
23 let mut key = String::new();
24 while let Some((idx, ch)) = chars.next() {
25 match (quote, ch) {
26 ('"', '\\') => {
27 let (_, escaped) = chars.next()?;
28 key.push(escaped);
29 }
30 ('\'', '\'') if chars.peek().is_some_and(|(_, next)| *next == '\'') => {
31 let _ = chars.next();
32 key.push('\'');
33 }
34 (_, ch) if ch == quote => {
35 return quoted[(idx + ch.len_utf8())..]
36 .trim_start()
37 .starts_with(':')
38 .then(|| finalize_yaml_key(key))
39 .flatten();
40 }
41 _ => key.push(ch),
42 }
43 }
44 return None;
45 }
46
47 let chars = after.chars();
48 let mut key = String::new();
49 for ch in chars {
50 if ch == ':' {
51 return finalize_yaml_key(key.trim().to_string());
52 }
53 if ch.is_whitespace() {
54 return None;
55 }
56 if ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.' | '/') {
57 key.push(ch);
58 continue;
59 }
60 return None;
61 }
62 None
63}
64
65#[must_use]
69pub fn unquote_yaml_scalar(value: &str) -> &str {
70 value
71 .strip_prefix('"')
72 .and_then(|value| value.strip_suffix('"'))
73 .or_else(|| {
74 value
75 .strip_prefix('\'')
76 .and_then(|value| value.strip_suffix('\''))
77 })
78 .unwrap_or(value)
79}
80
81pub fn mapping_colon_is_structural(text: &str, colon: usize) -> bool {
85 text[colon + 1..]
86 .chars()
87 .next()
88 .is_none_or(char::is_whitespace)
89}
90
91#[must_use]
94pub fn structural_mapping_colon(text: &str) -> Option<usize> {
95 first_mapping_colon_offset(text).filter(|&colon| mapping_colon_is_structural(text, colon))
96}
97
98#[expect(
100 clippy::indexing_slicing,
101 reason = "this hot byte scanner maintains explicit bounds before every direct access"
102)]
103pub fn first_mapping_colon_offset(line: &str) -> Option<usize> {
104 let bytes = line.as_bytes();
105 let mut idx = 0usize;
106 while idx < bytes.len() {
107 match bytes[idx] {
108 b'{' if bytes.get(idx + 1) == Some(&b'{') => {
109 idx += 2;
110 while idx + 1 < bytes.len() {
111 if bytes[idx] == b'}' && bytes[idx + 1] == b'}' {
112 idx += 2;
113 break;
114 }
115 idx += 1;
116 }
117 }
118 b'"' => {
119 idx += 1;
120 while idx < bytes.len() {
121 match bytes[idx] {
122 b'\\' => idx += 2,
123 b'"' => {
124 idx += 1;
125 break;
126 }
127 _ => idx += 1,
128 }
129 }
130 }
131 b'\'' => {
132 idx += 1;
133 while idx < bytes.len() {
134 if bytes[idx] == b'\'' {
135 if bytes.get(idx + 1) == Some(&b'\'') {
136 idx += 2;
137 continue;
138 }
139 idx += 1;
140 break;
141 }
142 idx += 1;
143 }
144 }
145 b':' => return Some(idx),
146 _ => idx += 1,
147 }
148 }
149 None
150}
151
152#[cfg(test)]
153#[path = "tests/yaml_scan.rs"]
154mod tests;