reactionview 0.4.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "herb"
4
+ require "herb/engine"
5
+ require "herb/engine/slots/dynamics_compiler"
6
+ require "herb/engine/slots/schema_compiler"
7
+
8
+ require "herb/engine/visitors/debug_visitor"
9
+ require "herb/engine/slots/visitor"
10
+ require "herb/engine/slots/dependencies"
11
+ require "herb/engine/visitors/content_for_visitor"
12
+ require "herb/engine/validators"
13
+
3
14
  module ReActionView
4
15
  class Template
5
16
  module Handlers
@@ -10,33 +21,201 @@ module ReActionView
10
21
 
11
22
  class_attribute :erb_implementation, default: Handlers::Herb::Herb
12
23
 
24
+ VALUES_ESCAPING = {
25
+ escape: true,
26
+ escapefunc: "::ERB::Util.h",
27
+ attrfunc: nil,
28
+ jsfunc: nil,
29
+ cssfunc: nil,
30
+ }.freeze
31
+
13
32
  def self.call(template, source, validation_mode: nil)
14
33
  new.call(template, source, validation_mode: validation_mode)
15
34
  end
16
35
 
36
+ def self.compile_for_dependencies(source, path)
37
+ new.compile_for_dependencies(source, path)
38
+ end
39
+
40
+ def self.compile_for_schema(source, path)
41
+ new.compile_for_schema(source, path)
42
+ end
43
+
44
+ def self.compile_for_block(source, path, index)
45
+ new.compile_for_block(source, path, index)
46
+ end
47
+
17
48
  def call(template, source, validation_mode: nil)
18
- visitors = []
49
+ mode = validation_mode || ReActionView.config.validation_mode
19
50
 
20
- if ::ReActionView.config.debug_mode_enabled? && local_template?(template)
21
- visitors << ::Herb::Engine::DebugVisitor.new(
22
- file_path: translate_path_for_editor(template.identifier),
23
- project_path: ::ReActionView.config.project_path
24
- )
25
- end
51
+ visitors = base_visitors(template, validation_mode: mode)
26
52
 
27
53
  config = {
28
- filename: template.identifier,
29
- project_path: Rails.root.to_s,
30
- validation_mode: validation_mode || ReActionView.config.validation_mode,
31
- content_for_head: reactionview_dev_tools_markup(template),
32
- visitors: visitors + ReActionView.config.transform_visitors,
54
+ filename: translate_path_for_editor(template.identifier),
55
+ project_path: ::ReActionView.config.project_path,
56
+ visitors: visitors,
57
+ **engine_options,
33
58
  }
34
59
 
60
+ return values_source(template, source, config) if values_format?(template)
61
+
62
+ slots = slot_visitors(template, source)
63
+
64
+ config[:visitors] = compile_stack(template, validation_mode: mode, slots: slots)
65
+ config.merge!(filename_annotation_options(template))
66
+
35
67
  erb_implementation.new(source, config).src
36
68
  end
37
69
 
70
+ def compile_for_dependencies(source, path)
71
+ template = ::Struct.new(:identifier, :format).new(path, :html)
72
+ visitor = slot_visitors(template, source, mark: false).first
73
+
74
+ return nil unless visitor
75
+
76
+ erb_implementation.new(
77
+ source,
78
+ filename: translate_path_for_editor(path),
79
+ project_path: ::ReActionView.config.project_path,
80
+ visitors: [*base_visitors(template), visitor],
81
+ **engine_options
82
+ ).src
83
+
84
+ visitor
85
+ end
86
+
87
+ def compile_for_block(source, path, index)
88
+ template = ::Struct.new(:identifier, :format).new(path, :html)
89
+ visitor = slot_visitors(template, source, mark: false).first
90
+
91
+ return nil unless visitor
92
+
93
+ ::Herb::Engine::Slots::DynamicsCompiler.new(
94
+ source,
95
+ filename: translate_path_for_editor(path),
96
+ project_path: ::ReActionView.config.project_path,
97
+ visitors: base_visitors(template),
98
+ slot_visitor: visitor,
99
+ block: index,
100
+ **engine_options,
101
+ **VALUES_ESCAPING
102
+ ).src
103
+ end
104
+
105
+ def compile_for_schema(source, path)
106
+ template = ::Struct.new(:identifier, :format).new(path, :html)
107
+
108
+ ::Herb::Engine::Slots::SchemaCompiler.call(
109
+ source,
110
+ filename: translate_path_for_editor(path),
111
+ mode: ::ReActionView.config.slot_mode_for(source),
112
+ visitors: -> { base_visitors(template, validation_mode: schema_validation_mode) },
113
+ engine: erb_implementation,
114
+ options: { project_path: ::ReActionView.config.project_path, **engine_options }
115
+ )
116
+ end
117
+
38
118
  private
39
119
 
120
+ def filename_annotation_options(template)
121
+ return {} unless ActionView::Base.annotate_rendered_view_with_filenames && template.format == :html
122
+
123
+ beginning = "<!-- BEGIN #{template.short_identifier}\n-->"
124
+ ending = "<!-- END #{template.short_identifier} -->"
125
+
126
+ {
127
+ preamble: "@output_buffer.safe_append=#{beginning.dump};",
128
+ postamble: "@output_buffer.safe_append=#{ending.dump};@output_buffer",
129
+ }
130
+ end
131
+
132
+ def base_visitors(template, validation_mode: ReActionView.config.validation_mode)
133
+ compile_stack(template, validation_mode: validation_mode).reject { |visitor| rewrites_erb_source?(visitor) }
134
+ end
135
+
136
+ def compile_stack(template, validation_mode:, slots: [])
137
+ visitors = [
138
+ *validation_visitors(validation_mode),
139
+ *debug_visitors(template),
140
+ *head_visitors(template),
141
+ *slots,
142
+ *::ReActionView.config.engine.visitors
143
+ ]
144
+
145
+ ::Herb::Visitor::Stack.arrange(visitors)
146
+ end
147
+
148
+ def engine_options
149
+ configured = ::ReActionView.config.engine.parser_options
150
+ options = resolver_option
151
+
152
+ return options if configured.empty?
153
+
154
+ defaults = ::Herb.configuration.engine_option("parser_options", {})
155
+
156
+ options.merge(parser_options: defaults.transform_keys(&:to_sym).merge(configured))
157
+ end
158
+
159
+ def resolver_option
160
+ return {} unless defined?(::Herb::Analysis::PartialResolver)
161
+
162
+ { resolver: ::ReActionView::Template::PartialResolver.current }
163
+ end
164
+
165
+ def rewrites_erb_source?(visitor)
166
+ klass = visitor.class
167
+
168
+ klass.respond_to?(:rewrites_erb_source?) && klass.rewrites_erb_source?
169
+ end
170
+
171
+ def schema_validation_mode
172
+ ReActionView.config.validation_mode == :none ? :none : :overlay
173
+ end
174
+
175
+ def values_source(template, source, config)
176
+ visitor = slot_visitors(template, source, mark: false).first
177
+
178
+ return "{}" unless visitor
179
+
180
+ ::Herb::Engine::Slots::DynamicsCompiler.new(source, config.merge(slot_visitor: visitor, **VALUES_ESCAPING)).src
181
+ end
182
+
183
+ def values_format?(template)
184
+ template.respond_to?(:format) && template.format == :slots
185
+ end
186
+
187
+ def validation_visitors(mode)
188
+ case mode
189
+ when :raise then ::Herb::Engine::Validators.all(fatal: true).to_a
190
+ when :overlay then ::Herb::Engine::Validators.all(fatal: false).to_a
191
+ else []
192
+ end
193
+ end
194
+
195
+ def debug_visitors(template)
196
+ return [] unless ::ReActionView.config.debug_mode_enabled? && local_template?(template)
197
+
198
+ [::Herb::Engine::DebugVisitor.new]
199
+ end
200
+
201
+ def head_visitors(template)
202
+ markup = reactionview_dev_tools_markup(template)
203
+
204
+ return [] if markup.nil? || markup.empty?
205
+
206
+ [::Herb::Engine::ContentForVisitor.new(markup, tag_name: "head")]
207
+ end
208
+
209
+ def slot_visitors(template, source, mark: true)
210
+ return [] unless values_format?(template) || (template.respond_to?(:format) && template.format == :html)
211
+
212
+ mode = ::ReActionView.config.slot_mode_for(source)
213
+
214
+ return [] unless mode
215
+
216
+ [::Herb::Engine::Slots::Visitor.new(mode: mode, mark: mark)]
217
+ end
218
+
40
219
  def layout_template?(template)
41
220
  return false unless template.respond_to?(:identifier) && template.identifier
42
221
 
@@ -76,9 +255,9 @@ module ReActionView
76
255
  end
77
256
 
78
257
  def reactionview_dev_tools_markup(template)
79
- return nil unless ::ReActionView.config.debug_mode_enabled?
80
258
  return nil unless layout_template?(template)
81
259
  return nil unless local_template?(template)
260
+ return slots_meta_markup unless ::ReActionView.config.debug_mode_enabled?
82
261
 
83
262
  <<~HTML
84
263
  <meta name="herb-debug-mode" content="true">
@@ -91,6 +270,15 @@ module ReActionView
91
270
  HTML
92
271
  end
93
272
 
273
+ def slots_meta_markup
274
+ return nil unless ::ReActionView.config.slots && ::ReActionView.config.development?
275
+
276
+ <<~HTML
277
+ <meta name="herb-project-path" content="#{Rails.root}">
278
+ #{dev_server_port_meta_tag}
279
+ HTML
280
+ end
281
+
94
282
  def dismiss_hint_template
95
283
  return unless ::ReActionView.config.validation_mode == :overlay
96
284
 
@@ -3,19 +3,50 @@
3
3
  module ReActionView
4
4
  class Template
5
5
  module LocalTemplate
6
+ FRAMEWORK_TEMPLATE_SEGMENT = "/action_dispatch/middleware/templates/" #: String
7
+
6
8
  private
7
9
 
10
+ def framework_template?(template)
11
+ return false unless template.respond_to?(:identifier) && template.identifier
12
+
13
+ template.identifier.to_s.include?(FRAMEWORK_TEMPLATE_SEGMENT)
14
+ end
15
+
8
16
  def local_template?(template)
9
- return true unless template.respond_to?(:identifier) && template.identifier
10
- return false if vendored_template?(template)
17
+ identifier = template_identifier(template)
18
+
19
+ return true unless identifier
20
+
21
+ app_root = application_root
22
+ dependencies = Dependencies.current
23
+
24
+ return false if under?(identifier, dependencies.engine_roots)
25
+ return false if under?(identifier, dependencies.gem_paths_outside(app_root))
26
+ return true if under?(identifier, dependencies.app_view_paths)
27
+ return true unless app_root
11
28
 
12
- template.identifier.start_with?(Rails.root.to_s)
29
+ under?(identifier, [app_root])
13
30
  end
14
31
 
15
- def vendored_template?(template)
16
- return false unless defined?(Bundler)
32
+ def template_identifier(template)
33
+ return nil unless template.respond_to?(:identifier)
34
+
35
+ identifier = template.identifier
36
+
37
+ return nil if identifier.nil? || identifier.to_s.empty?
38
+
39
+ File.expand_path(identifier.to_s)
40
+ end
41
+
42
+ def application_root
43
+ return nil unless defined?(::Rails) && ::Rails.respond_to?(:root) && ::Rails.root
44
+
45
+ "#{File.expand_path(::Rails.root.to_s).chomp("/")}/"
46
+ end
17
47
 
18
- template.identifier.start_with?(Bundler.bundle_path.to_s)
48
+ def under?(identifier, prefixes)
49
+ prefixes.any? { |prefix| identifier.start_with?(prefix) }
19
50
  end
20
51
  end
21
52
  end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReActionView
4
+ class Template
5
+ # Answers a render's partial name with the file Rails would render, across every view path the
6
+ # application has registered.
7
+ #
8
+ # Herb resolves partials while it compiles, to check that a render names a template that
9
+ # exists, to inline one, and to read the states a caller binds through `state:`. On its own it
10
+ # looks under a single view root, which is a fair description of an application and wrong for
11
+ # everything around it. Rails has the application's own paths, its engines' and its gems' all
12
+ # registered, in an order the application controls, and `shared/album_card` may live in any of
13
+ # them.
14
+ #
15
+ # This asks Herb's own resolver once per registered path, in the order Rails registered them,
16
+ # so the file is the one Rails would have found and the identifier is the one that file's own
17
+ # compile reports for itself. That last part is what a state binding keys on, so the two sides
18
+ # of a binding agree by construction.
19
+ #
20
+ class PartialResolver
21
+ #: () -> PartialResolver
22
+ def self.current
23
+ @current ||= new
24
+ end
25
+
26
+ #: () -> void
27
+ def self.reset!
28
+ @current = nil
29
+ end
30
+
31
+ #: (?project_path: untyped, ?view_paths: Array[String]?) -> void
32
+ def initialize(project_path: nil, view_paths: nil)
33
+ @project_path = project_path
34
+ @view_paths = view_paths
35
+ end
36
+
37
+ #: (String, ?from: untyped, ?format: String?) -> untyped
38
+ def resolve(name, from: nil, format: nil)
39
+ resolvers.each do |resolver|
40
+ found = resolver.resolve(name, from: from, format: format)
41
+
42
+ return found if found
43
+ end
44
+
45
+ nil
46
+ end
47
+
48
+ #: (String, ?from: untyped) -> Array[untyped]
49
+ def candidates(name, from: nil)
50
+ resolvers.flat_map { |resolver| resolver.candidates(name, from: from) }.uniq
51
+ end
52
+
53
+ #: (String, ?from: untyped, ?limit: Integer) -> Array[String]
54
+ def similar(name, from: nil, limit: 3)
55
+ resolvers.flat_map { |resolver| resolver.similar(name, from: from, limit: limit) }.uniq.first(limit)
56
+ end
57
+
58
+ #: (untyped) -> String
59
+ def identifier_for(path)
60
+ resolvers.first.identifier_for(path)
61
+ end
62
+
63
+ private
64
+
65
+ #: () -> Array[untyped]
66
+ def resolvers
67
+ dependencies = @view_paths ? nil : Dependencies.current
68
+
69
+ unless dependencies.equal?(@dependencies)
70
+ @dependencies = dependencies
71
+ @resolvers = nil
72
+ end
73
+
74
+ @resolvers ||= roots(dependencies).map { |root| ::Herb::Analysis::PartialResolver.new(project_path, view_root: root) }
75
+ end
76
+
77
+ #: (untyped) -> Array[String?]
78
+ def roots(dependencies)
79
+ paths = @view_paths || dependencies.view_paths
80
+
81
+ paths.empty? ? [nil] : paths
82
+ end
83
+
84
+ #: () -> untyped
85
+ def project_path
86
+ @project_path ||= ::ReActionView.config.project_path
87
+ end
88
+ end
89
+ end
90
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ReActionView
4
- VERSION = "0.4.1"
4
+ VERSION = "0.6.1"
5
5
  end
data/lib/reactionview.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/deprecation"
4
+
3
5
  require_relative "reactionview/version"
4
6
  require_relative "reactionview/config"
5
7
 
@@ -17,6 +19,14 @@ require_relative "reactionview/config"
17
19
 
18
20
  # require_relative "reactionview/source_annotation_extractor"
19
21
 
22
+ require_relative "reactionview/slots"
23
+ require_relative "reactionview/slots/resolver"
24
+ require_relative "reactionview/slots/rendering"
25
+ require_relative "reactionview/slots/state_overrides"
26
+ require_relative "reactionview/slots/dev_compiler"
27
+
28
+ require_relative "reactionview/template/dependencies"
29
+ require_relative "reactionview/template/partial_resolver"
20
30
  require_relative "reactionview/template/local_template"
21
31
 
22
32
  require_relative "reactionview/template/handlers/erb"
@@ -26,4 +36,7 @@ require_relative "reactionview/template/handlers/herb/herb"
26
36
  require_relative "reactionview/railtie" if defined?(Rails::Railtie)
27
37
 
28
38
  module ReActionView
39
+ def self.deprecator
40
+ @deprecator ||= ::ActiveSupport::Deprecation.new("1.0", "ReActionView")
41
+ end
29
42
  end
data/reactionview.gemspec CHANGED
@@ -9,8 +9,8 @@ Gem::Specification.new do |spec|
9
9
  spec.email = ["marco.roth@intergga.ch"]
10
10
  spec.license = "MIT"
11
11
 
12
- spec.summary = "An ActionView-compatible ERB engine with modern DX - re-imagined with Herb."
13
- spec.description = spec.summary
12
+ spec.summary = "Reactive views for the HTML+ERB you already have."
13
+ spec.description = "You get what a client-side framework gives you, without adopting one."
14
14
  spec.homepage = "https://reactionview.dev"
15
15
  spec.required_ruby_version = ">= 3.2.0"
16
16
 
@@ -36,6 +36,7 @@ Gem::Specification.new do |spec|
36
36
  spec.require_paths = ["lib"]
37
37
 
38
38
  spec.add_dependency "actionview", ">= 7.0"
39
- spec.add_dependency "cruise"
40
- spec.add_dependency "herb", ">= 0.10.4", "< 0.11.0"
39
+ spec.add_dependency "cruise", ">= 0.3.0"
40
+ spec.add_dependency "herb", ">= 0.11.2"
41
+ spec.add_dependency "websocket", "~> 1.2"
41
42
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: reactionview
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marco Roth
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-09-10 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: actionview
@@ -29,36 +29,43 @@ dependencies:
29
29
  requirements:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: '0'
32
+ version: 0.3.0
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: '0'
39
+ version: 0.3.0
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: herb
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: 0.10.4
47
- - - "<"
48
- - !ruby/object:Gem::Version
49
- version: 0.11.0
46
+ version: 0.11.2
50
47
  type: :runtime
51
48
  prerelease: false
52
49
  version_requirements: !ruby/object:Gem::Requirement
53
50
  requirements:
54
51
  - - ">="
55
52
  - !ruby/object:Gem::Version
56
- version: 0.10.4
57
- - - "<"
53
+ version: 0.11.2
54
+ - !ruby/object:Gem::Dependency
55
+ name: websocket
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.2'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
58
66
  - !ruby/object:Gem::Version
59
- version: 0.11.0
60
- description: An ActionView-compatible ERB engine with modern DX - re-imagined with
61
- Herb.
67
+ version: '1.2'
68
+ description: You get what a client-side framework gives you, without adopting one.
62
69
  email:
63
70
  - marco.roth@intergga.ch
64
71
  executables: []
@@ -72,18 +79,29 @@ files:
72
79
  - app/assets/javascripts/reactionview-dev-tools.esm.js.map
73
80
  - app/assets/javascripts/reactionview-dev-tools.umd.js
74
81
  - app/assets/javascripts/reactionview-dev-tools.umd.js.map
82
+ - app/assets/javascripts/reactionview.esm.js
83
+ - app/assets/javascripts/reactionview.esm.js.map
75
84
  - lib/generators/reactionview/install_generator.rb
76
85
  - lib/reaction_view.rb
77
86
  - lib/reactionview.rb
78
87
  - lib/reactionview/asset_manifest.rb
79
88
  - lib/reactionview/config.rb
89
+ - lib/reactionview/importmap.rb
90
+ - lib/reactionview/instrumentation.rb
80
91
  - lib/reactionview/middleware/asset_manifest_check.rb
81
92
  - lib/reactionview/railtie.rb
93
+ - lib/reactionview/slots.rb
94
+ - lib/reactionview/slots/dev_compiler.rb
95
+ - lib/reactionview/slots/rendering.rb
96
+ - lib/reactionview/slots/resolver.rb
97
+ - lib/reactionview/slots/state_overrides.rb
82
98
  - lib/reactionview/stale_asset_manifest_error.rb
99
+ - lib/reactionview/template/dependencies.rb
83
100
  - lib/reactionview/template/handlers/erb.rb
84
101
  - lib/reactionview/template/handlers/herb.rb
85
102
  - lib/reactionview/template/handlers/herb/herb.rb
86
103
  - lib/reactionview/template/local_template.rb
104
+ - lib/reactionview/template/partial_resolver.rb
87
105
  - lib/reactionview/version.rb
88
106
  - reactionview.gemspec
89
107
  - sig/reactionview.rbs
@@ -109,7 +127,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
109
127
  - !ruby/object:Gem::Version
110
128
  version: '0'
111
129
  requirements: []
112
- rubygems_version: 3.6.2
130
+ rubygems_version: 4.0.20
113
131
  specification_version: 4
114
- summary: An ActionView-compatible ERB engine with modern DX - re-imagined with Herb.
132
+ summary: Reactive views for the HTML+ERB you already have.
115
133
  test_files: []