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,519 @@
1
+ # Mermaid flowchart parser - character-by-character, no regex
2
+ # Parses graph TD/LR/BT/RL with nodes, edges, subgraphs
3
+
4
+ class MermaidParser
5
+ def parse(content)
6
+ @lines = content.split("\n")
7
+ @pos = 0
8
+ @nodes = {}
9
+ @subgraph_stack = []
10
+ parse_flowchart
11
+ end
12
+
13
+ def parse_flowchart
14
+ if @pos >= @lines.length
15
+ return MermaidDiagram.new(MERMAID_DIR_TB)
16
+ end
17
+
18
+ # Parse first line for direction
19
+ first_line = @lines[@pos].strip
20
+ direction = parse_direction(first_line)
21
+ @pos = @pos + 1
22
+
23
+ diagram = MermaidDiagram.new(direction)
24
+
25
+ while @pos < @lines.length
26
+ line = @lines[@pos]
27
+ stripped = line.strip
28
+ @pos = @pos + 1
29
+
30
+ # Skip blank lines and comments
31
+ if stripped.length == 0
32
+ next
33
+ end
34
+ if stripped.start_with?("%%")
35
+ next
36
+ end
37
+
38
+ # Check for subgraph
39
+ if stripped.start_with?("subgraph ")
40
+ title = stripped[9, stripped.length - 9].strip
41
+ sg_id = "sg_" + @subgraph_stack.length.to_s
42
+ sg = MermaidSubgraph.new(sg_id, title)
43
+ @subgraph_stack.push(sg)
44
+ next
45
+ end
46
+ if stripped == "subgraph"
47
+ sg = MermaidSubgraph.new("sg_" + @subgraph_stack.length.to_s, "")
48
+ @subgraph_stack.push(sg)
49
+ next
50
+ end
51
+
52
+ # Check for end (closes subgraph)
53
+ if stripped == "end"
54
+ if @subgraph_stack.length > 0
55
+ sg = @subgraph_stack.pop
56
+ diagram.add_subgraph(sg)
57
+ end
58
+ next
59
+ end
60
+
61
+ # Try to parse edge
62
+ edge_result = try_parse_edge(stripped)
63
+ if edge_result
64
+ src_id = edge_result[0]
65
+ src_label = edge_result[1]
66
+ src_shape = edge_result[2]
67
+ tgt_id = edge_result[3]
68
+ tgt_label = edge_result[4]
69
+ tgt_shape = edge_result[5]
70
+ edge_label = edge_result[6]
71
+ line_type = edge_result[7]
72
+ arrow_type = edge_result[8]
73
+
74
+ # Ensure source node exists
75
+ ensure_node(diagram, src_id, src_label, src_shape)
76
+ # Ensure target node exists
77
+ ensure_node(diagram, tgt_id, tgt_label, tgt_shape)
78
+
79
+ # Track subgraph membership
80
+ if @subgraph_stack.length > 0
81
+ current_sg = @subgraph_stack[@subgraph_stack.length - 1]
82
+ if !current_sg.node_ids.include?(src_id)
83
+ current_sg.node_ids.push(src_id)
84
+ end
85
+ if !current_sg.node_ids.include?(tgt_id)
86
+ current_sg.node_ids.push(tgt_id)
87
+ end
88
+ end
89
+
90
+ edge = MermaidEdge.new(src_id, tgt_id, edge_label, line_type, arrow_type)
91
+ diagram.add_edge(edge)
92
+ next
93
+ end
94
+
95
+ # Try to parse standalone node definition
96
+ node_def = try_parse_node_def(stripped)
97
+ if node_def
98
+ n_id = node_def[0]
99
+ n_label = node_def[1]
100
+ n_shape = node_def[2]
101
+ ensure_node(diagram, n_id, n_label, n_shape)
102
+ if @subgraph_stack.length > 0
103
+ current_sg = @subgraph_stack[@subgraph_stack.length - 1]
104
+ if !current_sg.node_ids.include?(n_id)
105
+ current_sg.node_ids.push(n_id)
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ # Close any unclosed subgraphs
112
+ while @subgraph_stack.length > 0
113
+ sg = @subgraph_stack.pop
114
+ diagram.add_subgraph(sg)
115
+ end
116
+
117
+ diagram
118
+ end
119
+
120
+ def parse_direction(line)
121
+ # Look for direction after "graph" or "flowchart"
122
+ rest = ""
123
+ if line.start_with?("graph ")
124
+ rest = line[6, line.length - 6].strip
125
+ elsif line.start_with?("flowchart ")
126
+ rest = line[10, line.length - 10].strip
127
+ elsif line.start_with?("graph")
128
+ return MERMAID_DIR_TB
129
+ elsif line.start_with?("flowchart")
130
+ return MERMAID_DIR_TB
131
+ else
132
+ return MERMAID_DIR_TB
133
+ end
134
+
135
+ if rest == "BT"
136
+ MERMAID_DIR_BT
137
+ elsif rest == "LR"
138
+ MERMAID_DIR_LR
139
+ elsif rest == "RL"
140
+ MERMAID_DIR_RL
141
+ else
142
+ MERMAID_DIR_TB
143
+ end
144
+ end
145
+
146
+ def ensure_node(diagram, id, label, shape)
147
+ existing = diagram.get_node(id)
148
+ if existing
149
+ # Update label/shape if we now have better info
150
+ if label.length > 0 && existing.label == existing.id
151
+ existing.label = label
152
+ end
153
+ if shape != MERMAID_SHAPE_RECT && existing.shape == MERMAID_SHAPE_RECT
154
+ existing.shape = shape
155
+ end
156
+ return
157
+ end
158
+ lbl = label
159
+ if lbl.length == 0
160
+ lbl = id
161
+ end
162
+ node = MermaidNode.new(id, lbl, shape)
163
+ diagram.add_node(node)
164
+ @nodes[id] = node
165
+ end
166
+
167
+ # --- Edge parsing ---
168
+
169
+ def try_parse_edge(line)
170
+ # Find arrow pattern in line
171
+ # Returns [src_id, src_label, src_shape, tgt_id, tgt_label, tgt_shape, edge_label, line_type, arrow_type] or nil
172
+
173
+ # Scan for source node (ID + optional shape)
174
+ i = 0
175
+ # Skip leading whitespace
176
+ while i < line.length && line[i, 1] == " "
177
+ i = i + 1
178
+ end
179
+
180
+ src_start = i
181
+ # Scan ID characters
182
+ while i < line.length && is_id_char(line[i, 1])
183
+ i = i + 1
184
+ end
185
+ if i == src_start
186
+ return nil
187
+ end
188
+ src_id = line[src_start, i - src_start]
189
+
190
+ # Try to parse source node shape
191
+ src_shape_result = try_parse_shape(line, i)
192
+ src_label = ""
193
+ src_shape = MERMAID_SHAPE_RECT
194
+ if src_shape_result
195
+ src_label = src_shape_result[0]
196
+ src_shape = src_shape_result[1]
197
+ i = src_shape_result[2]
198
+ end
199
+
200
+ # Skip whitespace
201
+ while i < line.length && line[i, 1] == " "
202
+ i = i + 1
203
+ end
204
+
205
+ # Try to find arrow
206
+ arrow_result = try_parse_arrow(line, i)
207
+ if !arrow_result
208
+ return nil
209
+ end
210
+ arrow_len = arrow_result[0]
211
+ line_type = arrow_result[1]
212
+ arrow_type = arrow_result[2]
213
+ i = i + arrow_len
214
+
215
+ # Check for |label| after arrow
216
+ edge_label = ""
217
+ # Skip whitespace
218
+ while i < line.length && line[i, 1] == " "
219
+ i = i + 1
220
+ end
221
+ if i < line.length && line[i, 1] == "|"
222
+ label_result = parse_pipe_label(line, i)
223
+ if label_result
224
+ edge_label = label_result[0]
225
+ i = label_result[1]
226
+ end
227
+ end
228
+
229
+ # Skip whitespace
230
+ while i < line.length && line[i, 1] == " "
231
+ i = i + 1
232
+ end
233
+
234
+ # Parse target node
235
+ tgt_start = i
236
+ while i < line.length && is_id_char(line[i, 1])
237
+ i = i + 1
238
+ end
239
+ if i == tgt_start
240
+ return nil
241
+ end
242
+ tgt_id = line[tgt_start, i - tgt_start]
243
+
244
+ # Try to parse target node shape
245
+ tgt_shape_result = try_parse_shape(line, i)
246
+ tgt_label = ""
247
+ tgt_shape = MERMAID_SHAPE_RECT
248
+ if tgt_shape_result
249
+ tgt_label = tgt_shape_result[0]
250
+ tgt_shape = tgt_shape_result[1]
251
+ i = tgt_shape_result[2]
252
+ end
253
+
254
+ [src_id, src_label, src_shape, tgt_id, tgt_label, tgt_shape, edge_label, line_type, arrow_type]
255
+ end
256
+
257
+ def try_parse_arrow(line, pos)
258
+ # Returns [length, line_type, arrow_type] or nil
259
+ if pos >= line.length
260
+ return nil
261
+ end
262
+
263
+ ch = line[pos, 1]
264
+
265
+ if ch == "="
266
+ # === or ==>
267
+ if pos + 2 < line.length && line[pos + 1, 1] == "=" && line[pos + 2, 1] == "="
268
+ if pos + 3 < line.length && line[pos + 3, 1] == ">"
269
+ return [4, MERMAID_LINE_THICK, MERMAID_ARROW_ARROW]
270
+ end
271
+ return [3, MERMAID_LINE_THICK, MERMAID_ARROW_OPEN]
272
+ elsif pos + 1 < line.length && line[pos + 1, 1] == "="
273
+ if pos + 2 < line.length && line[pos + 2, 1] == ">"
274
+ return [3, MERMAID_LINE_THICK, MERMAID_ARROW_ARROW]
275
+ end
276
+ end
277
+ return nil
278
+ end
279
+
280
+ if ch == "-"
281
+ # Check -.- or -.-> (dashed)
282
+ if pos + 1 < line.length && line[pos + 1, 1] == "."
283
+ # Dashed line: scan for end
284
+ j = pos + 2
285
+ while j < line.length && line[j, 1] == "-"
286
+ j = j + 1
287
+ end
288
+ if j < line.length && line[j, 1] == ">"
289
+ return [j - pos + 1, MERMAID_LINE_DASHED, MERMAID_ARROW_ARROW]
290
+ end
291
+ if j < line.length && line[j, 1] == "."
292
+ # -.- pattern
293
+ return [j - pos + 1, MERMAID_LINE_DASHED, MERMAID_ARROW_OPEN]
294
+ end
295
+ # -.-> check
296
+ if j > pos + 2
297
+ return [j - pos, MERMAID_LINE_DASHED, MERMAID_ARROW_OPEN]
298
+ end
299
+ return nil
300
+ end
301
+
302
+ # Count consecutive dashes
303
+ j = pos
304
+ while j < line.length && line[j, 1] == "-"
305
+ j = j + 1
306
+ end
307
+ dash_count = j - pos
308
+ if dash_count < 2
309
+ return nil
310
+ end
311
+
312
+ # Check what follows the dashes
313
+ if j < line.length
314
+ next_ch = line[j, 1]
315
+ if next_ch == ">"
316
+ return [j - pos + 1, MERMAID_LINE_SOLID, MERMAID_ARROW_ARROW]
317
+ elsif next_ch == "o"
318
+ return [j - pos + 1, MERMAID_LINE_SOLID, MERMAID_ARROW_CIRCLE]
319
+ elsif next_ch == "x"
320
+ return [j - pos + 1, MERMAID_LINE_SOLID, MERMAID_ARROW_CROSS]
321
+ end
322
+ end
323
+
324
+ # Just dashes (open/no arrow)
325
+ if dash_count >= 3
326
+ return [dash_count, MERMAID_LINE_SOLID, MERMAID_ARROW_OPEN]
327
+ end
328
+
329
+ return nil
330
+ end
331
+
332
+ nil
333
+ end
334
+
335
+ def parse_pipe_label(line, pos)
336
+ # Parse |label| starting at pos where line[pos]=='|'
337
+ # Returns [label, end_pos] or nil
338
+ if pos >= line.length || line[pos, 1] != "|"
339
+ return nil
340
+ end
341
+ close = pos + 1
342
+ while close < line.length && line[close, 1] != "|"
343
+ close = close + 1
344
+ end
345
+ if close >= line.length
346
+ return nil
347
+ end
348
+ label = line[pos + 1, close - pos - 1].strip
349
+ [label, close + 1]
350
+ end
351
+
352
+ # --- Node shape parsing ---
353
+
354
+ def try_parse_shape(line, pos)
355
+ # Returns [label, shape, end_pos] or nil
356
+ if pos >= line.length
357
+ return nil
358
+ end
359
+
360
+ ch = line[pos, 1]
361
+
362
+ if ch == "["
363
+ # [[ → SUBROUTINE or [ → RECT
364
+ if pos + 1 < line.length && line[pos + 1, 1] == "["
365
+ # Subroutine [[label]]
366
+ close = find_double_close(line, pos + 2, "]")
367
+ if close >= 0
368
+ label = line[pos + 2, close - pos - 2]
369
+ return [label, MERMAID_SHAPE_SUBROUTINE, close + 2]
370
+ end
371
+ end
372
+ # Rect [label]
373
+ close = find_close_char(line, pos + 1, "]")
374
+ if close >= 0
375
+ label = line[pos + 1, close - pos - 1]
376
+ return [label, MERMAID_SHAPE_RECT, close + 1]
377
+ end
378
+
379
+ elsif ch == "("
380
+ if pos + 1 < line.length && line[pos + 1, 1] == "["
381
+ # Stadium ([label])
382
+ close = find_close_pair(line, pos + 2, "])")
383
+ if close >= 0
384
+ label = line[pos + 2, close - pos - 2]
385
+ return [label, MERMAID_SHAPE_STADIUM, close + 2]
386
+ end
387
+ elsif pos + 1 < line.length && line[pos + 1, 1] == "("
388
+ # Circle ((label))
389
+ close = find_double_close(line, pos + 2, ")")
390
+ if close >= 0
391
+ label = line[pos + 2, close - pos - 2]
392
+ return [label, MERMAID_SHAPE_CIRCLE, close + 2]
393
+ end
394
+ end
395
+ # Rounded (label)
396
+ close = find_close_char(line, pos + 1, ")")
397
+ if close >= 0
398
+ label = line[pos + 1, close - pos - 1]
399
+ return [label, MERMAID_SHAPE_ROUNDED, close + 1]
400
+ end
401
+
402
+ elsif ch == "{"
403
+ if pos + 1 < line.length && line[pos + 1, 1] == "{"
404
+ # Hexagon {{label}}
405
+ close = find_double_close(line, pos + 2, "}")
406
+ if close >= 0
407
+ label = line[pos + 2, close - pos - 2]
408
+ return [label, MERMAID_SHAPE_HEXAGON, close + 2]
409
+ end
410
+ end
411
+ # Diamond {label}
412
+ close = find_close_char(line, pos + 1, "}")
413
+ if close >= 0
414
+ label = line[pos + 1, close - pos - 1]
415
+ return [label, MERMAID_SHAPE_DIAMOND, close + 1]
416
+ end
417
+ end
418
+
419
+ nil
420
+ end
421
+
422
+ def try_parse_node_def(line)
423
+ # Parse standalone node definition: ID[label] or ID(label) etc.
424
+ # Returns [id, label, shape] or nil
425
+ i = 0
426
+ while i < line.length && line[i, 1] == " "
427
+ i = i + 1
428
+ end
429
+
430
+ start = i
431
+ while i < line.length && is_id_char(line[i, 1])
432
+ i = i + 1
433
+ end
434
+ if i == start
435
+ return nil
436
+ end
437
+ id = line[start, i - start]
438
+
439
+ shape_result = try_parse_shape(line, i)
440
+ if shape_result
441
+ return [id, shape_result[0], shape_result[1]]
442
+ end
443
+
444
+ nil
445
+ end
446
+
447
+ # --- Helper methods ---
448
+
449
+ def is_id_char(ch)
450
+ # Alphanumeric or underscore
451
+ if ch == "_"
452
+ return true
453
+ end
454
+ if is_alpha(ch)
455
+ return true
456
+ end
457
+ if is_digit_char(ch)
458
+ return true
459
+ end
460
+ false
461
+ end
462
+
463
+ def is_alpha(ch)
464
+ ch == "a" || ch == "b" || ch == "c" || ch == "d" || ch == "e" ||
465
+ ch == "f" || ch == "g" || ch == "h" || ch == "i" || ch == "j" ||
466
+ ch == "k" || ch == "l" || ch == "m" || ch == "n" || ch == "o" ||
467
+ ch == "p" || ch == "q" || ch == "r" || ch == "s" || ch == "t" ||
468
+ ch == "u" || ch == "v" || ch == "w" || ch == "x" || ch == "y" ||
469
+ ch == "z" ||
470
+ ch == "A" || ch == "B" || ch == "C" || ch == "D" || ch == "E" ||
471
+ ch == "F" || ch == "G" || ch == "H" || ch == "I" || ch == "J" ||
472
+ ch == "K" || ch == "L" || ch == "M" || ch == "N" || ch == "O" ||
473
+ ch == "P" || ch == "Q" || ch == "R" || ch == "S" || ch == "T" ||
474
+ ch == "U" || ch == "V" || ch == "W" || ch == "X" || ch == "Y" ||
475
+ ch == "Z"
476
+ end
477
+
478
+ def is_digit_char(ch)
479
+ ch == "0" || ch == "1" || ch == "2" || ch == "3" || ch == "4" ||
480
+ ch == "5" || ch == "6" || ch == "7" || ch == "8" || ch == "9"
481
+ end
482
+
483
+ def find_close_char(line, start, ch)
484
+ i = start
485
+ while i < line.length
486
+ if line[i, 1] == ch
487
+ return i
488
+ end
489
+ i = i + 1
490
+ end
491
+ -1
492
+ end
493
+
494
+ def find_double_close(line, start, ch)
495
+ # Find ]] or )) or }}
496
+ i = start
497
+ while i + 1 < line.length
498
+ if line[i, 1] == ch && line[i + 1, 1] == ch
499
+ return i
500
+ end
501
+ i = i + 1
502
+ end
503
+ -1
504
+ end
505
+
506
+ def find_close_pair(line, start, pair)
507
+ # Find ]) for stadium shapes
508
+ c1 = pair[0, 1]
509
+ c2 = pair[1, 1]
510
+ i = start
511
+ while i + 1 < line.length
512
+ if line[i, 1] == c1 && line[i + 1, 1] == c2
513
+ return i
514
+ end
515
+ i = i + 1
516
+ end
517
+ -1
518
+ end
519
+ end