kdl/
document.rs

1#[cfg(feature = "span")]
2use miette::SourceSpan;
3use std::fmt::Display;
4
5#[cfg(feature = "v1")]
6use crate::KdlNodeFormat;
7use crate::{FormatConfig, KdlError, KdlNode, KdlValue};
8
9/// Represents a KDL
10/// [`Document`](https://github.com/kdl-org/kdl/blob/main/SPEC.md#document).
11///
12/// This type is also used to manage a [`KdlNode`]'s [`Children
13/// Block`](https://github.com/kdl-org/kdl/blob/main/SPEC.md#children-block),
14/// when present.
15///
16/// # Examples
17///
18/// The easiest way to create a `KdlDocument` is to parse it:
19/// ```rust
20/// # use kdl::KdlDocument;
21/// let kdl: KdlDocument = "foo 1 2 3\nbar 4 5 6".parse().expect("parse failed");
22/// ```
23#[derive(Debug, Clone, Eq)]
24pub struct KdlDocument {
25    pub(crate) nodes: Vec<KdlNode>,
26    pub(crate) format: Option<KdlDocumentFormat>,
27    #[cfg(feature = "span")]
28    pub(crate) span: SourceSpan,
29}
30
31impl PartialEq for KdlDocument {
32    fn eq(&self, other: &Self) -> bool {
33        self.nodes == other.nodes && self.format == other.format
34        // Intentionally omitted: self.span == other.span
35    }
36}
37
38impl std::hash::Hash for KdlDocument {
39    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
40        self.nodes.hash(state);
41        self.format.hash(state);
42        // Intentionally omitted: self.span.hash(state)
43    }
44}
45
46impl Default for KdlDocument {
47    fn default() -> Self {
48        Self {
49            nodes: Default::default(),
50            format: Default::default(),
51            #[cfg(feature = "span")]
52            span: SourceSpan::from(0..0),
53        }
54    }
55}
56
57impl KdlDocument {
58    /// Creates a new Document.
59    pub fn new() -> Self {
60        Default::default()
61    }
62
63    /// Gets this document's span.
64    ///
65    /// This value will be properly initialized when created via [`KdlDocument::parse`]
66    /// but may become invalidated if the document is mutated. We do not currently
67    /// guarantee this to yield any particularly consistent results at that point.
68    #[cfg(feature = "span")]
69    pub fn span(&self) -> SourceSpan {
70        self.span
71    }
72
73    /// Sets this document's span.
74    #[cfg(feature = "span")]
75    pub fn set_span(&mut self, span: impl Into<SourceSpan>) {
76        self.span = span.into();
77    }
78
79    /// Gets the first child node with a matching name.
80    pub fn get(&self, name: &str) -> Option<&KdlNode> {
81        self.nodes.iter().find(move |n| n.name().value() == name)
82    }
83
84    /// Gets a reference to the first child node with a matching name.
85    pub fn get_mut(&mut self, name: &str) -> Option<&mut KdlNode> {
86        self.nodes
87            .iter_mut()
88            .find(move |n| n.name().value() == name)
89    }
90
91    /// Gets the first argument (value) of the first child node with a
92    /// matching name. This is a shorthand utility for cases where a document
93    /// is being used as a key/value store.
94    ///
95    /// # Examples
96    ///
97    /// Given a document like this:
98    /// ```kdl
99    /// foo 1
100    /// bar false
101    /// ```
102    ///
103    /// You can fetch the value of `foo` in a single call like this:
104    /// ```rust
105    /// # use kdl::{KdlDocument, KdlValue};
106    /// # let doc: KdlDocument = "foo 1\nbar #false".parse().unwrap();
107    /// assert_eq!(doc.get_arg("foo"), Some(&1.into()));
108    /// ```
109    pub fn get_arg(&self, name: &str) -> Option<&KdlValue> {
110        self.get(name).and_then(|node| node.get(0))
111    }
112
113    /// Returns an iterator of the all node arguments (value) of the first child node with a
114    /// matching name. This is a shorthand utility for cases where a document
115    /// is being used as a key/value store and the value is expected to be
116    /// array-ish.
117    ///
118    /// If a node has no arguments, this will return an empty array.
119    ///
120    /// # Examples
121    ///
122    /// Given a document like this:
123    /// ```kdl
124    /// foo 1 2 3
125    /// bar #false
126    /// ```
127    ///
128    /// You can fetch the arguments for `foo` in a single call like this:
129    /// ```rust
130    /// # use kdl::{KdlDocument, KdlValue};
131    /// # let doc: KdlDocument = "foo 1 2 3\nbar #false".parse().unwrap();
132    /// assert_eq!(
133    ///   doc.iter_args("foo").collect::<Vec<&KdlValue>>(),
134    ///   vec![&1.into(), &2.into(), &3.into()]
135    /// );
136    /// ```
137    pub fn iter_args(&self, name: &str) -> impl Iterator<Item = &KdlValue> {
138        self.get(name)
139            .map(|n| n.entries())
140            .unwrap_or_default()
141            .iter()
142            .filter(|e| e.name().is_none())
143            .map(|e| e.value())
144    }
145
146    /// Gets a mutable reference to the first argument (value) of the first
147    /// child node with a matching name. This is a shorthand utility for cases
148    /// where a document is being used as a key/value store.
149    pub fn get_arg_mut(&mut self, name: &str) -> Option<&mut KdlValue> {
150        self.get_mut(name).and_then(|node| node.get_mut(0))
151    }
152
153    /// This utility makes it easy to interact with a KDL convention where
154    /// child nodes named `-` are treated as array-ish values.
155    ///
156    /// # Examples
157    ///
158    /// Given a document like this:
159    /// ```kdl
160    /// foo {
161    ///   - 1
162    ///   - 2
163    ///   - #false
164    /// }
165    /// ```
166    ///
167    /// You can fetch the dashed child values of `foo` in a single call like this:
168    /// ```rust
169    /// # use kdl::{KdlDocument, KdlValue};
170    /// # let doc: KdlDocument = "foo {\n - 1\n - 2\n - #false\n}".parse().unwrap();
171    /// assert_eq!(
172    ///     doc.iter_dash_args("foo").collect::<Vec<&KdlValue>>(),
173    ///     vec![&1.into(), &2.into(), &false.into()]
174    /// );
175    /// ```
176    pub fn iter_dash_args(&self, name: &str) -> impl Iterator<Item = &KdlValue> {
177        self.get(name)
178            .and_then(|n| n.children())
179            .map(|doc| doc.nodes())
180            .unwrap_or_default()
181            .iter()
182            .filter(|e| e.name().value() == "-")
183            .filter_map(|e| e.get(0))
184    }
185
186    /// Returns a reference to this document's child nodes.
187    pub fn nodes(&self) -> &[KdlNode] {
188        &self.nodes
189    }
190
191    /// Returns a mutable reference to this document's child nodes.
192    pub fn nodes_mut(&mut self) -> &mut Vec<KdlNode> {
193        &mut self.nodes
194    }
195
196    /// Gets the formatting details (including whitespace and comments) for this entry.
197    pub fn format(&self) -> Option<&KdlDocumentFormat> {
198        self.format.as_ref()
199    }
200
201    /// Gets a mutable reference to this entry's formatting details.
202    pub fn format_mut(&mut self) -> Option<&mut KdlDocumentFormat> {
203        self.format.as_mut()
204    }
205
206    /// Sets the formatting details for this entry.
207    pub fn set_format(&mut self, format: KdlDocumentFormat) {
208        self.format = Some(format);
209    }
210
211    /// Length of this document when rendered as a string.
212    pub fn len(&self) -> usize {
213        format!("{self}").len()
214    }
215
216    /// Returns true if this document is completely empty (including whitespace)
217    pub fn is_empty(&self) -> bool {
218        self.len() == 0
219    }
220
221    /// Clears leading and trailing text (whitespace, comments). `KdlNode`s in
222    /// this document will be unaffected.
223    ///
224    /// If you need to clear the `KdlNode`s, use [`Self::clear_format_recursive`].
225    pub fn clear_format(&mut self) {
226        self.format = None;
227    }
228
229    /// Clears leading and trailing text (whitespace, comments), also clearing
230    /// all the `KdlNode`s in the document.
231    pub fn clear_format_recursive(&mut self) {
232        self.clear_format();
233        for node in self.nodes.iter_mut() {
234            node.clear_format_recursive();
235        }
236    }
237
238    /// Auto-formats this Document, making everything nice while preserving
239    /// comments.
240    pub fn autoformat(&mut self) {
241        self.autoformat_config(&FormatConfig::default());
242    }
243
244    /// Formats the document and removes all comments from the document.
245    pub fn autoformat_no_comments(&mut self) {
246        self.autoformat_config(&FormatConfig {
247            no_comments: true,
248            ..Default::default()
249        });
250    }
251
252    /// Formats the document according to `config`.
253    pub fn autoformat_config(&mut self, config: &FormatConfig<'_>) {
254        if let Some(KdlDocumentFormat { leading, .. }) = (*self).format_mut() {
255            crate::fmt::autoformat_leading(leading, config);
256        }
257        let mut has_nodes = false;
258        for node in &mut self.nodes {
259            has_nodes = true;
260            node.autoformat_config(config);
261        }
262        if let Some(KdlDocumentFormat { trailing, .. }) = (*self).format_mut() {
263            crate::fmt::autoformat_trailing(trailing, config.no_comments);
264            if !has_nodes {
265                trailing.push('\n');
266            }
267        };
268    }
269
270    // TODO(@zkat): These should all be moved into the query module itself,
271    // instead of being methods on the models
272    //
273    // /// Queries this Document's children according to the KQL query language,
274    // /// returning an iterator over all matching nodes.
275    // ///
276    // /// # NOTE
277    // ///
278    // /// Any query selectors that try to select the toplevel `scope()` will
279    // /// fail to match when using this method, since there's no [`KdlNode`] to
280    // /// return in this case.
281    // pub fn query_all(
282    //     &self,
283    //     query: impl IntoKdlQuery,
284    // ) -> Result<KdlQueryIterator<'_>, KdlDiagnostic> {
285    //     let parsed = query.into_query()?;
286    //     Ok(KdlQueryIterator::new(None, Some(self), parsed))
287    // }
288
289    // /// Queries this Document's children according to the KQL query language,
290    // /// returning the first match, if any.
291    // ///
292    // /// # NOTE
293    // ///
294    // /// Any query selectors that try to select the toplevel `scope()` will
295    // /// fail to match when using this method, since there's no [`KdlNode`] to
296    // /// return in this case.
297    // pub fn query(&self, query: impl IntoKdlQuery) -> Result<Option<&KdlNode>, KdlDiagnostic> {
298    //     let mut iter = self.query_all(query)?;
299    //     Ok(iter.next())
300    // }
301
302    // /// Queries this Document's children according to the KQL query language,
303    // /// picking the first match, and calling `.get(key)` on it, if the query
304    // /// succeeded.
305    // ///
306    // /// # NOTE
307    // ///
308    // /// Any query selectors that try to select the toplevel `scope()` will
309    // /// fail to match when using this method, since there's no [`KdlNode`] to
310    // /// return in this case.
311    // pub fn query_get(
312    //     &self,
313    //     query: impl IntoKdlQuery,
314    //     key: impl Into<NodeKey>,
315    // ) -> Result<Option<&KdlValue>, KdlDiagnostic> {
316    //     Ok(self.query(query)?.and_then(|node| node.get(key)))
317    // }
318
319    // /// Queries this Document's children according to the KQL query language,
320    // /// returning an iterator over all matching nodes, returning the requested
321    // /// field from each of those nodes and filtering out nodes that don't have
322    // /// it.
323    // ///
324    // /// # NOTE
325    // ///
326    // /// Any query selectors that try to select the toplevel `scope()` will
327    // /// fail to match when using this method, since there's no [`KdlNode`] to
328    // /// return in this case.
329    // pub fn query_get_all(
330    //     &self,
331    //     query: impl IntoKdlQuery,
332    //     key: impl Into<NodeKey>,
333    // ) -> Result<impl Iterator<Item = &KdlValue>, KdlDiagnostic> {
334    //     let key: NodeKey = key.into();
335    //     Ok(self
336    //         .query_all(query)?
337    //         .filter_map(move |node| node.get(key.clone())))
338    // }
339
340    /// Parses a string into a document.
341    ///
342    /// If the `v1-fallback` feature is enabled, this method will first try to
343    /// parse the string as a KDL v2 document, and, if that fails, it will try
344    /// to parse again as a KDL v1 document. If both fail, a heuristic will be
345    /// applied to try and detect the "intended" KDL version, and that version's
346    /// error(s) will be returned.
347    pub fn parse(s: &str) -> Result<Self, KdlError> {
348        #[cfg(not(feature = "v1-fallback"))]
349        {
350            Self::parse_v2(s)
351        }
352        #[cfg(feature = "v1-fallback")]
353        {
354            let v2_res = KdlDocument::parse_v2(s);
355            if v2_res.is_err() {
356                let v1_res = KdlDocument::parse_v1(s);
357                if v1_res.is_ok() || detect_v1(s) {
358                    v1_res
359                } else {
360                    // TODO(@zkat): maybe we can add something to the error
361                    // message to specify that it's "uncertain"?
362                    // YOLO.
363                    v2_res
364                }
365            } else {
366                v2_res
367            }
368        }
369    }
370
371    /// Parses a KDL v2 string into a document.
372    pub fn parse_v2(s: &str) -> Result<Self, KdlError> {
373        crate::v2_parser::try_parse(crate::v2_parser::document, s)
374    }
375
376    /// Parses a KDL v1 string into a document.
377    #[cfg(feature = "v1")]
378    pub fn parse_v1(s: &str) -> Result<Self, KdlError> {
379        let ret: Result<kdlv1::KdlDocument, kdlv1::KdlError> = s.parse();
380        ret.map(|x| x.into()).map_err(|e| e.into())
381    }
382
383    /// Takes a KDL v1 document string and returns the same document, but
384    /// autoformatted into valid KDL v2 syntax.
385    #[cfg(feature = "v1")]
386    pub fn v1_to_v2(s: &str) -> Result<String, KdlError> {
387        let mut doc = KdlDocument::parse_v1(s)?;
388        doc.ensure_v2();
389        Ok(doc.to_string())
390    }
391
392    /// Takes a KDL v2 document string and returns the same document, but
393    /// autoformatted into valid KDL v1 syntax.
394    #[cfg(feature = "v1")]
395    pub fn v2_to_v1(s: &str) -> Result<String, KdlError> {
396        let mut doc = KdlDocument::parse_v2(s)?;
397        doc.ensure_v1();
398        Ok(doc.to_string())
399    }
400
401    /// Makes sure this document is in v2 format.
402    pub fn ensure_v2(&mut self) {
403        // No need to touch KdlDocumentFormat, probably. In the longer term,
404        // we'll want to make sure to parse out whitespace and comments and make
405        // sure they're actually compliant, but this is good enough for now.
406        for node in self.nodes_mut().iter_mut() {
407            node.ensure_v2();
408        }
409    }
410
411    /// Makes sure this document is in v1 format.
412    #[cfg(feature = "v1")]
413    pub fn ensure_v1(&mut self) {
414        // No need to touch KdlDocumentFormat, probably. In the longer term,
415        // we'll want to make sure to parse out whitespace and comments and make
416        // sure they're actually compliant, but this is good enough for now.
417
418        // the last node in v1 docs/children has to have a semicolon.
419        let mut iter = self.nodes_mut().iter_mut().rev();
420        let last = iter.next();
421        let penult = iter.next();
422        if let Some(last) = last {
423            if let Some(fmt) = last.format_mut() {
424                if !fmt.trailing.contains(';')
425                    && fmt
426                        .trailing
427                        .chars()
428                        .any(|c| crate::v2_parser::NEWLINES.iter().any(|nl| nl.contains(c)))
429                {
430                    fmt.terminator = ";".into();
431                }
432            } else {
433                let maybe_indent = {
434                    if let Some(penult) = penult {
435                        if let Some(fmt) = penult.format() {
436                            fmt.leading.clone()
437                        } else {
438                            "".into()
439                        }
440                    } else {
441                        "".into()
442                    }
443                };
444                last.format = Some(KdlNodeFormat {
445                    leading: maybe_indent,
446                    terminator: "\n".into(),
447                    ..Default::default()
448                })
449            }
450        }
451        for node in self.nodes_mut().iter_mut() {
452            node.ensure_v1();
453        }
454    }
455}
456
457#[cfg(feature = "v1")]
458impl From<kdlv1::KdlDocument> for KdlDocument {
459    fn from(value: kdlv1::KdlDocument) -> Self {
460        Self {
461            nodes: value.nodes().iter().map(|x| x.clone().into()).collect(),
462            format: Some(KdlDocumentFormat {
463                leading: value.leading().unwrap_or("").into(),
464                trailing: value.trailing().unwrap_or("").into(),
465            }),
466            #[cfg(feature = "span")]
467            span: SourceSpan::new(value.span().offset().into(), value.span().len()),
468        }
469    }
470}
471
472/// Applies heuristics to get an idea of whether the string might be intended to
473/// be v2.
474#[allow(unused)]
475pub(crate) fn detect_v2(input: &str) -> bool {
476    for line in input.lines() {
477        if line.contains("kdl-version 2")
478            || line.contains("#true")
479            || line.contains("#false")
480            || line.contains("#null")
481            || line.contains("#inf")
482            || line.contains("#-inf")
483            || line.contains("#nan")
484            || line.contains(" #\"")
485            || line.contains("\"\"\"")
486            // Very very rough attempt at finding unquoted strings. We give up
487            // the first time we see a quoted one on a line.
488            || (!line.contains('"') && line
489                .split_whitespace()
490                .skip(1)
491                .any(|x| {
492                    x.chars()
493                        .next()
494                        .map(|d| !d.is_ascii_digit() && d != '-' && d != '+')
495                        .unwrap_or_default()
496                }))
497        {
498            return true;
499        }
500    }
501    false
502}
503
504/// Applies heuristics to get an idea of whether the string might be intended to
505/// be v2.
506#[allow(unused)]
507pub(crate) fn detect_v1(input: &str) -> bool {
508    input
509        .lines()
510        .next()
511        .map(|l| l.contains("kdl-version 1"))
512        .unwrap_or(false)
513        || input.contains(" true")
514        || input.contains(" false")
515        || input.contains(" null")
516        || input.contains("r#\"")
517        || input.contains(" \"\n")
518        || input.contains(" \"\r\n")
519}
520
521impl std::str::FromStr for KdlDocument {
522    type Err = KdlError;
523
524    fn from_str(s: &str) -> Result<Self, Self::Err> {
525        Self::parse(s)
526    }
527}
528
529impl Display for KdlDocument {
530    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
531        self.stringify(f, 0)
532    }
533}
534
535impl KdlDocument {
536    pub(crate) fn stringify(
537        &self,
538        f: &mut std::fmt::Formatter<'_>,
539        indent: usize,
540    ) -> std::fmt::Result {
541        if let Some(KdlDocumentFormat { leading, .. }) = self.format() {
542            write!(f, "{leading}")?;
543        }
544        for node in &self.nodes {
545            node.stringify(f, indent)?;
546        }
547        if let Some(KdlDocumentFormat { trailing, .. }) = self.format() {
548            write!(f, "{trailing}")?;
549        }
550        Ok(())
551    }
552}
553
554impl IntoIterator for KdlDocument {
555    type Item = KdlNode;
556    type IntoIter = std::vec::IntoIter<Self::Item>;
557
558    fn into_iter(self) -> Self::IntoIter {
559        self.nodes.into_iter()
560    }
561}
562
563/// Formatting details for [`KdlDocument`]s.
564#[derive(Debug, Clone, Default, Hash, Eq, PartialEq)]
565pub struct KdlDocumentFormat {
566    /// Whitespace and comments preceding the document's first node.
567    pub leading: String,
568    /// Whitespace and comments following the document's last node.
569    pub trailing: String,
570}
571
572#[cfg(test)]
573mod test {
574    #[cfg(feature = "span")]
575    use crate::KdlIdentifier;
576    use crate::{KdlEntry, KdlValue};
577
578    use super::*;
579
580    #[test]
581    fn canonical_clear_fmt() -> miette::Result<()> {
582        let left_src = r#"
583// There is a node here
584first_node /*with cool comments, too */ param=1.03e2 /-"commented" "argument" {
585    // With nested nodes too
586    nested 1 2 3
587    nested_2 "hi" "world" // this one is cool
588}
589second_node param=153 { nested one=1 two=2; }"#;
590        let right_src = r#"
591first_node param=103.0       "argument" {
592        // Different indentation, because
593        // Why not
594        nested 1 2 3
595        nested_2 "hi" /* actually, "hello" */ "world"
596}
597// There is a node here
598second_node /* This time, the comment is here */ param=153 {
599        nested one=1 two=2
600}"#;
601        let mut left_doc: KdlDocument = left_src.parse()?;
602        let mut right_doc: KdlDocument = right_src.parse()?;
603        assert_ne!(left_doc, right_doc);
604        left_doc.clear_format_recursive();
605        right_doc.clear_format_recursive();
606        assert_eq!(left_doc, right_doc);
607        Ok(())
608    }
609
610    #[test]
611    fn basic_parsing() -> miette::Result<()> {
612        let src = r#"
613            // Hello, world!
614            node 1
615            node two
616            node item="three";
617            node {
618                nested 1 2 3
619                nested_2 hi "world"
620            }
621            (type)node ("type")what?
622            +false #true
623            null_id null_prop=#null
624                    foo indented
625            // normal comment?
626            /- comment
627            /* block comment */
628            inline /*comment*/ here
629            another /-comment there
630
631
632            after some whitespace
633            trailing /* multiline */
634            trailing // single line
635            "#;
636        let _doc: KdlDocument = src.parse()?;
637        Ok(())
638    }
639
640    #[test]
641    fn parsing() -> miette::Result<()> {
642        let src = "
643// This is the first node
644foo 1 2 three #null #true bar=\"baz\" {
645    - 1
646    - 2
647    - three
648    (mytype)something (\"name\")else\r
649}
650
651null_id null_prop=#null
652true_id true_prop=#null
653+false #true
654
655         bar \"indented\" // trailing whitespace after this\t
656/*
657Some random comment
658 */
659
660a; b; c;
661/-commented \"node\"
662
663another /*foo*/ \"node\" /-1 /*bar*/ #null;
664final;";
665        let mut doc: KdlDocument = src.parse()?;
666
667        assert_eq!(doc.get_arg("foo"), Some(&1.into()));
668        assert_eq!(
669            doc.iter_dash_args("foo").collect::<Vec<&KdlValue>>(),
670            vec![&1.into(), &2.into(), &"three".into()]
671        );
672        assert_eq!(doc.format().map(|f| &f.leading[..]), Some(""));
673
674        let foo = doc.get("foo").expect("expected a foo node");
675        assert_eq!(
676            foo.format().map(|f| &f.leading[..]),
677            Some("\n// This is the first node\n")
678        );
679        assert_eq!(foo.format().map(|f| &f.terminator[..]), Some("\n"));
680        assert_eq!(&foo[2], &"three".into());
681        assert_eq!(&foo["bar"], &"baz".into());
682        assert_eq!(
683            foo.children().unwrap().get_arg("something"),
684            Some(&"else".into())
685        );
686        assert_eq!(doc.get_arg("another"), Some(&"node".into()));
687
688        let null = doc.get("null_id").expect("expected a null_id node");
689        assert_eq!(&null["null_prop"], &KdlValue::Null);
690
691        let tru = doc.get("true_id").expect("expected a true_id node");
692        assert_eq!(&tru["true_prop"], &KdlValue::Null);
693
694        let plusfalse = doc.get("+false").expect("expected a +false node");
695        assert_eq!(&plusfalse[0], &KdlValue::Bool(true));
696
697        let bar = doc.get("bar").expect("expected a bar node");
698        assert_eq!(
699            format!("{bar}"),
700            "\n         bar \"indented\" // trailing whitespace after this\t\n"
701        );
702
703        let a = doc.get("a").expect("expected a node");
704        assert_eq!(
705            format!("{a}"),
706            "/*\nSome random comment\n */\n\na;".to_string()
707        );
708
709        let b = doc.get("b").expect("expected a node");
710        assert_eq!(format!("{b}"), " b;".to_string());
711
712        // Round-tripping works.
713        assert_eq!(format!("{doc}"), src);
714
715        // Programmatic manipulation works.
716        let mut node: KdlNode = "new\n".parse()?;
717        // Manual entry parsing preserves formatting/reprs. Note that
718        // if you're making KdlEntries this way, you need to inject
719        // your own whitespace (or format the node)
720        node.push(" \"blah\"=0xDEADbeef".parse::<KdlEntry>()?);
721        doc.nodes_mut().push(node);
722
723        assert_eq!(
724            format!("{doc}"),
725            format!("{}new \"blah\"=0xDEADbeef\n", src)
726        );
727
728        Ok(())
729    }
730
731    #[test]
732    fn construction() {
733        let mut doc = KdlDocument::new();
734        doc.nodes_mut().push(KdlNode::new("foo"));
735
736        let mut bar = KdlNode::new("bar");
737        bar.insert("prop", "value");
738        bar.push(1);
739        bar.push(2);
740        bar.push(false);
741        bar.push(KdlValue::Null);
742
743        let subdoc = bar.ensure_children();
744        subdoc.nodes_mut().push(KdlNode::new("barchild"));
745        doc.nodes_mut().push(bar);
746        doc.nodes_mut().push(KdlNode::new("baz"));
747
748        doc.autoformat();
749
750        assert_eq!(
751            r#"foo
752bar prop=value 1 2 #false #null {
753    barchild
754}
755baz
756"#,
757            format!("{doc}")
758        );
759    }
760
761    #[ignore = "There's still issues around formatting comments and esclines."]
762    #[test]
763    fn autoformat() -> miette::Result<()> {
764        let mut doc: KdlDocument = r##"
765
766        /* x */ foo    1 "bar"=0xDEADbeef {
767    child1     1  ;
768
769 // child 2 comment
770
771        child2 2 /-3 // comment
772
773               child3    "
774
775   string\t
776   " \
777{
778       /*
779
780
781       multiline*/
782                                    inner1    \
783                    #"value"# \
784                    ;
785
786        inner2      \ //comment
787        {
788            inner3
789        }
790    }
791               }
792
793        // trailing comment here
794
795        "##
796        .parse()?;
797
798        KdlDocument::autoformat(&mut doc);
799
800        assert_eq!(
801            doc.to_string(),
802            r#"/* x */
803foo 1 bar=0xdeadbeef {
804    child1 1
805    // child 2 comment
806    child2 2 /-3 // comment
807    child3 "\nstring\t" {
808        /*
809
810
811       multiline*/
812        inner1 value
813        inner2 {
814            inner3
815        }
816    }
817}
818// trailing comment here"#
819        );
820        Ok(())
821    }
822
823    #[test]
824    fn simple_autoformat() -> miette::Result<()> {
825        let mut doc: KdlDocument = "a { b { c { }; }; }".parse().unwrap();
826        KdlDocument::autoformat(&mut doc);
827        assert_eq!(
828            doc.to_string(),
829            r#"a {
830    b {
831        c {
832
833        }
834    }
835}
836"#
837        );
838        Ok(())
839    }
840
841    #[test]
842    fn simple_autoformat_two_spaces() -> miette::Result<()> {
843        let mut doc: KdlDocument = "a { b { c { }; }; }".parse().unwrap();
844        KdlDocument::autoformat_config(
845            &mut doc,
846            &FormatConfig {
847                indent: "  ",
848                ..Default::default()
849            },
850        );
851        assert_eq!(
852            doc.to_string(),
853            r#"a {
854  b {
855    c {
856
857    }
858  }
859}
860"#
861        );
862        Ok(())
863    }
864
865    #[test]
866    fn simple_autoformat_single_tabs() -> miette::Result<()> {
867        let mut doc: KdlDocument = "a { b { c { }; }; }".parse().unwrap();
868        KdlDocument::autoformat_config(
869            &mut doc,
870            &FormatConfig {
871                indent: "\t",
872                ..Default::default()
873            },
874        );
875        assert_eq!(doc.to_string(), "a {\n\tb {\n\t\tc {\n\n\t\t}\n\t}\n}\n");
876        Ok(())
877    }
878
879    #[test]
880    fn simple_autoformat_no_comments() -> miette::Result<()> {
881        let mut doc: KdlDocument =
882            "// a comment\na {\n// another comment\n b { c { // another comment\n }; }; }"
883                .parse()
884                .unwrap();
885        KdlDocument::autoformat_no_comments(&mut doc);
886        assert_eq!(
887            doc.to_string(),
888            r#"a {
889    b {
890        c {
891
892        }
893    }
894}
895"#
896        );
897        Ok(())
898    }
899
900    #[cfg(feature = "span")]
901    fn check_spans_for_doc(doc: &KdlDocument, source: &impl miette::SourceCode) {
902        for node in doc.nodes() {
903            check_spans_for_node(node, source);
904        }
905    }
906
907    #[cfg(feature = "span")]
908    fn check_spans_for_node(node: &KdlNode, source: &impl miette::SourceCode) {
909        use crate::KdlEntryFormat;
910
911        check_span_for_ident(node.name(), source);
912        if let Some(ty) = node.ty() {
913            check_span_for_ident(ty, source);
914        }
915
916        for entry in node.entries() {
917            if let Some(name) = entry.name() {
918                check_span_for_ident(name, source);
919            }
920            if let Some(ty) = entry.ty() {
921                check_span_for_ident(ty, source);
922            }
923            if let Some(KdlEntryFormat { value_repr, .. }) = entry.format() {
924                if entry.name().is_none() && entry.ty().is_none() {
925                    check_span(value_repr, entry.span(), source);
926                }
927            }
928        }
929        if let Some(children) = node.children() {
930            check_spans_for_doc(children, source);
931        }
932    }
933
934    #[cfg(feature = "span")]
935    #[track_caller]
936    fn check_span_for_ident(ident: &KdlIdentifier, source: &impl miette::SourceCode) {
937        if let Some(repr) = ident.repr() {
938            check_span(repr, ident.span(), source);
939        } else {
940            check_span(ident.value(), ident.span(), source);
941        }
942    }
943
944    #[cfg(feature = "span")]
945    #[track_caller]
946    fn check_span(expected: &str, span: SourceSpan, source: &impl miette::SourceCode) {
947        let span = source.read_span(&span, 0, 0).unwrap();
948        let span = std::str::from_utf8(span.data()).unwrap();
949        assert_eq!(span, expected);
950    }
951
952    #[cfg(feature = "span")]
953    #[test]
954    fn span_test() -> miette::Result<()> {
955        let input = r####"
956this {
957    is (a)"cool" document="to" read=(int)5 10.1 (u32)0x45
958    and x="" {
959        "it" /*shh*/ "has"="💯" ##"the"##
960        Best🎊est
961        "syntax ever"
962    }
963    "yknow?" 0x10
964}
965// that's
966nice
967inline { time; to; live "our" "dreams"; "y;all" }
968
969"####;
970
971        let doc: KdlDocument = input.parse()?;
972
973        // First check that all the identity-spans are correct
974        check_spans_for_doc(&doc, &input);
975
976        // Now check some more interesting concrete spans
977
978        // The whole document should be everything from the first node until the
979        // last before_terminator whitespace.
980        check_span(&input[1..(input.len() - 2)], doc.span(), &input);
981
982        // This one-liner node should be the whole line without leading whitespace
983        let is_node = doc
984            .get("this")
985            .unwrap()
986            .children()
987            .unwrap()
988            .get("is")
989            .unwrap();
990        check_span(
991            r##"is (a)"cool" document="to" read=(int)5 10.1 (u32)0x45"##,
992            is_node.span(),
993            &input,
994        );
995
996        // Some simple with/without type hints
997        check_span(r#"(a)"cool""#, is_node.entry(0).unwrap().span(), &input);
998        check_span(
999            r#"read=(int)5"#,
1000            is_node.entry("read").unwrap().span(),
1001            &input,
1002        );
1003        check_span(r#"10.1"#, is_node.entry(1).unwrap().span(), &input);
1004        check_span(r#"(u32)0x45"#, is_node.entry(2).unwrap().span(), &input);
1005
1006        // Now let's look at some messed up parts of that "and" node
1007        let and_node = doc
1008            .get("this")
1009            .unwrap()
1010            .children()
1011            .unwrap()
1012            .get("and")
1013            .unwrap();
1014
1015        // The node is what you expect, the whole line and its two braces
1016        check_span(
1017            r####"and x="" {
1018        "it" /*shh*/ "has"="💯" ##"the"##
1019        Best🎊est
1020        "syntax ever"
1021    }"####,
1022            and_node.span(),
1023            &input,
1024        );
1025
1026        // The child document is a little weird, it's the contents *inside* the braces
1027        // without the surrounding whitespace/comments. Just the actual contents.
1028        check_span(
1029            r####""it" /*shh*/ "has"="💯" ##"the"##
1030        Best🎊est
1031        "syntax ever""####,
1032            and_node.children().unwrap().span(),
1033            &input,
1034        );
1035
1036        // Oh hey don't forget to check that "x" entry
1037        check_span(r#"x="""#, and_node.entry("x").unwrap().span(), &input);
1038
1039        // Now the "it" node, more straightforward
1040        let it_node = and_node.children().unwrap().get("it").unwrap();
1041        check_span(
1042            r####""it" /*shh*/ "has"="💯" ##"the"##"####,
1043            it_node.span(),
1044            &input,
1045        );
1046        check_span(
1047            r#""has"="💯""#,
1048            it_node.entry("has").unwrap().span(),
1049            &input,
1050        );
1051        check_span(
1052            r####"##"the"##"####,
1053            it_node.entry(0).unwrap().span(),
1054            &input,
1055        );
1056
1057        // Make sure inline nodes work ok
1058        let inline_node = doc.get("inline").unwrap();
1059        check_span(
1060            r#"inline { time; to; live "our" "dreams"; "y;all" }"#,
1061            inline_node.span(),
1062            &input,
1063        );
1064
1065        let inline_children = inline_node.children().unwrap();
1066        check_span(
1067            r#"time; to; live "our" "dreams"; "y;all" "#,
1068            inline_children.span(),
1069            &input,
1070        );
1071
1072        let inline_nodes = inline_children.nodes();
1073        check_span("time", inline_nodes[0].span(), &input);
1074        check_span("to", inline_nodes[1].span(), &input);
1075        check_span(r#"live "our" "dreams""#, inline_nodes[2].span(), &input);
1076        check_span(r#""y;all" "#, inline_nodes[3].span(), &input);
1077
1078        Ok(())
1079    }
1080
1081    #[test]
1082    fn parse_examples() -> miette::Result<()> {
1083        include_str!("../examples/kdl-schema.kdl").parse::<KdlDocument>()?;
1084        include_str!("../examples/Cargo.kdl").parse::<KdlDocument>()?;
1085        include_str!("../examples/ci.kdl").parse::<KdlDocument>()?;
1086        include_str!("../examples/nuget.kdl").parse::<KdlDocument>()?;
1087        include_str!("../examples/website.kdl").parse::<KdlDocument>()?;
1088        include_str!("../examples/zellij.kdl").parse::<KdlDocument>()?;
1089        include_str!("../examples/zellij-unquoted-bindings.kdl").parse::<KdlDocument>()?;
1090        Ok(())
1091    }
1092
1093    #[cfg(feature = "v1")]
1094    #[test]
1095    fn v1_v2_conversions() -> miette::Result<()> {
1096        let v1 = r##"
1097// If you'd like to override the default keybindings completely, be sure to change "keybinds" to "keybinds clear-defaults=true"
1098keybinds {
1099    normal {
1100        // uncomment this and adjust key if using copy_on_select=false
1101        // bind "Alt c" { Copy; }
1102    }
1103    locked {
1104        bind "Ctrl g" { SwitchToMode "Normal"; }
1105    }
1106    resize {
1107        bind "Ctrl n" { SwitchToMode "Normal"; }
1108        bind "h" "Left" { Resize "Increase Left"; }
1109        bind "j" "Down" { Resize "Increase Down"; }
1110        bind "k" "Up" { Resize "Increase Up"; }
1111        bind "l" "Right" { Resize "Increase Right"; }
1112        bind "H" { Resize "Decrease Left"; }
1113        bind "J" { Resize "Decrease Down"; }
1114        bind "K" { Resize "Decrease Up"; }
1115        bind "L" { Resize "Decrease Right"; }
1116        bind "=" "+" { Resize "Increase"; }
1117        bind "-" { Resize "Decrease"; }
1118    }
1119}
1120// Plugin aliases - can be used to change the implementation of Zellij
1121// changing these requires a restart to take effect
1122plugins {
1123    tab-bar location="zellij:tab-bar"
1124    status-bar location="zellij:status-bar"
1125    welcome-screen location="zellij:session-manager" {
1126        welcome_screen true
1127    }
1128    filepicker location="zellij:strider" {
1129        cwd "\/"
1130    }
1131}
1132mouse_mode false
1133mirror_session true
1134"##;
1135        let v2 = r##"
1136// If you'd like to override the default keybindings completely, be sure to change "keybinds" to "keybinds clear-defaults=true"
1137keybinds {
1138    normal {
1139        // uncomment this and adjust key if using copy_on_select=false
1140        // bind "Alt c" { Copy; }
1141    }
1142    locked {
1143        bind "Ctrl g" { SwitchToMode Normal; }
1144    }
1145    resize {
1146        bind "Ctrl n" { SwitchToMode Normal; }
1147        bind h Left { Resize "Increase Left"; }
1148        bind j Down { Resize "Increase Down"; }
1149        bind k Up { Resize "Increase Up"; }
1150        bind l Right { Resize "Increase Right"; }
1151        bind H { Resize "Decrease Left"; }
1152        bind J { Resize "Decrease Down"; }
1153        bind K { Resize "Decrease Up"; }
1154        bind L { Resize "Decrease Right"; }
1155        bind "=" + { Resize Increase; }
1156        bind - { Resize Decrease; }
1157    }
1158}
1159// Plugin aliases - can be used to change the implementation of Zellij
1160// changing these requires a restart to take effect
1161plugins {
1162    tab-bar location=zellij:tab-bar
1163    status-bar location=zellij:status-bar
1164    welcome-screen location=zellij:session-manager {
1165        welcome_screen #true
1166    }
1167    filepicker location=zellij:strider {
1168        cwd "/"
1169    }
1170}
1171mouse_mode #false
1172mirror_session #true
1173"##;
1174        pretty_assertions::assert_eq!(KdlDocument::v1_to_v2(v1)?, v2, "Converting a v1 doc to v2");
1175        pretty_assertions::assert_eq!(KdlDocument::v2_to_v1(v2)?, v1, "Converting a v2 doc to v1");
1176        assert!(super::detect_v1(v1));
1177        assert!(super::detect_v2(v2));
1178        Ok(())
1179    }
1180}