diff --git a/src/url/query.zig b/src/url/query.zig index 5a6253e9..899e8ea1 100644 --- a/src/url/query.zig +++ b/src/url/query.zig @@ -79,19 +79,42 @@ pub const Values = struct { pub fn deleteValue(self: *Values, k: []const u8, v: []const u8) void { const list = self.map.getPtr(k) orelse return; - var i: usize = 0; - while (i < list.items.len) { - if (std.mem.eql(u8, v, list.items[i])) { + for (list.items, 0..) |vv, i| { + if (std.mem.eql(u8, v, vv)) { _ = list.swapRemove(i); return; } - i += 1; } } pub fn count(self: *Values) usize { return self.map.count(); } + + // the caller owned the returned string. + pub fn encode(self: *Values, writer: anytype) !void { + var i: usize = 0; + var it = self.map.iterator(); + while (it.next()) |entry| { + defer i += 1; + if (i > 0) try writer.writeByte('&'); + + if (entry.value_ptr.items.len == 0) { + try escape(writer, entry.key_ptr.*); + continue; + } + + const start = i; + for (entry.value_ptr.items) |v| { + defer i += 1; + if (start < i) try writer.writeByte('&'); + + try escape(writer, entry.key_ptr.*); + if (v.len > 0) try writer.writeByte('='); + try escape(writer, v); + } + } + } }; fn unhex(c: u8) u8 { @@ -137,6 +160,19 @@ test "unescape" { alloc.free(v); } +pub fn escape(writer: anytype, raw: []const u8) !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); @@ -213,3 +249,17 @@ test "parse query dup" { try std.testing.expect(std.mem.eql(u8, values.first("a"), "b")); try std.testing.expect(values.get("a").len == 2); } + +test "encode query" { + var values = try parseQuery(std.testing.allocator, "a=b&b=c"); + defer values.deinit(); + + try values.append("a", "~"); + + var buf: std.ArrayListUnmanaged(u8) = .{}; + defer buf.deinit(std.testing.allocator); + + try values.encode(buf.writer(std.testing.allocator)); + + try std.testing.expect(std.mem.eql(u8, buf.items, "a=b&a=%7E&b=c")); +} diff --git a/src/url/url.zig b/src/url/url.zig index f268493d..9c99587f 100644 --- a/src/url/url.zig +++ b/src/url/url.zig @@ -56,10 +56,18 @@ pub const URL = struct { // the caller must free the returned string. // TODO return a disposable string // https://github.com/lightpanda-io/jsruntime-lib/issues/195 - pub fn get_href(self: URL, alloc: std.mem.Allocator) ![]const u8 { + pub fn get_href(self: *URL, alloc: std.mem.Allocator) ![]const u8 { var buf = std.ArrayList(u8).init(alloc); defer buf.deinit(); + // retrieve the query search from search_params. + const cur = self.uri.query; + defer self.uri.query = cur; + var q = std.ArrayList(u8).init(alloc); + defer q.deinit(); + try self.search_params.values.encode(q.writer()); + self.uri.query = q.items; + try self.uri.writeToStream(.{ .scheme = true, .authentication = true, @@ -116,9 +124,14 @@ pub const URL = struct { // TODO return a disposable string // https://github.com/lightpanda-io/jsruntime-lib/issues/195 pub fn get_search(self: *URL, alloc: std.mem.Allocator) ![]const u8 { - if (self.uri.query == null) return try alloc.dupe(u8, ""); + if (self.search_params.get_size() == 0) return try alloc.dupe(u8, ""); - return try std.mem.concat(alloc, u8, &[_][]const u8{ "?", self.uri.query.? }); + var buf: std.ArrayListUnmanaged(u8) = .{}; + defer buf.deinit(alloc); + + try buf.append(alloc, '?'); + try self.search_params.values.encode(buf.writer(alloc)); + return buf.toOwnedSlice(alloc); } // the caller must free the returned string. @@ -207,12 +220,18 @@ pub fn testExecFn( try checkCases(js_env, &url); var qs = [_]Case{ - .{ .src = "var url = new URL('https://foo.bar/path?a=~&b=%7E')", .ex = "undefined" }, + .{ .src = "var url = new URL('https://foo.bar/path?a=~&b=%7E#fragment')", .ex = "undefined" }, .{ .src = "url.searchParams.get('a')", .ex = "~" }, .{ .src = "url.searchParams.get('b')", .ex = "~" }, .{ .src = "url.searchParams.append('c', 'foo')", .ex = "undefined" }, .{ .src = "url.searchParams.get('c')", .ex = "foo" }, .{ .src = "url.searchParams.size", .ex = "3" }, + + // search is dynamic + .{ .src = "url.search", .ex = "?a=%7E&b=%7E&c=foo" }, + // href is dynamic + .{ .src = "url.href", .ex = "https://foo.bar/path?a=%7E&b=%7E&c=foo#fragment" }, + .{ .src = "url.searchParams.delete('c', 'foo')", .ex = "undefined" }, .{ .src = "url.searchParams.get('c')", .ex = "" }, .{ .src = "url.searchParams.delete('a')", .ex = "undefined" },