just_kdl/
lib.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! [![Repository](https://img.shields.io/badge/repository-GitHub-brightgreen.svg)](https://github.com/1e1001/rsutil/tree/main/just-kdl)
3//! [![Crates.io](https://img.shields.io/crates/v/just-kdl)](https://crates.io/crates/just-kdl)
4//! [![docs.rs](https://img.shields.io/docsrs/just-kdl)](https://docs.rs/just-kdl)
5//! [![MIT OR Apache-2.0](https://img.shields.io/crates/l/just-kdl)](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}