merminal 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 (49) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +85 -0
  5. data/docs/adr/000-template.md +24 -0
  6. data/docs/adr/001-runtime.md +19 -0
  7. data/docs/adr/002-scene.md +18 -0
  8. data/docs/adr/003-layout.md +20 -0
  9. data/docs/adr/004-parser.md +19 -0
  10. data/docs/adr/005-public-api.md +23 -0
  11. data/docs/adr/README.md +10 -0
  12. data/docs/syntax/additional.md +58 -0
  13. data/docs/syntax/class.md +3 -0
  14. data/docs/syntax/er.md +3 -0
  15. data/docs/syntax/flowchart.md +16 -0
  16. data/docs/syntax/gantt.md +3 -0
  17. data/docs/syntax/mindmap.md +3 -0
  18. data/docs/syntax/pie.md +3 -0
  19. data/docs/syntax/sequence.md +14 -0
  20. data/docs/syntax/state.md +3 -0
  21. data/docs/syntax/timeline.md +3 -0
  22. data/docs/syntax/xychart.md +3 -0
  23. data/exe/merminal +7 -0
  24. data/lib/merminal/cli.rb +94 -0
  25. data/lib/merminal/diagrams/additional.rb +257 -0
  26. data/lib/merminal/diagrams/additional_layouts.rb +1274 -0
  27. data/lib/merminal/diagrams/base.rb +65 -0
  28. data/lib/merminal/diagrams/gantt.rb +109 -0
  29. data/lib/merminal/diagrams/mindmap.rb +94 -0
  30. data/lib/merminal/diagrams/pie.rb +64 -0
  31. data/lib/merminal/diagrams/sequence.rb +186 -0
  32. data/lib/merminal/diagrams/structure.rb +316 -0
  33. data/lib/merminal/diagrams/timeline.rb +60 -0
  34. data/lib/merminal/diagrams/xychart.rb +152 -0
  35. data/lib/merminal/flowchart/layout.rb +734 -0
  36. data/lib/merminal/flowchart.rb +279 -0
  37. data/lib/merminal/markdown.rb +37 -0
  38. data/lib/merminal/output.rb +93 -0
  39. data/lib/merminal/raster/box_drawing_table.rb +6 -0
  40. data/lib/merminal/raster.rb +175 -0
  41. data/lib/merminal/scene.rb +37 -0
  42. data/lib/merminal/shareable.rb +9 -0
  43. data/lib/merminal/source.rb +78 -0
  44. data/lib/merminal/text/east_asian_width_table.rb +8 -0
  45. data/lib/merminal/text.rb +110 -0
  46. data/lib/merminal/version.rb +5 -0
  47. data/lib/merminal.rb +122 -0
  48. data/sig/merminal.rbs +59 -0
  49. metadata +88 -0
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Merminal
6
+ # A nonfatal parser finding with original source coordinates.
7
+ Diagnostic = Data.define(:severity, :message, :line, :column, :length) do
8
+ def format(path = "<input>", source = nil)
9
+ header = "#{path}:#{line}:#{column}: #{severity}: #{message}"
10
+ return header unless source && line && source.lines[line - 1]
11
+
12
+ line_text = source.lines[line - 1].chomp
13
+ "#{header}\n #{line} | #{line_text}\n | #{' ' * [column - 1, 0].max}#{'^' * [length || 1, 1].max}"
14
+ end
15
+
16
+ alias to_s format
17
+ end
18
+
19
+ # Prepared Mermaid source with original line numbers.
20
+ Source = Data.define(:lines, :title, :directives, :line_map, :original, :diagnostics) do
21
+ def self.parse(input)
22
+ original = input.to_s.encode("UTF-8", invalid: :replace, undef: :replace).gsub(/\r\n?/, "\n")
23
+ rows = original.lines
24
+ title = nil
25
+ directives = {}
26
+ diagnostics = []
27
+ first = 0
28
+ if rows.first&.chomp == "---"
29
+ closing = (1...rows.length).find { |index| rows[index].chomp == "---" }
30
+ if closing
31
+ rows[1...closing].each_with_index do |line, index|
32
+ if line =~ /\A\s*title:\s*(.*?)\s*\z/
33
+ title = Regexp.last_match(1).delete_prefix('"').delete_suffix('"').delete_prefix("'").delete_suffix("'")
34
+ elsif line.include?(":")
35
+ diagnostics << Diagnostic.new(severity: :info, message: "frontmatter key ignored", line: index + 2, column: 1, length: 1)
36
+ end
37
+ end
38
+ first = closing + 1
39
+ else
40
+ diagnostics << Diagnostic.new(severity: :error, message: "unclosed frontmatter", line: 1, column: 1, length: 3)
41
+ first = rows.length
42
+ end
43
+ end
44
+ lines = []
45
+ line_map = []
46
+ rows.each_with_index do |row, index|
47
+ next if index < first
48
+
49
+ stripped = row.strip
50
+ if stripped.start_with?("%%{")
51
+ begin
52
+ payload = stripped.delete_prefix("%%{").delete_suffix("}%%")
53
+ key, body = payload.split(":", 2)
54
+ raise JSON::ParserError unless key == "init" && body && stripped.end_with?("}%%")
55
+
56
+ parsed = JSON.parse(body.tr("'", '"'))
57
+ raise JSON::ParserError unless parsed.is_a?(Hash)
58
+ directives.merge!(parsed)
59
+ parsed.each_key { |name| diagnostics << Diagnostic.new(severity: :info, message: "init key #{name} ignored", line: index + 1, column: 1, length: row.length) unless name == "theme" }
60
+ rescue JSON::ParserError
61
+ diagnostics << Diagnostic.new(severity: :warning, message: "invalid init directive", line: index + 1, column: 1, length: row.length)
62
+ end
63
+ next
64
+ end
65
+ next if stripped.empty? || stripped.start_with?("%%")
66
+
67
+ lines << row.chomp
68
+ line_map << index + 1
69
+ end
70
+ new(lines: lines.freeze, title: title, directives: directives.freeze, line_map: line_map.freeze,
71
+ original: original.freeze, diagnostics: diagnostics.freeze)
72
+ end
73
+ end
74
+
75
+ class Error < StandardError; end
76
+ class SyntaxError < Error; end
77
+ class UnsupportedDiagramError < Error; end
78
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+ # Unicode 17.0.0; generated from UCD files.
3
+
4
+ module Merminal::Text
5
+ WIDE = Merminal::Shareable.make([[4352, 4447], [8986, 8987], [9001, 9002], [9193, 9196], [9200, 9200], [9203, 9203], [9725, 9726], [9748, 9749], [9776, 9783], [9800, 9811], [9855, 9855], [9866, 9871], [9875, 9875], [9889, 9889], [9898, 9899], [9917, 9918], [9924, 9925], [9934, 9934], [9940, 9940], [9962, 9962], [9970, 9971], [9973, 9973], [9978, 9978], [9981, 9981], [9989, 9989], [9994, 9995], [10024, 10024], [10060, 10060], [10062, 10062], [10067, 10069], [10071, 10071], [10133, 10135], [10160, 10160], [10175, 10175], [11035, 11036], [11088, 11088], [11093, 11093], [11904, 11929], [11931, 12019], [12032, 12245], [12272, 12350], [12353, 12438], [12441, 12543], [12549, 12591], [12593, 12686], [12688, 12773], [12783, 12830], [12832, 12871], [12880, 42124], [42128, 42182], [43360, 43388], [44032, 55203], [63744, 64255], [65040, 65049], [65072, 65106], [65108, 65126], [65128, 65131], [65281, 65376], [65504, 65510], [94176, 94180], [94192, 94198], [94208, 101589], [101631, 101662], [101760, 101874], [110576, 110579], [110581, 110587], [110589, 110590], [110592, 110882], [110898, 110898], [110928, 110930], [110933, 110933], [110948, 110951], [110960, 111355], [119552, 119638], [119648, 119670], [126980, 126980], [127183, 127183], [127374, 127374], [127377, 127386], [127488, 127490], [127504, 127547], [127552, 127560], [127568, 127569], [127584, 127589], [127744, 127776], [127789, 127797], [127799, 127868], [127870, 127891], [127904, 127946], [127951, 127955], [127968, 127984], [127988, 127988], [127992, 128062], [128064, 128064], [128066, 128252], [128255, 128317], [128331, 128334], [128336, 128359], [128378, 128378], [128405, 128406], [128420, 128420], [128507, 128591], [128640, 128709], [128716, 128716], [128720, 128722], [128725, 128728], [128732, 128735], [128747, 128748], [128756, 128764], [128992, 129003], [129008, 129008], [129292, 129338], [129340, 129349], [129351, 129535], [129648, 129660], [129664, 129674], [129678, 129734], [129736, 129736], [129741, 129756], [129759, 129770], [129775, 129784], [131072, 196605], [196608, 262141]])
6
+ AMBIGUOUS = Merminal::Shareable.make([[161, 161], [164, 164], [167, 168], [170, 170], [173, 174], [176, 180], [182, 186], [188, 191], [198, 198], [208, 208], [215, 216], [222, 225], [230, 230], [232, 234], [236, 237], [240, 240], [242, 243], [247, 250], [252, 252], [254, 254], [257, 257], [273, 273], [275, 275], [283, 283], [294, 295], [299, 299], [305, 307], [312, 312], [319, 322], [324, 324], [328, 331], [333, 333], [338, 339], [358, 359], [363, 363], [462, 462], [464, 464], [466, 466], [468, 468], [470, 470], [472, 472], [474, 474], [476, 476], [593, 593], [609, 609], [708, 708], [711, 711], [713, 715], [717, 717], [720, 720], [728, 731], [733, 733], [735, 735], [768, 879], [913, 929], [931, 937], [945, 961], [963, 969], [1025, 1025], [1040, 1103], [1105, 1105], [8208, 8208], [8211, 8214], [8216, 8217], [8220, 8221], [8224, 8226], [8228, 8231], [8240, 8240], [8242, 8243], [8245, 8245], [8251, 8251], [8254, 8254], [8308, 8308], [8319, 8319], [8321, 8324], [8364, 8364], [8451, 8451], [8453, 8453], [8457, 8457], [8467, 8467], [8470, 8470], [8481, 8482], [8486, 8486], [8491, 8491], [8531, 8532], [8539, 8542], [8544, 8555], [8560, 8569], [8585, 8585], [8592, 8601], [8632, 8633], [8658, 8658], [8660, 8660], [8679, 8679], [8704, 8704], [8706, 8707], [8711, 8712], [8715, 8715], [8719, 8719], [8721, 8721], [8725, 8725], [8730, 8730], [8733, 8736], [8739, 8739], [8741, 8741], [8743, 8748], [8750, 8750], [8756, 8759], [8764, 8765], [8776, 8776], [8780, 8780], [8786, 8786], [8800, 8801], [8804, 8807], [8810, 8811], [8814, 8815], [8834, 8835], [8838, 8839], [8853, 8853], [8857, 8857], [8869, 8869], [8895, 8895], [8978, 8978], [9312, 9449], [9451, 9547], [9552, 9587], [9600, 9615], [9618, 9621], [9632, 9633], [9635, 9641], [9650, 9651], [9654, 9655], [9660, 9661], [9664, 9665], [9670, 9672], [9675, 9675], [9678, 9681], [9698, 9701], [9711, 9711], [9733, 9734], [9737, 9737], [9742, 9743], [9756, 9756], [9758, 9758], [9792, 9792], [9794, 9794], [9824, 9825], [9827, 9829], [9831, 9834], [9836, 9837], [9839, 9839], [9886, 9887], [9919, 9919], [9926, 9933], [9935, 9939], [9941, 9953], [9955, 9955], [9960, 9961], [9963, 9969], [9972, 9972], [9974, 9977], [9979, 9980], [9982, 9983], [10045, 10045], [10102, 10111], [11094, 11097], [12872, 12879], [57344, 63743], [65024, 65039], [65533, 65533], [127232, 127242], [127248, 127277], [127280, 127337], [127344, 127373], [127375, 127376], [127387, 127404], [917760, 917999], [983040, 1048573], [1048576, 1114109]])
7
+ ZERO = Merminal::Shareable.make([[768, 879], [1155, 1161], [1425, 1469], [1471, 1471], [1473, 1474], [1476, 1477], [1479, 1479], [1552, 1562], [1611, 1631], [1648, 1648], [1750, 1756], [1759, 1764], [1767, 1768], [1770, 1773], [1809, 1809], [1840, 1866], [1958, 1968], [2027, 2035], [2045, 2045], [2070, 2073], [2075, 2083], [2085, 2087], [2089, 2093], [2137, 2139], [2199, 2207], [2250, 2273], [2275, 2306], [2362, 2362], [2364, 2364], [2369, 2376], [2381, 2381], [2385, 2391], [2402, 2403], [2433, 2433], [2492, 2492], [2497, 2500], [2509, 2509], [2530, 2531], [2558, 2558], [2561, 2562], [2620, 2620], [2625, 2626], [2631, 2632], [2635, 2637], [2641, 2641], [2672, 2673], [2677, 2677], [2689, 2690], [2748, 2748], [2753, 2757], [2759, 2760], [2765, 2765], [2786, 2787], [2810, 2815], [2817, 2817], [2876, 2876], [2879, 2879], [2881, 2884], [2893, 2893], [2901, 2902], [2914, 2915], [2946, 2946], [3008, 3008], [3021, 3021], [3072, 3072], [3076, 3076], [3132, 3132], [3134, 3136], [3142, 3144], [3146, 3149], [3157, 3158], [3170, 3171], [3201, 3201], [3260, 3260], [3263, 3263], [3270, 3270], [3276, 3277], [3298, 3299], [3328, 3329], [3387, 3388], [3393, 3396], [3405, 3405], [3426, 3427], [3457, 3457], [3530, 3530], [3538, 3540], [3542, 3542], [3633, 3633], [3636, 3642], [3655, 3662], [3761, 3761], [3764, 3772], [3784, 3790], [3864, 3865], [3893, 3893], [3895, 3895], [3897, 3897], [3953, 3966], [3968, 3972], [3974, 3975], [3981, 3991], [3993, 4028], [4038, 4038], [4141, 4144], [4146, 4151], [4153, 4154], [4157, 4158], [4184, 4185], [4190, 4192], [4209, 4212], [4226, 4226], [4229, 4230], [4237, 4237], [4253, 4253], [4957, 4959], [5906, 5908], [5938, 5939], [5970, 5971], [6002, 6003], [6068, 6069], [6071, 6077], [6086, 6086], [6089, 6099], [6109, 6109], [6155, 6157], [6159, 6159], [6277, 6278], [6313, 6313], [6432, 6434], [6439, 6440], [6450, 6450], [6457, 6459], [6679, 6680], [6683, 6683], [6742, 6742], [6744, 6750], [6752, 6752], [6754, 6754], [6757, 6764], [6771, 6780], [6783, 6783], [6832, 6877], [6880, 6891], [6912, 6915], [6964, 6964], [6966, 6970], [6972, 6972], [6978, 6978], [7019, 7027], [7040, 7041], [7074, 7077], [7080, 7081], [7083, 7085], [7142, 7142], [7144, 7145], [7149, 7149], [7151, 7153], [7212, 7219], [7222, 7223], [7376, 7378], [7380, 7392], [7394, 7400], [7405, 7405], [7412, 7412], [7416, 7417], [7616, 7679], [8205, 8205], [8400, 8432], [11503, 11505], [11647, 11647], [11744, 11775], [12330, 12333], [12441, 12442], [42607, 42610], [42612, 42621], [42654, 42655], [42736, 42737], [43010, 43010], [43014, 43014], [43019, 43019], [43045, 43046], [43052, 43052], [43204, 43205], [43232, 43249], [43263, 43263], [43302, 43309], [43335, 43345], [43392, 43394], [43443, 43443], [43446, 43449], [43452, 43453], [43493, 43493], [43561, 43566], [43569, 43570], [43573, 43574], [43587, 43587], [43596, 43596], [43644, 43644], [43696, 43696], [43698, 43700], [43703, 43704], [43710, 43711], [43713, 43713], [43756, 43757], [43766, 43766], [44005, 44005], [44008, 44008], [44013, 44013], [64286, 64286], [65024, 65039], [65056, 65071], [66045, 66045], [66272, 66272], [66422, 66426], [68097, 68099], [68101, 68102], [68108, 68111], [68152, 68154], [68159, 68159], [68325, 68326], [68900, 68903], [68969, 68973], [69291, 69292], [69370, 69375], [69446, 69456], [69506, 69509], [69633, 69633], [69688, 69702], [69744, 69744], [69747, 69748], [69759, 69761], [69811, 69814], [69817, 69818], [69826, 69826], [69888, 69890], [69927, 69931], [69933, 69940], [70003, 70003], [70016, 70017], [70070, 70078], [70089, 70092], [70095, 70095], [70191, 70193], [70196, 70196], [70198, 70199], [70206, 70206], [70209, 70209], [70367, 70367], [70371, 70378], [70400, 70401], [70459, 70460], [70464, 70464], [70502, 70508], [70512, 70516], [70587, 70592], [70606, 70606], [70608, 70608], [70610, 70610], [70625, 70626], [70712, 70719], [70722, 70724], [70726, 70726], [70750, 70750], [70835, 70840], [70842, 70842], [70847, 70848], [70850, 70851], [71090, 71093], [71100, 71101], [71103, 71104], [71132, 71133], [71219, 71226], [71229, 71229], [71231, 71232], [71339, 71339], [71341, 71341], [71344, 71349], [71351, 71351], [71453, 71453], [71455, 71455], [71458, 71461], [71463, 71467], [71727, 71735], [71737, 71738], [71995, 71996], [71998, 71998], [72003, 72003], [72148, 72151], [72154, 72155], [72160, 72160], [72193, 72202], [72243, 72248], [72251, 72254], [72263, 72263], [72273, 72278], [72281, 72283], [72330, 72342], [72344, 72345], [72544, 72544], [72546, 72548], [72550, 72550], [72752, 72758], [72760, 72765], [72767, 72767], [72850, 72871], [72874, 72880], [72882, 72883], [72885, 72886], [73009, 73014], [73018, 73018], [73020, 73021], [73023, 73029], [73031, 73031], [73104, 73105], [73109, 73109], [73111, 73111], [73459, 73460], [73472, 73473], [73526, 73530], [73536, 73536], [73538, 73538], [73562, 73562], [78912, 78912], [78919, 78933], [90398, 90409], [90413, 90415], [92912, 92916], [92976, 92982], [94031, 94031], [94095, 94098], [94180, 94180], [113821, 113822], [118528, 118573], [118576, 118598], [119143, 119145], [119163, 119170], [119173, 119179], [119210, 119213], [119362, 119364], [121344, 121398], [121403, 121452], [121461, 121461], [121476, 121476], [121499, 121503], [121505, 121519], [122880, 122886], [122888, 122904], [122907, 122913], [122915, 122916], [122918, 122922], [123023, 123023], [123184, 123190], [123566, 123566], [123628, 123631], [124140, 124143], [124398, 124399], [124643, 124643], [124646, 124646], [124654, 124655], [124661, 124661], [125136, 125142], [125252, 125258], [917760, 917999]])
8
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "shareable"
4
+
5
+ module Merminal
6
+ # Terminal display widths, measured in cells rather than code points.
7
+ module Text
8
+ require_relative "text/east_asian_width_table"
9
+
10
+ module_function
11
+
12
+ def in_ranges?(code, ranges)
13
+ low = 0
14
+ high = ranges.length - 1
15
+ while low <= high
16
+ mid = (low + high) / 2
17
+ first, last = ranges[mid]
18
+ return true if code >= first && code <= last
19
+
20
+ code < first ? high = mid - 1 : low = mid + 1
21
+ end
22
+ false
23
+ end
24
+
25
+ def each_cell(string, ambiguous_width: 1)
26
+ return enum_for(__method__, string, ambiguous_width: ambiguous_width) unless block_given?
27
+
28
+ string.to_s.scrub.scan(/\X/).each do |cluster|
29
+ code = cluster.ord
30
+ width = if in_ranges?(code, ZERO)
31
+ 0
32
+ elsif in_ranges?(code, WIDE) || (ambiguous_width == 2 && in_ranges?(code, AMBIGUOUS))
33
+ 2
34
+ else
35
+ 1
36
+ end
37
+ yield cluster, width
38
+ end
39
+ end
40
+
41
+ def width(string, ambiguous_width: 1)
42
+ each_cell(string, ambiguous_width: ambiguous_width).sum { |_, cells| cells }
43
+ end
44
+
45
+ def truncate(string, max_width, omission: "…", ambiguous_width: 1)
46
+ return string if width(string, ambiguous_width: ambiguous_width) <= max_width
47
+
48
+ remaining = [max_width - width(omission, ambiguous_width: ambiguous_width), 0].max
49
+ result = +""
50
+ each_cell(string, ambiguous_width: ambiguous_width) do |cluster, cells|
51
+ break if cells > remaining
52
+
53
+ result << cluster
54
+ remaining -= cells
55
+ end
56
+ result + (max_width >= width(omission, ambiguous_width: ambiguous_width) ? omission : "")
57
+ end
58
+
59
+ def pad(string, target_width, align: :left, ambiguous_width: 1)
60
+ spaces = [target_width - width(string, ambiguous_width: ambiguous_width), 0].max
61
+ left = case align
62
+ when :right then spaces
63
+ when :center then spaces / 2
64
+ else 0
65
+ end
66
+ (" " * left) + string + (" " * (spaces - left))
67
+ end
68
+
69
+ def wrap(string, max_width, ambiguous_width: 1)
70
+ raise ArgumentError, "width must be positive" unless max_width.positive?
71
+
72
+ string.to_s.split("\n", -1).flat_map do |line|
73
+ result = []
74
+ current = +""
75
+ used = 0
76
+ line.scan(/\S+|\s+/).each do |token|
77
+ token_width = width(token, ambiguous_width: ambiguous_width)
78
+ if token.match?(/\A\s+\z/)
79
+ if used.positive? && used + token_width <= max_width
80
+ current << token
81
+ used += token_width
82
+ end
83
+ next
84
+ end
85
+ if used + token_width > max_width && used.positive?
86
+ result << current.rstrip
87
+ current = +""
88
+ used = 0
89
+ end
90
+ if token_width <= max_width
91
+ current << token
92
+ used += token_width
93
+ else
94
+ each_cell(token, ambiguous_width: ambiguous_width) do |cluster, cells|
95
+ if used + cells > max_width && used.positive?
96
+ result << current.rstrip
97
+ current = +""
98
+ used = 0
99
+ end
100
+ current << cluster
101
+ used += cells
102
+ end
103
+ end
104
+ end
105
+ result << current
106
+ result
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Merminal
4
+ VERSION = "0.1.0"
5
+ end
data/lib/merminal.rb ADDED
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "merminal/version"
4
+ require_relative "merminal/shareable"
5
+ require_relative "merminal/text"
6
+ require_relative "merminal/source"
7
+ require_relative "merminal/markdown"
8
+ require_relative "merminal/scene"
9
+ require_relative "merminal/raster"
10
+ require_relative "merminal/output"
11
+ require_relative "merminal/flowchart"
12
+ require_relative "merminal/diagrams/base"
13
+ require_relative "merminal/diagrams/pie"
14
+ require_relative "merminal/diagrams/timeline"
15
+ require_relative "merminal/diagrams/mindmap"
16
+ require_relative "merminal/diagrams/xychart"
17
+ require_relative "merminal/diagrams/gantt"
18
+ require_relative "merminal/diagrams/sequence"
19
+ require_relative "merminal/diagrams/structure"
20
+ require_relative "merminal/diagrams/additional"
21
+ require_relative "merminal/diagrams/additional_layouts"
22
+
23
+ # Pure Ruby terminal renderer for a documented subset of Mermaid.
24
+ module Merminal
25
+ @plugins = []
26
+
27
+ class << self
28
+ # Register a diagram plugin with keywords, parse and layout methods.
29
+ def register(plugin)
30
+ raise ArgumentError, "duplicate diagram keyword" if (@plugins.flat_map(&:keywords) & plugin.keywords).any?
31
+
32
+ @plugins << plugin
33
+ end
34
+
35
+ # Return supported diagram type symbols in registration order.
36
+ def diagram_types
37
+ @plugins.map(&:diagram_type)
38
+ end
39
+
40
+ # Parse a Mermaid document and collect recoverable diagnostics.
41
+ def parse(input, strict: false)
42
+ source = Shareable.make(Source.parse(input))
43
+ keyword = source.lines.first.to_s.lstrip[/\A[^\s;]+/]
44
+ plugin = @plugins.find { |candidate| candidate.keywords.include?(keyword) }
45
+ raise UnsupportedDiagramError, "unsupported diagram: #{keyword || '(empty)'}" unless plugin
46
+
47
+ ast, findings = plugin.parse(source)
48
+ ast = Shareable.make(ast)
49
+ diagnostics = Shareable.make(source.diagnostics + findings)
50
+ raise SyntaxError, diagnostics.select { |d| d.severity == :error }.map(&:message).join("; ") if strict && diagnostics.any? { |d| d.severity == :error }
51
+
52
+ Document.new(type: plugin.diagram_type, ast: ast, diagnostics: diagnostics, source: source, plugin: plugin)
53
+ end
54
+
55
+ # Render Mermaid text to Unicode or ASCII terminal text.
56
+ def render(input, **options)
57
+ strict = options.delete(:strict) || false
58
+ parse(input, strict: strict).render(**options)
59
+ end
60
+
61
+ # Extract Mermaid fences from Markdown in source order.
62
+ def markdown_blocks(markdown)
63
+ Markdown.blocks(markdown)
64
+ end
65
+ end
66
+
67
+ # Parsed document; the Scene can be rendered with different options.
68
+ Document = Data.define(:type, :ast, :diagnostics, :source, :plugin) do
69
+ def scene(**options)
70
+ picture = plugin.layout(ast, **options)
71
+ return Shareable.make(picture) unless source.title
72
+
73
+ title = Scene::Text.new(x: 0, y: 0, string: source.title, role: :emphasis, layer: :label, emphasis: nil)
74
+ Shareable.make(Scene.new(width: [picture.width, Text.width(source.title)].max, height: picture.height + 1,
75
+ items: ([title] + picture.items.map { |item| Scene.translate(item, dy: 1) }).freeze))
76
+ end
77
+
78
+ def render(width: nil, fit: :compact, compact: false, **options)
79
+ unless width && fit
80
+ preset = compact ? { node_gap: 1, rank_gap: 1, node_padding_x: 1, max_label_width: 12 } : {}
81
+ return render_once(**options, **preset)
82
+ end
83
+
84
+ raise ArgumentError, "width must be positive" unless width.positive?
85
+
86
+ presets = [
87
+ {}, { node_gap: 1, rank_gap: 1 }, { node_gap: 1, rank_gap: 1, node_padding_x: 1 },
88
+ { node_gap: 1, rank_gap: 1, node_padding_x: 1, max_label_width: 16 },
89
+ { node_gap: 1, rank_gap: 1, node_padding_x: 1, max_label_width: 12 }
90
+ ]
91
+ presets = [presets.last] if compact
92
+ presets << presets.last.merge(direction: :TB) if fit == :rotate && type == :flowchart && ast.direction == :LR
93
+ rendered = nil
94
+ presets.each do |preset|
95
+ rendered = render_once(**options, **preset)
96
+ break if rendered.lines.all? { |line| Text.width(line.gsub(/\e\[[\d;]*m/, "")) <= width }
97
+ end
98
+ rendered
99
+ end
100
+
101
+ def render_once(**options)
102
+ charset = options.fetch(:charset, :unicode)
103
+ picture = scene(**options)
104
+ grid = Raster.rasterize(picture, charset: charset, rounded: options.fetch(:rounded, true),
105
+ ambiguous_width: options.fetch(:ambiguous_width, 1), crossings: options.fetch(:crossings, :plain))
106
+ theme = options[:theme] || source.directives.fetch("theme", :default)
107
+ theme = :default unless theme.respond_to?(:to_sym) && Output::THEMES.key?(theme.to_sym)
108
+ Output.render(grid, charset: charset, color: options.fetch(:color, false), theme: theme)
109
+ end
110
+ end
111
+ end
112
+
113
+ Merminal.register(Merminal::Flowchart)
114
+ [
115
+ Merminal::Diagrams::Sequence, Merminal::Diagrams::State, Merminal::Diagrams::ClassDiagram,
116
+ Merminal::Diagrams::ER, Merminal::Diagrams::Pie, Merminal::Diagrams::XYChart,
117
+ Merminal::Diagrams::Gantt, Merminal::Diagrams::Timeline, Merminal::Diagrams::Mindmap
118
+ ].each { |plugin| Merminal.register(plugin) }
119
+ Merminal::Diagrams::Additional::KEYWORDS.each do |keyword|
120
+ aliases = Merminal::Diagrams::Additional::ALIASES.fetch(keyword, [keyword])
121
+ Merminal.register(Merminal::Diagrams::Additional.plugin(keyword, aliases: aliases))
122
+ end
data/sig/merminal.rbs ADDED
@@ -0,0 +1,59 @@
1
+ module Merminal
2
+ type charset = :unicode | :ascii
3
+ type color = bool | :auto | :always | :never
4
+ type theme = :default | :mono | :high_contrast | :solarized | String
5
+
6
+ VERSION: String
7
+
8
+ def self.render: (String source, ?charset: charset, ?color: color, ?theme: theme,
9
+ ?width: Integer?, ?rounded: bool, ?strict: bool, **untyped options) -> String
10
+ def self.parse: (String source, ?strict: bool) -> Document
11
+ def self.markdown_blocks: (String markdown) -> Array[MarkdownBlock]
12
+ def self.diagram_types: () -> Array[Symbol]
13
+ def self.register: (untyped plugin) -> untyped
14
+
15
+ class Error < StandardError
16
+ end
17
+
18
+ class SyntaxError < Error
19
+ end
20
+
21
+ class UnsupportedDiagramError < Error
22
+ end
23
+
24
+ class Document
25
+ def type: () -> Symbol
26
+ def diagnostics: () -> Array[Diagnostic]
27
+ def scene: (**untyped options) -> Scene
28
+ def render: (?charset: charset, ?color: color, ?theme: theme, ?width: Integer?,
29
+ ?rounded: bool, **untyped options) -> String
30
+ end
31
+
32
+ class Diagnostic
33
+ def severity: () -> Symbol
34
+ def message: () -> String
35
+ def line: () -> Integer
36
+ def column: () -> Integer
37
+ def format: (?String path, ?String source) -> String
38
+ end
39
+
40
+ class MarkdownBlock
41
+ def line: () -> Integer
42
+ def source: () -> String
43
+ def render: (?charset: charset, ?color: color, ?theme: theme, ?width: Integer?,
44
+ ?rounded: bool, ?strict: bool, **untyped options) -> String
45
+ end
46
+
47
+ class Scene
48
+ def width: () -> Integer
49
+ def height: () -> Integer
50
+ def items: () -> Array[untyped]
51
+ end
52
+
53
+ module Text
54
+ def self.width: (String string, ?ambiguous_width: Integer) -> Integer
55
+ def self.wrap: (String string, Integer max_width, ?ambiguous_width: Integer) -> Array[String]
56
+ def self.truncate: (String string, Integer max_width, ?omission: String, ?ambiguous_width: Integer) -> String
57
+ def self.pad: (String string, Integer target_width, ?align: Symbol, ?ambiguous_width: Integer) -> String
58
+ end
59
+ end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: merminal
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yudai Takada
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: A pure Ruby Mermaid diagram renderer for Unicode and ASCII terminals.
13
+ email:
14
+ - t.yudai92@gmail.com
15
+ executables:
16
+ - merminal
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - CHANGELOG.md
21
+ - LICENSE.txt
22
+ - README.md
23
+ - docs/adr/000-template.md
24
+ - docs/adr/001-runtime.md
25
+ - docs/adr/002-scene.md
26
+ - docs/adr/003-layout.md
27
+ - docs/adr/004-parser.md
28
+ - docs/adr/005-public-api.md
29
+ - docs/adr/README.md
30
+ - docs/syntax/additional.md
31
+ - docs/syntax/class.md
32
+ - docs/syntax/er.md
33
+ - docs/syntax/flowchart.md
34
+ - docs/syntax/gantt.md
35
+ - docs/syntax/mindmap.md
36
+ - docs/syntax/pie.md
37
+ - docs/syntax/sequence.md
38
+ - docs/syntax/state.md
39
+ - docs/syntax/timeline.md
40
+ - docs/syntax/xychart.md
41
+ - exe/merminal
42
+ - lib/merminal.rb
43
+ - lib/merminal/cli.rb
44
+ - lib/merminal/diagrams/additional.rb
45
+ - lib/merminal/diagrams/additional_layouts.rb
46
+ - lib/merminal/diagrams/base.rb
47
+ - lib/merminal/diagrams/gantt.rb
48
+ - lib/merminal/diagrams/mindmap.rb
49
+ - lib/merminal/diagrams/pie.rb
50
+ - lib/merminal/diagrams/sequence.rb
51
+ - lib/merminal/diagrams/structure.rb
52
+ - lib/merminal/diagrams/timeline.rb
53
+ - lib/merminal/diagrams/xychart.rb
54
+ - lib/merminal/flowchart.rb
55
+ - lib/merminal/flowchart/layout.rb
56
+ - lib/merminal/markdown.rb
57
+ - lib/merminal/output.rb
58
+ - lib/merminal/raster.rb
59
+ - lib/merminal/raster/box_drawing_table.rb
60
+ - lib/merminal/scene.rb
61
+ - lib/merminal/shareable.rb
62
+ - lib/merminal/source.rb
63
+ - lib/merminal/text.rb
64
+ - lib/merminal/text/east_asian_width_table.rb
65
+ - lib/merminal/version.rb
66
+ - sig/merminal.rbs
67
+ homepage: https://github.com/ydah/merminal
68
+ licenses:
69
+ - MIT
70
+ metadata: {}
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '3.3'
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubygems_version: 4.0.16
86
+ specification_version: 4
87
+ summary: Render Mermaid diagrams as terminal text
88
+ test_files: []