std/data/toon

Standard Library source code

Pure-Zuzu TOON codec.

Module

Name
std/data/toon
Area
Standard Library
Source
modules/std/data/toon.zzm
=encoding utf8

=head1 NAME

std/data/toon - Pure-Zuzu TOON codec.

=head1 SYNOPSIS

  from std/data/toon import TOON;

  let codec := new TOON( indent: 2 );
  let text := codec.encode({ answer: 42 });
  let data := codec.decode(text);

=head1 IMPLEMENTATION SUPPORT

This module is supported by zuzu.pl, zuzu-rust, and zuzu-js on Node and
Electron. It is partially supported by zuzu-js in the browser: in-memory
TOON parsing and serialization coverage passes, but fixture and load/dump
coverage is unsupported because browser filesystem capability is
unavailable.

=head1 DESCRIPTION

A pure ZuzuScript TOON codec with C<encode>, C<decode>, C<load>, and
C<dump>.

The implementation supports:

=over

=item * object and array roots

=item * inline primitive arrays and tabular/list arrays

=item * delimiter declarations (comma, tab, pipe)

=item * strict-mode validation for required colons

=back

Object decoding defaults to PairLists so key order is kept.

=head1 EXPORTS

=head2 Classes

=over

=item C<< TOON({ indent?: Number, strict?: Boolean, delimiter?: String, pairlists?: Boolean, expandPaths?, keyFolding?, flattenDepth? }) >>

Constructs a TOON codec. Returns: C<TOON>.

=over

=item C<< codec.decode(String text) >>

Parameters: C<text> is TOON text. Returns: value. Decodes TOON text into
ZuzuScript data.

=item C<< codec.decode_binarystring(BinaryString bytes) >>

Parameters: C<bytes> is UTF-8 TOON bytes. Returns: value. Decodes TOON
bytes into ZuzuScript data.

=item C<< codec.encode(value) >>

Parameters: C<value> is a TOON-encodable value. Returns: C<String>.
Encodes C<value> as TOON text.

=item C<< codec.encode_binarystring(value) >>

Parameters: C<value> is a TOON-encodable value. Returns:
C<BinaryString>. Encodes C<value> as UTF-8 TOON bytes.

=item C<< codec.load(Path path) >>

Parameters: C<path> is a C<std/io> C<Path>. Returns: value. Reads TOON
text from C<path> and decodes it.

=item C<< codec.dump(Path path, value) >>

Parameters: C<path> is a C<std/io> C<Path> and C<value> is a
TOON-encodable value. Returns: C<null>. Encodes C<value> and writes TOON
text to C<path>.

=back

=back

=head1 OPTIONS

=over

=item * C<indent> (default 2)

=item * C<strict> (default true)

=item * C<delimiter> (default comma)

=item * C<pairlists> (default true)

Decode objects as PairLists (preserves key order). Set false to decode
as Dict.

=item * C<expandPaths>, C<keyFolding>, C<flattenDepth>

Accepted for API compatibility.

=back

=head1 COPYRIGHT AND LICENCE

B<< std/data/toon >> is copyright Toby Inkster.

It is free software; you may redistribute it and/or modify it under
the terms of either the Artistic License 1.0 or the GNU General Public
License version 2.

=cut

from std/string import substr, index, replace;


function _trim ( String s ) {
	let a := 0;
	let b := length s;
	while ( a < b and ( substr( s, a, 1 ) ≡ " " or substr( s, a, 1 ) ≡ "\t" ) ) { a++; }
	while ( b > a and ( substr( s, b - 1, 1 ) ≡ " " or substr( s, b - 1, 1 ) ≡ "\t" ) ) { b--; }
	return substr( s, a, b - a );
}

function _split_lines ( String s ) {
	let out := [];
	let p := 0;
	let n := length s;
	while ( p <= n ) {
		let i := index( s, "\n", p );
		if ( i < 0 ) {
			out.push( substr( s, p, n - p ) );
			last;
		}
		out.push( substr( s, p, i - p ) );
		p := i + 1;
	}
	return out;
}

function _escape_string ( String s ) {
	let tmp := s;
	tmp := replace( tmp, /\\/, "\\\\", "g" );
	tmp := replace( tmp, /"/, "\\\"", "g" );
	tmp := replace( tmp, /\n/, "\\n", "g" );
	tmp := replace( tmp, /\r/, "\\r", "g" );
	tmp := replace( tmp, /\t/, "\\t", "g" );
	return tmp;
}

function _unescape_string ( String s, Boolean strict ) {
	let out := "";
	let i := 0;
	while ( i < length s ) {
		let ch := substr( s, i, 1 );
		if ( ch ≢ "\\" ) { out _= ch; i++; next; }
		i++;
		if ( i >= length s ) {
			die "Invalid escape" if strict;
			out _= "\\";
			last;
		}
		let e := substr( s, i, 1 );
		if ( e ≡ "n" ) { out _= "\n"; }
		else if ( e ≡ "r" ) { out _= "\r"; }
		else if ( e ≡ "t" ) { out _= "\t"; }
		else if ( e ≡ "\\" ) { out _= "\\"; }
		else if ( e ≡ "\"" ) { out _= "\""; }
		else {
			die "Invalid escape" if strict;
			out _= e;
		}
		i++;
	}
	return out;
}

function _is_identifier_segment ( String k ) {
	return k ~ /^[A-Za-z_][A-Za-z0-9_]*$/;
}

function _has_unquoted_colon ( String s ) {
	let i := 0;
	let in_q := false;
	let esc := false;
	while ( i < length s ) {
		let ch := substr( s, i, 1 );
		if ( in_q ) {
			if ( esc ) { esc := false; i++; next; }
			if ( ch ≡ "\\" ) { esc := true; i++; next; }
			if ( ch ≡ "\"" ) { in_q := false; i++; next; }
			i++;
			next;
		}
		if ( ch ≡ "\"" ) { in_q := true; i++; next; }
		if ( ch ≡ ":" ) { return i; }
		i++;
	}
	return -1;
}

function _split_quoted ( String s, String sep ) {
	let out := [];
	let cur := "";
	let i := 0;
	let in_q := false;
	let esc := false;
	while ( i < length s ) {
		let ch := substr( s, i, 1 );
		if ( in_q ) {
			cur _= ch;
			if ( esc ) { esc := false; i++; next; }
			if ( ch ≡ "\\" ) { esc := true; i++; next; }
			if ( ch ≡ "\"" ) { in_q := false; i++; next; }
			i++;
			next;
		}
		if ( ch ≡ "\"" ) { in_q := true; cur _= ch; i++; next; }
		if ( ch ≡ sep ) { out.push(cur); cur := ""; i++; next; }
		cur _= ch;
		i++;
	}
	out.push(cur);
	return out;
}

function _parse_scalar ( String tok, Boolean strict ) {
	let t := _trim(tok);
	if ( t ≡ "null" ) { return null; }
	if ( t ≡ "true" ) { return true; }
	if ( t ≡ "false" ) { return false; }
	if ( strict and ( t ~ /^"/ xor t ~ /"$/ ) ) { die "Unterminated string"; }
	if ( t ~ /^"/ and t ~ /"$/ ) {
		let inner := substr( t, 1, length t - 2 );
		return _unescape_string( inner, strict );
	}
	if ( t ~ /^-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?$/ and not ( t ~ /^-?0[0-9]+$/ ) ) {
		return t + 0;
	}
	return t;
}

function _encode_key ( String k ) {
	if ( k ~ /^[A-Za-z_][A-Za-z0-9_.]*$/ and not ( k ~ /^-|\s|:|\[|\]|\{|\}|,|\||\t/ ) and k ≢ "" ) {
		return k;
	}
	return "\"" _ _escape_string(k) _ "\"";
}

function _canonical_number ( Number v ) {
	let s := "" _ v;
	if ( s ≡ "-0" ) { return "0"; }
	if ( not ( s ~ /[eE]/ ) ) {
		if ( s ~ /\./ ) {
			let neg2 := v < 0;
			let av := neg2 ? -v : v;
			let ai := floor(av);
			let af := av - ai;
			if ( af > 0 and ai ≡ 0 and ai < 9007199254740992 ) {
				let digs := "";
				let x := af;
				let k := 0;
				while ( k < 17 and x > 0 ) {
					x := x * 10;
					let d := floor( x + 0.000000000001 );
					if ( d > 9 ) { d := 9; }
					digs _= "" _ d;
					x := x - d;
					k++;
				}
				digs := replace( digs, /0+$/, "", "" );
				if ( digs ≢ "" ) {
					let cand := ( "" _ ai ) _ "." _ digs;
					if ( ( cand + 0 ) ≡ av ) {
						s := neg2 ? "-" _ cand : cand;
					}
				}
			}
			s := replace( s, /0+$/, "", "" );
			s := replace( s, /\.$/, "", "" );
		}
		return s ≡ "-0" ? "0" : s;
	}
	let neg := false;
	if ( s ~ /^-/ ) {
		neg := true;
		s := substr( s, 1, length s - 1 );
	}
	let ep := index( s, "e" );
	if ( ep < 0 ) { ep := index( s, "E" ); }
	let mant := ep >= 0 ? substr( s, 0, ep ) : s;
	let exp := ep >= 0 ? substr( s, ep + 1, length s - ep - 1 ) + 0 : 0;
	let dot := index( mant, "." );
	let intp := dot >= 0 ? substr( mant, 0, dot ) : mant;
	let frac := dot >= 0 ? substr( mant, dot + 1, length mant - dot - 1 ) : "";
	let digits := intp _ frac;
	let pos := length intp + exp;
	let out := "";
	if ( pos <= 0 ) {
		out := "0.";
		let z := 0;
		while ( z < -pos ) { out _= "0"; z++; }
		out _= digits;
	}
	else if ( pos >= length digits ) {
		out := digits;
		let z2 := 0;
		while ( z2 < pos - length digits ) { out _= "0"; z2++; }
	}
	else {
		out := substr( digits, 0, pos ) _ "." _ substr( digits, pos, length digits - pos );
	}
	if ( out ~ /\./ ) {
		out := replace( out, /0+$/, "", "" );
		out := replace( out, /\.$/, "", "" );
	}
	if ( out ≡ "" ) { out := "0"; }
	if ( neg and out ≢ "0" ) { out := "-" _ out; }
	return out;
}

function _encode_scalar ( v, String delim, Boolean is_key ) {
	if ( is_key ) { return _encode_key( "" _ v ); }
	if ( v ≡ null ) { return "null"; }
	if ( v ≡ true ) { return "true"; }
	if ( v ≡ false ) { return "false"; }
	if ( v instanceof Number ) {
		return _canonical_number(v);
	}
	let s := "" _ v;
	if ( s ≡ "" or s ~ /^\s/ or s ~ /\s$/ or s ~ /^-($|\s)/ or s ~ /[\n\r\t\\"]/ or s ≡ "true" or s ≡ "false" or s ≡ "null" or s ~ /^-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?$/ or s ~ /^-?0[0-9]+$/ ) {
		return "\"" _ _escape_string(s) _ "\"";
	}
	if ( delim ≡ "," and ( s ~ /[:,\[\]{}]/ or s ~ /,/ ) ) {
		return "\"" _ _escape_string(s) _ "\"";
	}
	if ( delim ≡ "|" and s ~ /[|]/ ) {
		return "\"" _ _escape_string(s) _ "\"";
	}
	if ( delim ≡ "\t" and s ~ /\t/ ) {
		return "\"" _ _escape_string(s) _ "\"";
	}
	return s;
}

function _mk_obj ( Boolean pairlists ) {
	return pairlists ? new PairList() : new Dict();
}

function _obj_add ( obj, String k, v, Boolean pairlists ) {
	if ( pairlists ) { obj.add( k, v ); }
	else { obj.set( k, v ); }
}

function _obj_put ( obj, String k, v, Boolean pairlists ) {
	if ( pairlists ) { obj.remove(k); obj.add( k, v ); }
	else { obj.set( k, v ); }
}

function _obj_get ( obj, String key, fallback ) {
	if ( obj instanceof PairList ) { return obj.has(key) ? obj.get(key) : fallback; }
	if ( obj instanceof Dict ) { return obj.exists(key) ? obj.get(key) : fallback; }
	return fallback;
}

function _obj_has ( obj, String key ) {
	if ( obj instanceof PairList ) { return obj.has(key); }
	if ( obj instanceof Dict ) { return obj.exists(key); }
	return false;
}

function _parse_header ( String line, Boolean strict ) {
	let colon := _has_unquoted_colon(line);
	if ( colon < 0 ) { return null; }
	let lhs := _trim( substr( line, 0, colon ) );
	let rhs := _trim( substr( line, colon + 1, length line - colon - 1 ) );
	let i := length lhs - 1;
	while ( i >= 0 and substr( lhs, i, 1 ) ≢ "]" ) { i--; }
	if ( i < 0 ) { return null; }
	let rb := i;
	i--;
	while ( i >= 0 and substr( lhs, i, 1 ) ≢ "[" ) { i--; }
	if ( i < 0 ) { return null; }
	let lb := i;
	let key := _trim( substr( lhs, 0, lb ) );
	let inside := substr( lhs, lb + 1, rb - lb - 1 );
	let d := ",";
	if ( inside ~ /\t$/ ) { d := "\t"; inside := substr( inside, 0, length inside - 1 ); }
	else if ( inside ~ /\|$/ ) { d := "|"; inside := substr( inside, 0, length inside - 1 ); }
	inside := _trim(inside);
	if ( not ( inside ~ /^[0-9]+$/ ) ) { return null; }
	let len := inside + 0;
	let fields := null;
	let tail := _trim( substr( lhs, rb + 1, length lhs - rb - 1 ) );
	if ( tail ≢ "" ) {
		if ( not ( tail ~ /^\{/ and tail ~ /\}$/ ) ) { return null; }
		fields := substr( tail, 1, length tail - 2 );
	}
	if ( key ≡ "" ) { key := null; }
	let quoted := false;
	if ( key ≢ null and key ~ /^"/ and key ~ /"$/ ) {
		key := _parse_scalar( key, strict );
		quoted := true;
	}
	if ( key ≢ null and not quoted and ( key ~ /\[/ or key ~ /\]/ ) ) {
		return null;
	}
	return { key: key, len: len, delim: d, fields: fields, rhs: rhs, quotedKey: quoted };
}

function _expand_path_add ( root, String key, value, Boolean pairlists, Boolean strict, String mode, Boolean quoted_key ) {
	function is_map ( v ) { return v instanceof PairList or v instanceof Dict; }
	function is_conflict ( oldv, newv ) {
		return ( is_map(oldv) and not is_map(newv) ) or ( not is_map(oldv) and is_map(newv) );
	}

	if ( mode ≢ "safe" ) {
		_obj_add( root, key, value, pairlists );
		return;
	}
	if ( not quoted_key and key ~ /\./ ) {
		let segs := _split_quoted( key, "." );
		let ok := true;
		for ( let s in segs ) { if ( not _is_identifier_segment(s) ) { ok := false; last; } }
		if ( ok ) {
			let cur := root;
			let i := 0;
			while ( i < segs.length() - 1 ) {
				let seg := segs[i];
				let nxt := _obj_get( cur, seg, null );
				if ( nxt ≡ null ) {
					nxt := _mk_obj(pairlists);
					_obj_put( cur, seg, nxt, pairlists );
				}
				else if ( not( nxt instanceof PairList ) and not( nxt instanceof Dict ) ) {
					die "Path expansion conflict" if strict;
					nxt := _mk_obj(pairlists);
					_obj_put( cur, seg, nxt, pairlists );
				}
				cur := nxt;
				i++;
			}
			let leaf := segs[ segs.length() - 1 ];
			if ( _obj_has( cur, leaf ) ) {
				let oldv := _obj_get( cur, leaf, null );
				if ( is_conflict( oldv, value ) and strict ) { die "Path expansion conflict"; }
			}
			_obj_put( cur, leaf, value, pairlists );
			return;
		}
	}
	if ( _obj_has( root, key ) ) {
		let oldv2 := _obj_get( root, key, null );
		if ( is_conflict( oldv2, value ) and strict ) { die "Path expansion conflict"; }
	}
	_obj_put( root, key, value, pairlists );
}

function _decode_text ( String text, Boolean pairlists, Number indent, Boolean strict, String expandPaths ) {
	let lines := _split_lines( text ≡ null ? "" : replace( replace( text, /\r\n/, "\n", "g" ), /\r/, "\n", "g" ) );
	let n := lines.length();
	let i := 0;

	function _depth_of ( Number spaces ) {
		return strict ? spaces / indent : floor( spaces / indent );
	}

	function parse_object_block;

	function parse_array_from_header ( header, Number depth ) {
		let arr := [];
		let d := header.get("delim");
		if ( header.get("rhs") ≢ "" ) {
			for ( let p in _split_quoted( header.get("rhs"), d ) ) { arr.push( _parse_scalar( p, strict ) ); }
			if ( strict and arr.length() ≢ header.get("len") ) { die "Array length mismatch"; }
			return arr;
		}
		let child := depth + 1;
		if ( header.get("fields") ≢ null ) {
			let cols := [];
			for ( let c in _split_quoted( header.get("fields"), d ) ) {
				let t := _trim(c);
				cols.push( t ~ /^"/ and t ~ /"$/ ? _parse_scalar( t, strict ) : t );
			}
			while ( i < n ) {
				let raw := lines[i];
				if ( _trim(raw) ≡ "" ) { if ( strict ) { die "Blank line inside array"; } i++; next; }
				let sp := 0;
				while ( sp < length raw and substr( raw, sp, 1 ) ≡ " " ) { sp++; }
				if ( strict and sp mod indent ≢ 0 ) { die "Invalid indentation"; }
				let dd := _depth_of(sp);
				if ( dd < child ) { last; }
				if ( dd ≢ child ) { die "Invalid indentation" if strict; i++; next; }
				let vals := _split_quoted( _trim(raw), d );
				if ( strict and vals.length() ≢ cols.length() ) { die "Tabular row width mismatch"; }
				let obj := _mk_obj(pairlists);
				let k := 0;
				while ( k < cols.length() and k < vals.length() ) {
					_obj_add( obj, cols[k], _parse_scalar( vals[k], strict ), pairlists );
					k++;
				}
				arr.push(obj);
				i++;
			}
			if ( strict and arr.length() ≢ header.get("len") ) { die "Array length mismatch"; }
			return arr;
		}
		while ( i < n ) {
			let raw := lines[i];
			if ( _trim(raw) ≡ "" ) {
				if ( strict ) {
					let jblank := i + 1;
					while ( jblank < n and _trim( lines[jblank] ) ≡ "" ) { jblank++; }
					if ( jblank < n ) {
						let rblank := lines[jblank];
						let sblank := 0;
						while ( sblank < length rblank and substr( rblank, sblank, 1 ) ≡ " " ) { sblank++; }
						let dblank := _depth_of(sblank);
						if ( dblank >= child ) { die "Blank line inside array"; }
					}
				}
				i++;
				next;
			}
			let sp := 0;
			while ( sp < length raw and substr( raw, sp, 1 ) ≡ " " ) { sp++; }
			if ( strict and sp mod indent ≢ 0 ) { die "Invalid indentation"; }
			let dd := _depth_of(sp);
			if ( dd < child ) { last; }
			if ( dd ≢ child ) { die "Invalid indentation" if strict; i++; next; }
			let t := _trim(raw);
			if ( not ( t ~ /^-/ ) ) { last; }
			let payload := _trim( replace( t, /^-\s?/, "" ) );
			i++;
			if ( payload ≡ "" ) { arr.push( _mk_obj(pairlists) ); next; }
			let ih := _parse_header( payload, strict );
			if ( ih ≢ null and ih.get("key") ≡ null ) {
				arr.push( parse_array_from_header( ih, child ) );
				next;
			}
			if ( ih ≢ null and ih.get("key") ≢ null ) {
				let obj := _mk_obj(pairlists);
				_obj_add(
					obj,
					ih.get("key"),
					parse_array_from_header( ih, child + 1 ),
					pairlists,
				);
				while ( i < n ) {
					let r2 := lines[i];
					if ( _trim(r2) ≡ "" ) { if ( strict ) { die "Blank line inside array"; } i++; next; }
					let s2 := 0;
					while ( s2 < length r2 and substr( r2, s2, 1 ) ≡ " " ) { s2++; }
					if ( strict and s2 mod indent ≢ 0 ) { die "Invalid indentation"; }
					let d2 := _depth_of(s2);
					if ( d2 < child + 1 ) { last; }
					if ( d2 ≢ child + 1 ) { i++; next; }
					let l2 := _trim(r2);
					let ah2 := _parse_header( l2, strict );
					if ( ah2 ≢ null and ah2.get("key") ≢ null ) {
						i++;
						_obj_add(
							obj,
							ah2.get("key"),
							parse_array_from_header( ah2, child + 1 ),
							pairlists,
						);
						next;
					}
					let c2 := _has_unquoted_colon(l2);
					if ( c2 < 0 ) { last; }
					let k2 := _trim( substr( l2, 0, c2 ) );
					let v2 := _trim( substr( l2, c2 + 1, length l2 - c2 - 1 ) );
					if ( k2 ~ /^"/ and k2 ~ /"$/ ) { k2 := _parse_scalar( k2, strict ); }
					i++;
					if ( v2 ≡ "" ) {
						let j2 := i;
						while ( j2 < n and _trim(lines[j2]) ≡ "" ) { j2++; }
						if ( j2 < n ) {
							let r3 := lines[j2];
							let s3 := 0;
							while ( s3 < length r3 and substr( r3, s3, 1 ) ≡ " " ) { s3++; }
							let d3 := _depth_of(s3);
							if ( d3 > child + 1 ) {
								i := j2;
								_obj_add( obj, k2, parse_object_block( child + 2 ), pairlists );
								next;
							}
						}
						_obj_add( obj, k2, _mk_obj(pairlists), pairlists );
						next;
					}
					_obj_add( obj, k2, _parse_scalar( v2, strict ), pairlists );
				}
				arr.push(obj);
				next;
			}
			let c := _has_unquoted_colon(payload);
			if ( c >= 0 ) {
				let obj := _mk_obj(pairlists);
				let kk := _trim( substr( payload, 0, c ) );
				let vv := _trim( substr( payload, c + 1, length payload - c - 1 ) );
				if ( kk ~ /^"/ and kk ~ /"$/ ) { kk := _parse_scalar( kk, strict ); }
				if ( vv ≡ "" ) {
					let j0 := i;
					while ( j0 < n and _trim(lines[j0]) ≡ "" ) { j0++; }
					if ( j0 < n ) {
						let r0 := lines[j0];
						let s0 := 0;
						while ( s0 < length r0 and substr( r0, s0, 1 ) ≡ " " ) { s0++; }
						let d0 := _depth_of(s0);
						if ( d0 > child ) {
							i := j0;
							_obj_add( obj, kk, parse_object_block( child + 2 ), pairlists );
						}
						else {
							_obj_add( obj, kk, _mk_obj(pairlists), pairlists );
						}
					}
					else {
						_obj_add( obj, kk, _mk_obj(pairlists), pairlists );
					}
				}
				else {
					_obj_add( obj, kk, _parse_scalar( vv, strict ), pairlists );
				}
				while ( i < n ) {
					let r2 := lines[i];
					if ( _trim(r2) ≡ "" ) { if ( strict ) { die "Blank line inside array"; } i++; next; }
					let s2 := 0;
					while ( s2 < length r2 and substr( r2, s2, 1 ) ≡ " " ) { s2++; }
					if ( strict and s2 mod indent ≢ 0 ) { die "Invalid indentation"; }
					let d2 := _depth_of(s2);
					if ( d2 < child + 1 ) { last; }
					if ( d2 ≢ child + 1 ) { i++; next; }
					let l2 := _trim(r2);
					let ah2 := _parse_header( l2, strict );
					if ( ah2 ≢ null and ah2.get("key") ≢ null ) {
						i++;
						_obj_add(
							obj,
							ah2.get("key"),
							parse_array_from_header( ah2, child + 1 ),
							pairlists,
						);
						next;
					}
					let c2 := _has_unquoted_colon(l2);
					if ( c2 < 0 ) { last; }
					let k2 := _trim( substr( l2, 0, c2 ) );
					let v2 := _trim( substr( l2, c2 + 1, length l2 - c2 - 1 ) );
					if ( k2 ~ /^"/ and k2 ~ /"$/ ) { k2 := _parse_scalar( k2, strict ); }
					i++;
					if ( v2 ≡ "" ) {
						let j2 := i;
						while ( j2 < n and _trim(lines[j2]) ≡ "" ) { j2++; }
						if ( j2 < n ) {
							let r3 := lines[j2];
							let s3 := 0;
							while ( s3 < length r3 and substr( r3, s3, 1 ) ≡ " " ) { s3++; }
							let d3 := _depth_of(s3);
							if ( d3 > child + 1 ) {
								i := j2;
								_obj_add( obj, k2, parse_object_block( child + 2 ), pairlists );
								next;
							}
						}
						_obj_add( obj, k2, _mk_obj(pairlists), pairlists );
						next;
					}
					_obj_add( obj, k2, _parse_scalar( v2, strict ), pairlists );
				}
				arr.push(obj);
				next;
			}
			arr.push( _parse_scalar( payload, strict ) );
		}
		if ( strict and arr.length() ≢ header.get("len") ) { die "Array length mismatch"; }
		return arr;
	}

	function parse_object_block ( Number depth ) {
		let obj := _mk_obj(pairlists);
		while ( i < n ) {
			let raw := lines[i];
			if ( _trim(raw) ≡ "" ) { i++; next; }
			let sp := 0;
			while ( sp < length raw and substr( raw, sp, 1 ) ≡ " " ) { sp++; }
			if ( strict and sp < length raw and substr( raw, sp, 1 ) ≡ "\t" ) { die "Tab indentation not allowed"; }
			if ( strict and sp mod indent ≢ 0 ) { die "Invalid indentation"; }
			let d := _depth_of(sp);
			if ( d < depth ) { last; }
			if ( d > depth ) { die "Unexpected indentation" if strict; i++; next; }
			let line := _trim(raw);
			let arr_h := _parse_header( line, strict );
			if ( arr_h ≢ null and arr_h.get("key") ≢ null ) {
				i++;
				let arr := parse_array_from_header( arr_h, depth );
				_expand_path_add( obj, arr_h.get("key"), arr, pairlists, strict, expandPaths, arr_h.get("quotedKey") );
				next;
			}
			let c := _has_unquoted_colon(line);
			if ( c < 0 ) {
				if ( strict ) { die "Missing colon in key-value context"; }
				i++;
				next;
			}
			let kraw := _trim( substr( line, 0, c ) );
			let vraw := _trim( substr( line, c + 1, length line - c - 1 ) );
			let k := kraw;
			let k_quoted := false;
			if ( kraw ~ /^"/ and kraw ~ /"$/ ) { k := _parse_scalar( kraw, strict ); k_quoted := true; }
			i++;
			if ( vraw ≡ "" ) {
				let v := _mk_obj(pairlists);
				let j := i;
				while ( j < n and _trim(lines[j]) ≡ "" ) { j++; }
				if ( j < n ) {
					let r2 := lines[j];
					let s2 := 0;
					while ( s2 < length r2 and substr( r2, s2, 1 ) ≡ " " ) { s2++; }
					if ( strict and s2 mod indent ≢ 0 ) { die "Invalid indentation"; }
					let d2 := _depth_of(s2);
					if ( d2 > depth ) {
						i := j;
						v := parse_object_block( depth + 1 );
					}
				}
				_expand_path_add( obj, k, v, pairlists, strict, expandPaths, k_quoted );
			}
			else {
				_expand_path_add( obj, k, _parse_scalar( vraw, strict ), pairlists, strict, expandPaths, k_quoted );
			}
		}
		return obj;
	}

	while ( i < n and _trim(lines[i]) ≡ "" ) { i++; }
	if ( i >= n ) { return _mk_obj(pairlists); }
	let first := _trim( lines[i] );
	let rh := _parse_header( first, strict );
	if ( rh ≢ null and rh.get("key") ≡ null ) {
		i++;
		return parse_array_from_header( rh, 0 );
	}
	let c0 := _has_unquoted_colon(first);
	if ( c0 < 0 ) {
		let v0 := _parse_scalar( first, strict );
		i++;
		while ( i < n and _trim(lines[i]) ≡ "" ) { i++; }
		if ( strict and i < n ) { die "Two root values"; }
		return v0;
	}
	return parse_object_block(0);
}

function _pairs ( obj ) {
	let out := [];
	if ( obj instanceof PairList ) {
		for ( let p in obj.to_Array() ) { out.push( p{pair} ); }
		return out;
	}
	for ( let k in obj.keys() ) { out.push( [ k, obj.get(k) ] ); }
	return out;
}

function _is_obj ( v ) {
	return v instanceof PairList or v instanceof Dict;
}

function _join ( Array items, String sep ) {
	let out := "";
	let i := 0;
	while ( i < items.length() ) {
		if ( i > 0 ) { out _= sep; }
		out _= items[i];
		i++;
	}
	return out;
}

function _array_uniform_obj ( Array arr ) {
	if ( arr.length() ≡ 0 ) { return false; }
	let first := arr[0];
	if ( not _is_obj(first) ) { return false; }
	let cols := [];
	for ( let p in _pairs(first) ) {
		if ( _is_obj( p[1] ) or p[1] instanceof Array ) { return false; }
		cols.push( p[0] );
	}
	let i := 1;
	while ( i < arr.length() ) {
		if ( not _is_obj(arr[i]) ) { return false; }
		let pp := _pairs( arr[i] );
		if ( pp.length() ≢ cols.length() ) { return false; }
		let j := 0;
		while ( j < cols.length() ) {
			if ( not _obj_has( arr[i], cols[j] ) ) { return false; }
			let vv := _obj_get( arr[i], cols[j], null );
			if ( _is_obj(vv) or vv instanceof Array ) { return false; }
			j++;
		}
		i++;
	}
	return cols;
}

function _fold_chain_safe ( String key, value, parent_keys, Number flattenDepth, root_keys, String prefix ) {
	if ( not _is_identifier_segment(key) ) { return [ key, value, 1 ]; }
	if ( flattenDepth <= 1 ) { return [ key, value, 1 ]; }
	let k := key;
	let v := value;
	let segments := 1;
	while ( segments < flattenDepth and _is_obj(v) ) {
		let pp := _pairs(v);
		if ( pp.length() ≢ 1 ) { last; }
		let nk := pp[0][0];
		if ( not _is_identifier_segment(nk) ) { last; }
		let candidate := k _ "." _ nk;
		let collide := false;
		for ( let pk in parent_keys.keys() ) {
			if ( pk ≡ candidate and candidate ≢ key ) { collide := true; last; }
			if ( index( pk, candidate _ "." ) ≡ 0 ) { collide := true; last; }
		}
		if ( not collide and root_keys ≢ null ) {
			let full_candidate := prefix ≡ "" ? candidate : prefix _ "." _ candidate;
			for ( let rk in root_keys.keys() ) {
				if ( rk ≡ full_candidate ) { collide := true; last; }
				if ( index( rk, full_candidate _ "." ) ≡ 0 ) { collide := true; last; }
			}
		}
		if ( collide ) { last; }
		k := candidate;
		v := pp[0][1];
		segments++;
	}
	return [ k, v, segments ];
}

function _encode_array ( key, Array arr, Number depth, Number indent, String delim ) {
	let out := [];
	let ktxt := key ≡ null ? "" : _encode_scalar( key, delim, true );
	let dtag := delim ≡ "," ? "" : delim;
	let all_prim := true;
	for ( let v in arr ) {
		if ( _is_obj(v) or v instanceof Array ) { all_prim := false; last; }
	}
	if ( all_prim ) {
		let vals := [];
		for ( let v in arr ) { vals.push( _encode_scalar( v, delim, false ) ); }
		out.push( ktxt _ "[" _ arr.length() _ dtag _ "]:" _ ( vals.length() > 0 ? " " _ _join( vals, delim ) : "" ) );
		return out;
	}
	let cols := _array_uniform_obj(arr);
	if ( cols ≢ false ) {
		let ch := [];
		for ( let c in cols ) { ch.push( _encode_scalar( c, delim, true ) ); }
		out.push( ktxt _ "[" _ arr.length() _ dtag _ "]{" _ _join( ch, delim ) _ "}:" );
		let pad := "";
		let i := 0;
		while ( i < indent ) { pad _= " "; i++; }
		for ( let row in arr ) {
			let vals := [];
			for ( let c in cols ) { vals.push( _encode_scalar( _obj_get( row, c, null ), delim, false ) ); }
			out.push( pad _ _join( vals, delim ) );
		}
		return out;
	}
	out.push( ktxt _ "[" _ arr.length() _ dtag _ "]:" );
	let pad := "";
	let i2 := 0;
	while ( i2 < indent ) { pad _= " "; i2++; }
	for ( let v in arr ) {
		if ( v instanceof Array ) {
			let lnidx := 0;
			for ( let ln in _encode_array( null, v, depth + 1, indent, delim ) ) {
				out.push( pad _ ( lnidx ≡ 0 ? "- " : "" ) _ ln );
				lnidx++;
			}
			next;
		}
		if ( _is_obj(v) ) {
			let pp := _pairs(v);
			if ( pp.length() ≡ 0 ) { out.push( pad _ "-" ); next; }
			let first := pp[0];
			let rest := [];
			let j := 1;
			while ( j < pp.length() ) { rest.push( pp[j] ); j++; }
				if ( _is_obj(first[1]) ) {
					out.push( pad _ "- " _ _encode_scalar( first[0], delim, true ) _ ":" );
					for ( let p2 in _pairs( first[1] ) ) {
						out.push( pad _ "  " _ _encode_scalar( p2[0], delim, true ) _ ": " _ _encode_scalar( p2[1], delim, false ) );
					}
				}
			else if ( first[1] instanceof Array ) {
				let lnidx2 := 0;
				for ( let ln in _encode_array( first[0], first[1], depth + 1, indent, delim ) ) {
					out.push( pad _ ( lnidx2 ≡ 0 ? "- " : "  " ) _ ln );
					lnidx2++;
				}
			}
			else {
				out.push( pad _ "- " _ _encode_scalar( first[0], delim, true ) _ ": " _ _encode_scalar( first[1], delim, false ) );
			}
			for ( let rr in rest ) {
					if ( _is_obj(rr[1]) ) {
						out.push( pad _ "  " _ _encode_scalar( rr[0], delim, true ) _ ":" );
						for ( let p3 in _pairs( rr[1] ) ) {
							out.push( pad _ "    " _ _encode_scalar( p3[0], delim, true ) _ ": " _ _encode_scalar( p3[1], delim, false ) );
						}
					}
				else if ( rr[1] instanceof Array ) {
					for ( let ln2 in _encode_array( rr[0], rr[1], depth + 2, indent, delim ) ) { out.push( pad _ "  " _ ln2 ); }
				}
				else {
					out.push( pad _ "  " _ _encode_scalar( rr[0], delim, true ) _ ": " _ _encode_scalar( rr[1], delim, false ) );
				}
			}
			next;
		}
		out.push( pad _ "- " _ _encode_scalar( v, delim, false ) );
	}
	return out;
}

function _encode_obj ( obj, Number depth, Number indent, String delim, String keyFolding, Number flattenDepth, root_keys, String prefix ) {
	let lines := [];
	let pad := "";
	let i := 0;
	while ( i < depth * indent ) { pad _= " "; i++; }
	let parent_keys := new Dict();
	for ( let pair in _pairs(obj) ) { parent_keys.set( pair[0], true ); }
	for ( let pair in _pairs(obj) ) {
		let k := pair[0];
		let v := pair[1];
		let used_segments := 1;
		if ( keyFolding ≡ "safe" ) {
			let fv := _fold_chain_safe( k, v, parent_keys, flattenDepth, root_keys, prefix );
			k := fv[0];
			v := fv[1];
			used_segments := fv[2];
		}
		if ( _is_obj(v) ) {
			if ( _pairs(v).length() ≡ 0 ) { lines.push( pad _ _encode_scalar( k, delim, true ) _ ":" ); }
			else {
				lines.push( pad _ _encode_scalar( k, delim, true ) _ ":" );
				let rem_depth := flattenDepth - ( used_segments - 1 );
				let child_prefix := prefix ≡ "" ? k : prefix _ "." _ k;
				for ( let sub in _encode_obj( v, depth + 1, indent, delim, keyFolding, rem_depth, root_keys, child_prefix ) ) { lines.push(sub); }
			}
			next;
		}
		if ( v instanceof Array ) {
			for ( let l in _encode_array( k, v, depth, indent, delim ) ) { lines.push( pad _ l ); }
			next;
		}
		lines.push( pad _ _encode_scalar( k, delim, true ) _ ": " _ _encode_scalar( v, delim, false ) );
	}
	return lines;
}

class TOON {
	let Number indent := 2;
	let Boolean strict := true;
	let String delimiter := ",";
	let Boolean pairlists := true;
	let String expandPaths := "off";
	let String keyFolding := "off";
	let flattenDepth := 999999;

	method decode ( String text ) {
		return _decode_text( text, pairlists, indent, strict, expandPaths );
	}

	method decode_binarystring ( BinaryString raw ) {
		return self.decode( to_string(raw) );
	}

	method encode ( value ) {
		if ( value instanceof Array ) {
			return _join( _encode_array( null, value, 0, indent, delimiter ), "\n" );
		}
		if ( _is_obj(value) ) {
			let root_keys := new Dict();
			for ( let pair in _pairs(value) ) { root_keys.set( pair[0], true ); }
			return _join( _encode_obj( value, 0, indent, delimiter, keyFolding, flattenDepth, root_keys, "" ), "\n" );
		}
		return _encode_scalar( value, delimiter, false );
	}

	method encode_binarystring ( value ) {
		return to_binary( self.encode(value) );
	}

	method load ( path ) {
		from std/io import Path;
		die "TOON.load is denied by runtime policy" if __system__{deny_fs};
		die "TOON.load expects a std/io Path object" if not( path instanceof Path );
		return self.decode_binarystring( path.slurp() );
	}

	method dump ( path, value ) {
		from std/io import Path;
		die "TOON.dump is denied by runtime policy" if __system__{deny_fs};
		die "TOON.dump expects a std/io Path object" if not( path instanceof Path );
		path.spew( self.encode_binarystring(value) );
		return path;
	}
}