lib/view_component/template.rb



# frozen_string_literal: true

module ViewComponent
  class Template
    DataWithSource = Struct.new(:format, :identifier, :short_identifier, :type, keyword_init: true)
    DataNoSource = Struct.new(:source, :identifier, :type, keyword_init: true)

    attr_reader :variant, :this_format, :type

    def initialize(
      component:,
      type:,
      this_format: nil,
      variant: nil,
      lineno: nil,
      path: nil,
      extension: nil,
      source: nil,
      method_name: nil,
      defined_on_self: true
    )
      @component = component
      @type = type
      @this_format = this_format
      @variant = variant&.to_sym
      @lineno = lineno
      @path = path
      @extension = extension
      @source = source
      @method_name = method_name
      @defined_on_self = defined_on_self

      @source_originally_nil = @source.nil?

      @call_method_name =
        if @method_name
          @method_name
        else
          out = +"call"
          out << "_#{normalized_variant_name}" if @variant.present?
          out << "_#{@this_format}" if @this_format.present? && @this_format != ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT
          out
        end
    end

    def compile_to_component
      if !inline_call?
        @component.silence_redefinition_of_method(@call_method_name)

        # rubocop:disable Style/EvalWithLocation
        @component.class_eval <<-RUBY, @path, @lineno
        def #{@call_method_name}
          #{compiled_source}
        end
        RUBY
        # rubocop:enable Style/EvalWithLocation
      end

      @component.define_method(safe_method_name, @component.instance_method(@call_method_name))
    end

    def safe_method_name_call
      return safe_method_name unless inline_call?

      "maybe_escape_html(#{safe_method_name}) " \
      "{ Kernel.warn('WARNING: The #{@component} component rendered HTML-unsafe output. " \
      "The output will be automatically escaped, but you may want to investigate.') } "
    end

    def requires_compiled_superclass?
      inline_call? && !defined_on_self?
    end

    def inline_call?
      @type == :inline_call
    end

    def inline?
      @type == :inline
    end

    def default_format?
      @this_format == ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT
    end

    def format
      @this_format
    end

    def safe_method_name
      "_#{@call_method_name}_#{@component.name.underscore.gsub("/", "__")}"
    end

    def normalized_variant_name
      @variant.to_s.gsub("-", "__").gsub(".", "___")
    end

    def defined_on_self?
      @defined_on_self
    end

    private

    def source
      if @source_originally_nil
        # Load file each time we look up #source in case the file has been modified
        File.read(@path)
      else
        @source
      end
    end

    def compiled_source
      handler = ActionView::Template.handler_for_extension(@extension)
      this_source = source
      this_source.rstrip! if @component.strip_trailing_whitespace?

      short_identifier = defined?(Rails.root) ? @path.sub("#{Rails.root}/", "") : @path
      type = ActionView::Template::Types[@this_format]

      if handler.method(:call).parameters.length > 1
        handler.call(
          DataWithSource.new(format: @this_format, identifier: @path, short_identifier: short_identifier, type: type),
          this_source
        )
      # :nocov:
      # TODO: Remove in v4
      else
        handler.call(DataNoSource.new(source: this_source, identifier: @path, type: type))
      end
      # :nocov:
    end
  end
end