Compare commits

...

4 Commits

Author SHA1 Message Date
0fbe42c5cb
the formless revolves 2025-03-14 00:17:58 -06:00
5746fbbd5e
consider a tokenizer 2025-03-14 00:17:22 -06:00
11b7d3e06b
a whole big mess 2025-03-06 01:05:52 -07:00
ab1d4ad879
disaster, wrought by mine own hands 2025-03-06 01:05:52 -07:00
19 changed files with 1678 additions and 4619 deletions

View File

@ -4,21 +4,19 @@ pub fn build(b: *std.Build) void {
const target: std.Build.ResolvedTarget = b.standardTargetOptions(.{}); const target: std.Build.ResolvedTarget = b.standardTargetOptions(.{});
const optimize: std.builtin.Mode = b.standardOptimizeOption(.{}); const optimize: std.builtin.Mode = b.standardOptimizeOption(.{});
const noclip = b.addModule("noclip", .{
.root_source_file = b.path("source/noclip.zig"),
});
demo(b, noclip, target, optimize);
const test_step = b.step("test", "Run unit tests"); const test_step = b.step("test", "Run unit tests");
const tests = b.addTest(.{ const tests = b.addTest(.{
.name = "tests", .name = "tests",
.root_source_file = b.path("source/noclip.zig"), .root_source_file = b.path("source/parser.zig"),
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
}); });
test_step.dependOn(&tests.step); const run_main_tests = b.addRunArtifact(tests);
test_step.dependOn(&b.addInstallArtifact(tests, .{}).step);
test_step.dependOn(&run_main_tests.step);
} }
fn demo( fn demo(

View File

@ -1,13 +1,12 @@
const std = @import("std"); const std = @import("std");
const noclip = @import("noclip"); const noclip = @import("noclip");
const CommandBuilder = noclip.CommandBuilder;
const Choice = enum { first, second }; const Choice = enum { first, second };
const cli = cmd: { const U8Tuple = struct { u8, u8 };
var cmd = CommandBuilder(*u32){
.description = const Main = struct {
pub const description =
\\The definitive noclip demonstration utility. \\The definitive noclip demonstration utility.
\\ \\
\\This command demonstrates the functionality of the noclip library. cool! \\This command demonstrates the functionality of the noclip library. cool!
@ -15,7 +14,7 @@ const cli = cmd: {
\\> // implementing factorial recursively is a silly thing to do \\> // implementing factorial recursively is a silly thing to do
\\> pub fn fact(n: u64) u64 { \\> pub fn fact(n: u64) u64 {
\\> if (n == 0) return 1; \\> if (n == 0) return 1;
\\> return n*fact(n - 1); \\> return n * fact(n - 1);
\\> } \\> }
\\ \\
\\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor \\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
@ -24,131 +23,151 @@ const cli = cmd: {
\\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore \\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore
\\eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, \\eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident,
\\sunt in culpa qui officia deserunt mollit anim id est laborum. \\sunt in culpa qui officia deserunt mollit anim id est laborum.
, ;
};
cmd.addOption(.{ .OutputType = struct { u8, u8 } }, .{
.name = "test",
.short_tag = "-t",
.long_tag = "--test",
.env_var = "NOCLIP_TEST",
.description = "multi-value test option",
.nice_type_name = "int> <int",
});
cmd.addOption(.{ .OutputType = Choice }, .{
.name = "choice",
.short_tag = "-c",
.long_tag = "--choice",
.default = .second,
.env_var = "NOCLIP_CHOICE",
.description = "enum choice option",
.nice_type_name = "choice",
});
cmd.stringOption(.{
.name = "string",
.short_tag = "-s",
.long_tag = "--string",
.env_var = "NOCLIP_STRING",
.description = "A string value option",
});
cmd.addOption(.{ .OutputType = u32 }, .{
.name = "default",
.short_tag = "-d",
.long_tag = "--default",
.env_var = "NOCLIP_DEFAULT",
.default = 100,
.description = "default value integer option",
.nice_type_name = "uint",
});
cmd.addOption(.{ .OutputType = u8, .multi = true }, .{
.name = "multi",
.short_tag = "-m",
.long_tag = "--multi",
.description = "multiple specification test option",
});
cmd.addFlag(.{}, .{
.name = "flag",
.truthy = .{ .short_tag = "-f", .long_tag = "--flag" },
.falsy = .{ .short_tag = "-F", .long_tag = "--no-flag" },
.env_var = "NOCLIP_FLAG",
.description = "boolean flag",
});
cmd.addFlag(.{ .multi = true }, .{
.name = "multiflag",
.truthy = .{ .short_tag = "-M" },
.description = "multiple specification test flag ",
});
cmd.addOption(.{ .OutputType = u8 }, .{
.name = "env",
.env_var = "NOCLIP_ENVIRON",
.description = "environment variable only option",
});
break :cmd cmd; pub const options: noclip.CommandOptions = .{};
pub const parameters = struct {
// pub const tuple: noclip.Option(U8Tuple) = .{
// .short = 't',
// .long = "tuple",
// .env = "NOCLIP_TEST",
// .description = "tuple test option",
// };
pub const choice: noclip.Option(Choice) = .{
.short = 'c',
.long = "choice",
.env = "NOCLIP_CHOICE",
.description = "enum choice option",
};
pub const string: noclip.Option(noclip.String) = .{
.short = 's',
.long = "string",
.env = "NOCLIP_STRING",
.description = "A string value option",
};
pub const default: noclip.Option(u32) = .{
.name = "default",
.description = "default value integer option",
.short = 'd',
.long = "default",
.env = "NOCLIP_DEFAULT",
.default = 100,
// .nice_type_name = "uint",
};
pub const counter: noclip.Counter(u32) = .{
.name = "default",
.description = "default value integer option",
.short = 'd',
.long = "default",
.env = "NOCLIP_DEFAULT",
// .nice_type_name = "uint",
};
pub const multi: noclip.Option(noclip.Accumulate(u8)) = .{
.name = "multi",
.short_tag = 'm',
.long_tag = "multi",
.description = "multiple specification test option",
};
pub const flag: noclip.BoolGroup = .{
.name = "flag",
.truthy = .{ .short = 'f', .long = "flag" },
.falsy = .{ .short = 'F', .long = "no-flag" },
.env = "NOCLIP_FLAG",
.description = "boolean flag",
};
// pub const multiflag: noclip.Flag = .{
// .name = "multiflag",
// .truthy = .{ .short = 'M' },
// .description = "multiple specification test flag",
// .multi = .accumulate,
// };
pub const env_only: noclip.Option(u8) = .{
.env = "NOCLIP_ENVIRON",
.description = "environment variable only option",
};
// pub const group: noclip.Group(bool) = .{
// .parameters = struct {
// pub const a: noclip.Flag = .{
// .truthy = .{ .short = 'z' },
// };
// },
// };
};
pub const subcommands = struct {
pub const subcommand = Subcommand;
pub const deeply = struct {
pub const info: noclip.CommandInfo = .{ .description = "start of a deeply nested subcommand" };
pub const subcommands = struct {
pub const nested = struct {
pub const info: noclip.CommandInfo = .{ .description = "second level of a deeply nested subcommand" };
pub const subcommands = struct {
pub const subcommand = struct {
pub const info: noclip.CommandInfo = .{ .description = "third level of a deeply nested subcommand" };
pub const subcommands = struct {
pub const group = struct {
pub const info: noclip.CommandInfo = .{ .description = "final level of a deeply nested subcommand" };
pub fn run() void {
std.debug.print("but it does nothing\n");
}
};
};
};
};
};
};
};
};
pub fn run(args: noclip.Result(Main)) void {
if (args.choice) |choice| {
std.debug.print("You chose: {s}\n", .{@tagName(choice)});
} else {
std.debug.print("You chose nothing!\n", .{});
}
}
pub fn err(report: noclip.ErrorReport) void {
_ = report;
}
}; };
const subcommand = cmd: { const Subcommand = struct {
var cmd = CommandBuilder([]const u8){ pub const info: noclip.CommandInfo = .{
.name = "subcommand",
.description = .description =
\\Demonstrate subcommand functionality \\Demonstrate subcommand functionality
\\ \\
\\This command demonstrates how subcommands work. \\This command demonstrates how subcommands work.
, ,
.options = .{ .parse_error_behavior = .exit },
};
pub const parameters = struct {
pub const flag: noclip.BoolGroup = .{
.truthy = .{ .short = 'f', .long = "flag" },
.falsy = .{ .long = "no-flag" },
.env = "NOCLIP_SUBFLAG",
};
pub const first_arg: noclip.Argument(noclip.String) = .{};
pub const second_arg: noclip.Argument(noclip.String) = .{
.description = "This is an argument that doesn't really do anything, but it's very important.",
};
}; };
cmd.simpleFlag(.{
.name = "flag",
.truthy = .{ .short_tag = "-f", .long_tag = "--flag" },
.falsy = .{ .long_tag = "--no-flag" },
.env_var = "NOCLIP_SUBFLAG",
});
cmd.stringArgument(.{ .name = "argument" });
cmd.stringArgument(.{
.name = "arg",
.description = "This is an argument that doesn't really do anything, but it's very important.",
});
break :cmd cmd;
}; };
fn subHandler(context: []const u8, result: subcommand.Output()) !void {
std.debug.print("subcommand: {s}\n", .{result.argument});
std.debug.print("context: {s}\n", .{context});
}
fn cliHandler(context: *u32, result: cli.Output()) !void {
std.debug.print("context: {d}\n", .{context.*});
std.debug.print("callback is working {s}\n", .{result.string orelse "null"});
std.debug.print("callback is working {any}\n", .{result.choice});
std.debug.print("callback is working {d}\n", .{result.default});
context.* += 1;
}
pub fn main() !u8 { pub fn main() !u8 {
var gpa = std.heap.GeneralPurposeAllocator(.{}){}; var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit(); defer _ = gpa.deinit();
const allocator = gpa.allocator(); const allocator = gpa.allocator();
const base = try noclip.commandGroup(allocator, .{ .description = "base group" }); const args = try std.process.argsAlloc(allocator);
defer base.deinitTree(); defer std.process.argsFree(allocator, args);
var env = try std.process.getEnvMap(allocator);
defer env.deinit();
var context: u32 = 2; try noclip.execute(Main, .{ .args = args, .env = env });
const sc: []const u8 = "whassup";
try base.addSubcommand("main", try cli.createInterface(allocator, cliHandler, &context));
try base.addSubcommand("other", try subcommand.createInterface(allocator, subHandler, &sc));
const group = try noclip.commandGroup(allocator, .{ .description = "final level of a deeply nested subcommand" });
const subcon = try noclip.commandGroup(allocator, .{ .description = "third level of a deeply nested subcommand" });
const nested = try noclip.commandGroup(allocator, .{ .description = "second level of a deeply nested subcommand" });
const deeply = try noclip.commandGroup(allocator, .{ .description = "start of a deeply nested subcommand" });
try base.addSubcommand("deeply", deeply);
try deeply.addSubcommand("nested", nested);
try nested.addSubcommand("subcommand", subcon);
try subcon.addSubcommand("group", group);
try group.addSubcommand("run", try cli.createInterface(allocator, cliHandler, &context));
base.execute() catch |err| {
std.io.getStdErr().writeAll(base.getParseError()) catch {};
return err;
};
return 0; return 0;
} }

View File

@ -1,161 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>NOCLIP</title>
<link rel="stylesheet" type="text/css" href="style.css">
<link rel="icon" type="image/png" href="icon.png">
</head>
<body>
<div class="layout"> <!-- flex or grid -->
<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">NOCLIP</label>
<nav class="nav collapse-view">
<a href="#title"><div class="item">Title</div></a>
<a href="#title-2"><div class="item">Title The Second</div></a>
<a href="#title-3"><div class="item">Title The Third</div></a>
</nav>
</aside>
<main class="doc">
<div class="doc-padding">
<section>
<div id="title" class="header">Title</div>
<div class="description">
<p>Some written description</p>
</div>
<div class="example"><div class="codebox">
<pre class="code-markup"><code class="lang-zig"><span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span></code></pre>
</div></div>
<div class="description">
<p>Some written description</p>
</div>
<div class="example"><div class="codebox">
<pre class="code-markup"><code class="lang-zig"><span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span></code></pre>
</div></div>
<div class="description">
<p>Some written description</p>
</div>
</section>
<section>
<div id="title-2" class="header">Title The Second</div>
<div class="description">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sit amet blandit nisi, id tempor tortor. Aliquam feugiat luctus molestie. Maecenas vel consequat mauris, ut volutpat enim. Suspendisse in mauris nibh. Maecenas viverra enim quis nibh cursus condimentum. Aenean eleifend ante in magna vestibulum imperdiet ut ut augue. Sed cursus nunc turpis, sit amet congue ex varius a. Sed a lorem volutpat, tempus lectus vitae, vulputate dui. In eu convallis neque. Suspendisse potenti.</p>
<p>Vestibulum venenatis mi facilisis ullamcorper fringilla. Integer posuere arcu ac ante consequat malesuada. Phasellus condimentum ornare diam in fermentum. Cras dictum consequat vehicula. Aliquam varius mattis diam sit amet imperdiet. Nunc nec orci bibendum, porta purus nec, facilisis enim. Etiam ultrices sodales quam, a scelerisque quam dapibus et. Mauris dapibus viverra leo id bibendum. Donec congue tempor tortor, ac consectetur augue rutrum ut. Nullam blandit accumsan nibh et iaculis.</p>
<p>Ut hendrerit magna et turpis rutrum suscipit. Phasellus nulla enim, tincidunt sit amet mi vitae, vehicula posuere enim. Praesent vitae libero sollicitudin, faucibus nisl facilisis, pulvinar elit. Ut id magna id enim bibendum iaculis eget non orci. Aliquam elementum felis quam, quis varius felis malesuada non. Sed bibendum dui aliquet tortor rutrum, nec iaculis nisi dapibus. Donec condimentum est sed ligula volutpat, vitae consequat leo euismod. Integer nec erat magna. Curabitur non pellentesque magna, quis ultrices libero. Aenean mattis ut orci a gravida. Nullam dapibus aliquam lorem, vitae hendrerit mauris.</p>
<p>Integer nec elit aliquam, fringilla lacus ac, venenatis purus. Etiam mattis interdum bibendum. Pellentesque tincidunt ex ut hendrerit molestie. Mauris non enim porttitor nibh pretium auctor. Nulla vehicula commodo diam ac suscipit. Fusce blandit convallis erat et mattis. Nunc sagittis efficitur purus, at mattis magna sagittis sit amet. Quisque nec purus placerat, posuere tortor non, feugiat purus. Suspendisse eget dapibus mauris, eget dictum quam. Praesent turpis risus, interdum quis porttitor eu, faucibus ut nulla. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Duis aliquam nunc mauris, ac ullamcorper magna imperdiet sed. Ut quam mi, tincidunt vitae pretium id, aliquam sed ipsum. Proin quam massa, cursus in nisl non, lacinia egestas sem.</p>
<p>Nullam interdum lorem a mi rhoncus ullamcorper. Nulla malesuada lectus rhoncus turpis ullamcorper, eget faucibus purus elementum. Quisque consectetur ornare risus, non mattis quam pretium at. Etiam laoreet velit a risus dapibus maximus. Nam non ornare magna, eu accumsan erat. Sed mattis sodales orci, et sagittis neque placerat vel. In hac habitasse platea dictumst. Pellentesque posuere tincidunt nisi, eu faucibus nunc pulvinar nec. Morbi tempus a mi non eleifend. Sed gravida orci auctor risus consequat, at iaculis est vulputate. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Fusce tincidunt, purus sed pellentesque blandit, ante lacus mollis risus, et cursus magna nibh nec eros.</p>
</div>
<div class="example"><div class="codebox">
<pre class="code-markup"><code class="lang-zig"><span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line>const subcommand = cmd: {</span>
<span class=line> <span class="keyword">var</span> cmd = CommandBuilder(void){</span>
<span class=line> .description =</span>
<span class=line> <span class="string">\\Perform some sort of work</span></span>
<span class=line> <span class="string">\\</span></span>
<span class=line> <span class="string">\\This subcommand is a mystery. It probably does something, but nobody is sure what. This line just keeps going on forever and it never stops and it's very wide and I wish you could see how incredibly wide it is, it's so wide it's just the widest thing you can imagine, but even wider than that somehow.</span></span>
<span class=line> ,</span>
<span class=line> };</span>
<span class=line> cmd.add_flag(.{}, .{</span>
<span class=line> .name = "flag",</span>
<span class=line> .truthy = .{ .short_tag = "-f", .long_tag = "--flag" },</span>
<span class=line> .falsy = .{ .long_tag = "--no-flag" },</span>
<span class=line> .env_var = "NOCLIP_SUBFLAG",</span>
<span class=line> });</span>
<span class=line> cmd.add_argument(.{ .OutputType = []const u8 }, .{ .name = "argument" });</span>
<span class=line> break :cmd cmd;</span>
<span class=line>};</span></code></pre>
</div></div>
<div class="description">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sit amet blandit nisi, id tempor tortor. Aliquam feugiat luctus molestie. Maecenas vel consequat mauris, ut volutpat enim. Suspendisse in mauris nibh. Maecenas viverra enim quis nibh cursus condimentum. Aenean eleifend ante in magna vestibulum imperdiet ut ut augue. Sed cursus nunc turpis, sit amet congue ex varius a. Sed a lorem volutpat, tempus lectus vitae, vulputate dui. In eu convallis neque. Suspendisse potenti.</p>
<p>Vestibulum venenatis mi facilisis ullamcorper fringilla. Integer posuere arcu ac ante consequat malesuada. Phasellus condimentum ornare diam in fermentum. Cras dictum consequat vehicula. Aliquam varius mattis diam sit amet imperdiet. Nunc nec orci bibendum, porta purus nec, facilisis enim. Etiam ultrices sodales quam, a scelerisque quam dapibus et. Mauris dapibus viverra leo id bibendum. Donec congue tempor tortor, ac consectetur augue rutrum ut. Nullam blandit accumsan nibh et iaculis.</p>
<p>Ut hendrerit magna et turpis rutrum suscipit. Phasellus nulla enim, tincidunt sit amet mi vitae, vehicula posuere enim. Praesent vitae libero sollicitudin, faucibus nisl facilisis, pulvinar elit. Ut id magna id enim bibendum iaculis eget non orci. Aliquam elementum felis quam, quis varius felis malesuada non. Sed bibendum dui aliquet tortor rutrum, nec iaculis nisi dapibus. Donec condimentum est sed ligula volutpat, vitae consequat leo euismod. Integer nec erat magna. Curabitur non pellentesque magna, quis ultrices libero. Aenean mattis ut orci a gravida. Nullam dapibus aliquam lorem, vitae hendrerit mauris.</p>
<p>Integer nec elit aliquam, fringilla lacus ac, venenatis purus. Etiam mattis interdum bibendum. Pellentesque tincidunt ex ut hendrerit molestie. Mauris non enim porttitor nibh pretium auctor. Nulla vehicula commodo diam ac suscipit. Fusce blandit convallis erat et mattis. Nunc sagittis efficitur purus, at mattis magna sagittis sit amet. Quisque nec purus placerat, posuere tortor non, feugiat purus. Suspendisse eget dapibus mauris, eget dictum quam. Praesent turpis risus, interdum quis porttitor eu, faucibus ut nulla. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Duis aliquam nunc mauris, ac ullamcorper magna imperdiet sed. Ut quam mi, tincidunt vitae pretium id, aliquam sed ipsum. Proin quam massa, cursus in nisl non, lacinia egestas sem.</p>
<p>Nullam interdum lorem a mi rhoncus ullamcorper. Nulla malesuada lectus rhoncus turpis ullamcorper, eget faucibus purus elementum. Quisque consectetur ornare risus, non mattis quam pretium at. Etiam laoreet velit a risus dapibus maximus. Nam non ornare magna, eu accumsan erat. Sed mattis sodales orci, et sagittis neque placerat vel. In hac habitasse platea dictumst. Pellentesque posuere tincidunt nisi, eu faucibus nunc pulvinar nec. Morbi tempus a mi non eleifend. Sed gravida orci auctor risus consequat, at iaculis est vulputate. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Fusce tincidunt, purus sed pellentesque blandit, ante lacus mollis risus, et cursus magna nibh nec eros.</p>
</div>
<div class="example"><div class="codebox">
<pre class="code-markup"><code class="lang-zig"><span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="test">lonst</span> lubcommand = cmd: {</span>
<span class=line> <span class="keyword">var</span> cmd = CommandBuilder(void){</span>
<span class=line> .description =</span>
<span class=line> <span class="string">\\Perform some sort of work</span></span>
<span class=line> <span class="string">\\</span></span>
<span class=line> <span class="string">\\This subcommand is a mystery. It probably does something, but nobody is sure what.</span></span>
<span class=line> ,</span>
<span class=line> };</span>
<span class=line> cmd.add_flag(.{}, .{</span>
<span class=line> .name = "flag",</span>
<span class=line> .truthy = .{ .short_tag = "-f", .long_tag = "--flag" },</span>
<span class=line> .falsy = .{ .long_tag = "--no-flag" },</span>
<span class=line> .env_var = "NOCLIP_SUBFLAG",</span>
<span class=line> });</span>
<span class=line> cmd.add_argument(.{ .OutputType = []const u8 }, .{ .name = "argument" });</span>
<span class=line> break :cmd cmd;</span>
<span class=line>};</span></code></pre>
</div></div>
</section>
<section>
<div id="title-3" class="header">Title The Third</div>
<div class="description">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sit amet blandit nisi, id tempor tortor. Aliquam feugiat luctus molestie. Maecenas vel consequat mauris, ut volutpat enim. Suspendisse in mauris nibh. Maecenas viverra enim quis nibh cursus condimentum. Aenean eleifend ante in magna vestibulum imperdiet ut ut augue. Sed cursus nunc turpis, sit amet congue ex varius a. Sed a lorem volutpat, tempus lectus vitae, vulputate dui. In eu convallis neque. Suspendisse potenti.</p>
<p>Vestibulum venenatis mi facilisis ullamcorper fringilla. Integer posuere arcu ac ante consequat malesuada. Phasellus condimentum ornare diam in fermentum. Cras dictum consequat vehicula. Aliquam varius mattis diam sit amet imperdiet. Nunc nec orci bibendum, porta purus nec, facilisis enim. Etiam ultrices sodales quam, a scelerisque quam dapibus et. Mauris dapibus viverra leo id bibendum. Donec congue tempor tortor, ac consectetur augue rutrum ut. Nullam blandit accumsan nibh et iaculis.</p>
<p>Ut hendrerit magna et turpis rutrum suscipit. Phasellus nulla enim, tincidunt sit amet mi vitae, vehicula posuere enim. Praesent vitae libero sollicitudin, faucibus nisl facilisis, pulvinar elit. Ut id magna id enim bibendum iaculis eget non orci. Aliquam elementum felis quam, quis varius felis malesuada non. Sed bibendum dui aliquet tortor rutrum, nec iaculis nisi dapibus. Donec condimentum est sed ligula volutpat, vitae consequat leo euismod. Integer nec erat magna. Curabitur non pellentesque magna, quis ultrices libero. Aenean mattis ut orci a gravida. Nullam dapibus aliquam lorem, vitae hendrerit mauris.</p>
<p>Integer nec elit aliquam, fringilla lacus ac, venenatis purus. Etiam mattis interdum bibendum. Pellentesque tincidunt ex ut hendrerit molestie. Mauris non enim porttitor nibh pretium auctor. Nulla vehicula commodo diam ac suscipit. Fusce blandit convallis erat et mattis. Nunc sagittis efficitur purus, at mattis magna sagittis sit amet. Quisque nec purus placerat, posuere tortor non, feugiat purus. Suspendisse eget dapibus mauris, eget dictum quam. Praesent turpis risus, interdum quis porttitor eu, faucibus ut nulla. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Duis aliquam nunc mauris, ac ullamcorper magna imperdiet sed. Ut quam mi, tincidunt vitae pretium id, aliquam sed ipsum. Proin quam massa, cursus in nisl non, lacinia egestas sem.</p>
<p>Nullam interdum lorem a mi rhoncus ullamcorper. Nulla malesuada lectus rhoncus turpis ullamcorper, eget faucibus purus elementum. Quisque consectetur ornare risus, non mattis quam pretium at. Etiam laoreet velit a risus dapibus maximus. Nam non ornare magna, eu accumsan erat. Sed mattis sodales orci, et sagittis neque placerat vel. In hac habitasse platea dictumst. Pellentesque posuere tincidunt nisi, eu faucibus nunc pulvinar nec. Morbi tempus a mi non eleifend. Sed gravida orci auctor risus consequat, at iaculis est vulputate. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Fusce tincidunt, purus sed pellentesque blandit, ante lacus mollis risus, et cursus magna nibh nec eros.</p>
</div>
<div class="example"><div class="codebox">
<pre class="code-markup"><code class="lang-zig"><span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span></code></pre>
</div></div>
<div class="description">
<p>Some written description</p>
</div>
<div class="example"><div class="codebox">
<pre class="code-markup"><code class="lang-zig"><span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span>
<span class=line><span class="keyword">const</span> std = <span class="builtin">@import</span>(<span class="string">"std"</span>);</span></code></pre>
</div></div>
</section>
</div>
</main>
</div>
</body>
</html>

View File

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

View File

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

View File

@ -1,14 +0,0 @@
<!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

@ -1,317 +0,0 @@
@charset "UTF-8";
:root {
/* Base16-Eighties, lightly modified*/
--gray-n2: #1E1E1E;
--gray-n1: #262626;
--gray-0: #2D2D2D;
--gray-1: #393939;
--gray-2: #515151;
--gray-3: #747369;
--gray-4: #A09F93;
--gray-5: #D3D0C8; /* foreground */
--gray-6: #E8E6DF;
--gray-7: #F2F0EC;
--red: #F2777A;
--orange: #F99157;
--yellow: #FFCC66;
--green: #99CC99;
--aqua: #66CCCC;
--blue: #6699CC;
--purple: #CC99CC;
--pink: #FFCCFF;
/* Semantic aliases*/
--background-color: var(--gray-0);
--foreground-color: var(--gray-5);
--nav-color: var(--gray-n2);
--sidebar-color: var(--gray-n1);
--border-color: var(--gray-1);
--example-min-width: 950px;
--description-max-width: 850px;
--description-min-width: 600px;
--section-header-height: 70px;
--nav-sidebar-width: 270px;
--nav-horizontal-padding: 30px;
}
/*@media (prefers-color-scheme: light) {
:root {
--background-color: var(--gray-7);
--foreground-color: var(--gray-1);
--sidebar-color: var(--gray-6);
--border-color: var(--gray-6);
}
}*/
body {
background: var(--background-color);
color: var(--foreground-color);
margin: 0;
font-size: 16pt;
font-family: sans-serif;
}
pre { margin: 0; }
p {
color: var(--foreground-color);
hyphens: auto;
-webkit-hyphens: auto;
margin: 0;
}
p + p {
text-indent: 1em;
}
body .layout {
height: 100vh;
display: grid;
grid-template-rows: 1fr;
grid-template-columns: auto 1fr;
grid-template-areas: "sidebar doc";
max-width: 100vw;
}
body .layout .sidebar {
grid-area: sidebar;
/* border-right: 2px solid var(--border-color);*/
background: var(--nav-color);
overflow: scroll;
}
body .layout .sidebar nav {
padding-bottom: 1em;
}
body .layout .sidebar .header {
position: sticky;
top: 0;
box-sizing: border-box;
background: var(--nav-color);
height: var(--section-header-height);
line-height: var(--section-header-height);
font-size: 20pt;
padding: 0 var(--nav-horizontal-padding);
width: var(--nav-sidebar-width);
}
label.collapse-label {
display: block;
cursor: pointer;
}
input.collapse-toggle {
display: none;
}
input.collapse-toggle:checked + label.header {
color: var(--blue);
transform: rotate(90deg);
width: var(--section-header-height);
}
input.collapse-toggle:checked + label.collapse-label + .collapse-view {
max-height: 0;
max-width: 0;
padding: 0;
overflow: clip;
}
body .layout .sidebar .nav a {
color: var(--gray-4);
text-decoration: none;
}
body .layout .sidebar .nav a:hover {
color: var(--blue);
}
body .layout .sidebar .nav .item {
box-sizing: border-box;
padding: 0.2em var(--nav-horizontal-padding);
}
body .layout .doc {
grid-area: doc;
height: 100vh;
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 {
padding-bottom: 80vh;
max-width: 100%;
}
body .layout .doc .doc-padding::after {
bottom: -40vh;
color: var(--gray-1);
content: 'THIS OVERSCROLL INTENTIONALLY LEFT BLANK';
display: block;
font-size: 30px;
font-weight: bold;
position: relative;
text-align: center;
width: 100%;
}
body .layout .doc section {
display: grid;
grid-template-columns: minmax(var(--description-min-width), var(--description-max-width)) minmax(var(--example-min-width), 1fr);
grid-template-areas: "description example";
}
body .layout .doc section .header {
grid-column-start: description;
grid-column-end: example;
background: var(--sidebar-color);
font-size: 20pt;
line-height: var(--section-header-height);
padding: 0 1ex;
/* position: sticky;*/
/* top: 0;*/
/* border-bottom: 1px solid var(--border-color);*/
}
body .layout .doc section .header::before {
content: "#";
display: inline-block;
visibility: hidden;
margin-right: 0.3em;
color: var(--gray-3);
}
body .layout .doc section .header:hover::before {
visibility: visible;
}
body .layout .doc section .description {
grid-column-start: description;
grid-column-end: description;
box-sizing: border-box;
padding: 1em;
text-align: justify;
}
body .layout .doc section .example {
grid-column-start: example;
grid-column-end: example;
overflow: visible;
}
body .layout .doc section .example .codebox {
box-sizing: border-box;
position: sticky;
top: 0;
overflow-x: scroll;
background: var(--sidebar-color);
padding: 1em 1em;
border-bottom: 15px solid var(--background-color);
}
body .layout .doc section .example .code-markup {
counter-reset: example;
font-size: 12pt;
line-height: 1.3;
/* border-left: 2px inset black;*/
/* box-shadow: 10px 0 10px -10px var(--gray-n2) inset;*/
/* top: var(--section-header-height);*/
}
body .layout .doc section .example .code-markup .line {
padding-right: 1em;
}
.code-markup .keyword { color: var(--purple); }
.code-markup .type { color: var(--purple); }
.code-markup .builtin { color: var(--aqua); }
.code-markup .string { color: var(--green); }
.code-markup .comment { color: var(--gray-3); }
.code-markup .literal { color: var(--orange); }
.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.*/
@media (max-width: 1820px) {
body .layout .doc section {
display: grid;
grid-template-columns: 1fr;
grid-template-areas: "header" "description" "example";
}
body .layout .doc section .description {
max-width: var(--description-max-width);
}
body .layout .doc section .example .codebox {
max-width: calc(100vw - var(--nav-sidebar-width));
border-bottom: none;
position: initial;
}
body .layout:has(> .sidebar #destroyer-of-navs:checked) .doc section .example .codebox {
max-width: calc(100vw - var(--section-header-height));
}
body .layout .doc section .example:last-child {
margin-bottom: 15px;
}
}
@media (max-width: 1220px) {
body .layout {
display: grid;
grid-template-rows: auto auto;
grid-template-columns: 1fr;
grid-template-areas: "sidebar" "doc";
height: initial;
max-width: 100vw;
}
body .layout .sidebar {
overflow: visible;
position: sticky;
top: 0;
z-index: 1;
}
input.collapse-toggle:checked + label.header {
width: 100%;
transform: initial;
}
body .layout .doc {
height: initial;
overflow-y: initial;
}
body .layout .doc section .description {
width: 100%;
margin: auto;
}
body .layout .doc section .example .codebox,
body .layout:has(> .sidebar #destroyer-of-navs:checked) .doc section .example .codebox {
max-width: 100vw;
}
}

View File

@ -1,803 +0,0 @@
// this borrows code from zig-doctest
// zig-doctest is distributed under the MIT license Copyright (c) 2020 Loris Cro
// see: https://github.com/kristoff-it/zig-doctest/blob/db507d803dd23e2585166f5b7e479ffc96d8b5c9/LICENSE
const noclip = @import("noclip");
const std = @import("std");
const mem = std.mem;
const fs = std.fs;
const print = std.debug.print;
inline fn escape_char(out: anytype, char: u8) !void {
return try switch (char) {
'&' => out.writeAll("&amp;"),
'<' => out.writeAll("&lt;"),
'>' => out.writeAll("&gt;"),
'"' => out.writeAll("&quot;"),
else => out.writeByte(char),
};
}
fn write_escaped(out: anytype, input: []const u8, class: TokenClass) !void {
if (class == .whitespace) {
try write_whitespace(out, input);
} else {
for (input) |c| try escape_char(out, c);
}
}
fn write_whitespace(out: anytype, input: []const u8) !void {
var state: enum { normal, maybe_comment, maybe_docstring, comment } = .normal;
for (input) |c| {
switch (state) {
.normal => switch (c) {
'/' => state = .maybe_comment,
'\n' => try out.writeAll("</span>\n<span class=\"line\">"),
else => try escape_char(out, c),
},
.maybe_comment => switch (c) {
'/' => {
state = .maybe_docstring;
},
'\n' => {
try out.writeAll("</span>\n<span class=\"line\">");
state = .normal;
},
else => {
try out.writeByte('/');
try escape_char(out, c);
state = .normal;
},
},
.maybe_docstring => switch (c) {
'\n' => {
// actually it was an empty comment lol cool
try out.writeAll("<span class=\"comment\">//</span></span>\n<span class=\"line\">");
state = .normal;
},
'/', '!' => {
// it is a docstring, so don't respan it
try out.writeAll("//");
try out.writeByte(c);
state = .normal;
},
else => {
// this is also a comment
try out.writeAll("<span class=\"comment\">//");
try escape_char(out, c);
state = .comment;
},
},
.comment => switch (c) {
'\n' => {
try out.writeAll("</span></span>\n<span class=\"line\">");
state = .normal;
},
else => {
try escape_char(out, c);
},
},
}
}
}
// TODO: use more context to get better token resolution
//
// identifier preceded by dot, not preceded by name, and followed by (, | => | == | != | rbrace | rparen | and | or | ;) is an enum literal
//
// identifier followed by ( is always a function call
//
// identifier preceded by : is a type until = or , or ) (except after [, where its the terminator)
// identifier followed by { is a type
// identifier after | is a bind
const ContextToken = struct {
tag: std.zig.Token.Tag,
content: []const u8,
class: TokenClass = .needs_context,
};
const TokenClass = enum {
keyword,
string,
builtin,
type,
function,
label,
doc_comment,
literal_primitive,
literal_number,
literal_enum,
field_name,
symbology,
whitespace,
context_free,
needs_context,
pub fn name(self: @This()) []const u8 {
return switch (self) {
.doc_comment => "doc comment",
.literal_primitive => "literal primitive",
.literal_number => "literal number",
.literal_enum => "literal enum",
.field_name => "field-name",
.symbology => "",
.context_free => "",
.whitespace => "",
.needs_context => @panic("too late"),
else => @tagName(self),
};
}
};
pub const ContextManager = struct {
// const Queue = std.TailQueue(ContextToken);
tokens: std.ArrayList(ContextToken),
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator) @This() {
return .{
.allocator = allocator,
.tokens = std.ArrayList(ContextToken).init(allocator),
};
}
pub fn deinit(self: *@This()) void {
self.tokens.deinit();
}
pub fn push_back(self: *@This(), token: ContextToken) !void {
try self.tokens.append(token);
}
fn print_span(content: []const u8, class: TokenClass, out: anytype) !void {
const classname = class.name();
if (classname.len > 0) {
try out.print("<span class=\"{s}\">", .{classname});
try write_escaped(out, content, class);
try out.writeAll("</span>");
} else {
try write_escaped(out, content, class);
}
}
fn print_fused_span(tokens: []ContextToken, start: usize, end: usize, out: anytype) !void {
const classname = tokens[start].class.name();
if (classname.len > 0) try out.print("<span class=\"{s}\">", .{classname});
for (tokens[start..end]) |*token| {
try write_escaped(out, token.content, tokens[start].class);
}
if (classname.len > 0) try out.writeAll("</span>");
}
pub fn process(self: *@This(), out: anytype) !void {
const tokens = self.tokens.items;
if (tokens.len == 0) return;
for (tokens, 0..) |*token, idx| {
if (token.class == .needs_context)
if (!contextualize_identifier(tokens, idx)) @panic("failed to context");
}
var idx: usize = 0;
while (idx < tokens.len) : (idx += 1) {
const span_start = idx;
const token = &tokens[idx];
// std.debug.print("tok {d}: {s} {}\n", .{ idx, token.content, token.class });
var lookahead = idx + 1;
while (lookahead < tokens.len) : (lookahead += 1) {
// std.debug.print("look {d}: {s} {}\n", .{ lookahead, tokens[lookahead].content, tokens[lookahead].class });
if (tokens[lookahead].class != .whitespace) {
if (tokens[lookahead].class == token.class)
idx = lookahead
else
break;
} else {
if (std.mem.containsAtLeast(u8, tokens[lookahead].content, 1, "\n")) break;
}
}
if (idx > span_start) {
try print_fused_span(tokens, span_start, idx + 1, out);
} else {
try print_span(token.content, token.class, out);
}
}
}
fn contextualize_identifier(tokens: []ContextToken, current: usize) bool {
return (contextualize_function(tokens, current) or
contextualize_builtin_type(tokens, current) or
contextualize_label(tokens, current) or
contextualize_struct_field(tokens, current) or
contextualize_fallback(tokens, current));
}
fn contextualize_function(tokens: []ContextToken, current: usize) bool {
const prev = prev_valid(tokens, current) orelse return false;
if (tokens[prev].tag == .keyword_fn) {
tokens[current].class = .function;
return true;
}
if (current < tokens.len - 1 and tokens[current + 1].tag == .l_paren) {
tokens[current].class = .function;
return true;
}
return false;
}
fn contextualize_builtin_type(tokens: []ContextToken, current: usize) bool {
const content = tokens[current].content;
const is_int = blk: {
if ((content[0] != 'i' and content[0] != 'u') or content.len < 2 or content.len > 6)
break :blk false;
for (content[1..]) |char|
if (char < '0' or char > '9') break :blk false;
break :blk true;
};
if (is_int or is_type(content)) {
tokens[current].class = .type;
return true;
}
return false;
}
fn contextualize_label(tokens: []ContextToken, current: usize) bool {
blk: {
const prev = prev_valid(tokens, current) orelse break :blk;
if (tokens[prev].tag == .colon) {
const prev2 = prev_valid(tokens, prev) orelse break :blk;
switch (tokens[prev2].tag) {
.keyword_break, .keyword_continue => {
tokens[prev].class = .label;
tokens[current].class = .label;
return true;
},
else => break :blk,
}
}
}
blk: {
const next = next_valid(tokens, current) orelse break :blk;
if (tokens[next].tag == .colon) {
const next2 = next_valid(tokens, next) orelse break :blk;
switch (tokens[next2].tag) {
.keyword_inline, .keyword_for, .keyword_while, .l_brace => {
tokens[current].class = .label;
tokens[next].class = .label;
return true;
},
else => break :blk,
}
}
}
return false;
}
fn contextualize_struct_field(tokens: []ContextToken, current: usize) bool {
if (current == 0) return false;
if (tokens[current - 1].tag != .period) return false;
const precursor = prev_valid(tokens, current - 1) orelse return false;
const succesor = next_valid(tokens, current) orelse return false;
if ((tokens[precursor].tag == .l_brace or
tokens[precursor].tag == .comma) and
tokens[succesor].tag == .equal)
{
tokens[current - 1].class = .field_name;
tokens[current].class = .field_name;
return true;
}
return false;
}
fn contextualize_fallback(tokens: []ContextToken, current: usize) bool {
tokens[current].class = .context_free;
return true;
}
fn next_valid(tokens: []ContextToken, current: usize) ?usize {
var check = current + 1;
while (check < tokens.len) : (check += 1) {
if (tokens[check].class != .whitespace) return check;
}
return null;
}
fn prev_valid(tokens: []ContextToken, current: usize) ?usize {
if (current == 0) return null;
var check = current - 1;
while (check > 0) : (check -= 1) {
if (tokens[check].class != .whitespace) return check;
}
if (tokens[check].class != .whitespace) return check;
return null;
}
};
pub fn trimZ(comptime T: type, input: [:0]T, trimmer: []const T) [:0]T {
var begin: usize = 0;
var end: usize = input.len;
while (begin < end and std.mem.indexOfScalar(T, trimmer, input[begin]) != null) : (begin += 1) {}
while (end > begin and std.mem.indexOfScalar(T, trimmer, input[end - 1]) != null) : (end -= 1) {}
input[end] = 0;
return input[begin..end :0];
}
pub fn write_tokenized_html(raw_src: [:0]u8, allocator: std.mem.Allocator, out: anytype, full: bool) !void {
const src = trimZ(u8, raw_src, "\n");
var tokenizer = std.zig.Tokenizer.init(src);
var last_token_end: usize = 0;
if (full) try out.writeAll(html_preamble);
try out.writeAll("<pre class=\"code-markup\"><code class=\"lang-zig\"><span class=\"line\">");
var manager = ContextManager.init(allocator);
defer manager.deinit();
while (true) {
const token = tokenizer.next();
if (last_token_end < token.loc.start) {
try manager.push_back(.{
.tag = .invalid, // TODO: this is a big hack
.content = src[last_token_end..token.loc.start],
.class = .whitespace,
});
}
switch (token.tag) {
.eof => break,
.keyword_addrspace,
.keyword_align,
.keyword_and,
.keyword_asm,
.keyword_async,
.keyword_await,
.keyword_break,
.keyword_catch,
.keyword_comptime,
.keyword_const,
.keyword_continue,
.keyword_defer,
.keyword_else,
.keyword_enum,
.keyword_errdefer,
.keyword_error,
.keyword_export,
.keyword_extern,
.keyword_for,
.keyword_if,
.keyword_inline,
.keyword_noalias,
.keyword_noinline,
.keyword_nosuspend,
.keyword_opaque,
.keyword_or,
.keyword_orelse,
.keyword_packed,
.keyword_anyframe,
.keyword_pub,
.keyword_resume,
.keyword_return,
.keyword_linksection,
.keyword_callconv,
.keyword_struct,
.keyword_suspend,
.keyword_switch,
.keyword_test,
.keyword_threadlocal,
.keyword_try,
.keyword_union,
.keyword_unreachable,
.keyword_usingnamespace,
.keyword_var,
.keyword_volatile,
.keyword_allowzero,
.keyword_while,
.keyword_anytype,
.keyword_fn,
=> try manager.push_back(.{
.tag = token.tag,
.content = src[token.loc.start..token.loc.end],
.class = .keyword,
}),
.string_literal,
.char_literal,
=> try manager.push_back(.{
.tag = token.tag,
.content = src[token.loc.start..token.loc.end],
.class = .string,
}),
.multiline_string_literal_line => {
try manager.push_back(.{
.tag = token.tag,
.content = src[token.loc.start .. token.loc.end - 1],
.class = .string,
});
// multiline string literals contain a newline, but we don't want to
// tokenize it like that.
try manager.push_back(.{
.tag = .invalid,
.content = src[token.loc.end - 1 .. token.loc.end],
.class = .whitespace,
});
},
.builtin => try manager.push_back(.{
.tag = token.tag,
.content = src[token.loc.start..token.loc.end],
.class = .builtin,
}),
.doc_comment,
.container_doc_comment,
=> {
try manager.push_back(.{
.tag = token.tag,
.content = src[token.loc.start..token.loc.end],
.class = .doc_comment,
});
},
.identifier => {
const content = src[token.loc.start..token.loc.end];
try manager.push_back(.{
.tag = token.tag,
.content = content,
.class = if (mem.eql(u8, content, "undefined") or
mem.eql(u8, content, "null") or
mem.eql(u8, content, "true") or
mem.eql(u8, content, "false"))
.literal_primitive
else
.needs_context,
});
},
.number_literal => try manager.push_back(.{
.tag = token.tag,
.content = src[token.loc.start..token.loc.end],
.class = .literal_number,
}),
.bang,
.pipe,
.pipe_pipe,
.pipe_equal,
.equal,
.equal_equal,
.equal_angle_bracket_right,
.bang_equal,
.l_paren,
.r_paren,
.semicolon,
.percent,
.percent_equal,
.l_brace,
.r_brace,
.l_bracket,
.r_bracket,
.period,
.period_asterisk,
.ellipsis2,
.ellipsis3,
.caret,
.caret_equal,
.plus,
.plus_plus,
.plus_equal,
.plus_percent,
.plus_percent_equal,
.minus,
.minus_equal,
.minus_percent,
.minus_percent_equal,
.asterisk,
.asterisk_equal,
.asterisk_asterisk,
.asterisk_percent,
.asterisk_percent_equal,
.arrow,
.colon,
.slash,
.slash_equal,
.comma,
.ampersand,
.ampersand_equal,
.question_mark,
.angle_bracket_left,
.angle_bracket_left_equal,
.angle_bracket_angle_bracket_left,
.angle_bracket_angle_bracket_left_equal,
.angle_bracket_right,
.angle_bracket_right_equal,
.angle_bracket_angle_bracket_right,
.angle_bracket_angle_bracket_right_equal,
.tilde,
.plus_pipe,
.plus_pipe_equal,
.minus_pipe,
.minus_pipe_equal,
.asterisk_pipe,
.asterisk_pipe_equal,
.angle_bracket_angle_bracket_left_pipe,
.angle_bracket_angle_bracket_left_pipe_equal,
=> try manager.push_back(.{
.tag = token.tag,
.content = src[token.loc.start..token.loc.end],
.class = .symbology,
}),
.invalid,
.invalid_periodasterisks,
=> return parseError(src, token, "syntax error", .{}),
}
last_token_end = token.loc.end;
}
try manager.process(out);
try out.writeAll("</span></code></pre>");
if (full) try out.writeAll(html_epilogue);
}
// TODO: this function returns anyerror, interesting
fn parseError(src: []const u8, token: std.zig.Token, comptime fmt: []const u8, args: anytype) anyerror {
const loc = getTokenLocation(src, token);
// const args_prefix = .{ tokenizer.source_file_name, loc.line + 1, loc.column + 1 };
// print("{s}:{d}:{d}: error: " ++ fmt ++ "\n", args_prefix ++ args);
const args_prefix = .{ loc.line + 1, loc.column + 1 };
print("{d}:{d}: error: " ++ fmt ++ "\n", args_prefix ++ args);
if (loc.line_start <= loc.line_end) {
print("{s}\n", .{src[loc.line_start..loc.line_end]});
{
var i: usize = 0;
while (i < loc.column) : (i += 1) {
print(" ", .{});
}
}
{
const caret_count = token.loc.end - token.loc.start;
var i: usize = 0;
while (i < caret_count) : (i += 1) {
print("~", .{});
}
}
print("\n", .{});
}
return error.ParseError;
}
const builtin_types = [_][]const u8{
"f16", "f32", "f64", "f128", "c_longdouble", "c_short",
"c_ushort", "c_int", "c_uint", "c_long", "c_ulong", "c_longlong",
"c_ulonglong", "c_char", "c_void", "void", "bool", "isize",
"usize", "noreturn", "type", "anyerror", "comptime_int", "comptime_float",
};
fn is_type(name: []const u8) bool {
for (builtin_types) |t| {
if (mem.eql(u8, t, name))
return true;
}
return false;
}
const Location = struct {
line: usize,
column: usize,
line_start: usize,
line_end: usize,
};
fn getTokenLocation(src: []const u8, token: std.zig.Token) Location {
var loc = Location{
.line = 0,
.column = 0,
.line_start = 0,
.line_end = 0,
};
for (src, 0..) |c, i| {
if (i == token.loc.start) {
loc.line_end = i;
while (loc.line_end < src.len and src[loc.line_end] != '\n') : (loc.line_end += 1) {}
return loc;
}
if (c == '\n') {
loc.line += 1;
loc.column = 0;
loc.line_start = i + 1;
} else {
loc.column += 1;
}
}
return loc;
}
pub fn tokenize_buffer(
buffer: []const u8,
allocator: std.mem.Allocator,
writer: anytype,
full_html: bool,
) !void {
const intermediate = try allocator.dupeZ(u8, buffer);
defer allocator.free(intermediate);
try write_tokenized_html(intermediate, allocator, writer, full_html);
}
pub fn tokenize_file(
file_name: []const u8,
allocator: std.mem.Allocator,
writer: anytype,
full_html: bool,
) !void {
const srcbuf = blk: {
const file = fs.cwd().openFile(file_name, .{ .mode = .read_only }) catch |err| {
std.debug.print("couldnt open {s}\n", .{file_name});
return err;
};
defer file.close();
break :blk try file.readToEndAllocOptions(
allocator,
1_000_000,
null,
@alignOf(u8),
0,
);
};
defer allocator.free(srcbuf);
try write_tokenized_html(srcbuf, allocator, writer, full_html);
}
const html_preamble =
\\<!DOCTYPE html>
\\<html>
\\ <head>
\\ <style>
\\:root {
\\ --background: #2D2D2D;
\\ --foreground: #D3D0C8;
\\ --red: #F2777A;
\\ --orange: #F99157;
\\ --yellow: #FFCC66;
\\ --green: #99CC99;
\\ --aqua: #66CCCC;
\\ --blue: #6699CC;
\\ --purple: #CC99CC;
\\ --pink: #FFCCFF;
\\ --gray: #747369;
\\}
\\body {
\\ background: var(--background);
\\ color: var(--foreground);
\\}
\\.code-markup {
\\ padding: 0;
\\ font-size: 16pt;
\\ line-height: 1.1;
\\}
\\.code-markup .keyword { color: var(--purple); }
\\.code-markup .type { color: var(--purple); }
\\.code-markup .builtin { color: var(--aqua); }
\\.code-markup .string { color: var(--green); }
\\.code-markup .comment { color: var(--gray); }
\\.code-markup .literal { color: var(--orange); }
\\.code-markup .name { color: var(--red); }
\\.code-markup .function { color: var(--blue); }
\\.code-markup .label { color: var(--yellow); }
\\ </style>
\\ </head>
\\ <body>
;
const html_epilogue =
\\
\\ </body>
\\</html>
;
const tokenator = cmd: {
var cmd = noclip.CommandBuilder(*TokCtx){
.description =
\\Tokenize one or more zig files into HTML.
\\
\\Each file provided on the command line will be tokenized and the output will
\\be written to [filename].html. For example, 'tokenator foo.zig bar.zig' will
\\write foo.zig.html and bar.zig.html. Files are written directly, and if an
\\error occurs while processing a file, partial output will occur. When
\\processing multiple files, a failure will exit without processing any
\\successive files. Inputs should be less than 1MB in size.
\\
\\If the --stdout flag is provided, output will be written to the standard
\\output instead of to named files. Each file written to stdout will be
\\followed by a NUL character which acts as a separator for piping purposes.
,
};
cmd.simple_flag(.{
.name = "write_stdout",
.truthy = .{ .long_tag = "--stdout" },
.default = false,
.description = "write output to stdout instead of to files",
});
cmd.simple_flag(.{
.name = "full",
.truthy = .{ .short_tag = "-f", .long_tag = "--full" },
.default = false,
.description = "write full HTML files rather than just the pre fragment",
});
cmd.add_argument(.{ .OutputType = []const u8, .multi = true }, .{ .name = "files" });
break :cmd cmd;
};
const TokCtx = struct {
allocator: std.mem.Allocator,
};
fn tokenize_files_cli(context: *TokCtx, parameters: tokenator.Output()) !void {
const stdout = std.io.getStdOut().writer();
for (parameters.files.items) |file_name| {
if (parameters.write_stdout) {
try tokenize_file(file_name, context.allocator, stdout, parameters.full);
try stdout.writeByte(0);
} else {
const outname = try std.mem.join(context.allocator, ".", &[_][]const u8{ file_name, "html" });
defer context.allocator.free(outname);
const output = try fs.cwd().createFile(outname, .{});
defer output.close();
print("writing: {s}", .{outname});
errdefer print(" failed!\n", .{});
try tokenize_file(file_name, context.allocator, output.writer(), parameters.full);
print(" done\n", .{});
}
}
}
pub fn cli() !u8 {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var ctx = TokCtx{ .allocator = allocator };
var arena = std.heap.ArenaAllocator.init(gpa.allocator());
defer arena.deinit();
var cli_parser = tokenator.create_parser(tokenize_files_cli, arena.allocator());
try cli_parser.execute(&ctx);
return 0;
}

View File

@ -1,611 +0,0 @@
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 {
section,
description,
example,
include,
};
const ExampleFormat = enum {
zig,
console,
pub fn default_format() ExampleFormat {
return .zig;
}
};
const DescriptionFormat = enum {
markdown,
};
const ParseError = error{
LeadingGarbage,
ExpectedDirectivePrefix,
ExpectedDirectiveSuffix,
ExpectedNonemptySuffix,
ExpectedDirectiveTerminator,
UnknownDirective,
UnknownSuffix,
UnexpectedDirectiveMismatch,
MissingRequiredTrailer,
UnsupportedFormat,
UnexpectedDirective,
};
const RawDirectiveLine = struct {
directive: Directive,
suffix: ?[]const u8, // the part after the dot. Null if there is no dot
trailer: ?[]const u8, // the part after the colon. null if empty or whitespace-only
// line has had its trailing newline stripped
fn from_line(line: []const u8) ParseError!RawDirectiveLine {
if (line.len < 1 or line[0] != '@') return error.ExpectedDirectivePrefix;
var result: RawDirectiveLine = .{
.directive = undefined,
.suffix = null,
.trailer = null,
};
var offset: usize = blk: {
inline for (comptime std.meta.fields(Directive)) |field| {
const len = field.name.len + 1;
if (line.len > len and
(line[len] == ':' or line[len] == '.') and
std.mem.eql(u8, line[1..len], field.name))
{
result.directive = @field(Directive, field.name);
break :blk len;
}
}
return error.UnknownDirective;
};
if (line[offset] == '.') blk: {
const suffix_start = offset + 1;
while (offset < line.len) : (offset += 1) {
if (line[offset] == ':') {
if (offset <= suffix_start) return error.ExpectedNonemptySuffix;
result.suffix = line[suffix_start..offset];
break :blk;
}
}
return error.ExpectedDirectiveTerminator;
}
if (line[offset] != ':') return error.ExpectedDirectiveTerminator;
offset += 1;
while (offset < line.len) : (offset += 1) {
if (!std.ascii.isWhitespace(line[offset])) {
// TODO: also trim trailing whitespace
result.trailer = line[offset..line.len];
break;
}
}
return result;
}
};
fn expect_optional_string(candidate: ?[]const u8, expected: ?[]const u8) !void {
if (expected) |exstr| {
if (candidate) |canstr| {
try std.testing.expectEqualStrings(exstr, canstr);
} else {
std.debug.print("Expected \"{s}\", got null\n", .{exstr});
return error.TestExpectedEqual;
}
} else {
if (candidate) |canstr| {
std.debug.print("Expected null, got \"{s}\"\n", .{canstr});
return error.TestExpectedEqual;
}
}
}
fn expect_rdl(candidate: RawDirectiveLine, expected: RawDirectiveLine) !void {
try std.testing.expectEqual(expected.directive, candidate.directive);
try expect_optional_string(candidate.suffix, expected.suffix);
try expect_optional_string(candidate.trailer, expected.trailer);
}
test "RawDirectiveLine.from_line" {
try expect_rdl(
try RawDirectiveLine.from_line("@section:"),
.{ .directive = .section, .suffix = null, .trailer = null },
);
try expect_rdl(
try RawDirectiveLine.from_line("@example:"),
.{ .directive = .example, .suffix = null, .trailer = null },
);
try expect_rdl(
try RawDirectiveLine.from_line("@example.zig:"),
.{ .directive = .example, .suffix = "zig", .trailer = null },
);
try expect_rdl(
try RawDirectiveLine.from_line("@example.zig: ./example.file"),
.{ .directive = .example, .suffix = "zig", .trailer = "./example.file" },
);
try std.testing.expectError(
ParseError.UnknownDirective,
RawDirectiveLine.from_line("@unknown:"),
);
try std.testing.expectError(
ParseError.ExpectedDirectivePrefix,
RawDirectiveLine.from_line("hello"),
);
// TODO: this would be better if it produced error.ExpectedDirectiveTerminator
// instead, but it complicates the logic to do so.
try std.testing.expectError(
ParseError.UnknownDirective,
RawDirectiveLine.from_line("@section"),
);
try std.testing.expectError(
ParseError.ExpectedDirectiveTerminator,
RawDirectiveLine.from_line("@example.tag"),
);
try std.testing.expectError(
ParseError.ExpectedNonemptySuffix,
RawDirectiveLine.from_line("@example.:"),
);
}
const SectionDirectiveLine = struct {
name: []const u8,
fn from_raw(raw: RawDirectiveLine) ParseError!DirectiveLine {
if (raw.directive != .section) return error.UnexpectedDirectiveMismatch;
if (raw.suffix != null) return error.UnknownSuffix;
if (raw.trailer == null) return error.MissingRequiredTrailer;
return .{ .section = .{ .name = raw.trailer.? } };
}
};
test "SectionDirectiveLine.from_raw" {
const line = try SectionDirectiveLine.from_raw(.{ .directive = .section, .suffix = null, .trailer = "Section" });
try std.testing.expectEqualStrings("Section", line.section.name);
try std.testing.expectError(
ParseError.UnexpectedDirectiveMismatch,
SectionDirectiveLine.from_raw(.{ .directive = .example, .suffix = null, .trailer = "Section" }),
);
try std.testing.expectError(
ParseError.UnknownSuffix,
SectionDirectiveLine.from_raw(.{ .directive = .section, .suffix = "importante", .trailer = "Section" }),
);
try std.testing.expectError(
ParseError.MissingRequiredTrailer,
SectionDirectiveLine.from_raw(.{ .directive = .section, .suffix = null, .trailer = null }),
);
}
const DescriptionDirectiveLine = struct {
format: DescriptionFormat,
include: ?[]const u8,
fn from_raw(raw: RawDirectiveLine) ParseError!DirectiveLine {
if (raw.directive != .description) return error.UnexpectedDirectiveMismatch;
const format: DescriptionFormat = if (raw.suffix) |suffix|
std.meta.stringToEnum(DescriptionFormat, suffix) orelse return error.UnsupportedFormat
else
.markdown;
return .{ .description = .{ .format = format, .include = raw.trailer } };
}
};
const ExampleDirectiveLine = struct {
format: ExampleFormat,
include: ?[]const u8,
fn from_raw(raw: RawDirectiveLine) ParseError!DirectiveLine {
if (raw.directive != .example) return error.UnexpectedDirectiveMismatch;
const format: ExampleFormat = if (raw.suffix) |suffix|
std.meta.stringToEnum(ExampleFormat, suffix) orelse return error.UnsupportedFormat
else
.zig;
return .{ .example = .{ .format = format, .include = raw.trailer } };
}
};
const IncludeDirectiveLine = struct {
path: []const u8,
fn from_raw(raw: RawDirectiveLine) ParseError!DirectiveLine {
if (raw.directive != .include) return error.UnexpectedDirectiveMismatch;
if (raw.suffix != null) return error.UnknownSuffix;
if (raw.trailer == null) return error.MissingRequiredTrailer;
return .{ .include = .{ .path = raw.trailer.? } };
}
};
const DirectiveLine = union(Directive) {
section: SectionDirectiveLine,
description: DescriptionDirectiveLine,
example: ExampleDirectiveLine,
include: IncludeDirectiveLine,
fn from_line(line: []const u8) ParseError!DirectiveLine {
const raw = try RawDirectiveLine.from_line(line);
return try switch (raw.directive) {
.section => SectionDirectiveLine.from_raw(raw),
.description => DescriptionDirectiveLine.from_raw(raw),
.example => ExampleDirectiveLine.from_raw(raw),
.include => IncludeDirectiveLine.from_raw(raw),
};
}
};
fn slugify(allocator: std.mem.Allocator, source: []const u8) ![]const u8 {
const buf = try allocator.alloc(u8, source.len);
for (source, 0..) |char, idx| {
if (std.ascii.isAlphanumeric(char)) {
buf[idx] = std.ascii.toLower(char);
} else {
buf[idx] = '-';
}
}
return buf;
}
const Section = struct {
name: []const u8,
id: []const u8,
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) {
description: Description,
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 {
format: DescriptionFormat,
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;
fn read_directive(line: []const u8) ParseError!DirectiveLine {
if (line[0] != '@') return error.ExpectedDirective;
inline for (comptime std.meta.fields(Directive)) |field| {
const len = field.name.len + 1;
if (line.len > len and
(line[len] == ':' or line[len] == '.') and
std.mem.eql(u8, line[1..len], field.name))
{
return @field(Directive, field.name);
}
}
}
const ParserState = enum {
section_or_include,
any_directive,
};
fn slice_to_next_directive(lines: *std.mem.TokenIterator(u8)) ![]const u8 {
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()) {
if (DirectiveLine.from_line(line)) |_| {
return lines.buffer[start..lines.index];
} else |err| switch (err) {
error.ExpectedDirectivePrefix => {},
else => return err,
}
}
// we hit EOF
return lines.buffer[start..lines.buffer.len];
}
pub fn parse(allocator: std.mem.Allocator, input: []const u8, directory: []const u8) !Document {
var lines = std.mem.tokenize(u8, input, "\n");
var doc_builder = std.ArrayList(Section).init(allocator);
var section_builder = std.ArrayList(Segment).init(allocator);
var state: ParserState = .section_or_include;
var current_section: Section = .{
.name = undefined,
.id = undefined,
.segments = undefined,
};
while (lines.next()) |line| {
const dline = try DirectiveLine.from_line(line);
switch (state) {
.section_or_include => switch (dline) {
.section => |sline| {
current_section.name = sline.name;
current_section.id = try slugify(allocator, sline.name);
state = .any_directive;
},
.include => |iline| {
// read the file at iline.path
const doc = try parse(allocator, iline.path, try std.fs.path.join(allocator, &[_][]const u8{directory}));
defer allocator.free(doc);
try doc_builder.appendSlice(doc);
},
else => return error.UnexpectedDirective,
},
.any_directive => switch (dline) {
.section => |sline| {
current_section.segments = try section_builder.toOwnedSlice();
try doc_builder.append(current_section);
current_section.name = sline.name;
current_section.id = try slugify(allocator, sline.name);
},
.include => |iline| {
const doc = try parse(allocator, iline.path, try std.fs.path.join(allocator, &[_][]const u8{directory}));
defer allocator.free(doc);
try doc_builder.appendSlice(doc);
state = .section_or_include;
},
.example => |exline| {
try section_builder.append(.{ .example = .{
.format = exline.format,
.body = if (exline.include) |incl|
.{ .include = try std.fs.path.join(allocator, &[_][]const u8{ directory, incl }) }
else
.{ .in_line = try slice_to_next_directive(&lines) },
} });
},
.description => |desline| {
try section_builder.append(.{ .description = .{
.format = desline.format,
.body = if (desline.include) |incl|
.{ .include = try std.fs.path.join(allocator, &[_][]const u8{ directory, incl }) }
else
.{ .in_line = try slice_to_next_directive(&lines) },
} });
},
},
}
}
current_section.segments = try section_builder.toOwnedSlice();
try doc_builder.append(current_section);
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" {
const doc = try parse(
std.testing.allocator,
\\@section: first section
\\@example:
\\
\\what
\\have we got
\\here
\\@description: include
\\@section: second
\\@description:
\\words
\\@description:
,
);
defer free_doc(doc, std.testing.allocator);
for (doc) |section| {
std.debug.print("section: {s}\n", .{section.name});
for (section.segments) |seg| {
switch (seg) {
.description => |desc| switch (desc.body) {
.include => |inc| std.debug.print(" seg: description, body: include {s}\n", .{inc}),
.in_line => |inl| std.debug.print(" seg: description, body: inline {s}\n", .{inl}),
},
.example => |desc| switch (desc.body) {
.include => |inc| std.debug.print(" seg: example, body: include {s}\n", .{inc}),
.in_line => |inl| std.debug.print(" seg: example, body: inline {s}\n", .{inl}),
},
}
}
}
}
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();
}

View File

@ -1,531 +0,0 @@
const std = @import("std");
const StructField = std.builtin.Type.StructField;
const help = @import("./help.zig");
const ncmeta = @import("./meta.zig");
const parameters = @import("./parameters.zig");
const parser = @import("./parser.zig");
const ValueCount = parameters.ValueCount;
const ParameterGenerics = parameters.ParameterGenerics;
const OptionConfig = parameters.OptionConfig;
const FlagConfig = parameters.FlagConfig;
const ShortLongPair = parameters.ShortLongPair;
const FlagBias = parameters.FlagBias;
const makeOption = parameters.makeOption;
const makeArgument = parameters.makeArgument;
const Parser = parser.Parser;
const ParserInterface = parser.ParserInterface;
fn BuilderGenerics(comptime UserContext: type) type {
return struct {
OutputType: type = void,
value_count: ValueCount = .{ .fixed = 1 },
multi: bool = false,
pub fn argGen(comptime self: @This()) ParameterGenerics {
if (self.value_count == .flag) @compileError("argument may not be a flag");
if (self.value_count == .count) @compileError("argument may not be a count");
return ParameterGenerics{
.UserContext = UserContext,
.OutputType = self.OutputType,
.param_type = .Ordinal,
.value_count = ParameterGenerics.fixedValueCount(self.OutputType, self.value_count),
.multi = self.multi,
};
}
pub fn optGen(comptime self: @This()) ParameterGenerics {
if (self.value_count == .flag) @compileError("option may not be a flag");
if (self.value_count == .count) @compileError("option may not be a count");
return ParameterGenerics{
.UserContext = UserContext,
.OutputType = self.OutputType,
.param_type = .Nominal,
.value_count = ParameterGenerics.fixedValueCount(self.OutputType, self.value_count),
.multi = self.multi,
};
}
pub fn countGen(comptime _: @This()) ParameterGenerics {
return ParameterGenerics{
.UserContext = UserContext,
.OutputType = usize,
.param_type = .Nominal,
.value_count = .count,
.multi = true,
};
}
pub fn flagGen(comptime self: @This()) ParameterGenerics {
return ParameterGenerics{
.UserContext = UserContext,
.OutputType = bool,
.param_type = .Nominal,
.value_count = .flag,
.multi = self.multi,
};
}
};
}
pub const GroupOptions = struct {
help_flag: ShortLongPair = .{ .short_tag = "-h", .long_tag = "--help" },
description: []const u8,
};
pub fn commandGroup(allocator: std.mem.Allocator, comptime options: GroupOptions) !ParserInterface {
const cmd = comptime CommandBuilder(void){
.help_flag = options.help_flag,
.description = options.description,
.subcommand_required = true,
};
return try cmd.createInterface(allocator, cmd.noopCallback());
}
fn InterfaceCreator(comptime Command: type) type {
return if (Command.ICC.InputType()) |Type|
struct {
pub fn createInterface(
comptime self: Command,
allocator: std.mem.Allocator,
comptime callback: self.CallbackSignature(),
context: Type,
) !ParserInterface {
return try self._createInterfaceImpl(allocator, callback, context);
}
}
else
struct {
pub fn createInterface(
comptime self: Command,
allocator: std.mem.Allocator,
comptime callback: self.CallbackSignature(),
) !ParserInterface {
return try self._createInterfaceImpl(allocator, callback, void{});
}
};
}
pub const InterfaceContextCategory = union(enum) {
empty,
pointer: type,
value: type,
pub fn fromType(comptime ContextType: type) InterfaceContextCategory {
return switch (@typeInfo(ContextType)) {
.void => .empty,
.pointer => |info| if (info.size == .slice) .{ .value = ContextType } else .{ .pointer = ContextType },
// technically, i0, u0, and struct{} should be treated as empty, probably
else => .{ .value = ContextType },
};
}
pub fn InputType(comptime self: InterfaceContextCategory) ?type {
return switch (self) {
.empty => null,
.pointer => |Type| Type,
.value => |Type| *const Type,
};
}
pub fn OutputType(comptime self: InterfaceContextCategory) type {
return switch (self) {
.empty => void,
.pointer => |Type| Type,
.value => |Type| Type,
};
}
};
pub fn CommandBuilder(comptime UserContext: type) type {
return struct {
param_spec: ncmeta.TupleBuilder = .{},
// this is a strange hack, but it's easily the path of least resistance
help_flag: ShortLongPair = .{ .short_tag = "-h", .long_tag = "--help" },
/// if any subcommands are provided, one of them must be specified, or the command has failed.
subcommand_required: bool = true,
description: []const u8,
pub const UserContextType = UserContext;
pub const ICC: InterfaceContextCategory = InterfaceContextCategory.fromType(UserContextType);
pub fn createParser(
comptime self: @This(),
comptime callback: self.CallbackSignature(),
allocator: std.mem.Allocator,
) !Parser(self, callback) {
// note: this is freed in Parser.deinit
var arena = try allocator.create(std.heap.ArenaAllocator);
arena.* = std.heap.ArenaAllocator.init(allocator);
const arena_alloc = arena.allocator();
return Parser(self, callback){
.arena = arena,
.allocator = arena_alloc,
.subcommands = parser.CommandMap.init(arena_alloc),
.help_builder = help.HelpBuilder(self).init(arena_alloc),
};
}
pub const ifc = InterfaceCreator(@This());
pub const createInterface = ifc.createInterface;
fn _createInterfaceImpl(
comptime self: @This(),
allocator: std.mem.Allocator,
comptime callback: self.CallbackSignature(),
context: (ICC.InputType() orelse void),
) !ParserInterface {
var arena = try allocator.create(std.heap.ArenaAllocator);
arena.* = std.heap.ArenaAllocator.init(allocator);
const arena_alloc = arena.allocator();
var this_parser = try arena_alloc.create(Parser(self, callback));
this_parser.* = .{
.arena = arena,
.allocator = arena_alloc,
.subcommands = parser.CommandMap.init(arena_alloc),
.help_builder = help.HelpBuilder(self).init(arena_alloc),
};
if (comptime ICC == .empty) {
return this_parser.interface();
} else {
return this_parser.interface(context);
}
}
pub fn setHelpFlag(
comptime self: *@This(),
comptime tags: ShortLongPair,
) void {
self.help_flag = tags;
}
const string_generics = BuilderGenerics(UserContext){ .OutputType = [:0]const u8 };
pub fn stringOption(
comptime self: *@This(),
comptime cfg: OptionConfig(string_generics.optGen()),
) void {
const config = if (cfg.nice_type_name == null)
ncmeta.copyStruct(@TypeOf(cfg), cfg, .{ .nice_type_name = "string" })
else
cfg;
self.addOption(string_generics, config);
}
pub fn stringArgument(
comptime self: *@This(),
comptime cfg: OptionConfig(string_generics.argGen()),
) void {
const config = if (cfg.nice_type_name == null)
ncmeta.copyStruct(@TypeOf(cfg), cfg, .{ .nice_type_name = "string" })
else
cfg;
self.addArgument(string_generics, config);
}
pub fn simpleFlag(
comptime self: *@This(),
comptime cfg: FlagConfig(string_generics.flagGen()),
) void {
self.addFlag(string_generics, cfg);
}
pub fn addArgument(
comptime self: *@This(),
comptime bgen: BuilderGenerics(UserContext),
comptime config: OptionConfig(bgen.argGen()),
) void {
self.param_spec.add(makeArgument(bgen.argGen(), config));
}
pub fn addOption(
comptime self: *@This(),
comptime bgen: BuilderGenerics(UserContext),
comptime config: OptionConfig(bgen.optGen()),
) void {
if (comptime bgen.value_count == .fixed and bgen.value_count.fixed == 0) {
@compileError(
"please use add_flag rather than add_option to " ++
"create a 0-argument option",
);
}
self.param_spec.add(makeOption(bgen.optGen(), config));
}
pub fn addFlag(
comptime self: *@This(),
comptime bgen: BuilderGenerics(UserContext),
comptime config: FlagConfig(bgen.flagGen()),
) void {
comptime {
if (config.truthy == null and config.falsy == null and config.env_var == null) {
@compileError(
"flag " ++
config.name ++
" must have at least one of truthy flags, falsy flags, or env_var flags",
);
}
const generics = bgen.flagGen();
var args = OptionConfig(generics){
.name = config.name,
//
.short_tag = null,
.long_tag = null,
.env_var = null,
//
.description = config.description,
.default = config.default,
.converter = config.converter,
//
.eager = config.eager,
.required = config.required,
.global = config.global,
//
.secret = config.secret,
.nice_type_name = "flag",
};
if (config.truthy) |truthy_pair| {
if (truthy_pair.short_tag == null and truthy_pair.long_tag == null) {
@compileError(
"flag " ++
config.name ++
" truthy pair must have at least short or long tags set",
);
}
args.short_tag = truthy_pair.short_tag;
args.long_tag = truthy_pair.long_tag;
args.flag_bias = .truthy;
self.param_spec.add(makeOption(generics, args));
}
if (config.falsy) |falsy_pair| {
if (falsy_pair.short_tag == null and falsy_pair.long_tag == null) {
@compileError(
"flag " ++
config.name ++
" falsy pair must have at least short or long tags set",
);
}
args.short_tag = falsy_pair.short_tag;
args.long_tag = falsy_pair.long_tag;
args.flag_bias = .falsy;
self.param_spec.add(makeOption(generics, args));
}
if (config.env_var) |env_var| {
// @compileLog(env_var);
args.short_tag = null;
args.long_tag = null;
args.env_var = env_var;
args.flag_bias = .unbiased;
self.param_spec.add(makeOption(generics, args));
}
}
}
pub fn generate(comptime self: @This()) self.param_spec.TupleType() {
return self.param_spec.realTuple();
}
pub fn noopCallback(comptime self: @This()) self.CallbackSignature() {
return struct {
fn callback(_: UserContextType, _: self.Output()) !void {}
}.callback;
}
pub fn CallbackSignature(comptime self: @This()) type {
return *const fn (UserContextType, self.Output()) anyerror!void;
}
pub fn Output(comptime self: @This()) type {
comptime {
const spec = self.generate();
var fields: []const StructField = &[_]StructField{};
var flag_skip = 0;
var tag_fields: []const StructField = &[_]StructField{};
var env_var_fields: []const StructField = &[_]StructField{};
paramloop: for (spec, 0..) |param, idx| {
const PType = @TypeOf(param);
// these three blocks are to check for redundantly defined tags and
// environment variables. This only works within a command. It
// doesn't support compile time checks for conflict into
// subcommands because those are attached at runtime. also, only
// global tags and env_vars would conflict, which is less common.
if (param.short_tag) |short|
tag_fields = tag_fields ++ &[_]StructField{.{
// this goofy construct coerces the comptime []const u8 to
// [:0]const u8.
.name = short ++ "",
.type = void,
.default_value_ptr = null,
.is_comptime = false,
.alignment = 0,
}};
if (param.long_tag) |long|
tag_fields = tag_fields ++ &[_]StructField{.{
.name = long ++ "",
.type = void,
.default_value_ptr = null,
.is_comptime = false,
.alignment = 0,
}};
if (param.env_var) |env_var|
env_var_fields = env_var_fields ++ &[_]StructField{.{
.name = env_var ++ "",
.type = void,
.default_value_ptr = null,
.is_comptime = false,
.alignment = 0,
}};
if (!PType.has_output) continue :paramloop;
while (flag_skip > 0) {
flag_skip -= 1;
continue :paramloop;
}
if (PType.is_flag) {
var peek = idx + 1;
var bias_seen: [ncmeta.enumLength(FlagBias)]bool = [_]bool{false} ** ncmeta.enumLength(FlagBias);
bias_seen[@intFromEnum(param.flag_bias)] = true;
while (peek < spec.len) : (peek += 1) {
const peek_param = spec[peek];
if (@TypeOf(peek_param).is_flag and std.mem.eql(u8, param.name, peek_param.name)) {
if (bias_seen[@intFromEnum(peek_param.flag_bias)] == true) {
@compileError("redundant flag!!!! " ++ param.name);
} else {
bias_seen[@intFromEnum(peek_param.flag_bias)] = true;
}
flag_skip += 1;
} else {
break;
}
}
}
// the default field is already the optional type. Stripping
// the optional wrapper is an interesting idea for required
// fields. I do not foresee this greatly increasing complexity here.
const FieldType = if (param.required or param.default != null)
PType.G.ConvertedType()
else
?PType.G.ConvertedType();
const default = if (param.default) |def| &@as(FieldType, def) else @as(?*const anyopaque, null);
fields = fields ++ &[_]StructField{.{
.name = param.name ++ "",
.type = FieldType,
.default_value_ptr = @ptrCast(default),
.is_comptime = false,
.alignment = @alignOf(FieldType),
}};
}
_ = @Type(.{ .@"struct" = .{
.layout = .auto,
.fields = tag_fields,
.decls = &.{},
.is_tuple = false,
} });
_ = @Type(.{ .@"struct" = .{
.layout = .auto,
.fields = env_var_fields,
.decls = &.{},
.is_tuple = false,
} });
return @Type(.{ .@"struct" = .{
.layout = .auto,
.fields = fields,
.decls = &.{},
.is_tuple = false,
} });
}
}
pub fn Intermediate(comptime self: @This()) type {
comptime {
const spec = self.generate();
var fields: []const StructField = &[0]StructField{};
var flag_skip = 0;
paramloop: for (spec, 0..) |param, idx| {
while (flag_skip > 0) {
flag_skip -= 1;
continue :paramloop;
}
const PType = @TypeOf(param);
if (PType.is_flag) {
var peek = idx + 1;
var bias_seen: [ncmeta.enumLength(FlagBias)]bool = [_]bool{false} ** ncmeta.enumLength(FlagBias);
bias_seen[@intFromEnum(param.flag_bias)] = true;
while (peek < spec.len) : (peek += 1) {
const peek_param = spec[peek];
if (@TypeOf(peek_param).is_flag and std.mem.eql(u8, param.name, peek_param.name)) {
if (bias_seen[@intFromEnum(peek_param.flag_bias)] == true) {
@compileError("redundant flag!!!! " ++ param.name);
} else {
bias_seen[@intFromEnum(peek_param.flag_bias)] = true;
}
flag_skip += 1;
} else {
break;
}
}
}
const FieldType = if (PType.value_count == .count)
PType.G.IntermediateType()
else
?PType.G.IntermediateType();
fields = &(@as([fields.len]StructField, fields[0..fields.len].*) ++ [1]StructField{.{
.name = param.name ++ "",
.type = FieldType,
.default_value_ptr = @ptrCast(&@as(
FieldType,
if (PType.value_count == .count) 0 else null,
)),
.is_comptime = false,
.alignment = @alignOf(?[]const u8),
}});
}
return @Type(.{ .@"struct" = .{
.layout = .auto,
.fields = fields,
.decls = &.{},
.is_tuple = false,
} });
}
}
};
}

View File

@ -1,147 +0,0 @@
const std = @import("std");
const ConversionError = @import("./errors.zig").ConversionError;
const ncmeta = @import("./meta.zig");
const parameters = @import("./parameters.zig");
const ValueCount = parameters.ValueCount;
const ParameterGenerics = parameters.ParameterGenerics;
const ErrorWriter = std.ArrayList(u8).Writer;
pub fn ConverterSignature(comptime gen: ParameterGenerics) type {
return *const fn (
context: gen.UserContext,
input: gen.IntermediateType(),
failure: ErrorWriter,
) ConversionError!gen.ConvertedType();
}
pub fn DefaultConverter(comptime gen: ParameterGenerics) ?ConverterSignature(gen) {
return if (comptime gen.multi)
MultiConverter(gen)
else switch (@typeInfo(gen.OutputType)) {
.bool => FlagConverter(gen),
.int => IntConverter(gen),
.pointer => |info| if (info.size == .slice and info.child == u8)
StringConverter(gen)
else
null,
.@"enum" => |info| if (info.is_exhaustive) ChoiceConverter(gen) else null,
// TODO: how to handle structs with field defaults? maybe this should only work
// for tuples, which I don't think can have defaults.
.@"struct" => |info| if (gen.value_count == .fixed and gen.value_count.fixed == info.fields.len)
StructConverter(gen)
else
null,
else => null,
};
}
fn MultiConverter(comptime gen: ParameterGenerics) ?ConverterSignature(gen) {
const converter = DefaultConverter(
ncmeta.copyStruct(ParameterGenerics, gen, .{ .multi = false }),
) orelse
@compileError("no default converter");
const Intermediate = gen.IntermediateType();
return struct {
pub fn handler(context: gen.UserContext, input: Intermediate, failure: ErrorWriter) ConversionError!std.ArrayList(gen.OutputType) {
var output = std.ArrayList(gen.OutputType).initCapacity(input.allocator, input.items.len) catch
return ConversionError.ConversionFailed;
for (input.items) |item| {
output.appendAssumeCapacity(try converter(context, item, failure));
}
return output;
}
}.handler;
}
fn FlagConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
return struct {
pub fn handler(_: gen.UserContext, input: [:0]const u8, _: ErrorWriter) ConversionError!bool {
// treat an empty string as falsy
if (input.len == 0) return false;
if (input.len <= 5) {
var lowerBuf: [5]u8 = undefined;
const comp = std.ascii.lowerString(&lowerBuf, input);
inline for ([_][]const u8{ "false", "no", "0" }) |candidate| {
if (std.mem.eql(u8, comp, candidate)) return false;
}
}
return true;
}
}.handler;
}
fn StringConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
return struct {
pub fn handler(_: gen.UserContext, input: [:0]const u8, _: ErrorWriter) ConversionError![:0]const u8 {
return input;
}
}.handler;
}
fn IntConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
const IntType = gen.OutputType;
return struct {
pub fn handler(_: gen.UserContext, input: [:0]const u8, failure: ErrorWriter) ConversionError!IntType {
return std.fmt.parseInt(IntType, input, 0) catch {
try failure.print("cannot interpret \"{s}\" as an integer", .{input});
return ConversionError.ConversionFailed;
};
}
}.handler;
}
fn StructConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
const StructType = gen.OutputType;
const type_info = @typeInfo(StructType).@"struct";
const Intermediate = gen.IntermediateType();
return struct {
pub fn handler(context: gen.UserContext, input: Intermediate, failure: ErrorWriter) ConversionError!StructType {
if (input.items.len != type_info.fields.len) {
try failure.print(
"Wrong number of fields provided. Got {d}, needed {d}",
.{ input.items.len, type_info.fields.len },
);
return ConversionError.ConversionFailed;
}
var result: StructType = undefined;
inline for (comptime type_info.fields, 0..) |field, idx| {
const Converter = comptime DefaultConverter(
ncmeta.copyStruct(ParameterGenerics, gen, .{
.OutputType = field.type,
.value_count = @as(parameters.ValueCount, .{ .fixed = 1 }),
}),
) orelse
@compileError("cannot get converter for field" ++ field.name);
@field(result, field.name) = try Converter(context, input.items[idx], failure);
}
return result;
}
}.handler;
}
fn ChoiceConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
const EnumType = gen.OutputType;
return struct {
pub fn handler(_: gen.UserContext, input: [:0]const u8, failure: ErrorWriter) ConversionError!EnumType {
return std.meta.stringToEnum(gen.ConvertedType(), input) orelse {
try failure.print("\"{s}\" is not a valid choice", .{input});
return ConversionError.ConversionFailed;
};
}
}.handler;
}

View File

@ -1,18 +0,0 @@
pub const ConversionError = error{
OutOfMemory,
ConversionFailed,
};
pub const ParseError = error{
UnexpectedFailure,
EmptyArgs,
MissingValue,
ExtraValue,
FusedShortTagValueMissing,
UnknownLongTagParameter,
UnknownShortTagParameter,
RequiredParameterMissing,
OutOfMemory,
};
pub const NoclipError = ParseError || ConversionError;

View File

@ -1,525 +0,0 @@
const std = @import("std");
const NoclipError = @import("./errors.zig").NoclipError;
const ncmeta = @import("./meta.zig");
const FixedCount = @import("./parameters.zig").FixedCount;
const parser = @import("./parser.zig");
const AlignablePair = struct {
left: []const u8,
right: []const u8,
};
const OptionDescription = struct {
pairs: []AlignablePair,
just: usize,
};
pub fn StructuredPrinter(comptime Writer: type) type {
return struct {
wrap_width: usize = 100,
writer: Writer,
pub fn printPair(self: *@This(), pair: AlignablePair, leading_indent: u8, tabstop: usize) !void {
try self.writer.writeByteNTimes(' ', leading_indent);
const left = std.mem.trim(u8, pair.left, " \n");
try self.writer.writeAll(left);
const offset: usize = leading_indent + left.len;
// TODO: lol return a real error
if (offset > tabstop) return NoclipError.UnexpectedFailure;
try self.writer.writeByteNTimes(' ', tabstop - offset);
try self.printRewrap(std.mem.trim(u8, pair.right, " \n"), tabstop);
try self.writer.writeByte('\n');
}
pub fn printPairBrief(self: *@This(), pair: AlignablePair, leading_indent: u8, tabstop: usize) !void {
const brief = ncmeta.partition(u8, pair.right, &[_][]const u8{"\n\n"})[0];
const simulacrum: AlignablePair = .{
.left = pair.left,
.right = brief,
};
try self.printPair(simulacrum, leading_indent, tabstop);
}
pub fn printWrapped(self: *@This(), text: []const u8, leading_indent: usize) !void {
try self.writer.writeByteNTimes(' ', leading_indent);
try self.printRewrap(std.mem.trim(u8, text, "\n"), leading_indent);
}
fn printRewrap(self: *@This(), text: []const u8, indent: usize) !void {
// TODO: lol return a real error
if (indent >= self.wrap_width) return NoclipError.UnexpectedFailure;
if (text.len == 0) return;
// this assumes output stream has already had the first line properly
// indented.
var splitter = std.mem.splitScalar(u8, text, '\n');
var location: usize = indent;
while (splitter.next()) |line| {
if (line.len == 0) {
// we have a trailing line that needs to be cleaned up
if (location > indent)
_ = try self.clearLine(indent);
location = try self.clearLine(indent);
continue;
}
if (line[0] == '>') maybe: {
if (line.len > 1) {
if (line[1] == ' ') {
try self.writer.writeAll(line[2..]);
} else break :maybe;
}
location = try self.clearLine(indent);
continue;
}
var choppee = line;
var need_forced_break = false;
choppa: while (choppee.len > 0) {
const breakoff = self.wrap_width - location;
if (breakoff >= choppee.len) {
if (location > indent)
try self.writer.writeByte(' ');
try self.writer.writeAll(choppee);
location += choppee.len;
break;
}
var split = breakoff;
while (choppee[split] != ' ') : (split -= 1) {
if (split == 0) {
// we have encountered a word that is too long to break,
// so force breaking it
if (need_forced_break) {
split = breakoff;
break;
}
if (location != indent)
location = try self.clearLine(indent);
need_forced_break = true;
continue :choppa;
}
}
if (location > indent)
try self.writer.writeByte(' ');
try self.writer.writeAll(choppee[0..split]);
location = try self.clearLine(indent);
choppee = choppee[split + 1 ..];
}
}
}
fn clearLine(self: *@This(), indent: usize) !usize {
try self.writer.writeByte('\n');
try self.writer.writeByteNTimes(' ', indent);
return indent;
}
};
}
pub fn HelpBuilder(comptime command: anytype) type {
const help_info = optInfo(command.generate());
return struct {
writebuffer: std.ArrayList(u8),
pub fn init(allocator: std.mem.Allocator) @This() {
return @This(){
.writebuffer = std.ArrayList(u8).init(allocator),
};
}
pub fn buildMessage(
self: *@This(),
name: []const u8,
subcommands: parser.CommandMap,
) ![]const u8 {
const writer = self.writebuffer.writer();
try writer.print(
"Usage: {s}{s}{s}{s}\n\n",
.{
name,
self.optionBrief(),
try self.argsBrief(),
self.subcommandsBrief(subcommands),
},
);
var printer = StructuredPrinter(@TypeOf(writer)){ .writer = writer };
try printer.printWrapped(command.description, 2);
try writer.writeAll("\n\n");
const arguments = try self.describeArguments();
const options = try self.describeOptions();
const env_vars = try self.describeEnv();
const subcs = try self.describeSubcommands(subcommands);
const max_just = @max(arguments.just, @max(options.just, @max(env_vars.just, subcs.just)));
if (arguments.pairs.len > 0) {
try writer.writeAll("Arguments:\n");
for (arguments.pairs) |pair|
try printer.printPair(pair, 2, max_just + 4);
try writer.writeAll("\n");
}
if (options.pairs.len > 0) {
try writer.writeAll("Options:\n");
for (options.pairs) |pair|
try printer.printPair(pair, 2, max_just + 4);
try writer.writeAll("\n");
}
if (env_vars.pairs.len > 0) {
try writer.writeAll("Environment variables:\n");
for (env_vars.pairs) |pair|
try printer.printPair(pair, 2, max_just + 4);
try writer.writeAll("\n");
}
if (subcs.pairs.len > 0) {
try writer.writeAll("Subcommands:\n");
for (subcs.pairs) |pair|
try printer.printPairBrief(pair, 2, max_just + 4);
try writer.writeAll("\n");
}
return self.writebuffer.toOwnedSlice();
}
fn optionBrief(_: @This()) []const u8 {
return if (comptime help_info.options.len > 0)
" [options...]"
else
"";
}
fn argsBrief(self: @This()) ![]const u8 {
var buf = std.ArrayList(u8).init(self.writebuffer.allocator);
defer buf.deinit();
const writer = buf.writer();
for (comptime help_info.arguments) |arg| {
try writer.writeAll(" ");
if (!arg.required) try writer.writeAll("[");
try writer.writeByte('<');
try writer.writeAll(arg.name);
if (arg.multi)
try writer.print(" [{s} ...]", .{arg.name});
try writer.writeByte('>');
if (!arg.required) try writer.writeAll("]");
}
return buf.toOwnedSlice();
}
fn subcommandsBrief(_: @This(), subcommands: parser.CommandMap) []const u8 {
return if (subcommands.count() > 0)
" <subcommand ...>"
else
"";
}
fn describeArguments(self: @This()) !OptionDescription {
var pairs = std.ArrayList(AlignablePair).init(self.writebuffer.allocator);
defer pairs.deinit();
var just: usize = 0;
inline for (comptime help_info.arguments) |arg| {
const pair: AlignablePair = .{
.left = arg.name,
.right = arg.description,
};
if (pair.left.len > just) just = pair.left.len;
try pairs.append(pair);
}
return .{
.pairs = try pairs.toOwnedSlice(),
.just = just,
};
}
fn describeOptions(self: @This()) !OptionDescription {
var pairs = std.ArrayList(AlignablePair).init(self.writebuffer.allocator);
defer pairs.deinit();
var just: usize = 0;
inline for (help_info.options) |opt| {
const pair = try self.describeOption(opt);
if (pair.left.len > just) just = pair.left.len;
try pairs.append(pair);
}
return .{
.pairs = try pairs.toOwnedSlice(),
.just = just,
};
}
fn describeOption(self: @This(), comptime opt: OptHelp) !AlignablePair {
var buffer = std.ArrayList(u8).init(self.writebuffer.allocator);
defer buffer.deinit();
const writer = buffer.writer();
if (comptime opt.short_truthy) |tag| {
if (buffer.items.len > 0) try writer.writeAll(", ");
try writer.writeAll(tag);
}
if (comptime opt.long_truthy) |tag| {
if (buffer.items.len > 0) try writer.writeAll(", ");
try writer.writeAll(tag);
}
var falsy_seen = false;
if (comptime opt.short_falsy) |tag| {
if (buffer.items.len > 0)
try writer.writeAll(" / ")
else
try writer.writeAll("/ ");
try writer.writeAll(tag);
falsy_seen = true;
}
if (comptime opt.long_falsy) |tag| {
if (falsy_seen)
try writer.writeAll(", ")
else if (buffer.items.len > 0)
try writer.writeAll(" / ");
try writer.writeAll(tag);
}
if (opt.value_count > 0) {
try writer.print(" <{s}>", .{opt.type_name});
}
const left = try buffer.toOwnedSlice();
if (comptime opt.required) {
try writer.writeAll("[required]");
}
if (comptime opt.description.len > 0) {
if (buffer.items.len > 0) try writer.writeAll(" ");
try writer.writeAll(opt.description);
}
if (comptime opt.env_var) |env| {
if (buffer.items.len > 0) try writer.writeAll(" ");
try writer.print("(env: {s})", .{env});
}
if (comptime opt.default) |def| {
if (buffer.items.len > 0) try writer.writeAll(" ");
try writer.print("(default: {s})", .{def});
}
const right = try buffer.toOwnedSlice();
return .{ .left = left, .right = right };
}
fn describeEnv(self: @This()) !OptionDescription {
var pairs = std.ArrayList(AlignablePair).init(self.writebuffer.allocator);
defer pairs.deinit();
var just: usize = 0;
for (comptime help_info.env_vars) |env| {
if (env.description.len == 0) continue;
const pair: AlignablePair = .{
.left = env.env_var,
.right = env.description,
};
if (pair.left.len > just) just = pair.left.len;
try pairs.append(pair);
}
return .{
.pairs = try pairs.toOwnedSlice(),
.just = just,
};
}
fn describeSubcommands(self: @This(), subcommands: parser.CommandMap) !OptionDescription {
var pairs = std.ArrayList(AlignablePair).init(self.writebuffer.allocator);
defer pairs.deinit();
var just: usize = 0;
for (subcommands.keys()) |key| {
const pair: AlignablePair = .{
.left = key,
.right = subcommands.get(key).?.describe(),
};
if (pair.left.len > just) just = pair.left.len;
try pairs.append(pair);
}
return .{
.pairs = try pairs.toOwnedSlice(),
.just = just,
};
}
};
}
const CommandHelp = struct {
options: []const OptHelp,
arguments: []const ArgHelp,
env_vars: []const EnvHelp,
};
const OptHelp = struct {
short_truthy: ?[]const u8 = null,
long_truthy: ?[]const u8 = null,
short_falsy: ?[]const u8 = null,
long_falsy: ?[]const u8 = null,
env_var: ?[]const u8 = null,
description: []const u8 = "",
type_name: []const u8 = "",
extra: []const u8 = "",
default: ?[]const u8 = null,
// this is the pivot
value_count: FixedCount = 0,
required: bool = false,
multi: bool = false,
};
const EnvHelp = struct {
env_var: []const u8 = "",
description: []const u8 = "",
default: ?[]const u8 = null,
};
const ArgHelp = struct {
name: []const u8 = "",
description: []const u8 = "",
type_name: []const u8 = "",
multi: bool = false,
required: bool = true,
};
pub fn optInfo(comptime command: anytype) CommandHelp {
// TODO: this could be runtime and it would be slightly simpler.
comptime {
var options: []const OptHelp = &[_]OptHelp{};
var env_vars: []const EnvHelp = &[_]EnvHelp{};
var arguments: []const ArgHelp = &[_]ArgHelp{};
var last_name: []const u8 = "";
var last_option: OptHelp = .{};
paramloop: for (command) |param| {
const PType = @TypeOf(param);
if (PType.param_type == .Ordinal) {
arguments = arguments ++ &[_]ArgHelp{.{
.name = param.name,
.description = param.description,
.type_name = param.nice_type_name,
.multi = PType.multi,
.required = param.required,
}};
continue :paramloop;
}
if (!std.mem.eql(u8, last_name, param.name)) {
if (last_name.len > 0) {
if (envOnly(last_option)) {
env_vars = env_vars ++ &[_]EnvHelp{.{
.env_var = last_option.env_var,
.description = last_option.description,
.default = last_option.default,
}};
} else {
options = options ++ &[_]OptHelp{last_option};
}
}
last_name = param.name;
last_option = .{};
}
if (PType.is_flag) {
switch (param.flag_bias) {
.truthy => {
last_option.short_truthy = param.short_tag;
last_option.long_truthy = param.long_tag;
},
.falsy => {
last_option.short_falsy = param.short_tag;
last_option.long_falsy = param.long_tag;
},
.unbiased => last_option.env_var = param.env_var,
}
} else {
last_option.short_truthy = param.short_tag;
last_option.long_truthy = param.long_tag;
last_option.env_var = param.env_var;
last_option.value_count = PType.value_count.fixed;
}
last_option.type_name = param.nice_type_name;
last_option.description = param.description;
last_option.required = param.required;
last_option.multi = PType.multi;
if (param.default) |def| {
var buf = ncmeta.ComptimeSliceBuffer{};
const writer = buf.writer();
// TODO: this is only acceptable for some types. It behaves poorly on
// enum-based choice types because it prints the whole type name rather
// than just the tag name. Roll our own eventually.
blk: {
switch (@typeInfo(@TypeOf(def))) {
.pointer => |info| if (info.size == .Slice and info.child == u8) {
writer.print("{s}", .{def}) catch @compileError("no");
break :blk;
},
else => {},
}
writer.print("{any}", .{def}) catch @compileError("whoah");
}
last_option.default = buf.buffer;
}
}
if (last_name.len > 0) {
if (envOnly(last_option)) {
env_vars = env_vars ++ &[_]EnvHelp{.{
.env_var = last_option.env_var.?,
.description = last_option.description,
.default = last_option.default,
}};
} else {
options = options ++ &[_]OptHelp{last_option};
}
}
return .{
.options = options,
.arguments = arguments,
.env_vars = env_vars,
};
}
}
inline fn envOnly(option: OptHelp) bool {
return option.short_truthy == null and
option.long_truthy == null and
option.short_falsy == null and
option.long_falsy == null;
}

41
source/mem.zig Normal file
View File

@ -0,0 +1,41 @@
pub const Partition = struct { lhs: []const u8, rhs: ?[]const u8 };
pub const Codepoint = u21;
pub fn partition(str: []const u8, char: u8) Partition {
return if (std.mem.indexOfScalar(u8, str, char)) |idx|
.{ .lhs = str[0..idx], .rhs = str[idx + 1 ..] }
else
.{ .lhs = str, .rhs = null };
}
pub fn SliceIter(comptime T: type) type {
return struct {
slice: []const T,
index: usize = 0,
pub fn pop(self: *@This()) ?T {
defer self.index +|= 1;
return self.peek();
}
pub fn peek(self: *@This()) ?T {
if (self.index >= self.slice.len) return null;
return self.slice[self.index];
}
};
}
pub fn encodeShort(comptime short: Codepoint) []const u8 {
comptime {
const encoded = enc: {
const len = std.unicode.utf8CodepointSequenceLength(short) catch unreachable;
var buf: [len]u8 = undefined;
_ = std.unicode.utf8Encode(short, &buf) catch @compileError("invalid unicode character");
break :enc buf;
};
return encoded[0..];
}
}
const std = @import("std");

View File

@ -1,335 +0,0 @@
const std = @import("std");
const StructField = std.builtin.Type.StructField;
/// Given a type and a struct literal of defaults to add, this function creates
/// a simulacrum type with additional defaults set on its fields.
///
/// This function cannot remove default values from fields, but it can add some
/// to fields that don't have them, and it can overwrite existing defaults
pub fn UpdateDefaults(comptime input: type, comptime defaults: anytype) type {
comptime {
const inputInfo = @typeInfo(input);
const fieldcount = switch (inputInfo) {
.@"struct" => |spec| blk: {
if (spec.decls.len > 0) {
@compileError("UpdateDefaults only works on structs " ++
"without decls due to limitations in @Type.");
}
break :blk spec.fields.len;
},
else => @compileError("can only add default value to struct type"),
};
var fields: [fieldcount]StructField = undefined;
for (inputInfo.@"struct".fields, 0..) |field, idx| {
fields[idx] = .{
.name = field.name,
.field_type = field.field_type,
// the cast ostensibly does type checking for us. It also makes
// setting null defaults work, and it converts comptime_int to
// the appropriate type, which is nice for ergonomics. Not sure
// if it introduces weird edge cases. Probably it's fine?
.default_value_ptr = if (@hasField(@TypeOf(defaults), field.name))
@ptrCast(&@as(field.field_type, @field(defaults, field.name)))
else
field.default_value_ptr,
.is_comptime = field.is_comptime,
.alignment = field.alignment,
};
}
return @Type(.{ .@"struct" = .{
.layout = inputInfo.@"struct".layout,
.backing_integer = inputInfo.@"struct".backing_integer,
.fields = &fields,
.decls = inputInfo.@"struct".decls,
.is_tuple = inputInfo.@"struct".is_tuple,
} });
}
}
pub fn enumLength(comptime T: type) comptime_int {
return @typeInfo(T).@"enum".fields.len;
}
pub fn partition(comptime T: type, input: []const T, wedge: []const []const T) [3][]const T {
var idx: usize = 0;
while (idx < input.len) : (idx += 1) {
for (wedge) |splitter| {
if (input.len - idx < splitter.len) continue;
if (std.mem.eql(T, input[idx .. idx + splitter.len], splitter)) {
return [3][]const T{
input[0..idx],
input[idx..(idx + splitter.len)],
input[(idx + splitter.len)..],
};
}
}
}
return [3][]const T{
input[0..],
input[input.len..],
input[input.len..],
};
}
pub fn ComptimeWriter(
comptime Context: type,
comptime writeFn: fn (comptime context: Context, comptime bytes: []const u8) error{}!usize,
) type {
return struct {
context: Context,
const Self = @This();
pub const Error = error{};
pub fn write(comptime self: Self, comptime bytes: []const u8) Error!usize {
return writeFn(self.context, bytes);
}
pub fn writeAll(comptime self: Self, comptime bytes: []const u8) Error!void {
var index: usize = 0;
while (index != bytes.len) {
index += try self.write(bytes[index..]);
}
}
pub fn print(comptime self: Self, comptime format: []const u8, args: anytype) Error!void {
return std.fmt.format(self, format, args) catch @compileError("woah");
}
pub fn writeByte(comptime self: Self, byte: u8) Error!void {
const array = [1]u8{byte};
return self.writeAll(&array);
}
pub fn writeByteNTimes(comptime self: Self, byte: u8, n: usize) Error!void {
var bytes: [256]u8 = undefined;
std.mem.set(u8, bytes[0..], byte);
var remaining: usize = n;
while (remaining > 0) {
const to_write = std.math.min(remaining, bytes.len);
try self.writeAll(bytes[0..to_write]);
remaining -= to_write;
}
}
};
}
pub const ComptimeSliceBuffer = struct {
buffer: []const u8 = &[_]u8{},
const Writer = ComptimeWriter(*@This(), appendslice);
pub fn writer(comptime self: *@This()) Writer {
return .{ .context = self };
}
fn appendslice(comptime self: *@This(), comptime bytes: []const u8) error{}!usize {
self.buffer = self.buffer ++ bytes;
return bytes.len;
}
};
pub fn SliceIterator(comptime T: type) type {
// could be expanded to use std.meta.Elem, perhaps
const ResultType = std.meta.Child(T);
return struct {
index: usize,
data: T,
pub const InitError = error{};
pub fn wrap(value: T) @This() {
return @This(){ .index = 0, .data = value };
}
pub fn next(self: *@This()) ?ResultType {
if (self.index == self.data.len) return null;
defer self.index += 1;
return self.data[self.index];
}
pub fn peek(self: *@This()) ?ResultType {
if (self.index == self.data.len) return null;
return self.data[self.index];
}
pub fn rewind(self: *@This()) void {
if (self.index == 0) return;
self.index -= 1;
}
pub fn skip(self: *@This()) void {
if (self.index == self.data.len) return;
self.index += 1;
}
};
}
pub fn MutatingZSplitter(comptime T: type) type {
return struct {
buffer: [:0]T,
delimiter: T,
index: ?usize = 0,
const Self = @This();
/// Returns a slice of the next field, or null if splitting is complete.
pub fn next(self: *Self) ?[:0]T {
const start = self.index orelse return null;
const end = if (std.mem.indexOfScalarPos(T, self.buffer, start, self.delimiter)) |delim_idx| blk: {
self.buffer[delim_idx] = 0;
self.index = delim_idx + 1;
break :blk delim_idx;
} else blk: {
self.index = null;
break :blk self.buffer.len;
};
return self.buffer[start..end :0];
}
/// Returns a slice of the remaining bytes. Does not affect iterator state.
pub fn rest(self: Self) [:0]T {
const end = self.buffer.len;
const start = self.index orelse end;
return self.buffer[start..end :0];
}
};
}
pub fn copyStruct(comptime T: type, source: T, field_overrides: anytype) T {
var result: T = undefined;
comptime for (@typeInfo(@TypeOf(field_overrides)).@"struct".fields) |field| {
if (!@hasField(T, field.name)) @compileError("override contains bad field" ++ field);
};
inline for (comptime @typeInfo(T).@"struct".fields) |field| {
if (comptime @hasField(@TypeOf(field_overrides), field.name))
@field(result, field.name) = @field(field_overrides, field.name)
else
@field(result, field.name) = @field(source, field.name);
}
return result;
}
/// Stores type-erased pointers to items in comptime extensible data structures,
/// which allows e.g. assembling a tuple through multiple calls rather than all
/// at once.
pub const TupleBuilder = struct {
pointers: []const *const anyopaque = &[0]*const anyopaque{},
types: []const type = &[0]type{},
pub fn add(comptime self: *@This(), comptime item: anytype) void {
self.pointers = self.pointers ++ &[_]*const anyopaque{@as(*const anyopaque, &item)};
self.types = self.types ++ &[_]type{@TypeOf(item)};
}
pub fn retrieve(comptime self: @This(), comptime index: comptime_int) self.types[index] {
return @as(*const self.types[index], @ptrCast(@alignCast(self.pointers[index]))).*;
}
pub fn realTuple(comptime self: @This()) self.TupleType() {
comptime {
var result: self.TupleType() = undefined;
var idx = 0;
while (idx < self.types.len) : (idx += 1) {
result[idx] = self.retrieve(idx);
}
return result;
}
}
pub fn TupleType(comptime self: @This()) type {
comptime {
var fields: [self.types.len]StructField = undefined;
for (self.types, 0..) |Type, idx| {
fields[idx] = .{
.name = std.fmt.comptimePrint("{d}", .{idx}),
.type = Type,
.default_value_ptr = null,
// TODO: is this the right thing to do?
.is_comptime = false,
.alignment = if (@sizeOf(Type) > 0) @alignOf(Type) else 0,
};
}
return @Type(.{ .@"struct" = .{
.layout = .auto,
.fields = &fields,
.decls = &.{},
.is_tuple = true,
} });
}
}
};
test "add basic default" {
const Base = struct { a: u8 };
const Defaulted = UpdateDefaults(Base, .{ .a = 4 });
const value = Defaulted{};
try std.testing.expectEqual(@as(u8, 4), value.a);
}
test "overwrite basic default" {
const Base = struct { a: u8 = 0 };
const Defaulted = UpdateDefaults(Base, .{ .a = 1 });
const value = Defaulted{};
try std.testing.expectEqual(@as(u8, 1), value.a);
}
test "add string default" {
const Base = struct { a: []const u8 };
const Defaulted = UpdateDefaults(Base, .{ .a = "hello" });
const value = Defaulted{};
try std.testing.expectEqual(@as([]const u8, "hello"), value.a);
}
test "add null default" {
const Base = struct { a: ?u8 };
const Defaulted = UpdateDefaults(Base, .{ .a = null });
const value = Defaulted{};
try std.testing.expectEqual(@as(?u8, null), value.a);
}
test "add enum default" {
const Options = enum { good, bad };
const Base = struct { a: Options };
const Defaulted = UpdateDefaults(Base, .{ .a = .good });
const value = Defaulted{};
try std.testing.expectEqual(Options.good, value.a);
}
test "preserve existing default" {
const Base = struct { a: ?u8 = 2, b: u8 };
const Defaulted = UpdateDefaults(Base, .{ .b = 3 });
const value = Defaulted{};
try std.testing.expectEqual(@as(?u8, 2), value.a);
try std.testing.expectEqual(@as(?u8, 3), value.b);
}
test "add multiple defaults" {
const Base = struct { a: u8, b: i8, c: ?u8 };
const Defaulted = UpdateDefaults(Base, .{ .a = 3, .c = 2 });
const value = Defaulted{ .b = -1 };
try std.testing.expectEqual(@as(u8, 3), value.a);
try std.testing.expectEqual(@as(i8, -1), value.b);
try std.testing.expectEqual(@as(?u8, 2), value.c);
}

View File

@ -1,11 +1,464 @@
pub const command = @import("./command.zig"); pub const CommandOptions = struct {
pub const converters = @import("./converters.zig"); context_type: type = void,
pub const errors = @import("./errors.zig");
pub const help = @import("./help.zig");
pub const ncmeta = @import("./meta.zig");
pub const parameters = @import("./parameters.zig");
pub const parser = @import("./parser.zig");
pub const CommandBuilder = command.CommandBuilder; default_help_flags: bool = true,
pub const commandGroup = command.commandGroup; create_help_command: enum { always, never, if_subcommands } = .if_subcommands,
pub const ParserInterface = parser.ParserInterface; create_completion_helper: bool = true,
allow_colored_output: bool = true,
output_strategy: enum { type, iterator } = .type,
parse_error_behavior: enum { exit, propagate } = .propagate,
// pop the callback stack after parsing all arguments for the current subcommand
pipeline_subcommands: bool = false,
};
const __Canary = opaque {};
pub const ErrorReport = struct {
message: []const u8,
};
pub fn Status(comptime T: type) type {
return union(enum) {
success: T,
failure: ErrorReport,
pub fn succeed(arg: T) @This() {
return .{ .success = arg };
}
pub fn failFull(arg: ErrorReport) @This() {
return .{ .failure = arg };
}
pub fn fail(msg: []const u8) @This() {
return .{ .failure = .{ .message = msg } };
}
};
}
pub const String = struct {
bytes: []const u8,
};
pub const ParameterType = enum {
bool_group,
constant,
counter,
// counter
// fixed_value
// aggregate_flag
option,
// aggregate_option
argument,
// aggregate_argument
group,
};
pub const Scope = enum { local, global };
pub const MultiMode = enum {
first,
last,
accumulate,
count,
};
pub fn Accumulate(comptime T: type) type {
return struct {
const __noclip_canary__ = __Canary;
pub const Result: type = T;
pub const multi_mode: MultiMode = .accumulate;
};
}
pub fn Count(comptime T: type) type {
if (!@typeInfo(T) == .int) unreachable;
return struct {
const __noclip_canary__ = __Canary;
pub const Result: type = T;
pub const multi_mode: MultiMode = .count;
};
}
pub const OptScope = struct { opt: []const u8, scope: Scope, value: bool };
pub const BoolGroup = struct {
description: []const u8 = "",
truthy: Pair = .{},
falsy: Pair = .{},
env: ?[]const u8 = null,
/// If true, at least one of the variants of the flag must be provided by
/// the user on the command line, otherwise a parse error will be produced.
required: bool = false,
/// A default value that will be forwarded if the option is not provided on
/// the command line by the user. If a default is provided, then the
/// corresponding parsed value will not be optional. Note that flags are
/// tri-state values that may be `null`, `true`, or `false`. `null` will
/// never be forwarded if this is set to `true` or `false`, as `null` only
/// indicates that the flag was not specified on the command line.
default: ?Result = null,
// multi: Multi = .last,
scope: Scope = .local,
eager: bool = false,
hidden: bool = false,
pub const Result = bool;
pub const param_type: ParameterType = .bool_group;
pub const multi_mode: MultiMode = .last;
// accessors to easily read decls from an instance
pub fn Type(comptime _: *const BoolGroup) type {
return BoolGroup.Result;
}
pub fn mode(comptime _: *const BoolGroup) MultiMode {
return BoolGroup.multi_mode;
}
pub fn shorts(comptime self: BoolGroup) []const OptScope {
comptime {
var list: []const OptScope = &.{};
if (self.truthy.short) |short|
list = list ++ &[_]OptScope{.{ .opt = mem.encodeShort(short), .scope = self.scope, .value = false }};
if (self.falsy.short) |short|
list = list ++ &[_]OptScope{.{ .opt = mem.encodeShort(short), .scope = self.scope, .value = false }};
return list;
}
}
pub fn longs(comptime self: BoolGroup) []const OptScope {
comptime {
var list: []const OptScope = &.{};
if (self.truthy.long) |long|
list = list ++ &[_]OptScope{.{ .opt = long, .scope = self.scope, .value = false }};
if (self.falsy.long) |long|
list = list ++ &[_]OptScope{.{ .opt = long, .scope = self.scope, .value = false }};
return list;
}
}
pub const Pair = struct {
/// a single unicode codepoint that identifies this flag on the command
/// line, e.g. 'v'.
short: ?mem.Codepoint = null,
/// a string, beginning with the long flag sequence `--` that identifies
/// this flag on the command line, e.g. "--version". Multiple words
/// should be skewercase, i.e. "--multiple-words".
long: ?[]const u8 = null,
};
};
// figure this out: this is a zero-parameter flag that produces a non-boolean
// value, e.g. an int. for like -9 on gz. A flag is just a FixedValue with
pub fn Constant(comptime R: type) type {
return struct {
description: []const u8 = "",
short: ?mem.Codepoint = null,
long: ?[]const u8 = null,
env: ?[]const u8 = null,
/// Require that the user always provide a value for this option on the
/// command line.
required: bool = false,
/// The value associated with this flag
value: Result,
scope: Scope = .local,
eager: bool = false,
hidden: bool = false,
const Self = @This();
pub const Result = ScryResultType(R);
pub const param_type: ParameterType = .constant;
pub const multi_mode: MultiMode = scryMode(R);
// accessors to easily read decls from an instance
pub fn Type(comptime _: *const Self) type {
return Self.Result;
}
pub fn mode(comptime _: *const Self) MultiMode {
return Self.multi_mode;
}
pub fn shorts(comptime self: Self) []const OptScope {
comptime return if (self.short) |short|
&[_]OptScope{.{ .opt = mem.encodeShort(short), .scope = self.scope, .value = true }}
else
&.{};
}
pub fn longs(comptime self: Self) []const OptScope {
comptime return if (self.long) |long|
&[_]OptScope{.{ .opt = long, .scope = self.scope, .value = true }}
else
&.{};
}
};
}
pub fn Counter(comptime R: type) type {
return struct {
description: []const u8 = "",
short: ?mem.Codepoint = null,
long: ?[]const u8 = null,
env: ?[]const u8 = null,
/// Require that the user always provide a value for this option on the
/// command line.
required: bool = false,
/// The value associated with this flag
increment: Result = 1,
scope: Scope = .local,
eager: bool = false,
hidden: bool = false,
const Self = @This();
pub const Result = ScryResultType(R);
pub const param_type: ParameterType = .counter;
pub const multi_mode: MultiMode = scryMode(R);
// accessors to easily read decls from an instance
pub fn Type(comptime _: *const Self) type {
return Self.Result;
}
pub fn mode(comptime _: *const Self) MultiMode {
return Self.multi_mode;
}
pub fn shorts(comptime self: Self) []const OptScope {
comptime return if (self.short) |short|
&[_]OptScope{.{ .opt = mem.encodeShort(short), .scope = self.scope, .value = true }}
else
&.{};
}
pub fn longs(comptime self: Self) []const OptScope {
comptime return if (self.long) |long|
&[_]OptScope{.{ .opt = long, .scope = self.scope, .value = true }}
else
&.{};
}
};
}
pub fn Option(comptime R: type) type {
return struct {
description: []const u8 = "",
short: ?mem.Codepoint = null,
long: ?[]const u8 = null,
env: ?[]const u8 = null,
/// Require that the user always provide a value for this option on the
/// command line.
required: bool = false,
/// A default value that will be forwarded if the option is not provided
/// on the command line by the user. If a default is provided, then the
/// corresponding parsed value will not be optional.
default: DefaultType(R) = defaultTypeDefault(R),
/// note: .count is only valid for flags
/// note: .accumulate requires R to be a slice
// multi: Multi = .last,
scope: Scope = .local,
eager: bool = false,
hidden: bool = false,
const Self = @This();
pub const Result = ScryResultType(R);
pub const param_type: ParameterType = .option;
pub const multi_mode: MultiMode = scryMode(R);
// accessors to easily read decls from an instance
pub fn Type(comptime _: *const Self) type {
return Self.Result;
}
pub fn mode(comptime _: *const Self) MultiMode {
return Self.multi_mode;
}
pub fn shorts(comptime self: Self) []const OptScope {
comptime return if (self.short) |short|
&[_]OptScope{.{ .opt = mem.encodeShort(short), .scope = self.scope, .value = true }}
else
&.{};
}
pub fn longs(comptime self: Self) []const OptScope {
comptime return if (self.long) |long|
&[_]OptScope{.{ .opt = long, .scope = self.scope, .value = true }}
else
&.{};
}
};
}
pub fn Argument(comptime R: type) type {
return struct {
description: []const u8 = "",
const Self = @This();
pub const Result = ScryResultType(R);
pub const param_type: ParameterType = .argument;
pub const multi_mode: MultiMode = scryMode(R);
// accessors to easily read decls from an instance
pub fn Type(comptime _: *const Self) type {
return Self.Result;
}
pub fn mode(comptime _: *const Self) MultiMode {
return Self.multi_mode;
}
pub fn shorts(_: Self) []const OptScope {
return &.{};
}
pub fn longs(_: Self) []const OptScope {
return &.{};
}
};
}
pub fn Group(comptime R: type) type {
return struct {
description: []const u8 = "",
env: ?[]const u8 = null,
/// at least one of the parameters in the group must be provided
required: bool = false,
// if set, overrides the scope of all parameters
scope: ?Scope = null,
parameters: type,
const Self = @This();
pub const Result = ScryResultType(R);
pub const multi_mode: MultiMode = scryMode(R);
pub const param_type: ParameterType = .group;
// accessors to easily read decls from an instance
pub fn Type(comptime _: *const Self) type {
return Self.Result;
}
pub fn mode(comptime _: *const Self) MultiMode {
return Self.multi_mode;
}
pub fn shorts(comptime self: Self) []const OptScope {
comptime {
var list: []const OptScope = &.{};
for (@typeInfo(self.parameters).@"struct".decls) |decl| {
const param = @field(self.parameters, decl.name);
if (self.scope) |scope| {
for (param.shorts()) |short|
list = list ++ &[_]OptScope{.{ .opt = short.opt, .scope = scope, .value = short.value }};
} else {
list = list ++ param.shorts();
}
}
return list;
}
}
pub fn longs(comptime self: Self) []const OptScope {
comptime {
var list: []const OptScope = &.{};
for (@typeInfo(self.parameters).@"struct".decls) |decl| {
const param = @field(self.parameters, decl.name);
if (self.scope) |scope| {
for (param.longs()) |long|
list = list ++ &[_]OptScope{.{ .opt = long.opt, .scope = scope, .value = long.value }};
} else {
list = list ++ param.longs();
}
}
return list;
}
}
pub fn validate(self: @This()) Status(void) {
comptime {
for (@typeInfo(@TypeOf(self.parameters)).Struct.decls) |td| {
const decl = @field(@TypeOf(self.parameters), td.name);
std.debug.assert(decl.Result == Result);
}
}
}
};
}
fn hasCanary(comptime T: type) bool {
return @typeInfo(T) == .@"struct" and
@hasDecl(T, "__noclip_canary__") and
T.__noclip_canary__ == __Canary;
}
pub fn scryMode(comptime T: type) MultiMode {
return if (hasCanary(T))
T.multi_mode
else
.last;
}
pub fn ScryResultType(comptime T: type) type {
return if (hasCanary(T)) switch (T.multi_mode) {
.accumulate => []const T.Result,
.count, .first, .last => T.Result,
} else T;
}
pub fn DefaultType(comptime T: type) type {
return if (hasCanary(T)) switch (T.multi_mode) {
.accumulate => []const T.Result,
.count => T.Result,
.first, .last => ?T.Result,
} else ?T;
}
pub fn defaultTypeDefault(comptime T: type) DefaultType(T) {
return if (hasCanary(T)) switch (T.multi_mode) {
.accumulate => &.{},
.count => 0,
.first, .last => null,
} else null;
}
pub fn execute(comptime spec: type, args: ExecArgs(spec)) void {
var parser = Parser(spec, true).init(args.alloc, args.context);
defer parser.deinit();
switch (parser.parse(args.args, args.env)) {
.success => |callstack| {
for (callstack) |runner| {
try runner();
}
},
.fail => |report| {
switch (spec.info.options.parse_error_behavior) {
.exit => {},
.propagate => {
if (@hasField(spec, "err"))
spec.err(report);
},
}
},
}
}
pub fn ExecArgs(comptime spec: type) type {
return struct {
alloc: std.mem.Allocator,
args: []const [:0]const u8,
env: std.process.EnvMap,
context: spec.info.context_type,
};
}
pub fn ReturnType(comptime spec: type) type {
const info = @typeInfo(@TypeOf(spec.run)).Fn;
return switch (@typeInfo(info.return_type.?)) {
.ErrorUnion => |eu| blk: {
if (eu.payload != void) unreachable;
break :blk info.return_type.?;
},
.Void => void,
else => unreachable,
};
}
pub const Parser = @import("./parser.zig");
pub const mem = @import("./mem.zig");
const std = @import("std");

View File

@ -1,348 +0,0 @@
const std = @import("std");
const converters = @import("./converters.zig");
const ConverterSignature = converters.ConverterSignature;
const ParameterType = enum { Nominal, Ordinal };
pub const FixedCount = u32;
pub const ValueCount = union(enum) {
flag: void,
count: void,
fixed: FixedCount,
};
pub const FlagBias = enum {
falsy,
truthy,
unbiased,
pub fn string(comptime self: @This()) [:0]const u8 {
return switch (comptime self) {
.truthy => "true",
.falsy => "false",
else => @compileError("flag tag with unbiased bias?"),
};
}
};
pub const ParameterGenerics = struct {
UserContext: type = void,
/// If void, do not expose this parameter in the aggregate converted parameter
/// object. The converter for this parameter shall not return a value. This may be
/// useful for implementing complex conversion that produces output through its
/// side effects or by modifying the user context.
OutputType: type = void,
param_type: ParameterType,
value_count: ValueCount,
/// allow this named parameter to be passed multiple times.
/// values will be appended when it is encountered. If false, only the
/// final encountered instance will be used.
// since we now use multi in place of greedy values for simplicity, we may want to
// convert this an enum or add an additional flag to distinguish between the
// many-to-many and the many-to-one cases.
multi: bool,
pub fn fixedValueCount(comptime OutputType: type, comptime value_count: ValueCount) ValueCount {
return comptime if (value_count == .fixed)
switch (@typeInfo(OutputType)) {
.@"struct" => |info| .{ .fixed = info.fields.len },
.array => |info| .{ .fixed = info.len },
// TODO: this is a bit sloppy, but it can be refined later.
// .Pointer covers slices, which may be a many-to-many conversion.
.pointer => value_count,
else => .{ .fixed = 1 },
}
else
value_count;
}
pub fn hasContext(comptime self: @This()) bool {
return comptime self.UserContext != void;
}
pub fn hasOutput(comptime self: @This()) bool {
return self.OutputType != void;
}
pub fn isFlag(comptime self: @This()) bool {
return comptime switch (self.value_count) {
.flag, .count => true,
.fixed => false,
};
}
pub fn ConvertedType(comptime self: @This()) type {
// is this the correct way to collapse this?
return comptime if (self.multi and self.value_count != .count and self.OutputType != void)
std.ArrayList(self.ReturnValue())
else
self.ReturnValue();
}
pub fn IntermediateType(comptime self: @This()) type {
return comptime if (self.multi and self.value_count != .count)
std.ArrayList(self.IntermediateValue())
else
self.IntermediateValue();
}
pub fn ReturnValue(comptime self: @This()) type {
return comptime switch (self.value_count) {
.flag => bool,
.count => usize,
.fixed => |count| switch (count) {
0 => @compileError("bad fixed-zero parameter"),
1 => self.OutputType,
// it's actually impossible to use a list in the general case
// because the result may have varying types. A tuple would
// work, but cannot be iterated over without inline for. It may
// be worth adding a ".structured" value count for a type that
// consumes many inputs but produces a single output. It would
// be nice to parse a tag into a struct directly. For that use
// case, the output type must be decoupled from the input type.
else => self.OutputType,
},
};
}
pub fn IntermediateValue(comptime self: @This()) type {
return comptime switch (self.value_count) {
.flag => [:0]const u8,
.count => usize,
.fixed => |count| switch (count) {
0 => @compileError("bad fixed-zero parameter"),
1 => [:0]const u8,
else => std.ArrayList([:0]const u8),
},
};
}
pub fn nonscalar(comptime self: @This()) bool {
return comptime switch (self.value_count) {
.flag, .count => false,
.fixed => |count| switch (count) {
0 => @compileError("bad fixed-zero parameter"),
1 => false,
else => true,
},
};
}
};
// Consider a "namespace" parameter e.g. -Dfoo=val style. The namespace would be "D" and
// it takes the place of the second "-", but otherwise this is a long-style parameter.
// Could be parsed as forced-fused. Would work for flags as well, e.g. -fno-flag
pub fn OptionConfig(comptime generics: ParameterGenerics) type {
return struct {
name: []const u8,
short_tag: ?[]const u8 = null,
long_tag: ?[]const u8 = null,
env_var: ?[]const u8 = null,
description: []const u8 = "", // description for output in help text
default: ?generics.OutputType = null,
converter: ?ConverterSignature(generics) = null,
eager: bool = false,
required: bool = generics.param_type == .Ordinal,
global: bool = false,
secret: bool = false,
nice_type_name: ?[]const u8 = null,
flag_bias: FlagBias = .unbiased,
};
}
pub const ShortLongPair = struct {
short_tag: ?[]const u8 = null,
long_tag: ?[]const u8 = null,
};
pub fn FlagConfig(comptime generics: ParameterGenerics) type {
return struct {
name: []const u8,
truthy: ?ShortLongPair = null,
falsy: ?ShortLongPair = null,
env_var: ?[]const u8 = null,
description: []const u8 = "",
default: ?bool = null,
converter: ?ConverterSignature(generics) = null,
eager: bool = false,
required: bool = false,
global: bool = false,
secret: bool = false,
};
}
fn OptionType(comptime generics: ParameterGenerics) type {
return struct {
pub const G: ParameterGenerics = generics;
pub const param_type: ParameterType = generics.param_type;
pub const is_flag: bool = generics.isFlag();
pub const value_count: ValueCount = generics.value_count;
pub const multi: bool = generics.multi;
pub const has_output: bool = generics.hasOutput();
name: []const u8,
short_tag: ?[]const u8,
long_tag: ?[]const u8,
env_var: ?[]const u8,
/// description for output in help text
description: []const u8,
default: ?generics.OutputType,
converter: ConverterSignature(generics),
/// the option converter will be run eagerly, before full command line
/// validation.
eager: bool,
/// the option cannot be omitted from the command line.
required: bool,
/// this option is parsed in a pre-parsing pass that consumes it. It
/// may be present anywhere on the command line. A different way to
/// solve this problem is by using an environment variable. It must be
/// a tagged option.
global: bool,
/// if false, do not expose the resulting value in the output type.
/// the converter must have side effects for this option to do anything.
/// do not print help for this parameter
secret: bool,
/// friendly type name ("string" is better than "[]const u8")
nice_type_name: []const u8,
/// internal field for handling flag value biasing. Do not overwrite unless you
/// want weird things to happen.
flag_bias: FlagBias,
pub fn describe(self: @This(), allocator: std.mem.Allocator) std.mem.Allocator.Error![]const u8 {
var buf = std.ArrayList(u8).init(allocator);
try buf.append('"');
try buf.appendSlice(self.name);
try buf.append('"');
if (self.short_tag != null or self.long_tag != null) {
try buf.appendSlice(" (");
if (self.short_tag) |short|
try buf.appendSlice(short);
if (self.short_tag != null and self.long_tag != null)
try buf.appendSlice(", ");
if (self.long_tag) |long|
try buf.appendSlice(long);
try buf.append(')');
}
return try buf.toOwnedSlice();
}
pub fn IntermediateValue(comptime _: @This()) type {
return generics.IntermediateValue();
}
};
}
fn checkShort(comptime short_tag: ?[]const u8) void {
const short = comptime short_tag orelse return;
if (short.len != 2 or short[0] != '-') @compileError("bad short tag: " ++ short);
}
fn checkLong(comptime long_tag: ?[]const u8) void {
const long = comptime long_tag orelse return;
if (long.len < 3 or long[0] != '-' or long[1] != '-') @compileError("bad long tag: " ++ long);
}
pub fn makeOption(comptime generics: ParameterGenerics, comptime opts: OptionConfig(generics)) OptionType(generics) {
if (opts.short_tag == null and opts.long_tag == null and opts.env_var == null) {
@compileError(
"option " ++
opts.name ++
" must have at least one of a short tag, a long tag, or an environment variable",
);
}
checkShort(opts.short_tag);
checkLong(opts.long_tag);
// perform the logic to create the default converter here? Could be done
// when creating the OptionConfig instead. Need to do it here because there
// may be an error. That's the essential distinction between the OptionType
// and the OptionConfig, is the OptionConfig is just unvalidated parameters,
// whereas the OptionType is an instance of an object that has been
// validated.
const converter = opts.converter orelse
(converters.DefaultConverter(generics) orelse @compileError(
"no converter provided for " ++
opts.name ++
"and no default exists",
));
return OptionType(generics){
.name = opts.name,
//
.short_tag = opts.short_tag,
.long_tag = opts.long_tag,
.env_var = opts.env_var,
//
.description = opts.description,
.default = opts.default,
.converter = converter,
//
.eager = opts.eager,
.required = opts.required,
.global = opts.global,
//
.secret = opts.secret,
.nice_type_name = opts.nice_type_name orelse @typeName(generics.OutputType),
.flag_bias = opts.flag_bias,
};
}
pub fn makeArgument(
comptime generics: ParameterGenerics,
comptime opts: OptionConfig(generics),
) OptionType(generics) {
comptime {
if (opts.short_tag != null or opts.long_tag != null or opts.env_var != null) {
@compileError("argument " ++ opts.name ++ " must not have a long or short tag or an env var");
}
if (opts.global) {
@compileError("argument " ++ opts.name ++ " cannot be global");
}
const converter = opts.converter orelse
(converters.DefaultConverter(generics) orelse @compileError(
"no converter provided for " ++
opts.name ++
"and no default exists",
));
return OptionType(generics){
.name = opts.name,
//
.short_tag = opts.short_tag,
.long_tag = opts.long_tag,
.env_var = opts.env_var,
//
.description = opts.description,
.default = opts.default,
.converter = converter,
//
.eager = opts.eager,
.required = opts.required,
.global = opts.global,
//
.secret = opts.secret,
.nice_type_name = opts.nice_type_name orelse @typeName(generics.OutputType),
.flag_bias = .unbiased,
};
}
}

File diff suppressed because it is too large Load Diff

345
source/tokenizer.zig Normal file
View File

@ -0,0 +1,345 @@
pub const TokenContext = struct {
forward_ddash: bool = false,
short: Options,
long: Options,
positional: []const []const u8,
subcommands: Subcommands,
pub const Options = std.StaticStringMap(OptionContext);
pub const Subcommands = std.StaticStringMap(*const TokenContext);
pub const OptionContext = struct {
global: NestLevel = .none,
value: bool,
};
pub const NestLevel = enum(usize) {
root = 0,
none = std.math.maxInt(usize),
_,
pub fn wrap(lv: usize) NestLevel {
return @enumFromInt(lv);
}
pub fn incr(self: NestLevel) NestLevel {
return wrap(self.unwrap() + 1);
}
pub fn unwrap(self: NestLevel) usize {
return @intFromEnum(self);
}
};
};
pub const Token = union(enum) {
doubledash,
short: u21,
long: []const u8,
shortvalue: struct { name: u21, value: []const u8 },
longvalue: struct { name: []const u8, value: []const u8 },
value: []const u8,
subcommand: []const u8,
pub fn dump(self: Token) void {
switch (self) {
.doubledash => std.debug.print("'--'\n", .{}),
.short => |val| std.debug.print(".short => '{u}'\n", .{val}),
.long => |val| std.debug.print(".long => \"{s}\"\n", .{val}),
.shortvalue => |val| std.debug.print(".shortvalue => '{u}': \"{s}\"\n", .{ val.name, val.value }),
.longvalue => |val| std.debug.print(".shortvalue => {s}: \"{s}\"\n", .{ val.name, val.value }),
.value => |val| std.debug.print(".value => \"{s}\"\n", .{val}),
.subcommand => |val| std.debug.print(".subcommand => \"{s}\"\n", .{val}),
}
}
};
const Assembler = struct {
// this underallocates if fused short args are used and overallocates when
// values are stored in a separate arg. it probably overallocates on
// average, but we correct by growing it when fused arguments are
// encountered, so it always overallocates
tokens: std.ArrayListUnmanaged(Token) = .empty,
// this overallocates in every case except the case where every argument is
// a subcommand. There is no reason to change this after the initial
// allocation.
indices: [*]usize,
len: usize,
cap: usize,
fn init(alloc: std.mem.Allocator, cap: usize) !Assembler {
const idx = try alloc.alloc(usize, cap);
errdefer alloc.free(idx);
return .{
.tokens = try .initCapacity(alloc, cap),
.indices = idx.ptr,
.len = 0,
.cap = cap,
};
}
fn addCapacity(self: *Assembler, alloc: std.mem.Allocator, extra: usize) !void {
try self.tokens.ensureTotalCapacity(alloc, self.tokens.capacity + extra);
}
fn deinit(self: *Assembler, alloc: std.mem.Allocator) void {
alloc.free(self.indices[0..self.cap]);
self.tokens.deinit(alloc);
}
fn finish(self: *Assembler, alloc: std.mem.Allocator) ![]const Token {
return try self.tokens.toOwnedSlice(alloc);
}
fn pushCommand(self: *Assembler) void {
self.indices[self.len] = self.tokens.items.len;
self.len += 1;
}
fn append(self: *Assembler, tok: Token) void {
self.tokens.insertAssumeCapacity(self.indices[self.len - 1], tok);
self.indices[self.len - 1] += 1;
}
fn insert(self: *Assembler, level: TokenContext.NestLevel, tok: Token) void {
if (level == .none) {
self.append(tok);
return;
}
std.debug.assert(level.unwrap() < self.len);
self.tokens.insertAssumeCapacity(self.indices[level.unwrap()], tok);
for (level.unwrap()..self.len) |idx| {
self.indices[idx] += 1;
}
}
};
// This tokenizer is very sloppy; it will happily create tokens that are
// mismatch the details of the TokenContext it has (e.g. it may produce a .short
// token without a value even if the context indicates that flag must produce a
// .shortvalue token). There are two reasons for this approach: the first is
// that tokenization is the wrong place to get persnickety about these details;
// the parser has a lot more context that it can use to produce useful errors
// when the token type mismatches its expectation. The seconds reason is that it
// allows us to use the tokenizer in situations where incomplete or incorrect
// input is expected and we want to get partial results, e.g. for an incomplete
// command line asking for completion options. Theoretically, the only true
// failure modes that the tokenizer can experience are allocation failures (OOM)
// and utf-8 decode failures.
//
// This is also the piece of code responsible for associating global parameters
// with the command that declares them. It is possible to do that here because
// the Parser guarantees that global parameters cannot be shadowed. This does
// generally make the true original order of the command line impossible to
// recover, although this could be rectified by keeping an index of when the
// token was actually encountered. Rearranging the globals here saves the need
// for a two-pass parsing strategy (though creating the tokens and then actually
// iterating the tokens is two passes, no parsing has to be done on the tokens,
// only value conversion).
//
// The produced list of tokens store references to the data contained in the
// provided argument vector. That is, the tokens do not own all of their memory,
// so the argument vector must be kept allocated until the end of the lifetime
// of the list of tokens.
pub fn tokenize(alloc: std.mem.Allocator, tokctx: *const TokenContext, argv: []const []const u8) ![]const Token {
var assembler: Assembler = try .init(alloc, argv.len);
defer assembler.deinit(alloc);
assembler.pushCommand();
var cmdctx: *const TokenContext = tokctx;
var mode: enum { any, fused, ordered } = .any;
var argit: mem.SliceIter([]const u8) = .{ .slice = argv };
while (argit.pop()) |arg| {
mod: switch (mode) {
.any => if (std.mem.eql(u8, arg, "--") and !cmdctx.forward_ddash) {
mode = .ordered;
} else if (std.mem.startsWith(u8, arg, "--")) {
const part = mem.partition(arg[2..], '=');
if (part.rhs) |val| rhs: {
if (cmdctx.long.get(part.lhs)) |optctx| {
assembler.insert(optctx.global, .{
.longvalue = .{ .name = part.lhs, .value = val },
});
break :rhs;
}
assembler.append(
.{ .longvalue = .{ .name = part.lhs, .value = val } },
);
} else norhs: {
if (cmdctx.long.get(part.lhs)) |optctx| {
if (optctx.value) {
if (argit.pop()) |val| {
assembler.insert(optctx.global, .{
.longvalue = .{ .name = part.lhs, .value = val },
});
break :norhs;
}
}
assembler.insert(optctx.global, .{ .long = part.lhs });
break :norhs;
}
assembler.append(.{ .long = part.lhs });
}
} else if (std.mem.startsWith(u8, arg, "-") and arg.len > 1) {
const cpcount = try std.unicode.utf8CountCodepoints(arg[1..]);
if (cpcount > 1)
try assembler.addCapacity(alloc, cpcount);
continue :mod .fused;
} else {
continue :mod .ordered;
},
.fused => {
var iter: std.unicode.Utf8Iterator = .{ .bytes = arg[1..], .i = 0 };
u8i: while (iter.nextCodepointSlice()) |cps| {
const codepoint = std.unicode.utf8Decode(cps) catch unreachable;
if (cmdctx.short.get(cps)) |optctx| {
if (optctx.value and iter.peek(1).len == 0) {
if (argit.pop()) |val| {
assembler.insert(optctx.global, .{
.shortvalue = .{ .name = codepoint, .value = val },
});
continue :u8i;
}
}
assembler.insert(optctx.global, .{
.short = codepoint,
});
continue :u8i;
}
assembler.append(.{ .short = codepoint });
}
},
.ordered => if (cmdctx.subcommands.get(arg)) |scmd| {
mode = .any;
cmdctx = scmd;
assembler.pushCommand();
assembler.append(.{ .subcommand = arg });
} else {
assembler.append(.{ .value = arg });
},
}
}
return try assembler.finish(alloc);
}
const std = @import("std");
const mem = @import("./mem.zig");
fn makeContext() *const TokenContext {
const ToC = TokenContext.OptionContext;
const Nl = TokenContext.NestLevel;
const childa: TokenContext = .{
.short = .initComptime(&.{
.{ "z", ToC{ .global = .none, .value = false } },
.{ "y", ToC{ .global = .none, .value = true } },
.{ "x", ToC{ .global = .none, .value = false } },
.{ "w", ToC{ .global = .none, .value = true } },
// these are provided by the parent
.{ "c", ToC{ .global = Nl.wrap(0), .value = false } },
.{ "d", ToC{ .global = Nl.wrap(0), .value = true } },
}),
.long = .initComptime(&.{
.{ "long-z", ToC{ .global = .none, .value = false } },
.{ "global-a", ToC{ .global = Nl.wrap(0), .value = false } },
}),
.positional = &.{ "argument-z", "argument-y" },
.subcommands = .initComptime(&.{}),
};
const ctx: TokenContext = .{
.short = .initComptime(&.{
.{ "a", ToC{ .global = .none, .value = false } },
.{ "b", ToC{ .global = .none, .value = true } },
// global arguments are not global on the command that defines them
.{ "c", ToC{ .global = .none, .value = false } },
.{ "d", ToC{ .global = .none, .value = true } },
}),
.long = .initComptime(&.{
.{ "long-a", ToC{ .global = .none, .value = false } },
.{ "global-a", ToC{ .global = .none, .value = false } },
}),
.positional = &.{},
.subcommands = .initComptime(&.{
.{ "subcommand-a", &childa },
}),
};
return &ctx;
}
test "tokenize" {
const alloc = std.testing.allocator;
const context = comptime makeContext();
{
const tokens = try tokenize(alloc, context, &.{"-abc"});
defer alloc.free(tokens);
try std.testing.expectEqual(3, tokens.len);
try std.testing.expectEqual(Token{ .short = 'a' }, tokens[0]);
try std.testing.expectEqual(Token{ .short = 'b' }, tokens[1]);
try std.testing.expectEqual(Token{ .short = 'c' }, tokens[2]);
}
{
const tokens = try tokenize(alloc, context, &.{ "-abd", "dee" });
defer alloc.free(tokens);
try std.testing.expectEqual(3, tokens.len);
try std.testing.expectEqual(Token{ .short = 'a' }, tokens[0]);
try std.testing.expectEqual(Token{ .short = 'b' }, tokens[1]);
try std.testing.expectEqual(Token{ .shortvalue = .{ .name = 'd', .value = "dee" } }, tokens[2]);
}
{
const tokens = try tokenize(alloc, context, &.{ "-cba", "dee" });
defer alloc.free(tokens);
try std.testing.expectEqual(4, tokens.len);
try std.testing.expectEqual(Token{ .short = 'c' }, tokens[0]);
try std.testing.expectEqual(Token{ .short = 'b' }, tokens[1]);
try std.testing.expectEqual(Token{ .short = 'a' }, tokens[2]);
try std.testing.expectEqual(Token{ .value = "dee" }, tokens[3]);
}
{
const tokens = try tokenize(alloc, context, &.{ "-acb", "dee", "-d", "-zyx" });
defer alloc.free(tokens);
try std.testing.expectEqual(4, tokens.len);
try std.testing.expectEqual(Token{ .short = 'a' }, tokens[0]);
try std.testing.expectEqual(Token{ .short = 'c' }, tokens[1]);
try std.testing.expectEqual(Token{ .shortvalue = .{ .name = 'b', .value = "dee" } }, tokens[2]);
try std.testing.expectEqual(Token{ .shortvalue = .{ .name = 'd', .value = "-zyx" } }, tokens[3]);
}
{
const tokens = try tokenize(alloc, context, &.{ "-a", "-c", "subcommand-a", "-d", "dee", "-zyx", "--global-a" });
defer alloc.free(tokens);
try std.testing.expectEqual(8, tokens.len);
try std.testing.expectEqual(Token{ .short = 'a' }, tokens[0]);
try std.testing.expectEqual(Token{ .short = 'c' }, tokens[1]);
try std.testing.expectEqual(Token{ .shortvalue = .{ .name = 'd', .value = "dee" } }, tokens[2]);
try std.testing.expectEqualDeep(Token{ .long = "global-a" }, tokens[3]);
try std.testing.expectEqual(Token{ .subcommand = "subcommand-a" }, tokens[4]);
try std.testing.expectEqual(Token{ .short = 'z' }, tokens[5]);
try std.testing.expectEqual(Token{ .short = 'y' }, tokens[6]);
try std.testing.expectEqual(Token{ .short = 'x' }, tokens[7]);
}
}
// parameter styles to accept:
// --name value
// --name=value
// -n value
// -fused (parsed as -f -u -s -e -d)
// -fused value (parsed as -f -u -s -e -d value)
// ordered
// a named parameter can only take zero or one values. Establish a convention for compound values:
// --name val1,val2
// --name=val1,val2
// --name="val1, val2" (probably should not consume whitespace, since the user has to go out of their way to do quoting for it)
// --name key1=val1,key2=val2
// --name=key1=val1,key2=val2 (should be familiar from docker)