1use std::borrow::Cow;
41use std::fmt;
42
43use rich::{Console, Style};
44
45use crate::event::theme_style;
46
47mod config;
48mod diff;
49mod dotenv;
50mod explorer;
51mod flatten;
52mod helpers;
53mod ini;
54mod json;
55mod record;
56mod redact;
57mod search;
58pub mod select;
59mod ser;
60mod table;
61#[cfg(feature = "jsonpath")]
62pub mod transform;
63
64#[cfg(feature = "toml")]
65mod toml_doc;
66#[cfg(feature = "xml")]
67mod xml;
68#[cfg(feature = "yaml")]
69mod yaml;
70
71pub use config::ConfigFileView;
72pub use diff::{Change, ChangeKind, DiffView, diff};
73pub use explorer::{Explorer, View};
74pub use flatten::{FlatView, UnflattenError, flatten, unflatten};
75pub use helpers::{
76 json, print_json, print_json_to, print_table, print_table_to, print_tree, print_tree_to, table,
77 tree,
78};
79pub use record::{OpenBranch, RecordView};
80pub use redact::SECRET_KEYS;
81pub use redact::{Redaction, Redactor};
82pub use search::{MatchKind, SearchMatch, SearchQuery, SearchResults, search};
83#[cfg(feature = "jsonpath")]
84pub use select::{JsonPath, JsonPathSelector};
85pub use select::{SelectError, Selector, SelectorBackend, Selectors};
86pub use table::{TableOptions, TableView};
87
88#[cfg(any(feature = "yaml", feature = "xml"))]
93pub(crate) const MAX_DEPTH: usize = 512;
94
95pub const DATA_STYLES: &[(&str, &str)] = &[
98 ("data.match", "bold reverse yellow"),
99 ("data.anchor", "dim cyan"),
100 ("data.alias", "dim cyan"),
101 ("data.attribute", "not italic yellow"),
102 ("data.index", "dim"),
103 ("data.comment", "dim"),
104 ("data.path", "dim"),
105 ("data.type", "dim italic"),
106 ("data.summary", "dim"),
107 ("data.null", "dim"),
108 ("data.datetime", "magenta"),
109 ("data.section", "bold"),
110 ("data.added", "green"),
111 ("data.removed", "red"),
112 ("data.changed", "yellow"),
113];
114
115const JSON_STYLES: &[(&str, &str)] = &[
117 ("json.key", "bold blue"),
118 ("json.str", "not bold not italic green"),
119 ("json.number", "bold not italic cyan"),
120 ("json.bool_true", "italic bright_green"),
121 ("json.bool_false", "italic bright_red"),
122 ("json.null", "italic magenta"),
123];
124
125pub(crate) fn style(console: &Console, key: &str) -> Style {
127 let fallback = DATA_STYLES
128 .iter()
129 .chain(JSON_STYLES)
130 .find(|(name, _)| *name == key)
131 .map_or("", |(_, spec)| *spec);
132 theme_style(console, key, fallback)
133}
134
135#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
142pub struct Position {
143 pub line: usize,
144 pub column: usize,
145}
146
147impl Position {
148 pub fn new(line: usize, column: usize) -> Self {
149 Position { line, column }
150 }
151}
152
153impl fmt::Display for Position {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 write!(f, "{}:{}", self.line, self.column)
156 }
157}
158
159#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
161pub enum XmlKind {
162 Element,
164 Attribute,
166 Text,
168}
169
170#[derive(Clone, Debug, Default, PartialEq)]
172pub struct Meta {
173 pub position: Option<Position>,
175 pub anchor: Option<String>,
177 pub alias: Option<String>,
180 pub comment: Option<String>,
182 pub xml: Option<XmlKind>,
184}
185
186#[derive(Clone, Debug, PartialEq)]
188pub enum Value {
189 Null,
190 Bool(bool),
191 Int(i64),
192 UInt(u64),
194 Float(f64),
195 String(String),
196 DateTime(String),
198 Seq(Vec<Node>),
199 Map(Vec<(String, Node)>),
201}
202
203#[derive(Clone, Debug, PartialEq)]
205pub struct Node {
206 pub value: Value,
207 pub meta: Meta,
208}
209
210impl From<Value> for Node {
211 fn from(value: Value) -> Self {
212 Node::new(value)
213 }
214}
215
216impl<'a> From<Node> for Cow<'a, Node> {
217 fn from(node: Node) -> Self {
218 Cow::Owned(node)
219 }
220}
221
222impl<'a> From<&'a Node> for Cow<'a, Node> {
223 fn from(node: &'a Node) -> Self {
224 Cow::Borrowed(node)
225 }
226}
227
228impl Node {
229 pub fn new(value: Value) -> Self {
231 Node {
232 value,
233 meta: Meta::default(),
234 }
235 }
236
237 pub fn with_meta(value: Value, meta: Meta) -> Self {
239 Node { value, meta }
240 }
241
242 #[must_use]
244 pub fn at_position(mut self, position: Option<Position>) -> Self {
245 self.meta.position = position;
246 self
247 }
248
249 pub fn is_container(&self) -> bool {
251 matches!(self.value, Value::Seq(_) | Value::Map(_))
252 }
253
254 pub fn len(&self) -> usize {
256 match &self.value {
257 Value::Seq(items) => items.len(),
258 Value::Map(entries) => entries.len(),
259 _ => 0,
260 }
261 }
262
263 pub fn is_empty(&self) -> bool {
265 self.len() == 0
266 }
267
268 pub fn get(&self, key: &str) -> Option<&Node> {
270 match &self.value {
271 Value::Map(entries) => entries.iter().find(|(k, _)| k == key).map(|(_, v)| v),
272 _ => None,
273 }
274 }
275
276 pub fn index(&self, index: usize) -> Option<&Node> {
278 match &self.value {
279 Value::Seq(items) => items.get(index),
280 _ => None,
281 }
282 }
283
284 pub fn at(&self, path: &Path) -> Option<&Node> {
286 path.segments()
287 .iter()
288 .try_fold(self, |node, segment| match segment {
289 PathSegment::Key(key) => node.get(key),
290 PathSegment::Index(index) => node.index(*index),
291 })
292 }
293
294 pub fn as_str(&self) -> Option<&str> {
296 match &self.value {
297 Value::String(s) | Value::DateTime(s) => Some(s),
298 _ => None,
299 }
300 }
301
302 pub fn type_name(&self) -> &'static str {
305 match &self.value {
306 Value::Null => "null",
307 Value::Bool(_) => "bool",
308 Value::Int(_) | Value::UInt(_) => "int",
309 Value::Float(_) => "float",
310 Value::String(_) => "str",
311 Value::DateTime(_) => "datetime",
312 Value::Seq(_) => "seq",
313 Value::Map(_) => "map",
314 }
315 }
316
317 pub fn to_json(&self) -> serde_json::Value {
320 use serde_json::Value as J;
321 match &self.value {
322 Value::Null => J::Null,
323 Value::Bool(b) => J::Bool(*b),
324 Value::Int(i) => J::from(*i),
325 Value::UInt(u) => J::from(*u),
326 Value::Float(f) => serde_json::Number::from_f64(*f).map_or(J::Null, J::Number),
327 Value::String(s) | Value::DateTime(s) => J::String(s.clone()),
328 Value::Seq(items) => J::Array(items.iter().map(Node::to_json).collect()),
329 Value::Map(entries) => J::Object(
330 entries
331 .iter()
332 .map(|(k, v)| (k.clone(), v.to_json()))
333 .collect(),
334 ),
335 }
336 }
337
338 pub fn walk<'a>(&'a self, mut visit: impl FnMut(&Path, &'a Node)) {
340 let mut stack: Vec<(Path, &'a Node)> = vec![(Path::root(), self)];
343 while let Some((path, node)) = stack.pop() {
344 visit(&path, node);
345 match &node.value {
346 Value::Seq(items) => {
347 for (i, item) in items.iter().enumerate().rev() {
348 stack.push((path.child_index(i), item));
349 }
350 }
351 Value::Map(entries) => {
352 for (key, value) in entries.iter().rev() {
353 stack.push((path.child_key(key), value));
354 }
355 }
356 _ => {}
357 }
358 }
359 }
360
361 #[must_use]
364 pub fn redacted(&self, redactor: &dyn Redactor) -> Node {
365 redact::apply(self, redactor)
366 }
367}
368
369pub(crate) fn value_eq(a: &Node, b: &Node) -> bool {
372 match (&a.value, &b.value) {
373 (Value::Float(x), Value::Float(y)) => x.to_bits() == y.to_bits() || x == y,
374 (Value::Int(x), Value::UInt(y)) | (Value::UInt(y), Value::Int(x)) => {
375 u64::try_from(*x).is_ok_and(|x| x == *y)
376 }
377 (Value::Seq(x), Value::Seq(y)) => {
378 x.len() == y.len() && x.iter().zip(y).all(|(x, y)| value_eq(x, y))
379 }
380 (Value::Map(x), Value::Map(y)) => {
381 x.len() == y.len()
382 && x.iter()
383 .zip(y)
384 .all(|((kx, vx), (ky, vy))| kx == ky && value_eq(vx, vy))
385 }
386 (x, y) => x == y,
387 }
388}
389
390impl From<&serde_json::Value> for Node {
391 fn from(value: &serde_json::Value) -> Self {
392 use serde_json::Value as J;
393 Node::new(match value {
394 J::Null => Value::Null,
395 J::Bool(b) => Value::Bool(*b),
396 J::Number(n) => {
397 if let Some(i) = n.as_i64() {
398 Value::Int(i)
399 } else if let Some(u) = n.as_u64() {
400 Value::UInt(u)
401 } else {
402 Value::Float(n.as_f64().unwrap_or(f64::NAN))
403 }
404 }
405 J::String(s) => Value::String(s.clone()),
406 J::Array(items) => Value::Seq(items.iter().map(Node::from).collect()),
407 J::Object(entries) => Value::Map(
408 entries
409 .iter()
410 .map(|(k, v)| (k.clone(), Node::from(v)))
411 .collect(),
412 ),
413 })
414 }
415}
416
417impl From<serde_json::Value> for Node {
418 fn from(value: serde_json::Value) -> Self {
419 Node::from(&value)
420 }
421}
422
423pub fn from_serialize<T: serde::Serialize + ?Sized>(value: &T) -> Result<Node, DataError> {
438 value.serialize(ser::NodeSerializer)
439}
440
441pub(crate) fn scalar_text(value: &Value, quote: bool) -> (String, &'static str) {
449 match value {
450 Value::Null => ("null".into(), "json.null"),
451 Value::Bool(true) => ("true".into(), "json.bool_true"),
452 Value::Bool(false) => ("false".into(), "json.bool_false"),
453 Value::Int(i) => (i.to_string(), "json.number"),
454 Value::UInt(u) => (u.to_string(), "json.number"),
455 Value::Float(f) => (rich::pyformat::float_repr(*f), "json.number"),
456 Value::String(s) if quote => (quote_str(s), "json.str"),
457 Value::String(s) => (escape_controls(s), "json.str"),
458 Value::DateTime(s) => (s.clone(), "data.datetime"),
459 Value::Seq(_) | Value::Map(_) => (String::new(), "data.summary"),
460 }
461}
462
463pub fn display_value(node: &Node) -> String {
475 if node.is_container() {
476 summary(node)
477 } else {
478 scalar_text(&node.value, true).0
479 }
480}
481
482pub fn copy_text(node: &Node) -> String {
493 match &node.value {
494 Value::String(s) | Value::DateTime(s) => s.clone(),
495 _ if node.is_container() => {
496 serde_json::to_string_pretty(&node.to_json()).unwrap_or_default()
497 }
498 value => scalar_text(value, true).0,
499 }
500}
501
502pub(crate) fn quote_str(s: &str) -> String {
507 let quoted = serde_json::to_string(s).unwrap_or_else(|_| format!("{s:?}"));
508 if !quoted.chars().any(char::is_control) {
509 return quoted;
510 }
511 let mut out = String::with_capacity(quoted.len() + 8);
512 for c in quoted.chars() {
513 if c.is_control() {
514 out.push_str(&format!("\\u{:04x}", c as u32));
515 } else {
516 out.push(c);
517 }
518 }
519 out
520}
521
522pub(crate) fn escape_controls(s: &str) -> String {
525 if !s.chars().any(char::is_control) {
526 return s.to_string();
527 }
528 let quoted = quote_str(s);
529 quoted[1..quoted.len() - 1].replace("\\\"", "\"")
531}
532
533pub(crate) fn summary(node: &Node) -> String {
535 let plural = |n: usize, word: &str| {
536 if n == 1 {
537 format!("{n} {word}")
538 } else {
539 format!("{n} {word}s")
540 }
541 };
542 match &node.value {
543 Value::Map(e) if e.is_empty() => "{}".into(),
544 Value::Seq(i) if i.is_empty() => "[]".into(),
545 Value::Map(e) => format!("{{…}} {}", plural(e.len(), "key")),
546 Value::Seq(i) => format!("[…] {}", plural(i.len(), "item")),
547 _ => String::new(),
548 }
549}
550
551pub(crate) fn truncate_chars(text: &str, max: usize) -> Cow<'_, str> {
553 match text.char_indices().nth(max) {
554 Some((cut, _)) => Cow::Owned(format!("{}…", &text[..cut])),
555 None => Cow::Borrowed(text),
556 }
557}
558
559#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
565pub enum PathSegment {
566 Key(String),
567 Index(usize),
568}
569
570#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
574pub struct Path(Vec<PathSegment>);
575
576impl Path {
577 pub fn root() -> Self {
579 Path(Vec::new())
580 }
581
582 pub fn segments(&self) -> &[PathSegment] {
583 &self.0
584 }
585
586 pub fn is_root(&self) -> bool {
587 self.0.is_empty()
588 }
589
590 pub fn len(&self) -> usize {
591 self.0.len()
592 }
593
594 pub fn is_empty(&self) -> bool {
595 self.0.is_empty()
596 }
597
598 pub fn push(&mut self, segment: PathSegment) {
599 self.0.push(segment);
600 }
601
602 #[must_use]
604 pub fn child_key(&self, key: &str) -> Path {
605 let mut path = self.clone();
606 path.0.push(PathSegment::Key(key.to_string()));
607 path
608 }
609
610 #[must_use]
612 pub fn child_index(&self, index: usize) -> Path {
613 let mut path = self.clone();
614 path.0.push(PathSegment::Index(index));
615 path
616 }
617
618 pub fn parent(&self) -> Option<Path> {
620 (!self.0.is_empty()).then(|| Path(self.0[..self.0.len() - 1].to_vec()))
621 }
622
623 pub fn last(&self) -> Option<&PathSegment> {
624 self.0.last()
625 }
626
627 pub fn last_key(&self) -> Option<&str> {
630 self.0.iter().rev().find_map(|s| match s {
631 PathSegment::Key(k) => Some(k.as_str()),
632 PathSegment::Index(_) => None,
633 })
634 }
635}
636
637impl From<Vec<PathSegment>> for Path {
638 fn from(segments: Vec<PathSegment>) -> Self {
639 Path(segments)
640 }
641}
642
643pub(crate) fn is_identifier(key: &str) -> bool {
644 let mut chars = key.chars();
645 matches!(chars.next(), Some(c) if c.is_ascii_alphabetic() || c == '_')
646 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
647}
648
649pub(crate) fn segment_text(segment: &PathSegment, first: bool) -> String {
651 match segment {
652 PathSegment::Index(i) => format!("[{i}]"),
653 PathSegment::Key(k) if is_identifier(k) && first => k.clone(),
654 PathSegment::Key(k) if is_identifier(k) => format!(".{k}"),
655 PathSegment::Key(k) => format!("[{}]", quote_str(k)),
656 }
657}
658
659impl fmt::Display for Path {
660 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
661 for (i, segment) in self.0.iter().enumerate() {
662 f.write_str(&segment_text(segment, i == 0))?;
663 }
664 Ok(())
665 }
666}
667
668impl std::str::FromStr for Path {
669 type Err = String;
670
671 fn from_str(s: &str) -> Result<Self, Self::Err> {
674 let chars: Vec<char> = s.chars().collect();
675 let mut i = 0;
676 let mut path = Path::root();
677 if chars.first() == Some(&'$') {
678 i = 1;
679 }
680 let mut first = true;
681 while i < chars.len() {
682 match chars[i] {
683 '.' => {
684 i += 1;
685 let start = i;
686 while i < chars.len() && chars[i] != '.' && chars[i] != '[' {
687 i += 1;
688 }
689 if start == i {
690 return Err(format!("empty key at column {}", start + 1));
691 }
692 path.push(PathSegment::Key(chars[start..i].iter().collect()));
693 }
694 '[' => {
695 i += 1;
696 if chars.get(i) == Some(&'"') {
697 let start = i;
698 i += 1;
699 while i < chars.len() && chars[i] != '"' {
700 if chars[i] == '\\' {
701 i += 1;
702 }
703 i += 1;
704 }
705 let literal: String =
706 chars[start..(i + 1).min(chars.len())].iter().collect();
707 let key: String = serde_json::from_str(&literal)
708 .map_err(|_| format!("bad quoted key at column {}", start + 1))?;
709 i += 1;
710 path.push(PathSegment::Key(key));
711 } else {
712 let start = i;
713 while i < chars.len() && chars[i].is_ascii_digit() {
714 i += 1;
715 }
716 let digits: String = chars[start..i].iter().collect();
717 let index = digits
718 .parse()
719 .map_err(|_| format!("expected an index at column {}", start + 1))?;
720 path.push(PathSegment::Index(index));
721 }
722 if chars.get(i) != Some(&']') {
723 return Err(format!("expected `]` at column {}", i + 1));
724 }
725 i += 1;
726 }
727 _ if first => {
728 let start = i;
729 while i < chars.len() && chars[i] != '.' && chars[i] != '[' {
730 i += 1;
731 }
732 path.push(PathSegment::Key(chars[start..i].iter().collect()));
733 }
734 c => return Err(format!("unexpected `{c}` at column {}", i + 1)),
735 }
736 first = false;
737 }
738 Ok(path)
739 }
740}
741
742#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
748pub enum Format {
749 Json,
750 Yaml,
751 Toml,
752 Xml,
753 Ini,
754 Dotenv,
755}
756
757impl Format {
758 pub const ALL: [Format; 6] = [
760 Format::Json,
761 Format::Yaml,
762 Format::Toml,
763 Format::Xml,
764 Format::Ini,
765 Format::Dotenv,
766 ];
767
768 pub fn name(self) -> &'static str {
770 match self {
771 Format::Json => "json",
772 Format::Yaml => "yaml",
773 Format::Toml => "toml",
774 Format::Xml => "xml",
775 Format::Ini => "ini",
776 Format::Dotenv => "dotenv",
777 }
778 }
779
780 pub fn from_name(name: &str) -> Option<Format> {
782 match name.to_ascii_lowercase().as_str() {
783 "json" => Some(Format::Json),
784 "yaml" | "yml" => Some(Format::Yaml),
785 "toml" => Some(Format::Toml),
786 "xml" => Some(Format::Xml),
787 "ini" => Some(Format::Ini),
788 "dotenv" | "env" => Some(Format::Dotenv),
789 _ => None,
790 }
791 }
792
793 pub fn from_extension(extension: &str) -> Option<Format> {
796 let extension = extension.strip_prefix('.').unwrap_or(extension);
797 match extension.to_ascii_lowercase().as_str() {
798 "json" => Some(Format::Json),
799 "yaml" | "yml" => Some(Format::Yaml),
800 "toml" => Some(Format::Toml),
801 "xml" => Some(Format::Xml),
802 "ini" | "cfg" => Some(Format::Ini),
803 "env" => Some(Format::Dotenv),
804 _ => None,
805 }
806 }
807
808 pub fn from_file_name(name: &str) -> Option<Format> {
810 let base = name.rsplit(['/', '\\']).next().unwrap_or(name);
811 if base == ".env" || base.starts_with(".env.") {
812 return Some(Format::Dotenv);
813 }
814 let (stem, extension) = base.rsplit_once('.')?;
815 if stem.is_empty() {
816 return None;
817 }
818 Format::from_extension(extension)
819 }
820
821 pub fn is_enabled(self) -> bool {
823 match self {
824 Format::Json | Format::Ini | Format::Dotenv => true,
825 Format::Yaml => cfg!(feature = "yaml"),
826 Format::Toml => cfg!(feature = "toml"),
827 Format::Xml => cfg!(feature = "xml"),
828 }
829 }
830
831 pub fn detect(content: &str, name_hint: Option<&str>) -> Option<Format> {
870 if let Some(format) = name_hint.and_then(Format::from_file_name)
871 && format.is_enabled()
872 {
873 return Some(format);
874 }
875 let trimmed = content.trim_start_matches('\u{feff}').trim();
876 if trimmed.is_empty() {
877 return None;
878 }
879 if (trimmed.starts_with('{') || trimmed.starts_with('['))
880 && serde_json::from_str::<serde_json::Value>(trimmed)
881 .is_ok_and(|v| v.is_object() || v.is_array())
882 {
883 return Some(Format::Json);
884 }
885 #[cfg(feature = "xml")]
886 if xml::looks_like(trimmed) && xml::parse(content).is_ok() {
887 return Some(Format::Xml);
888 }
889 #[cfg(feature = "toml")]
890 if toml_doc::looks_like(content) {
891 return Some(Format::Toml);
892 }
893 if dotenv::looks_like(content) {
894 return Some(Format::Dotenv);
895 }
896 if ini::looks_like(content) {
897 return Some(Format::Ini);
898 }
899 #[cfg(feature = "yaml")]
900 if yaml::looks_like(content) {
901 return Some(Format::Yaml);
902 }
903 None
904 }
905}
906
907impl fmt::Display for Format {
908 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
909 f.write_str(match self {
910 Format::Json => "JSON",
911 Format::Yaml => "YAML",
912 Format::Toml => "TOML",
913 Format::Xml => "XML",
914 Format::Ini => "INI",
915 Format::Dotenv => "dotenv",
916 })
917 }
918}
919
920#[derive(Clone, Debug, PartialEq, Eq)]
922pub struct DataError {
923 pub format: Format,
924 pub message: String,
925 pub position: Option<Position>,
926}
927
928impl DataError {
929 pub fn new(format: Format, message: impl Into<String>, position: Option<Position>) -> Self {
933 DataError {
934 format,
935 message: escape_controls(&message.into()),
936 position,
937 }
938 }
939
940 pub fn to_diagnostic(&self, source: &str, name: &str) -> crate::diagnostic::Diagnostic {
957 use crate::diagnostic::{Diagnostic, Location, SourceSnippet};
958 use crate::event::EventView;
959 let mut diagnostic =
960 Diagnostic::error(format!("invalid {}: {}", self.format, self.message))
961 .view(EventView::Expanded);
962 let Some(position) = self.position else {
963 return diagnostic;
964 };
965 diagnostic = diagnostic.location(Location::new(
966 name,
967 Some(position.line),
968 Some(position.column),
969 ));
970 let start = byte_offset(source, position);
971 let end = source[start..]
972 .chars()
973 .next()
974 .filter(|c| *c != '\n' && *c != '\r')
975 .map_or(start, |c| start + c.len_utf8());
976 let (source, [start, end]) = picture_controls(source, [start, end]);
979 if let Ok(snippet) = SourceSnippet::new(name.to_string(), source, start..end, 1) {
980 diagnostic = diagnostic.snippet(snippet.primary_label(self.message.clone()));
981 }
982 diagnostic
983 }
984}
985
986fn picture_controls(source: &str, mut offsets: [usize; 2]) -> (String, [usize; 2]) {
991 let mut out = String::with_capacity(source.len());
992 let original = offsets;
993 let mut chars = source.char_indices().peekable();
994 while let Some((i, c)) = chars.next() {
995 for (offset, at) in offsets.iter_mut().zip(original) {
996 if at == i {
997 *offset = out.len();
998 }
999 }
1000 let keep = matches!(c, '\n' | '\t')
1001 || (c == '\r' && matches!(chars.peek(), None | Some((_, '\n'))));
1002 out.push(match c {
1003 _ if keep || !c.is_control() => c,
1004 '\u{0}'..='\u{1f}' => char::from_u32(0x2400 + c as u32).unwrap_or('\u{fffd}'),
1005 '\u{7f}' => '\u{2421}',
1006 _ => '\u{fffd}',
1007 });
1008 }
1009 for (offset, at) in offsets.iter_mut().zip(original) {
1010 if at >= source.len() {
1011 *offset = out.len();
1012 }
1013 }
1014 (out, offsets)
1015}
1016
1017impl fmt::Display for DataError {
1018 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1019 let message = escape_controls(&self.message);
1021 match self.position {
1022 Some(p) => write!(
1023 f,
1024 "invalid {} at line {}, column {}: {message}",
1025 self.format, p.line, p.column
1026 ),
1027 None => write!(f, "invalid {}: {message}", self.format),
1028 }
1029 }
1030}
1031
1032impl std::error::Error for DataError {}
1033
1034#[cfg(any(feature = "toml", feature = "xml"))]
1041pub(crate) struct LineIndex<'a> {
1042 source: &'a str,
1043 starts: Vec<usize>,
1044 last: std::cell::Cell<(usize, usize, usize)>,
1046}
1047
1048#[cfg(any(feature = "toml", feature = "xml"))]
1049impl<'a> LineIndex<'a> {
1050 pub(crate) fn new(source: &'a str) -> Self {
1051 let mut starts = vec![0];
1052 starts.extend(source.match_indices('\n').map(|(i, _)| i + 1));
1053 LineIndex {
1054 source,
1055 starts,
1056 last: std::cell::Cell::new((0, 1, 1)),
1057 }
1058 }
1059
1060 pub(crate) fn position(&self, offset: usize) -> Position {
1061 let mut offset = offset.min(self.source.len());
1062 while !self.source.is_char_boundary(offset) {
1063 offset -= 1;
1064 }
1065 let line = self.starts.partition_point(|s| *s <= offset);
1066 let start = self.starts[line - 1];
1067 let (last_offset, last_line, last_column) = self.last.get();
1068 let column = if last_line == line && last_offset >= start && last_offset <= offset {
1069 last_column + self.source[last_offset..offset].chars().count()
1070 } else {
1071 self.source[start..offset].chars().count() + 1
1072 };
1073 self.last.set((offset, line, column));
1074 Position::new(line, column)
1075 }
1076}
1077
1078pub(crate) fn byte_offset(source: &str, position: Position) -> usize {
1080 let mut offset = 0;
1081 for _ in 1..position.line {
1082 match source[offset..].find('\n') {
1083 Some(i) => offset += i + 1,
1084 None => return source.len(),
1085 }
1086 }
1087 let line_end = source[offset..]
1088 .find('\n')
1089 .map_or(source.len(), |i| offset + i);
1090 source[offset..line_end]
1091 .char_indices()
1092 .nth(position.column.saturating_sub(1))
1093 .map_or(line_end, |(i, _)| offset + i)
1094}
1095
1096pub fn parse(format: Format, content: &str) -> Result<Node, DataError> {
1105 match format {
1106 Format::Json => json::parse(content),
1107 Format::Ini => ini::parse(content),
1108 Format::Dotenv => dotenv::parse(content),
1109 #[cfg(feature = "yaml")]
1110 Format::Yaml => yaml::parse(content),
1111 #[cfg(feature = "toml")]
1112 Format::Toml => toml_doc::parse(content),
1113 #[cfg(feature = "xml")]
1114 Format::Xml => xml::parse(content),
1115 #[allow(unreachable_patterns)]
1116 other => Err(DataError::new(
1117 other,
1118 format!(
1119 "{} support is not compiled in (enable the `{}` feature)",
1120 other,
1121 other.name()
1122 ),
1123 None,
1124 )),
1125 }
1126}
1127
1128pub fn parse_json(content: &str) -> Result<Node, DataError> {
1132 json::parse(content)
1133}
1134
1135pub fn parse_ini(content: &str) -> Result<Node, DataError> {
1146 ini::parse(content)
1147}
1148
1149pub fn parse_dotenv(content: &str) -> Result<Node, DataError> {
1159 dotenv::parse(content)
1160}
1161
1162#[cfg(feature = "yaml")]
1172pub fn parse_yaml(content: &str) -> Result<Node, DataError> {
1173 yaml::parse(content)
1174}
1175
1176#[cfg(feature = "toml")]
1178pub fn parse_toml(content: &str) -> Result<Node, DataError> {
1179 toml_doc::parse(content)
1180}
1181
1182#[cfg(feature = "xml")]
1190pub fn parse_xml(content: &str) -> Result<Node, DataError> {
1191 xml::parse(content)
1192}
1193
1194#[cfg(all(test, any(feature = "toml", feature = "xml")))]
1195mod line_index_tests {
1196 use super::{LineIndex, Position};
1197
1198 fn naive(source: &str, offset: usize) -> Position {
1200 let mut offset = offset.min(source.len());
1201 while !source.is_char_boundary(offset) {
1202 offset -= 1;
1203 }
1204 let start = source[..offset].rfind('\n').map_or(0, |i| i + 1);
1205 let line = source[..offset].matches('\n').count() + 1;
1206 Position::new(line, source[start..offset].chars().count() + 1)
1207 }
1208
1209 #[test]
1210 fn cached_positions_match_counting_from_the_line_start() {
1211 let source = "ab\ncafé 👩👩👧 x\n\n<a b=\"é\">tëxt</a>\nlast";
1212 let index = LineIndex::new(source);
1213 let mut offsets: Vec<usize> = (0..=source.len() + 2).collect();
1216 let mut state = 7u64;
1217 for _ in 0..400 {
1218 state = state
1219 .wrapping_mul(6364136223846793005)
1220 .wrapping_add(1442695040888963407);
1221 offsets.push((state >> 33) as usize % (source.len() + 3));
1222 }
1223 for offset in offsets {
1224 assert_eq!(
1225 index.position(offset),
1226 naive(source, offset),
1227 "offset {offset}"
1228 );
1229 }
1230 }
1231}