diff --git a/src/browser/webapi/svg/PointList.zig b/src/browser/webapi/svg/PointList.zig
index 689dc78cd..7d77ee7b6 100644
--- a/src/browser/webapi/svg/PointList.zig
+++ b/src/browser/webapi/svg/PointList.zig
@@ -17,6 +17,7 @@
// along with this program. If not, see .
const std = @import("std");
+const lp = @import("lightpanda");
const js = @import("../../js/js.zig");
const Frame = @import("../../Frame.zig");
@@ -24,6 +25,7 @@ const Page = @import("../../Page.zig");
const DOMPoint = @import("../DOMPoint.zig");
const DOMPointReadOnly = @import("../DOMPointReadOnly.zig");
const Element = @import("../Element.zig");
+const reflected_list = @import("reflected_list.zig");
const PointList = @This();
@@ -35,6 +37,16 @@ _snapshot: std.ArrayList(u8) = .empty,
_items: std.ArrayList(*DOMPoint) = .empty,
_retired: std.ArrayList(*DOMPoint) = .empty,
+const M = reflected_list.Mixin(PointList, DOMPoint, .{
+ .attrName = attrName,
+ .parse = parse,
+ .writeItem = writeItem,
+ .prepareItem = prepareItem,
+ .attach = attach,
+ .detachItem = detachItem,
+ .releaseItem = releaseItem,
+});
+
pub const Kind = enum { base, animated };
pub const Key = struct {
@@ -61,114 +73,19 @@ pub fn getOrCreate(element: *Element, kind: Kind, frame: *Frame) !*PointList {
return gop.value_ptr.*;
}
-pub fn deinit(self: *PointList, page: *Page) void {
- for (self._items.items) |point| {
- point._proto.detach(self);
- point._proto.releaseRef(page);
- }
- self._items.clearRetainingCapacity();
- self.releaseRetired(page);
-}
+pub const deinit = M.deinit;
+pub const getLength = M.getLength;
+pub const getNumberOfItems = M.getNumberOfItems;
+pub const clear = M.clear;
+pub const initialize = M.initialize;
+pub const getItem = M.getItem;
+pub const insertItemBefore = M.insertItemBefore;
+pub const replaceItem = M.replaceItem;
+pub const removeItem = M.removeItem;
+pub const appendItem = M.appendItem;
-pub fn getLength(self: *PointList, frame: *Frame) !u32 {
- try self.sync(frame);
- return @intCast(self._items.items.len);
-}
-
-pub fn getNumberOfItems(self: *PointList, frame: *Frame) !u32 {
- return self.getLength(frame);
-}
-
-pub fn clear(self: *PointList, frame: *Frame) !void {
- try self.requireMutable();
- try self.sync(frame);
- try self.retireAll(frame);
- try self.setAttribute(&.{}, frame);
-}
-
-pub fn initialize(self: *PointList, item: *DOMPoint, frame: *Frame) !*DOMPoint {
- try self.requireMutable();
- try self.sync(frame);
-
- const prepared = try self.prepareItem(item, frame);
- errdefer prepared._proto.releaseRef(frame._page);
-
- try self.retireAll(frame);
- try self._items.ensureTotalCapacity(frame.arena, 1);
- try self.setAttribute(&.{prepared}, frame);
- self._items.appendAssumeCapacity(prepared);
- self.attach(prepared);
- return prepared;
-}
-
-pub fn getItem(self: *PointList, index: u32, frame: *Frame) !*DOMPoint {
- try self.sync(frame);
- if (index >= self._items.items.len) return error.IndexSizeError;
- return self._items.items[index];
-}
-
-pub fn insertItemBefore(self: *PointList, item: *DOMPoint, index: u32, frame: *Frame) !*DOMPoint {
- try self.requireMutable();
- try self.sync(frame);
-
- const prepared = try self.prepareItem(item, frame);
- errdefer prepared._proto.releaseRef(frame._page);
- const at = @min(@as(usize, index), self._items.items.len);
- const next = try frame.local_arena.alloc(*DOMPoint, self._items.items.len + 1);
- @memcpy(next[0..at], self._items.items[0..at]);
- next[at] = prepared;
- @memcpy(next[at + 1 ..], self._items.items[at..]);
-
- try self._items.ensureUnusedCapacity(frame.arena, 1);
- try self.setAttribute(next, frame);
- self._items.insertAssumeCapacity(at, prepared);
- self.attach(prepared);
- return prepared;
-}
-
-pub fn replaceItem(self: *PointList, item: *DOMPoint, index: u32, frame: *Frame) !*DOMPoint {
- try self.requireMutable();
- try self.sync(frame);
- if (index >= self._items.items.len) return error.IndexSizeError;
-
- const prepared = try self.prepareItem(item, frame);
- errdefer prepared._proto.releaseRef(frame._page);
- const next = try frame.local_arena.dupe(*DOMPoint, self._items.items);
- next[index] = prepared;
-
- try self._retired.ensureUnusedCapacity(frame.arena, 1);
- try self.setAttribute(next, frame);
- const replaced = self._items.items[index];
- replaced._proto.detach(self);
- self._retired.appendAssumeCapacity(replaced);
- self._items.items[index] = prepared;
- self.attach(prepared);
- return prepared;
-}
-
-pub fn removeItem(self: *PointList, index: u32, frame: *Frame) !*DOMPoint {
- try self.requireMutable();
- try self.sync(frame);
- if (index >= self._items.items.len) return error.IndexSizeError;
-
- const next = try frame.local_arena.alloc(*DOMPoint, self._items.items.len - 1);
- @memcpy(next[0..index], self._items.items[0..index]);
- @memcpy(next[index..], self._items.items[index + 1 ..]);
-
- try self._retired.ensureUnusedCapacity(frame.arena, 1);
- try self.setAttribute(next, frame);
- const removed = self._items.orderedRemove(index);
- removed._proto.detach(self);
- self._retired.appendAssumeCapacity(removed);
- return removed;
-}
-
-pub fn appendItem(self: *PointList, item: *DOMPoint, frame: *Frame) !*DOMPoint {
- return self.insertItemBefore(item, std.math.maxInt(u32), frame);
-}
-
-fn requireMutable(self: *const PointList) !void {
- if (self._read_only) return error.NoModificationAllowed;
+fn attrName(_: *const PointList) lp.String {
+ return .wrap("points");
}
fn prepareItem(_: *PointList, item: *DOMPoint, frame: *Frame) !*DOMPoint {
@@ -189,6 +106,14 @@ fn attach(self: *PointList, point: *DOMPoint) void {
}, self._read_only);
}
+fn detachItem(point: *DOMPoint, owner: *PointList) void {
+ point._proto.detach(owner);
+}
+
+fn releaseItem(point: *DOMPoint, page: *Page) void {
+ point._proto.releaseRef(page);
+}
+
fn mutatePoint(
context: *anyopaque,
point: *DOMPointReadOnly,
@@ -197,7 +122,7 @@ fn mutatePoint(
) anyerror!void {
const self: *PointList = @ptrCast(@alignCast(context));
const frame = self._frame;
- try self.sync(frame);
+ try M.sync(self, frame);
// An external attribute mutation detaches the old item during sync. The
// caller still owns that DOMPoint identity, but it no longer mutates the list.
@@ -220,43 +145,6 @@ fn mutatePoint(
point.setCoordinateRaw(coordinate, value);
}
-fn sync(self: *PointList, frame: *Frame) !void {
- self.releaseRetired(frame._page);
-
- const raw = self._element.getAttributeSafe(comptime .wrap("points")) orelse "";
- if (self._synced and std.mem.eql(u8, self._snapshot.items, raw)) {
- return;
- }
-
- self._synced = false;
- var parsed = parse(raw, frame) catch |err| switch (err) {
- error.SyntaxError => std.ArrayList(*DOMPoint).empty,
- else => return err,
- };
- errdefer for (parsed.items) |point| point._proto.releaseRef(frame._page);
-
- self._snapshot.clearRetainingCapacity();
- try self._snapshot.appendSlice(frame.arena, raw);
- try self.retireAll(frame);
- try self._items.ensureTotalCapacity(frame.arena, parsed.items.len);
- for (parsed.items) |point| {
- self._items.appendAssumeCapacity(point);
- self.attach(point);
- }
- parsed.clearRetainingCapacity();
- self._synced = true;
-}
-
-// A retired item must outlive the operation that retired it: removeItem's
-// return value has no JS wrapper until the bridge wraps it after we return.
-// By the next operation, anything still reachable holds its own ref.
-fn releaseRetired(self: *PointList, page: *Page) void {
- for (self._retired.items) |point| {
- point._proto.releaseRef(page);
- }
- self._retired.clearRetainingCapacity();
-}
-
fn parse(raw: []const u8, frame: *Frame) !std.ArrayList(*DOMPoint) {
var scanner = NumberScanner{ .input = raw };
var parsed: std.ArrayList(*DOMPoint) = .empty;
@@ -276,24 +164,8 @@ fn parse(raw: []const u8, frame: *Frame) !std.ArrayList(*DOMPoint) {
return parsed;
}
-fn retireAll(self: *PointList, frame: *Frame) !void {
- self._synced = false;
- try self._retired.ensureUnusedCapacity(frame.arena, self._items.items.len);
- for (self._items.items) |point| {
- point._proto.detach(self);
- self._retired.appendAssumeCapacity(point);
- }
- self._items.clearRetainingCapacity();
-}
-
-fn setAttribute(self: *PointList, items: []const *DOMPoint, frame: *Frame) !void {
- var serialized: std.Io.Writer.Allocating = .init(frame.local_arena);
- const writer = &serialized.writer;
- for (items, 0..) |point, i| {
- if (i != 0) try writer.writeByte(' ');
- try writer.print("{d} {d}", .{ point._proto._x, point._proto._y });
- }
- try self.commitAttribute(serialized.written(), frame);
+fn writeItem(point: *DOMPoint, writer: *std.Io.Writer) !void {
+ try writer.print("{d} {d}", .{ point._proto._x, point._proto._y });
}
fn setAttributeWithOverride(
@@ -311,15 +183,7 @@ fn setAttributeWithOverride(
const y = if (i == index and coordinate == .y) value else point._proto._y;
try writer.print("{d} {d}", .{ x, y });
}
- try self.commitAttribute(serialized.written(), frame);
-}
-
-fn commitAttribute(self: *PointList, serialized: []const u8, frame: *Frame) !void {
- self._synced = false;
- try self._element.setAttributeSafe(comptime .wrap("points"), .wrap(serialized), frame);
- self._snapshot.clearRetainingCapacity();
- try self._snapshot.appendSlice(frame.arena, serialized);
- self._synced = true;
+ try M.commitAttribute(self, serialized.written(), frame);
}
const NumberScanner = struct {
diff --git a/src/browser/webapi/svg/TransformList.zig b/src/browser/webapi/svg/TransformList.zig
index 15a8ae9a5..d79f29743 100644
--- a/src/browser/webapi/svg/TransformList.zig
+++ b/src/browser/webapi/svg/TransformList.zig
@@ -25,6 +25,7 @@ const Page = @import("../../Page.zig");
const DOMMatrixReadOnly = @import("../DOMMatrixReadOnly.zig");
const Element = @import("../Element.zig");
const Transform = @import("Transform.zig");
+const reflected_list = @import("reflected_list.zig");
const TransformList = @This();
@@ -37,6 +38,16 @@ _snapshot: std.ArrayList(u8) = .empty,
_items: std.ArrayList(*Transform) = .empty,
_retired: std.ArrayList(*Transform) = .empty,
+const M = reflected_list.Mixin(TransformList, Transform, .{
+ .attrName = attrName,
+ .parse = parse,
+ .writeItem = writeItem,
+ .prepareItem = prepareItem,
+ .attach = attach,
+ .detachItem = detachItem,
+ .releaseItem = releaseItem,
+});
+
pub fn createForAttribute(element: *Element, attr_name: lp.String, read_only: bool, frame: *Frame) !*TransformList {
return frame._factory.create(TransformList{
._frame = frame,
@@ -46,111 +57,20 @@ pub fn createForAttribute(element: *Element, attr_name: lp.String, read_only: bo
});
}
-pub fn deinit(self: *TransformList, page: *Page) void {
- for (self._items.items) |transform| {
- transform.detach(self);
- transform.releaseRef(page);
- }
- self._items.clearRetainingCapacity();
- self.releaseRetired(page);
-}
-
-pub fn getLength(self: *TransformList, frame: *Frame) !u32 {
- try self.sync(frame);
- return @intCast(self._items.items.len);
-}
-
-pub fn getNumberOfItems(self: *TransformList, frame: *Frame) !u32 {
- return self.getLength(frame);
-}
-
-pub fn clear(self: *TransformList, frame: *Frame) !void {
- try self.requireMutable();
- try self.sync(frame);
- try self.retireAll(frame);
- try self.setAttribute(&.{}, frame);
-}
-
-pub fn initialize(self: *TransformList, item: *Transform, frame: *Frame) !*Transform {
- try self.requireMutable();
- try self.sync(frame);
- const prepared = try self.prepareItem(item, frame);
- errdefer prepared.releaseRef(frame._page);
-
- try self.retireAll(frame);
- try self._items.ensureTotalCapacity(frame.arena, 1);
- try self.setAttribute(&.{prepared}, frame);
- self._items.appendAssumeCapacity(prepared);
- self.attach(prepared);
- return prepared;
-}
-
-pub fn getItem(self: *TransformList, index: u32, frame: *Frame) !*Transform {
- try self.sync(frame);
- if (index >= self._items.items.len) return error.IndexSizeError;
- return self._items.items[index];
-}
-
-pub fn insertItemBefore(self: *TransformList, item: *Transform, index: u32, frame: *Frame) !*Transform {
- try self.requireMutable();
- try self.sync(frame);
- const prepared = try self.prepareItem(item, frame);
- errdefer prepared.releaseRef(frame._page);
- const at = @min(@as(usize, index), self._items.items.len);
- const next = try frame.local_arena.alloc(*Transform, self._items.items.len + 1);
- @memcpy(next[0..at], self._items.items[0..at]);
- next[at] = prepared;
- @memcpy(next[at + 1 ..], self._items.items[at..]);
-
- try self._items.ensureUnusedCapacity(frame.arena, 1);
- try self.setAttribute(next, frame);
- self._items.insertAssumeCapacity(at, prepared);
- self.attach(prepared);
- return prepared;
-}
-
-pub fn replaceItem(self: *TransformList, item: *Transform, index: u32, frame: *Frame) !*Transform {
- try self.requireMutable();
- try self.sync(frame);
- if (index >= self._items.items.len) return error.IndexSizeError;
- const prepared = try self.prepareItem(item, frame);
- errdefer prepared.releaseRef(frame._page);
- const next = try frame.local_arena.dupe(*Transform, self._items.items);
- next[index] = prepared;
-
- try self._retired.ensureUnusedCapacity(frame.arena, 1);
- try self.setAttribute(next, frame);
- const replaced = self._items.items[index];
- replaced.detach(self);
- self._retired.appendAssumeCapacity(replaced);
- self._items.items[index] = prepared;
- self.attach(prepared);
- return prepared;
-}
-
-pub fn removeItem(self: *TransformList, index: u32, frame: *Frame) !*Transform {
- try self.requireMutable();
- try self.sync(frame);
- if (index >= self._items.items.len) return error.IndexSizeError;
- const next = try frame.local_arena.alloc(*Transform, self._items.items.len - 1);
- @memcpy(next[0..index], self._items.items[0..index]);
- @memcpy(next[index..], self._items.items[index + 1 ..]);
-
- try self._retired.ensureUnusedCapacity(frame.arena, 1);
- try self.setAttribute(next, frame);
- const removed = self._items.orderedRemove(index);
- removed.detach(self);
- self._retired.appendAssumeCapacity(removed);
- return removed;
-}
-
-pub fn appendItem(self: *TransformList, item: *Transform, frame: *Frame) !*Transform {
- return self.insertItemBefore(item, std.math.maxInt(u32), frame);
-}
+pub const deinit = M.deinit;
+pub const getLength = M.getLength;
+pub const getNumberOfItems = M.getNumberOfItems;
+pub const clear = M.clear;
+pub const initialize = M.initialize;
+pub const getItem = M.getItem;
+pub const insertItemBefore = M.insertItemBefore;
+pub const replaceItem = M.replaceItem;
+pub const removeItem = M.removeItem;
+pub const appendItem = M.appendItem;
pub fn consolidate(self: *TransformList, frame: *Frame) !?*Transform {
- try self.requireMutable();
- try self.sync(frame);
+ try M.requireMutable(self);
+ try M.sync(self, frame);
if (self._items.items.len == 0) return null;
var matrix = DOMMatrixReadOnly.identity();
@@ -173,16 +93,16 @@ pub fn consolidate(self: *TransformList, frame: *Frame) !?*Transform {
consolidated.acquireRef();
errdefer consolidated.releaseRef(frame._page);
- try self.retireAll(frame);
+ try M.retireAll(self, frame);
try self._items.ensureTotalCapacity(frame.arena, 1);
- try self.setAttribute(&.{consolidated}, frame);
+ try M.setAttribute(self, &.{consolidated}, frame);
self._items.appendAssumeCapacity(consolidated);
- self.attach(consolidated);
+ attach(self, consolidated);
return consolidated;
}
-fn requireMutable(self: *const TransformList) !void {
- if (self._read_only) return error.NoModificationAllowed;
+fn attrName(self: *const TransformList) lp.String {
+ return self._attr_name;
}
fn prepareItem(_: *TransformList, item: *Transform, frame: *Frame) !*Transform {
@@ -198,10 +118,18 @@ fn attach(self: *TransformList, transform: *Transform) void {
}, self._read_only);
}
+fn detachItem(transform: *Transform, owner: *TransformList) void {
+ transform.detach(owner);
+}
+
+fn releaseItem(transform: *Transform, page: *Page) void {
+ transform.releaseRef(page);
+}
+
fn mutateTransform(context: *anyopaque, transform: *Transform, state: Transform.State) anyerror!void {
const self: *TransformList = @ptrCast(@alignCast(context));
const frame = self._frame;
- try self.sync(frame);
+ try M.sync(self, frame);
if (!transform.isAttachedTo(self)) {
transform.applyStateRaw(state);
return;
@@ -213,41 +141,6 @@ fn mutateTransform(context: *anyopaque, transform: *Transform, state: Transform.
transform.applyStateRaw(state);
}
-fn sync(self: *TransformList, frame: *Frame) !void {
- self.releaseRetired(frame._page);
-
- const raw = self._element.getAttributeSafe(self._attr_name) orelse "";
- if (self._synced and std.mem.eql(u8, self._snapshot.items, raw)) return;
-
- self._synced = false;
- var parsed = parse(raw, frame) catch |err| switch (err) {
- error.SyntaxError => std.ArrayList(*Transform).empty,
- else => return err,
- };
- errdefer for (parsed.items) |transform| transform.releaseRef(frame._page);
-
- self._snapshot.clearRetainingCapacity();
- try self._snapshot.appendSlice(frame.arena, raw);
- try self.retireAll(frame);
- try self._items.ensureTotalCapacity(frame.arena, parsed.items.len);
- for (parsed.items) |transform| {
- self._items.appendAssumeCapacity(transform);
- self.attach(transform);
- }
- parsed.clearRetainingCapacity();
- self._synced = true;
-}
-
-// A retired item must outlive the operation that retired it: removeItem's
-// return value has no JS wrapper until the bridge wraps it after we return.
-// By the next operation, anything still reachable holds its own ref.
-fn releaseRetired(self: *TransformList, page: *Page) void {
- for (self._retired.items) |transform| {
- transform.releaseRef(page);
- }
- self._retired.clearRetainingCapacity();
-}
-
fn parse(raw: []const u8, frame: *Frame) !std.ArrayList(*Transform) {
var parsed: std.ArrayList(*Transform) = .empty;
errdefer for (parsed.items) |transform| transform.releaseRef(frame._page);
@@ -267,24 +160,8 @@ fn parse(raw: []const u8, frame: *Frame) !std.ArrayList(*Transform) {
return parsed;
}
-fn retireAll(self: *TransformList, frame: *Frame) !void {
- self._synced = false;
- try self._retired.ensureUnusedCapacity(frame.arena, self._items.items.len);
- for (self._items.items) |transform| {
- transform.detach(self);
- self._retired.appendAssumeCapacity(transform);
- }
- self._items.clearRetainingCapacity();
-}
-
-fn setAttribute(self: *TransformList, items: []const *Transform, frame: *Frame) !void {
- var serialized: std.Io.Writer.Allocating = .init(frame.local_arena);
- const writer = &serialized.writer;
- for (items, 0..) |transform, i| {
- if (i != 0) try writer.writeByte(' ');
- try Transform.writeState(transform.getState(), writer);
- }
- try self.commitAttribute(serialized.written(), frame);
+fn writeItem(transform: *Transform, writer: *std.Io.Writer) !void {
+ try Transform.writeState(transform.getState(), writer);
}
fn setAttributeWithOverride(self: *TransformList, index: usize, state: Transform.State, frame: *Frame) !void {
@@ -294,15 +171,7 @@ fn setAttributeWithOverride(self: *TransformList, index: usize, state: Transform
if (i != 0) try writer.writeByte(' ');
try Transform.writeState(if (i == index) state else transform.getState(), writer);
}
- try self.commitAttribute(serialized.written(), frame);
-}
-
-fn commitAttribute(self: *TransformList, serialized: []const u8, frame: *Frame) !void {
- self._synced = false;
- try self._element.setAttributeSafe(self._attr_name, .wrap(serialized), frame);
- self._snapshot.clearRetainingCapacity();
- try self._snapshot.appendSlice(frame.arena, serialized);
- self._synced = true;
+ try M.commitAttribute(self, serialized.written(), frame);
}
pub const JsApi = struct {
diff --git a/src/browser/webapi/svg/reflected_list.zig b/src/browser/webapi/svg/reflected_list.zig
new file mode 100644
index 000000000..9138ccdc4
--- /dev/null
+++ b/src/browser/webapi/svg/reflected_list.zig
@@ -0,0 +1,226 @@
+// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+//! Shared machinery for the attribute-reflecting SVG list interfaces whose
+//! items are live, ref-counted objects (SVGPointList, SVGTransformList):
+//! attribute-snapshot sync, the retire/release item lifetime protocol, and
+//! the DOM list operations. The concrete list supplies the item specifics
+//! via `hooks` and re-exports the operations as decl aliases.
+//!
+//! StringList reflects an attribute too but is deliberately not built on
+//! this: its items are plain slices (no ref counting, no attach/retire) and
+//! its commit path is list-authoritative rather than reparse-based.
+//!
+//! `List` must carry the `_frame`, `_element`, `_read_only`, `_synced`,
+//! `_snapshot`, `_items` and `_retired` fields. `hooks` must provide:
+//! attrName(*const List) String
+//! parse([]const u8, *Frame) !std.ArrayList(*Item) (SyntaxError -> empty list)
+//! writeItem(*Item, *std.Io.Writer) !void
+//! prepareItem(*List, *Item, *Frame) !*Item
+//! attach(*List, *Item) void
+//! detachItem(*Item, *List) void
+//! releaseItem(*Item, *Page) void
+//!
+//! The operations keep a strict commit order: reserve list capacity, write
+//! the attribute, then apply the infallible list update — so a failed
+//! setAttribute never leaves the mirror out of step with the attribute.
+
+const std = @import("std");
+
+const Frame = @import("../../Frame.zig");
+const Page = @import("../../Page.zig");
+
+pub fn Mixin(comptime List: type, comptime Item: type, comptime hooks: anytype) type {
+ return struct {
+ pub fn deinit(self: *List, page: *Page) void {
+ for (self._items.items) |item| {
+ hooks.detachItem(item, self);
+ hooks.releaseItem(item, page);
+ }
+ self._items.clearRetainingCapacity();
+ releaseRetired(self, page);
+ }
+
+ pub fn getLength(self: *List, frame: *Frame) !u32 {
+ try sync(self, frame);
+ return @intCast(self._items.items.len);
+ }
+
+ pub fn getNumberOfItems(self: *List, frame: *Frame) !u32 {
+ return getLength(self, frame);
+ }
+
+ pub fn clear(self: *List, frame: *Frame) !void {
+ try requireMutable(self);
+ try sync(self, frame);
+ try retireAll(self, frame);
+ try setAttribute(self, &.{}, frame);
+ }
+
+ pub fn initialize(self: *List, item: *Item, frame: *Frame) !*Item {
+ try requireMutable(self);
+ try sync(self, frame);
+
+ const prepared = try hooks.prepareItem(self, item, frame);
+ errdefer hooks.releaseItem(prepared, frame._page);
+
+ try retireAll(self, frame);
+ try self._items.ensureTotalCapacity(frame.arena, 1);
+ try setAttribute(self, &.{prepared}, frame);
+ self._items.appendAssumeCapacity(prepared);
+ hooks.attach(self, prepared);
+ return prepared;
+ }
+
+ pub fn getItem(self: *List, index: u32, frame: *Frame) !*Item {
+ try sync(self, frame);
+ if (index >= self._items.items.len) return error.IndexSizeError;
+ return self._items.items[index];
+ }
+
+ pub fn insertItemBefore(self: *List, item: *Item, index: u32, frame: *Frame) !*Item {
+ try requireMutable(self);
+ try sync(self, frame);
+
+ const prepared = try hooks.prepareItem(self, item, frame);
+ errdefer hooks.releaseItem(prepared, frame._page);
+ const at = @min(@as(usize, index), self._items.items.len);
+ const next = try frame.local_arena.alloc(*Item, self._items.items.len + 1);
+ @memcpy(next[0..at], self._items.items[0..at]);
+ next[at] = prepared;
+ @memcpy(next[at + 1 ..], self._items.items[at..]);
+
+ try self._items.ensureUnusedCapacity(frame.arena, 1);
+ try setAttribute(self, next, frame);
+ self._items.insertAssumeCapacity(at, prepared);
+ hooks.attach(self, prepared);
+ return prepared;
+ }
+
+ pub fn replaceItem(self: *List, item: *Item, index: u32, frame: *Frame) !*Item {
+ try requireMutable(self);
+ try sync(self, frame);
+ if (index >= self._items.items.len) return error.IndexSizeError;
+
+ const prepared = try hooks.prepareItem(self, item, frame);
+ errdefer hooks.releaseItem(prepared, frame._page);
+ const next = try frame.local_arena.dupe(*Item, self._items.items);
+ next[index] = prepared;
+
+ try self._retired.ensureUnusedCapacity(frame.arena, 1);
+ try setAttribute(self, next, frame);
+ const replaced = self._items.items[index];
+ hooks.detachItem(replaced, self);
+ self._retired.appendAssumeCapacity(replaced);
+ self._items.items[index] = prepared;
+ hooks.attach(self, prepared);
+ return prepared;
+ }
+
+ pub fn removeItem(self: *List, index: u32, frame: *Frame) !*Item {
+ try requireMutable(self);
+ try sync(self, frame);
+ if (index >= self._items.items.len) return error.IndexSizeError;
+
+ const next = try frame.local_arena.alloc(*Item, self._items.items.len - 1);
+ @memcpy(next[0..index], self._items.items[0..index]);
+ @memcpy(next[index..], self._items.items[index + 1 ..]);
+
+ try self._retired.ensureUnusedCapacity(frame.arena, 1);
+ try setAttribute(self, next, frame);
+ const removed = self._items.orderedRemove(index);
+ hooks.detachItem(removed, self);
+ self._retired.appendAssumeCapacity(removed);
+ return removed;
+ }
+
+ pub fn appendItem(self: *List, item: *Item, frame: *Frame) !*Item {
+ return insertItemBefore(self, item, std.math.maxInt(u32), frame);
+ }
+
+ pub fn requireMutable(self: *const List) !void {
+ if (self._read_only) return error.NoModificationAllowed;
+ }
+
+ pub fn sync(self: *List, frame: *Frame) !void {
+ releaseRetired(self, frame._page);
+
+ const raw = self._element.getAttributeSafe(hooks.attrName(self)) orelse "";
+ if (self._synced and std.mem.eql(u8, self._snapshot.items, raw)) {
+ return;
+ }
+
+ self._synced = false;
+ var parsed = hooks.parse(raw, frame) catch |err| switch (err) {
+ error.SyntaxError => std.ArrayList(*Item).empty,
+ else => return err,
+ };
+ errdefer for (parsed.items) |item| hooks.releaseItem(item, frame._page);
+
+ self._snapshot.clearRetainingCapacity();
+ try self._snapshot.appendSlice(frame.arena, raw);
+ try retireAll(self, frame);
+ try self._items.ensureTotalCapacity(frame.arena, parsed.items.len);
+ for (parsed.items) |item| {
+ self._items.appendAssumeCapacity(item);
+ hooks.attach(self, item);
+ }
+ parsed.clearRetainingCapacity();
+ self._synced = true;
+ }
+
+ // A retired item must outlive the operation that retired it:
+ // removeItem's return value has no JS wrapper until the bridge wraps
+ // it after we return. By the next operation, anything still reachable
+ // holds its own ref.
+ pub fn releaseRetired(self: *List, page: *Page) void {
+ for (self._retired.items) |item| {
+ hooks.releaseItem(item, page);
+ }
+ self._retired.clearRetainingCapacity();
+ }
+
+ pub fn retireAll(self: *List, frame: *Frame) !void {
+ self._synced = false;
+ try self._retired.ensureUnusedCapacity(frame.arena, self._items.items.len);
+ for (self._items.items) |item| {
+ hooks.detachItem(item, self);
+ self._retired.appendAssumeCapacity(item);
+ }
+ self._items.clearRetainingCapacity();
+ }
+
+ pub fn setAttribute(self: *List, items: []const *Item, frame: *Frame) !void {
+ var serialized: std.Io.Writer.Allocating = .init(frame.local_arena);
+ const writer = &serialized.writer;
+ for (items, 0..) |item, i| {
+ if (i != 0) try writer.writeByte(' ');
+ try hooks.writeItem(item, writer);
+ }
+ try commitAttribute(self, serialized.written(), frame);
+ }
+
+ pub fn commitAttribute(self: *List, serialized: []const u8, frame: *Frame) !void {
+ self._synced = false;
+ try self._element.setAttributeSafe(hooks.attrName(self), .wrap(serialized), frame);
+ self._snapshot.clearRetainingCapacity();
+ try self._snapshot.appendSlice(frame.arena, serialized);
+ self._synced = true;
+ }
+ };
+}