diff --git a/src/main/controllers.zig b/src/main/controllers.zig index 98cbcf5..ae62126 100644 --- a/src/main/controllers.zig +++ b/src/main/controllers.zig @@ -90,6 +90,8 @@ pub fn EndpointRequest(comptime Endpoint: type) type { body: Body, query: Query, + mount_path: []const u8, + const args_middleware = //if (Args == void) //mdw.injectContext(.{ .args = {} }) //else @@ -120,6 +122,8 @@ fn CallApiEndpoint(comptime Endpoint: type) type { .args = ctx.args, .body = ctx.body, .query = ctx.query_params, + + .mount_path = if (@hasField(@TypeOf(ctx), "mounted_at")) ctx.mounted_at else "", }; var response = Response{ .headers = http.Fields.init(ctx.allocator), .res = res }; diff --git a/src/main/controllers/web.zig b/src/main/controllers/web.zig index eed3e5f..cde66a8 100644 --- a/src/main/controllers/web.zig +++ b/src/main/controllers/web.zig @@ -245,8 +245,22 @@ const drive = struct { const info = try srv.driveGet(req.args.path); defer util.deepFree(srv.allocator, info); + var breadcrumbs = std.ArrayList([]const u8).init(srv.allocator); + defer breadcrumbs.deinit(); + + var iter = util.PathIter.from(req.args.path); + while (iter.next()) |p| { + std.log.debug("breadcrumb: {s}", .{p}); + try breadcrumbs.append(if (p.len != 0) p else continue); + } + switch (info) { - .dir => |dir| try res.template(.ok, srv, dir_tmpl, .{ .dir = dir }), + .dir => |dir| try res.template(.ok, srv, dir_tmpl, .{ + .dir = dir, + .breadcrumbs = breadcrumbs.items, + .mount_path = req.mount_path, + .base_drive_path = "drive", + }), else => unreachable, } } diff --git a/src/main/controllers/web/drive/directory.tmpl.html b/src/main/controllers/web/drive/directory.tmpl.html index 54f2136..f6d59bf 100644 --- a/src/main/controllers/web/drive/directory.tmpl.html +++ b/src/main/controllers/web/drive/directory.tmpl.html @@ -1,29 +1,52 @@
+ {#for .dir.children.? |$child| =} {#switch $child case dir |$dir| =} - - - + + {#case file |$file|} - - + - diff --git a/src/template/lib.zig b/src/template/lib.zig index 77460bd..4e8803c 100644 --- a/src/template/lib.zig +++ b/src/template/lib.zig @@ -82,13 +82,15 @@ fn executeStatement( const iterable = try evaluateExpression(loop.header.iterable, args, captures, context); const subtemplate = loop.subtemplate; //std.log.debug("{any}", .{subtemplate}); - for (iterable) |v| { + for (iterable) |v, i| { + const with_item_capture = addCapture(captures, loop.header.item_capture, v); + const with_idx_capture = if (comptime loop.header.idx_capture) |name| addCapture(with_item_capture, name, i) else with_item_capture; try executeTemplate( writer, templates, subtemplate, args, - addCapture(captures, loop.header.capture, v), + with_idx_capture, context, ); } @@ -211,35 +213,19 @@ fn print(writer: anytype, arg: anytype) !void { try std.fmt.format(writer, "{}", .{arg}); } -const DerefError = error{NullOptional}; -fn Deref(comptime T: type, comptime names: []const DerefDecl) type { - if (names.len == 0) return T; +const ExpressionError = error{ IndexOutOfBounds, NullOptional }; - // Compiler segfaults when I use std.meta to get this info so we search it manually - const F = switch (names[0]) { - .field => |name| blk: { - const field = for (@typeInfo(T).Struct.fields) |f| { - if (std.mem.eql(u8, f.name, name)) break f; - } else @compileError("Unknown field " ++ name ++ " in type " ++ @typeName(T)); - break :blk field.field_type; - }, - .optional_unwrap => std.meta.Child(T), - }; - - return Deref(F, names[1..]); -} - -fn deref(arg: anytype, comptime names: []const DerefDecl) DerefError!Deref(@TypeOf(arg), names) { - if (names.len == 0) return arg; - - switch (names[0]) { - .field => |name| return deref(@field(arg, name), names[1..]), - .optional_unwrap => return arg orelse error.NullOptional, +fn Deref(comptime T: type, comptime field: []const u8) type { + if (std.meta.trait.isIndexable(T) and std.mem.eql(u8, field, "len")) return usize; + switch (@typeInfo(T)) { + .Pointer => return Deref(std.meta.Child(T), field), + .Struct => |info| for (info.fields) |f| { + if (std.mem.eql(u8, field, f.name)) return f.field_type; + } else @compileError("Field " ++ field ++ " does not exist on type " ++ @typeName(T)), + else => @compileError("Cannot retrieve field " ++ field ++ " from type " ++ @typeName(T)), } } -const ExpressionError = error{IndexOutOfBounds} || DerefError; - fn EvaluateExpression( comptime expression: Expression, comptime Args: type, @@ -247,14 +233,20 @@ fn EvaluateExpression( comptime Context: type, ) type { return switch (expression) { - .arg_deref => |names| Deref(Args, names), - .capture_deref => |names| Deref(Captures, names), - .context_deref => |names| Deref(Context, names), + .args => Args, + .captures => Captures, + .context => Context, + .deref => |expr| { + const T = EvaluateExpression(expr.container, Args, Captures, Context); + return Deref(T, expr.field); + }, .equals => bool, .builtin => |call| switch (call.*) { .isTag => bool, .slice => |sl| []const std.meta.Elem(EvaluateExpression(sl.iterable, Args, Captures, Context)), }, + .optional_unwrap => |expr| std.meta.Child(EvaluateExpression(expr.*, Args, Captures, Context)), + .int => isize, }; } @@ -265,9 +257,15 @@ fn evaluateExpression( context: anytype, ) ExpressionError!EvaluateExpression(expression, @TypeOf(args), @TypeOf(captures), @TypeOf(context)) { return switch (expression) { - .arg_deref => |names| try deref(args, names), - .capture_deref => |names| try deref(captures, names), - .context_deref => |names| try deref(context, names), + .args => args, + .captures => captures, + .context => context, + .deref => |expr| { + return @field( + try evaluateExpression(expr.container, args, captures, context), + expr.field, + ); + }, .equals => |eql| { const lhs = try evaluateExpression(eql.lhs, args, captures, context); const rhs = try evaluateExpression(eql.rhs, args, captures, context); @@ -285,14 +283,19 @@ fn evaluateExpression( }, .slice => |sl| { const iterable = try evaluateExpression(sl.iterable, args, captures, context); - const start = try evaluateExpression(sl.start, args, captures, context); - const end = try evaluateExpression(sl.end, args, captures, context); + const start = std.math.cast(usize, try evaluateExpression(sl.start, args, captures, context)) orelse return error.IndexOutOfBounds; + const end = std.math.cast(usize, try evaluateExpression(sl.end, args, captures, context)) orelse return error.IndexOutOfBounds; if (comptime std.meta.trait.is(.Array)(@TypeOf(iterable))) @compileError("Cannot slice an array, pass a slice or pointer to array instead"); if (start > iterable.len or end > iterable.len) return error.IndexOutOfBounds; return iterable[start..end]; }, }, + .optional_unwrap => |expr| { + const val = try evaluateExpression(expr.*, args, captures, context); + return val orelse error.NullOptional; + }, + .int => |i| return i, }; } @@ -306,12 +309,14 @@ fn AddCapture(comptime Root: type, comptime name: []const u8, comptime Val: type .alignment = @alignOf(Val), }}; - return @Type(.{ .Struct = .{ + const Result = @Type(.{ .Struct = .{ .layout = .Auto, .fields = fields, .decls = &.{}, .is_tuple = false, } }); + + return Result; } fn addCapture(root: anytype, comptime name: []const u8, val: anytype) AddCapture(@TypeOf(root), name, @TypeOf(val)) { @@ -522,7 +527,7 @@ fn parseTemplateTokens(comptime tokens: ControlTokenIter) []const TemplateToken while (iter.next()) |token| switch (token) { .whitespace => |wsp| items = items ++ [_]TemplateToken{.{ .whitespace = wsp }}, - .text => |text| items = items ++ [_]TemplateToken{.{ .text = text }}, + .number, .text => |text| items = items ++ [_]TemplateToken{.{ .text = text }}, .open_bracket => { const next = iter.next() orelse @compileError("Unexpected end of template"); if (next == .open_bracket) { @@ -560,60 +565,113 @@ fn parseTemplateTokens(comptime tokens: ControlTokenIter) []const TemplateToken } } +fn tryParseIdentifier(comptime tokens: ControlTokenIter) ?ParseResult(ControlTokenIter, []const u8) { + comptime { + var iter = skipWhitespace(tokens); + + var ident: []const u8 = ""; + var first: bool = true; + while (iter.next()) |token| switch (token) { + .number, .text => |text| { + if (first and token == .number) return null; + ident = ident ++ text; + first = false; + }, + else => { + iter.putBack(token); + break; + }, + }; + + if (first) return null; + + return ParseResult(ControlTokenIter, []const u8){ + .new_iter = iter, + .item = ident, + }; + } +} + fn parseExpression(comptime tokens: ControlTokenIter) ParseResult(ControlTokenIter, Expression) { comptime { var iter = tokens; var last_valid_iter: ?ControlTokenIter = null; var expr: ?Expression = null; - while (iter.next()) |token| switch (token) { - .whitespace => {}, - .period => { - const names = parseDeref(iter); - iter = names.new_iter; - if (expr != null) break; - expr = .{ .arg_deref = names.item }; - last_valid_iter = iter; - }, - .dollar => { - const names = parseDeref(iter); - iter = names.new_iter; - if (expr != null) break; - expr = .{ .capture_deref = names.item }; - last_valid_iter = iter; - }, - .percent => { - const names = parseDeref(iter); - iter = names.new_iter; - if (expr != null) break; - expr = .{ .context_deref = names.item }; - last_valid_iter = iter; - }, - .equals => { - const next = iter.next() orelse break; - if (next == .equals) { - const lhs = expr orelse break; - const rhs = parseExpression(iter); - iter = rhs.new_iter; + while (iter.next()) |token| { + switch (token) { + .whitespace => {}, + .period => { + iter = skipWhitespace(iter); + if (expr == null) { + expr = .{ .args = {} }; + if (iter.peek()) |n| if (n == .text) iter.putBack(.{ .period = {} }); + } else if (tryParseIdentifier(iter)) |ident| { + iter = ident.new_iter; - expr = .{ - .equals = &.{ - .lhs = lhs, - .rhs = rhs.item, - }, + expr = .{ + .deref = &.{ + .container = expr.?, + .field = ident.item, + }, + }; + } else if (iter.peek()) |next| if (next == .question_mark) { + _ = iter.next(); + expr = .{ + .optional_unwrap = blk: { + const e = expr.?; + break :blk &e; + }, + }; }; last_valid_iter = iter; - } else break; - }, - .at => { - if (expr != null) break; - const builtin = parseBuiltin(iter); - iter = builtin.new_iter; - expr = .{ .builtin = &builtin.item }; - last_valid_iter = iter; - }, - else => break, - }; + }, + .dollar => { + if (expr != null) break; + iter = skipWhitespace(iter); + expr = .{ .captures = {} }; + if (iter.peek()) |n| if (n == .text) iter.putBack(.{ .period = {} }); + last_valid_iter = iter; + }, + .percent => { + if (expr != null) break; + iter = skipWhitespace(iter); + expr = .{ .context = {} }; + if (iter.peek()) |n| if (n == .text) iter.putBack(.{ .period = {} }); + last_valid_iter = iter; + }, + .equals => { + const next = iter.next() orelse break; + if (next == .equals) { + const lhs = expr orelse break; + const rhs = parseExpression(iter); + iter = rhs.new_iter; + + expr = .{ + .equals = &.{ + .lhs = lhs, + .rhs = rhs.item, + }, + }; + last_valid_iter = iter; + } else break; + }, + .at => { + if (expr != null) break; + const builtin = parseBuiltin(iter); + iter = builtin.new_iter; + expr = .{ .builtin = &builtin.item }; + last_valid_iter = iter; + }, + .number => |n| { + if (expr != null) break; + const num = std.fmt.parseInt(isize, n, 10) catch @compileError("Error parsing integer"); + expr = .{ .int = num }; + last_valid_iter = iter; + }, + else => break, + } + } return .{ .new_iter = last_valid_iter orelse @compileError("Invalid Expression"), @@ -797,7 +855,7 @@ fn parseControlBlock(comptime tokens: ControlTokenIter) ParseResult(ControlToken }, else => { @compileLog(iter.row); - @compileError("TODO " ++ @tagName(token) ++ " " ++ token.text); + @compileError("TODO " ++ @tagName(token)); }, }; @@ -830,32 +888,21 @@ fn endControlBlock(comptime tokens: ControlTokenIter) ControlTokenIter { fn parseForHeader(comptime tokens: ControlTokenIter) ParseResult(ControlTokenIter, ForHeader) { comptime { const iterable = parseExpression(tokens); - var iter = iterable.new_iter; + var iter = skipWhitespace(iterable.new_iter); - iter = skipWhitespace(iter); - { - const token = iter.next() orelse @compileError("Unexpected end of template"); - if (token != .pipe) @compileError("Unexpected token"); - } - { - const token = iter.next() orelse @compileError("Unexpected end of template"); - if (token != .dollar) @compileError("Unexpected token"); - } - const capture = blk: { - const token = iter.next() orelse @compileError("Unexpected end of template"); - if (token != .text) @compileError("Unexpected token"); - break :blk token.text; + const captures = tryParseCapture(iter) orelse { + @compileLog(iter.row); + @compileError("Expected capture"); }; - { - const token = iter.next() orelse @compileError("Unexpected end of template"); - if (token != .pipe) @compileError("Unexpected token"); - } + + if (captures.item.len == 0 or captures.item.len > 2) @compileError("Expected 1 or 2 captures"); return .{ - .new_iter = iter, + .new_iter = captures.new_iter, .item = .{ .iterable = iterable.item, - .capture = capture, + .item_capture = captures.item[0], + .idx_capture = if (captures.item.len == 2) captures.item[1] else null, }, }; } @@ -863,9 +910,8 @@ fn parseForHeader(comptime tokens: ControlTokenIter) ParseResult(ControlTokenIte fn tryParseCapture(comptime tokens: ControlTokenIter) ?ParseResult(ControlTokenIter, []const []const u8) { comptime { - var iter = tokens; + var iter = skipWhitespace(tokens); - iter = skipWhitespace(iter); if ((iter.next() orelse return null) != .pipe) return null; var captures: []const []const u8 = &.{}; while (true) { @@ -972,40 +1018,6 @@ fn parseSwitchHeader(comptime tokens: ControlTokenIter) ParseResult(ControlToken } } -fn parseDeref(comptime tokens: ControlTokenIter) ParseResult(ControlTokenIter, []const DerefDecl) { - comptime { - var iter = tokens; - var fields: []const DerefDecl = &.{}; - var wants = .text; - while (iter.peek()) |token| { - switch (token) { - .whitespace => {}, - .text => |text| { - if (wants == .period) break; - fields = fields ++ [1]DerefDecl{.{ .field = text }}; - wants = .period; - }, - .period => { - if (wants != .period) @compileError("Unexpected token \".\""); - wants = .text; - }, - .question_mark => { - if (wants == .period) break; - fields = fields ++ [1]DerefDecl{.{ .optional_unwrap = {} }}; - wants = .period; - }, - else => if (wants == .period or fields.len == 0) break else @compileError("Unexpected token"), - } - _ = iter.next(); - } - - return .{ - .new_iter = iter, - .item = fields, - }; - } -} - fn parseCallTemplate(comptime tokens: ControlTokenIter) ParseResult(ControlTokenIter, CallTemplate) { comptime { var iter = tokens; @@ -1034,7 +1046,7 @@ fn parseFormat(comptime tokens: ControlTokenIter) ParseResult(ControlTokenIter, expectToken(iter.next(), .double_quote); var fmt_str: []const u8 = ""; while (true) switch (iter.next() orelse @compileError("Unexpected end of template")) { - .text, .whitespace => |t| fmt_str = fmt_str ++ t, + .text, .number, .whitespace => |t| fmt_str = fmt_str ++ t, .open_bracket => fmt_str = fmt_str ++ "{", .close_bracket => fmt_str = fmt_str ++ "}", .period => fmt_str = fmt_str ++ ".", @@ -1076,9 +1088,9 @@ const TemplateItem = union(enum) { statement: Statement, }; -const DerefDecl = union(enum) { +const DerefExpr = struct { + container: Expression, field: []const u8, - optional_unwrap: void, }; const EqualsExpr = struct { @@ -1087,11 +1099,14 @@ const EqualsExpr = struct { }; const Expression = union(enum) { - arg_deref: []const DerefDecl, - capture_deref: []const DerefDecl, - context_deref: []const DerefDecl, + args: void, + captures: void, + context: void, + deref: *const DerefExpr, equals: *const EqualsExpr, builtin: *const BuiltinCall, + optional_unwrap: *const Expression, + int: isize, }; const For = struct { @@ -1101,7 +1116,8 @@ const For = struct { const ForHeader = struct { iterable: Expression, - capture: []const u8, + item_capture: []const u8, + idx_capture: ?[]const u8, }; const If = struct { @@ -1214,6 +1230,7 @@ const BuiltinCall = union(Builtin) { const ControlToken = union(enum) { text: []const u8, + number: []const u8, open_bracket: void, close_bracket: void, period: void, @@ -1235,14 +1252,29 @@ const ControlToken = union(enum) { const ControlTokenIter = struct { start: usize = 0, text: []const u8, - peeked_token: ?ControlToken = null, + peeked_tokens: [2]?ControlToken = [2]?ControlToken{ null, null }, + peeked_token_count: usize = 0, row: usize = 0, + fn isControlChar(ch: u8) bool { + return switch (ch) { + '{', '}', '.', '#', '|', '$', '/', '=', '@', ',', '%', '(', ')', '"', '?' => true, + + else => false, + }; + } + + fn isTextChar(ch: u8) bool { + return !std.ascii.isWhitespace(ch) and !std.ascii.isDigit(ch) and !isControlChar(ch); + } + fn next(self: *ControlTokenIter) ?ControlToken { - if (self.peeked_token) |token| { - self.peeked_token = null; - return token; + if (self.peeked_token_count != 0) { + const t = self.peeked_tokens[self.peeked_token_count - 1].?; + self.peeked_tokens[self.peeked_token_count - 1] = null; + self.peeked_token_count -= 1; + return t; } const remaining = self.text[self.start..]; @@ -1275,9 +1307,16 @@ const ControlTokenIter = struct { self.start += idx - 1; return .{ .whitespace = remaining[0..idx] }; }, + '0'...'9' => { + var idx: usize = 0; + while (idx < remaining.len and std.ascii.isDigit(remaining[idx])) : (idx += 1) {} + + self.start += idx - 1; + return .{ .number = remaining[0..idx] }; + }, else => { var idx: usize = 0; - while (idx < remaining.len and std.mem.indexOfScalar(u8, "{}.#|$/=@,%()\"? \t\n\r", remaining[idx]) == null) : (idx += 1) {} + while (idx < remaining.len and isTextChar(remaining[idx])) : (idx += 1) {} self.start += idx - 1; return .{ .text = remaining[0..idx] }; @@ -1287,13 +1326,14 @@ const ControlTokenIter = struct { fn peek(self: *ControlTokenIter) ?ControlToken { const token = self.next(); - self.peeked_token = token; + if (token) |t| self.putBack(t); return token; } fn putBack(self: *ControlTokenIter, token: ControlToken) void { - std.debug.assert(self.peeked_token == null); - self.peeked_token = token; + std.debug.assert(self.peeked_token_count < self.peeked_tokens.len); + self.peeked_tokens[self.peeked_token_count] = token; + self.peeked_token_count += 1; } }; @@ -1310,8 +1350,8 @@ test "template" { try testCase("abcd", .{}, "abcd"); try testCase("{.val}", .{ .val = 3 }, "3"); try testCase("{#if .val}1{/if}", .{ .val = true }, "1"); - try testCase("{#for .vals |$v|}{$v}{/for}", .{ .vals = [_]u8{ 1, 2, 3 } }, "123"); try testCase("{#for .vals |$v|=} {$v} {=/for}", .{ .vals = [_]u8{ 1, 2, 3 } }, "123"); + try testCase("{#for .vals |$val|}{$val}{/for}", .{ .vals = [_]u8{ 1, 2, 3 } }, "123"); try testCase("{#if .val}1{#else}0{/if}", .{ .val = true }, "1"); try testCase("{#if .val}1{#else}0{/if}", .{ .val = false }, "0"); try testCase("{#if .val}1{#elif .foo}2{/if}", .{ .val = false, .foo = true }, "2"); diff --git a/src/template/test.tmp.html b/src/template/test.tmp.html index 1b484b9..7023179 100644 --- a/src/template/test.tmp.html +++ b/src/template/test.tmp.html @@ -14,6 +14,7 @@ {$b}: {= /for =} {= /for} + {#for .baz |$f, $i| =}{$i}{/for} {#if .quxx == .quxx2}eql{#else}neq{/if} {#if .quxx == .qux}eql{#else}neq{/if} {#if @isTag(.snap, foo)}foo{/if} @@ -27,6 +28,7 @@ {=/if} sliced: {#for @slice(.foo, .start, .end) |$s|}{$s}, {/for} + sliced: {#for @slice(.foo, 1, 3) |$s|}{$s}, {/for} format: {#format "s" .x} diff --git a/static/site.css b/static/site.css index e6820a1..1a172eb 100644 --- a/static/site.css +++ b/static/site.css @@ -218,3 +218,13 @@ button:hover, a.button:hover { } } +.drive .breadcrumbs { + list-style: none; + display: flex; +} + +.drive .icons { + display: flex; + justify-content: flex-end; + vertical-align: bottom; +}
{$dir.name.?} + + {$dir.name.?} + + - {= #if %user |$u|} + + {#if %user |$u|} {#if $u.avatar_file_id == $file.meta.id =} - + {= #elif $u.header_file_id == $file.meta.id =} - + {= /if =} - {= /if =} - - {= #if $file.meta.sensitive =} - + {= /if} + {#if $file.meta.sensitive =} + {= #else =} - - {= /if =} + + {= /if} + + + {$file.name.?} + {$file.name.?} {#if $file.meta.content_type |$t|}{$t}{/if} {$file.meta.size} {$file.meta.created_at}