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,805 @@
1
+ # Markdown parser - pure Ruby line-by-line parser
2
+ # No regex, no external libraries. Uses start_with?, index, [] only.
3
+
4
+ class MarkdownParser
5
+ def initialize
6
+ @lines = []
7
+ @pos = 0
8
+ end
9
+
10
+ def parse(text)
11
+ @lines = text.split("\n")
12
+ @pos = 0
13
+ doc = MdNode.new(MD_DOCUMENT)
14
+ while @pos < @lines.length
15
+ node = parse_block
16
+ if node
17
+ doc.add_child(node)
18
+ end
19
+ end
20
+ doc
21
+ end
22
+
23
+ def parse_block
24
+ if @pos >= @lines.length
25
+ return nil
26
+ end
27
+ line = @lines[@pos]
28
+ stripped = line.strip
29
+
30
+ # Blank line
31
+ if stripped.length == 0
32
+ @pos = @pos + 1
33
+ return nil
34
+ end
35
+
36
+ # Heading: # ... ######
37
+ if stripped.start_with?("#")
38
+ return parse_heading
39
+ end
40
+
41
+ # Horizontal rule: --- or *** or ___
42
+ if is_horizontal_rule(stripped)
43
+ @pos = @pos + 1
44
+ return MdNode.new(MD_HORIZONTAL_RULE)
45
+ end
46
+
47
+ # Image on its own line: ![alt](src)
48
+ if stripped.start_with?("![")
49
+ img_node = try_parse_image_line(stripped)
50
+ if img_node
51
+ @pos = @pos + 1
52
+ return img_node
53
+ end
54
+ end
55
+
56
+ # Code block: ```
57
+ if stripped.start_with?("```")
58
+ return parse_code_block
59
+ end
60
+
61
+ # Blockquote: > text
62
+ if stripped.start_with?("> ") || stripped == ">"
63
+ return parse_blockquote
64
+ end
65
+
66
+ # Table: | col | col |
67
+ if stripped.start_with?("|") && is_table_start
68
+ return parse_table
69
+ end
70
+
71
+ # Unordered list: - item or * item
72
+ if is_unordered_list_start(stripped)
73
+ return parse_list(false)
74
+ end
75
+
76
+ # Ordered list: 1. item
77
+ if is_ordered_list_start(stripped)
78
+ return parse_list(true)
79
+ end
80
+
81
+ # Paragraph (default)
82
+ return parse_paragraph
83
+ end
84
+
85
+ def parse_heading
86
+ line = @lines[@pos].strip
87
+ @pos = @pos + 1
88
+ level = 0
89
+ i = 0
90
+ while i < line.length && line[i, 1] == "#"
91
+ level = level + 1
92
+ i = i + 1
93
+ end
94
+ if level > 6
95
+ level = 6
96
+ end
97
+ # Skip space after #
98
+ if i < line.length && line[i, 1] == " "
99
+ i = i + 1
100
+ end
101
+ text = ""
102
+ if i < line.length
103
+ text = line[i, line.length - i]
104
+ end
105
+ node = MdNode.new(MD_HEADING)
106
+ node.level = level
107
+ inline_nodes = parse_inline(text)
108
+ ci = 0
109
+ while ci < inline_nodes.length
110
+ node.add_child(inline_nodes[ci])
111
+ ci = ci + 1
112
+ end
113
+ node
114
+ end
115
+
116
+ def parse_code_block
117
+ first_line = @lines[@pos].strip
118
+ @pos = @pos + 1
119
+ # Extract language from ```lang
120
+ lang = ""
121
+ if first_line.length > 3
122
+ lang = first_line[3, first_line.length - 3].strip
123
+ end
124
+ content_parts = []
125
+ while @pos < @lines.length
126
+ line = @lines[@pos]
127
+ if line.strip.start_with?("```")
128
+ @pos = @pos + 1
129
+ break
130
+ end
131
+ content_parts.push(line)
132
+ @pos = @pos + 1
133
+ end
134
+ if lang == "mermaid"
135
+ node = MdNode.new(MD_MERMAID)
136
+ node.content = content_parts.join("\n")
137
+ return node
138
+ end
139
+ node = MdNode.new(MD_CODE_BLOCK)
140
+ node.language = lang
141
+ node.content = content_parts.join("\n")
142
+ node
143
+ end
144
+
145
+ def parse_blockquote
146
+ collected = []
147
+ while @pos < @lines.length
148
+ line = @lines[@pos]
149
+ stripped = line.strip
150
+ if stripped.start_with?("> ")
151
+ collected.push(stripped[2, stripped.length - 2])
152
+ @pos = @pos + 1
153
+ elsif stripped == ">"
154
+ collected.push("")
155
+ @pos = @pos + 1
156
+ else
157
+ break
158
+ end
159
+ end
160
+ # Recursively parse the collected content
161
+ inner_text = collected.join("\n")
162
+ inner_parser = MarkdownParser.new
163
+ inner_doc = inner_parser.parse(inner_text)
164
+ node = MdNode.new(MD_BLOCKQUOTE)
165
+ ci = 0
166
+ while ci < inner_doc.children.length
167
+ node.add_child(inner_doc.children[ci])
168
+ ci = ci + 1
169
+ end
170
+ node
171
+ end
172
+
173
+ def parse_list(ordered)
174
+ node = MdNode.new(MD_LIST)
175
+ node.ordered = ordered
176
+ if ordered
177
+ # Extract start number
178
+ line = @lines[@pos].strip
179
+ num_str = ""
180
+ ni = 0
181
+ while ni < line.length && is_digit(line[ni, 1])
182
+ num_str = num_str + line[ni, 1]
183
+ ni = ni + 1
184
+ end
185
+ if num_str.length > 0
186
+ node.start_num = num_str.to_i
187
+ end
188
+ end
189
+ counter = node.start_num
190
+ while @pos < @lines.length
191
+ line = @lines[@pos].strip
192
+ if line.length == 0
193
+ # Blank line might end the list or separate items
194
+ @pos = @pos + 1
195
+ # Check if next line continues the list
196
+ if @pos < @lines.length
197
+ next_line = @lines[@pos].strip
198
+ if ordered && is_ordered_list_start(next_line)
199
+ next
200
+ elsif !ordered && is_unordered_list_start(next_line)
201
+ next
202
+ else
203
+ break
204
+ end
205
+ else
206
+ break
207
+ end
208
+ elsif ordered && is_ordered_list_start(line)
209
+ item = parse_list_item_ordered
210
+ node.add_child(item)
211
+ counter = counter + 1
212
+ elsif !ordered && is_unordered_list_start(line)
213
+ item = parse_list_item_unordered
214
+ node.add_child(item)
215
+ else
216
+ break
217
+ end
218
+ end
219
+ node
220
+ end
221
+
222
+ def parse_list_item_unordered
223
+ line = @lines[@pos].strip
224
+ @pos = @pos + 1
225
+ # Strip "- " or "* "
226
+ text = line[2, line.length - 2]
227
+ item = MdNode.new(MD_LIST_ITEM)
228
+
229
+ # Check for task list: [ ] or [x] or [X]
230
+ if text.length >= 4 && text[0, 4] == "[ ] "
231
+ item.checked = 0
232
+ text = text[4, text.length - 4]
233
+ elsif text.length >= 4 && (text[0, 4] == "[x] " || text[0, 4] == "[X] ")
234
+ item.checked = 1
235
+ text = text[4, text.length - 4]
236
+ end
237
+
238
+ # Parse inline content as a paragraph
239
+ para = MdNode.new(MD_PARAGRAPH)
240
+ inline_nodes = parse_inline(text)
241
+ ci = 0
242
+ while ci < inline_nodes.length
243
+ para.add_child(inline_nodes[ci])
244
+ ci = ci + 1
245
+ end
246
+ item.add_child(para)
247
+
248
+ # Check for nested sub-list (indented lines)
249
+ parse_nested_list(item)
250
+
251
+ item
252
+ end
253
+
254
+ def parse_list_item_ordered
255
+ line = @lines[@pos].strip
256
+ @pos = @pos + 1
257
+ # Find ". " after digits
258
+ di = 0
259
+ while di < line.length && is_digit(line[di, 1])
260
+ di = di + 1
261
+ end
262
+ # Skip ". "
263
+ if di < line.length && line[di, 1] == "."
264
+ di = di + 1
265
+ end
266
+ if di < line.length && line[di, 1] == " "
267
+ di = di + 1
268
+ end
269
+ text = ""
270
+ if di < line.length
271
+ text = line[di, line.length - di]
272
+ end
273
+ item = MdNode.new(MD_LIST_ITEM)
274
+ para = MdNode.new(MD_PARAGRAPH)
275
+ inline_nodes = parse_inline(text)
276
+ ci = 0
277
+ while ci < inline_nodes.length
278
+ para.add_child(inline_nodes[ci])
279
+ ci = ci + 1
280
+ end
281
+ item.add_child(para)
282
+
283
+ # Check for nested sub-list (indented lines)
284
+ parse_nested_list(item)
285
+
286
+ item
287
+ end
288
+
289
+ def parse_paragraph
290
+ parts = []
291
+ while @pos < @lines.length
292
+ line = @lines[@pos]
293
+ stripped = line.strip
294
+ if stripped.length == 0
295
+ break
296
+ end
297
+ if stripped.start_with?("#") || stripped.start_with?("```") || stripped.start_with?("> ") || stripped == ">"
298
+ break
299
+ end
300
+ if stripped.start_with?("|") && is_table_start
301
+ break
302
+ end
303
+ if is_horizontal_rule(stripped)
304
+ break
305
+ end
306
+ if is_unordered_list_start(stripped) || is_ordered_list_start(stripped)
307
+ break
308
+ end
309
+ if stripped.start_with?("![")
310
+ break
311
+ end
312
+ parts.push(stripped)
313
+ @pos = @pos + 1
314
+ end
315
+ text = parts.join(" ")
316
+ node = MdNode.new(MD_PARAGRAPH)
317
+ inline_nodes = parse_inline(text)
318
+ ci = 0
319
+ while ci < inline_nodes.length
320
+ node.add_child(inline_nodes[ci])
321
+ ci = ci + 1
322
+ end
323
+ node
324
+ end
325
+
326
+ # --- Inline parsing ---
327
+
328
+ def parse_inline(text)
329
+ nodes = []
330
+ buf = ""
331
+ i = 0
332
+ while i < text.length
333
+ ch = text[i, 1]
334
+
335
+ if ch == "*"
336
+ # Check ** (bold) or * (italic)
337
+ if i + 1 < text.length && text[i + 1, 1] == "*"
338
+ # Bold **...**
339
+ flush_buf(buf, nodes)
340
+ buf = ""
341
+ close_idx = find_marker(text, i + 2, "**")
342
+ if close_idx >= 0
343
+ inner = text[i + 2, close_idx - (i + 2)]
344
+ node = MdNode.new(MD_STRONG)
345
+ inner_nodes = parse_inline(inner)
346
+ ni = 0
347
+ while ni < inner_nodes.length
348
+ node.add_child(inner_nodes[ni])
349
+ ni = ni + 1
350
+ end
351
+ nodes.push(node)
352
+ i = close_idx + 2
353
+ else
354
+ buf = buf + "**"
355
+ i = i + 2
356
+ end
357
+ else
358
+ # Italic *...*
359
+ flush_buf(buf, nodes)
360
+ buf = ""
361
+ close_idx = find_single_star(text, i + 1)
362
+ if close_idx >= 0
363
+ inner = text[i + 1, close_idx - (i + 1)]
364
+ node = MdNode.new(MD_EMPHASIS)
365
+ inner_nodes = parse_inline(inner)
366
+ ni = 0
367
+ while ni < inner_nodes.length
368
+ node.add_child(inner_nodes[ni])
369
+ ni = ni + 1
370
+ end
371
+ nodes.push(node)
372
+ i = close_idx + 1
373
+ else
374
+ buf = buf + "*"
375
+ i = i + 1
376
+ end
377
+ end
378
+
379
+ elsif ch == "~"
380
+ # Strikethrough ~~...~~
381
+ if i + 1 < text.length && text[i + 1, 1] == "~"
382
+ flush_buf(buf, nodes)
383
+ buf = ""
384
+ close_idx = find_marker(text, i + 2, "~~")
385
+ if close_idx >= 0
386
+ inner = text[i + 2, close_idx - (i + 2)]
387
+ node = MdNode.new(MD_STRIKETHROUGH)
388
+ inner_nodes = parse_inline(inner)
389
+ ni = 0
390
+ while ni < inner_nodes.length
391
+ node.add_child(inner_nodes[ni])
392
+ ni = ni + 1
393
+ end
394
+ nodes.push(node)
395
+ i = close_idx + 2
396
+ else
397
+ buf = buf + "~~"
398
+ i = i + 2
399
+ end
400
+ else
401
+ buf = buf + "~"
402
+ i = i + 1
403
+ end
404
+
405
+ elsif ch == "`"
406
+ # Inline code `...`
407
+ flush_buf(buf, nodes)
408
+ buf = ""
409
+ close_idx = find_char(text, i + 1, "`")
410
+ if close_idx >= 0
411
+ code_text = text[i + 1, close_idx - (i + 1)]
412
+ node = MdNode.new(MD_CODE_INLINE)
413
+ node.content = code_text
414
+ nodes.push(node)
415
+ i = close_idx + 1
416
+ else
417
+ buf = buf + "`"
418
+ i = i + 1
419
+ end
420
+
421
+ elsif ch == "["
422
+ # Link [text](url)
423
+ flush_buf(buf, nodes)
424
+ buf = ""
425
+ bracket_close = find_char(text, i + 1, "]")
426
+ if bracket_close >= 0 && bracket_close + 1 < text.length && text[bracket_close + 1, 1] == "("
427
+ paren_close = find_char(text, bracket_close + 2, ")")
428
+ if paren_close >= 0
429
+ link_text = text[i + 1, bracket_close - (i + 1)]
430
+ link_url = text[bracket_close + 2, paren_close - (bracket_close + 2)]
431
+ node = MdNode.new(MD_LINK)
432
+ node.href = link_url
433
+ inner_nodes = parse_inline(link_text)
434
+ ni = 0
435
+ while ni < inner_nodes.length
436
+ node.add_child(inner_nodes[ni])
437
+ ni = ni + 1
438
+ end
439
+ nodes.push(node)
440
+ i = paren_close + 1
441
+ else
442
+ buf = buf + "["
443
+ i = i + 1
444
+ end
445
+ else
446
+ buf = buf + "["
447
+ i = i + 1
448
+ end
449
+
450
+ else
451
+ buf = buf + ch
452
+ i = i + 1
453
+ end
454
+ end
455
+ flush_buf(buf, nodes)
456
+ nodes
457
+ end
458
+
459
+ # --- Helpers ---
460
+
461
+ def flush_buf(buf, nodes)
462
+ if buf.length > 0
463
+ node = MdNode.new(MD_TEXT)
464
+ node.content = buf
465
+ nodes.push(node)
466
+ end
467
+ end
468
+
469
+ def find_marker(text, start, marker)
470
+ # Find the position of marker string starting from start
471
+ mlen = marker.length
472
+ i = start
473
+ while i <= text.length - mlen
474
+ if text[i, mlen] == marker
475
+ return i
476
+ end
477
+ i = i + 1
478
+ end
479
+ -1
480
+ end
481
+
482
+ def find_single_star(text, start)
483
+ # Find closing * that is not part of **
484
+ i = start
485
+ while i < text.length
486
+ if text[i, 1] == "*"
487
+ # Make sure it's not **
488
+ if i + 1 < text.length && text[i + 1, 1] == "*"
489
+ i = i + 2
490
+ else
491
+ return i
492
+ end
493
+ else
494
+ i = i + 1
495
+ end
496
+ end
497
+ -1
498
+ end
499
+
500
+ def find_char(text, start, ch)
501
+ i = start
502
+ while i < text.length
503
+ if text[i, 1] == ch
504
+ return i
505
+ end
506
+ i = i + 1
507
+ end
508
+ -1
509
+ end
510
+
511
+ def is_horizontal_rule(line)
512
+ if line.length < 3
513
+ return false
514
+ end
515
+ # Check if line is all dashes, all asterisks, or all underscores (with optional spaces)
516
+ ch = ""
517
+ count = 0
518
+ i = 0
519
+ while i < line.length
520
+ c = line[i, 1]
521
+ if c == " "
522
+ i = i + 1
523
+ next
524
+ end
525
+ if ch == ""
526
+ if c == "-" || c == "*" || c == "_"
527
+ ch = c
528
+ count = 1
529
+ else
530
+ return false
531
+ end
532
+ elsif c == ch
533
+ count = count + 1
534
+ else
535
+ return false
536
+ end
537
+ i = i + 1
538
+ end
539
+ count >= 3
540
+ end
541
+
542
+ def is_unordered_list_start(line)
543
+ if line.length >= 2
544
+ if (line[0, 1] == "-" || line[0, 1] == "*") && line[1, 1] == " "
545
+ return true
546
+ end
547
+ end
548
+ false
549
+ end
550
+
551
+ def is_ordered_list_start(line)
552
+ # Check if line starts with digits followed by ". "
553
+ i = 0
554
+ while i < line.length && is_digit(line[i, 1])
555
+ i = i + 1
556
+ end
557
+ if i > 0 && i + 1 < line.length && line[i, 1] == "." && line[i + 1, 1] == " "
558
+ return true
559
+ end
560
+ false
561
+ end
562
+
563
+ def try_parse_image_line(line)
564
+ # Parse ![alt](src) as a block-level image
565
+ # Returns MD_PARAGRAPH containing MD_IMAGE node, or nil
566
+ if !line.start_with?("![")
567
+ return nil
568
+ end
569
+ bracket_close = find_char(line, 2, "]")
570
+ if bracket_close < 0
571
+ return nil
572
+ end
573
+ if bracket_close + 1 >= line.length || line[bracket_close + 1, 1] != "("
574
+ return nil
575
+ end
576
+ paren_close = find_char(line, bracket_close + 2, ")")
577
+ if paren_close < 0
578
+ return nil
579
+ end
580
+ alt_text = line[2, bracket_close - 2]
581
+ img_src = line[bracket_close + 2, paren_close - (bracket_close + 2)]
582
+ img_node = MdNode.new(MD_IMAGE)
583
+ img_node.content = alt_text
584
+ img_node.href = img_src
585
+ img_node
586
+ end
587
+
588
+ def is_digit(ch)
589
+ ch == "0" || ch == "1" || ch == "2" || ch == "3" || ch == "4" || ch == "5" || ch == "6" || ch == "7" || ch == "8" || ch == "9"
590
+ end
591
+
592
+ # --- Table parsing ---
593
+
594
+ def is_table_start
595
+ # Check if current line + next line form a table (header + separator)
596
+ if @pos + 1 >= @lines.length
597
+ return false
598
+ end
599
+ next_line = @lines[@pos + 1].strip
600
+ if next_line.length < 3
601
+ return false
602
+ end
603
+ # Separator must start with | and contain ---
604
+ if !next_line.start_with?("|")
605
+ return false
606
+ end
607
+ # Quick check: separator contains at least one ---
608
+ has_dash = false
609
+ i = 0
610
+ while i < next_line.length
611
+ if next_line[i, 1] == "-"
612
+ has_dash = true
613
+ break
614
+ end
615
+ i = i + 1
616
+ end
617
+ has_dash
618
+ end
619
+
620
+ def parse_table
621
+ table = MdNode.new(MD_TABLE)
622
+
623
+ # Parse header row
624
+ header_cells = split_table_row(@lines[@pos].strip)
625
+ @pos = @pos + 1
626
+
627
+ # Parse separator row for alignment
628
+ alignments = parse_table_separator(@lines[@pos].strip)
629
+ @pos = @pos + 1
630
+
631
+ # Build header row node
632
+ header_row = MdNode.new(MD_TABLE_ROW)
633
+ header_row.is_header = true
634
+ ci = 0
635
+ while ci < header_cells.length
636
+ cell = MdNode.new(MD_TABLE_CELL)
637
+ cell.is_header = true
638
+ if alignments && ci < alignments.length
639
+ cell.align = alignments[ci]
640
+ end
641
+ inline_nodes = parse_inline(header_cells[ci])
642
+ ni = 0
643
+ while ni < inline_nodes.length
644
+ cell.add_child(inline_nodes[ni])
645
+ ni = ni + 1
646
+ end
647
+ header_row.add_child(cell)
648
+ ci = ci + 1
649
+ end
650
+ table.add_child(header_row)
651
+
652
+ # Parse data rows
653
+ while @pos < @lines.length
654
+ line = @lines[@pos].strip
655
+ if line.length == 0 || !line.start_with?("|")
656
+ break
657
+ end
658
+ data_cells = split_table_row(line)
659
+ row = MdNode.new(MD_TABLE_ROW)
660
+ di = 0
661
+ while di < data_cells.length
662
+ cell = MdNode.new(MD_TABLE_CELL)
663
+ if alignments && di < alignments.length
664
+ cell.align = alignments[di]
665
+ end
666
+ inline_nodes = parse_inline(data_cells[di])
667
+ ni = 0
668
+ while ni < inline_nodes.length
669
+ cell.add_child(inline_nodes[ni])
670
+ ni = ni + 1
671
+ end
672
+ row.add_child(cell)
673
+ di = di + 1
674
+ end
675
+ table.add_child(row)
676
+ @pos = @pos + 1
677
+ end
678
+
679
+ table
680
+ end
681
+
682
+ def split_table_row(line)
683
+ # Strip leading and trailing |
684
+ inner = ""
685
+ start = 0
686
+ if line.length > 0 && line[0, 1] == "|"
687
+ start = 1
688
+ end
689
+ stop = line.length
690
+ if stop > 0 && line[stop - 1, 1] == "|"
691
+ stop = stop - 1
692
+ end
693
+ if start < stop
694
+ inner = line[start, stop - start]
695
+ end
696
+
697
+ # Split by | character
698
+ cells = []
699
+ buf = ""
700
+ i = 0
701
+ while i < inner.length
702
+ ch = inner[i, 1]
703
+ if ch == "|"
704
+ cells.push(buf.strip)
705
+ buf = ""
706
+ else
707
+ buf = buf + ch
708
+ end
709
+ i = i + 1
710
+ end
711
+ cells.push(buf.strip)
712
+ cells
713
+ end
714
+
715
+ def parse_table_separator(line)
716
+ cells = split_table_row(line)
717
+ alignments = []
718
+ ci = 0
719
+ while ci < cells.length
720
+ cell = cells[ci]
721
+ # Check alignment markers
722
+ left_colon = cell.length > 0 && cell[0, 1] == ":"
723
+ right_colon = cell.length > 0 && cell[cell.length - 1, 1] == ":"
724
+ if left_colon && right_colon
725
+ alignments.push(1) # center
726
+ elsif right_colon
727
+ alignments.push(2) # right
728
+ else
729
+ alignments.push(0) # left (default)
730
+ end
731
+ ci = ci + 1
732
+ end
733
+ alignments
734
+ end
735
+
736
+ # --- Nested list parsing ---
737
+
738
+ def count_leading_spaces(line)
739
+ count = 0
740
+ i = 0
741
+ while i < line.length
742
+ if line[i, 1] == " "
743
+ count = count + 1
744
+ else
745
+ break
746
+ end
747
+ i = i + 1
748
+ end
749
+ count
750
+ end
751
+
752
+ def parse_nested_list(item)
753
+ # Peek at next lines: if indented 2+ spaces with list marker, parse as sub-list
754
+ sub_lines = collect_indented_lines
755
+ if sub_lines.length == 0
756
+ return
757
+ end
758
+
759
+ # Parse sub-lines as a new document, extract list nodes
760
+ sub_parser = MarkdownParser.new
761
+ sub_doc = sub_parser.parse(sub_lines.join("\n"))
762
+ ci = 0
763
+ while ci < sub_doc.children.length
764
+ child = sub_doc.children[ci]
765
+ if child.type == MD_LIST
766
+ item.add_child(child)
767
+ end
768
+ ci = ci + 1
769
+ end
770
+ end
771
+
772
+ def collect_indented_lines
773
+ result = []
774
+ if @pos >= @lines.length
775
+ return result
776
+ end
777
+ first_line = @lines[@pos]
778
+ first_spaces = count_leading_spaces(first_line)
779
+ if first_spaces < 2
780
+ return result
781
+ end
782
+
783
+ # Check first line is a list start after stripping indent
784
+ first_stripped = first_line.strip
785
+ if !is_unordered_list_start(first_stripped) && !is_ordered_list_start(first_stripped)
786
+ return result
787
+ end
788
+
789
+ # Collect lines with sufficient indentation (use strip to remove indent)
790
+ while @pos < @lines.length
791
+ line = @lines[@pos]
792
+ stripped = line.strip
793
+ if stripped.length == 0
794
+ break
795
+ end
796
+ spaces = count_leading_spaces(line)
797
+ if spaces < first_spaces
798
+ break
799
+ end
800
+ result.push(stripped)
801
+ @pos = @pos + 1
802
+ end
803
+ result
804
+ end
805
+ end