#!/usr/bin/env ruby class Bcat # Converts ANSI color sequences to HTML. # # The ANSI module is based on code from the following libraries: # # ansi2html.sh: # http://github.com/pixelb/scripts/blob/master/scripts/ansi2html.sh # # HTML::FromANSI: # http://cpansearch.perl.org/src/NUFFIN/HTML-FromANSI-2.03/lib/HTML/FromANSI.pm class ANSI ESCAPE = "\x1b" # Linux console palette STYLES = { 'ef0' => 'color:#000', 'ef1' => 'color:#A00', 'ef2' => 'color:#0A0', 'ef3' => 'color:#A50', 'ef4' => 'color:#00A', 'ef5' => 'color:#A0A', 'ef6' => 'color:#0AA', 'ef7' => 'color:#AAA', 'ef8' => 'color:#555', 'ef9' => 'color:#F55', 'ef10' => 'color:#5F5', 'ef11' => 'color:#FF5', 'ef12' => 'color:#55F', 'ef13' => 'color:#F5F', 'ef14' => 'color:#5FF', 'ef15' => 'color:#FFF', 'eb0' => 'background-color:#000', 'eb1' => 'background-color:#A00', 'eb2' => 'background-color:#0A0', 'eb3' => 'background-color:#A50', 'eb4' => 'background-color:#00A', 'eb5' => 'background-color:#A0A', 'eb6' => 'background-color:#0AA', 'eb7' => 'background-color:#AAA', 'eb8' => 'background-color:#555', 'eb9' => 'background-color:#F55', 'eb10' => 'background-color:#5F5', 'eb11' => 'background-color:#FF5', 'eb12' => 'background-color:#55F', 'eb13' => 'background-color:#F5F', 'eb14' => 'background-color:#5FF', 'eb15' => 'background-color:#FFF' } ## # The default xterm 256 colour palette (0..5).each do |red| (0..5).each do |green| (0..5).each do |blue| c = 16 + (red * 36) + (green * 6) + blue r = red > 0 ? red * 40 + 55 : 0 g = green > 0 ? green * 40 + 55 : 0 b = blue > 0 ? blue * 40 + 55 : 0 STYLES["ef#{c}"] = "color:#%2.2x%2.2x%2.2x" % [r, g, b] STYLES["eb#{c}"] = "background-color:#%2.2x%2.2x%2.2x" % [r, g, b] end end end (0..23).each do |gray| c = gray+232 l = gray*10 + 8 STYLES["ef#{c}"] = "color:#%2.2x%2.2x%2.2x" % [l, l, l] STYLES["eb#{c}"] = "background-color:#%2.2x%2.2x%2.2x" % [l, l, l] end def initialize(input) @input = if input.respond_to?(:to_str) [input] elsif !input.respond_to?(:each) raise ArgumentError, "input must respond to each" else input end @stack = [] end def to_html buf = [] each { |chunk| buf << chunk } buf.join end def each buf = '' @input.each do |chunk| buf << chunk tokenize(buf) do |tok, data| case tok when :text yield data when :display case code = data when 0 ; yield reset_styles if @stack.any? when 1 ; yield push_tag("b") # bright when 2 ; #dim when 3, 4 ; yield push_tag("u") when 5, 6 ; yield push_tag("blink") when 7 ; #reverse when 8 ; yield push_style("display:none") when 9 ; yield push_tag("strike") when 30..37 ; yield push_style("ef#{code - 30}") when 40..47 ; yield push_style("eb#{code - 40}") when 90..97 ; yield push_style("ef#{8 + code - 90}") when 100..107 ; yield push_style("eb#{8 + code - 100}") end when :xterm256 code = data yield push_style("ef#{code}") end end end yield buf if !buf.empty? yield reset_styles if @stack.any? self end def push_tag(tag, style=nil) style = STYLES[style] if style && !style.include?(':') @stack.push tag [ "<#{tag}", (" style='#{style}'" if style), ">" ].join end def push_style(style) push_tag "span", style end def reset_styles stack, @stack = @stack, [] stack.reverse.map { |tag| "" }.join end def tokenize(text) tokens = [ # characters to remove completely [/\A\x08+/, lambda { |m| '' }], [/\A\x1b\[38;5;(\d+)m/, lambda { |m| yield :xterm256, $1.to_i; '' } ], # ansi escape sequences that mess with the display [/\A\x1b\[((?:\d{1,3};?)+|)m/, lambda { |m| m = '0' if m.strip.empty? m.chomp(';').split(';'). each { |code| yield :display, code.to_i }; '' }], # malformed sequences [/\A\x1b\[?[\d;]{0,3}/, lambda { |m| '' }], # real text [/\A([^\x1b\x08]+)/m, lambda { |m| yield :text, m; '' }] ] while (size = text.size) > 0 tokens.each do |pattern, sub| break if text.sub!(pattern) { sub.call($1) } end break if text.size == size end end end end data = [] STDIN.each_line do |line| data.push(line) end puts Bcat::ANSI.new(data).to_html