mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 01:25:53 -04:00
feat(webapi): implement W3C File API
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user