kumiki 0.1.1

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