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(
\\
{s}
\\
, .{std.mem.trim(u8, buf, " \n")}),
.include => @panic("included console example not supported"),
},
}
try writer.writeAll(
\\