use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Strictness {
Loose,
#[default]
Standard,
Strict,
}
impl Strictness {
pub fn from_arg(s: &str) -> Option<Strictness> {
match s.to_ascii_lowercase().as_str() {
"loose" | "1" => Some(Strictness::Loose),
"standard" | "2" => Some(Strictness::Standard),
"strict" | "3" => Some(Strictness::Strict),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
Error,
Hint,
}
#[derive(Debug, Clone)]
pub struct Diagnostic {
pub line: usize, pub severity: Severity,
pub message: String,
pub code: &'static str,
}
fn diag_code(msg: &str) -> &'static str {
if msg.starts_with("field mixed with list elements") {
"E001"
} else if msg.starts_with("value after selector on ") {
"E002"
} else if msg.starts_with("no instance ") {
"E003"
} else if msg.starts_with("wildcard selector is query-only") {
"E004"
} else if msg.starts_with("unterminated raw block") {
"E005"
} else if msg.starts_with("raw block with no parent field") {
"E006"
} else if msg.starts_with("list element with no parent field") {
"E007"
} else if msg.starts_with("list element mixed with field children") {
"E008"
} else if msg.starts_with("empty list element") {
"E009"
} else if msg.starts_with("bare comma in list element") {
"E010"
} else if msg.starts_with("field already has a value") {
"E011"
} else if msg.starts_with("indentation matches no open level") {
"E012"
} else if msg.starts_with("malformed line skipped") {
"E014"
} else if msg.starts_with("malformed line: ") {
"E013"
} else if msg.starts_with("missing colon") {
"E015"
} else if msg.starts_with("nesting deeper than") {
"E016"
} else if msg.starts_with("unterminated quote in value") {
"E017"
} else if msg.starts_with("merged with ") {
"H002"
} else if msg.starts_with("unknown field ") {
"V001"
} else if msg.starts_with("required path missing") {
"V002"
} else if msg.starts_with("wrong type at ") {
"V003"
} else if msg.starts_with("value not allowed at ") {
"V004"
} else if msg.starts_with("value below min at ") {
"V005"
} else if msg.starts_with("value above max at ") {
"V006"
} else if msg.starts_with("instance count out of bounds at ") {
"V007"
} else if msg.starts_with("unknown schema key ") {
"V090"
} else if msg.starts_with("unknown schema type ") {
"V091"
} else if msg.starts_with("bad schema constraint ") {
"V092"
} else if msg.starts_with("bad schema path") {
"V093"
} else if msg.starts_with("bad schema fragment") {
"V094"
} else if msg.starts_with("unknown schema fragment ") {
"V095"
} else if msg.starts_with("schema failed to load") {
"V099"
} else {
"E000" }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Status {
Good,
Empty,
NotFound,
BadType,
Multiple,
}
impl std::fmt::Display for Status {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Status::Good => "Good",
Status::Empty => "Empty",
Status::NotFound => "NotFound",
Status::BadType => "BadType",
Status::Multiple => "Multiple",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileStatus {
Clean, HadErrors, NotFound, Unreadable, }
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SaveError {
Refused { path: String, lost: usize },
Io(String),
}
impl std::fmt::Display for SaveError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SaveError::Refused { path, lost } => write!(
f,
"{}: refusing to save: load dropped {} line(s)/value(s) this write would delete (see diagnostics; save_file_lossy overrides)",
path, lost
),
SaveError::Io(m) => f.write_str(m),
}
}
}
impl std::error::Error for SaveError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WriteReason {
Writable,
BadPath, ValueInPath, Wildcard, NoSuchIndex, TooDeep, }
#[derive(Debug, Clone)]
pub struct Read<T> {
pub value: T,
pub status: Status,
pub raw: Option<String>,
pub slots: Vec<Status>,
pub line: usize,
pub quoted: bool,
}
impl<T> Read<T> {
fn new(value: T, status: Status, raw: Option<String>) -> Read<T> {
Read {
value,
status,
raw,
slots: Vec::new(),
line: 0,
quoted: false,
}
}
fn with_slots(value: T, status: Status, raw: Option<String>, slots: Vec<Status>) -> Read<T> {
Read {
value,
status,
raw,
slots,
line: 0,
quoted: false,
}
}
fn at(mut self, line: usize, quoted: bool) -> Read<T> {
self.line = line;
self.quoted = quoted;
self
}
pub fn ok(&self) -> bool {
matches!(self.status, Status::Good | Status::Empty)
}
}
#[derive(Debug)]
pub struct LoadError {
pub diagnostics: Vec<Diagnostic>,
pub document: Document,
}
impl std::fmt::Display for LoadError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let errs: Vec<&Diagnostic> = self
.diagnostics
.iter()
.filter(|d| d.severity == Severity::Error)
.collect();
write!(f, "strict load failed: {} error diagnostic(s)", errs.len())?;
for d in errs.iter().take(3) {
write!(f, "; line {}: {} {}", d.line, d.code, d.message)?;
}
if errs.len() > 3 {
write!(f, "; +{} more", errs.len() - 3)?;
}
Ok(())
}
}
impl std::error::Error for LoadError {}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ShclDateTime {
pub date: Option<(i32, u32, u32)>, pub time: Option<(u32, u32, Option<u32>)>, pub frac: Option<String>, pub zone: Option<ZoneSpec>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ZoneSpec {
Utc,
OffsetMinutes(i32),
}
impl std::fmt::Display for ShclDateTime {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
if let Some((y, m, d)) = self.date {
write!(f, "{:04}-{:02}-{:02}", y, m, d)?;
if self.time.is_some() {
write!(f, "T")?;
}
}
if let Some((h, mi, s)) = self.time {
write!(f, "{:02}:{:02}", h, mi)?;
if let Some(sec) = s {
write!(f, ":{:02}", sec)?;
}
if let Some(fr) = &self.frac {
write!(f, ".{}", fr)?;
}
}
match self.zone {
Some(ZoneSpec::Utc) => write!(f, "Z")?,
Some(ZoneSpec::OffsetMinutes(off)) => {
let sign = if off < 0 { '-' } else { '+' };
let a = off.abs();
write!(f, "{}{:02}:{:02}", sign, a / 60, a % 60)?;
}
None => {}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq)]
struct Element {
text: String, quoted: bool,
}
#[derive(Debug, Clone)]
struct Lead {
text: String,
blank_before: bool,
}
impl Lead {
fn plain(text: String) -> Lead {
Lead {
text,
blank_before: false,
}
}
}
struct Pend {
text: String,
indent: String,
blank_before: bool,
}
#[derive(Debug, Clone, PartialEq)]
enum Value {
Empty,
Cell(Vec<Element>), Raw(Box<RawVal>), }
#[derive(Debug, Clone, PartialEq)]
struct RawVal {
content: String,
info: String,
fence_char: u8,
fence_len: usize,
}
impl Value {
fn key(&self) -> String {
match self {
Value::Empty => "e".to_string(),
Value::Cell(els) => {
let mut k = String::from("c:");
for e in els {
k.push_str(&e.text.len().to_string());
k.push(':');
k.push_str(&e.text);
}
k
}
Value::Raw(r) => format!("r:{}:{}{}", r.info.len(), r.info, r.content),
}
}
fn display(&self) -> String {
match self {
Value::Empty => String::new(),
Value::Cell(els) => els
.iter()
.map(|e| e.text.clone())
.collect::<Vec<_>>()
.join(", "),
Value::Raw(r) => r.content.clone(),
}
}
fn is_empty(&self) -> bool {
matches!(self, Value::Empty)
}
}
#[derive(Debug, Clone)]
struct NodeData {
name: String, value: Value,
children: Vec<usize>,
parent: usize,
line: usize,
star_list: bool, star_mixed: bool, trivia: Option<Box<Trivia>>,
blank_before: bool,
src_set: bool,
src: Option<String>,
name_src: String,
}
#[derive(Debug, Clone, Default)]
struct Trivia {
leading: Vec<Lead>,
trailing: String, after: Vec<Lead>,
inside: Vec<Lead>,
}
impl NodeData {
fn leading(&self) -> &[Lead] {
self.trivia.as_deref().map_or(&[], |t| &t.leading)
}
fn trailing(&self) -> &str {
self.trivia.as_deref().map_or("", |t| &t.trailing)
}
fn after(&self) -> &[Lead] {
self.trivia.as_deref().map_or(&[], |t| &t.after)
}
fn inside(&self) -> &[Lead] {
self.trivia.as_deref().map_or(&[], |t| &t.inside)
}
fn triv_mut(&mut self) -> &mut Trivia {
self.trivia.get_or_insert_with(Default::default)
}
fn authored(&self) -> &str {
if self.name_src.is_empty() {
&self.name
} else {
&self.name_src
}
}
}
fn spelled(name: &str, name_src: &str) -> String {
if name_src == name {
String::new()
} else {
name_src.to_string()
}
}
fn src_matches_display(v: &Value, s: &str) -> bool {
match v {
Value::Empty => s.is_empty(),
Value::Raw(r) => s == r.content,
Value::Cell(els) => {
let mut rest = s;
for (i, e) in els.iter().enumerate() {
if i > 0 {
match rest.strip_prefix(", ") {
Some(r) => rest = r,
None => return false,
}
}
match rest.strip_prefix(e.text.as_str()) {
Some(r) => rest = r,
None => return false,
}
}
rest.is_empty()
}
}
}
#[derive(Debug, Clone)]
pub struct Document {
arena: Vec<NodeData>,
diags: Vec<Diagnostic>,
strictness: Strictness,
orphans: Vec<Lead>, lost: usize,
}
const ROOT: usize = 0;
fn fold_node_into(arena: &mut [NodeData], survivor: usize, loser: usize) {
let kids = std::mem::take(&mut arena[loser].children);
for &k in &kids {
arena[k].parent = survivor;
}
arena[survivor].children.extend(kids);
if let Some(mut lt) = arena[loser].trivia.take() {
let st = arena[survivor].triv_mut();
st.leading.append(&mut lt.leading);
if !lt.trailing.is_empty() {
if st.trailing.is_empty() {
st.trailing = std::mem::take(&mut lt.trailing);
} else {
st.leading
.push(Lead::plain(std::mem::take(&mut lt.trailing)));
}
}
st.after.append(&mut lt.after);
st.inside.append(&mut lt.inside);
}
}
pub const MAX_DEPTH: usize = 512;
fn fold_name(s: &str) -> String {
s.to_ascii_lowercase() }
fn is_bare_name_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '-' || c == '_'
}
fn split_comment(s: &str) -> (&str, Option<&str>) {
let mut in_quote: Option<char> = None;
let mut it = s.char_indices();
while let Some((byte, c)) = it.next() {
if c == '\\' {
it.next();
continue;
}
match in_quote {
Some(q) if c == q => in_quote = None,
None if c == '"' || c == '\'' => in_quote = Some(c),
None if c == '#' => return (&s[..byte], Some(&s[byte..])),
_ => {}
}
}
(s, None)
}
fn split_unquoted_commas(s: &str) -> Vec<&str> {
let mut parts = Vec::new();
let mut in_quote: Option<char> = None;
let mut start = 0usize;
let mut it = s.char_indices();
while let Some((byte, c)) = it.next() {
if c == '\\' {
it.next();
continue;
}
match in_quote {
Some(q) if c == q => in_quote = None,
None if c == '"' || c == '\'' => in_quote = Some(c),
None if c == ',' => {
parts.push(&s[start..byte]);
start = byte + 1;
}
_ => {}
}
}
parts.push(&s[start..]);
parts
}
fn normalize_dangling_backslash(mut t: String) -> String {
let run = t.chars().rev().take_while(|&c| c == '\\').count();
if run % 2 == 1 {
t.push('\\');
}
t
}
fn unterminated_quote(text: &str) -> bool {
for piece in split_unquoted_commas(text) {
let chars: Vec<char> = piece.trim().chars().collect();
let Some(&first) = chars.first() else {
continue;
};
if first != '"' && first != '\'' {
continue;
}
let closed = chars.len() >= 2 && chars[chars.len() - 1] == first && {
let mut esc = false;
for &c in &chars[1..chars.len() - 1] {
esc = c == '\\' && !esc;
}
!esc
};
if !closed {
return true;
}
}
false
}
fn parse_element(piece: &str) -> Option<Element> {
let t = piece.trim();
if t.is_empty() {
return None;
}
let chars: Vec<char> = t.chars().collect();
let first = chars[0];
if (first == '"' || first == '\'') && chars.len() >= 2 && chars[chars.len() - 1] == first {
let mut esc = false;
for &c in &chars[1..chars.len() - 1] {
esc = c == '\\' && !esc;
}
if !esc {
let inner: String = chars[1..chars.len() - 1].iter().collect();
return Some(Element {
text: inner,
quoted: true,
});
}
}
Some(Element {
text: normalize_dangling_backslash(t.to_string()),
quoted: false,
})
}
fn parse_cell(text: &str) -> Value {
let mut els = Vec::new();
for piece in split_unquoted_commas(text) {
if let Some(e) = parse_element(piece) {
els.push(e);
}
}
if els.is_empty() {
Value::Empty
} else {
Value::Cell(els)
}
}
fn apply_escapes(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut it = s.chars();
while let Some(c) = it.next() {
if c != '\\' {
out.push(c);
continue;
}
match it.next() {
Some('t') => out.push('\t'),
Some('n') => out.push('\n'),
Some('\\') => out.push('\\'),
Some('"') => out.push('"'),
Some('\'') => out.push('\''),
Some(other) => {
out.push('\\');
out.push(other);
}
None => out.push('\\'),
}
}
out
}
fn disp_key(v: &Value) -> String {
apply_escapes(&v.display())
}
fn single_scalar(v: &Value) -> bool {
matches!(v, Value::Cell(els) if els.len() == 1)
}
struct Fnv(u64);
impl Fnv {
fn new() -> Fnv {
Fnv(0xcbf2_9ce4_8422_2325)
}
fn byte(&mut self, b: u8) {
self.0 = (self.0 ^ u64::from(b)).wrapping_mul(0x100_0000_01b3);
}
fn bytes(&mut self, s: &[u8]) {
for &b in s {
self.byte(b);
}
}
fn dec(&mut self, mut n: usize) {
let mut buf = [0u8; 20];
let mut i = buf.len();
loop {
i -= 1;
buf[i] = b'0' + (n % 10) as u8;
n /= 10;
if n == 0 {
break;
}
}
self.bytes(&buf[i..]);
}
}
fn merge_hash(name: &str, v: &Value) -> u64 {
let mut h = Fnv::new();
h.bytes(name.as_bytes());
h.byte(0xFF); match v {
Value::Empty => h.byte(b'e'),
Value::Cell(els) => {
h.bytes(b"c:");
for e in els {
h.dec(e.text.len());
h.byte(b':');
h.bytes(e.text.as_bytes());
}
}
Value::Raw(r) => {
h.bytes(b"r:");
h.dec(r.info.len());
h.byte(b':');
h.bytes(r.info.as_bytes());
h.bytes(r.content.as_bytes());
}
}
h.0
}
fn value_hash(v: &Value) -> u64 {
merge_hash("", v)
}
fn merge_eq(name_a: &str, va: &Value, name_b: &str, vb: &Value) -> bool {
if name_a != name_b {
return false;
}
match (va, vb) {
(Value::Empty, Value::Empty) => true,
(Value::Cell(a), Value::Cell(b)) => {
a.len() == b.len() && a.iter().zip(b).all(|(x, y)| x.text == y.text)
}
(Value::Raw(a), Value::Raw(b)) => a.info == b.info && a.content == b.content,
_ => false,
}
}
struct EscHash {
h: Fnv,
pending: bool,
}
impl EscHash {
fn emit(&mut self, c: char) {
let mut b = [0u8; 4];
self.h.bytes(c.encode_utf8(&mut b).as_bytes());
}
fn push(&mut self, c: char) {
if self.pending {
self.pending = false;
match c {
't' => self.emit('\t'),
'n' => self.emit('\n'),
'\\' => self.emit('\\'),
'"' => self.emit('"'),
'\'' => self.emit('\''),
other => {
self.emit('\\');
self.emit(other);
}
}
} else if c == '\\' {
self.pending = true;
} else {
self.emit(c);
}
}
fn finish(mut self) -> u64 {
if self.pending {
self.emit('\\');
}
self.h.0
}
}
fn disp_hash(name: &str, v: &Value) -> u64 {
let mut h = Fnv::new();
h.bytes(name.as_bytes());
h.byte(0xFF);
let mut esc = EscHash { h, pending: false };
match v {
Value::Empty => {}
Value::Cell(els) => {
for (i, e) in els.iter().enumerate() {
if i > 0 {
esc.push(',');
esc.push(' ');
}
for c in e.text.chars() {
esc.push(c);
}
}
}
Value::Raw(r) => {
for c in r.content.chars() {
esc.push(c);
}
}
}
esc.finish()
}
fn disp_hash_text(name: &str, want: &str) -> u64 {
let mut h = Fnv::new();
h.bytes(name.as_bytes());
h.byte(0xFF);
h.bytes(want.as_bytes());
h.0
}
#[derive(Debug)]
enum Slot {
One(usize),
Many(Vec<usize>),
}
impl Slot {
fn push(&mut self, idx: usize) {
match self {
Slot::One(a) => *self = Slot::Many(vec![*a, idx]),
Slot::Many(v) => v.push(idx),
}
}
fn first_match(&self, f: impl Fn(usize) -> bool) -> Option<usize> {
match self {
Slot::One(a) => f(*a).then_some(*a),
Slot::Many(v) => v.iter().copied().find(|&c| f(c)),
}
}
fn remove(&mut self, idx: usize) -> bool {
match self {
Slot::One(a) => *a == idx,
Slot::Many(v) => {
v.retain(|&c| c != idx);
if v.len() == 1 {
let only = v[0];
*self = Slot::One(only);
return false;
}
v.is_empty()
}
}
}
}
fn fence_open(rest: &str) -> Option<(u8, usize, String)> {
let first = rest.as_bytes().first().copied()?;
if first != b'`' && first != b'~' {
return None;
}
let run = rest.bytes().take_while(|&b| b == first).count();
if run < 3 {
return None;
}
Some((first, run, rest[run..].trim().to_string()))
}
fn is_fence_close(line: &str, ch: u8, min_len: usize) -> bool {
let t = line.trim();
t.len() >= min_len && !t.is_empty() && t.bytes().all(|b| b == ch)
}
fn strip_common<'a>(line: &'a str, common: &str) -> &'a str {
let mut k = 0;
for (a, b) in common.chars().zip(line.chars()) {
if a != b {
break;
}
k += a.len_utf8();
}
&line[k..]
}
#[derive(Debug, Clone, PartialEq)]
enum Selector {
ByValue { text: String, quoted: bool },
ByIndex(u64), Wildcard,
}
#[derive(Debug, Clone)]
struct Segment {
name: String, name_src: String, selector: Option<Selector>,
star: bool, }
struct PathScan {
segments: Vec<Segment>,
value_text: Option<String>, }
fn index_usize(k: u64) -> Option<usize> {
usize::try_from(k).ok()
}
fn scan_path(input: &str) -> Result<PathScan, String> {
scan_path_ex(input, false)
}
fn scan_lookup(input: &str) -> Result<PathScan, String> {
scan_path_ex(input, true)
}
fn scan_path_ex(input: &str, stars: bool) -> Result<PathScan, String> {
let bytes = input.as_bytes();
let mut pos = 0usize;
fn char_at(s: &str, pos: usize) -> char {
s[pos..].chars().next().unwrap_or('\u{0}')
}
fn skip_ws(bytes: &[u8], pos: &mut usize) {
while *pos < bytes.len() && (bytes[*pos] == b' ' || bytes[*pos] == b'\t') {
*pos += 1;
}
}
fn read_quoted(s: &str, pos: &mut usize) -> Result<String, String> {
let q = char::from(s.as_bytes()[*pos]); *pos += 1;
let mut out = String::new();
loop {
if *pos >= s.len() {
return Err("unterminated quote".into());
}
let c = char_at(s, *pos);
if c == '\\' && *pos + 1 < s.len() {
let next = char_at(s, *pos + 1);
out.push(c);
out.push(next);
*pos += 1 + next.len_utf8();
continue;
}
*pos += c.len_utf8();
if c == q {
return Ok(out);
}
out.push(c);
}
}
let mut segments: Vec<Segment> = Vec::new();
loop {
skip_ws(bytes, &mut pos);
if pos >= bytes.len() {
return Err("empty path".into());
}
let mut star = false;
let name = if bytes[pos] == b'"' || bytes[pos] == b'\'' {
read_quoted(input, &mut pos)?
} else if stars && bytes[pos] == b'*' {
pos += 1;
star = true;
"*".to_string()
} else {
let start = pos;
while pos < bytes.len() && is_bare_name_char(char::from(bytes[pos])) {
pos += 1;
}
if pos == start {
return Err(format!(
"expected field name, found '{}'",
char_at(input, pos)
));
}
input[start..pos].to_string()
};
let mut selector: Option<Selector> = None;
skip_ws(bytes, &mut pos);
let mut bracket_at: Option<usize> = None;
if pos < bytes.len() && bytes[pos] == b'[' {
bracket_at = Some(pos);
} else if pos < bytes.len() && bytes[pos] == b':' {
let mut q = pos + 1;
skip_ws(bytes, &mut q);
if q < bytes.len() && bytes[q] == b'[' {
bracket_at = Some(q);
}
}
if let Some(b) = bracket_at {
pos = b + 1;
skip_ws(bytes, &mut pos);
if pos < bytes.len() && (bytes[pos] == b'"' || bytes[pos] == b'\'') {
let v = read_quoted(input, &mut pos)?;
selector = Some(Selector::ByValue {
text: v,
quoted: true,
}); } else {
let start = pos;
while pos < bytes.len() && bytes[pos] != b']' {
pos += 1;
}
let body: String = input[start..pos].trim().to_string();
selector = Some(if body == "*" {
Selector::Wildcard
} else if let Some(n) = body.strip_prefix('#').and_then(|d| d.parse::<u64>().ok()) {
Selector::ByIndex(n)
} else if let Ok(n) = body.parse::<u64>() {
Selector::ByIndex(n)
} else if body.is_empty() {
return Err("empty selector".into());
} else {
Selector::ByValue {
text: normalize_dangling_backslash(body),
quoted: false,
}
});
}
skip_ws(bytes, &mut pos);
if pos >= bytes.len() || bytes[pos] != b']' {
return Err("unterminated selector".into());
}
pos += 1;
skip_ws(bytes, &mut pos);
}
if star && selector.is_some() {
return Err("selector on a name wildcard".into());
}
segments.push(Segment {
name: fold_name(&apply_escapes(&name)),
name_src: name,
selector,
star,
});
if pos >= bytes.len() {
return Ok(PathScan {
segments,
value_text: None,
});
}
match bytes[pos] {
b'.' => {
pos += 1;
}
b':' => {
pos += 1;
return Ok(PathScan {
segments,
value_text: Some(input[pos..].trim().to_string()),
});
}
_ => return Err(format!("unexpected '{}' after field", char_at(input, pos))),
}
}
}
struct Parser {
arena: Vec<NodeData>,
diags: Vec<Diagnostic>,
stack: Vec<(String, usize)>,
#[allow(clippy::box_collection)]
child_map: Vec<Option<Box<HashMap<u64, Slot>>>>,
#[allow(clippy::box_collection)]
disp_map: Vec<Option<Box<HashMap<u64, usize>>>>,
pending: Vec<Pend>,
saw_blank: bool, star_open: Option<(usize, u64, u64)>,
reentered: HashMap<usize, usize>,
lost: usize, }
impl Parser {
fn new() -> Parser {
Parser {
arena: vec![NodeData {
name: String::new(),
value: Value::Empty,
children: Vec::new(),
parent: 0,
line: 0,
star_list: false,
star_mixed: false,
trivia: None,
blank_before: false,
src_set: false,
src: None,
name_src: String::new(),
}],
diags: Vec::new(),
stack: vec![(String::new(), ROOT)],
child_map: vec![None],
disp_map: vec![None],
pending: Vec::new(),
saw_blank: false,
star_open: None,
reentered: HashMap::new(),
lost: 0,
}
}
fn err(&mut self, line: usize, msg: impl Into<String>) {
let message = msg.into();
let code = diag_code(&message);
self.diags.push(Diagnostic {
line,
severity: Severity::Error,
message,
code,
});
}
fn select_or_create(
&mut self,
parent: usize,
name: &str,
name_src: &str,
value: Value,
line: usize,
) -> usize {
self.star_flush();
let h = merge_hash(name, &value);
if let Some(slot) = self.child_map[parent].as_deref().and_then(|m| m.get(&h))
&& let Some(c) = slot
.first_match(|c| merge_eq(&self.arena[c].name, &self.arena[c].value, name, &value))
{
return c;
}
let idx = self.arena.len();
let hd = disp_hash(name, &value);
self.arena.push(NodeData {
name: name.to_string(),
name_src: spelled(name, name_src),
value,
children: Vec::new(),
parent,
line,
star_list: false,
star_mixed: false,
trivia: None,
blank_before: false,
src_set: false,
src: None,
});
self.arena[parent].children.push(idx);
self.child_map.push(None);
self.disp_map.push(None);
self.child_map[parent]
.get_or_insert_with(Default::default)
.entry(h)
.and_modify(|s| s.push(idx))
.or_insert(Slot::One(idx));
self.disp_map[parent]
.get_or_insert_with(Default::default)
.entry(hd)
.or_insert(idx);
idx
}
fn star_flush(&mut self) {
if let Some((node, key, disp)) = self.star_open.take() {
self.remap_child(node, key, disp);
}
}
fn remap_child(&mut self, node: usize, old_key: u64, old_disp: u64) {
let parent = self.arena[node].parent;
if let Some(m) = self.child_map[parent].as_deref_mut()
&& let Some(slot) = m.get_mut(&old_key)
&& slot.remove(node)
{
m.remove(&old_key);
}
let new_key = merge_hash(&self.arena[node].name, &self.arena[node].value);
let already = self.child_map[parent]
.as_deref()
.and_then(|m| m.get(&new_key))
.and_then(|s| {
s.first_match(|c| {
merge_eq(
&self.arena[c].name,
&self.arena[c].value,
&self.arena[node].name,
&self.arena[node].value,
)
})
});
if already.is_none() {
self.child_map[parent]
.get_or_insert_with(Default::default)
.entry(new_key)
.and_modify(|s| s.push(node))
.or_insert(Slot::One(node));
}
if let Some(m) = self.disp_map[parent].as_deref_mut()
&& m.get(&old_disp) == Some(&node)
{
m.remove(&old_disp);
}
let new_disp = disp_hash(&self.arena[node].name, &self.arena[node].value);
self.disp_map[parent]
.get_or_insert_with(Default::default)
.entry(new_disp)
.or_insert(node);
}
fn fold_late_dups(&mut self) {
let mut stack = vec![ROOT];
while let Some(parent) = stack.pop() {
let kids = std::mem::take(&mut self.arena[parent].children);
let mut first: HashMap<u64, Slot> = HashMap::new();
let mut keep: Vec<usize> = Vec::with_capacity(kids.len());
for c in kids {
let h = merge_hash(&self.arena[c].name, &self.arena[c].value);
let survivor = first.get(&h).and_then(|s| {
s.first_match(|x| {
merge_eq(
&self.arena[x].name,
&self.arena[x].value,
&self.arena[c].name,
&self.arena[c].value,
)
})
});
match survivor {
Some(s) => fold_node_into(&mut self.arena, s, c),
None => {
first
.entry(h)
.and_modify(|s| s.push(c))
.or_insert(Slot::One(c));
keep.push(c);
}
}
}
stack.extend(keep.iter().copied());
self.arena[parent].children = keep;
}
}
fn attach_trivia(&mut self, node: usize, trailing: Option<&str>) {
if !self.pending.is_empty() {
let t = self.arena[node].triv_mut();
for p in self.pending.drain(..) {
t.leading.push(Lead {
text: p.text,
blank_before: p.blank_before,
});
}
}
if let Some(tr) = trailing {
let t = self.arena[node].triv_mut();
if t.trailing.is_empty() {
t.trailing = tr.to_string();
} else {
t.leading.push(Lead::plain(tr.to_string()));
}
}
}
fn hang_deeper_pending(&mut self, new_indent: &str) {
if self.pending.is_empty() {
return;
}
let taken = std::mem::take(&mut self.pending);
for p in taken {
if p.indent.len() > new_indent.len() {
let target = self
.stack
.iter()
.rev()
.find(|(ind, node)| {
*node != ROOT
&& ind.len() >= new_indent.len()
&& p.indent.starts_with(ind.as_str())
})
.map(|(ind, n)| (*n, ind.len() == p.indent.len()));
if let Some((n, at_own_level)) = target {
let lead = Lead {
text: p.text,
blank_before: p.blank_before,
};
if at_own_level {
self.arena[n].triv_mut().after.push(lead);
} else {
self.arena[n].triv_mut().inside.push(lead);
}
continue;
}
}
self.pending.push(p);
}
}
fn resolve_parent(&mut self, indent: &str) -> Option<usize> {
let (top_indent, top_node) = match self.stack.last() {
Some(t) => t.clone(),
None => return None, };
if indent.len() > top_indent.len() && indent.starts_with(&top_indent) {
return Some(top_node);
}
for i in (0..self.stack.len()).rev() {
if self.stack[i].0 == indent {
let parent = if i == 0 { ROOT } else { self.stack[i - 1].1 };
self.stack.truncate(i.max(1));
if i == 0 {
self.stack.truncate(1);
}
return Some(parent);
}
}
None
}
fn attach_path(
&mut self,
parent: usize,
segs: &[Segment],
value: Value,
line: usize,
) -> Option<usize> {
self.star_flush();
if self.arena[parent].star_list && !self.arena[parent].star_mixed {
self.arena[parent].star_mixed = true;
self.err(line, "field mixed with list elements");
}
let mut parent_depth = 0usize;
let mut up = parent;
while up != ROOT {
parent_depth += 1;
up = self.arena[up].parent;
}
if parent_depth + segs.len() > MAX_DEPTH {
self.err(
line,
format!("nesting deeper than {} levels; line skipped", MAX_DEPTH),
);
self.lost += 1;
return None;
}
let mut cur = parent;
for (i, seg) in segs.iter().enumerate() {
let is_last = i + 1 == segs.len();
match (&seg.selector, is_last) {
(Some(Selector::ByValue { text, quoted }), _) => {
let want = apply_escapes(text);
let found = self.disp_map[cur]
.as_deref()
.and_then(|m| m.get(&disp_hash_text(&seg.name, &want)))
.copied()
.filter(|&c| {
self.arena[c].name == seg.name && disp_key(&self.arena[c].value) == want
})
.filter(|&c| !*quoted || single_scalar(&self.arena[c].value))
.or_else(|| {
if !*quoted {
return None;
}
self.arena[cur].children.iter().copied().find(|&c| {
self.arena[c].name == seg.name
&& single_scalar(&self.arena[c].value)
&& disp_key(&self.arena[c].value) == want
})
});
cur = match found {
Some(c) => c,
None => {
let disc = Value::Cell(vec![Element {
text: text.clone(),
quoted: false,
}]);
self.select_or_create(cur, &seg.name, &seg.name_src, disc, line)
}
};
if is_last && !value.is_empty() {
self.err(
line,
format!("value after selector on '{}' ignored", seg.name),
);
self.lost += 1;
}
}
(Some(Selector::ByIndex(n)), _) => {
let matches: Vec<usize> = self.arena[cur]
.children
.iter()
.copied()
.filter(|&c| self.arena[c].name == seg.name)
.collect();
if let Some(&found) = index_usize(*n).and_then(|i| matches.get(i)) {
cur = found;
} else {
self.err(line, format!("no instance {} of '{}'", n, seg.name));
self.lost += 1;
return None;
}
}
(Some(Selector::Wildcard), _) => {
self.err(line, "wildcard selector is query-only");
self.lost += 1;
return None;
}
(None, false) => {
cur = self.select_or_create(cur, &seg.name, &seg.name_src, Value::Empty, line);
}
(None, true) => {
let parent = cur;
let before = self.arena.len();
cur = self.select_or_create(cur, &seg.name, &seg.name_src, value.clone(), line);
if cur < before && self.arena[cur].line != line {
let non_last = self.arena[parent].children.last() != Some(&cur);
let cross_region = self
.reentered
.get(&parent)
.is_some_and(|&rl| self.arena[cur].line < rl);
if non_last || cross_region {
let at = self.arena[cur].line;
let name = seg.name.clone();
self.diags.push(Diagnostic {
line,
severity: Severity::Hint,
message: format!(
"{}line {} (same name and value combine)",
h002_head(&name),
at
),
code: "H002",
});
self.reentered.insert(cur, line);
}
}
}
}
}
Some(cur)
}
fn consume_raw(
&mut self,
lines: &[&str],
mut i: usize,
open_line: usize,
ch: u8,
len: usize,
info: String,
) -> (Value, usize) {
let mut content: Vec<&str> = Vec::new();
let mut closed = false;
while i < lines.len() {
if is_fence_close(lines[i], ch, len) {
closed = true;
i += 1;
break;
}
content.push(lines[i]);
i += 1;
}
if !closed {
self.err(open_line, "unterminated raw block");
}
let mut common: Option<String> = None;
for l in content.iter().filter(|l| !l.trim().is_empty()) {
let lead: String = l.chars().take_while(|c| *c == ' ' || *c == '\t').collect();
common = Some(match common {
None => lead,
Some(prev) => {
let mut p = String::new();
for (a, b) in prev.chars().zip(lead.chars()) {
if a == b {
p.push(a);
} else {
break;
}
}
p
}
});
}
let common = common.unwrap_or_default();
let stripped: Vec<&str> = content.iter().map(|l| strip_common(l, &common)).collect();
(
Value::Raw(Box::new(RawVal {
content: stripped.join("\n"),
info,
fence_char: ch,
fence_len: len,
})),
i,
)
}
fn bind_block(&mut self, parent: usize, value: Value, line: usize) -> Option<usize> {
if parent == ROOT {
self.err(line, "raw block with no parent field");
self.lost += 1;
return None;
}
if self.arena[parent].value.is_empty() {
let old_key = merge_hash(&self.arena[parent].name, &self.arena[parent].value);
let old_disp = disp_hash(&self.arena[parent].name, &self.arena[parent].value);
self.arena[parent].value = value;
self.remap_child(parent, old_key, old_disp);
Some(parent)
} else {
let (name, name_src, grandparent) = (
self.arena[parent].name.clone(),
self.arena[parent].authored().to_string(),
self.arena[parent].parent,
);
Some(self.select_or_create(grandparent, &name, &name_src, value, line))
}
}
fn add_star_element(&mut self, parent: usize, body: &str, line: usize) {
if parent == ROOT {
self.err(line, "list element with no parent field");
self.lost += 1;
return;
}
if !self.arena[parent].children.is_empty() {
self.err(line, "list element mixed with field children; ignored");
self.lost += 1;
return;
}
let trimmed = body.trim();
if trimmed.is_empty() {
self.err(line, "empty list element");
self.lost += 1;
return;
}
if split_unquoted_commas(trimmed).len() > 1 {
self.err(line, "bare comma in list element (one element per line)");
self.lost += 1;
return;
}
if unterminated_quote(trimmed) {
self.err(line, "unterminated quote in value");
}
let el = match parse_element(trimmed) {
Some(e) => e,
None => {
self.err(line, "empty list element");
self.lost += 1;
return;
}
};
if self.arena[parent].value.is_empty() {
let old_key = merge_hash(&self.arena[parent].name, &self.arena[parent].value);
let old_disp = disp_hash(&self.arena[parent].name, &self.arena[parent].value);
self.arena[parent].value = Value::Cell(vec![el]);
self.arena[parent].star_list = true;
self.remap_child(parent, old_key, old_disp);
let k = merge_hash(&self.arena[parent].name, &self.arena[parent].value);
let d = disp_hash(&self.arena[parent].name, &self.arena[parent].value);
self.star_open = Some((parent, k, d));
} else if matches!(self.arena[parent].value, Value::Cell(_)) && self.arena[parent].star_list
{
if !matches!(self.star_open, Some((n, _, _)) if n == parent) {
self.star_flush();
let old_key = merge_hash(&self.arena[parent].name, &self.arena[parent].value);
let old_disp = disp_hash(&self.arena[parent].name, &self.arena[parent].value);
self.star_open = Some((parent, old_key, old_disp));
}
if let Value::Cell(els) = &mut self.arena[parent].value {
els.push(el);
}
} else {
self.err(line, "field already has a value; list element ignored");
self.lost += 1;
}
}
fn emit_repeated_leaf_hints(&mut self) {
let mut hints: Vec<(usize, String)> = Vec::new();
for parent in 0..self.arena.len() {
let mut group_of: HashMap<&str, usize> = HashMap::new();
let mut by_name: Vec<(&str, Vec<usize>)> = Vec::new();
for &c in &self.arena[parent].children {
let name = self.arena[c].name.as_str();
match group_of.get(name) {
Some(&g) => by_name[g].1.push(c),
None => {
group_of.insert(name, by_name.len());
by_name.push((name, vec![c]));
}
}
}
for (name, group) in by_name {
if group.len() < 2 {
continue;
}
let all_scalar_leaves = group.iter().all(|&c| {
self.arena[c].children.is_empty()
&& matches!(self.arena[c].value, Value::Cell(_))
&& !self.arena[c].star_list
});
if all_scalar_leaves {
let line = group.iter().map(|&c| self.arena[c].line).max().unwrap_or(0);
let joined = group
.iter()
.map(|&c| self.arena[c].value.display())
.collect::<Vec<_>>()
.join(", ");
hints.push((line, format!("{}{}'?", h001_head(name), joined)));
}
}
}
for (line, message) in hints {
self.diags.push(Diagnostic {
line,
severity: Severity::Hint,
message,
code: "H001", });
}
}
fn parse(mut self, text: &str, strictness: Strictness) -> Document {
let text = text.strip_prefix('\u{feff}').unwrap_or(text);
let lines: Vec<&str> = text.split('\n').map(|l| l.trim_end_matches('\r')).collect();
let mut i = 0usize;
while i < lines.len() {
let lineno = i + 1;
let line = lines[i].trim_end();
let ilen = line
.bytes()
.take_while(|&b| b == b' ' || b == b'\t')
.count();
let indent = &line[..ilen];
let rest = &line[ilen..];
if rest.is_empty() {
self.saw_blank = true;
i += 1;
continue;
}
if rest.starts_with('#') {
self.pending.push(Pend {
text: rest.to_string(),
indent: indent.to_string(),
blank_before: std::mem::take(&mut self.saw_blank),
});
i += 1;
continue;
}
let had_blank = std::mem::take(&mut self.saw_blank);
self.hang_deeper_pending(indent);
if let Some((ch, len, info)) = fence_open(rest) {
let parent = match self.resolve_parent(indent) {
Some(p) => p,
None => {
self.err(lineno, "indentation matches no open level");
self.lost += 1;
i += 1;
continue;
}
};
let (value, next) = self.consume_raw(&lines, i + 1, lineno, ch, len, info);
if let Some(node) = self.bind_block(parent, value, lineno) {
self.attach_trivia(node, None);
}
i = next;
continue;
}
if let Some(after) = rest.strip_prefix('*') {
if after.starts_with(' ') || after.starts_with('\t') {
let parent = match self.resolve_parent(indent) {
Some(p) => p,
None => {
self.err(lineno, "indentation matches no open level");
self.lost += 1;
i += 1;
continue;
}
};
let (body, comment) = split_comment(after);
if parent != ROOT {
self.attach_trivia(parent, comment);
}
self.add_star_element(parent, body, lineno);
i += 1;
continue;
}
self.err(lineno, "malformed line: '*' must be followed by a space");
self.pending.push(Pend {
text: rest.trim_end().to_string(),
indent: indent.to_string(),
blank_before: had_blank,
});
i += 1;
continue;
}
let (before, comment) = split_comment(rest);
let content = before.trim_end();
if content.is_empty() {
if let Some(c) = comment {
self.pending.push(Pend {
text: c.to_string(),
indent: indent.to_string(),
blank_before: had_blank,
});
}
i += 1;
continue;
}
let parent = match self.resolve_parent(indent) {
Some(p) => p,
None => {
self.err(lineno, "indentation matches no open level");
self.lost += 1;
i += 1;
continue;
}
};
let scan = match scan_path(content) {
Ok(s) => s,
Err(reason) => {
self.err(lineno, format!("malformed line skipped: {}", reason));
if rest.starts_with('\u{feff}') {
self.lost += 1;
} else {
self.pending.push(Pend {
text: rest.trim_end().to_string(),
indent: indent.to_string(),
blank_before: had_blank,
});
}
i += 1;
continue;
}
};
let mut next = i + 1;
let mut src_text: Option<String> = None;
let value = match &scan.value_text {
None => {
self.err(lineno, "missing colon; repaired as an empty value");
Value::Empty
}
Some(v) if v.is_empty() => Value::Empty,
Some(v) => {
if let Some((ch, len, info)) = fence_open(v) {
let (val, n) = self.consume_raw(&lines, i + 1, lineno, ch, len, info);
next = n;
val
} else {
if unterminated_quote(v) {
self.err(lineno, "unterminated quote in value");
}
src_text = Some(v.clone());
parse_cell(v)
}
}
};
let vkey = src_text.as_ref().map(|_| value_hash(&value));
if let Some(node) = self.attach_path(parent, &scan.segments, value, lineno) {
if let (Some(s), Some(k)) = (src_text, vkey)
&& !self.arena[node].src_set
&& value_hash(&self.arena[node].value) == k
{
self.arena[node].src_set = true;
if !src_matches_display(&self.arena[node].value, &s) {
self.arena[node].src = Some(s);
}
}
if had_blank {
self.arena[node].blank_before = true;
}
self.attach_trivia(node, comment);
self.stack.push((indent.to_string(), node));
}
i = next;
}
self.star_flush();
self.fold_late_dups();
self.emit_repeated_leaf_hints();
self.hang_deeper_pending("");
let orphans = self
.pending
.drain(..)
.map(|p| Lead {
text: p.text,
blank_before: p.blank_before,
})
.collect();
Document {
arena: self.arena,
diags: self.diags,
strictness,
orphans,
lost: self.lost,
}
}
}
impl Document {
pub fn parse(text: &str) -> Document {
Parser::new().parse(text, Strictness::Standard)
}
pub fn parse_with(text: &str, strictness: Strictness) -> Result<Document, LoadError> {
let doc = Parser::new().parse(text, strictness);
if strictness == Strictness::Strict
&& doc.diags.iter().any(|d| d.severity == Severity::Error)
{
return Err(LoadError {
diagnostics: doc.diags.clone(),
document: doc,
});
}
Ok(doc)
}
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diags
}
pub fn lost_count(&self) -> usize {
self.lost
}
pub fn error_count(&self) -> usize {
self.diags
.iter()
.filter(|d| d.severity == Severity::Error)
.count()
}
pub fn load_and_validate(text: &str, schema_text: &str, strictness: Strictness) -> Document {
let mut doc = Parser::new().parse(text, strictness);
if !schema_text.trim().is_empty() {
let schema = Document::parse(schema_text);
if schema.diags.iter().any(|d| d.severity == Severity::Error) {
doc.diags.push(Diagnostic {
line: 0,
severity: Severity::Error,
code: "V099",
message: "schema failed to load".to_string(),
});
return doc;
}
let vdiags = doc.validate(&schema);
doc.diags.extend(vdiags);
suppress_declared_repeats(&schema, &mut doc.diags);
suppress_declared_reopens(&schema, &mut doc.diags);
}
doc
}
pub fn strictness(&self) -> Strictness {
self.strictness
}
pub fn load_file(path: &str) -> (Document, FileStatus) {
Document::load_file_with(path, Strictness::Standard)
}
pub fn load_file_with(path: &str, level: Strictness) -> (Document, FileStatus) {
let text = match std::fs::read_to_string(path) {
Ok(t) => t,
Err(e) => {
let st = if e.kind() == std::io::ErrorKind::NotFound {
FileStatus::NotFound
} else {
FileStatus::Unreadable
};
return (Parser::new().parse("", level), st);
}
};
let doc = Parser::new().parse(&text, level);
let st = if doc.diags.iter().any(|d| d.severity == Severity::Error) {
FileStatus::HadErrors
} else {
FileStatus::Clean
};
(doc, st)
}
pub fn save_file(&self, path: &str) -> Result<(), SaveError> {
if self.lost > 0 {
return Err(SaveError::Refused {
path: path.to_string(),
lost: self.lost,
});
}
write_file_atomic(path, &self.to_canonical()).map_err(SaveError::Io)
}
pub fn save_file_lossy(&self, path: &str) -> Result<(), SaveError> {
write_file_atomic(path, &self.to_canonical()).map_err(SaveError::Io)
}
pub fn to_canonical(&self) -> String {
let mut out = String::new();
self.emit_children(&self.arena[ROOT].children, 0, &mut out);
for c in &self.orphans {
if c.blank_before && !out.is_empty() {
out.push('\n');
}
out.push_str(&c.text);
out.push('\n');
}
out
}
fn emit_children(&self, kids: &[usize], depth: usize, out: &mut String) {
let mut empties: std::collections::HashSet<&str> = std::collections::HashSet::new();
for &c in kids {
let n = &self.arena[c];
let wm = matches!(n.value, Value::Raw { .. }) && empties.contains(n.name.as_str());
if n.value.is_empty() {
empties.insert(n.name.as_str());
}
self.emit_node(c, depth, wm, out);
}
}
fn emit_node(&self, idx: usize, depth: usize, would_merge: bool, out: &mut String) {
let node = &self.arena[idx];
let pad: String = "\t".repeat(depth);
for c in node.leading() {
if c.blank_before && !out.is_empty() {
out.push('\n');
}
out.push_str(&pad);
out.push_str(&c.text);
out.push('\n');
}
if node.blank_before && !out.is_empty() {
out.push('\n');
}
if would_merge && !node.trailing().is_empty() {
out.push_str(&pad);
out.push_str(node.trailing());
out.push('\n');
}
out.push_str(&pad);
out.push_str(&emit_name(&node.name));
out.push(':');
match &node.value {
Value::Empty => {
push_trailing(out, node.trailing());
out.push('\n');
}
Value::Cell(els) => {
out.push(' ');
let joined = els.iter().map(emit_element).collect::<Vec<_>>().join(", ");
out.push_str(&joined);
push_trailing(out, node.trailing());
out.push('\n');
}
Value::Raw(r) => {
let (content, info, fence_char, fence_len) =
(&r.content, &r.info, &r.fence_char, &r.fence_len);
if would_merge {
out.push(' ');
} else {
push_trailing(out, node.trailing());
out.push('\n');
}
let pad: String = "\t".repeat(depth + 1); let fence: String = std::iter::repeat_n(*fence_char as char, *fence_len).collect();
if !would_merge {
out.push_str(&pad);
}
out.push_str(&fence);
if !info.is_empty() {
if info.as_bytes()[0] == *fence_char {
out.push(' ');
}
out.push_str(info);
}
out.push('\n');
if !content.is_empty() {
let all_blank = content.split('\n').all(|l| l.trim().is_empty());
for l in content.split('\n') {
if !l.is_empty() && !all_blank {
out.push_str(&pad);
}
out.push_str(l);
out.push('\n');
}
}
out.push_str(&pad);
out.push_str(&fence);
out.push('\n');
}
}
self.emit_children(&self.arena[idx].children, depth + 1, out);
let ipad: String = "\t".repeat(depth + 1);
for c in self.arena[idx].inside() {
if c.blank_before && !out.is_empty() {
out.push('\n');
}
out.push_str(&ipad);
out.push_str(&c.text);
out.push('\n');
}
for c in self.arena[idx].after() {
if c.blank_before && !out.is_empty() {
out.push('\n');
}
out.push_str(&pad);
out.push_str(&c.text);
out.push('\n');
}
}
}
fn push_trailing(out: &mut String, trailing: &str) {
if !trailing.is_empty() {
out.push_str(" ");
out.push_str(trailing);
}
}
fn escape_name(name: &str) -> String {
if !name.is_empty() && name.chars().all(is_bare_name_char) {
return name.to_string();
}
let mut out = String::with_capacity(name.len() + 2);
out.push('"');
for c in name.chars() {
match c {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\t' => out.push_str("\\t"),
'\n' => out.push_str("\\n"),
_ => out.push(c),
}
}
out.push('"');
out
}
fn emit_name(name: &str) -> String {
escape_name(name)
}
#[must_use]
pub fn format_f64(v: f64) -> String {
format!("{v}")
}
pub fn quote_segment(name: &str) -> String {
emit_name(name)
}
pub fn write_file_atomic(file: &str, data: &str) -> Result<(), String> {
use std::io::Write;
let target = std::fs::canonicalize(file).unwrap_or_else(|_| std::path::PathBuf::from(file));
let dir = match target.parent() {
Some(d) if !d.as_os_str().is_empty() => d,
_ => std::path::Path::new("."),
};
let base = target
.file_name()
.map(|b| b.to_string_lossy().into_owned())
.unwrap_or_else(|| file.to_string());
let existing = std::fs::metadata(&target).ok();
let mut file_handle = None;
let mut tmp = std::path::PathBuf::new();
let mut last = String::new();
for attempt in 0..8 {
tmp = dir.join(format!(".{}.tmp{}.{}", base, std::process::id(), attempt));
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(if existing.is_some() { 0o600 } else { 0o666 });
}
match opts.open(&tmp) {
Ok(f) => {
file_handle = Some(f);
break;
}
Err(e) => last = e.to_string(),
}
}
let Some(mut f) = file_handle else {
return Err(format!("{}: cannot create temporary file: {}", file, last));
};
let res = (|| -> std::io::Result<()> {
if let Some(m) = &existing {
let _ = f.set_permissions(m.permissions());
}
f.write_all(data.as_bytes())?;
f.sync_all()
})();
if let Err(e) = res {
let _ = std::fs::remove_file(&tmp);
return Err(format!("{}: {}", file, e));
}
drop(f);
publish_file(&tmp, &target).map_err(|e| {
let _ = std::fs::remove_file(&tmp);
format!("{}: {}", file, e)
})?;
sync_dir(dir);
Ok(())
}
fn publish_file(tmp: &std::path::Path, target: &std::path::Path) -> std::io::Result<()> {
#[cfg(windows)]
if target.exists() && windows_replace_file(tmp, target) {
return Ok(());
}
std::fs::rename(tmp, target)
}
#[cfg(windows)]
fn windows_replace_file(tmp: &std::path::Path, target: &std::path::Path) -> bool {
use std::os::windows::ffi::OsStrExt;
const REPLACEFILE_WRITE_THROUGH: u32 = 0x1;
#[link(name = "kernel32")]
unsafe extern "system" {
fn ReplaceFileW(
replaced: *const u16,
replacement: *const u16,
backup: *const u16,
flags: u32,
exclude: *mut core::ffi::c_void,
reserved: *mut core::ffi::c_void,
) -> i32;
}
fn wide(p: &std::path::Path) -> Vec<u16> {
p.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
let (replaced, replacement) = (wide(target), wide(tmp));
unsafe {
ReplaceFileW(
replaced.as_ptr(),
replacement.as_ptr(),
std::ptr::null(),
REPLACEFILE_WRITE_THROUGH,
std::ptr::null_mut(),
std::ptr::null_mut(),
) != 0
}
}
fn sync_dir(dir: &std::path::Path) {
#[cfg(unix)]
if let Ok(d) = std::fs::File::open(dir) {
let _ = d.sync_all();
}
#[cfg(not(unix))]
let _ = dir;
}
fn h001_head(name: &str) -> String {
format!(
"'{}' repeats as a bare leaf - did you mean '{}: ",
name, name
)
}
pub fn suppress_declared_repeats(schema: &Document, diags: &mut Vec<Diagnostic>) {
let mut groups: Vec<(String, Vec<String>)> =
vec![("field".to_string(), schema.instances("field"))];
for k in 0..schema.count("fragment") {
let base = format!("fragment[#{}].field", k);
let paths = schema.instances(&base);
groups.push((base, paths));
}
let mut names: Vec<String> = Vec::new();
for (base, paths) in &groups {
for (i, p) in paths.iter().enumerate() {
let rep = schema.read_int_array(&format!("{}[#{}].repeat", base, i));
if rep.status != Status::Good {
continue;
}
match rep.value.last() {
Some(&u) if u > 1 => {}
_ => continue,
}
let Ok(scan) = scan_lookup(p) else {
continue;
};
let Some(seg) = scan.segments.last() else {
continue;
};
if seg.star {
continue; }
names.push(seg.name.clone());
}
}
if names.is_empty() {
return;
}
let heads: Vec<String> = names.iter().map(|n| h001_head(n)).collect();
diags.retain(|d| d.code != "H001" || !heads.iter().any(|h| d.message.starts_with(h.as_str())));
}
fn h002_head(name: &str) -> String {
format!("merged with '{}' at ", name)
}
pub fn suppress_declared_reopens(schema: &Document, diags: &mut Vec<Diagnostic>) {
let mut groups: Vec<(String, Vec<String>)> =
vec![("field".to_string(), schema.instances("field"))];
for k in 0..schema.count("fragment") {
let base = format!("fragment[#{}].field", k);
let paths = schema.instances(&base);
groups.push((base, paths));
}
let mut names: Vec<String> = Vec::new();
for (base, paths) in &groups {
for (i, p) in paths.iter().enumerate() {
let re = schema.read_bool(&format!("{}[#{}].reopen", base, i));
if re.status != Status::Good || !re.value {
continue;
}
let Ok(scan) = scan_lookup(p) else {
continue;
};
let Some(seg) = scan.segments.last() else {
continue;
};
if seg.star {
continue; }
names.push(seg.name.clone());
}
}
if names.is_empty() {
return;
}
let heads: Vec<String> = names.iter().map(|n| h002_head(n)).collect();
diags.retain(|d| d.code != "H002" || !heads.iter().any(|h| d.message.starts_with(h.as_str())));
}
fn emit_element(e: &Element) -> String {
let t = &e.text;
let needs = t.is_empty()
|| t.chars()
.any(|c| matches!(c, ' ' | '\t' | ',' | ':' | '#' | '"' | '\'' | '[' | ']'))
|| t.starts_with(char::is_whitespace)
|| t.ends_with(char::is_whitespace)
|| fence_open(t).is_some()
|| (e.quoted && !is_data_format(e));
if needs { quote_text(t) } else { t.clone() }
}
fn is_data_format(e: &Element) -> bool {
let t = e.text.trim();
if t.bytes().any(|b| b.is_ascii_digit()) {
return parse_int_text(e, Strictness::Standard).is_some()
|| parse_float_text(e, Strictness::Standard).is_some()
|| parse_datetime(&e.text).is_some()
|| parse_bool_text(t, Strictness::Standard).is_some();
}
t.len() <= 5 && parse_bool_text(t, Strictness::Standard).is_some()
}
fn bare_quote_counts(t: &str) -> (usize, usize) {
let (mut dq, mut sq) = (0usize, 0usize);
let mut it = t.chars();
while let Some(c) = it.next() {
match c {
'\\' => {
it.next();
}
'"' => dq += 1,
'\'' => sq += 1,
_ => {}
}
}
(dq, sq)
}
fn quote_text(t: &str) -> String {
let normalized;
let t = if t.ends_with('\\') {
normalized = normalize_dangling_backslash(t.to_string());
normalized.as_str()
} else {
t
};
let (dq, sq) = bare_quote_counts(t);
if dq == 0 {
format!("\"{}\"", t)
} else if sq == 0 {
format!("'{}'", t)
} else {
let mut out = String::from("\"");
let mut it = t.chars();
while let Some(c) = it.next() {
match c {
'\\' => {
out.push(c);
if let Some(n) = it.next() {
out.push(n);
}
}
'"' => out.push_str("\\\""),
_ => out.push(c),
}
}
out.push('"');
out
}
}
enum Resolved {
None,
One(usize),
Many(Vec<usize>),
Slots(Vec<Result<usize, Status>>),
}
impl Document {
fn children_named(&self, parent: usize, name: &str) -> Vec<usize> {
self.arena[parent]
.children
.iter()
.copied()
.filter(|&c| self.arena[c].name == name)
.collect()
}
fn resolve_from(&self, start: &[usize], segs: &[Segment]) -> Resolved {
let mut cur: Vec<usize> = start.to_vec();
for (i, seg) in segs.iter().enumerate() {
let mut next: Vec<usize> = Vec::new();
for &n in &cur {
if seg.star {
next.extend(self.arena[n].children.iter().copied());
} else {
next.extend(self.children_named(n, &seg.name));
}
}
if seg.star {
let rest = &segs[i + 1..];
let mut slots: Vec<Result<usize, Status>> = Vec::new();
for inst in next {
if rest.is_empty() {
slots.push(Ok(inst));
} else {
match self.resolve_from(&[inst], rest) {
Resolved::One(x) => slots.push(Ok(x)),
Resolved::None => slots.push(Err(Status::NotFound)),
_ => slots.push(Err(Status::Multiple)),
}
}
}
return Resolved::Slots(slots);
}
match &seg.selector {
None => cur = next,
Some(Selector::ByValue { text, quoted }) => {
let want = apply_escapes(text);
cur = next
.into_iter()
.filter(|&c| {
disp_key(&self.arena[c].value) == want
&& (!quoted || single_scalar(&self.arena[c].value))
})
.collect();
}
Some(Selector::ByIndex(k)) => {
cur = index_usize(*k)
.and_then(|i| next.get(i))
.map(|&c| vec![c])
.unwrap_or_default();
}
Some(Selector::Wildcard) => {
let rest = &segs[i + 1..];
let mut slots: Vec<Result<usize, Status>> = Vec::new();
for inst in next {
if rest.is_empty() {
slots.push(Ok(inst));
} else {
match self.resolve_from(&[inst], rest) {
Resolved::One(x) => slots.push(Ok(x)),
Resolved::None => slots.push(Err(Status::NotFound)),
_ => slots.push(Err(Status::Multiple)),
}
}
}
return Resolved::Slots(slots);
}
}
}
match cur.len() {
0 => Resolved::None,
1 => Resolved::One(cur[0]),
_ => Resolved::Many(cur),
}
}
fn resolve(&self, path: &str) -> Result<Resolved, Status> {
let scan = scan_lookup(path).map_err(|_| Status::NotFound)?;
if scan.value_text.is_some() {
return Err(Status::NotFound); }
Ok(self.resolve_from(&[ROOT], &scan.segments))
}
pub fn count(&self, path: &str) -> usize {
match self.resolve(path) {
Ok(Resolved::None) | Err(_) => 0,
Ok(Resolved::One(_)) => 1,
Ok(Resolved::Many(v)) => v.len(),
Ok(Resolved::Slots(s)) => s.len(),
}
}
pub fn paths(&self) -> Vec<String> {
let mut out = Vec::new();
let mut seen = std::collections::HashSet::new();
let mut stack: Vec<(usize, String)> = self.arena[ROOT]
.children
.iter()
.rev()
.map(|&c| (c, String::new()))
.collect();
while let Some((node, prefix)) = stack.pop() {
let seg = emit_name(&self.arena[node].name);
let path = if prefix.is_empty() {
seg
} else {
format!("{}.{}", prefix, seg)
};
if seen.insert(path.clone()) {
out.push(path.clone());
}
for &c in self.arena[node].children.iter().rev() {
stack.push((c, path.clone()));
}
}
out
}
pub fn line(&self, path: &str) -> usize {
match self.resolve(path) {
Ok(Resolved::One(n)) => self.arena[n].line,
_ => 0,
}
}
pub fn authored_name(&self, path: &str) -> String {
match self.resolve(path) {
Ok(Resolved::One(n)) => self.arena[n].authored().to_string(),
_ => String::new(),
}
}
pub fn lines(&self, path: &str) -> Vec<usize> {
match self.resolve(path) {
Ok(Resolved::One(n)) => vec![self.arena[n].line],
Ok(Resolved::Many(v)) => v.iter().map(|&n| self.arena[n].line).collect(),
Ok(Resolved::Slots(s)) => s
.into_iter()
.map(|r| match r {
Ok(n) => self.arena[n].line,
Err(_) => 0,
})
.collect(),
_ => Vec::new(),
}
}
pub fn children(&self, path: &str) -> Vec<String> {
let node = if path.trim().is_empty() {
ROOT
} else {
match self.resolve(path) {
Ok(Resolved::One(n)) => n,
_ => return Vec::new(),
}
};
self.arena[node]
.children
.iter()
.map(|&c| self.arena[c].name.clone())
.collect()
}
pub fn instances(&self, path: &str) -> Vec<String> {
match self.resolve(path) {
Ok(Resolved::One(n)) => vec![self.arena[n].value.display()],
Ok(Resolved::Many(v)) => v.iter().map(|&n| self.arena[n].value.display()).collect(),
Ok(Resolved::Slots(s)) => s
.into_iter()
.map(|r| match r {
Ok(n) => self.arena[n].value.display(),
Err(_) => String::new(),
})
.collect(),
_ => Vec::new(),
}
}
}
fn literal_value(text: &str) -> Option<Value> {
if text.contains('\n') || text.contains('\r') {
return None;
}
let (v, _) = split_comment(text);
let v = v.trim();
if unterminated_quote(v) {
return None;
}
Some(parse_cell(v))
}
fn cell_of(text: String) -> Value {
Value::Cell(vec![Element {
text,
quoted: false,
}])
}
fn encode_string(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
_ => out.push(c),
}
}
out
}
fn choose_fence(content: &str) -> (u8, usize) {
let mut maxrun = 0usize;
for line in content.split('\n') {
let t = line.trim();
if !t.is_empty() && t.bytes().all(|b| b == b'`') {
maxrun = maxrun.max(t.len());
}
}
(b'`', (maxrun + 1).max(3))
}
fn array_cell(texts: Vec<String>) -> Value {
if texts.is_empty() {
Value::Empty
} else {
Value::Cell(
texts
.into_iter()
.map(|text| Element {
text,
quoted: false,
})
.collect(),
)
}
}
impl Document {
pub fn new() -> Document {
Document::parse("")
}
fn new_child(&mut self, parent: usize, name: &str, name_src: &str, value: Value) -> usize {
let idx = self.arena.len();
self.arena.push(NodeData {
name: name.to_string(),
name_src: spelled(name, name_src),
value,
children: Vec::new(),
parent,
line: 0,
star_list: false,
star_mixed: false,
trivia: None,
blank_before: parent == ROOT,
src_set: false,
src: None,
});
self.arena[parent].children.push(idx);
idx
}
pub fn write_reason(&self, path: &str) -> WriteReason {
let scan = match scan_lookup(path) {
Ok(s) => s,
Err(_) => return WriteReason::BadPath,
};
self.probe_write(&scan, &mut Vec::new())
}
fn probe_write(&self, scan: &PathScan, trail: &mut Vec<Option<usize>>) -> WriteReason {
trail.clear();
if scan.value_text.is_some() {
return WriteReason::ValueInPath;
}
if scan.segments.is_empty() {
return WriteReason::BadPath;
}
if scan.segments.len() > MAX_DEPTH {
return WriteReason::TooDeep;
}
let mut probe = Some(ROOT);
for seg in &scan.segments {
if seg.star {
return WriteReason::Wildcard;
}
if matches!(&seg.selector, Some(Selector::ByValue { text, .. }) if text.contains('\n'))
{
return WriteReason::BadPath;
}
match &seg.selector {
Some(Selector::Wildcard) => return WriteReason::Wildcard,
Some(Selector::ByIndex(k)) => {
let Some(c) = probe else {
return WriteReason::NoSuchIndex;
};
let matches: Vec<usize> = self.arena[c]
.children
.iter()
.copied()
.filter(|&n| self.arena[n].name == seg.name)
.collect();
match index_usize(*k).and_then(|i| matches.get(i)) {
Some(&m) => probe = Some(m),
None => return WriteReason::NoSuchIndex,
}
}
Some(Selector::ByValue { text, quoted }) => {
let want = apply_escapes(text);
probe = probe.and_then(|c| {
self.arena[c].children.iter().copied().find(|&n| {
self.arena[n].name == seg.name
&& disp_key(&self.arena[n].value) == want
&& (!quoted || single_scalar(&self.arena[n].value))
})
});
}
None => {
probe = probe.and_then(|c| {
self.arena[c]
.children
.iter()
.copied()
.find(|&n| self.arena[n].name == seg.name)
});
}
}
trail.push(probe);
}
WriteReason::Writable
}
fn place(&mut self, path: &str) -> Option<usize> {
let scan = scan_lookup(path).ok()?;
let mut trail: Vec<Option<usize>> = Vec::new();
if self.probe_write(&scan, &mut trail) != WriteReason::Writable {
return None;
}
let mut cur = ROOT;
for (i, seg) in scan.segments.iter().enumerate() {
if let Some(found) = trail[i] {
cur = found;
continue;
}
cur = match &seg.selector {
None => self.new_child(cur, &seg.name, &seg.name_src, Value::Empty),
Some(Selector::ByValue { text, .. }) => {
self.new_child(cur, &seg.name, &seg.name_src, cell_of(text.clone()))
}
Some(Selector::ByIndex(_)) | Some(Selector::Wildcard) => return None,
};
}
Some(cur)
}
fn set_value(&mut self, path: &str, value: Value) -> bool {
match self.place(path) {
Some(node) => {
self.arena[node].value = value;
self.arena[node].src = None; self.collapse_dup(node);
true
}
None => false,
}
}
fn collapse_dup(&mut self, node: usize) {
let parent = self.arena[node].parent;
let name = self.arena[node].name.clone();
let key = self.arena[node].value.key();
let siblings = &self.arena[parent].children;
let Some(other) = siblings
.iter()
.copied()
.find(|&c| c != node && self.arena[c].name == name && self.arena[c].value.key() == key)
else {
return;
};
let pos = |n: usize| {
self.arena[parent]
.children
.iter()
.position(|&c| c == n)
.unwrap_or(usize::MAX)
};
let (survivor, loser) = if pos(other) < pos(node) {
(other, node)
} else {
(node, other)
};
fold_node_into(&mut self.arena, survivor, loser);
self.arena[parent].children.retain(|&c| c != loser);
}
pub fn exists(&self, path: &str) -> bool {
match self.resolve(path) {
Ok(Resolved::One(_)) | Ok(Resolved::Many(_)) => true,
Ok(Resolved::Slots(s)) => s.iter().any(|r| r.is_ok()),
_ => false,
}
}
pub fn remove(&mut self, path: &str) -> usize {
let targets: Vec<usize> = match self.resolve(path) {
Ok(Resolved::One(n)) => vec![n],
Ok(Resolved::Many(v)) => v,
Ok(Resolved::Slots(s)) => s.into_iter().filter_map(|r| r.ok()).collect(),
_ => Vec::new(),
};
for &t in &targets {
let p = self.arena[t].parent;
self.arena[p].children.retain(|&c| c != t);
}
targets.len()
}
#[must_use = "a setter reports whether the write applied; an unusable path writes nothing (see write_reason)"]
pub fn set_comment(&mut self, path: &str, text: &str) -> bool {
match self.place(path) {
Some(node) => {
let line = text.split('\n').next().unwrap_or("");
let c = if line.starts_with('#') {
line.to_string()
} else {
format!("# {}", line)
};
self.arena[node].triv_mut().leading.push(Lead::plain(c));
true
}
None => false,
}
}
#[must_use = "a setter reports whether the write applied; an unusable path writes nothing (see write_reason)"]
pub fn set_int(&mut self, path: &str, v: i64) -> bool {
self.set_value(path, cell_of(v.to_string()))
}
#[must_use = "a setter reports whether the write applied; an unusable path writes nothing (see write_reason)"]
pub fn set_float(&mut self, path: &str, v: f64) -> bool {
self.set_value(path, cell_of(format_f64(v)))
}
#[must_use = "a setter reports whether the write applied; an unusable path writes nothing (see write_reason)"]
pub fn set_bool(&mut self, path: &str, v: bool) -> bool {
self.set_value(path, cell_of(if v { "true" } else { "false" }.to_string()))
}
#[must_use = "a setter reports whether the write applied; an unusable path writes nothing (see write_reason)"]
pub fn set_string(&mut self, path: &str, v: &str) -> bool {
self.set_value(path, cell_of(encode_string(v)))
}
#[must_use = "a setter reports whether the write applied; an unusable path writes nothing (see write_reason)"]
pub fn set_datetime(&mut self, path: &str, v: &ShclDateTime) -> bool {
self.set_value(path, cell_of(v.to_string()))
}
#[must_use = "a setter reports whether the write applied; an unusable path writes nothing (see write_reason)"]
pub fn set_raw(&mut self, path: &str, content: &str, info: &str) -> bool {
let (fence_char, fence_len) = choose_fence(content);
self.set_value(
path,
Value::Raw(Box::new(RawVal {
content: content.to_string(),
info: info.to_string(),
fence_char,
fence_len,
})),
)
}
#[must_use = "a setter reports whether the write applied; an unusable path writes nothing (see write_reason)"]
pub fn set_empty(&mut self, path: &str) -> bool {
self.set_value(path, Value::Empty)
}
#[must_use = "a setter reports whether the write applied; an unusable path writes nothing (see write_reason)"]
pub fn set_int_array(&mut self, path: &str, v: &[i64]) -> bool {
self.set_value(path, array_cell(v.iter().map(|x| x.to_string()).collect()))
}
#[must_use = "a setter reports whether the write applied; an unusable path writes nothing (see write_reason)"]
pub fn set_float_array(&mut self, path: &str, v: &[f64]) -> bool {
self.set_value(path, array_cell(v.iter().map(|x| format_f64(*x)).collect()))
}
#[must_use = "a setter reports whether the write applied; an unusable path writes nothing (see write_reason)"]
pub fn set_bool_array(&mut self, path: &str, v: &[bool]) -> bool {
self.set_value(
path,
array_cell(
v.iter()
.map(|x| if *x { "true" } else { "false" }.to_string())
.collect(),
),
)
}
#[must_use = "a setter reports whether the write applied; an unusable path writes nothing (see write_reason)"]
pub fn set_string_array(&mut self, path: &str, v: &[&str]) -> bool {
self.set_value(
path,
array_cell(v.iter().map(|x| encode_string(x)).collect()),
)
}
#[must_use = "a setter reports whether the write applied; an unusable path writes nothing (see write_reason)"]
pub fn set_datetime_array(&mut self, path: &str, v: &[ShclDateTime]) -> bool {
self.set_value(path, array_cell(v.iter().map(|x| x.to_string()).collect()))
}
#[must_use = "a setter reports whether the write applied; an unusable path writes nothing (see write_reason)"]
pub fn set_int_default(&mut self, path: &str, v: i64) -> bool {
if !self.exists(path) {
return self.set_int(path, v);
}
true
}
#[must_use = "a setter reports whether the write applied; an unusable path writes nothing (see write_reason)"]
pub fn set_float_default(&mut self, path: &str, v: f64) -> bool {
if !self.exists(path) {
return self.set_float(path, v);
}
true
}
#[must_use = "a setter reports whether the write applied; an unusable path writes nothing (see write_reason)"]
pub fn set_bool_default(&mut self, path: &str, v: bool) -> bool {
if !self.exists(path) {
return self.set_bool(path, v);
}
true
}
#[must_use = "a setter reports whether the write applied; an unusable path writes nothing (see write_reason)"]
pub fn set_string_default(&mut self, path: &str, v: &str) -> bool {
if !self.exists(path) {
return self.set_string(path, v);
}
true
}
#[must_use = "a setter reports whether the write applied; an unusable path writes nothing (see write_reason)"]
pub fn set_literal(&mut self, path: &str, text: &str) -> bool {
match literal_value(text) {
Some(v) => self.set_value(path, v),
None => false,
}
}
#[must_use = "a setter reports whether the write applied; an unusable path writes nothing (see write_reason)"]
pub fn set_literal_default(&mut self, path: &str, text: &str) -> bool {
if !self.exists(path) {
return self.set_literal(path, text);
}
true
}
#[must_use = "a setter reports whether the write applied; an unusable path writes nothing (see write_reason)"]
pub fn set_datetime_default(&mut self, path: &str, v: &ShclDateTime) -> bool {
if !self.exists(path) {
return self.set_datetime(path, v);
}
true
}
#[must_use = "a setter reports whether the write applied; an unusable path writes nothing (see write_reason)"]
pub fn set_raw_default(&mut self, path: &str, content: &str, info: &str) -> bool {
if !self.exists(path) {
return self.set_raw(path, content, info);
}
true
}
#[must_use = "a setter reports whether the write applied; an unusable path writes nothing (see write_reason)"]
pub fn set_int_array_default(&mut self, path: &str, v: &[i64]) -> bool {
if !self.exists(path) {
return self.set_int_array(path, v);
}
true
}
#[must_use = "a setter reports whether the write applied; an unusable path writes nothing (see write_reason)"]
pub fn set_float_array_default(&mut self, path: &str, v: &[f64]) -> bool {
if !self.exists(path) {
return self.set_float_array(path, v);
}
true
}
#[must_use = "a setter reports whether the write applied; an unusable path writes nothing (see write_reason)"]
pub fn set_bool_array_default(&mut self, path: &str, v: &[bool]) -> bool {
if !self.exists(path) {
return self.set_bool_array(path, v);
}
true
}
#[must_use = "a setter reports whether the write applied; an unusable path writes nothing (see write_reason)"]
pub fn set_string_array_default(&mut self, path: &str, v: &[&str]) -> bool {
if !self.exists(path) {
return self.set_string_array(path, v);
}
true
}
#[must_use = "a setter reports whether the write applied; an unusable path writes nothing (see write_reason)"]
pub fn set_datetime_array_default(&mut self, path: &str, v: &[ShclDateTime]) -> bool {
if !self.exists(path) {
return self.set_datetime_array(path, v);
}
true
}
}
impl Document {
pub fn merge(&mut self, over: &Document) {
self.lost += over.lost;
self.overlay(ROOT, over, ROOT);
for o in &over.orphans {
if !self.orphans.iter().any(|e| e.text == o.text) {
self.orphans.push(o.clone());
}
}
}
fn adopt_trivia(&mut self, base: usize, over: &Document, ok: usize) {
let Some(st) = over.arena[ok].trivia.as_deref() else {
return;
};
let bt = self.arena[base].triv_mut();
bt.leading.extend_from_slice(&st.leading);
if !st.trailing.is_empty() {
if bt.trailing.is_empty() {
bt.trailing = st.trailing.clone();
} else {
bt.leading.push(Lead::plain(st.trailing.clone()));
}
}
bt.after.extend_from_slice(&st.after);
bt.inside.extend_from_slice(&st.inside);
}
fn overlay(&mut self, base_parent: usize, over: &Document, over_parent: usize) {
let over_kids = over.arena[over_parent].children.clone();
let mut order: Vec<String> = Vec::new();
let mut groups: HashMap<String, Vec<usize>> = HashMap::new();
for &k in &over_kids {
let n = &over.arena[k].name;
groups
.entry(n.clone())
.or_insert_with(|| {
order.push(n.clone());
Vec::new()
})
.push(k);
}
let base_kids = self.arena[base_parent].children.clone();
let mut has_container: HashMap<String, bool> = HashMap::new();
let mut by_key: HashMap<(String, String), usize> = HashMap::new();
for &b in &base_kids {
let name = self.arena[b].name.clone();
let e = has_container.entry(name.clone()).or_insert(false);
*e = *e || !self.arena[b].children.is_empty();
by_key.entry((name, self.arena[b].value.key())).or_insert(b);
}
let mut replace: HashMap<String, Vec<usize>> = HashMap::new();
let mut appended: Vec<usize> = Vec::new();
let empty_key = Value::Empty.key();
for name in &order {
let group = &groups[name];
let over_leafy = group.iter().all(|&k| over.arena[k].children.is_empty());
let in_base = has_container.contains_key(name);
let base_container = has_container.get(name).copied().unwrap_or(false);
if over_leafy && !base_container {
let clones: Vec<usize> = group
.iter()
.map(|&ok| self.clone_subtree(over, ok, base_parent))
.collect();
if in_base {
replace.insert(name.clone(), clones);
} else {
appended.extend(clones);
}
} else {
for &ok in group {
let okey = over.arena[ok].value.key();
let mut target = by_key.get(&(name.clone(), okey.clone())).copied();
if target.is_none() && matches!(over.arena[ok].value, Value::Raw { .. }) {
let empty = (name.clone(), empty_key.clone());
let hit = by_key.get(&empty).copied();
if let Some(b) = hit {
self.arena[b].value = over.arena[ok].value.clone();
by_key.remove(&empty);
by_key.entry((name.clone(), okey)).or_insert(b);
target = Some(b);
}
}
match target {
Some(b) => {
self.adopt_trivia(b, over, ok);
self.overlay(b, over, ok);
}
None => {
let c = self.clone_subtree(over, ok, base_parent);
appended.push(c);
}
}
}
}
}
if replace.is_empty() && appended.is_empty() {
return;
}
let mut newkids: Vec<usize> = Vec::with_capacity(base_kids.len() + appended.len());
let mut spliced: std::collections::HashSet<&str> = std::collections::HashSet::new();
for &b in &base_kids {
let name = self.arena[b].name.as_str();
match replace.get(name) {
Some(clones) => {
if spliced.insert(name) {
newkids.extend(clones.iter().copied());
}
}
None => newkids.push(b),
}
}
newkids.extend(appended.iter().copied());
self.arena[base_parent].children = newkids;
}
fn clone_subtree(&mut self, over: &Document, oi: usize, parent: usize) -> usize {
let src = &over.arena[oi];
let node = NodeData {
name: src.name.clone(),
value: src.value.clone(),
children: Vec::new(),
parent,
line: src.line,
star_list: src.star_list,
star_mixed: src.star_mixed,
trivia: src.trivia.clone(),
blank_before: src.blank_before,
src_set: src.src_set,
src: src.src.clone(),
name_src: src.name_src.clone(),
};
let idx = self.arena.len();
self.arena.push(node);
let okids = over.arena[oi].children.clone();
for ok in okids {
let c = self.clone_subtree(over, ok, idx);
self.arena[idx].children.push(c);
}
idx
}
}
impl Default for Document {
fn default() -> Document {
Document::new()
}
}
impl std::str::FromStr for Document {
type Err = std::convert::Infallible;
fn from_str(text: &str) -> Result<Document, Self::Err> {
Ok(Document::parse(text))
}
}
impl std::fmt::Display for Document {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.to_canonical())
}
}
const CURRENCY: &[char] = &[
'$', '¢', '£', '¤', '¥', '₩', '₪', '₫', '€', '₭', '₮', '₱', '₲', '₴', '₹', '₺', '₼', '₽', '₾',
'₿',
];
fn strip_currency(t: &str) -> &str {
let mut it = t.chars();
match it.next() {
Some(c) if CURRENCY.contains(&c) => it.as_str(),
_ => t,
}
}
fn parse_int_text(e: &Element, level: Strictness) -> Option<i64> {
let mut t = e.text.trim();
if level == Strictness::Loose {
t = strip_currency(t);
}
let body = t.strip_prefix(['+', '-']).unwrap_or(t);
if !body.is_empty() && body.bytes().all(|b| b.is_ascii_digit()) {
return t.parse::<i64>().ok();
}
let (neg, hex) = match t.strip_prefix('-') {
Some(r) => (true, r),
None => (false, t.strip_prefix('+').unwrap_or(t)),
};
if let Some(h) = hex.strip_prefix("0x").or_else(|| hex.strip_prefix("0X"))
&& !h.is_empty()
&& h.bytes().all(|b| b.is_ascii_hexdigit())
{
let m = u64::from_str_radix(h, 16).ok()?;
return if neg {
if m == (i64::MAX as u64) + 1 {
Some(i64::MIN)
} else if m <= i64::MAX as u64 {
Some(-(m as i64))
} else {
None
}
} else if m <= i64::MAX as u64 {
Some(m as i64)
} else {
None
};
}
if e.quoted && t.contains(',') {
let sign_body = t.strip_prefix(['+', '-']).unwrap_or(t);
let groups: Vec<&str> = sign_body.split(',').collect();
let well_formed = groups.len() > 1
&& !groups[0].is_empty()
&& groups[0].len() <= 3
&& groups[0].bytes().all(|b| b.is_ascii_digit())
&& groups[1..]
.iter()
.all(|g| g.len() == 3 && g.bytes().all(|b| b.is_ascii_digit()));
if well_formed {
return t.replace(',', "").parse::<i64>().ok();
}
}
if level == Strictness::Loose
&& let Some(f) = parse_float_text(e, level)
{
let r = f.round();
if r >= i64::MIN as f64 && r <= i64::MAX as f64 {
return Some(r as i64);
}
}
None
}
fn float_shape_ok(t: &str) -> bool {
let body = t.strip_prefix(['+', '-']).unwrap_or(t);
if body.is_empty() {
return false;
}
let (mantissa, exp) = match body.split_once(['e', 'E']) {
Some((m, x)) => (m, Some(x)),
None => (body, None),
};
if let Some(x) = exp {
let xb = x.strip_prefix(['+', '-']).unwrap_or(x);
if xb.is_empty() || !xb.bytes().all(|b| b.is_ascii_digit()) {
return false;
}
}
let (int_part, frac_part) = match mantissa.split_once('.') {
Some((a, b)) => (a, b),
None => (mantissa, ""),
};
if int_part.is_empty() && frac_part.is_empty() {
return false;
}
int_part.bytes().all(|b| b.is_ascii_digit()) && frac_part.bytes().all(|b| b.is_ascii_digit())
}
fn parse_float_text(e: &Element, level: Strictness) -> Option<f64> {
let mut t = e.text.trim();
let mut percent = false;
if level == Strictness::Loose {
t = strip_currency(t);
if let Some(inner) = t.strip_suffix('%') {
t = inner.trim_end();
percent = true;
}
}
let v = if float_shape_ok(t) {
t.parse::<f64>().ok()?
} else {
let el = Element {
text: t.to_string(),
quoted: e.quoted,
};
parse_int_text_no_loose(&el)? as f64
};
Some(if percent { v / 100.0 } else { v })
}
fn parse_int_text_no_loose(e: &Element) -> Option<i64> {
parse_int_text(e, Strictness::Standard)
}
fn parse_bool_text(t: &str, level: Strictness) -> Option<bool> {
let s = t.trim().to_ascii_lowercase();
match (level, s.as_str()) {
(_, "true") => Some(true),
(_, "false") => Some(false),
(Strictness::Strict, _) => None,
(_, "yes") | (_, "on") | (_, "1") => Some(true),
(_, "no") | (_, "off") | (_, "0") => Some(false),
(Strictness::Loose, "t")
| (Strictness::Loose, "y")
| (Strictness::Loose, "enable")
| (Strictness::Loose, "enabled") => Some(true),
(Strictness::Loose, "f")
| (Strictness::Loose, "n")
| (Strictness::Loose, "disable")
| (Strictness::Loose, "disabled") => Some(false),
_ => None,
}
}
const MONTHS: &[(&str, u32)] = &[
("jan", 1),
("feb", 2),
("mar", 3),
("apr", 4),
("may", 5),
("jun", 6),
("jul", 7),
("aug", 8),
("sep", 9),
("oct", 10),
("nov", 11),
("dec", 12),
("january", 1),
("february", 2),
("march", 3),
("april", 4),
("june", 6),
("july", 7),
("august", 8),
("september", 9),
("october", 10),
("november", 11),
("december", 12),
];
fn month_from_name(s: &str) -> Option<u32> {
let l = s.to_ascii_lowercase();
MONTHS.iter().find(|(n, _)| *n == l).map(|(_, m)| *m)
}
fn days_in_month(y: i32, m: u32) -> u32 {
match m {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if (y % 4 == 0 && y % 100 != 0) || y % 400 == 0 {
29
} else {
28
}
}
_ => 0,
}
}
fn valid_date(y: i32, m: u32, d: u32) -> bool {
(1..=12).contains(&m) && d >= 1 && d <= days_in_month(y, m)
}
fn parse_date_part(s: &str) -> Option<(i32, u32, u32)> {
let s = s.trim();
if s.len() == 8 && s.bytes().all(|b| b.is_ascii_digit()) {
let y: i32 = s[..4].parse().ok()?;
let m: u32 = s[4..6].parse().ok()?;
let d: u32 = s[6..8].parse().ok()?;
return valid_date(y, m, d).then_some((y, m, d));
}
let toks: Vec<&str> = s.split_whitespace().collect();
if toks.len() == 3 {
if let Some(m) = month_from_name(toks[0]) {
let day_tok = toks[1].strip_suffix(',').unwrap_or(toks[1]);
let d: u32 = day_tok.parse().ok()?;
let y: i32 = parse_year4(toks[2])?;
return valid_date(y, m, d).then_some((y, m, d));
}
if let Some(m) = month_from_name(toks[1]) {
let d: u32 = toks[0].parse().ok()?;
let y: i32 = parse_year4(toks[2])?;
return valid_date(y, m, d).then_some((y, m, d));
}
return None;
}
if toks.len() != 1 {
return None;
}
let delim = s.chars().find(|c| matches!(c, '-' | '/' | '.'))?;
let parts: Vec<&str> = s.split(delim).collect();
if parts.len() != 3 || parts.iter().any(|p| p.is_empty()) {
return None;
}
if s.chars().filter(|c| matches!(c, '-' | '/' | '.')).count() != 2 {
return None;
}
if parts[0].len() == 4 && parts[0].bytes().all(|b| b.is_ascii_digit()) {
let y: i32 = parts[0].parse().ok()?;
let m: u32 = parse_num2(parts[1])?;
let d: u32 = parse_num2(parts[2])?;
return valid_date(y, m, d).then_some((y, m, d));
}
if let Some(m) = month_from_name(parts[0]) {
let d: u32 = parse_num2(parts[1])?;
let y: i32 = parse_year4(parts[2])?;
return valid_date(y, m, d).then_some((y, m, d));
}
if let Some(m) = month_from_name(parts[1]) {
let d: u32 = parse_num2(parts[0])?;
let y: i32 = parse_year4(parts[2])?;
return valid_date(y, m, d).then_some((y, m, d));
}
None }
fn parse_year4(s: &str) -> Option<i32> {
(s.len() == 4 && s.bytes().all(|b| b.is_ascii_digit())).then(|| s.parse().ok())?
}
fn parse_num2(s: &str) -> Option<u32> {
((s.len() == 1 || s.len() == 2) && s.bytes().all(|b| b.is_ascii_digit()))
.then(|| s.parse().ok())?
}
type TimeParts = ((u32, u32, Option<u32>), Option<String>, Option<ZoneSpec>);
fn parse_time_part(s: &str) -> Option<TimeParts> {
let mut t = s.trim();
let mut zone: Option<ZoneSpec> = None;
if let Some(rest) = t.strip_suffix(['Z', 'z']) {
zone = Some(ZoneSpec::Utc);
t = rest.trim_end();
} else if t.len() >= 6 {
let tail = &t.as_bytes()[t.len() - 6..];
let sign = tail[0];
if (sign == b'+' || sign == b'-')
&& tail[1].is_ascii_digit()
&& tail[2].is_ascii_digit()
&& tail[3] == b':'
&& tail[4].is_ascii_digit()
&& tail[5].is_ascii_digit()
{
let hh = i32::from(tail[1] - b'0') * 10 + i32::from(tail[2] - b'0');
let mm = i32::from(tail[4] - b'0') * 10 + i32::from(tail[5] - b'0');
if hh <= 23 && mm <= 59 {
let mut off = hh * 60 + mm;
if sign == b'-' {
off = -off;
}
zone = Some(ZoneSpec::OffsetMinutes(off));
t = t[..t.len() - 6].trim_end();
}
}
}
let mut meridiem: Option<bool> = None; let lower = t.to_ascii_lowercase();
if let Some(rest) = lower.strip_suffix("am") {
meridiem = Some(false);
t = &t[..rest.trim_end().len()];
} else if let Some(rest) = lower.strip_suffix("pm") {
meridiem = Some(true);
t = &t[..rest.trim_end().len()];
}
let t = t.trim_end();
let (hms, frac) = match t.split_once('.') {
Some((a, f)) => {
if f.is_empty() || f.len() > 9 || !f.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
(a, Some(f.to_string()))
}
None => (t, None),
};
let parts: Vec<&str> = hms.split(':').collect();
if parts.len() < 2 || parts.len() > 3 {
return None;
}
if frac.is_some() && parts.len() != 3 {
return None; }
let h_raw: u32 = parse_num2(parts[0])?;
let mi: u32 = (parts[1].len() == 2)
.then(|| parse_num2(parts[1]))
.flatten()?;
let sec: Option<u32> = match parts.get(2) {
Some(p) => Some((p.len() == 2).then(|| parse_num2(p)).flatten()?),
None => None,
};
if mi > 59 || sec.is_some_and(|x| x > 59) {
return None;
}
let h = match meridiem {
None => {
if h_raw > 23 {
return None;
}
h_raw
}
Some(pm) => {
if !(1..=12).contains(&h_raw) {
return None;
}
match (pm, h_raw) {
(false, 12) => 0,
(false, x) => x,
(true, 12) => 12,
(true, x) => x + 12,
}
}
};
Some(((h, mi, sec), frac, zone))
}
pub fn parse_datetime(text: &str) -> Option<ShclDateTime> {
let t = text.trim();
if t.is_empty() {
return None;
}
if let Some(colon) = t.find(':') {
let bytes = t.as_bytes();
let mut k = colon;
while k > 0 && bytes[k - 1].is_ascii_digit() && colon - k < 2 {
k -= 1;
}
if k == colon {
return None; }
if k == 0 {
let ((h, mi, s), frac, zone) = parse_time_part(t)?;
return Some(ShclDateTime {
date: None,
time: Some((h, mi, s)),
frac,
zone,
});
}
let sep = t[..k].chars().last()?;
if !matches!(sep, 'T' | 't' | ' ' | '_' | '-' | '/' | '.') {
return None;
}
let date_str = &t[..k - sep.len_utf8()];
let date = parse_date_part(date_str)?;
let ((h, mi, s), frac, zone) = parse_time_part(&t[k..])?;
return Some(ShclDateTime {
date: Some(date),
time: Some((h, mi, s)),
frac,
zone,
});
}
let date = parse_date_part(t)?;
Some(ShclDateTime {
date: Some(date),
time: None,
frac: None,
zone: None,
})
}
impl Document {
fn node_at(&self, path: &str) -> Result<usize, Status> {
match self.resolve(path)? {
Resolved::None => Err(Status::NotFound),
Resolved::Many(_) | Resolved::Slots(_) => Err(Status::Multiple),
Resolved::One(n) => Ok(n),
}
}
fn raw_of(&self, n: usize) -> String {
match &self.arena[n].src {
Some(s) => s.clone(),
None => self.arena[n].value.display(),
}
}
fn scalar_element<'a>(&self, v: &'a Value) -> Result<&'a Element, Status> {
match v {
Value::Empty => Err(Status::Empty),
Value::Raw { .. } => Err(Status::BadType),
Value::Cell(els) if els.len() == 1 => Ok(&els[0]),
Value::Cell(_) => Err(Status::BadType), }
}
fn read_scalar<T: Default>(
&self,
path: &str,
coerce: impl Fn(&Element) -> Option<T>,
) -> Read<T> {
let node = match self.node_at(path) {
Ok(n) => n,
Err(st) => return Read::new(T::default(), st, None),
};
let value = &self.arena[node].value;
let raw = Some(self.raw_of(node));
let line = self.arena[node].line;
match self.scalar_element(value) {
Ok(el) => match coerce(el) {
Some(v) => Read::new(v, Status::Good, raw).at(line, el.quoted),
None => Read::new(T::default(), Status::BadType, raw).at(line, el.quoted),
},
Err(st) => Read::new(T::default(), st, raw).at(line, false),
}
}
pub fn read_int(&self, path: &str) -> Read<i64> {
let lvl = self.strictness;
self.read_scalar(path, |e| parse_int_text(e, lvl))
}
pub fn read_float(&self, path: &str) -> Read<f64> {
let lvl = self.strictness;
self.read_scalar(path, |e| parse_float_text(e, lvl))
}
pub fn read_bool(&self, path: &str) -> Read<bool> {
let lvl = self.strictness;
self.read_scalar(path, |e| parse_bool_text(&e.text, lvl))
}
pub fn read_datetime(&self, path: &str) -> Read<ShclDateTime> {
self.read_scalar(path, |e| parse_datetime(&e.text))
}
pub fn read_string(&self, path: &str) -> Read<String> {
let node = match self.node_at(path) {
Ok(n) => n,
Err(st) => return Read::new(String::new(), st, None),
};
let value = &self.arena[node].value;
let raw = Some(self.raw_of(node));
let line = self.arena[node].line;
match value {
Value::Empty => Read::new(String::new(), Status::Empty, raw).at(line, false),
Value::Raw(r) => Read::new(r.content.clone(), Status::Good, raw).at(line, false),
Value::Cell(els) if els.len() == 1 => {
Read::new(apply_escapes(&els[0].text), Status::Good, raw).at(line, els[0].quoted)
}
Value::Cell(els) => Read::new(
els.iter().map(emit_element).collect::<Vec<_>>().join(", "),
Status::Good,
raw,
)
.at(line, false),
}
}
pub fn read_raw(&self, path: &str) -> Read<String> {
let node = match self.node_at(path) {
Ok(n) => n,
Err(st) => return Read::new(String::new(), st, None),
};
let value = &self.arena[node].value;
let raw = Some(self.raw_of(node));
let line = self.arena[node].line;
match value {
Value::Raw(r) => Read::new(r.content.clone(), Status::Good, raw).at(line, false),
Value::Empty => Read::new(String::new(), Status::Empty, raw).at(line, false),
_ => Read::new(String::new(), Status::BadType, raw).at(line, false),
}
}
pub fn read_raw_info(&self, path: &str) -> Read<String> {
let node = match self.node_at(path) {
Ok(n) => n,
Err(st) => return Read::new(String::new(), st, None),
};
let raw = Some(self.raw_of(node));
let line = self.arena[node].line;
match &self.arena[node].value {
Value::Raw(r) => Read::new(r.info.clone(), Status::Good, raw).at(line, false),
_ => Read::new(String::new(), Status::BadType, raw).at(line, false),
}
}
fn read_array<T: Default>(
&self,
path: &str,
coerce: impl Fn(&Element) -> Option<T>,
) -> Read<Vec<T>> {
match self.resolve(path) {
Err(st) => Read::new(Vec::new(), st, None),
Ok(Resolved::Slots(slots)) => {
let mut out: Vec<T> = Vec::new();
let mut sts: Vec<Status> = Vec::new();
for slot in &slots {
match slot {
Err(st) => {
out.push(T::default());
sts.push(*st);
}
Ok(n) => match self.scalar_element(&self.arena[*n].value) {
Ok(el) => match coerce(el) {
Some(v) => {
out.push(v);
sts.push(Status::Good);
}
None => {
out.push(T::default());
sts.push(Status::BadType);
}
},
Err(st) => {
out.push(T::default());
sts.push(st);
}
},
}
}
let status = if sts.is_empty() {
Status::Empty
} else {
sts.iter().copied().max().unwrap_or(Status::Good)
};
Read::with_slots(out, status, None, sts)
}
Ok(Resolved::None) => Read::new(Vec::new(), Status::NotFound, None),
Ok(Resolved::Many(_)) => Read::new(Vec::new(), Status::Multiple, None),
Ok(Resolved::One(n)) => {
let value = &self.arena[n].value;
let raw = Some(self.raw_of(n));
let line = self.arena[n].line;
match value {
Value::Empty => Read::new(Vec::new(), Status::Empty, raw).at(line, false),
Value::Raw { .. } => {
Read::new(Vec::new(), Status::BadType, raw).at(line, false)
}
Value::Cell(els) => {
let mut out = Vec::with_capacity(els.len());
let mut sts = Vec::with_capacity(els.len());
for el in els {
match coerce(el) {
Some(v) => {
out.push(v);
sts.push(Status::Good);
}
None => {
out.push(T::default());
sts.push(Status::BadType);
}
}
}
let status = sts.iter().copied().max().unwrap_or(Status::Good);
Read::with_slots(out, status, raw, sts).at(line, false)
}
}
}
}
}
pub fn read_int_array(&self, path: &str) -> Read<Vec<i64>> {
let lvl = self.strictness;
self.read_array(path, |e| parse_int_text(e, lvl))
}
pub fn read_float_array(&self, path: &str) -> Read<Vec<f64>> {
let lvl = self.strictness;
self.read_array(path, |e| parse_float_text(e, lvl))
}
pub fn read_bool_array(&self, path: &str) -> Read<Vec<bool>> {
let lvl = self.strictness;
self.read_array(path, |e| parse_bool_text(&e.text, lvl))
}
pub fn read_datetime_array(&self, path: &str) -> Read<Vec<ShclDateTime>> {
self.read_array(path, |e| parse_datetime(&e.text))
}
pub fn read_string_array(&self, path: &str) -> Read<Vec<String>> {
self.read_array(path, |e| Some(apply_escapes(&e.text)))
}
pub fn get_int(&self, path: &str) -> Result<i64, Status> {
let r = self.read_int(path);
if r.status == Status::Good {
Ok(r.value)
} else {
Err(r.status)
}
}
pub fn get_float(&self, path: &str) -> Result<f64, Status> {
let r = self.read_float(path);
if r.status == Status::Good {
Ok(r.value)
} else {
Err(r.status)
}
}
pub fn get_bool(&self, path: &str) -> Result<bool, Status> {
let r = self.read_bool(path);
if r.status == Status::Good {
Ok(r.value)
} else {
Err(r.status)
}
}
pub fn get_string(&self, path: &str) -> Result<String, Status> {
let r = self.read_string(path);
if r.status == Status::Good {
Ok(r.value)
} else {
Err(r.status)
}
}
pub fn get_raw(&self, path: &str) -> Result<String, Status> {
let r = self.read_raw(path);
if r.status == Status::Good {
Ok(r.value)
} else {
Err(r.status)
}
}
pub fn get_datetime(&self, path: &str) -> Result<ShclDateTime, Status> {
let r = self.read_datetime(path);
if r.status == Status::Good {
Ok(r.value)
} else {
Err(r.status)
}
}
pub fn get_int_array(&self, path: &str) -> Result<Vec<i64>, Status> {
let r = self.read_int_array(path);
if r.status == Status::Good {
Ok(r.value)
} else {
Err(r.status)
}
}
pub fn get_float_array(&self, path: &str) -> Result<Vec<f64>, Status> {
let r = self.read_float_array(path);
if r.status == Status::Good {
Ok(r.value)
} else {
Err(r.status)
}
}
pub fn get_bool_array(&self, path: &str) -> Result<Vec<bool>, Status> {
let r = self.read_bool_array(path);
if r.status == Status::Good {
Ok(r.value)
} else {
Err(r.status)
}
}
pub fn get_string_array(&self, path: &str) -> Result<Vec<String>, Status> {
let r = self.read_string_array(path);
if r.status == Status::Good {
Ok(r.value)
} else {
Err(r.status)
}
}
pub fn get_datetime_array(&self, path: &str) -> Result<Vec<ShclDateTime>, Status> {
let r = self.read_datetime_array(path);
if r.status == Status::Good {
Ok(r.value)
} else {
Err(r.status)
}
}
pub fn get_int_or(&self, path: &str, def: i64) -> i64 {
self.get_int(path).unwrap_or(def)
}
pub fn get_float_or(&self, path: &str, def: f64) -> f64 {
self.get_float(path).unwrap_or(def)
}
pub fn get_bool_or(&self, path: &str, def: bool) -> bool {
self.get_bool(path).unwrap_or(def)
}
pub fn get_string_or(&self, path: &str, def: String) -> String {
self.get_string(path).unwrap_or(def)
}
pub fn get_raw_or(&self, path: &str, def: String) -> String {
self.get_raw(path).unwrap_or(def)
}
pub fn get_datetime_or(&self, path: &str, def: ShclDateTime) -> ShclDateTime {
self.get_datetime(path).unwrap_or(def)
}
pub fn get_int_array_or(&self, path: &str, def: Vec<i64>) -> Vec<i64> {
self.get_int_array(path).unwrap_or(def)
}
pub fn get_float_array_or(&self, path: &str, def: Vec<f64>) -> Vec<f64> {
self.get_float_array(path).unwrap_or(def)
}
pub fn get_bool_array_or(&self, path: &str, def: Vec<bool>) -> Vec<bool> {
self.get_bool_array(path).unwrap_or(def)
}
pub fn get_string_array_or(&self, path: &str, def: Vec<String>) -> Vec<String> {
self.get_string_array(path).unwrap_or(def)
}
pub fn get_datetime_array_or(&self, path: &str, def: Vec<ShclDateTime>) -> Vec<ShclDateTime> {
self.get_datetime_array(path).unwrap_or(def)
}
}
const SCHEMA_TYPES: [&str; 11] = [
"int",
"float",
"bool",
"string",
"datetime",
"raw",
"int-array",
"float-array",
"bool-array",
"string-array",
"datetime-array",
];
#[derive(Clone)]
enum AllowedSet {
Ints(Vec<i64>),
Floats(Vec<f64>),
Bools(Vec<bool>),
Dates(Vec<ShclDateTime>),
Strings(Vec<String>),
}
#[derive(Clone)]
struct Constraint {
path: String, segs: Vec<Segment>,
ty: Option<String>, required: bool,
allowed: Option<AllowedSet>,
min_i: Option<i64>,
max_i: Option<i64>,
min_f: Option<f64>,
max_f: Option<f64>,
repeat: Option<(u64, u64)>,
inherits: Option<String>, inherits_line: usize, desc: Option<String>, default_text: Option<String>, }
struct SchemaDef {
cons: Vec<Constraint>,
frags: HashMap<String, Vec<Constraint>>,
paths_complete: bool,
}
fn vdiag(out: &mut Vec<Diagnostic>, line: usize, msg: String) {
let code = diag_code(&msg);
out.push(Diagnostic {
line,
severity: Severity::Error,
message: msg,
code,
});
}
fn single_text(v: &Value) -> Option<String> {
match v {
Value::Cell(els) if els.len() == 1 => Some(apply_escapes(&els[0].text)),
_ => None,
}
}
fn build_schema(schema: &Document) -> (SchemaDef, Vec<Diagnostic>) {
let mut faults: Vec<Diagnostic> = Vec::new();
let mut cons: Vec<Constraint> = Vec::new();
let mut frags: HashMap<String, Vec<Constraint>> = HashMap::new();
let mut paths_complete = true;
for &f in &schema.arena[ROOT].children {
let node = &schema.arena[f];
match node.name.as_str() {
"field" => {
if let Some(c) = parse_field(schema, f, &mut faults) {
cons.push(c);
} else {
paths_complete = false;
}
}
"fragment" => {
let name = single_text(&node.value).filter(|n| !n.is_empty());
let Some(name) = name else {
vdiag(&mut faults, node.line, "bad schema fragment".to_string());
continue;
};
if frags.contains_key(&name) {
vdiag(
&mut faults,
node.line,
format!("bad schema fragment '{}': duplicate", name),
);
continue;
}
let mut fcs: Vec<Constraint> = Vec::new();
for &k in &schema.arena[f].children {
let kid = &schema.arena[k];
if kid.name == "field" {
if let Some(c) = parse_field(schema, k, &mut faults) {
fcs.push(c);
} else {
paths_complete = false;
}
} else {
vdiag(
&mut faults,
kid.line,
format!("bad schema fragment '{}': unknown key '{}'", name, kid.name),
);
}
}
frags.insert(name, fcs);
}
other => {
vdiag(
&mut faults,
node.line,
format!("unknown schema key '{}'", other),
);
}
}
}
for c in cons.iter().chain(frags.values().flatten()) {
if let Some(fr) = &c.inherits
&& !frags.contains_key(fr)
{
vdiag(
&mut faults,
c.inherits_line,
format!("unknown schema fragment '{}'", fr),
);
paths_complete = false;
}
}
faults.sort_by_key(|d| d.line);
(
SchemaDef {
cons,
frags,
paths_complete,
},
faults,
)
}
fn parse_field(schema: &Document, f: usize, faults: &mut Vec<Diagnostic>) -> Option<Constraint> {
let node = &schema.arena[f];
let path = match single_text(&node.value) {
Some(p) => p,
None => {
vdiag(faults, node.line, "bad schema path".to_string());
return None;
}
};
let segs = match scan_lookup(&path) {
Ok(s) if s.value_text.is_none() => s.segments,
_ => {
vdiag(faults, node.line, format!("bad schema path: {}", path));
return None;
}
};
let mut c = Constraint {
path,
segs,
ty: None,
required: false,
allowed: None,
min_i: None,
max_i: None,
min_f: None,
max_f: None,
repeat: None,
inherits: None,
inherits_line: 0,
desc: None,
default_text: None,
};
let mut required: Option<bool> = None;
let mut reopen_seen = false;
let mut allowed_at: Option<usize> = None;
let mut min_at: Option<usize> = None;
let mut max_at: Option<usize> = None;
for &k in &schema.arena[f].children {
let kid = &schema.arena[k];
if kid.value.is_empty() {
continue; }
match kid.name.as_str() {
"type" => match single_text(&kid.value).map(|t| t.to_ascii_lowercase()) {
Some(t) if SCHEMA_TYPES.contains(&t.as_str()) => {
if c.ty.is_some() {
vdiag(faults, kid.line, "bad schema constraint 'type'".to_string());
} else {
c.ty = Some(t);
}
}
Some(t) => {
vdiag(faults, kid.line, format!("unknown schema type '{}'", t));
}
None => vdiag(faults, kid.line, "bad schema constraint 'type'".to_string()),
},
"required" => {
let v =
single_text(&kid.value).and_then(|t| parse_bool_text(&t, Strictness::Standard));
match v {
Some(b) if required.is_none() => required = Some(b),
_ => vdiag(
faults,
kid.line,
"bad schema constraint 'required'".to_string(),
),
}
}
"reopen" => {
let v =
single_text(&kid.value).and_then(|t| parse_bool_text(&t, Strictness::Standard));
match v {
Some(_) if !reopen_seen => reopen_seen = true,
_ => vdiag(
faults,
kid.line,
"bad schema constraint 'reopen'".to_string(),
),
}
}
"allowed" => match &kid.value {
Value::Cell(_) if allowed_at.is_none() => allowed_at = Some(k),
_ => vdiag(
faults,
kid.line,
"bad schema constraint 'allowed'".to_string(),
),
},
"min" => match &kid.value {
Value::Cell(els) if els.len() == 1 && min_at.is_none() => min_at = Some(k),
_ => vdiag(faults, kid.line, "bad schema constraint 'min'".to_string()),
},
"max" => match &kid.value {
Value::Cell(els) if els.len() == 1 && max_at.is_none() => max_at = Some(k),
_ => vdiag(faults, kid.line, "bad schema constraint 'max'".to_string()),
},
"repeat" => match &kid.value {
Value::Cell(els) if c.repeat.is_none() && matches!(els.len(), 1 | 2) => {
let lo = els[0].text.parse::<u64>().ok();
let hi = els.last().and_then(|e| e.text.parse::<u64>().ok());
match (lo, hi) {
(Some(a), Some(b)) if a <= b => c.repeat = Some((a, b)),
_ => vdiag(
faults,
kid.line,
"bad schema constraint 'repeat'".to_string(),
),
}
}
_ => vdiag(
faults,
kid.line,
"bad schema constraint 'repeat'".to_string(),
),
},
"inherits" => match single_text(&kid.value).filter(|t| !t.is_empty()) {
Some(t) if c.inherits.is_none() => {
c.inherits = Some(t);
c.inherits_line = kid.line;
}
_ => vdiag(
faults,
kid.line,
"bad schema constraint 'inherits'".to_string(),
),
},
"desc" => {
if c.desc.is_none() {
c.desc = single_text(&kid.value);
}
}
"default" => {
if c.default_text.is_none() {
c.default_text = emit_value_inline(&kid.value);
}
}
other => vdiag(faults, kid.line, format!("unknown schema key '{}'", other)),
}
}
c.required = required.unwrap_or(false);
let base =
c.ty.as_deref()
.map(|t| t.strip_suffix("-array").unwrap_or(t))
.unwrap_or("string");
if let Some(a) = allowed_at {
let kid = &schema.arena[a];
let Value::Cell(els) = &kid.value else {
return None;
};
let set = match base {
"int" => els
.iter()
.map(|e| parse_int_text(e, Strictness::Standard))
.collect::<Option<Vec<_>>>()
.map(AllowedSet::Ints),
"float" => els
.iter()
.map(|e| parse_float_text(e, Strictness::Standard))
.collect::<Option<Vec<_>>>()
.map(AllowedSet::Floats),
"bool" => els
.iter()
.map(|e| parse_bool_text(&e.text, Strictness::Standard))
.collect::<Option<Vec<_>>>()
.map(AllowedSet::Bools),
"datetime" => els
.iter()
.map(|e| parse_datetime(&e.text))
.collect::<Option<Vec<_>>>()
.map(AllowedSet::Dates),
"raw" => None, _ => Some(AllowedSet::Strings(
els.iter().map(|e| apply_escapes(&e.text)).collect(),
)),
};
match set {
Some(s) => c.allowed = Some(s),
None => vdiag(
faults,
kid.line,
"bad schema constraint 'allowed'".to_string(),
),
}
}
for (at, is_min) in [(min_at, true), (max_at, false)] {
let Some(m) = at else { continue };
let kid = &schema.arena[m];
let el = match &kid.value {
Value::Cell(els) if els.len() == 1 => &els[0],
_ => continue,
};
let key = if is_min { "min" } else { "max" };
match base {
"int" => match parse_int_text(el, Strictness::Standard) {
Some(v) if is_min => c.min_i = Some(v),
Some(v) => c.max_i = Some(v),
None => vdiag(faults, kid.line, format!("bad schema constraint '{}'", key)),
},
"float" => match parse_float_text(el, Strictness::Standard) {
Some(v) if is_min => c.min_f = Some(v),
Some(v) => c.max_f = Some(v),
None => vdiag(faults, kid.line, format!("bad schema constraint '{}'", key)),
},
_ => vdiag(faults, kid.line, format!("bad schema constraint '{}'", key)),
}
}
Some(c)
}
fn emit_value_inline(v: &Value) -> Option<String> {
match v {
Value::Cell(els) => Some(els.iter().map(emit_element).collect::<Vec<_>>().join(", ")),
_ => None,
}
}
fn allowed_join(a: &AllowedSet) -> String {
match a {
AllowedSet::Ints(v) => v
.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(", "),
AllowedSet::Floats(v) => v
.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(", "),
AllowedSet::Bools(v) => v
.iter()
.map(|x| if *x { "true" } else { "false" }.to_string())
.collect::<Vec<_>>()
.join(", "),
AllowedSet::Dates(v) => v
.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(", "),
AllowedSet::Strings(v) => v.join(", "),
}
}
fn gen_annotation(c: &Constraint, tyname: &str) -> String {
let mut parts: Vec<String> = vec![tyname.to_string()];
if let Some(a) = &c.allowed {
parts.push(format!("one of: {}", allowed_join(a)));
} else if c.min_i.is_some() || c.max_i.is_some() {
parts.push(match (c.min_i, c.max_i) {
(Some(lo), Some(hi)) => format!("{}-{}", lo, hi),
(Some(lo), None) => format!(">= {}", lo),
(None, Some(hi)) => format!("<= {}", hi),
(None, None) => String::new(), });
} else if c.min_f.is_some() || c.max_f.is_some() {
parts.push(match (c.min_f, c.max_f) {
(Some(lo), Some(hi)) => format!("{}-{}", lo, hi),
(Some(lo), None) => format!(">= {}", lo),
(None, Some(hi)) => format!("<= {}", hi),
(None, None) => String::new(), });
}
if let Some((lo, hi)) = c.repeat {
parts.push(if lo == hi {
format!("repeat {}", lo)
} else {
format!("repeat {}-{}", lo, hi)
});
}
if c.required {
parts.push("required".to_string());
}
parts.join(", ")
}
fn gen_default_text(v: &str) -> String {
if !v.contains('\n') {
return v.to_string();
}
let mut s = String::from("\"");
for ch in v.chars() {
match ch {
'\\' => s.push_str("\\\\"),
'"' => s.push_str("\\\""),
'\n' => s.push_str("\\n"),
'\t' => s.push_str("\\t"),
c => s.push(c),
}
}
s.push('"');
s
}
pub fn generate(schema: &Document, no_banner: bool) -> Result<String, Vec<Diagnostic>> {
let (def, faults) = build_schema(schema);
if !faults.is_empty() {
return Err(faults);
}
let (cons, cuts) = expand_mounts(&def);
if cons.len() >= GEN_MAX_FIELDS {
return Err(vec![Diagnostic {
line: 0,
severity: Severity::Error,
code: "V096",
message: format!(
"schema expands past {} fields; fragments mounted at more than one path multiply",
GEN_MAX_FIELDS
),
}]);
}
let must_exist = |c: &Constraint| c.required || matches!(c.repeat, Some((lo, _)) if lo >= 1);
let has_wild = |c: &Constraint| {
c.segs
.iter()
.any(|s| matches!(s.selector, Some(Selector::Wildcard)))
};
let unwritable = |c: &Constraint| {
c.segs.len() > MAX_DEPTH
|| c.segs
.iter()
.any(|s| matches!(s.selector, Some(Selector::ByIndex(_))) || s.star)
|| c.path.contains('\n')
};
fn names_of(segs: &[Segment]) -> Vec<&str> {
segs.iter().map(|s| s.name.as_str()).collect()
}
let mut live: Vec<Vec<&str>> = cons
.iter()
.filter(|c| !has_wild(c) && !unwritable(c) && must_exist(c))
.map(|c| names_of(&c.segs))
.collect();
let mut fill = vec![false; cons.len()];
loop {
let mut changed = false;
for (i, c) in cons.iter().enumerate() {
if fill[i] || !has_wild(c) || unwritable(c) || !must_exist(c) {
continue;
}
let Some(k) = c
.segs
.iter()
.position(|s| matches!(s.selector, Some(Selector::Wildcard)))
else {
continue;
};
let parent = names_of(&c.segs[..k + 1]);
if live
.iter()
.any(|p| p.len() >= parent.len() && p[..parent.len()] == parent[..])
{
fill[i] = true;
live.push(names_of(&c.segs));
changed = true;
}
}
if !changed {
break;
}
}
let mut out = String::new();
let mut wild: Vec<(String, String)> = Vec::new();
let mut first = true;
for (i, c) in cons.iter().enumerate() {
let tyname = c.ty.clone().unwrap_or_else(|| "any".to_string());
if unwritable(c) || (has_wild(c) && !fill[i]) {
wild.push((c.path.replace('\n', "\\n"), tyname));
continue;
}
if !first {
out.push('\n');
}
first = false;
if let Some(d) = &c.desc {
for line in d.split('\n') {
out.push_str("# ");
out.push_str(line);
out.push('\n');
}
}
out.push_str("# ");
out.push_str(&gen_annotation(c, &tyname).replace('\n', "\\n"));
out.push('\n');
let path = if fill[i] {
gen_path_text(&c.segs)
} else {
c.path.clone()
};
let prefix = if must_exist(c) { "" } else { "#" };
match &c.default_text {
Some(v) => out.push_str(&format!("{}{}: {}\n", prefix, path, gen_default_text(v))),
None => out.push_str(&format!("{}{}:\n", prefix, path)),
}
}
wild.extend(cuts);
if !wild.is_empty() {
if !first {
out.push('\n');
}
out.push_str("# Paths needing an instance name (not generated):\n");
for (p, t) in &wild {
out.push_str(&format!("# {} {}\n", p, t));
}
}
if !no_banner {
if !out.is_empty() {
out.push('\n');
}
out.push_str(GEN_BANNER);
}
Ok(out)
}
const GEN_MAX_FIELDS: usize = 10_000;
const GEN_BANNER: &str = "\
#
# This config file format is SHCL.
# \"Simple Hierarchical Config Language\"
# Home https://github.com/jim-collier/shcl
# Syntax https://github.com/jim-collier/shcl/blob/main/project/spec.md
# Legal SHCL is Copyright © 2026 Jim Collier. License: MIT. No warranty.
#
";
fn gen_path_text(segs: &[Segment]) -> String {
let mut out = String::new();
for (i, s) in segs.iter().enumerate() {
if i > 0 {
out.push('.');
}
if s.star {
out.push('*');
} else {
out.push_str(&emit_name(&s.name));
}
match &s.selector {
Some(Selector::ByValue { text, quoted }) => {
out.push('[');
if *quoted {
out.push_str("e_text(text));
} else {
out.push_str(text);
}
out.push(']');
}
Some(Selector::ByIndex(k)) => {
out.push_str(&format!("[#{}]", k));
}
Some(Selector::Wildcard) | None => {}
}
}
out
}
fn expand_mounts(def: &SchemaDef) -> (Vec<Constraint>, Vec<(String, String)>) {
fn go(
list: &[Constraint],
def: &SchemaDef,
at: Option<(&str, &[Segment])>,
stack: &mut Vec<String>,
out: &mut Vec<Constraint>,
cuts: &mut Vec<(String, String)>,
) {
for c in list {
let mut cc = c.clone();
if let Some((p, s)) = at {
cc.path = format!("{}.{}", p, c.path);
let mut segs = s.to_vec();
segs.extend(c.segs.iter().cloned());
cc.segs = segs;
}
let path = cc.path.clone();
let segs = cc.segs.clone();
if out.len() >= GEN_MAX_FIELDS {
return;
}
out.push(cc);
if let Some(fr) = &c.inherits {
if stack.iter().any(|x| x == fr) || stack.len() >= MAX_DEPTH {
cuts.push((path.replace('\n', "\\n"), fr.clone()));
} else if let Some(fcs) = def.frags.get(fr) {
stack.push(fr.clone());
go(fcs, def, Some((&path, &segs)), stack, out, cuts);
stack.pop();
}
}
}
}
let mut out: Vec<Constraint> = Vec::new();
let mut cuts: Vec<(String, String)> = Vec::new();
let mut stack: Vec<String> = Vec::new();
go(&def.cons, def, None, &mut stack, &mut out, &mut cuts);
(out, cuts)
}
fn edit_distance(a: &str, b: &str) -> usize {
let a: Vec<char> = a.chars().collect();
let b: Vec<char> = b.chars().collect();
let mut prev: Vec<usize> = (0..=b.len()).collect();
let mut cur = vec![0usize; b.len() + 1];
for i in 1..=a.len() {
cur[0] = i;
for j in 1..=b.len() {
let cost = if a[i - 1] == b[j - 1] { 0 } else { 1 };
cur[j] = (prev[j] + 1).min(cur[j - 1] + 1).min(prev[j - 1] + cost);
}
std::mem::swap(&mut prev, &mut cur);
}
prev[b.len()]
}
impl Document {
pub fn validate(&self, schema: &Document) -> Vec<Diagnostic> {
let (def, faults) = build_schema(schema);
let mut out = faults;
for c in &def.cons {
self.v_check(c, &def, &mut out);
}
if def.paths_complete {
self.v_unknown(&def, &mut out);
}
out
}
fn v_contexts(
&self,
start: Vec<usize>,
segs: &[Segment],
anchor: usize,
out: &mut Vec<(usize, Vec<usize>)>,
) {
let mut cur = start;
for (i, seg) in segs.iter().enumerate() {
let mut next: Vec<usize> = Vec::new();
for &n in &cur {
if seg.star {
next.extend(self.arena[n].children.iter().copied());
} else {
next.extend(self.children_named(n, &seg.name));
}
}
if seg.star {
let rest = &segs[i + 1..];
if rest.is_empty() {
out.push((anchor, next));
} else {
for inst in next {
let line = self.arena[inst].line;
self.v_contexts(vec![inst], rest, line, out);
}
}
return;
}
match &seg.selector {
None => cur = next,
Some(Selector::ByValue { text, quoted }) => {
let want = apply_escapes(text);
cur = next
.into_iter()
.filter(|&c| {
disp_key(&self.arena[c].value) == want
&& (!quoted || single_scalar(&self.arena[c].value))
})
.collect();
}
Some(Selector::ByIndex(k)) => {
cur = index_usize(*k)
.and_then(|i| next.get(i))
.map(|&c| vec![c])
.unwrap_or_default();
}
Some(Selector::Wildcard) => {
let rest = &segs[i + 1..];
if rest.is_empty() {
out.push((anchor, next));
} else {
for inst in next {
let line = self.arena[inst].line;
self.v_contexts(vec![inst], rest, line, out);
}
}
return;
}
}
}
out.push((anchor, cur));
}
fn v_check(&self, c: &Constraint, def: &SchemaDef, out: &mut Vec<Diagnostic>) {
let mut mounted = std::collections::HashSet::new();
self.v_check_from(c, def, ROOT, 0, out, &mut mounted);
}
fn v_check_from(
&self,
c: &Constraint,
def: &SchemaDef,
start: usize,
anchor0: usize,
out: &mut Vec<Diagnostic>,
mounted: &mut std::collections::HashSet<(String, usize)>,
) {
let mut ctxs: Vec<(usize, Vec<usize>)> = Vec::new();
self.v_contexts(vec![start], &c.segs, anchor0, &mut ctxs);
for (anchor, found) in &ctxs {
if c.required && found.is_empty() {
vdiag(out, *anchor, format!("required path missing: {}", c.path));
}
if let Some((lo, hi)) = c.repeat {
let n = found.len() as u64;
if n < lo || n > hi {
vdiag(
out,
*anchor,
format!(
"instance count out of bounds at '{}': {} not in {}..{}",
c.path, n, lo, hi
),
);
}
}
for &n in found {
self.v_node(c, n, out);
if let Some(fr) = &c.inherits
&& let Some(fcs) = def.frags.get(fr)
{
if mounted.insert((fr.clone(), n)) {
for fc in fcs {
self.v_check_from(fc, def, n, self.arena[n].line, out, mounted);
}
}
}
}
}
}
fn v_node(&self, c: &Constraint, n: usize, out: &mut Vec<Diagnostic>) {
let node = &self.arena[n];
let line = node.line;
let kind = c.ty.as_deref();
let base = kind
.map(|t| t.strip_suffix("-array").unwrap_or(t))
.unwrap_or("string");
let is_array = kind.is_some_and(|t| t.ends_with("-array"));
let wrong = |out: &mut Vec<Diagnostic>| {
vdiag(
out,
line,
format!(
"wrong type at '{}': value is not a valid {}",
c.path,
kind.unwrap_or("string")
),
);
};
match &node.value {
Value::Empty => {}
Value::Raw(r) => {
let content = &r.content;
if kind.is_some() && (base != "raw" && base != "string" || is_array) {
wrong(out);
return;
}
if let Some(AllowedSet::Strings(set)) = &c.allowed
&& !set.contains(content)
{
vdiag(
out,
line,
format!("value not allowed at '{}': {}", c.path, content),
);
}
}
Value::Cell(els) => {
if base == "raw" {
wrong(out);
return;
}
if kind.is_some() && !is_array && base != "string" && els.len() > 1 {
wrong(out);
return;
}
match base {
"int" => {
let mut vals: Vec<i64> = Vec::with_capacity(els.len());
for e in els {
match parse_int_text(e, self.strictness) {
Some(v) => vals.push(v),
None => {
wrong(out);
return;
}
}
}
if let Some(AllowedSet::Ints(set)) = &c.allowed
&& let Some(i) = vals.iter().position(|v| !set.contains(v))
{
vdiag(
out,
line,
format!("value not allowed at '{}': {}", c.path, els[i].text),
);
}
if let Some(lo) = c.min_i
&& vals.iter().any(|v| *v < lo)
{
vdiag(out, line, format!("value below min at '{}'", c.path));
}
if let Some(hi) = c.max_i
&& vals.iter().any(|v| *v > hi)
{
vdiag(out, line, format!("value above max at '{}'", c.path));
}
}
"float" => {
let mut vals: Vec<f64> = Vec::with_capacity(els.len());
for e in els {
match parse_float_text(e, self.strictness) {
Some(v) => vals.push(v),
None => {
wrong(out);
return;
}
}
}
if let Some(AllowedSet::Floats(set)) = &c.allowed
&& let Some(i) = vals.iter().position(|v| !set.contains(v))
{
vdiag(
out,
line,
format!("value not allowed at '{}': {}", c.path, els[i].text),
);
}
if let Some(lo) = c.min_f
&& vals.iter().any(|v| *v < lo)
{
vdiag(out, line, format!("value below min at '{}'", c.path));
}
if let Some(hi) = c.max_f
&& vals.iter().any(|v| *v > hi)
{
vdiag(out, line, format!("value above max at '{}'", c.path));
}
}
"bool" => {
let mut vals: Vec<bool> = Vec::with_capacity(els.len());
for e in els {
match parse_bool_text(&e.text, self.strictness) {
Some(v) => vals.push(v),
None => {
wrong(out);
return;
}
}
}
if let Some(AllowedSet::Bools(set)) = &c.allowed
&& let Some(i) = vals.iter().position(|v| !set.contains(v))
{
vdiag(
out,
line,
format!("value not allowed at '{}': {}", c.path, els[i].text),
);
}
}
"datetime" => {
let mut vals: Vec<ShclDateTime> = Vec::with_capacity(els.len());
for e in els {
match parse_datetime(&e.text) {
Some(v) => vals.push(v),
None => {
wrong(out);
return;
}
}
}
if let Some(AllowedSet::Dates(set)) = &c.allowed
&& let Some(i) = vals.iter().position(|v| !set.contains(v))
{
vdiag(
out,
line,
format!("value not allowed at '{}': {}", c.path, els[i].text),
);
}
}
_ => {
if let Some(AllowedSet::Strings(set)) = &c.allowed {
let bad = els
.iter()
.map(|e| apply_escapes(&e.text))
.find(|s| !set.contains(s));
if let Some(b) = bad {
vdiag(
out,
line,
format!("value not allowed at '{}': {}", c.path, b),
);
}
}
}
}
}
}
}
fn v_unknown(&self, def: &SchemaDef, out: &mut Vec<Diagnostic>) {
let cons = &def.cons;
let has_mounts = cons.iter().any(|c| c.inherits.is_some());
let mut legal: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut siblings: HashMap<String, Vec<String>> = HashMap::new();
let mut star_pats: Vec<&[Segment]> = Vec::new();
for c in cons {
if c.segs.iter().any(|s| s.star) {
star_pats.push(&c.segs);
}
let mut chain = String::new();
for s in &c.segs {
if s.star {
break; }
siblings
.entry(chain.clone())
.or_default()
.push(s.name.clone());
chain_push(&mut chain, &s.name);
legal.insert(chain.clone());
}
}
let mut stack: Vec<(usize, String, String)> = self.arena[ROOT]
.children
.iter()
.rev()
.map(|&c| (c, String::new(), String::new()))
.collect();
while let Some((n, pchain, pshown)) = stack.pop() {
let node = &self.arena[n];
let mut chain = pchain.clone();
chain_push(&mut chain, &node.name);
let shown = if pshown.is_empty() {
node.name.clone()
} else {
format!("{}.{}", pshown, node.name)
};
let known = legal.contains(&chain)
|| star_legal(&star_pats, &chain)
|| (has_mounts && chain_legal(cons, &def.frags, &chain));
if !known {
let hint = v_suggest(&siblings, &pchain, &node.name);
vdiag(out, node.line, format!("unknown field '{}'{}", shown, hint));
continue;
}
for &k in node.children.iter().rev() {
stack.push((k, chain.clone(), shown.clone()));
}
}
}
}
fn chain_push(chain: &mut String, name: &str) {
chain.push_str(&name.len().to_string());
chain.push(':');
chain.push_str(name);
}
fn chain_parts(chain: &str) -> Vec<&str> {
let mut parts = Vec::new();
let b = chain.as_bytes();
let mut i = 0;
while i < b.len() {
let mut n = 0usize;
while i < b.len() && b[i].is_ascii_digit() {
n = n * 10 + (b[i] - b'0') as usize;
i += 1;
}
if i >= b.len() || b[i] != b':' || i + 1 + n > b.len() {
break;
}
i += 1;
parts.push(&chain[i..i + n]);
i += n;
}
parts
}
fn star_legal(pats: &[&[Segment]], chain: &str) -> bool {
if pats.is_empty() {
return false;
}
let parts: Vec<&str> = chain_parts(chain);
pats.iter().any(|p| {
p.len() >= parts.len()
&& parts
.iter()
.enumerate()
.all(|(i, seg)| p[i].star || p[i].name == *seg)
})
}
fn chain_legal(cons: &[Constraint], frags: &HashMap<String, Vec<Constraint>>, chain: &str) -> bool {
let parts: Vec<&str> = chain_parts(chain);
chain_parts_legal(cons, frags, &parts)
}
fn chain_parts_legal(
cons: &[Constraint],
frags: &HashMap<String, Vec<Constraint>>,
parts: &[&str],
) -> bool {
for c in cons {
let n = c.segs.len();
let k = parts.len().min(n);
if (0..k).all(|i| c.segs[i].star || c.segs[i].name == parts[i]) {
if parts.len() <= n {
return true;
}
if let Some(fr) = &c.inherits
&& let Some(fcs) = frags.get(fr)
&& chain_parts_legal(fcs, frags, &parts[n..])
{
return true;
}
}
}
false
}
fn v_suggest(siblings: &HashMap<String, Vec<String>>, parent_chain: &str, name: &str) -> String {
let mut best: Option<(usize, &str)> = None;
if let Some(names) = siblings.get(parent_chain) {
for s in names {
let dist = edit_distance(name, s);
if dist <= 2 && best.is_none_or(|(bd, _)| dist < bd) {
best = Some((dist, s.as_str()));
}
}
}
match best {
Some((_, n)) => format!("; did you mean '{}'?", n),
None => String::new(),
}
}