╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────╮ │← previous│ ╰──────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────╮ │1453 base: []const u8, │ │1454 host: Host, │ │1455 ) !void { │ │1445 - const parts = removeParts(base) orelse return respondRemoveNotFound(request); │ │1456 + const parts = removeParts(host, base) orelse return respondRemoveNotFound(request);│ │1457 │ │1458 var author_arena = std.heap.ArenaAllocator.init(allocator); │ │1459 defer author_arena.deinit(); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────╮ │1548 } │ │1549 │ │1550 // set the status of the issue the url names (base is │ │1540 -// "/repo/<owner>/<name>/issue:<id>", identity elided in local mode) by│ │1551 +// "/<owner>:<name>/issue:<id>", identity elided in local mode) by │ │1552 // re-emitting its event, then redirect back to it │ │1553 fn handleThreadStatus( │ │1554 io: std.Io, │ ╰─────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────╮ │1559 open: bool, │ │1560 ) !void { │ │1561 const not_found = "thread not found"; │ │1551 - const parts = commentBaseParts(base) orelse { │ │1562 + const parts = commentBaseParts(host, base) orelse { │ │1563 try request.respond(not_found, .{ │ │1564 .status = .not_found, │ │1565 .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }},│ ╰──────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────╮ │1742 base: []const u8, │ │1743 host: Host, │ │1744 ) !void { │ │1734 - const parts = commentBaseParts(base) orelse { │ │1745 + const parts = commentBaseParts(host, base) orelse { │ │1746 try request.respond("thread not found", .{ │ │1747 .status = .not_found, │ │1748 .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }},│ ╰──────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1780 }); │ │1781 return; │ │1782 } │ │1772 - const route = ui.RoutablePage.fromUrl(base) orelse ui.RoutablePage.fromUrlLocal(base) orelse {│ │1783 + const route = routeFromUrl(host, base) orelse { │ │1784 try request.respond("thread not found", .{ │ │1785 .status = .not_found, │ │1786 .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }}, │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1837 // the posted path drops the action's own segment, so the route it parses is │ │1838 // the undo page the action applies to, never the confirmation page itself │ │1839 fn handleUndo(io: std.Io, request: *std.http.Server.Request, allocator: std.mem.Allocator, base: []const u8, host: Host, action: enum { undo, clear }) !void {│ │1829 - const route = (switch (host) { │ │1830 - .local => ui.RoutablePage.fromUrlLocal(base), │ │1831 - .server => ui.RoutablePage.fromUrl(base), │ │1832 - }) orelse return respondRepoNotFound(request); │ │1840 + const route = routeFromUrl(host, base) orelse return respondRepoNotFound(request); │ │1841 if (route != .repo_undo) return respondRepoNotFound(request); │ │1842 const target = route.repo_undo; │ │1843 var arena = std.heap.ArenaAllocator.init(allocator); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1844 defer arena.deinit(); │ │1837 - const repo_base = if (target.name.len == 0) "" else try std.fmt.allocPrint(arena.allocator(), "/repo/{s}", .{target.name.slice()});│ │1845 + const repo_base = if (target.name.len == 0) "" else try std.fmt.allocPrint(arena.allocator(), "/{s}", .{target.name.slice()}); │ │1846 _ = (try authorizeWrite(io, allocator, &arena, request, host, repo_base, .owner)) orelse return; │ │1847 const resolved = (try requestRepoSource(io, allocator, host, repo_base)) orelse return respondRepoNotFound(request); │ │1848 defer resolved.deinit(allocator); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2000 } │ │2001 │ │2002 fn findEmbed(request_path: []const u8) ?Embed { │ │1995 - const path = if (std.mem.eql(u8, request_path, "/")) │ │1996 - "index.html" │ │1997 - else if (request_path.len > 1 and request_path[0] == '/') │ │1998 - request_path[1..] │ │1999 - else │ │2000 - return null; │ │2003 + // embeds are matched before pages, so a dot keeps them from claiming a username or tab │ │2004 + comptime for (embeds) |embed| { │ │2005 + if (std.mem.indexOfScalar(u8, embed.path, '.') == null) @compileError("embed path needs a '.' so it can't shadow a page: " ++ embed.path);│ │2006 + }; │ │2007 + if (request_path.len < 2 or request_path[0] != '/') return null; │ │2008 + const path = request_path[1..]; │ │2009 │ │2010 for (embeds) |embed| { │ │2011 if (std.mem.eql(u8, path, embed.path)) return embed; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯