| # -*- coding: utf-8 -*- # |
| |
| module Rouge |
| module Formatters |
| # A formatter for 256-color terminals |
| class Terminal256 < Formatter |
| tag 'terminal256' |
| |
| # @private |
| attr_reader :theme |
| |
| |
| # @option opts :theme |
| # (default is thankful_eyes) the theme to render with. |
| def initialize(opts={}) |
| @theme = opts[:theme] || 'thankful_eyes' |
| @theme = Theme.find(@theme) if @theme.is_a? String |
| end |
| |
| def stream(tokens, &b) |
| tokens.each do |tok, val| |
| escape = escape_sequence(tok) |
| yield escape.style_string |
| yield val.gsub("\n", "\n#{escape.style_string}") |
| yield escape.reset_string |
| end |
| end |
| |
| class EscapeSequence |
| attr_reader :style |
| def initialize(style) |
| @style = style |
| end |
| |
| def self.xterm_colors |
| @xterm_colors ||= [].tap do |out| |
| # colors 0..15: 16 basic colors |
| out << [0x00, 0x00, 0x00] # 0 |
| out << [0xcd, 0x00, 0x00] # 1 |
| out << [0x00, 0xcd, 0x00] # 2 |
| out << [0xcd, 0xcd, 0x00] # 3 |
| out << [0x00, 0x00, 0xee] # 4 |
| out << [0xcd, 0x00, 0xcd] # 5 |
| out << [0x00, 0xcd, 0xcd] # 6 |
| out << [0xe5, 0xe5, 0xe5] # 7 |
| out << [0x7f, 0x7f, 0x7f] # 8 |
| out << [0xff, 0x00, 0x00] # 9 |
| out << [0x00, 0xff, 0x00] # 10 |
| out << [0xff, 0xff, 0x00] # 11 |
| out << [0x5c, 0x5c, 0xff] # 12 |
| out << [0xff, 0x00, 0xff] # 13 |
| out << [0x00, 0xff, 0xff] # 14 |
| out << [0xff, 0xff, 0xff] # 15 |
| |
| # colors 16..232: the 6x6x6 color cube |
| valuerange = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff] |
| |
| 217.times do |i| |
| r = valuerange[(i / 36) % 6] |
| g = valuerange[(i / 6) % 6] |
| b = valuerange[i % 6] |
| out << [r, g, b] |
| end |
| |
| # colors 233..253: grayscale |
| 1.upto 22 do |i| |
| v = 8 + i * 10 |
| out << [v, v, v] |
| end |
| end |
| end |
| |
| def fg |
| return @fg if instance_variable_defined? :@fg |
| @fg = style.fg && self.class.color_index(style.fg) |
| end |
| |
| def bg |
| return @bg if instance_variable_defined? :@bg |
| @bg = style.bg && self.class.color_index(style.bg) |
| end |
| |
| def style_string |
| @style_string ||= begin |
| attrs = [] |
| |
| attrs << ['38', '5', fg.to_s] if fg |
| attrs << ['48', '5', bg.to_s] if bg |
| attrs << '01' if style[:bold] |
| attrs << '04' if style[:italic] # underline, but hey, whatevs |
| escape(attrs) |
| end |
| end |
| |
| def reset_string |
| @reset_string ||= begin |
| attrs = [] |
| attrs << '39' if fg # fg reset |
| attrs << '49' if bg # bg reset |
| attrs << '00' if style[:bold] || style[:italic] |
| |
| escape(attrs) |
| end |
| end |
| |
| private |
| def escape(attrs) |
| return '' if attrs.empty? |
| "\e[#{attrs.join(';')}m" |
| end |
| |
| def self.color_index(color) |
| @color_index_cache ||= {} |
| @color_index_cache[color] ||= closest_color(*get_rgb(color)) |
| end |
| |
| def self.get_rgb(color) |
| color = $1 if color =~ /#([0-9a-f]+)/i |
| hexes = case color.size |
| when 3 |
| color.chars.map { |c| "#{c}#{c}" } |
| when 6 |
| color.scan(/../) |
| else |
| raise "invalid color: #{color}" |
| end |
| |
| hexes.map { |h| h.to_i(16) } |
| end |
| |
| # max distance between two colors, #000000 to #ffffff |
| MAX_DISTANCE = 257 * 257 * 3 |
| |
| def self.closest_color(r, g, b) |
| @@colors_cache ||= {} |
| key = (r << 16) + (g << 8) + b |
| @@colors_cache.fetch(key) do |
| distance = MAX_DISTANCE |
| |
| match = 0 |
| |
| xterm_colors.each_with_index do |(cr, cg, cb), i| |
| d = (r - cr)**2 + (g - cg)**2 + (b - cb)**2 |
| next if d >= distance |
| |
| match = i |
| distance = d |
| end |
| |
| match |
| end |
| end |
| end |
| |
| # private |
| def escape_sequence(token) |
| @escape_sequences ||= {} |
| @escape_sequences[token.qualname] ||= |
| EscapeSequence.new(get_style(token)) |
| end |
| |
| def get_style(token) |
| return text_style if token.ancestors.include? Token::Tokens::Text |
| |
| theme.get_own_style(token) || text_style |
| end |
| |
| def text_style |
| style = theme.get_style(Token['Text']) |
| # don't highlight text backgrounds |
| style.delete :bg |
| style |
| end |
| end |
| end |
| end |