diff --git a/src/document.rs b/src/document.rs index 0e2456b..a2c55b8 100644 --- a/src/document.rs +++ b/src/document.rs @@ -359,6 +359,15 @@ impl KdlDocument { let ret: Result = 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 { + let mut doc = KdlDocument::parse_v1(s)?; + doc.autoformat(); + Ok(doc.to_string()) + } } #[cfg(feature = "v1")] @@ -946,4 +955,90 @@ inline { time; to; live "our" "dreams"; "y;all" } include_str!("../examples/zellij-unquoted-bindings.kdl").parse::()?; Ok(()) } + + #[ignore = "Formatting is still seriously broken, and this is gonna need some extra love."] + #[cfg(feature = "v1")] + #[test] + fn v1_to_v2() -> miette::Result<()> { + let original = 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 expected = 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(original)?, expected); + Ok(()) + } } diff --git a/src/entry.rs b/src/entry.rs index 7bd02f4..0a71a30 100644 --- a/src/entry.rs +++ b/src/entry.rs @@ -165,13 +165,76 @@ impl KdlEntry { pub fn autoformat(&mut self) { // TODO once MSRV allows: //self.format.take_if(|f| !f.autoformat_keep); + let value_repr = self.format.as_ref().map(|x| { + match &self.value { + KdlValue::String(val) => { + // cleanup. I don't _think_ this should have any whitespace, + // but just in case. + let s = x.value_repr.trim(); + // convert raw strings to new format + let s = s.strip_prefix("r").unwrap_or(s); + let s = if crate::value::is_plain_ident(val) { + val.to_string() + } else if s + .find(|c| v2_parser::NEWLINES.iter().any(|nl| nl.contains(c))) + .is_some() + { + // Multiline string. Need triple quotes if they're not there already. + if s.contains("\"\"\"") { + // We're probably good. This could be more precise, but close enough. + s.to_string() + } else { + // `"` -> `"""` but also extra newlines need to be + // added because v2 strips the first and last ones. + let s = s.replacen("\"", "\"\"\"\n", 1); + s.chars() + .rev() + .collect::() + .replacen("\"", "\"\"\"\n", 1) + .chars() + .rev() + .collect::() + } + } else if !s.starts_with("#") { + // `/` is no longer an escaped char in v2. + s.replace("\\/", "/") + } else { + // We're all good! Let's move on. + s.to_string() + }; + s + } + // These have `#` prefixes now. The regular Display impl will + // take care of that. + KdlValue::Bool(_) | KdlValue::Null => format!("{}", self.value), + // These should be fine as-is? + KdlValue::Integer(_) | KdlValue::Float(_) => x.value_repr.clone(), + } + }); + if !self .format .as_ref() .map(|f| f.autoformat_keep) .unwrap_or(false) { - self.format = None + self.format = None; + } + + if let Some(value_repr) = value_repr.as_ref() { + self.format = Some( + self.format + .clone() + .map(|mut x| { + x.value_repr = value_repr.into(); + x + }) + .unwrap_or_else(|| KdlEntryFormat { + value_repr: value_repr.into(), + leading: " ".into(), + ..Default::default() + }), + ) } if let Some(name) = &mut self.name { @@ -455,4 +518,49 @@ mod test { let entry = KdlEntry::new_prop("name", KdlValue::Integer(42)); assert_eq!(format!("{}", entry), "name=42"); } + + #[cfg(feature = "v1")] + #[test] + fn v1_to_v2_format() -> miette::Result<()> { + let mut entry = KdlEntry::parse_v1(r##"r#"hello, world!"#"##)?; + entry.autoformat(); + assert_eq!(format!("{}", entry), r##" #"hello, world!"#"##); + + let mut entry = KdlEntry::parse_v1(r#""hello, \" world!""#)?; + entry.autoformat(); + assert_eq!(format!("{}", entry), r#" "hello, \" world!""#); + + let mut entry = KdlEntry::parse_v1("\"foo!`~.,<>\"")?; + entry.autoformat(); + assert_eq!(format!("{}", entry), " foo!`~.,<>"); + + let mut entry = KdlEntry::parse_v1("\"\nhello, world!\"")?; + entry.autoformat(); + assert_eq!(format!("{}", entry), " \"\"\"\n\nhello, world!\n\"\"\""); + + let mut entry = KdlEntry::parse_v1("r#\"\nhello, world!\"#")?; + entry.autoformat(); + assert_eq!(format!("{}", entry), " #\"\"\"\n\nhello, world!\n\"\"\"#"); + + let mut entry = KdlEntry::parse_v1("true")?; + entry.autoformat(); + assert_eq!(format!("{}", entry), " #true"); + + let mut entry = KdlEntry::parse_v1("false")?; + entry.autoformat(); + assert_eq!(format!("{}", entry), " #false"); + + let mut entry = KdlEntry::parse_v1("null")?; + entry.autoformat(); + assert_eq!(format!("{}", entry), " #null"); + + let mut entry = KdlEntry::parse_v1("1_234_567")?; + entry.autoformat(); + assert_eq!(format!("{}", entry), " 1_234_567"); + + let mut entry = KdlEntry::parse_v1("1_234_567E-10")?; + entry.autoformat(); + assert_eq!(format!("{}", entry), " 1_234_567E-10"); + Ok(()) + } } diff --git a/src/lib.rs b/src/lib.rs index 6f76209..0b42387 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,11 +10,20 @@ //! You can think of this crate as //! [`toml_edit`](https://crates.io/crates/toml_edit), but for KDL. //! -//! This crate supports parsing [the final KDL 2.0.0 -//! draft](https://github.com/kdl-org/kdl/pull/434), which might get a couple -//! more small modifications before things are truly finalized. It does not -//! support KDL 1.0 as of this release, but versions of this crate lower than -//! 5.0 do. +//! This crate supports both KDL v2.0.0 and v1.0.0 (when using the non-default +//! `v1` feature). +//! +//! There is also a `v1-fallback` feature that may be enabled in order to have +//! the various `Kdl*::parse` methods try to parse their input as v2, and, if +//! that fails, try again as v1. In either case, a dedicated `Kdl*::parse_v1` +//! method is available for v1-exclusive parsing, as long as either `v1` or +//! `v1-fallback` are enabled. +//! +//! Autoformatting a document parsed from a v1 doc will translate the document +//! to v2 format, preserving as much of the v1 trivia as possible (comments, +//! etc). It *should* generate a fully valid v2 document, but there may still be +//! some corner cases that don't translate well. Please file issues as needed +//! for those. //! //! ## Example //! diff --git a/src/v2_parser.rs b/src/v2_parser.rs index 6f3a347..3434e7d 100644 --- a/src/v2_parser.rs +++ b/src/v2_parser.rs @@ -1578,7 +1578,7 @@ fn escline_test() { assert_eq!(node.entries().len(), 2); } -static NEWLINES: [&str; 7] = [ +pub(crate) static NEWLINES: [&str; 7] = [ "\u{000D}\u{000A}", "\u{000D}", "\u{000A}", diff --git a/src/value.rs b/src/value.rs index 8fe4722..44d26e9 100644 --- a/src/value.rs +++ b/src/value.rs @@ -174,7 +174,7 @@ impl Display for KdlValue { } } -fn is_plain_ident(ident: &str) -> bool { +pub(crate) fn is_plain_ident(ident: &str) -> bool { let ident_bytes = ident.as_bytes(); ident .find(crate::v2_parser::is_disallowed_ident_char)