better error messaging

This commit is contained in:
Emil Lerch 2025-07-17 09:44:12 -07:00
parent a559a621c3
commit 3b195a9eb6
Signed by: lobo
GPG key ID: A7B62D657EF764F8
4 changed files with 156 additions and 12 deletions

View file

@ -169,9 +169,9 @@ pub fn generateFeed(allocator: Allocator, releases: []const Release) ![]u8 {
\\<feed xmlns="http://www.w3.org/2005/Atom">
\\<title>Repository Releases</title>
\\<subtitle>New releases from starred repositories</subtitle>
\\<link href="https://github.com" rel="alternate"/>
\\<link href="https://example.com/releases.xml" rel="self"/>
\\<id>https://example.com/releases</id>
\\<link href="https://releases.lerch.org" rel="alternate"/>
\\<link href="https://releases.lerch.org/atom.xml" rel="self"/>
\\<id>https://releases.lerch.org</id>
\\
);
@ -303,6 +303,37 @@ test "Atom feed generation with markdown" {
try std.testing.expect(std.mem.indexOf(u8, atom_content, "&lt;ul&gt;") != 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, "&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;") != null);
try std.testing.expect(std.mem.indexOf(u8, atom_content, "&lt;/code&gt;&lt;/pre&gt;") != null);
// Should contain the escaped code content
try std.testing.expect(std.mem.indexOf(u8, atom_content, "const greeting = &amp;apos;Hello World&amp;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",
},
};

View file

@ -376,9 +376,9 @@ test "atom feed generation" {
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<feed xmlns=\"http://www.w3.org/2005/Atom\">") != null);
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<title>Repository Releases</title>") != null);
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<subtitle>New releases from starred repositories</subtitle>") != null);
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<link href=\"https://github.com\" rel=\"alternate\"/>") != null);
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<link href=\"https://example.com/releases.xml\" rel=\"self\"/>") != null);
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<id>https://example.com/releases</id>") != null);
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<link href=\"https://releases.lerch.org\" rel=\"alternate\"/>") != null);
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<link href=\"https://releases.lerch.org/atom.xml\" rel=\"self\"/>") != null);
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<id>https://releases.lerch.org</id>") != null);
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<updated>") != null);
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<entry>") != null);
try std.testing.expect(std.mem.indexOf(u8, atom_content, "</feed>") != null);

View file

@ -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("</ul>\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("<pre><code class=\"language-");
try appendEscapedHtml(&result, lang_hint);
try result.appendSlice("\">");
} else {
try result.appendSlice("<pre><code>");
}
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("</code></pre>\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("<br/>\n");
continue;
@ -130,6 +173,11 @@ pub fn convertMarkdownToHtml(allocator: Allocator, markdown: []const u8) !Conver
try result.appendSlice("</ul>\n");
}
// Close any unclosed code block
if (in_code_block) {
try result.appendSlice("</code></pre>\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 = "<p>Here&apos;s some code:</p>\n<pre><code>const x = 42;\nconsole.log(x);\n</code></pre>\n<p>End of code.</p>\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 = "<pre><code class=\"language-javascript\">const greeting = &apos;Hello World&apos;;\nconsole.log(greeting);\n</code></pre>\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 = "<pre><code class=\"language-python\">def hello():\n print(&apos;Hello!&apos;)\n</code></pre>\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 = "<pre><code>unclosed code block\nmore code\n</code></pre>\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 = "<p>Use <code>const</code> for constants and <code>let</code> for variables.</p>\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;

View file

@ -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;
}