mcap/modules/Capture_Text/module.jai

747 lines
27 KiB
Plaintext

// Capture Text: the text format for a capture's structured data, the JSON files of a .snagx in
// the style of the Ex4 text formats. See docs/capture_text.md for the format itself.
//
// A Capture_Document allocates everything through the allocator it is given, and frees nothing on
// its own: give it an arena or a pool, and drop that to drop the document.
CAPTURE_TEXT_VERSION :: 1;
CAPTURE_TEXT_EXTENSION :: "capture-text";
Capture_Value_Kind :: enum u8 { NULL; BOOLEAN; NUMBER; STRING; ARRAY; OBJECT; }
Capture_Value :: struct {
kind: Capture_Value_Kind;
boolean: bool;
number: float64;
text: string;
items: [..] Capture_Value; // ARRAY
fields: [..] Capture_Field; // OBJECT, in the order they were added
}
Capture_Field :: struct {
key: string;
value: Capture_Value;
}
// One [[file "NAME"]] section: one of the capture's JSON files.
Capture_File :: struct {
name: string;
root: Capture_Value; // always an OBJECT
}
Capture_Document :: struct {
allocator: Allocator;
files: [..] Capture_File;
comment: string; // written as # lines under @version; not read back
mistakes: s64; // lines skipped by the last parse
}
capture_document_init :: (doc: *Capture_Document, allocator: Allocator) {
doc.* = .{};
doc.allocator = allocator;
doc.files.allocator = allocator;
}
capture_add_file :: (doc: *Capture_Document, name: string) -> *Capture_File {
file := array_add(*doc.files);
file.name = copy_string(name,, doc.allocator);
file.root = capture_object(doc);
return file;
}
capture_find_file :: (doc: *Capture_Document, name: string) -> *Capture_File {
for * doc.files if it.name == name return it;
return null;
}
// ---------------------------------------------------------------- values
capture_null :: () -> Capture_Value {
return .{kind = .NULL};
}
capture_boolean :: (b: bool) -> Capture_Value {
return .{kind = .BOOLEAN, boolean = b};
}
capture_number :: (n: float64) -> Capture_Value {
return .{kind = .NUMBER, number = n};
}
capture_string :: (doc: *Capture_Document, text: string) -> Capture_Value {
return .{kind = .STRING, text = copy_string(text,, doc.allocator)};
}
capture_array :: (doc: *Capture_Document) -> Capture_Value {
result: Capture_Value;
result.kind = .ARRAY;
result.items.allocator = doc.allocator;
return result;
}
capture_object :: (doc: *Capture_Document) -> Capture_Value {
result: Capture_Value;
result.kind = .OBJECT;
result.fields.allocator = doc.allocator;
return result;
}
// Adds key to object without checking whether it is already there; the key is copied.
capture_object_add :: (doc: *Capture_Document, object: *Capture_Value, key: string, value: Capture_Value) -> *Capture_Value {
assert(object.kind == .OBJECT);
field := array_add(*object.fields);
field.key = copy_string(key,, doc.allocator);
field.value = value;
return *field.value;
}
capture_object_find :: (object: *Capture_Value, key: string) -> *Capture_Value {
if object.kind != .OBJECT return null;
for * object.fields if it.key == key return *it.value;
return null;
}
// Equal as JSON values: the order of an object's keys does not matter, the order of an array's
// elements does. Numbers are compared bit for bit, so -0 and 0 differ.
capture_values_equal :: (a: Capture_Value, b: Capture_Value) -> bool {
if a.kind != b.kind return false;
if #complete a.kind == {
case .NULL; return true;
case .BOOLEAN; return a.boolean == b.boolean;
case .NUMBER; return (cast(*u64) *a.number).* == (cast(*u64) *b.number).*;
case .STRING; return a.text == b.text;
case .ARRAY;
if a.items.count != b.items.count return false;
for a.items if !capture_values_equal(it, b.items[it_index]) return false;
return true;
case .OBJECT;
if a.fields.count != b.fields.count return false;
for a.fields {
other := capture_object_find(*b, it.key);
if !other || !capture_values_equal(it.value, other.*) return false;
}
return true;
}
return false;
}
// ---------------------------------------------------------------- reading
// Reads file_data into doc, which should be empty. A bad line, or a bad [[file]] with its lines, is
// logged and skipped, and counted in doc.mistakes. Returns false when the file was not read at all:
// no @version 1 before the first line that needs it.
capture_text_parse :: (doc: *Capture_Document, path: string, file_data: string) -> bool {
push_allocator(doc.allocator);
doc.mistakes = 0;
data := file_data;
if data.count >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF data = advance(data, 3);
state: Capture_Parse_State;
state.doc = doc;
state.path = path;
// Every line stands alone: a value never continues onto the next line.
offset: s64;
line_number := 0;
while offset < data.count {
line_number += 1;
line, colon, valid := capture_scan_line(data, *offset);
if !valid { capture_mistake(*state, line_number, "Quote is unterminated. Ignored."); continue; }
body := trim(line);
if !body.count continue;
if colon >= 0 colon -= body.data - line.data; // from the start of body
if body[0] == #char "@" {
key, value := capture_split_directive(body);
if key == "version" {
rest := value;
version, version_ok := parse_int(*rest, s64);
if !version_ok || trim(rest).count || version < 1 || version > CAPTURE_TEXT_VERSION {
log_error("% is version %, but this build reads version %. It was not read.", path, value, CAPTURE_TEXT_VERSION);
return false;
}
state.version_seen = true;
}
continue;
}
if !state.version_seen {
log_error("% does not start with @version %. It was not read.", path, CAPTURE_TEXT_VERSION);
return false;
}
if begins_with(body, "[[") {
capture_open_file(*state, line_number, body);
} else if body[0] == #char "[" {
capture_open_section(*state, line_number, body);
} else {
capture_key_line(*state, line_number, body, colon);
}
}
return true;
}
// ---------------------------------------------------------------- writing
CAPTURE_MAX_KEY_WIDTH :: 40; // keys longer than this do not push the other values further right
// The document as its file holds it. Temp.
capture_text_write :: (doc: *Capture_Document) -> string {
builder: String_Builder;
builder.allocator = temp;
print_to_builder(*builder, "@version %\n", CAPTURE_TEXT_VERSION);
if doc.comment.count {
for split(doc.comment, "\n",, temp) print_to_builder(*builder, "# %\n", it);
}
for file: doc.files {
print_to_builder(*builder, "\n[[file %]]\n", capture_quote(file.name));
lines: [..] Capture_Line;
lines.allocator = temp;
for file.root.fields {
if capture_is_section_array(it.value) continue;
capture_collect_lines(*lines, capture_key_text(it.key), it.value);
}
capture_write_block(*builder, lines);
for field: file.root.fields {
if !capture_is_section_array(field.value) continue;
header := capture_key_text(field.key);
for item: field.value.items {
print_to_builder(*builder, "\n[%]\n", header);
array_reset_keeping_memory(*lines);
for item.fields capture_collect_lines(*lines, capture_key_text(it.key), it.value);
capture_write_block(*builder, lines);
}
}
}
return builder_to_string(*builder,, temp);
}
// Always quoted, a quote written twice. Control characters cannot be quoted; see capture_string_parts.
capture_quote :: (text: string) -> string {
return tprint("\"%\"", replace(text, "\"", "\"\"",, temp));
}
// A key as one segment of a key path: bare when it can be, quoted otherwise. A key cannot hold
// control characters (see capture_key_is_writable). Temp.
capture_key_text :: (key: string) -> string {
if capture_is_bare_key(key) return key;
return capture_quote(key);
}
// Whether a key can be written at all: a quoted key is one part, so it cannot hold the characters
// below space, or DEL, that a string writes as 0xNN bytes.
capture_key_is_writable :: (key: string) -> bool {
for 0..key.count - 1 if key[it] < 0x20 || key[it] == 0x7F return false;
return true;
}
// The shortest text that reads back as exactly n. Temp.
capture_number_text :: (n: float64) -> string {
bits := (cast(*u64) *n).*;
if n == 0 return ifx bits >> 63 then "-0" else "0";
// Whole numbers that a float64 holds exactly are written as integers: 100, not 100.0 or 1e+02.
if n > -9007199254740992.0 && n < 9007199254740992.0 && n == cast(float64) cast,trunc(s64) n {
return tprint("%", cast,trunc(s64) n);
}
// Basic's SHORTEST mode keeps only six significant digits, so the fewest digits that read back as
// exactly n are found by trying: plain decimals for ordinary magnitudes, an exponent otherwise.
// 17 significant digits always suffice for a float64.
magnitude := ifx n < 0 then -n else n;
if magnitude >= 0.000001 && magnitude < 1000000000000000000000.0 {
for digits: 1..24 {
text := trim(tprint("%", formatFloat(n, trailing_width = digits, zero_removal = .YES)));
if capture_number_reads_back(text, bits) return text;
}
}
for digits: 0..16 {
text := trim(tprint("%", formatFloat(n, trailing_width = digits, mode = .SCIENTIFIC, zero_removal = .YES)));
if capture_number_reads_back(text, bits) return text;
}
return tprint("%", formatFloat(n, trailing_width = 16, mode = .SCIENTIFIC, zero_removal = .NO));
}
capture_number_reads_back :: (text: string, bits: u64) -> bool {
if capture_number_length(text) != text.count return false;
value, ok, rest := string_to_float64(text);
return ok && !rest.count && (cast(*u64) *value).* == bits;
}
#scope_module
// ---------------------------------------------------------------- reading, in detail
Capture_Parse_State :: struct {
doc: *Capture_Document;
path: string;
version_seen: bool;
file: *Capture_File; // the [[file]] being read; null before the first, or after a bad one
base: *Capture_Value; // the object key lines go into: the file's, or a [KEY] section's
skipping: bool; // after a bad header, until the next good one
}
MAX_NESTING :: 64;
capture_mistake :: (state: *Capture_Parse_State, line_number: s64, format: string, args: ..Any) {
log_error("%:%: %", state.path, line_number, tprint(format, ..args));
state.doc.mistakes += 1;
} @PrintLike
capture_open_file :: (state: *Capture_Parse_State, line_number: s64, body: string) {
state.file = null;
state.base = null;
state.skipping = true;
if !ends_with(body, "]]") { capture_mistake(state, line_number, "Malformed header (expected `[[file \"NAME\"]]`); its lines are skipped."); return; }
inner := trim(slice(body, 2, body.count - 4));
if !begins_with(inner, "file") || inner.count < 5 || !is_space(inner[4]) {
capture_mistake(state, line_number, "Unknown header % (expected `[[file \"NAME\"]]`); its lines are skipped.", body);
return;
}
name_text := trim(advance(inner, 4));
name, rest, ok := capture_read_quoted(name_text);
if !ok || trim(rest).count { capture_mistake(state, line_number, "The file name in % must be one quoted string; its lines are skipped.", body); return; }
if capture_find_file(state.doc, name) { capture_mistake(state, line_number, "File \"%\" was already read; this section is skipped.", name); return; }
state.file = capture_add_file(state.doc, name);
state.base = *state.file.root;
state.skipping = false;
}
capture_open_section :: (state: *Capture_Parse_State, line_number: s64, body: string) {
state.base = null;
if !state.file { capture_mistake(state, line_number, "% comes before any [[file]] section; its lines are skipped.", body); state.skipping = true; return; }
state.skipping = true;
if body[body.count - 1] != #char "]" { capture_mistake(state, line_number, "Malformed header (expected `[KEY]`); its lines are skipped."); return; }
segments, ok := capture_parse_path(trim(slice(body, 1, body.count - 2)));
if !ok { capture_mistake(state, line_number, "% is not a key path; its lines are skipped.", body); return; }
array, error := capture_walk(state.doc, *state.file.root, segments, .ARRAY);
if !array { capture_mistake(state, line_number, "%: %; its lines are skipped.", body, error); return; }
element := array_add(*array.items);
element.* = capture_object(state.doc);
state.base = element;
state.skipping = false;
}
// colon: where in body the first : outside quotes is, or -1.
capture_key_line :: (state: *Capture_Parse_State, line_number: s64, body: string, colon: s64) {
if state.skipping return;
if !state.base { capture_mistake(state, line_number, "Key line before any [[file]] section. Ignored."); return; }
if colon < 0 { capture_mistake(state, line_number, "Expected `KEY: VALUE`. Ignored."); return; }
key_text := trim(slice(body, 0, colon));
value_text := trim(advance(body, colon + 1));
segments, path_ok := capture_parse_path(key_text);
if !path_ok { capture_mistake(state, line_number, "% is not a key path. Ignored.", key_text); return; }
rest := value_text;
value, value_ok := capture_parse_value(state.doc, *rest, 0);
if !value_ok || trim(rest).count { capture_mistake(state, line_number, "% has no value it can read in `%`. Ignored.", key_text, value_text); return; }
slot, error := capture_walk(state.doc, state.base, segments, value.kind, value_must_be_new = true);
if !slot { capture_mistake(state, line_number, "%: %. Ignored.", key_text, error); return; }
slot.* = value;
}
Capture_Path_Segment :: struct {
key: string;
index := -1; // an array index when 0 or more
}
// Key path: segments separated by dots, each a bare name, a quoted key, or digits for an array index.
capture_parse_path :: (text: string) -> [..] Capture_Path_Segment, bool {
segments: [..] Capture_Path_Segment;
segments.allocator = temp;
rest := text;
while true {
segment: Capture_Path_Segment;
if rest.count && rest[0] == #char "\"" {
key, after, ok := capture_read_quoted(rest);
if !ok return segments, false;
segment.key = key;
rest = after;
} else {
i := 0;
while i < rest.count && capture_is_bare_char(rest[i]) i += 1;
if i == 0 return segments, false;
word := slice(rest, 0, i);
rest = advance(rest, i);
if capture_is_digits(word) {
if word.count > 9 return segments, false; // no array is that long
index_text := word;
segment.index = parse_int(*index_text, s64);
} else {
segment.key = word;
}
}
array_add(*segments, segment);
if !rest.count return segments, true;
if rest[0] != #char "." return segments, false;
rest = advance(rest, 1);
}
return segments, false;
}
// Follows segments down from node, making the objects and arrays the path needs, and returns the
// slot at the end: a new one for a key line, which must not have a value yet, or for a [KEY] header
// the array that is there or a new one. On failure returns null and why.
capture_walk :: (doc: *Capture_Document, node: *Capture_Value, segments: [] Capture_Path_Segment, final_kind: Capture_Value_Kind, value_must_be_new := false) -> *Capture_Value, string {
current := node;
for segment, segment_index: segments {
last := segment_index == segments.count - 1;
// What a new slot here has to be: what the next segment needs, or what is being put at the end.
kind := final_kind;
if !last kind = ifx segments[segment_index + 1].index >= 0 then Capture_Value_Kind.ARRAY else .OBJECT;
child: *Capture_Value;
is_new := false;
if segment.index >= 0 {
if current.kind != .ARRAY return null, tprint("index % of something that is not an array", segment.index);
if segment.index > current.items.count return null, tprint("index % skips past the next free index %", segment.index, current.items.count);
if segment.index == current.items.count {
child = array_add(*current.items);
is_new = true;
} else {
child = *current.items[segment.index];
}
} else {
if current.kind != .OBJECT return null, tprint("key \"%\" of something that is not an object", segment.key);
child = capture_object_find(current, segment.key);
if !child {
child = capture_object_add(doc, current, segment.key, .{});
is_new = true;
}
}
if is_new {
if kind == .ARRAY child.* = capture_array(doc);
else if kind == .OBJECT child.* = capture_object(doc);
} else if last && value_must_be_new {
return null, "already has a value";
} else if child.kind != kind {
return null, tprint("already holds % where % is needed", capture_kind_name(child.kind), capture_kind_name(kind));
}
current = child;
}
return current, "";
}
capture_kind_name :: (kind: Capture_Value_Kind) -> string {
if #complete kind == {
case .NULL; return "null";
case .BOOLEAN; return "a boolean";
case .NUMBER; return "a number";
case .STRING; return "a string";
case .ARRAY; return "an array";
case .OBJECT; return "an object";
}
return "";
}
capture_parse_value :: (doc: *Capture_Document, s: *string, depth: s64) -> Capture_Value, bool {
fail: Capture_Value;
s.* = trim_left(s.*);
if !s.count || depth > MAX_NESTING return fail, false;
c := s.*[0];
if c == #char "\"" {
first_part, rest, ok := capture_read_quoted(s.*);
if !ok return fail, false;
s.* = rest;
// Nearly every string is one part, copied straight into the document. Further parts are joined in a builder.
builder: String_Builder;
builder.allocator = temp;
parts := 1;
while true {
after := trim_left(s.*, " \t"); // spaces or tabs between parts
if after.count && after[0] == #char "\"" {
text, rest, ok := capture_read_quoted(after);
if !ok return fail, false;
if parts == 1 append(*builder, first_part);
append(*builder, text);
s.* = rest;
} else if after.count >= 4 && after[0] == #char "0" && after[1] == #char "x" {
high := capture_hex_digit(after[2]);
low := capture_hex_digit(after[3]);
if high < 0 || low < 0 return fail, false;
if after.count > 4 && !capture_ends_token(after[4]) return fail, false;
if parts == 1 append(*builder, first_part);
append(*builder, cast(u8) (high * 16 + low));
s.* = advance(after, 4);
} else {
break;
}
parts += 1;
}
if parts == 1 return capture_string(doc, first_part), true;
return capture_string(doc, builder_to_string(*builder,, temp)), true;
}
if c == #char "[" {
result := capture_array(doc);
s.* = trim_left(advance(s.*, 1));
if s.count && s.*[0] == #char "]" { s.* = advance(s.*, 1); return result, true; }
while true {
item, ok := capture_parse_value(doc, s, depth + 1);
if !ok return fail, false;
array_add(*result.items, item);
s.* = trim_left(s.*);
if !s.count return fail, false;
if s.*[0] == #char "]" { s.* = advance(s.*, 1); return result, true; }
if s.*[0] != #char "," return fail, false;
s.* = advance(s.*, 1);
}
}
if c == #char "{" {
rest := trim_left(advance(s.*, 1));
if !rest.count || rest[0] != #char "}" return fail, false; // only {} is written inline
s.* = advance(rest, 1);
return capture_object(doc), true;
}
if c == #char "-" || (c >= #char "0" && c <= #char "9") {
length := capture_number_length(s.*);
if length <= 0 || (length < s.count && !capture_ends_token(s.*[length])) return fail, false;
n, ok, remainder := string_to_float64(slice(s.*, 0, length));
if !ok || remainder.count return fail, false;
s.* = advance(s.*, length);
return capture_number(n), true;
}
for word: string.["null", "true", "false"] {
if !begins_with(s.*, word) continue;
if s.count > word.count && !capture_ends_token(s.*[word.count]) continue;
s.* = advance(s.*, word.count);
if word == "null" return capture_null(), true;
return capture_boolean(word == "true"), true;
}
return fail, false;
}
// The length of a JSON number at the start of s: -?digits(.digits)?([eE][+-]?digits)?. 0 when there is none.
capture_number_length :: (s: string) -> s64 {
i := 0;
if i < s.count && s[i] == #char "-" i += 1;
digits_start := i;
while i < s.count && s[i] >= #char "0" && s[i] <= #char "9" i += 1;
if i == digits_start return 0;
if i < s.count && s[i] == #char "." {
i += 1;
fraction_start := i;
while i < s.count && s[i] >= #char "0" && s[i] <= #char "9" i += 1;
if i == fraction_start return 0;
}
if i < s.count && (s[i] == #char "e" || s[i] == #char "E") {
i += 1;
if i < s.count && (s[i] == #char "+" || s[i] == #char "-") i += 1;
exponent_start := i;
while i < s.count && s[i] >= #char "0" && s[i] <= #char "9" i += 1;
if i == exponent_start return 0;
}
return i;
}
capture_ends_token :: (c: u8) -> bool {
return is_space(c) || c == #char "," || c == #char "]";
}
capture_hex_digit :: (c: u8) -> s64 {
if c >= #char "0" && c <= #char "9" return c - #char "0";
if c >= #char "A" && c <= #char "F" return c - #char "A" + 10;
if c >= #char "a" && c <= #char "f" return c - #char "a" + 10;
return -1;
}
// One quoted string ("" for a quote) at the start of s. Returns the text and what follows. The text is
// a slice of s when it holds no doubled quote, as nearly every string does; otherwise it is temp.
capture_read_quoted :: (s: string) -> text: string, rest: string, ok: bool {
if !s.count || s[0] != #char "\"" return "", s, false;
i := 1;
while i < s.count && s[i] != #char "\"" i += 1;
if i >= s.count return "", s, false;
if i + 1 >= s.count || s[i + 1] != #char "\"" return slice(s, 1, i - 1), advance(s, i + 1), true;
// A doubled quote: the text has to be put together.
builder: String_Builder;
builder.allocator = temp;
run_start := 1;
while i < s.count {
if s[i] == #char "\"" {
append(*builder, slice(s, run_start, i - run_start));
if i + 1 < s.count && s[i + 1] == #char "\"" {
append(*builder, #char "\"");
i += 2;
run_start = i;
continue;
}
return builder_to_string(*builder,, temp), advance(s, i + 1), true;
}
i += 1;
}
return "", s, false;
}
// One line, from offset, in a single pass: the line without its comment or line ending, and where in it
// the first : outside quotes is (-1 when there is none). "#" opens a comment only outside quotes,
// and only at the start of the line or after whitespace. valid is false when the line ends inside quotes.
capture_scan_line :: (data: string, offset: *s64) -> line: string, colon: s64, valid: bool {
start := offset.*;
i := start;
colon := -1;
end := -1; // where a comment starts
while i < data.count {
c := data[i];
if c == #char "\"" {
// Straight to the closing quote: strings are most of the bytes. A doubled quote closes and opens.
i += 1;
while i < data.count && data[i] != #char "\"" && data[i] != #char "\n" i += 1;
if i >= data.count || data[i] == #char "\n" {
while i < data.count && data[i] != #char "\n" i += 1;
offset.* = ifx i < data.count then i + 1 else data.count;
return "", -1, false;
}
} else if c == #char "\n" {
break;
} else if c == #char "#" && (i == start || is_space(data[i - 1])) {
end = i;
while i < data.count && data[i] != #char "\n" i += 1;
break;
} else if c == #char ":" && colon < 0 {
colon = i - start;
}
i += 1;
}
offset.* = ifx i < data.count then i + 1 else data.count;
if end < 0 {
end = i;
if end > start && data[end - 1] == #char "\r" end -= 1; // CRLF
}
return string.{end - start, data.data + start}, colon, true;
}
capture_split_directive :: (body: string) -> key: string, value: string {
rest := advance(body, 1); // skip the @
i := 0;
while i < rest.count && !is_space(rest[i]) i += 1;
return slice(rest, 0, i), trim(advance(rest, i));
}
capture_is_bare_char :: (c: u8) -> bool {
return (c >= #char "a" && c <= #char "z") || (c >= #char "A" && c <= #char "Z") || (c >= #char "0" && c <= #char "9") || c == #char "_" || c == #char "-";
}
capture_is_digits :: (s: string) -> bool {
if !s.count return false;
for 0..s.count - 1 if s[it] < #char "0" || s[it] > #char "9" return false;
return true;
}
capture_is_bare_key :: (key: string) -> bool {
if !key.count || capture_is_digits(key) return false;
for 0..key.count - 1 if !capture_is_bare_char(key[it]) return false;
return true;
}
// ---------------------------------------------------------------- writing, in detail
Capture_Line :: struct {
path: string;
value: Capture_Value;
}
// A root array of objects is written as [KEY] sections, one per object.
capture_is_section_array :: (value: Capture_Value) -> bool {
if value.kind != .ARRAY || !value.items.count return false;
for value.items if it.kind != .OBJECT return false;
return true;
}
// An array that holds an object with keys, at any depth, cannot be written inline.
capture_needs_paths :: (value: Capture_Value) -> bool {
if value.kind == .OBJECT return value.fields.count > 0;
if value.kind == .ARRAY {
for value.items if capture_needs_paths(it) return true;
}
return false;
}
capture_collect_lines :: (lines: *[..] Capture_Line, path: string, value: Capture_Value) {
if capture_needs_paths(value) {
if value.kind == .OBJECT {
for value.fields capture_collect_lines(lines, tprint("%.%", path, capture_key_text(it.key)), it.value);
} else {
for value.items capture_collect_lines(lines, tprint("%.%", path, it_index), it);
}
return;
}
array_add(lines, .{path, value});
}
// Key lines with their values lined up, each on one line however long.
capture_write_block :: (builder: *String_Builder, lines: [] Capture_Line) {
key_width := 0;
for lines if it.path.count + 1 <= CAPTURE_MAX_KEY_WIDTH key_width = max(key_width, it.path.count + 1);
for lines {
head := tprint("%:", it.path);
append(builder, head);
for head.count..max(head.count, key_width) append(builder, #char " ");
capture_append_value(builder, it.value);
append(builder, #char "\n");
}
}
capture_append_value :: (builder: *String_Builder, value: Capture_Value) {
if #complete value.kind == {
case .NULL; append(builder, "null");
case .BOOLEAN; append(builder, ifx value.boolean then "true" else "false");
case .NUMBER; append(builder, capture_number_text(value.number));
case .STRING; append(builder, capture_string_text(value.text));
case .OBJECT; append(builder, "{}"); // only an empty one reaches here
case .ARRAY;
append(builder, #char "[");
for value.items {
if it_index > 0 append(builder, ", ");
capture_append_value(builder, it);
}
append(builder, #char "]");
}
}
// A string on one line: quoted, with control characters and DEL, which quotes cannot hold, written
// as 0xNN parts between quoted ones. Temp.
capture_string_text :: (text: string) -> string {
if !text.count return "\"\"";
builder: String_Builder;
builder.allocator = temp;
i := 0;
while i < text.count {
if i > 0 append(*builder, #char " ");
c := text[i];
if c < 0x20 || c == 0x7F {
if i == 0 append(*builder, "\"\" "); // the first part is always quoted
print_to_builder(*builder, "0x%", formatInt(c, base = 16, minimum_digits = 2));
i += 1;
continue;
}
end := i;
while end < text.count && text[end] >= 0x20 && text[end] != 0x7F end += 1;
append(*builder, capture_quote(slice(text, i, end - i)));
i = end;
}
return builder_to_string(*builder,, temp);
}
#import "Basic";
#import "String";