mirror of https://github.com/kdl-org/kdl-rs.git
feat(fmt): Configurable autoformat with `FormatConfig` (#95)
Fixes: https://github.com/kdl-org/kdl-rs/issues/85
This commit is contained in:
parent
876a4276bd
commit
014c7c57a2
103
src/document.rs
103
src/document.rs
|
|
@ -2,7 +2,7 @@
|
|||
use miette::SourceSpan;
|
||||
use std::fmt::Display;
|
||||
|
||||
use crate::{KdlNode, KdlParseFailure, KdlValue};
|
||||
use crate::{FormatConfig, KdlNode, KdlParseFailure, KdlValue};
|
||||
|
||||
/// Represents a KDL
|
||||
/// [`Document`](https://github.com/kdl-org/kdl/blob/main/SPEC.md#document).
|
||||
|
|
@ -232,12 +232,33 @@ impl KdlDocument {
|
|||
/// Auto-formats this Document, making everything nice while preserving
|
||||
/// comments.
|
||||
pub fn autoformat(&mut self) {
|
||||
self.autoformat_impl(0, false);
|
||||
self.autoformat_config(&FormatConfig::default());
|
||||
}
|
||||
|
||||
/// Formats the document and removes all comments from the document.
|
||||
pub fn autoformat_no_comments(&mut self) {
|
||||
self.autoformat_impl(0, true);
|
||||
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, .. }) = (&mut *self).format_mut() {
|
||||
crate::fmt::autoformat_leading(leading, config);
|
||||
}
|
||||
let mut has_nodes = false;
|
||||
for node in &mut (&mut *self).nodes {
|
||||
has_nodes = true;
|
||||
node.autoformat_config(config);
|
||||
}
|
||||
if let Some(KdlDocumentFormat { trailing, .. }) = (&mut *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,
|
||||
|
|
@ -326,23 +347,6 @@ impl Display for KdlDocument {
|
|||
}
|
||||
|
||||
impl KdlDocument {
|
||||
pub(crate) fn autoformat_impl(&mut self, indent: usize, no_comments: bool) {
|
||||
if let Some(KdlDocumentFormat { leading, .. }) = self.format_mut() {
|
||||
crate::fmt::autoformat_leading(leading, indent, no_comments);
|
||||
}
|
||||
let mut has_nodes = false;
|
||||
for node in &mut self.nodes {
|
||||
has_nodes = true;
|
||||
node.autoformat_impl(indent, no_comments);
|
||||
}
|
||||
if let Some(KdlDocumentFormat { trailing, .. }) = self.format_mut() {
|
||||
crate::fmt::autoformat_trailing(trailing, no_comments);
|
||||
if !has_nodes {
|
||||
trailing.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn stringify(
|
||||
&self,
|
||||
f: &mut std::fmt::Formatter<'_>,
|
||||
|
|
@ -648,6 +652,65 @@ foo 1 bar=0xdeadbeef {
|
|||
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() {
|
||||
|
|
|
|||
143
src/fmt.rs
143
src/fmt.rs
|
|
@ -1,19 +1,131 @@
|
|||
use std::fmt::Write as _;
|
||||
|
||||
pub(crate) fn autoformat_leading(leading: &mut String, indent: usize, no_comments: bool) {
|
||||
/// Formatting configuration for use with [`KdlDocument::autoformat_config`](`crate::KdlDocument::autoformat_config`)
|
||||
/// and [`KdlNode::autoformat_config`](`crate::KdlNode::autoformat_config`).
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug)]
|
||||
pub struct FormatConfig<'a> {
|
||||
/// How deeply to indent the overall node or document,
|
||||
/// in repetitions of [`indent`](`FormatConfig::indent`).
|
||||
/// Defaults to `0`.
|
||||
pub indent_level: usize,
|
||||
|
||||
/// The indentation to use at each level. Defaults to four spaces.
|
||||
pub indent: &'a str,
|
||||
|
||||
/// Whether to remove comments. Defaults to `false`.
|
||||
pub no_comments: bool,
|
||||
}
|
||||
|
||||
/// See field documentation for defaults.
|
||||
impl Default for FormatConfig<'_> {
|
||||
fn default() -> Self {
|
||||
Self::builder().build()
|
||||
}
|
||||
}
|
||||
|
||||
impl FormatConfig<'_> {
|
||||
/// Creates a new [`FormatConfigBuilder`] with default configuration.
|
||||
pub const fn builder() -> FormatConfigBuilder<'static> {
|
||||
FormatConfigBuilder::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`FormatConfig`] builder.
|
||||
///
|
||||
/// Note that setters can be repeated.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct FormatConfigBuilder<'a>(FormatConfig<'a>);
|
||||
|
||||
impl<'a> FormatConfigBuilder<'a> {
|
||||
/// Creates a new [`FormatConfig`] builder with default configuration.
|
||||
pub const fn new() -> Self {
|
||||
FormatConfigBuilder(FormatConfig {
|
||||
indent_level: 0,
|
||||
indent: " ",
|
||||
no_comments: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// How deeply to indent the overall node or document,
|
||||
/// in repetitions of [`indent`](`FormatConfig::indent`).
|
||||
/// Defaults to `0` iff not specified.
|
||||
pub const fn maybe_indent_level(mut self, indent_level: Option<usize>) -> Self {
|
||||
if let Some(indent_level) = indent_level {
|
||||
self.0.indent_level = indent_level;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// How deeply to indent the overall node or document,
|
||||
/// in repetitions of [`indent`](`FormatConfig::indent`).
|
||||
/// Defaults to `0` iff not specified.
|
||||
pub const fn indent_level(mut self, indent_level: usize) -> Self {
|
||||
self.0.indent_level = indent_level;
|
||||
self
|
||||
}
|
||||
|
||||
/// The indentation to use at each level.
|
||||
/// Defaults to four spaces iff not specified.
|
||||
pub const fn maybe_indent<'b, 'c>(self, indent: Option<&'b str>) -> FormatConfigBuilder<'c>
|
||||
where
|
||||
'a: 'b,
|
||||
'b: 'c,
|
||||
{
|
||||
if let Some(indent) = indent {
|
||||
self.indent(indent)
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// The indentation to use at each level.
|
||||
/// Defaults to four spaces if not specified.
|
||||
pub const fn indent<'b>(self, indent: &'b str) -> FormatConfigBuilder<'b> {
|
||||
FormatConfigBuilder(FormatConfig { indent, ..self.0 })
|
||||
}
|
||||
|
||||
/// Whether to remove comments.
|
||||
/// Defaults to `false` iff not specified.
|
||||
pub const fn maybe_no_comments(mut self, no_comments: Option<bool>) -> Self {
|
||||
if let Some(no_comments) = no_comments {
|
||||
self.0.no_comments = no_comments;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Whether to remove comments.
|
||||
/// Defaults to `false` iff not specified.
|
||||
pub const fn no_comments(mut self, no_comments: bool) -> Self {
|
||||
self.0.no_comments = no_comments;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builds the [`FormatConfig`].
|
||||
pub const fn build(self) -> FormatConfig<'a> {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn autoformat_leading(leading: &mut String, config: &FormatConfig<'_>) {
|
||||
let mut result = String::new();
|
||||
if !no_comments {
|
||||
if !config.no_comments {
|
||||
let input = leading.trim();
|
||||
if !input.is_empty() {
|
||||
for line in input.lines() {
|
||||
let trimmed = line.trim();
|
||||
if !trimmed.is_empty() {
|
||||
writeln!(result, "{:indent$}{}", "", trimmed, indent = indent).unwrap();
|
||||
for _ in 0..config.indent_level {
|
||||
result.push_str(config.indent);
|
||||
}
|
||||
writeln!(result, "{}", trimmed).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
write!(result, "{:indent$}", "", indent = indent).unwrap();
|
||||
for _ in 0..config.indent_level {
|
||||
result.push_str(config.indent);
|
||||
}
|
||||
*leading = result;
|
||||
}
|
||||
|
||||
|
|
@ -33,3 +145,26 @@ pub(crate) fn autoformat_trailing(decor: &mut String, no_comments: bool) {
|
|||
}
|
||||
*decor = result;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn builder() -> miette::Result<()> {
|
||||
let built = FormatConfig::builder()
|
||||
.indent_level(12)
|
||||
.indent(" \t")
|
||||
.no_comments(true)
|
||||
.build();
|
||||
assert!(matches!(
|
||||
built,
|
||||
FormatConfig {
|
||||
indent_level: 12,
|
||||
indent: " \t",
|
||||
no_comments: true,
|
||||
}
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,6 +175,7 @@
|
|||
pub use document::*;
|
||||
pub use entry::*;
|
||||
pub use error::*;
|
||||
pub use fmt::*;
|
||||
pub use identifier::*;
|
||||
pub use node::*;
|
||||
// pub use query::*;
|
||||
|
|
|
|||
112
src/node.rs
112
src/node.rs
|
|
@ -8,11 +8,10 @@ use std::{
|
|||
use miette::SourceSpan;
|
||||
|
||||
use crate::{
|
||||
v2_parser, KdlDocument, KdlDocumentFormat, KdlEntry, KdlIdentifier, KdlParseFailure, KdlValue,
|
||||
v2_parser, FormatConfig, KdlDocument, KdlDocumentFormat, KdlEntry, KdlIdentifier,
|
||||
KdlParseFailure, KdlValue,
|
||||
};
|
||||
|
||||
static INDENT: usize = 4;
|
||||
|
||||
/// Represents an individual KDL
|
||||
/// [`Node`](https://github.com/kdl-org/kdl/blob/main/SPEC.md#node) inside a
|
||||
/// KDL Document.
|
||||
|
|
@ -388,12 +387,68 @@ impl KdlNode {
|
|||
}
|
||||
/// Auto-formats this node and its contents.
|
||||
pub fn autoformat(&mut self) {
|
||||
self.autoformat_impl(0, false);
|
||||
self.autoformat_config(&FormatConfig::default());
|
||||
}
|
||||
|
||||
/// Auto-formats this node and its contents, stripping comments.
|
||||
pub fn autoformat_no_comments(&mut self) {
|
||||
self.autoformat_impl(0, true);
|
||||
self.autoformat_config(&FormatConfig {
|
||||
no_comments: true,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
/// Auto-formats this node and its contents according to `config`.
|
||||
pub fn autoformat_config(&mut self, config: &FormatConfig<'_>) {
|
||||
if let Some(KdlNodeFormat {
|
||||
leading,
|
||||
before_terminator,
|
||||
terminator,
|
||||
trailing,
|
||||
before_children,
|
||||
..
|
||||
}) = self.format_mut()
|
||||
{
|
||||
crate::fmt::autoformat_leading(leading, config);
|
||||
crate::fmt::autoformat_trailing(before_terminator, config.no_comments);
|
||||
crate::fmt::autoformat_trailing(trailing, config.no_comments);
|
||||
*trailing = trailing.trim().into();
|
||||
if !terminator.starts_with('\n') {
|
||||
*terminator = "\n".into();
|
||||
}
|
||||
if let Some(c) = trailing.chars().next() {
|
||||
if !c.is_whitespace() {
|
||||
trailing.insert(0, ' ');
|
||||
}
|
||||
}
|
||||
|
||||
*before_children = " ".into();
|
||||
} else {
|
||||
self.set_format(KdlNodeFormat {
|
||||
terminator: "\n".into(),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
self.name.clear_format();
|
||||
if let Some(ty) = self.ty.as_mut() {
|
||||
ty.clear_format()
|
||||
}
|
||||
for entry in &mut self.entries {
|
||||
entry.autoformat();
|
||||
}
|
||||
if let Some(children) = self.children.as_mut() {
|
||||
children.autoformat_config(&FormatConfig {
|
||||
indent_level: config.indent_level + 1,
|
||||
..*config
|
||||
});
|
||||
if let Some(KdlDocumentFormat { leading, trailing }) = children.format_mut() {
|
||||
*leading = leading.trim().into();
|
||||
leading.push('\n');
|
||||
for _ in 0..config.indent_level {
|
||||
trailing.push_str(config.indent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(@zkat): These should all be moved into the query module, instead
|
||||
|
|
@ -512,53 +567,6 @@ impl Display for KdlNode {
|
|||
}
|
||||
|
||||
impl KdlNode {
|
||||
pub(crate) fn autoformat_impl(&mut self, indent: usize, no_comments: bool) {
|
||||
if let Some(KdlNodeFormat {
|
||||
leading,
|
||||
before_terminator,
|
||||
terminator,
|
||||
trailing,
|
||||
before_children,
|
||||
..
|
||||
}) = self.format_mut()
|
||||
{
|
||||
crate::fmt::autoformat_leading(leading, indent, no_comments);
|
||||
crate::fmt::autoformat_trailing(before_terminator, no_comments);
|
||||
crate::fmt::autoformat_trailing(trailing, no_comments);
|
||||
*trailing = trailing.trim().into();
|
||||
if !terminator.starts_with('\n') {
|
||||
*terminator = "\n".into();
|
||||
}
|
||||
if let Some(c) = trailing.chars().next() {
|
||||
if !c.is_whitespace() {
|
||||
trailing.insert(0, ' ');
|
||||
}
|
||||
}
|
||||
|
||||
*before_children = " ".into();
|
||||
} else {
|
||||
self.set_format(KdlNodeFormat {
|
||||
terminator: "\n".into(),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
self.name.clear_format();
|
||||
if let Some(ty) = self.ty.as_mut() {
|
||||
ty.clear_format()
|
||||
}
|
||||
for entry in &mut self.entries {
|
||||
entry.autoformat();
|
||||
}
|
||||
if let Some(children) = self.children.as_mut() {
|
||||
children.autoformat_impl(indent + INDENT, no_comments);
|
||||
if let Some(KdlDocumentFormat { leading, trailing }) = children.format_mut() {
|
||||
*leading = leading.trim().into();
|
||||
leading.push('\n');
|
||||
trailing.push_str(format!("{:indent$}", "", indent = indent).as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn stringify(
|
||||
&self,
|
||||
f: &mut std::fmt::Formatter<'_>,
|
||||
|
|
|
|||
Loading…
Reference in New Issue