├── test ├── cases │ ├── empty.zml │ ├── element.zml │ ├── text.zml │ ├── display │ │ ├── none.zml │ │ ├── block.zml │ │ ├── inline.zml │ │ ├── inline-block.zml │ │ ├── block │ │ │ ├── text.zml │ │ │ ├── none.zml │ │ │ ├── block.zml │ │ │ └── inline.zml │ │ └── inline_block │ │ │ ├── inline-block.zml │ │ │ ├── fixed-width.zml │ │ │ ├── text child.zml │ │ │ ├── auto-width child.zml │ │ │ ├── inline-block child.zml │ │ │ ├── fixed-width child.zml │ │ │ ├── auto-width positioned child.zml │ │ │ └── two inline-block children.zml │ ├── position │ │ ├── static.zml │ │ └── relative.zml │ ├── text │ │ ├── text.zml │ │ └── inline-text.zml │ └── background-images │ │ └── zig.zml ├── ua-stylesheet.css ├── res │ ├── zig.png │ └── NotoSans-Regular.ttf ├── Test.zig ├── print.zig ├── memory.zig ├── check.zig ├── opengl.zig └── suite.zig ├── .gitignore ├── demo ├── zig.png ├── NotoSans-Regular.ttf ├── demo.zml └── demo.css ├── .gitattributes ├── source ├── render │ ├── fragment.glsl │ ├── vertex.glsl │ └── QuadTree.zig ├── values.zig ├── unicode.zig ├── Fonts.zig ├── zss.zig ├── debug.zig ├── stack.zig ├── Images.zig ├── Layout │ ├── initial.zig │ ├── AbsoluteContainingBlocks.zig │ ├── BoxTreeManaged.zig │ ├── StackingContextTreeBuilder.zig │ ├── StyleComputer.zig │ └── solve.zig ├── meta.zig ├── Layout.zig ├── values │ ├── types.zig │ ├── parse │ │ └── background.zig │ └── groups.zig ├── math.zig ├── Stylesheet.zig ├── cascade.zig ├── Utf8StringInterner.zig └── SegmentedUtf8String.zig ├── LICENSE.md ├── README.md ├── examples └── parse.zig ├── LICENSE-OFL.txt ├── LICENSE-CC0.txt └── docs └── zml.md /test/cases/empty.zml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/cases/element.zml: -------------------------------------------------------------------------------- 1 | * {} 2 | -------------------------------------------------------------------------------- /test/cases/text.zml: -------------------------------------------------------------------------------- 1 | "Hello" 2 | -------------------------------------------------------------------------------- /test/cases/display/none.zml: -------------------------------------------------------------------------------- 1 | * (display: none) {} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache 2 | zig-out 3 | test/output/ 4 | -------------------------------------------------------------------------------- /test/cases/display/block.zml: -------------------------------------------------------------------------------- 1 | * (display: block) {} 2 | -------------------------------------------------------------------------------- /test/cases/display/inline.zml: -------------------------------------------------------------------------------- 1 | * (display: inline) {} 2 | -------------------------------------------------------------------------------- /test/cases/position/static.zml: -------------------------------------------------------------------------------- 1 | * (position: static) {} 2 | -------------------------------------------------------------------------------- /test/cases/text/text.zml: -------------------------------------------------------------------------------- 1 | * { 2 | "Hello" 3 | } 4 | -------------------------------------------------------------------------------- /test/cases/position/relative.zml: -------------------------------------------------------------------------------- 1 | * (position: relative) {} 2 | -------------------------------------------------------------------------------- /test/ua-stylesheet.css: -------------------------------------------------------------------------------- 1 | :root { 2 | color: #fff; 3 | } 4 | -------------------------------------------------------------------------------- /test/cases/display/inline-block.zml: -------------------------------------------------------------------------------- 1 | * (display: inline-block) {} 2 | -------------------------------------------------------------------------------- /demo/zig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chadwain/zss/HEAD/demo/zig.png -------------------------------------------------------------------------------- /test/cases/display/block/text.zml: -------------------------------------------------------------------------------- 1 | * (display: block) { 2 | "Hello" 3 | } 4 | -------------------------------------------------------------------------------- /test/res/zig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chadwain/zss/HEAD/test/res/zig.png -------------------------------------------------------------------------------- /test/cases/display/block/none.zml: -------------------------------------------------------------------------------- 1 | * (display: block) { 2 | * (display: none) {} 3 | } 4 | -------------------------------------------------------------------------------- /demo/NotoSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chadwain/zss/HEAD/demo/NotoSans-Regular.ttf -------------------------------------------------------------------------------- /test/cases/display/block/block.zml: -------------------------------------------------------------------------------- 1 | * (display: block) { 2 | * (display: block) {} 3 | } 4 | -------------------------------------------------------------------------------- /test/cases/display/block/inline.zml: -------------------------------------------------------------------------------- 1 | * (display: block) { 2 | * (display: inline) {} 3 | } 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.md text eol=lf 2 | *.txt text eol=lf 3 | *.zig text eol=lf 4 | *.zon text eol=lf 5 | -------------------------------------------------------------------------------- /test/cases/display/inline_block/inline-block.zml: -------------------------------------------------------------------------------- 1 | * { 2 | * (display: inline-block) {} 3 | } 4 | -------------------------------------------------------------------------------- /test/cases/text/inline-text.zml: -------------------------------------------------------------------------------- 1 | * { 2 | * (display: inline) { 3 | "Hello" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/res/NotoSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chadwain/zss/HEAD/test/res/NotoSans-Regular.ttf -------------------------------------------------------------------------------- /test/cases/display/inline_block/fixed-width.zml: -------------------------------------------------------------------------------- 1 | * { 2 | * (display: inline-block; width: 100px) {} 3 | } 4 | -------------------------------------------------------------------------------- /test/cases/display/inline_block/text child.zml: -------------------------------------------------------------------------------- 1 | * { 2 | * (display: inline-block) { 3 | "Hello" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/cases/display/inline_block/auto-width child.zml: -------------------------------------------------------------------------------- 1 | * { 2 | * (display: inline-block) { 3 | * (display: block) {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/cases/display/inline_block/inline-block child.zml: -------------------------------------------------------------------------------- 1 | * { 2 | * (display: inline-block) { 3 | * (display: inline-block) {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/cases/display/inline_block/fixed-width child.zml: -------------------------------------------------------------------------------- 1 | * { 2 | * (display: inline-block) { 3 | * (display: block; width: 100px) {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/cases/background-images/zig.zml: -------------------------------------------------------------------------------- 1 | * (display: block; width: 250px; height: 250px; background-image: src("zig.png"); background-size: contain) { 2 | "Take off every Zig" 3 | } 4 | -------------------------------------------------------------------------------- /test/cases/display/inline_block/auto-width positioned child.zml: -------------------------------------------------------------------------------- 1 | * { 2 | * (display: inline-block) { 3 | * (display: block; position: relative) {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/cases/display/inline_block/two inline-block children.zml: -------------------------------------------------------------------------------- 1 | * { 2 | * (display: inline-block) { 3 | * (display: inline-block) {} 4 | * (display: inline-block) {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /source/render/fragment.glsl: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | uniform sampler2D Texture; 4 | 5 | flat in vec4 Color; 6 | in vec2 TexCoords; 7 | 8 | layout(location = 0) out vec4 color; 9 | 10 | void main() 11 | { 12 | color = texture(Texture, TexCoords) * Color; 13 | } 14 | -------------------------------------------------------------------------------- /source/values.zig: -------------------------------------------------------------------------------- 1 | pub const groups = @import("values/groups.zig"); 2 | pub const parse = @import("values/parse.zig"); 3 | pub const types = @import("values/types.zig"); 4 | 5 | comptime { 6 | if (@import("builtin").is_test) { 7 | _ = parse; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/Test.zig: -------------------------------------------------------------------------------- 1 | const Test = @This(); 2 | 3 | const zss = @import("zss"); 4 | 5 | name: []const u8, 6 | document: zss.zml.Document, 7 | stylesheet: zss.Stylesheet, 8 | images: *const zss.Images, 9 | fonts: *const zss.Fonts, 10 | font_handle: zss.Fonts.Handle, 11 | 12 | width: u32 = 400, 13 | height: u32 = 400, 14 | -------------------------------------------------------------------------------- /demo/demo.zml: -------------------------------------------------------------------------------- 1 | * { 2 | header { 3 | * { 4 | @name(title) "File name goes here" 5 | } 6 | } 7 | #nuisance (display: block; width: 500px; height: 500px; background-color: #f0f) {} 8 | main { 9 | * { 10 | @name(body) "File contents go here" 11 | } 12 | } 13 | footer (background-image: src("zig.png")) {} 14 | } 15 | -------------------------------------------------------------------------------- /source/unicode.zig: -------------------------------------------------------------------------------- 1 | pub fn latin1ToLowercase(codepoint: u21) u21 { 2 | return switch (codepoint) { 3 | 'A'...'Z' => codepoint - 'A' + 'a', 4 | else => codepoint, 5 | }; 6 | } 7 | 8 | pub fn hexDigitToNumber(codepoint: u21) !u4 { 9 | return switch (codepoint) { 10 | '0'...'9' => @intCast(codepoint - '0'), 11 | 'A'...'F' => @intCast(codepoint - 'A' + 10), 12 | 'a'...'f' => @intCast(codepoint - 'a' + 10), 13 | else => return error.InvalidCodepoint, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /source/render/vertex.glsl: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | layout(location = 0) in ivec2 position; 4 | layout(location = 1) in vec4 color; 5 | layout(location = 2) in vec2 tex_coords; 6 | 7 | uniform ivec2 viewport; 8 | uniform ivec2 translation; 9 | 10 | const mat4 projection = mat4( 11 | vec4(2.0, 0.0, 0.0, -1.0), 12 | vec4(0.0, -2.0, 0.0, 1.0), 13 | vec4(0.0, 0.0, 0.0, 0.0), 14 | vec4(0.0, 0.0, 0.0, 1.0) 15 | ); 16 | 17 | flat out vec4 Color; 18 | out vec2 TexCoords; 19 | 20 | void main() 21 | { 22 | gl_Position = vec4(vec2(position + translation) / vec2(viewport), 0.0, 1.0) * projection; 23 | Color = color; 24 | TexCoords = tex_coords; 25 | } 26 | -------------------------------------------------------------------------------- /source/Fonts.zig: -------------------------------------------------------------------------------- 1 | const Fonts = @This(); 2 | 3 | const hb = @import("harfbuzz").c; 4 | 5 | pub const Handle = enum { invalid, the_only_handle }; 6 | 7 | /// Externally managed. 8 | the_only_font: ?*hb.hb_font_t, 9 | 10 | pub fn init() Fonts { 11 | return .{ .the_only_font = null }; 12 | } 13 | 14 | pub fn deinit(fonts: *Fonts) void { 15 | _ = fonts; 16 | } 17 | 18 | pub fn setFont(fonts: *Fonts, font: *hb.hb_font_t) Handle { 19 | fonts.the_only_font = font; 20 | return .the_only_handle; 21 | } 22 | 23 | pub fn query(fonts: Fonts) Handle { 24 | return if (fonts.the_only_font) |_| .the_only_handle else .invalid; 25 | } 26 | 27 | pub fn get(fonts: Fonts, handle: Handle) ?*hb.hb_font_t { 28 | return switch (handle) { 29 | .invalid => null, 30 | .the_only_handle => fonts.the_only_font.?, 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Chadwain Holness is the author of this software for the purpose of copyright. 2 | 3 | All files are marked with CC0 1.0 Universal unless otherwise noted. To view a copy of this license, see [LICENSE-CC0](LICENSE-CC0.txt), or visit [creativecommons.org](https://creativecommons.org/publicdomain/zero/1.0/). 4 | 5 | All font files from the [Noto Fonts project](https://notofonts.github.io/) are redistributed under the terms of the SIL Open Font License. To view a copy of this license, see [LICENSE-OFL](LICENSE-OFL.txt), or visit [openfontlicense.org](https://openfontlicense.org/open-font-license-official-text/). 6 | 7 | The [Zig Project Logo](https://github.com/ziglang/logo) by the [Zig Project](https://ziglang.org) is licensed under CC BY-SA 4.0. To view a copy of this license, visit [creativecommons.org](https://creativecommons.org/licenses/by-sa/4.0/). 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zss 2 | zss is a [CSS](https://www.w3.org/Style/CSS/) layout engine and document renderer, written in [Zig](https://ziglang.org/). 3 | 4 | # Building zss 5 | To build zss, simply run `zig build --help` to see your options. 6 | 7 | zss uses version 0.15.2 of the zig compiler. 8 | 9 | # Standards Implemented 10 | In general, zss tries to implement the standards contained in [CSS Snapshot 2023](https://www.w3.org/TR/css-2023/). 11 | 12 | | Module | Level | Progress | 13 | | ------ | ----- | ----- | 14 | | CSS Level 2 | 2.2 | Partial | 15 | | Syntax | 3 | Partial | 16 | | Selectors | 3 | Partial | 17 | | Cascading and Inheritance | 4 | Partial | 18 | | Backgrounds and Borders | 3 | Partial | 19 | | Values and Units | 3 | Partial | 20 | | Namespaces | 3 | Partial | 21 | 22 | # License 23 | See [LICENSE.md](LICENSE.md) for detailed licensing information. 24 | -------------------------------------------------------------------------------- /demo/demo.css: -------------------------------------------------------------------------------- 1 | :root { 2 | display: block; 3 | min-width: 200px; 4 | padding: 30px; 5 | border-width: 10px; 6 | border-style: solid; 7 | border-color: #af2233; 8 | background-color: #efefef; 9 | color: #101010; 10 | } 11 | 12 | header { 13 | display: block; 14 | position: relative; 15 | z-index: -1; 16 | border-bottom-width: 2px solid #202020; 17 | margin-bottom: 24px; 18 | } 19 | 20 | header > * { 21 | display: inline; 22 | padding: 0px 10px 5px 10px; 23 | border-width: 10px; 24 | border-style: solid; 25 | border-top-color: #aa1010; 26 | border-right-color: #10aa10; 27 | border-bottom-color: #504090; 28 | border-left-color: #1010aa; 29 | background-color: #fa58007f; 30 | } 31 | 32 | main { 33 | display: block; 34 | } 35 | 36 | main > * { 37 | display: inline; 38 | color: #1010507f; 39 | } 40 | 41 | footer { 42 | display: block; 43 | height: 200px; 44 | border-width: inherit; 45 | border-style: inherit; 46 | border-color: inherit; 47 | margin-top: 10px; 48 | background-clip: padding-box; 49 | background-position: 50% 50%; 50 | background-repeat: space no-repeat; 51 | background-size: contain; 52 | } 53 | 54 | #nuisance { 55 | display: none !important; 56 | } 57 | -------------------------------------------------------------------------------- /source/zss.zig: -------------------------------------------------------------------------------- 1 | pub const cascade = @import("cascade.zig"); 2 | pub const debug = @import("debug.zig"); 3 | pub const math = @import("math.zig"); 4 | pub const meta = @import("meta.zig"); 5 | pub const property = @import("property.zig"); 6 | pub const selectors = @import("selectors.zig"); 7 | pub const syntax = @import("syntax.zig"); 8 | pub const unicode = @import("unicode.zig"); 9 | pub const values = @import("values.zig"); 10 | pub const zml = @import("zml.zig"); 11 | 12 | pub const BoxTree = @import("BoxTree.zig"); 13 | pub const Declarations = @import("Declarations.zig"); 14 | pub const Environment = @import("Environment.zig"); 15 | pub const Fonts = @import("Fonts.zig"); 16 | pub const Images = @import("Images.zig"); 17 | pub const Layout = @import("Layout.zig"); 18 | pub const OpenGlFreetypeRenderer = @import("render/opengl_freetype.zig").Renderer; 19 | pub const Stack = @import("stack.zig").Stack; 20 | pub const SegmentedUtf8String = @import("SegmentedUtf8String.zig"); 21 | pub const Stylesheet = @import("Stylesheet.zig"); 22 | pub const Utf8StringInterner = @import("Utf8StringInterner.zig"); 23 | 24 | pub const log = @import("std").log.scoped(.zss); 25 | 26 | comptime { 27 | if (@import("builtin").is_test) { 28 | @import("std").testing.refAllDecls(@This()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/print.zig: -------------------------------------------------------------------------------- 1 | const zss = @import("zss"); 2 | 3 | const std = @import("std"); 4 | const assert = std.debug.assert; 5 | 6 | const Test = @import("./Test.zig"); 7 | 8 | pub fn run(tests: []const *Test, _: []const u8) !void { 9 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 10 | defer assert(gpa.deinit() == .ok); 11 | const allocator = gpa.allocator(); 12 | 13 | var stdout_buffer: [8192]u8 = undefined; 14 | var stdout_writer = std.fs.File.stdout().writerStreaming(&stdout_buffer); 15 | const stdout = &stdout_writer.interface; 16 | 17 | for (tests, 0..) |t, i| { 18 | try stdout.print("print: ({}/{}) \"{s}\" ... \n", .{ i + 1, tests.len, t.name }); 19 | try stdout.flush(); 20 | 21 | var layout = zss.Layout.init( 22 | &t.document.env, 23 | allocator, 24 | t.width, 25 | t.height, 26 | t.images, 27 | t.fonts, 28 | ); 29 | defer layout.deinit(); 30 | 31 | var box_tree = try layout.run(allocator); 32 | defer box_tree.deinit(); 33 | try box_tree.debug.print(stdout, allocator); 34 | 35 | try stdout.writeAll("\n"); 36 | try stdout.flush(); 37 | } 38 | 39 | try stdout.print("print: all {} tests passed\n", .{tests.len}); 40 | try stdout.flush(); 41 | } 42 | -------------------------------------------------------------------------------- /source/debug.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const zss = @import("zss.zig"); 4 | const Stack = zss.Stack; 5 | 6 | pub const runtime_safety = switch (@import("builtin").mode) { 7 | .Debug, .ReleaseSafe => true, 8 | .ReleaseFast, .ReleaseSmall => false, 9 | }; 10 | 11 | /// Iterate over an array of skips, while also being given the depth of each element. 12 | pub fn skipArrayIterate( 13 | comptime Size: type, 14 | skips: []const Size, 15 | context: anytype, 16 | comptime callback: fn (@TypeOf(context), index: Size, depth: Size) anyerror!void, 17 | allocator: std.mem.Allocator, 18 | ) !void { 19 | if (skips.len == 0) return; 20 | 21 | const Interval = struct { 22 | begin: Size, 23 | end: Size, 24 | }; 25 | 26 | var stack: Stack(Interval) = .{}; 27 | defer stack.deinit(allocator); 28 | stack.top = .{ .begin = 0, .end = skips[0] }; 29 | 30 | while (stack.top) |*top| { 31 | const index = index: { 32 | if (top.begin == top.end) { 33 | _ = stack.pop(); 34 | continue; 35 | } 36 | defer top.begin += skips[top.begin]; 37 | break :index top.begin; 38 | }; 39 | try callback(context, index, @intCast(stack.lenExcludingTop())); 40 | const skip = skips[index]; 41 | if (skip != 1) { 42 | try stack.push(allocator, .{ .begin = index + 1, .end = index + skip }); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/memory.zig: -------------------------------------------------------------------------------- 1 | const zss = @import("zss"); 2 | const ElementTree = zss.ElementTree; 3 | const Element = ElementTree.Element; 4 | 5 | const std = @import("std"); 6 | const assert = std.debug.assert; 7 | 8 | const Test = @import("./Test.zig"); 9 | 10 | pub fn run(tests: []const *Test, _: []const u8) !void { 11 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 12 | defer assert(gpa.deinit() == .ok); 13 | const allocator = gpa.allocator(); 14 | 15 | var stdout_buffer: [200]u8 = undefined; 16 | var stdout_writer = std.fs.File.stdout().writerStreaming(&stdout_buffer); 17 | const stdout = &stdout_writer.interface; 18 | 19 | for (tests, 0..) |t, i| { 20 | try stdout.print("memory: ({}/{}) \"{s}\" ... ", .{ i + 1, tests.len, t.name }); 21 | try stdout.flush(); 22 | 23 | try std.testing.checkAllAllocationFailures(allocator, testFn, .{ 24 | &t.document.env, 25 | t.width, 26 | t.height, 27 | t.images, 28 | t.fonts, 29 | }); 30 | 31 | try stdout.writeAll("success\n"); 32 | try stdout.flush(); 33 | } 34 | 35 | try stdout.print("memory: all {} tests passed\n", .{tests.len}); 36 | try stdout.flush(); 37 | } 38 | 39 | fn testFn( 40 | allocator: std.mem.Allocator, 41 | env: *const zss.Environment, 42 | width: u32, 43 | height: u32, 44 | images: *const zss.Images, 45 | fonts: *const zss.Fonts, 46 | ) !void { 47 | var layout = zss.Layout.init(env, allocator, width, height, images, fonts); 48 | defer layout.deinit(); 49 | 50 | var box_tree = try layout.run(allocator); 51 | defer box_tree.deinit(); 52 | } 53 | -------------------------------------------------------------------------------- /source/stack.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | const ArrayListUnmanaged = std.ArrayListUnmanaged; 4 | 5 | /// A stack data structure, where the top-most value is stored on the (program) stack. 6 | pub fn Stack(comptime T: type) type { 7 | return struct { 8 | /// The top of the stack. When this is null, the stack is completely empty. 9 | top: ?T = null, 10 | /// The rest of the stack. Items lower in the stack are earlier in the list. 11 | rest: ArrayListUnmanaged(T) = .empty, 12 | 13 | pub const Item = T; 14 | 15 | pub fn init(top: T) Stack(T) { 16 | return .{ .top = top }; 17 | } 18 | 19 | pub fn deinit(stack: *Stack(T), allocator: Allocator) void { 20 | stack.rest.deinit(allocator); 21 | } 22 | 23 | pub fn lenExcludingTop(stack: Stack(T)) usize { 24 | std.debug.assert(stack.top != null); 25 | return stack.rest.items.len; 26 | } 27 | 28 | /// Causes `new_top` to become the new highest item in the stack. 29 | /// This function must only be called after setting `stack.top` to a non-null value. 30 | pub fn push(stack: *Stack(T), allocator: Allocator, new_top: T) !void { 31 | try stack.rest.append(allocator, stack.top.?); 32 | stack.top = new_top; 33 | } 34 | 35 | /// Returns the current value of `stack.top`, and replaces it with the last value from `stack.rest`. 36 | /// If there is nothing in `stack.rest`, then `stack.top` becomes null. 37 | /// This function must not be called if `stack.top` is already null. 38 | pub fn pop(stack: *Stack(T)) T { 39 | defer stack.top = stack.rest.pop(); 40 | return stack.top.?; 41 | } 42 | 43 | pub fn clear(stack: *Stack(T)) void { 44 | stack.rest.clearRetainingCapacity(); 45 | stack.top = null; 46 | } 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /source/Images.zig: -------------------------------------------------------------------------------- 1 | const Images = @This(); 2 | 3 | const std = @import("std"); 4 | const Allocator = std.mem.Allocator; 5 | 6 | pub const Handle = enum(u32) { _ }; 7 | 8 | pub const Description = struct { 9 | dimensions: Dimensions, 10 | format: Format, 11 | /// Externally managed image data. 12 | /// Use `null` to signal that the data is not available. 13 | data: ?[]const u8, 14 | }; 15 | 16 | pub const Dimensions = struct { 17 | width_px: u32, 18 | height_px: u32, 19 | }; 20 | 21 | pub const Format = enum { 22 | rgba, 23 | }; 24 | 25 | descriptions: std.MultiArrayList(Description).Slice, 26 | 27 | pub fn init() Images { 28 | return .{ 29 | .descriptions = .empty, 30 | }; 31 | } 32 | 33 | pub fn deinit(images: *Images, allocator: Allocator) void { 34 | images.descriptions.deinit(allocator); 35 | } 36 | 37 | pub fn addImage(images: *Images, allocator: Allocator, desc: Description) !Handle { 38 | var list = images.descriptions.toMultiArrayList(); 39 | defer images.descriptions = list.slice(); 40 | 41 | const handle = images.nextHandle() orelse return error.OutOfImages; 42 | try list.append(allocator, desc); 43 | return handle; 44 | } 45 | 46 | fn nextHandle(images: Images) ?Handle { 47 | if (images.descriptions.len == std.math.maxInt(std.meta.Tag(Handle))) return null; 48 | return @enumFromInt(images.descriptions.len); 49 | } 50 | 51 | pub fn dimensions(images: *const Images, handle: Handle) Dimensions { 52 | return images.descriptions.items(.dimensions)[@intFromEnum(handle)]; 53 | } 54 | 55 | pub fn format(images: *const Images, handle: Handle) Format { 56 | return images.descriptions.items(.format)[@intFromEnum(handle)]; 57 | } 58 | 59 | pub fn data(images: *const Images, handle: Handle) []const u8 { 60 | return images.descriptions.items(.data)[@intFromEnum(handle)]; 61 | } 62 | 63 | pub fn get(images: *const Images, handle: Handle) Description { 64 | return images.descriptions.get(@intFromEnum(handle)); 65 | } 66 | -------------------------------------------------------------------------------- /source/Layout/initial.zig: -------------------------------------------------------------------------------- 1 | const zss = @import("../zss.zig"); 2 | const BoxTree = zss.BoxTree; 3 | const NodeId = zss.Environment.NodeId; 4 | const StyleComputer = zss.Layout.StyleComputer; 5 | 6 | const BoxGen = zss.Layout.BoxGen; 7 | const SctBuilder = BoxGen.StackingContextTreeBuilder; 8 | 9 | const flow = @import("./flow.zig"); 10 | 11 | pub fn beginMode(box_gen: *BoxGen) !void { 12 | const layout = box_gen.getLayout(); 13 | try box_gen.pushInitialSubtree(); 14 | const ref = try box_gen.pushInitialContainingBlock(layout.viewport); 15 | layout.box_tree.ptr.initial_containing_block = ref; 16 | } 17 | 18 | fn endMode(box_gen: *BoxGen) void { 19 | box_gen.popInitialContainingBlock(); 20 | box_gen.popSubtree(); 21 | } 22 | 23 | pub fn blockElement(box_gen: *BoxGen, node: NodeId, inner_block: BoxTree.BoxStyle.InnerBlock, position: BoxTree.BoxStyle.Position) !void { 24 | const layout = box_gen.getLayout(); 25 | const sizes = flow.solveAllSizes(&layout.computer, position, .{ .normal = layout.viewport.w }, layout.viewport.h); 26 | const stacking_context = rootBlockSolveStackingContext(&layout.computer); 27 | layout.computer.commitNode(.box_gen); 28 | 29 | switch (inner_block) { 30 | .flow => { 31 | const ref = try box_gen.pushFlowBlock(sizes, .normal, stacking_context, node); 32 | try layout.box_tree.setGeneratedBox(node, .{ .block_ref = ref }); 33 | try layout.pushNode(); 34 | return box_gen.beginFlowMode(.root); 35 | }, 36 | } 37 | } 38 | 39 | pub fn nullNode(box_gen: *BoxGen) void { 40 | endMode(box_gen); 41 | } 42 | 43 | pub fn afterFlowMode(box_gen: *BoxGen) void { 44 | box_gen.popFlowBlock(.normal); 45 | box_gen.getLayout().popNode(); 46 | } 47 | 48 | pub fn afterStfMode() noreturn { 49 | unreachable; 50 | } 51 | 52 | pub fn beforeInlineMode() BoxGen.SizeMode { 53 | return .normal; 54 | } 55 | 56 | pub fn afterInlineMode() void {} 57 | 58 | fn rootBlockSolveStackingContext(computer: *StyleComputer) SctBuilder.Type { 59 | const z_index = computer.getSpecifiedValue(.box_gen, .z_index); 60 | computer.setComputedValue(.box_gen, .z_index, z_index); 61 | // TODO: Use z-index? 62 | return .{ .parentable = 0 }; 63 | } 64 | -------------------------------------------------------------------------------- /examples/parse.zig: -------------------------------------------------------------------------------- 1 | const zss = @import("zss"); 2 | const Ast = zss.syntax.Ast; 3 | const Parser = zss.syntax.Parser; 4 | const SourceCode = zss.syntax.SourceCode; 5 | 6 | const std = @import("std"); 7 | const Allocator = std.mem.Allocator; 8 | 9 | pub fn main() !u8 { 10 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 11 | defer _ = gpa.deinit(); 12 | const allocator = gpa.allocator(); 13 | 14 | const args = try std.process.argsAlloc(allocator); 15 | defer std.process.argsFree(allocator, args); 16 | 17 | if (args.len > 2) return 1; 18 | 19 | var stdout_buffer: [1024]u8 = undefined; 20 | var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); 21 | const stdout = &stdout_writer.interface; 22 | 23 | var stderr_buffer: [1024]u8 = undefined; 24 | var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer); 25 | const stderr = &stderr_writer.interface; 26 | 27 | const input = blk: { 28 | var stdin_buffer: [1024]u8 = undefined; 29 | var stdin_reader = std.fs.File.stdin().readerStreaming(&stdin_buffer); 30 | break :blk try stdin_reader.interface.allocRemaining(allocator, .limited(1_000_000)); 31 | }; 32 | defer allocator.free(input); 33 | const source_code = try SourceCode.init(input); 34 | 35 | if (args.len == 1 or std.mem.eql(u8, args[1], "stylesheet")) { 36 | try runParser(Parser.parseCssStylesheet, source_code, allocator, stdout, stderr); 37 | } else if (std.mem.eql(u8, args[1], "zml")) { 38 | try runParser(Parser.parseZmlDocument, source_code, allocator, stdout, stderr); 39 | } else if (std.mem.eql(u8, args[1], "components")) { 40 | try runParser(Parser.parseListOfComponentValues, source_code, allocator, stdout, stderr); 41 | } else if (std.mem.eql(u8, args[1], "tokens")) { 42 | var tokenizer = zss.syntax.Tokenizer.init(source_code); 43 | var index: usize = 0; 44 | while (try tokenizer.next()) |item| : (index += 1) { 45 | const token, _ = item; 46 | try stdout.print("{}: {s}\n", .{ index, @tagName(token) }); 47 | } 48 | try stdout.flush(); 49 | } else { 50 | return 1; 51 | } 52 | 53 | return 0; 54 | } 55 | 56 | fn runParser( 57 | parse_fn: *const fn (*Parser, Allocator) Parser.Error!struct { Ast, Ast.Index }, 58 | source_code: SourceCode, 59 | allocator: Allocator, 60 | stdout: *std.Io.Writer, 61 | stderr: *std.Io.Writer, 62 | ) !void { 63 | var parser = zss.syntax.Parser.init(source_code, allocator); 64 | defer parser.deinit(); 65 | 66 | var ast, _ = parse_fn(&parser, allocator) catch |err| { 67 | switch (err) { 68 | error.ParseError => { 69 | try stderr.print("error at location {}: {s}\n", .{ @intFromEnum(parser.failure.location), parser.failure.cause.debugErrMsg() }); 70 | try stderr.flush(); 71 | }, 72 | else => {}, 73 | } 74 | return err; 75 | }; 76 | defer ast.deinit(allocator); 77 | 78 | try ast.debug.print(allocator, stdout); 79 | try stdout.flush(); 80 | } 81 | -------------------------------------------------------------------------------- /test/check.zig: -------------------------------------------------------------------------------- 1 | const zss = @import("zss"); 2 | const BoxTree = zss.BoxTree; 3 | 4 | const std = @import("std"); 5 | const assert = std.debug.assert; 6 | const expect = std.testing.expect; 7 | const Allocator = std.mem.Allocator; 8 | 9 | const Test = @import("./Test.zig"); 10 | 11 | pub fn run(tests: []const *Test, _: []const u8) !void { 12 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 13 | defer assert(gpa.deinit() == .ok); 14 | const allocator = gpa.allocator(); 15 | 16 | var stdout_buffer: [200]u8 = undefined; 17 | var stdout_writer = std.fs.File.stdout().writerStreaming(&stdout_buffer); 18 | const stdout = &stdout_writer.interface; 19 | 20 | for (tests, 0..) |t, i| { 21 | try stdout.print("check: ({}/{}) \"{s}\" ... ", .{ i + 1, tests.len, t.name }); 22 | try stdout.flush(); 23 | 24 | var layout = zss.Layout.init( 25 | &t.document.env, 26 | allocator, 27 | t.width, 28 | t.height, 29 | t.images, 30 | t.fonts, 31 | ); 32 | defer layout.deinit(); 33 | 34 | var box_tree = try layout.run(allocator); 35 | defer box_tree.deinit(); 36 | 37 | try validateStackingContexts(&box_tree, allocator); 38 | for (box_tree.ifcs.items) |ifc| { 39 | try validateInline(ifc, allocator); 40 | } 41 | 42 | try stdout.writeAll("success\n"); 43 | try stdout.flush(); 44 | } 45 | 46 | try stdout.print("check: all {} tests passed\n", .{tests.len}); 47 | try stdout.flush(); 48 | } 49 | 50 | fn validateInline(inl: *BoxTree.InlineFormattingContext, allocator: Allocator) !void { 51 | @setRuntimeSafety(true); 52 | const Index = BoxTree.InlineFormattingContext.Size; 53 | const glyphs = inl.glyphs.items(.index); 54 | 55 | var stack = std.ArrayList(Index).empty; 56 | defer stack.deinit(allocator); 57 | var i: usize = 0; 58 | while (i < glyphs.len) : (i += 1) { 59 | if (glyphs[i] == 0) { 60 | i += 1; 61 | const special = BoxTree.InlineFormattingContext.Special.decode(glyphs[i]); 62 | switch (special.kind) { 63 | .BoxStart => stack.append(allocator, @as(Index, special.data)) catch unreachable, 64 | .BoxEnd => _ = stack.pop(), 65 | else => {}, 66 | } 67 | } 68 | } 69 | try expect(stack.items.len == 0); 70 | } 71 | 72 | fn validateStackingContexts(box_tree: *zss.BoxTree, allocator: Allocator) !void { 73 | @setRuntimeSafety(true); 74 | const Size = BoxTree.StackingContextTree.Size; 75 | const ZIndex = BoxTree.ZIndex; 76 | 77 | const view = box_tree.sct.view(); 78 | if (view.len == 0) return; 79 | const skips = view.items(.skip); 80 | const z_indeces = view.items(.z_index); 81 | 82 | var stack = std.ArrayList(struct { current: Size, end: Size }).empty; 83 | defer stack.deinit(allocator); 84 | 85 | try expect(z_indeces[0] == 0); 86 | stack.append(allocator, .{ .current = 0, .end = skips[0] }) catch unreachable; 87 | while (stack.items.len > 0) { 88 | const parent = stack.pop().?; 89 | var child = parent.current + 1; 90 | var previous_z_index: ZIndex = std.math.minInt(ZIndex); 91 | while (child < parent.end) : (child += skips[child]) { 92 | const z_index = z_indeces[child]; 93 | try expect(previous_z_index <= z_index); 94 | previous_z_index = z_index; 95 | stack.append(allocator, .{ .current = child, .end = child + skips[child] }) catch unreachable; 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /source/Layout/AbsoluteContainingBlocks.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | 4 | const zss = @import("../zss.zig"); 5 | const BlockRef = zss.BoxTree.BlockRef; 6 | const BoxStyle = zss.BoxTree.BoxStyle; 7 | const NodeId = zss.Environment.NodeId; 8 | const Position = zss.values.types.Position; 9 | 10 | const Absolute = @This(); 11 | 12 | containing_block_tag: std.ArrayListUnmanaged(Tag) = .{}, 13 | containing_block_index: std.ArrayListUnmanaged(u32) = .{}, 14 | current_containing_block_index: u32 = undefined, 15 | 16 | containing_blocks: std.MultiArrayList(ContainingBlock) = .{}, 17 | next_containing_block_id: std.meta.Tag(ContainingBlock.Id) = 0, 18 | 19 | blocks: std.ArrayListUnmanaged(Block) = .{}, 20 | 21 | pub fn deinit(absolute: *Absolute, allocator: Allocator) void { 22 | absolute.containing_block_tag.deinit(allocator); 23 | absolute.containing_block_index.deinit(allocator); 24 | absolute.containing_blocks.deinit(allocator); 25 | } 26 | 27 | pub const ContainingBlock = struct { 28 | id: Id, 29 | ref: BlockRef, 30 | 31 | pub const Id = enum(u32) { _ }; 32 | }; 33 | 34 | pub const Block = struct { 35 | containing_block: ContainingBlock.Id, 36 | node: NodeId, 37 | inner_box_style: BoxStyle.InnerBlock, 38 | }; 39 | 40 | const Tag = enum { 41 | none, 42 | exists, 43 | }; 44 | 45 | pub fn pushContainingBlock(absolute: *Absolute, allocator: Allocator, box_style: BoxStyle, ref: BlockRef) !?ContainingBlock.Id { 46 | switch (box_style.position) { 47 | .static => { 48 | try absolute.containing_block_tag.append(allocator, .none); 49 | return null; 50 | }, 51 | .relative, .absolute => return try absolute.newContainingBlock(allocator, ref), 52 | } 53 | } 54 | 55 | pub const pushInitialContainingBlock = newContainingBlock; 56 | 57 | fn newContainingBlock(absolute: *Absolute, allocator: Allocator, ref: BlockRef) !ContainingBlock.Id { 58 | const id: ContainingBlock.Id = @enumFromInt(absolute.next_containing_block_id); 59 | const index: u32 = @intCast(absolute.containing_blocks.len); 60 | try absolute.containing_block_tag.append(allocator, .exists); 61 | try absolute.containing_block_index.append(allocator, index); 62 | try absolute.containing_blocks.append(allocator, .{ .id = id, .ref = ref }); 63 | absolute.current_containing_block_index = index; 64 | absolute.next_containing_block_id += 1; 65 | return id; 66 | } 67 | 68 | pub fn popContainingBlock(absolute: *Absolute) void { 69 | const tag = absolute.containing_block_tag.pop(); 70 | if (tag == .none) return; 71 | _ = absolute.containing_block_index.pop(); 72 | 73 | if (absolute.containing_block_tag.items.len > 0) { 74 | absolute.current_containing_block_index = absolute.containing_block_index.items[absolute.containing_block_index.items.len - 1]; 75 | } else { 76 | absolute.current_containing_block_index = undefined; 77 | } 78 | } 79 | 80 | pub fn fixupContainingBlock(absolute: *Absolute, id: ContainingBlock.Id, ref: BlockRef) void { 81 | const slice = absolute.containing_blocks.slice(); 82 | const index: u32 = @intCast(std.mem.indexOfScalar(ContainingBlock.Id, slice.items(.id), id).?); 83 | slice.items(.ref)[index] = ref; 84 | } 85 | 86 | pub fn addBlock(absolute: *Absolute, allocator: Allocator, node: NodeId, inner_box_style: BoxStyle.InnerBlock) !void { 87 | const index = absolute.current_containing_block_index; 88 | const id = absolute.containing_blocks.items[index].id; 89 | try absolute.blocks.append(allocator, .{ .containing_block = id, .node = node, .inner_box_style = inner_box_style }); 90 | } 91 | -------------------------------------------------------------------------------- /source/meta.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | /// Let `From = @TypeOf(from)`. 4 | /// `From` is coercible to an enum `To` if for every field of `From`, at least one of the following is true: 5 | /// 1. there is a field in `To` with the same name and value 6 | /// 2. `To` is non-exhaustive and does not have a field with the same name 7 | /// 8 | /// `from` can be either an enum or tagged union. 9 | pub fn coerceEnum(comptime To: type, from: anytype) To { 10 | comptime { 11 | const to_info = @typeInfo(To).@"enum"; 12 | @setEvalBranchQuota(to_info.fields.len * 1000); 13 | const From = @TypeOf(from); 14 | const from_fields = switch (@typeInfo(From)) { 15 | .@"enum" => |@"enum"| @"enum".fields, 16 | .@"union" => |@"union"| @typeInfo(@"union".tag_type.?).@"enum".fields, 17 | else => unreachable, 18 | }; 19 | for (from_fields) |field| { 20 | if (@hasField(To, field.name)) { 21 | const to_value = @intFromEnum(@field(To, field.name)); 22 | if (field.value != to_value) { 23 | @compileError(std.fmt.comptimePrint( 24 | "{s}.{s} has value {}, expected {}", 25 | .{ @typeName(To), field.name, to_value, field.value }, 26 | )); 27 | } 28 | } else if (to_info.is_exhaustive) { 29 | @compileError(std.fmt.comptimePrint( 30 | "{s} has no field named {s}", 31 | .{ @typeName(To), field.name }, 32 | )); 33 | } else { 34 | _ = std.math.cast(to_info.tag_type, field.value) orelse @compileError(std.fmt.comptimePrint( 35 | "Value {} cannot cast into enum {s} with tag type {s}", 36 | .{ field.value, @typeName(To), @typeName(to_info.tag_type) }, 37 | )); 38 | } 39 | } 40 | } 41 | 42 | return @enumFromInt(@intFromEnum(from)); 43 | } 44 | 45 | /// Returns a struct with a field corresponding to each named enum value. 46 | /// The type and default value of each field is determined by `map`. 47 | /// 48 | /// note: You can think of this as a generalized version of `std.enums.EnumFieldStruct`. 49 | pub fn EnumFieldMapStruct( 50 | comptime Enum: type, 51 | /// Inputs a named enum value and outputs a type and a pointer to a default value. 52 | comptime fieldMap: fn (comptime Enum) struct { type, ?*const anyopaque }, 53 | ) type { 54 | const fields = @typeInfo(Enum).@"enum".fields; 55 | var struct_fields: [fields.len]std.builtin.Type.StructField = undefined; 56 | for (fields, &struct_fields) |in, *out| { 57 | const field_type, const default_value_ptr = fieldMap(@enumFromInt(in.value)); 58 | out.* = .{ 59 | .name = in.name, 60 | .type = field_type, 61 | .default_value_ptr = default_value_ptr, 62 | .is_comptime = false, 63 | .alignment = @alignOf(field_type), 64 | }; 65 | } 66 | return @Type(.{ .@"struct" = .{ 67 | .layout = .auto, 68 | .fields = &struct_fields, 69 | .decls = &.{}, 70 | .is_tuple = false, 71 | } }); 72 | } 73 | 74 | /// Casts a union tag to a union value, asserting that the payload has type `void`. 75 | pub fn unionInitVoid(comptime Union: type, tag: std.meta.Tag(Union)) Union { 76 | switch (tag) { 77 | inline else => |comptime_tag| { 78 | const Payload = @FieldType(Union, @tagName(comptime_tag)); 79 | if (Payload != void) unreachable; 80 | return @unionInit(Union, @tagName(comptime_tag), {}); 81 | }, 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /source/Layout.zig: -------------------------------------------------------------------------------- 1 | const Layout = @This(); 2 | 3 | const std = @import("std"); 4 | const assert = std.debug.assert; 5 | const Allocator = std.mem.Allocator; 6 | 7 | const zss = @import("zss.zig"); 8 | const math = zss.math; 9 | const BoxTree = zss.BoxTree; 10 | const Environment = zss.Environment; 11 | const Fonts = zss.Fonts; 12 | const Images = zss.Images; 13 | const NodeId = Environment.NodeId; 14 | 15 | const cosmetic = @import("Layout/cosmetic.zig"); 16 | pub const BoxGen = @import("Layout/BoxGen.zig"); 17 | pub const BoxTreeManaged = @import("Layout/BoxTreeManaged.zig"); 18 | pub const StyleComputer = @import("Layout/StyleComputer.zig"); 19 | 20 | box_tree: BoxTreeManaged, 21 | computer: StyleComputer, 22 | viewport: math.Size, 23 | inputs: Inputs, 24 | allocator: Allocator, 25 | node_stack: zss.Stack(?NodeId), 26 | box_gen: BoxGen, 27 | 28 | pub const Inputs = struct { 29 | width: u32, 30 | height: u32, 31 | env: *const Environment, 32 | images: *const Images, 33 | fonts: *const Fonts, 34 | }; 35 | 36 | pub const Error = error{ 37 | OutOfMemory, 38 | SizeLimitExceeded, 39 | ViewportTooLarge, 40 | }; 41 | 42 | const Mode = enum { 43 | flow, 44 | stf, 45 | @"inline", 46 | }; 47 | 48 | pub const IsRoot = enum { 49 | root, 50 | not_root, 51 | }; 52 | 53 | pub fn init( 54 | env: *const Environment, 55 | allocator: Allocator, 56 | /// The width of the viewport in pixels. 57 | width: u32, 58 | /// The height of the viewport in pixels. 59 | height: u32, 60 | images: *const Images, 61 | fonts: *const Fonts, 62 | ) Layout { 63 | return .{ 64 | .box_tree = undefined, 65 | .computer = StyleComputer.init(env, allocator), 66 | .viewport = undefined, 67 | .inputs = .{ 68 | .width = width, 69 | .height = height, 70 | .env = env, 71 | .images = images, 72 | .fonts = fonts, 73 | }, 74 | .allocator = allocator, 75 | .node_stack = .{}, 76 | .box_gen = .{}, 77 | }; 78 | } 79 | 80 | pub fn deinit(layout: *Layout) void { 81 | layout.computer.deinit(); 82 | layout.node_stack.deinit(layout.allocator); 83 | layout.box_gen.deinit(); 84 | } 85 | 86 | pub fn run(layout: *Layout, allocator: Allocator) Error!BoxTree { 87 | const cast = math.pixelsToUnits; 88 | const width_units = cast(layout.inputs.width) orelse return error.ViewportTooLarge; 89 | const height_units = cast(layout.inputs.height) orelse return error.ViewportTooLarge; 90 | layout.viewport = .{ 91 | .w = width_units, 92 | .h = height_units, 93 | }; 94 | 95 | var box_tree = BoxTree{ .allocator = allocator }; 96 | errdefer box_tree.deinit(); 97 | layout.box_tree = .{ .ptr = &box_tree }; 98 | 99 | { 100 | layout.node_stack.top = layout.inputs.env.root_node; 101 | layout.computer.stage = .{ .box_gen = .{} }; 102 | defer layout.computer.deinitStage(.box_gen); 103 | try layout.box_gen.run(); 104 | } 105 | 106 | try cosmeticLayout(layout); 107 | 108 | return box_tree; 109 | } 110 | 111 | fn cosmeticLayout(layout: *Layout) !void { 112 | layout.computer.stage = .{ .cosmetic = .{} }; 113 | defer layout.computer.deinitStage(.cosmetic); 114 | 115 | layout.node_stack.top = layout.inputs.env.root_node; 116 | 117 | try cosmetic.run(layout); 118 | } 119 | 120 | pub fn currentNode(layout: Layout) ?NodeId { 121 | return layout.node_stack.top.?; 122 | } 123 | 124 | pub fn pushNode(layout: *Layout) !void { 125 | const node = &layout.node_stack.top.?; 126 | const child = node.*.?.firstChild(layout.inputs.env); 127 | node.* = node.*.?.nextSibling(layout.inputs.env); 128 | try layout.node_stack.push(layout.allocator, child); 129 | } 130 | 131 | pub fn popNode(layout: *Layout) void { 132 | _ = layout.node_stack.pop(); 133 | } 134 | 135 | pub fn advanceNode(layout: *Layout) void { 136 | const node = &layout.node_stack.top.?; 137 | node.* = node.*.?.nextSibling(layout.inputs.env); 138 | } 139 | -------------------------------------------------------------------------------- /LICENSE-OFL.txt: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------- 2 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 3 | ----------------------------------------------------------- 4 | 5 | PREAMBLE 6 | The goals of the Open Font License (OFL) are to stimulate worldwide 7 | development of collaborative font projects, to support the font creation 8 | efforts of academic and linguistic communities, and to provide a free and 9 | open framework in which fonts may be shared and improved in partnership 10 | with others. 11 | 12 | The OFL allows the licensed fonts to be used, studied, modified and 13 | redistributed freely as long as they are not sold by themselves. The 14 | fonts, including any derivative works, can be bundled, embedded, 15 | redistributed and/or sold with any software provided that any reserved 16 | names are not used by derivative works. The fonts and derivatives, 17 | however, cannot be released under any other type of license. The 18 | requirement for fonts to remain under this license does not apply 19 | to any document created using the fonts or their derivatives. 20 | 21 | DEFINITIONS 22 | "Font Software" refers to the set of files released by the Copyright 23 | Holder(s) under this license and clearly marked as such. This may 24 | include source files, build scripts and documentation. 25 | 26 | "Reserved Font Name" refers to any names specified as such after the 27 | copyright statement(s). 28 | 29 | "Original Version" refers to the collection of Font Software components as 30 | distributed by the Copyright Holder(s). 31 | 32 | "Modified Version" refers to any derivative made by adding to, deleting, 33 | or substituting -- in part or in whole -- any of the components of the 34 | Original Version, by changing formats or by porting the Font Software to a 35 | new environment. 36 | 37 | "Author" refers to any designer, engineer, programmer, technical 38 | writer or other person who contributed to the Font Software. 39 | 40 | PERMISSION & CONDITIONS 41 | Permission is hereby granted, free of charge, to any person obtaining 42 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 43 | redistribute, and sell modified and unmodified copies of the Font 44 | Software, subject to the following conditions: 45 | 46 | 1) Neither the Font Software nor any of its individual components, 47 | in Original or Modified Versions, may be sold by itself. 48 | 49 | 2) Original or Modified Versions of the Font Software may be bundled, 50 | redistributed and/or sold with any software, provided that each copy 51 | contains the above copyright notice and this license. These can be 52 | included either as stand-alone text files, human-readable headers or 53 | in the appropriate machine-readable metadata fields within text or 54 | binary files as long as those fields can be easily viewed by the user. 55 | 56 | 3) No Modified Version of the Font Software may use the Reserved Font 57 | Name(s) unless explicit written permission is granted by the corresponding 58 | Copyright Holder. This restriction only applies to the primary font name as 59 | presented to the users. 60 | 61 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 62 | Software shall not be used to promote, endorse or advertise any 63 | Modified Version, except to acknowledge the contribution(s) of the 64 | Copyright Holder(s) and the Author(s) or with their explicit written 65 | permission. 66 | 67 | 5) The Font Software, modified or unmodified, in part or in whole, 68 | must be distributed entirely under this license, and must not be 69 | distributed under any other license. The requirement for fonts to 70 | remain under this license does not apply to any document created 71 | using the Font Software. 72 | 73 | TERMINATION 74 | This license becomes null and void if any of the above conditions are 75 | not met. 76 | 77 | DISCLAIMER 78 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 79 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 80 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 81 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 82 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 83 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 84 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 85 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 86 | OTHER DEALINGS IN THE FONT SOFTWARE. 87 | -------------------------------------------------------------------------------- /source/Layout/BoxTreeManaged.zig: -------------------------------------------------------------------------------- 1 | const BoxTreeManaged = @This(); 2 | 3 | const std = @import("std"); 4 | const zss = @import("../zss.zig"); 5 | 6 | const BoxTree = zss.BoxTree; 7 | const BackgroundImage = BoxTree.BackgroundImage; 8 | const BackgroundImages = BoxTree.BackgroundImages; 9 | const BlockRef = BoxTree.BlockRef; 10 | const GeneratedBox = BoxTree.GeneratedBox; 11 | const Ifc = BoxTree.InlineFormattingContext; 12 | const Subtree = BoxTree.Subtree; 13 | 14 | ptr: *BoxTree, 15 | 16 | pub fn setGeneratedBox(box_tree: BoxTreeManaged, node: zss.Environment.NodeId, generated_box: GeneratedBox) !void { 17 | try box_tree.ptr.node_to_generated_box.putNoClobber(box_tree.ptr.allocator, node, generated_box); 18 | } 19 | 20 | pub fn newSubtree(box_tree: BoxTreeManaged) !*Subtree { 21 | const all_subtrees = &box_tree.ptr.subtrees; 22 | const id_int = std.math.cast(std.meta.Tag(Subtree.Id), all_subtrees.items.len) orelse return error.SizeLimitExceeded; 23 | 24 | try all_subtrees.ensureUnusedCapacity(box_tree.ptr.allocator, 1); 25 | const subtree = try box_tree.ptr.allocator.create(Subtree); 26 | all_subtrees.appendAssumeCapacity(subtree); 27 | subtree.* = .{ .id = @enumFromInt(id_int), .parent = null }; 28 | return subtree; 29 | } 30 | 31 | pub fn appendBlockBox(box_tree: BoxTreeManaged, subtree: *Subtree) !Subtree.Size { 32 | const new_len = std.math.add(Subtree.Size, @intCast(subtree.blocks.len), 1) catch return error.SizeLimitExceeded; 33 | try subtree.blocks.resize(box_tree.ptr.allocator, new_len); 34 | return new_len - 1; 35 | } 36 | 37 | pub fn newIfc(box_tree: BoxTreeManaged, parent_block: BlockRef) !*Ifc { 38 | const all_ifcs = &box_tree.ptr.ifcs; 39 | const id_int = std.math.cast(std.meta.Tag(Ifc.Id), all_ifcs.items.len) orelse return error.SizeLimitExceeded; 40 | 41 | try all_ifcs.ensureUnusedCapacity(box_tree.ptr.allocator, 1); 42 | const ifc = try box_tree.ptr.allocator.create(Ifc); 43 | all_ifcs.appendAssumeCapacity(ifc); 44 | ifc.* = .{ .id = @enumFromInt(id_int), .parent_block = parent_block }; 45 | return ifc; 46 | } 47 | 48 | pub fn appendInlineBox(box_tree: BoxTreeManaged, ifc: *Ifc) !Ifc.Size { 49 | const new_len = std.math.add(Ifc.Size, @intCast(ifc.inline_boxes.len), 1) catch return error.SizeLimitExceeded; 50 | try ifc.inline_boxes.resize(box_tree.ptr.allocator, new_len); 51 | return new_len - 1; 52 | } 53 | 54 | pub fn appendGlyph(box_tree: BoxTreeManaged, ifc: *Ifc, glyph: Ifc.GlyphIndex) !void { 55 | try ifc.glyphs.append(box_tree.ptr.allocator, .{ .index = glyph, .metrics = undefined }); 56 | } 57 | 58 | /// This enum is derived from `Ifc.Special.Kind` 59 | pub const SpecialGlyph = union(enum(u16)) { 60 | ZeroGlyphIndex = 1, 61 | BoxStart: Ifc.Size, 62 | BoxEnd: Ifc.Size, 63 | InlineBlock: Subtree.Size, 64 | /// Represents a mandatory line break in the text. 65 | /// data has no meaning. 66 | LineBreak, 67 | }; 68 | 69 | pub fn appendSpecialGlyph( 70 | box_tree: BoxTreeManaged, 71 | ifc: *Ifc, 72 | comptime tag: std.meta.Tag(SpecialGlyph), 73 | data: @TypeOf(@field(@as(SpecialGlyph, undefined), @tagName(tag))), 74 | ) !void { 75 | const special: Ifc.Special = .{ 76 | .kind = zss.meta.coerceEnum(Ifc.Special.Kind, tag), 77 | .data = switch (tag) { 78 | .ZeroGlyphIndex, .LineBreak => undefined, 79 | .BoxStart, .BoxEnd, .InlineBlock => data, 80 | }, 81 | }; 82 | try ifc.glyphs.append(box_tree.ptr.allocator, .{ .index = 0, .metrics = undefined }); 83 | try ifc.glyphs.append(box_tree.ptr.allocator, .{ .index = @bitCast(special), .metrics = undefined }); 84 | } 85 | 86 | pub fn appendLineBox(box_tree: BoxTreeManaged, ifc: *Ifc, line_box: Ifc.LineBox) !void { 87 | try ifc.line_boxes.append(box_tree.ptr.allocator, line_box); 88 | } 89 | 90 | pub fn allocBackgroundImages(box_tree: BoxTreeManaged, count: BackgroundImages.Size) !struct { BackgroundImages.Handle, []BackgroundImage } { 91 | const bi = &box_tree.ptr.background_images; 92 | const handle_int = std.math.add(std.meta.Tag(BackgroundImages.Handle), @intCast(bi.slices.items.len), 1) catch return error.SizeLimitExceeded; 93 | const begin: BackgroundImages.Size = @intCast(bi.images.items.len); 94 | const end = std.math.add(BackgroundImages.Size, begin, count) catch return error.SizeLimitExceeded; 95 | 96 | try bi.slices.ensureUnusedCapacity(box_tree.ptr.allocator, 1); 97 | const images = try bi.images.addManyAsSlice(box_tree.ptr.allocator, count); 98 | bi.slices.appendAssumeCapacity(.{ .begin = begin, .end = end }); 99 | 100 | return .{ @enumFromInt(handle_int), images }; 101 | } 102 | -------------------------------------------------------------------------------- /source/values/types.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zss = @import("../zss.zig"); 3 | 4 | pub const CssWideKeyword = enum(u2) { 5 | initial = 1, 6 | inherit = 2, 7 | unset = 3, 8 | }; 9 | 10 | pub const Display = enum { 11 | block, 12 | @"inline", 13 | inline_block, 14 | none, 15 | }; 16 | 17 | //pub const Display = enum { 18 | // // display-outside, display-inside 19 | // block, 20 | // inline_, 21 | // run_in, 22 | // flow, 23 | // flow_root, 24 | // table, 25 | // flex, 26 | // grid, 27 | // ruby, 28 | // block_flow, 29 | // block_flow_root, 30 | // block_table, 31 | // block_flex, 32 | // block_grid, 33 | // block_ruby, 34 | // inline_flow, 35 | // inline_flow_root, 36 | // inline_table, 37 | // inline_flex, 38 | // inline_grid, 39 | // inline_ruby, 40 | // run_in_flow, 41 | // run_in_flow_root, 42 | // run_in_table, 43 | // run_in_flex, 44 | // run_in_grid, 45 | // run_in_ruby, 46 | // // display-listitem 47 | // list_item, 48 | // block_list_item, 49 | // inline_list_item, 50 | // run_in_list_item, 51 | // flow_list_item, 52 | // flow_root_list_item, 53 | // block_flow_list_item, 54 | // block_flow_root_list_item, 55 | // inline_flow_list_item, 56 | // inline_flow_root_list_item, 57 | // run_in_flow_list_item, 58 | // run_in_flow_root_list_item, 59 | // // display-internal 60 | // table_row_group, 61 | // table_header_group, 62 | // table_footer_group, 63 | // table_row, 64 | // table_cell, 65 | // table_column_group, 66 | // table_column, 67 | // table_caption, 68 | // ruby_base, 69 | // ruby_text, 70 | // ruby_base_container, 71 | // ruby_text_container, 72 | // // display-box 73 | // contents, 74 | // none, 75 | // // display-legacy 76 | // legacy_inline_block, 77 | // legacy_inline_table, 78 | // legacy_inline_flex, 79 | // legacy_inline_grid, 80 | // // css-wide 81 | //}; 82 | 83 | pub const Position = enum { 84 | static, 85 | relative, 86 | absolute, 87 | sticky, 88 | fixed, 89 | }; 90 | 91 | pub const ZIndex = union(enum) { 92 | integer: i32, 93 | auto, 94 | }; 95 | 96 | pub const Float = enum { 97 | left, 98 | right, 99 | none, 100 | }; 101 | 102 | pub const Clear = enum { 103 | left, 104 | right, 105 | both, 106 | none, 107 | }; 108 | 109 | pub const LengthPercentageAuto = union(enum) { 110 | px: f32, 111 | percentage: f32, 112 | auto, 113 | }; 114 | 115 | pub const Size = LengthPercentageAuto; 116 | pub const Margin = LengthPercentageAuto; 117 | pub const Inset = LengthPercentageAuto; 118 | 119 | pub const LengthPercentage = union(enum) { 120 | px: f32, 121 | percentage: f32, 122 | }; 123 | 124 | pub const MinSize = LengthPercentage; 125 | pub const Padding = LengthPercentage; 126 | 127 | pub const MaxSize = union(enum) { 128 | px: f32, 129 | percentage: f32, 130 | none, 131 | }; 132 | 133 | pub const BorderWidth = union(enum) { 134 | px: f32, 135 | thin, 136 | medium, 137 | thick, 138 | }; 139 | 140 | pub const BorderStyle = enum { 141 | none, 142 | hidden, 143 | dotted, 144 | dashed, 145 | solid, 146 | double, 147 | groove, 148 | ridge, 149 | inset, 150 | outset, 151 | }; 152 | 153 | pub const Color = union(enum) { 154 | rgba: u32, 155 | current_color, 156 | transparent, 157 | 158 | pub const black = Color{ .rgba = 0xff }; 159 | }; 160 | 161 | pub const BackgroundImage = union(enum) { 162 | image: zss.Images.Handle, 163 | url: zss.Environment.UrlId, 164 | none, 165 | }; 166 | 167 | pub const BackgroundRepeat = struct { 168 | pub const Style = enum { repeat, no_repeat, space, round }; 169 | 170 | x: Style = .repeat, 171 | y: Style = .repeat, 172 | }; 173 | 174 | pub const BackgroundAttachment = enum { 175 | scroll, 176 | fixed, 177 | local, 178 | }; 179 | 180 | pub const BackgroundPosition = struct { 181 | pub const Side = enum { start, end, center }; 182 | pub const Offset = union(enum) { 183 | px: f32, 184 | percentage: f32, 185 | }; 186 | 187 | // TODO: Make this a tagged union instead 188 | pub const SideOffset = struct { 189 | /// `.start` corresponds to left (x-axis) and top (y-axis) 190 | /// `.end` corresponds to right (x-axis) and bottom (y-axis) 191 | /// `.center` corresponds to center (either axis), and will cause `offset` to be ignored during layout 192 | side: Side, 193 | offset: Offset, 194 | }; 195 | 196 | x: SideOffset, 197 | y: SideOffset, 198 | }; 199 | 200 | pub const BackgroundClip = enum { 201 | border_box, 202 | padding_box, 203 | content_box, 204 | }; 205 | 206 | pub const BackgroundOrigin = enum { 207 | border_box, 208 | padding_box, 209 | content_box, 210 | }; 211 | 212 | pub const BackgroundSize = union(enum) { 213 | pub const SizeType = union(enum) { 214 | px: f32, 215 | percentage: f32, 216 | auto, 217 | }; 218 | 219 | size: struct { 220 | width: SizeType, 221 | height: SizeType, 222 | }, 223 | contain, 224 | cover, 225 | }; 226 | 227 | pub const Font = enum { 228 | default, 229 | none, 230 | }; 231 | -------------------------------------------------------------------------------- /source/math.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const assert = std.debug.assert; 3 | const expect = std.testing.expect; 4 | 5 | // About the zss coordinate space: 6 | // 7 | // The zss coordinate space is a space made of integral points (also called 'units'). 8 | // The x-axis increases going to the right, and the y-axis increases going downwards. 9 | 10 | /// The smallest unit of space in the zss coordinate system. 11 | pub const Unit = i32; 12 | 13 | /// The number of Units contained wthin the width or height of 1 screen pixel. 14 | pub const units_per_pixel = 4; 15 | 16 | pub fn pixelsToUnits(px: anytype) ?Unit { 17 | const scaled = std.math.mul(@TypeOf(px), px, units_per_pixel) catch return null; 18 | return std.math.cast(Unit, scaled); 19 | } 20 | 21 | pub const Vector = struct { 22 | x: Unit, 23 | y: Unit, 24 | 25 | pub const zero: Vector = .{ .x = 0, .y = 0 }; 26 | 27 | pub fn add(lhs: Vector, rhs: Vector) Vector { 28 | return Vector{ .x = lhs.x + rhs.x, .y = lhs.y + rhs.y }; 29 | } 30 | 31 | pub fn sub(lhs: Vector, rhs: Vector) Vector { 32 | return Vector{ .x = lhs.x - rhs.x, .y = lhs.y - rhs.y }; 33 | } 34 | 35 | pub fn eql(lhs: Vector, rhs: Vector) bool { 36 | return lhs.x == rhs.x and lhs.y == rhs.y; 37 | } 38 | }; 39 | 40 | pub const Size = struct { 41 | w: Unit, 42 | h: Unit, 43 | }; 44 | 45 | pub const Range = struct { 46 | start: Unit, 47 | length: Unit, 48 | }; 49 | 50 | pub const Rect = struct { 51 | x: Unit, 52 | y: Unit, 53 | w: Unit, 54 | h: Unit, 55 | 56 | pub fn xRange(rect: Rect) Range { 57 | return .{ .start = rect.x, .length = rect.w }; 58 | } 59 | 60 | pub fn yRange(rect: Rect) Range { 61 | return .{ .start = rect.y, .length = rect.h }; 62 | } 63 | 64 | // TODO: Is this a good definition of "emptiness"? 65 | pub fn isEmpty(rect: Rect) bool { 66 | return rect.w < 0 or rect.h < 0; 67 | } 68 | 69 | pub fn translate(rect: Rect, vec: Vector) Rect { 70 | return Rect{ 71 | .x = rect.x + vec.x, 72 | .y = rect.y + vec.y, 73 | .w = rect.w, 74 | .h = rect.h, 75 | }; 76 | } 77 | 78 | pub fn intersect(a: Rect, b: Rect) Rect { 79 | const left = @max(a.x, b.x); 80 | const right = @min(a.x + a.w, b.x + b.w); 81 | const top = @max(a.y, b.y); 82 | const bottom = @min(a.y + a.h, b.y + b.h); 83 | 84 | return Rect{ 85 | .x = left, 86 | .y = top, 87 | .w = right - left, 88 | .h = bottom - top, 89 | }; 90 | } 91 | }; 92 | 93 | test Rect { 94 | const r1 = Rect{ .x = 0, .y = 0, .w = 10, .h = 10 }; 95 | const r2 = Rect{ .x = 3, .y = 5, .w = 17, .h = 4 }; 96 | const r3 = Rect{ .x = 15, .y = 0, .w = 20, .h = 9 }; 97 | const r4 = Rect{ .x = 20, .y = 1, .w = 10, .h = 0 }; 98 | 99 | const intersect = Rect.intersect; 100 | try expect(std.meta.eql(intersect(r1, r2), Rect{ .x = 3, .y = 5, .w = 7, .h = 4 })); 101 | try expect(intersect(r1, r3).isEmpty()); 102 | try expect(intersect(r1, r4).isEmpty()); 103 | try expect(std.meta.eql(intersect(r2, r3), Rect{ .x = 15, .y = 5, .w = 5, .h = 4 })); 104 | try expect(intersect(r2, r4).isEmpty()); 105 | try expect(!intersect(r3, r4).isEmpty()); 106 | } 107 | 108 | pub const Ratio = struct { 109 | num: Unit, 110 | den: Unit, 111 | }; 112 | 113 | pub fn divRound(a: anytype, b: anytype) @TypeOf(a, b) { 114 | const Return = @TypeOf(a, b); 115 | return @divFloor(a, b) + @as(Return, @intFromBool(2 * @mod(a, b) >= b)); 116 | } 117 | 118 | pub fn roundUp(a: anytype, comptime multiple: comptime_int) @TypeOf(a) { 119 | const Return = @TypeOf(a); 120 | const mod = @mod(a, multiple); 121 | return a + (multiple - mod) * @as(Return, @intFromBool(mod != 0)); 122 | } 123 | 124 | test roundUp { 125 | try expect(roundUp(0, 4) == 0); 126 | try expect(roundUp(1, 4) == 4); 127 | try expect(roundUp(3, 4) == 4); 128 | try expect(roundUp(62, 7) == 63); 129 | } 130 | 131 | pub const Color = extern struct { 132 | r: u8, 133 | g: u8, 134 | b: u8, 135 | a: u8, 136 | 137 | pub fn toRgbaArray(color: Color) [4]u8 { 138 | return @bitCast(color); 139 | } 140 | 141 | pub fn toRgbaInt(color: Color) u32 { 142 | return std.mem.bigToNative(u32, @bitCast(color)); 143 | } 144 | 145 | pub fn fromRgbaInt(value: u32) Color { 146 | return @bitCast(std.mem.nativeToBig(u32, value)); 147 | } 148 | 149 | // TODO: change to a test 150 | comptime { 151 | const eql = std.meta.eql; 152 | assert(eql(toRgbaArray(.{ .r = 0, .g = 0, .b = 0, .a = 0 }), .{ 0x00, 0x00, 0x00, 0x00 })); 153 | assert(eql(toRgbaArray(.{ .r = 255, .g = 0, .b = 0, .a = 128 }), .{ 0xff, 0x00, 0x00, 0x80 })); 154 | assert(eql(toRgbaArray(.{ .r = 0, .g = 20, .b = 50, .a = 200 }), .{ 0x00, 0x14, 0x32, 0xC8 })); 155 | 156 | assert(toRgbaInt(.{ .r = 0, .g = 0, .b = 0, .a = 0 }) == 0x00000000); 157 | assert(toRgbaInt(.{ .r = 255, .g = 0, .b = 0, .a = 128 }) == 0xff000080); 158 | assert(toRgbaInt(.{ .r = 0, .g = 20, .b = 50, .a = 200 }) == 0x001432C8); 159 | } 160 | 161 | pub const transparent = Color{ .r = 0, .g = 0, .b = 0, .a = 0 }; 162 | pub const white = Color{ .r = 0xff, .g = 0xff, .b = 0xff, .a = 0xff }; 163 | pub const black = Color{ .r = 0, .g = 0, .b = 0, .a = 0xff }; 164 | }; 165 | 166 | pub fn CheckedInt(comptime Int: type) type { 167 | return struct { 168 | overflow: bool, 169 | value: Int, 170 | 171 | pub fn init(int: Int) CheckedInt(Int) { 172 | return .{ 173 | .overflow = false, 174 | .value = int, 175 | }; 176 | } 177 | 178 | pub fn unwrap(checked: CheckedInt(Int)) error{Overflow}!Int { 179 | if (checked.overflow) return error.Overflow; 180 | return checked.value; 181 | } 182 | 183 | pub fn add(checked: *CheckedInt(Int), int: Int) void { 184 | const add_result = @addWithOverflow(checked.value, int); 185 | checked.value = add_result[0]; 186 | checked.overflow = checked.overflow or @bitCast(add_result[1]); 187 | } 188 | 189 | pub fn multiply(checked: *CheckedInt(Int), int: Int) void { 190 | const mul_result = @mulWithOverflow(checked.value, int); 191 | checked.value = mul_result[0]; 192 | checked.overflow = checked.overflow or @bitCast(mul_result[1]); 193 | } 194 | 195 | pub fn negate(checked: *CheckedInt(Int)) void { 196 | const sub_result = @subWithOverflow(0, checked.value); 197 | checked.value = sub_result[0]; 198 | checked.overflow = checked.overflow or @bitCast(sub_result[1]); 199 | } 200 | }; 201 | } 202 | -------------------------------------------------------------------------------- /test/opengl.zig: -------------------------------------------------------------------------------- 1 | const zss = @import("zss"); 2 | const BoxTree = zss.BoxTree; 3 | const units_per_pixel = zss.math.units_per_pixel; 4 | 5 | const std = @import("std"); 6 | const assert = std.debug.assert; 7 | const Allocator = std.mem.Allocator; 8 | 9 | const Test = @import("Test.zig"); 10 | 11 | const glfw = @import("mach-glfw"); 12 | const hb = @import("harfbuzz").c; 13 | const zgl = @import("zgl"); 14 | const zigimg = @import("zigimg"); 15 | 16 | pub fn run(tests: []const *Test, output_parent_dir: []const u8) !void { 17 | if (!glfw.init(.{})) return error.GlfwError; 18 | defer glfw.terminate(); 19 | 20 | const window = glfw.Window.create(1, 1, "zss opengl render tests", null, null, .{ 21 | .context_version_major = 3, 22 | .context_version_minor = 3, 23 | .opengl_profile = .opengl_core_profile, 24 | .visible = false, 25 | }) orelse return error.GlfwError; 26 | defer window.destroy(); 27 | 28 | glfw.makeContextCurrent(window); 29 | defer glfw.makeContextCurrent(null); 30 | 31 | glfw.swapInterval(1); 32 | 33 | const getProcAddressWrapper = struct { 34 | fn f(_: void, symbol_name: [:0]const u8) ?*const anyopaque { 35 | return glfw.getProcAddress(symbol_name); 36 | } 37 | }.f; 38 | try zgl.loadExtensions({}, getProcAddressWrapper); 39 | 40 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 41 | defer assert(gpa.deinit() == .ok); 42 | const allocator = gpa.allocator(); 43 | 44 | var output_dir = blk: { 45 | const path = try std.fs.path.join(allocator, &.{ output_parent_dir, "opengl" }); 46 | defer allocator.free(path); 47 | break :blk try std.fs.cwd().makeOpenPath(path, .{}); 48 | }; 49 | defer output_dir.close(); 50 | 51 | var stdout_buffer: [200]u8 = undefined; 52 | var stdout_writer = std.fs.File.stdout().writerStreaming(&stdout_buffer); 53 | const stdout = &stdout_writer.interface; 54 | 55 | for (tests, 0..) |t, ti| { 56 | try stdout.print("opengl: ({}/{}) \"{s}\" ... ", .{ ti + 1, tests.len, t.name }); 57 | try stdout.flush(); 58 | 59 | var layout = zss.Layout.init(&t.document.env, allocator, t.width, t.height, t.images, t.fonts); 60 | defer layout.deinit(); 61 | 62 | var box_tree = try layout.run(allocator); 63 | defer box_tree.deinit(); 64 | 65 | var renderer = try zss.OpenGlFreetypeRenderer.init(allocator, t.fonts); 66 | defer renderer.deinit(); 67 | try renderer.updateBoxTree(&box_tree); 68 | 69 | setIcbBackgroundColor(&box_tree, zss.math.Color.fromRgbaInt(0x202020ff)); 70 | const root_block_size = rootBlockSize(&box_tree, t.document.env.root_node); 71 | 72 | const pages = try std.math.divCeil(u32, root_block_size.height, t.height); 73 | var image = try zigimg.Image.create(allocator, root_block_size.width, pages * t.height, .rgba32); 74 | defer image.deinit(allocator); 75 | const image_pixels = image.pixels.asBytes(); 76 | 77 | const temp_buffer = try allocator.alloc(u8, image_pixels.len); 78 | defer allocator.free(temp_buffer); 79 | 80 | window.setSize(.{ .width = t.width, .height = t.height }); 81 | zgl.viewport(0, 0, t.width, t.height); 82 | for (0..pages) |i| { 83 | zgl.clearColor(0, 0, 0, 0); 84 | zgl.clear(.{ .color = true }); 85 | 86 | const viewport = zss.math.Rect{ 87 | .x = @intCast(root_block_size.x * units_per_pixel), 88 | .y = @intCast((i * t.height + root_block_size.y) * units_per_pixel), 89 | .w = @intCast(t.width * units_per_pixel), 90 | .h = @intCast(t.height * units_per_pixel), 91 | }; 92 | try renderer.drawBoxTree(t.images, &box_tree, allocator, viewport); 93 | zgl.flush(); 94 | 95 | const y: u32 = @intCast(i * t.height); 96 | const w = root_block_size.width; 97 | const h: u32 = t.height; 98 | zgl.readPixels(0, 0, w, h, .rgba, .unsigned_byte, temp_buffer[((pages - 1) * t.height - y) * w * 4 ..][0 .. w * h * 4].ptr); 99 | } 100 | 101 | // Flip everything vertically 102 | for (0..(pages * t.height)) |y| { 103 | const inverted_y = pages * t.height - 1 - y; 104 | @memcpy( 105 | image_pixels[inverted_y * root_block_size.width * 4 ..][0 .. root_block_size.width * 4], 106 | temp_buffer[y * root_block_size.width * 4 ..][0 .. root_block_size.width * 4], 107 | ); 108 | } 109 | 110 | if (image_pixels.len == 0) { 111 | // TODO: Delete the output file if it already exists 112 | try stdout.writeAll("success, no file written\n"); 113 | } else { 114 | const image_path = try std.mem.concat(allocator, u8, &.{ t.name, ".png" }); 115 | defer allocator.free(image_path); 116 | if (std.fs.path.dirname(image_path)) |parent_dir| { 117 | try output_dir.makePath(parent_dir); 118 | } 119 | const image_file = try output_dir.createFile(image_path, .{}); 120 | defer image_file.close(); 121 | var write_buffer: [4096]u8 = undefined; 122 | try image.writeToFile(allocator, image_file, &write_buffer, .{ .png = .{} }); 123 | 124 | try stdout.writeAll("success\n"); 125 | } 126 | 127 | try stdout.flush(); 128 | } 129 | 130 | try stdout.print("opengl: all {} tests passed\n", .{tests.len}); 131 | try stdout.flush(); 132 | } 133 | 134 | fn rootBlockSize(box_tree: *BoxTree, root_element_or_null: ?zss.Environment.NodeId) struct { x: u32, y: u32, width: u32, height: u32 } { 135 | const root_element = root_element_or_null orelse return .{ .x = 0, .y = 0, .width = 0, .height = 0 }; 136 | const generated_box = box_tree.node_to_generated_box.get(root_element) orelse return .{ .x = 0, .y = 0, .width = 0, .height = 0 }; 137 | const ref = switch (generated_box) { 138 | .block_ref => |ref| ref, 139 | .text => |ifc_id| box_tree.getIfc(ifc_id).parent_block, 140 | .inline_box => unreachable, 141 | }; 142 | const subtree = box_tree.getSubtree(ref.subtree).view(); 143 | const box_offsets = subtree.items(.box_offsets)[ref.index]; 144 | return .{ 145 | .x = @intCast(@divFloor(box_offsets.border_pos.x, units_per_pixel)), 146 | .y = @intCast(@divFloor(box_offsets.border_pos.y, units_per_pixel)), 147 | .width = @intCast(@divFloor(box_offsets.border_size.w, units_per_pixel)), 148 | .height = @intCast(@divFloor(box_offsets.border_size.h, units_per_pixel)), 149 | }; 150 | } 151 | 152 | fn setIcbBackgroundColor(box_tree: *BoxTree, color: zss.math.Color) void { 153 | // TODO: This wouldn't be necessary if [background propagation](https://www.w3.org/TR/css-backgrounds-3/#special-backgrounds) was implemented. 154 | const icb = box_tree.initial_containing_block; 155 | const subtree = box_tree.getSubtree(icb.subtree).view(); 156 | const background = &subtree.items(.background)[icb.index]; 157 | background.color = color; 158 | } 159 | -------------------------------------------------------------------------------- /LICENSE-CC0.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /source/Layout/StackingContextTreeBuilder.zig: -------------------------------------------------------------------------------- 1 | const Builder = @This(); 2 | 3 | const std = @import("std"); 4 | const assert = std.debug.assert; 5 | const Allocator = std.mem.Allocator; 6 | 7 | const zss = @import("../zss.zig"); 8 | const Stack = zss.Stack; 9 | 10 | const BoxTree = zss.BoxTree; 11 | const BlockRef = BoxTree.BlockRef; 12 | const IfcId = BoxTree.InlineFormattingContext.Id; 13 | const ZIndex = BoxTree.ZIndex; 14 | const StackingContext = BoxTree.StackingContext; 15 | const StackingContextTree = BoxTree.StackingContextTree; 16 | const Size = StackingContextTree.Size; 17 | const Id = StackingContextTree.Id; 18 | 19 | /// A value is pushed for every stacking context. 20 | contexts: Stack(struct { 21 | index: Size, 22 | parentable: bool, 23 | num_nones: Size, 24 | }) = .{}, 25 | /// A value is pushed for every parentable stacking context. 26 | parentables: Stack(struct { index: Size }) = .{}, 27 | next_id: std.meta.Tag(Id) = 0, 28 | /// The set of stacking contexts which do not yet have an associated block box, and are therefore "incomplete". 29 | /// This is for debugging purposes only, and will have no effect if runtime safety is disabled. 30 | incompletes: IncompleteStackingContexts = .{}, 31 | 32 | const IncompleteStackingContexts = switch (zss.debug.runtime_safety) { 33 | true => struct { 34 | set: std.AutoHashMapUnmanaged(Id, void) = .empty, 35 | 36 | fn deinit(self: *IncompleteStackingContexts, allocator: Allocator) void { 37 | self.set.deinit(allocator); 38 | } 39 | 40 | fn insert(self: *IncompleteStackingContexts, allocator: Allocator, id: Id) !void { 41 | try self.set.putNoClobber(allocator, id, {}); 42 | } 43 | 44 | fn remove(self: *IncompleteStackingContexts, id: Id) void { 45 | assert(self.set.remove(id)); 46 | } 47 | 48 | fn empty(self: *const IncompleteStackingContexts) bool { 49 | return self.set.count() == 0; 50 | } 51 | }, 52 | false => struct { 53 | fn deinit(_: *IncompleteStackingContexts, _: Allocator) void {} 54 | 55 | fn insert(_: *IncompleteStackingContexts, _: Allocator, _: Id) !void {} 56 | 57 | fn remove(_: *IncompleteStackingContexts, _: Id) void {} 58 | 59 | fn empty(_: *const IncompleteStackingContexts) bool { 60 | return true; 61 | } 62 | }, 63 | }; 64 | 65 | pub const Type = union(enum) { 66 | /// Represents no stacking context. 67 | none, 68 | /// Represents a stacking context that can have child stacking contexts. 69 | parentable: ZIndex, 70 | /// Represents a stacking context that cannot have child stacking contexts. 71 | /// When one tries to create new stacking context as a child of one of these ones, it instead becomes its sibling. 72 | /// This type of stacking context is created by, for example, static-positioned inline-blocks, or absolute-positioned blocks. 73 | non_parentable: ZIndex, 74 | }; 75 | 76 | pub fn deinit(b: *Builder, allocator: Allocator) void { 77 | b.contexts.deinit(allocator); 78 | b.parentables.deinit(allocator); 79 | b.incompletes.deinit(allocator); 80 | } 81 | 82 | pub fn endFrame(b: *Builder) void { 83 | assert(b.incompletes.empty()); 84 | } 85 | 86 | pub fn pushInitial(b: *Builder, box_tree: *BoxTree, ref: BlockRef) !Id { 87 | assert(b.contexts.top == null); 88 | assert(b.parentables.top == null); 89 | assert(box_tree.sct.list.len == 0); 90 | 91 | const index: Size = 0; 92 | b.contexts.top = .{ 93 | .index = index, 94 | .parentable = true, 95 | .num_nones = 0, 96 | }; 97 | b.parentables.top = .{ .index = index }; 98 | return (try b.newStackingContext(index, 0, box_tree, ref)).?; 99 | } 100 | 101 | pub fn push(b: *Builder, allocator: Allocator, ty: Type, box_tree: *BoxTree, ref: BlockRef) !?Id { 102 | assert(b.contexts.top != null); 103 | assert(b.parentables.top != null); 104 | 105 | const z_index = switch (ty) { 106 | .none => { 107 | b.contexts.top.?.num_nones += 1; 108 | return null; 109 | }, 110 | .parentable, .non_parentable => |z_index| z_index, 111 | }; 112 | 113 | const sct = box_tree.sct.list.slice(); 114 | const parent_index = b.parentables.top.?.index; 115 | const index: Size = blk: { 116 | const skips = sct.items(.skip); 117 | const z_indeces = sct.items(.z_index); 118 | 119 | var index = parent_index + 1; 120 | const end = parent_index + skips[parent_index]; 121 | while (index < end and z_index >= z_indeces[index]) { 122 | index += skips[index]; 123 | } 124 | 125 | break :blk index; 126 | }; 127 | 128 | const parentable = switch (ty) { 129 | .none => unreachable, 130 | .parentable => blk: { 131 | try b.parentables.push(allocator, .{ .index = index }); 132 | break :blk true; 133 | }, 134 | .non_parentable => blk: { 135 | sct.items(.skip)[parent_index] += 1; 136 | break :blk false; 137 | }, 138 | }; 139 | try b.contexts.push(allocator, .{ 140 | .index = index, 141 | .parentable = parentable, 142 | .num_nones = 0, 143 | }); 144 | return b.newStackingContext(index, z_index, box_tree, ref); 145 | } 146 | 147 | /// If the return value is not null, caller must eventually follow up with a call to `setBlock`. 148 | /// Failure to do so is safety-checked undefined behavior. 149 | pub fn pushWithoutBlock(b: *Builder, allocator: Allocator, ty: Type, box_tree: *BoxTree) !?Id { 150 | const id_opt = try push(b, allocator, ty, box_tree, undefined); 151 | if (id_opt) |id| try b.incompletes.insert(allocator, id); 152 | return id_opt; 153 | } 154 | 155 | fn newStackingContext(b: *Builder, index: Size, z_index: ZIndex, box_tree: *BoxTree, ref: BlockRef) !?Id { 156 | const id: Id = @enumFromInt(b.next_id); 157 | try box_tree.sct.list.insert( 158 | box_tree.allocator, 159 | index, 160 | .{ 161 | .skip = 1, 162 | .id = id, 163 | .z_index = z_index, 164 | .ref = ref, 165 | .ifcs = .empty, 166 | }, 167 | ); 168 | b.next_id += 1; 169 | return id; 170 | } 171 | 172 | pub fn popInitial(b: *Builder) void { 173 | const this = b.contexts.pop(); 174 | _ = b.parentables.pop(); 175 | assert(this.num_nones == 0); 176 | assert(b.contexts.top == null); 177 | assert(b.parentables.top == null); 178 | } 179 | 180 | pub fn pop(b: *Builder, box_tree: *BoxTree) void { 181 | assert(b.contexts.top != null); 182 | assert(b.parentables.top != null); 183 | 184 | const num_nones = &b.contexts.top.?.num_nones; 185 | if (num_nones.* > 0) { 186 | num_nones.* -= 1; 187 | return; 188 | } 189 | 190 | const this = b.contexts.pop(); 191 | if (this.parentable) { 192 | assert(this.index == b.parentables.pop().index); 193 | const skips = box_tree.sct.list.items(.skip); 194 | const parent_index = b.parentables.top.?.index; 195 | skips[parent_index] += skips[this.index]; 196 | } 197 | } 198 | 199 | pub fn setBlock(b: *Builder, id: Id, box_tree: *BoxTree, ref: BlockRef) void { 200 | const sct = box_tree.sct.list.slice(); 201 | const ids = sct.items(.id); 202 | const index: Size = @intCast(std.mem.indexOfScalar(Id, ids, id).?); 203 | sct.items(.ref)[index] = ref; 204 | b.incompletes.remove(id); 205 | } 206 | 207 | pub fn addIfc(b: *Builder, box_tree: *BoxTree, ifc_id: IfcId) !void { 208 | const index = b.contexts.top.?.index; 209 | const list = &box_tree.sct.list.items(.ifcs)[index]; 210 | try list.append(box_tree.allocator, ifc_id); 211 | } 212 | -------------------------------------------------------------------------------- /docs/zml.md: -------------------------------------------------------------------------------- 1 | # zml - zss markup language 2 | 3 | **zml** is a document language made for zss. It has the following features: 4 | - It's a simple format for quickly creating documents for testing zss 5 | - Has the bare minimum amount of features to satisfy the above point 6 | - It's an example of integrating a document language with zss 7 | - Borrows syntax elements from CSS to make for an intuitive syntax 8 | 9 | ## Creating a document 10 | A document is just a tree of nodes. zml has two types of nodes: **text nodes** and **element nodes**. 11 | 12 | ### Text nodes 13 | Defining a text node is as simple as typing a quoted string: 14 | ```css 15 | "Hello from zml" 16 | ``` 17 | The syntax for quoted strings is exactly the same as the CSS syntax for strings. 18 | 19 | ### Element nodes 20 | An element node consists of three parts: a **list of features**, an **inline style block**, and a **block of child nodes**. Features change how the element is matched against CSS selectors. The inline style block contains CSS styles that apply directly to the element. The block of child nodes is self-explanatory. 21 | 22 | Example: 23 | ```css 24 | #main (display: block) {} 25 | ``` 26 | This example defines a single element node. This element has a single feature: it has an ID feature with the name "main". This element also has an inline style block; it contains a declaration of the CSS 'display' property. Finally, it ends in a block of child nodes, which in this case is empty. 27 | 28 | #### Features 29 | A "feature" is something that affects how an element interacts with CSS selectors. An element can have any number of features, separated by spaces. In zml, the features you can define on an element are as follows: 30 | 31 | ##### Type 32 | A type feature (also sometimes known as a tag name) is any valid CSS identifier. Unlike other features, an element is limited to having only 0 or 1 type features. 33 | 34 | Examples: 35 | ```css 36 | html 37 | my-weird-type-42 38 | ``` 39 | 40 | ##### ID 41 | An ID feature consists of a CSS hash token along with a valid CSS identifier. An ID uniquely identifies an element in the document. If more than one element has the same ID, then only the one that appears first in the document will have that ID. 42 | 43 | Examples: 44 | ```css 45 | #alice 46 | #jane-doe 47 | ``` 48 | 49 | ##### Class 50 | A class feature adds an element to a class. It consists of a dot along with a valid CSS identifier, with no space in between. 51 | 52 | Examples: 53 | ```css 54 | .class1 55 | .flex 56 | ``` 57 | 58 | ##### Attribute 59 | An attribute feature gives an attribute to an element. Optionally, an attribute feature may also include a value. An attribute feature without a value looks like `[name]`, where "name" is a CSS identifier. An attribute feature with a value looks like `[name="value"]`, where "name" is like before, and "value" is either a CSS identifier or CSS string. 60 | 61 | Examples: 62 | ```css 63 | [wrap] 64 | [width = auto] 65 | [charset = 'utf-8'] 66 | ``` 67 | 68 | #### Empty elements 69 | It is also possible to define an element that has no features at all. Such an element is called "empty", and can be defined using the "empty feature": an asterisk. 70 | 71 | ```css 72 | * {} /* An empty element. */ 73 | ``` 74 | 75 | The empty feature `*` cannot appear alongside any other features. 76 | 77 | #### Inline style blocks 78 | An element may have an inline style block. This contains CSS declarations that apply directly to said element (these are also known as "style attributes"). Inline style blocks are optional, and if it exists, it must contain at least one declaration. Inline style blocks must appear after the element's list of features. 79 | 80 | Syntactically, an inline style block is a series of CSS declarations surrounded by round brackets `()`. Each CSS declaration is exactly the same as ordinary CSS: a property name, followed by a colon `:`, followed by the property's value, followed by a semicolon `;`. Optionally, the last declaration in the block can omit its trailing semicolon. 81 | 82 | Examples: 83 | ```css 84 | (display: inline) 85 | (width: 1280px; height: 720px;) 86 | ``` 87 | 88 | Declarations within the inline style block are treated as style attributes, and therefore have a higher precedence within the CSS cascade. 89 | 90 | #### Child nodes 91 | Element nodes can have child nodes, which are themselves other element or text nodes. Child nodes appear within a curly bracket `{}` block after an element's features and inline style block. The block of child nodes must be present even if there are no child nodes. 92 | 93 | Example: 94 | ```css 95 | body { 96 | "Hello" 97 | span { 98 | "World" 99 | } 100 | div {} 101 | } 102 | ``` 103 | This example shows an element (**body**) with three child nodes: a text node and two element nodes. The first child element node (**span**) itself has a child text node. The second child element node (**div**) has no children of its own. 104 | 105 | ## Directives 106 | zml defines directives, which can be used to modify a node in some way. Directives must appear just before the node they are modifying. Syntactically, directives look like `@directive(arguments)`, where `arguments` depends on the directive being used. 107 | 108 | ### List of directives 109 | Each item here gives a description of each directive and the correct syntax for its arguments. At this time, zml only defines a single directive. 110 | 111 | #### @name 112 | **Syntax**: a CSS identifier 113 | **Description**: Give an internal name to a node. When accessing the document using a programming language, this internal name can be used as a reference to this node. It is an error for more than one node to have the same internal name. The internal name is *not* a feature, and therefore doesn't affect the CSS cascade. 114 | 115 | Example document: 116 | ```css 117 | body { 118 | @name(my-custom-name) "Please replace this text" 119 | } 120 | ``` 121 | 122 | Example Zig pseudo-code: 123 | ```zig 124 | const string = someRuntimeKnownString(...); 125 | const document = createDocument(...); 126 | const node = document.getNodeByName("my-custom-name"); 127 | node.setText(string); 128 | ``` 129 | 130 | This example shows using a text node's internal name to directly refer to it and perform operations on it (in this case, replacing its textual content). 131 | 132 | ## Grammar 133 | 134 | The grammar of zml documents is presented here. 135 | This grammar definition uses the value definition syntax described in CSS Values and Units Level 4. 136 | 137 | ``` 138 | = 139 | = * [ | ] 140 | = '(' ')' 141 | 142 | = ? 143 | = 144 | 145 | = '*' | [ | | | ]+ 146 | = 147 | = 148 | = '.' 149 | = '[' [ '=' ]? ']' 150 | = | 151 | 152 | = '(' ')' 153 | 154 | = '{' * '}' 155 | 156 | = 157 | = 158 | = 159 | = 160 | = 161 | = 162 | ``` 163 | 164 | Whitespace or comments are required between the components of ``. 165 | The `` component of `` must be an "id" hash token. 166 | No whitespace or comments are allowed between the components of ``. 167 | No whitespace or comments are allowed between the `` and '(' of ``. 168 | -------------------------------------------------------------------------------- /source/values/parse/background.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const zss = @import("../../zss.zig"); 4 | const SourceCode = zss.syntax.SourceCode; 5 | 6 | const values = zss.values; 7 | const types = values.types; 8 | const Context = values.parse.Context; 9 | const Urls = values.parse.Urls; 10 | 11 | // Spec: CSS Backgrounds and Borders Level 3 12 | // Syntax: | none 13 | // = | 14 | // = | | | 15 | pub fn image(ctx: *Context, urls: Urls.Managed) !?types.BackgroundImage { 16 | // TODO: parse gradient functions 17 | if (values.parse.keyword(ctx, types.BackgroundImage, &.{.{ "none", .none }})) |value| { 18 | return value; 19 | } else if (try values.parse.urlManaged(ctx, urls, .background_image)) |value| { 20 | return .{ .url = value }; 21 | } else { 22 | return null; 23 | } 24 | } 25 | 26 | // Spec: CSS Backgrounds and Borders Level 3 27 | // Syntax: = repeat-x | repeat-y | [repeat | space | round | no-repeat]{1,2} 28 | pub fn repeat(ctx: *Context) ?types.BackgroundRepeat { 29 | if (values.parse.keyword(ctx, types.BackgroundRepeat, &.{ 30 | .{ "repeat-x", .{ .x = .repeat, .y = .no_repeat } }, 31 | .{ "repeat-y", .{ .x = .no_repeat, .y = .repeat } }, 32 | })) |value| { 33 | return value; 34 | } 35 | 36 | const Style = types.BackgroundRepeat.Style; 37 | const map = comptime &[_]SourceCode.KV(Style){ 38 | .{ "repeat", .repeat }, 39 | .{ "space", .space }, 40 | .{ "round", .round }, 41 | .{ "no-repeat", .no_repeat }, 42 | }; 43 | if (values.parse.keyword(ctx, Style, map)) |x| { 44 | const y = values.parse.keyword(ctx, Style, map) orelse x; 45 | return .{ .x = x, .y = y }; 46 | } 47 | 48 | return null; 49 | } 50 | 51 | // Spec: CSS Backgrounds and Borders Level 3 52 | // Syntax: = scroll | fixed | local 53 | pub fn attachment(ctx: *Context) ?types.BackgroundAttachment { 54 | return values.parse.keyword(ctx, types.BackgroundAttachment, &.{ 55 | .{ "scroll", .scroll }, 56 | .{ "fixed", .fixed }, 57 | .{ "local", .local }, 58 | }); 59 | } 60 | 61 | const bg_position = struct { 62 | const Side = types.BackgroundPosition.Side; 63 | const Offset = types.BackgroundPosition.Offset; 64 | const Axis = enum { x, y, either }; 65 | 66 | const KeywordMapValue = struct { axis: Axis, side: Side }; 67 | // zig fmt: off 68 | const keyword_map = &[_]SourceCode.KV(KeywordMapValue){ 69 | .{ "center", .{ .axis = .either, .side = .center } }, 70 | .{ "left", .{ .axis = .x, .side = .start } }, 71 | .{ "right", .{ .axis = .x, .side = .end } }, 72 | .{ "top", .{ .axis = .y, .side = .start } }, 73 | .{ "bottom", .{ .axis = .y, .side = .end } }, 74 | }; 75 | // zig fmt: on 76 | 77 | const Info = struct { 78 | axis: Axis, 79 | side: Side, 80 | offset: Offset, 81 | }; 82 | 83 | const ResultTuple = struct { 84 | bg_position: types.BackgroundPosition, 85 | num_items_used: u3, 86 | }; 87 | }; 88 | 89 | /// Spec: CSS Backgrounds and Borders Level 3 90 | /// Syntax: = [ left | center | right | top | bottom | ] 91 | /// | 92 | /// [ left | center | right | ] 93 | /// [ top | center | bottom | ] 94 | /// | 95 | /// [ center | [ left | right ] ? ] && 96 | /// [ center | [ top | bottom ] ? ] 97 | pub fn position(ctx: *Context) ?types.BackgroundPosition { 98 | const save_point = ctx.savePoint(); 99 | return backgroundPosition3Or4Values(ctx) orelse blk: { 100 | ctx.resetPoint(save_point); 101 | break :blk backgroundPosition1Or2Values(ctx); 102 | }; 103 | } 104 | 105 | /// Spec: CSS Backgrounds and Borders Level 3 106 | /// Syntax: [ center | [ left | right ] ? ] && 107 | /// [ center | [ top | bottom ] ? ] 108 | fn backgroundPosition3Or4Values(ctx: *Context) ?types.BackgroundPosition { 109 | const first, const num_values1 = backgroundPosition3Or4ValuesInfo(ctx) orelse return null; 110 | const second, const num_values2 = backgroundPosition3Or4ValuesInfo(ctx) orelse return null; 111 | if (num_values1 + num_values2 < 3) return null; 112 | 113 | var x_axis: *const bg_position.Info = undefined; 114 | var y_axis: *const bg_position.Info = undefined; 115 | 116 | switch (first.axis) { 117 | .x => { 118 | x_axis = &first; 119 | y_axis = switch (second.axis) { 120 | .x => return null, 121 | .y => &second, 122 | .either => &second, 123 | }; 124 | }, 125 | .y => { 126 | x_axis = switch (second.axis) { 127 | .x => &second, 128 | .y => return null, 129 | .either => &second, 130 | }; 131 | y_axis = &first; 132 | }, 133 | .either => switch (second.axis) { 134 | .x => { 135 | x_axis = &second; 136 | y_axis = &first; 137 | }, 138 | .y, .either => { 139 | x_axis = &first; 140 | y_axis = &second; 141 | }, 142 | }, 143 | } 144 | 145 | return .{ 146 | .x = .{ 147 | .side = x_axis.side, 148 | .offset = x_axis.offset, 149 | }, 150 | .y = .{ 151 | .side = y_axis.side, 152 | .offset = y_axis.offset, 153 | }, 154 | }; 155 | } 156 | 157 | fn backgroundPosition3Or4ValuesInfo(ctx: *Context) ?struct { bg_position.Info, u3 } { 158 | const map_value = values.parse.keyword(ctx, bg_position.KeywordMapValue, bg_position.keyword_map) orelse return null; 159 | 160 | const offset: bg_position.Offset, const num_values: u3 = blk: { 161 | if (map_value.side != .center) { 162 | if (values.parse.lengthPercentage(ctx, bg_position.Offset)) |value| { 163 | break :blk .{ value, 2 }; 164 | } 165 | } 166 | break :blk .{ .{ .percentage = 0 }, 1 }; 167 | }; 168 | 169 | return .{ .{ .axis = map_value.axis, .side = map_value.side, .offset = offset }, num_values }; 170 | } 171 | 172 | /// Spec: CSS Backgrounds and Borders Level 3 173 | /// Syntax: [ left | center | right | top | bottom | ] 174 | /// | 175 | /// [ left | center | right | ] 176 | /// [ top | center | bottom | ] 177 | fn backgroundPosition1Or2Values(ctx: *Context) ?types.BackgroundPosition { 178 | const first = backgroundPosition1Or2ValuesInfo(ctx) orelse return null; 179 | twoValues: { 180 | if (first.axis == .y) break :twoValues; 181 | const save_point = ctx.savePoint(); 182 | const second = backgroundPosition1Or2ValuesInfo(ctx) orelse break :twoValues; 183 | if (second.axis == .x) { 184 | ctx.resetPoint(save_point); 185 | break :twoValues; 186 | } 187 | 188 | return .{ 189 | .x = .{ 190 | .side = first.side, 191 | .offset = first.offset, 192 | }, 193 | .y = .{ 194 | .side = second.side, 195 | .offset = second.offset, 196 | }, 197 | }; 198 | } 199 | 200 | var result = types.BackgroundPosition{ 201 | .x = .{ 202 | .side = first.side, 203 | .offset = first.offset, 204 | }, 205 | .y = .{ 206 | .side = .center, 207 | .offset = .{ .percentage = 0 }, 208 | }, 209 | }; 210 | if (first.axis == .y) { 211 | std.mem.swap(types.BackgroundPosition.SideOffset, &result.x, &result.y); 212 | } 213 | return result; 214 | } 215 | 216 | fn backgroundPosition1Or2ValuesInfo(ctx: *Context) ?bg_position.Info { 217 | if (values.parse.keyword(ctx, bg_position.KeywordMapValue, bg_position.keyword_map)) |map_value| { 218 | return .{ .axis = map_value.axis, .side = map_value.side, .offset = .{ .percentage = 0 } }; 219 | } else if (values.parse.lengthPercentage(ctx, bg_position.Offset)) |offset| { 220 | return .{ .axis = .either, .side = .start, .offset = offset }; 221 | } else { 222 | return null; 223 | } 224 | } 225 | 226 | /// Spec: CSS Backgrounds and Borders Level 3 227 | /// Syntax: = border-box | padding-box | content-box 228 | pub fn clip(ctx: *Context) ?types.BackgroundClip { 229 | return values.parse.keyword(ctx, types.BackgroundClip, &.{ 230 | .{ "border-box", .border_box }, 231 | .{ "padding-box", .padding_box }, 232 | .{ "content-box", .content_box }, 233 | }); 234 | } 235 | 236 | /// Spec: CSS Backgrounds and Borders Level 3 237 | /// Syntax: = border-box | padding-box | content-box 238 | pub fn origin(ctx: *Context) ?types.BackgroundOrigin { 239 | return values.parse.keyword(ctx, types.BackgroundOrigin, &.{ 240 | .{ "border-box", .border_box }, 241 | .{ "padding-box", .padding_box }, 242 | .{ "content-box", .content_box }, 243 | }); 244 | } 245 | 246 | /// Spec: CSS Backgrounds and Borders Level 3 247 | /// Syntax: = [ | auto ]{1,2} | cover | contain 248 | pub fn size(ctx: *Context) ?types.BackgroundSize { 249 | if (values.parse.keyword(ctx, types.BackgroundSize, &.{ 250 | .{ "cover", .cover }, 251 | .{ "contain", .contain }, 252 | })) |value| return value; 253 | 254 | const save_point = ctx.savePoint(); 255 | // TODO: Range checking? 256 | if (values.parse.lengthPercentageAuto(ctx, types.BackgroundSize.SizeType)) |width| { 257 | const height = values.parse.lengthPercentageAuto(ctx, types.BackgroundSize.SizeType) orelse width; 258 | return types.BackgroundSize{ .size = .{ .width = width, .height = height } }; 259 | } 260 | 261 | ctx.resetPoint(save_point); 262 | return null; 263 | } 264 | -------------------------------------------------------------------------------- /test/suite.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const assert = std.debug.assert; 3 | const Allocator = std.mem.Allocator; 4 | const ArenaAllocator = std.heap.ArenaAllocator; 5 | 6 | const zss = @import("zss"); 7 | const Ast = zss.syntax.Ast; 8 | const ElementTree = zss.ElementTree; 9 | const Element = ElementTree.Element; 10 | const SourceCode = zss.syntax.SourceCode; 11 | 12 | const hb = @import("harfbuzz").c; 13 | const zigimg = @import("zigimg"); 14 | 15 | const Test = @import("Test.zig"); 16 | 17 | const Args = struct { 18 | test_cases_path: []const u8, 19 | resources_path: []const u8, 20 | output_path: []const u8, 21 | filters: []const []const u8, 22 | 23 | fn init(arena: *ArenaAllocator) !Args { 24 | const allocator = arena.allocator(); 25 | const argv = try std.process.argsAlloc(allocator); 26 | var args = Args{ 27 | .test_cases_path = argv[1], 28 | .resources_path = argv[2], 29 | .output_path = argv[3], 30 | .filters = undefined, 31 | }; 32 | 33 | var filters = std.ArrayList([]const u8).empty; 34 | var stderr = std.fs.File.stderr().writer(&.{}); 35 | var i: usize = 4; 36 | while (i < argv.len) { 37 | const arg = argv[i]; 38 | if (std.mem.eql(u8, arg, "--test-filter")) { 39 | i += 1; 40 | if (i == argv.len) { 41 | stderr.interface.writeAll("Missing argument after '--test-filter'\n") catch {}; 42 | stderr.interface.flush() catch {}; 43 | std.process.exit(1); 44 | } 45 | try filters.append(allocator, argv[i]); 46 | i += 1; 47 | } else { 48 | stderr.interface.print("Unrecognized argument: {s}\n", .{arg}) catch {}; 49 | stderr.interface.flush() catch {}; 50 | std.process.exit(1); 51 | } 52 | } 53 | 54 | args.filters = filters.items; 55 | 56 | return args; 57 | } 58 | }; 59 | 60 | pub fn main() !void { 61 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 62 | defer assert(gpa.deinit() == .ok); 63 | 64 | var arena = ArenaAllocator.init(gpa.allocator()); 65 | defer arena.deinit(); 66 | 67 | const args = try Args.init(&arena); 68 | 69 | var library: hb.FT_Library = undefined; 70 | if (hb.FT_Init_FreeType(&library) != hb.FT_Err_Ok) return error.FreeTypeError; 71 | defer _ = hb.FT_Done_FreeType(library); 72 | 73 | const font_name = try std.fs.path.joinZ(arena.allocator(), &.{ args.resources_path, "NotoSans-Regular.ttf" }); 74 | const font_size = 12; 75 | var face: hb.FT_Face = undefined; 76 | if (hb.FT_New_Face(library, font_name.ptr, 0, &face) != hb.FT_Err_Ok) return error.FreeTypeError; 77 | defer _ = hb.FT_Done_Face(face); 78 | if (hb.FT_Select_Charmap(face, hb.FT_ENCODING_UNICODE) != hb.FT_Err_Ok) return error.FreeTypeError; 79 | if (hb.FT_Set_Char_Size(face, 0, font_size * 64, 96, 96) != hb.FT_Err_Ok) return error.FreeTypeError; 80 | 81 | const font = hb.hb_ft_font_create_referenced(face).?; 82 | hb.hb_ft_font_set_funcs(font); 83 | 84 | var images = zss.Images.init(); 85 | defer images.deinit(arena.allocator()); 86 | 87 | var fonts = zss.Fonts.init(); 88 | defer fonts.deinit(); 89 | const font_handle = fonts.setFont(font); 90 | 91 | const tests = try getAllTests(args, &arena, &images, &fonts, font_handle); 92 | 93 | const Category = enum { check, memory, opengl, print }; 94 | inline for (@import("build-options").test_categories) |category| { 95 | const module = comptime switch (std.meta.stringToEnum(Category, category) orelse @compileError("unknown test category: " ++ category)) { 96 | .check => @import("check.zig"), 97 | .memory => @import("memory.zig"), 98 | .opengl => @import("opengl.zig"), 99 | .print => @import("print.zig"), 100 | }; 101 | try module.run(tests, args.output_path); 102 | } 103 | } 104 | 105 | fn getAllTests( 106 | args: Args, 107 | arena: *ArenaAllocator, 108 | images: *zss.Images, 109 | fonts: *const zss.Fonts, 110 | font_handle: zss.Fonts.Handle, 111 | ) ![]*Test { 112 | const allocator = arena.allocator(); 113 | 114 | const cwd = std.fs.cwd(); 115 | 116 | var cases_dir = try cwd.openDir(args.test_cases_path, .{ .iterate = true }); 117 | defer cases_dir.close(); 118 | 119 | var loader = try ResourceLoader.init(args); 120 | defer loader.deinit(); 121 | 122 | const ua_stylesheet = blk: { 123 | const source_code = try SourceCode.init(@embedFile("ua-stylesheet.css")); 124 | var parser = zss.syntax.Parser.init(source_code, allocator); 125 | const ast, const rule_list = try parser.parseCssStylesheet(allocator); 126 | break :blk UaStylesheet{ .ast = ast, .rule_list = rule_list, .source_code = source_code }; 127 | }; 128 | 129 | var walker = try cases_dir.walk(allocator); 130 | defer walker.deinit(); 131 | 132 | var list = std.ArrayList(*Test).empty; 133 | 134 | while (try walker.next()) |entry| { 135 | if (entry.kind != .file or !std.mem.endsWith(u8, entry.basename, ".zml")) continue; 136 | if (args.filters.len > 0) { 137 | for (args.filters) |filter| { 138 | if (std.mem.indexOf(u8, entry.path, filter)) |_| break; 139 | } else continue; 140 | } 141 | 142 | const source = try cases_dir.readFileAlloc(allocator, entry.path, 100_000); 143 | const source_code = try SourceCode.init(source); 144 | const ast, const zml_document_index = blk: { 145 | var parser = zss.syntax.Parser.init(source_code, allocator); 146 | break :blk try parser.parseZmlDocument(allocator); 147 | }; 148 | 149 | const name = try allocator.dupe(u8, entry.path[0 .. entry.path.len - ".zml".len]); 150 | 151 | const t = try createTest(arena, name, ast, source_code, zml_document_index, images, fonts, font_handle, &loader, ua_stylesheet); 152 | try list.append(allocator, t); 153 | } 154 | 155 | return list.items; 156 | } 157 | 158 | const UaStylesheet = struct { 159 | ast: zss.syntax.Ast, 160 | rule_list: zss.syntax.Ast.Index, 161 | source_code: SourceCode, 162 | }; 163 | 164 | fn createTest( 165 | arena: *ArenaAllocator, 166 | name: []const u8, 167 | ast: Ast, 168 | source_code: SourceCode, 169 | zml_document_index: Ast.Index, 170 | images: *zss.Images, 171 | fonts: *const zss.Fonts, 172 | font_handle: zss.Fonts.Handle, 173 | loader: *ResourceLoader, 174 | ua_stylesheet: UaStylesheet, 175 | ) !*Test { 176 | const allocator = arena.allocator(); 177 | 178 | const t = try allocator.create(Test); 179 | t.* = .{ 180 | .name = name, 181 | .images = images, 182 | .fonts = fonts, 183 | .font_handle = font_handle, 184 | 185 | .document = undefined, 186 | .stylesheet = undefined, 187 | }; 188 | 189 | t.document = try zss.zml.createDocument(allocator, ast, source_code, zml_document_index); 190 | t.stylesheet = try zss.Stylesheet.create(allocator, ua_stylesheet.ast, ua_stylesheet.rule_list, ua_stylesheet.source_code, &t.document.env); 191 | 192 | const cascade_list = zss.cascade.List{ 193 | .author = &.{&.{ .leaf = &t.document.cascade_source }}, 194 | .user_agent = &.{&.{ .leaf = &t.stylesheet.cascade_source }}, 195 | }; 196 | try zss.cascade.run(&cascade_list, &t.document.env, allocator); 197 | 198 | try loader.loadResourcesFromUrls(arena, t, images, source_code); 199 | 200 | return t; 201 | } 202 | 203 | const ResourceLoader = struct { 204 | res_dir: std.fs.Dir, 205 | /// maps image URLs to image handles 206 | seen_images: std.StringHashMapUnmanaged(zss.Images.Handle), 207 | 208 | fn init(args: Args) !ResourceLoader { 209 | const res_dir = try std.fs.cwd().openDir(args.resources_path, .{}); 210 | return .{ 211 | .res_dir = res_dir, 212 | .seen_images = .empty, 213 | }; 214 | } 215 | 216 | fn deinit(loader: *ResourceLoader) void { 217 | loader.res_dir.close(); 218 | } 219 | 220 | fn loadResourcesFromUrls( 221 | loader: *ResourceLoader, 222 | arena: *ArenaAllocator, 223 | t: *Test, 224 | images: *zss.Images, 225 | source_code: SourceCode, 226 | ) !void { 227 | const allocator = arena.allocator(); 228 | for (0..t.document.urls.len) |index| { 229 | const url = t.document.urls.get(index); 230 | const string = switch (url.src_loc) { 231 | .url_token => |location| try source_code.copyUrl(location, .{ .allocator = allocator }), 232 | .string_token => |location| try source_code.copyString(location, .{ .allocator = allocator }), 233 | }; 234 | 235 | switch (url.type) { 236 | .background_image => { 237 | const gop = try loader.seen_images.getOrPut(allocator, string); 238 | if (gop.found_existing) { 239 | try t.document.env.linkUrlToImage(url.id, gop.value_ptr.*); 240 | continue; 241 | } 242 | 243 | const file = try loader.res_dir.openFile(string, .{ .mode = .read_only }); 244 | defer file.close(); 245 | 246 | var read_buffer: [4096]u8 = undefined; 247 | const zigimg_image = try zigimg.Image.fromFile(allocator, file, &read_buffer); 248 | const zss_image = try images.addImage(allocator, .{ 249 | .dimensions = .{ 250 | .width_px = @intCast(zigimg_image.width), 251 | .height_px = @intCast(zigimg_image.height), 252 | }, 253 | .format = switch (zigimg_image.pixelFormat()) { 254 | .rgba32 => .rgba, 255 | else => return error.UnsupportedPixelFormat, 256 | }, 257 | .data = zigimg_image.rawBytes(), 258 | }); 259 | 260 | gop.value_ptr.* = zss_image; 261 | try t.document.env.linkUrlToImage(url.id, zss_image); 262 | }, 263 | } 264 | } 265 | } 266 | }; 267 | -------------------------------------------------------------------------------- /source/Layout/StyleComputer.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const assert = std.debug.assert; 3 | const Allocator = std.mem.Allocator; 4 | const ArrayListUnmanaged = std.ArrayListUnmanaged; 5 | 6 | const zss = @import("../zss.zig"); 7 | const CascadeStorage = zss.cascade.Database.Storage; 8 | const Environment = zss.Environment; 9 | const NodeId = Environment.NodeId; 10 | const Stack = zss.Stack; 11 | 12 | const groups = zss.values.groups; 13 | const SpecifiedValues = groups.Tag.SpecifiedValues; 14 | const ComputedValues = groups.Tag.ComputedValues; 15 | 16 | const solve = @import("./solve.zig"); 17 | 18 | const StyleComputer = @This(); 19 | 20 | pub const Stage = enum { 21 | box_gen, 22 | cosmetic, 23 | 24 | fn ComputedValues(comptime stage: Stage) type { 25 | return switch (stage) { 26 | .box_gen => BoxGenComputedValues, 27 | .cosmetic => CosmeticComputedValues, 28 | }; 29 | } 30 | }; 31 | 32 | const BoxGenComputedValues = struct { 33 | box_style: ?ComputedValues(.box_style) = null, 34 | content_width: ?ComputedValues(.content_width) = null, 35 | horizontal_edges: ?ComputedValues(.horizontal_edges) = null, 36 | content_height: ?ComputedValues(.content_height) = null, 37 | vertical_edges: ?ComputedValues(.vertical_edges) = null, 38 | border_styles: ?ComputedValues(.border_styles) = null, 39 | insets: ?ComputedValues(.insets) = null, 40 | z_index: ?ComputedValues(.z_index) = null, 41 | font: ?ComputedValues(.font) = null, 42 | }; 43 | 44 | const CosmeticComputedValues = struct { 45 | box_style: ?ComputedValues(.box_style) = null, 46 | border_colors: ?ComputedValues(.border_colors) = null, 47 | border_styles: ?ComputedValues(.border_styles) = null, 48 | background_color: ?ComputedValues(.background_color) = null, 49 | background_clip: ?ComputedValues(.background_clip) = null, 50 | background: ?ComputedValues(.background) = null, 51 | color: ?ComputedValues(.color) = null, 52 | insets: ?ComputedValues(.insets) = null, 53 | }; 54 | 55 | const Current = struct { 56 | node: NodeId, 57 | cascaded_values: *const CascadeStorage, 58 | }; 59 | 60 | env: *const Environment, 61 | current: Current, 62 | allocator: Allocator, 63 | stage: union { 64 | box_gen: struct { 65 | map: std.AutoHashMapUnmanaged(NodeId, BoxGenComputedValues) = .{}, 66 | current_computed: BoxGenComputedValues = undefined, 67 | }, 68 | cosmetic: struct { 69 | map: std.AutoHashMapUnmanaged(NodeId, CosmeticComputedValues) = .{}, 70 | current_computed: CosmeticComputedValues = undefined, 71 | }, 72 | }, 73 | 74 | pub fn init(env: *const Environment, allocator: Allocator) StyleComputer { 75 | return .{ 76 | .env = env, 77 | .allocator = allocator, 78 | .current = undefined, 79 | .stage = undefined, 80 | }; 81 | } 82 | 83 | pub fn deinit(self: *StyleComputer) void { 84 | _ = self; 85 | } 86 | 87 | pub fn deinitStage(sc: *StyleComputer, comptime stage: Stage) void { 88 | const current_stage = &@field(sc.stage, @tagName(stage)); 89 | current_stage.map.deinit(sc.allocator); 90 | } 91 | 92 | // TODO: Setting the current node should not require allocating 93 | pub fn setCurrentNode(sc: *StyleComputer, comptime stage: Stage, node: NodeId) !void { 94 | const cascaded_values: *const CascadeStorage = switch (sc.env.getNodeProperty(.category, node)) { 95 | .element => sc.env.cascade_db.getStorage(node) orelse &.{}, 96 | .text => undefined, 97 | }; 98 | sc.current = .{ 99 | .node = node, 100 | .cascaded_values = cascaded_values, 101 | }; 102 | 103 | const current_stage = &@field(sc.stage, @tagName(stage)); 104 | const gop_result = try current_stage.map.getOrPut(sc.allocator, node); 105 | if (!gop_result.found_existing) { 106 | gop_result.value_ptr.* = .{}; 107 | } 108 | current_stage.current_computed = gop_result.value_ptr.*; 109 | } 110 | 111 | pub fn commitNode(sc: *StyleComputer, comptime stage: Stage) void { 112 | const node = sc.current.node; 113 | const current_stage = &@field(sc.stage, @tagName(stage)); 114 | current_stage.map.putAssumeCapacity(node, current_stage.current_computed); 115 | } 116 | 117 | pub fn getText(sc: StyleComputer) []const u8 { 118 | const node = sc.current.node; 119 | assert(sc.env.getNodeProperty(.category, node) == .text); 120 | const id = sc.env.getNodeProperty(.text, node); 121 | return sc.env.getText(id); 122 | } 123 | 124 | pub fn getTextFont(sc: StyleComputer, comptime stage: Stage) ComputedValues(.font) { 125 | const node = sc.current.node; 126 | assert(sc.env.getNodeProperty(.category, node) == .text); 127 | var inherited_value = InheritedValue(.font){ .node = node }; 128 | return inherited_value.get(sc, stage); 129 | } 130 | 131 | pub fn setComputedValue(sc: *StyleComputer, comptime stage: Stage, comptime group: groups.Tag, value: ComputedValues(group)) void { 132 | const current_stage = &@field(sc.stage, @tagName(stage)); 133 | const field = &@field(current_stage.current_computed, @tagName(group)); 134 | assert(field.* == null); 135 | field.* = value; 136 | } 137 | 138 | pub fn getSpecifiedValue(sc: StyleComputer, comptime stage: Stage, comptime group: groups.Tag) SpecifiedValues(group) { 139 | return sc.getSpecifiedValueForElement(stage, group, sc.current.node, sc.current.cascaded_values); 140 | } 141 | 142 | fn getSpecifiedValueForElement( 143 | self: StyleComputer, 144 | comptime stage: Stage, 145 | comptime group: groups.Tag, 146 | node: NodeId, 147 | cascaded_values: *const CascadeStorage, 148 | ) SpecifiedValues(group) { 149 | assert(self.env.getNodeProperty(.category, node) == .element); 150 | const cascaded_value = cascaded_values.getPtr(group); 151 | 152 | const inheritance_type = comptime group.inheritanceType(); 153 | const default: enum { inherit, initial } = default: { 154 | // Use the value of the 'all' property. 155 | // 156 | // TODO: Handle 'direction', 'unicode-bidi', and custom properties specially here. 157 | // CSS-CASCADE-4§3.2: The all property is a shorthand that resets all CSS properties except direction and unicode-bidi. 158 | // [...] It does not reset custom properties. 159 | if (cascaded_values.all) |all| switch (all) { 160 | .initial => break :default .initial, 161 | .inherit => break :default .inherit, 162 | .unset => {}, 163 | }; 164 | 165 | // Just use the inheritance type. 166 | switch (inheritance_type) { 167 | .inherited => break :default .inherit, 168 | .not_inherited => break :default .initial, 169 | } 170 | }; 171 | 172 | const initial_value = group.initialValues(); 173 | if (cascaded_value == null and default == .initial) { 174 | return initial_value; 175 | } 176 | 177 | var inherited_value = InheritedValue(group){ .node = node }; 178 | if (cascaded_value == null and default == .inherit) { 179 | return inherited_value.get(self, stage); 180 | } 181 | 182 | var specified: SpecifiedValues(group) = undefined; 183 | inline for (group.fields()) |field| { 184 | const cascaded_property = @field(cascaded_value.?, field.name); 185 | const specified_property = &@field(specified, field.name); 186 | specified_property.* = switch (cascaded_property) { 187 | .inherit => @field(inherited_value.get(self, stage), field.name), 188 | .initial => @field(initial_value, field.name), 189 | .unset => switch (inheritance_type) { 190 | .inherited => @field(inherited_value.get(self, stage), field.name), 191 | .not_inherited => @field(initial_value, field.name), 192 | }, 193 | .undeclared => switch (default) { 194 | .inherit => @field(inherited_value.get(self, stage), field.name), 195 | .initial => @field(initial_value, field.name), 196 | }, 197 | .declared => |declared| declared, 198 | }; 199 | } 200 | 201 | return specified; 202 | } 203 | 204 | fn InheritedValue(comptime group: groups.Tag) type { 205 | return struct { 206 | value: ?ComputedValues(group) = null, 207 | node: NodeId, 208 | 209 | fn get(self: *@This(), sc: StyleComputer, comptime stage: Stage) ComputedValues(group) { 210 | if (self.value) |value| return value; 211 | 212 | const current_stage = @field(sc.stage, @tagName(stage)); 213 | self.value = if (self.node.parent(sc.env)) |parent| blk: { // TODO: Should check for equality with the root node instead 214 | if (current_stage.map.get(parent)) |parent_computed_values| { 215 | if (@field(parent_computed_values, @tagName(group))) |inherited_value| { 216 | break :blk inherited_value; 217 | } 218 | } 219 | // TODO: Cache the parent's computed value for faster access in future calls. 220 | 221 | const cascaded_values: *const CascadeStorage = sc.env.cascade_db.getStorage(parent) orelse &.{}; 222 | // TODO: Recursive call here 223 | const specified_value = sc.getSpecifiedValueForElement(stage, group, parent, cascaded_values); 224 | break :blk specifiedToComputed(group, specified_value, sc, parent); 225 | } else group.initialValues(); 226 | return self.value.?; 227 | } 228 | }; 229 | } 230 | 231 | /// Given a specified value, returns a computed value. 232 | fn specifiedToComputed(comptime group: groups.Tag, specified: SpecifiedValues(group), sc: StyleComputer, node: NodeId) ComputedValues(group) { 233 | switch (group) { 234 | .box_style => { 235 | const parent = node.parent(sc.env); 236 | const computed_value, _ = if (parent == null) // TODO: Should check for equality with the root node instead 237 | solve.boxStyle(specified, .root) 238 | else 239 | solve.boxStyle(specified, .not_root); 240 | return computed_value; 241 | }, 242 | .font => { 243 | // TODO: This is not the correct computed value for fonts. 244 | return specified; 245 | }, 246 | .color => { 247 | return .{ 248 | .color = specified.color, 249 | }; 250 | }, 251 | else => std.debug.panic("TODO: specifiedToComputed for aggregate '{s}'", .{@tagName(group)}), 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /source/Stylesheet.zig: -------------------------------------------------------------------------------- 1 | /// The set of namespace prefixes and their corresponding namespace ids. 2 | namespaces: Namespaces, 3 | /// URLs found while parsing declaration blocks. 4 | decl_urls: Urls, 5 | cascade_source: cascade.Source, 6 | 7 | pub const Namespaces = struct { 8 | indexer: zss.Utf8StringInterner = .init(.{ .max_size = NamespaceId.max_unique_values, .case = .sensitive }), 9 | /// Maps namespace prefixes to namespace ids. 10 | ids: std.ArrayListUnmanaged(NamespaceId) = .empty, 11 | /// The default namespace, or `null` if there is no default namespace. 12 | default: ?NamespaceId = null, 13 | 14 | pub fn deinit(namespaces: *Namespaces, allocator: Allocator) void { 15 | namespaces.indexer.deinit(allocator); 16 | namespaces.ids.deinit(allocator); 17 | } 18 | }; 19 | 20 | const Stylesheet = @This(); 21 | 22 | const zss = @import("zss.zig"); 23 | const cascade = zss.cascade; 24 | const Ast = zss.syntax.Ast; 25 | const AtRule = zss.syntax.Token.AtRule; 26 | const Declarations = zss.Declarations; 27 | const Environment = zss.Environment; 28 | const Importance = Declarations.Importance; 29 | const NamespaceId = Environment.Namespaces.Id; 30 | const SourceCode = zss.syntax.SourceCode; 31 | const Urls = zss.values.parse.Urls; 32 | 33 | const selectors = zss.selectors; 34 | const Specificity = selectors.Specificity; 35 | 36 | const std = @import("std"); 37 | const assert = std.debug.assert; 38 | const Allocator = std.mem.Allocator; 39 | 40 | /// Releases all resources associated with the stylesheet. 41 | pub fn deinit(stylesheet: *Stylesheet, allocator: Allocator) void { 42 | stylesheet.namespaces.deinit(allocator); 43 | stylesheet.decl_urls.deinit(allocator); 44 | stylesheet.cascade_source.deinit(allocator); 45 | } 46 | 47 | pub fn parseAndCreate(allocator: Allocator, source_code: SourceCode, env: *Environment) !Stylesheet { 48 | var parser = zss.syntax.Parser.init(source_code, allocator); 49 | defer parser.deinit(); 50 | var ast, const rule_list_index = try parser.parseCssStylesheet(allocator); 51 | defer ast.deinit(allocator); 52 | return create(allocator, ast, rule_list_index, source_code, env); 53 | } 54 | 55 | /// Create a `Stylesheet` from an Ast `rule_list` node. 56 | /// Free using `deinit`. 57 | pub fn create( 58 | allocator: Allocator, 59 | ast: Ast, 60 | rule_list_index: Ast.Index, 61 | source_code: SourceCode, 62 | env: *Environment, 63 | ) !Stylesheet { 64 | var stylesheet = Stylesheet{ 65 | .namespaces = .{}, 66 | .decl_urls = .init(env), 67 | .cascade_source = .{}, 68 | }; 69 | errdefer stylesheet.deinit(allocator); 70 | 71 | var selector_parser = selectors.Parser.init(env, allocator, source_code, ast, &stylesheet.namespaces); 72 | defer selector_parser.deinit(); 73 | 74 | var unsorted_selectors = std.MultiArrayList(struct { index: selectors.Data.ListIndex, specificity: Specificity }){}; 75 | defer unsorted_selectors.deinit(allocator); 76 | 77 | assert(rule_list_index.tag(ast) == .rule_list); 78 | var rule_sequence = rule_list_index.children(ast); 79 | while (rule_sequence.nextSkipSpaces(ast)) |index| { 80 | switch (index.tag(ast)) { 81 | .at_rule => { 82 | const at_rule = index.extra(ast).at_rule orelse { 83 | zss.log.warn("Ignoring unknown at-rule: @{f}", .{source_code.formatAtKeywordToken(index.location(ast))}); 84 | continue; 85 | }; 86 | atRule(&stylesheet.namespaces, allocator, ast, source_code, env, at_rule, index) catch |err| switch (err) { 87 | error.InvalidAtRule => { 88 | // NOTE: This is no longer a valid style sheet. 89 | zss.log.warn("Ignoring invalid @{s} at-rule", .{@tagName(at_rule)}); 90 | continue; 91 | }, 92 | error.UnrecognizedAtRule => { 93 | zss.log.warn("Ignoring unknown at-rule: @{s}", .{@tagName(at_rule)}); 94 | continue; 95 | }, 96 | else => |e| return e, 97 | }; 98 | }, 99 | .qualified_rule => { 100 | // TODO: Handle invalid style rules 101 | 102 | // Parse selectors 103 | const selector_sequence = ast.qualifiedRulePrelude(index); 104 | const first_complex_selector: selectors.Data.ListIndex = @intCast(stylesheet.cascade_source.selector_data.items.len); 105 | selector_parser.parseComplexSelectorList(&stylesheet.cascade_source.selector_data, allocator, selector_sequence) catch |err| switch (err) { 106 | error.ParseError => continue, 107 | else => |e| return e, 108 | }; 109 | 110 | // Parse the style block 111 | const last_declaration = selector_sequence.end.extra(ast).index; 112 | var buffer: [zss.property.recommended_buffer_size]u8 = undefined; 113 | const decl_block = try zss.property.parseDeclarationsFromAst(env, ast, source_code, &buffer, last_declaration, stylesheet.decl_urls.toManaged(allocator)); 114 | 115 | var index_of_complex_selector = first_complex_selector; 116 | for (selector_parser.specificities.items) |specificity| { 117 | const selector_number: selectors.Data.ListIndex = @intCast(unsorted_selectors.len); 118 | try unsorted_selectors.append(allocator, .{ .index = index_of_complex_selector, .specificity = specificity }); 119 | 120 | for ([_]Importance{ .important, .normal }) |importance| { 121 | const destination_list = switch (importance) { 122 | .important => &stylesheet.cascade_source.selectors_important, 123 | .normal => &stylesheet.cascade_source.selectors_normal, 124 | }; 125 | if (!env.decls.hasValues(decl_block, importance)) continue; 126 | 127 | try destination_list.append(allocator, .{ 128 | // Temporarily store the selector number; after sorting, this is replaced with the selector index. 129 | .selector = selector_number, 130 | .block = decl_block, 131 | }); 132 | } 133 | 134 | index_of_complex_selector = stylesheet.cascade_source.selector_data.items[index_of_complex_selector].next_complex_selector; 135 | } 136 | assert(index_of_complex_selector == stylesheet.cascade_source.selector_data.items.len); 137 | }, 138 | else => unreachable, 139 | } 140 | } 141 | 142 | stylesheet.decl_urls.commit(env); 143 | 144 | const unsorted_selectors_slice = unsorted_selectors.slice(); 145 | 146 | // Sort the selectors such that items with a higher cascade order appear earlier in each list. 147 | for ([_]Importance{ .important, .normal }) |importance| { 148 | const list = switch (importance) { 149 | .important => &stylesheet.cascade_source.selectors_important, 150 | .normal => &stylesheet.cascade_source.selectors_normal, 151 | }; 152 | const SortContext = struct { 153 | selector_number: []const selectors.Data.ListIndex, 154 | blocks: []const Declarations.Block, 155 | specificities: []const Specificity, 156 | 157 | pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool { 158 | const a_spec = ctx.specificities[ctx.selector_number[a_index]]; 159 | const b_spec = ctx.specificities[ctx.selector_number[b_index]]; 160 | switch (a_spec.order(b_spec)) { 161 | .lt => return false, 162 | .gt => return true, 163 | .eq => {}, 164 | } 165 | 166 | const a_block = ctx.blocks[a_index]; 167 | const b_block = ctx.blocks[b_index]; 168 | return !a_block.earlierThan(b_block); 169 | } 170 | }; 171 | list.sortUnstable(SortContext{ 172 | .selector_number = list.items(.selector), 173 | .blocks = list.items(.block), 174 | .specificities = unsorted_selectors_slice.items(.specificity), 175 | }); 176 | 177 | for (list.items(.selector)) |*selector_index| { 178 | selector_index.* = unsorted_selectors_slice.items(.index)[selector_index.*]; 179 | } 180 | } 181 | 182 | return stylesheet; 183 | } 184 | 185 | fn atRule( 186 | namespaces: *Namespaces, 187 | allocator: Allocator, 188 | ast: Ast, 189 | source_code: SourceCode, 190 | env: *Environment, 191 | at_rule: AtRule, 192 | at_rule_index: Ast.Index, 193 | ) !void { 194 | // TODO: There are rules involving how some at-rules must be ordered 195 | // Example 1: @namespace rules must come after @charset and @import 196 | // Example 2: @import and @namespace must come before any other non-ignored at-rules and style rules 197 | 198 | const parse = zss.values.parse; 199 | var parse_ctx: parse.Context = .init(ast, source_code); 200 | switch (at_rule) { 201 | .import => return error.UnrecognizedAtRule, 202 | .namespace => { 203 | // Spec: CSS Namespaces Level 3 Editor's Draft 204 | // Syntax: ? [ | ] 205 | // = 206 | 207 | parse_ctx.initSequence(at_rule_index.children(ast)); 208 | const prefix_or_null = parse.identifier(&parse_ctx); 209 | const namespace: Environment.NamespaceLocation = 210 | if (parse.string(&parse_ctx)) |location| 211 | .{ .string_token = location } 212 | else if (parse.url(&parse_ctx)) |url| 213 | switch (url) { 214 | .string_token => |location| .{ .string_token = location }, 215 | .url_token => |location| .{ .url_token = location }, 216 | } 217 | else 218 | return error.InvalidAtRule; 219 | if (!parse_ctx.empty()) return error.InvalidAtRule; 220 | 221 | const id = try env.addNamespaceFromToken(namespace, source_code); 222 | if (prefix_or_null) |prefix| { 223 | const index = try namespaces.indexer.addFromIdentToken(.sensitive, allocator, prefix, source_code); 224 | if (index == namespaces.ids.items.len) { 225 | try namespaces.ids.append(allocator, id); 226 | } else { 227 | // NOTE: Later @namespace rules override previous ones. 228 | namespaces.ids.items[index] = id; 229 | } 230 | } else { 231 | // NOTE: Later @namespace rules override previous ones. 232 | namespaces.default = id; 233 | } 234 | }, 235 | } 236 | } 237 | 238 | test "create a stylesheet" { 239 | const allocator = std.testing.allocator; 240 | 241 | const input = 242 | \\@charset "utf-8"; 243 | \\@import "import.css"; 244 | \\@namespace test "example.com"; 245 | \\@namespace test src("foo.bar"); 246 | \\@namespace src("xyz"); 247 | \\@namespace url(xyz); 248 | \\@namespace url("xyz"); 249 | \\test {display: block} 250 | ; 251 | const source_code = try SourceCode.init(input); 252 | 253 | var ast, const rule_list_index = blk: { 254 | var parser = zss.syntax.Parser.init(source_code, allocator); 255 | defer parser.deinit(); 256 | break :blk try parser.parseCssStylesheet(allocator); 257 | }; 258 | defer ast.deinit(allocator); 259 | 260 | var env = Environment.init(allocator, &.empty_document, .all_insensitive, .no_quirks); 261 | defer env.deinit(); 262 | 263 | var stylesheet = try create(allocator, ast, rule_list_index, source_code, &env); 264 | defer stylesheet.deinit(allocator); 265 | } 266 | -------------------------------------------------------------------------------- /source/render/QuadTree.zig: -------------------------------------------------------------------------------- 1 | const QuadTree = @This(); 2 | 3 | const zss = @import("../zss.zig"); 4 | const Unit = zss.math.Unit; 5 | const Rect = zss.math.Rect; 6 | const DrawList = @import("./DrawList.zig"); 7 | 8 | const std = @import("std"); 9 | const assert = std.debug.assert; 10 | const Allocator = std.mem.Allocator; 11 | const ArrayListUnmanaged = std.ArrayListUnmanaged; 12 | 13 | // A "patch" is a square with side length `top_level_size`. 14 | // Space is represented as an infinite grid of patches. 15 | // Each patch has a coordinate (x, y), with x increasing to the left and y increasing downwards. 16 | // Patches can be subdivided into 4 equally sized smaller squares called quadrants. 17 | // Each quadrant can also be subdivided recursively, up to a maximum of `maximum_node_depth` times. 18 | 19 | const top_level_size: Unit = 1024 * zss.math.units_per_pixel; 20 | const maximum_node_depth = 7; 21 | 22 | /// The coordinates of a patch. 23 | const PatchCoord = struct { 24 | x: i32, 25 | y: i32, 26 | }; 27 | 28 | /// Represents a 2D range of patches. 29 | /// The bottom-right-most patch coordinate is NOT included in the range. 30 | const PatchSpan = struct { 31 | top_left: PatchCoord, 32 | bottom_right: PatchCoord, 33 | 34 | fn intersects(a: PatchSpan, b: PatchSpan) bool { 35 | const left = @max(a.top_left.x, b.top_left.x); 36 | const right = @min(a.bottom_right.x, b.bottom_right.x); 37 | const top = @max(a.top_left.y, b.top_left.y); 38 | const bottom = @min(a.bottom_right.y, b.bottom_right.y); 39 | return left < right and top < bottom; 40 | } 41 | }; 42 | 43 | /// The objects that are stored in the QuadTree. 44 | pub const Object = DrawList.DrawableRef; 45 | 46 | /// An object is considered "large" if its bounding box spans more than one patch. 47 | const LargeObject = struct { 48 | object: Object, 49 | patch_span: PatchSpan, 50 | }; 51 | 52 | patch_map: std.AutoHashMapUnmanaged(PatchCoord, *Node) = .{}, 53 | large_objects: ArrayListUnmanaged(LargeObject) = .{}, 54 | 55 | /// Destroy the QuadTree. 56 | pub fn deinit(quad_tree: *QuadTree, allocator: Allocator) void { 57 | quad_tree.large_objects.deinit(allocator); 58 | var iterator = quad_tree.patch_map.iterator(); 59 | while (iterator.next()) |entry| { 60 | const node = entry.value_ptr.*; 61 | node.deinit(allocator); 62 | } 63 | quad_tree.patch_map.deinit(allocator); 64 | } 65 | 66 | /// A `Node` represents either a patch or a patch quadrant. 67 | const Node = struct { 68 | depth: u3, 69 | objects: ArrayListUnmanaged(Object) = .{}, 70 | /// children are ordered: top left, top right, bottom left, bottom right 71 | children: [4]?*Node = .{ null, null, null, null }, 72 | 73 | fn deinit(node: *Node, allocator: Allocator) void { 74 | node.objects.deinit(allocator); 75 | for (node.children) |child| { 76 | if (child) |child_node| child_node.deinit(allocator); 77 | } 78 | allocator.destroy(node); 79 | } 80 | 81 | fn insert(node: *Node, allocator: Allocator, patch_intersect: Rect, object: Object) error{OutOfMemory}!void { 82 | assert(!patch_intersect.isEmpty()); 83 | const patch_size = top_level_size >> node.depth; 84 | const quadrant_size = patch_size >> 1; 85 | if (patch_intersect.w > quadrant_size or patch_intersect.h > quadrant_size or node.depth == maximum_node_depth) { 86 | try node.objects.append(allocator, object); 87 | return; 88 | } 89 | 90 | const quadrant_rects = [4]Rect{ 91 | Rect{ .x = 0, .y = 0, .w = quadrant_size, .h = quadrant_size }, 92 | Rect{ .x = quadrant_size, .y = 0, .w = quadrant_size, .h = quadrant_size }, 93 | Rect{ .x = 0, .y = quadrant_size, .w = quadrant_size, .h = quadrant_size }, 94 | Rect{ .x = quadrant_size, .y = quadrant_size, .w = quadrant_size, .h = quadrant_size }, 95 | }; 96 | 97 | var quadrant_index: u2 = undefined; 98 | var quadrant_intersect: Rect = undefined; 99 | var num_intersects: u3 = 0; 100 | for (quadrant_rects, 0..) |rect, i| { 101 | const intersection = rect.intersect(patch_intersect); 102 | if (!intersection.isEmpty()) { 103 | num_intersects += 1; 104 | quadrant_index = @intCast(i); 105 | quadrant_intersect = intersection; 106 | } 107 | } 108 | 109 | switch (num_intersects) { 110 | 0, 5...7 => unreachable, 111 | 1 => { 112 | const child_rect = quadrant_rects[quadrant_index]; 113 | const child = node.children[quadrant_index] orelse blk: { 114 | const child = try allocator.create(Node); 115 | child.* = .{ .depth = node.depth + 1 }; 116 | node.children[quadrant_index] = child; 117 | break :blk child; 118 | }; 119 | try child.insert(allocator, .{ 120 | .x = quadrant_intersect.x - child_rect.x, 121 | .y = quadrant_intersect.y - child_rect.y, 122 | .w = quadrant_intersect.w, 123 | .h = quadrant_intersect.h, 124 | }, object); 125 | return; 126 | }, 127 | 2...4 => { 128 | try node.objects.append(allocator, object); 129 | return; 130 | }, 131 | } 132 | } 133 | 134 | fn findObjectsInRect( 135 | node: *const Node, 136 | patch_intersect: Rect, 137 | list: *ArrayListUnmanaged(Object), 138 | allocator: Allocator, 139 | ) error{OutOfMemory}!void { 140 | assert(!patch_intersect.isEmpty()); 141 | try list.appendSlice(allocator, node.objects.items); 142 | 143 | if (node.depth == 7) { 144 | return; 145 | } 146 | 147 | const patch_size = top_level_size >> node.depth; 148 | const quadrant_size = patch_size >> 1; 149 | const quadrant_rects = [4]Rect{ 150 | Rect{ .x = 0, .y = 0, .w = quadrant_size, .h = quadrant_size }, 151 | Rect{ .x = quadrant_size, .y = 0, .w = quadrant_size, .h = quadrant_size }, 152 | Rect{ .x = 0, .y = quadrant_size, .w = quadrant_size, .h = quadrant_size }, 153 | Rect{ .x = quadrant_size, .y = quadrant_size, .w = quadrant_size, .h = quadrant_size }, 154 | }; 155 | 156 | for (quadrant_rects, node.children) |rect, child_opt| { 157 | const child = child_opt orelse continue; 158 | const intersection = rect.intersect(patch_intersect); 159 | if (intersection.isEmpty()) continue; 160 | try child.findObjectsInRect(.{ 161 | .x = intersection.x - rect.x, 162 | .y = intersection.y - rect.y, 163 | .w = intersection.w, 164 | .h = intersection.h, 165 | }, list, allocator); 166 | } 167 | } 168 | 169 | fn print(node: Node, writer: anytype) @TypeOf(writer).Error!void { 170 | const indent = @as(usize, node.depth) * 4; 171 | try writer.writeByteNTimes(' ', indent); 172 | try writer.print("Depth {}\n", .{node.depth}); 173 | for (node.objects.items) |object| { 174 | try writer.writeByteNTimes(' ', indent); 175 | try writer.print("{}\n", .{object}); 176 | } 177 | for (node.children, 0..) |child, i| { 178 | if (child) |child_node| { 179 | const quadrant_string = switch (@as(u2, @intCast(i))) { 180 | 0 => "top left", 181 | 1 => "top right", 182 | 2 => "bottom left", 183 | 3 => "bottom right", 184 | }; 185 | try writer.writeByteNTimes(' ', indent); 186 | try writer.print("Quadrant {s}\n", .{quadrant_string}); 187 | try child_node.print(writer); 188 | } 189 | } 190 | } 191 | }; 192 | 193 | /// Insert an object into the QuadTree. 194 | pub fn insert(quad_tree: *QuadTree, allocator: Allocator, bounding_box: Rect, object: Object) !void { 195 | const patch_span = rectToPatchSpan(bounding_box); 196 | 197 | if (patch_span.bottom_right.x - patch_span.top_left.x > 1 or patch_span.bottom_right.y - patch_span.top_left.y > 1) { 198 | try quad_tree.large_objects.append(allocator, .{ 199 | .patch_span = patch_span, 200 | .object = object, 201 | }); 202 | } else { 203 | const patch_coord = patch_span.top_left; 204 | const node = try quad_tree.getNode(allocator, patch_coord); 205 | const patch_rect = getPatchRect(patch_coord); 206 | const patch_intersect = patch_rect.intersect(bounding_box).translate(.{ .x = -patch_rect.x, .y = -patch_rect.y }); 207 | try node.insert(allocator, patch_intersect, object); 208 | } 209 | } 210 | 211 | fn getNode(quad_tree: *QuadTree, allocator: Allocator, patch_coord: PatchCoord) !*Node { 212 | const gop_result = try quad_tree.patch_map.getOrPut(allocator, patch_coord); 213 | if (gop_result.found_existing) { 214 | return gop_result.value_ptr.*; 215 | } else { 216 | errdefer quad_tree.patch_map.removeByPtr(gop_result.key_ptr); 217 | const node = try allocator.create(Node); 218 | node.* = .{ .depth = 0 }; 219 | gop_result.value_ptr.* = node; 220 | return node; 221 | } 222 | } 223 | 224 | pub fn print(quad_tree: QuadTree, writer: anytype) !void { 225 | try writer.writeAll("Large objects:\n"); 226 | for (quad_tree.large_objects.items) |large_object| { 227 | try writer.print("\tPatch span ({}, {}) to ({}, {}), {}\n", .{ 228 | large_object.patch_span.top_left.x, 229 | large_object.patch_span.top_left.y, 230 | large_object.patch_span.bottom_right.x, 231 | large_object.patch_span.bottom_right.y, 232 | large_object.object, 233 | }); 234 | } 235 | try writer.writeAll("\n"); 236 | 237 | var iterator = quad_tree.patch_map.iterator(); 238 | while (iterator.next()) |entry| { 239 | const patch_coord = entry.key_ptr.*; 240 | const node = entry.value_ptr.*; 241 | try writer.print("Patch ({}, {})\n", .{ patch_coord.x, patch_coord.y }); 242 | try node.print(writer); 243 | } 244 | } 245 | 246 | /// Creates an unordered list of objects which may intersect with the rectangle `rect`. 247 | /// Some objects may not actually intersect the rectangle. 248 | /// The memory is owned by the caller. 249 | 250 | // TODO: Return a list of patches, instead of a list of individual objects 251 | pub fn findObjectsInRect(quad_tree: QuadTree, rect: Rect, allocator: Allocator) ![]Object { 252 | var result = ArrayListUnmanaged(Object){}; 253 | defer result.deinit(allocator); 254 | 255 | var patch_span = rectToPatchSpan(rect); 256 | 257 | for (quad_tree.large_objects.items) |large_object| { 258 | if (patch_span.intersects(large_object.patch_span)) { 259 | try result.append(allocator, large_object.object); 260 | } 261 | } 262 | 263 | while (patch_span.top_left.x < patch_span.bottom_right.x) : (patch_span.top_left.x += 1) { 264 | while (patch_span.top_left.y < patch_span.bottom_right.y) : (patch_span.top_left.y += 1) { 265 | const patch_coord = PatchCoord{ .x = patch_span.top_left.x, .y = patch_span.top_left.y }; 266 | if (quad_tree.patch_map.get(patch_coord)) |node| { 267 | const patch_rect = getPatchRect(patch_coord); 268 | const patch_intersect = patch_rect.intersect(rect).translate(.{ .x = -patch_rect.x, .y = -patch_rect.y }); 269 | try node.findObjectsInRect(patch_intersect, &result, allocator); 270 | } 271 | } 272 | } 273 | 274 | return result.toOwnedSlice(allocator); 275 | } 276 | 277 | /// Given a rectangle, returns a span of all patches that the rectangle intersects. 278 | fn rectToPatchSpan(rect: Rect) PatchSpan { 279 | assert(rect.w >= 0); 280 | assert(rect.h >= 0); 281 | const roundUp = zss.math.roundUp; 282 | return PatchSpan{ 283 | .top_left = .{ 284 | .x = @divFloor(rect.x, top_level_size), 285 | .y = @divFloor(rect.y, top_level_size), 286 | }, 287 | .bottom_right = .{ 288 | .x = @divFloor(roundUp(rect.x + rect.w, top_level_size), top_level_size), 289 | .y = @divFloor(roundUp(rect.y + rect.h, top_level_size), top_level_size), 290 | }, 291 | }; 292 | } 293 | 294 | /// Returns the region of space associated with a patch. 295 | fn getPatchRect(patch_coord: PatchCoord) Rect { 296 | return .{ 297 | .x = patch_coord.x * top_level_size, 298 | .y = patch_coord.y * top_level_size, 299 | .w = top_level_size, 300 | .h = top_level_size, 301 | }; 302 | } 303 | -------------------------------------------------------------------------------- /source/cascade.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const assert = std.debug.assert; 3 | const Allocator = std.mem.Allocator; 4 | 5 | const zss = @import("zss.zig"); 6 | const selectors = zss.selectors; 7 | const Block = zss.Declarations.Block; 8 | const Environment = zss.Environment; 9 | const Importance = zss.Declarations.Importance; 10 | 11 | /// The list of all cascade sources, grouped by their cascade origin. 12 | /// 13 | /// You can affect the CSS cascade by inserting nodes into/removing nodes from the `user`, `author`, or `user_agent` lists. 14 | /// Each list is independent of each other. 15 | /// Nodes earlier in each list are considered to have a higher cascade order than later nodes in the same list. 16 | /// 17 | /// During the cascade, each node is visited in the following way: 18 | /// - If the node is a leaf node, its cascade source is applied. 19 | /// - If the node is an inner node, each of its child nodes are visited in order, recursively. 20 | pub const List = struct { 21 | user: []const *const Node = &.{}, 22 | author: []const *const Node = &.{}, 23 | user_agent: []const *const Node = &.{}, 24 | }; 25 | 26 | pub const Origin = enum { user, author, user_agent }; 27 | 28 | pub const Node = union(enum) { 29 | leaf: *const Source, 30 | inner: []const *const Node, 31 | }; 32 | 33 | /// Contains the data necessary for a document to participate in the CSS cascade. 34 | /// Every document that contains CSS style information should produce one of these. 35 | /// 36 | /// During the cascade (if this cascade source participates in it), this cascade source will get applied. 37 | /// Applying a cascade source means to assign all of its style information to the appropriate elements in the document tree. 38 | pub const Source = struct { 39 | /// Pairs of elements and important declaration blocks. 40 | /// These declaration blocks must be the results of parsing [style attributes](https://www.w3.org/TR/css-style-attr/), 41 | /// or some equivalent mechanism by which the document applies style information directly to a specific element. 42 | style_attrs_important: std.AutoHashMapUnmanaged(zss.Environment.NodeId, Block) = .empty, 43 | /// Pairs of elements and normal declaration blocks. 44 | /// These declaration blocks must be the results of parsing [style attributes](https://www.w3.org/TR/css-style-attr/), 45 | /// or some equivalent mechanism by which the document applies style information directly to a specific element. 46 | style_attrs_normal: std.AutoHashMapUnmanaged(zss.Environment.NodeId, Block) = .empty, 47 | /// Pairs of complex selectors and important declaration blocks. 48 | /// This list must be sorted such that selectors with higher cascade order appear earlier. 49 | selectors_important: std.MultiArrayList(SelectorBlock) = .empty, 50 | /// Pairs of complex selectors and normal declaration blocks. 51 | /// This list must be sorted such that selectors with higher cascade order appear earlier. 52 | selectors_normal: std.MultiArrayList(SelectorBlock) = .empty, 53 | selector_data: std.ArrayList(selectors.Data) = .empty, 54 | 55 | pub const SelectorBlock = struct { 56 | selector: selectors.Data.ListIndex, 57 | block: Block, 58 | }; 59 | 60 | pub fn deinit(source: *Source, allocator: Allocator) void { 61 | source.style_attrs_important.deinit(allocator); 62 | source.style_attrs_normal.deinit(allocator); 63 | source.selectors_important.deinit(allocator); 64 | source.selectors_normal.deinit(allocator); 65 | source.selector_data.deinit(allocator); 66 | } 67 | }; 68 | 69 | /// A structure capable of storing the cascaded values of all CSS properties for every document node. 70 | pub const Database = struct { 71 | node_map: std.AutoHashMapUnmanaged(Environment.NodeId, Storage) = .empty, 72 | arena: std.heap.ArenaAllocator.State = .{}, 73 | 74 | pub fn deinit(db: *Database, allocator: Allocator) void { 75 | db.node_map.deinit(allocator); 76 | 77 | var arena = db.arena.promote(allocator); 78 | defer db.arena = arena.state; 79 | arena.deinit(); 80 | } 81 | 82 | pub fn addStorage(db: *Database, allocator: Allocator, node: Environment.NodeId) !*Storage { 83 | const gop = try db.node_map.getOrPut(allocator, node); 84 | if (!gop.found_existing) gop.value_ptr.* = .{}; 85 | return gop.value_ptr; 86 | } 87 | 88 | pub fn getStorage(db: *const Database, node: Environment.NodeId) ?*Storage { 89 | return db.node_map.getPtr(node); 90 | } 91 | 92 | /// Stores the cascaded values of all CSS properties for a single document node. 93 | /// Pointers to cascaded values are not stable. 94 | pub const Storage = struct { 95 | /// Maps each value group to its cascaded values. 96 | group_map: Map = .{}, 97 | /// The cascaded value for the 'all' CSS property. 98 | all: ?CssWideKeyword = null, 99 | 100 | pub const Map = std.EnumMap(groups.Tag, usize); 101 | 102 | const CssWideKeyword = zss.values.types.CssWideKeyword; 103 | const groups = zss.values.groups; 104 | 105 | /// The main operation performed during the CSS cascade is "applying a declaration". 106 | /// 107 | /// To apply a declaration "decl" to a destination value "destValue" means the following: 108 | /// 1. If "destValue" is NOT equal to `.undeclared`, do nothing and return. 109 | /// 2. If "decl" is affected by the CSS 'all' property, then copy the value of the 'all' property into "destValue" and return. 110 | /// 3. Copy "decl" into "destValue". 111 | /// 112 | /// You can also apply an entire declaration block to a destination storage 113 | /// (where "destination storage" is some arbitrary data structure than can hold cascaded values). 114 | /// 115 | /// To apply a declaration block "block" to a destination storage "destStorage" means the following: 116 | /// 1. For each declaration "decl" within "block", apply "decl" to the corresponding value within "destStorage". 117 | /// 118 | /// Declaration blocks must be passed to this function in cascade order. 119 | pub fn applyDeclBlock( 120 | storage: *Storage, 121 | /// The `Database` that `storage` belongs to. 122 | db: *Database, 123 | /// The database's allocator. 124 | allocator: Allocator, 125 | decls: *const zss.Declarations, 126 | block: zss.Declarations.Block, 127 | importance: Importance, 128 | ) !void { 129 | // TODO: The 'all' property does not affect some properties 130 | if (storage.all != null) return; 131 | 132 | if (decls.getAll(block, importance)) |all| storage.all = all; 133 | 134 | var iterator = decls.groupIterator(block, importance); 135 | while (iterator.next()) |group| { 136 | const needs_init = !storage.group_map.contains(group); 137 | const map_value = if (needs_init) storage.group_map.putUninitialized(group) else storage.group_map.getPtrAssertContains(group); 138 | 139 | switch (group) { 140 | inline else => |comptime_group| { 141 | const CascadedValues = comptime_group.CascadedValues(); 142 | const cascaded_values: *CascadedValues = switch (comptime canFitWithinUsize(CascadedValues)) { 143 | true => blk: { 144 | const values: *CascadedValues = @ptrCast(map_value); 145 | if (needs_init) values.* = .{}; 146 | break :blk values; 147 | }, 148 | false => blk: { 149 | if (needs_init) { 150 | var arena = db.arena.promote(allocator); 151 | defer db.arena = arena.state; 152 | 153 | const values = try arena.allocator().create(CascadedValues); 154 | values.* = .{}; 155 | map_value.* = @intFromPtr(values); 156 | } 157 | break :blk @ptrFromInt(map_value.*); 158 | }, 159 | }; 160 | decls.apply(comptime_group, block, importance, cascaded_values); 161 | }, 162 | } 163 | } 164 | } 165 | 166 | /// If there is a cascaded value for the value group `group`, returns a pointer to it. Otherwise returns `null`. 167 | pub fn getPtr(storage: *const Storage, comptime group: groups.Tag) ?*const group.CascadedValues() { 168 | const map_value_ptr = storage.group_map.getPtrConst(group) orelse return null; 169 | const CascadedValues = group.CascadedValues(); 170 | return switch (comptime canFitWithinUsize(CascadedValues)) { 171 | true => @ptrCast(map_value_ptr), 172 | false => @ptrFromInt(map_value_ptr.*), 173 | }; 174 | } 175 | 176 | fn canFitWithinUsize(comptime T: type) bool { 177 | return (@alignOf(T) <= @alignOf(usize) and @sizeOf(T) <= @sizeOf(usize)); 178 | } 179 | 180 | pub fn reset(storage: *Storage) void { 181 | storage.group_map = .{}; // TODO: Leaks memory (but okay, because of arena allocation) 182 | storage.all = null; 183 | } 184 | }; 185 | }; 186 | 187 | const RunContext = struct { 188 | arena: std.heap.ArenaAllocator, 189 | element_to_decl_block_list: std.AutoArrayHashMapUnmanaged(Environment.NodeId, std.ArrayListUnmanaged(BlockImportance)) = .empty, 190 | cascade_node_stack: zss.Stack([]const *const Node) = .{}, 191 | document_node_stack: zss.Stack(?Environment.NodeId) = .{}, 192 | 193 | const BlockImportance = struct { 194 | block: Block, 195 | importance: Importance, 196 | }; 197 | 198 | fn appendDeclBlock(ctx: *RunContext, node: zss.Environment.NodeId, block: Block, importance: Importance) !void { 199 | const allocator = ctx.arena.allocator(); 200 | const gop = try ctx.element_to_decl_block_list.getOrPut(allocator, node); 201 | if (!gop.found_existing) { 202 | gop.value_ptr.* = .{}; 203 | } 204 | try gop.value_ptr.append(allocator, .{ .block = block, .importance = importance }); 205 | } 206 | }; 207 | 208 | /// Runs the CSS cascade. 209 | pub fn run(list: *const List, env: *Environment, temp_allocator: Allocator) !void { 210 | var ctx = RunContext{ .arena = .init(temp_allocator) }; 211 | defer ctx.arena.deinit(); 212 | 213 | const order: [6]struct { Origin, Importance } = .{ 214 | .{ .user_agent, .important }, 215 | .{ .user, .important }, 216 | .{ .author, .important }, 217 | .{ .author, .normal }, 218 | .{ .user, .normal }, 219 | .{ .user_agent, .normal }, 220 | }; 221 | for (order) |item| { 222 | const origin, const importance = item; 223 | try traverseList(&ctx, list, env, origin, importance); 224 | } 225 | 226 | var element_iterator = ctx.element_to_decl_block_list.iterator(); 227 | while (element_iterator.next()) |entry| { 228 | const node = entry.key_ptr.*; 229 | const cascaded_values = try env.cascade_db.addStorage(env.allocator, node); 230 | cascaded_values.reset(); 231 | for (entry.value_ptr.*.items) |item| { 232 | try cascaded_values.applyDeclBlock(&env.cascade_db, env.allocator, &env.decls, item.block, item.importance); 233 | } 234 | } 235 | } 236 | 237 | fn traverseList(ctx: *RunContext, list: *const List, env: *const Environment, origin: Origin, importance: Importance) !void { 238 | const node_list = switch (origin) { 239 | .user => list.user, 240 | .author => list.author, 241 | .user_agent => list.user_agent, 242 | }; 243 | const allocator = ctx.arena.allocator(); 244 | 245 | assert(ctx.cascade_node_stack.top == null); 246 | ctx.cascade_node_stack.top = node_list; 247 | while (ctx.cascade_node_stack.top) |*top| { 248 | if (top.*.len == 0) { 249 | _ = ctx.cascade_node_stack.pop(); 250 | continue; 251 | } 252 | const node: *const Node = top.*[0]; 253 | top.* = top.*[1..]; 254 | 255 | switch (node.*) { 256 | .inner => |inner| try ctx.cascade_node_stack.push(allocator, inner), 257 | .leaf => |source| try applySource(ctx, source, env, importance), 258 | } 259 | } 260 | } 261 | 262 | fn applySource(ctx: *RunContext, source: *const Source, env: *const Environment, importance: Importance) !void { 263 | { 264 | // TODO: Style attrs can only appear in sources with author origin 265 | const style_attrs = switch (importance) { 266 | .important => source.style_attrs_important, 267 | .normal => source.style_attrs_normal, 268 | }; 269 | var it = style_attrs.iterator(); 270 | while (it.next()) |entry| { 271 | const node = entry.key_ptr.*; 272 | switch (env.getNodeProperty(.category, node)) { 273 | .text => unreachable, 274 | .element => {}, 275 | } 276 | const block = entry.value_ptr.*; 277 | try ctx.appendDeclBlock(node, block, importance); 278 | } 279 | } 280 | 281 | const selector_list = switch (importance) { 282 | .important => source.selectors_important, 283 | .normal => source.selectors_normal, 284 | }; 285 | const allocator = ctx.arena.allocator(); 286 | 287 | for (selector_list.items(.selector), selector_list.items(.block)) |selector, block| { 288 | assert(ctx.document_node_stack.top == null); 289 | ctx.document_node_stack.top = env.root_node; 290 | while (ctx.document_node_stack.top) |*top| { 291 | const node = top.* orelse { 292 | _ = ctx.document_node_stack.pop(); 293 | continue; 294 | }; 295 | top.* = node.nextSibling(env); 296 | switch (env.getNodeProperty(.category, node)) { 297 | .text => continue, 298 | .element => {}, 299 | } 300 | if (node.firstChild(env)) |first_child| try ctx.document_node_stack.push(allocator, first_child); 301 | 302 | if (zss.selectors.matchElement(source.selector_data.items, selector, env, node)) { 303 | try ctx.appendDeclBlock(node, block, importance); 304 | } 305 | } 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /source/Utf8StringInterner.zig: -------------------------------------------------------------------------------- 1 | //! Assigns index numbers to UTF-8 strings. 2 | //! Indeces start from 0 and increase by 1 for every unique string. 3 | //! Equality is determined by codepoints alone. Unicode normalization forms don't apply. 4 | 5 | const Utf8StringInterner = @This(); 6 | 7 | const zss = @import("zss.zig"); 8 | const Location = SourceCode.Location; 9 | const SourceCode = zss.syntax.SourceCode; 10 | 11 | const std = @import("std"); 12 | const assert = std.debug.assert; 13 | const Allocator = std.mem.Allocator; 14 | 15 | indexer: std.AutoArrayHashMapUnmanaged(void, Range), 16 | string: zss.SegmentedUtf8String, 17 | max_size: usize, 18 | debug: Debug, 19 | 20 | const Range = struct { 21 | position: zss.SegmentedUtf8String.Size, 22 | len: zss.SegmentedUtf8String.Size, 23 | }; 24 | 25 | const Debug = switch (zss.debug.runtime_safety) { 26 | true => struct { 27 | case: Case, 28 | 29 | fn init(case: Case) Debug { 30 | return .{ .case = case }; 31 | } 32 | 33 | fn assertCase(debug: *const Debug, case: Case) void { 34 | assert(debug.case == case); 35 | } 36 | }, 37 | false => struct { 38 | fn init(_: Case) Debug { 39 | return .{}; 40 | } 41 | 42 | fn assertCase(_: *const Debug, _: Case) void {} 43 | }, 44 | }; 45 | 46 | pub const Case = enum { 47 | sensitive, 48 | insensitive, 49 | }; 50 | 51 | pub const Options = struct { 52 | /// The maximum amount of unique strings that can be held. 53 | max_size: usize, 54 | /// Whether strings are compared case-sensitively or not. 55 | /// You must always pass the same value that you passed to `init` to all functions that accept a case as a parameter. 56 | case: Case, 57 | }; 58 | 59 | pub fn init(options: Options) Utf8StringInterner { 60 | return .{ 61 | .indexer = .empty, 62 | .string = .init(1 << 10, 1 << 31), 63 | .max_size = options.max_size - @intFromBool(options.max_size +% 1 == 0), 64 | .debug = .init(options.case), 65 | }; 66 | } 67 | 68 | pub fn deinit(interner: *Utf8StringInterner, allocator: Allocator) void { 69 | interner.indexer.deinit(allocator); 70 | interner.string.deinit(allocator); 71 | } 72 | 73 | const Hasher = struct { 74 | buffer: [32]u8 = undefined, 75 | len: u8 = 0, 76 | 77 | fn full(hasher: Hasher) bool { 78 | return hasher.len == hasher.buffer.len; 79 | } 80 | 81 | fn end(hasher: *Hasher, comptime case: Utf8StringInterner.Case) u32 { 82 | const slice = hasher.buffer[0..hasher.len]; 83 | switch (case) { 84 | .sensitive => {}, 85 | .insensitive => { 86 | for (slice) |*c| { 87 | c.* = std.ascii.toLower(c.*); 88 | } 89 | }, 90 | } 91 | 92 | var wyhash = std.hash.Wyhash.init(0); 93 | wyhash.update(slice); 94 | return @truncate(wyhash.final()); 95 | } 96 | 97 | fn addCodepoint(hasher: *Hasher, codepoint: u21) void { 98 | var codepoint_buffer: [4]u8 = undefined; 99 | const codepoint_len = std.unicode.utf8Encode(codepoint, &codepoint_buffer) catch unreachable; 100 | const hashed_len = @min(hasher.buffer.len - hasher.len, codepoint_len); 101 | @memcpy(hasher.buffer[hasher.len..][0..hashed_len], codepoint_buffer[0..hashed_len]); 102 | hasher.len += @intCast(hashed_len); 103 | } 104 | 105 | fn addString(hasher: *Hasher, string: []const u8) void { 106 | const hashed_len = @min(hasher.buffer.len - hasher.len, string.len); 107 | @memcpy(hasher.buffer[hasher.len..][0..hashed_len], string[0..hashed_len]); 108 | hasher.len += @intCast(hashed_len); 109 | } 110 | }; 111 | 112 | fn adjustCase(interner: *const Utf8StringInterner, comptime case: Case, range: Range) void { 113 | switch (case) { 114 | .sensitive => {}, 115 | .insensitive => { 116 | var segment_iterator = interner.string.iterator(range.position, range.len); 117 | while (segment_iterator.next()) |segment| { 118 | for (segment) |*c| c.* = std.ascii.toLower(c.*); 119 | } 120 | }, 121 | } 122 | } 123 | 124 | /// Returns an iterator for the string represented by `index`. 125 | pub fn iterator(interner: *const Utf8StringInterner, index: usize) zss.SegmentedUtf8String.Iterator { 126 | const range = interner.indexer.values()[index]; 127 | return interner.string.iterator(range.position, range.len); 128 | } 129 | 130 | pub fn addFromIdentToken( 131 | interner: *Utf8StringInterner, 132 | comptime case: Case, 133 | allocator: Allocator, 134 | /// Must be the location of an . 135 | location: Location, 136 | source_code: SourceCode, 137 | ) !usize { 138 | return addFromTokenIterator(interner, case, allocator, source_code.identTokenIterator(location)); 139 | } 140 | 141 | pub fn addFromStringToken( 142 | interner: *Utf8StringInterner, 143 | comptime case: Case, 144 | allocator: Allocator, 145 | /// Must be the location of a . 146 | location: Location, 147 | source_code: SourceCode, 148 | ) !usize { 149 | return addFromTokenIterator(interner, case, allocator, source_code.stringTokenIterator(location)); 150 | } 151 | 152 | pub fn addFromHashIdToken( 153 | interner: *Utf8StringInterner, 154 | comptime case: Case, 155 | allocator: Allocator, 156 | /// Must be the location of an ID . 157 | location: Location, 158 | source_code: SourceCode, 159 | ) !usize { 160 | return addFromTokenIterator(interner, case, allocator, source_code.hashIdTokenIterator(location)); 161 | } 162 | 163 | pub fn getFromIdentToken( 164 | interner: *const Utf8StringInterner, 165 | comptime case: Case, 166 | /// Must be the location of an . 167 | location: Location, 168 | source_code: SourceCode, 169 | ) ?usize { 170 | return getFromTokenIterator(interner, case, source_code.identTokenIterator(location)); 171 | } 172 | 173 | fn TokenIteratorAdapter(comptime TokenIterator: type, comptime case: Case) type { 174 | return struct { 175 | interner: *const Utf8StringInterner, 176 | 177 | pub fn hash(_: @This(), key: TokenIterator) u32 { 178 | var hasher = Hasher{}; 179 | var it = key; 180 | while (it.next()) |codepoint| { 181 | if (hasher.full()) break; 182 | hasher.addCodepoint(codepoint); 183 | } 184 | return hasher.end(case); 185 | } 186 | 187 | pub fn eql(adapter: @This(), key: TokenIterator, _: void, index: usize) bool { 188 | var key_it = key; 189 | const range = adapter.interner.indexer.values()[index]; 190 | var string_it = adapter.interner.string.iterator(range.position, range.len); 191 | while (string_it.next()) |segment| { 192 | var string_index: usize = 0; 193 | while (string_index < segment.len) { 194 | const key_codepoint = key_it.next() orelse return false; 195 | const key_codepoint_adjusted = switch (case) { 196 | .sensitive => key_codepoint, 197 | .insensitive => zss.unicode.latin1ToLowercase(key_codepoint), 198 | }; 199 | const string_codepoint_len = std.unicode.utf8ByteSequenceLength(segment[string_index]) catch unreachable; 200 | const string_codepoint = std.unicode.utf8Decode(segment[string_index..][0..string_codepoint_len]) catch unreachable; 201 | if (key_codepoint_adjusted != string_codepoint) return false; 202 | string_index += string_codepoint_len; 203 | } 204 | } 205 | return key_it.next() == null; 206 | } 207 | }; 208 | } 209 | 210 | fn getFromTokenIterator( 211 | interner: *const Utf8StringInterner, 212 | comptime case: Case, 213 | token_iterator: anytype, 214 | ) ?usize { 215 | const Adapter = TokenIteratorAdapter(@TypeOf(token_iterator), case); 216 | interner.debug.assertCase(case); 217 | return interner.indexer.getIndexAdapted( 218 | token_iterator, 219 | Adapter{ .interner = interner }, 220 | ); 221 | } 222 | 223 | fn addFromTokenIterator( 224 | interner: *Utf8StringInterner, 225 | comptime case: Case, 226 | allocator: Allocator, 227 | token_iterator: anytype, 228 | ) !usize { 229 | const Adapter = TokenIteratorAdapter(@TypeOf(token_iterator), case); 230 | 231 | interner.debug.assertCase(case); 232 | const gop = try interner.indexer.getOrPutAdapted( 233 | allocator, 234 | token_iterator, 235 | Adapter{ .interner = interner }, 236 | ); 237 | if (gop.found_existing) return gop.index; 238 | if (gop.index == interner.max_size) { 239 | interner.indexer.swapRemoveAt(gop.index); 240 | return error.MaxSizeExceeded; 241 | } 242 | 243 | // TODO: Find a way to reserve space upfront 244 | var range = Range{ .position = interner.string.position, .len = 0 }; 245 | var it = token_iterator; 246 | var buffer: [4]u8 = undefined; 247 | while (it.next()) |codepoint| { 248 | const len = std.unicode.utf8Encode(codepoint, &buffer) catch unreachable; 249 | try interner.string.append(allocator, buffer[0..len]); 250 | range.len += len; 251 | } 252 | adjustCase(interner, case, range); 253 | 254 | gop.value_ptr.* = range; 255 | return gop.index; 256 | } 257 | 258 | /// `string` must be UTF-8 encoded. 259 | pub fn addFromString(interner: *Utf8StringInterner, comptime case: Case, allocator: Allocator, string: []const u8) !usize { 260 | switch (case) { 261 | .sensitive => @compileError("addFromString not implemented for case sensitive strings"), 262 | .insensitive => {}, 263 | } 264 | 265 | const Adapter = struct { 266 | interner: *const Utf8StringInterner, 267 | 268 | pub fn hash(_: @This(), key: []const u8) u32 { 269 | var hasher = Hasher{}; 270 | hasher.addString(key); 271 | return hasher.end(.insensitive); 272 | } 273 | 274 | pub fn eql(adapter: @This(), key: []const u8, _: void, index: usize) bool { 275 | const range = adapter.interner.indexer.values()[index]; 276 | if (key.len != range.len) return false; 277 | 278 | var key_index: usize = 0; 279 | var segment_iterator = adapter.interner.string.iterator(range.position, range.len); 280 | while (segment_iterator.next()) |segment| { 281 | const key_slice = key[key_index..][0..segment.len]; 282 | for (key_slice, segment) |a, b| { 283 | if (std.ascii.toLower(a) != b) return false; 284 | } 285 | key_index += segment.len; 286 | } 287 | return true; 288 | } 289 | }; 290 | 291 | interner.debug.assertCase(.insensitive); 292 | const gop = try interner.indexer.getOrPutAdapted(allocator, string, Adapter{ .interner = interner }); 293 | if (gop.found_existing) return gop.index; 294 | if (gop.index == interner.max_size) { 295 | interner.indexer.swapRemoveAt(gop.index); 296 | return error.MaxSizeExceeded; 297 | } 298 | 299 | const range = Range{ .position = interner.string.position, .len = @intCast(string.len) }; 300 | try interner.string.append(allocator, string); 301 | adjustCase(interner, .insensitive, range); 302 | 303 | gop.value_ptr.* = range; 304 | return gop.index; 305 | } 306 | 307 | test "Utf8StringInterner" { 308 | const allocator = std.testing.allocator; 309 | const source_code = try SourceCode.init("apple banana cucumber durian \"apple\" CUCUMBER"); 310 | var ast, const component_list_index = ast: { 311 | var parser = zss.syntax.Parser.init(source_code, allocator); 312 | defer parser.deinit(); 313 | break :ast try parser.parseListOfComponentValues(allocator); 314 | }; 315 | defer ast.deinit(allocator); 316 | var children = component_list_index.children(ast); 317 | const ast_nodes = .{ 318 | .apple_ident = children.nextSkipSpaces(ast).?, 319 | .banana = children.nextSkipSpaces(ast).?, 320 | .cucumber = children.nextSkipSpaces(ast).?, 321 | .durian = children.nextSkipSpaces(ast).?, 322 | .apple_string = children.nextSkipSpaces(ast).?, 323 | .cucumber_uppercase = children.nextSkipSpaces(ast).?, 324 | }; 325 | 326 | { 327 | var interner = init(.{ .max_size = 3, .case = .insensitive }); 328 | defer interner.deinit(allocator); 329 | const indeces = .{ 330 | .apple_ident = try interner.addFromIdentToken(.insensitive, allocator, ast_nodes.apple_ident.location(ast), source_code), 331 | .banana = try interner.addFromIdentToken(.insensitive, allocator, ast_nodes.banana.location(ast), source_code), 332 | .cucumber = try interner.addFromIdentToken(.insensitive, allocator, ast_nodes.cucumber.location(ast), source_code), 333 | .durian = durian: { 334 | try std.testing.expectError(error.MaxSizeExceeded, interner.addFromIdentToken(.insensitive, allocator, ast_nodes.durian.location(ast), source_code)); 335 | break :durian undefined; 336 | }, 337 | .apple_string = try interner.addFromStringToken(.insensitive, allocator, ast_nodes.apple_string.location(ast), source_code), 338 | .banana_string = try interner.addFromString(.insensitive, allocator, "banana"), 339 | .cucumber_uppercase = try interner.addFromIdentToken(.insensitive, allocator, ast_nodes.cucumber_uppercase.location(ast), source_code), 340 | }; 341 | try std.testing.expectEqual(@as(usize, 0), indeces.apple_ident); 342 | try std.testing.expectEqual(@as(usize, 1), indeces.banana); 343 | try std.testing.expectEqual(@as(usize, 2), indeces.cucumber); 344 | try std.testing.expectEqual(@as(usize, 0), indeces.apple_string); 345 | try std.testing.expectEqual(@as(usize, 1), indeces.banana_string); 346 | try std.testing.expectEqual(@as(usize, 2), indeces.cucumber_uppercase); 347 | } 348 | 349 | { 350 | var interner = init(.{ .max_size = 2, .case = .sensitive }); 351 | defer interner.deinit(allocator); 352 | const indeces = .{ 353 | .cucumber_lowercase = try interner.addFromIdentToken(.sensitive, allocator, ast_nodes.cucumber.location(ast), source_code), 354 | .cucumber_uppercase = try interner.addFromIdentToken(.sensitive, allocator, ast_nodes.cucumber_uppercase.location(ast), source_code), 355 | }; 356 | try std.testing.expectEqual(@as(usize, 0), indeces.cucumber_lowercase); 357 | try std.testing.expectEqual(@as(usize, 1), indeces.cucumber_uppercase); 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /source/values/groups.zig: -------------------------------------------------------------------------------- 1 | //! In zss, CSS properties are organized into groups. 2 | //! Each group contains properties that are likely to be used together. 3 | //! Within a group, each property *must* have the same inheritance type 4 | //! (i.e. they must be all inherited properties or all non-inherited properties. 5 | //! See https://www.w3.org/TR/css-cascade/#inherited-property). 6 | //! 7 | //! Each group has comments which map each field to a CSS property, in this format: 8 | //! /// -> 9 | 10 | const zss = @import("../zss.zig"); 11 | const types = zss.values.types; 12 | 13 | const std = @import("std"); 14 | 15 | pub const Tag = enum { 16 | box_style, 17 | content_width, 18 | horizontal_edges, 19 | content_height, 20 | vertical_edges, 21 | z_index, 22 | insets, 23 | border_colors, 24 | border_styles, 25 | background_color, 26 | background_clip, 27 | background, 28 | 29 | color, 30 | font, 31 | 32 | fn Value(comptime tag: Tag) type { 33 | return switch (tag) { 34 | .box_style => BoxStyle, 35 | .content_width => ContentWidth, 36 | .horizontal_edges => HorizontalEdges, 37 | .content_height => ContentHeight, 38 | .vertical_edges => VerticalEdges, 39 | .z_index => ZIndex, 40 | .insets => Insets, 41 | .border_colors => BorderColors, 42 | .border_styles => BorderStyles, 43 | .background_color => BackgroundColor, 44 | .background_clip => BackgroundClip, 45 | .background => Background, 46 | .color => Color, 47 | .font => Font, 48 | }; 49 | } 50 | 51 | pub const InheritanceType = enum { inherited, not_inherited }; 52 | 53 | pub fn inheritanceType(comptime tag: Tag) InheritanceType { 54 | return switch (tag) { 55 | .box_style, 56 | .content_width, 57 | .horizontal_edges, 58 | .content_height, 59 | .vertical_edges, 60 | .z_index, 61 | .insets, 62 | .border_colors, 63 | .border_styles, 64 | .background_color, 65 | .background_clip, 66 | .background, 67 | => .not_inherited, 68 | 69 | .color, 70 | .font, 71 | => .inherited, 72 | }; 73 | } 74 | 75 | pub const Size = enum { single, multi }; 76 | 77 | pub fn size(comptime tag: Tag) Size { 78 | return switch (tag) { 79 | .box_style, 80 | .content_width, 81 | .horizontal_edges, 82 | .content_height, 83 | .vertical_edges, 84 | .z_index, 85 | .insets, 86 | .border_colors, 87 | .border_styles, 88 | .background_color, 89 | .color, 90 | .font, // TODO: should probably be multi 91 | => .single, 92 | 93 | .background_clip, 94 | .background, 95 | => .multi, 96 | }; 97 | } 98 | 99 | /// A struct that can represent the declared values of all fields within 100 | /// the group represented by `tag`. 101 | pub fn DeclaredValues(comptime tag: Tag) type { 102 | const ns = struct { 103 | fn fieldMap(comptime field: tag.FieldEnum()) struct { type, ?*const anyopaque } { 104 | const Field = tag.FieldType(field); 105 | const Type = switch (tag.size()) { 106 | .single => SingleValue(Field), 107 | .multi => MultiValue(Field), 108 | }; 109 | const default: *const Type = &.undeclared; 110 | return .{ Type, default }; 111 | } 112 | }; 113 | return zss.meta.EnumFieldMapStruct(tag.FieldEnum(), ns.fieldMap); 114 | } 115 | 116 | /// A struct that can represent the cascaded values of all fields within 117 | /// the group represented by `tag`. 118 | pub const CascadedValues = DeclaredValues; 119 | 120 | /// A struct that can represent the specified values of all fields within 121 | /// the group represented by `tag`. 122 | pub fn SpecifiedValues(comptime tag: Tag) type { 123 | const ns = struct { 124 | fn fieldMap(comptime field: tag.FieldEnum()) struct { type, ?*const anyopaque } { 125 | const Field = tag.FieldType(field); 126 | const Type = switch (tag.size()) { 127 | .single => Field, 128 | .multi => []const Field, 129 | }; 130 | return .{ Type, null }; 131 | } 132 | }; 133 | return zss.meta.EnumFieldMapStruct(tag.FieldEnum(), ns.fieldMap); 134 | } 135 | 136 | /// A struct that can represent the computed values of all fields within 137 | /// the group represented by `tag`. 138 | pub const ComputedValues = SpecifiedValues; 139 | 140 | pub fn initialValues(comptime tag: Tag) tag.SpecifiedValues() { 141 | return comptime blk: { 142 | const Group = tag.Value(); 143 | var result: tag.SpecifiedValues() = undefined; 144 | switch (tag.size()) { 145 | .single => { 146 | for (std.meta.fields(Group)) |field| { 147 | @field(result, field.name) = @field(Group.initial_values, field.name); 148 | } 149 | }, 150 | .multi => { 151 | for (std.meta.fields(Group)) |field| { 152 | @field(result, field.name) = &.{@field(Group.initial_values, field.name)}; 153 | } 154 | }, 155 | } 156 | break :blk result; 157 | }; 158 | } 159 | 160 | const GroupField = struct { 161 | name: []const u8, 162 | type: type, 163 | }; 164 | 165 | pub fn fields(comptime tag: Tag) []const GroupField { 166 | return comptime blk: { 167 | const Group = tag.Value(); 168 | const group_fields = std.meta.fields(Group); 169 | var result: [group_fields.len]GroupField = undefined; 170 | for (group_fields, &result) |src, *dest| { 171 | dest.* = .{ .name = src.name, .type = src.type }; 172 | } 173 | break :blk &result; 174 | }; 175 | } 176 | 177 | pub fn FieldEnum(comptime tag: Tag) type { 178 | const Group = tag.Value(); 179 | @setEvalBranchQuota(@typeInfo(Group).@"struct".fields.len * 800); 180 | return std.meta.FieldEnum(Group); 181 | } 182 | 183 | pub fn FieldType(comptime tag: Tag, comptime field: tag.FieldEnum()) type { 184 | return @FieldType(tag.Value(), @tagName(field)); 185 | } 186 | }; 187 | 188 | /// Represents either a CSS value, or a CSS-wide keyword, or `undeclared` (the absence of a declared value) 189 | pub const DeclaredValueTag = enum { 190 | undeclared, 191 | initial, 192 | inherit, 193 | unset, 194 | declared, 195 | }; 196 | 197 | /// Represents either a CSS value, or a CSS-wide keyword, or `undeclared` (the absence of a declared value) 198 | pub fn SingleValue(comptime T: type) type { 199 | return union(DeclaredValueTag) { 200 | undeclared, 201 | initial, 202 | inherit, 203 | unset, 204 | declared: T, 205 | 206 | pub fn expectEqual(actual: @This(), expected: @This()) !void { 207 | try std.testing.expectEqual(@as(DeclaredValueTag, expected), @as(DeclaredValueTag, actual)); 208 | switch (expected) { 209 | .declared => |expected_value| try std.testing.expectEqual(expected_value, actual.declared), 210 | else => {}, 211 | } 212 | } 213 | }; 214 | } 215 | 216 | /// Represents either a CSS value, or a CSS-wide keyword, or `undeclared` (the absence of a declared value) 217 | pub fn MultiValue(comptime T: type) type { 218 | return union(DeclaredValueTag) { 219 | undeclared, 220 | initial, 221 | inherit, 222 | unset, 223 | declared: []const T, 224 | 225 | pub fn expectEqual(actual: @This(), expected: @This()) !void { 226 | try std.testing.expectEqual(@as(DeclaredValueTag, expected), @as(DeclaredValueTag, actual)); 227 | switch (expected) { 228 | .declared => |expected_value| try std.testing.expectEqualSlices(T, expected_value, actual.declared), 229 | else => {}, 230 | } 231 | } 232 | }; 233 | } 234 | 235 | // TODO: font does not correspond to any CSS property 236 | pub const Font = struct { 237 | font: types.Font, 238 | 239 | pub const initial_values = Font{ 240 | .font = .default, 241 | }; 242 | }; 243 | 244 | /// display -> display 245 | /// position -> position 246 | /// float -> float 247 | pub const BoxStyle = struct { 248 | display: types.Display, 249 | position: types.Position, 250 | float: types.Float, 251 | 252 | pub const initial_values = BoxStyle{ 253 | .display = .@"inline", 254 | .position = .static, 255 | .float = .none, 256 | }; 257 | }; 258 | 259 | /// width -> width 260 | /// min_width -> min-width 261 | /// max_width -> max-width 262 | pub const ContentWidth = struct { 263 | width: types.Size, 264 | min_width: types.MinSize, 265 | max_width: types.MaxSize, 266 | 267 | pub const initial_values = ContentWidth{ 268 | .width = .auto, 269 | .min_width = .{ .px = 0 }, 270 | .max_width = .none, 271 | }; 272 | }; 273 | 274 | /// height -> height 275 | /// min_height -> min-height 276 | /// max_height -> max-height 277 | pub const ContentHeight = struct { 278 | height: types.Size, 279 | min_height: types.MinSize, 280 | max_height: types.MaxSize, 281 | 282 | pub const initial_values = ContentHeight{ 283 | .height = .auto, 284 | .min_height = .{ .px = 0 }, 285 | .max_height = .none, 286 | }; 287 | }; 288 | 289 | /// padding_left -> padding-left 290 | /// padding_right -> padding-right 291 | /// border_left -> border-width-left 292 | /// border_right -> border-width-right 293 | /// margin_left -> margin-left 294 | /// margin_right -> margin-right 295 | pub const HorizontalEdges = struct { 296 | padding_left: types.Padding, 297 | padding_right: types.Padding, 298 | border_left: types.BorderWidth, 299 | border_right: types.BorderWidth, 300 | margin_left: types.Margin, 301 | margin_right: types.Margin, 302 | 303 | pub const initial_values = HorizontalEdges{ 304 | .padding_left = .{ .px = 0 }, 305 | .padding_right = .{ .px = 0 }, 306 | .border_left = .medium, 307 | .border_right = .medium, 308 | .margin_left = .{ .px = 0 }, 309 | .margin_right = .{ .px = 0 }, 310 | }; 311 | }; 312 | 313 | /// padding_top -> padding-top 314 | /// padding_bottom -> padding-bottom 315 | /// border_top -> border-width-top 316 | /// border_bottom -> border-width-bottom 317 | /// margin_top -> margin-top 318 | /// margin_bottom -> margin-bottom 319 | pub const VerticalEdges = struct { 320 | padding_top: types.Padding, 321 | padding_bottom: types.Padding, 322 | border_top: types.BorderWidth, 323 | border_bottom: types.BorderWidth, 324 | margin_top: types.Margin, 325 | margin_bottom: types.Margin, 326 | 327 | pub const initial_values = VerticalEdges{ 328 | .padding_top = .{ .px = 0 }, 329 | .padding_bottom = .{ .px = 0 }, 330 | .border_top = .medium, 331 | .border_bottom = .medium, 332 | .margin_top = .{ .px = 0 }, 333 | .margin_bottom = .{ .px = 0 }, 334 | }; 335 | }; 336 | 337 | /// z_index -> z-index 338 | pub const ZIndex = struct { 339 | z_index: types.ZIndex, 340 | 341 | pub const initial_values = ZIndex{ 342 | .z_index = .auto, 343 | }; 344 | }; 345 | 346 | /// left -> left 347 | /// right -> right 348 | /// top -> top 349 | /// bottom -> bottom 350 | pub const Insets = struct { 351 | left: types.Inset, 352 | right: types.Inset, 353 | top: types.Inset, 354 | bottom: types.Inset, 355 | 356 | pub const initial_values = Insets{ 357 | .left = .auto, 358 | .right = .auto, 359 | .top = .auto, 360 | .bottom = .auto, 361 | }; 362 | }; 363 | 364 | /// color -> color 365 | pub const Color = struct { 366 | color: types.Color, 367 | 368 | pub const initial_values = Color{ 369 | // TODO: According to CSS Color Level 4, the initial value is 'CanvasText'. 370 | .color = types.Color.black, 371 | }; 372 | }; 373 | 374 | /// left -> border-left-color 375 | /// right -> border-right-color 376 | /// top -> border-top-color 377 | /// bottom -> border-bottom-color 378 | pub const BorderColors = struct { 379 | left: types.Color, 380 | right: types.Color, 381 | top: types.Color, 382 | bottom: types.Color, 383 | 384 | pub const initial_values = BorderColors{ 385 | .left = .current_color, 386 | .right = .current_color, 387 | .top = .current_color, 388 | .bottom = .current_color, 389 | }; 390 | }; 391 | 392 | /// left -> border-left-style 393 | /// right -> border-right-style 394 | /// top -> border-top-style 395 | /// bottom -> border-bottom-style 396 | pub const BorderStyles = struct { 397 | left: types.BorderStyle, 398 | right: types.BorderStyle, 399 | top: types.BorderStyle, 400 | bottom: types.BorderStyle, 401 | 402 | pub const initial_values = BorderStyles{ 403 | .left = .none, 404 | .right = .none, 405 | .top = .none, 406 | .bottom = .none, 407 | }; 408 | }; 409 | 410 | /// color -> background-color 411 | pub const BackgroundColor = struct { 412 | color: types.Color, 413 | 414 | pub const initial_values = BackgroundColor{ 415 | .color = types.Color.transparent, 416 | }; 417 | }; 418 | 419 | /// clip -> background-clip 420 | pub const BackgroundClip = struct { 421 | clip: types.BackgroundClip, 422 | 423 | pub const initial_values = BackgroundClip{ 424 | .clip = .border_box, 425 | }; 426 | }; 427 | 428 | /// image -> background-image 429 | /// repeat -> background-repeat 430 | /// position -> background-position 431 | /// origin -> background-origin 432 | /// size -> background-size 433 | pub const Background = struct { 434 | image: types.BackgroundImage, 435 | repeat: types.BackgroundRepeat, 436 | attachment: types.BackgroundAttachment, 437 | position: types.BackgroundPosition, 438 | origin: types.BackgroundOrigin, 439 | size: types.BackgroundSize, 440 | 441 | pub const initial_values = Background{ 442 | .image = .none, 443 | .repeat = .{ .x = .repeat, .y = .repeat }, 444 | .attachment = .scroll, 445 | .position = .{ 446 | .x = .{ .side = .start, .offset = .{ .percentage = 0 } }, 447 | .y = .{ .side = .start, .offset = .{ .percentage = 0 } }, 448 | }, 449 | .origin = .padding_box, 450 | .size = .{ .size = .{ .width = .auto, .height = .auto } }, 451 | }; 452 | }; 453 | -------------------------------------------------------------------------------- /source/SegmentedUtf8String.zig: -------------------------------------------------------------------------------- 1 | //! An append-only list data structure that holds a UTF-8 encoded string. 2 | //! The string is stored in segments, with each new segment being double the size of the previous one. 3 | //! The segments, once allocated, are never moved in memory. 4 | //! Care is taken such that a single codepoint is never split between two different segments. 5 | 6 | segments: [*]Segment, 7 | /// The position of the next place to append codepoints. 8 | /// This number represents an index into the (imaginary) array that you would get from concatenating all of the segments together, from smallest to largest. 9 | position: Size, 10 | first_segment_len_log2: SegmentIndex, 11 | /// The maximum length of the `segments` array. 12 | max_segments_len: SegmentsLen, 13 | debug: Debug, 14 | 15 | /// To ensure that codepoints are not split between two different segments, the last 1-3 bytes of a segment may not get used. 16 | /// For example, if a segment only has 3 bytes left, and you try to append a codepoint that takes up 4 bytes, the string will create a new segment and append it there instead. 17 | /// The last 3 bytes of the previous segment become unusable. 18 | /// Segments are allocated with an alignment of 4, so that the last 2 bits of the pointer address can be used to store the number of unusable bytes. 19 | /// Segments cannot be completely empty; they must always have a non-zero amount of either used or unusable bytes. 20 | const Segment = struct { 21 | addr: usize, 22 | 23 | const mask: usize = 0b11; 24 | 25 | fn unusableLen(segment: Segment) u2 { 26 | return @intCast(segment.addr & mask); 27 | } 28 | 29 | fn setUnusableLen(segment: *Segment, len: u2) void { 30 | segment.addr |= len; 31 | } 32 | 33 | fn ptr(segment: Segment) [*]align(4) u8 { 34 | return @ptrFromInt(segment.addr & ~mask); 35 | } 36 | }; 37 | 38 | pub const Size = u32; 39 | 40 | /// A type large enough to hold an index into the segments array. 41 | pub const SegmentIndex = std.math.Log2Int(Size); 42 | 43 | /// A type large enough to hold the maximum length of the segments array. 44 | pub const SegmentsLen = std.math.Log2IntCeil(Size); 45 | 46 | // TODO: Possible improvements: 47 | // - Statically allocate the segment array 48 | // - Choose a smaller integer index type 49 | // - Rollback in case an error is returned 50 | const SegmentedUtf8String = @This(); 51 | 52 | const std = @import("std"); 53 | const assert = std.debug.assert; 54 | const Allocator = std.mem.Allocator; 55 | 56 | /// The theoretical maximum length in bytes of this string is `2 * last_segment_len - first_segment_len`. 57 | pub fn init( 58 | /// The length in bytes of the very first segment to be allocated. Must be a power of 2. 59 | first_segment_len: Size, 60 | /// The length in bytes of the very last segment to be allocated. Must be a power of 2. Must be greater than or equal to `first_segment_len`. 61 | last_segment_len: Size, 62 | ) SegmentedUtf8String { 63 | assert(std.math.isPowerOfTwo(first_segment_len)); 64 | assert(std.math.isPowerOfTwo(last_segment_len)); 65 | const first_segment_len_log2 = std.math.log2_int(Size, first_segment_len); 66 | const last_segment_len_log2 = std.math.log2_int(Size, last_segment_len); 67 | return .{ 68 | .segments = undefined, 69 | .position = 0, 70 | .first_segment_len_log2 = first_segment_len_log2, 71 | .max_segments_len = @as(SegmentsLen, 1) + (last_segment_len_log2 - first_segment_len_log2), 72 | .debug = .{}, 73 | }; 74 | } 75 | 76 | pub fn deinit(string: *SegmentedUtf8String, allocator: Allocator) void { 77 | const segments = string.segments[0..string.numSegments()]; 78 | for (segments, 0..) |segment, segment_index_usize| { 79 | const complete_len = string.segmentCompleteLen(@intCast(segment_index_usize)); 80 | allocator.free(segment.ptr()[0..complete_len]); 81 | } 82 | allocator.free(segments); 83 | } 84 | 85 | /// The length of the array pointed to by `string.segments`. 86 | fn numSegments(string: *const SegmentedUtf8String) SegmentsLen { 87 | const location = string.positionToLocation(string.position); 88 | return location.segment_index + @intFromBool(location.byte_offset != 0); 89 | } 90 | 91 | fn segmentCompleteLen(string: *const SegmentedUtf8String, segment_index: SegmentIndex) Size { 92 | return @as(Size, 1) << (string.first_segment_len_log2 + segment_index); 93 | } 94 | 95 | const Location = struct { 96 | segment_index: SegmentsLen, 97 | byte_offset: Size, 98 | }; 99 | 100 | fn positionToLocation(string: *const SegmentedUtf8String, position: Size) Location { 101 | const first_segment_len = @as(Size, 1) << string.first_segment_len_log2; 102 | const max_position = max_position: { 103 | const last_segment_len_log2: SegmentIndex = @intCast(string.max_segments_len - 1 + string.first_segment_len_log2); 104 | const last_segment_len = @as(Size, 1) << last_segment_len_log2; 105 | // Computes `2 * last_segment_len - first_segment_len` while avoiding overflow. 106 | break :max_position last_segment_len - first_segment_len + last_segment_len; 107 | }; 108 | if (position == max_position) return .{ 109 | .segment_index = string.max_segments_len, 110 | .byte_offset = 0, 111 | }; 112 | 113 | const normalized_position = position + first_segment_len; 114 | const normalized_position_log2 = std.math.log2_int(Size, normalized_position); 115 | const normalized_position_log2_remainder = normalized_position - (@as(Size, 1) << normalized_position_log2); 116 | return .{ 117 | .segment_index = normalized_position_log2 - string.first_segment_len_log2, 118 | .byte_offset = normalized_position_log2_remainder, 119 | }; 120 | } 121 | 122 | /// The new substring can be referred to via a tuple of `string.position` (before calling this function) and `items.len`. 123 | pub fn append(string: *SegmentedUtf8String, allocator: Allocator, items: []const u8) !void { 124 | var remaining_items = items; 125 | while (remaining_items.len > 0) { 126 | const location = string.positionToLocation(string.position); 127 | if (location.segment_index == string.max_segments_len) return error.OutOfSegments; 128 | 129 | const complete_len = string.segmentCompleteLen(@intCast(location.segment_index)); 130 | if (location.byte_offset == 0) { 131 | // `string.position` is the position where new codepoints are appended. 132 | // Therefore, if `location.byte_offset == 0`, then `location` points to past the end of the current segment. 133 | // In that case, a new segment needs to be allocated in order to append codepoints. 134 | 135 | // TODO: Consider reserving all the new segments beforehand 136 | const segment = try allocator.alignedAlloc(u8, .fromByteUnits(4), complete_len); 137 | errdefer allocator.free(segment); 138 | 139 | const old_segments = string.segments[0..location.segment_index]; 140 | // TODO: Consider allocating all segment pointers beforehand 141 | const new_segments = try allocator.realloc(old_segments, location.segment_index + 1); 142 | new_segments[location.segment_index] = .{ .addr = @intFromPtr(segment.ptr) }; 143 | string.segments = new_segments.ptr; 144 | } 145 | 146 | const segment = &string.segments[location.segment_index]; 147 | assert(segment.unusableLen() == 0); 148 | const usable_len = complete_len - location.byte_offset; 149 | 150 | const copyable_len = copyable_len: { 151 | if (remaining_items.len <= usable_len) { 152 | string.position += @intCast(remaining_items.len); 153 | break :copyable_len remaining_items.len; 154 | } else { 155 | string.position += usable_len; 156 | 157 | const copyable_len = blk: { 158 | // Backtrack to find the start byte of the most recent codepoint within `remaining_items`. 159 | for (0..@min(4, usable_len)) |i| { 160 | const index = usable_len - 1 - i; 161 | const byte = remaining_items[index]; 162 | const codepoint_len = std.unicode.utf8ByteSequenceLength(byte) catch continue; 163 | break :blk if (codepoint_len > usable_len - index) index else usable_len; 164 | } else unreachable; // Invalid UTF-8 165 | }; 166 | 167 | segment.setUnusableLen(@intCast(usable_len - copyable_len)); 168 | break :copyable_len copyable_len; 169 | } 170 | }; 171 | 172 | @memcpy(segment.ptr() + location.byte_offset, remaining_items[0..copyable_len]); 173 | remaining_items = remaining_items[copyable_len..]; 174 | } 175 | } 176 | 177 | pub const Iterator = struct { 178 | string: *const SegmentedUtf8String, 179 | remaining: Size, 180 | location: Location, 181 | 182 | /// You may modify the data within the returned slice, but 183 | /// it must remain a valid UTF-8 byte sequence. 184 | pub fn next(it: *Iterator) ?[]u8 { 185 | if (it.remaining == 0) return null; 186 | assert(it.location.segment_index != it.string.max_segments_len); 187 | 188 | const segment = it.string.segments[it.location.segment_index]; 189 | const complete_len = it.string.segmentCompleteLen(@intCast(it.location.segment_index)); 190 | const usable_len = complete_len - it.location.byte_offset - segment.unusableLen(); 191 | const used_len = @min(it.remaining, usable_len); 192 | 193 | defer { 194 | it.remaining -= used_len; 195 | it.location.segment_index += 1; 196 | it.location.byte_offset = 0; 197 | } 198 | 199 | return segment.ptr()[it.location.byte_offset..][0..used_len]; 200 | } 201 | 202 | pub fn eql(it: *Iterator, string: []const u8) bool { 203 | if (it.remaining != string.len) return false; 204 | var remaining = string; 205 | while (it.next()) |segment| { 206 | if (!std.mem.eql(u8, remaining[0..segment.len], segment)) return false; 207 | remaining = remaining[segment.len..]; 208 | } 209 | return true; 210 | } 211 | }; 212 | 213 | /// Returns an iterator over a substring. 214 | pub fn iterator( 215 | string: *const SegmentedUtf8String, 216 | position: Size, 217 | /// The length in bytes of the substring. 218 | len: Size, 219 | ) Iterator { 220 | return .{ 221 | .string = string, 222 | .remaining = len, 223 | .location = string.positionToLocation(position), 224 | }; 225 | } 226 | 227 | pub const Debug = struct { 228 | /// The length in bytes of the entire string. 229 | pub fn len(debug: *const Debug) Size { 230 | const string: *const SegmentedUtf8String = @alignCast(@fieldParentPtr("debug", debug)); 231 | const num_segments = string.numSegments(); 232 | if (num_segments == 0) return 0; 233 | 234 | var result: Size = 0; 235 | for (string.segments[0 .. string.numSegments() - 1], 0..) |segment, segment_index| { 236 | const complete_len = string.segmentCompleteLen(@intCast(segment_index)); 237 | const usable_len = complete_len - segment.unusableLen(); 238 | result += usable_len; 239 | } 240 | 241 | const end_location = string.positionToLocation(string.position); 242 | if (end_location.byte_offset != 0) { 243 | result += end_location.byte_offset; 244 | } else { 245 | const last_segment = string.segments[num_segments - 1]; 246 | const complete_len = string.segmentCompleteLen(@intCast(num_segments - 1)); 247 | const usable_len = complete_len - last_segment.unusableLen(); 248 | result += usable_len; 249 | } 250 | 251 | return result; 252 | } 253 | 254 | pub fn print(debug: *const Debug, writer: std.io.AnyWriter) !void { 255 | const string: *const SegmentedUtf8String = @alignCast(@fieldParentPtr("debug", debug)); 256 | const string_len = debug.len(); 257 | try writer.print("{} segments, {} bytes\n", .{ string.numSegments(), string_len }); 258 | 259 | var segment_index: SegmentsLen = 0; 260 | var it = string.iterator(0, string_len); 261 | while (it.next()) |segment| : (segment_index += 1) { 262 | try writer.print("Segment {} ({} bytes): \"{s}\"\n", .{ segment_index, segment.len, segment }); 263 | } 264 | } 265 | }; 266 | 267 | test init { 268 | _ = init(1, 1); 269 | _ = init(1, 16); 270 | _ = init(4, 16); 271 | _ = init(16, 16); 272 | _ = init(1, 1 << (@bitSizeOf(Size) - 1)); 273 | } 274 | 275 | test append { 276 | const allocator = std.testing.allocator; 277 | 278 | { 279 | var string = init(8, 8); 280 | defer string.deinit(allocator); 281 | try string.append(allocator, "abcdefgh"); 282 | } 283 | { 284 | var string = init(4, 8); 285 | defer string.deinit(allocator); 286 | try string.append(allocator, "abcdefghwxyz"); 287 | } 288 | { 289 | var string = init(8, 16); 290 | defer string.deinit(allocator); 291 | try string.append(allocator, "あいうえお"); 292 | } 293 | { 294 | var string = init(1, 16); 295 | defer string.deinit(allocator); 296 | try string.append(allocator, "日月火水木金土"); 297 | } 298 | { 299 | var string = init(4, 8); 300 | defer string.deinit(allocator); 301 | try string.append(allocator, "🙂abc"); 302 | } 303 | { 304 | var string = init(1, 4); 305 | defer string.deinit(allocator); 306 | try std.testing.expectError(error.OutOfSegments, string.append(allocator, "1234567890")); 307 | } 308 | } 309 | 310 | test iterator { 311 | const allocator = std.testing.allocator; 312 | 313 | const ns = struct { 314 | fn compareIterator(string: *const SegmentedUtf8String, position: Size, len: Size, expected: []const u8) !void { 315 | var it = string.iterator(position, len); 316 | var remaining = expected; 317 | while (it.next()) |segment| { 318 | try std.testing.expectEqualStrings(remaining[0..segment.len], segment); 319 | remaining = remaining[segment.len..]; 320 | } 321 | try std.testing.expectEqual(remaining.len, 0); 322 | } 323 | }; 324 | 325 | { 326 | var string = init(4, 8); 327 | defer string.deinit(allocator); 328 | const pos = string.position; 329 | try string.append(allocator, "abcdefghwxyz"); 330 | try ns.compareIterator(&string, pos, 12, "abcdefghwxyz"); 331 | } 332 | { 333 | var string = init(4, 16); 334 | defer string.deinit(allocator); 335 | const pos = string.position; 336 | try string.append(allocator, "日月火水木金土"); 337 | try ns.compareIterator(&string, pos, 15, "日月火水木"); 338 | } 339 | { 340 | var string = init(4, 8); 341 | defer string.deinit(allocator); 342 | try string.append(allocator, "abcdef"); 343 | const pos = string.position; 344 | try string.append(allocator, "ghwxyz"); 345 | try ns.compareIterator(&string, pos, 6, "ghwxyz"); 346 | } 347 | { 348 | var string = init(4, 16); 349 | defer string.deinit(allocator); 350 | const pos1 = string.position; 351 | try string.append(allocator, "日月"); 352 | const pos2 = string.position; 353 | try string.append(allocator, "火水木"); 354 | const pos3 = string.position; 355 | try string.append(allocator, "金土"); 356 | try ns.compareIterator(&string, pos1, 6, "日月"); 357 | try ns.compareIterator(&string, pos2, 9, "火水木"); 358 | try ns.compareIterator(&string, pos3, 6, "金土"); 359 | try ns.compareIterator(&string, pos2, 15, "火水木金土"); 360 | try ns.compareIterator(&string, pos1, 21, "日月火水木金土"); 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /source/Layout/solve.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const zss = @import("../zss.zig"); 4 | const types = zss.values.types; 5 | const BoxTree = zss.BoxTree; 6 | 7 | const groups = zss.values.groups; 8 | const ComputedValues = groups.Tag.ComputedValues; 9 | const SpecifiedValues = groups.Tag.SpecifiedValues; 10 | 11 | const math = zss.math; 12 | const Unit = math.Unit; 13 | const units_per_pixel = math.units_per_pixel; 14 | 15 | pub const LengthUnit = enum { px }; 16 | 17 | pub fn length(comptime unit: LengthUnit, value: f32) Unit { 18 | return switch (unit) { 19 | .px => @as(Unit, @intFromFloat(@round(value * units_per_pixel))), 20 | }; 21 | } 22 | 23 | pub fn positiveLength(comptime unit: LengthUnit, value: f32) Unit { 24 | if (value < 0.0 or !std.math.isNormal(value)) return 0; 25 | return length(unit, value); 26 | } 27 | 28 | pub fn percentage(value: f32, unit: Unit) Unit { 29 | return @intFromFloat(@round(@as(f32, @floatFromInt(unit)) * value)); 30 | } 31 | 32 | pub fn positivePercentage(value: f32, unit: Unit) Unit { 33 | if (value < 0.0 or !std.math.isNormal(value)) return 0; 34 | return percentage(value, unit); 35 | } 36 | 37 | pub fn clampSize(size: Unit, min_size: Unit, max_size: Unit) Unit { 38 | return @max(min_size, @min(size, max_size)); 39 | } 40 | 41 | pub fn borderWidth(comptime thickness: std.meta.Tag(types.BorderWidth)) f32 { 42 | return switch (thickness) { 43 | // TODO: Let these values be user-customizable. 44 | .thin => 1, 45 | .medium => 3, 46 | .thick => 5, 47 | else => @compileError("invalid value"), 48 | }; 49 | } 50 | 51 | pub fn borderWidthMultiplier(border_style: types.BorderStyle) f32 { 52 | return switch (border_style) { 53 | .none, .hidden => 0, 54 | .solid, 55 | .dotted, 56 | .dashed, 57 | .double, 58 | .groove, 59 | .ridge, 60 | .inset, 61 | .outset, 62 | => 1, 63 | }; 64 | } 65 | 66 | pub fn color(col: types.Color, current_color: math.Color) math.Color { 67 | return switch (col) { 68 | .rgba => |rgba| math.Color.fromRgbaInt(rgba), 69 | .transparent => .transparent, 70 | .current_color => current_color, 71 | }; 72 | } 73 | 74 | /// Use to resolve the value of the 'color' property. 75 | /// To resolve the value of just a normal color value, use `color` instead. 76 | pub fn colorProperty(specified: SpecifiedValues(.color)) struct { ComputedValues(.color), math.Color } { 77 | const computed = specified; 78 | const used: math.Color = switch (computed.color) { 79 | .rgba => |rgba| .fromRgbaInt(rgba), 80 | .transparent => .transparent, 81 | .current_color => std.debug.panic("TODO: 'currentColor' on the 'color' property", .{}), 82 | }; 83 | return .{ computed, used }; 84 | } 85 | 86 | /// Implements the rules specified in section 9.7 of CSS2.2. 87 | pub fn boxStyle(specified: SpecifiedValues(.box_style), comptime is_root: zss.Layout.IsRoot) struct { ComputedValues(.box_style), BoxTree.BoxStyle } { 88 | var computed: ComputedValues(.box_style) = .{ 89 | .display = undefined, 90 | .position = specified.position, 91 | .float = specified.float, 92 | }; 93 | 94 | if (specified.display == .none) { 95 | computed.display = .none; 96 | return .{ computed, .{ .outer = .none, .position = .static } }; 97 | } 98 | 99 | var position: BoxTree.BoxStyle.Position = undefined; 100 | switch (is_root) { 101 | .not_root => { 102 | switch (specified.position) { 103 | .absolute => { 104 | computed.display = blockify(specified.display); 105 | computed.float = .none; 106 | const used: BoxTree.BoxStyle = .{ 107 | .outer = .{ .absolute = innerBlockType(computed.display) }, 108 | .position = .absolute, 109 | }; 110 | return .{ computed, used }; 111 | }, 112 | .fixed => std.debug.panic("TODO: fixed positioning", .{}), 113 | .static, .relative, .sticky => {}, 114 | } 115 | 116 | if (specified.float != .none) { 117 | std.debug.panic("TODO: floats", .{}); 118 | } 119 | 120 | computed.display = specified.display; 121 | position = switch (computed.position) { 122 | .static => .static, 123 | .relative => .relative, 124 | .sticky => std.debug.panic("TODO: sticky positioning", .{}), 125 | .absolute, .fixed => unreachable, 126 | }; 127 | }, 128 | .root => { 129 | computed.display = blockify(specified.display); 130 | computed.position = .static; 131 | computed.float = .none; 132 | position = .static; 133 | }, 134 | } 135 | 136 | const used: BoxTree.BoxStyle = .{ 137 | .outer = switch (computed.display) { 138 | .block => .{ .block = .flow }, 139 | .@"inline" => .{ .@"inline" = .@"inline" }, 140 | .inline_block => .{ .@"inline" = .{ .block = .flow } }, 141 | .none => unreachable, 142 | }, 143 | .position = position, 144 | }; 145 | 146 | return .{ computed, used }; 147 | } 148 | 149 | /// Given a specified value for 'display', returns the computed value according to the table found in section 9.7 of CSS2.2. 150 | fn blockify(display: types.Display) types.Display { 151 | // TODO: This is incomplete, fill in the rest when more values of the 'display' property are supported. 152 | // TODO: There should be a slightly different version of this switch table for the root element. (See rule 4 of secion 9.7) 153 | return switch (display) { 154 | .block => .block, 155 | .@"inline", .inline_block => .block, 156 | .none => unreachable, 157 | }; 158 | } 159 | 160 | fn innerBlockType(computed_display: types.Display) BoxTree.BoxStyle.InnerBlock { 161 | return switch (computed_display) { 162 | .block => .flow, 163 | .@"inline", .inline_block, .none => unreachable, 164 | }; 165 | } 166 | 167 | pub fn insets(specified: SpecifiedValues(.insets)) ComputedValues(.insets) { 168 | var computed: ComputedValues(.insets) = undefined; 169 | inline for (std.meta.fields(ComputedValues(.insets))) |field_info| { 170 | @field(computed, field_info.name) = switch (@field(specified, field_info.name)) { 171 | .px => |value| .{ .px = value }, 172 | .percentage => |value| .{ .percentage = value }, 173 | .auto => .auto, 174 | }; 175 | } 176 | return computed; 177 | } 178 | 179 | pub fn borderColors(border_colors: SpecifiedValues(.border_colors), current_color: math.Color) BoxTree.BorderColors { 180 | return .{ 181 | .left = color(border_colors.left, current_color), 182 | .right = color(border_colors.right, current_color), 183 | .top = color(border_colors.top, current_color), 184 | .bottom = color(border_colors.bottom, current_color), 185 | }; 186 | } 187 | 188 | pub fn borderStyles(border_styles: SpecifiedValues(.border_styles)) void { 189 | const ns = struct { 190 | fn solveOne(border_style: types.BorderStyle) void { 191 | switch (border_style) { 192 | .none, .hidden, .solid => {}, 193 | .dotted, 194 | .dashed, 195 | .double, 196 | .groove, 197 | .ridge, 198 | .inset, 199 | .outset, 200 | => std.debug.panic("TODO: border-style: {s}", .{@tagName(border_style)}), 201 | } 202 | } 203 | }; 204 | 205 | inline for (std.meta.fields(groups.BorderStyles)) |field_info| { 206 | ns.solveOne(@field(border_styles, field_info.name)); 207 | } 208 | } 209 | 210 | pub fn backgroundClip(clip: types.BackgroundClip) BoxTree.BackgroundClip { 211 | return switch (clip) { 212 | .border_box => .border, 213 | .padding_box => .padding, 214 | .content_box => .content, 215 | }; 216 | } 217 | 218 | pub fn inlineBoxBackground(col: types.Color, clip: types.BackgroundClip, current_color: math.Color) BoxTree.InlineBoxBackground { 219 | return .{ 220 | .color = color(col, current_color), 221 | .clip = backgroundClip(clip), 222 | }; 223 | } 224 | 225 | pub fn backgroundImage( 226 | handle: zss.Images.Handle, 227 | dimensions: zss.Images.Dimensions, 228 | specified: struct { 229 | origin: types.BackgroundOrigin, 230 | position: types.BackgroundPosition, 231 | size: types.BackgroundSize, 232 | repeat: types.BackgroundRepeat, 233 | attachment: types.BackgroundAttachment, 234 | clip: types.BackgroundClip, 235 | }, 236 | box_offsets: *const BoxTree.BoxOffsets, 237 | borders: *const BoxTree.Borders, 238 | ) BoxTree.BackgroundImage { 239 | // TODO: Handle background-attachment 240 | 241 | const NaturalSize = struct { 242 | width: Unit, 243 | height: Unit, 244 | has_aspect_ratio: bool, 245 | }; 246 | 247 | const natural_size: NaturalSize = blk: { 248 | const width = positiveLength(.px, @floatFromInt(dimensions.width_px)); 249 | const height = positiveLength(.px, @floatFromInt(dimensions.height_px)); 250 | break :blk .{ 251 | .width = width, 252 | .height = height, 253 | .has_aspect_ratio = width != 0 and height != 0, 254 | }; 255 | }; 256 | 257 | const border_width = box_offsets.border_size.w; 258 | const border_height = box_offsets.border_size.h; 259 | const padding_width = border_width - borders.left - borders.right; 260 | const padding_height = border_height - borders.top - borders.bottom; 261 | const content_width = box_offsets.content_size.w; 262 | const content_height = box_offsets.content_size.h; 263 | const positioning_area: struct { origin: BoxTree.BackgroundImage.Origin, width: Unit, height: Unit } = switch (specified.origin) { 264 | .border_box => .{ .origin = .border, .width = border_width, .height = border_height }, 265 | .padding_box => .{ .origin = .padding, .width = padding_width, .height = padding_height }, 266 | .content_box => .{ .origin = .content, .width = content_width, .height = content_height }, 267 | }; 268 | 269 | var width_was_auto = false; 270 | var height_was_auto = false; 271 | var size: BoxTree.BackgroundImage.Size = switch (specified.size) { 272 | .size => |size| .{ 273 | .w = switch (size.width) { 274 | .px => |val| positiveLength(.px, val), 275 | .percentage => |p| positivePercentage(p, positioning_area.width), 276 | .auto => blk: { 277 | width_was_auto = true; 278 | break :blk 0; 279 | }, 280 | }, 281 | .h = switch (size.height) { 282 | .px => |val| positiveLength(.px, val), 283 | .percentage => |p| positivePercentage(p, positioning_area.height), 284 | .auto => blk: { 285 | height_was_auto = true; 286 | break :blk 0; 287 | }, 288 | }, 289 | }, 290 | .contain, .cover => blk: { 291 | if (!natural_size.has_aspect_ratio) break :blk BoxTree.BackgroundImage.Size{ .w = natural_size.width, .h = natural_size.height }; 292 | 293 | const positioning_area_is_wider_than_image = positioning_area.width * natural_size.height > positioning_area.height * natural_size.width; 294 | const is_contain = (specified.size == .contain); 295 | 296 | if (positioning_area_is_wider_than_image == is_contain) { 297 | break :blk BoxTree.BackgroundImage.Size{ 298 | .w = @divFloor(positioning_area.height * natural_size.width, natural_size.height), 299 | .h = positioning_area.height, 300 | }; 301 | } else { 302 | break :blk BoxTree.BackgroundImage.Size{ 303 | .w = positioning_area.width, 304 | .h = @divFloor(positioning_area.width * natural_size.height, natural_size.width), 305 | }; 306 | } 307 | }, 308 | }; 309 | 310 | const repeat: BoxTree.BackgroundImage.Repeat = .{ 311 | .x = switch (specified.repeat.x) { 312 | .no_repeat => .none, 313 | .repeat => .repeat, 314 | .space => .space, 315 | .round => .round, 316 | }, 317 | .y = switch (specified.repeat.y) { 318 | .no_repeat => .none, 319 | .repeat => .repeat, 320 | .space => .space, 321 | .round => .round, 322 | }, 323 | }; 324 | 325 | // TODO: Needs review 326 | if (width_was_auto or height_was_auto or repeat.x == .round or repeat.y == .round) { 327 | const divRound = math.divRound; 328 | 329 | if (width_was_auto and height_was_auto) { 330 | size.w = natural_size.width; 331 | size.h = natural_size.height; 332 | } else if (width_was_auto) { 333 | size.w = if (natural_size.has_aspect_ratio) 334 | divRound(size.h * natural_size.width, natural_size.height) 335 | else 336 | positioning_area.width; 337 | } else if (height_was_auto) { 338 | size.h = if (natural_size.has_aspect_ratio) 339 | divRound(size.w * natural_size.height, natural_size.width) 340 | else 341 | positioning_area.height; 342 | } 343 | 344 | if (repeat.x == .round and repeat.y == .round) { 345 | size.w = @divFloor(positioning_area.width, @max(1, divRound(positioning_area.width, size.w))); 346 | size.h = @divFloor(positioning_area.height, @max(1, divRound(positioning_area.height, size.h))); 347 | } else if (repeat.x == .round) { 348 | if (size.w > 0) size.w = @divFloor(positioning_area.width, @max(1, divRound(positioning_area.width, size.w))); 349 | if (height_was_auto and natural_size.has_aspect_ratio) size.h = @divFloor(size.w * natural_size.height, natural_size.width); 350 | } else if (repeat.y == .round) { 351 | if (size.h > 0) size.h = @divFloor(positioning_area.height, @max(1, divRound(positioning_area.height, size.h))); 352 | if (width_was_auto and natural_size.has_aspect_ratio) size.w = @divFloor(size.h * natural_size.width, natural_size.height); 353 | } 354 | } 355 | 356 | const position: BoxTree.BackgroundImage.Position = .{ 357 | .x = blk: { 358 | const available_space = positioning_area.width - size.w; 359 | switch (specified.position.x.side) { 360 | .start, .end => { 361 | switch (specified.position.x.offset) { 362 | .px => |val| { 363 | const offset = length(.px, val); 364 | const offset_adjusted = if (specified.position.x.side == .start) offset else available_space - offset; 365 | break :blk offset_adjusted; 366 | }, 367 | .percentage => |p| { 368 | const percentage_adjusted = if (specified.position.x.side == .start) p else 1 - p; 369 | break :blk percentage(percentage_adjusted, available_space); 370 | }, 371 | } 372 | }, 373 | .center => break :blk percentage(0.5, available_space), 374 | } 375 | }, 376 | .y = blk: { 377 | const available_space = positioning_area.height - size.h; 378 | switch (specified.position.y.side) { 379 | .start, .end => { 380 | switch (specified.position.y.offset) { 381 | .px => |val| { 382 | const offset = length(.px, val); 383 | const offset_adjusted = if (specified.position.y.side == .start) offset else available_space - offset; 384 | break :blk offset_adjusted; 385 | }, 386 | .percentage => |p| { 387 | const percentage_adjusted = if (specified.position.y.side == .start) p else 1 - p; 388 | break :blk percentage(percentage_adjusted, available_space); 389 | }, 390 | } 391 | }, 392 | .center => break :blk percentage(0.5, available_space), 393 | } 394 | }, 395 | }; 396 | 397 | const clip = backgroundClip(specified.clip); 398 | 399 | return BoxTree.BackgroundImage{ 400 | .handle = handle, 401 | .origin = positioning_area.origin, 402 | .position = position, 403 | .size = size, 404 | .repeat = repeat, 405 | .clip = clip, 406 | }; 407 | } 408 | --------------------------------------------------------------------------------