Merge pull request #1267 from lightpanda-io/blob

port remaining blob functionality
This commit is contained in:
Karl Seguin
2025-12-11 15:31:11 +08:00
committed by GitHub
3 changed files with 105 additions and 68 deletions

View File

@@ -196,8 +196,8 @@ pub fn blob(self: *Factory, child: anytype) !*@TypeOf(child) {
const blob_ptr = chain.get(0); const blob_ptr = chain.get(0);
blob_ptr.* = .{ blob_ptr.* = .{
._type = unionInit(Blob.Type, chain.get(1)), ._type = unionInit(Blob.Type, chain.get(1)),
.slice = "", ._slice = "",
.mime = "", ._mime = "",
}; };
chain.setLeaf(1, child); chain.setLeaf(1, child);

View File

@@ -1,8 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<head id="the_head"> <meta charset="UTF-8">
<title>Test Document Title</title> <script src="./testing.js"></script>
<script src="./testing.js"></script>
</head>
<script id=basic> <script id=basic>
{ {
@@ -13,7 +11,7 @@
const expected = parts.join(""); const expected = parts.join("");
testing.expectEqual(expected.length, blob.size); testing.expectEqual(expected.length, blob.size);
testing.expectEqual("text/html", blob.type); testing.expectEqual("text/html", blob.type);
testing.async(blob.text(), result => testing.expectEqual(expected, result)); testing.async(async () => { testing.expectEqual(expected, await blob.text()) });
} }
{ {
@@ -23,34 +21,7 @@
const expected = "\nhello\n\nwor\nld"; const expected = "\nhello\n\nwor\nld";
testing.expectEqual(expected.length, blob.size); testing.expectEqual(expected.length, blob.size);
testing.async(blob.text(), result => testing.expectEqual(expected, result)); testing.async(async () => { testing.expectEqual(expected, await blob.text()) });
}
</script>
<script id=slice>
{
const parts = ["la", "symphonie", "des", "éclairs"];
const blob = new Blob(parts);
let temp = blob.slice(0);
testing.expectEqual(blob.size, temp.size);
testing.async(temp.text(), result => {
testing.expectEqual("lasymphoniedeséclairs", result);
});
temp = blob.slice(-4, -2, "custom");
testing.expectEqual(2, temp.size);
testing.expectEqual("custom", temp.type);
testing.async(temp.text(), result => testing.expectEqual("ai", result));
temp = blob.slice(14);
testing.expectEqual(8, temp.size);
testing.async(temp.text(), result => testing.expectEqual("éclairs", result));
temp = blob.slice(6, -10, "text/eclair");
testing.expectEqual(6, temp.size);
testing.expectEqual("text/eclair", temp.type);
testing.async(temp.text(), result => testing.expectEqual("honied", result));
} }
</script> </script>
@@ -60,10 +31,11 @@
const parts = ["light ", "panda ", "rocks ", "!"]; const parts = ["light ", "panda ", "rocks ", "!"];
const blob = new Blob(parts); const blob = new Blob(parts);
testing.async(blob.bytes(), result => { testing.async(async() => {
const expected = new Uint8Array([108, 105, 103, 104, 116, 32, 112, 97, const expected = new Uint8Array([108, 105, 103, 104, 116, 32, 112, 97,
110, 100, 97, 32, 114, 111, 99, 107, 115, 110, 100, 97, 32, 114, 111, 99, 107, 115,
32, 33]); 32, 33]);
const result = await blob.bytes();
testing.expectEqual(true, result instanceof Uint8Array); testing.expectEqual(true, result instanceof Uint8Array);
testing.expectEqual(expected, result); testing.expectEqual(expected, result);
}); });
@@ -81,7 +53,7 @@
const blob = new Blob(parts, { type: "text/html", endings: "native" }); const blob = new Blob(parts, { type: "text/html", endings: "native" });
testing.expectEqual(161, blob.size); testing.expectEqual(161, blob.size);
testing.expectEqual("text/html", blob.type); testing.expectEqual("text/html", blob.type);
testing.async(blob.bytes(), result => { testing.async(async() => {
const expected = new Uint8Array([10, 84, 104, 101, 32, 111, 112, 101, 110, const expected = new Uint8Array([10, 84, 104, 101, 32, 111, 112, 101, 110,
101, 100, 32, 112, 97, 99, 107, 97, 103, 101, 100, 32, 112, 97, 99, 107, 97, 103,
101, 10, 111, 102, 32, 112, 111, 116, 97, 101, 10, 111, 102, 32, 112, 111, 116, 97,
@@ -100,8 +72,65 @@
10, 10, 116, 111, 32, 115, 111, 108, 118, 101, 10, 10, 116, 111, 32, 115, 111, 108, 118, 101,
32, 116, 104, 101, 32, 10, 99, 114, 105, 109, 32, 116, 104, 101, 32, 10, 99, 114, 105, 109,
101, 46, 10]); 101, 46, 10]);
const result = await blob.bytes();
testing.expectEqual(true, result instanceof Uint8Array); testing.expectEqual(true, result instanceof Uint8Array);
testing.expectEqual(expected, result); testing.expectEqual(expected, result);
}); });
} }
</script> </script>
<script id=stream>
{
const parts = ["may", "thy", "knife", "chip", "and", "shatter"];
const blob = new Blob(parts);
const reader = blob.stream().getReader();
testing.async(async () => {
const {done: done, value: value} = await reader.read()
const expected = new Uint8Array([109, 97, 121, 116, 104, 121, 107, 110,
105, 102, 101, 99, 104, 105, 112, 97,
110, 100, 115, 104, 97, 116, 116, 101,
114]);
testing.expectEqual(false, done);
testing.expectEqual(true, value instanceof Uint8Array);
testing.expectEqual(expected, value);
});
}
</script>
<script id=slice>
{
const parts = ["la", "symphonie", "des", "éclairs"];
const blob = new Blob(parts);
testing.async(async () => {
const result = await blob.arrayBuffer();
testing.expectEqual(true, result instanceof ArrayBuffer)
});
let temp = blob.slice(0);
testing.expectEqual(blob.size, temp.size);
testing.async(async () => {
testing.expectEqual("lasymphoniedeséclairs", await temp.text());
});
temp = blob.slice(-4, -2, "custom");
testing.expectEqual(2, temp.size);
testing.expectEqual("custom", temp.type);
testing.async(async () => {
testing.expectEqual("ai", await temp.text());
});
temp = blob.slice(14);
testing.expectEqual(8, temp.size);
testing.async(async () => {
testing.expectEqual("éclairs", await temp.text());
});
temp = blob.slice(6, -10, "text/eclair");
testing.expectEqual(6, temp.size);
testing.expectEqual("text/eclair", temp.type);
testing.async(async () => {
testing.expectEqual("honied", await temp.text());
});
}
</script>

View File

@@ -27,14 +27,15 @@ const Page = @import("../Page.zig");
const Blob = @This(); const Blob = @This();
const _prototype_root = true; const _prototype_root = true;
_type: Type, _type: Type,
/// Immutable slice of blob. /// Immutable slice of blob.
/// Note that another blob may hold a pointer/slice to this, /// Note that another blob may hold a pointer/slice to this,
/// so its better to leave the deallocation of it to arena allocator. /// so its better to leave the deallocation of it to arena allocator.
slice: []const u8, _slice: []const u8,
/// MIME attached to blob. Can be an empty string. /// MIME attached to blob. Can be an empty string.
mime: []const u8, _mime: []const u8,
pub const Type = union(enum) { pub const Type = union(enum) {
generic, generic,
@@ -66,7 +67,7 @@ pub fn init(
break :blk try page.arena.dupe(u8, t); break :blk try page.arena.dupe(u8, t);
}; };
const slice = blk: { const data = blk: {
if (maybe_blob_parts) |blob_parts| { if (maybe_blob_parts) |blob_parts| {
var w: Writer.Allocating = .init(page.arena); var w: Writer.Allocating = .init(page.arena);
const use_native_endings = std.mem.eql(u8, options.endings, "native"); const use_native_endings = std.mem.eql(u8, options.endings, "native");
@@ -80,8 +81,8 @@ pub fn init(
return page._factory.create(Blob{ return page._factory.create(Blob{
._type = .generic, ._type = .generic,
.slice = slice, ._slice = data,
.mime = mime, ._mime = mime,
}); });
} }
@@ -147,8 +148,8 @@ fn writeBlobParts(
while (end + vector_len <= part.len) : (end += vector_len) { while (end + vector_len <= part.len) : (end += vector_len) {
const cr: Vec = @splat('\r'); const cr: Vec = @splat('\r');
// Load chunk as vectors. // Load chunk as vectors.
const slice = part[end..][0..vector_len]; const data = part[end..][0..vector_len];
const chunk: Vec = slice.*; const chunk: Vec = data.*;
// Look for CR. // Look for CR.
const match = chunk == cr; const match = chunk == cr;
@@ -160,16 +161,16 @@ fn writeBlobParts(
var iter = bitset.iterator(.{}); var iter = bitset.iterator(.{});
var relative_start: usize = 0; var relative_start: usize = 0;
while (iter.next()) |index| { while (iter.next()) |index| {
_ = try writer.writeVec(&.{ slice[relative_start..index], "\n" }); _ = try writer.writeVec(&.{ data[relative_start..index], "\n" });
if (index + 1 != slice.len and slice[index + 1] == '\n') { if (index + 1 != data.len and data[index + 1] == '\n') {
relative_start = index + 2; relative_start = index + 2;
} else { } else {
relative_start = index + 1; relative_start = index + 1;
} }
} }
_ = try writer.writeVec(&.{slice[relative_start..]}); _ = try writer.writeVec(&.{data[relative_start..]});
} }
} }
@@ -204,16 +205,21 @@ fn writeBlobParts(
/// Returns a Promise that resolves with the contents of the blob /// Returns a Promise that resolves with the contents of the blob
/// as binary data contained in an ArrayBuffer. /// as binary data contained in an ArrayBuffer.
//pub fn arrayBuffer(self: *const Blob, page: *Page) !js.Promise { pub fn arrayBuffer(self: *const Blob, page: *Page) !js.Promise {
// return page.js.resolvePromise(js.ArrayBuffer{ .values = self.slice }); return page.js.resolvePromise(js.ArrayBuffer{ .values = self._slice });
//} }
// TODO: Implement `stream`; requires `ReadableStream`. const ReadableStream = @import("streams/ReadableStream.zig");
/// Returns a ReadableStream which upon reading returns the data
/// contained within the Blob.
pub fn stream(self: *const Blob, page: *Page) !*ReadableStream {
return ReadableStream.initWithData(self._slice, page);
}
/// Returns a Promise that resolves with a string containing /// Returns a Promise that resolves with a string containing
/// the contents of the blob, interpreted as UTF-8. /// the contents of the blob, interpreted as UTF-8.
pub fn text(self: *const Blob, page: *Page) !js.Promise { pub fn text(self: *const Blob, page: *Page) !js.Promise {
return page.js.resolvePromise(self.slice); return page.js.resolvePromise(self._slice);
} }
/// Extension to Blob; works on Firefox and Safari. /// Extension to Blob; works on Firefox and Safari.
@@ -221,12 +227,12 @@ pub fn text(self: *const Blob, page: *Page) !js.Promise {
/// Returns a Promise that resolves with a Uint8Array containing /// Returns a Promise that resolves with a Uint8Array containing
/// the contents of the blob as an array of bytes. /// the contents of the blob as an array of bytes.
pub fn bytes(self: *const Blob, page: *Page) !js.Promise { pub fn bytes(self: *const Blob, page: *Page) !js.Promise {
return page.js.resolvePromise(js.TypedArray(u8){ .values = self.slice }); return page.js.resolvePromise(js.TypedArray(u8){ .values = self._slice });
} }
/// Returns a new Blob object which contains data /// Returns a new Blob object which contains data
/// from a subset of the blob on which it's called. /// from a subset of the blob on which it's called.
pub fn getSlice( pub fn slice(
self: *const Blob, self: *const Blob,
maybe_start: ?i32, maybe_start: ?i32,
maybe_end: ?i32, maybe_end: ?i32,
@@ -239,56 +245,56 @@ pub fn getSlice(
break :blk ""; break :blk "";
} }
break :blk try page.arena.dupe(u8, content_type); break :blk try page.dupeString(content_type);
} }
break :blk ""; break :blk "";
}; };
const slice = self.slice; const data = self._slice;
if (maybe_start) |_start| { if (maybe_start) |_start| {
const start = blk: { const start = blk: {
if (_start < 0) { if (_start < 0) {
break :blk slice.len -| @abs(_start); break :blk data.len -| @abs(_start);
} }
break :blk @min(slice.len, @as(u31, @intCast(_start))); break :blk @min(data.len, @as(u31, @intCast(_start)));
}; };
const end: usize = blk: { const end: usize = blk: {
if (maybe_end) |_end| { if (maybe_end) |_end| {
if (_end < 0) { if (_end < 0) {
break :blk @max(start, slice.len -| @abs(_end)); break :blk @max(start, data.len -| @abs(_end));
} }
break :blk @min(slice.len, @max(start, @as(u31, @intCast(_end)))); break :blk @min(data.len, @max(start, @as(u31, @intCast(_end))));
} }
break :blk slice.len; break :blk data.len;
}; };
return page._factory.create(Blob{ return page._factory.create(Blob{
._type = .generic, ._type = .generic,
.slice = slice[start..end], ._slice = data[start..end],
.mime = mime, ._mime = mime,
}); });
} }
return page._factory.create(Blob{ return page._factory.create(Blob{
._type = .generic, ._type = .generic,
.slice = slice, ._slice = data,
.mime = mime, ._mime = mime,
}); });
} }
/// Returns the size of the Blob in bytes. /// Returns the size of the Blob in bytes.
pub fn getSize(self: *const Blob) usize { pub fn getSize(self: *const Blob) usize {
return self.slice.len; return self._slice.len;
} }
/// Returns the type of Blob; likely a MIME type, yet anything can be given. /// Returns the type of Blob; likely a MIME type, yet anything can be given.
pub fn getType(self: *const Blob) []const u8 { pub fn getType(self: *const Blob) []const u8 {
return self.mime; return self._mime;
} }
pub const JsApi = struct { pub const JsApi = struct {
@@ -303,9 +309,11 @@ pub const JsApi = struct {
pub const constructor = bridge.constructor(Blob.init, .{}); pub const constructor = bridge.constructor(Blob.init, .{});
pub const text = bridge.function(Blob.text, .{}); pub const text = bridge.function(Blob.text, .{});
pub const bytes = bridge.function(Blob.bytes, .{}); pub const bytes = bridge.function(Blob.bytes, .{});
pub const slice = bridge.function(Blob.getSlice, .{}); pub const slice = bridge.function(Blob.slice, .{});
pub const size = bridge.accessor(Blob.getSize, null, .{}); pub const size = bridge.accessor(Blob.getSize, null, .{});
pub const @"type" = bridge.accessor(Blob.getType, null, .{}); pub const @"type" = bridge.accessor(Blob.getType, null, .{});
pub const stream = bridge.function(Blob.stream, .{});
pub const arrayBuffer = bridge.function(Blob.arrayBuffer, .{});
}; };
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");