terminal_rb 1.0.6 → 1.0.8

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.
@@ -392,18 +392,41 @@ module Terminal
392
392
 
393
393
  line.encode!(ENCODING) if line.encoding != ENCODING
394
394
 
395
- line.scan(SCAN_REGEX) do |newline, space, _, _, char|
396
- if char
397
- (last = @lex[-1]).is_a?(Word) and next last.add(char)
398
- next @lex << Word.new(char)
395
+ line.scan(SCAN_REGEX) do |token|
396
+ b = token.getbyte(0)
397
+
398
+ if b > 0x20 # word chunk: an ASCII run or a single grapheme
399
+ # a single-byte token is printable ASCII: one character, one
400
+ # column - no need to ask for its width
401
+ size = token.bytesize
402
+ if size == 1
403
+ (last = @lex[-1]).is_a?(Word) and next last.add(token, 1)
404
+ next @lex << Word.new(token, 1)
405
+ end
406
+ # an ASCII run is the only multi-byte token which cannot be a
407
+ # grapheme cluster - there its width is simply its byte count
408
+ size = nil unless b < 0x80 && token.ascii_only?
409
+ if (last = @lex[-1]).is_a?(Word)
410
+ next size ? last.add(token, size, true) : last.add(token)
411
+ end
412
+ next @lex << Word.new(token, size, true) if size
413
+ next @lex << Word.new(token)
399
414
  end
400
415
 
401
- if space
402
- (last = @lex[-1]).is_a?(Space) and next last.inc
403
- next @lex << Space.new
416
+ if SPACE_BYTE[b] && !(b == 0x0d && token.bytesize == 2)
417
+ size = token.bytesize
418
+ (last = @lex[-1]).is_a?(Space) and next last.size += size
419
+ next @lex << Space.new(size)
404
420
  end
405
421
 
406
- next @lex << EOL if newline
422
+ next @lex << EOL if b == 0x0a || b == 0x0d
423
+
424
+ # escape sequences are dropped without `ansi`, any other control
425
+ # character is text
426
+ next if b == 0x1b && ((c = token.getbyte(1)) == 0x5b || c == 0x5d)
427
+
428
+ (last = @lex[-1]).is_a?(Word) and next last.add(token)
429
+ @lex << Word.new(token)
407
430
  end
408
431
 
409
432
  @lex[-1] == EOL ? @lex[-1] = EOP : @lex << EOP
@@ -421,23 +444,46 @@ module Terminal
421
444
  line.encode!(ENCODING) if line.encoding != ENCODING
422
445
  csis = 0
423
446
 
424
- line.scan(SCAN_REGEX) do |newline, space, csi, osc, char|
425
- if char
426
- (last = @lex[-1]).is_a?(Word) and next last.add(char)
427
- next @lex << Word.new(char)
447
+ line.scan(SCAN_REGEX) do |token|
448
+ b = token.getbyte(0)
449
+
450
+ if b > 0x20 # word chunk: an ASCII run or a single grapheme
451
+ # a single-byte token is printable ASCII: one character, one
452
+ # column - no need to ask for its width
453
+ size = token.bytesize
454
+ if size == 1
455
+ (last = @lex[-1]).is_a?(Word) and next last.add(token, 1)
456
+ next @lex << Word.new(token, 1)
457
+ end
458
+ # an ASCII run is the only multi-byte token which cannot be a
459
+ # grapheme cluster - there its width is simply its byte count
460
+ size = nil unless b < 0x80 && token.ascii_only?
461
+ if (last = @lex[-1]).is_a?(Word)
462
+ next size ? last.add(token, size, true) : last.add(token)
463
+ end
464
+ next @lex << Word.new(token, size, true) if size
465
+ next @lex << Word.new(token)
428
466
  end
429
467
 
430
- if space
431
- (last = @lex[-1]).is_a?(Space) and next last.inc
432
- next @lex << Space.new
468
+ if SPACE_BYTE[b] && !(b == 0x0d && token.bytesize == 2)
469
+ size = token.bytesize
470
+ (last = @lex[-1]).is_a?(Space) and next last.size += size
471
+ next @lex << Space.new(size)
433
472
  end
434
473
 
435
- next @lex << EOL if newline
474
+ next @lex << EOL if b == 0x0a || b == 0x0d
436
475
 
437
- next @lex << Osc.new(osc) if osc
476
+ if (c = token.getbyte(1)) == 0x5d # Osc
477
+ next @lex << Osc.new(token)
478
+ end
479
+ if c != 0x5b # Csi
480
+ # a bare escape is just text
481
+ (last = @lex[-1]).is_a?(Word) and next last.add(token)
482
+ next @lex << Word.new(token)
483
+ end
438
484
 
439
- # Handle csi...
440
- if csi == "\e[m" || csi == "\e[0m"
485
+ # Handle Csi...
486
+ if token == "\e[m" || token == "\e[0m"
441
487
  next if csis == 0
442
488
 
443
489
  if @lex[-1].is_a?(Csi)
@@ -449,21 +495,17 @@ module Terminal
449
495
  next @lex << CsiEnd
450
496
  end
451
497
 
452
- (last = @lex[-1]).is_a?(Csi) and next last.add(csi)
498
+ (last = @lex[-1]).is_a?(Csi) and next last.add(token)
453
499
 
454
500
  csis += 1
455
- @lex << Csi.new(csi)
501
+ @lex << Csi.new(token)
456
502
  end
457
503
 
458
504
  @lex.pop if (last = @lex[-1]) == EOL
459
505
 
460
506
  next @lex << EOP if csis == 0
461
507
 
462
- if last.is_a?(Csi) && csis == 1
463
- @lex.pop
464
- else
465
- @lex << CsiEnd
466
- end
508
+ last.is_a?(Csi) && csis == 1 ? @lex.pop : @lex << CsiEnd
467
509
  @lex << EOP
468
510
  end
469
511
 
@@ -517,33 +559,57 @@ module Terminal
517
559
  class Space
518
560
  attr_accessor :size
519
561
  def to_str = (@size == 1 ? ' ' : ' ' * @size)
520
- def initialize = (@size = 1)
562
+ def initialize(size = 1) = (@size = size)
521
563
  def inc = (@size += 1)
522
564
  def inspect = @size == 1 ? '<Space>' : "<Space #{@size}>"
523
565
  end
524
566
 
525
567
  # @private
568
+ # A chunk is either a single grapheme cluster or a run of printable
569
+ # ASCII, for which the display width equals the character count.
526
570
  class Word
527
- def to_str = (@to_str ||= @chars.join)
528
- def size = (@size ||= @sizes.sum)
529
- def inspect = "<Word #{@sizes.sum}:#{@chars.join.inspect}>"
530
-
531
- def initialize(char, size = Text.char_width(char))
532
- @chars = [char]
571
+ def to_str = (@to_str ||= @chunks.size == 1 ? @chunks[0] : @chunks.join)
572
+ def size = (@size ||= @sizes.size == 1 ? @sizes[0] : @sizes.sum)
573
+ def inspect = "<Word #{@sizes.sum}:#{@chunks.join.inspect}>"
574
+
575
+ # `run` marks a chunk holding more than one character; only the lexer
576
+ # creates those and it knows without having to inspect the chunk again
577
+ def initialize(chunk, size = Text.char_width(chunk), run = false)
578
+ @chunks = [chunk]
533
579
  @sizes = [size]
580
+ @runs = run
534
581
  end
535
582
 
536
- def add(char, size = Text.char_width(char))
537
- @chars << char
583
+ def add(chunk, size = Text.char_width(chunk), run = false)
584
+ @chunks << chunk
538
585
  @sizes << size
586
+ @runs ||= run
539
587
  nil
540
588
  end
541
589
 
542
590
  def split(width)
543
- return [self] if @chars.size == 1
591
+ if @runs
592
+ # ASCII runs have to become single characters again to be split -
593
+ # a multi-character grapheme cluster must stay untouched
594
+ chars = []
595
+ sizes = []
596
+ @chunks.each_with_index do |chunk, idx|
597
+ if chunk.bytesize > 1 && chunk.getbyte(0) < 0x80 &&
598
+ chunk.ascii_only?
599
+ chunk.each_char { chars << it and sizes << 1 }
600
+ else
601
+ chars << chunk
602
+ sizes << @sizes[idx]
603
+ end
604
+ end
605
+ else
606
+ chars = @chunks
607
+ sizes = @sizes
608
+ end
609
+ return [self] if chars.size == 1
544
610
  ret = last = size = nil
545
- @chars.each_with_index do |char, idx|
546
- csize = @sizes[idx]
611
+ chars.each_with_index do |char, idx|
612
+ csize = sizes[idx]
547
613
  next ret = [last = Word.new(char, size = csize)] unless ret
548
614
  next last.add(char, csize) if (size += csize) <= width
549
615
  ret << (last = Word.new(char, size = csize))
@@ -597,16 +663,36 @@ module Terminal
597
663
  space_cache[2]
598
664
  SPACE_CACHE = space_cache.freeze
599
665
 
666
+ # A run of printable ASCII is only taken when the following character is
667
+ # ASCII too: everything which can extend a grapheme cluster (combining
668
+ # marks, ZWJ, variation selectors, ...) is non-ASCII, so a run can never
669
+ # swallow the base of a cluster `\X` would have kept together.
600
670
  SCAN_REGEX =
601
- /\G(?:
602
- (\r?\n)
603
- | (\s)
604
- | (\e\[[\x30-\x3f]*[\x20-\x2f]*[a-zA-Z])
605
- | (\e\]\d+(?:;[^\a\e]+)*(?:\a|\e\\))
606
- | (\X)
607
- )/x
671
+ /
672
+ [\x21-\x7e]+(?=[\x00-\x7f]|\z)
673
+ | \x20+
674
+ | \r?\n
675
+ | \e\[[\x30-\x3f]*[\x20-\x2f]*[a-zA-Z]
676
+ | \e\]\d+(?:;[^\a\e]+)*(?:\a|\e\\)
677
+ | \s
678
+ | \X
679
+ /x
680
+
681
+ SPACE_BYTE =
682
+ Hash
683
+ .new(false)
684
+ .compare_by_identity
685
+ .merge!(
686
+ 0x20 => true,
687
+ 0x09 => true,
688
+ 0x0b => true,
689
+ 0x0c => true,
690
+ 0x0d => true
691
+ )
692
+ .freeze
608
693
 
609
694
  private_constant(
695
+ :SPACE_BYTE,
610
696
  :Space,
611
697
  :Word,
612
698
  :Osc,
data/lib/terminal/text.rb CHANGED
@@ -29,7 +29,13 @@ module Terminal
29
29
  # Terminal::Text.ambiguous_char_width = 2 # for CJK terminals
30
30
  #
31
31
  # @return [Integer] default: `1`
32
- attr_accessor :ambiguous_char_width
32
+ attr_reader :ambiguous_char_width
33
+
34
+ # @attribute [w] ambiguous_char_width
35
+ def ambiguous_char_width=(value)
36
+ autoload?(:CharWidth) or CharWidth.clear
37
+ @ambiguous_char_width = value
38
+ end
33
39
 
34
40
  # Calculate the display width of a string in terminal columns.
35
41
  #
@@ -206,66 +212,66 @@ module Terminal
206
212
  def char_width(char)
207
213
  ord = char.ord
208
214
  return @ctrlchar_width[ord] if ord < 0x20
209
- if char.size == 1
210
- return ord < 0xa1 ? 1 : CharWidth[ord] || @ambiguous_char_width
211
- end
215
+ # CharWidth knows this rule too - a comparison just beats a memo hit
216
+ return ord < 0xa1 ? 1 : CharWidth[ord] if char.bytesize == 1
217
+ return 2 if ord == 0x1f3f3 && @very_special_flags.include?(char)
212
218
  sum = 0
213
- zwj = false
214
- char.each_char do |c|
215
- next zwj = false if zwj
216
- ord = c.ord
217
- next zwj = true if ord == 0x200d # zero with joiner
218
- sum += CharWidth[ord] || @ambiguous_char_width
219
+ skip = false
220
+ char.each_codepoint do |cp|
221
+ next skip = false if skip
222
+ next skip = true if cp == 0x200d # zero width joiner
223
+ sum += CharWidth[cp]
219
224
  end
220
225
  sum
221
226
  end
222
227
  end
223
228
 
224
- @ambiguous_char_width = 1
229
+ @very_special_flags = [
230
+ "🏳\u{FE0F}\u{200D}🌈", # rainbow flag
231
+ "🏳\u{FE0F}\u{200D}⚧\u{FE0F}" # transgender flag
232
+ ].freeze
225
233
 
226
- @ctrlchar_width =
227
- Hash
228
- .new(2)
229
- .compare_by_identity
230
- .merge!(
231
- 0x00 => 0,
232
- 0x01 => 1,
233
- 0x02 => 1,
234
- 0x03 => 1,
235
- 0x04 => 1,
236
- 0x05 => 0,
237
- 0x06 => 1,
238
- 0x07 => 0,
239
- 0x08 => 0,
240
- 0x09 => 8,
241
- 0x0a => 0,
242
- 0x0b => 0,
243
- 0x0c => 0,
244
- 0x0d => 0,
245
- 0x0e => 0,
246
- 0x0f => 0,
247
- 0x10 => 1,
248
- 0x11 => 1,
249
- 0x12 => 1,
250
- 0x13 => 1,
251
- 0x14 => 1,
252
- 0x15 => 1,
253
- 0x16 => 1,
254
- 0x17 => 1,
255
- 0x18 => 1,
256
- 0x19 => 1,
257
- 0x1a => 1,
258
- 0x1b => 1,
259
- 0x1c => 1,
260
- 0x1d => 1,
261
- 0x1e => 1,
262
- 0x1f => 1
263
- )
264
- .freeze
234
+ # indexed by ordinal, only ever asked for control characters
235
+ @ctrlchar_width = [
236
+ 0,
237
+ 1,
238
+ 1,
239
+ 1,
240
+ 1,
241
+ 0,
242
+ 1,
243
+ 0,
244
+ 0,
245
+ 1,
246
+ 0,
247
+ 0,
248
+ 0,
249
+ 0,
250
+ 0,
251
+ 0,
252
+ 1,
253
+ 1,
254
+ 1,
255
+ 1,
256
+ 1,
257
+ 1,
258
+ 1,
259
+ 1,
260
+ 1,
261
+ 1,
262
+ 1,
263
+ 1,
264
+ 1,
265
+ 1,
266
+ 1,
267
+ 1
268
+ ].freeze
269
+
270
+ @ambiguous_char_width = 1
265
271
 
266
- cw_file = "#{__dir__}/text/char_width.rb"
267
- autoload :CharWidth, cw_file
268
- autoload :UNICODE_VERSION, cw_file
272
+ char_width_file = "#{__dir__}/text/char_width.rb"
273
+ autoload :CharWidth, char_width_file
274
+ autoload :UNICODE_VERSION, char_width_file
269
275
  private_constant :CharWidth
270
276
  end
271
277
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Terminal
4
4
  # The version number of the gem.
5
- VERSION = '1.0.6'
5
+ VERSION = '1.0.8'
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: terminal_rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.6
4
+ version: 1.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Blumtritt
@@ -60,7 +60,7 @@ metadata:
60
60
  yard.run: yard
61
61
  source_code_uri: https://codeberg.org/mblumtritt/Terminal.rb
62
62
  bug_tracker_uri: https://codeberg.org/mblumtritt/Terminal.rb/issues
63
- documentation_uri: https://rubydoc.info/gems/terminal_rb/1.0.6
63
+ documentation_uri: https://rubydoc.info/gems/terminal_rb/1.0.8
64
64
  rdoc_options: []
65
65
  require_paths:
66
66
  - lib
@@ -75,7 +75,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
75
75
  - !ruby/object:Gem::Version
76
76
  version: '0'
77
77
  requirements: []
78
- rubygems_version: 4.0.15
78
+ rubygems_version: 4.0.18
79
79
  specification_version: 4
80
80
  summary: Fast terminal access with ANSI, CSIu, mouse events, BBCode, word-wise line
81
81
  break support and much more.