tty_string 1.1.1 → 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: ce94f319deb6062d8492d19e856f93d1e4b7fb1ef454e31e6824a1814b59a319
4
- data.tar.gz: '05826ce61d59609df5db320f5e4bf83084b494a9f045f90b13cf8a3c18f07b40'
3
+ metadata.gz: 73cc746b842f0d06ee1999b3f489a6ff01dc369dab4541bc35b095fe48e040ca
4
+ data.tar.gz: 5e4ab9ee53fc27904a98e59c8f264fe14618ed324e9fc2097dd408da9614ff9d
5
5
  SHA512:
6
- metadata.gz: 4299d0e7ffca36bf56f6899cb971a882e0646a71777cdbf0fd34a4569cfe3986d057fb0b10137815747b163d2e4da44b6243692d36ab8bee0141695df93416b1
7
- data.tar.gz: 81e5f9aeee83e5b65df853dcfe94f41398eb5de0d690acafeded7aa896cfe80ba5c1cb09fad9ed4ff0a84edeb495cf6b4185a832261ce7ca9a248508015d0de9
6
+ metadata.gz: df7fa737a041d1593609f342e060c5a92a78c17fb8b7df8658c72c803d9f8ca72a6c8137ccda4ecdfadd9f042a9652c6ec33c554ad090edc9d2aad7eef1deb03
7
+ data.tar.gz: bd1246abfe031c1206d932e30a724b64e14455c99e9d7298774dbf8c76157ec365de312f32173b0adcfed11f906100bee7ded4151c82699332255d4b0d4cdd54
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ # v2.0.0
2
+ - TTYString is now a module not a class.
3
+ - Address the issue where preserved styles and unknown codes would cause the cursor to be misaligned when moving, and potentially overwriting styles unexpectedly:
4
+ - `clear_style: false` is now `style: TTYString::RENDER`, which modifies styles to display as they would rather than just passing them through them unprocessed
5
+ - `clear_style: true` is now `style: TTYString::DROP` (and still the default)
6
+ - Unknown codes are now dropped by default
7
+ - it's now possible to set `unknown: TTYString::RAISE` to raise on unrecognized CSI codes (`unknown: TTYString::DROP` is the default)
8
+ - drop more codes that are known to do nothing for the display of text `\e[?5l`,`\e[?5h`,`\e[?25l`,`\e[?25h`,`\e[?1004l`,`\e[?1004h`,`\e[?1049l`,`\e[?1049h`
9
+
1
10
  # v1.1.1
2
11
  - i forgot how arity works
3
12
 
data/README.md CHANGED
@@ -32,11 +32,20 @@ Intended for use in tests of command line interfaces.
32
32
  | `\e[nK` | _n_=`0`: clear the line from the cursor forward <br>_n_=`1`: clear the line from the cursor backward <br>_n_=`2`: clear the line | _n_=`0` |
33
33
  | `\e[nS` | scroll up _n_ rows | _n_=`1` |
34
34
  | `\e[nT` | scroll down _n_ rows | _n_=`1` |
35
- | `\e[m` | styling codes: optionally suppressed with `clear_style: false` | |
36
- | `\e[?2004h` | enabled bracketed paste: suppressed | |
37
- | `\e[?2004l` | disable bracketed paste: suppressed | |
38
- | `\e[200~` | bracketed paste start: suppressed | |
39
- | `\e[201~` | bracketed paste end: suppressed | |
35
+ | `\e[m` | styling codes: dropped with `style: :drop` (default), rendered with `style: :render`. | |
36
+ | `\e[?5h` | reverse the screen: dropped | |
37
+ | `\e[?5l` | normal the screen: dropped | |
38
+ | `\e[?25h` | show the cursor: dropped | |
39
+ | `\e[?25l` | hide the cursor: dropped | |
40
+ | `\e[?1004h` | enable reporting focus: dropped | |
41
+ | `\e[?1004l` | disable reporting focus: dropped | |
42
+ | `\e[?1049h` | enable alternate screen buffer: dropped | |
43
+ | `\e[?1049l` | disable alternate screen buffer: dropped | |
44
+ | `\e[?2004h` | enable bracketed paste mode: dropped | |
45
+ | `\e[?2004l` | disable bracketed paste mode: dropped | |
46
+ | `\e[200~` | bracketed paste start: dropped | |
47
+ | `\e[201~` | bracketed paste end: dropped | |
48
+ | `\e[` | any other valid CSI code: dropped with `unknown: :drop` (default), raises TTYString::Error with `unknown: :raise`. | |
40
49
 
41
50
  ## Installation
42
51
 
@@ -61,14 +70,19 @@ TTYString.parse("th\ta string\e[3Gis is")
61
70
  => "this is a string"
62
71
  ```
63
72
 
64
- Styling information is suppressed by default:
73
+ Styling information is dropped by default:
65
74
  ```ruby
66
75
  TTYString.parse("th\ta \e[31mstring\e[0m\e[3Gis is")
67
76
  => "this is a string"
68
77
  ```
69
- But can be passed through:
78
+ But can be rendered:
70
79
  ```ruby
71
- TTYString.parse("th\ta \e[31mstring\e[0m\e[3Gis is", clear_style: false)
80
+ TTYString.parse("th\ta \e[31mstring\e[0m\e[3Gis is", style: :render)
81
+ => "this is a \e[31mstring\e[0m"
82
+ ```
83
+
84
+ ```ruby
85
+ TTYString.parse("th\ta \e[31mstring\e[0m\e[3Gis is", style: :render)
72
86
  => "this is a \e[31mstring\e[0m"
73
87
  ```
74
88
 
@@ -81,10 +95,7 @@ Just for fun TTYString.to_proc provides the `parse` method as a lambda, so:
81
95
  ## Limitations
82
96
 
83
97
  - Various terminals are wildly variously permissive with what they accept,
84
- so this doesn't even try to cover all possible cases,
85
- instead it covers the narrowest possible case, and leaves the codes in place when unrecognized
86
-
87
- - `clear_style: false` treats the style codes as regular text which may work differently when rendering codes that move the cursor.
98
+ so this doesn't even try to cover all possible cases
88
99
 
89
100
  ## Development
90
101
 
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTYString
4
+ class Cell
5
+ attr_reader :style, :value
6
+
7
+ def initialize(value, style: NullStyle)
8
+ @style = style
9
+ @value = value
10
+ end
11
+
12
+ def to_s(style_context: NullStyle)
13
+ "#{style.to_s(context: style_context)}#{value}"
14
+ end
15
+ end
16
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class TTYString
3
+ module TTYString
4
4
  class Code
5
5
  class << self
6
6
  def descendants
@@ -3,7 +3,7 @@
3
3
  require_relative 'code'
4
4
  require_relative 'csi_code'
5
5
 
6
- class TTYString
6
+ module TTYString
7
7
  class Code
8
8
  class SlashA < TTYString::Code # leftovers:allow
9
9
  char "\a"
@@ -32,7 +32,7 @@ class TTYString
32
32
  def action
33
33
  cursor.down
34
34
  cursor.col = 0
35
- screen.write('')
35
+ screen.ensure_row
36
36
  end
37
37
  end
38
38
 
@@ -2,12 +2,12 @@
2
2
 
3
3
  require_relative 'code'
4
4
 
5
- class TTYString
5
+ module TTYString
6
6
  class CSICode < TTYString::Code
7
7
  class << self
8
8
  def default_arg(value = nil)
9
9
  @default_arg ||= value
10
- @default_arg || 1
10
+ @default_arg || '0'
11
11
  end
12
12
 
13
13
  private
@@ -19,7 +19,7 @@ class TTYString
19
19
  def args(parser)
20
20
  a = parser.matched.slice(2..-2).split(';')
21
21
  a = a.slice(0, max_args) unless max_args == -1
22
- a.map! { |n| n.empty? ? default_arg : n.to_i }
22
+ a.map! { |n| n.empty? ? default_arg : n }
23
23
  a
24
24
  end
25
25
 
@@ -27,19 +27,14 @@ class TTYString
27
27
  @re ||= /\e\[#{args_re}#{char}/
28
28
  end
29
29
 
30
- def args_re # rubocop:disable Metrics/MethodLength
30
+ def args_re
31
31
  case max_args
32
- when 0 then nil
33
- when 1 then /#{arg_re}?/
34
- when -1 then /(#{arg_re}?(;#{arg_re})*)?/
35
- else /(#{arg_re}?(;#{arg_re}){0,#{max_args - 1}})?/
32
+ when 0, 1 then /(?:[0-:<-?]*){0,#{max_args}}/
33
+ when -1 then %r{[0-?]*[ -/]*}
34
+ else /(?:(?:[0-:<-?]*)?(?:;(?:[0-:<-?]*)?){0,#{max_args - 1}})?/
36
35
  end
37
36
  end
38
37
 
39
- def arg_re
40
- /\d*/
41
- end
42
-
43
38
  def max_args
44
39
  @max_args ||= begin
45
40
  params = instance_method(:action).parameters
@@ -49,6 +44,12 @@ class TTYString
49
44
  end
50
45
  end
51
46
  end
47
+
48
+ def integer(value)
49
+ return value.to_i if value.match?(/\A\d+\z/)
50
+
51
+ parser.unknown
52
+ end
52
53
  end
53
54
  end
54
55
 
@@ -2,63 +2,103 @@
2
2
 
3
3
  require_relative 'csi_code'
4
4
 
5
- class TTYString
5
+ module TTYString
6
6
  class CSICode
7
7
  class A < TTYString::CSICode # leftovers:allow
8
- def action(rows = 1)
9
- cursor.up(rows)
8
+ def action(rows = '1')
9
+ rows = integer(rows)
10
+ cursor.up(rows) if rows
10
11
  end
11
12
  end
12
13
 
13
14
  class B < TTYString::CSICode # leftovers:allow
14
- def action(rows = 1)
15
- cursor.down(rows)
15
+ default_arg '1'
16
+
17
+ def action(rows = '1')
18
+ rows = integer(rows)
19
+ cursor.down(rows) if rows
16
20
  end
17
21
  end
18
22
 
19
23
  class C < TTYString::CSICode # leftovers:allow
20
- def action(cols = 1)
21
- cursor.right(cols)
24
+ default_arg 1
25
+
26
+ def action(cols = '1')
27
+ cols = integer(cols)
28
+ cursor.right(cols) if cols
22
29
  end
23
30
  end
24
31
 
25
32
  class D < TTYString::CSICode # leftovers:allow
26
- def action(cols = 1)
27
- cursor.left(cols)
33
+ default_arg '1'
34
+
35
+ def action(cols = '1')
36
+ cols = integer(cols)
37
+ cursor.left(cols) if cols
28
38
  end
29
39
  end
30
40
 
31
41
  class E < TTYString::CSICode # leftovers:allow
32
- def action(rows = 1)
42
+ default_arg '1'
43
+
44
+ def action(rows = '1')
45
+ rows = integer(rows)
46
+ return unless rows
47
+
33
48
  cursor.down(rows)
34
49
  cursor.col = 0
35
50
  end
36
51
  end
37
52
 
38
53
  class F < TTYString::CSICode # leftovers:allow
39
- def action(rows = 1)
54
+ default_arg '1'
55
+
56
+ def action(rows = '1')
57
+ rows = integer(rows)
58
+ return unless rows
59
+
40
60
  cursor.up(rows)
41
61
  cursor.col = 0
42
62
  end
43
63
  end
44
64
 
45
65
  class G < TTYString::CSICode # leftovers:allow
46
- def action(col = 1)
66
+ default_arg '1'
67
+
68
+ def action(col = '1')
69
+ col = integer(col)
70
+ return unless col
71
+
47
72
  # cursor is zero indexed, arg is 1 indexed
48
- cursor.col = col.to_i - 1
73
+ cursor.col = col - 1
49
74
  end
50
75
  end
51
76
 
52
77
  class H < TTYString::CSICode # leftovers:allow
53
- def action(row = 1, col = 1)
78
+ default_arg '1'
79
+
80
+ def action(row = '1', col = '1')
81
+ col = integer(col)
82
+ row = integer(row)
83
+ return unless col && row
84
+
54
85
  # cursor is zero indexed, arg is 1 indexed
55
- cursor.row = row.to_i - 1
56
- cursor.col = col.to_i - 1
86
+ cursor.row = row - 1
87
+ cursor.col = col - 1
57
88
  end
58
89
  end
59
90
 
60
91
  class LowH < TTYString::CSICode # leftovers:allow
61
- char(/\?2004h/)
92
+ char('h')
93
+
94
+ def action(code)
95
+ case code
96
+ when '?5', '?25', '?1004', '?1049', '?2004'
97
+ # drop
98
+ else
99
+ parser.unknown
100
+ end
101
+ end
62
102
  end
63
103
 
64
104
  class LowF < TTYString::CSICode::H # leftovers:allow
@@ -66,74 +106,70 @@ class TTYString
66
106
  end
67
107
 
68
108
  class J < TTYString::CSICode # leftovers:allow
69
- default_arg 0
70
-
71
- def self.arg_re
72
- /[0-3]?/
73
- end
74
-
75
- def action(mode = 0)
76
- # :nocov: else won't ever be called. don't worry about it
109
+ def action(mode = '0') # rubocop:disable Metrics/MethodLength
77
110
  case mode
78
- # :nocov:
79
- when 0 then screen.clear_forward
80
- when 1 then screen.clear_backward
81
- when 2, 3 then screen.clear
111
+ when '0' then screen.clear_forward
112
+ when '1' then screen.clear_backward
113
+ when '2', '3' then screen.clear
114
+ else parser.unknown
82
115
  end
83
116
  end
84
117
  end
85
118
 
86
119
  class K < TTYString::CSICode # leftovers:allow
87
- default_arg 0
88
-
89
- def self.arg_re
90
- /[0-2]?/
91
- end
92
-
93
- def action(mode = 0)
94
- # :nocov: else won't ever be called. don't worry about it
120
+ def action(mode = '0') # rubocop:disable Metrics/MethodLength
95
121
  case mode
96
- # :nocov:
97
- when 0 then screen.clear_line_forward
98
- when 1 then screen.clear_line_backward
99
- when 2 then screen.clear_line
122
+ when '0' then screen.clear_line_forward
123
+ when '1' then screen.clear_line_backward
124
+ when '2' then screen.clear_line
125
+ else parser.unknown
100
126
  end
101
127
  end
102
128
  end
103
129
 
104
- class LowL < TTYString::CSICode # leftovers:allow
105
- char(/\?2004l/)
130
+ class LowL < TTYString::CSICode::LowH # leftovers:allow
131
+ char('l')
106
132
  end
107
133
 
108
134
  class LowM < TTYString::CSICode # leftovers:allow
109
135
  char 'm'
110
136
 
111
- def self.arg_re
112
- # 0-255
113
- /(?:\d|\d\d|1\d\d|2[0-4]\d|25[0-5])?/
114
- end
115
-
116
- def self.render(renderer)
117
- super if renderer.clear_style
137
+ def action(arg = '0', *args)
138
+ screen.style(args.unshift(arg))
118
139
  end
119
-
120
- def action(*args); end
121
140
  end
122
141
 
123
142
  class S < TTYString::CSICode # leftovers:allow
124
- def action(rows = 1)
125
- rows.times { screen.scroll_up }
143
+ def action(rows = '1')
144
+ integer(rows)&.times { screen.scroll_up }
126
145
  end
127
146
  end
128
147
 
129
148
  class T < TTYString::CSICode # leftovers:allow
130
- def action(rows = 1)
131
- rows.times { screen.scroll_down }
149
+ def action(rows = '1')
150
+ integer(rows)&.times { screen.scroll_down }
132
151
  end
133
152
  end
134
153
 
135
154
  class Tilde < TTYString::CSICode # leftovers:allow
136
- char(/(?:200|201)~/)
155
+ char('~')
156
+
157
+ def action(arg)
158
+ case arg
159
+ when '200', '201'
160
+ # bracketed paste
161
+ else
162
+ parser.unknown
163
+ end
164
+ end
165
+ end
166
+
167
+ class Unknown < TTYString::CSICode # leftovers:allow
168
+ char(/[@-~]/)
169
+
170
+ def action(*)
171
+ parser.unknown
172
+ end
137
173
  end
138
174
  end
139
175
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class TTYString
3
+ module TTYString
4
4
  # point on a screen. you can move it
5
5
  class Cursor
6
6
  attr_reader :row, :col
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTYString
4
+ class NullStyle
5
+ class << self
6
+ def new(*)
7
+ self
8
+ end
9
+
10
+ def to_s(*)
11
+ ''
12
+ end
13
+ end
14
+ end
15
+ end
@@ -3,18 +3,21 @@
3
3
  require 'strscan'
4
4
  require_relative 'code_definitions'
5
5
  require_relative 'csi_code_definitions'
6
-
6
+ require_relative 'style'
7
+ require_relative 'null_style'
7
8
  require_relative 'screen'
8
9
 
9
- class TTYString
10
+ module TTYString
10
11
  # Reads the text string a
11
12
  class Parser < StringScanner
12
- attr_accessor :clear_style
13
- attr_reader :screen
13
+ attr_reader :style_handler, :screen
14
+
15
+ def render(style:, unknown:) # rubocop:disable Metrics/MethodLength
16
+ @style_handler = style
17
+ @unknown_handler = unknown
14
18
 
15
- def render
16
19
  reset
17
- @screen = Screen.new
20
+ @screen = Screen.new(initial_style: initial_style)
18
21
  read until eos?
19
22
  screen.to_s
20
23
  end
@@ -23,14 +26,37 @@ class TTYString
23
26
  screen.cursor
24
27
  end
25
28
 
29
+ def unknown # rubocop:disable Metrics/MethodLength
30
+ case unknown_handler
31
+ when RAISE
32
+ raise(
33
+ UnknownCodeError,
34
+ if block_given?
35
+ yield(matched)
36
+ else
37
+ "Unknown code #{matched.inspect}"
38
+ end
39
+ )
40
+ end
41
+ end
42
+
43
+ def initial_style
44
+ @initial_style ||= case style_handler
45
+ when RENDER then Style.new(parser: self)
46
+ else NullStyle
47
+ end
48
+ end
49
+
26
50
  private
27
51
 
52
+ attr_reader :unknown_handler
53
+
28
54
  def write(string)
29
55
  screen.write(string)
30
56
  end
31
57
 
32
58
  def read
33
- TTYString::Code.descendants.any? { |c| c.render(self) } || default
59
+ Code.descendants.any? { |c| c.render(self) } || default
34
60
  end
35
61
 
36
62
  def default
@@ -1,29 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'cursor'
4
+ require_relative 'cell'
5
+ require_relative 'style'
4
6
 
5
- class TTYString
7
+ module TTYString
6
8
  # a grid to draw on
7
9
  class Screen
8
10
  attr_reader :cursor
9
11
 
10
- def initialize
12
+ def initialize(initial_style:)
11
13
  @cursor = Cursor.new
12
14
  @screen = []
15
+ @current_style = @initial_style = initial_style
13
16
  end
14
17
 
15
- def to_s
16
- screen.map { |c| Array(c).map { |x| x || ' ' }.join.rstrip }.join("\n")
18
+ def to_s # rubocop:disable Metrics/MethodLength
19
+ style_context = initial_style
20
+ screen.map do |row|
21
+ Array(row).map do |cell|
22
+ if cell
23
+ value = cell.to_s(style_context: style_context)
24
+ style_context = cell.style
25
+ value
26
+ else
27
+ ' '
28
+ end
29
+ end.join.rstrip
30
+ end.join("\n") + current_style.to_s(context: style_context)
17
31
  end
18
32
 
19
- def []=((row, col), *value)
33
+ def []=((row, col), value)
20
34
  screen[row] ||= []
21
- screen[row][col] = value.flatten(1)
22
- end
23
-
24
- def []((row, col))
25
- screen[row] ||= []
26
- screen[row][col]
35
+ screen[row][col] = value
27
36
  end
28
37
 
29
38
  def clear_at_cursor
@@ -74,18 +83,25 @@ class TTYString
74
83
  clear_line_forward
75
84
  end
76
85
 
77
- def write(string)
78
- return self[cursor] if string.empty?
86
+ def ensure_row
87
+ screen[row] ||= []
88
+ end
79
89
 
90
+ def write(string)
80
91
  string.each_char do |char|
81
- self[cursor] = char
92
+ self[cursor] = Cell.new(char, style: current_style)
82
93
  cursor.right
83
94
  end
84
95
  end
85
96
 
97
+ def style(style_codes)
98
+ self.current_style = current_style.new(style_codes)
99
+ end
100
+
86
101
  private
87
102
 
88
- attr_reader :screen
103
+ attr_reader :screen, :initial_style
104
+ attr_accessor :current_style
89
105
 
90
106
  def row
91
107
  cursor.row
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTYString
4
+ class Style # rubocop:disable Metrics/ClassLength
5
+ attr_reader :properties
6
+
7
+ def initialize(style_codes = ['0'], parser:, properties: {})
8
+ @properties = properties.dup
9
+ @parser = parser
10
+ parse_code(style_codes)
11
+ end
12
+
13
+ def new(style_codes)
14
+ self.class.new(style_codes, properties: properties, parser: parser)
15
+ end
16
+
17
+ def to_s(context:)
18
+ return '' if self == context
19
+
20
+ values = properties.map { |k, v| v if context.properties[k] != v }.compact.uniq
21
+ return '' if values.empty?
22
+
23
+ "\e[#{values.join(';')}m"
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :parser
29
+
30
+ # delete then write to keep the order
31
+ def set(*new_properties, code)
32
+ new_properties.each do |property|
33
+ properties.delete(property)
34
+ properties[property] = code
35
+ end
36
+ end
37
+
38
+ def slurp_color(enum, code)
39
+ case (subcode = enum.next)
40
+ when '2' then "#{code};#{subcode};#{color_param(enum)};#{color_param(enum)};#{color_param(enum)}"
41
+ when '5' then "#{code};#{subcode};#{color_param(enum)}"
42
+ else unknown(subcode)
43
+ end
44
+ end
45
+
46
+ def color_param(enum)
47
+ code = enum.next
48
+ return unknown(code) unless code.match?(/\A\d*\z/)
49
+
50
+ code_i = code.to_i
51
+ return unknown(code) unless code_i < 256
52
+
53
+ code_i
54
+ end
55
+
56
+ def unknown(style_code)
57
+ parser.unknown { |code| "Unknown style code #{style_code.inspect} in #{code.inspect}" }
58
+ end
59
+
60
+ def parse_code(style_codes) # rubocop:disable Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize
61
+ enum = style_codes.each
62
+ loop do # rubocop:disable Metrics/BlockLength
63
+ case (code = enum.next)
64
+ when '0'
65
+ set(
66
+ :background,
67
+ :blink,
68
+ :bold,
69
+ :color,
70
+ :conceal,
71
+ :dim,
72
+ :encircle,
73
+ :font,
74
+ :frame,
75
+ :ideogram_overline,
76
+ :ideogram_stress,
77
+ :ideogram_underline,
78
+ :italic,
79
+ :overline,
80
+ :vertical_position,
81
+ :proportional,
82
+ :reverse,
83
+ :strike,
84
+ :underline_color,
85
+ :underline,
86
+ :double_underline_or_not_bold,
87
+ code
88
+ )
89
+ when '1'
90
+ set(:bold, code)
91
+ when '2'
92
+ set(:dim, code)
93
+ when '3'
94
+ set(:italic, code)
95
+ when '4', '24'
96
+ set(:underline, code)
97
+ when '5', '6', '25'
98
+ set(:blink, code)
99
+ when '7', '27'
100
+ set(:reverse, code)
101
+ when '8', '28'
102
+ set(:conceal, code)
103
+ when '9', '29'
104
+ set(:strike, code)
105
+ when '10'..'20'
106
+ set(:font, code)
107
+ when '21'
108
+ set(:double_underline_or_not_bold, code)
109
+ when '22'
110
+ set(:bold, :dim, code)
111
+ when '23'
112
+ set(:font, :italic, code)
113
+ when '26', '50'
114
+ set(:proportional, code)
115
+ when '30'..'37', '39', '90'..'97'
116
+ set(:color, code)
117
+ when '38'
118
+ set(:color, slurp_color(enum, code))
119
+ when '40'..'47', '49', '100'..'107'
120
+ set(:background, code)
121
+ when '48'
122
+ set(:background, slurp_color(enum, code))
123
+ when '51'
124
+ set(:frame, code)
125
+ when '52'
126
+ set(:encircle, code)
127
+ when '53', '55'
128
+ set(:overline, code)
129
+ when '54'
130
+ set(:frame, :encircle, code)
131
+ when '58'
132
+ set(:underline_color, slurp_color(enum, code))
133
+ when '59'
134
+ set(:underline_color, code)
135
+ when '60', '61'
136
+ set(:ideogram_underline, code)
137
+ when '62', '63'
138
+ set(:ideogram_overline, code)
139
+ when '64'
140
+ set(:ideogram_stress, code)
141
+ when '65'
142
+ set(:ideogram_stress, :ideogram_overline, :ideogram_underline, code)
143
+ when '73', '74', '75'
144
+ set(:vertical_position, code)
145
+ else
146
+ unknown(code)
147
+ end
148
+ end
149
+ rescue StopIteration
150
+ # go until the end
151
+ end
152
+ end
153
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class TTYString
4
- VERSION = '1.1.1'
3
+ module TTYString
4
+ VERSION = '2.0.0'
5
5
  end
data/lib/tty_string.rb CHANGED
@@ -4,27 +4,33 @@ require_relative 'tty_string/parser'
4
4
 
5
5
  # Renders a string taking into ANSI escape codes and \t\r\n etc
6
6
  # Usage: TTYString.parse("This\r\e[KThat") => "That"
7
- class TTYString
7
+ module TTYString
8
+ class Error < StandardError; end
9
+ class UnknownCodeError < Error; end
10
+
11
+ RENDER = :render
12
+ RAISE = :raise
13
+ DROP = :drop
14
+
15
+ UNKNOWN_OPTIONS = [RAISE, DROP].freeze
16
+ private_constant :UNKNOWN_OPTIONS
17
+ STYLE_OPTIONS = [RENDER, DROP].freeze
18
+ private_constant :STYLE_OPTIONS
19
+
8
20
  class << self
9
- def parse(input_string, clear_style: true)
10
- new(input_string, clear_style: clear_style).to_s
21
+ def parse(input_string, style: DROP, unknown: DROP) # rubocop:disable Metrics/MethodLength
22
+ unless STYLE_OPTIONS.include?(style)
23
+ raise ArgumentError, '`style:` must be either TTYString::RENDER or TTYString::DROP (default)'
24
+ end
25
+ unless UNKNOWN_OPTIONS.include?(unknown)
26
+ raise ArgumentError, '`unknown:` must be either TTYString::RAISE or TTYString::DROP (default)'
27
+ end
28
+
29
+ Parser.new(input_string).render(style: style, unknown: unknown)
11
30
  end
12
31
 
13
32
  def to_proc
14
33
  method(:parse).to_proc
15
34
  end
16
35
  end
17
-
18
- def initialize(input_string, clear_style: true)
19
- @parser = Parser.new(input_string)
20
- @parser.clear_style = clear_style
21
- end
22
-
23
- def to_s
24
- parser.render
25
- end
26
-
27
- private
28
-
29
- attr_reader :clear_style, :parser
30
36
  end
data/tty_string.gemspec CHANGED
@@ -30,7 +30,8 @@ Gem::Specification.new do |spec|
30
30
  spec.required_ruby_version = '>= 2.4'
31
31
  spec.require_paths = ['lib']
32
32
 
33
- spec.add_development_dependency 'bundler', '~> 2.0'
33
+ spec.add_development_dependency 'base64'
34
+ spec.add_development_dependency 'bundler'
34
35
  spec.add_development_dependency 'fast_ignore', '>= 0.15.1'
35
36
  spec.add_development_dependency 'leftovers', '>= 0.2.0'
36
37
  spec.add_development_dependency 'pry', '~> 0.12'
metadata CHANGED
@@ -1,29 +1,42 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tty_string
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dana Sherson
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2021-06-09 00:00:00.000000000 Z
10
+ date: 2025-01-25 00:00:00.000000000 Z
12
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: base64
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
13
26
  - !ruby/object:Gem::Dependency
14
27
  name: bundler
15
28
  requirement: !ruby/object:Gem::Requirement
16
29
  requirements:
17
- - - "~>"
30
+ - - ">="
18
31
  - !ruby/object:Gem::Version
19
- version: '2.0'
32
+ version: '0'
20
33
  type: :development
21
34
  prerelease: false
22
35
  version_requirements: !ruby/object:Gem::Requirement
23
36
  requirements:
24
- - - "~>"
37
+ - - ">="
25
38
  - !ruby/object:Gem::Version
26
- version: '2.0'
39
+ version: '0'
27
40
  - !ruby/object:Gem::Dependency
28
41
  name: fast_ignore
29
42
  requirement: !ruby/object:Gem::Requirement
@@ -178,7 +191,6 @@ dependencies:
178
191
  - - ">="
179
192
  - !ruby/object:Gem::Version
180
193
  version: 0.8.1
181
- description:
182
194
  email:
183
195
  - robot@dana.sh
184
196
  executables: []
@@ -190,13 +202,16 @@ files:
190
202
  - LICENSE.txt
191
203
  - README.md
192
204
  - lib/tty_string.rb
205
+ - lib/tty_string/cell.rb
193
206
  - lib/tty_string/code.rb
194
207
  - lib/tty_string/code_definitions.rb
195
208
  - lib/tty_string/csi_code.rb
196
209
  - lib/tty_string/csi_code_definitions.rb
197
210
  - lib/tty_string/cursor.rb
211
+ - lib/tty_string/null_style.rb
198
212
  - lib/tty_string/parser.rb
199
213
  - lib/tty_string/screen.rb
214
+ - lib/tty_string/style.rb
200
215
  - lib/tty_string/version.rb
201
216
  - tty_string.gemspec
202
217
  homepage: https://github.com/robotdana/tty_string
@@ -206,7 +221,6 @@ metadata:
206
221
  homepage_uri: https://github.com/robotdana/tty_string
207
222
  source_code_uri: https://github.com/robotdana/tty_string
208
223
  changelog_uri: https://github.com/robotdana/tty_string/blob/main/CHANGELOG.md
209
- post_install_message:
210
224
  rdoc_options: []
211
225
  require_paths:
212
226
  - lib
@@ -221,8 +235,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
221
235
  - !ruby/object:Gem::Version
222
236
  version: '0'
223
237
  requirements: []
224
- rubygems_version: 3.0.3
225
- signing_key:
238
+ rubygems_version: 3.6.2
226
239
  specification_version: 4
227
240
  summary: Render a string using ANSI TTY codes
228
241
  test_files: []