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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cbe165ab1447fb74478d15f529f288a7693359b49df68e981ace194c9a4d543f
4
+ data.tar.gz: d77e448ca8c2b4300c5468040c259ff7aeaf48b190ba35c66bac413feef88594
5
+ SHA512:
6
+ metadata.gz: cd06fee96beec91b0eaee71c387811fac366844213f2e4fa635840b31d2986c27a8f12762410e27d48878867e936f52fb1a83a374d70272b805dca3e0bc195f0
7
+ data.tar.gz: 65b2310ad2a6d7576fb3bc9be2f4748a1db423f0f55e7bc07e2e73f9947d62b136ac569d201dca385b16ef8d006df323d1774c808a51c17f78d0e647a0a8fe93
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2026-09-22
4
+
5
+ - Initial release.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Yudai Takada
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # merminal
2
+
3
+ merminal is a pure Ruby terminal renderer for Mermaid diagrams. It runs without Node.js, a browser, external processes, or runtime gem dependencies. The Ruby API namespace is `Merminal`.
4
+
5
+ ## Install
6
+
7
+ From this checkout:
8
+
9
+ ```sh
10
+ gem build merminal.gemspec
11
+ gem install ./merminal-0.1.0.gem
12
+ ```
13
+
14
+ Ruby 3.3 or newer is required.
15
+
16
+ ## CLI
17
+
18
+ ```sh
19
+ printf 'flowchart LR\nA[Start] --> B[Done]\n' | merminal
20
+ merminal --ascii diagram.mmd
21
+ merminal --check --strict diagram.mmd
22
+ merminal --markdown README.md
23
+ merminal --width 80 --theme solarized --color always diagram.mmd
24
+ ```
25
+
26
+ `merminal --help` lists every option. When a requested width cannot hold the diagram, the CLI reports the actual width and keeps the full drawing.
27
+
28
+ ## Ruby API
29
+
30
+ ```ruby
31
+ require "merminal"
32
+
33
+ puts Merminal.render("flowchart LR\nA --> B")
34
+ document = Merminal.parse("pie\n\"A\" : 3\n\"B\" : 7")
35
+ warn document.diagnostics.map(&:to_s).join("\n")
36
+ puts document.render(charset: :ascii)
37
+
38
+ Merminal.markdown_blocks("```mermaid\ngraph LR\nA-->B\n```").each do |block|
39
+ puts block.line, block.render
40
+ end
41
+ ```
42
+
43
+ `Merminal.register` accepts a plugin with `diagram_type`, `keywords`, `parse(source)`, and `layout(ast, **options)` methods. `Document#scene` exposes immutable drawing primitives. The Scene and plugin interfaces are experimental.
44
+
45
+ ## Supported diagrams
46
+
47
+ The parser accepts the Mermaid diagram declarations listed below. The core types
48
+ have dedicated parsers and layouts; the additional syntax types also have
49
+ diagram-specific semantic handling and terminal layouts. Core parser errors produce
50
+ line numbered diagnostics; additional syntax follows the documented subset and ignores
51
+ browser-only directives that have no terminal representation.
52
+
53
+ | Diagram | Syntax |
54
+ |---|---|
55
+ | Flowchart | [Flowchart](docs/syntax/flowchart.md) |
56
+ | Sequence | [Sequence](docs/syntax/sequence.md) |
57
+ | State | [State](docs/syntax/state.md) |
58
+ | Class | [Class](docs/syntax/class.md) |
59
+ | ER | [ER](docs/syntax/er.md) |
60
+ | Pie | [Pie](docs/syntax/pie.md) |
61
+ | XY chart | [XY chart](docs/syntax/xychart.md) |
62
+ | Gantt | [Gantt](docs/syntax/gantt.md) |
63
+ | Timeline | [Timeline](docs/syntax/timeline.md) |
64
+ | Mindmap | [Mindmap](docs/syntax/mindmap.md) |
65
+ | Additional syntax diagrams | [Use case, requirement, journey, GitGraph, C4, quadrant, Sankey, block, packet, Kanban, architecture, radar, treemap, Venn, Ishikawa, Wardley, TreeView, Cynefin, swimlane, Event Modeling, Agentflow, ZenUML, Railroad syntax diagrams](docs/syntax/additional.md) |
66
+
67
+ The [gallery](gallery.html) shows Unicode, ASCII, and the four color themes for every fixture.
68
+
69
+ Pie charts appear as horizontal bars because cell based terminals communicate proportions more clearly that way. Unicode ambiguous width defaults to one cell; set `ambiguous_width: 2` for terminals that use two. Emoji ZWJ sequences use the first grapheme code point's width, so some terminal fonts may differ. ASCII mode replaces non ASCII labels with `?`.
70
+
71
+ This is an independent implementation. Thanks to [termaid](https://github.com/fasouto/termaid), [mermaid-ascii](https://github.com/AlexanderGrooff/mermaid-ascii), and [beautiful-mermaid](https://github.com/lukilabs/beautiful-mermaid) for showing what terminal Mermaid tools can offer. Mermaid syntax belongs to the [Mermaid project](https://mermaid.js.org/).
72
+
73
+ ## Known limitations
74
+
75
+ Self loops use outside lanes, and dense edge labels can still cross lines. [The structural layout decision](docs/adr/003-layout.md) and [scene boundary](docs/adr/002-scene.md) describe the architecture. Scene and plugin interfaces are experimental.
76
+
77
+ ## Development
78
+
79
+ ```sh
80
+ bundle install
81
+ bundle exec rake spec
82
+ bundle exec rake gallery
83
+ ```
84
+
85
+ `rake unicode:generate` accepts `EAW_FILE` and `UCD_FILE` paths to the official Unicode 17.0 files. It regenerates the checked in width and box drawing tables.
@@ -0,0 +1,24 @@
1
+ # ADR NNN: Implementation decision title
2
+
3
+ - Status: Proposed
4
+ - Date: YYYY-MM-DD
5
+
6
+ ## Context
7
+
8
+ Describe the concrete implementation question and its compatibility, data,
9
+ runtime, or component constraints. Operational workflow and release policy do
10
+ not belong in this directory; see [the scope](README.md).
11
+
12
+ Name the credible alternatives. If there was no meaningful alternative, record
13
+ the behavior in reference documentation instead of creating an ADR.
14
+
15
+ ## Decision
16
+
17
+ Describe the durable boundary or architecture choice. Leave command syntax,
18
+ field-by-field formats, test evidence, benchmark results, and migration steps
19
+ in their authoritative documents.
20
+
21
+ ## Consequences
22
+
23
+ Describe the important positive and negative trade-offs, including what would
24
+ make this decision worth revisiting.
@@ -0,0 +1,19 @@
1
+ # ADR 001: Runtime baseline
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-09-21
5
+
6
+ ## Context
7
+
8
+ Terminal use should work in Ruby environments with limited package access. The
9
+ implementation uses current Ruby language features and its standard library.
10
+
11
+ ## Decision
12
+
13
+ Require CRuby 3.3 or newer and keep runtime gem dependencies at zero.
14
+
15
+ ## Consequences
16
+
17
+ Users install no additional runtime gems. The implementation stays small by
18
+ targeting one maintained Ruby baseline; other Ruby implementations are tested
19
+ as best effort.
@@ -0,0 +1,18 @@
1
+ # ADR 002: Scene layer
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-09-21
5
+
6
+ ## Context
7
+
8
+ Diagram layout should not choose terminal border characters. Letting each
9
+ diagram plugin render terminal cells would duplicate rasterization behavior.
10
+
11
+ ## Decision
12
+
13
+ Pass immutable geometric Scene primitives to one shared rasterizer.
14
+
15
+ ## Consequences
16
+
17
+ All diagrams share line joining, themes, width fitting, and ASCII fallback.
18
+ Plugins remain independent of terminal cell representation.
@@ -0,0 +1,20 @@
1
+ # ADR 003: Structural diagram layout
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-09-21
5
+
6
+ ## Context
7
+
8
+ Flow, state, class, and ER diagrams need predictable placement and routing. A
9
+ shared layout model avoids a separate strategy for each structural diagram.
10
+
11
+ ## Decision
12
+
13
+ Use a deterministic layered layout with rank channels. Reserve space for long
14
+ edges and labels before routing, keep nested subgraphs together, and route loops
15
+ through outside lanes.
16
+
17
+ ## Consequences
18
+
19
+ Structural diagrams share predictable placement and routing. The exact sweep,
20
+ crossing, and rasterization algorithms remain implementation details.
@@ -0,0 +1,19 @@
1
+ # ADR 004: Parser diagnostics
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-09-21
5
+
6
+ ## Context
7
+
8
+ Real Mermaid files may contain unsupported statements. Rejecting an entire
9
+ document would hide the parts that can still be rendered.
10
+
11
+ ## Decision
12
+
13
+ Collect line-numbered diagnostics and render recognized statements. Raise in
14
+ strict mode so callers can choose partial or validated parsing.
15
+
16
+ ## Consequences
17
+
18
+ Partial diagrams remain useful while CI can enforce strict parsing. Syntax
19
+ reference documents define the accepted subset.
@@ -0,0 +1,23 @@
1
+ # ADR 005: Public API boundary
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-09-21
5
+
6
+ ## Context
7
+
8
+ The documented render and parse calls need a stable contract. Scene primitives
9
+ and plugin parsing methods expose layout internals that may change as more
10
+ syntax is supported.
11
+
12
+ ## Decision
13
+
14
+ The intended stable v1.0 API is the render, parse, Markdown extraction, diagram
15
+ registration, and diagram type calls documented in the README and RBS. Their
16
+ document and block result fields are stable as documented.
17
+
18
+ Keep `Document#scene`, Scene primitives, and the plugin interface experimental at v1.0. Their names are available for advanced use, but their shape may change in later minor versions.
19
+
20
+ ## Consequences
21
+
22
+ RBS describes the stable calls. Scene primitives and plugin interfaces remain
23
+ experimental extension points.
@@ -0,0 +1,10 @@
1
+ # Architecture Decision Records
2
+
3
+ This directory records durable implementation choices that affect component or
4
+ runtime boundaries and had credible alternatives. API reference, command syntax,
5
+ field formats, test evidence, benchmark results, migrations, release policy, and
6
+ operational workflow belong in their authoritative documentation instead.
7
+
8
+ Copy [ADR 000](000-template.md) when recording a decision. Decision records use
9
+ three-digit numbers starting at `001`, assigned without gaps. Keep accepted,
10
+ rejected, and superseded records so later entries do not need renumbering.
@@ -0,0 +1,58 @@
1
+ # Additional Mermaid syntax diagrams
2
+
3
+ The parser recognizes the Mermaid syntax declarations below:
4
+
5
+ `journey`, `quadrantChart`, `requirementDiagram`, `usecaseDiagram`,
6
+ `usecase-beta`, `gitGraph`, `C4Context`, `C4Container`, `C4Component`,
7
+ `C4Dynamic`, `C4Deployment`, `zenuml`, `sankey-beta`, `block-beta`,
8
+ `packet-beta`, `kanban`, `architecture-beta`, `radar-beta`, `treemap-beta`,
9
+ `venn-beta`, `ishikawa-beta`, `wardley-beta`, `treeView`, `treeView-beta`,
10
+ `cynefin-beta`, `swimlane-beta`, `eventModeling`, `agentflow-beta`,
11
+ `railroad-ebnf-beta`, `railroad-abnf-beta`, `railroad-peg-beta`, and
12
+ `railroad-beta`.
13
+
14
+ Each declaration has a diagram-specific terminal layout. Relationship diagrams
15
+ use the shared layered routing engine where that matches their semantics;
16
+ journeys, quadrants, packets, Kanban boards, radar charts, treemaps, Venn
17
+ diagrams, Ishikawa causes, Wardley maps, swimlanes, and event models use their
18
+ own terminal geometry. Ishikawa cause indentation and Treemap hierarchy are
19
+ retained in the terminal layout.
20
+ Railroad declarations render grammar rules with distinct terminal
21
+ and non-terminal nodes. Mermaid directives that only affect browser layout or
22
+ SVG styling are ignored.
23
+
24
+ Kanban task metadata written as `@{ ... }` is shown with the task label. Wardley
25
+ maps support `component`, `anchor`, their `inertia` and source-strategy
26
+ decorators, links (including labels and reverse/dashed forms), `evolve`,
27
+ `pipeline`, `evolution`, trends,
28
+ positioned `note`, and numbered `annotation` statements. Event Modeling supports compact and relaxed time frames, reset
29
+ frames (reset frames retain an `rf` marker), `->>` relations, and named `data` blocks; UI/processor,
30
+ command/read model, and event aliases share their corresponding swimlanes.
31
+
32
+ Railroad rules end with `;`: EBNF uses `=` or `::=`, ABNF uses `=`, and PEG
33
+ uses `<-`. Quoted strings are terminal nodes, identifiers are non-terminal
34
+ nodes, and top-level `|` or `/` alternatives become separate paths. The IR
35
+ form (`railroad-beta`) accepts `terminal("text")`, `nonterminal("name")`,
36
+ `special("text")`, `sequence(...)`, `choice(...)`, `optional(...)`,
37
+ `zeroOrMore(...)`, and `oneOrMore(...)`. EBNF/PEG comments and ABNF trailing
38
+ comments are ignored.
39
+
40
+ An `agentflow-beta` flow with no nodes is rendered as a labeled terminal card;
41
+ it does not fall back to the generic additional-syntax renderer.
42
+
43
+ ```mermaid
44
+ railroad-ebnf-beta
45
+ title "Letter"
46
+ letter = "a" | "b" ;
47
+ ```
48
+
49
+ ```mermaid
50
+ usecase-beta
51
+ actor User
52
+ User --> (Use system)
53
+ ```
54
+
55
+ The core diagram types have dedicated syntax references in the neighboring
56
+ files. This page keeps the additional declaration names and terminal subset in
57
+ one place. Browser-only styling and interaction directives are intentionally
58
+ ignored because they have no terminal equivalent.
@@ -0,0 +1,3 @@
1
+ # Class syntax
2
+
3
+ `classDiagram` accepts `class Name`, bodies in `{ ... }`, attributes, operations, `Name : member`, and annotations such as `class Name <<interface>>` or `Name <<interface>>`. Classes have name, attribute, and operation compartments. Relations include inheritance `<|--`, realization `..|>`, composition `*--`, aggregation `o--`, dependency `..>`, association `-->`, plain `--`, and reverse forms. A label can follow `:`.
data/docs/syntax/er.md ADDED
@@ -0,0 +1,3 @@
1
+ # ER syntax
2
+
3
+ `erDiagram` accepts `ENTITY { ... }` attribute blocks and relation lines such as `CUSTOMER ||--o{ ORDER : places`. Solid and dotted relationships are rendered; cardinalities are written as `1`, `0..1`, `1..*`, and `0..*` in the relation label.
@@ -0,0 +1,16 @@
1
+ # Flowchart syntax
2
+
3
+ Headers: `graph`, `flowchart`, `flowchart-v2`, or `flowchart-elk`, followed by
4
+ `TB`, `TD`, `BT`, `LR`, or `RL`.
5
+
6
+ Nodes: `A`, `A[text]`, `A(text)`, `A([text])`, `A[[text]]`, `A((text))`, `A(((text)))`, `A{text}`, `A{{text}}`, `A[(text)]`, `A>text]`, `A[/text/]`, `A[/text\]`, and `A@{ shape: diamond }`. `<br>` and HTML entities in labels are accepted.
7
+
8
+ Edges: `-->`, `---`, `-.->`, `==>`, `~~~`, longer variants, edge labels `-->|text|`, chains, and `&` groups. `subgraph ...` / `end` creates a frame. `direction` changes the top level orientation.
9
+
10
+ ```mermaid
11
+ flowchart LR
12
+ A[Start] -->|yes| B{Ready?}
13
+ B --> C[Done]
14
+ ```
15
+
16
+ `style`, `classDef`, `class`, `linkStyle`, and `:::` accept `fill`, `stroke`, and `color` hex values for ANSI output. `click` produces an information diagnostic and is ignored. Unknown statements produce errors, and valid statements continue rendering.
@@ -0,0 +1,3 @@
1
+ # Gantt syntax
2
+
3
+ `gantt` accepts `title`, `dateFormat`, `section`, `excludes weekends`, and tasks like `Design :done, d1, 2026-01-01, 3d` or `Build :active, b1, after d1, 5d`. `done`, `active`, `crit`, and `milestone` states are accepted. Durations use days (`d`) or weeks (`w`).
@@ -0,0 +1,3 @@
1
+ # Mindmap syntax
2
+
3
+ `mindmap` accepts indented nodes. The first node is the root; indentation determines parentage. `id((label))`, `id(label)`, `id[label]`, and `id))label((` select circle, rounded, rectangle, and cloud frames. Other labels use a rectangle.
@@ -0,0 +1,3 @@
1
+ # Pie syntax
2
+
3
+ `pie` accepts `showData`, `title`, and quoted `"Label" : nonnegative-number` entries. Values are shown as horizontal proportional bars and percentages. `showData` adds the original values.
@@ -0,0 +1,14 @@
1
+ # Sequence syntax
2
+
3
+ `sequenceDiagram` accepts `participant` and `actor` declarations, optional `as` labels, message operators `->`, `-->`, `->>`, `-->>`, `-x`, `--x`, `-)`, and `--)`, self messages, `Note left of` / `right of` / `over`, `activate`, `deactivate`, and `autonumber`. `+` or `-` before the recipient activates or deactivates a participant. `loop`, `alt` / `else`, `opt`, `par` / `and`, `critical`, `break`, and `rect` create frames.
4
+
5
+ ```mermaid
6
+ sequenceDiagram
7
+ participant A as Alice
8
+ participant B as Bob
9
+ A->>+B: Request
10
+ B-->>-A: Response
11
+ ```
12
+
13
+ Unsupported statements produce errors. Participant declaration order determines columns; undeclared participants use first appearance order.
14
+ Pass `repeat_participants: true` to `render` to show participant boxes again at the bottom.
@@ -0,0 +1,3 @@
1
+ # State syntax
2
+
3
+ `stateDiagram` and `stateDiagram-v2` accept `A --> B`, transition labels after `:`, `[*]` start and end, `state "Label" as ID`, `state ID <<choice>>`, `<<fork>>`, `<<join>>`, and `state ID { ... }` composite frames. `direction` accepts flowchart directions. `note left of ID` and `note right of ID` accept inline text after `:` or a block ending in `end note`.
@@ -0,0 +1,3 @@
1
+ # Timeline syntax
2
+
3
+ `timeline` accepts `title`, `section`, and `period : event` lines. Events retain source order and appear as a vertical list.
@@ -0,0 +1,3 @@
1
+ # XY chart syntax
2
+
3
+ `xychart-beta` or `xychart` accepts `title`, `x-axis [labels]`, numeric `y-axis min --> max`, `bar [values]`, and `line [values]`. Bars use eighth block characters, lines use braille pixels, and ASCII mode uses `#` and `*`. Add `horizontal` to the header for horizontal bars.
data/exe/merminal ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/merminal"
5
+ require_relative "../lib/merminal/cli"
6
+
7
+ exit Merminal::CLI.run
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+
5
+ module Merminal
6
+ # Command line interface for files, pipes and Markdown fences.
7
+ module CLI
8
+ module_function
9
+
10
+ def run(argv = ARGV, stdin: $stdin, stdout: $stdout, stderr: $stderr)
11
+ options = { charset: :unicode, color: :auto, theme: nil, rounded: true, crossings: :plain,
12
+ fit: :compact, check: false, strict: false, markdown: false, compact: false }
13
+ parser = OptionParser.new do |p|
14
+ p.banner = "Usage: merminal [options] [FILE ...]"
15
+ p.on("-a", "--ascii", "Use printable ASCII") { options[:charset] = :ascii }
16
+ p.on("-t", "--theme NAME", "Color theme") { |name| options[:theme] = name.to_sym }
17
+ p.on("--list-themes", "List themes") { options[:list_themes] = true }
18
+ p.on("--color WHEN", %w[auto always never], "auto, always or never") { |value| options[:color] = value.to_sym }
19
+ p.on("-w", "--width N", Integer, "Target terminal width") { |value| options[:width] = value }
20
+ p.on("--no-fit", "Disable width fitting") { options[:fit] = nil }
21
+ p.on("--fit MODE", %w[compact rotate], "compact or rotate") { |value| options[:fit] = value.to_sym }
22
+ p.on("--compact", "Use compact spacing") { options[:compact] = true }
23
+ p.on("--sharp", "Use sharp corners") { options[:rounded] = false }
24
+ p.on("--crossings MODE", %w[plain bridge], "Crossing style") { |value| options[:crossings] = value.to_sym }
25
+ p.on("--markdown", "Read Mermaid fences") { options[:markdown] = true }
26
+ p.on("--check", "Check syntax without rendering") { options[:check] = true }
27
+ p.on("--strict", "Fail on parser errors") { options[:strict] = true }
28
+ p.on("-o", "--output FILE", "Write output to file") { |path| options[:output] = path }
29
+ p.on("-v", "--version", "Print version") { stdout.puts VERSION; return 0 }
30
+ p.on("-h", "--help", "Print help") { stdout.puts p; return 0 }
31
+ end
32
+ files = parser.parse(argv)
33
+ if options[:list_themes]
34
+ stdout.puts Output::THEMES.keys
35
+ return 0
36
+ end
37
+ raise OptionParser::InvalidArgument, "width must be positive" if options[:width] && !options[:width].positive?
38
+ raise OptionParser::InvalidArgument, "unknown theme" if options[:theme] && !Output::THEMES.key?(options[:theme])
39
+
40
+ parts = []
41
+ error_found = false
42
+ (files.empty? ? ["-"] : files).each do |path|
43
+ input = path == "-" ? stdin.read : File.read(path)
44
+ markdown = options[:markdown] || path.match?(/\.(?:md|markdown)\z/i)
45
+ blocks = markdown ? Merminal.markdown_blocks(input) : [MarkdownBlock.new(source: input, line: 1)]
46
+ blocks.each do |block|
47
+ document = Merminal.parse(block.source)
48
+ document.diagnostics.each do |diagnostic|
49
+ error_found ||= diagnostic.severity == :error
50
+ shifted = diagnostic.with(line: diagnostic.line + block.line - 1)
51
+ stderr.puts shifted.format(path, markdown ? input : block.source)
52
+ end
53
+ next if options[:check]
54
+
55
+ parts << fitted_render(document, options, stdout, stderr)
56
+ end
57
+ end
58
+ output = parts.join("\n\n")
59
+ options[:output] ? File.write(options[:output], output + (output.empty? ? "" : "\n")) : stdout.puts(output) unless options[:check]
60
+ error_found && (options[:strict] || options[:check]) ? 1 : 0
61
+ rescue UnsupportedDiagramError => e
62
+ stderr.puts e.message
63
+ 3
64
+ rescue OptionParser::ParseError, Errno::ENOENT, Errno::EACCES, IOError, ArgumentError => e
65
+ stderr.puts e.message
66
+ 2
67
+ end
68
+
69
+ def fitted_render(document, options, stdout, stderr)
70
+ width = options[:width] || (stdout.tty? ? terminal_width : nil)
71
+ rendered = document.render(width: width, fit: options[:fit], compact: options[:compact],
72
+ charset: options[:charset], color: options[:color], theme: options[:theme],
73
+ rounded: options[:rounded], crossings: options[:crossings])
74
+ if width && rendered.lines.any? { |line| Merminal::Text.width(line.gsub(/\e\[[\d;]*m/, "")) > width }
75
+ actual = rendered.lines.map { |line| Merminal::Text.width(line.gsub(/\e\[[\d;]*m/, "")) }.max
76
+ stderr.puts "diagram width #{actual} exceeds target #{width}"
77
+ end
78
+ rendered
79
+ end
80
+
81
+ def terminal_width
82
+ return ENV["COLUMNS"].to_i if ENV["COLUMNS"].to_i.positive?
83
+
84
+ begin
85
+ require "io/console"
86
+ width = IO.console&.winsize&.last
87
+ return width if width && width.positive?
88
+ rescue LoadError, Errno::ENOTTY, IOError
89
+ nil
90
+ end
91
+ 80
92
+ end
93
+ end
94
+ end