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::{diff, Change, ChangeKind, DiffView};
73pub use explorer::{Explorer, View};
74pub use flatten::{flatten, unflatten, FlatView, UnflattenError};
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::{search, MatchKind, SearchMatch, SearchQuery, SearchResults};
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 pub fn at_position(mut self, position: Option<Position>) -> Self {
244 self.meta.position = position;
245 self
246 }
247
248 pub fn is_container(&self) -> bool {
250 matches!(self.value, Value::Seq(_) | Value::Map(_))
251 }
252
253 pub fn len(&self) -> usize {
255 match &self.value {
256 Value::Seq(items) => items.len(),
257 Value::Map(entries) => entries.len(),
258 _ => 0,
259 }
260 }
261
262 pub fn is_empty(&self) -> bool {
264 self.len() == 0
265 }
266
267 pub fn get(&self, key: &str) -> Option<&Node> {
269 match &self.value {
270 Value::Map(entries) => entries.iter().find(|(k, _)| k == key).map(|(_, v)| v),
271 _ => None,
272 }
273 }
274
275 pub fn index(&self, index: usize) -> Option<&Node> {
277 match &self.value {
278 Value::Seq(items) => items.get(index),
279 _ => None,
280 }
281 }
282
283 pub fn at(&self, path: &Path) -> Option<&Node> {
285 path.segments()
286 .iter()
287 .try_fold(self, |node, segment| match segment {
288 PathSegment::Key(key) => node.get(key),
289 PathSegment::Index(index) => node.index(*index),
290 })
291 }
292
293 pub fn as_str(&self) -> Option<&str> {
295 match &self.value {
296 Value::String(s) | Value::DateTime(s) => Some(s),
297 _ => None,
298 }
299 }
300
301 pub fn type_name(&self) -> &'static str {
304 match &self.value {
305 Value::Null => "null",
306 Value::Bool(_) => "bool",
307 Value::Int(_) | Value::UInt(_) => "int",
308 Value::Float(_) => "float",
309 Value::String(_) => "str",
310 Value::DateTime(_) => "datetime",
311 Value::Seq(_) => "seq",
312 Value::Map(_) => "map",
313 }
314 }
315
316 pub fn to_json(&self) -> serde_json::Value {
319 use serde_json::Value as J;
320 match &self.value {
321 Value::Null => J::Null,
322 Value::Bool(b) => J::Bool(*b),
323 Value::Int(i) => J::from(*i),
324 Value::UInt(u) => J::from(*u),
325 Value::Float(f) => serde_json::Number::from_f64(*f).map_or(J::Null, J::Number),
326 Value::String(s) | Value::DateTime(s) => J::String(s.clone()),
327 Value::Seq(items) => J::Array(items.iter().map(Node::to_json).collect()),
328 Value::Map(entries) => J::Object(
329 entries
330 .iter()
331 .map(|(k, v)| (k.clone(), v.to_json()))
332 .collect(),
333 ),
334 }
335 }
336
337 pub fn walk<'a>(&'a self, mut visit: impl FnMut(&Path, &'a Node)) {
339 let mut stack: Vec<(Path, &'a Node)> = vec![(Path::root(), self)];
342 while let Some((path, node)) = stack.pop() {
343 visit(&path, node);
344 match &node.value {
345 Value::Seq(items) => {
346 for (i, item) in items.iter().enumerate().rev() {
347 stack.push((path.child_index(i), item));
348 }
349 }
350 Value::Map(entries) => {
351 for (key, value) in entries.iter().rev() {
352 stack.push((path.child_key(key), value));
353 }
354 }
355 _ => {}
356 }
357 }
358 }
359
360 pub fn redacted(&self, redactor: &dyn Redactor) -> Node {
363 redact::apply(self, redactor)
364 }
365}
366
367pub(crate) fn value_eq(a: &Node, b: &Node) -> bool {
370 match (&a.value, &b.value) {
371 (Value::Float(x), Value::Float(y)) => x.to_bits() == y.to_bits() || x == y,
372 (Value::Int(x), Value::UInt(y)) | (Value::UInt(y), Value::Int(x)) => {
373 u64::try_from(*x).is_ok_and(|x| x == *y)
374 }
375 (Value::Seq(x), Value::Seq(y)) => {
376 x.len() == y.len() && x.iter().zip(y).all(|(x, y)| value_eq(x, y))
377 }
378 (Value::Map(x), Value::Map(y)) => {
379 x.len() == y.len()
380 && x.iter()
381 .zip(y)
382 .all(|((kx, vx), (ky, vy))| kx == ky && value_eq(vx, vy))
383 }
384 (x, y) => x == y,
385 }
386}
387
388impl From<&serde_json::Value> for Node {
389 fn from(value: &serde_json::Value) -> Self {
390 use serde_json::Value as J;
391 Node::new(match value {
392 J::Null => Value::Null,
393 J::Bool(b) => Value::Bool(*b),
394 J::Number(n) => {
395 if let Some(i) = n.as_i64() {
396 Value::Int(i)
397 } else if let Some(u) = n.as_u64() {
398 Value::UInt(u)
399 } else {
400 Value::Float(n.as_f64().unwrap_or(f64::NAN))
401 }
402 }
403 J::String(s) => Value::String(s.clone()),
404 J::Array(items) => Value::Seq(items.iter().map(Node::from).collect()),
405 J::Object(entries) => Value::Map(
406 entries
407 .iter()
408 .map(|(k, v)| (k.clone(), Node::from(v)))
409 .collect(),
410 ),
411 })
412 }
413}
414
415impl From<serde_json::Value> for Node {
416 fn from(value: serde_json::Value) -> Self {
417 Node::from(&value)
418 }
419}
420
421pub fn from_serialize<T: serde::Serialize + ?Sized>(value: &T) -> Result<Node, DataError> {
436 value.serialize(ser::NodeSerializer)
437}
438
439pub(crate) fn scalar_text(value: &Value, quote: bool) -> (String, &'static str) {
447 match value {
448 Value::Null => ("null".into(), "json.null"),
449 Value::Bool(true) => ("true".into(), "json.bool_true"),
450 Value::Bool(false) => ("false".into(), "json.bool_false"),
451 Value::Int(i) => (i.to_string(), "json.number"),
452 Value::UInt(u) => (u.to_string(), "json.number"),
453 Value::Float(f) => (rich::pyformat::float_repr(*f), "json.number"),
454 Value::String(s) if quote => (quote_str(s), "json.str"),
455 Value::String(s) => (escape_controls(s), "json.str"),
456 Value::DateTime(s) => (s.clone(), "data.datetime"),
457 Value::Seq(_) | Value::Map(_) => (String::new(), "data.summary"),
458 }
459}
460
461pub fn display_value(node: &Node) -> String {
473 if node.is_container() {
474 summary(node)
475 } else {
476 scalar_text(&node.value, true).0
477 }
478}
479
480pub fn copy_text(node: &Node) -> String {
491 match &node.value {
492 Value::String(s) | Value::DateTime(s) => s.clone(),
493 _ if node.is_container() => {
494 serde_json::to_string_pretty(&node.to_json()).unwrap_or_default()
495 }
496 value => scalar_text(value, true).0,
497 }
498}
499
500pub(crate) fn quote_str(s: &str) -> String {
505 let quoted = serde_json::to_string(s).unwrap_or_else(|_| format!("{s:?}"));
506 if !quoted.chars().any(char::is_control) {
507 return quoted;
508 }
509 let mut out = String::with_capacity(quoted.len() + 8);
510 for c in quoted.chars() {
511 if c.is_control() {
512 out.push_str(&format!("\\u{:04x}", c as u32));
513 } else {
514 out.push(c);
515 }
516 }
517 out
518}
519
520pub(crate) fn escape_controls(s: &str) -> String {
523 if !s.chars().any(char::is_control) {
524 return s.to_string();
525 }
526 let quoted = quote_str(s);
527 quoted[1..quoted.len() - 1].replace("\\\"", "\"")
529}
530
531pub(crate) fn summary(node: &Node) -> String {
533 let plural = |n: usize, word: &str| {
534 if n == 1 {
535 format!("{n} {word}")
536 } else {
537 format!("{n} {word}s")
538 }
539 };
540 match &node.value {
541 Value::Map(e) if e.is_empty() => "{}".into(),
542 Value::Seq(i) if i.is_empty() => "[]".into(),
543 Value::Map(e) => format!("{{…}} {}", plural(e.len(), "key")),
544 Value::Seq(i) => format!("[…] {}", plural(i.len(), "item")),
545 _ => String::new(),
546 }
547}
548
549pub(crate) fn truncate_chars(text: &str, max: usize) -> Cow<'_, str> {
551 match text.char_indices().nth(max) {
552 Some((cut, _)) => Cow::Owned(format!("{}…", &text[..cut])),
553 None => Cow::Borrowed(text),
554 }
555}
556
557#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
563pub enum PathSegment {
564 Key(String),
565 Index(usize),
566}
567
568#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
572pub struct Path(Vec<PathSegment>);
573
574impl Path {
575 pub fn root() -> Self {
577 Path(Vec::new())
578 }
579
580 pub fn segments(&self) -> &[PathSegment] {
581 &self.0
582 }
583
584 pub fn is_root(&self) -> bool {
585 self.0.is_empty()
586 }
587
588 pub fn len(&self) -> usize {
589 self.0.len()
590 }
591
592 pub fn is_empty(&self) -> bool {
593 self.0.is_empty()
594 }
595
596 pub fn push(&mut self, segment: PathSegment) {
597 self.0.push(segment);
598 }
599
600 pub fn child_key(&self, key: &str) -> Path {
602 let mut path = self.clone();
603 path.0.push(PathSegment::Key(key.to_string()));
604 path
605 }
606
607 pub fn child_index(&self, index: usize) -> Path {
609 let mut path = self.clone();
610 path.0.push(PathSegment::Index(index));
611 path
612 }
613
614 pub fn parent(&self) -> Option<Path> {
616 (!self.0.is_empty()).then(|| Path(self.0[..self.0.len() - 1].to_vec()))
617 }
618
619 pub fn last(&self) -> Option<&PathSegment> {
620 self.0.last()
621 }
622
623 pub fn last_key(&self) -> Option<&str> {
626 self.0.iter().rev().find_map(|s| match s {
627 PathSegment::Key(k) => Some(k.as_str()),
628 PathSegment::Index(_) => None,
629 })
630 }
631}
632
633impl From<Vec<PathSegment>> for Path {
634 fn from(segments: Vec<PathSegment>) -> Self {
635 Path(segments)
636 }
637}
638
639pub(crate) fn is_identifier(key: &str) -> bool {
640 let mut chars = key.chars();
641 matches!(chars.next(), Some(c) if c.is_ascii_alphabetic() || c == '_')
642 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
643}
644
645pub(crate) fn segment_text(segment: &PathSegment, first: bool) -> String {
647 match segment {
648 PathSegment::Index(i) => format!("[{i}]"),
649 PathSegment::Key(k) if is_identifier(k) && first => k.clone(),
650 PathSegment::Key(k) if is_identifier(k) => format!(".{k}"),
651 PathSegment::Key(k) => format!("[{}]", quote_str(k)),
652 }
653}
654
655impl fmt::Display for Path {
656 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
657 for (i, segment) in self.0.iter().enumerate() {
658 f.write_str(&segment_text(segment, i == 0))?;
659 }
660 Ok(())
661 }
662}
663
664impl std::str::FromStr for Path {
665 type Err = String;
666
667 fn from_str(s: &str) -> Result<Self, Self::Err> {
670 let chars: Vec<char> = s.chars().collect();
671 let mut i = 0;
672 let mut path = Path::root();
673 if chars.first() == Some(&'$') {
674 i = 1;
675 }
676 let mut first = true;
677 while i < chars.len() {
678 match chars[i] {
679 '.' => {
680 i += 1;
681 let start = i;
682 while i < chars.len() && chars[i] != '.' && chars[i] != '[' {
683 i += 1;
684 }
685 if start == i {
686 return Err(format!("empty key at column {}", start + 1));
687 }
688 path.push(PathSegment::Key(chars[start..i].iter().collect()));
689 }
690 '[' => {
691 i += 1;
692 if chars.get(i) == Some(&'"') {
693 let start = i;
694 i += 1;
695 while i < chars.len() && chars[i] != '"' {
696 if chars[i] == '\\' {
697 i += 1;
698 }
699 i += 1;
700 }
701 let literal: String =
702 chars[start..(i + 1).min(chars.len())].iter().collect();
703 let key: String = serde_json::from_str(&literal)
704 .map_err(|_| format!("bad quoted key at column {}", start + 1))?;
705 i += 1;
706 path.push(PathSegment::Key(key));
707 } else {
708 let start = i;
709 while i < chars.len() && chars[i].is_ascii_digit() {
710 i += 1;
711 }
712 let digits: String = chars[start..i].iter().collect();
713 let index = digits
714 .parse()
715 .map_err(|_| format!("expected an index at column {}", start + 1))?;
716 path.push(PathSegment::Index(index));
717 }
718 if chars.get(i) != Some(&']') {
719 return Err(format!("expected `]` at column {}", i + 1));
720 }
721 i += 1;
722 }
723 _ if first => {
724 let start = i;
725 while i < chars.len() && chars[i] != '.' && chars[i] != '[' {
726 i += 1;
727 }
728 path.push(PathSegment::Key(chars[start..i].iter().collect()));
729 }
730 c => return Err(format!("unexpected `{c}` at column {}", i + 1)),
731 }
732 first = false;
733 }
734 Ok(path)
735 }
736}
737
738#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
744pub enum Format {
745 Json,
746 Yaml,
747 Toml,
748 Xml,
749 Ini,
750 Dotenv,
751}
752
753impl Format {
754 pub const ALL: [Format; 6] = [
756 Format::Json,
757 Format::Yaml,
758 Format::Toml,
759 Format::Xml,
760 Format::Ini,
761 Format::Dotenv,
762 ];
763
764 pub fn name(self) -> &'static str {
766 match self {
767 Format::Json => "json",
768 Format::Yaml => "yaml",
769 Format::Toml => "toml",
770 Format::Xml => "xml",
771 Format::Ini => "ini",
772 Format::Dotenv => "dotenv",
773 }
774 }
775
776 pub fn from_name(name: &str) -> Option<Format> {
778 match name.to_ascii_lowercase().as_str() {
779 "json" => Some(Format::Json),
780 "yaml" | "yml" => Some(Format::Yaml),
781 "toml" => Some(Format::Toml),
782 "xml" => Some(Format::Xml),
783 "ini" => Some(Format::Ini),
784 "dotenv" | "env" => Some(Format::Dotenv),
785 _ => None,
786 }
787 }
788
789 pub fn from_extension(extension: &str) -> Option<Format> {
792 let extension = extension.strip_prefix('.').unwrap_or(extension);
793 match extension.to_ascii_lowercase().as_str() {
794 "json" => Some(Format::Json),
795 "yaml" | "yml" => Some(Format::Yaml),
796 "toml" => Some(Format::Toml),
797 "xml" => Some(Format::Xml),
798 "ini" | "cfg" => Some(Format::Ini),
799 "env" => Some(Format::Dotenv),
800 _ => None,
801 }
802 }
803
804 pub fn from_file_name(name: &str) -> Option<Format> {
806 let base = name.rsplit(['/', '\\']).next().unwrap_or(name);
807 if base == ".env" || base.starts_with(".env.") {
808 return Some(Format::Dotenv);
809 }
810 let (stem, extension) = base.rsplit_once('.')?;
811 if stem.is_empty() {
812 return None;
813 }
814 Format::from_extension(extension)
815 }
816
817 pub fn is_enabled(self) -> bool {
819 match self {
820 Format::Json | Format::Ini | Format::Dotenv => true,
821 Format::Yaml => cfg!(feature = "yaml"),
822 Format::Toml => cfg!(feature = "toml"),
823 Format::Xml => cfg!(feature = "xml"),
824 }
825 }
826
827 pub fn detect(content: &str, name_hint: Option<&str>) -> Option<Format> {
866 if let Some(format) = name_hint.and_then(Format::from_file_name) {
867 if format.is_enabled() {
868 return Some(format);
869 }
870 }
871 let trimmed = content.trim_start_matches('\u{feff}').trim();
872 if trimmed.is_empty() {
873 return None;
874 }
875 if (trimmed.starts_with('{') || trimmed.starts_with('['))
876 && serde_json::from_str::<serde_json::Value>(trimmed)
877 .is_ok_and(|v| v.is_object() || v.is_array())
878 {
879 return Some(Format::Json);
880 }
881 #[cfg(feature = "xml")]
882 if xml::looks_like(trimmed) && xml::parse(content).is_ok() {
883 return Some(Format::Xml);
884 }
885 #[cfg(feature = "toml")]
886 if toml_doc::looks_like(content) {
887 return Some(Format::Toml);
888 }
889 if dotenv::looks_like(content) {
890 return Some(Format::Dotenv);
891 }
892 if ini::looks_like(content) {
893 return Some(Format::Ini);
894 }
895 #[cfg(feature = "yaml")]
896 if yaml::looks_like(content) {
897 return Some(Format::Yaml);
898 }
899 None
900 }
901}
902
903impl fmt::Display for Format {
904 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
905 f.write_str(match self {
906 Format::Json => "JSON",
907 Format::Yaml => "YAML",
908 Format::Toml => "TOML",
909 Format::Xml => "XML",
910 Format::Ini => "INI",
911 Format::Dotenv => "dotenv",
912 })
913 }
914}
915
916#[derive(Clone, Debug, PartialEq, Eq)]
918pub struct DataError {
919 pub format: Format,
920 pub message: String,
921 pub position: Option<Position>,
922}
923
924impl DataError {
925 pub fn new(format: Format, message: impl Into<String>, position: Option<Position>) -> Self {
929 DataError {
930 format,
931 message: escape_controls(&message.into()),
932 position,
933 }
934 }
935
936 pub fn to_diagnostic(&self, source: &str, name: &str) -> crate::diagnostic::Diagnostic {
953 use crate::diagnostic::{Diagnostic, Location, SourceSnippet};
954 use crate::event::EventView;
955 let mut diagnostic =
956 Diagnostic::error(format!("invalid {}: {}", self.format, self.message))
957 .view(EventView::Expanded);
958 let Some(position) = self.position else {
959 return diagnostic;
960 };
961 diagnostic = diagnostic.location(Location::new(
962 name,
963 Some(position.line),
964 Some(position.column),
965 ));
966 let start = byte_offset(source, position);
967 let end = source[start..]
968 .chars()
969 .next()
970 .filter(|c| *c != '\n' && *c != '\r')
971 .map_or(start, |c| start + c.len_utf8());
972 let (source, [start, end]) = picture_controls(source, [start, end]);
975 if let Ok(snippet) = SourceSnippet::new(name.to_string(), source, start..end, 1) {
976 diagnostic = diagnostic.snippet(snippet.primary_label(self.message.clone()));
977 }
978 diagnostic
979 }
980}
981
982fn picture_controls(source: &str, mut offsets: [usize; 2]) -> (String, [usize; 2]) {
987 let mut out = String::with_capacity(source.len());
988 let original = offsets;
989 let mut chars = source.char_indices().peekable();
990 while let Some((i, c)) = chars.next() {
991 for (offset, at) in offsets.iter_mut().zip(original) {
992 if at == i {
993 *offset = out.len();
994 }
995 }
996 let keep = matches!(c, '\n' | '\t')
997 || (c == '\r' && matches!(chars.peek(), None | Some((_, '\n'))));
998 out.push(match c {
999 _ if keep || !c.is_control() => c,
1000 '\u{0}'..='\u{1f}' => char::from_u32(0x2400 + c as u32).unwrap_or('\u{fffd}'),
1001 '\u{7f}' => '\u{2421}',
1002 _ => '\u{fffd}',
1003 });
1004 }
1005 for (offset, at) in offsets.iter_mut().zip(original) {
1006 if at >= source.len() {
1007 *offset = out.len();
1008 }
1009 }
1010 (out, offsets)
1011}
1012
1013impl fmt::Display for DataError {
1014 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1015 let message = escape_controls(&self.message);
1017 match self.position {
1018 Some(p) => write!(
1019 f,
1020 "invalid {} at line {}, column {}: {message}",
1021 self.format, p.line, p.column
1022 ),
1023 None => write!(f, "invalid {}: {message}", self.format),
1024 }
1025 }
1026}
1027
1028impl std::error::Error for DataError {}
1029
1030#[cfg(any(feature = "toml", feature = "xml"))]
1037pub(crate) struct LineIndex<'a> {
1038 source: &'a str,
1039 starts: Vec<usize>,
1040 last: std::cell::Cell<(usize, usize, usize)>,
1042}
1043
1044#[cfg(any(feature = "toml", feature = "xml"))]
1045impl<'a> LineIndex<'a> {
1046 pub(crate) fn new(source: &'a str) -> Self {
1047 let mut starts = vec![0];
1048 starts.extend(source.match_indices('\n').map(|(i, _)| i + 1));
1049 LineIndex {
1050 source,
1051 starts,
1052 last: std::cell::Cell::new((0, 1, 1)),
1053 }
1054 }
1055
1056 pub(crate) fn position(&self, offset: usize) -> Position {
1057 let mut offset = offset.min(self.source.len());
1058 while !self.source.is_char_boundary(offset) {
1059 offset -= 1;
1060 }
1061 let line = self.starts.partition_point(|s| *s <= offset);
1062 let start = self.starts[line - 1];
1063 let (last_offset, last_line, last_column) = self.last.get();
1064 let column = if last_line == line && last_offset >= start && last_offset <= offset {
1065 last_column + self.source[last_offset..offset].chars().count()
1066 } else {
1067 self.source[start..offset].chars().count() + 1
1068 };
1069 self.last.set((offset, line, column));
1070 Position::new(line, column)
1071 }
1072}
1073
1074pub(crate) fn byte_offset(source: &str, position: Position) -> usize {
1076 let mut offset = 0;
1077 for _ in 1..position.line {
1078 match source[offset..].find('\n') {
1079 Some(i) => offset += i + 1,
1080 None => return source.len(),
1081 }
1082 }
1083 let line_end = source[offset..]
1084 .find('\n')
1085 .map_or(source.len(), |i| offset + i);
1086 source[offset..line_end]
1087 .char_indices()
1088 .nth(position.column.saturating_sub(1))
1089 .map_or(line_end, |(i, _)| offset + i)
1090}
1091
1092pub fn parse(format: Format, content: &str) -> Result<Node, DataError> {
1101 match format {
1102 Format::Json => json::parse(content),
1103 Format::Ini => ini::parse(content),
1104 Format::Dotenv => dotenv::parse(content),
1105 #[cfg(feature = "yaml")]
1106 Format::Yaml => yaml::parse(content),
1107 #[cfg(feature = "toml")]
1108 Format::Toml => toml_doc::parse(content),
1109 #[cfg(feature = "xml")]
1110 Format::Xml => xml::parse(content),
1111 #[allow(unreachable_patterns)]
1112 other => Err(DataError::new(
1113 other,
1114 format!(
1115 "{} support is not compiled in (enable the `{}` feature)",
1116 other,
1117 other.name()
1118 ),
1119 None,
1120 )),
1121 }
1122}
1123
1124pub fn parse_json(content: &str) -> Result<Node, DataError> {
1128 json::parse(content)
1129}
1130
1131pub fn parse_ini(content: &str) -> Result<Node, DataError> {
1142 ini::parse(content)
1143}
1144
1145pub fn parse_dotenv(content: &str) -> Result<Node, DataError> {
1155 dotenv::parse(content)
1156}
1157
1158#[cfg(feature = "yaml")]
1168pub fn parse_yaml(content: &str) -> Result<Node, DataError> {
1169 yaml::parse(content)
1170}
1171
1172#[cfg(feature = "toml")]
1174pub fn parse_toml(content: &str) -> Result<Node, DataError> {
1175 toml_doc::parse(content)
1176}
1177
1178#[cfg(feature = "xml")]
1186pub fn parse_xml(content: &str) -> Result<Node, DataError> {
1187 xml::parse(content)
1188}
1189
1190#[cfg(all(test, any(feature = "toml", feature = "xml")))]
1191mod line_index_tests {
1192 use super::{LineIndex, Position};
1193
1194 fn naive(source: &str, offset: usize) -> Position {
1196 let mut offset = offset.min(source.len());
1197 while !source.is_char_boundary(offset) {
1198 offset -= 1;
1199 }
1200 let start = source[..offset].rfind('\n').map_or(0, |i| i + 1);
1201 let line = source[..offset].matches('\n').count() + 1;
1202 Position::new(line, source[start..offset].chars().count() + 1)
1203 }
1204
1205 #[test]
1206 fn cached_positions_match_counting_from_the_line_start() {
1207 let source = "ab\ncafé 👩👩👧 x\n\n<a b=\"é\">tëxt</a>\nlast";
1208 let index = LineIndex::new(source);
1209 let mut offsets: Vec<usize> = (0..=source.len() + 2).collect();
1212 let mut state = 7u64;
1213 for _ in 0..400 {
1214 state = state
1215 .wrapping_mul(6364136223846793005)
1216 .wrapping_add(1442695040888963407);
1217 offsets.push((state >> 33) as usize % (source.len() + 3));
1218 }
1219 for offset in offsets {
1220 assert_eq!(
1221 index.position(offset),
1222 naive(source, offset),
1223 "offset {offset}"
1224 );
1225 }
1226 }
1227}