feat(parser): v2 parser (#70)

This is a major rewrite of kdl-rs to comply with the KDL v2 spec.
This commit is contained in:
Kat Marchán 2024-10-05 20:51:09 -07:00 committed by GitHub
parent 6044ef9776
commit c9898cdbd2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
290 changed files with 2960 additions and 1290 deletions

View File

@ -28,7 +28,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
rust: [1.56.0, stable]
rust: [1.70.0, stable]
os: [ubuntu-latest, macOS-latest, windows-latest]
steps:

View File

@ -16,10 +16,11 @@ default = ["span"]
span = []
[dependencies]
miette = "5.7.0"
miette = "7.2.0"
nom = "7.1.1"
thiserror = "1.0.40"
winnow = { version = "0.6.20", features = ["alloc", "unstable-recover"] }
[dev-dependencies]
miette = { version = "5.7.0", features = ["fancy"] }
miette = { version = "7.2.0", features = ["fancy"] }
pretty_assertions = "1.3.0"

View File

@ -66,38 +66,6 @@ assert_eq!(&doc.to_string(), node_str);
[`KdlDocument`], [`KdlNode`], [`KdlEntry`], and [`KdlIdentifier`] can all
be parsed and managed this way.
#### Query Engine
`kdl` includes a query engine for
[KQL](https://github.com/kdl-org/kdl/blob/main/QUERY-SPEC.md), which lets you
pick out nodes from a document using a CSS Selectors-style syntax.
Queries can be done from either a [`KdlDocument`] or a [`KdlNode`], with
mostly the same semantics.
```rust
use kdl::KdlDocument;
let doc = r#"
a {
b 1
c 2
d 3 {
e prop="hello"
}
}
"#.parse::<KdlDocument>().expect("failed to parse KDL");
let results = doc.query("a > b").expect("failed to parse query");
assert_eq!(results, Some(&doc.nodes()[0].children().unwrap().nodes()[0]));
let results = doc.query_get("e", "prop").expect("failed to parse query");
assert_eq!(results, Some(&"hello".into()));
let results = doc.query_get_all("a > []", 0).expect("failed to parse query").collect::<Vec<_>>();
assert_eq!(results, vec![&1.into(), &2.into(), &3.into()]);
```
### Error Reporting
[`KdlError`] implements [`miette::Diagnostic`] and can be used to display
@ -154,6 +122,10 @@ means a few things:
you [`KdlDocument::fmt`] in which case the original representation will be
thrown away and the actual value will be used when serializing.
### Minimum Supported Rust Version
You must be at least `1.70.0` tall to get on this ride.
### License
The code in this repository is covered by [the Apache-2.0

View File

@ -1,9 +1,9 @@
package {
name "kdl"
name kdl
version "0.0.0"
description "kat's document language"
description "The kdl document language"
authors "Kat Marchán <kzm@zkat.tech>"
license-file "LICENSE.md"
license-file LICENSE.md
edition "2018"
}

View File

@ -1,47 +1,52 @@
// This example is a GitHub Action if it used KDL syntax.
// See .github/workflows/ci.yml for the file this was based on.
name "CI"
name CI
on "push" "pull_request"
on push pull_request
env {
RUSTFLAGS "-Dwarnings"
RUSTFLAGS -Dwarnings
}
jobs {
fmt_and_docs "Check fmt & build docs" {
runs-on "ubuntu-latest"
runs-on ubuntu-latest
steps {
step uses="actions/checkout@v1"
step "Install Rust" uses="actions-rs/toolchain@v1" {
profile "minimal"
toolchain "stable"
components "rustfmt"
override true
profile minimal
toolchain stable
components rustfmt
override #true
}
step "rustfmt" run="cargo fmt --all -- --check"
step "docs" run="cargo doc --no-deps"
step rustfmt { run cargo fmt --all -- --check }
step docs { run cargo doc --no-deps }
}
}
build_and_test "Build & Test" {
runs-on "${{ matrix.os }}"
strategy {
matrix {
rust "1.46.0" "stable"
os "ubuntu-latest" "macOS-latest" "windows-latest"
rust "1.46.0" stable
os ubuntu-latest macOS-latest windows-latest
}
}
steps {
step uses="actions/checkout@v1"
step "Install Rust" uses="actions-rs/toolchain@v1" {
profile "minimal"
profile minimal
toolchain "${{ matrix.rust }}"
components "clippy"
override true
components clippy
override #true
}
step "Clippy" run="cargo clippy --all -- -D warnings"
step "Run tests" run="cargo test --all --verbose"
step Clippy { run cargo clippy --all -- -D warnings }
step "Run tests" { run cargo test --all --verbose }
step "Other Stuff" run="
echo foo
echo bar
echo baz
"
}
}
}

View File

@ -1,47 +1,50 @@
/// Show how to build your own diagnostics, without having to use the
/// `fancy` feature or having `main()` return `miette::Result`
use kdl::KdlDocument;
use miette::Diagnostic;
use miette::SourceSpan;
// TODO(@zkat): Error stuff has changed, so this needs updating.
#[derive(Debug)]
pub struct MyError {
pub message: String,
}
// /// Show how to build your own diagnostics, without having to use the
// /// `fancy` feature or having `main()` return `miette::Result`
// ///
// use kdl::KdlDocument;
// use miette::Diagnostic;
// use miette::SourceSpan;
fn parse(input: &str) -> Result<KdlDocument, MyError> {
let doc = input.parse::<KdlDocument>();
doc.map_err(|error| {
let source = error
.source_code()
.expect("parse errors should have source code");
let help = error.help.unwrap_or_default();
let span: SourceSpan = error.span;
let contents = source
.read_span(&span, 0, 0)
.expect("source should have span contents");
// miette uses 0 based indexes, but humans prefer 1-based
let line = contents.line() + 1;
let column = contents.column() + 1;
let message = format!(
"line {}, column {}: {}\n help: {}",
line, column, error, help
);
MyError { message }
})
}
// #[derive(Debug)]
// pub struct MyError {
// pub message: String,
// }
// fn parse(input: &str) -> Result<KdlDocument, MyError> {
// let doc = input.parse::<KdlDocument>();
// doc.map_err(|error| {
// let source = error
// .source_code()
// .expect("parse errors should have source code");
// let help = error.help.unwrap_or_default();
// let span: SourceSpan = error.span;
// let contents = source
// .read_span(&span, 0, 0)
// .expect("source should have span contents");
// // miette uses 0 based indexes, but humans prefer 1-based
// let line = contents.line() + 1;
// let column = contents.column() + 1;
// let message = format!(
// "line {}, column {}: {}\n help: {}",
// line, column, error, help
// );
// MyError { message }
// })
// }
fn main() {
let input = r#"
foo {
bar {
baz 1.
}
}
"#;
let err = parse(input).unwrap_err();
eprintln!("{}", err.message);
// Output:
// line 4, column 14: Expected valid value.
// help: Floating point numbers must be base 10, and have numbers after the decimal point.
// let input = r#"
// foo {
// bar {
// baz 1.
// }
// }
// "#;
// let err = parse(input).unwrap_err();
// eprintln!("{}", err.message);
// // Output:
// // line 4, column 14: Expected valid value.
// // help: Floating point numbers must be base 10, and have numbers after the decimal point.
}

View File

@ -27,7 +27,7 @@ words {
let word_node = KdlNode::new(identifier);
word_nodes.push(word_node);
word_nodes.sort_by(sort_by_name);
words_section.fmt();
words_section.autoformat();
println!("{}", doc);

View File

@ -1,371 +1,374 @@
document {
info {
title "KDL Schema" lang="en"
description "KDL Schema KDL schema in KDL" lang="en"
title "KDL Schema" lang=en
description "KDL Schema KDL schema in KDL" lang=en
author "Kat Marchán" {
link "https://github.com/zkat" rel="self"
link "https://github.com/zkat" rel=self
}
contributor "Lars Willighagen" {
link "https://github.com/larsgw" rel="self"
link "https://github.com/larsgw" rel=self
}
link "https://github.com/zkat/kdl" rel="documentation"
license "Creative Commons Attribution-ShareAlike 4.0 International License" spdx="CC-BY-SA-4.0" {
link "https://creativecommons.org/licenses/by-sa/4.0/" lang="en"
link "https://github.com/zkat/kdl" rel=documentation
license "Creative Commons Attribution-ShareAlike 4.0 International License" spdx=CC-BY-SA-4.0 {
link "https://creativecommons.org/licenses/by-sa/4.0/" lang=en
}
published "2021-08-31"
modified "2021-09-01"
}
node "document" {
node document {
min 1
max 1
children id="node-children" {
node "node-names" id="node-names-node" description="Validations to apply specifically to arbitrary node names" {
children ref=r#"[id="validations"]"#
children id=node-children {
node node-names id=node-names-node description="Validations to apply specifically to arbitrary node names" {
children ref=#"[id="validations"]"#
}
node "other-nodes-allowed" id="other-nodes-allowed-node" description="Whether to allow child nodes other than the ones explicitly listed. Defaults to 'false'." {
node other-nodes-allowed id=other-nodes-allowed-node description="Whether to allow child nodes other than the ones explicitly listed. Defaults to '#false'." {
max 1
value {
min 1
max 1
type "boolean"
type boolean
}
}
node "tag-names" description="Validations to apply specifically to arbitrary type tag names" {
children ref=r#"[id="validations"]"#
node tag-names description="Validations to apply specifically to arbitrary type tag names" {
children ref=#"[id="validations"]"#
}
node "other-tags-allowed" description="Whether to allow child node tags other than the ones explicitly listed. Defaults to 'false'." {
node other-tags-allowed description="Whether to allow child node tags other than the ones explicitly listed. Defaults to '#false'." {
max 1
value {
min 1
max 1
type "boolean"
type boolean
}
}
node "info" description="A child node that describes the schema itself." {
node info description="A child node that describes the schema itself." {
children {
node "title" description="The title of the schema or the format it describes" {
node title description="The title of the schema or the format it describes" {
value description="The title text" {
type "string"
type string
min 1
max 1
}
prop "lang" id="info-lang" description="The language of the text" {
type "string"
prop lang id=info-lang description="The language of the text" {
type string
}
}
node "description" description="A description of the schema or the format it describes" {
node description description="A description of the schema or the format it describes" {
value description="The description text" {
type "string"
type string
min 1
max 1
}
prop ref=r#"[id="info-lang"]"#
prop ref=#"[id="info-lang"]"#
}
node "author" description="Author of the schema" {
value id="info-person-name" description="Person name" {
type "string"
node author description="Author of the schema" {
value id=info-person-name description="Person name" {
type string
min 1
max 1
}
prop "orcid" id="info-orcid" description="The ORCID of the person" {
type "string"
pattern r"\d{4}-\d{4}-\d{4}-\d{4}"
prop orcid id=info-orcid description="The ORCID of the person" {
type string
pattern #"\d{4}-\d{4}-\d{4}-\d{4}"#
}
children {
node ref=r#"[id="info-link"]"#
node ref=#"[id="info-link"]"#
}
}
node "contributor" description="Contributor to the schema" {
value ref=r#"[id="info-person-name"]"#
prop ref=r#"[id="info-orcid"]"#
node contributor description="Contributor to the schema" {
value ref=#"[id="info-person-name"]"#
prop ref=#"[id="info-orcid"]"#
children {
node ref=#"[id="info-link"]"#
}
}
node "link" id="info-link" description="Links to itself, and to sources describing it" {
node link id=info-link description="Links to itself, and to sources describing it" {
value description="A URL that the link points to" {
type "string"
format "url" "irl"
type string
format url irl
min 1
max 1
}
prop "rel" description="The relation between the current entity and the URL" {
type "string"
enum "self" "documentation"
prop rel description="The relation between the current entity and the URL" {
type string
enum self documentation
}
prop ref=r#"[id="info-lang"]"#
prop ref=#"[id="info-lang"]"#
}
node "license" description="The license(s) that the schema is licensed under" {
node license description="The license(s) that the schema is licensed under" {
value description="Name of the used license" {
type "string"
type string
min 1
max 1
}
prop "spdx" description="An SPDX license identifier" {
type "string"
prop spdx description="An SPDX license identifier" {
type string
}
children {
node ref=r#"[id="info-link"]"#
node ref=#"[id="info-link"]"#
}
}
node "published" description="When the schema was published" {
node published description="When the schema was published" {
value description="Publication date" {
type "string"
format "date"
type string
format date
min 1
max 1
}
prop "time" id="info-time" description="A time to accompany the date" {
type "string"
format "time"
prop time id=info-time description="A time to accompany the date" {
type string
format time
}
}
node "modified" description="When the schema was last modified" {
node modified description="When the schema was last modified" {
value description="Modification date" {
type "string"
format "date"
type string
format date
min 1
max 1
}
prop ref=r#"[id="info-time"]"#
prop ref=#"[id="info-time"]"#
}
node "version" description="The version number of this version of the schema" {
node version description="The version number of this version of the schema" {
value description="Semver version number" {
type "string"
pattern r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
type string
pattern #"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"#
min 1
max 1
}
}
}
}
node "tag" id="tag-node" description="A tag belonging to a child node of `document` or another node." {
node tag id=tag-node description="A tag belonging to a child node of `document` or another node." {
value description="The name of the tag. If a tag name is not supplied, the node rules apply to _all_ nodes belonging to the parent." {
type "string"
type string
max 1
}
prop "description" description="A description of this node's purpose." {
type "string"
prop description description="A description of this node's purpose." {
type string
}
prop "id" description="A globally-unique ID for this node." {
type "string"
prop id description="A globally-unique ID for this node." {
type string
}
prop "ref" description="A globally unique reference to another node." {
type "string"
format "kdl-query"
prop ref description="A globally unique reference to another node." {
type string
format kdl-query
}
children {
node ref=r#"[id="node-names-node"]"#
node ref=r#"[id="other-nodes-allowed-node"]"#
node ref=r#"[id="node-node"]"#
node ref=#"[id="node-names-node"]"#
node ref=#"[id="other-nodes-allowed-node"]"#
node ref=#"[id="node-node"]"#
}
}
node "node" id="node-node" description="A child node belonging either to `document` or to another `node`. Nodes may be anonymous." {
node node id=node-node description="A child node belonging either to `document` or to another `node`. Nodes may be anonymous." {
value description="The name of the node. If a node name is not supplied, the node rules apply to _all_ nodes belonging to the parent." {
type "string"
type string
max 1
}
prop "description" description="A description of this node's purpose." {
type "string"
prop description description="A description of this node's purpose." {
type string
}
prop "id" description="A globally-unique ID for this node." {
type "string"
prop id description="A globally-unique ID for this node." {
type string
}
prop "ref" description="A globally unique reference to another node." {
type "string"
format "kdl-query"
prop ref description="A globally unique reference to another node." {
type string
format kdl-query
}
children {
node "prop-names" description="Validations to apply specifically to arbitrary property names" {
children ref=r#"[id="validations"]"#
node prop-names description="Validations to apply specifically to arbitrary property names" {
children ref=#"[id="validations"]"#
}
node "other-props-allowed" description="Whether to allow properties other than the ones explicitly listed. Defaults to 'false'." {
node other-props-allowed description="Whether to allow properties other than the ones explicitly listed. Defaults to '#false'." {
max 1
value {
min 1
max 1
type "boolean"
type boolean
}
}
node "min" description="minimum number of instances of this node in its parent's children." {
node min description="minimum number of instances of this node in its parent's children." {
max 1
value {
min 1
max 1
type "number"
type number
}
}
node "max" description="maximum number of instances of this node in its parent's children." {
node max description="maximum number of instances of this node in its parent's children." {
max 1
value {
min 1
max 1
type "number"
type number
}
}
node ref=r#"[id="value-tag-node"]"#
node "prop" id="prop-node" description="A node property key/value pair." {
node ref=#"[id="value-tag-node"]"#
node prop id="prop-node" description="A node property key/value pair." {
value description="The property key." {
type "string"
type string
}
prop "id" description="A globally-unique ID of this property." {
type "string"
prop id description="A globally-unique ID of this property." {
type string
}
prop "ref" description="A globally unique reference to another property node." {
type "string"
format "kdl-query"
prop ref description="A globally unique reference to another property node." {
type string
format kdl-query
}
prop "description" description="A description of this property's purpose." {
type "string"
prop description description="A description of this property's purpose." {
type string
}
children description="Property-specific validations." {
node "required" description="Whether this property is required if its parent is present." {
node required description="Whether this property is required if its parent is present." {
max 1
value {
min 1
max 1
type "boolean"
type boolean
}
}
}
children id="validations" description="General value validations." {
node "tag" id="value-tag-node" description="The tags associated with this value" {
children id=validations description="General value validations." {
node tag id=value-tag-node description="The tags associated with this value" {
max 1
children ref=r#"[id="validations"]"#
children ref=#"[id="validations"]"#
}
node "type" description="The type for this prop's value." {
node type description="The type for this prop's value." {
max 1
value {
min 1
type "string"
type string
}
}
node "enum" description="An enumeration of possible values" {
node enum description="An enumeration of possible values" {
max 1
value description="Enumeration choices" {
min 1
}
}
node "pattern" description="PCRE (Regex) pattern or patterns to test prop values against." {
node pattern description="PCRE (Regex) pattern or patterns to test prop values against." {
value {
min 1
type "string"
type string
}
}
node "min-length" description="Minimum length of prop value, if it's a string." {
node min-length description="Minimum length of prop value, if it's a string." {
max 1
value {
min 1
type "number"
type number
}
}
node "max-length" description="Maximum length of prop value, if it's a string." {
node max-length description="Maximum length of prop value, if it's a string." {
max 1
value {
min 1
type "number"
type number
}
}
node "format" description="Intended data format." {
node format description="Intended data format." {
max 1
value {
min 1
type "string"
type string
// https://json-schema.org/understanding-json-schema/reference/string.html#format
enum "date-time" "date" "time" "duration" "decimal" "currency" "country-2" "country-3" "country-subdivision" "email" "idn-email" "hostname" "idn-hostname" "ipv4" "ipv6" "url" "url-reference" "irl" "irl-reference" "url-template" "regex" "uuid" "kdl-query" "i8" "i16" "i32" "i64" "u8" "u16" "u32" "u64" "isize" "usize" "f32" "f64" "decimal64" "decimal128"
enum date-time date time duration decimal currency country-2 country-3 country-subdivision email idn-email hostname idn-hostname ipv4 ipv6 url url-reference irl irl-reference url-template regex uuid kdl-query i8 i16 i32 i64 u8 u16 u32 u64 isize usize f32 f64 decimal64 decimal128
}
}
node "%" description="Only used for numeric values. Constrains them to be multiples of the given number(s)" {
node % description="Only used for numeric values. Constrains them to be multiples of the given number(s)" {
max 1
value {
min 1
type "number"
type number
}
}
node ">" description="Only used for numeric values. Constrains them to be greater than the given number(s)" {
node gt description="Only used for numeric values. Constrains them to be greater than the given number(s)" {
max 1
value {
min 1
max 1
type "number"
type number
}
}
node ">=" description="Only used for numeric values. Constrains them to be greater than or equal to the given number(s)" {
node gte description="Only used for numeric values. Constrains them to be greater than or equal to the given number(s)" {
max 1
value {
min 1
max 1
type "number"
type number
}
}
node "<" description="Only used for numeric values. Constrains them to be less than the given number(s)" {
node lt description="Only used for numeric values. Constrains them to be less than the given number(s)" {
max 1
value {
min 1
max 1
type "number"
type number
}
}
node "<=" description="Only used for numeric values. Constrains them to be less than or equal to the given number(s)" {
node lte description="Only used for numeric values. Constrains them to be less than or equal to the given number(s)" {
max 1
value {
min 1
max 1
type "number"
type number
}
}
}
}
node "value" id="value-node" description="one or more direct node values" {
prop "id" description="A globally-unique ID of this value." {
type "string"
node value id=value-node description="one or more direct node values" {
prop id description="A globally-unique ID of this value." {
type string
}
prop "ref" description="A globally unique reference to another value node." {
type "string"
format "kdl-query"
prop ref description="A globally unique reference to another value node." {
type string
format kdl-query
}
prop "description" description="A description of this property's purpose." {
type "string"
prop description description="A description of this property's purpose." {
type string
}
children ref=r#"[id="validations"]"#
children ref=#"[id="validations"]"#
children description="Node value-specific validations" {
node "min" description="minimum number of values for this node." {
node min description="minimum number of values for this node." {
max 1
value {
min 1
max 1
type "number"
type number
}
}
node "max" description="maximum number of values for this node." {
node max description="maximum number of values for this node." {
max 1
value {
min 1
max 1
type "number"
type number
}
}
}
}
node "children" id="children-node" {
prop "id" description="A globally-unique ID of this children node." {
type "string"
node children id=children-node {
prop id description="A globally-unique ID of this children node." {
type string
}
prop "ref" description="A globally unique reference to another children node." {
type "string"
format "kdl-query"
prop ref description="A globally unique reference to another children node." {
type string
format kdl-query
}
prop "description" description="A description of this these children's purpose." {
type "string"
prop description description="A description of this these children's purpose." {
type string
}
children ref=r#"[id="node-children"]"#
children ref=#"[id="node-children"]"#
}
}
}
node "definitions" description="Definitions to reference in parts of the top-level nodes" {
node definitions description="Definitions to reference in parts of the top-level nodes" {
children {
node ref=r#"[id="node-node"]"#
node ref=r#"[id="value-node"]"#
node ref=r#"[id="prop-node"]"#
node ref=r#"[id="children-node"]"#
node ref=r#"[id="tag-node"]"#
node ref=#"[id="node-node"]"#
node ref=#"[id="value-node"]"#
node ref=#"[id="prop-node"]"#
node ref=#"[id="children-node"]"#
node ref=#"[id="tag-node"]"#
}
}
}

View File

@ -1,48 +1,48 @@
// Based on https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Clients/NuGet.CommandLine/NuGet.CommandLine.csproj
Project {
PropertyGroup {
IsCommandLinePackage true
IsCommandLinePackage #true
}
Import Project=r"$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), 'README.md'))\build\common.props"
Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk"
Import Project="ilmerge.props"
Import Project=#"$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), 'README.md'))\build\common.props"#
Import Project=Sdk.props Sdk=Microsoft.NET.Sdk
Import Project=ilmerge.props
PropertyGroup {
RootNamespace "NuGet.CommandLine"
AssemblyName "NuGet"
RootNamespace NuGet.CommandLine
AssemblyName NuGet
AssemblyTitle "NuGet Command Line"
PackageId "NuGet.CommandLine"
PackageId NuGet.CommandLine
TargetFramework "$(NETFXTargetFramework)"
GenerateDocumentationFile false
GenerateDocumentationFile #false
Description "NuGet Command Line Interface."
ApplicationManifest "app.manifest"
Shipping true
OutputType "Exe"
ComVisible false
ApplicationManifest app.manifest
Shipping #true
OutputType Exe
ComVisible #false
// Pack properties
PackProject true
IncludeBuildOutput false
PackProject #true
IncludeBuildOutput #false
TargetsForTfmSpecificContentInPackage "$(TargetsForTfmSpecificContentInPackage)" "CreateCommandlineNupkg"
SuppressDependenciesWhenPacking true
DevelopmentDependency true
PackageRequireLicenseAcceptance false
UsePublicApiAnalyzer false
SuppressDependenciesWhenPacking #true
DevelopmentDependency #true
PackageRequireLicenseAcceptance #false
UsePublicApiAnalyzer #false
}
Target Name="CreateCommandlineNupkg" {
Target Name=CreateCommandlineNupkg {
ItemGroup {
TfmSpecificPackageFile Include=r"$(ArtifactsDirectory)$(VsixOutputDirName)\NuGet.exe" {
TfmSpecificPackageFile Include=#"$(ArtifactsDirectory)$(VsixOutputDirName)\NuGet.exe"# {
PackagePath "tools/"
}
TfmSpecificPackageFile Include=r"$(ArtifactsDirectory)$(VsixOutputDirName)\NuGet.pdb" {
TfmSpecificPackageFile Include=#"$(ArtifactsDirectory)$(VsixOutputDirName)\NuGet.pdb"# {
PackagePath "tools/"
}
}
}
ItemGroup Condition="$(DefineConstants.Contains(SIGNED_BUILD))" {
AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo" {
AssemblyAttribute Include=System.Runtime.CompilerServices.InternalsVisibleTo {
_Parameter1 "NuGet.CommandLine.FuncTest, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293"
}
AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo" {
@ -51,81 +51,81 @@ Project {
}
ItemGroup Condition="!$(DefineConstants.Contains(SIGNED_BUILD))" {
AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo" {
_Parameter1 "NuGet.CommandLine.FuncTest"
AssemblyAttribute Include=System.Runtime.CompilerServices.InternalsVisibleTo {
_Parameter1 NuGet.CommandLine.FuncTest
}
AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo" {
_Parameter1 "NuGet.CommandLine.Test"
AssemblyAttribute Include=System.Runtime.CompilerServices.InternalsVisibleTo {
_Parameter1 NuGet.CommandLine.Test
}
}
ItemGroup Condition="$(DefineConstants.Contains(SIGNED_BUILD))" {
AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo" {
AssemblyAttribute Include=System.Runtime.CompilerServices.InternalsVisibleTo {
_Parameter1 "NuGet.CommandLine.Test, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293"
}
}
ItemGroup Condition="!$(DefineConstants.Contains(SIGNED_BUILD))" {
AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo" {
_Parameter1 "NuGet.CommandLine.Test"
AssemblyAttribute Include=System.Runtime.CompilerServices.InternalsVisibleTo {
_Parameter1 NuGet.CommandLine.Test
}
}
ItemGroup {
Reference Include="Microsoft.Build.Utilities.v4.0"
Reference Include="Microsoft.CSharp"
Reference Include="System"
Reference Include="System.ComponentModel.Composition"
Reference Include="System.ComponentModel.Composition.Registration"
Reference Include="System.ComponentModel.DataAnnotations"
Reference Include="System.IO.Compression"
Reference Include="System.Net.Http"
Reference Include="System.Xml"
Reference Include="System.Xml.Linq"
Reference Include="NuGet.Core" {
HintPath r"$(SolutionPackagesFolder)nuget.core\2.14.0-rtm-832\lib\net40-Client\NuGet.Core.dll"
Aliases "CoreV2"
Reference Include=Microsoft.Build.Utilities.v4.0
Reference Include=Microsoft.CSharp
Reference Include=System
Reference Include=System.ComponentModel.Composition
Reference Include=System.ComponentModel.Composition.Registration
Reference Include=System.ComponentModel.DataAnnotations
Reference Include=System.IO.Compression
Reference Include=System.Net.Http
Reference Include=System.Xml
Reference Include=System.Xml.Linq
Reference Include=NuGet.Core {
HintPath #"$(SolutionPackagesFolder)nuget.core\2.14.0-rtm-832\lib\net40-Client\NuGet.Core.dll"#
Aliases CoreV2
}
}
ItemGroup {
PackageReference Include="Microsoft.VisualStudio.Setup.Configuration.Interop"
ProjectReference Include=r"$(NuGetCoreSrcDirectory)NuGet.PackageManagement\NuGet.PackageManagement.csproj"
ProjectReference Include=r"$(NuGetCoreSrcDirectory)NuGet.Build.Tasks\NuGet.Build.Tasks.csproj"
PackageReference Include=Microsoft.VisualStudio.Setup.Configuration.Interop
ProjectReference Include=#"$(NuGetCoreSrcDirectory)NuGet.PackageManagement\NuGet.PackageManagement.csproj"#
ProjectReference Include=#"$(NuGetCoreSrcDirectory)NuGet.Build.Tasks\NuGet.Build.Tasks.csproj"#
}
ItemGroup {
EmbeddedResource Update="NuGetCommand.resx" {
Generator "ResXFileCodeGenerator"
LastGenOutput "NuGetCommand.Designer.cs"
EmbeddedResource Update=NuGetCommand.resx {
Generator ResXFileCodeGenerator
LastGenOutput NuGetCommand.Designer.cs
}
Compile Update="NuGetCommand.Designer.cs" {
DesignTime true
AutoGen true
DependentUpon "NuGetCommand.resx"
Compile Update=NuGetCommand.Designer.cs {
DesignTime #true
AutoGen #true
DependentUpon NuGetCommand.resx
}
EmbeddedResource Update="NuGetResources.resx" {
EmbeddedResource Update=NuGetResources.resx {
// Strings are shared by other projects, use public strings.
Generator "PublicResXFileCodeGenerator"
LastGenOutput "NuGetResources.Designer.cs"
Generator PublicResXFileCodeGenerator
LastGenOutput NuGetResources.Designer.cs
}
Compile Update="NuGetResources.Designer.cs" {
DesignTime true
AutoGen true
DependentUpon "NuGetResources.resx"
Compile Update=NuGetResources.Designer.cs {
DesignTime #true
AutoGen #true
DependentUpon NuGetResources.resx
}
}
ItemGroup {
EmbeddedResource Include=r"$(NuGetCoreSrcDirectory)NuGet.Build.Tasks\NuGet.targets" {
Link "NuGet.targets"
SubType "Designer"
EmbeddedResource Include=#"$(NuGetCoreSrcDirectory)NuGet.Build.Tasks\NuGet.targets"# {
Link NuGet.targets
SubType Designer
}
}
// Since we are moving some code and strings from NuGet.CommandLine to NuGet.Commands, we opted to go through normal localization process (build .resources.dll) and then add them to the ILMerged nuget.exe
// This will also be called from CI build, after assemblies are localized, since our test infra takes nuget.exe before Localization
Target Name="ILMergeNuGetExe" \
AfterTargets="Build" \
Target Name=ILMergeNuGetExe \
AfterTargets=Build \
Condition="'$(BuildingInsideVisualStudio)' != 'true' and '$(SkipILMergeOfNuGetExe)' != 'true'" \
{
PropertyGroup {
@ -133,9 +133,9 @@ Project {
ExpectedLocalizedArtifactCount 0 Condition="'$(ExpectedLocalizedArtifactCount)' == ''"
}
ItemGroup {
BuildArtifacts Include=r"$(OutputPath)\*.dll" Exclude="@(MergeExclude)"
BuildArtifacts Include=#"$(OutputPath)\*.dll"# Exclude="@(MergeExclude)"
// NuGet.exe needs all NuGet.Commands.resources.dll merged in
LocalizedArtifacts Include=r"$(ArtifactsDirectory)\NuGet.Commands\**\$(NETFXTargetFramework)\**\*.resources.dll"
LocalizedArtifacts Include=#"$(ArtifactsDirectory)\NuGet.Commands\**\$(NETFXTargetFramework)\**\*.resources.dll"#
}
Error Text="Build dependencies are inconsistent with mergeinclude specified in ilmerge.props" \
Condition="'@(BuildArtifacts-&gt;Count())' != '@(MergeInclude-&gt;Count())'"
@ -143,36 +143,36 @@ Project {
Condition="'@(LocalizedArtifacts-&gt;Count())' != '$(ExpectedLocalizedArtifactCount)'"
PropertyGroup {
PathToBuiltNuGetExe "$(OutputPath)NuGet.exe"
IlmergeCommand r"$(ILMergeExePath) /lib:$(OutputPath) /out:$(ArtifactsDirectory)$(VsixOutputDirName)\NuGet.exe @(MergeAllowDup -> '/allowdup:%(Identity)', ' ') /log:$(OutputPath)IlMergeLog.txt"
IlmergeCommand #"$(ILMergeExePath) /lib:$(OutputPath) /out:$(ArtifactsDirectory)$(VsixOutputDirName)\NuGet.exe @(MergeAllowDup -> '/allowdup:%(Identity)', ' ') /log:$(OutputPath)IlMergeLog.txt"#
IlmergeCommand Condition="Exists($(MS_PFX_PATH))" "$(IlmergeCommand) /delaysign /keyfile:$(MS_PFX_PATH)"
// LocalizedArtifacts need fullpath, since there will be duplicate file names
IlmergeCommand "$(IlmergeCommand) $(PathToBuiltNuGetExe) @(BuildArtifacts->'%(filename)%(extension)', ' ') @(LocalizedArtifacts->'%(fullpath)', ' ')"
}
MakeDir Directories="$(ArtifactsDirectory)$(VsixOutputDirName)"
Exec Command="$(IlmergeCommand)" ContinueOnError="false"
Exec Command="$(IlmergeCommand)" ContinueOnError=#false
}
Import Project="$(BuildCommonDirectory)common.targets"
Import Project="$(BuildCommonDirectory)embedinterop.targets"
// Do nothing. This basically strips away the framework assemblies from the resulting nuspec.
Target Name="_GetFrameworkAssemblyReferences" DependsOnTargets="ResolveReferences"
Target Name=_GetFrameworkAssemblyReferences DependsOnTargets=ResolveReferences
Target Name="GetSigningInputs" Returns="@(DllsToSign)" {
Target Name=GetSigningInputs Returns="@(DllsToSign)" {
ItemGroup {
DllsToSign Include=r"$(ArtifactsDirectory)$(VsixOutputDirName)\NuGet.exe" {
StrongName "MsSharedLib72"
Authenticode "Microsoft400"
DllsToSign Include=#"$(ArtifactsDirectory)$(VsixOutputDirName)\NuGet.exe"# {
StrongName MsSharedLib72
Authenticode Microsoft400
}
}
}
Target Name="GetSymbolsToIndex" Returns="@(SymbolsToIndex)" {
Target Name=GetSymbolsToIndex Returns="@(SymbolsToIndex)" {
ItemGroup {
SymbolsToIndex Include=r"$(ArtifactsDirectory)$(VsixOutputDirName)\NuGet.exe"
SymbolsToIndex Include=r"$(ArtifactsDirectory)$(VsixOutputDirName)\NuGet.pdb"
SymbolsToIndex Include=#"$(ArtifactsDirectory)$(VsixOutputDirName)\NuGet.exe"#
SymbolsToIndex Include=#"$(ArtifactsDirectory)$(VsixOutputDirName)\NuGet.pdb"#
}
}
Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk"
Import Project=Sdk.targets Sdk=Microsoft.NET.Sdk
}

47
examples/website.kdl Normal file
View File

@ -0,0 +1,47 @@
!doctype html
html lang=en {
head {
meta charset=utf-8
meta name=viewport content="width=device-width, initial-scale=1.0"
meta \
name=description \
content="kdl is a document language, mostly based on SDLang, with xml-like semantics that looks like you're invoking a bunch of CLI commands!"
title "kdl - The KDL Document Language"
link rel=stylesheet href="/styles/global.css"
}
body {
main {
header class="py-10 bg-gray-300" {
h1 class="text-4xl text-center" "kdl - The KDL Document Language"
}
section class=kdl-section id=description {
p {
- "kdl is a document language, mostly based on "
a href="https://sdlang.org" "SDLang"
- " with xml-like semantics that looks like you're invoking a bunch of CLI commands"
}
p "It's meant to be used both as a serialization format and a configuration language, and is relatively light on syntax compared to XML."
}
section class=kdl-section id=design-and-discussion {
h2 "Design and Discussion"
p {
- "kdl is still extremely new, and discussion about the format should happen over on the "
a href="https://github.com/kdoclang/kdl/discussions" {
- "discussions"
}
- " page in the Github repo. Feel free to jump in and give us your 2 cents!"
}
}
section class=kdl-section id=design-principles {
h2 "Design Principles"
ol {
li Maintainability
li Flexibility
li "Cognitive simplicity and Learnability"
li "Ease of de/serialization"
li "Ease of implementation"
}
}
}
}
}

View File

@ -1,8 +1,8 @@
#[cfg(feature = "span")]
use miette::SourceSpan;
use std::{fmt::Display, str::FromStr};
use std::fmt::Display;
use crate::{parser, IntoKdlQuery, KdlError, KdlNode, KdlQueryIterator, KdlValue, NodeKey};
use crate::{KdlNode, KdlValue};
/// Represents a KDL
/// [`Document`](https://github.com/kdl-org/kdl/blob/main/SPEC.md#document).
@ -20,27 +20,23 @@ use crate::{parser, IntoKdlQuery, KdlError, KdlNode, KdlQueryIterator, KdlValue,
/// ```
#[derive(Debug, Clone, Eq)]
pub struct KdlDocument {
pub(crate) leading: Option<String>,
pub(crate) nodes: Vec<KdlNode>,
pub(crate) trailing: Option<String>,
pub(crate) format: Option<KdlDocumentFormat>,
#[cfg(feature = "span")]
pub(crate) span: SourceSpan,
}
impl PartialEq for KdlDocument {
fn eq(&self, other: &Self) -> bool {
self.leading == other.leading
&& self.nodes == other.nodes
&& self.trailing == other.trailing
self.nodes == other.nodes && self.format == other.format
// Intentionally omitted: self.span == other.span
}
}
impl std::hash::Hash for KdlDocument {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.leading.hash(state);
self.nodes.hash(state);
self.trailing.hash(state);
self.format.hash(state);
// Intentionally omitted: self.span.hash(state)
}
}
@ -48,9 +44,8 @@ impl std::hash::Hash for KdlDocument {
impl Default for KdlDocument {
fn default() -> Self {
Self {
leading: Default::default(),
nodes: Default::default(),
trailing: Default::default(),
format: Default::default(),
#[cfg(feature = "span")]
span: SourceSpan::from(0..0),
}
@ -106,7 +101,7 @@ impl KdlDocument {
/// You can fetch the value of `foo` in a single call like this:
/// ```rust
/// # use kdl::{KdlDocument, KdlValue};
/// # let doc: KdlDocument = "foo 1\nbar false".parse().unwrap();
/// # let doc: KdlDocument = "foo 1\nbar #false".parse().unwrap();
/// assert_eq!(doc.get_arg("foo"), Some(&1.into()));
/// ```
pub fn get_arg(&self, name: &str) -> Option<&KdlValue> {
@ -125,13 +120,13 @@ impl KdlDocument {
/// Given a document like this:
/// ```kdl
/// foo 1 2 3
/// bar false
/// bar #false
/// ```
///
/// You can fetch the arguments for `foo` in a single call like this:
/// ```rust
/// # use kdl::{KdlDocument, KdlValue};
/// # let doc: KdlDocument = "foo 1 2 3\nbar false".parse().unwrap();
/// # let doc: KdlDocument = "foo 1 2 3\nbar #false".parse().unwrap();
/// assert_eq!(doc.get_args("foo"), vec![&1.into(), &2.into(), &3.into()]);
/// ```
pub fn get_args(&self, name: &str) -> Vec<&KdlValue> {
@ -161,14 +156,14 @@ impl KdlDocument {
/// foo {
/// - 1
/// - 2
/// - false
/// - #false
/// }
/// ```
///
/// You can fetch the dashed child values of `foo` in a single call like this:
/// ```rust
/// # use kdl::{KdlDocument, KdlValue};
/// # let doc: KdlDocument = "foo {\n - 1\n - 2\n - false\n}".parse().unwrap();
/// # let doc: KdlDocument = "foo {\n - 1\n - 2\n - #false\n}".parse().unwrap();
/// assert_eq!(doc.get_dash_args("foo"), vec![&1.into(), &2.into(), &false.into()]);
/// ```
pub fn get_dash_args(&self, name: &str) -> Vec<&KdlValue> {
@ -192,24 +187,19 @@ impl KdlDocument {
&mut self.nodes
}
/// Gets leading text (whitespace, comments) for this KdlDocument.
pub fn leading(&self) -> Option<&str> {
self.leading.as_deref()
/// Gets the formatting details for this entry.
pub fn format(&self) -> Option<&KdlDocumentFormat> {
self.format.as_ref()
}
/// Sets leading text (whitespace, comments) for this KdlDocument.
pub fn set_leading(&mut self, leading: impl Into<String>) {
self.leading = Some(leading.into());
/// Gets a mutable reference to this entry's formatting details.
pub fn format_mut(&mut self) -> Option<&mut KdlDocumentFormat> {
self.format.as_mut()
}
/// Gets trailing text (whitespace, comments) for this KdlDocument.
pub fn trailing(&self) -> Option<&str> {
self.trailing.as_deref()
}
/// Sets trailing text (whitespace, comments) for this KdlDocument.
pub fn set_trailing(&mut self, trailing: impl Into<String>) {
self.trailing = Some(trailing.into());
/// Sets the formatting details for this entry.
pub fn set_format(&mut self, format: KdlDocumentFormat) {
self.format = Some(format);
}
/// Length of this document when rendered as a string.
@ -226,94 +216,99 @@ impl KdlDocument {
/// this document will be unaffected.
///
/// If you need to clear the `KdlNode`s, use [`Self::clear_fmt_recursive`].
pub fn clear_fmt(&mut self) {
self.leading = None;
self.trailing = None;
pub fn clear_format(&mut self) {
self.format = None;
}
/// Clears leading and trailing text (whitespace, comments), also clearing
/// all the `KdlNode`s in the document.
pub fn clear_fmt_recursive(&mut self) {
self.clear_fmt();
pub fn clear_format_recursive(&mut self) {
self.clear_format();
for node in self.nodes.iter_mut() {
node.clear_fmt_recursive();
node.clear_format_recursive();
}
}
/// Auto-formats this Document, making everything nice while preserving
/// comments.
pub fn fmt(&mut self) {
self.fmt_impl(0, false);
pub fn autoformat(&mut self) {
self.autoformat_impl(0, false);
}
/// Formats the document and removes all comments from the document.
pub fn fmt_no_comments(&mut self) {
self.fmt_impl(0, true);
pub fn autoformat_no_comments(&mut self) {
self.autoformat_impl(0, true);
}
/// Queries this Document's children according to the KQL query language,
/// returning an iterator over all matching nodes.
///
/// # NOTE
///
/// Any query selectors that try to select the toplevel `scope()` will
/// fail to match when using this method, since there's no [`KdlNode`] to
/// return in this case.
pub fn query_all(&self, query: impl IntoKdlQuery) -> Result<KdlQueryIterator<'_>, KdlError> {
let parsed = query.into_query()?;
Ok(KdlQueryIterator::new(None, Some(self), parsed))
}
// TODO(@zkat): These should all be moved into the query module itself,
// instead of being methods on the models
//
// /// Queries this Document's children according to the KQL query language,
// /// returning an iterator over all matching nodes.
// ///
// /// # NOTE
// ///
// /// Any query selectors that try to select the toplevel `scope()` will
// /// fail to match when using this method, since there's no [`KdlNode`] to
// /// return in this case.
// pub fn query_all(
// &self,
// query: impl IntoKdlQuery,
// ) -> Result<KdlQueryIterator<'_>, KdlDiagnostic> {
// let parsed = query.into_query()?;
// Ok(KdlQueryIterator::new(None, Some(self), parsed))
// }
/// Queries this Document's children according to the KQL query language,
/// returning the first match, if any.
///
/// # NOTE
///
/// Any query selectors that try to select the toplevel `scope()` will
/// fail to match when using this method, since there's no [`KdlNode`] to
/// return in this case.
pub fn query(&self, query: impl IntoKdlQuery) -> Result<Option<&KdlNode>, KdlError> {
let mut iter = self.query_all(query)?;
Ok(iter.next())
}
// /// Queries this Document's children according to the KQL query language,
// /// returning the first match, if any.
// ///
// /// # NOTE
// ///
// /// Any query selectors that try to select the toplevel `scope()` will
// /// fail to match when using this method, since there's no [`KdlNode`] to
// /// return in this case.
// pub fn query(&self, query: impl IntoKdlQuery) -> Result<Option<&KdlNode>, KdlDiagnostic> {
// let mut iter = self.query_all(query)?;
// Ok(iter.next())
// }
/// Queries this Document's children according to the KQL query language,
/// picking the first match, and calling `.get(key)` on it, if the query
/// succeeded.
///
/// # NOTE
///
/// Any query selectors that try to select the toplevel `scope()` will
/// fail to match when using this method, since there's no [`KdlNode`] to
/// return in this case.
pub fn query_get(
&self,
query: impl IntoKdlQuery,
key: impl Into<NodeKey>,
) -> Result<Option<&KdlValue>, KdlError> {
Ok(self.query(query)?.and_then(|node| node.get(key)))
}
// /// Queries this Document's children according to the KQL query language,
// /// picking the first match, and calling `.get(key)` on it, if the query
// /// succeeded.
// ///
// /// # NOTE
// ///
// /// Any query selectors that try to select the toplevel `scope()` will
// /// fail to match when using this method, since there's no [`KdlNode`] to
// /// return in this case.
// pub fn query_get(
// &self,
// query: impl IntoKdlQuery,
// key: impl Into<NodeKey>,
// ) -> Result<Option<&KdlValue>, KdlDiagnostic> {
// Ok(self.query(query)?.and_then(|node| node.get(key)))
// }
/// Queries this Document's children according to the KQL query language,
/// returning an iterator over all matching nodes, returning the requested
/// field from each of those nodes and filtering out nodes that don't have
/// it.
///
/// # NOTE
///
/// Any query selectors that try to select the toplevel `scope()` will
/// fail to match when using this method, since there's no [`KdlNode`] to
/// return in this case.
pub fn query_get_all(
&self,
query: impl IntoKdlQuery,
key: impl Into<NodeKey>,
) -> Result<impl Iterator<Item = &KdlValue>, KdlError> {
let key: NodeKey = key.into();
Ok(self
.query_all(query)?
.filter_map(move |node| node.get(key.clone())))
}
// /// Queries this Document's children according to the KQL query language,
// /// returning an iterator over all matching nodes, returning the requested
// /// field from each of those nodes and filtering out nodes that don't have
// /// it.
// ///
// /// # NOTE
// ///
// /// Any query selectors that try to select the toplevel `scope()` will
// /// fail to match when using this method, since there's no [`KdlNode`] to
// /// return in this case.
// pub fn query_get_all(
// &self,
// query: impl IntoKdlQuery,
// key: impl Into<NodeKey>,
// ) -> Result<impl Iterator<Item = &KdlValue>, KdlDiagnostic> {
// let key: NodeKey = key.into();
// Ok(self
// .query_all(query)?
// .filter_map(move |node| node.get(key.clone())))
// }
}
impl Display for KdlDocument {
@ -323,19 +318,19 @@ impl Display for KdlDocument {
}
impl KdlDocument {
pub(crate) fn fmt_impl(&mut self, indent: usize, no_comments: bool) {
if let Some(s) = self.leading.as_mut() {
crate::fmt::fmt_leading(s, indent, no_comments);
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.fmt_impl(indent, no_comments);
node.autoformat_impl(indent, no_comments);
}
if let Some(s) = self.trailing.as_mut() {
crate::fmt::fmt_trailing(s, no_comments);
if let Some(KdlDocumentFormat { trailing, .. }) = self.format_mut() {
crate::fmt::autoformat_trailing(trailing, no_comments);
if !has_nodes {
s.push('\n');
trailing.push('\n');
}
}
}
@ -345,16 +340,13 @@ impl KdlDocument {
f: &mut std::fmt::Formatter<'_>,
indent: usize,
) -> std::fmt::Result {
if let Some(leading) = &self.leading {
if let Some(KdlDocumentFormat { leading, .. }) = self.format() {
write!(f, "{}", leading)?;
}
for node in &self.nodes {
node.stringify(f, indent)?;
if node.trailing.is_none() {
writeln!(f)?;
}
}
if let Some(trailing) = &self.trailing {
if let Some(KdlDocumentFormat { trailing, .. }) = self.format() {
write!(f, "{}", trailing)?;
}
Ok(())
@ -370,13 +362,13 @@ impl IntoIterator for KdlDocument {
}
}
impl FromStr for KdlDocument {
type Err = KdlError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let kdl_parser = parser::KdlParser::new(input);
kdl_parser.parse(parser::document(&kdl_parser))
}
/// Formatting details for [`KdlDocument`]s.
#[derive(Debug, Clone, Default, Hash, Eq, PartialEq)]
pub struct KdlDocumentFormat {
/// Whitespace and comments preceding the document's first node.
pub leading: String,
/// Whitespace and comments following the document's last node.
pub trailing: String,
}
#[cfg(test)]
@ -411,26 +403,56 @@ second_node /* This time, the comment is here */ param=153 {
let mut left_doc: KdlDocument = left_src.parse()?;
let mut right_doc: KdlDocument = right_src.parse()?;
assert_ne!(left_doc, right_doc);
left_doc.clear_fmt_recursive();
right_doc.clear_fmt_recursive();
left_doc.clear_format_recursive();
right_doc.clear_format_recursive();
assert_eq!(left_doc, right_doc);
Ok(())
}
#[test]
fn basic_parsing() -> miette::Result<()> {
let src = r#"
// Hello, world!
node 1
node two
node item="three";
node {
nested 1 2 3
nested_2 hi "world"
}
(type)node ("type")what?
+false #true
null_id null_prop=#null
foo indented
// normal comment?
/- comment
/* block comment */
inline /*comment*/ here
another /-commend there
after some whitespace
trailing /* multiline */
trailing // single line
"#;
let _doc: KdlDocument = src.parse()?;
Ok(())
}
#[test]
fn parsing() -> miette::Result<()> {
let src = "
// This is the first node
foo 1 2 \"three\" null true bar=\"baz\" {
foo 1 2 three #null #true bar=\"baz\" {
- 1
- 2
- \"three\"
(mytype)something (\"name\")\"else\"\r
- three
(mytype)something (\"name\")else\r
}
null_id null_prop=null
true_id true_prop=null
+false true
null_id null_prop=#null
true_id true_prop=#null
+false #true
bar \"indented\" // trailing whitespace after this\t
/*
@ -440,19 +462,22 @@ Some random comment
a; b; c;
/-commented \"node\"
another /*foo*/ \"node\" /-1 /*bar*/ null;
another /*foo*/ \"node\" /-1 /*bar*/ #null;
final;";
let mut doc: KdlDocument = src.parse()?;
assert_eq!(doc.leading, Some("".into()));
assert_eq!(doc.get_arg("foo"), Some(&1.into()));
assert_eq!(
doc.get_dash_args("foo"),
vec![&1.into(), &2.into(), &"three".into()]
);
assert_eq!(
doc.format().map(|f| &f.leading[..]),
Some("\n// This is the first node\n")
);
let foo = doc.get("foo").expect("expected a foo node");
assert_eq!(foo.leading, Some("\n// This is the first node\n".into()));
assert_eq!(foo.format().map(|f| &f.trailing[..]), Some("\n"));
assert_eq!(&foo[2], &"three".into());
assert_eq!(&foo["bar"], &"baz".into());
assert_eq!(
@ -479,19 +504,21 @@ final;";
let a = doc.get("a").expect("expected a node");
assert_eq!(
format!("{}", a),
"/*\nSome random comment\n */\n\na; ".to_string()
"/*\nSome random comment\n */\n\na;".to_string()
);
let b = doc.get("b").expect("expected a node");
assert_eq!(format!("{}", b), "b; ".to_string());
assert_eq!(format!("{}", b), " b;".to_string());
// Round-tripping works.
assert_eq!(format!("{}", doc), src);
// Programmatic manipulation works.
let mut node: KdlNode = "new\n".parse()?;
// Manual entry parsing preserves formatting/reprs.
node.push("\"blah\"=0xDEADbeef".parse::<KdlEntry>()?);
// Manual entry parsing preserves formatting/reprs. Note that
// if you're making KdlEntries this way, you need to inject
// your own whitespace (or format the node)
node.push(" \"blah\"=0xDEADbeef".parse::<KdlEntry>()?);
doc.nodes_mut().push(node);
assert_eq!(
@ -519,9 +546,11 @@ final;";
doc.nodes_mut().push(bar);
doc.nodes_mut().push(KdlNode::new("baz"));
doc.autoformat();
assert_eq!(
r#"foo
bar prop="value" 1 2 false null {
bar prop=value 1 2 #false #null {
barchild
}
baz
@ -530,9 +559,10 @@ baz
);
}
#[ignore = "There's still issues around formatting comments and esclines."]
#[test]
fn fmt() -> miette::Result<()> {
let mut doc: KdlDocument = r#"
fn autoformat() -> miette::Result<()> {
let mut doc: KdlDocument = r##"
/* x */ foo 1 "bar"=0xDEADbeef {
child1 1 ;
@ -543,14 +573,15 @@ baz
child3 "
string\t" \
string\t
" \
{
/*
multiline*/
inner1 \
r"value" \
#"value"# \
;
inner2 \ //comment
@ -562,12 +593,11 @@ baz
// trailing comment here
"#
"##
.parse()?;
KdlDocument::fmt(&mut doc);
KdlDocument::autoformat(&mut doc);
print!("{}", doc);
assert_eq!(
doc.to_string(),
r#"/* x */
@ -575,12 +605,12 @@ foo 1 bar=0xdeadbeef {
child1 1
// child 2 comment
child2 2 // comment
child3 "\n\n string\t" {
child3 "\nstring\t" {
/*
multiline*/
inner1 r"value"
inner1 value
inner2 {
inner3
}
@ -592,10 +622,9 @@ foo 1 bar=0xdeadbeef {
}
#[test]
fn simple_fmt() -> miette::Result<()> {
fn simple_autoformat() -> miette::Result<()> {
let mut doc: KdlDocument = "a { b { c { }; }; }".parse().unwrap();
KdlDocument::fmt(&mut doc);
print!("{}", doc);
KdlDocument::autoformat(&mut doc);
assert_eq!(
doc.to_string(),
r#"a {
@ -619,6 +648,8 @@ foo 1 bar=0xdeadbeef {
#[cfg(feature = "span")]
fn check_spans_for_node(node: &KdlNode, source: &impl miette::SourceCode) {
use crate::KdlEntryFormat;
check_span_for_ident(node.name(), source);
if let Some(ty) = node.ty() {
check_span_for_ident(ty, source);
@ -631,9 +662,9 @@ foo 1 bar=0xdeadbeef {
if let Some(ty) = entry.ty() {
check_span_for_ident(ty, source);
}
if let Some(repr) = entry.value_repr() {
if let Some(KdlEntryFormat { value_repr, .. }) = entry.format() {
if entry.name().is_none() && entry.ty().is_none() {
check_span(repr, entry.span(), source);
check_span(value_repr, entry.span(), source);
}
}
}
@ -667,7 +698,7 @@ foo 1 bar=0xdeadbeef {
this {
is (a)"cool" document="to" read=(int)5 10.1 (u32)0x45
and x="" {
"it" /*shh*/ "has"="💯" r##"the"##
"it" /*shh*/ "has"="💯" ##"the"##
Best🎊est
"syntax ever"
}
@ -678,7 +709,7 @@ nice
inline { time; to; live "our" "dreams"; "y;all"; }
"####;
let doc: KdlDocument = input.parse().unwrap();
let doc: KdlDocument = input.parse()?;
// First check that all the identity-spans are correct
check_spans_for_doc(&doc, &input);
@ -724,7 +755,7 @@ inline { time; to; live "our" "dreams"; "y;all"; }
// The node is what you expect, the whole line and its two braces
check_span(
r####"and x="" {
"it" /*shh*/ "has"="💯" r##"the"##
"it" /*shh*/ "has"="💯" ##"the"##
Best🎊est
"syntax ever"
}"####,
@ -735,11 +766,11 @@ inline { time; to; live "our" "dreams"; "y;all"; }
// The child document is a little weird, it's the contents *inside* the braces
// with extra newlines on both ends.
check_span(
r####"
"it" /*shh*/ "has"="💯" r##"the"##
r####"{
"it" /*shh*/ "has"="💯" ##"the"##
Best🎊est
"syntax ever"
"####,
}"####,
and_node.children().unwrap().span(),
&input,
);
@ -750,7 +781,7 @@ inline { time; to; live "our" "dreams"; "y;all"; }
// Now the "it" node, more straightforward
let it_node = and_node.children().unwrap().get("it").unwrap();
check_span(
r####""it" /*shh*/ "has"="💯" r##"the"##"####,
r####""it" /*shh*/ "has"="💯" ##"the"##"####,
it_node.span(),
&input,
);
@ -760,7 +791,7 @@ inline { time; to; live "our" "dreams"; "y;all"; }
&input,
);
check_span(
r####"r##"the"##"####,
r####"##"the"##"####,
it_node.entry(0).unwrap().span(),
&input,
);
@ -775,7 +806,7 @@ inline { time; to; live "our" "dreams"; "y;all"; }
let inline_children = inline_node.children().unwrap();
check_span(
r#" time; to; live "our" "dreams"; "y;all"; "#,
r#"{ time; to; live "our" "dreams"; "y;all"; }"#,
inline_children.span(),
&input,
);

View File

@ -2,7 +2,7 @@
use miette::SourceSpan;
use std::{fmt::Display, str::FromStr};
use crate::{parser, KdlError, KdlIdentifier, KdlValue};
use crate::{v2_parser, KdlIdentifier, KdlParseFailure, KdlValue};
/// KDL Entries are the "arguments" to KDL nodes: either a (positional)
/// [`Argument`](https://github.com/kdl-org/kdl/blob/main/SPEC.md#argument) or
@ -10,36 +10,30 @@ use crate::{parser, KdlError, KdlIdentifier, KdlValue};
/// [`Property`](https://github.com/kdl-org/kdl/blob/main/SPEC.md#property)
#[derive(Debug, Clone, Eq)]
pub struct KdlEntry {
pub(crate) leading: Option<String>,
pub(crate) ty: Option<KdlIdentifier>,
pub(crate) value: KdlValue,
pub(crate) value_repr: Option<String>,
pub(crate) name: Option<KdlIdentifier>,
pub(crate) trailing: Option<String>,
pub(crate) format: Option<KdlEntryFormat>,
#[cfg(feature = "span")]
pub(crate) span: SourceSpan,
}
impl PartialEq for KdlEntry {
fn eq(&self, other: &Self) -> bool {
self.leading == other.leading
&& self.ty == other.ty
self.ty == other.ty
&& self.value == other.value
&& self.value_repr == other.value_repr
&& self.name == other.name
&& self.trailing == other.trailing
&& self.format == other.format
// intentionally omitted: self.span == other.span
}
}
impl std::hash::Hash for KdlEntry {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.leading.hash(state);
self.ty.hash(state);
self.value.hash(state);
self.value_repr.hash(state);
self.name.hash(state);
self.trailing.hash(state);
self.format.hash(state);
// intentionally omitted: self.span.hash(state)
}
}
@ -48,14 +42,12 @@ impl KdlEntry {
/// Creates a new Argument (positional) KdlEntry.
pub fn new(value: impl Into<KdlValue>) -> Self {
KdlEntry {
leading: None,
ty: None,
value: value.into(),
value_repr: None,
name: None,
trailing: None,
format: None,
#[cfg(feature = "span")]
span: SourceSpan::from(0..0),
span: (0..0).into(),
}
}
@ -64,6 +56,16 @@ impl KdlEntry {
self.name.as_ref()
}
/// Gets a mutable reference to this node's name.
pub fn name_mut(&mut self) -> Option<&mut KdlIdentifier> {
self.name.as_mut()
}
/// Sets this node's name.
pub fn set_name(&mut self, name: Option<impl Into<KdlIdentifier>>) {
self.name = name.map(|x| x.into());
}
/// Gets the entry's value.
pub fn value(&self) -> &KdlValue {
&self.value
@ -110,64 +112,45 @@ impl KdlEntry {
self.ty = Some(ty.into());
}
/// Gets the formatting details for this entry.
pub fn format(&self) -> Option<&KdlEntryFormat> {
self.format.as_ref()
}
/// Gets a mutable reference to this entry's formatting details.
pub fn format_mut(&mut self) -> Option<&mut KdlEntryFormat> {
self.format.as_mut()
}
/// Sets the formatting details for this entry.
pub fn set_format(&mut self, format: KdlEntryFormat) {
self.format = Some(format);
}
/// Creates a new Property (key/value) KdlEntry.
pub fn new_prop(key: impl Into<KdlIdentifier>, value: impl Into<KdlValue>) -> Self {
KdlEntry {
leading: None,
ty: None,
value: value.into(),
value_repr: None,
name: Some(key.into()),
trailing: None,
format: None,
#[cfg(feature = "span")]
span: SourceSpan::from(0..0),
}
}
/// Gets leading text (whitespace, comments) for this KdlEntry.
pub fn leading(&self) -> Option<&str> {
self.leading.as_deref()
}
/// Sets leading text (whitespace, comments) for this KdlEntry.
pub fn set_leading(&mut self, leading: impl Into<String>) {
self.leading = Some(leading.into());
}
/// Gets trailing text (whitespace, comments) for this KdlEntry.
pub fn trailing(&self) -> Option<&str> {
self.trailing.as_deref()
}
/// Sets trailing text (whitespace, comments) for this KdlEntry.
pub fn set_trailing(&mut self, trailing: impl Into<String>) {
self.trailing = Some(trailing.into());
}
/// Clears leading and trailing text (whitespace, comments), as well as
/// resetting this entry's value to its default representation.
pub fn clear_fmt(&mut self) {
self.leading = None;
self.trailing = None;
self.value_repr = None;
pub fn clear_format(&mut self) {
self.format = None;
if let Some(ty) = &mut self.ty {
ty.clear_fmt();
ty.clear_format();
}
if let Some(name) = &mut self.name {
name.clear_fmt();
name.clear_format();
}
}
/// Gets the custom string representation for this KdlEntry's [`KdlValue`].
pub fn value_repr(&self) -> Option<&str> {
self.value_repr.as_deref()
}
/// Sets a custom string representation for this KdlEntry's [`KdlValue`].
pub fn set_value_repr(&mut self, repr: impl Into<String>) {
self.value_repr = Some(repr.into());
}
/// Length of this entry when rendered as a string.
pub fn len(&self) -> usize {
format!("{}", self).len()
@ -179,33 +162,54 @@ impl KdlEntry {
}
/// Auto-formats this entry.
pub fn fmt(&mut self) {
self.leading = None;
self.trailing = None;
self.value_repr = None;
pub fn autoformat(&mut self) {
self.format = None;
if let Some(name) = &mut self.name {
name.fmt();
name.autoformat();
}
}
}
impl Display for KdlEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(leading) = &self.leading {
if let Some(KdlEntryFormat { leading, .. }) = &self.format {
write!(f, "{}", leading)?;
}
if let Some(name) = &self.name {
write!(f, "{}=", name)?;
write!(f, "{}", name)?;
if let Some(KdlEntryFormat {
after_key,
after_eq,
..
}) = &self.format
{
write!(f, "{}={}", after_key, after_eq)?;
} else {
write!(f, "=")?;
}
}
if let Some(ty) = &self.ty {
write!(f, "({})", ty)?;
write!(f, "(")?;
if let Some(KdlEntryFormat { before_ty_name, .. }) = &self.format {
write!(f, "{}", before_ty_name)?;
}
write!(f, "{}", ty)?;
if let Some(KdlEntryFormat { after_ty_name, .. }) = &self.format {
write!(f, "{}", after_ty_name)?;
}
write!(f, ")")?;
}
if let Some(repr) = &self.value_repr {
write!(f, "{}", repr)?;
if let Some(KdlEntryFormat {
after_ty,
value_repr,
..
}) = &self.format
{
write!(f, "{}{}", after_ty, value_repr)?;
} else {
write!(f, "{}", self.value)?;
}
if let Some(trailing) = &self.trailing {
if let Some(KdlEntryFormat { trailing, .. }) = &self.format {
write!(f, "{}", trailing)?;
}
Ok(())
@ -232,14 +236,42 @@ where
}
impl FromStr for KdlEntry {
type Err = KdlError;
type Err = KdlParseFailure;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let kdl_parser = parser::KdlParser::new(s);
kdl_parser.parse(parser::entry_with_trailing(&kdl_parser))
let (maybe_val, errs) = v2_parser::try_parse(v2_parser::padded_node_entry, s);
if let (Some(Some(v)), true) = (maybe_val, errs.is_empty()) {
Ok(v)
} else {
Err(v2_parser::failure_from_errs(errs, s))
}
}
}
/// Formatting details for [`KdlEntry`]s.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct KdlEntryFormat {
/// The actual text representation of the entry's value.
pub value_repr: String,
/// Whitespace and comments preceding the entry itself.
pub leading: String,
/// Whitespace and comments following the entry itself.
pub trailing: String,
/// Whitespace and comments after the entry's type annotation's closing
/// `)`, before its value.
pub after_ty: String,
/// Whitespace and comments between the opening `(` of an entry's type
/// annotation and its actual type name.
pub before_ty_name: String,
/// Whitespace and comments between the actual type name and the closing
/// `)` in an entry's type annotation.
pub after_ty_name: String,
/// Whitespace and comments between an entry's key name and its equals sign.
pub after_key: String,
/// Whitespace and comments between an entry's equals sign and its value.
pub after_eq: String,
}
#[cfg(test)]
mod test {
use super::*;
@ -249,8 +281,8 @@ mod test {
let mut left_entry: KdlEntry = " name=1.03e2".parse()?;
let mut right_entry: KdlEntry = " name=103.0".parse()?;
assert_ne!(left_entry, right_entry);
left_entry.clear_fmt();
right_entry.clear_fmt();
left_entry.clear_format();
right_entry.clear_format();
assert_eq!(left_entry, right_entry);
Ok(())
}
@ -261,12 +293,10 @@ mod test {
assert_eq!(
entry,
KdlEntry {
leading: None,
ty: None,
value: KdlValue::Base10(42),
value_repr: None,
value: KdlValue::Integer(42),
name: None,
trailing: None,
format: None,
#[cfg(feature = "span")]
span: SourceSpan::from(0..0),
}
@ -276,12 +306,10 @@ mod test {
assert_eq!(
entry,
KdlEntry {
leading: None,
ty: None,
value: KdlValue::Base10(42),
value_repr: None,
value: KdlValue::Integer(42),
name: Some("name".into()),
trailing: None,
format: None,
#[cfg(feature = "span")]
span: SourceSpan::from(0..0),
}
@ -290,18 +318,55 @@ mod test {
#[test]
fn parsing() -> miette::Result<()> {
let entry: KdlEntry = " \\\n (\"m\\\"eh\")0xDEADbeef\t\\\n".parse()?;
let entry: KdlEntry = "foo".parse()?;
assert_eq!(
entry,
KdlEntry {
leading: Some(" \\\n ".into()),
ty: Some("\"m\\\"eh\"".parse()?),
value: KdlValue::Base16(0xdeadbeef),
value_repr: Some("0xDEADbeef".into()),
ty: None,
value: KdlValue::from("foo"),
name: None,
trailing: Some("\t\\\n".into()),
format: Some(KdlEntryFormat {
value_repr: "foo".into(),
..Default::default()
}),
#[cfg(feature = "span")]
span: SourceSpan::from(0..0),
span: SourceSpan::from(0..3),
}
);
let entry: KdlEntry = "foo=bar".parse()?;
assert_eq!(
entry,
KdlEntry {
ty: None,
value: KdlValue::from("bar"),
name: Some("foo".parse()?),
format: Some(KdlEntryFormat {
value_repr: "bar".into(),
..Default::default()
}),
#[cfg(feature = "span")]
span: SourceSpan::from(0..7),
}
);
let entry: KdlEntry = " \\\n (\"m\\\"eh\")0xDEADbeef\t\\\n".parse()?;
let mut ty: KdlIdentifier = "\"m\\\"eh\"".parse()?;
ty.span = (5..12).into();
assert_eq!(
entry,
KdlEntry {
ty: Some(ty),
value: KdlValue::Integer(0xdeadbeef),
name: None,
format: Some(KdlEntryFormat {
leading: " \\\n ".into(),
trailing: "\t\\\n".into(),
value_repr: "0xDEADbeef".into(),
..Default::default()
}),
#[cfg(feature = "span")]
span: SourceSpan::from(0..26),
}
);
@ -309,12 +374,19 @@ mod test {
assert_eq!(
entry,
KdlEntry {
leading: Some(" \\\n ".into()),
format: Some(KdlEntryFormat {
leading: " \\\n ".into(),
trailing: "\t\\\n".into(),
value_repr: "0xDEADbeef".into(),
before_ty_name: "".into(),
after_ty_name: "".into(),
after_ty: "".into(),
after_key: "".into(),
after_eq: "".into(),
}),
ty: Some("\"m\\\"eh\"".parse()?),
value: KdlValue::Base16(0xdeadbeef),
value_repr: Some("0xDEADbeef".into()),
value: KdlValue::Integer(0xdeadbeef),
name: Some("\"foo\"".parse()?),
trailing: Some("\t\\\n".into()),
#[cfg(feature = "span")]
span: SourceSpan::from(0..0),
}
@ -325,10 +397,10 @@ mod test {
#[test]
fn display() {
let entry = KdlEntry::new(KdlValue::Base10(42));
let entry = KdlEntry::new(KdlValue::Integer(42));
assert_eq!(format!("{}", entry), "42");
let entry = KdlEntry::new_prop("name", KdlValue::Base10(42));
let entry = KdlEntry::new_prop("name", KdlValue::Integer(42));
assert_eq!(format!("{}", entry), "name=42");
}
}

View File

@ -1,4 +1,7 @@
use std::num::{ParseFloatError, ParseIntError};
use std::{
num::{ParseFloatError, ParseIntError},
sync::Arc,
};
use miette::{Diagnostic, SourceSpan};
use nom::error::{ContextError, ErrorKind, FromExternalError, ParseError};
@ -10,11 +13,12 @@ use {
std::convert::{TryFrom, TryInto},
};
/// An error that occurs when parsing a KDL document.
/// The toplevel `Error` type for KDL: this is returned when a KDL document
/// failed to parse entirely.
///
/// This error implements [`miette::Diagnostic`] and can be used to display
/// detailed, pretty-printed diagnostic messages when using [`miette::Result`]
/// and the `"fancy"` feature flag for `miette`:
/// This diagnostic implements [`miette::Diagnostic`] and can be used to
/// display detailed, pretty-printed diagnostic messages when using
/// [`miette::Result`] and the `"fancy"` feature flag for `miette`:
///
/// ```no_run
/// fn main() -> miette::Result<()> {
@ -35,11 +39,26 @@ use {
/// help: Floating point numbers must be base 10, and have numbers after the decimal point.
/// ```
#[derive(Debug, Diagnostic, Clone, Eq, PartialEq, Error)]
#[error("{kind}")]
pub struct KdlError {
/// Source string for the KDL document that failed to parse.
#[error("Failed to parse KDL.")]
pub struct KdlParseFailure {
/// Original input that this failure came from.
#[source_code]
pub input: String,
pub input: Arc<String>,
/// Sub-diagnostics for this failure.
#[related]
pub diagnostics: Vec<KdlDiagnostic>,
}
/// An individual diagnostic message for a KDL parsing issue.
///
/// While generally signifying errors, they can also be treated as warnings.
#[derive(Debug, Diagnostic, Clone, Eq, PartialEq, Error)]
#[error("{kind}")]
pub struct KdlDiagnostic {
/// Shared source for the diagnostic.
#[source_code]
pub input: Arc<String>,
/// Offset in chars of the error.
#[label("{}", label.unwrap_or("here"))]
@ -52,6 +71,10 @@ pub struct KdlError {
#[help]
pub help: Option<&'static str>,
/// Severity level for the Diagnostic.
#[diagnostic(severity)]
pub severity: miette::Severity,
/// Specific error kind for this parser error.
pub kind: KdlErrorKind,
}
@ -77,7 +100,7 @@ pub enum KdlErrorKind {
/// Generic unspecified error. If this is returned, the call site should
/// be annotated with context, if possible.
#[error("An unspecified error occurred.")]
#[error("An unspecified parse error occurred.")]
#[diagnostic(code(kdl::other))]
Other,
}

View File

@ -1,20 +1,15 @@
use std::fmt::Write as _;
pub(crate) fn fmt_leading(leading: &mut String, indent: usize, no_comments: bool) {
if leading.is_empty() {
return;
}
pub(crate) fn autoformat_leading(leading: &mut String, indent: usize, no_comments: bool) {
let mut result = String::new();
if !no_comments {
let input = leading.trim();
let kdl_parser = crate::parser::KdlParser { full_input: input };
let comments = kdl_parser
.parse(crate::parser::leading_comments(&kdl_parser))
.expect("invalid leading text");
for line in comments {
let trimmed = line.trim();
if !trimmed.is_empty() {
writeln!(result, "{:indent$}{}", "", trimmed, indent = indent).unwrap();
if !input.is_empty() {
for line in input.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
writeln!(result, "{:indent$}{}", "", trimmed, indent = indent).unwrap();
}
}
}
}
@ -22,20 +17,18 @@ pub(crate) fn fmt_leading(leading: &mut String, indent: usize, no_comments: bool
*leading = result;
}
pub(crate) fn fmt_trailing(decor: &mut String, no_comments: bool) {
pub(crate) fn autoformat_trailing(decor: &mut String, no_comments: bool) {
if decor.is_empty() {
return;
}
*decor = decor.trim().to_string();
let mut result = String::new();
if !no_comments {
let input = &*decor;
let kdl_parser = crate::parser::KdlParser { full_input: input };
let comments = kdl_parser
.parse(crate::parser::trailing_comments(&kdl_parser))
.expect("invalid trailing text");
for comment in comments {
result.push_str(comment);
if !decor.is_empty() && !no_comments {
if decor.trim_start() == &decor[..] {
write!(result, " ").unwrap();
}
for comment in decor.lines() {
writeln!(result, "{comment}").unwrap();
}
}
*decor = result;

View File

@ -2,7 +2,7 @@
use miette::SourceSpan;
use std::{fmt::Display, str::FromStr};
use crate::{parser, KdlError};
use crate::{v2_parser, KdlParseFailure, KdlValue};
/// Represents a KDL
/// [Identifier](https://github.com/kdl-org/kdl/blob/main/SPEC.md#identifier).
@ -79,12 +79,12 @@ impl KdlIdentifier {
/// Resets this identifier to its default representation. It will attempt
/// to make it an unquoted identifier, and fall back to a string
/// representation if that would be invalid.
pub fn clear_fmt(&mut self) {
pub fn clear_format(&mut self) {
self.repr = None;
}
/// Auto-formats this identifier.
pub fn fmt(&mut self) {
pub fn autoformat(&mut self) {
self.repr = None;
}
}
@ -93,85 +93,12 @@ impl Display for KdlIdentifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(repr) = &self.repr {
write!(f, "{}", repr)
} else if self.plain_value() {
write!(f, "{}", self.value)
} else {
write!(f, "{:?}", self.value)
write!(f, "{}", KdlValue::String(self.value().into()))
}
}
}
impl KdlIdentifier {
pub(crate) fn is_identifier_char(c: char) -> bool {
!((c as u32) < 0x20
|| (c as u32) > 0x10ffff
|| matches!(
c,
'\\' | '/'
| '('
| ')'
| '{'
| '}'
| '<'
| '>'
| ';'
| '['
| ']'
| '='
| ','
| '"'
// Newlines
| '\r'
| '\n'
| '\u{0085}'
| '\u{000C}'
| '\u{2028}'
| '\u{2029}'
// Whitespace
| ' '
| '\t'
| '\u{FEFF}'
| '\u{00A0}'
| '\u{1680}'
| '\u{2000}'
| '\u{2001}'
| '\u{2002}'
| '\u{2003}'
| '\u{2004}'
| '\u{2005}'
| '\u{2006}'
| '\u{2007}'
| '\u{2008}'
| '\u{2009}'
| '\u{200A}'
| '\u{202F}'
| '\u{205F}'
| '\u{3000}'
))
}
pub(crate) fn is_initial_char(c: char) -> bool {
!c.is_numeric() && Self::is_identifier_char(c)
}
fn plain_value(&self) -> bool {
let mut iter = self.value.chars();
if let Some(c) = iter.next() {
if !Self::is_initial_char(c) {
return false;
}
} else {
return false;
}
for char in iter {
if !Self::is_identifier_char(char) {
return false;
}
}
true
}
}
impl From<&str> for KdlIdentifier {
fn from(value: &str) -> Self {
KdlIdentifier {
@ -201,11 +128,15 @@ impl From<KdlIdentifier> for String {
}
impl FromStr for KdlIdentifier {
type Err = KdlError;
type Err = KdlParseFailure;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let kdl_parser = crate::parser::KdlParser::new(s);
kdl_parser.parse(parser::identifier(&kdl_parser))
let (maybe_val, errs) = v2_parser::try_parse(v2_parser::identifier, s);
if let Some(v) = maybe_val {
Ok(v)
} else {
Err(v2_parser::failure_from_errs(errs, s))
}
}
}
@ -226,7 +157,7 @@ mod test {
}
);
let quoted = "\"foo\\\"bar\"";
let quoted = r#""foo\"bar""#;
assert_eq!(
quoted.parse::<KdlIdentifier>()?,
KdlIdentifier {

View File

@ -64,38 +64,6 @@
//! [`KdlDocument`], [`KdlNode`], [`KdlEntry`], and [`KdlIdentifier`] can all
//! be parsed and managed this way.
//!
//! ### Query Engine
//!
//! `kdl` includes a query engine for
//! [KQL](https://github.com/kdl-org/kdl/blob/main/QUERY-SPEC.md), which lets you
//! pick out nodes from a document using a CSS Selectors-style syntax.
//!
//! Queries can be done from either a [`KdlDocument`] or a [`KdlNode`], with
//! mostly the same semantics.
//!
//! ```rust
//! use kdl::KdlDocument;
//!
//! let doc = r#"
//! a {
//! b 1
//! c 2
//! d 3 {
//! e prop="hello"
//! }
//! }
//! "#.parse::<KdlDocument>().expect("failed to parse KDL");
//!
//! let results = doc.query("a > b").expect("failed to parse query");
//! assert_eq!(results, Some(&doc.nodes()[0].children().unwrap().nodes()[0]));
//!
//! let results = doc.query_get("e", "prop").expect("failed to parse query");
//! assert_eq!(results, Some(&"hello".into()));
//!
//! let results = doc.query_get_all("a > []", 0).expect("failed to parse query").collect::<Vec<_>>();
//! assert_eq!(results, vec![&1.into(), &2.into(), &3.into()]);
//! ```
//!
//! ## Error Reporting
//!
//! [`KdlError`] implements [`miette::Diagnostic`] and can be used to display
@ -152,14 +120,51 @@
//! you [`KdlDocument::fmt`] in which case the original representation will be
//! thrown away and the actual value will be used when serializing.
//!
//! ## Minimum Supported Rust Version
//!
//! You must be at least `1.70.0` tall to get on this ride.
//!
//! ## License
//!
//! The code in this repository is covered by [the Apache-2.0
//! License](LICENSE.md).
// TODO(@zkat): bring this back later.
// ### Query Engine
// `kdl` includes a query engine for
// [KQL](https://github.com/kdl-org/kdl/blob/main/QUERY-SPEC.md), which lets you
// pick out nodes from a document using a CSS Selectors-style syntax.
// Queries can be done from either a [`KdlDocument`] or a [`KdlNode`], with
// mostly the same semantics.
// ```rust
// use kdl::KdlDocument;
// let doc = r#"
// a {
// b 1
// c 2
// d 3 {
// e prop="hello"
// }
// }
// "#.parse::<KdlDocument>().expect("failed to parse KDL");
// let results = doc.query("a > b").expect("failed to parse query");
// assert_eq!(results, Some(&doc.nodes()[0].children().unwrap().nodes()[0]));
// let results = doc.query_get("e", "prop").expect("failed to parse query");
// assert_eq!(results, Some(&"hello".into()));
// let results = doc.query_get_all("a > []", 0).expect("failed to parse query").collect::<Vec<_>>();
// assert_eq!(results, vec![&1.into(), &2.into(), &3.into()]);
// ```
#![deny(missing_debug_implementations, nonstandard_style)]
#![warn(missing_docs, unreachable_pub, rust_2018_idioms, unreachable_pub)]
#![cfg_attr(test, deny(warnings))]
#![warn(missing_docs, rust_2018_idioms, unreachable_pub)]
// #![cfg_attr(test, deny(warnings))]
#![doc(html_favicon_url = "https://kdl.dev/favicon.ico")]
#![doc(html_logo_url = "https://kdl.dev/logo.svg")]
@ -168,7 +173,7 @@ pub use entry::*;
pub use error::*;
pub use identifier::*;
pub use node::*;
pub use query::*;
// pub use query::*;
pub use value::*;
mod document;
@ -177,8 +182,10 @@ mod error;
mod fmt;
mod identifier;
mod node;
mod nom_compat;
mod parser;
mod query;
mod query_parser;
// mod nom_compat;
// mod query;
// mod query_parser;
// mod v1_parser;
mod value;
mod v2_parser;

View File

@ -8,49 +8,44 @@ use std::{
use miette::SourceSpan;
use crate::{
parser, IntoKdlQuery, KdlDocument, KdlEntry, KdlError, KdlIdentifier, KdlQueryIterator,
KdlValue,
v2_parser, 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.
#[derive(Debug, Clone, Eq)]
pub struct KdlNode {
pub(crate) leading: Option<String>,
pub(crate) ty: Option<KdlIdentifier>,
pub(crate) name: KdlIdentifier,
// TODO: consider using `hashlink` for this instead, later.
pub(crate) entries: Vec<KdlEntry>,
pub(crate) before_children: Option<String>,
pub(crate) children: Option<KdlDocument>,
pub(crate) trailing: Option<String>,
pub(crate) format: Option<KdlNodeFormat>,
#[cfg(feature = "span")]
pub(crate) span: SourceSpan,
}
impl PartialEq for KdlNode {
fn eq(&self, other: &Self) -> bool {
self.leading == other.leading
&& self.ty == other.ty
self.ty == other.ty
&& self.name == other.name
&& self.entries == other.entries
&& self.before_children == other.before_children
&& self.children == other.children
&& self.trailing == other.trailing
&& self.format == other.format
// intentionally omitted: self.span == other.span
}
}
impl std::hash::Hash for KdlNode {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.leading.hash(state);
self.ty.hash(state);
self.name.hash(state);
self.entries.hash(state);
self.before_children.hash(state);
self.children.hash(state);
self.trailing.hash(state);
self.format.hash(state);
// Intentionally omitted: self.span.hash(state);
}
}
@ -60,12 +55,13 @@ impl KdlNode {
pub fn new(name: impl Into<KdlIdentifier>) -> Self {
Self {
name: name.into(),
leading: None,
ty: None,
entries: Vec::new(),
before_children: None,
children: None,
trailing: None,
format: Some(KdlNodeFormat {
trailing: "\n".into(),
..Default::default()
}),
#[cfg(feature = "span")]
span: SourceSpan::from(0..0),
}
@ -128,36 +124,6 @@ impl KdlNode {
&mut self.entries
}
/// Gets leading text (whitespace, comments) for this node.
pub fn leading(&self) -> Option<&str> {
self.leading.as_deref()
}
/// Sets leading text (whitespace, comments) for this node.
pub fn set_leading(&mut self, leading: impl Into<String>) {
self.leading = Some(leading.into());
}
/// Gets text (whitespace, comments) right before the children block's starting `{`.
pub fn before_children(&self) -> Option<&str> {
self.before_children.as_deref()
}
/// Gets text (whitespace, comments) right before the children block's starting `{`.
pub fn set_before_children(&mut self, before: impl Into<String>) {
self.before_children = Some(before.into());
}
/// Gets trailing text (whitespace, comments) for this node.
pub fn trailing(&self) -> Option<&str> {
self.trailing.as_deref()
}
/// Sets trailing text (whitespace, comments) for this node.
pub fn set_trailing(&mut self, trailing: impl Into<String>) {
self.trailing = Some(trailing.into());
}
/// Length of this node when rendered as a string.
pub fn len(&self) -> usize {
format!("{}", self).len()
@ -174,23 +140,21 @@ impl KdlNode {
///
/// If you want to clear formatting on all children and entries as well,
/// use [`Self::clear_fmt_recursive`].
pub fn clear_fmt(&mut self) {
self.leading = None;
self.trailing = None;
self.before_children = None;
pub fn clear_format(&mut self) {
self.format = None;
}
/// Clears leading and trailing text (whitespace, comments), as well as
/// the space before the children block, if any. Individual entries and
/// children formatting will also be cleared.
pub fn clear_fmt_recursive(&mut self) {
self.clear_fmt();
self.name.clear_fmt();
pub fn clear_format_recursive(&mut self) {
self.clear_format();
self.name.clear_format();
if let Some(children) = &mut self.children {
children.clear_fmt_recursive();
children.clear_format_recursive();
}
for entry in self.entries.iter_mut() {
entry.clear_fmt();
entry.clear_format();
}
}
@ -408,54 +372,71 @@ impl KdlNode {
self.children_mut().as_mut().unwrap()
}
/// Gets the formatting details for this node.
pub fn format(&self) -> Option<&KdlNodeFormat> {
self.format.as_ref()
}
/// Gets a mutable reference to this node's formatting details.
pub fn format_mut(&mut self) -> Option<&mut KdlNodeFormat> {
self.format.as_mut()
}
/// Sets the formatting details for this node.
pub fn set_format(&mut self, format: KdlNodeFormat) {
self.format = Some(format);
}
/// Auto-formats this node and its contents.
pub fn fmt(&mut self) {
self.fmt_impl(0, false);
pub fn autoformat(&mut self) {
self.autoformat_impl(0, false);
}
/// Auto-formats this node and its contents, stripping comments.
pub fn fmt_no_comments(&mut self) {
self.fmt_impl(0, true);
pub fn autoformat_no_comments(&mut self) {
self.autoformat_impl(0, true);
}
/// Queries this Node according to the KQL query language,
/// returning an iterator over all matching nodes.
pub fn query_all(&self, query: impl IntoKdlQuery) -> Result<KdlQueryIterator<'_>, KdlError> {
let q = query.into_query()?;
Ok(KdlQueryIterator::new(Some(self), None, q))
}
// TODO(@zkat): These should all be moved into the query module, instead
// of being model methods.
//
// /// Queries this Node according to the KQL
// query language, /// returning an iterator over all matching nodes. pub
// fn query_all( &self, query: impl IntoKdlQuery, ) ->
// Result<KdlQueryIterator<'_>, KdlDiagnostic> { let q =
// query.into_query()?; Ok(KdlQueryIterator::new(Some(self), None, q))
// }
/// Queries this Node according to the KQL query language,
/// returning the first match, if any.
pub fn query(&self, query: impl IntoKdlQuery) -> Result<Option<&KdlNode>, KdlError> {
Ok(self.query_all(query)?.next())
}
// /// Queries this Node according to the KQL query language,
// /// returning the first match, if any.
// pub fn query(&self, query: impl IntoKdlQuery) -> Result<Option<&KdlNode>, KdlDiagnostic> {
// Ok(self.query_all(query)?.next())
// }
/// Queries this Node according to the KQL query language,
/// picking the first match, and calling `.get(key)` on it, if the query
/// succeeded.
pub fn query_get(
&self,
query: impl IntoKdlQuery,
key: impl Into<NodeKey>,
) -> Result<Option<&KdlValue>, KdlError> {
Ok(self.query(query)?.and_then(|node| node.get(key)))
}
// /// Queries this Node according to the KQL query language,
// /// picking the first match, and calling `.get(key)` on it, if the query
// /// succeeded.
// pub fn query_get(
// &self,
// query: impl IntoKdlQuery,
// key: impl Into<NodeKey>,
// ) -> Result<Option<&KdlValue>, KdlDiagnostic> {
// Ok(self.query(query)?.and_then(|node| node.get(key)))
// }
/// Queries this Node according to the KQL query language,
/// returning an iterator over all matching nodes, returning the requested
/// field from each of those nodes and filtering out nodes that don't have
/// it.
pub fn query_get_all(
&self,
query: impl IntoKdlQuery,
key: impl Into<NodeKey>,
) -> Result<impl Iterator<Item = &KdlValue>, KdlError> {
let key: NodeKey = key.into();
Ok(self
.query_all(query)?
.filter_map(move |node| node.get(key.clone())))
}
// /// Queries this Node according to the KQL query language,
// /// returning an iterator over all matching nodes, returning the requested
// /// field from each of those nodes and filtering out nodes that don't have
// /// it.
// pub fn query_get_all(
// &self,
// query: impl IntoKdlQuery,
// key: impl Into<NodeKey>,
// ) -> Result<impl Iterator<Item = &KdlValue>, KdlDiagnostic> {
// let key: NodeKey = key.into();
// Ok(self
// .query_all(query)?
// .filter_map(move |node| node.get(key.clone())))
// }
}
/// Represents a [`KdlNode`]'s entry key.
@ -517,11 +498,15 @@ impl IndexMut<&str> for KdlNode {
}
impl FromStr for KdlNode {
type Err = KdlError;
type Err = KdlParseFailure;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let kdl_parser = crate::parser::KdlParser::new(input);
kdl_parser.parse(parser::node(&kdl_parser))
let (maybe_val, errs) = v2_parser::try_parse(v2_parser::padded_node, input);
if let (Some(v), true) = (maybe_val, errs.is_empty()) {
Ok(v)
} else {
Err(v2_parser::failure_from_errs(errs, input))
}
}
}
@ -532,36 +517,46 @@ impl Display for KdlNode {
}
impl KdlNode {
pub(crate) fn fmt_impl(&mut self, indent: usize, no_comments: bool) {
if let Some(s) = self.leading.as_mut() {
crate::fmt::fmt_leading(s, indent, no_comments);
}
if let Some(s) = self.trailing.as_mut() {
crate::fmt::fmt_trailing(s, no_comments);
if s.starts_with(';') {
s.remove(0);
pub(crate) fn autoformat_impl(&mut self, indent: usize, no_comments: bool) {
if let Some(KdlNodeFormat {
leading,
trailing,
before_children,
..
}) = self.format_mut()
{
crate::fmt::autoformat_leading(leading, indent, no_comments);
crate::fmt::autoformat_trailing(trailing, no_comments);
*trailing = trailing.trim().into();
if trailing.starts_with(';') {
trailing.remove(0);
}
if let Some(c) = s.chars().next() {
if let Some(c) = trailing.chars().next() {
if !c.is_whitespace() {
s.insert(0, ' ');
trailing.insert(0, ' ');
}
}
s.push('\n');
trailing.push('\n');
*before_children = " ".into();
} else {
self.set_format(KdlNodeFormat {
trailing: "\n".into(),
..Default::default()
})
}
self.before_children = None;
self.name.clear_fmt();
self.name.clear_format();
if let Some(ty) = self.ty.as_mut() {
ty.clear_fmt()
ty.clear_format()
}
for entry in &mut self.entries {
entry.fmt();
entry.autoformat();
}
if let Some(children) = self.children.as_mut() {
children.fmt_impl(indent + 4, no_comments);
if let Some(leading) = children.leading.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');
}
if let Some(trailing) = children.trailing.as_mut() {
trailing.push_str(format!("{:indent$}", "", indent = indent).as_str());
}
}
@ -572,7 +567,7 @@ impl KdlNode {
f: &mut std::fmt::Formatter<'_>,
indent: usize,
) -> std::fmt::Result {
if let Some(leading) = &self.leading {
if let Some(KdlNodeFormat { leading, .. }) = self.format() {
write!(f, "{}", leading)?;
} else {
write!(f, "{:indent$}", "", indent = indent)?;
@ -583,35 +578,56 @@ impl KdlNode {
write!(f, "{}", self.name)?;
let mut space_before_children = true;
for entry in &self.entries {
if entry.leading.is_none() {
if entry.format().is_none() {
write!(f, " ")?;
}
write!(f, "{}", entry)?;
space_before_children = entry.trailing.is_none();
space_before_children = entry.format().is_none();
}
if let Some(children) = &self.children {
if let Some(before) = self.before_children() {
write!(f, "{}", before)?;
if let Some(KdlNodeFormat {
before_children, ..
}) = self.format()
{
write!(f, "{before_children}")?;
} else if space_before_children {
write!(f, " ")?;
}
write!(f, "{{")?;
if children.leading.is_none() {
if children.format().is_none() {
writeln!(f)?;
}
children.stringify(f, indent + 4)?;
if children.trailing.is_none() {
if children.format().is_none() {
write!(f, "{:indent$}", "", indent = indent)?;
}
write!(f, "}}")?;
}
if let Some(trailing) = &self.trailing {
if let Some(KdlNodeFormat { trailing, .. }) = self.format() {
write!(f, "{}", trailing)?;
}
Ok(())
}
}
/// Formatting details for [`KdlNode`].
#[derive(Debug, Clone, Default, Hash, Eq, PartialEq)]
pub struct KdlNodeFormat {
/// Whitespace and comments preceding the node itself.
pub leading: String,
/// Whitespace and comments between the opening `(` of a type annotation and the actual annotation name.
pub before_ty_name: String,
/// Whitespace and comments between the annotation name and the closing `)`.
pub after_ty_name: String,
/// Whitespace and comments after a node's type annotation.
pub after_ty: String,
/// Whitespace and comments preceding the node's children block.
pub before_children: String,
/// Whitespace and comments following the node itself, including the
/// optional semicolon.
pub trailing: String,
}
#[cfg(test)]
mod test {
use super::*;
@ -625,26 +641,38 @@ mod test {
.parse()?;
let mut right_node: KdlNode = "node param_name=103.0 { nested 1 2 3; }".parse()?;
assert_ne!(left_node, right_node);
left_node.clear_fmt_recursive();
right_node.clear_fmt_recursive();
assert_eq!(left_node, right_node);
left_node.clear_format_recursive();
right_node.clear_format_recursive();
assert_eq!(left_node.to_string(), right_node.to_string());
Ok(())
}
#[test]
fn parsing() -> miette::Result<()> {
let node: KdlNode = "\n\t (\"ty\")\"node\" 0xDEADbeef;\n".parse()?;
assert_eq!(node.leading(), Some("\n\t "));
assert_eq!(node.trailing(), Some(";\n"));
assert_eq!(node.ty(), Some(&"\"ty\"".parse()?));
assert_eq!(node.name(), &"\"node\"".parse()?);
assert_eq!(node.entry(0), Some(&"0xDEADbeef".parse()?));
assert_eq!(node.entry(0), Some(&" 0xDEADbeef".parse()?));
assert_eq!(
node.format(),
Some(&KdlNodeFormat {
leading: "\n\t ".into(),
trailing: ";\n".into(),
before_ty_name: "".into(),
after_ty_name: "".into(),
after_ty: "".into(),
before_children: "".into(),
})
);
r#"
node "test" {
link "blah" anything="self"
}"#
let node: KdlNode = r#"node test {
link "blah" anything=self
}
"#
.parse::<KdlNode>()?;
assert_eq!(node.entry(0), Some(&" test".parse()?));
assert_eq!(node.children().unwrap().nodes().len(), 1);
Ok(())
}

View File

@ -1,6 +1,6 @@
use std::{collections::VecDeque, str::FromStr, sync::Arc};
use crate::{query_parser::KdlQueryParser, KdlDocument, KdlError, KdlNode, KdlValue};
use crate::{query_parser::KdlQueryParser, KdlDiagnostic, KdlDocument, KdlNode, KdlValue};
/// A parsed KQL query. For details on the syntax, see the [KQL
/// spec](https://github.com/kdl-org/kdl/blob/main/QUERY-SPEC.md).
@ -8,7 +8,7 @@ use crate::{query_parser::KdlQueryParser, KdlDocument, KdlError, KdlNode, KdlVal
pub struct KdlQuery(pub(crate) Vec<KdlQuerySelector>);
impl FromStr for KdlQuery {
type Err = KdlError;
type Err = KdlDiagnostic;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parser = KdlQueryParser::new(s);
@ -26,29 +26,29 @@ impl<'a> IntoKdlQuery for &'a String {}
#[doc(hidden)]
pub trait IntoQuerySealed {
fn into_query(self) -> Result<KdlQuery, KdlError>;
fn into_query(self) -> Result<KdlQuery, KdlDiagnostic>;
}
impl IntoQuerySealed for KdlQuery {
fn into_query(self) -> Result<KdlQuery, KdlError> {
fn into_query(self) -> Result<KdlQuery, KdlDiagnostic> {
Ok(self)
}
}
impl IntoQuerySealed for &str {
fn into_query(self) -> Result<KdlQuery, KdlError> {
fn into_query(self) -> Result<KdlQuery, KdlDiagnostic> {
self.parse()
}
}
impl IntoQuerySealed for String {
fn into_query(self) -> Result<KdlQuery, KdlError> {
fn into_query(self) -> Result<KdlQuery, KdlDiagnostic> {
self.parse()
}
}
impl IntoQuerySealed for &String {
fn into_query(self) -> Result<KdlQuery, KdlError> {
fn into_query(self) -> Result<KdlQuery, KdlDiagnostic> {
self.parse()
}
}

View File

@ -1,10 +1,11 @@
use crate::nom_compat::many0;
use crate::parser::{value, KdlParser};
use crate::query::{
KdlQuery, KdlQueryAttributeOp, KdlQueryMatcher, KdlQueryMatcherAccessor,
KdlQueryMatcherDetails, KdlQuerySelector, KdlQuerySelectorSegment, KdlSegmentCombinator,
};
use crate::{KdlError, KdlErrorKind, KdlParseError, KdlValue};
use crate::v1_parser::{value, KdlParser};
use crate::{KdlDiagnostic, KdlErrorKind, KdlParseError, KdlValue};
use miette::Severity;
use nom::branch::alt;
use nom::bytes::complete::tag;
use nom::combinator::{all_consuming, cut, map, opt, recognize};
@ -20,7 +21,7 @@ impl<'a> KdlQueryParser<'a> {
Self(KdlParser::new(full_input))
}
pub(crate) fn parse<T, P>(&self, parser: P) -> Result<T, KdlError>
pub(crate) fn parse<T, P>(&self, parser: P) -> Result<T, KdlDiagnostic>
where
P: Parser<&'a str, T, KdlParseError<&'a str>>,
{
@ -29,7 +30,7 @@ impl<'a> KdlQueryParser<'a> {
.map(|(_, arg)| arg)
.map_err(|e| {
let span_substr = &e.input[..e.len];
KdlError {
KdlDiagnostic {
input: self.0.full_input.into(),
span: self.0.span_from_substr(span_substr),
help: if let Some(help) = e.help {
@ -47,6 +48,7 @@ impl<'a> KdlQueryParser<'a> {
} else {
KdlErrorKind::Context("a valid KQL query")
},
severity: Severity::Error,
}
})
}
@ -176,7 +178,7 @@ fn node_matchers<'a: 'b, 'b>(
}
}
let (input, node) = opt(crate::parser::identifier(&kdl_parser.0))(input)?;
let (input, node) = opt(crate::v1_parser::identifier(&kdl_parser.0))(input)?;
if let Some(node) = node {
matchers.push(KdlQueryMatcherDetails {
op: KdlQueryAttributeOp::Equal,
@ -230,7 +232,7 @@ fn node_matchers<'a: 'b, 'b>(
}
// Check for trailing node name matcher.
let (end, ident) = opt(crate::parser::identifier(&kdl_parser.0))(input)?;
let (end, ident) = opt(crate::v1_parser::identifier(&kdl_parser.0))(input)?;
if ident.is_some() {
return Err(nom::Err::Error(KdlParseError {
input: start,
@ -275,7 +277,7 @@ fn attribute_matcher_inner<'a: 'b, 'b>(
let (input, _) = whitespace(input)?;
if let Some(op) = op {
let prev = input;
let (input, val) = opt(crate::parser::value)(input)?;
let (input, val) = opt(crate::v1_parser::value)(input)?;
// Make sure it's a syntax error to try and use string
// operators with non-string arguments.
if let Some((_, value)) = val {
@ -370,7 +372,7 @@ fn annotation_matcher<'a: 'b, 'b>(
let start = input;
let (input, _) = tag("(")(input)?;
let (input, _) = whitespace(input)?;
let (input, ty) = opt(crate::parser::identifier(&kdl_parser.0))(input)?;
let (input, ty) = opt(crate::v1_parser::identifier(&kdl_parser.0))(input)?;
let (input, _) = context("closing ')' for type annotation", cut(tag(")")))(input)
.map_err(|e| set_details(e, start, Some("annotation"), Some("annotations can only be KDL identifiers (including string identifiers), and can't have any space inside the parentheses.")))?;
Ok((
@ -464,7 +466,7 @@ fn prop_name_accessor<'a: 'b, 'b>(
) -> impl Fn(&'a str) -> IResult<&'a str, KdlQueryMatcherAccessor, KdlParseError<&'a str>> + 'b {
move |input| {
let start = input;
let (input, prop_name) = crate::parser::identifier(&kdl_parser.0)(input)?;
let (input, prop_name) = crate::v1_parser::identifier(&kdl_parser.0)(input)?;
let (_, paren) = opt(preceded(whitespace, tag("(")))(input)?;
if paren.is_some() {
Err(nom::Err::Error(KdlParseError {
@ -512,7 +514,7 @@ fn parenthesized_prop<'a: 'b, 'b>(
) -> impl Fn(&'a str) -> IResult<&'a str, String, KdlParseError<&'a str>> + 'b {
move |input| {
let (input, _) = tag("(")(input)?;
let (input, prop) = crate::parser::identifier(&kdl_parser.0)(input)?;
let (input, prop) = crate::v1_parser::identifier(&kdl_parser.0)(input)?;
let (input, _) = tag(")")(input)?;
Ok((input, prop.value().to_owned()))
}
@ -547,7 +549,7 @@ fn bad_accessor<'a: 'b, 'b>(
}
let (input, ident) = opt(terminated(
crate::parser::identifier(&kdl_parser.0),
crate::v1_parser::identifier(&kdl_parser.0),
preceded(
whitespace,
terminated(tag("("), opt(preceded(whitespace, tag(")")))),
@ -585,7 +587,7 @@ fn bad_accessor<'a: 'b, 'b>(
fn whitespace(input: &str) -> IResult<&str, &str, KdlParseError<&str>> {
recognize(many0(alt((
crate::parser::unicode_space,
crate::parser::newline,
crate::v1_parser::unicode_space,
crate::v1_parser::newline,
))))(input)
}

View File

@ -6,7 +6,7 @@
use std::ops::RangeTo;
use crate::nom_compat::{many0, many1, many_till};
use miette::SourceSpan;
use miette::{Severity, SourceSpan};
use nom::branch::alt;
use nom::bytes::complete::{tag, take_until, take_while, take_while_m_n};
use nom::character::complete::{anychar, char, none_of, one_of};
@ -16,7 +16,8 @@ use nom::sequence::{delimited, preceded, terminated, tuple};
use nom::{Finish, IResult, Offset, Parser, Slice};
use crate::{
KdlDocument, KdlEntry, KdlError, KdlErrorKind, KdlIdentifier, KdlNode, KdlParseError, KdlValue,
KdlDiagnostic, KdlDocument, KdlEntry, KdlErrorKind, KdlIdentifier, KdlNode, KdlParseError,
KdlValue,
};
/// The parser for the entire input.
@ -36,7 +37,7 @@ impl<'a> KdlParser<'a> {
Self { full_input }
}
pub(crate) fn parse<T, P>(&self, parser: P) -> Result<T, KdlError>
pub(crate) fn parse<T, P>(&self, parser: P) -> Result<T, KdlDiagnostic>
where
P: Parser<&'a str, T, KdlParseError<&'a str>>,
{
@ -45,7 +46,7 @@ impl<'a> KdlParser<'a> {
.map(|(_, arg)| arg)
.map_err(|e| {
let span_substr = &e.input[..e.len];
KdlError {
KdlDiagnostic {
input: self.full_input.into(),
span: self.span_from_substr(span_substr),
help: e.help,
@ -57,6 +58,7 @@ impl<'a> KdlParser<'a> {
} else {
KdlErrorKind::Other
},
severity: Severity::Error,
}
})
}
@ -840,7 +842,7 @@ mod node_tests {
#[test]
fn basic() {
let input = "foo 1 \"bar\"=false";
let kdl_parser = crate::parser::KdlParser::new(input);
let kdl_parser = crate::v1_parser::KdlParser::new(input);
match node(&kdl_parser)(input) {
Ok(("", parsed)) => {
let mut ident = KdlIdentifier::from("foo");
@ -873,7 +875,7 @@ mod node_tests {
#[test]
fn errant_ident1() {
let input = "struct Vec { }";
let kdl_parser = crate::parser::KdlParser::new(input);
let kdl_parser = crate::v1_parser::KdlParser::new(input);
let res = kdl_parser.parse(document(&kdl_parser));
let e = res.unwrap_err();
check_span("Vec", &e.span, &input);
@ -885,7 +887,7 @@ mod node_tests {
let input = r##"
some_node
bad evil"##;
let kdl_parser = crate::parser::KdlParser::new(input);
let kdl_parser = crate::v1_parser::KdlParser::new(input);
let res = kdl_parser.parse(document(&kdl_parser));
let e = res.unwrap_err();
check_span("evil", &e.span, &input);
@ -895,7 +897,7 @@ mod node_tests {
#[test]
fn errant_ident3() {
let input = r##"node "ok" wait "fine""##;
let kdl_parser = crate::parser::KdlParser::new(input);
let kdl_parser = crate::v1_parser::KdlParser::new(input);
let res = kdl_parser.parse(document(&kdl_parser));
let e = res.unwrap_err();
check_span("wait", &e.span, &input);
@ -905,7 +907,7 @@ mod node_tests {
#[test]
fn errant_ident4() {
let input = r##"node x="ok" oof z="5"##;
let kdl_parser = crate::parser::KdlParser::new(input);
let kdl_parser = crate::v1_parser::KdlParser::new(input);
let res = kdl_parser.parse(document(&kdl_parser));
let e = res.unwrap_err();
check_span("oof", &e.span, &input);
@ -917,7 +919,7 @@ mod node_tests {
// NOTE: this one is a different situation and doesn't provide as good help still!
// But at least it's clear that the value is bad, which is ok!
let input = r##"node x=bad"##;
let kdl_parser = crate::parser::KdlParser::new(input);
let kdl_parser = crate::v1_parser::KdlParser::new(input);
let res = kdl_parser.parse(document(&kdl_parser));
let e = res.unwrap_err();
check_span("", &e.span, &input);
@ -929,7 +931,7 @@ mod node_tests {
// NOTE: this one is a different situation and doesn't provide as good help still!
// But at least it's clear that the value is bad, which is ok!
let input = r##"node (int)bad"##;
let kdl_parser = crate::parser::KdlParser::new(input);
let kdl_parser = crate::v1_parser::KdlParser::new(input);
let res = kdl_parser.parse(document(&kdl_parser));
let e = res.unwrap_err();
check_span("", &e.span, &input);
@ -952,7 +954,7 @@ mod whitespace_tests {
use super::all_whitespace;
let input = " \t\n\r";
let kdl_parser = crate::parser::KdlParser::new(input);
let kdl_parser = crate::v1_parser::KdlParser::new(input);
assert_eq!(all_whitespace(&kdl_parser)(input), Ok(("", " \t\n\r")));
}
}
@ -964,18 +966,18 @@ mod comment_tests {
#[test]
fn single_line() {
let input = "// Hello world";
let kdl_parser = crate::parser::KdlParser::new(input);
let kdl_parser = crate::v1_parser::KdlParser::new(input);
assert_eq!(comment(&kdl_parser)(input), Ok(("", "// Hello world")));
}
#[test]
fn multi_line() {
let input = "/* Hello world */";
let kdl_parser = crate::parser::KdlParser::new(input);
let kdl_parser = crate::v1_parser::KdlParser::new(input);
assert_eq!(comment(&kdl_parser)(input), Ok(("", "/* Hello world */")));
let input = "/* Hello /* world */ blah */";
let kdl_parser = crate::parser::KdlParser::new(input);
let kdl_parser = crate::v1_parser::KdlParser::new(input);
assert_eq!(
comment(&kdl_parser)(input),
Ok(("", "/* Hello /* world */ blah */"))
@ -985,7 +987,7 @@ mod comment_tests {
#[test]
fn slashdash() {
let input = "/-foo 1 2";
let kdl_parser = crate::parser::KdlParser::new(input);
let kdl_parser = crate::v1_parser::KdlParser::new(input);
assert_eq!(comment(&kdl_parser)(input), Ok(("", "/-foo 1 2")));
}
@ -1071,7 +1073,7 @@ mod value_tests {
))
);
let input = "node 0x0123_4567_89ab_cdef";
let kdl_parser = crate::parser::KdlParser::new(input);
let kdl_parser = crate::v1_parser::KdlParser::new(input);
let (_, n) = node(&kdl_parser)(input).expect("failed to parse node");
assert_eq!(&n[0], &KdlValue::Base16(0x0123456789abcdef));
assert_eq!(

1444
src/v2_parser.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -3,36 +3,16 @@ use std::fmt::Display;
/// A specific [KDL Value](https://github.com/kdl-org/kdl/blob/main/SPEC.md#value).
#[derive(Debug, Clone, PartialOrd)]
pub enum KdlValue {
/// A [KDL Raw String](https://github.com/kdl-org/kdl/blob/main/SPEC.md#raw-string).
RawString(String),
/// A [KDL String](https://github.com/kdl-org/kdl/blob/main/SPEC.md#string).
String(String),
/// A [KDL
/// Number](https://github.com/kdl-org/kdl/blob/main/SPEC.md#number) in
/// binary form (e.g. `0b010101`).
Base2(i64),
/// A non-float [KDL
/// Number](https://github.com/kdl-org/kdl/blob/main/SPEC.md#number)
Integer(i64),
/// A [KDL
/// Number](https://github.com/kdl-org/kdl/blob/main/SPEC.md#number) in
/// octal form (e.g. `0o12345670`).
Base8(i64),
/// A [KDL
/// Number](https://github.com/kdl-org/kdl/blob/main/SPEC.md#number) in
/// decimal form (e.g. `1234567890`).
Base10(i64),
/// A [KDL
/// Number](https://github.com/kdl-org/kdl/blob/main/SPEC.md#number) in
/// decimal form (e.g. `1234567890.123`), interpreted as a Rust f64.
Base10Float(f64),
/// A [KDL
/// Number](https://github.com/kdl-org/kdl/blob/main/SPEC.md#number) in
/// hexadecimal form (e.g. `1234567890abcdef`).
Base16(i64),
/// A floating point [KDL
/// Number](https://github.com/kdl-org/kdl/blob/main/SPEC.md#number)
Float(f64),
/// A [KDL Boolean](https://github.com/kdl-org/kdl/blob/main/SPEC.md#boolean).
Bool(bool),
@ -46,12 +26,9 @@ impl Eq for KdlValue {}
impl PartialEq for KdlValue {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::RawString(l0), Self::RawString(r0)) => l0 == r0,
(Self::String(l0), Self::String(r0)) => l0 == r0,
(Self::Base2(l0), Self::Base2(r0)) => l0 == r0,
(Self::Base8(l0), Self::Base8(r0)) => l0 == r0,
(Self::Base10(l0), Self::Base10(r0)) => l0 == r0,
(Self::Base10Float(l0), Self::Base10Float(r0)) => {
(Self::Integer(l0), Self::Integer(r0)) => l0 == r0,
(Self::Float(l0), Self::Float(r0)) => {
let l0 = if l0 == &f64::INFINITY {
f64::MAX
} else if l0 == &f64::NEG_INFINITY {
@ -74,7 +51,6 @@ impl PartialEq for KdlValue {
};
l0 == r0
}
(Self::Base16(l0), Self::Base16(r0)) => l0 == r0,
(Self::Bool(l0), Self::Bool(r0)) => l0 == r0,
_ => core::mem::discriminant(self) == core::mem::discriminant(other),
}
@ -86,12 +62,9 @@ impl PartialEq for KdlValue {
impl std::hash::Hash for KdlValue {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
match self {
KdlValue::RawString(val) => val.hash(state),
KdlValue::String(val) => val.hash(state),
KdlValue::Base2(val) => val.hash(state),
KdlValue::Base8(val) => val.hash(state),
KdlValue::Base10(val) => val.hash(state),
KdlValue::Base10Float(val) => {
KdlValue::Integer(val) => val.hash(state),
KdlValue::Float(val) => {
let val = if val == &f64::INFINITY {
f64::MAX
} else if val == &f64::NEG_INFINITY {
@ -106,7 +79,6 @@ impl std::hash::Hash for KdlValue {
(val.trunc() as i64).hash(state);
(val.fract() as i64).hash(state);
}
KdlValue::Base16(val) => val.hash(state),
KdlValue::Bool(val) => val.hash(state),
KdlValue::Null => core::mem::discriminant(self).hash(state),
}
@ -114,58 +86,19 @@ impl std::hash::Hash for KdlValue {
}
impl KdlValue {
/// Returns `true` if the value is a [`KdlValue::RawString`].
pub fn is_raw_string(&self) -> bool {
matches!(self, Self::RawString(..))
}
/// Returns `true` if the value is a [`KdlValue::String`].
pub fn is_string(&self) -> bool {
matches!(self, Self::String(..))
}
/// Returns `true` if the value is a [`KdlValue::String`] or [`KdlValue::RawString`].
pub fn is_string_value(&self) -> bool {
matches!(self, Self::String(..) | Self::RawString(..))
/// Returns `true` if the value is a [`KdlValue::Integer`].
pub fn is_integer(&self) -> bool {
matches!(self, Self::Integer(..))
}
/// Returns `true` if the value is a [`KdlValue::Base2`].
pub fn is_base2(&self) -> bool {
matches!(self, Self::Base2(..))
}
/// Returns `true` if the value is a [`KdlValue::Base8`].
pub fn is_base8(&self) -> bool {
matches!(self, Self::Base8(..))
}
/// Returns `true` if the value is a [`KdlValue::Base10`].
pub fn is_base10(&self) -> bool {
matches!(self, Self::Base10(..))
}
/// Returns `true` if the value is a [`KdlValue::Base16`].
pub fn is_base16(&self) -> bool {
matches!(self, Self::Base16(..))
}
/// Returns `true` if the value is a [`KdlValue::Base2`],
/// [`KdlValue::Base8`], [`KdlValue::Base10`], or [`KdlValue::Base16`].
pub fn is_i64_value(&self) -> bool {
matches!(
self,
Self::Base2(..) | Self::Base8(..) | Self::Base10(..) | Self::Base16(..)
)
}
/// Returns `true` if the value is a [`KdlValue::Base10Float`].
pub fn is_base10_float(&self) -> bool {
matches!(self, Self::Base10Float(..))
}
/// Returns `true` if the value is a [`KdlValue::Base10Float`].
pub fn is_float_value(&self) -> bool {
matches!(self, Self::Base10Float(..))
/// Returns `true` if the value is a [`KdlValue::Float`].
pub fn is_float(&self) -> bool {
matches!(self, Self::Float(..))
}
/// Returns `true` if the value is a [`KdlValue::Bool`].
@ -178,34 +111,32 @@ impl KdlValue {
matches!(self, Self::Null)
}
/// Returns `Some(&str)` if the `KdlValue` is a [`KdlValue::RawString`] or a
/// [`KdlValue::String`], otherwise returns `None`.
/// Returns `Some(&str)` if the `KdlValue` is a [`KdlValue::String`],
/// otherwise returns `None`.
pub fn as_string(&self) -> Option<&str> {
use KdlValue::*;
match self {
String(s) | RawString(s) => Some(s),
String(s) => Some(s),
_ => None,
}
}
/// Returns `Some(i64)` if the `KdlValue` is a [`KdlValue::Base2`],
/// [`KdlValue::Base8`], [`KdlValue::Base10`], or [`KdlValue::Base16`],
/// Returns `Some(i64)` if the `KdlValue` is a [`KdlValue::Integer`],
/// otherwise returns `None`.
pub fn as_i64(&self) -> Option<i64> {
pub fn as_integer(&self) -> Option<i64> {
use KdlValue::*;
match self {
Base2(i) | Base8(i) | Base10(i) | Base16(i) => Some(*i),
Integer(i) => Some(*i),
_ => None,
}
}
/// Returns `Some(f64)` if the `KdlValue` is a [`KdlValue::Base10Float`],
/// Returns `Some(f64)` if the `KdlValue` is a [`KdlValue::Float`],
/// otherwise returns `None`.
pub fn as_f64(&self) -> Option<f64> {
if let Self::Base10Float(v) = self {
Some(*v)
} else {
None
pub fn as_float(&self) -> Option<f64> {
match self {
Self::Float(i) => Some(*i),
_ => None,
}
}
@ -222,79 +153,81 @@ impl KdlValue {
impl Display for KdlValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::RawString(_) => self.write_raw_string(f),
Self::String(_) => self.write_string(f),
Self::Base2(value) => write!(f, "0b{:b}", value),
Self::Base8(value) => write!(f, "0o{:o}", value),
Self::Base10(value) => write!(f, "{:?}", value),
Self::Base10Float(value) => write!(
Self::Integer(value) => write!(f, "{:?}", value),
Self::Float(value) => write!(
f,
"{:?}",
"{}",
if value == &f64::INFINITY {
f64::MAX
"#inf".into()
} else if value == &f64::NEG_INFINITY {
-f64::MAX
"#-inf".into()
} else if value.is_nan() {
0.0
"#nan".into()
} else {
*value
format!("{:?}", *value)
}
),
Self::Base16(value) => write!(f, "0x{:x}", value),
Self::Bool(value) => write!(f, "{}", value),
Self::Null => write!(f, "null"),
Self::Bool(value) => write!(f, "#{}", value),
Self::Null => write!(f, "#null"),
}
}
}
fn is_plain_ident(ident: &str) -> bool {
let ident_bytes = ident.as_bytes();
ident
.find(crate::v2_parser::is_disallowed_ident_char)
.is_none()
&& ident_bytes.first().map(|c| c.is_ascii_digit()) != Some(true)
&& !(ident
.chars()
.next()
.map(|c| c == '.' || c == '-' || c == '+')
== Some(true)
&& ident_bytes.get(1).map(|c| c.is_ascii_digit()) == Some(true))
}
#[cfg(test)]
#[test]
fn plain_ident_test() {
assert!(is_plain_ident("foo123,bar"));
assert!(is_plain_ident("foo123~!@$%^&*.:'|?+<>,"));
}
impl KdlValue {
fn write_string(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let string = self.as_string().unwrap();
write!(f, "\"")?;
for char in string.chars() {
match char {
'\\' | '"' => write!(f, "\\{}", char)?,
'\n' => write!(f, "\\n")?,
'\r' => write!(f, "\\r")?,
'\t' => write!(f, "\\t")?,
'\u{08}' => write!(f, "\\b")?,
'\u{0C}' => write!(f, "\\f")?,
_ => write!(f, "{}", char)?,
if !string.is_empty() && is_plain_ident(string) {
write!(f, "{string}")?;
} else {
write!(f, "\"")?;
for char in string.chars() {
match char {
'\\' | '"' => write!(f, "\\{}", char)?,
'\n' => write!(f, "\\n")?,
'\r' => write!(f, "\\r")?,
'\t' => write!(f, "\\t")?,
'\u{08}' => write!(f, "\\b")?,
'\u{0C}' => write!(f, "\\f")?,
_ => write!(f, "{}", char)?,
}
}
write!(f, "\"")?;
}
write!(f, "\"")?;
Ok(())
}
fn write_raw_string(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let raw = self.as_string().unwrap();
let mut consecutive = 0usize;
let mut maxhash = 0usize;
for char in raw.chars() {
if char == '#' {
consecutive += 1;
} else if char == '"' {
maxhash = maxhash.max(consecutive + 1);
} else {
consecutive = 0;
}
}
write!(f, "r")?;
write!(f, "{}", "#".repeat(maxhash))?;
write!(f, "\"{}\"", raw)?;
write!(f, "{}", "#".repeat(maxhash))?;
Ok(())
}
}
impl From<i64> for KdlValue {
fn from(value: i64) -> Self {
KdlValue::Base10(value)
KdlValue::Integer(value)
}
}
impl From<f64> for KdlValue {
fn from(value: f64) -> Self {
KdlValue::Base10Float(value)
KdlValue::Float(value)
}
}
@ -334,34 +267,19 @@ mod test {
#[test]
fn formatting() {
let raw = KdlValue::RawString(r###"r##"foor#"bar"#baz"##"###.into());
assert_eq!(
format!("{}", raw),
r####"r###"r##"foor#"bar"#baz"##"###"####
);
let string = KdlValue::String("foo\n".into());
assert_eq!(format!("{}", string), r#""foo\n""#);
let base2 = KdlValue::Base2(0b1010_1010);
assert_eq!(format!("{}", base2), "0b10101010");
let integer = KdlValue::Integer(1234567890);
assert_eq!(format!("{}", integer), "1234567890");
let base8 = KdlValue::Base8(0o12345670);
assert_eq!(format!("{}", base8), "0o12345670");
let base10 = KdlValue::Base10(1234567890);
assert_eq!(format!("{}", base10), "1234567890");
let base10float = KdlValue::Base10Float(1234567890.12345);
assert_eq!(format!("{}", base10float), "1234567890.12345");
let base16 = KdlValue::Base16(0x1234567890ABCDEF);
assert_eq!(format!("{}", base16), "0x1234567890abcdef");
let float = KdlValue::Float(1234567890.12345);
assert_eq!(format!("{}", float), "1234567890.12345");
let boolean = KdlValue::Bool(true);
assert_eq!(format!("{}", boolean), "true");
assert_eq!(format!("{}", boolean), "#true");
let null = KdlValue::Null;
assert_eq!(format!("{}", null), "null");
assert_eq!(format!("{}", null), "#null");
}
}

View File

@ -4,8 +4,42 @@ use std::{
path::{Path, PathBuf},
};
use kdl::{KdlDocument, KdlError, KdlIdentifier, KdlValue};
use miette::IntoDiagnostic;
use kdl::{KdlDocument, KdlIdentifier, KdlParseFailure, KdlValue};
use miette::{Diagnostic, IntoDiagnostic};
use thiserror::Error;
#[derive(Debug, Error, Diagnostic)]
#[error("Compliance test suite failed {} of {total_checks}.", diagnostics.len())]
struct ComplianceSuiteFailure {
total_checks: usize,
#[related]
diagnostics: Vec<ComplianceDiagnostic>,
}
#[derive(Debug, Error, Diagnostic)]
enum ComplianceDiagnostic {
#[error("{}", PathBuf::from(.0.file_name().unwrap()).display())]
#[diagnostic(code(kdl::compliance::parse_failure))]
KdlParseFailure(
PathBuf,
#[source]
#[diagnostic_source]
KdlParseFailure,
),
#[error("{}:\nExpected:\n{expected}\nActual:\n{actual}", PathBuf::from(file.file_name().unwrap()).display())]
#[diagnostic(code(kdl::compliance::expectation_mismatch))]
ExpectationMismatch {
file: PathBuf,
original: String,
expected: String,
actual: String,
},
#[error(transparent)]
#[diagnostic(code(kdl::compliance::io_error))]
IoError(#[from] std::io::Error),
}
#[test]
fn spec_compliance() -> miette::Result<()> {
@ -13,21 +47,37 @@ fn spec_compliance() -> miette::Result<()> {
.join("tests")
.join("test_cases")
.join("input");
for test_name in fs::read_dir(&input).into_diagnostic()? {
let mut failures = Vec::new();
let mut count = 0usize;
for test_name in fs::read_dir(input).into_diagnostic()? {
let test_path = test_name.into_diagnostic()?.path();
println!(
"parsing {}:",
PathBuf::from(test_path.file_name().unwrap()).display()
);
let src = normalize_line_endings(fs::read_to_string(&test_path).into_diagnostic()?);
println!("src: {}", src);
let res: Result<KdlDocument, KdlError> = src.parse();
validate_res(res, &test_path)?;
let res = src.parse();
if let Err(e) = validate_res(res, &test_path, &src) {
failures.push(e);
}
count += 1;
}
if failures.is_empty() {
Ok(())
} else {
let mut output = String::new();
for failure in &failures {
output.push_str(format!("\n{failure}").as_str());
}
Err(ComplianceSuiteFailure {
total_checks: count,
diagnostics: failures,
}
.into())
}
Ok(())
}
fn validate_res(res: Result<KdlDocument, KdlError>, path: &Path) -> miette::Result<()> {
fn validate_res(
res: Result<KdlDocument, KdlParseFailure>,
path: &Path,
src: &String,
) -> Result<(), ComplianceDiagnostic> {
let file_name = path.file_name().unwrap();
let expected_dir = path
.parent()
@ -38,20 +88,24 @@ fn validate_res(res: Result<KdlDocument, KdlError>, path: &Path) -> miette::Resu
let expected_path = expected_dir.join(file_name);
let underscored = expected_dir.join(format!("_{}", PathBuf::from(file_name).display()));
if expected_path.exists() {
let doc = res?;
let expected =
normalize_line_endings(fs::read_to_string(&expected_path).into_diagnostic()?);
println!("expected: {}", expected);
let stringified = stringify_to_expected(doc);
println!("stringified: {}", stringified);
assert_eq!(stringified, expected);
let doc = res.map_err(|e| ComplianceDiagnostic::KdlParseFailure(path.into(), e))?;
let expected = normalize_line_endings(fs::read_to_string(&expected_path)?);
let actual = stringify_to_expected(doc);
if actual != expected {
return Err(ComplianceDiagnostic::ExpectationMismatch {
file: path.into(),
original: src.clone(),
expected,
actual,
});
}
} else if underscored.exists() {
println!(
eprintln!(
"skipped reserialization for {}",
PathBuf::from(file_name).display()
);
} else {
assert!(res.is_err(), "parse should not have succeeded");
// } else {
// res.map_err(|e| ComplianceDiagnostic::KdlParseFailure(path.into(), e))?;
}
Ok(())
}
@ -61,27 +115,14 @@ fn normalize_line_endings(src: String) -> String {
}
fn stringify_to_expected(mut doc: KdlDocument) -> String {
doc.fmt_no_comments();
normalize_numbers(&mut doc);
doc.autoformat_no_comments();
normalize_strings(&mut doc);
normalize_identifiers(&mut doc);
dedupe_props(&mut doc);
remove_empty_children(&mut doc);
doc.to_string()
}
fn normalize_numbers(doc: &mut KdlDocument) {
for node in doc.nodes_mut() {
for entry in node.entries_mut() {
if let Some(value) = entry.value().as_i64() {
*entry.value_mut() = KdlValue::Base10(value);
}
}
if let Some(children) = node.children_mut() {
normalize_numbers(children);
}
}
}
fn normalize_strings(doc: &mut KdlDocument) {
for node in doc.nodes_mut() {
for entry in node.entries_mut() {
@ -95,6 +136,22 @@ fn normalize_strings(doc: &mut KdlDocument) {
}
}
fn normalize_identifiers(doc: &mut KdlDocument) {
for node in doc.nodes_mut() {
node.name_mut().clear_format();
for entry in node.entries_mut() {
if entry.name().is_some() {
if let Some(x) = entry.name_mut() {
x.clear_format()
}
}
}
if let Some(children) = node.children_mut() {
normalize_identifiers(children);
}
}
}
fn dedupe_props(doc: &mut KdlDocument) {
for node in doc.nodes_mut() {
let mut props = HashMap::<KdlIdentifier, Vec<usize>>::new();

View File

@ -11,7 +11,7 @@ fn build_and_format() {
let mut doc = KdlDocument::new();
doc.nodes_mut().push(a);
doc.fmt();
doc.autoformat();
let fmt = doc.to_string();
println!("{}", fmt);
assert_eq!(

View File

@ -1 +0,0 @@
node 1 1.0 10000000000.0 1e-10 1 7 2 "arg" "arg\\\\" true false null

View File

@ -1 +0,0 @@
node prop=1.23E+1000

View File

@ -1 +0,0 @@
node prop=1.23E-1000

View File

@ -1 +1 @@
node "\"\\/\b\f\n\r\t"
node "\"\\\b\f\n\r\t "

View File

@ -1,3 +1,3 @@
node "arg" prop="val" {
node arg prop=val {
inner_node
}

View File

@ -1 +1 @@
node "arg" arg="val"
node arg arg=val

View File

@ -0,0 +1 @@
node a

View File

@ -1 +1 @@
node (type)false
node (type)#false

View File

@ -1 +1 @@
node (type)null
node (type)#null

View File

@ -1 +1 @@
node (type)"str"
node (type)str

View File

@ -1 +1 @@
node (type)"str"
node (type)str

View File

@ -1 +1 @@
node (type)true
node (type)#true

View File

@ -1 +1 @@
node (type)"arg"
node (type)arg

View File

@ -1 +1 @@
😁 "happy!"
😁 happy!

View File

@ -0,0 +1 @@
node .

View File

@ -0,0 +1 @@
node +

View File

@ -0,0 +1 @@
node +.

View File

@ -1 +1 @@
node key=("")true
node key=("")#true

View File

@ -1 +1 @@
node "arg"
node arg

View File

@ -1 +1 @@
node "arg"
node arg

View File

@ -0,0 +1 @@
node arg

View File

@ -1 +1 @@
node false true
node #false #true

View File

@ -1 +1 @@
node prop1=true prop2=false
node prop1=#true prop2=#false

View File

@ -0,0 +1 @@
foo123<bar>foo weeee

View File

@ -0,0 +1 @@
foo123,bar weeee

View File

@ -0,0 +1 @@
node (type)10

View File

@ -0,0 +1 @@
(type)node

View File

@ -0,0 +1 @@
node key=(type)10

View File

@ -0,0 +1,2 @@
node1
node2

View File

@ -0,0 +1 @@
node (type)10

View File

@ -0,0 +1 @@
(type)node

View File

@ -0,0 +1 @@
node key=(type)10

View File

@ -1 +1 @@
node "arg2"
node arg2

View File

@ -1 +1 @@
node "arg"
node arg

View File

@ -1 +1 @@
node "arg"
node arg

View File

@ -0,0 +1 @@
node --

View File

@ -1 +1 @@
node "😀"
node 😀

View File

@ -0,0 +1 @@
node

View File

@ -1 +1 @@
"" "arg"
"" arg

View File

@ -1 +1 @@
node ""="empty"
node ""=empty

View File

@ -0,0 +1 @@
node

View File

@ -0,0 +1 @@
node "Hello\n\tWorld" "Hello\n\tWorld" "Hello\n\tWorld" "Hello\n\tWorld" "Hello\n\tWorld"

View File

@ -1 +1 @@
node "arg"
node arg

View File

@ -1 +1 @@
node "arg" "arg2\n"
node arg arg2

View File

@ -0,0 +1 @@
floats #inf #-inf #nan

View File

@ -1 +0,0 @@
node 1311768467294899695

View File

@ -1 +1 @@
node 1311768467294899695
node 188900966474565

View File

@ -0,0 +1 @@
another-node

View File

@ -1 +1 @@
node "arg"
node arg

View File

@ -1 +1 @@
node "arg1" "arg2"
node arg1 arg2

View File

@ -0,0 +1 @@
node "hey\neveryone\nhow goes?"

View File

@ -0,0 +1 @@
node " hey\n everyone\n how goes?"

View File

@ -1 +1 @@
node " hey\neveryone\nhow goes?\n"
node "hey\neveryone\nhow goes?"

View File

@ -0,0 +1 @@
node " hey\n everyone\n how goes?"

View File

@ -1 +1 @@
node "arg"
node arg

View File

@ -1 +1 @@
node "arg"
node arg

View File

@ -1 +1 @@
node "arg"
node arg

View File

@ -1 +1 @@
node "arg"
node arg

View File

@ -1 +1 @@
node false
node #false

View File

@ -1 +1 @@
node true
node #true

View File

@ -1 +1 @@
node null
node #null

View File

@ -1 +1 @@
node prop=null
node prop=#null

View File

@ -0,0 +1,5 @@
node {
foo
bar
baz
}

View File

@ -0,0 +1 @@
node 1 1.0 10000000000.0 1e-10 1 7 2 arg arg "arg\\" #true #false #null

View File

@ -1 +1 @@
node key=(type)false
node key=(type)#false

View File

@ -0,0 +1 @@
node key=(type)str

View File

@ -1 +1 @@
node key=(type)null
node key=(type)#null

View File

@ -1 +1 @@
node key=(type)"str"
node key=(type)str

View File

@ -1 +1 @@
node key=(type)"str"
node key=(type)str

View File

@ -1 +1 @@
node key=(type)true
node key=(type)#true

Some files were not shown because too many files have changed in this diff Show More