╭ changed ────────────╮ │src/assets/index.html│ ╰─────────────────────╯ ╭────────────────────────────────────────────────────────────────────────╮ │28 </style> │ │29 </head> │ │30 <body> │ │31 - <div id="grid">{{{ HAXY_CONTENT }}}</div> │ │32 - <!-- script src="script.js"></script --> │ │31 + <div id="grid">{{{ HAXY_HTML }}}</div> │ │32 + <script id="page" type="text/plain">{{{ HAXY_JSON }}}</script>│ │33 + <script src="script.js"></script> │ │34 </body> │ │35 </html> │ │36 │ ╰────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────╮ │src/assets/script.js│ ╰────────────────────╯ ╭──────────────────────────────────────────────────────────────────────╮ │1 const grid = document.getElementById("grid"); │ │2 +const pageJsonBase64 = document.getElementById("page").textContent;│ │3 │ │4 let wasmInstance; │ │5 const decoder = new TextDecoder(); │ ╰──────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │27 │ │28 WebAssembly.instantiateStreaming(fetch("haxy.wasm"), importObject).then((result) => { │ │29 wasmInstance = result.instance; │ │29 - wasmInstance.exports._start(); │ │30 + │ │31 + // the page is embedded as base64 so it can sit inside the host html │ │32 + // without worrying about characters that would terminate the script tag. │ │33 + // decode here and hand the raw json bytes to the wasm. │ │34 + const jsonBytes = Uint8Array.from(atob(pageJsonBase64), (c) => c.charCodeAt(0)); │ │35 + const ptr = wasmInstance.exports._alloc(jsonBytes.length); │ │36 + new Uint8Array(wasmInstance.exports.memory.buffer, ptr, jsonBytes.length).set(jsonBytes); │ │37 + wasmInstance.exports._start(ptr, jsonBytes.length); │ │38 │ │39 document.addEventListener("keydown", (event) => { │ │32 - if (["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End"].includes(event.key)) { │ │40 + if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "PageUp", "PageDown", "Home", "End"].includes(event.key)) {│ │41 event.preventDefault(); │ │42 } │ │43 wasmInstance.exports._onKeyDown(event.keyCode); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/main_wasm.zig│ ╰─────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │15 setHtml(html); │ │16 } │ │17 │ │18 -// TODO: receive the Page as json from the host and parse it back into │ │19 -// a Page so initRoot can render real data. for now we initialize with │ │20 -// an empty page just so the wasm build compiles. │ │21 -var page_maybe: ?pg.Page = null; │ │18 +// the page is allocated into this arena via parseFromSliceLeaky. it has │ │19 +// to outlive `root`, because `root` holds slices into the parsed strings. │ │20 +var page_arena: ?std.heap.ArenaAllocator = null; │ │21 +var page: ?pg.Page = null; │ │22 │ │23 -fn start() !void { │ │24 - const page = page_maybe orelse pg.Page{ .user_repo = pg.UserRepoPage.empty() }; │ │23 +fn start(json: []const u8) !void { │ │24 + if (page_arena) |*a| a.deinit(); │ │25 + page_arena = std.heap.ArenaAllocator.init(allocator); │ │26 + │ │27 + // alloc_always so the parsed strings live entirely inside the arena │ │28 + // and don't borrow from the caller's json buffer. │ │29 + page = try std.json.parseFromSliceLeaky(pg.Page, (page_arena orelse unreachable).allocator(), json, .{│ │30 + .allocate = .alloc_always, │ │31 + }); │ │32 │ │26 - var next_root = try ui.initRoot(allocator, &page); │ │33 + var next_root = try ui.initRoot(allocator, &(page orelse unreachable)); │ │34 errdefer next_root.deinit(); │ │35 │ │36 if (root) |*old_root| old_root.deinit(); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────╮ │56 34 => .page_down, │ │57 35 => .end, │ │58 36 => .home, │ │59 + 37 => .arrow_left, │ │60 38 => .arrow_up, │ │61 + 39 => .arrow_right,│ │62 40 => .arrow_down, │ │63 else => return, │ │64 }; │ ╰───────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │76 extern fn _consoleLog(arg: [*]const u8, len: u32) void; │ │77 extern fn _setHtml(arg: [*]const u8, len: u32) void; │ │78 │ │70 -export fn _start() void { │ │71 - start() catch |err| { │ │79 +/// js calls this first to get a wasm pointer it can write the page json into. │ │80 +export fn _alloc(len: u32) ?[*]u8 { │ │81 + const slice = allocator.alloc(u8, len) catch return null; │ │82 + return slice.ptr; │ │83 +} │ │84 + │ │85 +export fn _start(json_ptr: [*]u8, json_len: u32) void { │ │86 + const json = json_ptr[0..json_len]; │ │87 + defer allocator.free(json); │ │88 + start(json) catch |err| { │ │89 var buf: [256]u8 = undefined; │ │90 const str = std.fmt.bufPrint(&buf, "start: {}", .{err}) catch unreachable;│ │91 consoleLog(str); │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 98 const content = try ui.generateHtml(allocator, &root); │ │ 99 defer allocator.free(content); │ │100 │ │101 - const needle = "{{{ HAXY_CONTENT }}}"; │ │102 - const index = std.mem.indexOf(u8, template, needle) orelse return error.MissingTemplateToken; │ │101 + // serialize the page so the wasm side can parse it back without making │ │102 + // a second request. base64-encoded so the json can be embedded in html. │ │103 + var json: std.Io.Writer.Allocating = .init(allocator); │ │104 + defer json.deinit(); │ │105 + try std.json.Stringify.value(page, .{}, &json.writer); │ │106 │ │107 + const b64 = std.base64.standard.Encoder; │ │108 + const json_b64 = try allocator.alloc(u8, b64.calcSize(json.written().len)); │ │109 + defer allocator.free(json_b64); │ │110 + _ = b64.encode(json_b64, json.written()); │ │111 + │ │112 var out: std.ArrayList(u8) = .empty; │ │113 errdefer out.deinit(allocator); │ │106 - try out.appendSlice(allocator, template[0..index]); │ │107 - try out.appendSlice(allocator, content); │ │108 - try out.appendSlice(allocator, template[index + needle.len ..]); │ │114 + { │ │115 + var cursor: usize = 0; │ │116 + for (&[_]struct { needle: []const u8, replacement: []const u8 }{ │ │117 + .{ .needle = "{{{ HAXY_HTML }}}", .replacement = content }, │ │118 + .{ .needle = "{{{ HAXY_JSON }}}", .replacement = json_b64 }, │ │119 + }) |sub| { │ │120 + const idx = std.mem.indexOfPos(u8, template, cursor, sub.needle) orelse return error.MissingTemplateToken;│ │121 + try out.appendSlice(allocator, template[cursor..idx]); │ │122 + try out.appendSlice(allocator, sub.replacement); │ │123 + cursor = idx + sub.needle.len; │ │124 + } │ │125 + try out.appendSlice(allocator, template[cursor..]); │ │126 + } │ │127 return try out.toOwnedSlice(allocator); │ │128 } │ │129 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯