=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;
}
}
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