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 {
|
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