From 3b195a9eb6f3a35064c782ab9ec3354e7b779263 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Thu, 17 Jul 2025 09:44:12 -0700 Subject: [PATCH] better error messaging --- src/atom.zig | 39 ++++++++++++-- src/main.zig | 6 +-- src/markdown.zig | 112 +++++++++++++++++++++++++++++++++++++-- src/providers/GitHub.zig | 11 +++- 4 files changed, 156 insertions(+), 12 deletions(-) diff --git a/src/atom.zig b/src/atom.zig index 4394d08..1a13eb5 100644 --- a/src/atom.zig +++ b/src/atom.zig @@ -169,9 +169,9 @@ pub fn generateFeed(allocator: Allocator, releases: []const Release) ![]u8 { \\ \\Repository Releases \\New releases from starred repositories - \\ - \\ - \\https://example.com/releases + \\ + \\ + \\https://releases.lerch.org \\ ); @@ -303,6 +303,37 @@ test "Atom feed generation with markdown" { try std.testing.expect(std.mem.indexOf(u8, atom_content, "<ul>") != null); } +test "Atom feed with fenced code blocks" { + const allocator = std.testing.allocator; + + const releases = [_]Release{ + Release{ + .repo_name = "test/repo", + .tag_name = "v1.0.0", + .published_at = @intCast(@divTrunc( + (try zeit.instant(.{ .source = .{ .iso8601 = "2024-01-01T00:00:00Z" } })).timestamp, + std.time.ns_per_s, + )), + .html_url = "https://github.com/test/repo/releases/tag/v1.0.0", + .description = "Here's some code:\n```javascript\nconst greeting = 'Hello World';\nconsole.log(greeting);\n```\nEnd of example.", + .provider = "github", + }, + }; + + const atom_content = try generateFeed(allocator, &releases); + defer allocator.free(atom_content); + + // Should NOT contain fallback metadata since fenced code blocks are now supported + try std.testing.expect(std.mem.indexOf(u8, atom_content, "markdown-fallback") == null); + + // Should contain proper HTML code block structure + try std.testing.expect(std.mem.indexOf(u8, atom_content, "<pre><code class="language-javascript">") != null); + try std.testing.expect(std.mem.indexOf(u8, atom_content, "</code></pre>") != null); + + // Should contain the escaped code content + try std.testing.expect(std.mem.indexOf(u8, atom_content, "const greeting = &apos;Hello World&apos;;") != null); +} + test "Atom feed with fallback markdown" { const allocator = std.testing.allocator; @@ -315,7 +346,7 @@ test "Atom feed with fallback markdown" { std.time.ns_per_s, )), .html_url = "https://github.com/test/repo/releases/tag/v1.0.0", - .description = "```javascript\nconst x = 1;\n```", + .description = "| Column 1 | Column 2 |\n|----------|----------|\n| Value 1 | Value 2 |", .provider = "github", }, }; diff --git a/src/main.zig b/src/main.zig index fcfd3c8..614b6cd 100644 --- a/src/main.zig +++ b/src/main.zig @@ -376,9 +376,9 @@ test "atom feed generation" { try std.testing.expect(std.mem.indexOf(u8, atom_content, "") != null); try std.testing.expect(std.mem.indexOf(u8, atom_content, "Repository Releases") != null); try std.testing.expect(std.mem.indexOf(u8, atom_content, "New releases from starred repositories") != null); - try std.testing.expect(std.mem.indexOf(u8, atom_content, "") != null); - try std.testing.expect(std.mem.indexOf(u8, atom_content, "") != null); - try std.testing.expect(std.mem.indexOf(u8, atom_content, "https://example.com/releases") != null); + try std.testing.expect(std.mem.indexOf(u8, atom_content, "") != null); + try std.testing.expect(std.mem.indexOf(u8, atom_content, "") != null); + try std.testing.expect(std.mem.indexOf(u8, atom_content, "https://releases.lerch.org") != null); try std.testing.expect(std.mem.indexOf(u8, atom_content, "") != null); try std.testing.expect(std.mem.indexOf(u8, atom_content, "") != null); try std.testing.expect(std.mem.indexOf(u8, atom_content, "") != null); diff --git a/src/markdown.zig b/src/markdown.zig index 3e277b7..c24bc96 100644 --- a/src/markdown.zig +++ b/src/markdown.zig @@ -21,10 +21,53 @@ pub fn convertMarkdownToHtml(allocator: Allocator, markdown: []const u8) !Conver var lines = std.mem.splitScalar(u8, markdown, '\n'); var in_list = false; var list_type: ?u8 = null; // '*' or '-' + var in_code_block = false; + var code_block_fence: []const u8 = ""; while (lines.next()) |line| { const trimmed = std.mem.trim(u8, line, " \t\r"); + // Handle fenced code blocks + if (std.mem.startsWith(u8, trimmed, "```") or std.mem.startsWith(u8, trimmed, "~~~")) { + const fence = if (std.mem.startsWith(u8, trimmed, "```")) "```" else "~~~"; + + if (!in_code_block) { + // Starting a code block + if (in_list) { + try result.appendSlice("\n"); + in_list = false; + list_type = null; + } + + in_code_block = true; + code_block_fence = fence; + + // Extract language hint if present + const lang_hint = std.mem.trim(u8, trimmed[fence.len..], " \t\r"); + if (lang_hint.len > 0) { + try result.appendSlice("
");
+                } else {
+                    try result.appendSlice("
");
+                }
+                continue;
+            } else if (std.mem.eql(u8, fence, code_block_fence)) {
+                // Ending the code block
+                in_code_block = false;
+                code_block_fence = "";
+                try result.appendSlice("
\n"); + continue; + } + } + + // If we're inside a code block, just add the line as-is (escaped) + if (in_code_block) { + try appendEscapedHtml(&result, line); + try result.appendSlice("\n"); + continue; + } + if (trimmed.len == 0) { try result.appendSlice("
\n"); continue; @@ -130,6 +173,11 @@ pub fn convertMarkdownToHtml(allocator: Allocator, markdown: []const u8) !Conver try result.appendSlice("\n"); } + // Close any unclosed code block + if (in_code_block) { + try result.appendSlice("
\n"); + } + return ConversionResult{ .html = try result.toOwnedSlice(), .has_fallback = has_fallback, @@ -313,9 +361,6 @@ fn findInlineCode(text: []const u8) ?TextInfo { /// Check if text contains complex markdown patterns we don't handle fn hasComplexMarkdown(text: []const u8) bool { - // Code blocks - if (std.mem.indexOf(u8, text, "```") != null) return true; - // Tables if (std.mem.indexOf(u8, text, "|") != null) return true; @@ -334,7 +379,66 @@ fn hasComplexMarkdown(text: []const u8) bool { return false; } -// Tests +test "convert fenced code blocks" { + const allocator = testing.allocator; + + // Test basic fenced code block with backticks + const markdown1 = "Here's some code:\n```\nconst x = 42;\nconsole.log(x);\n```\nEnd of code."; + const result1 = try convertMarkdownToHtml(allocator, markdown1); + defer result1.deinit(allocator); + + const expected1 = "

Here's some code:

\n
const x = 42;\nconsole.log(x);\n
\n

End of code.

\n"; + try testing.expectEqualStrings(expected1, result1.html); + try testing.expect(!result1.has_fallback); + + // Test fenced code block with language hint + const markdown2 = "```javascript\nconst greeting = 'Hello World';\nconsole.log(greeting);\n```"; + const result2 = try convertMarkdownToHtml(allocator, markdown2); + defer result2.deinit(allocator); + + const expected2 = "
const greeting = 'Hello World';\nconsole.log(greeting);\n
\n"; + try testing.expectEqualStrings(expected2, result2.html); + try testing.expect(!result2.has_fallback); + + // Test fenced code block with tildes + const markdown3 = "~~~python\ndef hello():\n print('Hello!')\n~~~"; + const result3 = try convertMarkdownToHtml(allocator, markdown3); + defer result3.deinit(allocator); + + const expected3 = "
def hello():\n    print('Hello!')\n
\n"; + try testing.expectEqualStrings(expected3, result3.html); + try testing.expect(!result3.has_fallback); + + // Test unclosed code block (should auto-close) + const markdown4 = "```\nunclosed code block\nmore code"; + const result4 = try convertMarkdownToHtml(allocator, markdown4); + defer result4.deinit(allocator); + + const expected4 = "
unclosed code block\nmore code\n
\n"; + try testing.expectEqualStrings(expected4, result4.html); + try testing.expect(!result4.has_fallback); + + if (std.process.hasEnvVar(allocator, "test-debug") catch false) { + std.debug.print("Fenced code blocks test - Input: {s}\nOutput: {s}\n", .{ markdown1, result1.html }); + } +} + +test "inline code with backticks" { + const allocator = testing.allocator; + + const markdown = "Use `const` for constants and `let` for variables."; + const result = try convertMarkdownToHtml(allocator, markdown); + defer result.deinit(allocator); + + const expected = "

Use const for constants and let for variables.

\n"; + try testing.expectEqualStrings(expected, result.html); + try testing.expect(!result.has_fallback); + + if (std.process.hasEnvVar(allocator, "test-debug") catch false) { + std.debug.print("Inline code test - Input: {s}\nOutput: {s}\n", .{ markdown, result.html }); + } +} + test "convert headers" { const allocator = testing.allocator; diff --git a/src/providers/GitHub.zig b/src/providers/GitHub.zig index b119c41..c89e338 100644 --- a/src/providers/GitHub.zig +++ b/src/providers/GitHub.zig @@ -132,7 +132,7 @@ fn fetchRepoReleasesTask(task: *RepoFetchTask) void { defer client.deinit(); const repo_releases = getRepoReleases(task.allocator, &client, task.token, task.repo) catch |err| { - task.error_msg = std.fmt.allocPrint(task.allocator, "{}", .{err}) catch "Unknown error"; + task.error_msg = std.fmt.allocPrint(task.allocator, "{s}: {}", .{ task.repo, err }) catch "Unknown error"; return; }; @@ -378,6 +378,15 @@ fn getRepoReleases(allocator: Allocator, client: *http.Client, token: []const u8 try req.wait(); if (req.response.status != .ok) { + const is_test = @import("builtin").is_test; + if (!is_test) { + // Try to read the error response body for more details + const error_body = req.reader().readAllAlloc(allocator, 4096) catch ""; + defer if (error_body.len > 0) allocator.free(error_body); + + const stderr = std.io.getStdErr().writer(); + stderr.print("GitHub: Failed to fetch releases for {s}: HTTP {} - {s}\n", .{ repo, @intFromEnum(req.response.status), error_body }) catch {}; + } return error.HttpRequestFailed; }