better error messaging
This commit is contained in:
parent
a559a621c3
commit
3b195a9eb6
4 changed files with 156 additions and 12 deletions
39
src/atom.zig
39
src/atom.zig
|
@ -169,9 +169,9 @@ pub fn generateFeed(allocator: Allocator, releases: []const Release) ![]u8 {
|
||||||
\\<feed xmlns="http://www.w3.org/2005/Atom">
|
\\<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
\\<title>Repository Releases</title>
|
\\<title>Repository Releases</title>
|
||||||
\\<subtitle>New releases from starred repositories</subtitle>
|
\\<subtitle>New releases from starred repositories</subtitle>
|
||||||
\\<link href="https://github.com" rel="alternate"/>
|
\\<link href="https://releases.lerch.org" rel="alternate"/>
|
||||||
\\<link href="https://example.com/releases.xml" rel="self"/>
|
\\<link href="https://releases.lerch.org/atom.xml" rel="self"/>
|
||||||
\\<id>https://example.com/releases</id>
|
\\<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, "<ul>") != null);
|
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" {
|
test "Atom feed with fallback markdown" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
|
@ -315,7 +346,7 @@ test "Atom feed with fallback markdown" {
|
||||||
std.time.ns_per_s,
|
std.time.ns_per_s,
|
||||||
)),
|
)),
|
||||||
.html_url = "https://github.com/test/repo/releases/tag/v1.0.0",
|
.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",
|
.provider = "github",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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, "<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, "<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, "<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://releases.lerch.org\" 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, "<link href=\"https://releases.lerch.org/atom.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, "<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, "<updated>") != null);
|
||||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<entry>") != 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);
|
try std.testing.expect(std.mem.indexOf(u8, atom_content, "</feed>") != null);
|
||||||
|
|
112
src/markdown.zig
112
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 lines = std.mem.splitScalar(u8, markdown, '\n');
|
||||||
var in_list = false;
|
var in_list = false;
|
||||||
var list_type: ?u8 = null; // '*' or '-'
|
var list_type: ?u8 = null; // '*' or '-'
|
||||||
|
var in_code_block = false;
|
||||||
|
var code_block_fence: []const u8 = "";
|
||||||
|
|
||||||
while (lines.next()) |line| {
|
while (lines.next()) |line| {
|
||||||
const trimmed = std.mem.trim(u8, line, " \t\r");
|
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) {
|
if (trimmed.len == 0) {
|
||||||
try result.appendSlice("<br/>\n");
|
try result.appendSlice("<br/>\n");
|
||||||
continue;
|
continue;
|
||||||
|
@ -130,6 +173,11 @@ pub fn convertMarkdownToHtml(allocator: Allocator, markdown: []const u8) !Conver
|
||||||
try result.appendSlice("</ul>\n");
|
try result.appendSlice("</ul>\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close any unclosed code block
|
||||||
|
if (in_code_block) {
|
||||||
|
try result.appendSlice("</code></pre>\n");
|
||||||
|
}
|
||||||
|
|
||||||
return ConversionResult{
|
return ConversionResult{
|
||||||
.html = try result.toOwnedSlice(),
|
.html = try result.toOwnedSlice(),
|
||||||
.has_fallback = has_fallback,
|
.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
|
/// Check if text contains complex markdown patterns we don't handle
|
||||||
fn hasComplexMarkdown(text: []const u8) bool {
|
fn hasComplexMarkdown(text: []const u8) bool {
|
||||||
// Code blocks
|
|
||||||
if (std.mem.indexOf(u8, text, "```") != null) return true;
|
|
||||||
|
|
||||||
// Tables
|
// Tables
|
||||||
if (std.mem.indexOf(u8, text, "|") != null) return true;
|
if (std.mem.indexOf(u8, text, "|") != null) return true;
|
||||||
|
|
||||||
|
@ -334,7 +379,66 @@ fn hasComplexMarkdown(text: []const u8) bool {
|
||||||
return false;
|
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'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 = 'Hello World';\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('Hello!')\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" {
|
test "convert headers" {
|
||||||
const allocator = testing.allocator;
|
const allocator = testing.allocator;
|
||||||
|
|
||||||
|
|
|
@ -132,7 +132,7 @@ fn fetchRepoReleasesTask(task: *RepoFetchTask) void {
|
||||||
defer client.deinit();
|
defer client.deinit();
|
||||||
|
|
||||||
const repo_releases = getRepoReleases(task.allocator, &client, task.token, task.repo) catch |err| {
|
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;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -378,6 +378,15 @@ fn getRepoReleases(allocator: Allocator, client: *http.Client, token: []const u8
|
||||||
try req.wait();
|
try req.wait();
|
||||||
|
|
||||||
if (req.response.status != .ok) {
|
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;
|
return error.HttpRequestFailed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue