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
- .apply(state, codes) ⇒ Object
- .empty_state?(state) ⇒ Boolean
- .open_span(state) ⇒ Object
-
.scan(str) {|[:text, str[pos..]]| ... } ⇒ Object
Yields [:text, “…”] and [:sgr, [int, …]] segments in order.
- .strip(str) ⇒ Object
-
.to_html(str) ⇒ Object
Turn raw text containing SGR codes into HTML.
-
.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.
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
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.
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 |