Skip to content

Commit

Permalink
feat(v1): Add utility to auto-translate v1 to v2
Browse files Browse the repository at this point in the history
  • Loading branch information
zkat committed Dec 20, 2024
1 parent 3e5d7a3 commit c486cda
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 8 deletions.
95 changes: 95 additions & 0 deletions src/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,15 @@ impl KdlDocument {
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, KdlParseFailure> {
let mut doc = KdlDocument::parse_v1(s)?;
doc.autoformat();
Ok(doc.to_string())
}
}

#[cfg(feature = "v1")]
Expand Down Expand Up @@ -946,4 +955,90 @@ inline { time; to; live "our" "dreams"; "y;all" }
include_str!("../examples/zellij-unquoted-bindings.kdl").parse::<KdlDocument>()?;
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(())
}
}
110 changes: 109 additions & 1 deletion src/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<String>()
.replacen("\"", "\"\"\"\n", 1)
.chars()
.rev()
.collect::<String>()
}
} 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 {
Expand Down Expand Up @@ -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(())
}
}
19 changes: 14 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
//!
Expand Down
2 changes: 1 addition & 1 deletion src/v2_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
Expand Down
2 changes: 1 addition & 1 deletion src/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit c486cda

Please sign in to comment.