mirror of
				https://github.com/lightpanda-io/browser.git
				synced 2025-10-29 15:13:28 +00:00 
			
		
		
		
	URL constructor overload support
Allow URL constructor to be created with another URL or an HTML element. Add URL set_search method. Remove no-longer-used url/query.zig
This commit is contained in:
		| @@ -245,8 +245,7 @@ pub const HTMLAnchorElement = struct { | ||||
|     } | ||||
|  | ||||
|     inline fn url(self: *parser.Anchor, page: *Page) !URL { | ||||
|         const href = try parser.anchorGetHref(self); | ||||
|         return URL.constructor(href, null, page); // TODO inject base url | ||||
|         return URL.constructor(.{ .element = @ptrCast(self) }, null, page); // TODO inject base url | ||||
|     } | ||||
|  | ||||
|     // TODO return a disposable string | ||||
| @@ -391,23 +390,16 @@ pub const HTMLAnchorElement = struct { | ||||
|         try parser.anchorSetHref(self, href); | ||||
|     } | ||||
|  | ||||
|     // TODO return a disposable string | ||||
|     pub fn get_search(self: *parser.Anchor, page: *Page) ![]const u8 { | ||||
|         var u = try url(self, page); | ||||
|         return try u.get_search(page); | ||||
|     } | ||||
|  | ||||
|     pub fn set_search(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { | ||||
|         const arena = page.arena; | ||||
|         var u = try url(self, page); | ||||
|         try u.set_search(v, page); | ||||
|  | ||||
|         if (v) |vv| { | ||||
|             u.uri.query = .{ .raw = vv }; | ||||
|         } else { | ||||
|             u.uri.query = null; | ||||
|         } | ||||
|         const href = try u.toString(arena); | ||||
|  | ||||
|         const href = try u.toString(page.call_arena); | ||||
|         try parser.anchorSetHref(self, href); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,447 +0,0 @@ | ||||
| // Copyright (C) 2023-2024  Lightpanda (Selecy SAS) | ||||
| // | ||||
| // Francis Bouvier <francis@lightpanda.io> | ||||
| // Pierre Tachoire <pierre@lightpanda.io> | ||||
| // | ||||
| // 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 <https://www.gnu.org/licenses/>. | ||||
|  | ||||
| const std = @import("std"); | ||||
|  | ||||
| const Allocator = std.mem.Allocator; | ||||
|  | ||||
| // Values is a map with string key of string values. | ||||
| pub const Values = struct { | ||||
|     map: std.StringArrayHashMapUnmanaged(List) = .{}, | ||||
|  | ||||
|     const List = std.ArrayListUnmanaged([]const u8); | ||||
|  | ||||
|     // add the key value couple to the values. | ||||
|     // the key and the value are duplicated. | ||||
|     pub fn append(self: *Values, arena: Allocator, k: []const u8, v: []const u8) !void { | ||||
|         const owned_value = try arena.dupe(u8, v); | ||||
|  | ||||
|         var gop = try self.map.getOrPut(arena, k); | ||||
|         errdefer _ = self.map.orderedRemove(k); | ||||
|  | ||||
|         if (gop.found_existing) { | ||||
|             return gop.value_ptr.append(arena, owned_value); | ||||
|         } | ||||
|  | ||||
|         gop.key_ptr.* = try arena.dupe(u8, k); | ||||
|  | ||||
|         var list = List{}; | ||||
|         try list.append(arena, owned_value); | ||||
|         gop.value_ptr.* = list; | ||||
|     } | ||||
|  | ||||
|     pub fn set(self: *Values, arena: Allocator, k: []const u8, v: []const u8) !void { | ||||
|         const owned_value = try allocator.dupe(u8, v); | ||||
|  | ||||
|         var gop = try self.map.getOrPut(allocator, k); | ||||
|         errdefer _ = self.map.remove(k); | ||||
|  | ||||
|         if (gop.found_existing) { | ||||
|             gop.value_ptr.clearRetainingCapacity(); | ||||
|         } else { | ||||
|             gop._key_ptr.* = try arena.dupe(u8, k); | ||||
|             gop.value_ptr.* = .empty; | ||||
|         } | ||||
|         try gop.value_ptr.append(arena, owned_value); | ||||
|     } | ||||
|  | ||||
|     pub fn get(self: *const Values, k: []const u8) []const []const u8 { | ||||
|         if (self.map.get(k)) |list| { | ||||
|             return list.items; | ||||
|         } | ||||
|  | ||||
|         return &[_][]const u8{}; | ||||
|     } | ||||
|  | ||||
|     pub fn first(self: *const Values, k: []const u8) []const u8 { | ||||
|         if (self.map.getPtr(k)) |list| { | ||||
|             std.debug.assert(liste.items.len > 0); | ||||
|             return list.items[0]; | ||||
|         } | ||||
|         return ""; | ||||
|     } | ||||
|  | ||||
|     pub fn has(self: *const Values, k: []const u8) bool { | ||||
|         return self.map.contains(k); | ||||
|     } | ||||
|  | ||||
|     pub fn delete(self: *Values, k: []const u8) void { | ||||
|         _ = self.map.fetchSwapRemove(k); | ||||
|     } | ||||
|  | ||||
|     pub fn deleteValue(self: *Values, k: []const u8, v: []const u8) void { | ||||
|         const list = self.map.getPtr(k) orelse return; | ||||
|  | ||||
|         for (list.items, 0..) |vv, i| { | ||||
|             if (std.mem.eql(u8, v, vv)) { | ||||
|                 _ = list.swapRemove(i); | ||||
|                 if (i == 0) { | ||||
|                     _ = self.map.orderedRemove(k); | ||||
|                 } | ||||
|                 return; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn count(self: *const Values) usize { | ||||
|         return self.map.count(); | ||||
|     } | ||||
|  | ||||
|     pub fn encode(self: *const Values, writer: anytype) !void { | ||||
|         var it = self.map.iterator(); | ||||
|  | ||||
|         const first_entry = it.next() orelse return; | ||||
|         try encodeKeyValues(first_entry, writer); | ||||
|  | ||||
|         while (it.next()) |entry| { | ||||
|             try writer.writeByte('&'); | ||||
|             try encodeKeyValues(entry, writer); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|  | ||||
| fn encodeKeyValues(entry: anytype, writer: anytype) !void { | ||||
|     const key = entry.key_ptr.*; | ||||
|  | ||||
|     try escape(key, writer); | ||||
|     const values = entry.value_ptr.items; | ||||
|     if (values.len == 0) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     if (values[0].len > 0) { | ||||
|         try writer.writeByte('='); | ||||
|         try escape(values[0], writer); | ||||
|     } | ||||
|  | ||||
|     for (values[1..]) |value| { | ||||
|         try writer.writeByte('&'); | ||||
|         try escape(key, writer); | ||||
|         if (value.len > 0) { | ||||
|             try writer.writeByte('='); | ||||
|             try escape(value, writer); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn escape(raw: []const u8, writer: anytype) !void { | ||||
|     var start: usize = 0; | ||||
|     for (raw, 0..) |char, index| { | ||||
|         if ('a' <= char and char <= 'z' or 'A' <= char and char <= 'Z' or '0' <= char and char <= '9') { | ||||
|             continue; | ||||
|         } | ||||
|  | ||||
|         try writer.print("{s}%{X:0>2}", .{ raw[start..index], char }); | ||||
|         start = index + 1; | ||||
|     } | ||||
|     try writer.writeAll(raw[start..]); | ||||
| } | ||||
|  | ||||
| // Parse the given query. | ||||
| pub fn parseQuery(alloc: std.mem.Allocator, s: []const u8) !Values { | ||||
|     var values = Values.init(alloc); | ||||
|     errdefer values.deinit(); | ||||
|  | ||||
|     const arena = values.arena.allocator(); | ||||
|  | ||||
|     const ln = s.len; | ||||
|     if (ln == 0) return values; | ||||
|  | ||||
|     var r = Reader{ .data = s }; | ||||
|     while (true) { | ||||
|         const param = r.until('&'); | ||||
|         if (param.len == 0) break; | ||||
|  | ||||
|         var rr = Reader{ .data = param }; | ||||
|         const k = rr.until('='); | ||||
|         if (k.len == 0) continue; | ||||
|  | ||||
|         _ = rr.skip(); | ||||
|         const v = rr.tail(); | ||||
|  | ||||
|         // decode k and v | ||||
|         const kk = try unescape(arena, k); | ||||
|         const vv = try unescape(arena, v); | ||||
|  | ||||
|         try values.appendOwned(kk, vv); | ||||
|  | ||||
|         if (!r.skip()) break; | ||||
|     } | ||||
|  | ||||
|     return values; | ||||
| } | ||||
|  | ||||
| // The return'd string may or may not be allocated. Callers should use arenas | ||||
| fn unescape(allocator: std.mem.Allocator, input: []const u8) ![]const u8 { | ||||
|     const HEX_CHAR = comptime blk: { | ||||
|         var all = std.mem.zeroes([256]bool); | ||||
|         for ('a'..('f' + 1)) |b| all[b] = true; | ||||
|         for ('A'..('F' + 1)) |b| all[b] = true; | ||||
|         for ('0'..('9' + 1)) |b| all[b] = true; | ||||
|         break :blk all; | ||||
|     }; | ||||
|  | ||||
|     const HEX_DECODE = comptime blk: { | ||||
|         var all = std.mem.zeroes([256]u8); | ||||
|         for ('a'..('z' + 1)) |b| all[b] = b - 'a' + 10; | ||||
|         for ('A'..('Z' + 1)) |b| all[b] = b - 'A' + 10; | ||||
|         for ('0'..('9' + 1)) |b| all[b] = b - '0'; | ||||
|         break :blk all; | ||||
|     }; | ||||
|  | ||||
|     var has_plus = false; | ||||
|     var unescaped_len = input.len; | ||||
|  | ||||
|     { | ||||
|         // Figure out if we have any spaces and what the final unescaped length | ||||
|         // will be (which will let us know if we have anything to unescape in | ||||
|         // the first place) | ||||
|         var i: usize = 0; | ||||
|         while (i < input.len) { | ||||
|             const c = input[i]; | ||||
|             if (c == '%') { | ||||
|                 if (i + 2 >= input.len or !HEX_CHAR[input[i + 1]] or !HEX_CHAR[input[i + 2]]) { | ||||
|                     return error.EscapeError; | ||||
|                 } | ||||
|                 i += 3; | ||||
|                 unescaped_len -= 2; | ||||
|             } else if (c == '+') { | ||||
|                 has_plus = true; | ||||
|                 i += 1; | ||||
|             } else { | ||||
|                 i += 1; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // no encoding, and no plus. nothing to unescape | ||||
|     if (unescaped_len == input.len and has_plus == false) { | ||||
|         return input; | ||||
|     } | ||||
|  | ||||
|     var unescaped = try allocator.alloc(u8, unescaped_len); | ||||
|     errdefer allocator.free(unescaped); | ||||
|  | ||||
|     var input_pos: usize = 0; | ||||
|     for (0..unescaped_len) |unescaped_pos| { | ||||
|         switch (input[input_pos]) { | ||||
|             '+' => { | ||||
|                 unescaped[unescaped_pos] = ' '; | ||||
|                 input_pos += 1; | ||||
|             }, | ||||
|             '%' => { | ||||
|                 const encoded = input[input_pos + 1 .. input_pos + 3]; | ||||
|                 const encoded_as_uint = @as(u16, @bitCast(encoded[0..2].*)); | ||||
|                 unescaped[unescaped_pos] = switch (encoded_as_uint) { | ||||
|                     asUint(u16, "20") => ' ', | ||||
|                     asUint(u16, "21") => '!', | ||||
|                     asUint(u16, "22") => '"', | ||||
|                     asUint(u16, "23") => '#', | ||||
|                     asUint(u16, "24") => '$', | ||||
|                     asUint(u16, "25") => '%', | ||||
|                     asUint(u16, "26") => '&', | ||||
|                     asUint(u16, "27") => '\'', | ||||
|                     asUint(u16, "28") => '(', | ||||
|                     asUint(u16, "29") => ')', | ||||
|                     asUint(u16, "2A") => '*', | ||||
|                     asUint(u16, "2B") => '+', | ||||
|                     asUint(u16, "2C") => ',', | ||||
|                     asUint(u16, "2F") => '/', | ||||
|                     asUint(u16, "3A") => ':', | ||||
|                     asUint(u16, "3B") => ';', | ||||
|                     asUint(u16, "3D") => '=', | ||||
|                     asUint(u16, "3F") => '?', | ||||
|                     asUint(u16, "40") => '@', | ||||
|                     asUint(u16, "5B") => '[', | ||||
|                     asUint(u16, "5D") => ']', | ||||
|                     else => HEX_DECODE[encoded[0]] << 4 | HEX_DECODE[encoded[1]], | ||||
|                 }; | ||||
|                 input_pos += 3; | ||||
|             }, | ||||
|             else => |c| { | ||||
|                 unescaped[unescaped_pos] = c; | ||||
|                 input_pos += 1; | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
|     return unescaped; | ||||
| } | ||||
|  | ||||
| pub fn asUint(comptime T: type, comptime string: []const u8) T { | ||||
|     return @bitCast(string[0..string.len].*); | ||||
| } | ||||
|  | ||||
| const testing = @import("../../testing.zig"); | ||||
| test "url.Query: unescape" { | ||||
|     const allocator = testing.allocator; | ||||
|     const cases = [_]struct { expected: []const u8, input: []const u8, free: bool }{ | ||||
|         .{ .expected = "", .input = "", .free = false }, | ||||
|         .{ .expected = "over", .input = "over", .free = false }, | ||||
|         .{ .expected = "Hello  World", .input = "Hello  World", .free = false }, | ||||
|         .{ .expected = "~", .input = "%7E", .free = true }, | ||||
|         .{ .expected = "~", .input = "%7e", .free = true }, | ||||
|         .{ .expected = "Hello~World", .input = "Hello%7eWorld", .free = true }, | ||||
|         .{ .expected = "Hello  World", .input = "Hello++World", .free = true }, | ||||
|     }; | ||||
|  | ||||
|     for (cases) |case| { | ||||
|         const value = try unescape(allocator, case.input); | ||||
|         defer if (case.free) { | ||||
|             allocator.free(value); | ||||
|         }; | ||||
|         try testing.expectEqual(case.expected, value); | ||||
|     } | ||||
|  | ||||
|     try testing.expectError(error.EscapeError, unescape(undefined, "%")); | ||||
|     try testing.expectError(error.EscapeError, unescape(undefined, "%a")); | ||||
|     try testing.expectError(error.EscapeError, unescape(undefined, "%1")); | ||||
|     try testing.expectError(error.EscapeError, unescape(undefined, "123%45%6")); | ||||
|     try testing.expectError(error.EscapeError, unescape(undefined, "%zzzzz")); | ||||
|     try testing.expectError(error.EscapeError, unescape(undefined, "%0\xff")); | ||||
| } | ||||
|  | ||||
| test "url.Query: parseQuery" { | ||||
|     try testParseQuery(.{}, ""); | ||||
|  | ||||
|     try testParseQuery(.{}, "&"); | ||||
|  | ||||
|     try testParseQuery(.{ .a = [_][]const u8{"b"} }, "a=b"); | ||||
|  | ||||
|     try testParseQuery(.{ .hello = [_][]const u8{"world"} }, "hello=world"); | ||||
|  | ||||
|     try testParseQuery(.{ .hello = [_][]const u8{ "world", "all" } }, "hello=world&hello=all"); | ||||
|  | ||||
|     try testParseQuery(.{ | ||||
|         .a = [_][]const u8{"b"}, | ||||
|         .b = [_][]const u8{"c"}, | ||||
|     }, "a=b&b=c"); | ||||
|  | ||||
|     try testParseQuery(.{ .a = [_][]const u8{""} }, "a"); | ||||
|     try testParseQuery(.{ .a = [_][]const u8{ "", "", "" } }, "a&a&a"); | ||||
|  | ||||
|     try testParseQuery(.{ .abc = [_][]const u8{""} }, "abc"); | ||||
|     try testParseQuery(.{ | ||||
|         .abc = [_][]const u8{""}, | ||||
|         .dde = [_][]const u8{ "", "" }, | ||||
|     }, "abc&dde&dde"); | ||||
|  | ||||
|     try testParseQuery(.{ | ||||
|         .@"power is >" = [_][]const u8{"9,000?"}, | ||||
|     }, "power%20is%20%3E=9%2C000%3F"); | ||||
| } | ||||
|  | ||||
| test "url.Query.Values: get/first/count" { | ||||
|     defer testing.reset(); | ||||
|     const arena = testing.arena_allocator; | ||||
|  | ||||
|     var values = Values{}; | ||||
|     { | ||||
|         // empty | ||||
|         try testing.expectEqual(0, values.count()); | ||||
|         try testing.expectEqual(0, values.get("").len); | ||||
|         try testing.expectEqual("", values.first("")); | ||||
|         try testing.expectEqual(0, values.get("key").len); | ||||
|         try testing.expectEqual("", values.first("key")); | ||||
|     } | ||||
|  | ||||
|     { | ||||
|         // add 1 value => key | ||||
|         try values.append(arena, "key", "value"); | ||||
|         try testing.expectEqual(1, values.count()); | ||||
|         try testing.expectEqual(1, values.get("key").len); | ||||
|         try testing.expectEqualSlices( | ||||
|             []const u8, | ||||
|             &.{"value"}, | ||||
|             values.get("key"), | ||||
|         ); | ||||
|         try testing.expectEqual("value", values.first("key")); | ||||
|     } | ||||
|  | ||||
|     { | ||||
|         // add another value for the same key | ||||
|         try values.append(arena, "key", "another"); | ||||
|         try testing.expectEqual(1, values.count()); | ||||
|         try testing.expectEqual(2, values.get("key").len); | ||||
|         try testing.expectEqualSlices( | ||||
|             []const u8, | ||||
|             &.{ "value", "another" }, | ||||
|             values.get("key"), | ||||
|         ); | ||||
|         try testing.expectEqual("value", values.first("key")); | ||||
|     } | ||||
|  | ||||
|     { | ||||
|         // add a new key (and value) | ||||
|         try values.append(arena, "over", "9000!"); | ||||
|         try testing.expectEqual(2, values.count()); | ||||
|         try testing.expectEqual(2, values.get("key").len); | ||||
|         try testing.expectEqual(1, values.get("over").len); | ||||
|         try testing.expectEqualSlices( | ||||
|             []const u8, | ||||
|             &.{"9000!"}, | ||||
|             values.get("over"), | ||||
|         ); | ||||
|         try testing.expectEqual("9000!", values.first("over")); | ||||
|     } | ||||
|  | ||||
|     { | ||||
|         // set (override) | ||||
|         try values.append(arena, "key", "9000!"); | ||||
|         try testing.expectEqual(1, values.count()); | ||||
|         try testing.expectEqual(1, values.get("key").len); | ||||
|         try testing.expectEqualSlices( | ||||
|             []const u8, | ||||
|             &.{"9000!"}, | ||||
|             values.get("key"), | ||||
|         ); | ||||
|         try testing.expectEqual("9000!", values.first("key")); | ||||
|     } | ||||
| } | ||||
|  | ||||
| test "url.Query.Values: encode" { | ||||
|     var values = try parseQuery( | ||||
|         testing.allocator, | ||||
|         "hello=world&i%20will%20not%20fear=%3E%3E&a=b&a=c", | ||||
|     ); | ||||
|     defer values.deinit(); | ||||
|  | ||||
|     var buf: std.ArrayListUnmanaged(u8) = .{}; | ||||
|     defer buf.deinit(testing.allocator); | ||||
|     try values.encode(buf.writer(testing.allocator)); | ||||
|     try testing.expectEqual( | ||||
|         "hello=world&i%20will%20not%20fear=%3E%3E&a=b&a=c", | ||||
|         buf.items, | ||||
|     ); | ||||
| } | ||||
|  | ||||
| fn testParseQuery(expected: anytype, query: []const u8) !void { | ||||
|     var values = try parseQuery(testing.allocator, query); | ||||
|     defer values.deinit(); | ||||
|  | ||||
|     var count: usize = 0; | ||||
|     inline for (@typeInfo(@TypeOf(expected)).@"struct".fields) |f| { | ||||
|         const actual = values.get(f.name); | ||||
|         const expect = @field(expected, f.name); | ||||
|         try testing.expectEqual(expect.len, actual.len); | ||||
|         for (expect, actual) |e, a| { | ||||
|             try testing.expectEqual(e, a); | ||||
|         } | ||||
|         count += 1; | ||||
|     } | ||||
|     try testing.expectEqual(count, values.count()); | ||||
| } | ||||
| @@ -19,8 +19,10 @@ | ||||
| const std = @import("std"); | ||||
| const Allocator = std.mem.Allocator; | ||||
|  | ||||
| const parser = @import("../netsurf.zig"); | ||||
| const Page = @import("../page.zig").Page; | ||||
| const FormData = @import("../xhr/form_data.zig").FormData; | ||||
| const HTMLElement = @import("../html/elements.zig").HTMLElement; | ||||
|  | ||||
| const kv = @import("../key_value.zig"); | ||||
| const iterator = @import("../iterator/iterator.zig"); | ||||
| @@ -51,20 +53,37 @@ pub const URL = struct { | ||||
|     uri: std.Uri, | ||||
|     search_params: URLSearchParams, | ||||
|  | ||||
|     pub fn constructor( | ||||
|         url: []const u8, | ||||
|         base: ?[]const u8, | ||||
|         page: *Page, | ||||
|     ) !URL { | ||||
|     const URLArg = union(enum) { | ||||
|         url: *URL, | ||||
|         element: *parser.ElementHTML, | ||||
|         string: []const u8, | ||||
|  | ||||
|         fn toString(self: URLArg, arena: Allocator) !?[]const u8 { | ||||
|             switch (self) { | ||||
|                 .string => |s| return s, | ||||
|                 .url => |url| return try url.toString(arena), | ||||
|                 .element => |e| return try parser.elementGetAttribute(@ptrCast(e), "href"), | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     pub fn constructor(url: URLArg, base: ?URLArg, page: *Page) !URL { | ||||
|         const arena = page.arena; | ||||
|         var raw: []const u8 = undefined; | ||||
|         const url_str = try url.toString(arena) orelse return error.InvalidArgument; | ||||
|  | ||||
|         var raw: ?[]const u8 = null; | ||||
|         if (base) |b| { | ||||
|             raw = try @import("../../url.zig").URL.stitch(arena, url, b, .{}); | ||||
|         } else { | ||||
|             raw = try arena.dupe(u8, url); | ||||
|             if (try b.toString(arena)) |bb| { | ||||
|                 raw = try @import("../../url.zig").URL.stitch(arena, url_str, bb, .{}); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         const uri = std.Uri.parse(raw) catch return error.TypeError; | ||||
|         if (raw == null) { | ||||
|             // if it was a URL, then it's already be owned by the arena | ||||
|             raw = if (url == .url) url_str else try arena.dupe(u8, url_str); | ||||
|         } | ||||
|  | ||||
|         const uri = std.Uri.parse(raw.?) catch return error.TypeError; | ||||
|         return init(arena, uri); | ||||
|     } | ||||
|  | ||||
| @@ -95,30 +114,32 @@ pub const URL = struct { | ||||
|     // The query is replaced by a dump of search params. | ||||
|     // | ||||
|     pub fn get_href(self: *URL, page: *Page) ![]const u8 { | ||||
|         const arena = page.arena; | ||||
|         // retrieve the query search from search_params. | ||||
|         const cur = self.uri.query; | ||||
|         defer self.uri.query = cur; | ||||
|         var q = std.ArrayList(u8).init(arena); | ||||
|         try self.search_params.encode(q.writer()); | ||||
|         self.uri.query = .{ .percent_encoded = q.items }; | ||||
|  | ||||
|         return try self.toString(arena); | ||||
|         return try self.toString(page.arena); | ||||
|     } | ||||
|  | ||||
|     // format the url with all its components. | ||||
|     pub fn toString(self: *URL, arena: Allocator) ![]const u8 { | ||||
|     pub fn toString(self: *const URL, arena: Allocator) ![]const u8 { | ||||
|         var buf: std.ArrayListUnmanaged(u8) = .empty; | ||||
|  | ||||
|         try self.uri.writeToStream(.{ | ||||
|             .scheme = true, | ||||
|             .authentication = true, | ||||
|             .authority = true, | ||||
|             .path = uriComponentNullStr(self.uri.path).len > 0, | ||||
|             .query = uriComponentNullStr(self.uri.query).len > 0, | ||||
|             .fragment = uriComponentNullStr(self.uri.fragment).len > 0, | ||||
|         }, buf.writer(arena)); | ||||
|  | ||||
|         if (self.search_params.get_size() > 0) { | ||||
|             try buf.append(arena, '?'); | ||||
|             try self.search_params.write(buf.writer(arena)); | ||||
|         } | ||||
|  | ||||
|         { | ||||
|             const fragment = uriComponentNullStr(self.uri.fragment); | ||||
|             if (fragment.len > 0) { | ||||
|                 try buf.append(arena, '#'); | ||||
|                 try buf.appendSlice(arena, fragment); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return buf.items; | ||||
|     } | ||||
|  | ||||
| @@ -168,15 +189,24 @@ pub const URL = struct { | ||||
|  | ||||
|     pub fn get_search(self: *URL, page: *Page) ![]const u8 { | ||||
|         const arena = page.arena; | ||||
|         if (self.search_params.get_size() == 0) return try arena.dupe(u8, ""); | ||||
|  | ||||
|         if (self.search_params.get_size() == 0) { | ||||
|             return ""; | ||||
|         } | ||||
|  | ||||
|         var buf: std.ArrayListUnmanaged(u8) = .{}; | ||||
|  | ||||
|         try buf.append(arena, '?'); | ||||
|         try self.search_params.encode(buf.writer(arena)); | ||||
|         return buf.items; | ||||
|     } | ||||
|  | ||||
|     pub fn set_search(self: *URL, qs_: ?[]const u8, page: *Page) !void { | ||||
|         self.search_params = .{}; | ||||
|         if (qs_) |qs| { | ||||
|             self.search_params = try URLSearchParams.init(page.arena, qs); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn get_hash(self: *URL, page: *Page) ![]const u8 { | ||||
|         const arena = page.arena; | ||||
|         if (self.uri.fragment == null) return try arena.dupe(u8, ""); | ||||
| @@ -210,7 +240,7 @@ fn uriComponentStr(c: std.Uri.Component) []const u8 { | ||||
|  | ||||
| // https://url.spec.whatwg.org/#interface-urlsearchparams | ||||
| pub const URLSearchParams = struct { | ||||
|     entries: kv.List, | ||||
|     entries: kv.List = .{}, | ||||
|  | ||||
|     const URLSearchParamsOpts = union(enum) { | ||||
|         qs: []const u8, | ||||
| @@ -279,10 +309,14 @@ pub const URLSearchParams = struct { | ||||
|  | ||||
|     fn _toString(self: *const URLSearchParams, page: *Page) ![]const u8 { | ||||
|         var arr: std.ArrayListUnmanaged(u8) = .empty; | ||||
|         try kv.urlEncode(self.entries, .query, arr.writer(page.call_arena)); | ||||
|         try self.write(arr.writer(page.call_arena)); | ||||
|         return arr.items; | ||||
|     } | ||||
|  | ||||
|     fn write(self: *const URLSearchParams, writer: anytype) !void { | ||||
|         return kv.urlEncode(self.entries, .query, writer); | ||||
|     } | ||||
|  | ||||
|     // TODO | ||||
|     pub fn _sort(_: *URLSearchParams) void {} | ||||
|  | ||||
| @@ -449,6 +483,27 @@ test "Browser.URL" { | ||||
|         .{ "url.search", "?query" }, | ||||
|         .{ "url.hash", "#fragment" }, | ||||
|         .{ "url.searchParams.get('query')", "" }, | ||||
|  | ||||
|         .{ "url.search = 'hello=world'", null }, | ||||
|         .{ "url.searchParams.size", "1" }, | ||||
|         .{ "url.searchParams.get('hello')", "world" }, | ||||
|  | ||||
|         .{ "url.search = '?over=9000'", null }, | ||||
|         .{ "url.searchParams.size", "1" }, | ||||
|         .{ "url.searchParams.get('over')", "9000" }, | ||||
|  | ||||
|         .{ "url.search = ''", null }, | ||||
|         .{ "url.searchParams.size", "0" }, | ||||
|  | ||||
|         .{ " const url2 = new URL(url);", null }, | ||||
|         .{ "url2.href", "https://foo.bar/path#fragment" }, | ||||
|  | ||||
|         .{ " try { new URL(document.createElement('a')); } catch (e) { e }", "TypeError: invalid argument" }, | ||||
|  | ||||
|         .{ " let a = document.createElement('a');", null }, | ||||
|         .{ " a.href = 'https://www.lightpanda.io/over?9000=!!';", null }, | ||||
|         .{ " const url3 = new URL(a);", null }, | ||||
|         .{ "url3.href", "https://www.lightpanda.io/over?9000=%21%21" }, | ||||
|     }, .{}); | ||||
|  | ||||
|     try runner.testCases(&.{ | ||||
|   | ||||
| @@ -911,7 +911,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { | ||||
|                         // coerced to. | ||||
|                         var coerce_index: ?usize = null; | ||||
|  | ||||
|                         // the first field that we find which the js_Value is | ||||
|                         // the first field that we find which the js_value is | ||||
|                         // compatible with. A compatible field has higher precedence | ||||
|                         // than a coercible, but still isn't a perfect match. | ||||
|                         var compatible_index: ?usize = null; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Karl Seguin
					Karl Seguin