Module: Kramdown::ANSI::Width

Extended by:
Term::ANSIColor
Includes:
Term::ANSIColor
Included in:
Kramdown::ANSI
Defined in:
lib/kramdown/ansi/width.rb

Overview

A module that provides functionality for calculating terminal width percentages and wrapping/truncating text accordingly.

The Width module includes methods to determine the available terminal width based on a percentage, and provides text wrapping and truncation capabilities that respect terminal dimensions.

Examples:

Wrapping text to 80% of terminal width

Kramdown::ANSI::Width.wrap("This is a long line of text", percentage: 80)

Truncating text to 50 characters

Kramdown::ANSI::Width.truncate("This is a long line of text", length: 50)

Defined Under Namespace

Classes: Token

Constant Summary collapse

ANSI_ESCAPES =
/\e\[.*?m|\e\].*?(\e|\a)\\?/

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.display_width(line) ⇒ Integer

Calculates the display width of a string after removing ANSI escape sequences.

Parameters:

  • line (String) —

    the string to measure

Returns:

  • (Integer) —

    the number of columns the string occupies in a terminal



47
48
49
# File 'lib/kramdown/ansi/width.rb', line 47

def display_width(line)
  Unicode::DisplayWidth.of(remove_ansi_escapes(line))
end

.remove_ansi_escapes(line) ⇒ String

Removes ANSI escape sequences from the given string.

Parameters:

  • line (String) —

    the string potentially containing ANSI escape codes.

Returns:

  • (String) —

    the string with all ANSI escape sequences removed.



30
31
32
# File 'lib/kramdown/ansi/width.rb', line 30

def remove_ansi_escapes(line)
  line.to_s.gsub(ANSI_ESCAPES, '')
end

.truncate(text, percentage: nil, length: nil, ellipsis: ?…) ⇒ String

Truncates a given string to a specified length or percentage. If the text is longer an ellipsis sequence is added at the end of the generated string, to indicate that a truncation has been performed.

Parameters:

  • text (String) —

    the input string to truncate

  • percentage (Hash) (defaults to: nil) —

    a customizable set of options

  • length (Hash) (defaults to: nil) —

    a customizable set of options

  • ellipsis (Hash) (defaults to: ?…) —

    a customizable set of options

Options Hash (percentage:):

  • the (Numeric) —

    percentage value for the width (defaults to nil)

Options Hash (length:):

  • the (Integer) —

    character length for the width (defaults to nil)

Options Hash (ellipsis:):

  • the (Character) —

    truncation indicator (defaults to ?…)

Returns:

  • (String) —

    the truncated string

Raises:

  • (ArgumentError) —

    if neither percentage nor length is provided



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/kramdown/ansi/width.rb', line 130

def truncate(text, percentage: nil, length: nil, ellipsis: ?…)
  percentage.nil? ^ length.nil? or
    raise ArgumentError, "either pass percentage or length argument"
  percentage and length ||= width(percentage:)
  ellipsis_length = display_width(remove_ansi_escapes(ellipsis))
  found_esc       = false
  result          = +''

  return result if text.empty?
  return result if length < ellipsis_length

  current_length = 0
  tokens = []

  text.scan(/(?<esc>#{ANSI_ESCAPES})|(?<txt>.)/m) do
    token = Token.new(
      type:   $~[:esc] ? :esc : :txt,
      value:  $&,
      length: $~[:esc] ? 0 : display_width($&)
    )
    tokens << token
  end

  while token = tokens.shift
    if token.type == :esc
      found_esc = true
      result << token.value
      next
    end

    if current_length + token.length == length && tokens.empty?
      result << token.value
      current_length += token.length
      break
    elsif current_length + token.length + ellipsis_length > length
      result << ellipsis
      break
    elsif current_length + token.length + ellipsis_length == length
      result << token.value
      current_length += token.length
      remaining_text_length = tokens.select { _1.type == :txt }.map(&:length).sum
      if current_length + remaining_text_length == length
        next
      elsif current_length + remaining_text_length > length
        result << ellipsis
        break
      else
        next
      end
    else
      current_length += token.length
      result << token.value
    end

  end

  result << reset if found_esc && !result.end_with?(reset)
  result
end

.width(percentage: 100.0) ⇒ Integer

Returns the width of the terminal in characters, given a percentage.

Parameters:

  • percentage (Numeric) (defaults to: 100.0) —

    the percentage value (defaults to 100.0)

Returns:

  • (Integer) —

    the calculated width



38
39
40
# File 'lib/kramdown/ansi/width.rb', line 38

def width(percentage: 100.0)
  ((Float(percentage) * Tins::Terminal.columns) / 100).floor
end

.wrap(text, percentage: nil, length: nil) ⇒ String

Wraps a string to a given width, either by percentage of terminal width or by an explicit character length.

Examples:

Kramdown::ANSI::Width.wrap("Hello world!", percentage: 50)
# => "Hello\nworld!"

Parameters:

  • text (String) —

    the text to wrap

  • percentage (Hash) (defaults to: nil) —

    a customizable set of options

  • length (Hash) (defaults to: nil) —

    a customizable set of options

Options Hash (percentage:):

  • the (Numeric) —

    percentage of terminal width to use

Options Hash (length:):

  • the (Integer) —

    explicit character width to wrap to

Returns:

  • (String) —

    the wrapped string, preserving ANSI escape sequences

Raises:

  • (ArgumentError) —

    if neither percentage nor length is provided



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/kramdown/ansi/width.rb', line 62

def wrap(text, percentage: nil, length: nil)
  percentage.nil? ^ length.nil? or
    raise ArgumentError, "either pass percentage or length argument"
  percentage and length ||= width(percentage:)
  return text if length <= 0
  begin
    length = length.to_i
  rescue FloatDomainError
    return text
  end
  length = length.clamp(1..)
  length_without_ansi = 0
  result = +''
  text.scan(/(?<nws>\S+)|(?<ws>\s)/) do
    token_value  = $&
    token_length = display_width(token_value)
    if $~[:nws]
      if length_without_ansi + token_length > length
        length_without_ansi = token_length
        if result[-1] == ' '
          result[-1, 1] = "\n#{token_value}"
        else
          result << "\n#{token_value}"
        end
      else
        length_without_ansi += token_length
        result << token_value
      end
    else
      length_without_ansi += token_length
      result << token_value
    end
  end
  result
end

Instance Method Details

#display_width(line) ⇒ Integer (private)

Calculates the display width of a string after removing ANSI escape sequences.

Parameters:

  • line (String) —

    the string to measure

Returns:

  • (Integer) —

    the number of columns the string occupies in a terminal



47
48
49
# File 'lib/kramdown/ansi/width.rb', line 47

def display_width(line)
  Unicode::DisplayWidth.of(remove_ansi_escapes(line))
end

#remove_ansi_escapes(line) ⇒ String (private)

Removes ANSI escape sequences from the given string.

Parameters:

  • line (String) —

    the string potentially containing ANSI escape codes.

Returns:

  • (String) —

    the string with all ANSI escape sequences removed.



30
31
32
# File 'lib/kramdown/ansi/width.rb', line 30

def remove_ansi_escapes(line)
  line.to_s.gsub(ANSI_ESCAPES, '')
end

#truncate(text, percentage: nil, length: nil, ellipsis: ?…) ⇒ String (private)

Truncates a given string to a specified length or percentage. If the text is longer an ellipsis sequence is added at the end of the generated string, to indicate that a truncation has been performed.

Parameters:

  • text (String) —

    the input string to truncate

Options Hash (percentage:):

  • the (Numeric) —

    percentage value for the width (defaults to nil)

Options Hash (length:):

  • the (Integer) —

    character length for the width (defaults to nil)

Options Hash (ellipsis:):

  • the (Character) —

    truncation indicator (defaults to ?…)

Returns:

  • (String) —

    the truncated string

Raises:

  • (ArgumentError) —

    if neither percentage nor length is provided



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/kramdown/ansi/width.rb', line 130

def truncate(text, percentage: nil, length: nil, ellipsis: ?…)
  percentage.nil? ^ length.nil? or
    raise ArgumentError, "either pass percentage or length argument"
  percentage and length ||= width(percentage:)
  ellipsis_length = display_width(remove_ansi_escapes(ellipsis))
  found_esc       = false
  result          = +''

  return result if text.empty?
  return result if length < ellipsis_length

  current_length = 0
  tokens = []

  text.scan(/(?<esc>#{ANSI_ESCAPES})|(?<txt>.)/m) do
    token = Token.new(
      type:   $~[:esc] ? :esc : :txt,
      value:  $&,
      length: $~[:esc] ? 0 : display_width($&)
    )
    tokens << token
  end

  while token = tokens.shift
    if token.type == :esc
      found_esc = true
      result << token.value
      next
    end

    if current_length + token.length == length && tokens.empty?
      result << token.value
      current_length += token.length
      break
    elsif current_length + token.length + ellipsis_length > length
      result << ellipsis
      break
    elsif current_length + token.length + ellipsis_length == length
      result << token.value
      current_length += token.length
      remaining_text_length = tokens.select { _1.type == :txt }.map(&:length).sum
      if current_length + remaining_text_length == length
        next
      elsif current_length + remaining_text_length > length
        result << ellipsis
        break
      else
        next
      end
    else
      current_length += token.length
      result << token.value
    end

  end

  result << reset if found_esc && !result.end_with?(reset)
  result
end

#width(percentage: 100.0) ⇒ Integer (private)

Returns the width of the terminal in characters, given a percentage.

Parameters:

  • percentage (Numeric) (defaults to: 100.0) —

    the percentage value (defaults to 100.0)

Returns:

  • (Integer) —

    the calculated width



38
39
40
# File 'lib/kramdown/ansi/width.rb', line 38

def width(percentage: 100.0)
  ((Float(percentage) * Tins::Terminal.columns) / 100).floor
end

#wrap(text, percentage: nil, length: nil) ⇒ String (private)

Wraps a string to a given width, either by percentage of terminal width or by an explicit character length.

Examples:

Kramdown::ANSI::Width.wrap("Hello world!", percentage: 50)
# => "Hello\nworld!"

Parameters:

  • text (String) —

    the text to wrap

Options Hash (percentage:):

  • the (Numeric) —

    percentage of terminal width to use

Options Hash (length:):

  • the (Integer) —

    explicit character width to wrap to

Returns:

  • (String) —

    the wrapped string, preserving ANSI escape sequences

Raises:

  • (ArgumentError) —

    if neither percentage nor length is provided



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/kramdown/ansi/width.rb', line 62

def wrap(text, percentage: nil, length: nil)
  percentage.nil? ^ length.nil? or
    raise ArgumentError, "either pass percentage or length argument"
  percentage and length ||= width(percentage:)
  return text if length <= 0
  begin
    length = length.to_i
  rescue FloatDomainError
    return text
  end
  length = length.clamp(1..)
  length_without_ansi = 0
  result = +''
  text.scan(/(?<nws>\S+)|(?<ws>\s)/) do
    token_value  = $&
    token_length = display_width(token_value)
    if $~[:nws]
      if length_without_ansi + token_length > length
        length_without_ansi = token_length
        if result[-1] == ' '
          result[-1, 1] = "\n#{token_value}"
        else
          result << "\n#{token_value}"
        end
      else
        length_without_ansi += token_length
        result << token_value
      end
    else
      length_without_ansi += token_length
      result << token_value
    end
  end
  result
end