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 miette::SourceSpan;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
use crate::{KdlNode, KdlParseFailure, KdlValue};
|
use crate::{FormatConfig, KdlNode, KdlParseFailure, KdlValue};
|
||||||
|
|
||||||
/// Represents a KDL
|
/// Represents a KDL
|
||||||
/// [`Document`](https://github.com/kdl-org/kdl/blob/main/SPEC.md#document).
|
/// [`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
|
/// Auto-formats this Document, making everything nice while preserving
|
||||||
/// comments.
|
/// comments.
|
||||||
pub fn autoformat(&mut self) {
|
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.
|
/// Formats the document and removes all comments from the document.
|
||||||
pub fn autoformat_no_comments(&mut self) {
|
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,
|
// TODO(@zkat): These should all be moved into the query module itself,
|
||||||
|
|
@ -326,23 +347,6 @@ impl Display for KdlDocument {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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(
|
pub(crate) fn stringify(
|
||||||
&self,
|
&self,
|
||||||
f: &mut std::fmt::Formatter<'_>,
|
f: &mut std::fmt::Formatter<'_>,
|
||||||
|
|
@ -648,6 +652,65 @@ foo 1 bar=0xdeadbeef {
|
||||||
Ok(())
|
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")]
|
#[cfg(feature = "span")]
|
||||||
fn check_spans_for_doc(doc: &KdlDocument, source: &impl miette::SourceCode) {
|
fn check_spans_for_doc(doc: &KdlDocument, source: &impl miette::SourceCode) {
|
||||||
for node in doc.nodes() {
|
for node in doc.nodes() {
|
||||||
|
|
|
||||||
143
src/fmt.rs
143
src/fmt.rs
|
|
@ -1,19 +1,131 @@
|
||||||
use std::fmt::Write as _;
|
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();
|
let mut result = String::new();
|
||||||
if !no_comments {
|
if !config.no_comments {
|
||||||
let input = leading.trim();
|
let input = leading.trim();
|
||||||
if !input.is_empty() {
|
if !input.is_empty() {
|
||||||
for line in input.lines() {
|
for line in input.lines() {
|
||||||
let trimmed = line.trim();
|
let trimmed = line.trim();
|
||||||
if !trimmed.is_empty() {
|
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;
|
*leading = result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,3 +145,26 @@ pub(crate) fn autoformat_trailing(decor: &mut String, no_comments: bool) {
|
||||||
}
|
}
|
||||||
*decor = result;
|
*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 document::*;
|
||||||
pub use entry::*;
|
pub use entry::*;
|
||||||
pub use error::*;
|
pub use error::*;
|
||||||
|
pub use fmt::*;
|
||||||
pub use identifier::*;
|
pub use identifier::*;
|
||||||
pub use node::*;
|
pub use node::*;
|
||||||
// pub use query::*;
|
// pub use query::*;
|
||||||
|
|
|
||||||
112
src/node.rs
112
src/node.rs
|
|
@ -8,11 +8,10 @@ use std::{
|
||||||
use miette::SourceSpan;
|
use miette::SourceSpan;
|
||||||
|
|
||||||
use crate::{
|
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
|
/// Represents an individual KDL
|
||||||
/// [`Node`](https://github.com/kdl-org/kdl/blob/main/SPEC.md#node) inside a
|
/// [`Node`](https://github.com/kdl-org/kdl/blob/main/SPEC.md#node) inside a
|
||||||
/// KDL Document.
|
/// KDL Document.
|
||||||
|
|
@ -388,12 +387,68 @@ impl KdlNode {
|
||||||
}
|
}
|
||||||
/// Auto-formats this node and its contents.
|
/// Auto-formats this node and its contents.
|
||||||
pub fn autoformat(&mut self) {
|
pub fn autoformat(&mut self) {
|
||||||
self.autoformat_impl(0, false);
|
self.autoformat_config(&FormatConfig::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Auto-formats this node and its contents, stripping comments.
|
/// Auto-formats this node and its contents, stripping comments.
|
||||||
pub fn autoformat_no_comments(&mut self) {
|
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
|
// TODO(@zkat): These should all be moved into the query module, instead
|
||||||
|
|
@ -512,53 +567,6 @@ impl Display for KdlNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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(
|
pub(crate) fn stringify(
|
||||||
&self,
|
&self,
|
||||||
f: &mut std::fmt::Formatter<'_>,
|
f: &mut std::fmt::Formatter<'_>,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue