const std = @import("std"); const zlua = @import("zlua"); const Lua = zlua.Lua; const fern = @import("fern"); const display = @import("lua/lua_display.zig"); const str = @import("lua/lua_str.zig"); const maivi = @import("root.zig"); // void elements // https://developer.mozilla.org/en-US/docs/Glossary/Void_element const VOID_ELEMENTS = [_][]const u8{ "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "source", "wbr", }; const ElementConfig = struct { close: bool = true, empty: bool = false, raw: bool = false, pub fn default(ntype: ?[]const u8) @This() { const name = ntype orelse return .{}; for (VOID_ELEMENTS) |n| if (std.mem.eql(u8, name, n)) return .{ .close = false, }; return .{}; } }; pub const Error = error{ ExpectedTable, ExpectedString, } || error{ LuaTableMissingValue, LuaValueNotATable, LuaRuntime, LuaMsgHandler, UnexpectedStackSize, } || std.Io.Writer.Error || std.mem.Allocator.Error; fn elem(l: *Lua, w: *std.Io.Writer) Error!void { const stack_size_on_entry = l.getTop(); if (!l.isTable(-1)) return error.ExpectedTable; const name: ?[]u8 = blk: { const ltype_type = l.getField(-1, "type"); defer l.pop(1); if (ltype_type == .nil) break :blk null; if (ltype_type != .string) return error.ExpectedString; l.pushValue(-1); defer l.pop(1); const namez = try l.toString(-1); break :blk try maivi.allocator.dupe(u8, namez); }; defer if (name) |m| maivi.allocator.free(m); const config: ElementConfig = blk: { l.getMetatable(-1) catch break :blk null; defer l.pop(1); const ltype_config = l.getField(-1, "config"); defer l.pop(1); if (ltype_config == .nil) break :blk null; if (ltype_config != .table) return error.ExpectedTable; break :blk try l.toStruct(ElementConfig, null, false, -1); } orelse .default(name); if (name) |n| { try w.writeByte('<'); try w.writeAll(n); } // props props: { const ltype_props = l.getField(-1, "props"); defer l.pop(1); if (ltype_props == .nil) break :props; if (ltype_props != .table) return error.ExpectedTable; try writeProps(l, w); } { const nchildren: usize = nchildren: { const ltype_children = l.getField(-1, "children"); if (ltype_children == .nil) break :nchildren 0; if (ltype_children != .table) return error.ExpectedTable; break :nchildren l.lenRaw(-1); }; // -1 => children; -2 => elem defer l.pop(1); // pops "children" if (config.empty and nchildren == 0) { if (name != null) try w.writeAll(" />"); try w.flush(); return; } if (name != null) try w.writeByte('>'); // children if (nchildren > 0) try writeChildren(l, w, config); } // close if (config.close) if (name) |n| { try w.writeAll("'); }; if (l.getTop() != stack_size_on_entry) return error.UnexpectedStackSize; try w.flush(); } fn writeProps(l: *Lua, w: *std.Io.Writer) !void { var arena: std.heap.ArenaAllocator = .init(maivi.allocator); defer arena.deinit(); var allocator = arena.allocator(); l.pushValue(-1); defer l.pop(1); l.pushNil(); // stack now contains: -1 => nil; -2 => table while (l.next(-2)) { // stack now contains: -1 => value; -2 => key; -3 => table try w.writeByte(' '); const k = k: { l.pushValue(-2); defer l.pop(1); const kz = try l.toString(-1); break :k try allocator.dupe(u8, kz); }; // uses -1 => value _ = try display.attr(l); // stack now contains: -1 => value (parsed); -2 => value; -3 => key; -4 => table // uses -1 => value (parsed) _ = try str.escape(l); // stack now contains: -1 => value (escaped); -2 => value (parsed); -3 => value; -4 => key; -5 => table const vz = try l.toString(-1); const v = try allocator.dupe(u8, vz); try w.writeAll(k); try w.writeByte('='); try w.writeByte('"'); try w.writeAll(v); try w.writeByte('"'); // pop value + parsed + escaped, leaving original key l.pop(3); // stack now contains: -1 => key; -2 => table } } fn writeChildren(l: *Lua, w: *std.Io.Writer, config: ElementConfig) !void { var arena: std.heap.ArenaAllocator = .init(maivi.allocator); defer arena.deinit(); var allocator = arena.allocator(); l.pushValue(-1); defer l.pop(1); l.pushNil(); // stack now contains: -1 => nil; -2 => table while (l.next(-2)) { // stack now contains: -1 => value; -2 => key; -3 => table if (l.isFunction(-1)) { try maivi.lua.pcall(l, .{ .results = 1 }); } if (!l.isTable(-1)) { // uses -1 => value _ = try display.elem(l); // stack now contains: -1 => value (parsed); -2 => value; -3 => key; -4 => table if (!config.raw) { // uses -1 => value (parsed) _ = try str.escape(l); // stack now contains: -1 => value (escaped); -2 => value (parsed); -3 => value; -4 => key; -5 => table } const vz = try l.toString(-1); const v = try allocator.dupe(u8, vz); if (!config.raw) l.pop(1); // pop escaped l.pop(1); // pop parsed try w.writeAll(v); } else { // uses -1 => value try elem(l, w); } // pop value, leaving original key l.pop(1); // stack now contains: -1 => key; -2 => table } } pub fn render(l: *Lua, w: *std.Io.Writer) !void { try elem(l, w); }