1use indexmap::IndexMap;
2use kdl::{KdlEntry, KdlEntryFormat, KdlNode, KdlValue};
3use miette::SourceSpan;
4use std::fmt::Debug;
5use std::ops::RangeBounds;
6
7use crate::error::UsageErr;
8use crate::spec::context::ParsingContext;
9
10fn raw_multiline_hash_count(value: &str) -> usize {
17 let mut max_count = 0;
18 let bytes = value.as_bytes();
19 for i in 0..bytes.len().saturating_sub(2) {
20 if bytes[i] == b'"' && bytes[i + 1] == b'"' && bytes[i + 2] == b'"' {
21 let count = value[i + 3..].chars().take_while(|&c| c == '#').count();
22 max_count = max_count.max(count);
23 }
24 }
25 max_count + 1
26}
27
28fn escape_string(value: &str) -> String {
30 let mut out = String::with_capacity(value.len() + 2);
31 out.push('"');
32 for ch in value.chars() {
33 match ch {
34 '"' => out.push_str("\\\""),
35 '\\' => out.push_str("\\\\"),
36 '\n' => out.push_str("\\n"),
37 '\r' => out.push_str("\\r"),
38 '\t' => out.push_str("\\t"),
39 c if c.is_control() => out.push_str(&format!("\\u{{{:x}}}", c as u32)),
40 c => out.push(c),
41 }
42 }
43 out.push('"');
44 out
45}
46
47fn quoted_format(value_repr: &str) -> KdlEntryFormat {
49 KdlEntryFormat {
50 value_repr: value_repr.to_string(),
51 leading: " ".into(),
52 trailing: "".into(),
53 after_ty: "".into(),
54 before_ty_name: "".into(),
55 after_ty_name: "".into(),
56 after_key: "".into(),
57 after_eq: "".into(),
58 autoformat_keep: true,
59 }
60}
61
62pub(crate) fn string_entry(key: Option<&str>, value: &str) -> KdlEntry {
66 let mut entry = match key {
67 Some(k) => KdlEntry::new_prop(k, KdlValue::String(value.to_string())),
68 None => KdlEntry::new(KdlValue::String(value.to_string())),
69 };
70 let dashed_argument = key.is_none() && value.starts_with('-');
79 let has_control = value
83 .chars()
84 .any(|c| c.is_control() && c != '\n' && c != '\t');
85 if dashed_argument || has_control {
86 entry.set_format(quoted_format(&escape_string(value)));
87 return entry;
88 }
89 if value.contains('\n') {
90 let n = raw_multiline_hash_count(value);
91 let hashes = "#".repeat(n);
92 let repr = format!("{hashes}\"\"\"\n{value}\n\"\"\"{hashes}");
93 entry.set_format(KdlEntryFormat {
94 value_repr: repr,
95 leading: " ".into(),
96 trailing: "".into(),
97 after_ty: "".into(),
98 before_ty_name: "".into(),
99 after_ty_name: "".into(),
100 after_key: "".into(),
101 after_eq: "".into(),
102 autoformat_keep: true,
103 });
104 }
105 entry
106}
107
108#[derive(Debug)]
109pub struct NodeHelper<'a> {
110 pub(crate) node: &'a KdlNode,
111 pub(crate) ctx: &'a ParsingContext,
112}
113
114impl<'a> NodeHelper<'a> {
115 pub(crate) fn new(ctx: &'a ParsingContext, node: &'a KdlNode) -> Self {
116 Self { node, ctx }
117 }
118
119 pub(crate) fn name(&self) -> &str {
120 self.node.name().value()
121 }
122 pub(crate) fn span(&self) -> SourceSpan {
123 (self.node.span().offset(), self.node.span().len()).into()
124 }
125 pub(crate) fn ensure_arg_len<R>(&self, range: R) -> Result<&Self, UsageErr>
126 where
127 R: RangeBounds<usize> + Debug,
128 {
129 let count = self.args().count();
130 if !range.contains(&count) {
131 let ctx = self.ctx;
132 let span = self.span();
133 bail_parse!(ctx, span, "expected {range:?} arguments, got {count}",)
134 }
135 Ok(self)
136 }
137 pub(crate) fn get(&self, key: &str) -> Option<ParseEntry<'_>> {
138 self.node.entry(key).map(|e| ParseEntry::new(self.ctx, e))
139 }
140 pub(crate) fn arg(&self, i: usize) -> Result<ParseEntry<'_>, UsageErr> {
141 if let Some(entry) = self.args().nth(i) {
142 return Ok(entry);
143 }
144 bail_parse!(self.ctx, self.span(), "missing argument")
145 }
146 pub(crate) fn args(&self) -> impl Iterator<Item = ParseEntry<'_>> + '_ {
147 self.node
148 .entries()
149 .iter()
150 .filter(|e| e.name().is_none())
151 .map(|e| ParseEntry::new(self.ctx, e))
152 }
153 pub(crate) fn props(&self) -> IndexMap<&str, ParseEntry<'_>> {
154 self.node
155 .entries()
156 .iter()
157 .filter_map(|e| {
158 e.name()
159 .map(|key| (key.value(), ParseEntry::new(self.ctx, e)))
160 })
161 .collect()
162 }
163 pub(crate) fn children(&self) -> Vec<Self> {
164 self.node
165 .children()
166 .map(|c| {
167 c.nodes()
168 .iter()
169 .map(|n| NodeHelper::new(self.ctx, n))
170 .collect()
171 })
172 .unwrap_or_default()
173 }
174}
175
176#[derive(Debug)]
177pub(crate) struct ParseEntry<'a> {
178 pub(crate) ctx: &'a ParsingContext,
179 pub(crate) entry: &'a KdlEntry,
180 pub(crate) value: &'a KdlValue,
181}
182
183impl<'a> ParseEntry<'a> {
184 fn new(ctx: &'a ParsingContext, entry: &'a KdlEntry) -> Self {
185 Self {
186 ctx,
187 entry,
188 value: entry.value(),
189 }
190 }
191
192 fn span(&self) -> SourceSpan {
193 (self.entry.span().offset(), self.entry.span().len()).into()
194 }
195}
196
197impl ParseEntry<'_> {
198 pub fn ensure_usize(&self) -> Result<usize, UsageErr> {
199 match self.value.as_integer() {
200 Some(i) => Ok(i as usize),
201 None => bail_parse!(self.ctx, self.span(), "expected usize"),
202 }
203 }
204 #[allow(dead_code)]
205 pub fn ensure_f64(&self) -> Result<f64, UsageErr> {
206 match self.value.as_float() {
207 Some(f) => Ok(f),
208 None => bail_parse!(self.ctx, self.span(), "expected float"),
209 }
210 }
211 pub fn ensure_bool(&self) -> Result<bool, UsageErr> {
212 match self.value.as_bool() {
213 Some(b) => Ok(b),
214 None => bail_parse!(self.ctx, self.span(), "expected bool"),
215 }
216 }
217 pub fn ensure_string(&self) -> Result<String, UsageErr> {
218 match self.value.as_string() {
219 Some(s) => Ok(s.to_string()),
220 None => bail_parse!(self.ctx, self.span(), "expected string"),
221 }
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228 use kdl::KdlDocument;
229 use std::path::Path;
230
231 fn parse_node(input: &str) -> (ParsingContext, KdlDocument) {
232 let ctx = ParsingContext::new(Path::new("test.kdl"), input);
233 let doc: KdlDocument = input.parse().unwrap();
234 (ctx, doc)
235 }
236
237 #[test]
238 fn test_node_helper_name() {
239 let (ctx, doc) = parse_node("test_node \"arg1\"");
240 let node = doc.nodes().first().unwrap();
241 let helper = NodeHelper::new(&ctx, node);
242 assert_eq!(helper.name(), "test_node");
243 }
244
245 #[test]
246 fn test_node_helper_arg() {
247 let (ctx, doc) = parse_node("node \"first\" \"second\"");
248 let node = doc.nodes().first().unwrap();
249 let helper = NodeHelper::new(&ctx, node);
250
251 assert_eq!(helper.arg(0).unwrap().ensure_string().unwrap(), "first");
252 assert_eq!(helper.arg(1).unwrap().ensure_string().unwrap(), "second");
253 }
254
255 #[test]
256 fn test_node_helper_args_count() {
257 let (ctx, doc) = parse_node("node \"a\" \"b\" \"c\"");
258 let node = doc.nodes().first().unwrap();
259 let helper = NodeHelper::new(&ctx, node);
260
261 assert_eq!(helper.args().count(), 3);
262 }
263
264 #[test]
265 fn test_node_helper_props() {
266 let (ctx, doc) = parse_node("node key1=\"value1\" key2=\"value2\"");
267 let node = doc.nodes().first().unwrap();
268 let helper = NodeHelper::new(&ctx, node);
269
270 let props = helper.props();
271 assert_eq!(props.len(), 2);
272 assert_eq!(props["key1"].ensure_string().unwrap(), "value1");
273 assert_eq!(props["key2"].ensure_string().unwrap(), "value2");
274 }
275
276 #[test]
277 fn test_node_helper_get() {
278 let (ctx, doc) = parse_node("node name=\"test\"");
279 let node = doc.nodes().first().unwrap();
280 let helper = NodeHelper::new(&ctx, node);
281
282 assert!(helper.get("name").is_some());
283 assert!(helper.get("nonexistent").is_none());
284 }
285
286 #[test]
287 fn test_node_helper_children() {
288 let (ctx, doc) = parse_node("parent { child1; child2 }");
289 let node = doc.nodes().first().unwrap();
290 let helper = NodeHelper::new(&ctx, node);
291
292 let children = helper.children();
293 assert_eq!(children.len(), 2);
294 assert_eq!(children[0].name(), "child1");
295 assert_eq!(children[1].name(), "child2");
296 }
297
298 #[test]
299 fn test_node_helper_ensure_arg_len_valid() {
300 let (ctx, doc) = parse_node("node \"a\" \"b\"");
301 let node = doc.nodes().first().unwrap();
302 let helper = NodeHelper::new(&ctx, node);
303
304 assert!(helper.ensure_arg_len(2..=2).is_ok());
305 assert!(helper.ensure_arg_len(1..=3).is_ok());
306 assert!(helper.ensure_arg_len(0..).is_ok());
307 }
308
309 #[test]
310 fn test_node_helper_ensure_arg_len_invalid() {
311 let (ctx, doc) = parse_node("node \"a\"");
312 let node = doc.nodes().first().unwrap();
313 let helper = NodeHelper::new(&ctx, node);
314
315 assert!(helper.ensure_arg_len(2..=2).is_err());
316 }
317
318 #[test]
319 fn test_parse_entry_ensure_usize() {
320 let (ctx, doc) = parse_node("node 42");
321 let node = doc.nodes().first().unwrap();
322 let helper = NodeHelper::new(&ctx, node);
323
324 assert_eq!(helper.arg(0).unwrap().ensure_usize().unwrap(), 42);
325 }
326
327 #[test]
328 fn test_parse_entry_ensure_bool() {
329 let (ctx, doc) = parse_node("node #true");
330 let node = doc.nodes().first().unwrap();
331 let helper = NodeHelper::new(&ctx, node);
332
333 assert!(helper.arg(0).unwrap().ensure_bool().unwrap());
334 }
335
336 #[test]
337 fn test_parse_entry_ensure_string() {
338 let (ctx, doc) = parse_node("node \"hello\"");
339 let node = doc.nodes().first().unwrap();
340 let helper = NodeHelper::new(&ctx, node);
341
342 assert_eq!(helper.arg(0).unwrap().ensure_string().unwrap(), "hello");
343 }
344
345 #[test]
346 fn test_parse_entry_type_mismatch() {
347 let (ctx, doc) = parse_node("node \"not_a_number\"");
348 let node = doc.nodes().first().unwrap();
349 let helper = NodeHelper::new(&ctx, node);
350
351 assert!(helper.arg(0).unwrap().ensure_usize().is_err());
352 }
353
354 #[test]
355 fn overlapping_quotes_raise_the_raw_multiline_hash_count() {
356 assert_eq!(raw_multiline_hash_count("\"\"\"#"), 2);
357 assert_eq!(raw_multiline_hash_count("\"\"\"\"#"), 2);
358 assert_eq!(raw_multiline_hash_count("\"\"\"\"##"), 3);
359 assert_eq!(raw_multiline_hash_count("plain"), 1);
360 }
361
362 #[test]
363 fn overlapping_quotes_round_trip_through_parse() {
364 for value in [
365 "before\n\"\"\"\"#\nafter",
366 "before\n\"\"\"\"##\nafter",
367 "line\n\"\"\"#\nstill",
368 ] {
369 let mut node = KdlNode::new("about");
370 node.push(string_entry(None, value));
371 let kdl = format!("name ex\nbin ex\n{node}\n");
372 let spec: crate::Spec = kdl
373 .parse()
374 .unwrap_or_else(|e| panic!("emitted spec must parse: {e}\n{kdl}"));
375 assert_eq!(spec.about.as_deref(), Some(value), "{kdl}");
376 }
377 }
378}