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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +85 -0
- data/docs/adr/000-template.md +24 -0
- data/docs/adr/001-runtime.md +19 -0
- data/docs/adr/002-scene.md +18 -0
- data/docs/adr/003-layout.md +20 -0
- data/docs/adr/004-parser.md +19 -0
- data/docs/adr/005-public-api.md +23 -0
- data/docs/adr/README.md +10 -0
- data/docs/syntax/additional.md +58 -0
- data/docs/syntax/class.md +3 -0
- data/docs/syntax/er.md +3 -0
- data/docs/syntax/flowchart.md +16 -0
- data/docs/syntax/gantt.md +3 -0
- data/docs/syntax/mindmap.md +3 -0
- data/docs/syntax/pie.md +3 -0
- data/docs/syntax/sequence.md +14 -0
- data/docs/syntax/state.md +3 -0
- data/docs/syntax/timeline.md +3 -0
- data/docs/syntax/xychart.md +3 -0
- data/exe/merminal +7 -0
- data/lib/merminal/cli.rb +94 -0
- data/lib/merminal/diagrams/additional.rb +257 -0
- data/lib/merminal/diagrams/additional_layouts.rb +1274 -0
- data/lib/merminal/diagrams/base.rb +65 -0
- data/lib/merminal/diagrams/gantt.rb +109 -0
- data/lib/merminal/diagrams/mindmap.rb +94 -0
- data/lib/merminal/diagrams/pie.rb +64 -0
- data/lib/merminal/diagrams/sequence.rb +186 -0
- data/lib/merminal/diagrams/structure.rb +316 -0
- data/lib/merminal/diagrams/timeline.rb +60 -0
- data/lib/merminal/diagrams/xychart.rb +152 -0
- data/lib/merminal/flowchart/layout.rb +734 -0
- data/lib/merminal/flowchart.rb +279 -0
- data/lib/merminal/markdown.rb +37 -0
- data/lib/merminal/output.rb +93 -0
- data/lib/merminal/raster/box_drawing_table.rb +6 -0
- data/lib/merminal/raster.rb +175 -0
- data/lib/merminal/scene.rb +37 -0
- data/lib/merminal/shareable.rb +9 -0
- data/lib/merminal/source.rb +78 -0
- data/lib/merminal/text/east_asian_width_table.rb +8 -0
- data/lib/merminal/text.rb +110 -0
- data/lib/merminal/version.rb +5 -0
- data/lib/merminal.rb +122 -0
- data/sig/merminal.rbs +59 -0
- 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
|
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: []
|