just_kdl/lib.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! [](https://github.com/1e1001/rsutil/tree/main/just-kdl)
3//! [](https://crates.io/crates/just-kdl)
4//! [](https://docs.rs/just-kdl)
5//! [](https://github.com/1e1001/rsutil/blob/main/just-kdl/README.md#License)
6//!
7//! Small streaming [KDL] v2.0.0 parser
8//!
9//! Designed for reasonable performance and memory efficiency, at the expense
10//! (or benefit, depending on use) of not storing formatting information
11//!
12//! ## Why?
13//!
14//! The [official Rust implementation][kdl-rs] is designed to support editing of
15//! kdl files. While this is normally useful, my main use of KDL is to just
16//! parse the values into some internal data structure (configuration, document
17//! trees, etc.) where formatting information is entirely redundant and just
18//! wasteful of parsing time and memory.
19//!
20//! Additionally, this implementation has a few other benefits:
21//! - Full v2.0.0 compliance
22//! - Significantly fewer dependencies!
23//!
24//! ## Benchmarks
25//!
26//! On my personal laptop, on low power setting:
27//! ```text
28//! // in release mode
29//! bench "html-standard-compact.kdl" {
30//! JustKdlDom {
31//! time "688.9548ms"
32//! memory new=217_494_981 free=28_488_457 net=189_006_524
33//! }
34//! Kdl {
35//! time "13.228400752s"
36//! memory new=5_219_542_533 free=4_305_774_657 net=913_767_876
37//! }
38//! }
39//! bench "html-standard.kdl" {
40//! JustKdlDom {
41//! time "628.389088ms"
42//! memory new=357_151_222 free=35_738_264 net=321_412_958
43//! }
44//! Kdl {
45//! time "18.194044124s"
46//! memory new=8_146_781_320 free=6_584_885_611 net=1_561_895_709
47//! }
48//! }
49//!
50//! // in debug mode
51//! bench "html-standard-compact.kdl" {
52//! JustKdlDom {
53//! time "4.169828469s"
54//! memory new=217_494_981 free=28_488_457 net=189_006_524
55//! }
56//! Kdl {
57//! time "149.249652517s"
58//! memory new=5_219_542_533 free=4_305_774_657 net=913_767_876
59//! }
60//! }
61//! bench "html-standard.kdl" {
62//! JustKdlDom {
63//! time "5.756129305s"
64//! memory new=357_151_222 free=35_738_264 net=321_412_958
65//! }
66//! Kdl {
67//! time "211.812757636s"
68//! memory new=8_146_781_320 free=6_584_885_611 net=1_561_895_709
69//! }
70//! }
71//! ```
72//! In summary:
73//! - 19-36 times faster (on average, will likely be less in practice)
74//! - *significantly* fewer temporary allocations
75//! - fewer output allocations (even with cleared formatting!)
76//!
77//! [kdl]: <https://kdl.dev>
78//! [kdl-rs]: https://docs.rs/kdl
79
80use std::borrow::Cow;
81use std::fmt;
82
83pub mod dom;
84pub mod number;
85pub mod stream;
86
87#[cfg(test)]
88mod tests;
89
90fn cow_static<T: ?Sized + ToOwned>(value: Cow<'_, T>) -> Cow<'static, T> {
91 Cow::Owned(value.into_owned())
92}
93
94struct IdentDisplay<'text>(&'text str);
95impl fmt::Display for IdentDisplay<'_> {
96 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
97 let text = self.0;
98 let is_number_like = {
99 let text = text.strip_prefix('+').unwrap_or(text);
100 let text = text.strip_prefix('-').unwrap_or(text);
101 let text = text.strip_prefix('.').unwrap_or(text);
102 matches!(text.chars().next(), Some('0'..='9'))
103 };
104 if text.is_empty()
105 || is_number_like
106 || text.contains([
107 '\u{0}', '\u{1}', '\u{2}', '\u{3}', '\u{4}', '\u{5}', '\u{6}', '\u{7}', '\u{8}',
108 '\u{E}', '\u{F}', '\u{10}', '\u{11}', '\u{12}', '\u{13}', '\u{14}', '\u{15}',
109 '\u{16}', '\u{17}', '\u{18}', '\u{19}', '\u{1A}', '\u{1B}', '\u{1C}', '\u{1D}',
110 '\u{1E}', '\u{1F}', '\u{7F}', '\u{200E}', '\u{200F}', '\u{202A}', '\u{202B}',
111 '\u{202C}', '\u{202D}', '\u{202E}', '\u{2066}', '\u{2067}', '\u{2068}', '\u{2069}',
112 '\u{FEFF}', '\\', '/', '(', ')', '{', '}', ';', '[', ']', '"', '#', '=', '\u{9}',
113 '\u{20}', '\u{A0}', '\u{1680}', '\u{2000}', '\u{2001}', '\u{2002}', '\u{2003}',
114 '\u{2004}', '\u{2005}', '\u{2006}', '\u{2007}', '\u{2008}', '\u{2009}', '\u{200A}',
115 '\u{202F}', '\u{205F}', '\u{3000}', '\u{A}', '\u{B}', '\u{C}', '\u{D}', '\u{85}',
116 '\u{2028}', '\u{2029}',
117 ]) {
118 f.write_str("\"")?;
119 for ch in text.chars() {
120 match ch {
121 '\u{8}' => f.write_str("\\b"),
122 '\u{C}' => f.write_str("\\f"),
123 '\'' => f.write_str("'"),
124 _ => fmt::Display::fmt(&ch.escape_debug(), f),
125 }?;
126 }
127 f.write_str("\"")
128 } else {
129 fmt::Display::fmt(&text, f)
130 }
131 }
132}