File: template.rb

package info (click to toggle)
ruby-view-component 4.4.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 472 kB
  • sloc: ruby: 2,278; makefile: 4
file content (203 lines) | stat: -rw-r--r-- 5,919 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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
65
66
67
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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# frozen_string_literal: true

module ViewComponent
  class Template
    DEFAULT_FORMAT = :html
    private_constant :DEFAULT_FORMAT

    DataWithSource = Struct.new(:format, :identifier, :short_identifier, :type, keyword_init: true)

    attr_reader :details, :path

    delegate :virtual_path, to: :@component
    delegate :format, :variant, to: :@details

    def initialize(component:, details:, lineno: nil, path: nil)
      @component = component
      @details = details
      @lineno = lineno
      @path = path
    end

    class File < Template
      def initialize(component:, details:, path:)
        @strip_annotation_line = false

        # Rails 8.1 added a newline to compiled ERB output (rails/rails#53731).
        # Use -1 to compensate for correct line numbers in stack traces.
        # However, negative line numbers cause segfaults when Ruby's coverage
        # is enabled (bugs.ruby-lang.org/issues/19363). In that case, strip the
        # annotation line from compiled source instead.
        lineno =
          if Rails::VERSION::MAJOR >= 8 && Rails::VERSION::MINOR > 0 && details.handler == :erb
            if coverage_running?
              # Can't use negative lineno with coverage (causes segfault on Linux).
              # Strip annotation line if enabled to preserve correct line numbers.
              @strip_annotation_line = ActionView::Base.annotate_rendered_view_with_filenames
              0
            else
              -1
            end
          else
            0
          end

        super(
          component: component,
          details: details,
          path: path,
          lineno: lineno
        )
      end

      def type
        :file
      end

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

      private

      def compiled_source
        result = super
        # Strip the annotation line to maintain correct line numbers when coverage
        # is running (avoids segfault from negative lineno)
        result = result.partition(";").last if @strip_annotation_line
        result
      end
    end

    class Inline < Template
      attr_reader :source

      def initialize(component:, inline_template:)
        details = ActionView::TemplateDetails.new(nil, inline_template.language.to_sym, nil, nil)

        # Rails 8.1 added a newline to compiled ERB output (rails/rails#53731).
        # Subtract 1 to compensate for correct line numbers in stack traces.
        # Inline templates start at line 2+ (defined inside a class), so this
        # won't result in negative line numbers that cause segfaults with coverage.
        lineno =
          if Rails::VERSION::MAJOR >= 8 && Rails::VERSION::MINOR > 0 && details.handler == :erb
            inline_template.lineno - 1
          else
            inline_template.lineno
          end

        super(
          component: component,
          details: details,
          path: inline_template.path,
          lineno: lineno,
        )

        @source = inline_template.source.dup
      end

      def type
        :inline
      end
    end

    class InlineCall < Template
      def initialize(component:, method_name:, defined_on_self:)
        variant = method_name.to_s.include?("call_") ? method_name.to_s.sub("call_", "").to_sym : nil
        details = ActionView::TemplateDetails.new(nil, nil, nil, variant)

        super(component: component, details: details)

        @call_method_name = method_name
        @defined_on_self = defined_on_self
      end

      def type
        :inline_call
      end

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

      def safe_method_name_call
        m = safe_method_name
        proc do
          __vc_maybe_escape_html(send(m)) do
            Kernel.warn("WARNING: The #{self.class} component rendered HTML-unsafe output. " \
                          "The output will be automatically escaped, but you may want to investigate.")
          end
        end
      end

      def defined_on_self?
        @defined_on_self
      end
    end

    def compile_to_component
      @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

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

    def coverage_running?
      defined?(Coverage) && Coverage.running?
    end

    def safe_method_name_call
      m = safe_method_name
      proc { send(m) }
    end

    def requires_compiled_superclass?
      inline_call? && !defined_on_self?
    end

    def inline_call?
      type == :inline_call
    end

    def default_format?
      format.nil? || format == DEFAULT_FORMAT
    end
    alias_method :html?, :default_format?

    def call_method_name
      @call_method_name ||=
        ["call", (normalized_variant_name if variant.present?), (format unless default_format?)]
          .compact.join("_").to_sym
    end

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

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

    private

    def compiled_source
      handler = details.handler_class
      this_source = source
      this_source.rstrip! if @component.strip_trailing_whitespace?

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

      handler.call(DataWithSource.new(format:, identifier: @path, short_identifier:, type:), this_source)
    end
  end
end