tty_string 1.1.1 → 2.0.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 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: []