diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index 8736bd7809..5dde6a6606 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -58,6 +58,7 @@ pub const BunObject = struct { pub const SHA512 = toJSGetter(Crypto.SHA512.getter); pub const SHA512_256 = toJSGetter(Crypto.SHA512_256.getter); pub const TOML = toJSGetter(Bun.getTOMLObject); + pub const YAML = toJSGetter(Bun.getYAMLObject); pub const Transpiler = toJSGetter(Bun.getTranspilerConstructor); pub const argv = toJSGetter(Bun.getArgv); pub const cwd = toJSGetter(Bun.getCWD); @@ -117,6 +118,7 @@ pub const BunObject = struct { @export(BunObject.SHA512_256, .{ .name = getterName("SHA512_256") }); @export(BunObject.TOML, .{ .name = getterName("TOML") }); + @export(BunObject.YAML, .{ .name = getterName("YAML") }); @export(BunObject.Glob, .{ .name = getterName("Glob") }); @export(BunObject.Transpiler, .{ .name = getterName("Transpiler") }); @export(BunObject.argv, .{ .name = getterName("argv") }); @@ -3394,6 +3396,10 @@ pub fn getTOMLObject(globalThis: *JSC.JSGlobalObject, _: *JSC.JSObject) JSC.JSVa return TOMLObject.create(globalThis); } +pub fn getYAMLObject(globalThis: *JSC.JSGlobalObject, _: *JSC.JSObject) JSC.JSValue { + return YAMLObject.create(globalThis); +} + pub fn getGlobConstructor(globalThis: *JSC.JSGlobalObject, _: *JSC.JSObject) JSC.JSValue { return JSC.API.Glob.getConstructor(globalThis); } @@ -3500,61 +3506,73 @@ const UnsafeObject = struct { } }; -const TOMLObject = struct { - const TOMLParser = @import("../../toml/toml_parser.zig").TOML; +fn TOMLLikeParser(comptime Parser: anytype, comptime default_name: []const u8) type { + return struct { + const ThisParser = Parser; + const name = default_name; - pub fn create(globalThis: *JSC.JSGlobalObject) JSC.JSValue { - const object = JSValue.createEmptyObject(globalThis, 1); - object.put( - globalThis, - ZigString.static("parse"), - JSC.createCallback( + pub fn create(globalThis: *JSC.JSGlobalObject) JSC.JSValue { + const object = JSValue.createEmptyObject(globalThis, 1); + object.put( globalThis, ZigString.static("parse"), - 1, - parse, - ), - ); + JSC.createCallback( + globalThis, + ZigString.static("parse"), + 1, + parse, + ), + ); - return object; - } - - pub fn parse( - globalThis: *JSC.JSGlobalObject, - callframe: *JSC.CallFrame, - ) bun.JSError!JSC.JSValue { - var arena = bun.ArenaAllocator.init(globalThis.allocator()); - const allocator = arena.allocator(); - defer arena.deinit(); - var log = logger.Log.init(default_allocator); - const arguments = callframe.arguments_old(1).slice(); - if (arguments.len == 0 or arguments[0].isEmptyOrUndefinedOrNull()) { - return globalThis.throwInvalidArguments("Expected a string to parse", .{}); + return object; } - var input_slice = arguments[0].toSlice(globalThis, bun.default_allocator); - defer input_slice.deinit(); - var source = logger.Source.initPathString("input.toml", input_slice.slice()); - const parse_result = TOMLParser.parse(&source, &log, allocator, false) catch { - return globalThis.throwValue(log.toJS(globalThis, default_allocator, "Failed to parse toml")); - }; + pub fn parse( + globalThis: *JSC.JSGlobalObject, + callframe: *JSC.CallFrame, + ) bun.JSError!JSC.JSValue { + var arena = bun.ArenaAllocator.init(globalThis.allocator()); + const allocator = arena.allocator(); + defer arena.deinit(); + var log = logger.Log.init(default_allocator); + defer log.deinit(); + const arguments = callframe.arguments_old(1).slice(); + if (arguments.len == 0 or arguments[0].isEmptyOrUndefinedOrNull()) { + return globalThis.throwInvalidArguments("Expected a string to parse", .{}); + } - // for now... - const buffer_writer = js_printer.BufferWriter.init(allocator) catch { - return globalThis.throwValue(log.toJS(globalThis, default_allocator, "Failed to print toml")); - }; - var writer = js_printer.BufferPrinter.init(buffer_writer); - _ = js_printer.printJSON(*js_printer.BufferPrinter, &writer, parse_result, &source, .{}) catch { - return globalThis.throwValue(log.toJS(globalThis, default_allocator, "Failed to print toml")); - }; + var input_slice: JSC.Node.StringOrBuffer = JSC.Node.StringOrBuffer.fromJS(globalThis, bun.default_allocator, arguments[0]) orelse { + if (!globalThis.hasException()) { + return globalThis.throwInvalidArguments("Expected a string to parse", .{}); + } + return error.JSError; + }; + defer input_slice.deinit(); + var source = logger.Source.initPathString(name, input_slice.slice()); + const parse_result = ThisParser.parse(&source, &log, allocator, false) catch { + return globalThis.throwValue(log.toJS(globalThis, default_allocator, "Failed to parse toml")); + }; - const slice = writer.ctx.buffer.slice(); - var out = bun.String.fromUTF8(slice); - defer out.deref(); + // for now... + const buffer_writer = js_printer.BufferWriter.init(allocator) catch { + return globalThis.throwValue(log.toJS(globalThis, default_allocator, "Failed to print toml")); + }; + var writer = js_printer.BufferPrinter.init(buffer_writer); + _ = js_printer.printJSON(*js_printer.BufferPrinter, &writer, parse_result, &source, .{}) catch { + return globalThis.throwValue(log.toJS(globalThis, default_allocator, "Failed to print toml")); + }; - return out.toJSByParseJSON(globalThis); - } -}; + const slice = writer.ctx.buffer.slice(); + var out = bun.String.fromUTF8(slice); + defer out.deref(); + + return out.toJSByParseJSON(globalThis); + } + }; +} + +pub const TOMLObject = TOMLLikeParser(@import("../../toml/toml_parser.zig").TOML, "input.toml"); +pub const YAMLObject = TOMLLikeParser(@import("../../yaml/yaml_parser.zig").YAML, "input.yaml"); const Debugger = JSC.Debugger; diff --git a/src/bun.js/bindings/BunObject+exports.h b/src/bun.js/bindings/BunObject+exports.h index b638d6eb26..8f59955d08 100644 --- a/src/bun.js/bindings/BunObject+exports.h +++ b/src/bun.js/bindings/BunObject+exports.h @@ -17,20 +17,21 @@ macro(SHA512_256) \ macro(TOML) \ macro(Transpiler) \ + macro(YAML) \ macro(argv) \ macro(assetPrefix) \ macro(cwd) \ + macro(embeddedFiles) \ macro(enableANSIColors) \ macro(hash) \ macro(inspect) \ macro(main) \ macro(origin) \ + macro(semver) \ macro(stderr) \ macro(stdin) \ macro(stdout) \ macro(unsafe) \ - macro(semver) \ - macro(embeddedFiles) \ // --- Callbacks --- #define FOR_EACH_CALLBACK(macro) \ diff --git a/src/bun.js/bindings/BunObject.cpp b/src/bun.js/bindings/BunObject.cpp index 9693629256..39f15f1a0d 100644 --- a/src/bun.js/bindings/BunObject.cpp +++ b/src/bun.js/bindings/BunObject.cpp @@ -628,6 +628,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj SHA512 BunObject_getter_wrap_SHA512 DontDelete|PropertyCallback SHA512_256 BunObject_getter_wrap_SHA512_256 DontDelete|PropertyCallback TOML BunObject_getter_wrap_TOML DontDelete|PropertyCallback + YAML BunObject_getter_wrap_YAML DontDelete|PropertyCallback Transpiler BunObject_getter_wrap_Transpiler DontDelete|PropertyCallback embeddedFiles BunObject_getter_wrap_embeddedFiles DontDelete|PropertyCallback allocUnsafe BunObject_callback_allocUnsafe DontDelete|Function 1 diff --git a/src/js_ast.zig b/src/js_ast.zig index fe7456598e..1881d4b899 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -1917,6 +1917,10 @@ pub const E = struct { return if (asProperty(self, key)) |query| query.expr else @as(?Expr, null); } + pub fn getPtr(self: *const Object, key: string) ?*Expr { + return if (asProperty(self, key)) |query| &self.properties.slice()[query.i].value.? else null; + } + pub fn toJS(this: *Object, allocator: std.mem.Allocator, globalObject: *JSC.JSGlobalObject) ToJSError!JSC.JSValue { var obj = JSC.JSValue.createEmptyObject(globalObject, this.properties.len); obj.protect(); @@ -1965,7 +1969,7 @@ pub const E = struct { // this is terribly, shamefully slow pub fn setRope(self: *Object, rope: *const Rope, allocator: std.mem.Allocator, value: Expr) SetError!void { - if (self.get(rope.head.data.e_string.data)) |existing| { + if (self.getPtr(rope.head.data.e_string.data)) |existing| { switch (existing.data) { .e_array => |array| { if (rope.next == null) { @@ -1993,6 +1997,18 @@ pub const E = struct { return error.Clobber; }, + .e_null => { + // null can replace. + if (rope.next) |next| { + var obj = Expr.init(E.Object, E.Object{ .properties = .{} }, rope.head.loc); + existing.* = obj; + try obj.data.e_object.setRope(next, allocator, value); + return; + } + + existing.* = value; + return; + }, else => { return error.Clobber; }, @@ -3398,6 +3414,10 @@ pub const Expr = struct { return if (asProperty(expr, name)) |query| query.expr else null; } + pub fn getPtr(expr: *const Expr, name: string) ?*Expr { + return if (asProperty(expr, name)) |*query| &expr.data.e_object.properties.slice()[query.i] else null; + } + /// Don't use this if you care about performance. /// /// Sets the value of a property, creating it if it doesn't exist. diff --git a/src/yaml/yaml_lexer.zig b/src/yaml/yaml_lexer.zig index 8721bda188..b4ebeea3fe 100644 --- a/src/yaml/yaml_lexer.zig +++ b/src/yaml/yaml_lexer.zig @@ -37,8 +37,6 @@ pub const T = enum { t_null, // null // YAML specific - t_indent, // Increased indentation level - t_dedent, // Decreased indentation level t_newline, // Line break t_pipe, // | - Literal block scalar t_gt, // > - Folded block scalar @@ -72,7 +70,7 @@ pub const ComplexKey = struct { pub const BlockScalarHeader = struct { chomping: enum { clip, strip, keep } = .clip, - indent: ?u8 = null, + indent: ?u16 = null, style: enum { literal, folded }, }; @@ -99,11 +97,8 @@ pub const Lexer = struct { should_redact_logs: bool, // Indentation tracking - indent_stack: std.ArrayList(usize), - current_indent: usize = 0, - indent_width: ?usize = null, // Will be set on first indent + current_indent: u16 = 0, at_line_start: bool = true, - pending_dedents: usize = 0, // Anchor/Alias resolution anchors: AnchorMap, @@ -141,6 +136,20 @@ pub const Lexer = struct { block_scalar_header: ?BlockScalarHeader = null, block_scalar_indent: ?usize = null, + is_ascii: ?bool = null, + + pub fn remaining(self: *const Lexer) []const u8 { + return self.source.contents[@min(self.current, self.source.contents.len)..]; + } + + pub fn isASCII(self: *Lexer) bool { + if (self.is_ascii == null) { + self.is_ascii = strings.isAllASCII(self.remaining()); + } + + return self.is_ascii.?; + } + pub const FlowCommaState = packed struct(u32) { level: u15, has_comma: bool = false, @@ -293,31 +302,26 @@ pub const Lexer = struct { pub fn next(lexer: *Lexer) !void { lexer.has_newline_before = lexer.end == 0; - // Handle pending dedents - if (lexer.pending_dedents > 0) { - lexer.pending_dedents -= 1; - lexer.token = T.t_dedent; - return; - } - while (true) { lexer.start = lexer.end; lexer.token = T.t_end_of_file; switch (lexer.code_point) { -1 => { - // Generate dedents for any remaining indentation levels - if (lexer.indent_stack.items.len > 1) { - lexer.pending_dedents = lexer.indent_stack.items.len - 1; - lexer.indent_stack.shrinkRetainingCapacity(1); - lexer.token = T.t_dedent; - return; - } lexer.token = T.t_end_of_file; + return; }, - '\r', '\n', 0x2028, 0x2029 => { - lexer.step(); + '\r', '\n', 0x2028, 0x2029 => |cp| { + if (cp == '\r') { + try lexer.step(); + if (lexer.code_point != '\n') { + try lexer.addDefaultError("Unexpected \r carriage return. Carriage returns should be followed by a newline character."); + return error.SyntaxError; + } + } + + try lexer.step(); lexer.has_newline_before = true; lexer.at_line_start = true; lexer.current_indent = 0; @@ -335,7 +339,7 @@ pub const Lexer = struct { '\t' => { if (lexer.at_line_start) { - lexer.current_indent += 8; + lexer.current_indent += 1; } lexer.step(); continue; @@ -453,28 +457,6 @@ pub const Lexer = struct { // Handle indentation after processing the token if (lexer.at_line_start) { - const last_indent = lexer.indent_stack.items[lexer.indent_stack.items.len - 1]; - if (lexer.current_indent > last_indent) { - // This is an indent - try lexer.indent_stack.append(lexer.current_indent); - lexer.token = T.t_indent; - } else if (lexer.current_indent < last_indent) { - // This is one or more dedents - var dedent_count: usize = 0; - while (lexer.indent_stack.items.len > 0 and lexer.current_indent < lexer.indent_stack.items[lexer.indent_stack.items.len - 1]) { - _ = lexer.indent_stack.pop(); - dedent_count += 1; - } - - if (lexer.current_indent != lexer.indent_stack.items[lexer.indent_stack.items.len - 1]) { - try lexer.addDefaultError("Invalid indentation"); - } - - if (dedent_count > 1) { - lexer.pending_dedents = dedent_count - 1; - } - lexer.token = T.t_dedent; - } lexer.at_line_start = false; } @@ -555,7 +537,7 @@ pub const Lexer = struct { .prev_error_loc = logger.Loc.Empty, .allocator = allocator, .should_redact_logs = redact_logs, - .indent_stack = std.ArrayList(usize).init(allocator), + .anchors = AnchorMap.init(allocator), .tag_library = TagMap.init(allocator), .tag_handles = std.ArrayList(TagHandle).init(allocator), @@ -566,20 +548,21 @@ pub const Lexer = struct { .merge_key_stack = std.ArrayList(js_ast.Expr).init(allocator), }; - // Initialize with base indent level - try lex.indent_stack.append(0); - - // Add default tag handles - try lex.tag_handles.append(.{ .handle = "!", .prefix = "!" }); - try lex.tag_handles.append(.{ .handle = "!!", .prefix = "tag:yaml.org,2002:" }); - lex.step(); try lex.next(); return lex; } - pub inline fn toString(lexer: *Lexer, loc_: logger.Loc) js_ast.Expr { + pub fn toPropertyKey(lexer: *const Lexer, loc_: logger.Loc) js_ast.Expr { + if (lexer.token == .t_identifier) { + return js_ast.Expr.init(js_ast.E.String, js_ast.E.String{ .data = lexer.identifier }, loc_); + } + bun.debugAssert(lexer.token == .t_string_literal); + return js_ast.Expr.init(js_ast.E.String, js_ast.E.String{ .data = lexer.string_literal_slice }, loc_); + } + + pub fn toString(lexer: *const Lexer, loc_: logger.Loc) js_ast.Expr { if (lexer.string_literal_is_ascii) { return js_ast.Expr.init(js_ast.E.String, js_ast.E.String{ .data = lexer.string_literal_slice }, loc_); } @@ -712,12 +695,9 @@ pub const Lexer = struct { line_start = true; current_indent = 0; }, - ' ' => { + '\t', ' ' => { if (line_start) current_indent += 1; }, - '\t' => { - if (line_start) current_indent += 8; - }, else => { if (line_start and c != '\n' and c != '\r') { if (min_indent == null or current_indent < min_indent.?) { @@ -1248,7 +1228,6 @@ pub const Lexer = struct { fn parsePlainScalar(lexer: *Lexer) !void { var result = std.ArrayList(u8).init(lexer.allocator); - errdefer result.deinit(); var first = true; var spaces: usize = 0; diff --git a/src/yaml/yaml_parser.zig b/src/yaml/yaml_parser.zig index ab13b17bdb..5ac4447a0f 100644 --- a/src/yaml/yaml_parser.zig +++ b/src/yaml/yaml_parser.zig @@ -141,8 +141,6 @@ pub const YAML = struct { } fn runParser(p: *YAML) anyerror!Expr { - var current_sequence: ?*E.Array = null; - var is_top_level_sequence = true; var root_expr: ?Expr = null; var stack = std.heap.stackFallback(@sizeOf(Rope) * 6, p.allocator); @@ -163,75 +161,66 @@ pub const YAML = struct { }, .t_dash => { // Start of sequence item - try p.lexer.next(); - - // Create a new sequence if we're not already in one - if (current_sequence == null) { - const array_expr = p.e(E.Array{}, p.lexer.loc()); - current_sequence = array_expr.data.e_array; - - if (is_top_level_sequence) { - // This is a top-level sequence, make it the root - root_expr = array_expr; - is_top_level_sequence = false; - } else { - // If we're in a mapping context, we need a key for this sequence - if (root_expr == null) { - root_expr = p.e(E.Object{}, p.lexer.loc()); - } - const head = root_expr.?.data.e_object; - - const key = try p.parseKey(key_allocator); - try p.lexer.expect(.t_colon); - head.setRope(key, p.allocator, array_expr) catch |err| { - switch (err) { - error.Clobber => { - try p.lexer.addDefaultError("Cannot redefine key"); - return error.SyntaxError; - }, - else => return err, - } - }; - } + if (root_expr == null) { + // First dash, create the sequence + root_expr = p.e(E.Array{ + .items = .{}, + .is_single_line = false, + }, p.lexer.loc()); + } + if (root_expr != null and root_expr.?.data != .e_array) { + try p.lexer.addDefaultError("Top-level sequence must be an array or object"); + return error.SyntaxError; } - // Parse the sequence item - const item = try p.parseValue(); - try current_sequence.?.push(p.allocator, item); - - // Handle indentation and newlines - while (p.lexer.token == .t_newline) { - try p.lexer.next(); - if (p.lexer.token != .t_dash) { - current_sequence = null; // End of sequence - break; - } - try p.lexer.next(); - } - }, - .t_indent => { - try p.lexer.next(); + const array = root_expr.?.data.e_array; + const value = try p.parseValue(); + try array.push(p.allocator, value); continue; }, - .t_dedent => { + .t_newline => { try p.lexer.next(); - current_sequence = null; // End of sequence on dedent continue; }, .t_identifier, .t_string_literal => { - // As soon as we see a key-value pair, we're no longer in a top-level sequence context - is_top_level_sequence = false; - + const initial_indent = p.lexer.current_indent; // Create root object if needed if (root_expr == null) { root_expr = p.e(E.Object{}, p.lexer.loc()); } + if (root_expr.?.data != .e_object) { + try p.lexer.addDefaultError("Top-level sequence must be an array or object"); + return error.SyntaxError; + } const head = root_expr.?.data.e_object; // Key-value pair - const key = try p.parseKey(key_allocator); + const key = try key_allocator.create(Rope); + key.* = .{ + .head = p.lexer.toPropertyKey(p.lexer.loc()), + .next = null, + }; + try p.lexer.next(); try p.lexer.expect(.t_colon); - const value = try p.parseValue(); + while (p.lexer.token == .t_newline) { + try p.lexer.next(); + } + + const value = value: { + const new_indent = p.lexer.current_indent; + if (new_indent > initial_indent) { + const value = try p.parseObjectOrArraySequence(p.lexer.loc(), new_indent); + break :value value; + } else if (p.lexer.token == .t_dash) { + try p.lexer.addDefaultError("An array cannot be nested inside an object"); + return error.SyntaxError; + } else if (p.lexer.token == .t_end_of_file) { + break :value p.e(E.Null{}, p.lexer.loc()); + } + + break :value try p.parseValue(); + }; + head.setRope(key, p.allocator, value) catch |err| { switch (err) { error.Clobber => { @@ -241,6 +230,11 @@ pub const YAML = struct { else => return err, } }; + + // Handle any trailing newlines after the value + while (p.lexer.token == .t_newline) { + try p.lexer.next(); + } }, else => { try p.lexer.unexpected(); @@ -248,6 +242,98 @@ pub const YAML = struct { }, } } + + return root_expr orelse p.e(E.Object{}, p.lexer.loc()); + } + + fn parseObjectOrArraySequence(p: *YAML, loc: logger.Loc, indent: u16) anyerror!Expr { + // Check what follows to determine if it's an array or object + if (p.lexer.token == .t_dash) { + // The start of an array sequence + const array = p.e(E.Array{ + .items = .{}, + }, loc); + + while (p.lexer.token == .t_dash) { + try p.lexer.next(); + + while (p.lexer.token == .t_newline) { + try p.lexer.next(); + } + + if (p.lexer.token == .t_end_of_file or p.lexer.current_indent < indent) { + break; + } + + if (p.lexer.current_indent > indent) { + try array.data.e_array.push(p.allocator, try p.runParser()); + } else { + try array.data.e_array.push(p.allocator, try p.parseValue()); + } + + while (p.lexer.token == .t_newline) { + try p.lexer.next(); + } + } + + return array; + } else { + var root: ?Expr = null; + + // Parse key-value pairs at this indentation level + while (true) { + while (p.lexer.token == .t_newline) { + try p.lexer.next(); + } + + if (p.lexer.token == .t_end_of_file or p.lexer.current_indent < indent) { + break; + } + + const key = try p.parseKey(p.allocator); + + try p.lexer.expect(.t_colon); + + while (p.lexer.token == .t_newline) { + try p.lexer.next(); + } + + // A single object with { [key]: null } + if (p.lexer.token == .t_end_of_file or p.lexer.current_indent < indent) { + if (root == null) { + root = p.e(E.Object{}, loc); + } + + root.?.data.e_object.setRope(key, p.allocator, p.e(E.Null{}, loc)) catch { + try p.lexer.addDefaultError("Cannot redefine key"); + return error.SyntaxError; + }; + break; + } + + // Handle potential indent after the colon + const value = if (p.lexer.current_indent > indent) + try p.runParser() + else + try p.parseValue(); + + if (root == null) { + root = p.e(E.Object{}, loc); + } + + root.?.data.e_object.setRope(key, p.allocator, value) catch |err| { + switch (err) { + error.Clobber => { + try p.lexer.addDefaultError("Cannot redefine key"); + return error.SyntaxError; + }, + else => return err, + } + }; + } + + return root orelse p.e(E.Null{}, loc); + } } pub fn parseValue(p: *YAML) anyerror!Expr { @@ -297,9 +383,11 @@ pub const YAML = struct { }, // Handle quoted strings: "quoted" or 'quoted' .t_string_literal => brk: { - const result = p.lexer.toString(loc); + const str_loc = p.lexer.loc(); + const str = p.lexer.toString(str_loc); try p.lexer.next(); - break :brk result; + + break :brk str; }, // Handle unquoted scalars: plain_text .t_identifier => brk: { @@ -314,26 +402,9 @@ pub const YAML = struct { break :brk p.e(E.Number{ .value = value }, loc); }, - // Handle block sequences (indentation-based) - // Example: - // - item1 - // - item2 - .t_dash => brk: { - try p.lexer.next(); - var items = std.ArrayList(Expr).init(p.allocator); - errdefer items.deinit(); - while (true) { - if (p.lexer.token != .t_dash) break; - try p.lexer.next(); - try items.append(try p.parseValue()); - if (p.lexer.token != .t_newline) break; - try p.lexer.next(); - } - - break :brk p.e(E.Array{ - .items = ExprNodeList.fromList(items), - .is_single_line = false, - }, loc); + .t_dash => { + p.lexer.addError(loc.toUsize(), "Unexpected array element. Try either adding an indentation, or wrapping in quotes", .{}); + return error.SyntaxError; }, // Handle flow sequences (bracket-based) @@ -347,10 +418,18 @@ pub const YAML = struct { if (items.items.len > 0) { if (p.lexer.token != .t_comma) break; try p.lexer.next(); + // Handle newlines after commas + while (p.lexer.token == .t_newline) { + try p.lexer.next(); + } } try items.append(try p.parseValue()); } + while (p.lexer.token == .t_newline) { + try p.lexer.next(); + } + try p.lexer.expect(.t_close_bracket); break :brk p.e(E.Array{ .items = ExprNodeList.fromList(items), @@ -363,12 +442,21 @@ pub const YAML = struct { .t_open_brace => brk: { try p.lexer.next(); + // Handle newlines before the first key + while (p.lexer.token == .t_newline) { + try p.lexer.next(); + } + const expr = p.e(E.Object{}, loc); const obj = expr.data.e_object; while (p.lexer.token != .t_close_brace) { if (obj.properties.len > 0) { if (p.lexer.token != .t_comma) break; try p.lexer.next(); + // Handle newlines after commas + while (p.lexer.token == .t_newline) { + try p.lexer.next(); + } } const key = try p.parseKey(p.allocator); @@ -386,6 +474,15 @@ pub const YAML = struct { else => return err, } }; + + // Handle newlines after values + while (p.lexer.token == .t_newline) { + try p.lexer.next(); + } + } + + while (p.lexer.token == .t_newline) { + try p.lexer.next(); } try p.lexer.expect(.t_close_brace); @@ -448,6 +545,11 @@ pub const YAML = struct { p.lexer.current_tag = null; } + // Handle any trailing newlines after the value + while (p.lexer.token == .t_newline) { + try p.lexer.next(); + } + return value; } };