Add js.NullableString

When a WebAPI takes `[]const u8`, we coerce values to strings. But when it
takes a `?[]const u8` how should we handle `null`?  Some APIs might want to know
that it was null, others might just want `"null``.

Currently when `null` is passed to `?[]const u8`, we'll get null.

This adds a discriminator type, js.NullableString. When `null` is passed to it
it'll be converted to `"null"`.
This commit is contained in:
Karl Seguin
2026-02-20 07:24:43 +08:00
parent 1b369489df
commit 9d60142828
8 changed files with 49 additions and 13 deletions

View File

@@ -453,6 +453,13 @@ pub fn jsValueToZig(self: *const Local, comptime T: type, js_val: js.Value) !T {
return js_val; return js_val;
} }
if (comptime o.child == js.NullableString) {
if (js_val.isUndefined()) {
return null;
}
return .{ .value = try js_val.toStringSlice() };
}
if (comptime o.child == js.Object) { if (comptime o.child == js.Object) {
return js.Object{ return js.Object{
.local = self, .local = self,

View File

@@ -168,6 +168,16 @@ pub fn ArrayBufferRef(comptime kind: ArrayType) type {
}; };
} }
// If a WebAPI takes a []const u8, then we'll coerce any JS value to that string
// so null -> "null". But if a WebAPI takes an optional string, ?[]const u8,
// how should we handle null? If the parameter _isn't_ passed, then it's obvious
// that it should be null, but what if `null` is passed? It's ambiguous, should
// that be null, or "null"? It could depend on the api. So, `null` passed to
// ?[]const u8 will be `null`. If you want it to be "null", use a `.js.NullableString`.
pub const NullableString = struct {
value: []const u8,
};
pub const Exception = struct { pub const Exception = struct {
local: *const Local, local: *const Local,
handle: *const v8.Value, handle: *const v8.Value,

View File

@@ -4,4 +4,6 @@
<script id=comment> <script id=comment>
testing.expectEqual('', new Comment().data); testing.expectEqual('', new Comment().data);
testing.expectEqual('over 9000! ', new Comment('over 9000! ').data); testing.expectEqual('over 9000! ', new Comment('over 9000! ').data);
testing.expectEqual('null', new Comment(null).data);
</script> </script>

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<a id="link" href="foo" class="ok">OK</a> <a id="link" href="foo" class="ok">OK</a>
<script src="../../testing.js"></script> <script src="../testing.js"></script>
<script id=text> <script id=text>
let t = new Text('foo'); let t = new Text('foo');
testing.expectEqual('foo', t.data); testing.expectEqual('foo', t.data);
@@ -16,4 +16,7 @@
let split = text.splitText('OK'.length); let split = text.splitText('OK'.length);
testing.expectEqual(' modified', split.data); testing.expectEqual(' modified', split.data);
testing.expectEqual('OK', text.data); testing.expectEqual('OK', text.data);
let x = new Text(null);
testing.expectEqual("null", x.data);
</script> </script>

View File

@@ -108,6 +108,20 @@
} }
</script> </script>
<script id=createHTMLDocument_nulll_title>
{
const impl = document.implementation;
const doc = impl.createHTMLDocument(null);
testing.expectEqual('null', doc.title);
// Should have title element in head
const titleElement = doc.head.querySelector('title');
testing.expectEqual(true, titleElement !== null);
testing.expectEqual('null', titleElement.textContent);
}
</script>
<script id=createHTMLDocument_structure> <script id=createHTMLDocument_structure>
{ {
const impl = document.implementation; const impl = document.implementation;

View File

@@ -31,7 +31,7 @@ pub fn createDocumentType(_: *const DOMImplementation, qualified_name: []const u
return DocumentType.init(qualified_name, public_id, system_id, page); return DocumentType.init(qualified_name, public_id, system_id, page);
} }
pub fn createHTMLDocument(_: *const DOMImplementation, title: ?[]const u8, page: *Page) !*Document { pub fn createHTMLDocument(_: *const DOMImplementation, title: ?js.NullableString, page: *Page) !*Document {
const document = (try page._factory.document(Node.Document.HTMLDocument{ ._proto = undefined })).asDocument(); const document = (try page._factory.document(Node.Document.HTMLDocument{ ._proto = undefined })).asDocument();
document._ready_state = .complete; document._ready_state = .complete;
document._url = "about:blank"; document._url = "about:blank";
@@ -55,7 +55,7 @@ pub fn createHTMLDocument(_: *const DOMImplementation, title: ?[]const u8, page:
if (title) |t| { if (title) |t| {
const title_node = try page.createElementNS(.html, "title", null); const title_node = try page.createElementNS(.html, "title", null);
_ = try head_node.appendChild(title_node, page); _ = try head_node.appendChild(title_node, page);
const text_node = try page.createTextNode(t); const text_node = try page.createTextNode(t.value);
_ = try title_node.appendChild(text_node, page); _ = try title_node.appendChild(text_node, page);
} }

View File

@@ -25,8 +25,8 @@ const Comment = @This();
_proto: *CData, _proto: *CData,
pub fn init(content: ?[]const u8, page: *Page) !*Comment { pub fn init(str: ?js.NullableString, page: *Page) !*Comment {
const node = try page.createComment(content orelse ""); const node = try page.createComment(if (str) |s| s.value else "");
return node.as(Comment); return node.as(Comment);
} }
@@ -42,3 +42,8 @@ pub const JsApi = struct {
pub const constructor = bridge.constructor(Comment.init, .{}); pub const constructor = bridge.constructor(Comment.init, .{});
}; };
const testing = @import("../../../testing.zig");
test "WebApi: CData.Text" {
try testing.htmlRunner("cdata/comment.html", .{});
}

View File

@@ -16,6 +16,7 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const CData = @import("../CData.zig"); const CData = @import("../CData.zig");
@@ -23,8 +24,8 @@ const Text = @This();
_proto: *CData, _proto: *CData,
pub fn init(str: ?[]const u8, page: *Page) !*Text { pub fn init(str: ?js.NullableString, page: *Page) !*Text {
const node = try page.createTextNode(str orelse ""); const node = try page.createTextNode(if (str) |s| s.value else "");
return node.as(Text); return node.as(Text);
} }
@@ -56,13 +57,7 @@ pub fn splitText(self: *Text, offset: usize, page: *Page) !*Text {
return new_text; return new_text;
} }
const testing = @import("../../../testing.zig");
test "WebApi: CData.Text" {
try testing.htmlRunner("cdata/text", .{});
}
pub const JsApi = struct { pub const JsApi = struct {
const js = @import("../../js/js.zig");
pub const bridge = js.Bridge(Text); pub const bridge = js.Bridge(Text);
pub const Meta = struct { pub const Meta = struct {