konpeito 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. checksums.yaml +7 -0
  2. data/.ruby-version +1 -0
  3. data/CHANGELOG.md +75 -0
  4. data/CONTRIBUTING.md +123 -0
  5. data/LICENSE +21 -0
  6. data/README.md +257 -0
  7. data/Rakefile +11 -0
  8. data/bin/konpeito +6 -0
  9. data/konpeito.gemspec +43 -0
  10. data/lib/konpeito/ast/typed_ast.rb +620 -0
  11. data/lib/konpeito/ast/visitor.rb +78 -0
  12. data/lib/konpeito/cache/cache_manager.rb +230 -0
  13. data/lib/konpeito/cache/dependency_graph.rb +192 -0
  14. data/lib/konpeito/cache.rb +8 -0
  15. data/lib/konpeito/cli/base_command.rb +187 -0
  16. data/lib/konpeito/cli/build_command.rb +220 -0
  17. data/lib/konpeito/cli/check_command.rb +104 -0
  18. data/lib/konpeito/cli/config.rb +231 -0
  19. data/lib/konpeito/cli/deps_command.rb +128 -0
  20. data/lib/konpeito/cli/doctor_command.rb +340 -0
  21. data/lib/konpeito/cli/fmt_command.rb +199 -0
  22. data/lib/konpeito/cli/init_command.rb +312 -0
  23. data/lib/konpeito/cli/lsp_command.rb +40 -0
  24. data/lib/konpeito/cli/run_command.rb +150 -0
  25. data/lib/konpeito/cli/test_command.rb +248 -0
  26. data/lib/konpeito/cli/watch_command.rb +212 -0
  27. data/lib/konpeito/cli.rb +301 -0
  28. data/lib/konpeito/codegen/builtin_methods.rb +229 -0
  29. data/lib/konpeito/codegen/cruby_backend.rb +1090 -0
  30. data/lib/konpeito/codegen/debug_info.rb +352 -0
  31. data/lib/konpeito/codegen/inliner.rb +486 -0
  32. data/lib/konpeito/codegen/jvm_backend.rb +197 -0
  33. data/lib/konpeito/codegen/jvm_generator.rb +13412 -0
  34. data/lib/konpeito/codegen/llvm_generator.rb +13191 -0
  35. data/lib/konpeito/codegen/loop_optimizer.rb +363 -0
  36. data/lib/konpeito/codegen/monomorphizer.rb +359 -0
  37. data/lib/konpeito/codegen/profile_runtime.c +341 -0
  38. data/lib/konpeito/codegen/profiler.rb +99 -0
  39. data/lib/konpeito/compiler.rb +592 -0
  40. data/lib/konpeito/dependency_resolver.rb +296 -0
  41. data/lib/konpeito/diagnostics/collector.rb +127 -0
  42. data/lib/konpeito/diagnostics/diagnostic.rb +237 -0
  43. data/lib/konpeito/diagnostics/renderer.rb +144 -0
  44. data/lib/konpeito/formatter/formatter.rb +1214 -0
  45. data/lib/konpeito/hir/builder.rb +7167 -0
  46. data/lib/konpeito/hir/nodes.rb +2465 -0
  47. data/lib/konpeito/lsp/document_manager.rb +820 -0
  48. data/lib/konpeito/lsp/server.rb +183 -0
  49. data/lib/konpeito/lsp/transport.rb +38 -0
  50. data/lib/konpeito/parser/prism_adapter.rb +65 -0
  51. data/lib/konpeito/platform.rb +103 -0
  52. data/lib/konpeito/profile/report.rb +136 -0
  53. data/lib/konpeito/rbs_inline/preprocessor.rb +199 -0
  54. data/lib/konpeito/stdlib/compression/compression.rb +72 -0
  55. data/lib/konpeito/stdlib/compression/compression.rbs +60 -0
  56. data/lib/konpeito/stdlib/compression/compression_native.c +415 -0
  57. data/lib/konpeito/stdlib/compression/extconf.rb +19 -0
  58. data/lib/konpeito/stdlib/crypto/crypto.rb +85 -0
  59. data/lib/konpeito/stdlib/crypto/crypto.rbs +74 -0
  60. data/lib/konpeito/stdlib/crypto/crypto_native.c +312 -0
  61. data/lib/konpeito/stdlib/crypto/extconf.rb +40 -0
  62. data/lib/konpeito/stdlib/http/extconf.rb +19 -0
  63. data/lib/konpeito/stdlib/http/http.rb +125 -0
  64. data/lib/konpeito/stdlib/http/http.rbs +57 -0
  65. data/lib/konpeito/stdlib/http/http_native.c +440 -0
  66. data/lib/konpeito/stdlib/json/extconf.rb +17 -0
  67. data/lib/konpeito/stdlib/json/json.rb +44 -0
  68. data/lib/konpeito/stdlib/json/json.rbs +33 -0
  69. data/lib/konpeito/stdlib/json/json_native.c +286 -0
  70. data/lib/konpeito/stdlib/ui/extconf.rb +216 -0
  71. data/lib/konpeito/stdlib/ui/konpeito_ui_native.cpp +1625 -0
  72. data/lib/konpeito/stdlib/ui/konpeito_ui_native.h +162 -0
  73. data/lib/konpeito/stdlib/ui/ui.rb +318 -0
  74. data/lib/konpeito/stdlib/ui/ui.rbs +247 -0
  75. data/lib/konpeito/type_checker/annotation_parser.rb +67 -0
  76. data/lib/konpeito/type_checker/hm_inferrer.rb +2565 -0
  77. data/lib/konpeito/type_checker/inferrer.rb +565 -0
  78. data/lib/konpeito/type_checker/rbs_loader.rb +1621 -0
  79. data/lib/konpeito/type_checker/type_resolver.rb +276 -0
  80. data/lib/konpeito/type_checker/types.rb +1434 -0
  81. data/lib/konpeito/type_checker/unification.rb +323 -0
  82. data/lib/konpeito/ui/animation/animated_state.rb +80 -0
  83. data/lib/konpeito/ui/animation/easing.rb +59 -0
  84. data/lib/konpeito/ui/animation/value_tween.rb +66 -0
  85. data/lib/konpeito/ui/app.rb +379 -0
  86. data/lib/konpeito/ui/box.rb +38 -0
  87. data/lib/konpeito/ui/castella.rb +70 -0
  88. data/lib/konpeito/ui/castella_native.rb +76 -0
  89. data/lib/konpeito/ui/chart/area_chart.rb +305 -0
  90. data/lib/konpeito/ui/chart/bar_chart.rb +288 -0
  91. data/lib/konpeito/ui/chart/base_chart.rb +210 -0
  92. data/lib/konpeito/ui/chart/chart_helpers.rb +79 -0
  93. data/lib/konpeito/ui/chart/gauge_chart.rb +171 -0
  94. data/lib/konpeito/ui/chart/heatmap_chart.rb +222 -0
  95. data/lib/konpeito/ui/chart/line_chart.rb +289 -0
  96. data/lib/konpeito/ui/chart/pie_chart.rb +219 -0
  97. data/lib/konpeito/ui/chart/scales.rb +77 -0
  98. data/lib/konpeito/ui/chart/scatter_chart.rb +303 -0
  99. data/lib/konpeito/ui/chart/stacked_bar_chart.rb +276 -0
  100. data/lib/konpeito/ui/column.rb +271 -0
  101. data/lib/konpeito/ui/core.rb +2199 -0
  102. data/lib/konpeito/ui/dsl.rb +443 -0
  103. data/lib/konpeito/ui/frame.rb +171 -0
  104. data/lib/konpeito/ui/frame_native.rb +494 -0
  105. data/lib/konpeito/ui/markdown/ast.rb +124 -0
  106. data/lib/konpeito/ui/markdown/mermaid/layout.rb +387 -0
  107. data/lib/konpeito/ui/markdown/mermaid/models.rb +232 -0
  108. data/lib/konpeito/ui/markdown/mermaid/parser.rb +519 -0
  109. data/lib/konpeito/ui/markdown/mermaid/renderer.rb +336 -0
  110. data/lib/konpeito/ui/markdown/parser.rb +805 -0
  111. data/lib/konpeito/ui/markdown/renderer.rb +639 -0
  112. data/lib/konpeito/ui/markdown/theme.rb +165 -0
  113. data/lib/konpeito/ui/render_node.rb +260 -0
  114. data/lib/konpeito/ui/row.rb +207 -0
  115. data/lib/konpeito/ui/spacer.rb +18 -0
  116. data/lib/konpeito/ui/style.rb +799 -0
  117. data/lib/konpeito/ui/theme.rb +563 -0
  118. data/lib/konpeito/ui/themes/material.rb +35 -0
  119. data/lib/konpeito/ui/themes/tokyo_night.rb +6 -0
  120. data/lib/konpeito/ui/widgets/button.rb +103 -0
  121. data/lib/konpeito/ui/widgets/calendar.rb +1034 -0
  122. data/lib/konpeito/ui/widgets/checkbox.rb +119 -0
  123. data/lib/konpeito/ui/widgets/container.rb +91 -0
  124. data/lib/konpeito/ui/widgets/data_table.rb +667 -0
  125. data/lib/konpeito/ui/widgets/divider.rb +29 -0
  126. data/lib/konpeito/ui/widgets/image.rb +105 -0
  127. data/lib/konpeito/ui/widgets/input.rb +485 -0
  128. data/lib/konpeito/ui/widgets/markdown.rb +57 -0
  129. data/lib/konpeito/ui/widgets/modal.rb +163 -0
  130. data/lib/konpeito/ui/widgets/multiline_input.rb +968 -0
  131. data/lib/konpeito/ui/widgets/multiline_text.rb +180 -0
  132. data/lib/konpeito/ui/widgets/net_image.rb +100 -0
  133. data/lib/konpeito/ui/widgets/progress_bar.rb +70 -0
  134. data/lib/konpeito/ui/widgets/radio_buttons.rb +93 -0
  135. data/lib/konpeito/ui/widgets/slider.rb +133 -0
  136. data/lib/konpeito/ui/widgets/switch.rb +84 -0
  137. data/lib/konpeito/ui/widgets/tabs.rb +157 -0
  138. data/lib/konpeito/ui/widgets/text.rb +110 -0
  139. data/lib/konpeito/ui/widgets/tree.rb +426 -0
  140. data/lib/konpeito/version.rb +5 -0
  141. data/lib/konpeito.rb +109 -0
  142. data/test_native_array.rb +172 -0
  143. data/test_native_array_class.rb +197 -0
  144. data/test_native_class.rb +151 -0
  145. data/tools/konpeito-asm/build.sh +65 -0
  146. data/tools/konpeito-asm/lib/asm-9.7.1.jar +0 -0
  147. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KArray.class +0 -0
  148. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KCompression.class +0 -0
  149. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KConditionVariable.class +0 -0
  150. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KCrypto.class +0 -0
  151. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KFile.class +0 -0
  152. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KHTTP.class +0 -0
  153. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KHash.class +0 -0
  154. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KJSON$Parser.class +0 -0
  155. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KJSON.class +0 -0
  156. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KMath.class +0 -0
  157. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KRactor.class +0 -0
  158. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KRactorPort.class +0 -0
  159. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KSizedQueue.class +0 -0
  160. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KThread.class +0 -0
  161. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KTime.class +0 -0
  162. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/RubyDispatch.class +0 -0
  163. data/tools/konpeito-asm/src/ClassIntrospector.java +312 -0
  164. data/tools/konpeito-asm/src/KonpeitoAssembler.java +659 -0
  165. data/tools/konpeito-asm/src/konpeito/runtime/KArray.java +390 -0
  166. data/tools/konpeito-asm/src/konpeito/runtime/KCompression.java +168 -0
  167. data/tools/konpeito-asm/src/konpeito/runtime/KConditionVariable.java +48 -0
  168. data/tools/konpeito-asm/src/konpeito/runtime/KCrypto.java +151 -0
  169. data/tools/konpeito-asm/src/konpeito/runtime/KFile.java +100 -0
  170. data/tools/konpeito-asm/src/konpeito/runtime/KHTTP.java +113 -0
  171. data/tools/konpeito-asm/src/konpeito/runtime/KHash.java +228 -0
  172. data/tools/konpeito-asm/src/konpeito/runtime/KJSON.java +405 -0
  173. data/tools/konpeito-asm/src/konpeito/runtime/KMath.java +54 -0
  174. data/tools/konpeito-asm/src/konpeito/runtime/KRactor.java +244 -0
  175. data/tools/konpeito-asm/src/konpeito/runtime/KRactorPort.java +53 -0
  176. data/tools/konpeito-asm/src/konpeito/runtime/KSizedQueue.java +49 -0
  177. data/tools/konpeito-asm/src/konpeito/runtime/KThread.java +49 -0
  178. data/tools/konpeito-asm/src/konpeito/runtime/KTime.java +53 -0
  179. data/tools/konpeito-asm/src/konpeito/runtime/RubyDispatch.java +416 -0
  180. metadata +267 -0
@@ -0,0 +1,639 @@
1
+ # Markdown renderer - cursor-based rendering using KUIRuntime painter API
2
+ # Walks the MdNode AST and emits drawing commands
3
+
4
+ class MarkdownRenderer
5
+ def initialize(theme)
6
+ @theme = theme
7
+ @cursor_x = 0.0
8
+ @cursor_y = 0.0
9
+ @list_depth = 0
10
+ @list_counters = []
11
+ @font_family = $theme.font_family
12
+ end
13
+
14
+ def render(painter, ast, width, padding)
15
+ @cursor_x = padding
16
+ @cursor_y = padding
17
+ @list_depth = 0
18
+ @list_counters = []
19
+ content_width = width - padding * 2.0
20
+ if content_width < 1.0
21
+ content_width = 1.0
22
+ end
23
+ i = 0
24
+ while i < ast.children.length
25
+ render_node(painter, ast.children[i], content_width)
26
+ i = i + 1
27
+ end
28
+ @cursor_y + padding
29
+ end
30
+
31
+ def measure_height(painter, ast, width, padding)
32
+ total = padding * 2.0
33
+ content_width = width - padding * 2.0
34
+ if content_width < 1.0
35
+ content_width = 1.0
36
+ end
37
+ i = 0
38
+ while i < ast.children.length
39
+ total = total + estimate_node_height(painter, ast.children[i], content_width)
40
+ i = i + 1
41
+ end
42
+ total
43
+ end
44
+
45
+ def render_node(painter, node, width)
46
+ t = node.type
47
+ if t == MD_HEADING
48
+ render_heading(painter, node, width)
49
+ elsif t == MD_PARAGRAPH
50
+ render_paragraph(painter, node, width)
51
+ elsif t == MD_CODE_BLOCK
52
+ render_code_block(painter, node, width)
53
+ elsif t == MD_BLOCKQUOTE
54
+ render_blockquote(painter, node, width)
55
+ elsif t == MD_LIST
56
+ render_list(painter, node, width)
57
+ elsif t == MD_TABLE
58
+ render_table(painter, node, width)
59
+ elsif t == MD_HORIZONTAL_RULE
60
+ render_hr(painter, width)
61
+ elsif t == MD_IMAGE
62
+ render_image(painter, node, width)
63
+ elsif t == MD_MERMAID
64
+ render_mermaid(painter, node, width)
65
+ end
66
+ end
67
+
68
+ # --- Heading ---
69
+ def render_heading(painter, node, width)
70
+ font_size = @theme.heading_size(node.level)
71
+ @cursor_y = @cursor_y + @theme.block_spacing
72
+
73
+ text = extract_text(node.children)
74
+ ascent = painter.get_text_ascent(@font_family, font_size)
75
+ color = @theme.heading_color
76
+
77
+ # Faux bold: draw twice with 0.5px offset
78
+ painter.draw_text(text, @cursor_x, @cursor_y + ascent, @font_family, font_size, color)
79
+ painter.draw_text(text, @cursor_x + 0.5, @cursor_y + ascent, @font_family, font_size, color)
80
+
81
+ @cursor_y = @cursor_y + font_size * 1.5
82
+ end
83
+
84
+ # --- Paragraph ---
85
+ def render_paragraph(painter, node, width)
86
+ segments = collect_segments(node.children, false, false, false, false, "")
87
+ font_size = @theme.base_font_size
88
+ line_height = font_size * 1.5
89
+ ascent = painter.get_text_ascent(@font_family, font_size)
90
+
91
+ indent = @list_depth * @theme.list_indent
92
+ x = @cursor_x + indent
93
+ available_width = width - indent
94
+ start_x = x
95
+
96
+ si = 0
97
+ while si < segments.length
98
+ seg = segments[si]
99
+ seg_text = seg[0]
100
+ seg_bold = seg[1]
101
+ seg_italic = seg[2]
102
+ seg_strike = seg[3]
103
+ seg_code = seg[4]
104
+ seg_href = seg[5]
105
+
106
+ # Determine color
107
+ if seg_href.length > 0
108
+ color = @theme.link_color
109
+ elsif seg_code
110
+ color = @theme.code_color
111
+ elsif seg_italic
112
+ color = @theme.emphasis_color
113
+ elsif seg_strike
114
+ color = @theme.strikethrough_color
115
+ else
116
+ color = @theme.text_color
117
+ end
118
+
119
+ # Split segment text by words for wrapping
120
+ words = seg_text.split(" ")
121
+ wi = 0
122
+ while wi < words.length
123
+ word = words[wi]
124
+ if wi > 0
125
+ word = " " + word
126
+ end
127
+ word_w = painter.measure_text_width(word, @font_family, font_size)
128
+
129
+ # Check if word fits on current line
130
+ if x + word_w > @cursor_x + available_width && x > start_x
131
+ # Wrap to next line
132
+ @cursor_y = @cursor_y + line_height
133
+ x = start_x
134
+ # Trim leading space after wrap
135
+ if word.start_with?(" ")
136
+ word = word[1, word.length - 1]
137
+ word_w = painter.measure_text_width(word, @font_family, font_size)
138
+ end
139
+ end
140
+
141
+ # Draw inline code background
142
+ if seg_code
143
+ code_pad = 3.0
144
+ painter.fill_round_rect(x - code_pad, @cursor_y, word_w + code_pad * 2.0, font_size * 1.2, 3.0, @theme.code_inline_bg)
145
+ end
146
+
147
+ # Draw text
148
+ painter.draw_text(word, x, @cursor_y + ascent, @font_family, font_size, color)
149
+
150
+ # Faux bold
151
+ if seg_bold
152
+ painter.draw_text(word, x + 0.5, @cursor_y + ascent, @font_family, font_size, color)
153
+ end
154
+
155
+ # Strikethrough line
156
+ if seg_strike
157
+ strike_y = @cursor_y + font_size * 0.55
158
+ painter.draw_line(x, strike_y, x + word_w, strike_y, color, 1.0)
159
+ end
160
+
161
+ # Link underline
162
+ if seg_href.length > 0
163
+ underline_y = @cursor_y + ascent + 2.0
164
+ painter.fill_rect(x, underline_y, word_w, 1.0, color)
165
+ end
166
+
167
+ x = x + word_w
168
+ wi = wi + 1
169
+ end
170
+
171
+ si = si + 1
172
+ end
173
+
174
+ @cursor_y = @cursor_y + line_height + @theme.paragraph_spacing
175
+ end
176
+
177
+ # --- Code block ---
178
+ def render_code_block(painter, node, width)
179
+ font_size = @theme.base_font_size
180
+ line_height = font_size + 4.0
181
+ code_padding = 8.0
182
+
183
+ lines = node.content.split("\n")
184
+ block_height = lines.length * line_height + code_padding * 2.0
185
+
186
+ # Background
187
+ painter.fill_round_rect(@cursor_x, @cursor_y, width, block_height, 4.0, @theme.code_bg_color)
188
+
189
+ # Lines
190
+ ascent = painter.get_text_ascent(@font_family, font_size)
191
+ y = @cursor_y + code_padding + ascent
192
+ li = 0
193
+ while li < lines.length
194
+ painter.draw_text(lines[li], @cursor_x + code_padding, y, @font_family, font_size, @theme.code_color)
195
+ y = y + line_height
196
+ li = li + 1
197
+ end
198
+
199
+ @cursor_y = @cursor_y + block_height + @theme.block_spacing
200
+ end
201
+
202
+ # --- Blockquote ---
203
+ def render_blockquote(painter, node, width)
204
+ indent = @theme.blockquote_indent
205
+ bar_width = 4.0
206
+ start_y = @cursor_y
207
+
208
+ @cursor_x = @cursor_x + indent
209
+ ci = 0
210
+ while ci < node.children.length
211
+ render_node(painter, node.children[ci], width - indent)
212
+ ci = ci + 1
213
+ end
214
+ @cursor_x = @cursor_x - indent
215
+
216
+ # Draw left bar
217
+ bar_height = @cursor_y - start_y
218
+ if bar_height > 0.0
219
+ painter.fill_rect(@cursor_x, start_y, bar_width, bar_height, @theme.blockquote_bg)
220
+ end
221
+ end
222
+
223
+ # --- List ---
224
+ def render_list(painter, node, width)
225
+ @list_depth = @list_depth + 1
226
+ counter = node.start_num
227
+ if node.ordered
228
+ @list_counters.push(counter)
229
+ end
230
+
231
+ ci = 0
232
+ while ci < node.children.length
233
+ child = node.children[ci]
234
+ if child.type == MD_LIST_ITEM
235
+ render_list_item(painter, child, width, node.ordered)
236
+ if node.ordered
237
+ counter = counter + 1
238
+ if @list_counters.length > 0
239
+ @list_counters[@list_counters.length - 1] = counter
240
+ end
241
+ end
242
+ end
243
+ ci = ci + 1
244
+ end
245
+
246
+ if node.ordered && @list_counters.length > 0
247
+ @list_counters.pop
248
+ end
249
+ @list_depth = @list_depth - 1
250
+ end
251
+
252
+ def render_list_item(painter, node, width, ordered)
253
+ indent = @list_depth * @theme.list_indent
254
+ font_size = @theme.base_font_size
255
+ ascent = painter.get_text_ascent(@font_family, font_size)
256
+ color = @theme.text_color
257
+
258
+ marker_x = @cursor_x + indent - 16.0
259
+ marker_y = @cursor_y + ascent
260
+
261
+ if node.checked >= 0
262
+ # Task list item: draw checkbox
263
+ box_size = font_size * 0.75
264
+ box_x = marker_x
265
+ box_y = @cursor_y + (font_size * 1.5 - box_size) / 2.0
266
+ if node.checked == 1
267
+ # Filled checkbox
268
+ painter.fill_rect(box_x, box_y, box_size, box_size, @theme.checkbox_checked_color)
269
+ # Draw checkmark as text
270
+ painter.draw_text("v", box_x + 1.0, box_y + box_size - 2.0, @font_family, font_size * 0.65, 0xFFFFFFFF)
271
+ else
272
+ # Empty checkbox outline (draw 4 sides)
273
+ painter.fill_rect(box_x, box_y, box_size, 1.0, @theme.checkbox_unchecked_color)
274
+ painter.fill_rect(box_x, box_y + box_size - 1.0, box_size, 1.0, @theme.checkbox_unchecked_color)
275
+ painter.fill_rect(box_x, box_y, 1.0, box_size, @theme.checkbox_unchecked_color)
276
+ painter.fill_rect(box_x + box_size - 1.0, box_y, 1.0, box_size, @theme.checkbox_unchecked_color)
277
+ end
278
+ elsif ordered
279
+ counter = 1
280
+ if @list_counters.length > 0
281
+ counter = @list_counters[@list_counters.length - 1]
282
+ end
283
+ marker = counter.to_s + "."
284
+ painter.draw_text(marker, marker_x, marker_y, @font_family, font_size, color)
285
+ else
286
+ painter.draw_text("*", marker_x + 4.0, marker_y, @font_family, font_size, color)
287
+ end
288
+
289
+ # Render item children
290
+ ci = 0
291
+ while ci < node.children.length
292
+ render_node(painter, node.children[ci], width)
293
+ ci = ci + 1
294
+ end
295
+ end
296
+
297
+ # --- Horizontal rule ---
298
+ def render_hr(painter, width)
299
+ @cursor_y = @cursor_y + @theme.block_spacing / 2.0
300
+ painter.fill_rect(@cursor_x, @cursor_y, width, 1.0, @theme.text_color)
301
+ @cursor_y = @cursor_y + @theme.block_spacing / 2.0 + 1.0
302
+ end
303
+
304
+ # --- Table ---
305
+ def render_table(painter, node, width)
306
+ font_size = @theme.base_font_size
307
+ ascent = painter.get_text_ascent(@font_family, font_size)
308
+ cell_pad = 8.0
309
+ row_height = font_size * 1.5 + cell_pad * 2.0
310
+
311
+ # Count columns from header row
312
+ num_cols = 0
313
+ if node.children.length > 0
314
+ num_cols = node.children[0].children.length
315
+ end
316
+ if num_cols == 0
317
+ return
318
+ end
319
+
320
+ # Measure phase: compute max cell text width per column
321
+ col_widths = []
322
+ ci = 0
323
+ while ci < num_cols
324
+ col_widths.push(0.0)
325
+ ci = ci + 1
326
+ end
327
+
328
+ ri = 0
329
+ while ri < node.children.length
330
+ row = node.children[ri]
331
+ ci = 0
332
+ while ci < row.children.length && ci < num_cols
333
+ cell = row.children[ci]
334
+ text = extract_text(cell.children)
335
+ text_w = painter.measure_text_width(text, @font_family, font_size)
336
+ cell_w = text_w + cell_pad * 2.0
337
+ if cell_w > col_widths[ci]
338
+ col_widths[ci] = cell_w
339
+ end
340
+ ci = ci + 1
341
+ end
342
+ ri = ri + 1
343
+ end
344
+
345
+ # Distribute widths: scale to fit available width
346
+ total_w = 0.0
347
+ ci = 0
348
+ while ci < num_cols
349
+ total_w = total_w + col_widths[ci]
350
+ ci = ci + 1
351
+ end
352
+
353
+ if total_w < width && total_w > 0.0
354
+ # Scale up proportionally
355
+ scale = width / total_w
356
+ ci = 0
357
+ while ci < num_cols
358
+ col_widths[ci] = col_widths[ci] * scale
359
+ ci = ci + 1
360
+ end
361
+ elsif total_w > width && total_w > 0.0
362
+ # Scale down proportionally
363
+ scale = width / total_w
364
+ ci = 0
365
+ while ci < num_cols
366
+ col_widths[ci] = col_widths[ci] * scale
367
+ ci = ci + 1
368
+ end
369
+ end
370
+
371
+ table_x = @cursor_x
372
+ table_width = 0.0
373
+ ci = 0
374
+ while ci < num_cols
375
+ table_width = table_width + col_widths[ci]
376
+ ci = ci + 1
377
+ end
378
+
379
+ @cursor_y = @cursor_y + @theme.block_spacing / 2.0
380
+
381
+ # Draw rows
382
+ ri = 0
383
+ while ri < node.children.length
384
+ row = node.children[ri]
385
+ row_y = @cursor_y
386
+
387
+ # Header row background
388
+ if row.is_header
389
+ painter.fill_rect(table_x, row_y, table_width, row_height, @theme.table_header_bg)
390
+ end
391
+
392
+ # Draw cells
393
+ cell_x = table_x
394
+ ci = 0
395
+ while ci < row.children.length && ci < num_cols
396
+ cell = row.children[ci]
397
+ text = extract_text(cell.children)
398
+ text_w = painter.measure_text_width(text, @font_family, font_size)
399
+ col_w = col_widths[ci]
400
+
401
+ # Compute text x position based on alignment
402
+ align = cell.align
403
+ if align == 1
404
+ # Center
405
+ tx = cell_x + (col_w - text_w) / 2.0
406
+ elsif align == 2
407
+ # Right
408
+ tx = cell_x + col_w - text_w - cell_pad
409
+ else
410
+ # Left
411
+ tx = cell_x + cell_pad
412
+ end
413
+
414
+ text_y = row_y + cell_pad + ascent
415
+ color = @theme.text_color
416
+
417
+ painter.draw_text(text, tx, text_y, @font_family, font_size, color)
418
+ # Faux bold for header
419
+ if row.is_header
420
+ painter.draw_text(text, tx + 0.5, text_y, @font_family, font_size, color)
421
+ end
422
+
423
+ cell_x = cell_x + col_w
424
+ ci = ci + 1
425
+ end
426
+
427
+ # Draw horizontal line below row
428
+ painter.fill_rect(table_x, row_y + row_height, table_width, 1.0, @theme.table_border_color)
429
+
430
+ @cursor_y = @cursor_y + row_height + 1.0
431
+ ri = ri + 1
432
+ end
433
+
434
+ # Draw vertical column separator lines
435
+ line_x = table_x
436
+ table_top = @cursor_y - (node.children.length * (row_height + 1.0))
437
+ table_bottom = @cursor_y
438
+ ci = 0
439
+ while ci < num_cols + 1
440
+ painter.fill_rect(line_x, table_top, 1.0, table_bottom - table_top, @theme.table_border_color)
441
+ if ci < num_cols
442
+ line_x = line_x + col_widths[ci]
443
+ end
444
+ ci = ci + 1
445
+ end
446
+
447
+ @cursor_y = @cursor_y + @theme.block_spacing / 2.0
448
+ end
449
+
450
+ # --- Image ---
451
+ def render_image(painter, node, width)
452
+ # Load image (Java side caches by path hash)
453
+ img_id = painter.load_image(node.href)
454
+ if img_id == 0
455
+ render_image_placeholder(painter, node, width)
456
+ return
457
+ end
458
+ # Get natural dimensions
459
+ img_w = painter.get_image_width(img_id) * 1.0
460
+ img_h = painter.get_image_height(img_id) * 1.0
461
+ if img_w < 1.0 || img_h < 1.0
462
+ render_image_placeholder(painter, node, width)
463
+ return
464
+ end
465
+ # Scale to fit width (maintain aspect ratio)
466
+ if img_w > width
467
+ scale = width / img_w
468
+ img_w = width
469
+ img_h = img_h * scale
470
+ end
471
+ painter.draw_image(img_id, @cursor_x, @cursor_y, img_w, img_h)
472
+ @cursor_y = @cursor_y + img_h + @theme.block_spacing
473
+ end
474
+
475
+ def render_image_placeholder(painter, node, width)
476
+ # Draw a placeholder box with alt text
477
+ box_h = 60.0
478
+ font_size = @theme.base_font_size
479
+ ascent = painter.get_text_ascent(@font_family, font_size)
480
+
481
+ painter.fill_round_rect(@cursor_x, @cursor_y, width, box_h, 4.0, @theme.code_bg_color)
482
+ painter.stroke_round_rect(@cursor_x, @cursor_y, width, box_h, 4.0, @theme.table_border_color, 1.0)
483
+
484
+ # Icon placeholder text
485
+ icon_text = "[Image]"
486
+ if node.content.length > 0
487
+ icon_text = "[" + node.content + "]"
488
+ end
489
+ text_w = painter.measure_text_width(icon_text, @font_family, font_size)
490
+ tx = @cursor_x + (width - text_w) / 2.0
491
+ ty = @cursor_y + (box_h + ascent) / 2.0
492
+ painter.draw_text(icon_text, tx, ty, @font_family, font_size, @theme.strikethrough_color)
493
+
494
+ @cursor_y = @cursor_y + box_h + @theme.block_spacing
495
+ end
496
+
497
+ # --- Mermaid ---
498
+ def render_mermaid(painter, node, width)
499
+ mermaid_parser = MermaidParser.new
500
+ diagram = mermaid_parser.parse(node.content)
501
+ mermaid_renderer = MermaidRenderer.new(@theme)
502
+ height = mermaid_renderer.render(painter, diagram, @cursor_x, @cursor_y, width)
503
+ @cursor_y = @cursor_y + height + @theme.block_spacing
504
+ end
505
+
506
+ # --- Segment collection ---
507
+
508
+ def collect_segments(nodes, bold, italic, strikethrough, code, href)
509
+ segments = []
510
+ i = 0
511
+ while i < nodes.length
512
+ node = nodes[i]
513
+ t = node.type
514
+ if t == MD_TEXT
515
+ segments.push([node.content, bold, italic, strikethrough, code, href])
516
+ elsif t == MD_STRONG
517
+ inner = collect_segments(node.children, true, italic, strikethrough, code, href)
518
+ si = 0
519
+ while si < inner.length
520
+ segments.push(inner[si])
521
+ si = si + 1
522
+ end
523
+ elsif t == MD_EMPHASIS
524
+ inner = collect_segments(node.children, bold, true, strikethrough, code, href)
525
+ si = 0
526
+ while si < inner.length
527
+ segments.push(inner[si])
528
+ si = si + 1
529
+ end
530
+ elsif t == MD_STRIKETHROUGH
531
+ inner = collect_segments(node.children, bold, italic, true, code, href)
532
+ si = 0
533
+ while si < inner.length
534
+ segments.push(inner[si])
535
+ si = si + 1
536
+ end
537
+ elsif t == MD_CODE_INLINE
538
+ segments.push([node.content, bold, italic, strikethrough, true, href])
539
+ elsif t == MD_LINK
540
+ inner = collect_segments(node.children, bold, italic, strikethrough, code, node.href)
541
+ si = 0
542
+ while si < inner.length
543
+ segments.push(inner[si])
544
+ si = si + 1
545
+ end
546
+ elsif t == MD_SOFT_BREAK
547
+ segments.push([" ", bold, italic, strikethrough, code, href])
548
+ end
549
+ i = i + 1
550
+ end
551
+ segments
552
+ end
553
+
554
+ # --- Height estimation ---
555
+
556
+ def estimate_node_height(painter, node, width)
557
+ t = node.type
558
+ if t == MD_HEADING
559
+ font_size = @theme.heading_size(node.level)
560
+ font_size * 1.5 + @theme.block_spacing
561
+ elsif t == MD_PARAGRAPH
562
+ text = extract_text(node.children)
563
+ font_size = @theme.base_font_size
564
+ text_w = painter.measure_text_width(text, @font_family, font_size)
565
+ safe_width = width
566
+ if safe_width < 1.0
567
+ safe_width = 1.0
568
+ end
569
+ lines = (text_w / safe_width).to_i + 1
570
+ if lines < 1
571
+ lines = 1
572
+ end
573
+ lines * font_size * 1.5 + @theme.paragraph_spacing
574
+ elsif t == MD_CODE_BLOCK
575
+ line_count = node.content.split("\n").length
576
+ line_count * (@theme.base_font_size + 4.0) + 16.0 + @theme.block_spacing
577
+ elsif t == MD_BLOCKQUOTE
578
+ h = 0.0
579
+ ci = 0
580
+ while ci < node.children.length
581
+ child_width = width - @theme.blockquote_indent
582
+ if child_width < 1.0
583
+ child_width = 1.0
584
+ end
585
+ h = h + estimate_node_height(painter, node.children[ci], child_width)
586
+ ci = ci + 1
587
+ end
588
+ h
589
+ elsif t == MD_LIST
590
+ h = 0.0
591
+ ci = 0
592
+ while ci < node.children.length
593
+ h = h + estimate_node_height(painter, node.children[ci], width)
594
+ ci = ci + 1
595
+ end
596
+ h
597
+ elsif t == MD_LIST_ITEM
598
+ h = 0.0
599
+ ci = 0
600
+ while ci < node.children.length
601
+ h = h + estimate_node_height(painter, node.children[ci], width)
602
+ ci = ci + 1
603
+ end
604
+ h
605
+ elsif t == MD_TABLE
606
+ cell_pad = 8.0
607
+ row_h = @theme.base_font_size * 1.5 + cell_pad * 2.0 + 1.0
608
+ num_rows = node.children.length
609
+ num_rows * row_h + @theme.block_spacing
610
+ elsif t == MD_HORIZONTAL_RULE
611
+ @theme.block_spacing + 1.0
612
+ elsif t == MD_IMAGE
613
+ 200.0 + @theme.block_spacing
614
+ elsif t == MD_MERMAID
615
+ 300.0 + @theme.block_spacing
616
+ else
617
+ @theme.base_font_size * 1.5
618
+ end
619
+ end
620
+
621
+ # --- Helpers ---
622
+
623
+ def extract_text(nodes)
624
+ parts = []
625
+ i = 0
626
+ while i < nodes.length
627
+ node = nodes[i]
628
+ if node.type == MD_TEXT
629
+ parts.push(node.content)
630
+ elsif node.type == MD_CODE_INLINE
631
+ parts.push(node.content)
632
+ else
633
+ parts.push(extract_text(node.children))
634
+ end
635
+ i = i + 1
636
+ end
637
+ parts.join("")
638
+ end
639
+ end