// 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";