mirror of https://github.com/kdl-org/kdl-rs.git
1180 lines
36 KiB
Rust
1180 lines
36 KiB
Rust
#[cfg(feature = "span")]
|
|
use miette::SourceSpan;
|
|
use std::fmt::Display;
|
|
|
|
#[cfg(feature = "v1")]
|
|
use crate::KdlNodeFormat;
|
|
use crate::{FormatConfig, KdlError, KdlNode, KdlValue};
|
|
|
|
/// Represents a KDL
|
|
/// [`Document`](https://github.com/kdl-org/kdl/blob/main/SPEC.md#document).
|
|
///
|
|
/// This type is also used to manage a [`KdlNode`]'s [`Children
|
|
/// Block`](https://github.com/kdl-org/kdl/blob/main/SPEC.md#children-block),
|
|
/// when present.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// The easiest way to create a `KdlDocument` is to parse it:
|
|
/// ```rust
|
|
/// # use kdl::KdlDocument;
|
|
/// let kdl: KdlDocument = "foo 1 2 3\nbar 4 5 6".parse().expect("parse failed");
|
|
/// ```
|
|
#[derive(Debug, Clone, Eq)]
|
|
pub struct KdlDocument {
|
|
pub(crate) nodes: Vec<KdlNode>,
|
|
pub(crate) format: Option<KdlDocumentFormat>,
|
|
#[cfg(feature = "span")]
|
|
pub(crate) span: SourceSpan,
|
|
}
|
|
|
|
impl PartialEq for KdlDocument {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.nodes == other.nodes && self.format == other.format
|
|
// Intentionally omitted: self.span == other.span
|
|
}
|
|
}
|
|
|
|
impl std::hash::Hash for KdlDocument {
|
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
|
self.nodes.hash(state);
|
|
self.format.hash(state);
|
|
// Intentionally omitted: self.span.hash(state)
|
|
}
|
|
}
|
|
|
|
impl Default for KdlDocument {
|
|
fn default() -> Self {
|
|
Self {
|
|
nodes: Default::default(),
|
|
format: Default::default(),
|
|
#[cfg(feature = "span")]
|
|
span: SourceSpan::from(0..0),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl KdlDocument {
|
|
/// Creates a new Document.
|
|
pub fn new() -> Self {
|
|
Default::default()
|
|
}
|
|
|
|
/// Gets this document's span.
|
|
///
|
|
/// This value will be properly initialized when created via [`KdlDocument::parse`]
|
|
/// but may become invalidated if the document is mutated. We do not currently
|
|
/// guarantee this to yield any particularly consistent results at that point.
|
|
#[cfg(feature = "span")]
|
|
pub fn span(&self) -> SourceSpan {
|
|
self.span
|
|
}
|
|
|
|
/// Sets this document's span.
|
|
#[cfg(feature = "span")]
|
|
pub fn set_span(&mut self, span: impl Into<SourceSpan>) {
|
|
self.span = span.into();
|
|
}
|
|
|
|
/// Gets the first child node with a matching name.
|
|
pub fn get(&self, name: &str) -> Option<&KdlNode> {
|
|
self.nodes.iter().find(move |n| n.name().value() == name)
|
|
}
|
|
|
|
/// Gets a reference to the first child node with a matching name.
|
|
pub fn get_mut(&mut self, name: &str) -> Option<&mut KdlNode> {
|
|
self.nodes
|
|
.iter_mut()
|
|
.find(move |n| n.name().value() == name)
|
|
}
|
|
|
|
/// Gets the first argument (value) of the first child node with a
|
|
/// matching name. This is a shorthand utility for cases where a document
|
|
/// is being used as a key/value store.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// Given a document like this:
|
|
/// ```kdl
|
|
/// foo 1
|
|
/// bar false
|
|
/// ```
|
|
///
|
|
/// You can fetch the value of `foo` in a single call like this:
|
|
/// ```rust
|
|
/// # use kdl::{KdlDocument, KdlValue};
|
|
/// # let doc: KdlDocument = "foo 1\nbar #false".parse().unwrap();
|
|
/// assert_eq!(doc.get_arg("foo"), Some(&1.into()));
|
|
/// ```
|
|
pub fn get_arg(&self, name: &str) -> Option<&KdlValue> {
|
|
self.get(name).and_then(|node| node.get(0))
|
|
}
|
|
|
|
/// Returns an iterator of the all node arguments (value) of the first child node with a
|
|
/// matching name. This is a shorthand utility for cases where a document
|
|
/// is being used as a key/value store and the value is expected to be
|
|
/// array-ish.
|
|
///
|
|
/// If a node has no arguments, this will return an empty array.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// Given a document like this:
|
|
/// ```kdl
|
|
/// foo 1 2 3
|
|
/// bar #false
|
|
/// ```
|
|
///
|
|
/// You can fetch the arguments for `foo` in a single call like this:
|
|
/// ```rust
|
|
/// # use kdl::{KdlDocument, KdlValue};
|
|
/// # let doc: KdlDocument = "foo 1 2 3\nbar #false".parse().unwrap();
|
|
/// assert_eq!(
|
|
/// doc.iter_args("foo").collect::<Vec<&KdlValue>>(),
|
|
/// vec![&1.into(), &2.into(), &3.into()]
|
|
/// );
|
|
/// ```
|
|
pub fn iter_args(&self, name: &str) -> impl Iterator<Item = &KdlValue> {
|
|
self.get(name)
|
|
.map(|n| n.entries())
|
|
.unwrap_or_default()
|
|
.iter()
|
|
.filter(|e| e.name().is_none())
|
|
.map(|e| e.value())
|
|
}
|
|
|
|
/// Gets a mutable reference to the first argument (value) of the first
|
|
/// child node with a matching name. This is a shorthand utility for cases
|
|
/// where a document is being used as a key/value store.
|
|
pub fn get_arg_mut(&mut self, name: &str) -> Option<&mut KdlValue> {
|
|
self.get_mut(name).and_then(|node| node.get_mut(0))
|
|
}
|
|
|
|
/// This utility makes it easy to interact with a KDL convention where
|
|
/// child nodes named `-` are treated as array-ish values.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// Given a document like this:
|
|
/// ```kdl
|
|
/// foo {
|
|
/// - 1
|
|
/// - 2
|
|
/// - #false
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// You can fetch the dashed child values of `foo` in a single call like this:
|
|
/// ```rust
|
|
/// # use kdl::{KdlDocument, KdlValue};
|
|
/// # let doc: KdlDocument = "foo {\n - 1\n - 2\n - #false\n}".parse().unwrap();
|
|
/// assert_eq!(
|
|
/// doc.iter_dash_args("foo").collect::<Vec<&KdlValue>>(),
|
|
/// vec![&1.into(), &2.into(), &false.into()]
|
|
/// );
|
|
/// ```
|
|
pub fn iter_dash_args(&self, name: &str) -> impl Iterator<Item = &KdlValue> {
|
|
self.get(name)
|
|
.and_then(|n| n.children())
|
|
.map(|doc| doc.nodes())
|
|
.unwrap_or_default()
|
|
.iter()
|
|
.filter(|e| e.name().value() == "-")
|
|
.filter_map(|e| e.get(0))
|
|
}
|
|
|
|
/// Returns a reference to this document's child nodes.
|
|
pub fn nodes(&self) -> &[KdlNode] {
|
|
&self.nodes
|
|
}
|
|
|
|
/// Returns a mutable reference to this document's child nodes.
|
|
pub fn nodes_mut(&mut self) -> &mut Vec<KdlNode> {
|
|
&mut self.nodes
|
|
}
|
|
|
|
/// Gets the formatting details (including whitespace and comments) for this entry.
|
|
pub fn format(&self) -> Option<&KdlDocumentFormat> {
|
|
self.format.as_ref()
|
|
}
|
|
|
|
/// Gets a mutable reference to this entry's formatting details.
|
|
pub fn format_mut(&mut self) -> Option<&mut KdlDocumentFormat> {
|
|
self.format.as_mut()
|
|
}
|
|
|
|
/// Sets the formatting details for this entry.
|
|
pub fn set_format(&mut self, format: KdlDocumentFormat) {
|
|
self.format = Some(format);
|
|
}
|
|
|
|
/// Length of this document when rendered as a string.
|
|
pub fn len(&self) -> usize {
|
|
format!("{self}").len()
|
|
}
|
|
|
|
/// Returns true if this document is completely empty (including whitespace)
|
|
pub fn is_empty(&self) -> bool {
|
|
self.len() == 0
|
|
}
|
|
|
|
/// Clears leading and trailing text (whitespace, comments). `KdlNode`s in
|
|
/// this document will be unaffected.
|
|
///
|
|
/// If you need to clear the `KdlNode`s, use [`Self::clear_format_recursive`].
|
|
pub fn clear_format(&mut self) {
|
|
self.format = None;
|
|
}
|
|
|
|
/// Clears leading and trailing text (whitespace, comments), also clearing
|
|
/// all the `KdlNode`s in the document.
|
|
pub fn clear_format_recursive(&mut self) {
|
|
self.clear_format();
|
|
for node in self.nodes.iter_mut() {
|
|
node.clear_format_recursive();
|
|
}
|
|
}
|
|
|
|
/// Auto-formats this Document, making everything nice while preserving
|
|
/// comments.
|
|
pub fn autoformat(&mut self) {
|
|
self.autoformat_config(&FormatConfig::default());
|
|
}
|
|
|
|
/// Formats the document and removes all comments from the document.
|
|
pub fn autoformat_no_comments(&mut self) {
|
|
self.autoformat_config(&FormatConfig {
|
|
no_comments: true,
|
|
..Default::default()
|
|
});
|
|
}
|
|
|
|
/// Formats the document according to `config`.
|
|
pub fn autoformat_config(&mut self, config: &FormatConfig<'_>) {
|
|
if let Some(KdlDocumentFormat { leading, .. }) = (*self).format_mut() {
|
|
crate::fmt::autoformat_leading(leading, config);
|
|
}
|
|
let mut has_nodes = false;
|
|
for node in &mut self.nodes {
|
|
has_nodes = true;
|
|
node.autoformat_config(config);
|
|
}
|
|
if let Some(KdlDocumentFormat { trailing, .. }) = (*self).format_mut() {
|
|
crate::fmt::autoformat_trailing(trailing, config.no_comments);
|
|
if !has_nodes {
|
|
trailing.push('\n');
|
|
}
|
|
};
|
|
}
|
|
|
|
// TODO(@zkat): These should all be moved into the query module itself,
|
|
// instead of being methods on the models
|
|
//
|
|
// /// Queries this Document's children according to the KQL query language,
|
|
// /// returning an iterator over all matching nodes.
|
|
// ///
|
|
// /// # NOTE
|
|
// ///
|
|
// /// Any query selectors that try to select the toplevel `scope()` will
|
|
// /// fail to match when using this method, since there's no [`KdlNode`] to
|
|
// /// return in this case.
|
|
// pub fn query_all(
|
|
// &self,
|
|
// query: impl IntoKdlQuery,
|
|
// ) -> Result<KdlQueryIterator<'_>, KdlDiagnostic> {
|
|
// let parsed = query.into_query()?;
|
|
// Ok(KdlQueryIterator::new(None, Some(self), parsed))
|
|
// }
|
|
|
|
// /// Queries this Document's children according to the KQL query language,
|
|
// /// returning the first match, if any.
|
|
// ///
|
|
// /// # NOTE
|
|
// ///
|
|
// /// Any query selectors that try to select the toplevel `scope()` will
|
|
// /// fail to match when using this method, since there's no [`KdlNode`] to
|
|
// /// return in this case.
|
|
// pub fn query(&self, query: impl IntoKdlQuery) -> Result<Option<&KdlNode>, KdlDiagnostic> {
|
|
// let mut iter = self.query_all(query)?;
|
|
// Ok(iter.next())
|
|
// }
|
|
|
|
// /// Queries this Document's children according to the KQL query language,
|
|
// /// picking the first match, and calling `.get(key)` on it, if the query
|
|
// /// succeeded.
|
|
// ///
|
|
// /// # NOTE
|
|
// ///
|
|
// /// Any query selectors that try to select the toplevel `scope()` will
|
|
// /// fail to match when using this method, since there's no [`KdlNode`] to
|
|
// /// return in this case.
|
|
// pub fn query_get(
|
|
// &self,
|
|
// query: impl IntoKdlQuery,
|
|
// key: impl Into<NodeKey>,
|
|
// ) -> Result<Option<&KdlValue>, KdlDiagnostic> {
|
|
// Ok(self.query(query)?.and_then(|node| node.get(key)))
|
|
// }
|
|
|
|
// /// Queries this Document's children according to the KQL query language,
|
|
// /// returning an iterator over all matching nodes, returning the requested
|
|
// /// field from each of those nodes and filtering out nodes that don't have
|
|
// /// it.
|
|
// ///
|
|
// /// # NOTE
|
|
// ///
|
|
// /// Any query selectors that try to select the toplevel `scope()` will
|
|
// /// fail to match when using this method, since there's no [`KdlNode`] to
|
|
// /// return in this case.
|
|
// pub fn query_get_all(
|
|
// &self,
|
|
// query: impl IntoKdlQuery,
|
|
// key: impl Into<NodeKey>,
|
|
// ) -> Result<impl Iterator<Item = &KdlValue>, KdlDiagnostic> {
|
|
// let key: NodeKey = key.into();
|
|
// Ok(self
|
|
// .query_all(query)?
|
|
// .filter_map(move |node| node.get(key.clone())))
|
|
// }
|
|
|
|
/// Parses a string into a document.
|
|
///
|
|
/// If the `v1-fallback` feature is enabled, this method will first try to
|
|
/// parse the string as a KDL v2 document, and, if that fails, it will try
|
|
/// to parse again as a KDL v1 document. If both fail, a heuristic will be
|
|
/// applied to try and detect the "intended" KDL version, and that version's
|
|
/// error(s) will be returned.
|
|
pub fn parse(s: &str) -> Result<Self, KdlError> {
|
|
#[cfg(not(feature = "v1-fallback"))]
|
|
{
|
|
Self::parse_v2(s)
|
|
}
|
|
#[cfg(feature = "v1-fallback")]
|
|
{
|
|
let v2_res = KdlDocument::parse_v2(s);
|
|
if v2_res.is_err() {
|
|
let v1_res = KdlDocument::parse_v1(s);
|
|
if v1_res.is_ok() || detect_v1(s) {
|
|
v1_res
|
|
} else {
|
|
// TODO(@zkat): maybe we can add something to the error
|
|
// message to specify that it's "uncertain"?
|
|
// YOLO.
|
|
v2_res
|
|
}
|
|
} else {
|
|
v2_res
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Parses a KDL v2 string into a document.
|
|
pub fn parse_v2(s: &str) -> Result<Self, KdlError> {
|
|
crate::v2_parser::try_parse(crate::v2_parser::document, s)
|
|
}
|
|
|
|
/// Parses a KDL v1 string into a document.
|
|
#[cfg(feature = "v1")]
|
|
pub fn parse_v1(s: &str) -> Result<Self, KdlError> {
|
|
let ret: Result<kdlv1::KdlDocument, kdlv1::KdlError> = s.parse();
|
|
ret.map(|x| x.into()).map_err(|e| e.into())
|
|
}
|
|
|
|
/// Takes a KDL v1 document string and returns the same document, but
|
|
/// autoformatted into valid KDL v2 syntax.
|
|
#[cfg(feature = "v1")]
|
|
pub fn v1_to_v2(s: &str) -> Result<String, KdlError> {
|
|
let mut doc = KdlDocument::parse_v1(s)?;
|
|
doc.ensure_v2();
|
|
Ok(doc.to_string())
|
|
}
|
|
|
|
/// Takes a KDL v2 document string and returns the same document, but
|
|
/// autoformatted into valid KDL v1 syntax.
|
|
#[cfg(feature = "v1")]
|
|
pub fn v2_to_v1(s: &str) -> Result<String, KdlError> {
|
|
let mut doc = KdlDocument::parse_v2(s)?;
|
|
doc.ensure_v1();
|
|
Ok(doc.to_string())
|
|
}
|
|
|
|
/// Makes sure this document is in v2 format.
|
|
pub fn ensure_v2(&mut self) {
|
|
// No need to touch KdlDocumentFormat, probably. In the longer term,
|
|
// we'll want to make sure to parse out whitespace and comments and make
|
|
// sure they're actually compliant, but this is good enough for now.
|
|
for node in self.nodes_mut().iter_mut() {
|
|
node.ensure_v2();
|
|
}
|
|
}
|
|
|
|
/// Makes sure this document is in v1 format.
|
|
#[cfg(feature = "v1")]
|
|
pub fn ensure_v1(&mut self) {
|
|
// No need to touch KdlDocumentFormat, probably. In the longer term,
|
|
// we'll want to make sure to parse out whitespace and comments and make
|
|
// sure they're actually compliant, but this is good enough for now.
|
|
|
|
// the last node in v1 docs/children has to have a semicolon.
|
|
let mut iter = self.nodes_mut().iter_mut().rev();
|
|
let last = iter.next();
|
|
let penult = iter.next();
|
|
if let Some(last) = last {
|
|
if let Some(fmt) = last.format_mut() {
|
|
if !fmt.trailing.contains(';')
|
|
&& fmt
|
|
.trailing
|
|
.chars()
|
|
.any(|c| crate::v2_parser::NEWLINES.iter().any(|nl| nl.contains(c)))
|
|
{
|
|
fmt.terminator = ";".into();
|
|
}
|
|
} else {
|
|
let maybe_indent = {
|
|
if let Some(penult) = penult {
|
|
if let Some(fmt) = penult.format() {
|
|
fmt.leading.clone()
|
|
} else {
|
|
"".into()
|
|
}
|
|
} else {
|
|
"".into()
|
|
}
|
|
};
|
|
last.format = Some(KdlNodeFormat {
|
|
leading: maybe_indent,
|
|
terminator: "\n".into(),
|
|
..Default::default()
|
|
})
|
|
}
|
|
}
|
|
for node in self.nodes_mut().iter_mut() {
|
|
node.ensure_v1();
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
impl From<kdlv1::KdlDocument> for KdlDocument {
|
|
fn from(value: kdlv1::KdlDocument) -> Self {
|
|
KdlDocument {
|
|
nodes: value.nodes().iter().map(|x| x.clone().into()).collect(),
|
|
format: Some(KdlDocumentFormat {
|
|
leading: value.leading().unwrap_or("").into(),
|
|
trailing: value.trailing().unwrap_or("").into(),
|
|
}),
|
|
#[cfg(feature = "span")]
|
|
span: SourceSpan::new(value.span().offset().into(), value.span().len()),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Applies heuristics to get an idea of whether the string might be intended to
|
|
/// be v2.
|
|
#[allow(unused)]
|
|
pub(crate) fn detect_v2(input: &str) -> bool {
|
|
for line in input.lines() {
|
|
if line.contains("kdl-version 2")
|
|
|| line.contains("#true")
|
|
|| line.contains("#false")
|
|
|| line.contains("#null")
|
|
|| line.contains("#inf")
|
|
|| line.contains("#-inf")
|
|
|| line.contains("#nan")
|
|
|| line.contains(" #\"")
|
|
|| line.contains("\"\"\"")
|
|
// Very very rough attempt at finding unquoted strings. We give up
|
|
// the first time we see a quoted one on a line.
|
|
|| (!line.contains('"') && line
|
|
.split_whitespace()
|
|
.skip(1)
|
|
.any(|x| {
|
|
x.chars()
|
|
.next()
|
|
.map(|d| !d.is_ascii_digit() && d != '-' && d != '+')
|
|
.unwrap_or_default()
|
|
}))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Applies heuristics to get an idea of whether the string might be intended to
|
|
/// be v2.
|
|
#[allow(unused)]
|
|
pub(crate) fn detect_v1(input: &str) -> bool {
|
|
input
|
|
.lines()
|
|
.next()
|
|
.map(|l| l.contains("kdl-version 1"))
|
|
.unwrap_or(false)
|
|
|| input.contains(" true")
|
|
|| input.contains(" false")
|
|
|| input.contains(" null")
|
|
|| input.contains("r#\"")
|
|
|| input.contains(" \"\n")
|
|
|| input.contains(" \"\r\n")
|
|
}
|
|
|
|
impl std::str::FromStr for KdlDocument {
|
|
type Err = KdlError;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
Self::parse(s)
|
|
}
|
|
}
|
|
|
|
impl Display for KdlDocument {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
self.stringify(f, 0)
|
|
}
|
|
}
|
|
|
|
impl KdlDocument {
|
|
pub(crate) fn stringify(
|
|
&self,
|
|
f: &mut std::fmt::Formatter<'_>,
|
|
indent: usize,
|
|
) -> std::fmt::Result {
|
|
if let Some(KdlDocumentFormat { leading, .. }) = self.format() {
|
|
write!(f, "{leading}")?;
|
|
}
|
|
for node in &self.nodes {
|
|
node.stringify(f, indent)?;
|
|
}
|
|
if let Some(KdlDocumentFormat { trailing, .. }) = self.format() {
|
|
write!(f, "{trailing}")?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl IntoIterator for KdlDocument {
|
|
type Item = KdlNode;
|
|
type IntoIter = std::vec::IntoIter<Self::Item>;
|
|
|
|
fn into_iter(self) -> Self::IntoIter {
|
|
self.nodes.into_iter()
|
|
}
|
|
}
|
|
|
|
/// Formatting details for [`KdlDocument`]s.
|
|
#[derive(Debug, Clone, Default, Hash, Eq, PartialEq)]
|
|
pub struct KdlDocumentFormat {
|
|
/// Whitespace and comments preceding the document's first node.
|
|
pub leading: String,
|
|
/// Whitespace and comments following the document's last node.
|
|
pub trailing: String,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
#[cfg(feature = "span")]
|
|
use crate::KdlIdentifier;
|
|
use crate::{KdlEntry, KdlValue};
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn canonical_clear_fmt() -> miette::Result<()> {
|
|
let left_src = r#"
|
|
// There is a node here
|
|
first_node /*with cool comments, too */ param=1.03e2 /-"commented" "argument" {
|
|
// With nested nodes too
|
|
nested 1 2 3
|
|
nested_2 "hi" "world" // this one is cool
|
|
}
|
|
second_node param=153 { nested one=1 two=2; }"#;
|
|
let right_src = r#"
|
|
first_node param=103.0 "argument" {
|
|
// Different indentation, because
|
|
// Why not
|
|
nested 1 2 3
|
|
nested_2 "hi" /* actually, "hello" */ "world"
|
|
}
|
|
// There is a node here
|
|
second_node /* This time, the comment is here */ param=153 {
|
|
nested one=1 two=2
|
|
}"#;
|
|
let mut left_doc: KdlDocument = left_src.parse()?;
|
|
let mut right_doc: KdlDocument = right_src.parse()?;
|
|
assert_ne!(left_doc, right_doc);
|
|
left_doc.clear_format_recursive();
|
|
right_doc.clear_format_recursive();
|
|
assert_eq!(left_doc, right_doc);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn basic_parsing() -> miette::Result<()> {
|
|
let src = r#"
|
|
// Hello, world!
|
|
node 1
|
|
node two
|
|
node item="three";
|
|
node {
|
|
nested 1 2 3
|
|
nested_2 hi "world"
|
|
}
|
|
(type)node ("type")what?
|
|
+false #true
|
|
null_id null_prop=#null
|
|
foo indented
|
|
// normal comment?
|
|
/- comment
|
|
/* block comment */
|
|
inline /*comment*/ here
|
|
another /-comment there
|
|
|
|
|
|
after some whitespace
|
|
trailing /* multiline */
|
|
trailing // single line
|
|
"#;
|
|
let _doc: KdlDocument = src.parse()?;
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn parsing() -> miette::Result<()> {
|
|
let src = "
|
|
// This is the first node
|
|
foo 1 2 three #null #true bar=\"baz\" {
|
|
- 1
|
|
- 2
|
|
- three
|
|
(mytype)something (\"name\")else\r
|
|
}
|
|
|
|
null_id null_prop=#null
|
|
true_id true_prop=#null
|
|
+false #true
|
|
|
|
bar \"indented\" // trailing whitespace after this\t
|
|
/*
|
|
Some random comment
|
|
*/
|
|
|
|
a; b; c;
|
|
/-commented \"node\"
|
|
|
|
another /*foo*/ \"node\" /-1 /*bar*/ #null;
|
|
final;";
|
|
let mut doc: KdlDocument = src.parse()?;
|
|
|
|
assert_eq!(doc.get_arg("foo"), Some(&1.into()));
|
|
assert_eq!(
|
|
doc.iter_dash_args("foo").collect::<Vec<&KdlValue>>(),
|
|
vec![&1.into(), &2.into(), &"three".into()]
|
|
);
|
|
assert_eq!(
|
|
doc.format().map(|f| &f.leading[..]),
|
|
Some("\n// This is the first node\n")
|
|
);
|
|
|
|
let foo = doc.get("foo").expect("expected a foo node");
|
|
assert_eq!(foo.format().map(|f| &f.terminator[..]), Some("\n"));
|
|
assert_eq!(&foo[2], &"three".into());
|
|
assert_eq!(&foo["bar"], &"baz".into());
|
|
assert_eq!(
|
|
foo.children().unwrap().get_arg("something"),
|
|
Some(&"else".into())
|
|
);
|
|
assert_eq!(doc.get_arg("another"), Some(&"node".into()));
|
|
|
|
let null = doc.get("null_id").expect("expected a null_id node");
|
|
assert_eq!(&null["null_prop"], &KdlValue::Null);
|
|
|
|
let tru = doc.get("true_id").expect("expected a true_id node");
|
|
assert_eq!(&tru["true_prop"], &KdlValue::Null);
|
|
|
|
let plusfalse = doc.get("+false").expect("expected a +false node");
|
|
assert_eq!(&plusfalse[0], &KdlValue::Bool(true));
|
|
|
|
let bar = doc.get("bar").expect("expected a bar node");
|
|
assert_eq!(
|
|
format!("{}", bar),
|
|
"\n bar \"indented\" // trailing whitespace after this\t\n"
|
|
);
|
|
|
|
let a = doc.get("a").expect("expected a node");
|
|
assert_eq!(
|
|
format!("{}", a),
|
|
"/*\nSome random comment\n */\n\na;".to_string()
|
|
);
|
|
|
|
let b = doc.get("b").expect("expected a node");
|
|
assert_eq!(format!("{}", b), " b;".to_string());
|
|
|
|
// Round-tripping works.
|
|
assert_eq!(format!("{}", doc), src);
|
|
|
|
// Programmatic manipulation works.
|
|
let mut node: KdlNode = "new\n".parse()?;
|
|
// Manual entry parsing preserves formatting/reprs. Note that
|
|
// if you're making KdlEntries this way, you need to inject
|
|
// your own whitespace (or format the node)
|
|
node.push(" \"blah\"=0xDEADbeef".parse::<KdlEntry>()?);
|
|
doc.nodes_mut().push(node);
|
|
|
|
assert_eq!(
|
|
format!("{}", doc),
|
|
format!("{}new \"blah\"=0xDEADbeef\n", src)
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn construction() {
|
|
let mut doc = KdlDocument::new();
|
|
doc.nodes_mut().push(KdlNode::new("foo"));
|
|
|
|
let mut bar = KdlNode::new("bar");
|
|
bar.insert("prop", "value");
|
|
bar.push(1);
|
|
bar.push(2);
|
|
bar.push(false);
|
|
bar.push(KdlValue::Null);
|
|
|
|
let subdoc = bar.ensure_children();
|
|
subdoc.nodes_mut().push(KdlNode::new("barchild"));
|
|
doc.nodes_mut().push(bar);
|
|
doc.nodes_mut().push(KdlNode::new("baz"));
|
|
|
|
doc.autoformat();
|
|
|
|
assert_eq!(
|
|
r#"foo
|
|
bar prop=value 1 2 #false #null {
|
|
barchild
|
|
}
|
|
baz
|
|
"#,
|
|
format!("{}", doc)
|
|
);
|
|
}
|
|
|
|
#[ignore = "There's still issues around formatting comments and esclines."]
|
|
#[test]
|
|
fn autoformat() -> miette::Result<()> {
|
|
let mut doc: KdlDocument = r##"
|
|
|
|
/* x */ foo 1 "bar"=0xDEADbeef {
|
|
child1 1 ;
|
|
|
|
// child 2 comment
|
|
|
|
child2 2 /-3 // comment
|
|
|
|
child3 "
|
|
|
|
string\t
|
|
" \
|
|
{
|
|
/*
|
|
|
|
|
|
multiline*/
|
|
inner1 \
|
|
#"value"# \
|
|
;
|
|
|
|
inner2 \ //comment
|
|
{
|
|
inner3
|
|
}
|
|
}
|
|
}
|
|
|
|
// trailing comment here
|
|
|
|
"##
|
|
.parse()?;
|
|
|
|
KdlDocument::autoformat(&mut doc);
|
|
|
|
assert_eq!(
|
|
doc.to_string(),
|
|
r#"/* x */
|
|
foo 1 bar=0xdeadbeef {
|
|
child1 1
|
|
// child 2 comment
|
|
child2 2 /-3 // comment
|
|
child3 "\nstring\t" {
|
|
/*
|
|
|
|
|
|
multiline*/
|
|
inner1 value
|
|
inner2 {
|
|
inner3
|
|
}
|
|
}
|
|
}
|
|
// trailing comment here"#
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn simple_autoformat() -> miette::Result<()> {
|
|
let mut doc: KdlDocument = "a { b { c { }; }; }".parse().unwrap();
|
|
KdlDocument::autoformat(&mut doc);
|
|
assert_eq!(
|
|
doc.to_string(),
|
|
r#"a {
|
|
b {
|
|
c {
|
|
|
|
}
|
|
}
|
|
}
|
|
"#
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn simple_autoformat_two_spaces() -> miette::Result<()> {
|
|
let mut doc: KdlDocument = "a { b { c { }; }; }".parse().unwrap();
|
|
KdlDocument::autoformat_config(
|
|
&mut doc,
|
|
&FormatConfig {
|
|
indent: " ",
|
|
..Default::default()
|
|
},
|
|
);
|
|
assert_eq!(
|
|
doc.to_string(),
|
|
r#"a {
|
|
b {
|
|
c {
|
|
|
|
}
|
|
}
|
|
}
|
|
"#
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn simple_autoformat_single_tabs() -> miette::Result<()> {
|
|
let mut doc: KdlDocument = "a { b { c { }; }; }".parse().unwrap();
|
|
KdlDocument::autoformat_config(
|
|
&mut doc,
|
|
&FormatConfig {
|
|
indent: "\t",
|
|
..Default::default()
|
|
},
|
|
);
|
|
assert_eq!(doc.to_string(), "a {\n\tb {\n\t\tc {\n\n\t\t}\n\t}\n}\n");
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn simple_autoformat_no_comments() -> miette::Result<()> {
|
|
let mut doc: KdlDocument =
|
|
"// a comment\na {\n// another comment\n b { c { // another comment\n }; }; }"
|
|
.parse()
|
|
.unwrap();
|
|
KdlDocument::autoformat_no_comments(&mut doc);
|
|
assert_eq!(
|
|
doc.to_string(),
|
|
r#"a {
|
|
b {
|
|
c {
|
|
|
|
}
|
|
}
|
|
}
|
|
"#
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(feature = "span")]
|
|
fn check_spans_for_doc(doc: &KdlDocument, source: &impl miette::SourceCode) {
|
|
for node in doc.nodes() {
|
|
check_spans_for_node(node, source);
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "span")]
|
|
fn check_spans_for_node(node: &KdlNode, source: &impl miette::SourceCode) {
|
|
use crate::KdlEntryFormat;
|
|
|
|
check_span_for_ident(node.name(), source);
|
|
if let Some(ty) = node.ty() {
|
|
check_span_for_ident(ty, source);
|
|
}
|
|
|
|
for entry in node.entries() {
|
|
if let Some(name) = entry.name() {
|
|
check_span_for_ident(name, source);
|
|
}
|
|
if let Some(ty) = entry.ty() {
|
|
check_span_for_ident(ty, source);
|
|
}
|
|
if let Some(KdlEntryFormat { value_repr, .. }) = entry.format() {
|
|
if entry.name().is_none() && entry.ty().is_none() {
|
|
check_span(value_repr, entry.span(), source);
|
|
}
|
|
}
|
|
}
|
|
if let Some(children) = node.children() {
|
|
check_spans_for_doc(children, source);
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "span")]
|
|
#[track_caller]
|
|
fn check_span_for_ident(ident: &KdlIdentifier, source: &impl miette::SourceCode) {
|
|
if let Some(repr) = ident.repr() {
|
|
check_span(repr, ident.span(), source);
|
|
} else {
|
|
check_span(ident.value(), ident.span(), source);
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "span")]
|
|
#[track_caller]
|
|
fn check_span(expected: &str, span: SourceSpan, source: &impl miette::SourceCode) {
|
|
let span = source.read_span(&span, 0, 0).unwrap();
|
|
let span = std::str::from_utf8(span.data()).unwrap();
|
|
assert_eq!(span, expected);
|
|
}
|
|
|
|
#[cfg(feature = "span")]
|
|
#[test]
|
|
fn span_test() -> miette::Result<()> {
|
|
let input = r####"
|
|
this {
|
|
is (a)"cool" document="to" read=(int)5 10.1 (u32)0x45
|
|
and x="" {
|
|
"it" /*shh*/ "has"="💯" ##"the"##
|
|
Best🎊est
|
|
"syntax ever"
|
|
}
|
|
"yknow?" 0x10
|
|
}
|
|
// that's
|
|
nice
|
|
inline { time; to; live "our" "dreams"; "y;all" }
|
|
|
|
"####;
|
|
|
|
let doc: KdlDocument = input.parse()?;
|
|
|
|
// First check that all the identity-spans are correct
|
|
check_spans_for_doc(&doc, &input);
|
|
|
|
// Now check some more interesting concrete spans
|
|
|
|
// The whole document should be everything from the first node until the
|
|
// last before_terminator whitespace.
|
|
check_span(&input[1..(input.len() - 2)], doc.span(), &input);
|
|
|
|
// This one-liner node should be the whole line without leading whitespace
|
|
let is_node = doc
|
|
.get("this")
|
|
.unwrap()
|
|
.children()
|
|
.unwrap()
|
|
.get("is")
|
|
.unwrap();
|
|
check_span(
|
|
r##"is (a)"cool" document="to" read=(int)5 10.1 (u32)0x45"##,
|
|
is_node.span(),
|
|
&input,
|
|
);
|
|
|
|
// Some simple with/without type hints
|
|
check_span(r#"(a)"cool""#, is_node.entry(0).unwrap().span(), &input);
|
|
check_span(
|
|
r#"read=(int)5"#,
|
|
is_node.entry("read").unwrap().span(),
|
|
&input,
|
|
);
|
|
check_span(r#"10.1"#, is_node.entry(1).unwrap().span(), &input);
|
|
check_span(r#"(u32)0x45"#, is_node.entry(2).unwrap().span(), &input);
|
|
|
|
// Now let's look at some messed up parts of that "and" node
|
|
let and_node = doc
|
|
.get("this")
|
|
.unwrap()
|
|
.children()
|
|
.unwrap()
|
|
.get("and")
|
|
.unwrap();
|
|
|
|
// The node is what you expect, the whole line and its two braces
|
|
check_span(
|
|
r####"and x="" {
|
|
"it" /*shh*/ "has"="💯" ##"the"##
|
|
Best🎊est
|
|
"syntax ever"
|
|
}"####,
|
|
and_node.span(),
|
|
&input,
|
|
);
|
|
|
|
// The child document is a little weird, it's the contents *inside* the braces
|
|
// without the surrounding whitespace/comments. Just the actual contents.
|
|
check_span(
|
|
r####""it" /*shh*/ "has"="💯" ##"the"##
|
|
Best🎊est
|
|
"syntax ever""####,
|
|
and_node.children().unwrap().span(),
|
|
&input,
|
|
);
|
|
|
|
// Oh hey don't forget to check that "x" entry
|
|
check_span(r#"x="""#, and_node.entry("x").unwrap().span(), &input);
|
|
|
|
// Now the "it" node, more straightforward
|
|
let it_node = and_node.children().unwrap().get("it").unwrap();
|
|
check_span(
|
|
r####""it" /*shh*/ "has"="💯" ##"the"##"####,
|
|
it_node.span(),
|
|
&input,
|
|
);
|
|
check_span(
|
|
r#""has"="💯""#,
|
|
it_node.entry("has").unwrap().span(),
|
|
&input,
|
|
);
|
|
check_span(
|
|
r####"##"the"##"####,
|
|
it_node.entry(0).unwrap().span(),
|
|
&input,
|
|
);
|
|
|
|
// Make sure inline nodes work ok
|
|
let inline_node = doc.get("inline").unwrap();
|
|
check_span(
|
|
r#"inline { time; to; live "our" "dreams"; "y;all" }"#,
|
|
inline_node.span(),
|
|
&input,
|
|
);
|
|
|
|
let inline_children = inline_node.children().unwrap();
|
|
check_span(
|
|
r#"time; to; live "our" "dreams"; "y;all" "#,
|
|
inline_children.span(),
|
|
&input,
|
|
);
|
|
|
|
let inline_nodes = inline_children.nodes();
|
|
check_span("time", inline_nodes[0].span(), &input);
|
|
check_span("to", inline_nodes[1].span(), &input);
|
|
check_span(r#"live "our" "dreams""#, inline_nodes[2].span(), &input);
|
|
check_span(r#""y;all" "#, inline_nodes[3].span(), &input);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn parse_examples() -> miette::Result<()> {
|
|
include_str!("../examples/kdl-schema.kdl").parse::<KdlDocument>()?;
|
|
include_str!("../examples/Cargo.kdl").parse::<KdlDocument>()?;
|
|
include_str!("../examples/ci.kdl").parse::<KdlDocument>()?;
|
|
include_str!("../examples/nuget.kdl").parse::<KdlDocument>()?;
|
|
include_str!("../examples/website.kdl").parse::<KdlDocument>()?;
|
|
include_str!("../examples/zellij.kdl").parse::<KdlDocument>()?;
|
|
include_str!("../examples/zellij-unquoted-bindings.kdl").parse::<KdlDocument>()?;
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
#[test]
|
|
fn v1_v2_conversions() -> miette::Result<()> {
|
|
let v1 = r##"
|
|
// If you'd like to override the default keybindings completely, be sure to change "keybinds" to "keybinds clear-defaults=true"
|
|
keybinds {
|
|
normal {
|
|
// uncomment this and adjust key if using copy_on_select=false
|
|
// bind "Alt c" { Copy; }
|
|
}
|
|
locked {
|
|
bind "Ctrl g" { SwitchToMode "Normal"; }
|
|
}
|
|
resize {
|
|
bind "Ctrl n" { SwitchToMode "Normal"; }
|
|
bind "h" "Left" { Resize "Increase Left"; }
|
|
bind "j" "Down" { Resize "Increase Down"; }
|
|
bind "k" "Up" { Resize "Increase Up"; }
|
|
bind "l" "Right" { Resize "Increase Right"; }
|
|
bind "H" { Resize "Decrease Left"; }
|
|
bind "J" { Resize "Decrease Down"; }
|
|
bind "K" { Resize "Decrease Up"; }
|
|
bind "L" { Resize "Decrease Right"; }
|
|
bind "=" "+" { Resize "Increase"; }
|
|
bind "-" { Resize "Decrease"; }
|
|
}
|
|
}
|
|
// Plugin aliases - can be used to change the implementation of Zellij
|
|
// changing these requires a restart to take effect
|
|
plugins {
|
|
tab-bar location="zellij:tab-bar"
|
|
status-bar location="zellij:status-bar"
|
|
welcome-screen location="zellij:session-manager" {
|
|
welcome_screen true
|
|
}
|
|
filepicker location="zellij:strider" {
|
|
cwd "\/"
|
|
}
|
|
}
|
|
mouse_mode false
|
|
mirror_session true
|
|
"##;
|
|
let v2 = r##"
|
|
// If you'd like to override the default keybindings completely, be sure to change "keybinds" to "keybinds clear-defaults=true"
|
|
keybinds {
|
|
normal {
|
|
// uncomment this and adjust key if using copy_on_select=false
|
|
// bind "Alt c" { Copy; }
|
|
}
|
|
locked {
|
|
bind "Ctrl g" { SwitchToMode Normal; }
|
|
}
|
|
resize {
|
|
bind "Ctrl n" { SwitchToMode Normal; }
|
|
bind h Left { Resize "Increase Left"; }
|
|
bind j Down { Resize "Increase Down"; }
|
|
bind k Up { Resize "Increase Up"; }
|
|
bind l Right { Resize "Increase Right"; }
|
|
bind H { Resize "Decrease Left"; }
|
|
bind J { Resize "Decrease Down"; }
|
|
bind K { Resize "Decrease Up"; }
|
|
bind L { Resize "Decrease Right"; }
|
|
bind "=" + { Resize Increase; }
|
|
bind - { Resize Decrease; }
|
|
}
|
|
}
|
|
// Plugin aliases - can be used to change the implementation of Zellij
|
|
// changing these requires a restart to take effect
|
|
plugins {
|
|
tab-bar location=zellij:tab-bar
|
|
status-bar location=zellij:status-bar
|
|
welcome-screen location=zellij:session-manager {
|
|
welcome_screen #true
|
|
}
|
|
filepicker location=zellij:strider {
|
|
cwd "/"
|
|
}
|
|
}
|
|
mouse_mode #false
|
|
mirror_session #true
|
|
"##;
|
|
pretty_assertions::assert_eq!(KdlDocument::v1_to_v2(v1)?, v2, "Converting a v1 doc to v2");
|
|
pretty_assertions::assert_eq!(KdlDocument::v2_to_v1(v2)?, v1, "Converting a v2 doc to v1");
|
|
assert!(super::detect_v1(v1));
|
|
assert!(super::detect_v2(v2));
|
|
Ok(())
|
|
}
|
|
}
|