Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -69,23 +69,23 @@ pub fn build(b: *std.Build) void {
test_step.dependOn(&run_lib_unit_tests.step);
test_step.dependOn(&run_exe_unit_tests.step);

const diff = b.addSystemCommand(&.{
"git",
"diff",
"--cached", // see git_add comment
"--exit-code",
const snapshot_check_mod = b.createModule(.{
.root_source_file = b.path("tools/check_snapshots.zig"),
.target = target,
.optimize = optimize,
});
diff.addDirectoryArg(b.path("snapshots/"));

test_step.dependOn(&diff.step);

const git_add = b.addSystemCommand(&.{
"git",
"add",
"snapshots/",
const snapshot_check = b.addExecutable(.{
.name = "check-snapshots",
.root_module = snapshot_check_mod,
});

diff.step.dependOn(&git_add.step);
const run_snapshot_check = b.addRunArtifact(snapshot_check);
run_snapshot_check.stdio = .inherit;
run_snapshot_check.has_side_effects = true;
run_snapshot_check.addDirectoryArg(b.path("snapshots"));
run_snapshot_check.step.dependOn(&run_lib_unit_tests.step);
test_step.dependOn(&run_snapshot_check.step);
}

const std = @import("std");
2 changes: 1 addition & 1 deletion snapshots/md/single_line_mixed.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
**Hello**, world! *italic* underline [s]strike
**Hello**, world! *italic* underline ~~strike~~
1 change: 1 addition & 0 deletions snapshots/md/single_line_strikethrough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
~~strike~~
49 changes: 49 additions & 0 deletions src/formatters/markdown.zig
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//! ## Supported Conversions
//! - `[b]text[/b]` → `**text**`
//! - `[i]text[/i]` → `*text*`
//! - `[s]text[/s]` → `~~text~~`
//! - `[url=link]text[/url]` → `[text](link)`
//! - `[email=addr]text[/email]` → `[text](mailto:addr)`
//! - `[code]text[/code]` → `` `text` ``
Expand Down Expand Up @@ -116,6 +117,7 @@ pub const MarkdownElement = enum {
list,
listItem,
underline,
strikethrough,
noOp,
};

Expand All @@ -131,6 +133,7 @@ const element_map = std.StaticStringMap(MarkdownElement).initComptime(&.{
.{ "list", .list },
.{ "*", .listItem },
.{ "u", .underline },
.{ "s", .strikethrough },
});

/// Renders a BBCode document as Markdown text.
Expand Down Expand Up @@ -224,6 +227,7 @@ pub fn writeElement(node: Node, element: MarkdownElement, ctx: *const WriteConte
.list => try writeListElement(node, ctx),
.listItem => try writeListItemElement(node, ctx),
.underline => try writeUnderlineElement(node, ctx),
.strikethrough => try writeStrikethroughElement(node, ctx),
.noOp => try writeNoOpElement(node, ctx),
}
}
Expand Down Expand Up @@ -342,6 +346,12 @@ pub fn writeItalicElement(node: Node, ctx: *const WriteContext) !void {
try ctx.writer.writeAll("*");
}

pub fn writeStrikethroughElement(node: Node, ctx: *const WriteContext) !void {
try ctx.writer.writeAll("~~");
try render(node, ctx);
try ctx.writer.writeAll("~~");
}

pub fn writeCodeElement(node: Node, ctx: *const WriteContext) !void {
try ctx.writer.writeAll("`");
try render(node, ctx);
Expand Down Expand Up @@ -392,6 +402,31 @@ pub fn writeEmailElement(node: Node, ctx: *const WriteContext) !void {
}
}

test "strikethrough" {
const cases = [_]struct {
bbcode: []const u8,
markdown: []const u8,
}{
.{ .bbcode = "[s]strike[/s]", .markdown = "~~strike~~" },
.{ .bbcode = "before [s]strike[/s] after", .markdown = "before ~~strike~~ after" },
.{
.bbcode = "[s]strike [b]bold[/b] and [i]italic[/i][/s]",
.markdown = "~~strike **bold** and *italic*~~",
},
};

for (cases) |case| {
var document = try Document.loadFromBuffer(testing.allocator, case.bbcode, .{});
defer document.deinit();

var output = std.Io.Writer.Allocating.init(testing.allocator);
defer output.deinit();

try renderDocument(testing.allocator, document, &output.writer, .{});
try testing.expectEqualStrings(case.markdown, output.written());
}
}

test render {
const bbcode_document =
\\[b]Hello, World![/b]
Expand Down Expand Up @@ -489,6 +524,20 @@ test "single lines" {
try writer.flush();
}

{
var document = try Document.loadFromBuffer(testing.allocator, "[s]strike[/s]", .{});
defer document.deinit();

var file = try std.Io.Dir.cwd().createFile(testing.io, "snapshots/md/single_line_strikethrough.md", .{});
defer file.close(testing.io);

var buf: [1024]u8 = undefined;
var file_writer = file.writer(testing.io, &buf);
var writer = &file_writer.interface;
try renderDocument(testing.allocator, document, writer, .{});
try writer.flush();
}

{
var document = try Document.loadFromBuffer(testing.allocator, "[b]Hello[/b], world! [i]italic[/i] [u]underline[/u] [s]strike[/s]", .{});
defer document.deinit();
Expand Down
166 changes: 166 additions & 0 deletions tools/check_snapshots.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
const std = @import("std");

const RepositoryKind = enum { jj, git, unknown };

pub fn main(init: std.process.Init) !void {
const io = init.io;
var arena = std.heap.ArenaAllocator.init(init.gpa);
defer arena.deinit();
const allocator = arena.allocator();
const cwd = std.Io.Dir.cwd();

const repository_kind: RepositoryKind = repository_kind: {
cwd.access(io, ".jj", .{}) catch {
cwd.access(io, ".git", .{}) catch break :repository_kind .unknown;
break :repository_kind .git;
};
break :repository_kind .jj;
};

switch (repository_kind) {
.jj => try checkJjSnapshots(allocator, io, cwd),
.git => try checkGitSnapshots(allocator, io),
.unknown => {
try fatal(io, "No .git or .jj directory found; cannot verify snapshots.\n", .{});
std.process.exit(1);
},
}
}

fn checkGitSnapshots(allocator: std.mem.Allocator, io: std.Io) !void {
_ = try runCapture(allocator, io, &.{ "git", "add", "snapshots/" });
_ = try runCapture(allocator, io, &.{ "git", "diff", "--cached", "--exit-code", "snapshots/" });
}

fn checkJjSnapshots(allocator: std.mem.Allocator, io: std.Io, cwd: std.Io.Dir) !void {
const committed_snapshot_paths = try collectCommittedJjSnapshotPaths(allocator, io);
const worktree_snapshot_paths = try collectWorktreeSnapshotPaths(allocator, io, cwd);

const comparePaths = struct {
fn lessThan(_: void, lhs: []const u8, rhs: []const u8) bool {
return std.mem.lessThan(u8, lhs, rhs);
}
}.lessThan;
std.sort.pdq([]const u8, committed_snapshot_paths.items, {}, comparePaths);
std.sort.pdq([]const u8, worktree_snapshot_paths.items, {}, comparePaths);

const snapshot_path_sets_match = match: {
if (committed_snapshot_paths.items.len != worktree_snapshot_paths.items.len) break :match false;
for (committed_snapshot_paths.items, worktree_snapshot_paths.items) |committed_path, worktree_path| {
if (!std.mem.eql(u8, committed_path, worktree_path)) break :match false;
}
break :match true;
};

if (!snapshot_path_sets_match) {
try fatal(io, "Snapshot file set differs from the current jj commit.\n", .{});
try fatal(io, "Committed snapshots:\n", .{});
for (committed_snapshot_paths.items) |path| try fatal(io, " {s}\n", .{path});
try fatal(io, "Worktree snapshots:\n", .{});
for (worktree_snapshot_paths.items) |path| try fatal(io, " {s}\n", .{path});
std.process.exit(1);
}

for (committed_snapshot_paths.items) |path| {
try compareJjSnapshot(allocator, io, cwd, path);
}
}

fn collectCommittedJjSnapshotPaths(allocator: std.mem.Allocator, io: std.Io) !std.ArrayList([]const u8) {
const result = try runCapture(allocator, io, &.{
"jj",
"--ignore-working-copy",
"--no-pager",
"file",
"list",
"-r",
"@",
"snapshots/",
});

var paths: std.ArrayList([]const u8) = .empty;
var lines = std.mem.splitScalar(u8, result.stdout, '\n');
while (lines.next()) |line| {
if (line.len == 0) continue;
try paths.append(allocator, try allocator.dupe(u8, line));
}

return paths;
}

fn collectWorktreeSnapshotPaths(allocator: std.mem.Allocator, io: std.Io, cwd: std.Io.Dir) !std.ArrayList([]const u8) {
var snapshots_dir = try cwd.openDir(io, "snapshots", .{ .iterate = true });
defer snapshots_dir.close(io);

var walker = try snapshots_dir.walk(allocator);
defer walker.deinit();

var paths: std.ArrayList([]const u8) = .empty;
while (try walker.next(io)) |entry| {
if (entry.kind != .file) continue;

// jj paths always use '/', while the directory walker uses the platform separator.
const normalized_relative_path = try allocator.dupe(u8, entry.path);
if (std.fs.path.sep != '/') {
std.mem.replaceScalar(u8, normalized_relative_path, std.fs.path.sep, '/');
}

const snapshot_path = try std.mem.concat(allocator, u8, &.{ "snapshots/", normalized_relative_path });
try paths.append(allocator, snapshot_path);
}

return paths;
}

fn compareJjSnapshot(allocator: std.mem.Allocator, io: std.Io, cwd: std.Io.Dir, path: []const u8) !void {
const expected_result = try runCapture(allocator, io, &.{
"jj",
"--ignore-working-copy",
"--no-pager",
"file",
"show",
"-r",
"@",
path,
});
const actual = try cwd.readFileAlloc(io, path, allocator, .unlimited);

if (!std.mem.eql(u8, expected_result.stdout, actual)) {
try fatal(io, "Snapshot differs from the current jj commit: {s}\n", .{path});
std.process.exit(1);
}
}

const RunCaptureResult = struct {
stdout: []u8,
stderr: []u8,
};

fn runCapture(allocator: std.mem.Allocator, io: std.Io, argv: []const []const u8) !RunCaptureResult {
const result = try std.process.run(allocator, io, .{
.argv = argv,
.stdout_limit = .unlimited,
.stderr_limit = .unlimited,
});

// TODO(zig-0.16): Use result.term.success() once Zig 0.16 is removed from CI.
const command_succeeded = switch (result.term) {
.exited => |code| code == 0,
else => false,
};
if (!command_succeeded) {
try fatal(io, "Command failed: {s}\n", .{argv[0]});
if (result.stdout.len > 0) try fatal(io, "{s}", .{result.stdout});
if (result.stderr.len > 0) try fatal(io, "{s}", .{result.stderr});
std.process.exit(1);
}

return .{ .stdout = result.stdout, .stderr = result.stderr };
}

fn fatal(io: std.Io, comptime fmt: []const u8, args: anytype) !void {
var buf: [1024]u8 = undefined;
var writer = std.Io.File.stderr().writerStreaming(io, &buf);
try writer.interface.print(fmt, args);
try writer.interface.flush();
}
Loading