Skip to content

Commit c486cda

Browse files
committed
feat(v1): Add utility to auto-translate v1 to v2
1 parent 3e5d7a3 commit c486cda

File tree

5 files changed

+220
-8
lines changed

5 files changed

+220
-8
lines changed

src/document.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,15 @@ impl KdlDocument {
359359
let ret: Result<kdlv1::KdlDocument, kdlv1::KdlError> = s.parse();
360360
ret.map(|x| x.into()).map_err(|e| e.into())
361361
}
362+
363+
/// Takes a KDL v1 document string and returns the same document, but
364+
/// autoformatted into valid KDL v2 syntax.
365+
#[cfg(feature = "v1")]
366+
pub fn v1_to_v2(s: &str) -> Result<String, KdlParseFailure> {
367+
let mut doc = KdlDocument::parse_v1(s)?;
368+
doc.autoformat();
369+
Ok(doc.to_string())
370+
}
362371
}
363372

364373
#[cfg(feature = "v1")]
@@ -946,4 +955,90 @@ inline { time; to; live "our" "dreams"; "y;all" }
946955
include_str!("../examples/zellij-unquoted-bindings.kdl").parse::<KdlDocument>()?;
947956
Ok(())
948957
}
958+
959+
#[ignore = "Formatting is still seriously broken, and this is gonna need some extra love."]
960+
#[cfg(feature = "v1")]
961+
#[test]
962+
fn v1_to_v2() -> miette::Result<()> {
963+
let original = r##"
964+
// If you'd like to override the default keybindings completely, be sure to change "keybinds" to "keybinds clear-defaults=true"
965+
keybinds {
966+
normal {
967+
// uncomment this and adjust key if using copy_on_select=false
968+
// bind "Alt c" { Copy; }
969+
}
970+
locked {
971+
bind "Ctrl g" { SwitchToMode "Normal"; }
972+
}
973+
resize {
974+
bind "Ctrl n" { SwitchToMode "Normal"; }
975+
bind "h" "Left" { Resize "Increase Left"; }
976+
bind "j" "Down" { Resize "Increase Down"; }
977+
bind "k" "Up" { Resize "Increase Up"; }
978+
bind "l" "Right" { Resize "Increase Right"; }
979+
bind "H" { Resize "Decrease Left"; }
980+
bind "J" { Resize "Decrease Down"; }
981+
bind "K" { Resize "Decrease Up"; }
982+
bind "L" { Resize "Decrease Right"; }
983+
bind "=" "+" { Resize "Increase"; }
984+
bind "-" { Resize "Decrease"; }
985+
}
986+
}
987+
// Plugin aliases - can be used to change the implementation of Zellij
988+
// changing these requires a restart to take effect
989+
plugins {
990+
tab-bar location="zellij:tab-bar"
991+
status-bar location="zellij:status-bar"
992+
welcome-screen location="zellij:session-manager" {
993+
welcome_screen true
994+
}
995+
filepicker location="zellij:strider" {
996+
cwd "/"
997+
}
998+
}
999+
mouse_mode false
1000+
mirror_session true
1001+
"##;
1002+
let expected = r##"
1003+
// If you'd like to override the default keybindings completely, be sure to change "keybinds" to "keybinds clear-defaults=true"
1004+
keybinds {
1005+
normal {
1006+
// uncomment this and adjust key if using copy_on_select=false
1007+
// bind "Alt c" { Copy; }
1008+
}
1009+
locked {
1010+
bind "Ctrl g" { SwitchToMode Normal; }
1011+
}
1012+
resize {
1013+
bind "Ctrl n" { SwitchToMode Normal; }
1014+
bind h Left { Resize "Increase Left"; }
1015+
bind j Down { Resize "Increase Down"; }
1016+
bind k Up { Resize "Increase Up"; }
1017+
bind l Right { Resize "Increase Right"; }
1018+
bind H { Resize "Decrease Left"; }
1019+
bind J { Resize "Decrease Down"; }
1020+
bind K { Resize "Decrease Up"; }
1021+
bind L { Resize "Decrease Right"; }
1022+
bind "=" + { Resize Increase; }
1023+
bind - { Resize Decrease; }
1024+
}
1025+
}
1026+
// Plugin aliases - can be used to change the implementation of Zellij
1027+
// changing these requires a restart to take effect
1028+
plugins {
1029+
tab-bar location=zellij:tab-bar
1030+
status-bar location=zellij:status-bar
1031+
welcome-screen location=zellij:session-manager {
1032+
welcome_screen #true
1033+
}
1034+
filepicker location=zellij:strider {
1035+
cwd "/"
1036+
}
1037+
}
1038+
mouse_mode #false
1039+
mirror_session #true
1040+
"##;
1041+
pretty_assertions::assert_eq!(KdlDocument::v1_to_v2(original)?, expected);
1042+
Ok(())
1043+
}
9491044
}

src/entry.rs

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,13 +165,76 @@ impl KdlEntry {
165165
pub fn autoformat(&mut self) {
166166
// TODO once MSRV allows:
167167
//self.format.take_if(|f| !f.autoformat_keep);
168+
let value_repr = self.format.as_ref().map(|x| {
169+
match &self.value {
170+
KdlValue::String(val) => {
171+
// cleanup. I don't _think_ this should have any whitespace,
172+
// but just in case.
173+
let s = x.value_repr.trim();
174+
// convert raw strings to new format
175+
let s = s.strip_prefix("r").unwrap_or(s);
176+
let s = if crate::value::is_plain_ident(val) {
177+
val.to_string()
178+
} else if s
179+
.find(|c| v2_parser::NEWLINES.iter().any(|nl| nl.contains(c)))
180+
.is_some()
181+
{
182+
// Multiline string. Need triple quotes if they're not there already.
183+
if s.contains("\"\"\"") {
184+
// We're probably good. This could be more precise, but close enough.
185+
s.to_string()
186+
} else {
187+
// `"` -> `"""` but also extra newlines need to be
188+
// added because v2 strips the first and last ones.
189+
let s = s.replacen("\"", "\"\"\"\n", 1);
190+
s.chars()
191+
.rev()
192+
.collect::<String>()
193+
.replacen("\"", "\"\"\"\n", 1)
194+
.chars()
195+
.rev()
196+
.collect::<String>()
197+
}
198+
} else if !s.starts_with("#") {
199+
// `/` is no longer an escaped char in v2.
200+
s.replace("\\/", "/")
201+
} else {
202+
// We're all good! Let's move on.
203+
s.to_string()
204+
};
205+
s
206+
}
207+
// These have `#` prefixes now. The regular Display impl will
208+
// take care of that.
209+
KdlValue::Bool(_) | KdlValue::Null => format!("{}", self.value),
210+
// These should be fine as-is?
211+
KdlValue::Integer(_) | KdlValue::Float(_) => x.value_repr.clone(),
212+
}
213+
});
214+
168215
if !self
169216
.format
170217
.as_ref()
171218
.map(|f| f.autoformat_keep)
172219
.unwrap_or(false)
173220
{
174-
self.format = None
221+
self.format = None;
222+
}
223+
224+
if let Some(value_repr) = value_repr.as_ref() {
225+
self.format = Some(
226+
self.format
227+
.clone()
228+
.map(|mut x| {
229+
x.value_repr = value_repr.into();
230+
x
231+
})
232+
.unwrap_or_else(|| KdlEntryFormat {
233+
value_repr: value_repr.into(),
234+
leading: " ".into(),
235+
..Default::default()
236+
}),
237+
)
175238
}
176239

177240
if let Some(name) = &mut self.name {
@@ -455,4 +518,49 @@ mod test {
455518
let entry = KdlEntry::new_prop("name", KdlValue::Integer(42));
456519
assert_eq!(format!("{}", entry), "name=42");
457520
}
521+
522+
#[cfg(feature = "v1")]
523+
#[test]
524+
fn v1_to_v2_format() -> miette::Result<()> {
525+
let mut entry = KdlEntry::parse_v1(r##"r#"hello, world!"#"##)?;
526+
entry.autoformat();
527+
assert_eq!(format!("{}", entry), r##" #"hello, world!"#"##);
528+
529+
let mut entry = KdlEntry::parse_v1(r#""hello, \" world!""#)?;
530+
entry.autoformat();
531+
assert_eq!(format!("{}", entry), r#" "hello, \" world!""#);
532+
533+
let mut entry = KdlEntry::parse_v1("\"foo!`~.,<>\"")?;
534+
entry.autoformat();
535+
assert_eq!(format!("{}", entry), " foo!`~.,<>");
536+
537+
let mut entry = KdlEntry::parse_v1("\"\nhello, world!\"")?;
538+
entry.autoformat();
539+
assert_eq!(format!("{}", entry), " \"\"\"\n\nhello, world!\n\"\"\"");
540+
541+
let mut entry = KdlEntry::parse_v1("r#\"\nhello, world!\"#")?;
542+
entry.autoformat();
543+
assert_eq!(format!("{}", entry), " #\"\"\"\n\nhello, world!\n\"\"\"#");
544+
545+
let mut entry = KdlEntry::parse_v1("true")?;
546+
entry.autoformat();
547+
assert_eq!(format!("{}", entry), " #true");
548+
549+
let mut entry = KdlEntry::parse_v1("false")?;
550+
entry.autoformat();
551+
assert_eq!(format!("{}", entry), " #false");
552+
553+
let mut entry = KdlEntry::parse_v1("null")?;
554+
entry.autoformat();
555+
assert_eq!(format!("{}", entry), " #null");
556+
557+
let mut entry = KdlEntry::parse_v1("1_234_567")?;
558+
entry.autoformat();
559+
assert_eq!(format!("{}", entry), " 1_234_567");
560+
561+
let mut entry = KdlEntry::parse_v1("1_234_567E-10")?;
562+
entry.autoformat();
563+
assert_eq!(format!("{}", entry), " 1_234_567E-10");
564+
Ok(())
565+
}
458566
}

src/lib.rs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,20 @@
1010
//! You can think of this crate as
1111
//! [`toml_edit`](https://crates.io/crates/toml_edit), but for KDL.
1212
//!
13-
//! This crate supports parsing [the final KDL 2.0.0
14-
//! draft](https://github.com/kdl-org/kdl/pull/434), which might get a couple
15-
//! more small modifications before things are truly finalized. It does not
16-
//! support KDL 1.0 as of this release, but versions of this crate lower than
17-
//! 5.0 do.
13+
//! This crate supports both KDL v2.0.0 and v1.0.0 (when using the non-default
14+
//! `v1` feature).
15+
//!
16+
//! There is also a `v1-fallback` feature that may be enabled in order to have
17+
//! the various `Kdl*::parse` methods try to parse their input as v2, and, if
18+
//! that fails, try again as v1. In either case, a dedicated `Kdl*::parse_v1`
19+
//! method is available for v1-exclusive parsing, as long as either `v1` or
20+
//! `v1-fallback` are enabled.
21+
//!
22+
//! Autoformatting a document parsed from a v1 doc will translate the document
23+
//! to v2 format, preserving as much of the v1 trivia as possible (comments,
24+
//! etc). It *should* generate a fully valid v2 document, but there may still be
25+
//! some corner cases that don't translate well. Please file issues as needed
26+
//! for those.
1827
//!
1928
//! ## Example
2029
//!

src/v2_parser.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1578,7 +1578,7 @@ fn escline_test() {
15781578
assert_eq!(node.entries().len(), 2);
15791579
}
15801580

1581-
static NEWLINES: [&str; 7] = [
1581+
pub(crate) static NEWLINES: [&str; 7] = [
15821582
"\u{000D}\u{000A}",
15831583
"\u{000D}",
15841584
"\u{000A}",

src/value.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ impl Display for KdlValue {
174174
}
175175
}
176176

177-
fn is_plain_ident(ident: &str) -> bool {
177+
pub(crate) fn is_plain_ident(ident: &str) -> bool {
178178
let ident_bytes = ident.as_bytes();
179179
ident
180180
.find(crate::v2_parser::is_disallowed_ident_char)

0 commit comments

Comments
 (0)