From 35614e6b711cb76266d709a6c051f77eee852001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=28xq=29=20Quei=C3=9Fner?= Date: Sat, 13 Mar 2021 19:18:09 +0100 Subject: [PATCH] Adds automatic article index rendering. --- src/main.zig | 437 +++++++++++++++--- website/articles/2021-03-12 - async-await.md | 3 + website/articles/2021-03-16 - replace-qmk.md | 3 + website/articles/2021-04-10 - zCOM.md | 3 + website/index.md | 22 +- .../01-embedded-basics.md | 0 6 files changed, 373 insertions(+), 95 deletions(-) create mode 100644 website/articles/2021-03-12 - async-await.md create mode 100644 website/articles/2021-03-16 - replace-qmk.md create mode 100644 website/articles/2021-04-10 - zCOM.md rename website/{articles => tutorials}/01-embedded-basics.md (100%) diff --git a/src/main.zig b/src/main.zig index 4b14d28..494347a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,96 +1,99 @@ const std = @import("std"); const koino = @import("koino"); +const markdown_options = koino.Options{ + .extensions = .{ + .table = true, + .autolink = true, + .strikethrough = true, + }, +}; + +/// verifies and parses a file name in the format +/// "YYYY-MM-DD - " [.*] ".md" +/// +fn isValidArticleFileName(path: []const u8) ?Date { + if (path.len < 16) + return null; + if (!std.mem.endsWith(u8, path, ".md")) + return null; + if (path[4] != '-' or path[7] != '-' or !std.mem.eql(u8, path[10..13], " - ")) + return null; + return Date{ + .year = std.fmt.parseInt(u16, path[0..4], 10) catch return null, + .month = std.fmt.parseInt(u8, path[5..7], 10) catch return null, + .day = std.fmt.parseInt(u8, path[8..10], 10) catch return null, + }; +} + pub fn main() anyerror!void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = &gpa.allocator; - var root_dir = try std.fs.walkPath(allocator, "website"); - defer root_dir.deinit(); - - const markdown_options = koino.Options{ - .extensions = .{ - .table = true, - .autolink = true, - .strikethrough = true, - }, + var website = Website{ + .allocator = allocator, + .arena = std.heap.ArenaAllocator.init(allocator), + .articles = std.ArrayList(Article).init(allocator), }; + defer website.deinit(); - while (try root_dir.next()) |entry| { - switch (entry.kind) { - // we create the directories by forcing them - .Directory => {}, - - .File => { - const ext = std.fs.path.extension(entry.path); - if (std.mem.eql(u8, ext, ".md")) { - std.log.info("render {s}", .{entry.path}); - - const out_name = try std.mem.concat(allocator, u8, &[_][]const u8{ - entry.path[8 .. entry.path.len - 3], - ".htm", - }); - defer allocator.free(out_name); - - var out_path = try std.fs.path.join(allocator, &[_][]const u8{ - "render", out_name, - }); - defer allocator.free(out_path); - - if (std.fs.path.dirname(out_path)) |dir| { - std.debug.print("{s}\n", .{dir}); - try std.fs.cwd().makePath(dir); - } + // gather step + { + var root_dir = try std.fs.cwd().openDir("website", .{}); + defer root_dir.close(); + + // gather articles + { + var dir = try root_dir.openDir("articles", .{ .iterate = true }); + defer dir.close(); - var markdown_input = try std.fs.cwd().readFileAlloc(allocator, entry.path, 10_000_000); - defer allocator.free(markdown_input); - - var rendered_markdown = try markdownToHtml(allocator, markdown_options, markdown_input); - defer gpa.allocator.free(rendered_markdown); - - var output_file = try std.fs.cwd().createFile(out_path, .{}); - defer output_file.close(); - - var writer = output_file.writer(); - - try writer.writeAll( - \\ - \\ - \\ - \\ - \\ - \\ - \\ ZEG - \\ - \\ - \\ - ); - - try writer.writeAll(rendered_markdown); - - try writer.writeAll( - \\ - \\ - \\ - ); + var iter = dir.iterate(); + while (try iter.next()) |entry| { + if (entry.kind != .File) { + std.log.err("Illegal folder in directory website/articles: {s}", .{entry.name}); + continue; } - }, - else => std.debug.panic("Unsupported file type {s} in directory!", .{@tagName(entry.kind)}), + const date = isValidArticleFileName(entry.name) orelse { + std.log.err("Illegal file name in directory website/articles: {s}", .{entry.name}); + continue; + }; + + var article = Article{ + .title = "Not yet generated", + .src_file = undefined, + .date = date, + }; + + article.src_file = try std.fs.path.join(&website.arena.allocator, &[_][]const u8{ + "website", + "articles", + entry.name, + }); + + try website.addArticle(article); + } } } + + try website.prepareRendering(); + + // final rendering + { + var root_dir = try std.fs.cwd().makeOpenPath("render", .{}); + defer root_dir.close(); + + try website.renderIndexFile("website/index.md", root_dir, "index.htm"); + + try website.renderArticleIndex(root_dir, "articles.htm"); + + var art_dir = try root_dir.makeOpenPath("articles", .{}); + defer art_dir.close(); + + try website.renderArticles(art_dir); + } } fn markdownToHtmlInternal(resultAllocator: *std.mem.Allocator, internalAllocator: *std.mem.Allocator, options: koino.Options, markdown: []const u8) ![]u8 { @@ -110,3 +113,283 @@ pub fn markdownToHtml(allocator: *std.mem.Allocator, options: koino.Options, mar defer arena.deinit(); return markdownToHtmlInternal(allocator, &arena.allocator, options, markdown); } + +const Date = struct { + const Self = @This(); + + day: u8, + month: u8, + year: u16, + + fn toInteger(self: Self) u32 { + return @as(u32, self.day) + 33 * @as(u32, self.month) + (33 * 13) * @as(u32, self.year); + } + + pub fn lessThan(lhs: Self, rhs: Self) bool { + return lhs.toInteger() < rhs.toInteger(); + } + + pub fn eql(a: Self, b: Self) bool { + return std.meta.eql(a, b); + } + + pub fn format(self: Self, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { + try writer.print("{d:0>4}-{d:0>2}-{d:0>2}", .{ + self.year, self.month, self.day, + }); + } +}; + +const Article = struct { + date: Date, + src_file: []const u8, + title: []const u8, +}; + +const Website = struct { + const Self = @This(); + + is_prepared: bool = false, + allocator: *std.mem.Allocator, + arena: std.heap.ArenaAllocator, + articles: std.ArrayList(Article), + + fn deinit(self: *Self) void { + self.articles.deinit(); + self.arena.deinit(); + self.* = undefined; + } + + fn addArticle(self: *Self, article: Article) !void { + self.is_prepared = false; + try self.articles.append(Article{ + .date = article.date, + .src_file = try self.arena.allocator.dupe(u8, article.src_file), + .title = try self.arena.allocator.dupe(u8, article.title), + }); + } + + fn prepareRendering(self: *Self) !void { + std.sort.sort(Article, self.articles.items, self.*, sortArticlesDesc); + + for (self.articles.items) |*article| { + var doc = blk: { + var p = try koino.parser.Parser.init(self.allocator, markdown_options); + defer p.deinit(); + + const markdown = try std.fs.cwd().readFileAlloc(self.allocator, article.src_file, 10_000_000); + defer self.allocator.free(markdown); + + try p.feed(markdown); + + break :blk try p.finish(); + }; + defer doc.deinit(); + + std.debug.assert(doc.data.value == .Document); + + var iter = doc.first_child; + var heading_or_null: ?*koino.nodes.AstNode = while (iter) |item| : (iter = item.next) { + if (item.data.value == .Heading) { + if (item.data.value.Heading.level == 1) { + break item; + } + } + } else null; + + if (heading_or_null) |heading| { + const string = try koino.html.print(&self.arena.allocator, markdown_options, heading); + + std.debug.assert(std.mem.startsWith(u8, string, "

")); + std.debug.assert(std.mem.endsWith(u8, string, "

\n")); + + article.title = string[4 .. string.len - 6]; + } + } + + self.is_prepared = true; + } + + fn sortArticlesDesc(self: Self, lhs: Article, rhs: Article) bool { + if (lhs.date.lessThan(rhs.date)) + return false; + if (rhs.date.lessThan(lhs.date)) + return true; + return (std.mem.order(u8, lhs.title, rhs.title) == .gt); + } + + fn removeExtension(src_name: []const u8) []const u8 { + const ext = std.fs.path.extension(src_name); + return src_name[0 .. src_name.len - ext.len]; + } + + fn changeExtension(self: *Self, src_name: []const u8, new_ext: []const u8) ![]const u8 { + return std.mem.join(&self.arena.allocator, "", &[_][]const u8{ + removeExtension(src_name), + new_ext, + }); + } + + fn urlEscape(self: *Self, text: []const u8) ![]u8 { + const legal_character = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"; + + var len: usize = 0; + for (text) |c| { + len += if (std.mem.indexOfScalar(u8, legal_character, c) == null) + @as(usize, 3) + else + @as(usize, 1); + } + + const buf = try self.arena.allocator.alloc(u8, len); + var offset: usize = 0; + for (text) |c| { + if (std.mem.indexOfScalar(u8, legal_character, c) == null) { + const hexdigits = "0123456789ABCDEF"; + buf[offset + 0] = '%'; + buf[offset + 1] = hexdigits[(c >> 4) & 0xF]; + buf[offset + 2] = hexdigits[(c >> 0) & 0xF]; + offset += 3; + } else { + buf[offset] = c; + offset += 1; + } + } + + return buf; + } + + fn renderArticles(self: *Self, dst_dir: std.fs.Dir) !void { + std.debug.assert(self.is_prepared); + for (self.articles.items) |art| { + try self.renderMarkdownFile( + art.src_file, + dst_dir, + try self.changeExtension(std.fs.path.basename(art.src_file), ".htm"), + ); + } + } + + /// Renders the root file and replaces `` with the first 10 articles, + /// in descending order + fn renderIndexFile(self: *Self, src_path: []const u8, dst_dir: std.fs.Dir, file_name: []const u8) !void { + std.debug.assert(self.is_prepared); + + var src_code = try std.fs.cwd().readFileAlloc(self.allocator, src_path, 10_000_000); + defer self.allocator.free(src_code); + + var array_buffer = std.ArrayList(u8).init(self.allocator); + defer array_buffer.deinit(); + + const offset = std.mem.indexOf(u8, src_code, "") orelse return error.MissingArticlesMarker; + + var writer = array_buffer.writer(); + + try writer.writeAll(src_code[0..offset]); + + for (self.articles.items[0..std.math.min(self.articles.items.len, 10)]) |art| { + try writer.print("- [{} - {s}](articles/{s}.htm)\n", .{ + art.date, + art.title, + try self.urlEscape(removeExtension(std.fs.path.basename(art.src_file))), + }); + } + + try writer.writeAll(src_code[offset + 17 ..]); + + try self.renderMarkdown(array_buffer.items, dst_dir, file_name); + } + + /// Renders the root file and replaces `` with the first 10 articles, + /// in descending order + fn renderArticleIndex(self: *Self, dst_dir: std.fs.Dir, file_name: []const u8) !void { + std.debug.assert(self.is_prepared); + + var array_buffer = std.ArrayList(u8).init(self.allocator); + defer array_buffer.deinit(); + + var writer = array_buffer.writer(); + + try writer.writeAll("# Articles\n"); + try writer.writeAll("\n"); + + for (self.articles.items[0..std.math.min(self.articles.items.len, 10)]) |art| { + try writer.print("- [{} - {s}](articles/{s}.htm)\n", .{ + art.date, + art.title, + try self.urlEscape(removeExtension(std.fs.path.basename(art.src_file))), + }); + } + + try self.renderMarkdown(array_buffer.items, dst_dir, file_name); + } + + /// Render a given markdown file into `dst_path`. + fn renderMarkdownFile(self: Self, src_path: []const u8, dst_dir: std.fs.Dir, dst_path: []const u8) !void { + std.debug.assert(self.is_prepared); + + var markdown_input = try std.fs.cwd().readFileAlloc(self.allocator, src_path, 10_000_000); + defer self.allocator.free(markdown_input); + + try self.renderMarkdown(markdown_input, dst_dir, dst_path); + } + + /// Render the given markdown source into `dst_path`. + fn renderMarkdown(self: Self, source: []const u8, dst_dir: std.fs.Dir, dst_path: []const u8) !void { + std.debug.assert(self.is_prepared); + + var rendered_markdown = try markdownToHtml(self.allocator, markdown_options, source); + defer self.allocator.free(rendered_markdown); + + try self.renderHtml(rendered_markdown, dst_dir, dst_path); + } + + /// Render the markdown body into `dst_path`. + fn renderHtml(self: Self, source: []const u8, dst_dir: std.fs.Dir, dst_path: []const u8) !void { + std.debug.assert(self.is_prepared); + + var output_file = try dst_dir.createFile(dst_path, .{}); + defer output_file.close(); + + var writer = output_file.writer(); + + try self.renderHeader(writer); + try writer.writeAll(source); + try self.renderFooter(writer); + } + + fn renderHeader(self: Self, writer: anytype) !void { + std.debug.assert(self.is_prepared); + try writer.writeAll( + \\ + \\ + \\ + \\ + \\ + \\ + \\ ZEG + \\ + \\ + \\ + ); + } + + fn renderFooter(self: Self, writer: anytype) !void { + std.debug.assert(self.is_prepared); + try writer.writeAll( + \\ + \\ + \\ + ); + } +}; diff --git a/website/articles/2021-03-12 - async-await.md b/website/articles/2021-03-12 - async-await.md new file mode 100644 index 0000000..2c6ddd6 --- /dev/null +++ b/website/articles/2021-03-12 - async-await.md @@ -0,0 +1,3 @@ +# `async`/`await` on embedded platforms + +Blabla this is a stub \ No newline at end of file diff --git a/website/articles/2021-03-16 - replace-qmk.md b/website/articles/2021-03-16 - replace-qmk.md new file mode 100644 index 0000000..b09b36f --- /dev/null +++ b/website/articles/2021-03-16 - replace-qmk.md @@ -0,0 +1,3 @@ +# Make your own keyboard with zig (and replace qmk) + +Blabla this is a stub \ No newline at end of file diff --git a/website/articles/2021-04-10 - zCOM.md b/website/articles/2021-04-10 - zCOM.md new file mode 100644 index 0000000..e61f2c8 --- /dev/null +++ b/website/articles/2021-04-10 - zCOM.md @@ -0,0 +1,3 @@ +# zCOM, a network stack for embedded devices + +Blabla this is a stub \ No newline at end of file diff --git a/website/index.md b/website/index.md index 08af373..dca8a92 100644 --- a/website/index.md +++ b/website/index.md @@ -32,17 +32,9 @@ If you've never done any embedded development before, it's a good point to start The latest articles on embedded programming with Zig: -- [2021-03-15 zCOM, a network stack for embedded devices](#) -- [2021-03-12 Make your own keyboard with zig (and replace qmk)](#) -- [2021-03-10 `async`/`await` on embedded platforms](#) -- [2021-XX-YY Dummy Article](#) -- [2021-XX-YY Dummy Article](#) -- [2021-XX-YY Dummy Article](#) -- [2021-XX-YY Dummy Article](#) -- [2021-XX-YY Dummy Article](#) -- [2021-XX-YY Dummy Article](#) - -[See all articles...](#) + + +[See all articles...](articles.htm) ## Code @@ -56,7 +48,7 @@ Here are some highlighted projects the ZEG provides: - [zCOM Network Driver](#) - [TinySSL](#) -[See all repositories...](#) +[See all repositories...](https://github.com/ZigEmbeddedGroup/) ## Community @@ -71,9 +63,3 @@ This group uses the already existing community infrastructures that exist for Zi - [Vesim](https://github.com/vesim987/) - [Timon "FireFox317" Kruiper](https://github.com/FireFox317) - [Martin "SpexGuy" Wickham](https://github.com/SpexGuy) - ---- - -## Required Stuff - -- Fanart by the guy from ziglings \ No newline at end of file diff --git a/website/articles/01-embedded-basics.md b/website/tutorials/01-embedded-basics.md similarity index 100% rename from website/articles/01-embedded-basics.md rename to website/tutorials/01-embedded-basics.md