const std = @import("std"); const xml = @import("./xml.zig"); const Position = @import("./position.zig"); const Color = @import("./color.zig"); const Object = @This(); pub const Shape = union (Type) { pub const Type = enum { rectangle, point, ellipse, polygon, tile, // TODO: template text }; pub const Rectangle = struct { x: f32, y: f32, width: f32, height: f32, }; pub const Tile = struct { x: f32, y: f32, width: f32, height: f32, gid: u32 }; pub const Ellipse = struct { x: f32, y: f32, width: f32, height: f32, }; pub const Polygon = struct { x: f32, y: f32, points: []const Position }; pub const Text = struct { pub const Font = struct { family: []const u8, pixel_size: f32, bold: bool, italic: bool, underline: bool, strikeout: bool, kerning: bool }; pub const HorizontalAlign = enum { left, center, right, justify, const map: std.StaticStringMap(HorizontalAlign) = .initComptime(.{ .{ "left", .left }, .{ "center", .center }, .{ "right", .right }, .{ "justify", .justify }, }); }; pub const VerticalAlign = enum { top, center, bottom, const map: std.StaticStringMap(VerticalAlign) = .initComptime(.{ .{ "top", .top }, .{ "center", .center }, .{ "bottom", .bottom }, }); }; x: f32, y: f32, width: f32, height: f32, word_wrap: bool, color: Color, font: Font, horizontal_align: HorizontalAlign, vertical_align: VerticalAlign, content: []const u8 }; pub const Point = struct { x: f32, y: f32, }; rectangle: Rectangle, point: Point, ellipse: Ellipse, polygon: Polygon, tile: Tile, text: Text }; id: u32, name: []const u8, class: []const u8, rotation: f32, // TODO: maybe this field should be moved to Shape struct visible: bool, shape: Shape, pub fn initFromXml( arena: std.mem.Allocator, scratch: *std.heap.ArenaAllocator, lexer: *xml.Lexer, ) !Object { var iter = xml.TagParser.init(lexer); const attrs = try iter.begin("object"); const id = try attrs.getNumber(u32, "id") orelse return error.MissingId; const name = try attrs.getDupe(arena, "name") orelse ""; const class = try attrs.getDupe(arena, "type") orelse ""; const x = try attrs.getNumber(f32, "x") orelse 0; const y = try attrs.getNumber(f32, "y") orelse 0; const width = try attrs.getNumber(f32, "width") orelse 0; const height = try attrs.getNumber(f32, "height") orelse 0; const rotation = try attrs.getNumber(f32, "rotation") orelse 0; const visible = try attrs.getBool("visible", "1", "0") orelse true; var shape: ?Shape = null; while (try iter.next()) |node| { if (shape == null) { if (node.isTag("point")) { shape = .{ .point = Shape.Point{ .x = x, .y = y, } }; } else if (node.isTag("ellipse")) { shape = .{ .ellipse = Shape.Ellipse{ .x = x, .y = y, .width = width, .height = height } }; } else if (node.isTag("text")) { const HorizontalShape = Shape.Text.HorizontalAlign; const VerticalAlign = Shape.Text.VerticalAlign; const text_attrs = node.tag.attributes; const word_wrap = try text_attrs.getBool("wrap", "1", "0") orelse false; const color = try text_attrs.getColor("color", true) orelse Color.black; const horizontal_align = try text_attrs.getEnum(HorizontalShape, "halign", HorizontalShape.map) orelse .left; const vertical_align = try text_attrs.getEnum(VerticalAlign, "valign", VerticalAlign.map) orelse .top; const bold = try text_attrs.getBool("bold", "1", "0") orelse false; const italic = try text_attrs.getBool("italic", "1", "0") orelse false; const strikeout = try text_attrs.getBool("strikeout", "1", "0") orelse false; const underline = try text_attrs.getBool("underline", "1", "0") orelse false; const kerning = try text_attrs.getBool("kerning", "1", "0") orelse true; const pixel_size = try text_attrs.getNumber(f32, "pixelsize") orelse 16; const font_family = try text_attrs.getDupe(arena, "fontfamily") orelse "sans-serif"; _ = try lexer.nextExpectStartTag("text"); var content: []const u8 = ""; const content_value = try lexer.peek(); if (content_value != null and content_value.? == .text) { content = try arena.dupe(u8, content_value.?.text); } try lexer.skipUntilMatchingEndTag("text"); shape = .{ .text = Shape.Text{ .x = x, .y = y, .width = width, .height = height, .word_wrap = word_wrap, .color = color, .horizontal_align = horizontal_align, .vertical_align = vertical_align, .content = try arena.dupe(u8, content), .font = .{ .bold = bold, .italic = italic, .strikeout = strikeout, .underline = underline, .pixel_size = pixel_size, .kerning = kerning, .family = font_family, } } }; continue; } else if (node.isTag("polygon")) { const points_str = node.tag.attributes.get("points") orelse ""; var points: std.ArrayList(Position) = .empty; var point_iter = std.mem.splitScalar(u8, points_str, ' '); while (point_iter.next()) |point_str| { const point = try Position.parseCommaDelimited(point_str); try points.append(scratch.allocator(), point); } shape = .{ .polygon = Shape.Polygon{ .x = x, .y = y, .points = try arena.dupe(Position, points.items) } }; } } try iter.skip(); } if (shape == null) { if (try attrs.getNumber(u32, "gid")) |gid| { shape = .{ .tile = Shape.Tile{ .x = x, .y = y, .width = width, .height = height, .gid = gid } }; } else { shape = .{ .rectangle = Shape.Rectangle{ .x = x, .y = y, .width = width, .height = height } }; } } try iter.finish("object"); return Object{ .id = id, .name = name, .class = class, .rotation = rotation, .visible = visible, .shape = shape orelse return error.UnknownShapeType }; } fn expectParsedEquals(expected: Object, body: []const u8) !void { const allocator = std.testing.allocator; var ctx: xml.Lexer.TestingContext = undefined; ctx.init(allocator, body); defer ctx.deinit(); var scratch = std.heap.ArenaAllocator.init(allocator); defer scratch.deinit(); var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); const parsed = try initFromXml(arena.allocator(), &scratch, &ctx.lexer); try std.testing.expectEqualDeep(expected, parsed); } test Object { try expectParsedEquals( Object{ .id = 10, .name = "rectangle", .class = "object class", .rotation = 0, .visible = true, .shape = .{ .rectangle = .{ .x = 12.34, .y = 56.78, .width = 31.5, .height = 20.25, } } }, \\ ); try expectParsedEquals( Object{ .id = 3, .name = "point", .class = "foo", .rotation = 0, .visible = true, .shape = .{ .point = .{ .x = 77.125, .y = 99.875 } } }, \\ \\ \\ ); try expectParsedEquals( Object{ .id = 4, .name = "ellipse", .class = "", .rotation = 0, .visible = true, .shape = .{ .ellipse = .{ .x = 64.25, .y = 108.25, .width = 22.375, .height = 15.375 } } }, \\ \\ \\ ); try expectParsedEquals( Object{ .id = 5, .name = "", .class = "", .rotation = 0, .visible = true, .shape = .{ .polygon = .{ .x = 40.125, .y = 96.25, .points = &[_]Position{ .{ .x = 0, .y = 0 }, .{ .x = 13.25, .y = -4.25 }, .{ .x = 10.125, .y = 18.625 }, .{ .x = 2.25, .y = 17.375 }, .{ .x = -0.125, .y = 25.75 }, .{ .x = -3.875, .y = 20.75 }, } } } }, \\ \\ \\ ); try expectParsedEquals( Object{ .id = 2, .name = "tile", .class = "", .rotation = 0, .visible = true, .shape = .{ .tile = .{ .x = 60.125, .y = 103.5, .width = 8, .height = 8, .gid = 35 } } }, \\ ); try expectParsedEquals( Object{ .id = 6, .name = "text", .class = "", .rotation = 0, .visible = true, .shape = .{ .text = .{ .x = 64.3906, .y = 92.8594, .width = 87.7188, .height = 21.7813, .content = "Hello World", .word_wrap = true, .color = .black, .horizontal_align = .center, .vertical_align = .top, .font = .{ .family = "sans-serif", .pixel_size = 16, .bold = false, .italic = false, .underline = false, .strikeout = false, .kerning = true }, } } }, \\ \\ Hello World \\ ); }