lib/rouge/theme.rb



# -*- coding: utf-8 -*- #
# frozen_string_literal: true

module Rouge
  class Theme
    include Token::Tokens

    class Style < Hash
      def initialize(theme, hsh={})
        super()
        @theme = theme
        merge!(hsh)
      end

      [:fg, :bg].each do |mode|
        define_method mode do
          return self[mode] unless @theme
          @theme.palette(self[mode]) if self[mode]
        end
      end

      def render(selector, &b)
        return enum_for(:render, selector).to_a.join("\n") unless b

        return if empty?

        yield "#{selector} {"
        rendered_rules.each do |rule|
          yield "  #{rule};"
        end
        yield "}"
      end

      def rendered_rules(&b)
        return enum_for(:rendered_rules) unless b
        yield "color: #{fg}" if fg
        yield "background-color: #{bg}" if bg
        yield "font-weight: bold" if self[:bold]
        yield "font-style: italic" if self[:italic]
        yield "text-decoration: underline" if self[:underline]

        (self[:rules] || []).each(&b)
      end
    end

    def styles
      @styles ||= self.class.styles.dup
    end

    @palette = {}
    def self.palette(arg={})
      @palette ||= InheritableHash.new(superclass.palette)

      if arg.is_a? Hash
        @palette.merge! arg
        @palette
      else
        case arg
        when /#[0-9a-f]+/i
          arg
        else
          @palette[arg] or raise "not in palette: #{arg.inspect}"
        end
      end
    end

    def palette(*a) self.class.palette(*a) end

    @styles = {}
    def self.styles
      @styles ||= InheritableHash.new(superclass.styles)
    end

    def self.render(opts={}, &b)
      new(opts).render(&b)
    end

    def get_own_style(token)
      self.class.get_own_style(token)
    end

    def get_style(token)
      self.class.get_style(token)
    end

    def name
      self.class.name
    end

    class << self
      def style(*tokens)
        style = tokens.last.is_a?(Hash) ? tokens.pop : {}

        tokens.each do |tok|
          styles[tok] = style
        end
      end

      def get_own_style(token)
        token.token_chain.reverse_each do |anc|
          return Style.new(self, styles[anc]) if styles[anc]
        end

        nil
      end

      def get_style(token)
        get_own_style(token) || base_style
      end

      def base_style
        get_own_style(Token::Tokens::Text)
      end

      def name(n=nil)
        return @name if n.nil?

        @name = n.to_s
        register(@name)
      end

      def register(name)
        Theme.registry[name.to_s] = self
      end

      def find(n)
        registry[n.to_s]
      end

      def registry
        @registry ||= {}
      end
    end
  end

  module HasModes
    def mode(arg=:absent)
      return @mode if arg == :absent

      @modes ||= {}
      @modes[arg] ||= get_mode(arg)
    end

    def get_mode(mode)
      return self if self.mode == mode

      new_name = "#{self.name}.#{mode}"
      Class.new(self) { name(new_name); set_mode!(mode) }
    end

    def set_mode!(mode)
      @mode = mode
      send("make_#{mode}!")
    end

    def mode!(arg)
      alt_name = "#{self.name}.#{arg}"
      register(alt_name)
      set_mode!(arg)
    end
  end

  class CSSTheme < Theme
    def initialize(opts={})
      @scope = opts[:scope] || '.highlight'
    end

    def render(&b)
      return enum_for(:render).to_a.join("\n") unless b

      # shared styles for tableized line numbers
      yield "#{@scope} table td { padding: 5px; }"
      yield "#{@scope} table pre { margin: 0; }"

      styles.each do |tok, style|
        Style.new(self, style).render(css_selector(tok), &b)
      end
    end

    def render_base(selector, &b)
      self.class.base_style.render(selector, &b)
    end

    def style_for(tok)
      self.class.get_style(tok)
    end

  private
    def css_selector(token)
      inflate_token(token).map do |tok|
        raise "unknown token: #{tok.inspect}" if tok.shortname.nil?

        single_css_selector(tok)
      end.join(', ')
    end

    def single_css_selector(token)
      return @scope if token == Text

      "#{@scope} .#{token.shortname}"
    end

    # yield all of the tokens that should be styled the same
    # as the given token.  Essentially this recursively all of
    # the subtokens, except those which are more specifically
    # styled.
    def inflate_token(tok, &b)
      return enum_for(:inflate_token, tok) unless block_given?

      yield tok
      tok.sub_tokens.each do |(_, st)|
        next if styles[st]

        inflate_token(st, &b)
      end
    end
  end
end