From 6d952892747f55750c0a16abb420b11ccb8095ad Mon Sep 17 00:00:00 2001 From: Simon Hartcher Date: Sat, 21 Mar 2026 22:10:47 +1100 Subject: [PATCH 1/2] feat: convert Godot BBCode cross-refs and codeblocks to Markdown Use bbcodez's callback system to handle Godot-specific BBCode tags that were passing through unconverted to the markdown cache: - Cross-reference tags ([member], [method], [param], [constant], [signal], [enum], [annotation], [theme_item]) now render as inline code or italic markdown - Bare class references ([Node2D], [@GDScript]) render as inline code - [codeblock] tags render as fenced code blocks with dedented content - [codeblocks] with [gdscript]/[csharp] sections render as separate language-tagged fenced blocks Configured bbcodez with equals_required_in_parameters=false to parse space-separated Godot tags, is_self_closing_fn for tags without closing pairs, and verbatim_tags for code block content preservation. --- src/DocDatabase.zig | 319 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 317 insertions(+), 2 deletions(-) diff --git a/src/DocDatabase.zig b/src/DocDatabase.zig index d69efc3..a589b43 100644 --- a/src/DocDatabase.zig +++ b/src/DocDatabase.zig @@ -43,14 +43,195 @@ pub const Entry = struct { fn bbcodeToMarkdown(allocator: Allocator, input: []const u8) ![]const u8 { var output: std.Io.Writer.Allocating = .init(allocator); - const bbcode_doc = try bbcodez.loadFromBuffer(allocator, input, .{}); + const bbcode_doc = try bbcodez.loadFromBuffer(allocator, input, .{ + .verbatim_tags = &.{ "code", "codeblock", "codeblocks" }, + .tokenizer_options = .{ .equals_required_in_parameters = false }, + .parser_options = .{ .is_self_closing_fn = &isGodotSelfClosing }, + }); defer bbcode_doc.deinit(); - try bbcodez.fmt.md.renderDocument(allocator, bbcode_doc, &output.writer, .{}); + try bbcodez.fmt.md.renderDocument(allocator, bbcode_doc, &output.writer, .{ + .write_element_fn = &writeGodotElement, + }); return try output.toOwnedSlice(); } +/// Known Godot cross-reference tag names (self-closing, space-separated value). +const godot_cross_ref_tags = [_][]const u8{ + "member", + "method", + "param", + "constant", + "signal", + "enum", + "annotation", + "theme_item", +}; + +fn isGodotCrossRefTag(name: []const u8) bool { + for (&godot_cross_ref_tags) |tag| { + if (std.mem.eql(u8, name, tag)) return true; + } + return false; +} + +/// Returns true if a tag is a bare Godot class reference like [Node2D] or [@GDScript]. +fn isBareClassRef(name: []const u8) bool { + if (name.len < 2) return false; + if (std.ascii.isUpper(name[0])) return true; + if (name[0] == '@' and name.len > 1 and std.ascii.isUpper(name[1])) return true; + return false; +} + +/// Parser callback: marks Godot cross-ref tags and bare class refs as self-closing. +fn isGodotSelfClosing(_: ?*anyopaque, token: bbcodez.tokenizer.TokenResult.Token) bool { + return isGodotCrossRefTag(token.name) or isBareClassRef(token.name); +} + +/// Renderer callback: converts Godot-specific elements to markdown. +fn writeGodotElement(node: bbcodez.Node, ctx_ptr: ?*const anyopaque) anyerror!bool { + if (node.type != .element) return false; + + const ctx: *const bbcodez.fmt.md.WriteContext = @ptrCast(@alignCast(ctx_ptr)); + const name = try node.getName(); + + // Cross-reference tags: [member X], [method X], [param X], etc. + if (isGodotCrossRefTag(name)) { + const value = (try node.getValue()) orelse return false; + if (std.mem.eql(u8, name, "method")) { + try ctx.writer.print("`{s}()`", .{value}); + } else if (std.mem.eql(u8, name, "param")) { + try ctx.writer.print("*{s}*", .{value}); + } else { + try ctx.writer.print("`{s}`", .{value}); + } + return true; + } + + // Code blocks: [codeblock]...[/codeblock] + if (std.mem.eql(u8, name, "codeblock")) { + const content = getVerbatimContent(node); + try writeFencedCode(ctx.writer, content, null); + return true; + } + + // Multi-language code blocks: [codeblocks][gdscript]...[/gdscript][csharp]...[/csharp][/codeblocks] + if (std.mem.eql(u8, name, "codeblocks")) { + const content = getVerbatimContent(node); + try writeCodeblocks(ctx.writer, content); + return true; + } + + // Bare class references: [Node2D], [Transform2D], [@GDScript] + if (node.type == .element and isBareClassRef(name)) { + try ctx.writer.print("`{s}`", .{name}); + return true; + } + + return false; +} + +/// Extracts the text content from a verbatim element's children. +fn getVerbatimContent(node: bbcodez.Node) []const u8 { + if (node.type != .element) return ""; + for (node.children.items) |child| { + if (child.type == .text) { + return child.value.text; + } + } + return ""; +} + +/// Extracts language blocks from [codeblocks] verbatim content and writes fenced code. +fn writeCodeblocks(writer: *std.Io.Writer, content: []const u8) !void { + const lang_tags = [_][]const u8{ "gdscript", "csharp" }; + var wrote_any = false; + + for (&lang_tags) |lang| { + if (findTagContent(content, lang)) |span| { + const trimmed = std.mem.trim(u8, span, " \t\n\r"); + if (trimmed.len > 0) { + if (wrote_any) try writer.writeAll("\n"); + try writeFencedCode(writer, span, lang); + wrote_any = true; + } + } + } + + if (!wrote_any) { + // Fallback: emit raw content as plain code block + try writeFencedCode(writer, content, null); + } +} + +/// Finds content between [tag]...[/tag] in a string. +fn findTagContent(content: []const u8, tag: []const u8) ?[]const u8 { + // Build "[tag]" pattern + var open_buf: [64]u8 = undefined; + const open_tag = std.fmt.bufPrint(&open_buf, "[{s}]", .{tag}) catch return null; + + // Build "[/tag]" pattern + var close_buf: [64]u8 = undefined; + const close_tag = std.fmt.bufPrint(&close_buf, "[/{s}]", .{tag}) catch return null; + + const open_pos = std.mem.indexOf(u8, content, open_tag) orelse return null; + const content_start = open_pos + open_tag.len; + const close_pos = std.mem.indexOfPos(u8, content, content_start, close_tag) orelse return null; + + return content[content_start..close_pos]; +} + +/// Writes content as a fenced code block, stripping common leading indentation. +fn writeFencedCode(writer: *std.Io.Writer, raw: []const u8, lang: ?[]const u8) !void { + // Strip only leading/trailing newlines, preserving indentation structure + const trimmed = std.mem.trim(u8, raw, "\n\r"); + if (std.mem.trim(u8, trimmed, " \t").len == 0) return; + + // Find minimum indentation across non-empty lines + var min_indent: usize = std.math.maxInt(usize); + var line_iter = std.mem.splitScalar(u8, trimmed, '\n'); + while (line_iter.next()) |line| { + const stripped = std.mem.trimLeft(u8, line, " \t"); + if (stripped.len == 0) continue; + min_indent = @min(min_indent, line.len - stripped.len); + } + if (min_indent == std.math.maxInt(usize)) min_indent = 0; + + // Write fenced block with dedented lines + if (lang) |l| { + try writer.print("\n```{s}\n", .{l}); + } else { + try writer.writeAll("\n```\n"); + } + + // Count non-trailing-blank lines + var line_count: usize = 0; + var count_iter = std.mem.splitScalar(u8, trimmed, '\n'); + while (count_iter.next()) |_| line_count += 1; + // Find last non-blank line + var last_content_line: usize = 0; + var idx: usize = 0; + var scan_iter = std.mem.splitScalar(u8, trimmed, '\n'); + while (scan_iter.next()) |line| : (idx += 1) { + if (std.mem.trimLeft(u8, line, " \t").len > 0) last_content_line = idx; + } + + line_iter = std.mem.splitScalar(u8, trimmed, '\n'); + var line_idx: usize = 0; + var first = true; + while (line_iter.next()) |line| : (line_idx += 1) { + if (line_idx > last_content_line) break; + if (!first) try writer.writeAll("\n"); + first = false; + if (line.len > min_indent) { + try writer.writeAll(line[min_indent..]); + } + } + + try writer.writeAll("\n```\n"); +} + pub fn lookupSymbolExact(self: DocDatabase, symbol: []const u8) DocDatabase.Error!Entry { return self.symbols.get(symbol) orelse return DocDatabase.Error.SymbolNotFound; } @@ -1171,6 +1352,140 @@ test "lookupSymbolExact returns SymbolNotFound for missing symbol in XML databas try std.testing.expectError(DocDatabase.Error.SymbolNotFound, result); } +// --- Godot BBCode conversion tests --- + +test "bbcodeToMarkdown converts [member] to backtick-wrapped name" { + const allocator = std.testing.allocator; + const result = try bbcodeToMarkdown(allocator, "[member position]"); + defer allocator.free(result); + try std.testing.expectEqualStrings("`position`", result); +} + +test "bbcodeToMarkdown converts [method] to backtick-wrapped name with parens" { + const allocator = std.testing.allocator; + const result = try bbcodeToMarkdown(allocator, "[method look_at]"); + defer allocator.free(result); + try std.testing.expectEqualStrings("`look_at()`", result); +} + +test "bbcodeToMarkdown converts [param] to italic" { + const allocator = std.testing.allocator; + const result = try bbcodeToMarkdown(allocator, "[param delta]"); + defer allocator.free(result); + try std.testing.expectEqualStrings("*delta*", result); +} + +test "bbcodeToMarkdown converts [constant] to backtick-wrapped" { + const allocator = std.testing.allocator; + const result = try bbcodeToMarkdown(allocator, "[constant NAN]"); + defer allocator.free(result); + try std.testing.expectEqualStrings("`NAN`", result); +} + +test "bbcodeToMarkdown converts [signal] with dotted name" { + const allocator = std.testing.allocator; + const result = try bbcodeToMarkdown(allocator, "[signal Animation.finished]"); + defer allocator.free(result); + try std.testing.expectEqualStrings("`Animation.finished`", result); +} + +test "bbcodeToMarkdown converts [enum] to backtick-wrapped" { + const allocator = std.testing.allocator; + const result = try bbcodeToMarkdown(allocator, "[enum LoopMode]"); + defer allocator.free(result); + try std.testing.expectEqualStrings("`LoopMode`", result); +} + +test "bbcodeToMarkdown converts [annotation] to backtick-wrapped" { + const allocator = std.testing.allocator; + const result = try bbcodeToMarkdown(allocator, "[annotation @export]"); + defer allocator.free(result); + try std.testing.expectEqualStrings("`@export`", result); +} + +test "bbcodeToMarkdown converts [theme_item] to backtick-wrapped" { + const allocator = std.testing.allocator; + const result = try bbcodeToMarkdown(allocator, "[theme_item color]"); + defer allocator.free(result); + try std.testing.expectEqualStrings("`color`", result); +} + +test "bbcodeToMarkdown converts bare class ref [Node2D] to backtick-wrapped" { + const allocator = std.testing.allocator; + const result = try bbcodeToMarkdown(allocator, "[Node2D]"); + defer allocator.free(result); + try std.testing.expectEqualStrings("`Node2D`", result); +} + +test "bbcodeToMarkdown converts [@GDScript] bare ref" { + const allocator = std.testing.allocator; + const result = try bbcodeToMarkdown(allocator, "[@GDScript]"); + defer allocator.free(result); + try std.testing.expectEqualStrings("`@GDScript`", result); +} + +test "bbcodeToMarkdown handles mixed Godot tags and standard BBCode" { + const allocator = std.testing.allocator; + const result = try bbcodeToMarkdown(allocator, "See [member x] and [b]bold[/b] text"); + defer allocator.free(result); + try std.testing.expectEqualStrings("See `x` and **bold** text", result); +} + +test "bbcodeToMarkdown preserves standard BBCode" { + const allocator = std.testing.allocator; + const result = try bbcodeToMarkdown(allocator, "[b]bold[/b] and [i]italic[/i]"); + defer allocator.free(result); + try std.testing.expectEqualStrings("**bold** and *italic*", result); +} + +test "bbcodeToMarkdown converts [codeblock] to fenced code" { + const allocator = std.testing.allocator; + const result = try bbcodeToMarkdown(allocator, "[codeblock]print(\"hi\")[/codeblock]"); + defer allocator.free(result); + try std.testing.expectEqualStrings("\n```\nprint(\"hi\")\n```\n", result); +} + +test "bbcodeToMarkdown converts [codeblocks] with gdscript and csharp" { + const allocator = std.testing.allocator; + const input = "[codeblocks][gdscript]print(\"hi\")[/gdscript][csharp]Console.Write(\"hi\")[/csharp][/codeblocks]"; + const result = try bbcodeToMarkdown(allocator, input); + defer allocator.free(result); + // Should contain both language blocks + try std.testing.expect(std.mem.indexOf(u8, result, "```gdscript") != null); + try std.testing.expect(std.mem.indexOf(u8, result, "print(\"hi\")") != null); + try std.testing.expect(std.mem.indexOf(u8, result, "```csharp") != null); + try std.testing.expect(std.mem.indexOf(u8, result, "Console.Write(\"hi\")") != null); +} + +test "bbcodeToMarkdown converts Godot tags within loadFromXmlDir" { + const allocator = std.testing.allocator; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + const tmp_path = try tmp_dir.dir.realpathAlloc(allocator, "."); + defer allocator.free(tmp_path); + + const xml_content = + \\ + \\ + \\ See [member position] and [method look_at]. + \\ Uses [param delta] with [Node2D] class. + \\ + ; + try tmp_dir.dir.writeFile(.{ .sub_path = "TestGodotTags.xml", .data = xml_content }); + + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + const db = try DocDatabase.loadFromXmlDir(arena.allocator(), allocator, tmp_path); + const entry = db.symbols.get("TestGodotTags").?; + + // Cross-refs should be converted + try std.testing.expect(std.mem.indexOf(u8, entry.brief_description.?, "`position`") != null); + try std.testing.expect(std.mem.indexOf(u8, entry.brief_description.?, "`look_at()`") != null); + try std.testing.expect(std.mem.indexOf(u8, entry.description.?, "*delta*") != null); + try std.testing.expect(std.mem.indexOf(u8, entry.description.?, "`Node2D`") != null); +} + const std = @import("std"); const ArenaAllocator = std.heap.ArenaAllocator; const Allocator = std.mem.Allocator; From d6b646571156cdc6b01881e83d19fdc4f2d8dafc Mon Sep 17 00:00:00 2001 From: Simon Hartcher Date: Sat, 21 Mar 2026 22:37:12 +1100 Subject: [PATCH 2/2] test: add snapshot tests for Godot BBCode conversion Snapshot tests covering cross-reference tags, bare class refs, mixed Godot + standard BBCode, codeblocks, multi-language codeblocks, and full XML-to-markdown roundtrip. --- snapshots/godot_bare_class_refs.md | 3 + snapshots/godot_codeblock.md | 11 ++ snapshots/godot_codeblocks_multilang.md | 20 +++ snapshots/godot_cross_refs.md | 7 + snapshots/godot_mixed_with_bbcode.md | 5 + snapshots/godot_xml_roundtrip_class.md | 13 ++ snapshots/godot_xml_roundtrip_method.md | 7 + src/DocDatabase.zig | 171 +++++++++++------------- 8 files changed, 147 insertions(+), 90 deletions(-) create mode 100644 snapshots/godot_bare_class_refs.md create mode 100644 snapshots/godot_codeblock.md create mode 100644 snapshots/godot_codeblocks_multilang.md create mode 100644 snapshots/godot_cross_refs.md create mode 100644 snapshots/godot_mixed_with_bbcode.md create mode 100644 snapshots/godot_xml_roundtrip_class.md create mode 100644 snapshots/godot_xml_roundtrip_method.md diff --git a/snapshots/godot_bare_class_refs.md b/snapshots/godot_bare_class_refs.md new file mode 100644 index 0000000..9490dfb --- /dev/null +++ b/snapshots/godot_bare_class_refs.md @@ -0,0 +1,3 @@ +Both `Node2D` and `Control` inherit from `CanvasItem`. + +See also `@GDScript` and `Transform2D`. \ No newline at end of file diff --git a/snapshots/godot_codeblock.md b/snapshots/godot_codeblock.md new file mode 100644 index 0000000..64c02d6 --- /dev/null +++ b/snapshots/godot_codeblock.md @@ -0,0 +1,11 @@ +Example usage: + + +``` +for i in range(10): + var node = get_child(i) + node.queue_free() +``` + + +This frees all children. \ No newline at end of file diff --git a/snapshots/godot_codeblocks_multilang.md b/snapshots/godot_codeblocks_multilang.md new file mode 100644 index 0000000..2b4fd8a --- /dev/null +++ b/snapshots/godot_codeblocks_multilang.md @@ -0,0 +1,20 @@ +Iterate through collisions: + + +```gdscript +for i in get_slide_collision_count(): + var collision = get_slide_collision(i) + print(collision.get_collider().name) +``` + + +```csharp +for (int i = 0; i < GetSlideCollisionCount(); i++) +{ + var collision = GetSlideCollision(i); + GD.Print(collision.GetCollider().Name); +} +``` + + +See `move_and_slide()`. \ No newline at end of file diff --git a/snapshots/godot_cross_refs.md b/snapshots/godot_cross_refs.md new file mode 100644 index 0000000..a6761ac --- /dev/null +++ b/snapshots/godot_cross_refs.md @@ -0,0 +1,7 @@ +See `position` and `look_at()`. + +Uses *delta* with `NAN`. + +Emits `finished` for `LoopMode`. + +Apply `@export` to `color`. \ No newline at end of file diff --git a/snapshots/godot_mixed_with_bbcode.md b/snapshots/godot_mixed_with_bbcode.md new file mode 100644 index 0000000..8f573b1 --- /dev/null +++ b/snapshots/godot_mixed_with_bbcode.md @@ -0,0 +1,5 @@ +**Note:** The `position` is relative to the parent. + +Use `get_node()` or `get_child()` to access children. + +See *also* [the docs](https://docs.godotengine.org). \ No newline at end of file diff --git a/snapshots/godot_xml_roundtrip_class.md b/snapshots/godot_xml_roundtrip_class.md new file mode 100644 index 0000000..793c616 --- /dev/null +++ b/snapshots/godot_xml_roundtrip_class.md @@ -0,0 +1,13 @@ +# TestGodotTags + +*Inherits: Node2D* + +See `position` and `look_at()`. + +## Description + +Uses *delta* with `Node2D` class. See `NOTIFICATION_READY`. + +## Methods + +- **do_thing(speed: float)** - Applies *speed* using `move_and_slide()`. Returns `OK` on success. diff --git a/snapshots/godot_xml_roundtrip_method.md b/snapshots/godot_xml_roundtrip_method.md new file mode 100644 index 0000000..095806f --- /dev/null +++ b/snapshots/godot_xml_roundtrip_method.md @@ -0,0 +1,7 @@ +# TestGodotTags.do_thing(speed: float) + +**Parent**: TestGodotTags + +## Description + +Applies *speed* using `move_and_slide()`. Returns `OK` on success. diff --git a/src/DocDatabase.zig b/src/DocDatabase.zig index a589b43..c87c185 100644 --- a/src/DocDatabase.zig +++ b/src/DocDatabase.zig @@ -1352,112 +1352,91 @@ test "lookupSymbolExact returns SymbolNotFound for missing symbol in XML databas try std.testing.expectError(DocDatabase.Error.SymbolNotFound, result); } -// --- Godot BBCode conversion tests --- +// --- Godot BBCode conversion snapshot tests --- -test "bbcodeToMarkdown converts [member] to backtick-wrapped name" { - const allocator = std.testing.allocator; - const result = try bbcodeToMarkdown(allocator, "[member position]"); - defer allocator.free(result); - try std.testing.expectEqualStrings("`position`", result); -} - -test "bbcodeToMarkdown converts [method] to backtick-wrapped name with parens" { - const allocator = std.testing.allocator; - const result = try bbcodeToMarkdown(allocator, "[method look_at]"); - defer allocator.free(result); - try std.testing.expectEqualStrings("`look_at()`", result); -} - -test "bbcodeToMarkdown converts [param] to italic" { - const allocator = std.testing.allocator; - const result = try bbcodeToMarkdown(allocator, "[param delta]"); - defer allocator.free(result); - try std.testing.expectEqualStrings("*delta*", result); -} - -test "bbcodeToMarkdown converts [constant] to backtick-wrapped" { - const allocator = std.testing.allocator; - const result = try bbcodeToMarkdown(allocator, "[constant NAN]"); - defer allocator.free(result); - try std.testing.expectEqualStrings("`NAN`", result); -} - -test "bbcodeToMarkdown converts [signal] with dotted name" { - const allocator = std.testing.allocator; - const result = try bbcodeToMarkdown(allocator, "[signal Animation.finished]"); - defer allocator.free(result); - try std.testing.expectEqualStrings("`Animation.finished`", result); -} - -test "bbcodeToMarkdown converts [enum] to backtick-wrapped" { - const allocator = std.testing.allocator; - const result = try bbcodeToMarkdown(allocator, "[enum LoopMode]"); - defer allocator.free(result); - try std.testing.expectEqualStrings("`LoopMode`", result); -} - -test "bbcodeToMarkdown converts [annotation] to backtick-wrapped" { - const allocator = std.testing.allocator; - const result = try bbcodeToMarkdown(allocator, "[annotation @export]"); - defer allocator.free(result); - try std.testing.expectEqualStrings("`@export`", result); -} - -test "bbcodeToMarkdown converts [theme_item] to backtick-wrapped" { - const allocator = std.testing.allocator; - const result = try bbcodeToMarkdown(allocator, "[theme_item color]"); - defer allocator.free(result); - try std.testing.expectEqualStrings("`color`", result); -} +fn writeSnapshot(content: []const u8, path: []const u8) !void { + var file = try std.fs.cwd().createFile(path, .{}); + defer file.close(); -test "bbcodeToMarkdown converts bare class ref [Node2D] to backtick-wrapped" { - const allocator = std.testing.allocator; - const result = try bbcodeToMarkdown(allocator, "[Node2D]"); - defer allocator.free(result); - try std.testing.expectEqualStrings("`Node2D`", result); + var buf: [4096]u8 = undefined; + var file_writer = file.writer(&buf); + try file_writer.interface.writeAll(content); + try file_writer.interface.flush(); } -test "bbcodeToMarkdown converts [@GDScript] bare ref" { +test "snapshot: cross-reference tags" { const allocator = std.testing.allocator; - const result = try bbcodeToMarkdown(allocator, "[@GDScript]"); + const result = try bbcodeToMarkdown(allocator, + \\See [member position] and [method look_at]. + \\Uses [param delta] with [constant NAN]. + \\Emits [signal finished] for [enum LoopMode]. + \\Apply [annotation @export] to [theme_item color]. + ); defer allocator.free(result); - try std.testing.expectEqualStrings("`@GDScript`", result); + try writeSnapshot(result, "snapshots/godot_cross_refs.md"); } -test "bbcodeToMarkdown handles mixed Godot tags and standard BBCode" { +test "snapshot: bare class references" { const allocator = std.testing.allocator; - const result = try bbcodeToMarkdown(allocator, "See [member x] and [b]bold[/b] text"); + const result = try bbcodeToMarkdown(allocator, + \\Both [Node2D] and [Control] inherit from [CanvasItem]. + \\See also [@GDScript] and [Transform2D]. + ); defer allocator.free(result); - try std.testing.expectEqualStrings("See `x` and **bold** text", result); + try writeSnapshot(result, "snapshots/godot_bare_class_refs.md"); } -test "bbcodeToMarkdown preserves standard BBCode" { +test "snapshot: mixed Godot tags with standard BBCode" { const allocator = std.testing.allocator; - const result = try bbcodeToMarkdown(allocator, "[b]bold[/b] and [i]italic[/i]"); + const result = try bbcodeToMarkdown(allocator, + \\[b]Note:[/b] The [member position] is relative to the parent. + \\Use [code]get_node()[/code] or [method get_child] to access children. + \\See [i]also[/i] [url=https://docs.godotengine.org]the docs[/url]. + ); defer allocator.free(result); - try std.testing.expectEqualStrings("**bold** and *italic*", result); + try writeSnapshot(result, "snapshots/godot_mixed_with_bbcode.md"); } -test "bbcodeToMarkdown converts [codeblock] to fenced code" { +test "snapshot: codeblock with indented content" { const allocator = std.testing.allocator; - const result = try bbcodeToMarkdown(allocator, "[codeblock]print(\"hi\")[/codeblock]"); + const result = try bbcodeToMarkdown(allocator, + \\Example usage: + \\[codeblock] + \\for i in range(10): + \\ var node = get_child(i) + \\ node.queue_free() + \\[/codeblock] + \\This frees all children. + ); defer allocator.free(result); - try std.testing.expectEqualStrings("\n```\nprint(\"hi\")\n```\n", result); + try writeSnapshot(result, "snapshots/godot_codeblock.md"); } -test "bbcodeToMarkdown converts [codeblocks] with gdscript and csharp" { +test "snapshot: codeblocks with gdscript and csharp" { const allocator = std.testing.allocator; - const input = "[codeblocks][gdscript]print(\"hi\")[/gdscript][csharp]Console.Write(\"hi\")[/csharp][/codeblocks]"; - const result = try bbcodeToMarkdown(allocator, input); + const result = try bbcodeToMarkdown(allocator, + \\Iterate through collisions: + \\[codeblocks] + \\[gdscript] + \\for i in get_slide_collision_count(): + \\ var collision = get_slide_collision(i) + \\ print(collision.get_collider().name) + \\[/gdscript] + \\[csharp] + \\for (int i = 0; i < GetSlideCollisionCount(); i++) + \\{ + \\ var collision = GetSlideCollision(i); + \\ GD.Print(collision.GetCollider().Name); + \\} + \\[/csharp] + \\[/codeblocks] + \\See [method move_and_slide]. + ); defer allocator.free(result); - // Should contain both language blocks - try std.testing.expect(std.mem.indexOf(u8, result, "```gdscript") != null); - try std.testing.expect(std.mem.indexOf(u8, result, "print(\"hi\")") != null); - try std.testing.expect(std.mem.indexOf(u8, result, "```csharp") != null); - try std.testing.expect(std.mem.indexOf(u8, result, "Console.Write(\"hi\")") != null); + try writeSnapshot(result, "snapshots/godot_codeblocks_multilang.md"); } -test "bbcodeToMarkdown converts Godot tags within loadFromXmlDir" { +test "snapshot: XML roundtrip with Godot tags" { const allocator = std.testing.allocator; var tmp_dir = std.testing.tmpDir(.{}); @@ -1467,9 +1446,16 @@ test "bbcodeToMarkdown converts Godot tags within loadFromXmlDir" { const xml_content = \\ - \\ + \\ \\ See [member position] and [method look_at]. - \\ Uses [param delta] with [Node2D] class. + \\ Uses [param delta] with [Node2D] class. See [constant NOTIFICATION_READY]. + \\ + \\ + \\ + \\ + \\ Applies [param speed] using [method move_and_slide]. Returns [constant OK] on success. + \\ + \\ \\ ; try tmp_dir.dir.writeFile(.{ .sub_path = "TestGodotTags.xml", .data = xml_content }); @@ -1477,13 +1463,18 @@ test "bbcodeToMarkdown converts Godot tags within loadFromXmlDir" { var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); const db = try DocDatabase.loadFromXmlDir(arena.allocator(), allocator, tmp_path); - const entry = db.symbols.get("TestGodotTags").?; - // Cross-refs should be converted - try std.testing.expect(std.mem.indexOf(u8, entry.brief_description.?, "`position`") != null); - try std.testing.expect(std.mem.indexOf(u8, entry.brief_description.?, "`look_at()`") != null); - try std.testing.expect(std.mem.indexOf(u8, entry.description.?, "*delta*") != null); - try std.testing.expect(std.mem.indexOf(u8, entry.description.?, "`Node2D`") != null); + // Write class index snapshot + var allocating: Writer.Allocating = .init(allocator); + defer allocating.deinit(); + try db.generateMarkdownForSymbol(allocator, "TestGodotTags", &allocating.writer); + try writeSnapshot(allocating.written(), "snapshots/godot_xml_roundtrip_class.md"); + + // Write method snapshot + var allocating2: Writer.Allocating = .init(allocator); + defer allocating2.deinit(); + try db.generateMarkdownForSymbol(allocator, "TestGodotTags.do_thing", &allocating2.writer); + try writeSnapshot(allocating2.written(), "snapshots/godot_xml_roundtrip_method.md"); } const std = @import("std");