From 8b1c0fa40861947a2a9386c0ad610f178d9947e9 Mon Sep 17 00:00:00 2001 From: Loris Cro Date: Fri, 26 Jul 2024 17:22:19 +0200 Subject: [PATCH] implemented check and interface, lsp accepts languageid aliases --- src/Ast.zig | 75 ++++++++++++ src/cli.zig | 7 +- src/cli/check.zig | 274 ++++++++++++++++++++++++++++++++++++++++++ src/cli/interface.zig | 169 ++++++++++++++++++++++++++ src/cli/lsp.zig | 2 +- src/root.zig | 15 +++ 6 files changed, 538 insertions(+), 4 deletions(-) create mode 100644 src/cli/check.zig create mode 100644 src/cli/interface.zig diff --git a/src/Ast.zig b/src/Ast.zig index 8e4be95..e14126c 100644 --- a/src/Ast.zig +++ b/src/Ast.zig @@ -1746,6 +1746,81 @@ const Parser = struct { } }; +pub fn printInterfaceAsHtml( + ast: Ast, + html_ast: html.Ast, + path: ?[]const u8, + out: anytype, +) !void { + if (path) |p| { + try out.print("\n", .{p}); + } + var it = ast.interface.iterator(); + var at_least_one = false; + while (it.next()) |kv| { + at_least_one = true; + const id = kv.key_ptr.*; + const parent_idx = kv.value_ptr.*; + const tag_name = ast.nodes[parent_idx].superBlock( + ast.src, + html_ast, + ).parent_tag_name.slice(ast.src); + try out.print("<{s} id=\"{s}\">\n", .{ + tag_name, + id, + tag_name, + }); + } + + if (!at_least_one) { + try out.print( + \\ + \\ + \\ + , .{}); + } +} + +pub fn printErrors(ast: Ast, src: []const u8, path: ?[]const u8) void { + for (ast.errors) |err| { + const range = err.main_location.range(src); + std.debug.print("{s}:{}:{}: {s}\n", .{ + path orelse "", + range.start.row, + range.start.col, + @tagName(err.kind), + }); + } +} + +pub fn interfaceFormatter( + ast: Ast, + html_ast: html.Ast, + path: ?[]const u8, +) Formatter { + return .{ .ast = ast, .html = html_ast, .path = path }; +} +const Formatter = struct { + ast: Ast, + html: html.Ast, + path: ?[]const u8, + + pub fn format( + f: Formatter, + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + out_stream: anytype, + ) !void { + _ = fmt; + _ = options; + + try f.ast.printInterfaceAsHtml(f.html, f.path, out_stream); + } +}; + fn is(str1: []const u8, str2: []const u8) bool { return std.ascii.eqlIgnoreCase(str1, str2); } diff --git a/src/cli.zig b/src/cli.zig index 678410b..e043717 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -3,6 +3,8 @@ const builtin = @import("builtin"); const build_options = @import("build_options"); const super = @import("super"); const logging = @import("cli/logging.zig"); +const interface_exe = @import("cli/interface.zig"); +const check_exe = @import("cli/check.zig"); const fmt_exe = @import("cli/fmt.zig"); const lsp_exe = @import("cli/lsp.zig"); @@ -80,13 +82,12 @@ pub fn main() !void { if (cmd == .lsp) lsp_mode = true; _ = switch (cmd) { - // .check => check_exe.run(gpa, args[2..]), - // .interface, .i => interface_exe.run(gpa, args[2..]), + .check => check_exe.run(gpa, args[2..]), + .interface, .i => interface_exe.run(gpa, args[2..]), .fmt => fmt_exe.run(gpa, args[2..]), .lsp => lsp_exe.run(gpa, args[2..]), .help => fatalHelp(), .version => printVersion(), - else => fatalHelp(), } catch |err| fatal("unexpected error: {s}\n", .{@errorName(err)}); } diff --git a/src/cli/check.zig b/src/cli/check.zig new file mode 100644 index 0000000..4688234 --- /dev/null +++ b/src/cli/check.zig @@ -0,0 +1,274 @@ +const std = @import("std"); +const super = @import("superhtml"); + +const FileType = enum { html, super }; + +pub fn run(gpa: std.mem.Allocator, args: []const []const u8) !void { + const cmd = Command.parse(args); + var any_error = false; + switch (cmd.mode) { + .stdin => { + var buf = std.ArrayList(u8).init(gpa); + try std.io.getStdIn().reader().readAllArrayList(&buf, super.max_size); + const in_bytes = try buf.toOwnedSliceSentinel(0); + + try checkHtml(gpa, null, in_bytes); + }, + .stdin_super => { + var buf = std.ArrayList(u8).init(gpa); + try std.io.getStdIn().reader().readAllArrayList(&buf, super.max_size); + const in_bytes = try buf.toOwnedSliceSentinel(0); + + try checkSuper(gpa, null, in_bytes); + }, + .paths => |paths| { + // checkFile will reset the arena at the end of each call + var arena_impl = std.heap.ArenaAllocator.init(gpa); + for (paths) |path| { + checkFile( + &arena_impl, + std.fs.cwd(), + path, + path, + &any_error, + ) catch |err| switch (err) { + error.IsDir, error.AccessDenied => { + checkDir( + gpa, + &arena_impl, + path, + &any_error, + ) catch |dir_err| { + std.debug.print("Error walking dir '{s}': {s}\n", .{ + path, + @errorName(dir_err), + }); + std.process.exit(1); + }; + }, + else => { + std.debug.print("Error while accessing '{s}': {s}\n", .{ + path, @errorName(err), + }); + std.process.exit(1); + }, + }; + } + }, + } + + if (any_error) { + std.process.exit(1); + } +} + +fn checkDir( + gpa: std.mem.Allocator, + arena_impl: *std.heap.ArenaAllocator, + path: []const u8, + any_error: *bool, +) !void { + var dir = try std.fs.cwd().openDir(path, .{ .iterate = true }); + defer dir.close(); + var walker = dir.walk(gpa) catch oom(); + defer walker.deinit(); + while (try walker.next()) |item| { + switch (item.kind) { + .file => { + try checkFile( + arena_impl, + item.dir, + item.basename, + item.path, + any_error, + ); + }, + else => {}, + } + } +} + +fn checkFile( + arena_impl: *std.heap.ArenaAllocator, + base_dir: std.fs.Dir, + sub_path: []const u8, + full_path: []const u8, + any_error: *bool, +) !void { + _ = any_error; + defer _ = arena_impl.reset(.retain_capacity); + const arena = arena_impl.allocator(); + + const file = try base_dir.openFile(sub_path, .{}); + defer file.close(); + + const stat = try file.stat(); + if (stat.kind == .directory) + return error.IsDir; + + const file_type: FileType = blk: { + const ext = std.fs.path.extension(sub_path); + if (std.mem.eql(u8, ext, ".html") or + std.mem.eql(u8, ext, ".htm")) + { + break :blk .html; + } + + if (std.mem.eql(u8, ext, ".shtml")) { + break :blk .super; + } + return; + }; + + var buf = std.ArrayList(u8).init(arena); + defer buf.deinit(); + + try file.reader().readAllArrayList(&buf, super.max_size); + + const in_bytes = try buf.toOwnedSliceSentinel(0); + + switch (file_type) { + .html => try checkHtml( + arena, + full_path, + in_bytes, + ), + .super => try checkSuper( + arena, + full_path, + in_bytes, + ), + } +} + +pub fn checkHtml( + arena: std.mem.Allocator, + path: ?[]const u8, + code: [:0]const u8, +) !void { + const ast = try super.html.Ast.init(arena, code, .html); + if (ast.errors.len > 0) { + ast.printErrors(code, path); + std.process.exit(1); + } +} + +fn checkSuper( + arena: std.mem.Allocator, + path: ?[]const u8, + code: [:0]const u8, +) !void { + const html = try super.html.Ast.init(arena, code, .superhtml); + if (html.errors.len > 0) { + html.printErrors(code, path); + std.process.exit(1); + } + + const s = try super.Ast.init(arena, html, code); + if (s.errors.len > 0) { + s.printErrors(code, path); + std.process.exit(1); + } +} + +fn oom() noreturn { + std.debug.print("Out of memory\n", .{}); + std.process.exit(1); +} + +const Command = struct { + mode: Mode, + + const Mode = union(enum) { + stdin, + stdin_super, + paths: []const []const u8, + }; + + fn parse(args: []const []const u8) Command { + var mode: ?Mode = null; + + var idx: usize = 0; + while (idx < args.len) : (idx += 1) { + const arg = args[idx]; + if (std.mem.eql(u8, arg, "--help") or + std.mem.eql(u8, arg, "-h")) + { + fatalHelp(); + } + + if (std.mem.startsWith(u8, arg, "-")) { + if (std.mem.eql(u8, arg, "--stdin") or + std.mem.eql(u8, arg, "-")) + { + if (mode != null) { + std.debug.print("unexpected flag: '{s}'\n", .{arg}); + std.process.exit(1); + } + + mode = .stdin; + } else if (std.mem.eql(u8, arg, "--stdin-super")) { + if (mode != null) { + std.debug.print("unexpected flag: '{s}'\n", .{arg}); + std.process.exit(1); + } + + mode = .stdin_super; + } else { + std.debug.print("unexpected flag: '{s}'\n", .{arg}); + std.process.exit(1); + } + } else { + const paths_start = idx; + while (idx < args.len) : (idx += 1) { + if (std.mem.startsWith(u8, args[idx], "-")) { + break; + } + } + idx -= 1; + + if (mode != null) { + std.debug.print( + "unexpected path argument(s): '{s}'...\n", + .{args[paths_start]}, + ); + std.process.exit(1); + } + + const paths = args[paths_start .. idx + 1]; + mode = .{ .paths = paths }; + } + } + + const m = mode orelse { + std.debug.print("missing argument(s)\n\n", .{}); + fatalHelp(); + }; + + return .{ .mode = m }; + } + + fn fatalHelp() noreturn { + std.debug.print( + \\Usage: super check PATH [PATH...] [OPTIONS] + \\ + \\ Checks for syntax errors. If PATH is a directory, it will + \\ be searched recursively for HTML and SuperHTML files. + \\ + \\ Detected extensions: + \\ HTML .html, .htm + \\ SuperHTML .shtml + \\ + \\Options: + \\ + \\ --stdin Format bytes from stdin and ouptut to stdout. + \\ Mutually exclusive with other input aguments. + \\ + \\ --stdin-super Same as --stdin but for SuperHTML files. + \\ + \\ --help, -h Prints this help and exits. + , .{}); + + std.process.exit(1); + } +}; diff --git a/src/cli/interface.zig b/src/cli/interface.zig new file mode 100644 index 0000000..209ad3d --- /dev/null +++ b/src/cli/interface.zig @@ -0,0 +1,169 @@ +const std = @import("std"); +const super = @import("superhtml"); + +const FileType = enum { html, super }; + +pub fn run(gpa: std.mem.Allocator, args: []const []const u8) !void { + const cmd = Command.parse(args); + switch (cmd.mode) { + .stdin => { + var buf = std.ArrayList(u8).init(gpa); + try std.io.getStdIn().reader().readAllArrayList(&buf, super.max_size); + const in_bytes = try buf.toOwnedSliceSentinel(0); + + const out_bytes = try renderInterface(gpa, null, in_bytes); + try std.io.getStdOut().writeAll(out_bytes); + }, + .path => |path| { + var arena_impl = std.heap.ArenaAllocator.init(gpa); + const out_bytes = printInterfaceFromFile( + &arena_impl, + std.fs.cwd(), + path, + path, + ) catch |err| switch (err) { + error.IsDir => { + std.debug.print("error: '{s}' is a directory\n\n", .{ + path, + }); + std.process.exit(1); + }, + else => { + std.debug.print("error while accessing '{s}': {}\n\n", .{ + path, + err, + }); + std.process.exit(1); + }, + }; + + try std.io.getStdOut().writeAll(out_bytes); + }, + } +} + +fn printInterfaceFromFile( + arena_impl: *std.heap.ArenaAllocator, + base_dir: std.fs.Dir, + sub_path: []const u8, + full_path: []const u8, +) ![]const u8 { + defer _ = arena_impl.reset(.retain_capacity); + const arena = arena_impl.allocator(); + + const file = try base_dir.openFile(sub_path, .{}); + defer file.close(); + + const stat = try file.stat(); + if (stat.kind == .directory) + return error.IsDir; + + var buf = std.ArrayList(u8).init(arena); + defer buf.deinit(); + + try file.reader().readAllArrayList(&buf, super.max_size); + + const in_bytes = try buf.toOwnedSliceSentinel(0); + + return renderInterface(arena, full_path, in_bytes); +} + +fn renderInterface( + arena: std.mem.Allocator, + path: ?[]const u8, + code: [:0]const u8, +) ![]const u8 { + const html_ast = try super.html.Ast.init(arena, code, .superhtml); + if (html_ast.errors.len > 0) { + html_ast.printErrors(code, path); + std.process.exit(1); + } + + const s = try super.Ast.init(arena, html_ast, code); + if (s.errors.len > 0) { + s.printErrors(code, path); + std.process.exit(1); + } + + return std.fmt.allocPrint(arena, "{}", .{ + s.interfaceFormatter(html_ast, path), + }); +} + +fn oom() noreturn { + std.debug.print("Out of memory\n", .{}); + std.process.exit(1); +} + +const Command = struct { + mode: Mode, + + const Mode = union(enum) { + stdin, + path: []const u8, + }; + + fn parse(args: []const []const u8) Command { + var mode: ?Mode = null; + + var idx: usize = 0; + while (idx < args.len) : (idx += 1) { + const arg = args[idx]; + if (std.mem.eql(u8, arg, "--help") or + std.mem.eql(u8, arg, "-h")) + { + fatalHelp(); + } + + if (std.mem.startsWith(u8, arg, "-")) { + if (std.mem.eql(u8, arg, "--stdin") or + std.mem.eql(u8, arg, "-")) + { + if (mode != null) { + std.debug.print("unexpected flag: '{s}'\n", .{arg}); + std.process.exit(1); + } + + mode = .stdin; + } else { + std.debug.print("unexpected flag: '{s}'\n", .{arg}); + std.process.exit(1); + } + } else { + if (mode != null) { + std.debug.print( + "unexpected path argument: '{s}'...\n", + .{args[idx]}, + ); + std.process.exit(1); + } + + mode = .{ .path = args[idx] }; + } + } + + const m = mode orelse { + std.debug.print("missing argument\n\n", .{}); + fatalHelp(); + }; + + return .{ .mode = m }; + } + + fn fatalHelp() noreturn { + std.debug.print( + \\Usage: super i [FILE] [OPTIONS] + \\ + \\ Prints a SuperHTML template's interface. + \\ + \\Options: + \\ + \\ --stdin Read the template from stdin instead of + \\ reading from a file. + \\ + \\ --help, -h Prints this help and exits. + , .{}); + + std.process.exit(1); + } +}; diff --git a/src/cli/lsp.zig b/src/cli/lsp.zig index 2640e30..6156151 100644 --- a/src/cli/lsp.zig +++ b/src/cli/lsp.zig @@ -144,7 +144,7 @@ pub const Handler = struct { errdefer self.gpa.free(new_text); const language_id = notification.textDocument.languageId; - const language = std.meta.stringToEnum(super.Language, language_id) orelse { + const language = super.Language.fromSliceResilient(language_id) orelse { log.err("unrecognized language id: '{s}'", .{language_id}); try self.windowNotification( .Error, diff --git a/src/root.zig b/src/root.zig index 80325fc..3ac29e9 100644 --- a/src/root.zig +++ b/src/root.zig @@ -33,6 +33,21 @@ pub const Language = enum { html, superhtml, xml, + + /// Use to map file extensions to a Language, supports aliases. + pub fn fromSliceResilient(s: []const u8) ?Language { + const Alias = enum { html, superhtml, shtml, xml }; + + const alias = std.meta.stringToEnum(Alias, s) orelse { + return null; + }; + + return switch (alias) { + .superhtml, .shtml => .superhtml, + .html => .html, + .xml => .xml, + }; + } }; pub const max_size = 4 * 1024 * 1024 * 1024;