feat(webapi): implement W3C File API

This commit is contained in:
Armaan Sandhu
2026-05-24 13:04:33 +05:30
parent 93d2e9689b
commit a7e3bea672
3 changed files with 102 additions and 12 deletions

View File

@@ -1,12 +1,52 @@
<!DOCTYPE html>
<head id="the_head">
<title>Test Document Title</title>
<title>Test File Web API</title>
<script src="./testing.js"></script>
</head>
<script id=file>
const file = new File();
<script id=file_basic>
// File requires parts and name.
// Verify basic construction.
const f = new File(["test data"], "test.txt");
testing.expectEqual(true, file instanceof File);
testing.expectEqual(true, file instanceof Blob);
testing.expectEqual(true, f instanceof File);
testing.expectEqual(true, f instanceof Blob);
testing.expectEqual("test.txt", f.name);
testing.expectEqual(9, f.size);
testing.expectEqual("", f.type);
testing.expectTrue(typeof f.lastModified === 'number');
// lastModified should be close to now (within 5 seconds)
const now = Date.now();
testing.expectTrue(Math.abs(now - f.lastModified) < 5000);
</script>
<script id=file_options type=module>
const state = await testing.async();
// Constructor with full option properties
const customTime = 1234567890;
const f2 = new File(["foo", "bar"], "data.csv", {
type: "text/csv",
lastModified: customTime
});
testing.expectEqual("data.csv", f2.name);
testing.expectEqual(6, f2.size);
testing.expectEqual("text/csv", f2.type);
testing.expectEqual(customTime, f2.lastModified);
// Verify async reader methods (inherited from Blob)
const text = await f2.text();
state.resolve();
await state.done(() => {
testing.expectEqual("foobar", text);
});
</script>
<script id=file_empty>
const fEmpty = new File([], "");
testing.expectEqual("", fEmpty.name);
testing.expectEqual(0, fEmpty.size);
testing.expectEqual("", fEmpty.type);
</script>

View File

@@ -113,7 +113,7 @@ pub fn initFromBytes(data: []const u8, content_type: []const u8, validate_mime:
}
/// Validates and normalizes MIME type according to spec.
fn validateMimeType(arena: Allocator, mime_type: []const u8, full_validation: bool) ![]const u8 {
pub fn validateMimeType(arena: Allocator, mime_type: []const u8, full_validation: bool) ![]const u8 {
if (mime_type.len == 0) {
return "";
}
@@ -171,7 +171,7 @@ const vector_sizes = blk: {
};
/// Writes a single part with optional line ending normalization.
fn writePartWithEndings(part: []const u8, use_native_endings: bool, writer: *Writer) !void {
pub fn writePartWithEndings(part: []const u8, use_native_endings: bool, writer: *Writer) !void {
// Transparent - no conversion needed.
if (!use_native_endings) {
try writer.writeAll(part);

View File

@@ -1,4 +1,4 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
@@ -27,13 +27,61 @@ const Blob = @import("Blob.zig");
const File = @This();
_proto: *Blob,
_name: []const u8,
_last_modified: i64,
// TODO: Implement File API.
pub fn init(page: *Page) !*File {
pub const InitOptions = struct {
type: []const u8 = "",
endings: []const u8 = "transparent",
lastModified: ?i64 = null,
};
pub fn init(
parts_: ?[]const js.Value,
name_: []const u8,
opts_: ?InitOptions,
page: *Page,
) !*File {
const session = page.session;
const arena = try session.getArena(.tiny, "File");
const arena = try session.getArena(.large, "File");
errdefer session.releaseArena(arena);
return page.factory.blob(arena, File{ ._proto = undefined });
const opts = opts_ orelse InitOptions{};
const mime = try Blob.validateMimeType(arena, opts.type, false);
const data = blk: {
if (parts_) |blob_parts| {
const use_native_endings = std.mem.eql(u8, opts.endings, "native");
var w: std.Io.Writer.Allocating = .init(arena);
for (blob_parts) |js_val| {
const part = try js_val.toStringSmart();
try Blob.writePartWithEndings(part, use_native_endings, &w.writer);
}
break :blk w.written();
}
break :blk "";
};
const last_modified = opts.lastModified orelse std.time.milliTimestamp();
const file = try page.factory.blob(arena, File{
._proto = undefined,
._name = try arena.dupe(u8, name_),
._last_modified = last_modified,
});
file._proto._slice = data;
file._proto._mime = mime;
return file;
}
pub fn getName(self: *const File) []const u8 {
return self._name;
}
pub fn getLastModified(self: *const File) f64 {
return @floatFromInt(self._last_modified);
}
pub const JsApi = struct {
@@ -46,6 +94,8 @@ pub const JsApi = struct {
};
pub const constructor = bridge.constructor(File.init, .{});
pub const name = bridge.accessor(File.getName, null, .{});
pub const lastModified = bridge.accessor(File.getLastModified, null, .{});
};
const testing = @import("../../testing.zig");