From a01619911df25da4badc768772192fa9dd34d2aa Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Sun, 13 Oct 2024 12:57:44 +0200 Subject: [PATCH] add(zmd): parse markdown file contents and display the parsed contents Not everything can be parsed and displayed correctly yet, however this will the default file format to use for adding page contents (i.e. normal pages, blog entries, etc.) --- build.zig | 5 ++ build.zig.zon | 4 ++ src/widget/Header.zig | 28 ++++----- src/widget/PopupMenu.zig | 28 ++++----- src/widget/ViewPort.zig | 52 +++++++++-------- src/widget/node2buffer.zig | 117 +++++++++++++++++++++++++++++++++++++ 6 files changed, 184 insertions(+), 50 deletions(-) create mode 100644 src/widget/node2buffer.zig diff --git a/build.zig b/build.zig index c5be9c5..2534bdd 100644 --- a/build.zig +++ b/build.zig @@ -25,6 +25,10 @@ pub fn build(b: *std.Build) void { .target = target, .timestamp = true, }); + const zmd_dep = b.dependency("zmd", .{ + .optimize = optimize, + .target = target, + }); const exe = b.addExecutable(.{ .name = "tui-website", @@ -34,6 +38,7 @@ pub fn build(b: *std.Build) void { }); exe.root_module.addImport("vaxis", vaxis_dep.module("vaxis")); exe.root_module.addImport("zlog", zlog_dep.module("zlog")); + exe.root_module.addImport("zmd", zmd_dep.module("zmd")); // This declares intent for the executable to be installed into the // standard location when the user invokes the "install" step (the default diff --git a/build.zig.zon b/build.zig.zon index 1aea534..192c11c 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -31,6 +31,10 @@ .url = "git+https://github.com/rockorager/libvaxis#e3062d62b60bf8a351f267a77e1a94e2614394aa", .hash = "1220479d09031168a100748e103ef5a8ffec5d4b0ff74a7e45cf2a9b09f82342b91f", }, + .zmd = .{ + .url = "git+https://github.com/jetzig-framework/zmd#90e52429cacd4fdc90fd615596fe584ae40ec8e9", + .hash = "12202a4edefedd52478223a44cdc9a3b41d4bc5cf3670497a48377f45ff16a5e3363", + }, }, .paths = .{ "build.zig", diff --git a/src/widget/Header.zig b/src/widget/Header.zig index 44c3f7e..c2c2989 100644 --- a/src/widget/Header.zig +++ b/src/widget/Header.zig @@ -33,18 +33,6 @@ pub fn deinit(this: *@This()) void { fn fillView(this: *@This()) void { this.view.?.clear(); - if (this.path) |path| { - for (0..path.len, this.view.?.screen.width / 2 - path.len / 2..) |i, col| { - const cell: vaxis.Cell = .{ - .char = .{ .grapheme = path[i .. i + 1] }, - .style = .{ - .ul_style = .single, - }, - }; - this.view.?.writeCell(col, 0, cell); - } - } - const msg = "Yves Biener"; for (msg, 0..) |_, i| { const cell: vaxis.Cell = .{ @@ -58,8 +46,22 @@ fn fillView(this: *@This()) void { .bold = true, }, }; - this.view.?.writeCell(this.view.?.screen.width - msg.len - 3 + i, 0, cell); + this.view.?.writeCell(i + 1, 0, cell); } + + if (this.path) |path| { + for (0..path.len, this.view.?.screen.width / 2 - path.len / 2..) |i, col| { + const cell: vaxis.Cell = .{ + .char = .{ .grapheme = path[i .. i + 1] }, + .style = .{ + .ul_style = .single, + }, + }; + this.view.?.writeCell(col, 0, cell); + } + } + + // TODO: github icon, discord icon, mail icon with corresponding links to contact me } /// Update loop for a given widget to react to the provided `Event`. It may diff --git a/src/widget/PopupMenu.zig b/src/widget/PopupMenu.zig index 53f7f7e..90d00c7 100644 --- a/src/widget/PopupMenu.zig +++ b/src/widget/PopupMenu.zig @@ -2,7 +2,9 @@ const std = @import("std"); const vaxis = @import("vaxis"); +const Zmd = @import("zmd").Zmd; +const node2buffer = @import("node2buffer.zig"); const widget = @import("../widget.zig"); const Event = widget.Event; @@ -51,23 +53,21 @@ pub fn draw(this: *@This(), win: vaxis.Window) void { const msg = \\# Goto \\ - \\*a* about - \\*h* home + \\**a** about + \\**h** home ; + var zmd = Zmd.init(this.allocator); + defer zmd.deinit(); + + var cells = std.ArrayList(vaxis.Cell).init(this.allocator); + defer cells.deinit(); + + zmd.parse(msg) catch @panic("failed to parse markdown file"); + node2buffer.toBuffer(zmd.nodes.items[0], this.allocator, msg, &cells, .{}) catch @panic("failed to transform to cell array"); + var col: usize = 0; var row: usize = 0; - for (msg, 0..) |_, i| { - const cell: vaxis.Cell = .{ - // each cell takes a _grapheme_ as opposed to a single - // codepoint. This allows Vaxis to handle emoji properly, - // particularly with terminals that the Unicode Core extension - // (IE Mode 2027) - .char = .{ .grapheme = msg[i .. i + 1] }, - .style = .{ - .fg = .{ .index = 6 }, - .bold = true, - }, - }; + for (cells.items) |cell| { view.writeCell(col, row, cell); if (std.mem.eql(u8, cell.char.grapheme, "\n")) { col = 0; diff --git a/src/widget/ViewPort.zig b/src/widget/ViewPort.zig index b98aba4..d1e11ae 100644 --- a/src/widget/ViewPort.zig +++ b/src/widget/ViewPort.zig @@ -2,27 +2,34 @@ const std = @import("std"); const vaxis = @import("vaxis"); +const Zmd = @import("zmd").Zmd; +const node2buffer = @import("node2buffer.zig"); const widget = @import("../widget.zig"); const Event = widget.Event; allocator: std.mem.Allocator = undefined, unicode: *const vaxis.Unicode = undefined, -buffer: vaxis.widgets.TextView.Buffer = undefined, +contents: ?[]const u8 = null, +buffer: std.ArrayList(vaxis.Cell) = undefined, view: vaxis.widgets.ScrollView = undefined, pub fn init(allocator: std.mem.Allocator, unicode: *const vaxis.Unicode) @This() { return .{ .allocator = allocator, .unicode = unicode, - .buffer = .{}, + .contents = null, + .buffer = std.ArrayList(vaxis.Cell).init(allocator), .view = .{ .vertical_scrollbar = null }, }; } pub fn deinit(this: *@This()) void { - this.buffer.deinit(this.allocator); + if (this.contents) |*content| { + this.allocator.free(content.*); + } + this.buffer.deinit(); this.* = undefined; } @@ -60,10 +67,17 @@ pub fn update(this: *@This(), event: Event) void { return; }; defer file.close(); + if (this.contents) |*content| { + this.allocator.free(content.*); + } + this.contents = file.readToEndAlloc(this.allocator, 4096) catch @panic("could not read to end"); - this.buffer.clear(this.allocator); - const writer = this.buffer.writer(this.allocator, &this.unicode.grapheme_data, &this.unicode.width_data); - file.reader().streamUntilDelimiter(writer, 0, null) catch {}; + var zmd = Zmd.init(this.allocator); + defer zmd.deinit(); + + this.buffer.clearRetainingCapacity(); + zmd.parse(this.contents.?) catch @panic("failed to parse markdown contents"); + node2buffer.toBuffer(zmd.nodes.items[0], this.allocator, this.contents.?, &this.buffer, .{}) catch @panic("failed to transform to cell array"); }, else => {}, } @@ -73,30 +87,22 @@ pub fn update(this: *@This(), event: Event) void { /// the dimension one widget may take on the screen. The widget itself has no /// control over this. pub fn draw(this: *@This(), win: vaxis.Window) void { - this.view.draw(win, .{ .cols = this.buffer.cols, .rows = this.buffer.rows }); + // FIXME: the current node2buffer implementation cannot determine how many rows and columns there are from the read file contents + // this.view.draw(win, .{ .cols = this.buffer.cols, .rows = this.buffer.rows }); // needed for scroll bar scaling and display const Pos = struct { x: usize = 0, y: usize = 0 }; var pos: Pos = .{}; const bounds = this.view.bounds(win); - for (this.buffer.grapheme.items(.len), this.buffer.grapheme.items(.offset), 0..) |g_len, g_offset, index| { + for (this.buffer.items) |cell| { if (bounds.above(pos.y)) break; + // if (!bounds.colInside(pos.x)) continue; // FIX: how can I prevent writes out of bounce of the viewport? - const cluster = this.buffer.content.items[g_offset..][0..g_len]; - if (std.mem.eql(u8, cluster, "\n")) { - if (index == this.buffer.grapheme.len - 1) break; - - pos.y +|= 1; + this.view.writeCell(win, pos.x, pos.y, cell); + if (std.mem.eql(u8, cell.char.grapheme, "\n")) { pos.x = 0; - continue; - } else if (bounds.below(pos.y)) { - continue; + pos.y += 1; + } else { + pos.x += 1; } - - const width = win.gwidth(cluster); - defer pos.x +|= width; - - if (!bounds.colInside(pos.x)) continue; - - this.view.writeCell(win, pos.x, pos.y, .{ .char = .{ .grapheme = cluster, .width = width }, .style = .{} }); } } diff --git a/src/widget/node2buffer.zig b/src/widget/node2buffer.zig new file mode 100644 index 0000000..19ee4e7 --- /dev/null +++ b/src/widget/node2buffer.zig @@ -0,0 +1,117 @@ +///! Transform a given `zmd.Note` into a buffer which can be used by any `vaxis.widgets.View` +const std = @import("std"); +const vaxis = @import("vaxis"); +const zmd = @import("zmd"); + +pub fn toBuffer(node: *zmd.Node, allocator: std.mem.Allocator, input: []const u8, array: *std.ArrayList(vaxis.Cell), s: vaxis.Cell.Style) !void { + const content = switch (node.token.element.type) { + .text => input[node.token.start..node.token.end], + .code, .block => node.content, + else => "", + }; + var style = s; + + // determine general styling changes + switch (node.token.element.type) { + .bold => { + style.bold = true; + }, + .bold_close => { + style.bold = false; + }, + .italic => { + style.italic = true; + }, + .italic_close => { + style.italic = false; + }, + .block => { + // TODO: what should I do with blocks? + }, + .block_close => { + // TODO: what should I do with blocks? + }, + .code => { + style.dim = true; + }, + .code_close => { + style.dim = false; + }, + else => {}, + } + + switch (node.token.element.type) { + .root, .none, .eof => {}, + .linebreak, .paragraph => { + try array.append(.{ + .char = .{ .grapheme = "\n" }, + .style = style, + }); + style.ul_style = .off; + }, + .h1 => { + style.ul_style = .single; + try array.append(.{ + .char = .{ .grapheme = "#" }, + .style = style, + }); + }, + .h2 => { + style.ul_style = .single; + for (0..2) |_| { + try array.append(.{ + .char = .{ .grapheme = "#" }, + .style = style, + }); + } + }, + .h3 => { + style.ul_style = .single; + for (0..3) |_| { + try array.append(.{ + .char = .{ .grapheme = "#" }, + .style = style, + }); + } + }, + .h4 => { + style.ul_style = .single; + for (0..4) |_| { + try array.append(.{ + .char = .{ .grapheme = "#" }, + .style = style, + }); + } + }, + .h5 => { + style.ul_style = .single; + for (0..5) |_| { + try array.append(.{ + .char = .{ .grapheme = "#" }, + .style = style, + }); + } + }, + .h6 => { + style.ul_style = .single; + for (0..6) |_| { + try array.append(.{ + .char = .{ .grapheme = "#" }, + .style = style, + }); + } + }, + else => { + for (content, 0..) |_, i| { + try array.append(.{ + .char = .{ .grapheme = content[i .. i + 1] }, + .style = style, + }); + } + }, + } + + for (node.children.items) |child_node| { + try toBuffer(child_node, allocator, input, array, style); + } +}