Makko, the people-oriented static site generator made for blogging.
forge.starlightnet.work/Team/Makko
ssg
static-site-generator
makko
starlight-network
1const std = @import("std");
2const httpz = @import("httpz");
3const helper = @import("helper.zig");
4
5const Makko = @import("Makko.zig");
6const QR = @import("qrcode").QR;
7const Log = @import("Log.zig");
8
9const getLanIP = @import("getLanIP.zig").getLanIP;
10
11const Handle = struct {
12 changed: std.atomic.Value(i16),
13 log: Log,
14 directory: std.fs.Dir,
15 port: u16,
16
17 pub fn update(self: *Handle) void {
18 self.log.header("Reloading");
19 _ = self.changed.fetchAdd(1, .monotonic);
20 }
21
22 pub fn dispatch(
23 self: *Handle,
24 action: httpz.Action(*Handle),
25 req: *httpz.Request,
26 res: *httpz.Response,
27 ) !void {
28 if (std.mem.startsWith(u8, req.url.path, "/.makko"))
29 return action(self, req, res);
30
31 const args = .{ @tagName(req.method), req.url.path };
32 if (self.log.colors)
33 self.log.raw("[{s}] \x1b[2m{s}\x1b[0m\n", args)
34 else
35 self.log.raw("[{s}] {s}\n", args);
36
37 action(self, req, res) catch |err| {
38 const writer = res.writer();
39 try writer.print(
40 @embedFile("gui/error.html"),
41 .{ 500, req.url.path, err },
42 );
43
44 const query = try req.query();
45 const enable_banner = query.get("makko-disable-banner") == null;
46
47 if (enable_banner)
48 try writer.writeAll(@embedFile("gui/banner.html"));
49
50 res.content_type = .HTML;
51 res.status = 500;
52 };
53 }
54};
55
56pub const Server = httpz.Server(*Handle);
57
58pub fn init(
59 makko: *Makko,
60 public: bool,
61 port: u16,
62) !Server {
63 const address = if (public) "0.0.0.0" else "127.0.0.1";
64
65 const printable = if (public) blk: {
66 var buffer: [32]u8 = undefined;
67 if (getLanIP(&buffer)) |lan_ip| {
68 try showQR(makko.allocator, lan_ip, port);
69 break :blk lan_ip;
70 }
71
72 makko.log.err("Could not fetch local IP! No QR, sorry.", .{});
73 break :blk "0.0.0.0";
74 } else "127.0.0.1";
75
76 var handler: Handle = .{
77 .changed = .{ .raw = 0 },
78 .directory = makko.paths.output,
79 .log = makko.log,
80 .port = port,
81 };
82
83 var server = try Server.init(
84 makko.allocator,
85 .{
86 .port = port,
87 .address = address,
88 },
89 &handler,
90 );
91
92 var router = try server.router(.{});
93
94 router.get("/*", static, .{});
95 router.get("/", redirect, .{});
96 router.get("/.makko/state", state, .{});
97 router.get("/.makko/", makko_ui, .{});
98
99 handler.log.info("Serving at http://{s}:{}/\n", .{ printable, port });
100
101 return server;
102}
103
104// For rendering our QR code
105fn showQR(allocator: std.mem.Allocator, address: []const u8, port: u16) !void {
106 const whole = try std.fmt.allocPrint(
107 allocator,
108 "http://{s}:{}",
109 .{ address, port },
110 );
111 defer allocator.free(whole);
112
113 var qr = try QR.fromText(allocator, 2, .low, whole);
114 defer qr.deinit();
115
116 const stdout = std.io.getStdOut();
117 const writer = stdout.writer();
118
119 try writer.writeByte(' '); // small left margin
120
121 // We iterate through each pixel of the QR code and
122 // we render it using ASCII codes.
123 var y: usize = 0;
124 while (y < qr.size) : (y += 2) {
125 try writer.writeAll("\n ");
126
127 for (0..qr.size) |x| {
128 const top = qr.getModule(@intCast(x), @intCast(y));
129 const bottom = qr.getModule(@intCast(x), @intCast(y + 1));
130
131 const char = if (top and bottom)
132 "█"
133 else if (top and !bottom)
134 "▀"
135 else if (!top and bottom)
136 "▄"
137 else
138 " ";
139
140 try writer.writeAll(char);
141 }
142 }
143
144 try writer.writeAll("\n\n");
145}
146
147// in case we want to access either /directory/ or just /, we redirect to
148// /directory/index.html and /index.html
149fn redirect(_: *Handle, req: *httpz.Request, res: *httpz.Response) !void {
150 const query = try req.query();
151 const enable_banner = query.get("makko-disable-banner") == null;
152
153 const alloc = try std.mem.concat(res.arena, u8, &.{
154 req.url.path,
155 "index.html",
156 if (!enable_banner) "?makko-disable-banner=1" else "",
157 });
158 res.headers.add("Location", alloc);
159 res.status = 308;
160 return;
161}
162
163fn makko_ui(handle: *Handle, req: *httpz.Request, res: *httpz.Response) !void {
164 const addr = req.conn.address.in.sa.addr;
165
166 // checks if NOT accessing from eiher 127.0.0.1 or 0.0.0.0
167 const is_local = addr == 0x0100007f or addr == 0;
168 if (!is_local) {
169 const new_addr =
170 try std.fmt.allocPrint(res.arena, "http://localhost:{}/.makko", .{handle.port});
171
172 res.headers.add("Location", new_addr);
173 res.status = 308;
174 return;
175 }
176
177 const writer = res.writer();
178 try writer.writeAll(@embedFile("gui/shell.html"));
179 res.content_type = .HTML;
180 res.status = 200;
181}
182
183fn state(handle: *Handle, _: *httpz.Request, res: *httpz.Response) !void {
184 try res.writer().print("{}", .{handle.changed.raw});
185 res.status = 200;
186 res.content_type = .TEXT;
187}
188
189fn static(handle: *Handle, req: *httpz.Request, res: *httpz.Response) !void {
190 const query = try req.query();
191 const enable_banner = query.get("makko-disable-banner") == null;
192
193 const path = req.url.path[1..];
194
195 // If path is a directory, attempt to redirect to $(path)/index.html
196 if (path[path.len - 1] == '/')
197 return try redirect(handle, req, res);
198
199 const output = handle.directory;
200 if (!helper.exists(output, path)) {
201 const writer = res.writer();
202 try writer.print(
203 @embedFile("gui/error.html"),
204 .{ 404, path, error.FileNotFound },
205 );
206
207 if (enable_banner)
208 try writer.writeAll(@embedFile("gui/banner.html"));
209
210 res.content_type = .HTML;
211 res.status = 404;
212 return;
213 }
214
215 // open the file!
216 const file = try output.openFile(path, .{});
217 defer file.close();
218
219 res.content_type = httpz.ContentType.forFile(path);
220
221 const writer = res.writer();
222 const size = try file.getEndPos();
223
224 // we read the file up and write as we read
225 var file_buf: [4096]u8 = undefined;
226 var remaining = size;
227 while (remaining > 0) {
228 const len = try file.read(&file_buf);
229 if (len == 0) break;
230 remaining -= len;
231 try writer.writeAll(file_buf[0..len]);
232 }
233
234 // we inject the banner if we're dealing with HTML
235 if (enable_banner and res.content_type.? == .HTML)
236 try writer.writeAll(@embedFile("gui/banner.html"));
237
238 try res.write();
239 res.status = 200;
240}