mirror of
				https://github.com/lightpanda-io/browser.git
				synced 2025-10-30 15:41:48 +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 { |     inline fn url(self: *parser.Anchor, page: *Page) !URL { | ||||||
|         const href = try parser.anchorGetHref(self); |         return URL.constructor(.{ .element = @ptrCast(self) }, null, page); // TODO inject base url | ||||||
|         return URL.constructor(href, null, page); // TODO inject base url |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // TODO return a disposable string |     // TODO return a disposable string | ||||||
| @@ -391,23 +390,16 @@ pub const HTMLAnchorElement = struct { | |||||||
|         try parser.anchorSetHref(self, href); |         try parser.anchorSetHref(self, href); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // TODO return a disposable string |  | ||||||
|     pub fn get_search(self: *parser.Anchor, page: *Page) ![]const u8 { |     pub fn get_search(self: *parser.Anchor, page: *Page) ![]const u8 { | ||||||
|         var u = try url(self, page); |         var u = try url(self, page); | ||||||
|         return try u.get_search(page); |         return try u.get_search(page); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub fn set_search(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { |     pub fn set_search(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { | ||||||
|         const arena = page.arena; |  | ||||||
|         var u = try url(self, page); |         var u = try url(self, page); | ||||||
|  |         try u.set_search(v, page); | ||||||
|  |  | ||||||
|         if (v) |vv| { |         const href = try u.toString(page.call_arena); | ||||||
|             u.uri.query = .{ .raw = vv }; |  | ||||||
|         } else { |  | ||||||
|             u.uri.query = null; |  | ||||||
|         } |  | ||||||
|         const href = try u.toString(arena); |  | ||||||
|  |  | ||||||
|         try parser.anchorSetHref(self, href); |         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 std = @import("std"); | ||||||
| const Allocator = std.mem.Allocator; | const Allocator = std.mem.Allocator; | ||||||
|  |  | ||||||
|  | const parser = @import("../netsurf.zig"); | ||||||
| const Page = @import("../page.zig").Page; | const Page = @import("../page.zig").Page; | ||||||
| const FormData = @import("../xhr/form_data.zig").FormData; | const FormData = @import("../xhr/form_data.zig").FormData; | ||||||
|  | const HTMLElement = @import("../html/elements.zig").HTMLElement; | ||||||
|  |  | ||||||
| const kv = @import("../key_value.zig"); | const kv = @import("../key_value.zig"); | ||||||
| const iterator = @import("../iterator/iterator.zig"); | const iterator = @import("../iterator/iterator.zig"); | ||||||
| @@ -51,20 +53,37 @@ pub const URL = struct { | |||||||
|     uri: std.Uri, |     uri: std.Uri, | ||||||
|     search_params: URLSearchParams, |     search_params: URLSearchParams, | ||||||
|  |  | ||||||
|     pub fn constructor( |     const URLArg = union(enum) { | ||||||
|         url: []const u8, |         url: *URL, | ||||||
|         base: ?[]const u8, |         element: *parser.ElementHTML, | ||||||
|         page: *Page, |         string: []const u8, | ||||||
|     ) !URL { |  | ||||||
|  |         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; |         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| { |         if (base) |b| { | ||||||
|             raw = try @import("../../url.zig").URL.stitch(arena, url, b, .{}); |             if (try b.toString(arena)) |bb| { | ||||||
|         } else { |                 raw = try @import("../../url.zig").URL.stitch(arena, url_str, bb, .{}); | ||||||
|             raw = try arena.dupe(u8, url); |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         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); |         return init(arena, uri); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -95,30 +114,32 @@ pub const URL = struct { | |||||||
|     // The query is replaced by a dump of search params. |     // The query is replaced by a dump of search params. | ||||||
|     // |     // | ||||||
|     pub fn get_href(self: *URL, page: *Page) ![]const u8 { |     pub fn get_href(self: *URL, page: *Page) ![]const u8 { | ||||||
|         const arena = page.arena; |         return try self.toString(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); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // format the url with all its components. |     // 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; |         var buf: std.ArrayListUnmanaged(u8) = .empty; | ||||||
|  |  | ||||||
|         try self.uri.writeToStream(.{ |         try self.uri.writeToStream(.{ | ||||||
|             .scheme = true, |             .scheme = true, | ||||||
|             .authentication = true, |             .authentication = true, | ||||||
|             .authority = true, |             .authority = true, | ||||||
|             .path = uriComponentNullStr(self.uri.path).len > 0, |             .path = uriComponentNullStr(self.uri.path).len > 0, | ||||||
|             .query = uriComponentNullStr(self.uri.query).len > 0, |  | ||||||
|             .fragment = uriComponentNullStr(self.uri.fragment).len > 0, |  | ||||||
|         }, buf.writer(arena)); |         }, 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; |         return buf.items; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -168,15 +189,24 @@ pub const URL = struct { | |||||||
|  |  | ||||||
|     pub fn get_search(self: *URL, page: *Page) ![]const u8 { |     pub fn get_search(self: *URL, page: *Page) ![]const u8 { | ||||||
|         const arena = page.arena; |         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) = .{}; |         var buf: std.ArrayListUnmanaged(u8) = .{}; | ||||||
|  |  | ||||||
|         try buf.append(arena, '?'); |         try buf.append(arena, '?'); | ||||||
|         try self.search_params.encode(buf.writer(arena)); |         try self.search_params.encode(buf.writer(arena)); | ||||||
|         return buf.items; |         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 { |     pub fn get_hash(self: *URL, page: *Page) ![]const u8 { | ||||||
|         const arena = page.arena; |         const arena = page.arena; | ||||||
|         if (self.uri.fragment == null) return try arena.dupe(u8, ""); |         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 | // https://url.spec.whatwg.org/#interface-urlsearchparams | ||||||
| pub const URLSearchParams = struct { | pub const URLSearchParams = struct { | ||||||
|     entries: kv.List, |     entries: kv.List = .{}, | ||||||
|  |  | ||||||
|     const URLSearchParamsOpts = union(enum) { |     const URLSearchParamsOpts = union(enum) { | ||||||
|         qs: []const u8, |         qs: []const u8, | ||||||
| @@ -279,10 +309,14 @@ pub const URLSearchParams = struct { | |||||||
|  |  | ||||||
|     fn _toString(self: *const URLSearchParams, page: *Page) ![]const u8 { |     fn _toString(self: *const URLSearchParams, page: *Page) ![]const u8 { | ||||||
|         var arr: std.ArrayListUnmanaged(u8) = .empty; |         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; |         return arr.items; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     fn write(self: *const URLSearchParams, writer: anytype) !void { | ||||||
|  |         return kv.urlEncode(self.entries, .query, writer); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     // TODO |     // TODO | ||||||
|     pub fn _sort(_: *URLSearchParams) void {} |     pub fn _sort(_: *URLSearchParams) void {} | ||||||
|  |  | ||||||
| @@ -449,6 +483,27 @@ test "Browser.URL" { | |||||||
|         .{ "url.search", "?query" }, |         .{ "url.search", "?query" }, | ||||||
|         .{ "url.hash", "#fragment" }, |         .{ "url.hash", "#fragment" }, | ||||||
|         .{ "url.searchParams.get('query')", "" }, |         .{ "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(&.{ |     try runner.testCases(&.{ | ||||||
|   | |||||||
| @@ -911,7 +911,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { | |||||||
|                         // coerced to. |                         // coerced to. | ||||||
|                         var coerce_index: ?usize = null; |                         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 |                         // compatible with. A compatible field has higher precedence | ||||||
|                         // than a coercible, but still isn't a perfect match. |                         // than a coercible, but still isn't a perfect match. | ||||||
|                         var compatible_index: ?usize = null; |                         var compatible_index: ?usize = null; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Karl Seguin
					Karl Seguin