documentation: semi-minimally-functional zed

It can load things from files now, but the cmark dependency needs to be
properly integrated into the build still.
This commit is contained in:
torque 2023-05-11 11:06:28 -07:00
parent 95aa6d01c6
commit faa43a1941
Signed by: torque
SSH Key Fingerprint: SHA256:nCrXefBNo6EbjNSQhv0nXmEg/VuNq3sMF5b8zETw3Tk
5 changed files with 270 additions and 23 deletions

View File

@ -0,0 +1,5 @@
</div>
</main>
</div>
</body>
</html>

View File

@ -0,0 +1,4 @@
</nav>
</aside>
<main class="doc">
<div class="doc-padding">

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{[0]s}</title>
<style>{[1]s}</style>
</head>
<body>
<div class="layout">
<aside class="sidebar">
<!-- <div class="header">NOCLIP</div> -->
<input id="destroyer-of-navs" class="collapse-toggle" type="checkbox">
<label for="destroyer-of-navs" class="header collapse-label" tabindex="0">{[0]s}</label>
<nav class="nav collapse-view">

View File

@ -140,6 +140,22 @@ body .layout .doc {
overflow-y: scroll; overflow-y: scroll;
} }
a {
text-decoration: none;
font-weight: bold;
color: var(--blue);
}
a:hover {
text-decoration: underline;
}
p code {
background: var(--sidebar-color);
color: var(--gray-4);
/* border-radius: 10px;*/
}
body .layout .doc .doc-padding { body .layout .doc .doc-padding {
padding-bottom: 80vh; padding-bottom: 80vh;
max-width: 100%; max-width: 100%;
@ -230,7 +246,10 @@ body .layout .doc section .example .code-markup .line {
.code-markup .string { color: var(--green); } .code-markup .string { color: var(--green); }
.code-markup .comment { color: var(--gray-3); } .code-markup .comment { color: var(--gray-3); }
.code-markup .literal { color: var(--orange); } .code-markup .literal { color: var(--orange); }
.code-markup .name { color: var(--red); } .code-markup .label { color: var(--yellow); }
.code-markup .field-name { color: var(--red); }
.code-markup .variable { color: var(--red); }
.code-markup .function { color: var(--blue); }
/*It turns out these have to come after the directives they override.*/ /*It turns out these have to come after the directives they override.*/
@media (max-width: 1820px) { @media (max-width: 1820px) {

View File

@ -1,4 +1,12 @@
const std = @import("std"); const std = @import("std");
const noclip = @import("noclip");
const tokenator = @import("./tokenator.zig");
const cmark = @cImport({
@cInclude("cmark.h");
@cInclude("cmark_version.h");
@cInclude("cmark_export.h");
});
const Directive = enum { const Directive = enum {
section, section,
@ -245,19 +253,39 @@ const DirectiveLine = union(Directive) {
} }
}; };
const Body = union(enum) { fn slugify(allocator: std.mem.Allocator, source: []const u8) ![]const u8 {
in_line: []const u8, const buf = try allocator.alloc(u8, source.len);
include: []const u8, for (source, 0..) |char, idx| {
}; if (std.ascii.isAlphanumeric(char)) {
buf[idx] = std.ascii.toLower(char);
} else {
buf[idx] = '-';
}
}
const Example = struct { return buf;
format: ExampleFormat, }
body: Body,
};
const Section = struct { const Section = struct {
name: []const u8, name: []const u8,
id: []const u8,
segments: []const Segment, segments: []const Segment,
fn emit(self: Section, allocator: std.mem.Allocator, writer: anytype) !void {
try writer.print(
\\<section>
\\<div id="{s}" class = "header">{s}</div>
\\
, .{ self.id, self.name });
for (self.segments) |segment| {
switch (segment) {
inline else => |seg| try seg.emit(allocator, writer),
}
}
try writer.writeAll("</section>");
}
}; };
const Segment = union(enum) { const Segment = union(enum) {
@ -265,9 +293,68 @@ const Segment = union(enum) {
example: Example, example: Example,
}; };
const Example = struct {
format: ExampleFormat,
body: Body,
fn emit(self: Example, allocator: std.mem.Allocator, writer: anytype) !void {
try writer.writeAll(
\\<div class="example">
\\<div class="codebox">
\\
);
switch (self.format) {
.zig => switch (self.body) {
.in_line => |buf| try tokenator.tokenize_buffer(buf, allocator, writer, false),
.include => |fln| try tokenator.tokenize_file(fln, allocator, writer, false),
},
.console => switch (self.body) {
.in_line => |buf| try writer.print(
\\<pre class="code-markup"><code class="lang-console">{s}</code></pre>
\\
, .{std.mem.trim(u8, buf, " \n")}),
.include => @panic("included console example not supported"),
},
}
try writer.writeAll(
\\</div>
\\</div>
\\
);
}
};
const Description = struct { const Description = struct {
format: DescriptionFormat, format: DescriptionFormat,
body: Body, body: Body,
fn emit(self: Description, allocator: std.mem.Allocator, writer: anytype) !void {
try writer.writeAll(
\\<div class="description">
\\
);
_ = allocator;
switch (self.format) {
.markdown => switch (self.body) {
.in_line => |buf| {
const converted = cmark.cmark_markdown_to_html(buf.ptr, buf.len, 0);
if (converted == null) return error.OutOfMemory;
try writer.writeAll(std.mem.sliceTo(converted, 0));
},
.include => |fln| {
_ = fln;
@panic("include description not implemented");
},
},
}
try writer.writeAll("</div>\n");
}
};
const Body = union(enum) {
in_line: []const u8,
include: []const u8,
}; };
const Document = []const Section; const Document = []const Section;
@ -290,20 +377,26 @@ const ParserState = enum {
any_directive, any_directive,
}; };
fn slice_to_next_directive(lines: *std.mem.TokenIterator(u8)) []const u8 { fn slice_to_next_directive(lines: *std.mem.TokenIterator(u8)) ![]const u8 {
const start = lines.index + 1; const start = lines.index + 1;
// the directive is the last line in the file
if (start >= lines.buffer.len) return "";
while (lines.peek()) |line| : (_ = lines.next()) { while (lines.peek()) |line| : (_ = lines.next()) {
// this approach is likely too sloppy
if (DirectiveLine.from_line(line)) |_| { if (DirectiveLine.from_line(line)) |_| {
return lines.buffer[start..lines.index]; return lines.buffer[start..lines.index];
} else |_| {} } else |err| switch (err) {
error.ExpectedDirectivePrefix => {},
else => return err,
}
} }
// we hit EOF // we hit EOF
return lines.buffer[start..lines.buffer.len]; return lines.buffer[start..lines.buffer.len];
} }
pub fn parse(allocator: std.mem.Allocator, input: []const u8) !Document { pub fn parse(allocator: std.mem.Allocator, input: []const u8, directory: []const u8) !Document {
var lines = std.mem.tokenize(u8, input, "\n"); var lines = std.mem.tokenize(u8, input, "\n");
var doc_builder = std.ArrayList(Section).init(allocator); var doc_builder = std.ArrayList(Section).init(allocator);
@ -311,7 +404,11 @@ pub fn parse(allocator: std.mem.Allocator, input: []const u8) !Document {
var state: ParserState = .section_or_include; var state: ParserState = .section_or_include;
var current_section: Section = undefined; var current_section: Section = .{
.name = undefined,
.id = undefined,
.segments = undefined,
};
while (lines.next()) |line| { while (lines.next()) |line| {
const dline = try DirectiveLine.from_line(line); const dline = try DirectiveLine.from_line(line);
@ -319,11 +416,12 @@ pub fn parse(allocator: std.mem.Allocator, input: []const u8) !Document {
.section_or_include => switch (dline) { .section_or_include => switch (dline) {
.section => |sline| { .section => |sline| {
current_section.name = sline.name; current_section.name = sline.name;
current_section.id = try slugify(allocator, sline.name);
state = .any_directive; state = .any_directive;
}, },
.include => |iline| { .include => |iline| {
// read the file at iline.path // read the file at iline.path
const doc = try parse(allocator, iline.path); const doc = try parse(allocator, iline.path, try std.fs.path.join(allocator, &[_][]const u8{directory}));
defer allocator.free(doc); defer allocator.free(doc);
try doc_builder.appendSlice(doc); try doc_builder.appendSlice(doc);
}, },
@ -334,9 +432,10 @@ pub fn parse(allocator: std.mem.Allocator, input: []const u8) !Document {
current_section.segments = try section_builder.toOwnedSlice(); current_section.segments = try section_builder.toOwnedSlice();
try doc_builder.append(current_section); try doc_builder.append(current_section);
current_section.name = sline.name; current_section.name = sline.name;
current_section.id = try slugify(allocator, sline.name);
}, },
.include => |iline| { .include => |iline| {
const doc = try parse(allocator, iline.path); const doc = try parse(allocator, iline.path, try std.fs.path.join(allocator, &[_][]const u8{directory}));
defer allocator.free(doc); defer allocator.free(doc);
try doc_builder.appendSlice(doc); try doc_builder.appendSlice(doc);
state = .section_or_include; state = .section_or_include;
@ -345,18 +444,18 @@ pub fn parse(allocator: std.mem.Allocator, input: []const u8) !Document {
try section_builder.append(.{ .example = .{ try section_builder.append(.{ .example = .{
.format = exline.format, .format = exline.format,
.body = if (exline.include) |incl| .body = if (exline.include) |incl|
.{ .include = incl } .{ .include = try std.fs.path.join(allocator, &[_][]const u8{ directory, incl }) }
else else
.{ .in_line = slice_to_next_directive(&lines) }, .{ .in_line = try slice_to_next_directive(&lines) },
} }); } });
}, },
.description => |desline| { .description => |desline| {
try section_builder.append(.{ .description = .{ try section_builder.append(.{ .description = .{
.format = desline.format, .format = desline.format,
.body = if (desline.include) |incl| .body = if (desline.include) |incl|
.{ .include = incl } .{ .include = try std.fs.path.join(allocator, &[_][]const u8{ directory, incl }) }
else else
.{ .in_line = slice_to_next_directive(&lines) }, .{ .in_line = try slice_to_next_directive(&lines) },
} }); } });
}, },
}, },
@ -368,6 +467,14 @@ pub fn parse(allocator: std.mem.Allocator, input: []const u8) !Document {
return doc_builder.toOwnedSlice(); return doc_builder.toOwnedSlice();
} }
pub fn free_doc(doc: Document, allocator: std.mem.Allocator) void {
for (doc) |section| {
allocator.free(section.id);
allocator.free(section.segments);
}
allocator.free(doc);
}
test "parser" { test "parser" {
const doc = try parse( const doc = try parse(
std.testing.allocator, std.testing.allocator,
@ -385,6 +492,8 @@ test "parser" {
, ,
); );
defer free_doc(doc, std.testing.allocator);
for (doc) |section| { for (doc) |section| {
std.debug.print("section: {s}\n", .{section.name}); std.debug.print("section: {s}\n", .{section.name});
for (section.segments) |seg| { for (section.segments) |seg| {
@ -400,7 +509,103 @@ test "parser" {
} }
} }
} }
}
for (doc) |section| std.testing.allocator.free(section.segments);
std.testing.allocator.free(doc); const pre_nav = @embedFile("./templates/pre-nav.fragment.html");
const style = @embedFile("./templates/style.css");
const post_nav = @embedFile("./templates/post-nav.fragment.html");
const post_body = @embedFile("./templates/post-body.fragment.html");
const nav_item_template =
\\<a href="#{s}"><div class="item">{s}</div></a>
\\
;
const dezed_cmd = cmd: {
var cmd = noclip.CommandBuilder(*ZedCtx){
.description =
\\Convert a ZED file into HTML
\\
,
};
cmd.string_option(.{
.name = "output",
.short_tag = "-o",
.long_tag = "--output",
.description = "write output to file (- to write to stdout). If omitted, output will be written to <input>.html",
});
cmd.string_argument(.{ .name = "input" });
break :cmd cmd;
};
const ZedCtx = struct {
allocator: std.mem.Allocator,
};
fn dezed_cli(context: *ZedCtx, parameters: dezed_cmd.Output()) !void {
const outname = parameters.output orelse if (std.mem.eql(u8, parameters.input, "-"))
"-"
else
try std.mem.join(
context.allocator,
".",
&[_][]const u8{ parameters.input, "html" },
);
// this theoretically leaks the file handle, though we should be able to extract it
// from the reader/writer
const input = blk: {
if (std.mem.eql(u8, parameters.input, "-")) {
break :blk std.io.getStdIn().reader();
} else {
break :blk (try std.fs.cwd().openFile(parameters.input, .{ .mode = .read_only })).reader();
}
};
const output = blk: {
if (std.mem.eql(u8, outname, "-")) {
break :blk std.io.getStdOut().writer();
} else {
break :blk (try std.fs.cwd().createFile(outname, .{})).writer();
}
};
const cwd = try std.process.getCwdAlloc(context.allocator);
const filedir = try std.fs.path.join(context.allocator, &[_][]const u8{ cwd, std.fs.path.dirname(outname) orelse return error.OutOfMemory });
const data = try input.readAllAlloc(context.allocator, 1_000_000);
const doc = try parse(context.allocator, data, filedir);
defer free_doc(doc, context.allocator);
try output.print(pre_nav, .{ "NOCLIP", style });
for (doc) |section| {
try output.print(nav_item_template, .{ section.id, section.name });
}
try output.writeAll(post_nav);
for (doc) |section| {
try section.emit(context.allocator, output);
}
try output.writeAll(post_body);
}
pub fn cli() !u8 {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var arena = std.heap.ArenaAllocator.init(gpa.allocator());
defer arena.deinit();
var ctx: ZedCtx = .{ .allocator = arena.allocator() };
var cli_parser = dezed_cmd.create_parser(dezed_cli, ctx.allocator);
try cli_parser.execute(&ctx);
return 0;
}
pub fn main() !u8 {
return try cli();
} }