feat(v1): add v2 -> v1 translation and fix translations to not autoformat

This commit is contained in:
Kat Marchán 2024-12-21 10:43:52 -08:00
parent ec73cdfa05
commit b332eed4a0
No known key found for this signature in database
GPG Key ID: AEB529C08A3C7E9E
6 changed files with 395 additions and 75 deletions

View File

@ -43,4 +43,4 @@ jobs:
- name: Clippy
run: cargo clippy --all -- -D warnings
- name: Run tests
run: cargo test --all --verbose
run: cargo test --features span --features --v1 --all --verbose

View File

@ -12,7 +12,7 @@ rust-version = "1.70.0"
edition = "2021"
[features]
default = ["span"]
default = ["span", "v1"]
span = []
v1-fallback = ["v1"]
v1 = ["kdlv1"]

View File

@ -2,7 +2,7 @@
use miette::SourceSpan;
use std::fmt::Display;
use crate::{FormatConfig, KdlError, KdlNode, KdlValue};
use crate::{FormatConfig, KdlError, KdlNode, KdlNodeFormat, KdlValue};
/// Represents a KDL
/// [`Document`](https://github.com/kdl-org/kdl/blob/main/SPEC.md#document).
@ -344,15 +344,20 @@ impl KdlDocument {
pub fn parse(s: &str) -> Result<Self, KdlError> {
#[cfg(not(feature = "v1-fallback"))]
{
crate::v2_parser::try_parse(crate::v2_parser::document, s)
KdlDocument::parse_v2(s)
}
#[cfg(feature = "v1-fallback")]
{
crate::v2_parser::try_parse(crate::v2_parser::document, s)
.or_else(|e| KdlDocument::parse_v1(s).map_err(|_| e))
KdlDocument::parse_v2(s).or_else(|e| KdlDocument::parse_v1(s).map_err(|_| e))
}
}
/// Parses a KDL v2 string into a document.
#[cfg(feature = "v1")]
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> {
@ -365,9 +370,74 @@ impl KdlDocument {
#[cfg(feature = "v1")]
pub fn v1_to_v2(s: &str) -> Result<String, KdlError> {
let mut doc = KdlDocument::parse_v1(s)?;
doc.autoformat();
doc.ensure_v2();
Ok(doc.to_string())
}
/// Takes a KDL v2 document string and returns the same document, but
/// autoformatted into valid KDL v2 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.
#[cfg(feature = "v1")]
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")]
@ -956,10 +1026,95 @@ inline { time; to; live "our" "dreams"; "y;all" }
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<()> {
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");
Ok(())
}
#[cfg(feature = "v1")]
#[test]
fn v2_to_v1() -> 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 {

View File

@ -171,9 +171,8 @@ impl KdlEntry {
/// Auto-formats this entry.
pub fn autoformat(&mut self) {
// TODO once MSRV allows:
// TODO once MSRV allows (1.80.0):
//self.format.take_if(|f| !f.autoformat_keep);
let old_fmt = self.format.clone();
if !self
.format
.as_ref()
@ -182,68 +181,13 @@ impl KdlEntry {
{
self.format = None;
} else {
let value_repr = old_fmt.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,
}
#[cfg(feature = "v1")]
self.ensure_v2();
self.format = self.format.take().map(|f| KdlEntryFormat {
value_repr: f.value_repr,
leading: f.leading,
..Default::default()
});
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 {
@ -275,6 +219,168 @@ impl KdlEntry {
let ret: Result<kdlv1::KdlEntry, kdlv1::KdlError> = s.parse();
ret.map(|x| x.into()).map_err(|e| e.into())
}
/// Makes sure this entry is in v2 format.
#[cfg(feature = "v1")]
pub fn ensure_v2(&mut self) {
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.into()
} 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 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()
}),
)
}
}
/// Makes sure this entry is in v1 format.
#[cfg(feature = "v1")]
pub fn ensure_v1(&mut self) {
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 v1 format
let s = if s.starts_with("#") {
format!("r{s}")
} else {
s.to_string()
};
let s = if crate::value::is_plain_ident(val)
&& !s.starts_with('\"')
&& !s.starts_with("r#")
{
format!("\"{val}\"")
} else if s
.find(|c| v2_parser::NEWLINES.iter().any(|nl| nl.contains(c)))
.is_some()
{
// Multiline string. Let's make sure it's v1.
if s.contains("\"\"\"") {
let prefix = s
.chars()
.rev()
.skip_while(|c| c == &'"')
.take_while(|c| {
v2_parser::NEWLINES.iter().any(|nl| nl.contains(*c))
})
.collect::<String>();
let prefix = prefix.chars().rev().collect::<String>();
// Sigh. Yeah. I didn't promise this would be _efficient_.
let mut s = s;
for nl in v2_parser::NEWLINES {
s = s.replace(&format!("{nl}{prefix}"), nl);
}
// And now we strips the beginning and ending newlines.
// Finally, replace `"""` with `"`.
s
} else {
// It's already a v1 string
s
}
} else if !s.starts_with("r#") {
// `/` is an escaped char in v2
let s = s.replace("\\/", "/"); // Maneuvering. Will fix in a sec.
s.replace('/', "\\/")
} else {
// We're all good! Let's move on.
s.to_string()
};
s
}
// No more # prefix for these
KdlValue::Bool(b) => b.to_string(),
KdlValue::Null => "null".to_string(),
// These should be fine as-is?
KdlValue::Integer(_) | KdlValue::Float(_) => x.value_repr.clone(),
}
});
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()
}),
)
} else {
let v1_val = match self.value() {
KdlValue::String(s) => kdlv1::KdlValue::String(s.clone()),
KdlValue::Integer(i) => kdlv1::KdlValue::Base10(*i as i64),
KdlValue::Float(f) => kdlv1::KdlValue::Base10Float(*f),
KdlValue::Bool(b) => kdlv1::KdlValue::Bool(*b),
KdlValue::Null => kdlv1::KdlValue::Null,
};
self.format = Some(KdlEntryFormat {
value_repr: v1_val.to_string(),
leading: " ".into(),
..Default::default()
})
}
}
}
#[cfg(feature = "v1")]

View File

@ -121,7 +121,7 @@ impl From<kdlv1::KdlIdentifier> for KdlIdentifier {
value: value.value().into(),
repr: value.repr().map(|x| x.into()),
#[cfg(feature = "span")]
span: SourceSpan::new(value.span().offset().into(), value.span().len()),
span: (value.span().offset(), value.span().len()).into(),
}
}
}

View File

@ -353,11 +353,70 @@ impl KdlNode {
let ret: Result<kdlv1::KdlNode, kdlv1::KdlError> = s.parse();
ret.map(|x| x.into()).map_err(|e| e.into())
}
/// Makes sure this node is in v2 format.
#[cfg(feature = "v1")]
pub fn ensure_v2(&mut self) {
self.ty = self.ty.take().map(|ty| ty.value().into());
let v2_name: KdlIdentifier = self.name.value().into();
self.name = v2_name;
for entry in self.iter_mut() {
entry.ensure_v2();
}
self.children = self.children.take().map(|mut doc| {
doc.ensure_v2();
doc
});
}
/// Makes sure this node is in v1 format.
#[cfg(feature = "v1")]
pub fn ensure_v1(&mut self) {
self.ty = self.ty.take().map(|ty| {
let v1_name: kdlv1::KdlIdentifier = ty.value().into();
v1_name.into()
});
let v1_name: kdlv1::KdlIdentifier = self.name.value().into();
self.name = v1_name.into();
for entry in self.iter_mut() {
entry.ensure_v1();
}
self.children = self.children.take().map(|mut children| {
children.ensure_v1();
children
});
}
}
#[cfg(feature = "v1")]
impl From<kdlv1::KdlNode> for KdlNode {
fn from(value: kdlv1::KdlNode) -> Self {
let terminator = value
.trailing()
.map(|t| if t.contains(";") { ";" } else { "\n" })
.unwrap_or("\n");
let trailing = value.trailing().map(|t| {
if t.contains(";") {
t.replace(';', "")
} else {
let t = t.replace("\r\n", "\n");
let t = t
.chars()
.map(|c| {
if v2_parser::NEWLINES.iter().any(|nl| nl.contains(c)) {
'\n'
} else {
c
}
})
.collect::<String>();
if terminator == ";" {
t
} else {
t.replacen('\n', "", 1)
}
}
});
KdlNode {
ty: value.ty().map(|x| x.clone().into()),
name: value.name().clone().into(),
@ -370,8 +429,8 @@ impl From<kdlv1::KdlNode> for KdlNode {
after_ty: "".into(),
before_children: value.before_children().unwrap_or("").into(),
before_terminator: "".into(),
terminator: "".into(),
trailing: value.trailing().unwrap_or("").into(),
terminator: terminator.into(),
trailing: trailing.unwrap_or_else(|| "".into()),
}),
#[cfg(feature = "span")]
span: SourceSpan::new(value.span().offset().into(), value.span().len()),