mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-02-04 06:23:45 +00:00
form submitt
This commit is contained in:
@@ -197,10 +197,16 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
|
|||||||
event._event_phase = .none;
|
event._event_phase = .none;
|
||||||
|
|
||||||
// Execute default action if not prevented
|
// Execute default action if not prevented
|
||||||
if (!event._prevent_default and event._type_string.eqlSlice("click")) {
|
if (event._prevent_default) {
|
||||||
|
// can't return in a defer (╯°□°)╯︵ ┻━┻
|
||||||
|
} else if (event._type_string.eqlSlice("click")) {
|
||||||
self.page.handleClick(target) catch |err| {
|
self.page.handleClick(target) catch |err| {
|
||||||
log.warn(.event, "page.click", .{ .err = err });
|
log.warn(.event, "page.click", .{ .err = err });
|
||||||
};
|
};
|
||||||
|
} else if (event._type_string.eqlSlice("keydown")) {
|
||||||
|
self.page.handleKeydown(target, event) catch |err| {
|
||||||
|
log.warn(.event, "page.keydown", .{ .err = err });
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const ScriptManager = @import("ScriptManager.zig");
|
|||||||
|
|
||||||
const Parser = @import("parser/Parser.zig");
|
const Parser = @import("parser/Parser.zig");
|
||||||
|
|
||||||
const URL = @import("webapi/URL.zig");
|
const URL = @import("URL.zig");
|
||||||
const Node = @import("webapi/Node.zig");
|
const Node = @import("webapi/Node.zig");
|
||||||
const Event = @import("webapi/Event.zig");
|
const Event = @import("webapi/Event.zig");
|
||||||
const CData = @import("webapi/CData.zig");
|
const CData = @import("webapi/CData.zig");
|
||||||
@@ -62,7 +62,9 @@ const KeyboardEvent = @import("webapi/event/KeyboardEvent.zig");
|
|||||||
const timestamp = @import("../datetime.zig").timestamp;
|
const timestamp = @import("../datetime.zig").timestamp;
|
||||||
const milliTimestamp = @import("../datetime.zig").milliTimestamp;
|
const milliTimestamp = @import("../datetime.zig").milliTimestamp;
|
||||||
|
|
||||||
var default_url = URL{ ._raw = "about:blank" };
|
const WebApiURL = @import("webapi/URL.zig");
|
||||||
|
|
||||||
|
var default_url = WebApiURL{ ._raw = "about:blank" };
|
||||||
pub var default_location: Location = Location{ ._url = &default_url };
|
pub var default_location: Location = Location{ ._url = &default_url };
|
||||||
|
|
||||||
pub const BUF_SIZE = 1024;
|
pub const BUF_SIZE = 1024;
|
||||||
@@ -297,13 +299,11 @@ pub fn getTitle(self: *Page) !?[]const u8 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn getOrigin(self: *Page, allocator: Allocator) !?[]const u8 {
|
pub fn getOrigin(self: *Page, allocator: Allocator) !?[]const u8 {
|
||||||
const URLRaw = @import("URL.zig");
|
return try URL.getOrigin(allocator, self.url);
|
||||||
return try URLRaw.getOrigin(allocator, self.url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn isSameOrigin(self: *const Page, url: [:0]const u8) !bool {
|
pub fn isSameOrigin(self: *const Page, url: [:0]const u8) !bool {
|
||||||
const URLRaw = @import("URL.zig");
|
const current_origin = (try URL.getOrigin(self.call_arena, self.url)) orelse return false;
|
||||||
const current_origin = (try URLRaw.getOrigin(self.call_arena, self.url)) orelse return false;
|
|
||||||
return std.mem.startsWith(u8, url, current_origin);
|
return std.mem.startsWith(u8, url, current_origin);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,7 +440,6 @@ pub fn scheduleNavigation(self: *Page, request_url: []const u8, opts: NavigateOp
|
|||||||
}
|
}
|
||||||
|
|
||||||
const session = self._session;
|
const session = self._session;
|
||||||
const URLRaw = @import("URL.zig");
|
|
||||||
|
|
||||||
const resolved_url = try URL.resolve(
|
const resolved_url = try URL.resolve(
|
||||||
session.transfer_arena,
|
session.transfer_arena,
|
||||||
@@ -449,7 +448,7 @@ pub fn scheduleNavigation(self: *Page, request_url: []const u8, opts: NavigateOp
|
|||||||
.{ .always_dupe = true },
|
.{ .always_dupe = true },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!opts.force and URLRaw.eqlDocument(self.url, resolved_url)) {
|
if (!opts.force and URL.eqlDocument(self.url, resolved_url)) {
|
||||||
self.url = try self.arena.dupeZ(u8, resolved_url);
|
self.url = try self.arena.dupeZ(u8, resolved_url);
|
||||||
self.window._location = try Location.init(self.url, self);
|
self.window._location = try Location.init(self.url, self);
|
||||||
self.document._location = self.window._location;
|
self.document._location = self.window._location;
|
||||||
@@ -2421,50 +2420,73 @@ pub fn triggerMouseClick(self: *Page, x: f64, y: f64) !void {
|
|||||||
pub fn handleClick(self: *Page, target: *Node) !void {
|
pub fn handleClick(self: *Page, target: *Node) !void {
|
||||||
// TODO: Also support <area> elements when implement
|
// TODO: Also support <area> elements when implement
|
||||||
const element = target.is(Element) orelse return;
|
const element = target.is(Element) orelse return;
|
||||||
const anchor = element.is(Element.Html.Anchor) orelse return;
|
const html_element = element.is(Element.Html) orelse return;
|
||||||
|
|
||||||
const href = element.getAttributeSafe("href") orelse return;
|
switch (html_element._type) {
|
||||||
if (href.len == 0) {
|
.anchor => |anchor| {
|
||||||
return;
|
const href = element.getAttributeSafe("href") orelse return;
|
||||||
|
if (href.len == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.startsWith(u8, href, "#")) {
|
||||||
|
// Hash-only links (#foo) should be handled as same-document navigation
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.startsWith(u8, href, "javascript:")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check target attribute - don't navigate if opening in new window/tab
|
||||||
|
const target_val = anchor.getTarget();
|
||||||
|
if (target_val.len > 0 and !std.mem.eql(u8, target_val, "_self")) {
|
||||||
|
log.warn(.browser, "not implemented", .{
|
||||||
|
.feature = "anchor with target attribute click",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (try element.hasAttribute("download", self)) {
|
||||||
|
log.warn(.browser, "not implemented", .{
|
||||||
|
.feature = "anchor with download attribute click",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.scheduleNavigation(href, .{
|
||||||
|
.reason = .script,
|
||||||
|
.kind = .{ .push = null },
|
||||||
|
}, .anchor);
|
||||||
|
},
|
||||||
|
.input => |input| switch (input._input_type) {
|
||||||
|
.submit => return self.submitForm(element, input.getForm(self)),
|
||||||
|
else => self.window._document._active_element = element,
|
||||||
|
},
|
||||||
|
.button => |button| {
|
||||||
|
if (std.mem.eql(u8, button.getType(), "submit")) {
|
||||||
|
return self.submitForm(element, button.getForm(self));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.select, .textarea => self.window._document._active_element = element,
|
||||||
|
else => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.startsWith(u8, href, "#")) {
|
|
||||||
// Hash-only links (#foo) should be handled as same-document navigation
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.mem.startsWith(u8, href, "javascript:")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check target attribute - don't navigate if opening in new window/tab
|
|
||||||
const target_val = anchor.getTarget();
|
|
||||||
if (target_val.len > 0 and !std.mem.eql(u8, target_val, "_self")) {
|
|
||||||
log.warn(.browser, "not implemented", .{
|
|
||||||
.feature = "anchor with target attribute click",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (try element.hasAttribute("download", self)) {
|
|
||||||
log.warn(.browser, "not implemented", .{
|
|
||||||
.feature = "anchor with download attribute click",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try self.scheduleNavigation(href, .{
|
|
||||||
.reason = .script,
|
|
||||||
.kind = .{ .push = null },
|
|
||||||
}, .anchor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn triggerKeyboard(self: *Page, keyboard_event: *KeyboardEvent) !void {
|
pub fn triggerKeyboard(self: *Page, keyboard_event: *KeyboardEvent) !void {
|
||||||
const element = self.window._document._active_element orelse return;
|
const element = self.window._document._active_element orelse return;
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
log.debug(.page, "page keydown", .{
|
||||||
|
.url = self.url,
|
||||||
|
.node = element,
|
||||||
|
.key = keyboard_event._key,
|
||||||
|
});
|
||||||
|
}
|
||||||
try self._event_manager.dispatch(element.asEventTarget(), keyboard_event.asEvent());
|
try self._event_manager.dispatch(element.asEventTarget(), keyboard_event.asEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handleKeydown(self: *Page, target: *Element, keyboard_event: *KeyboardEvent) !void {
|
pub fn handleKeydown(self: *Page, target: *Node, event: *Event) !void {
|
||||||
|
const keyboard_event = event.as(KeyboardEvent);
|
||||||
const key = keyboard_event.getKey();
|
const key = keyboard_event.getKey();
|
||||||
|
|
||||||
if (key == .Dead) {
|
if (key == .Dead) {
|
||||||
@@ -2473,11 +2495,7 @@ pub fn handleKeydown(self: *Page, target: *Element, keyboard_event: *KeyboardEve
|
|||||||
|
|
||||||
if (target.is(Element.Html.Input)) |input| {
|
if (target.is(Element.Html.Input)) |input| {
|
||||||
if (key == .Enter) {
|
if (key == .Enter) {
|
||||||
if (input.getForm(self)) |form| {
|
return self.submitForm(input.asElement(), input.getForm(self));
|
||||||
// TODO: Implement form submission
|
|
||||||
_ = form;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't handle text input for radio/checkbox
|
// Don't handle text input for radio/checkbox
|
||||||
@@ -2496,14 +2514,59 @@ pub fn handleKeydown(self: *Page, target: *Element, keyboard_event: *KeyboardEve
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (target.is(Element.Html.TextArea)) |textarea| {
|
if (target.is(Element.Html.TextArea)) |textarea| {
|
||||||
|
// zig fmt: off
|
||||||
const append =
|
const append =
|
||||||
if (key == .Enter) "\n" else if (key.isPrintable()) key.asString() else return;
|
if (key == .Enter) "\n"
|
||||||
|
else if (key.isPrintable()) key.asString()
|
||||||
|
else return
|
||||||
|
;
|
||||||
|
// zig fmt: on
|
||||||
const current_value = textarea.getValue();
|
const current_value = textarea.getValue();
|
||||||
const new_value = try std.mem.concat(self.arena, u8, &.{ current_value, append });
|
const new_value = try std.mem.concat(self.arena, u8, &.{ current_value, append });
|
||||||
return textarea.setValue(new_value, self);
|
return textarea.setValue(new_value, self);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn submitForm(self: *Page, submitter_: ?*Element, form_: ?*Element.Html.Form) !void {
|
||||||
|
const form = form_ orelse return;
|
||||||
|
|
||||||
|
if (submitter_) |submitter| {
|
||||||
|
if (submitter.getAttributeSafe("disabled") != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const form_element = form.asElement();
|
||||||
|
|
||||||
|
const FormData = @import("webapi/net/FormData.zig");
|
||||||
|
// The submitter can be an input box (if enter was entered on the box)
|
||||||
|
// I don't think this is technically correct, but FormData handles it ok
|
||||||
|
const form_data = try FormData.init(form, submitter_, self);
|
||||||
|
|
||||||
|
const transfer_arena = self._session.transfer_arena;
|
||||||
|
|
||||||
|
const encoding = form_element.getAttributeSafe("enctype");
|
||||||
|
|
||||||
|
var buf = std.Io.Writer.Allocating.init(transfer_arena);
|
||||||
|
try form_data.write(encoding, &buf.writer);
|
||||||
|
|
||||||
|
const method = form_element.getAttributeSafe("method") orelse "";
|
||||||
|
var action = form_element.getAttributeSafe("action") orelse self.url;
|
||||||
|
|
||||||
|
var opts = NavigateOpts{
|
||||||
|
.reason = .form,
|
||||||
|
.kind = .{ .push = null },
|
||||||
|
};
|
||||||
|
if (std.ascii.eqlIgnoreCase(method, "post")) {
|
||||||
|
opts.method = .POST;
|
||||||
|
opts.body = buf.written();
|
||||||
|
// form_data.write currently only supports this encoding, so we know this has to be the content type
|
||||||
|
opts.header = "Content-Type: application/x-www-form-urlencoded";
|
||||||
|
} else {
|
||||||
|
action = try URL.concatQueryString(transfer_arena, action, buf.written());
|
||||||
|
}
|
||||||
|
return self.scheduleNavigation(action, opts, .form);
|
||||||
|
}
|
||||||
|
|
||||||
const RequestCookieOpts = struct {
|
const RequestCookieOpts = struct {
|
||||||
is_http: bool = true,
|
is_http: bool = true,
|
||||||
is_navigation: bool = false,
|
is_navigation: bool = false,
|
||||||
|
|||||||
@@ -471,6 +471,30 @@ pub fn setHash(current: [:0]const u8, value: []const u8, allocator: Allocator) !
|
|||||||
return buildUrl(allocator, protocol, host, pathname, search, hash);
|
return buildUrl(allocator, protocol, host, pathname, search, hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn concatQueryString(arena: Allocator, url: []const u8, query_string: []const u8) ![:0]const u8 {
|
||||||
|
if (query_string.len == 0) {
|
||||||
|
return arena.dupeZ(u8, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf: std.ArrayList(u8) = .empty;
|
||||||
|
|
||||||
|
// the most space well need is the url + ('?' or '&') + the query_string + null terminator
|
||||||
|
try buf.ensureTotalCapacity(arena, url.len + 2 + query_string.len);
|
||||||
|
buf.appendSliceAssumeCapacity(url);
|
||||||
|
|
||||||
|
if (std.mem.indexOfScalar(u8, url, '?')) |index| {
|
||||||
|
const last_index = url.len - 1;
|
||||||
|
if (index != last_index and url[last_index] != '&') {
|
||||||
|
buf.appendAssumeCapacity('&');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buf.appendAssumeCapacity('?');
|
||||||
|
}
|
||||||
|
buf.appendSliceAssumeCapacity(query_string);
|
||||||
|
buf.appendAssumeCapacity(0);
|
||||||
|
return buf.items[0 .. buf.items.len - 1 :0];
|
||||||
|
}
|
||||||
|
|
||||||
const KnownProtocol = enum {
|
const KnownProtocol = enum {
|
||||||
@"http:",
|
@"http:",
|
||||||
@"https:",
|
@"https:",
|
||||||
@@ -707,3 +731,33 @@ test "URL: eqlDocument" {
|
|||||||
try testing.expectEqual(false, eqlDocument(url1, url2));
|
try testing.expectEqual(false, eqlDocument(url1, url2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "URL: concatQueryString" {
|
||||||
|
defer testing.reset();
|
||||||
|
const arena = testing.arena_allocator;
|
||||||
|
|
||||||
|
{
|
||||||
|
const url = try concatQueryString(arena, "https://www.lightpanda.io/", "");
|
||||||
|
try testing.expectEqual("https://www.lightpanda.io/", url);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const url = try concatQueryString(arena, "https://www.lightpanda.io/index?", "");
|
||||||
|
try testing.expectEqual("https://www.lightpanda.io/index?", url);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const url = try concatQueryString(arena, "https://www.lightpanda.io/index?", "a=b");
|
||||||
|
try testing.expectEqual("https://www.lightpanda.io/index?a=b", url);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const url = try concatQueryString(arena, "https://www.lightpanda.io/index?1=2", "a=b");
|
||||||
|
try testing.expectEqual("https://www.lightpanda.io/index?1=2&a=b", url);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const url = try concatQueryString(arena, "https://www.lightpanda.io/index?1=2&", "a=b");
|
||||||
|
try testing.expectEqual("https://www.lightpanda.io/index?1=2&a=b", url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ pub fn is(self: *Element, comptime T: type) ?*T {
|
|||||||
const type_name = @typeName(T);
|
const type_name = @typeName(T);
|
||||||
switch (self._type) {
|
switch (self._type) {
|
||||||
.html => |el| {
|
.html => |el| {
|
||||||
if (T == *Html) {
|
if (T == Html) {
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
if (comptime std.mem.startsWith(u8, type_name, "browser.webapi.element.html.")) {
|
if (comptime std.mem.startsWith(u8, type_name, "browser.webapi.element.html.")) {
|
||||||
@@ -85,7 +85,7 @@ pub fn is(self: *Element, comptime T: type) ?*T {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
.svg => |svg| {
|
.svg => |svg| {
|
||||||
if (T == *Svg) {
|
if (T == Svg) {
|
||||||
return svg;
|
return svg;
|
||||||
}
|
}
|
||||||
if (comptime std.mem.startsWith(u8, type_name, "webapi.element.svg.")) {
|
if (comptime std.mem.startsWith(u8, type_name, "webapi.element.svg.")) {
|
||||||
|
|||||||
@@ -85,6 +85,31 @@ pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*Event {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn as(self: *Event, comptime T: type) *T {
|
||||||
|
return self.is(T).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is(self: *Event, comptime T: type) ?*T {
|
||||||
|
switch (self._type) {
|
||||||
|
.generic => return if (T == Event) self else null,
|
||||||
|
.error_event => |e| return if (T == @import("event/ErrorEvent.zig")) e else null,
|
||||||
|
.custom_event => |e| return if (T == @import("event/CustomEvent.zig")) e else null,
|
||||||
|
.message_event => |e| return if (T == @import("event/MessageEvent.zig")) e else null,
|
||||||
|
.progress_event => |e| return if (T == @import("event/ProgressEvent.zig")) e else null,
|
||||||
|
.composition_event => |e| return if (T == @import("event/CompositionEvent.zig")) e else null,
|
||||||
|
.navigation_current_entry_change_event => |e| return if (T == @import("event/NavigationCurrentEntryChangeEvent.zig")) e else null,
|
||||||
|
.page_transition_event => |e| return if (T == @import("event/PageTransitionEvent.zig")) e else null,
|
||||||
|
.pop_state_event => |e| return if (T == @import("event/PopStateEvent.zig")) e else null,
|
||||||
|
.ui_event => |e| {
|
||||||
|
if (T == @import("event/UIEvent.zig")) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
return e.is(T);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn getType(self: *const Event) []const u8 {
|
pub fn getType(self: *const Event) []const u8 {
|
||||||
return self._type_string.str();
|
return self._type_string.str();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,16 @@ pub fn registerTypes() []const type {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Normalizer = *const fn ([]const u8, *Page) []const u8;
|
const Normalizer = *const fn ([]const u8, *Page) []const u8;
|
||||||
|
|
||||||
|
pub const Entry = struct {
|
||||||
|
name: String,
|
||||||
|
value: String,
|
||||||
|
|
||||||
|
pub fn format(self: Entry, writer: *std.Io.Writer) !void {
|
||||||
|
return writer.print("{f}: {f}", .{ self.name, self.value });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
pub const KeyValueList = @This();
|
pub const KeyValueList = @This();
|
||||||
|
|
||||||
_entries: std.ArrayListUnmanaged(Entry) = .empty,
|
_entries: std.ArrayListUnmanaged(Entry) = .empty,
|
||||||
@@ -85,15 +95,6 @@ pub fn fromArray(arena: Allocator, kvs: []const [2][]const u8, comptime normaliz
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const Entry = struct {
|
|
||||||
name: String,
|
|
||||||
value: String,
|
|
||||||
|
|
||||||
pub fn format(self: Entry, writer: *std.Io.Writer) !void {
|
|
||||||
return writer.print("{f}: {f}", .{ self.name, self.value });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn init() KeyValueList {
|
pub fn init() KeyValueList {
|
||||||
return .{};
|
return .{};
|
||||||
}
|
}
|
||||||
@@ -172,6 +173,77 @@ pub fn items(self: *const KeyValueList) []const Entry {
|
|||||||
return self._entries.items;
|
return self._entries.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const URLEncodeMode = enum {
|
||||||
|
form,
|
||||||
|
query,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn urlEncode(self: *const KeyValueList, comptime mode: URLEncodeMode, writer: *std.Io.Writer) !void {
|
||||||
|
const entries = self._entries.items;
|
||||||
|
if (entries.len == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try urlEncodeEntry(entries[0], mode, writer);
|
||||||
|
for (entries[1..]) |entry| {
|
||||||
|
try writer.writeByte('&');
|
||||||
|
try urlEncodeEntry(entry, mode, writer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn urlEncodeEntry(entry: Entry, comptime mode: URLEncodeMode, writer: *std.Io.Writer) !void {
|
||||||
|
try urlEncodeValue(entry.name.str(), mode, writer);
|
||||||
|
|
||||||
|
// for a form, for an empty value, we'll do "spice="
|
||||||
|
// but for a query, we do "spice"
|
||||||
|
if ((comptime mode == .query) and entry.value.len == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try writer.writeByte('=');
|
||||||
|
try urlEncodeValue(entry.value.str(), mode, writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn urlEncodeValue(value: []const u8, comptime mode: URLEncodeMode, writer: *std.Io.Writer) !void {
|
||||||
|
if (!urlEncodeShouldEscape(value, mode)) {
|
||||||
|
return writer.writeAll(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (value) |b| {
|
||||||
|
if (urlEncodeUnreserved(b, mode)) {
|
||||||
|
try writer.writeByte(b);
|
||||||
|
} else if (b == ' ') {
|
||||||
|
try writer.writeByte('+');
|
||||||
|
} else if (b >= 0x80) {
|
||||||
|
// Double-encode: treat byte as Latin-1 code point, encode to UTF-8, then percent-encode
|
||||||
|
// For bytes 0x80-0xFF (U+0080 to U+00FF), UTF-8 encoding is 2 bytes:
|
||||||
|
// [0xC0 | (b >> 6), 0x80 | (b & 0x3F)]
|
||||||
|
const byte1 = 0xC0 | (b >> 6);
|
||||||
|
const byte2 = 0x80 | (b & 0x3F);
|
||||||
|
try writer.print("%{X:0>2}%{X:0>2}", .{ byte1, byte2 });
|
||||||
|
} else {
|
||||||
|
try writer.print("%{X:0>2}", .{b});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn urlEncodeShouldEscape(value: []const u8, comptime mode: URLEncodeMode) bool {
|
||||||
|
for (value) |b| {
|
||||||
|
if (!urlEncodeUnreserved(b, mode)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn urlEncodeUnreserved(b: u8, comptime mode: URLEncodeMode) bool {
|
||||||
|
return switch (b) {
|
||||||
|
'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '*' => true,
|
||||||
|
'~' => comptime mode == .form,
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub const Iterator = struct {
|
pub const Iterator = struct {
|
||||||
index: u32 = 0,
|
index: u32 = 0,
|
||||||
kv: *KeyValueList,
|
kv: *KeyValueList,
|
||||||
|
|||||||
@@ -58,6 +58,14 @@ pub fn setName(self: *Button, name: []const u8, page: *Page) !void {
|
|||||||
try self.asElement().setAttributeSafe("name", name, page);
|
try self.asElement().setAttributeSafe("name", name, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getType(self: *const Button) []const u8 {
|
||||||
|
return self.asConstElement().getAttributeSafe("type") orelse "submit";
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setType(self: *Button, typ: []const u8, page: *Page) !void {
|
||||||
|
try self.asElement().setAttributeSafe("type", typ, page);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn getValue(self: *const Button) []const u8 {
|
pub fn getValue(self: *const Button) []const u8 {
|
||||||
return self.asConstElement().getAttributeSafe("value") orelse "";
|
return self.asConstElement().getAttributeSafe("value") orelse "";
|
||||||
}
|
}
|
||||||
@@ -116,6 +124,7 @@ pub const JsApi = struct {
|
|||||||
pub const required = bridge.accessor(Button.getRequired, Button.setRequired, .{});
|
pub const required = bridge.accessor(Button.getRequired, Button.setRequired, .{});
|
||||||
pub const form = bridge.accessor(Button.getForm, null, .{});
|
pub const form = bridge.accessor(Button.getForm, null, .{});
|
||||||
pub const value = bridge.accessor(Button.getValue, Button.setValue, .{});
|
pub const value = bridge.accessor(Button.getValue, Button.setValue, .{});
|
||||||
|
pub const @"type" = bridge.accessor(Button.getType, Button.setType, .{});
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Build = struct {
|
pub const Build = struct {
|
||||||
|
|||||||
@@ -88,6 +88,10 @@ pub fn getLength(self: *Form, page: *Page) !u32 {
|
|||||||
return elements.length(page);
|
return elements.length(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn submit(self: *Form, page: *Page) !void {
|
||||||
|
return page.submitForm(null, self);
|
||||||
|
}
|
||||||
|
|
||||||
pub const JsApi = struct {
|
pub const JsApi = struct {
|
||||||
pub const bridge = js.Bridge(Form);
|
pub const bridge = js.Bridge(Form);
|
||||||
pub const Meta = struct {
|
pub const Meta = struct {
|
||||||
@@ -100,6 +104,7 @@ pub const JsApi = struct {
|
|||||||
pub const method = bridge.accessor(Form.getMethod, Form.setMethod, .{});
|
pub const method = bridge.accessor(Form.getMethod, Form.setMethod, .{});
|
||||||
pub const elements = bridge.accessor(Form.getElements, null, .{});
|
pub const elements = bridge.accessor(Form.getElements, null, .{});
|
||||||
pub const length = bridge.accessor(Form.getLength, null, .{});
|
pub const length = bridge.accessor(Form.getLength, null, .{});
|
||||||
|
pub const submit = bridge.function(Form.submit, .{});
|
||||||
};
|
};
|
||||||
|
|
||||||
const testing = @import("../../../../testing.zig");
|
const testing = @import("../../../../testing.zig");
|
||||||
|
|||||||
@@ -61,6 +61,19 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*UIEvent {
|
|||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn as(self: *UIEvent, comptime T: type) *T {
|
||||||
|
return self.is(T).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is(self: *UIEvent, comptime T: type) ?*T {
|
||||||
|
switch (self._type) {
|
||||||
|
.generic => return if (T == UIEvent) self else null,
|
||||||
|
.mouse_event => |e| return if (T == @import("MouseEvent.zig")) e else null,
|
||||||
|
.keyboard_event => |e| return if (T == @import("KeyboardEvent.zig")) e else null,
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn populateFromOptions(self: *UIEvent, opts: anytype) void {
|
pub fn populateFromOptions(self: *UIEvent, opts: anytype) void {
|
||||||
self._detail = opts.detail;
|
self._detail = opts.detail;
|
||||||
self._view = opts.view;
|
self._view = opts.view;
|
||||||
|
|||||||
@@ -87,6 +87,21 @@ pub fn forEach(self: *FormData, cb_: js.Function, js_this_: ?js.Object) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn write(self: *const FormData, encoding_: ?[]const u8, writer: *std.Io.Writer) !void {
|
||||||
|
const encoding = encoding_ orelse {
|
||||||
|
return self._list.urlEncode(.form, writer);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (std.ascii.eqlIgnoreCase(encoding, "application/x-www-form-urlencoded")) {
|
||||||
|
return self._list.urlEncode(.form, writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug(.not_implemented, "not implemented", .{
|
||||||
|
.feature = "form data encoding",
|
||||||
|
.encoding = encoding,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pub const Iterator = struct {
|
pub const Iterator = struct {
|
||||||
index: u32 = 0,
|
index: u32 = 0,
|
||||||
list: *const FormData,
|
list: *const FormData,
|
||||||
|
|||||||
@@ -109,22 +109,7 @@ pub fn entries(self: *URLSearchParams, page: *Page) !*KeyValueList.EntryIterator
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn toString(self: *const URLSearchParams, writer: *std.Io.Writer) !void {
|
pub fn toString(self: *const URLSearchParams, writer: *std.Io.Writer) !void {
|
||||||
const items = self._params._entries.items;
|
return self._params.urlEncode(.query, writer);
|
||||||
if (items.len == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try writeEntry(&items[0], writer);
|
|
||||||
for (items[1..]) |entry| {
|
|
||||||
try writer.writeByte('&');
|
|
||||||
try writeEntry(&entry, writer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn writeEntry(entry: *const KeyValueList.Entry, writer: *std.Io.Writer) !void {
|
|
||||||
try escape(entry.name.str(), writer);
|
|
||||||
try writer.writeByte('=');
|
|
||||||
try escape(entry.value.str(), writer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn format(self: *const URLSearchParams, writer: *std.Io.Writer) !void {
|
pub fn format(self: *const URLSearchParams, writer: *std.Io.Writer) !void {
|
||||||
|
|||||||
Reference in New Issue
Block a user