cli-ui 1.5.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8de8c1fd6c855f812125e71e85dbfc12163dfd504215fab462d56ad5f4d2cc7e
4
- data.tar.gz: 404f164084fd9cf85aa382a5d5f93b77b08ea4bc529840ebfe230e1b5ea7c620
3
+ metadata.gz: d8dec5198cdbbb3a6060fc448a9a03b5bc6eca461b94a62ead0f128664bb0ee2
4
+ data.tar.gz: bed02e7e27771141c08859c11d7e01f690438bb46d4022f5225d8cb42e39efe4
5
5
  SHA512:
6
- metadata.gz: 9f737c534e372589165a3263854d3a90f024181f632f6ae918fcb7aafea69abad27f50246d35c2957c238447a74871f4705add26d1bf272ad58f4fcc52d31ae6
7
- data.tar.gz: 56c449900c817637b97d0e99b05ac01375bbde1458db3f2d5e7122027cdd525e0b8aaf9b9d5290cc2f1fa8cae314efb1365ac46ab277629f5cebf5d113e39b0d
6
+ metadata.gz: 6944f7c1b8492e69bd078b2ca0d9901d26694e4542976cb07729efff2cb59438cbc2ec61692b75ccf178ffcec896a297264be5734520695f9bc11779f2698ead
7
+ data.tar.gz: c9795b1749d3db7cb57bccca815495ed9b2e8ee02092fe95b46bf451f2993e65c33f8b48e9f6520b2984b7278db64cb9e92bb1fe58a761691a9b313dab6ef464
data/README.md CHANGED
@@ -3,7 +3,7 @@ CLI UI
3
3
 
4
4
  CLI UI is a small framework for generating nice command-line user interfaces
5
5
 
6
- - [Master Documentation](http://www.rubydoc.info/github/Shopify/cli-ui/master/CLI/UI)
6
+ - [Master Documentation](http://www.rubydoc.info/github/Shopify/cli-ui/main/CLI/UI)
7
7
  - [Documentation of the Rubygems version](http://www.rubydoc.info/gems/cli-ui/)
8
8
  - [Rubygems](https://rubygems.org/gems/cli-ui)
9
9
 
@@ -23,7 +23,7 @@ In your code, simply add a `require 'cli/ui'`. Most options assume `CLI::UI::Std
23
23
 
24
24
  ## Features
25
25
 
26
- This may not be an exhaustive list. Please check our [documentation](http://www.rubydoc.info/github/Shopify/cli-ui/master/CLI/UI) for more information.
26
+ This may not be an exhaustive list. Please check our [documentation](http://www.rubydoc.info/github/Shopify/cli-ui/main/CLI/UI) for more information.
27
27
 
28
28
  ---
29
29
 
@@ -83,10 +83,10 @@ CLI::UI.ask('Is CLI UI Awesome?', default: 'It is great!')
83
83
  Handle many multi-threaded processes while suppressing output unless there is an issue. Can update title to show state.
84
84
 
85
85
  ```ruby
86
- spin_group = CLI::UI::SpinGroup.new
87
- spin_group.add('Title') { |spinner| sleep 3.0 }
88
- spin_group.add('Title 2') { |spinner| sleep 3.0; spinner.update_title('New Title'); sleep 3.0 }
89
- spin_group.wait
86
+ CLI::UI::SpinGroup.new do |spin_group|
87
+ spin_group.add('Title') { |spinner| sleep 3.0 }
88
+ spin_group.add('Title 2') { |spinner| sleep 3.0; spinner.update_title('New Title'); sleep 3.0 }
89
+ end
90
90
  ```
91
91
 
92
92
  ![Spinner Group](https://user-images.githubusercontent.com/3074765/33798295-d94fd822-dce3-11e7-819b-43e5502d490e.gif)
@@ -187,22 +187,22 @@ CLI::UI::StdoutRouter.enable
187
187
  CLI::UI::Frame.open('{{*}} {{bold:a}}', color: :green) do
188
188
  CLI::UI::Frame.open('{{i}} b', color: :magenta) do
189
189
  CLI::UI::Frame.open('{{?}} c', color: :cyan) do
190
- sg = CLI::UI::SpinGroup.new
191
- sg.add('wow') do |spinner|
192
- sleep(2.5)
193
- spinner.update_title('second round!')
194
- sleep (1.0)
190
+ CLI::UI::SpinGroup.new do |sg|
191
+ sg.add('wow') do |spinner|
192
+ sleep(2.5)
193
+ spinner.update_title('second round!')
194
+ sleep (1.0)
195
+ end
196
+ sg.add('such spin') { sleep(1.6) }
197
+ sg.add('many glyph') { sleep(2.0) }
195
198
  end
196
- sg.add('such spin') { sleep(1.6) }
197
- sg.add('many glyph') { sleep(2.0) }
198
- sg.wait
199
199
  end
200
200
  end
201
201
  CLI::UI::Frame.divider('{{v}} lol')
202
202
  puts CLI::UI.fmt '{{info:words}} {{red:oh no!}} {{green:success!}}'
203
- sg = CLI::UI::SpinGroup.new
204
- sg.add('more spins') { sleep(0.5) ; raise 'oh no' }
205
- sg.wait
203
+ CLI::UI::SpinGroup.new do |sg|
204
+ sg.add('more spins') { sleep(0.5) ; raise 'oh no' }
205
+ end
206
206
  end
207
207
  ```
208
208
 
data/lib/cli/ui/ansi.rb CHANGED
@@ -1,156 +1,184 @@
1
+ # typed: true
2
+
1
3
  require 'cli/ui'
2
4
 
3
5
  module CLI
4
6
  module UI
5
7
  module ANSI
8
+ extend T::Sig
9
+
6
10
  ESC = "\x1b"
7
11
 
8
- # ANSI escape sequences (like \x1b[31m) have zero width.
9
- # when calculating the padding width, we must exclude them.
10
- # This also implements a basic version of utf8 character width calculation like
11
- # we could get for real from something like utf8proc.
12
- #
13
- def self.printing_width(str)
14
- zwj = false
15
- strip_codes(str).codepoints.reduce(0) do |acc, cp|
16
- if zwj
17
- zwj = false
18
- next acc
19
- end
20
- case cp
21
- when 0x200d # zero-width joiner
22
- zwj = true
23
- acc
24
- when "\n"
25
- acc
26
- else
27
- acc + 1
12
+ class << self
13
+ extend T::Sig
14
+
15
+ # ANSI escape sequences (like \x1b[31m) have zero width.
16
+ # when calculating the padding width, we must exclude them.
17
+ # This also implements a basic version of utf8 character width calculation like
18
+ # we could get for real from something like utf8proc.
19
+ #
20
+ sig { params(str: String).returns(Integer) }
21
+ def printing_width(str)
22
+ zwj = T.let(false, T::Boolean)
23
+ strip_codes(str).codepoints.reduce(0) do |acc, cp|
24
+ if zwj
25
+ zwj = false
26
+ next acc
27
+ end
28
+ case cp
29
+ when 0x200d # zero-width joiner
30
+ zwj = true
31
+ acc
32
+ when "\n"
33
+ acc
34
+ else
35
+ acc + 1
36
+ end
28
37
  end
29
38
  end
30
- end
31
39
 
32
- # Strips ANSI codes from a str
33
- #
34
- # ==== Attributes
35
- #
36
- # - +str+ - The string from which to strip codes
37
- #
38
- def self.strip_codes(str)
39
- str.gsub(/\x1b\[[\d;]+[A-z]|\r/, '')
40
- end
40
+ # Strips ANSI codes from a str
41
+ #
42
+ # ==== Attributes
43
+ #
44
+ # - +str+ - The string from which to strip codes
45
+ #
46
+ sig { params(str: String).returns(String) }
47
+ def strip_codes(str)
48
+ str.gsub(/\x1b\[[\d;]+[A-z]|\r/, '')
49
+ end
41
50
 
42
- # Returns an ANSI control sequence
43
- #
44
- # ==== Attributes
45
- #
46
- # - +args+ - Argument to pass to the ANSI control sequence
47
- # - +cmd+ - ANSI control sequence Command
48
- #
49
- def self.control(args, cmd)
50
- ESC + '[' + args + cmd
51
- end
51
+ # Returns an ANSI control sequence
52
+ #
53
+ # ==== Attributes
54
+ #
55
+ # - +args+ - Argument to pass to the ANSI control sequence
56
+ # - +cmd+ - ANSI control sequence Command
57
+ #
58
+ sig { params(args: String, cmd: String).returns(String) }
59
+ def control(args, cmd)
60
+ ESC + '[' + args + cmd
61
+ end
52
62
 
53
- # https://en.wikipedia.org/wiki/ANSI_escape_code#graphics
54
- def self.sgr(params)
55
- control(params.to_s, 'm')
56
- end
63
+ # https://en.wikipedia.org/wiki/ANSI_escape_code#graphics
64
+ sig { params(params: String).returns(String) }
65
+ def sgr(params)
66
+ control(params, 'm')
67
+ end
57
68
 
58
- # Cursor Movement
59
-
60
- # Move the cursor up n lines
61
- #
62
- # ==== Attributes
63
- #
64
- # * +n+ - number of lines by which to move the cursor up
65
- #
66
- def self.cursor_up(n = 1)
67
- return '' if n.zero?
68
- control(n.to_s, 'A')
69
- end
69
+ # Cursor Movement
70
70
 
71
- # Move the cursor down n lines
72
- #
73
- # ==== Attributes
74
- #
75
- # * +n+ - number of lines by which to move the cursor down
76
- #
77
- def self.cursor_down(n = 1)
78
- return '' if n.zero?
79
- control(n.to_s, 'B')
80
- end
71
+ # Move the cursor up n lines
72
+ #
73
+ # ==== Attributes
74
+ #
75
+ # * +n+ - number of lines by which to move the cursor up
76
+ #
77
+ sig { params(n: Integer).returns(String) }
78
+ def cursor_up(n = 1)
79
+ return '' if n.zero?
81
80
 
82
- # Move the cursor forward n columns
83
- #
84
- # ==== Attributes
85
- #
86
- # * +n+ - number of columns by which to move the cursor forward
87
- #
88
- def self.cursor_forward(n = 1)
89
- return '' if n.zero?
90
- control(n.to_s, 'C')
91
- end
81
+ control(n.to_s, 'A')
82
+ end
92
83
 
93
- # Move the cursor back n columns
94
- #
95
- # ==== Attributes
96
- #
97
- # * +n+ - number of columns by which to move the cursor back
98
- #
99
- def self.cursor_back(n = 1)
100
- return '' if n.zero?
101
- control(n.to_s, 'D')
102
- end
84
+ # Move the cursor down n lines
85
+ #
86
+ # ==== Attributes
87
+ #
88
+ # * +n+ - number of lines by which to move the cursor down
89
+ #
90
+ sig { params(n: Integer).returns(String) }
91
+ def cursor_down(n = 1)
92
+ return '' if n.zero?
93
+
94
+ control(n.to_s, 'B')
95
+ end
103
96
 
104
- # Move the cursor to a specific column
105
- #
106
- # ==== Attributes
107
- #
108
- # * +n+ - The column to move to
109
- #
110
- def self.cursor_horizontal_absolute(n = 1)
111
- cmd = control(n.to_s, 'G')
112
- cmd += control('1', 'D') if CLI::UI::OS.current.shift_cursor_on_line_reset?
113
- cmd
114
- end
97
+ # Move the cursor forward n columns
98
+ #
99
+ # ==== Attributes
100
+ #
101
+ # * +n+ - number of columns by which to move the cursor forward
102
+ #
103
+ sig { params(n: Integer).returns(String) }
104
+ def cursor_forward(n = 1)
105
+ return '' if n.zero?
106
+
107
+ control(n.to_s, 'C')
108
+ end
115
109
 
116
- # Show the cursor
117
- #
118
- def self.show_cursor
119
- control('', '?25h')
120
- end
110
+ # Move the cursor back n columns
111
+ #
112
+ # ==== Attributes
113
+ #
114
+ # * +n+ - number of columns by which to move the cursor back
115
+ #
116
+ sig { params(n: Integer).returns(String) }
117
+ def cursor_back(n = 1)
118
+ return '' if n.zero?
119
+
120
+ control(n.to_s, 'D')
121
+ end
121
122
 
122
- # Hide the cursor
123
- #
124
- def self.hide_cursor
125
- control('', '?25l')
126
- end
123
+ # Move the cursor to a specific column
124
+ #
125
+ # ==== Attributes
126
+ #
127
+ # * +n+ - The column to move to
128
+ #
129
+ sig { params(n: Integer).returns(String) }
130
+ def cursor_horizontal_absolute(n = 1)
131
+ cmd = control(n.to_s, 'G')
132
+ cmd += cursor_back if CLI::UI::OS.current.shift_cursor_back_on_horizontal_absolute?
133
+ cmd
134
+ end
127
135
 
128
- # Save the cursor position
129
- #
130
- def self.cursor_save
131
- control('', 's')
132
- end
136
+ # Show the cursor
137
+ #
138
+ sig { returns(String) }
139
+ def show_cursor
140
+ control('', '?25h')
141
+ end
133
142
 
134
- # Restore the saved cursor position
135
- #
136
- def self.cursor_restore
137
- control('', 'u')
138
- end
143
+ # Hide the cursor
144
+ #
145
+ sig { returns(String) }
146
+ def hide_cursor
147
+ control('', '?25l')
148
+ end
139
149
 
140
- # Move to the next line
141
- #
142
- def self.next_line
143
- cursor_down + cursor_horizontal_absolute
144
- end
150
+ # Save the cursor position
151
+ #
152
+ sig { returns(String) }
153
+ def cursor_save
154
+ control('', 's')
155
+ end
145
156
 
146
- # Move to the previous line
147
- #
148
- def self.previous_line
149
- cursor_up + cursor_horizontal_absolute
150
- end
157
+ # Restore the saved cursor position
158
+ #
159
+ sig { returns(String) }
160
+ def cursor_restore
161
+ control('', 'u')
162
+ end
163
+
164
+ # Move to the next line
165
+ #
166
+ sig { returns(String) }
167
+ def next_line
168
+ cursor_down + cursor_horizontal_absolute
169
+ end
170
+
171
+ # Move to the previous line
172
+ #
173
+ sig { returns(String) }
174
+ def previous_line
175
+ cursor_up + cursor_horizontal_absolute
176
+ end
151
177
 
152
- def self.clear_to_end_of_line
153
- control('', 'K')
178
+ sig { returns(String) }
179
+ def clear_to_end_of_line
180
+ control('', 'K')
181
+ end
154
182
  end
155
183
  end
156
184
  end
data/lib/cli/ui/color.rb CHANGED
@@ -1,9 +1,17 @@
1
+ # typed: true
2
+
1
3
  require 'cli/ui'
2
4
 
3
5
  module CLI
4
6
  module UI
5
7
  class Color
6
- attr_reader :sgr, :name, :code
8
+ extend T::Sig
9
+
10
+ sig { returns(String) }
11
+ attr_reader :sgr, :code
12
+
13
+ sig { returns(Symbol) }
14
+ attr_reader :name
7
15
 
8
16
  # Creates a new color mapping
9
17
  # Signatures can be found here:
@@ -14,6 +22,7 @@ module CLI
14
22
  # * +sgr+ - The color signature
15
23
  # * +name+ - The name of the color
16
24
  #
25
+ sig { params(sgr: String, name: Symbol).void }
17
26
  def initialize(sgr, name)
18
27
  @sgr = sgr
19
28
  @code = CLI::UI::ANSI.sgr(sgr)
@@ -32,7 +41,7 @@ module CLI
32
41
  WHITE = new('97', :white)
33
42
 
34
43
  # 240 is very dark gray; 255 is very light gray. 244 is somewhat dark.
35
- GRAY = new('38;5;244', :grey)
44
+ GRAY = new('38;5;244', :gray)
36
45
 
37
46
  MAP = {
38
47
  red: RED,
@@ -47,11 +56,15 @@ module CLI
47
56
  }.freeze
48
57
 
49
58
  class InvalidColorName < ArgumentError
59
+ extend T::Sig
60
+
61
+ sig { params(name: Symbol).void }
50
62
  def initialize(name)
51
63
  super
52
64
  @name = name
53
65
  end
54
66
 
67
+ sig { returns(String) }
55
68
  def message
56
69
  keys = Color.available.map(&:inspect).join(',')
57
70
  "invalid color: #{@name.inspect} " \
@@ -59,25 +72,31 @@ module CLI
59
72
  end
60
73
  end
61
74
 
62
- # Looks up a color code by name
63
- #
64
- # ==== Raises
65
- # Raises a InvalidColorName if the color is not available
66
- # You likely need to add it to the +MAP+ or you made a typo
67
- #
68
- # ==== Returns
69
- # Returns a color code
70
- #
71
- def self.lookup(name)
72
- MAP.fetch(name)
73
- rescue KeyError
74
- raise InvalidColorName, name
75
- end
75
+ class << self
76
+ extend T::Sig
76
77
 
77
- # All available colors by name
78
- #
79
- def self.available
80
- MAP.keys
78
+ # Looks up a color code by name
79
+ #
80
+ # ==== Raises
81
+ # Raises a InvalidColorName if the color is not available
82
+ # You likely need to add it to the +MAP+ or you made a typo
83
+ #
84
+ # ==== Returns
85
+ # Returns a color code
86
+ #
87
+ sig { params(name: T.any(Symbol, String)).returns(Color) }
88
+ def lookup(name)
89
+ MAP.fetch(name.to_sym)
90
+ rescue KeyError
91
+ raise InvalidColorName, name
92
+ end
93
+
94
+ # All available colors by name
95
+ #
96
+ sig { returns(T::Array[Symbol]) }
97
+ def available
98
+ MAP.keys
99
+ end
81
100
  end
82
101
  end
83
102
  end
@@ -1,10 +1,14 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
3
+
2
4
  require('cli/ui')
3
5
  require('strscan')
4
6
 
5
7
  module CLI
6
8
  module UI
7
9
  class Formatter
10
+ extend T::Sig
11
+
8
12
  # Available mappings of formattings
9
13
  # To use any of them, you can use {{<key>:<string>}}
10
14
  # There are presentational (colours and formatters)
@@ -19,6 +23,8 @@ module CLI
19
23
  'blue' => '94', # 9x = high-intensity fg color x
20
24
  'magenta' => '35',
21
25
  'cyan' => '36',
26
+ 'gray' => '38;5;244',
27
+ 'white' => '97',
22
28
  'bold' => '1',
23
29
  'italic' => '3',
24
30
  'underline' => '4',
@@ -49,12 +55,21 @@ module CLI
49
55
 
50
56
  DISCARD_BRACES = 0..-3
51
57
 
52
- LITERAL_BRACES = :__literal_braces__
58
+ LITERAL_BRACES = Class.new
59
+
60
+ Stack = T.type_alias { T::Array[T.any(String, LITERAL_BRACES)] }
53
61
 
54
62
  class FormatError < StandardError
55
- attr_accessor :input, :index
63
+ extend T::Sig
64
+
65
+ sig { returns(String) }
66
+ attr_accessor :input
56
67
 
57
- def initialize(message = nil, input = nil, index = nil)
68
+ sig { returns(Integer) }
69
+ attr_accessor :index
70
+
71
+ sig { params(message: String, input: String, index: Integer).void }
72
+ def initialize(message, input, index)
58
73
  super(message)
59
74
  @input = input
60
75
  @index = index
@@ -67,8 +82,10 @@ module CLI
67
82
  #
68
83
  # * +text+ - the text to format
69
84
  #
85
+ sig { params(text: String).void }
70
86
  def initialize(text)
71
87
  @text = text
88
+ @nodes = T.let([], T::Array[[String, Stack]])
72
89
  end
73
90
 
74
91
  # Format the text using a map.
@@ -81,10 +98,11 @@ module CLI
81
98
  #
82
99
  # * +:enable_color+ - enable color output? Default is true unless output is redirected
83
100
  #
101
+ sig { params(sgr_map: T::Hash[String, String], enable_color: T::Boolean).returns(String) }
84
102
  def format(sgr_map = SGR_MAP, enable_color: CLI::UI.enable_color?)
85
- @nodes = []
103
+ @nodes.replace([])
86
104
  stack = parse_body(StringScanner.new(@text))
87
- prev_fmt = nil
105
+ prev_fmt = T.let(nil, T.nilable(Stack))
88
106
  content = @nodes.each_with_object(+'') do |(text, fmt), str|
89
107
  if prev_fmt != fmt && enable_color
90
108
  text = apply_format(text, fmt, sgr_map)
@@ -93,12 +111,12 @@ module CLI
93
111
  prev_fmt = fmt
94
112
  end
95
113
 
96
- stack.reject! { |e| e == LITERAL_BRACES }
114
+ stack.reject! { |e| e.is_a?(LITERAL_BRACES) }
97
115
 
98
116
  return content unless enable_color
99
117
  return content if stack == prev_fmt
100
118
 
101
- unless stack.empty? && (@nodes.size.zero? || @nodes.last[1].empty?)
119
+ unless stack.empty? && (@nodes.size.zero? || T.must(@nodes.last)[1].empty?)
102
120
  content << apply_format('', stack, sgr_map)
103
121
  end
104
122
  content
@@ -106,25 +124,28 @@ module CLI
106
124
 
107
125
  private
108
126
 
127
+ sig { params(text: String, fmt: Stack, sgr_map: T::Hash[String, String]).returns(String) }
109
128
  def apply_format(text, fmt, sgr_map)
110
129
  sgr = fmt.each_with_object(+'0') do |name, str|
111
- next if name == LITERAL_BRACES
130
+ next if name.is_a?(LITERAL_BRACES)
131
+
112
132
  begin
113
133
  str << ';' << sgr_map.fetch(name)
114
134
  rescue KeyError
115
135
  raise FormatError.new(
116
136
  "invalid format specifier: #{name}",
117
137
  @text,
118
- -1
138
+ -1,
119
139
  )
120
140
  end
121
141
  end
122
142
  CLI::UI::ANSI.sgr(sgr) + text
123
143
  end
124
144
 
145
+ sig { params(sc: StringScanner, stack: Stack).returns(Stack) }
125
146
  def parse_expr(sc, stack)
126
147
  if (match = sc.scan(SCAN_GLYPH))
127
- glyph_handle = match[0]
148
+ glyph_handle = T.must(match[0])
128
149
  begin
129
150
  glyph = Glyph.lookup(glyph_handle)
130
151
  emit(glyph.char, [glyph.color.name.to_s])
@@ -133,20 +154,20 @@ module CLI
133
154
  raise FormatError.new(
134
155
  "invalid glyph handle at index #{index}: '#{glyph_handle}'",
135
156
  @text,
136
- index
157
+ index,
137
158
  )
138
159
  end
139
160
  elsif (match = sc.scan(SCAN_WIDGET))
140
- match_data = SCAN_WIDGET.match(match) # Regexp.last_match doesn't work here
141
- widget_handle = match_data['handle']
161
+ match_data = T.must(SCAN_WIDGET.match(match)) # Regexp.last_match doesn't work here
162
+ widget_handle = T.must(match_data['handle'])
142
163
  begin
143
164
  widget = Widgets.lookup(widget_handle)
144
- emit(widget.call(match_data['args']), stack)
165
+ emit(widget.call(T.must(match_data['args'])), stack)
145
166
  rescue Widgets::InvalidWidgetHandle
146
167
  index = sc.pos - 2 # rewind past '}}'
147
168
  raise(FormatError.new(
148
169
  "invalid widget handle at index #{index}: '#{widget_handle}'",
149
- @text, index,
170
+ @text, index
150
171
  ))
151
172
  end
152
173
  elsif (match = sc.scan(SCAN_FUNCNAME))
@@ -158,20 +179,21 @@ module CLI
158
179
  # We do kind of assume that the text will probably have balanced
159
180
  # pairs of {{ }} at least.
160
181
  emit('{{', stack)
161
- stack.push(LITERAL_BRACES)
182
+ stack.push(LITERAL_BRACES.new)
162
183
  end
163
184
  parse_body(sc, stack)
164
185
  stack
165
186
  end
166
187
 
188
+ sig { params(sc: StringScanner, stack: Stack).returns(Stack) }
167
189
  def parse_body(sc, stack = [])
168
190
  match = sc.scan(SCAN_BODY)
169
191
  if match&.end_with?(BEGIN_EXPR)
170
- emit(match[DISCARD_BRACES], stack)
192
+ emit(T.must(match[DISCARD_BRACES]), stack)
171
193
  parse_expr(sc, stack)
172
194
  elsif match&.end_with?(END_EXPR)
173
- emit(match[DISCARD_BRACES], stack)
174
- if stack.pop == LITERAL_BRACES
195
+ emit(T.must(match[DISCARD_BRACES]), stack)
196
+ if stack.pop.is_a?(LITERAL_BRACES)
175
197
  emit('}}', stack)
176
198
  end
177
199
  parse_body(sc, stack)
@@ -183,9 +205,11 @@ module CLI
183
205
  stack
184
206
  end
185
207
 
208
+ sig { params(text: String, stack: Stack).void }
186
209
  def emit(text, stack)
187
- return if text.nil? || text.empty?
188
- @nodes << [text, stack.reject { |n| n == LITERAL_BRACES }]
210
+ return if text.empty?
211
+
212
+ @nodes << [text, stack.reject { |n| n.is_a?(LITERAL_BRACES) }]
189
213
  end
190
214
  end
191
215
  end