diff --git a/.gitignore b/.gitignore index 340f093f5..dfe6728fd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.gem *.rbc .ruby-version +.DS_Store /.config /coverage/assets /coverage/index.html diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 868adf283..9287c2522 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -24,6 +24,10 @@ nav_order: 5 *Joel Hawksley* +* BREAKING: Remove support for variant names containing `.` to be consistent with Rails. + + *Stephen Nelson* + * Ensure HTML output safety wrapper is used for all inline templates. *Joel Hawksley* diff --git a/docs/index.md b/docs/index.md index c810d6471..659efa588 100644 --- a/docs/index.md +++ b/docs/index.md @@ -193,6 +193,7 @@ ViewComponent is built by over a hundred members of the community, including: sammyhenningsson sampart seanpdoyle +sfnelson simonrand skryukov smashwilson diff --git a/lib/view_component/compiler.rb b/lib/view_component/compiler.rb index c4ab88df4..8e80ae026 100644 --- a/lib/view_component/compiler.rb +++ b/lib/view_component/compiler.rb @@ -169,27 +169,12 @@ def template_errors def gather_templates @templates ||= begin + path_parser = ActionView::Resolver::PathParser.new templates = @component.sidecar_files( ActionView::Template.template_handler_extensions ).map do |path| - # Extract format and variant from template filename - this_format, variant = - File - .basename(path) # "variants_component.html+mini.watch.erb" - .split(".")[1..-2] # ["html+mini", "watch"] - .join(".") # "html+mini.watch" - .split("+") # ["html", "mini.watch"] - .map(&:to_sym) # [:html, :"mini.watch"] - - out = Template.new( - component: @component, - type: :file, - path: path, - lineno: 0, - extension: path.split(".").last, - this_format: this_format.to_s.split(".").last&.to_sym, # strip locale from this_format, see #2113 - variant: variant - ) + details = path_parser.parse(path).details + out = Template::File.new(component: @component, path: path, details: details) out end @@ -201,24 +186,17 @@ def gather_templates ).flat_map { |ancestor| ancestor.instance_methods(false).grep(/^call(_|$)/) } .uniq .each do |method_name| - templates << Template.new( + templates << Template::InlineCall.new( component: @component, - type: :inline_call, - this_format: ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT, - variant: method_name.to_s.include?("call_") ? method_name.to_s.sub("call_", "").to_sym : nil, method_name: method_name, defined_on_self: component_instance_methods_on_self.include?(method_name) ) end if @component.inline_template.present? - templates << Template.new( + templates << Template::Inline.new( component: @component, - type: :inline, - path: @component.inline_template.path, - lineno: @component.inline_template.lineno, - source: @component.inline_template.source.dup, - extension: @component.inline_template.language + inline_template: @component.inline_template ) end diff --git a/lib/view_component/template.rb b/lib/view_component/template.rb index f01fd9368..09d7bdc5f 100644 --- a/lib/view_component/template.rb +++ b/lib/view_component/template.rb @@ -5,56 +5,112 @@ 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 + attr_reader :details + + delegate :format, :variant, to: :details def initialize( component:, - type:, - this_format: nil, - variant: nil, + details:, lineno: nil, path: nil, - extension: nil, - source: nil, - method_name: nil, - defined_on_self: true + method_name: nil ) @component = component - @type = type - @this_format = this_format - @variant = variant&.to_sym + @details = details @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 << "_#{normalized_variant_name}" if variant.present? + out << "_#{format}" if format.present? && format != ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT out end end + class File < Template + def initialize(component:, details:, path:) + super( + component: component, + details: details, + path: path, + lineno: 0 + ) + 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 + end + + class Inline < Template + attr_reader :source + + def initialize(component:, inline_template:) + details = ActionView::TemplateDetails.new(nil, inline_template.language.to_sym, nil, nil) + + super( + component: component, + details: details, + path: inline_template.path, + lineno: inline_template.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, + 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 defined_on_self? + @defined_on_self + end + end + def compile_to_component - if !inline_call? - @component.silence_redefinition_of_method(@call_method_name) + @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 + # 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 @@ -72,19 +128,15 @@ def requires_compiled_superclass? end def inline_call? - @type == :inline_call + type == :inline_call end def inline? - @type == :inline + type == :inline end def default_format? - @this_format == ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT - end - - def format - @this_format + format.nil? || format == ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT end def safe_method_name @@ -92,35 +144,23 @@ def safe_method_name end def normalized_variant_name - @variant.to_s.gsub("-", "__").gsub(".", "___") - end - - def defined_on_self? - @defined_on_self + variant.to_s.gsub("-", "__") 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) + 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 - type = ActionView::Template::Types[@this_format] + format = self.format || ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT + type = ActionView::Template::Types[format] if handler.method(:call).parameters.length > 1 handler.call( - DataWithSource.new(format: @this_format, identifier: @path, short_identifier: short_identifier, type: type), + DataWithSource.new(format: format, identifier: @path, short_identifier: short_identifier, type: type), this_source ) # :nocov: diff --git a/test/sandbox/app/components/variants_component.html+mini.watch.erb b/test/sandbox/app/components/variants_component.html+mini.watch.erb deleted file mode 100644 index b44d8505e..000000000 --- a/test/sandbox/app/components/variants_component.html+mini.watch.erb +++ /dev/null @@ -1 +0,0 @@ -Mini Watch with dot diff --git a/test/sandbox/test/rendering_test.rb b/test/sandbox/test/rendering_test.rb index d52840f85..10dd2e758 100644 --- a/test/sandbox/test/rendering_test.rb +++ b/test/sandbox/test/rendering_test.rb @@ -198,14 +198,6 @@ def test_renders_component_with_variant_containing_a_dash end end - def test_renders_component_with_variant_containing_a_dot - with_variant :"mini.watch" do - render_inline(VariantsComponent.new) - - assert_text("Mini Watch with dot") - end - end - def test_renders_default_template_when_variant_template_is_not_present with_variant :variant_without_template do render_inline(VariantsComponent.new)