use std::borrow::Cow;
use std::fmt;
use rich::{Console, Style};
use crate::event::theme_style;
mod config;
mod diff;
mod dotenv;
mod explorer;
mod flatten;
mod helpers;
mod ini;
mod json;
mod redact;
mod search;
pub mod select;
mod ser;
mod table;
#[cfg(feature = "jsonpath")]
pub mod transform;
#[cfg(feature = "toml")]
mod toml_doc;
#[cfg(feature = "xml")]
mod xml;
#[cfg(feature = "yaml")]
mod yaml;
pub use config::ConfigFileView;
pub use diff::{diff, Change, ChangeKind, DiffView};
pub use explorer::{Explorer, View};
pub use flatten::{flatten, unflatten, FlatView, UnflattenError};
pub use helpers::{
json, print_json, print_json_to, print_table, print_table_to, print_tree, print_tree_to, table,
tree,
};
pub use redact::SECRET_KEYS;
pub use redact::{Redaction, Redactor};
pub use search::{search, MatchKind, SearchMatch, SearchQuery, SearchResults};
#[cfg(feature = "jsonpath")]
pub use select::{JsonPath, JsonPathSelector};
pub use select::{SelectError, Selector, SelectorBackend, Selectors};
pub use table::{TableOptions, TableView};
#[cfg(any(feature = "yaml", feature = "xml"))]
pub(crate) const MAX_DEPTH: usize = 512;
pub const DATA_STYLES: &[(&str, &str)] = &[
("data.match", "bold reverse yellow"),
("data.anchor", "dim cyan"),
("data.alias", "dim cyan"),
("data.attribute", "not italic yellow"),
("data.index", "dim"),
("data.comment", "dim"),
("data.path", "dim"),
("data.type", "dim italic"),
("data.summary", "dim"),
("data.null", "dim"),
("data.datetime", "magenta"),
("data.section", "bold"),
("data.added", "green"),
("data.removed", "red"),
("data.changed", "yellow"),
];
const JSON_STYLES: &[(&str, &str)] = &[
("json.key", "bold blue"),
("json.str", "not bold not italic green"),
("json.number", "bold not italic cyan"),
("json.bool_true", "italic bright_green"),
("json.bool_false", "italic bright_red"),
("json.null", "italic magenta"),
];
pub(crate) fn style(console: &Console, key: &str) -> Style {
let fallback = DATA_STYLES
.iter()
.chain(JSON_STYLES)
.find(|(name, _)| *name == key)
.map_or("", |(_, spec)| *spec);
theme_style(console, key, fallback)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Position {
pub line: usize,
pub column: usize,
}
impl Position {
pub fn new(line: usize, column: usize) -> Self {
Position { line, column }
}
}
impl fmt::Display for Position {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.line, self.column)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum XmlKind {
Element,
Attribute,
Text,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Meta {
pub position: Option<Position>,
pub anchor: Option<String>,
pub alias: Option<String>,
pub comment: Option<String>,
pub xml: Option<XmlKind>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Value {
Null,
Bool(bool),
Int(i64),
UInt(u64),
Float(f64),
String(String),
DateTime(String),
Seq(Vec<Node>),
Map(Vec<(String, Node)>),
}
#[derive(Clone, Debug, PartialEq)]
pub struct Node {
pub value: Value,
pub meta: Meta,
}
impl From<Value> for Node {
fn from(value: Value) -> Self {
Node::new(value)
}
}
impl<'a> From<Node> for Cow<'a, Node> {
fn from(node: Node) -> Self {
Cow::Owned(node)
}
}
impl<'a> From<&'a Node> for Cow<'a, Node> {
fn from(node: &'a Node) -> Self {
Cow::Borrowed(node)
}
}
impl Node {
pub fn new(value: Value) -> Self {
Node {
value,
meta: Meta::default(),
}
}
pub fn with_meta(value: Value, meta: Meta) -> Self {
Node { value, meta }
}
pub fn at_position(mut self, position: Option<Position>) -> Self {
self.meta.position = position;
self
}
pub fn is_container(&self) -> bool {
matches!(self.value, Value::Seq(_) | Value::Map(_))
}
pub fn len(&self) -> usize {
match &self.value {
Value::Seq(items) => items.len(),
Value::Map(entries) => entries.len(),
_ => 0,
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn get(&self, key: &str) -> Option<&Node> {
match &self.value {
Value::Map(entries) => entries.iter().find(|(k, _)| k == key).map(|(_, v)| v),
_ => None,
}
}
pub fn index(&self, index: usize) -> Option<&Node> {
match &self.value {
Value::Seq(items) => items.get(index),
_ => None,
}
}
pub fn at(&self, path: &Path) -> Option<&Node> {
path.segments()
.iter()
.try_fold(self, |node, segment| match segment {
PathSegment::Key(key) => node.get(key),
PathSegment::Index(index) => node.index(*index),
})
}
pub fn as_str(&self) -> Option<&str> {
match &self.value {
Value::String(s) | Value::DateTime(s) => Some(s),
_ => None,
}
}
pub fn type_name(&self) -> &'static str {
match &self.value {
Value::Null => "null",
Value::Bool(_) => "bool",
Value::Int(_) | Value::UInt(_) => "int",
Value::Float(_) => "float",
Value::String(_) => "str",
Value::DateTime(_) => "datetime",
Value::Seq(_) => "seq",
Value::Map(_) => "map",
}
}
pub fn to_json(&self) -> serde_json::Value {
use serde_json::Value as J;
match &self.value {
Value::Null => J::Null,
Value::Bool(b) => J::Bool(*b),
Value::Int(i) => J::from(*i),
Value::UInt(u) => J::from(*u),
Value::Float(f) => serde_json::Number::from_f64(*f).map_or(J::Null, J::Number),
Value::String(s) | Value::DateTime(s) => J::String(s.clone()),
Value::Seq(items) => J::Array(items.iter().map(Node::to_json).collect()),
Value::Map(entries) => J::Object(
entries
.iter()
.map(|(k, v)| (k.clone(), v.to_json()))
.collect(),
),
}
}
pub fn walk<'a>(&'a self, mut visit: impl FnMut(&Path, &'a Node)) {
let mut stack: Vec<(Path, &'a Node)> = vec![(Path::root(), self)];
while let Some((path, node)) = stack.pop() {
visit(&path, node);
match &node.value {
Value::Seq(items) => {
for (i, item) in items.iter().enumerate().rev() {
stack.push((path.child_index(i), item));
}
}
Value::Map(entries) => {
for (key, value) in entries.iter().rev() {
stack.push((path.child_key(key), value));
}
}
_ => {}
}
}
}
pub fn redacted(&self, redactor: &dyn Redactor) -> Node {
redact::apply(self, redactor)
}
}
pub(crate) fn value_eq(a: &Node, b: &Node) -> bool {
match (&a.value, &b.value) {
(Value::Float(x), Value::Float(y)) => x.to_bits() == y.to_bits() || x == y,
(Value::Int(x), Value::UInt(y)) | (Value::UInt(y), Value::Int(x)) => {
u64::try_from(*x).is_ok_and(|x| x == *y)
}
(Value::Seq(x), Value::Seq(y)) => {
x.len() == y.len() && x.iter().zip(y).all(|(x, y)| value_eq(x, y))
}
(Value::Map(x), Value::Map(y)) => {
x.len() == y.len()
&& x.iter()
.zip(y)
.all(|((kx, vx), (ky, vy))| kx == ky && value_eq(vx, vy))
}
(x, y) => x == y,
}
}
impl From<&serde_json::Value> for Node {
fn from(value: &serde_json::Value) -> Self {
use serde_json::Value as J;
Node::new(match value {
J::Null => Value::Null,
J::Bool(b) => Value::Bool(*b),
J::Number(n) => {
if let Some(i) = n.as_i64() {
Value::Int(i)
} else if let Some(u) = n.as_u64() {
Value::UInt(u)
} else {
Value::Float(n.as_f64().unwrap_or(f64::NAN))
}
}
J::String(s) => Value::String(s.clone()),
J::Array(items) => Value::Seq(items.iter().map(Node::from).collect()),
J::Object(entries) => Value::Map(
entries
.iter()
.map(|(k, v)| (k.clone(), Node::from(v)))
.collect(),
),
})
}
}
impl From<serde_json::Value> for Node {
fn from(value: serde_json::Value) -> Self {
Node::from(&value)
}
}
pub fn from_serialize<T: serde::Serialize + ?Sized>(value: &T) -> Result<Node, DataError> {
value.serialize(ser::NodeSerializer)
}
pub(crate) fn scalar_text(value: &Value, quote: bool) -> (String, &'static str) {
match value {
Value::Null => ("null".into(), "json.null"),
Value::Bool(true) => ("true".into(), "json.bool_true"),
Value::Bool(false) => ("false".into(), "json.bool_false"),
Value::Int(i) => (i.to_string(), "json.number"),
Value::UInt(u) => (u.to_string(), "json.number"),
Value::Float(f) => (rich::pyformat::float_repr(*f), "json.number"),
Value::String(s) if quote => (quote_str(s), "json.str"),
Value::String(s) => (escape_controls(s), "json.str"),
Value::DateTime(s) => (s.clone(), "data.datetime"),
Value::Seq(_) | Value::Map(_) => (String::new(), "data.summary"),
}
}
pub fn display_value(node: &Node) -> String {
if node.is_container() {
summary(node)
} else {
scalar_text(&node.value, true).0
}
}
pub fn copy_text(node: &Node) -> String {
match &node.value {
Value::String(s) | Value::DateTime(s) => s.clone(),
_ if node.is_container() => {
serde_json::to_string_pretty(&node.to_json()).unwrap_or_default()
}
value => scalar_text(value, true).0,
}
}
pub(crate) fn quote_str(s: &str) -> String {
let quoted = serde_json::to_string(s).unwrap_or_else(|_| format!("{s:?}"));
if !quoted.chars().any(char::is_control) {
return quoted;
}
let mut out = String::with_capacity(quoted.len() + 8);
for c in quoted.chars() {
if c.is_control() {
out.push_str(&format!("\\u{:04x}", c as u32));
} else {
out.push(c);
}
}
out
}
pub(crate) fn escape_controls(s: &str) -> String {
if !s.chars().any(char::is_control) {
return s.to_string();
}
let quoted = quote_str(s);
quoted[1..quoted.len() - 1].replace("\\\"", "\"")
}
pub(crate) fn summary(node: &Node) -> String {
let plural = |n: usize, word: &str| {
if n == 1 {
format!("{n} {word}")
} else {
format!("{n} {word}s")
}
};
match &node.value {
Value::Map(e) if e.is_empty() => "{}".into(),
Value::Seq(i) if i.is_empty() => "[]".into(),
Value::Map(e) => format!("{{…}} {}", plural(e.len(), "key")),
Value::Seq(i) => format!("[…] {}", plural(i.len(), "item")),
_ => String::new(),
}
}
pub(crate) fn truncate_chars(text: &str, max: usize) -> Cow<'_, str> {
match text.char_indices().nth(max) {
Some((cut, _)) => Cow::Owned(format!("{}…", &text[..cut])),
None => Cow::Borrowed(text),
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum PathSegment {
Key(String),
Index(usize),
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Path(Vec<PathSegment>);
impl Path {
pub fn root() -> Self {
Path(Vec::new())
}
pub fn segments(&self) -> &[PathSegment] {
&self.0
}
pub fn is_root(&self) -> bool {
self.0.is_empty()
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn push(&mut self, segment: PathSegment) {
self.0.push(segment);
}
pub fn child_key(&self, key: &str) -> Path {
let mut path = self.clone();
path.0.push(PathSegment::Key(key.to_string()));
path
}
pub fn child_index(&self, index: usize) -> Path {
let mut path = self.clone();
path.0.push(PathSegment::Index(index));
path
}
pub fn parent(&self) -> Option<Path> {
(!self.0.is_empty()).then(|| Path(self.0[..self.0.len() - 1].to_vec()))
}
pub fn last(&self) -> Option<&PathSegment> {
self.0.last()
}
pub fn last_key(&self) -> Option<&str> {
self.0.iter().rev().find_map(|s| match s {
PathSegment::Key(k) => Some(k.as_str()),
PathSegment::Index(_) => None,
})
}
}
impl From<Vec<PathSegment>> for Path {
fn from(segments: Vec<PathSegment>) -> Self {
Path(segments)
}
}
pub(crate) fn is_identifier(key: &str) -> bool {
let mut chars = key.chars();
matches!(chars.next(), Some(c) if c.is_ascii_alphabetic() || c == '_')
&& chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
}
pub(crate) fn segment_text(segment: &PathSegment, first: bool) -> String {
match segment {
PathSegment::Index(i) => format!("[{i}]"),
PathSegment::Key(k) if is_identifier(k) && first => k.clone(),
PathSegment::Key(k) if is_identifier(k) => format!(".{k}"),
PathSegment::Key(k) => format!("[{}]", quote_str(k)),
}
}
impl fmt::Display for Path {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, segment) in self.0.iter().enumerate() {
f.write_str(&segment_text(segment, i == 0))?;
}
Ok(())
}
}
impl std::str::FromStr for Path {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let chars: Vec<char> = s.chars().collect();
let mut i = 0;
let mut path = Path::root();
if chars.first() == Some(&'$') {
i = 1;
}
let mut first = true;
while i < chars.len() {
match chars[i] {
'.' => {
i += 1;
let start = i;
while i < chars.len() && chars[i] != '.' && chars[i] != '[' {
i += 1;
}
if start == i {
return Err(format!("empty key at column {}", start + 1));
}
path.push(PathSegment::Key(chars[start..i].iter().collect()));
}
'[' => {
i += 1;
if chars.get(i) == Some(&'"') {
let start = i;
i += 1;
while i < chars.len() && chars[i] != '"' {
if chars[i] == '\\' {
i += 1;
}
i += 1;
}
let literal: String =
chars[start..(i + 1).min(chars.len())].iter().collect();
let key: String = serde_json::from_str(&literal)
.map_err(|_| format!("bad quoted key at column {}", start + 1))?;
i += 1;
path.push(PathSegment::Key(key));
} else {
let start = i;
while i < chars.len() && chars[i].is_ascii_digit() {
i += 1;
}
let digits: String = chars[start..i].iter().collect();
let index = digits
.parse()
.map_err(|_| format!("expected an index at column {}", start + 1))?;
path.push(PathSegment::Index(index));
}
if chars.get(i) != Some(&']') {
return Err(format!("expected `]` at column {}", i + 1));
}
i += 1;
}
_ if first => {
let start = i;
while i < chars.len() && chars[i] != '.' && chars[i] != '[' {
i += 1;
}
path.push(PathSegment::Key(chars[start..i].iter().collect()));
}
c => return Err(format!("unexpected `{c}` at column {}", i + 1)),
}
first = false;
}
Ok(path)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Format {
Json,
Yaml,
Toml,
Xml,
Ini,
Dotenv,
}
impl Format {
pub const ALL: [Format; 6] = [
Format::Json,
Format::Yaml,
Format::Toml,
Format::Xml,
Format::Ini,
Format::Dotenv,
];
pub fn name(self) -> &'static str {
match self {
Format::Json => "json",
Format::Yaml => "yaml",
Format::Toml => "toml",
Format::Xml => "xml",
Format::Ini => "ini",
Format::Dotenv => "dotenv",
}
}
pub fn from_name(name: &str) -> Option<Format> {
match name.to_ascii_lowercase().as_str() {
"json" => Some(Format::Json),
"yaml" | "yml" => Some(Format::Yaml),
"toml" => Some(Format::Toml),
"xml" => Some(Format::Xml),
"ini" => Some(Format::Ini),
"dotenv" | "env" => Some(Format::Dotenv),
_ => None,
}
}
pub fn from_extension(extension: &str) -> Option<Format> {
let extension = extension.strip_prefix('.').unwrap_or(extension);
match extension.to_ascii_lowercase().as_str() {
"json" => Some(Format::Json),
"yaml" | "yml" => Some(Format::Yaml),
"toml" => Some(Format::Toml),
"xml" => Some(Format::Xml),
"ini" | "cfg" => Some(Format::Ini),
"env" => Some(Format::Dotenv),
_ => None,
}
}
pub fn from_file_name(name: &str) -> Option<Format> {
let base = name.rsplit(['/', '\\']).next().unwrap_or(name);
if base == ".env" || base.starts_with(".env.") {
return Some(Format::Dotenv);
}
let (stem, extension) = base.rsplit_once('.')?;
if stem.is_empty() {
return None;
}
Format::from_extension(extension)
}
pub fn is_enabled(self) -> bool {
match self {
Format::Json | Format::Ini | Format::Dotenv => true,
Format::Yaml => cfg!(feature = "yaml"),
Format::Toml => cfg!(feature = "toml"),
Format::Xml => cfg!(feature = "xml"),
}
}
pub fn detect(content: &str, name_hint: Option<&str>) -> Option<Format> {
if let Some(format) = name_hint.and_then(Format::from_file_name) {
if format.is_enabled() {
return Some(format);
}
}
let trimmed = content.trim_start_matches('\u{feff}').trim();
if trimmed.is_empty() {
return None;
}
if (trimmed.starts_with('{') || trimmed.starts_with('['))
&& serde_json::from_str::<serde_json::Value>(trimmed)
.is_ok_and(|v| v.is_object() || v.is_array())
{
return Some(Format::Json);
}
#[cfg(feature = "xml")]
if xml::looks_like(trimmed) && xml::parse(content).is_ok() {
return Some(Format::Xml);
}
#[cfg(feature = "toml")]
if toml_doc::looks_like(content) {
return Some(Format::Toml);
}
if dotenv::looks_like(content) {
return Some(Format::Dotenv);
}
if ini::looks_like(content) {
return Some(Format::Ini);
}
#[cfg(feature = "yaml")]
if yaml::looks_like(content) {
return Some(Format::Yaml);
}
None
}
}
impl fmt::Display for Format {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Format::Json => "JSON",
Format::Yaml => "YAML",
Format::Toml => "TOML",
Format::Xml => "XML",
Format::Ini => "INI",
Format::Dotenv => "dotenv",
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DataError {
pub format: Format,
pub message: String,
pub position: Option<Position>,
}
impl DataError {
pub fn new(format: Format, message: impl Into<String>, position: Option<Position>) -> Self {
DataError {
format,
message: escape_controls(&message.into()),
position,
}
}
pub fn to_diagnostic(&self, source: &str, name: &str) -> crate::diagnostic::Diagnostic {
use crate::diagnostic::{Diagnostic, Location, SourceSnippet};
use crate::event::EventView;
let mut diagnostic =
Diagnostic::error(format!("invalid {}: {}", self.format, self.message))
.view(EventView::Expanded);
let Some(position) = self.position else {
return diagnostic;
};
diagnostic = diagnostic.location(Location::new(
name,
Some(position.line),
Some(position.column),
));
let start = byte_offset(source, position);
let end = source[start..]
.chars()
.next()
.filter(|c| *c != '\n' && *c != '\r')
.map_or(start, |c| start + c.len_utf8());
let (source, [start, end]) = picture_controls(source, [start, end]);
if let Ok(snippet) = SourceSnippet::new(name.to_string(), source, start..end, 1) {
diagnostic = diagnostic.snippet(snippet.primary_label(self.message.clone()));
}
diagnostic
}
}
fn picture_controls(source: &str, mut offsets: [usize; 2]) -> (String, [usize; 2]) {
let mut out = String::with_capacity(source.len());
let original = offsets;
let mut chars = source.char_indices().peekable();
while let Some((i, c)) = chars.next() {
for (offset, at) in offsets.iter_mut().zip(original) {
if at == i {
*offset = out.len();
}
}
let keep = matches!(c, '\n' | '\t')
|| (c == '\r' && matches!(chars.peek(), None | Some((_, '\n'))));
out.push(match c {
_ if keep || !c.is_control() => c,
'\u{0}'..='\u{1f}' => char::from_u32(0x2400 + c as u32).unwrap_or('\u{fffd}'),
'\u{7f}' => '\u{2421}',
_ => '\u{fffd}',
});
}
for (offset, at) in offsets.iter_mut().zip(original) {
if at >= source.len() {
*offset = out.len();
}
}
(out, offsets)
}
impl fmt::Display for DataError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = escape_controls(&self.message);
match self.position {
Some(p) => write!(
f,
"invalid {} at line {}, column {}: {message}",
self.format, p.line, p.column
),
None => write!(f, "invalid {}: {message}", self.format),
}
}
}
impl std::error::Error for DataError {}
#[cfg(any(feature = "toml", feature = "xml"))]
pub(crate) struct LineIndex<'a> {
source: &'a str,
starts: Vec<usize>,
last: std::cell::Cell<(usize, usize, usize)>,
}
#[cfg(any(feature = "toml", feature = "xml"))]
impl<'a> LineIndex<'a> {
pub(crate) fn new(source: &'a str) -> Self {
let mut starts = vec![0];
starts.extend(source.match_indices('\n').map(|(i, _)| i + 1));
LineIndex {
source,
starts,
last: std::cell::Cell::new((0, 1, 1)),
}
}
pub(crate) fn position(&self, offset: usize) -> Position {
let mut offset = offset.min(self.source.len());
while !self.source.is_char_boundary(offset) {
offset -= 1;
}
let line = self.starts.partition_point(|s| *s <= offset);
let start = self.starts[line - 1];
let (last_offset, last_line, last_column) = self.last.get();
let column = if last_line == line && last_offset >= start && last_offset <= offset {
last_column + self.source[last_offset..offset].chars().count()
} else {
self.source[start..offset].chars().count() + 1
};
self.last.set((offset, line, column));
Position::new(line, column)
}
}
pub(crate) fn byte_offset(source: &str, position: Position) -> usize {
let mut offset = 0;
for _ in 1..position.line {
match source[offset..].find('\n') {
Some(i) => offset += i + 1,
None => return source.len(),
}
}
let line_end = source[offset..]
.find('\n')
.map_or(source.len(), |i| offset + i);
source[offset..line_end]
.char_indices()
.nth(position.column.saturating_sub(1))
.map_or(line_end, |(i, _)| offset + i)
}
pub fn parse(format: Format, content: &str) -> Result<Node, DataError> {
match format {
Format::Json => json::parse(content),
Format::Ini => ini::parse(content),
Format::Dotenv => dotenv::parse(content),
#[cfg(feature = "yaml")]
Format::Yaml => yaml::parse(content),
#[cfg(feature = "toml")]
Format::Toml => toml_doc::parse(content),
#[cfg(feature = "xml")]
Format::Xml => xml::parse(content),
#[allow(unreachable_patterns)]
other => Err(DataError::new(
other,
format!(
"{} support is not compiled in (enable the `{}` feature)",
other,
other.name()
),
None,
)),
}
}
pub fn parse_json(content: &str) -> Result<Node, DataError> {
json::parse(content)
}
pub fn parse_ini(content: &str) -> Result<Node, DataError> {
ini::parse(content)
}
pub fn parse_dotenv(content: &str) -> Result<Node, DataError> {
dotenv::parse(content)
}
#[cfg(feature = "yaml")]
pub fn parse_yaml(content: &str) -> Result<Node, DataError> {
yaml::parse(content)
}
#[cfg(feature = "toml")]
pub fn parse_toml(content: &str) -> Result<Node, DataError> {
toml_doc::parse(content)
}
#[cfg(feature = "xml")]
pub fn parse_xml(content: &str) -> Result<Node, DataError> {
xml::parse(content)
}
#[cfg(all(test, any(feature = "toml", feature = "xml")))]
mod line_index_tests {
use super::{LineIndex, Position};
fn naive(source: &str, offset: usize) -> Position {
let mut offset = offset.min(source.len());
while !source.is_char_boundary(offset) {
offset -= 1;
}
let start = source[..offset].rfind('\n').map_or(0, |i| i + 1);
let line = source[..offset].matches('\n').count() + 1;
Position::new(line, source[start..offset].chars().count() + 1)
}
#[test]
fn cached_positions_match_counting_from_the_line_start() {
let source = "ab\ncafé 👩👩👧 x\n\n<a b=\"é\">tëxt</a>\nlast";
let index = LineIndex::new(source);
let mut offsets: Vec<usize> = (0..=source.len() + 2).collect();
let mut state = 7u64;
for _ in 0..400 {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
offsets.push((state >> 33) as usize % (source.len() + 3));
}
for offset in offsets {
assert_eq!(
index.position(offset),
naive(source, offset),
"offset {offset}"
);
}
}
}