const std = @import("std"); const Allocator = std.mem.Allocator; const Token = @import("lex.zig").Token; const SupportedCodePage = @import("code_pages.zig").SupportedCodePage; pub const Tree = struct { node: *Node, input_code_pages: CodePageLookup, output_code_pages: CodePageLookup, /// not owned by the tree source: []const u8, arena: std.heap.ArenaAllocator.State, allocator: Allocator, pub fn deinit(self: *Tree) void { self.arena.promote(self.allocator).deinit(); } pub fn root(self: *Tree) *Node.Root { return @alignCast(@fieldParentPtr("base", self.node)); } pub fn dump(self: *Tree, writer: anytype) @TypeOf(writer).Error!void { try self.node.dump(self, writer, 0); } }; pub const CodePageLookup = struct { lookup: std.ArrayListUnmanaged(SupportedCodePage) = .empty, allocator: Allocator, default_code_page: SupportedCodePage, pub fn init(allocator: Allocator, default_code_page: SupportedCodePage) CodePageLookup { return .{ .allocator = allocator, .default_code_page = default_code_page, }; } pub fn deinit(self: *CodePageLookup) void { self.lookup.deinit(self.allocator); } /// line_num is 1-indexed pub fn setForLineNum(self: *CodePageLookup, line_num: usize, code_page: SupportedCodePage) !void { const index = line_num - 1; if (index >= self.lookup.items.len) { const new_size = line_num; const missing_lines_start_index = self.lookup.items.len; try self.lookup.resize(self.allocator, new_size); // If there are any gaps created, we need to fill them in with the value of the // last line before the gap. This can happen for e.g. string literals that // span multiple lines, or if the start of a file has multiple empty lines. const fill_value = if (missing_lines_start_index > 0) self.lookup.items[missing_lines_start_index - 1] else self.default_code_page; var i: usize = missing_lines_start_index; while (i < new_size - 1) : (i += 1) { self.lookup.items[i] = fill_value; } } self.lookup.items[index] = code_page; } pub fn setForToken(self: *CodePageLookup, token: Token, code_page: SupportedCodePage) !void { return self.setForLineNum(token.line_number, code_page); } /// line_num is 1-indexed pub fn getForLineNum(self: CodePageLookup, line_num: usize) SupportedCodePage { return self.lookup.items[line_num - 1]; } pub fn getForToken(self: CodePageLookup, token: Token) SupportedCodePage { return self.getForLineNum(token.line_number); } }; test "CodePageLookup" { var lookup = CodePageLookup.init(std.testing.allocator, .windows1252); defer lookup.deinit(); try lookup.setForLineNum(5, .utf8); try std.testing.expectEqual(SupportedCodePage.windows1252, lookup.getForLineNum(1)); try std.testing.expectEqual(SupportedCodePage.windows1252, lookup.getForLineNum(2)); try std.testing.expectEqual(SupportedCodePage.windows1252, lookup.getForLineNum(3)); try std.testing.expectEqual(SupportedCodePage.windows1252, lookup.getForLineNum(4)); try std.testing.expectEqual(SupportedCodePage.utf8, lookup.getForLineNum(5)); try std.testing.expectEqual(@as(usize, 5), lookup.lookup.items.len); try lookup.setForLineNum(7, .windows1252); try std.testing.expectEqual(SupportedCodePage.windows1252, lookup.getForLineNum(1)); try std.testing.expectEqual(SupportedCodePage.windows1252, lookup.getForLineNum(2)); try std.testing.expectEqual(SupportedCodePage.windows1252, lookup.getForLineNum(3)); try std.testing.expectEqual(SupportedCodePage.windows1252, lookup.getForLineNum(4)); try std.testing.expectEqual(SupportedCodePage.utf8, lookup.getForLineNum(5)); try std.testing.expectEqual(SupportedCodePage.utf8, lookup.getForLineNum(6)); try std.testing.expectEqual(SupportedCodePage.windows1252, lookup.getForLineNum(7)); try std.testing.expectEqual(@as(usize, 7), lookup.lookup.items.len); } pub const Node = struct { id: Id, pub const Id = enum { root, resource_external, resource_raw_data, literal, binary_expression, grouped_expression, not_expression, accelerators, accelerator, dialog, control_statement, toolbar, menu, menu_item, menu_item_separator, menu_item_ex, popup, popup_ex, version_info, version_statement, block, block_value, block_value_value, string_table, string_table_string, language_statement, font_statement, simple_statement, invalid, pub fn Type(comptime id: Id) type { return switch (id) { .root => Root, .resource_external => ResourceExternal, .resource_raw_data => ResourceRawData, .literal => Literal, .binary_expression => BinaryExpression, .grouped_expression => GroupedExpression, .not_expression => NotExpression, .accelerators => Accelerators, .accelerator => Accelerator, .dialog => Dialog, .control_statement => ControlStatement, .toolbar => Toolbar, .menu => Menu, .menu_item => MenuItem, .menu_item_separator => MenuItemSeparator, .menu_item_ex => MenuItemEx, .popup => Popup, .popup_ex => PopupEx, .version_info => VersionInfo, .version_statement => VersionStatement, .block => Block, .block_value => BlockValue, .block_value_value => BlockValueValue, .string_table => StringTable, .string_table_string => StringTableString, .language_statement => LanguageStatement, .font_statement => FontStatement, .simple_statement => SimpleStatement, .invalid => Invalid, }; } }; pub fn cast(base: *Node, comptime id: Id) ?*id.Type() { if (base.id == id) { return @alignCast(@fieldParentPtr("base", base)); } return null; } pub const Root = struct { base: Node = .{ .id = .root }, body: []*Node, }; pub const ResourceExternal = struct { base: Node = .{ .id = .resource_external }, id: Token, type: Token, common_resource_attributes: []Token, filename: *Node, }; pub const ResourceRawData = struct { base: Node = .{ .id = .resource_raw_data }, id: Token, type: Token, common_resource_attributes: []Token, begin_token: Token, raw_data: []*Node, end_token: Token, }; pub const Literal = struct { base: Node = .{ .id = .literal }, token: Token, }; pub const BinaryExpression = struct { base: Node = .{ .id = .binary_expression }, operator: Token, left: *Node, right: *Node, }; pub const GroupedExpression = struct { base: Node = .{ .id = .grouped_expression }, open_token: Token, expression: *Node, close_token: Token, }; pub const NotExpression = struct { base: Node = .{ .id = .not_expression }, not_token: Token, number_token: Token, }; pub const Accelerators = struct { base: Node = .{ .id = .accelerators }, id: Token, type: Token, common_resource_attributes: []Token, optional_statements: []*Node, begin_token: Token, accelerators: []*Node, end_token: Token, }; pub const Accelerator = struct { base: Node = .{ .id = .accelerator }, event: *Node, idvalue: *Node, type_and_options: []Token, }; pub const Dialog = struct { base: Node = .{ .id = .dialog }, id: Token, type: Token, common_resource_attributes: []Token, x: *Node, y: *Node, width: *Node, height: *Node, help_id: ?*Node, optional_statements: []*Node, begin_token: Token, controls: []*Node, end_token: Token, }; pub const ControlStatement = struct { base: Node = .{ .id = .control_statement }, type: Token, text: ?Token, /// Only relevant for the user-defined CONTROL control class: ?*Node, id: *Node, x: *Node, y: *Node, width: *Node, height: *Node, style: ?*Node, exstyle: ?*Node, help_id: ?*Node, extra_data_begin: ?Token, extra_data: []*Node, extra_data_end: ?Token, /// Returns true if this node describes a user-defined CONTROL control /// https://learn.microsoft.com/en-us/windows/win32/menurc/control-control pub fn isUserDefined(self: *const ControlStatement) bool { return self.class != null; } }; pub const Toolbar = struct { base: Node = .{ .id = .toolbar }, id: Token, type: Token, common_resource_attributes: []Token, button_width: *Node, button_height: *Node, begin_token: Token, /// Will contain Literal and SimpleStatement nodes buttons: []*Node, end_token: Token, }; pub const Menu = struct { base: Node = .{ .id = .menu }, id: Token, type: Token, common_resource_attributes: []Token, optional_statements: []*Node, /// `help_id` will never be non-null if `type` is MENU help_id: ?*Node, begin_token: Token, items: []*Node, end_token: Token, }; pub const MenuItem = struct { base: Node = .{ .id = .menu_item }, menuitem: Token, text: Token, result: *Node, option_list: []Token, }; pub const MenuItemSeparator = struct { base: Node = .{ .id = .menu_item_separator }, menuitem: Token, separator: Token, }; pub const MenuItemEx = struct { base: Node = .{ .id = .menu_item_ex }, menuitem: Token, text: Token, id: ?*Node, type: ?*Node, state: ?*Node, }; pub const Popup = struct { base: Node = .{ .id = .popup }, popup: Token, text: Token, option_list: []Token, begin_token: Token, items: []*Node, end_token: Token, }; pub const PopupEx = struct { base: Node = .{ .id = .popup_ex }, popup: Token, text: Token, id: ?*Node, type: ?*Node, state: ?*Node, help_id: ?*Node, begin_token: Token, items: []*Node, end_token: Token, }; pub const VersionInfo = struct { base: Node = .{ .id = .version_info }, id: Token, versioninfo: Token, common_resource_attributes: []Token, /// Will contain VersionStatement and/or SimpleStatement nodes fixed_info: []*Node, begin_token: Token, block_statements: []*Node, end_token: Token, }; /// Used for FILEVERSION and PRODUCTVERSION statements pub const VersionStatement = struct { base: Node = .{ .id = .version_statement }, type: Token, /// Between 1-4 parts parts: []*Node, }; pub const Block = struct { base: Node = .{ .id = .block }, /// The BLOCK token itself identifier: Token, key: Token, /// This is undocumented but BLOCK statements support values after /// the key just like VALUE statements. values: []*Node, begin_token: Token, children: []*Node, end_token: Token, }; pub const BlockValue = struct { base: Node = .{ .id = .block_value }, /// The VALUE token itself identifier: Token, key: Token, /// These will be BlockValueValue nodes values: []*Node, }; pub const BlockValueValue = struct { base: Node = .{ .id = .block_value_value }, expression: *Node, /// Whether or not the value has a trailing comma is relevant trailing_comma: bool, }; pub const StringTable = struct { base: Node = .{ .id = .string_table }, type: Token, common_resource_attributes: []Token, optional_statements: []*Node, begin_token: Token, strings: []*Node, end_token: Token, }; pub const StringTableString = struct { base: Node = .{ .id = .string_table_string }, id: *Node, maybe_comma: ?Token, string: Token, }; pub const LanguageStatement = struct { base: Node = .{ .id = .language_statement }, /// The LANGUAGE token itself language_token: Token, primary_language_id: *Node, sublanguage_id: *Node, }; pub const FontStatement = struct { base: Node = .{ .id = .font_statement }, /// The FONT token itself identifier: Token, point_size: *Node, typeface: Token, weight: ?*Node, italic: ?*Node, char_set: ?*Node, }; /// A statement with one value associated with it. /// Used for CAPTION, CHARACTERISTICS, CLASS, EXSTYLE, MENU, STYLE, VERSION, /// as well as VERSIONINFO-specific statements FILEFLAGSMASK, FILEFLAGS, FILEOS, /// FILETYPE, FILESUBTYPE pub const SimpleStatement = struct { base: Node = .{ .id = .simple_statement }, identifier: Token, value: *Node, }; pub const Invalid = struct { base: Node = .{ .id = .invalid }, context: []Token, }; pub fn isNumberExpression(node: *const Node) bool { switch (node.id) { .literal => { const literal: *const Node.Literal = @alignCast(@fieldParentPtr("base", node)); return switch (literal.token.id) { .number => true, else => false, }; }, .binary_expression, .grouped_expression, .not_expression => return true, else => return false, } } pub fn isStringLiteral(node: *const Node) bool { switch (node.id) { .literal => { const literal: *const Node.Literal = @alignCast(@fieldParentPtr("base", node)); return switch (literal.token.id) { .quoted_ascii_string, .quoted_wide_string => true, else => false, }; }, else => return false, } } pub fn getFirstToken(node: *const Node) Token { switch (node.id) { .root => unreachable, .resource_external => { const casted: *const Node.ResourceExternal = @alignCast(@fieldParentPtr("base", node)); return casted.id; }, .resource_raw_data => { const casted: *const Node.ResourceRawData = @alignCast(@fieldParentPtr("base", node)); return casted.id; }, .literal => { const casted: *const Node.Literal = @alignCast(@fieldParentPtr("base", node)); return casted.token; }, .binary_expression => { const casted: *const Node.BinaryExpression = @alignCast(@fieldParentPtr("base", node)); return casted.left.getFirstToken(); }, .grouped_expression => { const casted: *const Node.GroupedExpression = @alignCast(@fieldParentPtr("base", node)); return casted.open_token; }, .not_expression => { const casted: *const Node.NotExpression = @alignCast(@fieldParentPtr("base", node)); return casted.not_token; }, .accelerators => { const casted: *const Node.Accelerators = @alignCast(@fieldParentPtr("base", node)); return casted.id; }, .accelerator => { const casted: *const Node.Accelerator = @alignCast(@fieldParentPtr("base", node)); return casted.event.getFirstToken(); }, .dialog => { const casted: *const Node.Dialog = @alignCast(@fieldParentPtr("base", node)); return casted.id; }, .control_statement => { const casted: *const Node.ControlStatement = @alignCast(@fieldParentPtr("base", node)); return casted.type; }, .toolbar => { const casted: *const Node.Toolbar = @alignCast(@fieldParentPtr("base", node)); return casted.id; }, .menu => { const casted: *const Node.Menu = @alignCast(@fieldParentPtr("base", node)); return casted.id; }, inline .menu_item, .menu_item_separator, .menu_item_ex => |menu_item_type| { const casted: *const menu_item_type.Type() = @alignCast(@fieldParentPtr("base", node)); return casted.menuitem; }, inline .popup, .popup_ex => |popup_type| { const casted: *const popup_type.Type() = @alignCast(@fieldParentPtr("base", node)); return casted.popup; }, .version_info => { const casted: *const Node.VersionInfo = @alignCast(@fieldParentPtr("base", node)); return casted.id; }, .version_statement => { const casted: *const Node.VersionStatement = @alignCast(@fieldParentPtr("base", node)); return casted.type; }, .block => { const casted: *const Node.Block = @alignCast(@fieldParentPtr("base", node)); return casted.identifier; }, .block_value => { const casted: *const Node.BlockValue = @alignCast(@fieldParentPtr("base", node)); return casted.identifier; }, .block_value_value => { const casted: *const Node.BlockValueValue = @alignCast(@fieldParentPtr("base", node)); return casted.expression.getFirstToken(); }, .string_table => { const casted: *const Node.StringTable = @alignCast(@fieldParentPtr("base", node)); return casted.type; }, .string_table_string => { const casted: *const Node.StringTableString = @alignCast(@fieldParentPtr("base", node)); return casted.id.getFirstToken(); }, .language_statement => { const casted: *const Node.LanguageStatement = @alignCast(@fieldParentPtr("base", node)); return casted.language_token; }, .font_statement => { const casted: *const Node.FontStatement = @alignCast(@fieldParentPtr("base", node)); return casted.identifier; }, .simple_statement => { const casted: *const Node.SimpleStatement = @alignCast(@fieldParentPtr("base", node)); return casted.identifier; }, .invalid => { const casted: *const Node.Invalid = @alignCast(@fieldParentPtr("base", node)); return casted.context[0]; }, } } pub fn getLastToken(node: *const Node) Token { switch (node.id) { .root => unreachable, .resource_external => { const casted: *const Node.ResourceExternal = @alignCast(@fieldParentPtr("base", node)); return casted.filename.getLastToken(); }, .resource_raw_data => { const casted: *const Node.ResourceRawData = @alignCast(@fieldParentPtr("base", node)); return casted.end_token; }, .literal => { const casted: *const Node.Literal = @alignCast(@fieldParentPtr("base", node)); return casted.token; }, .binary_expression => { const casted: *const Node.BinaryExpression = @alignCast(@fieldParentPtr("base", node)); return casted.right.getLastToken(); }, .grouped_expression => { const casted: *const Node.GroupedExpression = @alignCast(@fieldParentPtr("base", node)); return casted.close_token; }, .not_expression => { const casted: *const Node.NotExpression = @alignCast(@fieldParentPtr("base", node)); return casted.number_token; }, .accelerators => { const casted: *const Node.Accelerators = @alignCast(@fieldParentPtr("base", node)); return casted.end_token; }, .accelerator => { const casted: *const Node.Accelerator = @alignCast(@fieldParentPtr("base", node)); if (casted.type_and_options.len > 0) return casted.type_and_options[casted.type_and_options.len - 1]; return casted.idvalue.getLastToken(); }, .dialog => { const casted: *const Node.Dialog = @alignCast(@fieldParentPtr("base", node)); return casted.end_token; }, .control_statement => { const casted: *const Node.ControlStatement = @alignCast(@fieldParentPtr("base", node)); if (casted.extra_data_end) |token| return token; if (casted.help_id) |help_id_node| return help_id_node.getLastToken(); if (casted.exstyle) |exstyle_node| return exstyle_node.getLastToken(); // For user-defined CONTROL controls, the style comes before 'x', but // otherwise it comes after 'height' so it could be the last token if // it's present. if (!casted.isUserDefined()) { if (casted.style) |style_node| return style_node.getLastToken(); } return casted.height.getLastToken(); }, .toolbar => { const casted: *const Node.Toolbar = @alignCast(@fieldParentPtr("base", node)); return casted.end_token; }, .menu => { const casted: *const Node.Menu = @alignCast(@fieldParentPtr("base", node)); return casted.end_token; }, .menu_item => { const casted: *const Node.MenuItem = @alignCast(@fieldParentPtr("base", node)); if (casted.option_list.len > 0) return casted.option_list[casted.option_list.len - 1]; return casted.result.getLastToken(); }, .menu_item_separator => { const casted: *const Node.MenuItemSeparator = @alignCast(@fieldParentPtr("base", node)); return casted.separator; }, .menu_item_ex => { const casted: *const Node.MenuItemEx = @alignCast(@fieldParentPtr("base", node)); if (casted.state) |state_node| return state_node.getLastToken(); if (casted.type) |type_node| return type_node.getLastToken(); if (casted.id) |id_node| return id_node.getLastToken(); return casted.text; }, inline .popup, .popup_ex => |popup_type| { const casted: *const popup_type.Type() = @alignCast(@fieldParentPtr("base", node)); return casted.end_token; }, .version_info => { const casted: *const Node.VersionInfo = @alignCast(@fieldParentPtr("base", node)); return casted.end_token; }, .version_statement => { const casted: *const Node.VersionStatement = @alignCast(@fieldParentPtr("base", node)); return casted.parts[casted.parts.len - 1].getLastToken(); }, .block => { const casted: *const Node.Block = @alignCast(@fieldParentPtr("base", node)); return casted.end_token; }, .block_value => { const casted: *const Node.BlockValue = @alignCast(@fieldParentPtr("base", node)); if (casted.values.len > 0) return casted.values[casted.values.len - 1].getLastToken(); return casted.key; }, .block_value_value => { const casted: *const Node.BlockValueValue = @alignCast(@fieldParentPtr("base", node)); return casted.expression.getLastToken(); }, .string_table => { const casted: *const Node.StringTable = @alignCast(@fieldParentPtr("base", node)); return casted.end_token; }, .string_table_string => { const casted: *const Node.StringTableString = @alignCast(@fieldParentPtr("base", node)); return casted.string; }, .language_statement => { const casted: *const Node.LanguageStatement = @alignCast(@fieldParentPtr("base", node)); return casted.sublanguage_id.getLastToken(); }, .font_statement => { const casted: *const Node.FontStatement = @alignCast(@fieldParentPtr("base", node)); if (casted.char_set) |char_set_node| return char_set_node.getLastToken(); if (casted.italic) |italic_node| return italic_node.getLastToken(); if (casted.weight) |weight_node| return weight_node.getLastToken(); return casted.typeface; }, .simple_statement => { const casted: *const Node.SimpleStatement = @alignCast(@fieldParentPtr("base", node)); return casted.value.getLastToken(); }, .invalid => { const casted: *const Node.Invalid = @alignCast(@fieldParentPtr("base", node)); return casted.context[casted.context.len - 1]; }, } } pub fn dump( node: *const Node, tree: *const Tree, writer: anytype, indent: usize, ) @TypeOf(writer).Error!void { try writer.writeByteNTimes(' ', indent); try writer.writeAll(@tagName(node.id)); switch (node.id) { .root => { try writer.writeAll("\n"); const root: *const Node.Root = @alignCast(@fieldParentPtr("base", node)); for (root.body) |body_node| { try body_node.dump(tree, writer, indent + 1); } }, .resource_external => { const resource: *const Node.ResourceExternal = @alignCast(@fieldParentPtr("base", node)); try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ resource.id.slice(tree.source), resource.type.slice(tree.source), resource.common_resource_attributes.len }); try resource.filename.dump(tree, writer, indent + 1); }, .resource_raw_data => { const resource: *const Node.ResourceRawData = @alignCast(@fieldParentPtr("base", node)); try writer.print(" {s} {s} [{d} common_resource_attributes] raw data: {}\n", .{ resource.id.slice(tree.source), resource.type.slice(tree.source), resource.common_resource_attributes.len, resource.raw_data.len }); for (resource.raw_data) |data_expression| { try data_expression.dump(tree, writer, indent + 1); } }, .literal => { const literal: *const Node.Literal = @alignCast(@fieldParentPtr("base", node)); try writer.writeAll(" "); try writer.writeAll(literal.token.slice(tree.source)); try writer.writeAll("\n"); }, .binary_expression => { const binary: *const Node.BinaryExpression = @alignCast(@fieldParentPtr("base", node)); try writer.writeAll(" "); try writer.writeAll(binary.operator.slice(tree.source)); try writer.writeAll("\n"); try binary.left.dump(tree, writer, indent + 1); try binary.right.dump(tree, writer, indent + 1); }, .grouped_expression => { const grouped: *const Node.GroupedExpression = @alignCast(@fieldParentPtr("base", node)); try writer.writeAll("\n"); try writer.writeByteNTimes(' ', indent); try writer.writeAll(grouped.open_token.slice(tree.source)); try writer.writeAll("\n"); try grouped.expression.dump(tree, writer, indent + 1); try writer.writeByteNTimes(' ', indent); try writer.writeAll(grouped.close_token.slice(tree.source)); try writer.writeAll("\n"); }, .not_expression => { const not: *const Node.NotExpression = @alignCast(@fieldParentPtr("base", node)); try writer.writeAll(" "); try writer.writeAll(not.not_token.slice(tree.source)); try writer.writeAll(" "); try writer.writeAll(not.number_token.slice(tree.source)); try writer.writeAll("\n"); }, .accelerators => { const accelerators: *const Node.Accelerators = @alignCast(@fieldParentPtr("base", node)); try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ accelerators.id.slice(tree.source), accelerators.type.slice(tree.source), accelerators.common_resource_attributes.len }); for (accelerators.optional_statements) |statement| { try statement.dump(tree, writer, indent + 1); } try writer.writeByteNTimes(' ', indent); try writer.writeAll(accelerators.begin_token.slice(tree.source)); try writer.writeAll("\n"); for (accelerators.accelerators) |accelerator| { try accelerator.dump(tree, writer, indent + 1); } try writer.writeByteNTimes(' ', indent); try writer.writeAll(accelerators.end_token.slice(tree.source)); try writer.writeAll("\n"); }, .accelerator => { const accelerator: *const Node.Accelerator = @alignCast(@fieldParentPtr("base", node)); for (accelerator.type_and_options, 0..) |option, i| { if (i != 0) try writer.writeAll(","); try writer.writeByte(' '); try writer.writeAll(option.slice(tree.source)); } try writer.writeAll("\n"); try accelerator.event.dump(tree, writer, indent + 1); try accelerator.idvalue.dump(tree, writer, indent + 1); }, .dialog => { const dialog: *const Node.Dialog = @alignCast(@fieldParentPtr("base", node)); try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ dialog.id.slice(tree.source), dialog.type.slice(tree.source), dialog.common_resource_attributes.len }); inline for (.{ "x", "y", "width", "height" }) |arg| { try writer.writeByteNTimes(' ', indent + 1); try writer.writeAll(arg ++ ":\n"); try @field(dialog, arg).dump(tree, writer, indent + 2); } if (dialog.help_id) |help_id| { try writer.writeByteNTimes(' ', indent + 1); try writer.writeAll("help_id:\n"); try help_id.dump(tree, writer, indent + 2); } for (dialog.optional_statements) |statement| { try statement.dump(tree, writer, indent + 1); } try writer.writeByteNTimes(' ', indent); try writer.writeAll(dialog.begin_token.slice(tree.source)); try writer.writeAll("\n"); for (dialog.controls) |control| { try control.dump(tree, writer, indent + 1); } try writer.writeByteNTimes(' ', indent); try writer.writeAll(dialog.end_token.slice(tree.source)); try writer.writeAll("\n"); }, .control_statement => { const control: *const Node.ControlStatement = @alignCast(@fieldParentPtr("base", node)); try writer.print(" {s}", .{control.type.slice(tree.source)}); if (control.text) |text| { try writer.print(" text: {s}", .{text.slice(tree.source)}); } try writer.writeByte('\n'); if (control.class) |class| { try writer.writeByteNTimes(' ', indent + 1); try writer.writeAll("class:\n"); try class.dump(tree, writer, indent + 2); } inline for (.{ "id", "x", "y", "width", "height" }) |arg| { try writer.writeByteNTimes(' ', indent + 1); try writer.writeAll(arg ++ ":\n"); try @field(control, arg).dump(tree, writer, indent + 2); } inline for (.{ "style", "exstyle", "help_id" }) |arg| { if (@field(control, arg)) |val_node| { try writer.writeByteNTimes(' ', indent + 1); try writer.writeAll(arg ++ ":\n"); try val_node.dump(tree, writer, indent + 2); } } if (control.extra_data_begin != null) { try writer.writeByteNTimes(' ', indent); try writer.writeAll(control.extra_data_begin.?.slice(tree.source)); try writer.writeAll("\n"); for (control.extra_data) |data_node| { try data_node.dump(tree, writer, indent + 1); } try writer.writeByteNTimes(' ', indent); try writer.writeAll(control.extra_data_end.?.slice(tree.source)); try writer.writeAll("\n"); } }, .toolbar => { const toolbar: *const Node.Toolbar = @alignCast(@fieldParentPtr("base", node)); try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ toolbar.id.slice(tree.source), toolbar.type.slice(tree.source), toolbar.common_resource_attributes.len }); inline for (.{ "button_width", "button_height" }) |arg| { try writer.writeByteNTimes(' ', indent + 1); try writer.writeAll(arg ++ ":\n"); try @field(toolbar, arg).dump(tree, writer, indent + 2); } try writer.writeByteNTimes(' ', indent); try writer.writeAll(toolbar.begin_token.slice(tree.source)); try writer.writeAll("\n"); for (toolbar.buttons) |button_or_sep| { try button_or_sep.dump(tree, writer, indent + 1); } try writer.writeByteNTimes(' ', indent); try writer.writeAll(toolbar.end_token.slice(tree.source)); try writer.writeAll("\n"); }, .menu => { const menu: *const Node.Menu = @alignCast(@fieldParentPtr("base", node)); try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ menu.id.slice(tree.source), menu.type.slice(tree.source), menu.common_resource_attributes.len }); for (menu.optional_statements) |statement| { try statement.dump(tree, writer, indent + 1); } if (menu.help_id) |help_id| { try writer.writeByteNTimes(' ', indent + 1); try writer.writeAll("help_id:\n"); try help_id.dump(tree, writer, indent + 2); } try writer.writeByteNTimes(' ', indent); try writer.writeAll(menu.begin_token.slice(tree.source)); try writer.writeAll("\n"); for (menu.items) |item| { try item.dump(tree, writer, indent + 1); } try writer.writeByteNTimes(' ', indent); try writer.writeAll(menu.end_token.slice(tree.source)); try writer.writeAll("\n"); }, .menu_item => { const menu_item: *const Node.MenuItem = @alignCast(@fieldParentPtr("base", node)); try writer.print(" {s} {s} [{d} options]\n", .{ menu_item.menuitem.slice(tree.source), menu_item.text.slice(tree.source), menu_item.option_list.len }); try menu_item.result.dump(tree, writer, indent + 1); }, .menu_item_separator => { const menu_item: *const Node.MenuItemSeparator = @alignCast(@fieldParentPtr("base", node)); try writer.print(" {s} {s}\n", .{ menu_item.menuitem.slice(tree.source), menu_item.separator.slice(tree.source) }); }, .menu_item_ex => { const menu_item: *const Node.MenuItemEx = @alignCast(@fieldParentPtr("base", node)); try writer.print(" {s} {s}\n", .{ menu_item.menuitem.slice(tree.source), menu_item.text.slice(tree.source) }); inline for (.{ "id", "type", "state" }) |arg| { if (@field(menu_item, arg)) |val_node| { try writer.writeByteNTimes(' ', indent + 1); try writer.writeAll(arg ++ ":\n"); try val_node.dump(tree, writer, indent + 2); } } }, .popup => { const popup: *const Node.Popup = @alignCast(@fieldParentPtr("base", node)); try writer.print(" {s} {s} [{d} options]\n", .{ popup.popup.slice(tree.source), popup.text.slice(tree.source), popup.option_list.len }); try writer.writeByteNTimes(' ', indent); try writer.writeAll(popup.begin_token.slice(tree.source)); try writer.writeAll("\n"); for (popup.items) |item| { try item.dump(tree, writer, indent + 1); } try writer.writeByteNTimes(' ', indent); try writer.writeAll(popup.end_token.slice(tree.source)); try writer.writeAll("\n"); }, .popup_ex => { const popup: *const Node.PopupEx = @alignCast(@fieldParentPtr("base", node)); try writer.print(" {s} {s}\n", .{ popup.popup.slice(tree.source), popup.text.slice(tree.source) }); inline for (.{ "id", "type", "state", "help_id" }) |arg| { if (@field(popup, arg)) |val_node| { try writer.writeByteNTimes(' ', indent + 1); try writer.writeAll(arg ++ ":\n"); try val_node.dump(tree, writer, indent + 2); } } try writer.writeByteNTimes(' ', indent); try writer.writeAll(popup.begin_token.slice(tree.source)); try writer.writeAll("\n"); for (popup.items) |item| { try item.dump(tree, writer, indent + 1); } try writer.writeByteNTimes(' ', indent); try writer.writeAll(popup.end_token.slice(tree.source)); try writer.writeAll("\n"); }, .version_info => { const version_info: *const Node.VersionInfo = @alignCast(@fieldParentPtr("base", node)); try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ version_info.id.slice(tree.source), version_info.versioninfo.slice(tree.source), version_info.common_resource_attributes.len }); for (version_info.fixed_info) |fixed_info| { try fixed_info.dump(tree, writer, indent + 1); } try writer.writeByteNTimes(' ', indent); try writer.writeAll(version_info.begin_token.slice(tree.source)); try writer.writeAll("\n"); for (version_info.block_statements) |block| { try block.dump(tree, writer, indent + 1); } try writer.writeByteNTimes(' ', indent); try writer.writeAll(version_info.end_token.slice(tree.source)); try writer.writeAll("\n"); }, .version_statement => { const version_statement: *const Node.VersionStatement = @alignCast(@fieldParentPtr("base", node)); try writer.print(" {s}\n", .{version_statement.type.slice(tree.source)}); for (version_statement.parts) |part| { try part.dump(tree, writer, indent + 1); } }, .block => { const block: *const Node.Block = @alignCast(@fieldParentPtr("base", node)); try writer.print(" {s} {s}\n", .{ block.identifier.slice(tree.source), block.key.slice(tree.source) }); for (block.values) |value| { try value.dump(tree, writer, indent + 1); } try writer.writeByteNTimes(' ', indent); try writer.writeAll(block.begin_token.slice(tree.source)); try writer.writeAll("\n"); for (block.children) |child| { try child.dump(tree, writer, indent + 1); } try writer.writeByteNTimes(' ', indent); try writer.writeAll(block.end_token.slice(tree.source)); try writer.writeAll("\n"); }, .block_value => { const block_value: *const Node.BlockValue = @alignCast(@fieldParentPtr("base", node)); try writer.print(" {s} {s}\n", .{ block_value.identifier.slice(tree.source), block_value.key.slice(tree.source) }); for (block_value.values) |value| { try value.dump(tree, writer, indent + 1); } }, .block_value_value => { const block_value: *const Node.BlockValueValue = @alignCast(@fieldParentPtr("base", node)); if (block_value.trailing_comma) { try writer.writeAll(" ,"); } try writer.writeAll("\n"); try block_value.expression.dump(tree, writer, indent + 1); }, .string_table => { const string_table: *const Node.StringTable = @alignCast(@fieldParentPtr("base", node)); try writer.print(" {s} [{d} common_resource_attributes]\n", .{ string_table.type.slice(tree.source), string_table.common_resource_attributes.len }); for (string_table.optional_statements) |statement| { try statement.dump(tree, writer, indent + 1); } try writer.writeByteNTimes(' ', indent); try writer.writeAll(string_table.begin_token.slice(tree.source)); try writer.writeAll("\n"); for (string_table.strings) |string| { try string.dump(tree, writer, indent + 1); } try writer.writeByteNTimes(' ', indent); try writer.writeAll(string_table.end_token.slice(tree.source)); try writer.writeAll("\n"); }, .string_table_string => { try writer.writeAll("\n"); const string: *const Node.StringTableString = @alignCast(@fieldParentPtr("base", node)); try string.id.dump(tree, writer, indent + 1); try writer.writeByteNTimes(' ', indent + 1); try writer.print("{s}\n", .{string.string.slice(tree.source)}); }, .language_statement => { const language: *const Node.LanguageStatement = @alignCast(@fieldParentPtr("base", node)); try writer.print(" {s}\n", .{language.language_token.slice(tree.source)}); try language.primary_language_id.dump(tree, writer, indent + 1); try language.sublanguage_id.dump(tree, writer, indent + 1); }, .font_statement => { const font: *const Node.FontStatement = @alignCast(@fieldParentPtr("base", node)); try writer.print(" {s} typeface: {s}\n", .{ font.identifier.slice(tree.source), font.typeface.slice(tree.source) }); try writer.writeByteNTimes(' ', indent + 1); try writer.writeAll("point_size:\n"); try font.point_size.dump(tree, writer, indent + 2); inline for (.{ "weight", "italic", "char_set" }) |arg| { if (@field(font, arg)) |arg_node| { try writer.writeByteNTimes(' ', indent + 1); try writer.writeAll(arg ++ ":\n"); try arg_node.dump(tree, writer, indent + 2); } } }, .simple_statement => { const statement: *const Node.SimpleStatement = @alignCast(@fieldParentPtr("base", node)); try writer.print(" {s}\n", .{statement.identifier.slice(tree.source)}); try statement.value.dump(tree, writer, indent + 1); }, .invalid => { const invalid: *const Node.Invalid = @alignCast(@fieldParentPtr("base", node)); try writer.print(" context.len: {}\n", .{invalid.context.len}); for (invalid.context) |context_token| { try writer.writeByteNTimes(' ', indent + 1); try writer.print("{s}:{s}", .{ @tagName(context_token.id), context_token.slice(tree.source) }); try writer.writeByte('\n'); } }, } } };