Module: Llv::Ansi

Defined in:
lib/llv/ansi.rb

Overview

Translates a small subset of ANSI SGR escape codes (the ones Rails actually emits in development.log) into either nothing (strip) or HTML spans (to_html).

Constant Summary collapse

SGR =
/\e\[([0-9;]*)m/
FG =
{
  30 => "black", 31 => "red", 32 => "green", 33 => "yellow",
  34 => "blue", 35 => "magenta", 36 => "cyan", 37 => "white",
  90 => "bright-black", 91 => "bright-red", 92 => "bright-green",
  93 => "bright-yellow", 94 => "bright-blue", 95 => "bright-magenta",
  96 => "bright-cyan", 97 => "bright-white"
}.freeze
BG =
{
  40 => "black", 41 => "red", 42 => "green", 43 => "yellow",
  44 => "blue", 45 => "magenta", 46 => "cyan", 47 => "white"
}.freeze

Class Method Summary collapse

Class Method Details

.apply(state, codes) ⇒ Object



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/llv/ansi.rb', line 110

def apply(state, codes)
  i = 0
  while i < codes.length
    code = codes[i]
    case code
    when 0
      state[:bold] = false
      state[:italic] = false
      state[:underline] = false
      state[:fg] = nil
      state[:bg] = nil
    when 1 then state[:bold] = true
    when 3 then state[:italic] = true
    when 4 then state[:underline] = true
    when 22 then state[:bold] = false
    when 23 then state[:italic] = false
    when 24 then state[:underline] = false
    when 30..37, 90..97
      state[:fg] = FG[code]
    when 39
      state[:fg] = nil
    when 40..47
      state[:bg] = BG[code]
    when 49
      state[:bg] = nil
    end
    i += 1
  end
end

.empty_state?(state) ⇒ Boolean

Returns:

  • (Boolean)


140
141
142
# File 'lib/llv/ansi.rb', line 140

def empty_state?(state)
  !state[:bold] && !state[:italic] && !state[:underline] && state[:fg].nil? && state[:bg].nil?
end

.open_span(state) ⇒ Object



144
145
146
147
148
149
150
151
152
# File 'lib/llv/ansi.rb', line 144

def open_span(state)
  classes = []
  classes << "ansi-bold" if state[:bold]
  classes << "ansi-italic" if state[:italic]
  classes << "ansi-underline" if state[:underline]
  classes << "ansi-fg-#{state[:fg]}" if state[:fg]
  classes << "ansi-bg-#{state[:bg]}" if state[:bg]
  %(<span class="#{classes.join(" ")}">)
end

.scan(str) {|[:text, str[pos..]]| ... } ⇒ Object

Yields [:text, “…”] and [:sgr, [int, …]] segments in order.

Yields:

  • ([:text, str[pos..]])


95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/llv/ansi.rb', line 95

def scan(str)
  pos = 0
  str.scan(SGR) do
    match = Regexp.last_match
    if match.begin(0) > pos
      yield [:text, str[pos...match.begin(0)]]
    end
    codes = match[1].to_s.split(";").map { |c| c.empty? ? 0 : c.to_i }
    codes = [0] if codes.empty?
    yield [:sgr, codes]
    pos = match.end(0)
  end
  yield [:text, str[pos..]] if pos < str.length
end

.strip(str) ⇒ Object



26
27
28
# File 'lib/llv/ansi.rb', line 26

def strip(str)
  str.gsub(SGR, "")
end

.to_html(str) ⇒ Object

Turn raw text containing SGR codes into HTML. Adjacent text under the same state is wrapped in one <span class=“…”> per state-change boundary.



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
# File 'lib/llv/ansi.rb', line 68

def to_html(str)
  out = +""
  state = { bold: false, italic: false, underline: false, fg: nil, bg: nil }
  open = false

  scan(str) do |segment|
    case segment
    in [:text, text]
      next if text.empty?

      out << open_span(state) unless open || empty_state?(state)
      open = true unless empty_state?(state)
      out << CGI.escape_html(text)
    in [:sgr, codes]
      if open
        out << "</span>"
        open = false
      end
      apply(state, codes)
    end
  end

  out << "</span>" if open
  out
end

.truncate(str, max_visible) ⇒ Object

Truncate ‘str` to at most `max_visible` printable characters, ignoring SGR escape codes for measurement and preserving every SGR encountered up to the cut so colours don’t bleed past the truncation. Adds an ellipsis and a final reset when the string is shortened.



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/llv/ansi.rb', line 34

def truncate(str, max_visible)
  return str if max_visible <= 0
  return str if strip(str).length <= max_visible

  out = +""
  visible = 0
  cut = false

  scan(str) do |segment|
    case segment
    in [:sgr, codes]
      out << "\e[#{codes.join(";")}m"
    in [:text, text]
      break if cut

      remaining = max_visible - visible
      if text.length <= remaining
        out << text
        visible += text.length
      else
        out << text[0, [remaining - 1, 0].max]
        out << ""
        visible = max_visible
        cut = true
      end
    end
  end

  out << "\e[0m" if cut
  out
end