floatable-rails 0.1.2 → 0.1.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d7ba3f4dd67001b178b9cc2ce798c344f93f1f6048835797d50ab30e3b8b233
4
- data.tar.gz: 6ac0062262bb636e6e53db639f9cf5ae4efa3abeace90203c93691ccf205b6cc
3
+ metadata.gz: edb701a8b2d34308e5a086f5a01943c3e0e240278367f79315e1dbcff3bb7937
4
+ data.tar.gz: '0982c58d7ffea82bae36e0119af731daa15e6e5a239a10ae3dffa79b7801a30e'
5
5
  SHA512:
6
- metadata.gz: 0f446eb455149fd63b77525932f3d61e978f898c687e373218b538e06f307fce16e289073616c4e41b75de9ba55f60b718a1928506399f1793c77d4df0408c97
7
- data.tar.gz: 0cfa54163524653e4f0624c17f6677c0ba686e1ef904d168dd7868076ab21a2913d0c3974c9be6801767acaaab2dc83586dfba309128755122b0b1fb071fc298
6
+ metadata.gz: 9a94cc87a936389b7f2c314308007169e749c33a4af2d45f355de8c4713915205a4d1368245db70573a6a8e9566f86456a88ce51d9d91f6aee83736de111447c
7
+ data.tar.gz: 25ff3bc99798da03cb4121b4d6e27f0d27eaf92ab7e38af66b3d11d580d5b3693cc2bcc81f59182c8c9615c301caad6ad39e2a79f3870bd57bb8b2f7b8c55bd2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.3
4
+
5
+ - Add `instrumentation_enabled` config to toggle HTML instrumentation separately from script injection.
6
+ - Switch ERB/Slim instrumentation to view markers and assign Base64-encoded ids instead of line numbers.
7
+ - Remove `data-floatable-content` population from tag instrumentation.
8
+ - Log middleware backtraces and re-raise errors in development/test.
9
+
3
10
  ## 0.1.2
4
11
 
5
12
  - Fix Slim instrumentation for doctype/header structure, filters, and control-flow continuations.
data/README.md CHANGED
@@ -47,6 +47,10 @@ Floatable::Rails.configure do |config|
47
47
  Rails.env.development? || ENV["FLOATABLE_ENABLED"] == "1"
48
48
  end
49
49
 
50
+ # Disable HTML instrumentation (data attributes, view markers).
51
+ # Script injection still follows `config.enabled`.
52
+ config.instrumentation_enabled = true
53
+
50
54
  # Metadata passed as data attributes on the script tag.
51
55
  config.script_workspace = "your-floatable-workspace"
52
56
  config.script_repository = "your-floatable-repo"
@@ -1,8 +1,8 @@
1
1
  require "rails"
2
2
  require "floatable/rails/helpers"
3
3
  require "floatable/rails/tag_instrumentation"
4
- require "floatable/rails/erb_line_instrumentation"
5
- require "floatable/rails/slim_line_instrumentation"
4
+ require "floatable/rails/erb_view_instrumentation"
5
+ require "floatable/rails/slim_view_instrumentation"
6
6
  require "floatable/rails/response_middleware"
7
7
 
8
8
  module Floatable
@@ -17,14 +17,14 @@ module Floatable
17
17
  end
18
18
  end
19
19
 
20
- initializer "floatable.rails.erb_line_instrumentation" do
20
+ initializer "floatable.rails.erb_view_instrumentation" do
21
21
  ActiveSupport.on_load(:action_view) do
22
22
  ::ActionView::Template::Handlers::ERB.prepend(::Floatable::Rails::ErbHandlerPatch)
23
- ::ActionView::Template::Handlers::ERB.erb_implementation = ::Floatable::Rails::ErubiWithFloatableLines
23
+ ::ActionView::Template::Handlers::ERB.erb_implementation = ::Floatable::Rails::ErubiWithFloatableViewMarkers
24
24
  end
25
25
  end
26
26
 
27
- initializer "floatable.rails.slim_line_instrumentation" do
27
+ initializer "floatable.rails.slim_view_instrumentation" do
28
28
  ActiveSupport.on_load(:action_view) do
29
29
  next unless defined?(::Slim::RailsTemplate)
30
30
 
@@ -32,7 +32,6 @@ module Floatable
32
32
  end
33
33
  end
34
34
 
35
-
36
35
  initializer "floatable.rails.controller_helpers" do
37
36
  ActiveSupport.on_load(:action_controller_base) do
38
37
  helper ::Floatable::Rails::Helpers
@@ -0,0 +1,67 @@
1
+ require "pathname"
2
+
3
+ module Floatable
4
+ module Rails
5
+ module ErbHandlerPatch
6
+ def call(template, source)
7
+ source ||= template.source
8
+ template_source = source.b
9
+
10
+ erb = template_source.gsub(self.class::ENCODING_TAG, "")
11
+ encoding = $2
12
+
13
+ erb.force_encoding valid_encoding(source.dup, encoding)
14
+ erb.encode!
15
+ erb.chomp! if strip_trailing_newlines
16
+
17
+ options = {
18
+ escape: (self.class.escape_ignore_list.include? template.type),
19
+ trim: (self.class.erb_trim_mode == "-"),
20
+ filename: template.identifier
21
+ }
22
+
23
+ self.class.erb_implementation.new(erb, options).src
24
+ end
25
+ end
26
+
27
+ class ErubiWithFloatableViewMarkers < ::ActionView::Template::Handlers::ERB::Erubi
28
+ def initialize(input, properties = {})
29
+ view_path = floatable_views_path(properties[:filename])
30
+
31
+ if view_path.present?
32
+ properties = properties.dup
33
+ bufvar = properties[:bufvar] || "@output_buffer"
34
+ postamble = properties[:postamble] || bufvar
35
+
36
+ properties[:preamble] =
37
+ "#{properties[:preamble]}" \
38
+ "@output_buffer.safe_append='<!-- #{::Floatable::Rails::VIEW_COMMENT_BEGIN} #{view_path} -->' if Thread.current[:floatable_enabled];"
39
+
40
+ properties[:postamble] =
41
+ "@output_buffer.safe_append='<!-- #{::Floatable::Rails::VIEW_COMMENT_END} #{view_path} -->' if Thread.current[:floatable_enabled];" \
42
+ "#{postamble}"
43
+ end
44
+
45
+ super
46
+ end
47
+
48
+ private
49
+
50
+ def floatable_views_path(filename)
51
+ return nil if filename.nil?
52
+
53
+ str = filename.to_s
54
+ return nil unless str.include?("/app/views/") || str.start_with?("app/views/")
55
+
56
+ return str if str.start_with?("app/views/")
57
+
58
+ app_root = ::Rails.root.to_s
59
+ return str unless app_root.present?
60
+
61
+ Pathname.new(str).relative_path_from(Pathname.new(app_root)).to_s
62
+ rescue
63
+ nil
64
+ end
65
+ end
66
+ end
67
+ end
@@ -1,7 +1,5 @@
1
1
  require "nokogiri"
2
- require "erb"
3
- require "json"
4
- require "floatable/rails/erb_line_instrumentation"
2
+ require "base64"
5
3
 
6
4
  module Floatable
7
5
  module Rails
@@ -12,7 +10,9 @@ module Floatable
12
10
 
13
11
  def call(env)
14
12
  enabled = floatable_enabled?(env)
15
- Thread.current[:floatable_enabled] = enabled
13
+ instrument = enabled && ::Floatable::Rails.instrumentation_enabled?
14
+ Thread.current[:floatable_enabled] = instrument
15
+
16
16
  status = headers = body = nil
17
17
  begin
18
18
  status, headers, body = @app.call(env)
@@ -20,14 +20,14 @@ module Floatable
20
20
  Thread.current[:floatable_enabled] = nil
21
21
  end
22
22
 
23
- return [ status, headers, body ] unless html_response?(status, headers)
24
23
  return [ status, headers, body ] unless enabled
24
+ return [ status, headers, body ] unless html_response?(status, headers)
25
25
 
26
26
  raw = +""
27
27
  body.each { |chunk| raw << chunk.to_s }
28
28
  body.close if body.respond_to?(:close)
29
29
 
30
- instrumented = instrument_html(raw, env)
30
+ instrumented = instrument ? instrument_html(raw) : raw
31
31
  final_html = inject_script(instrumented)
32
32
 
33
33
  headers["Content-Length"] = final_html.bytesize.to_s if headers["Content-Length"]
@@ -35,6 +35,14 @@ module Floatable
35
35
  [ status, headers, [ final_html ] ]
36
36
  rescue => e
37
37
  ::Rails.logger.error("[Floatable::Rails::ResponseMiddleware] failed: #{e.class} - #{e.message}")
38
+ if e.backtrace
39
+ e.backtrace.first(30).each do |line|
40
+ ::Rails.logger.error(" #{line}")
41
+ end
42
+ end
43
+
44
+ raise if ::Rails.env.development? || ::Rails.env.test?
45
+
38
46
  status ||= 500
39
47
  headers ||= {}
40
48
  body ||= []
@@ -57,40 +65,18 @@ module Floatable
57
65
  false
58
66
  end
59
67
 
60
- def instrument_html(html, env)
68
+ def instrument_html(html)
61
69
  return html if html.strip.empty?
62
70
 
63
71
  doctype = html[/\A\s*<!DOCTYPE[^>]*>/i]
64
72
  doc = Nokogiri::HTML.parse(html)
73
+
65
74
  doc.css("*").each do |node|
66
75
  name = node.name.to_s.downcase
67
76
  next if %w[script style].include?(name)
68
-
69
- # Don't override helper-provided metadata
70
- has_path = node["data-floatable-path"].present?
71
-
72
- unless node["data-floatable-content"].present?
73
- data = {
74
- name: node.name,
75
- text: node.text.to_s.strip.presence,
76
- class: node["class"].to_s.presence,
77
- id: node["id"].to_s.presence
78
- }.compact
79
-
80
- if data.any?
81
- encoded = ERB::Util.url_encode(JSON.generate(data)) rescue nil
82
- node["data-floatable-content"] = encoded if encoded
83
- end
84
- end
85
-
86
- next if has_path
87
77
  end
88
78
 
89
- apply_view_annotations(doc)
90
- # Top → down: let descendants inherit path from ancestors when safe
91
- propagate_file_metadata_down(doc)
92
- # Ensure ids are present and consistent once paths are known
93
- assign_floatable_ids(doc)
79
+ apply_view_markers_and_assign_ids(doc)
94
80
 
95
81
  rendered = doc.to_html
96
82
  return rendered if doctype.nil? || rendered.lstrip.start_with?("<!DOCTYPE")
@@ -101,31 +87,54 @@ module Floatable
101
87
  html
102
88
  end
103
89
 
104
- # Top → down: walk the tree and carry the closest ancestor's path
105
- # down to all descendants that don't have their own.
106
- def propagate_file_metadata_down(doc)
107
- doc.children.each do |child|
108
- next unless child.element?
109
- propagate_file_metadata_down_from_node(child, current_path: nil)
110
- end
111
- rescue => e
112
- ::Rails.logger.debug("[Floatable::Rails] propagate_file_metadata_down failed: #{e.class} - #{e.message}")
113
- end
90
+ def apply_view_markers_and_assign_ids(doc)
91
+ view_stack = []
92
+ counters = Hash.new(0)
93
+
94
+ doc.traverse do |node|
95
+ if node.comment?
96
+ content = node.content.to_s.strip
114
97
 
115
- def propagate_file_metadata_down_from_node(node, current_path:)
116
- # Update context with this node's path if it has it
117
- node_path = node["data-floatable-path"].presence || current_path
98
+ view_comment_begin = ::Floatable::Rails::VIEW_COMMENT_BEGIN
99
+ if content.start_with?(view_comment_begin)
100
+ path = content.delete_prefix(view_comment_begin).strip
101
+ view_stack << (app_views_path?(path) ? path : nil)
102
+ next
103
+ end
118
104
 
119
- # If this node doesn't have path but context does, inherit it
120
- if node_path && node["data-floatable-path"].blank?
121
- node["data-floatable-path"] = node_path
122
- end
105
+ if content.start_with?(::Floatable::Rails::VIEW_COMMENT_END)
106
+ view_stack.pop
107
+ next
108
+ end
123
109
 
124
- next_path = layout_path?(node_path) ? nil : node_path
110
+ next
111
+ end
125
112
 
126
- node.element_children.each do |child|
127
- propagate_file_metadata_down_from_node(child, current_path: next_path)
113
+ next unless node.element?
114
+
115
+ name = node.name.to_s.downcase
116
+ next if %w[script style].include?(name)
117
+
118
+ current_path = view_stack.reverse.find(&:present?)
119
+ next if current_path.blank?
120
+
121
+ node["data-floatable-path"] ||= current_path
122
+ if node["data-floatable-id"].blank?
123
+ counters[current_path] += 1
124
+ encoded = Base64.urlsafe_encode64(counters[current_path].to_s, padding: false)
125
+ node["data-floatable-id"] = "#{current_path}:#{encoded}"
126
+ end
128
127
  end
128
+
129
+ # Remove all comments, including our markers.
130
+ doc.xpath("//comment()").remove
131
+ rescue => e
132
+ ::Rails.logger.debug("[Floatable::Rails] apply_view_markers_and_assign_ids failed: #{e.class} - #{e.message}")
133
+ end
134
+
135
+ def app_views_path?(path)
136
+ str = path.to_s
137
+ str.include?("/app/views/") || str.start_with?("app/views/")
129
138
  end
130
139
 
131
140
  def inject_script(html)
@@ -148,79 +157,6 @@ module Floatable
148
157
  ::Rails.logger.error("[Floatable::Rails::ResponseMiddleware] inject_script failed: #{e.class} - #{e.message}")
149
158
  html
150
159
  end
151
-
152
- def assign_floatable_ids(doc)
153
- doc.css("*[data-floatable-path]").each do |node|
154
- line = node["data-floatable-line"].presence
155
- next unless line
156
-
157
- desired = "#{node["data-floatable-path"]}:#{line}"
158
- node["data-floatable-id"] = desired if node["data-floatable-id"] != desired
159
- end
160
- rescue => e
161
- ::Rails.logger.debug("[Floatable::Rails] assign_floatable_ids failed: #{e.class} - #{e.message}")
162
- end
163
-
164
- def layout_path?(path)
165
- str = path.to_s
166
- str.include?("/app/views/layouts/") || str.start_with?("app/views/layouts/")
167
- end
168
-
169
- def app_views_path?(path)
170
- str = path.to_s
171
- str.include?("/app/views/") || str.start_with?("app/views/")
172
- end
173
-
174
- def apply_view_annotations(doc)
175
- view_stack = []
176
- line_prefix = ::Floatable::Rails::ErbLineInstrumentation::LINE_MARKER_PREFIX
177
- expr_line_prefix = ::Floatable::Rails::ErbLineInstrumentation::EXPR_LINE_MARKER_PREFIX
178
- line_comment_re = /\A#{Regexp.escape(line_prefix)}\d+\z/
179
- expr_comment_re = /\A#{Regexp.escape(expr_line_prefix)}\d+\z/
180
- doc.traverse do |node|
181
- if node.comment?
182
- content = node.content.to_s.strip
183
- if content.start_with?("BEGIN ")
184
- path = content.delete_prefix("BEGIN ").strip
185
- view_stack << path if app_views_path?(path)
186
- next
187
- end
188
-
189
- if content.start_with?("END ")
190
- path = content.delete_prefix("END ").strip
191
- view_stack.pop if view_stack.last == path
192
- next
193
- end
194
-
195
- next
196
- end
197
-
198
- next unless node.element?
199
-
200
- current_path = view_stack.last
201
- node["data-floatable-path"] ||= current_path if current_path.present?
202
-
203
- line = floatable_line_from_previous_siblings(node, line_prefix, expr_line_prefix, line_comment_re, expr_comment_re)
204
- node["data-floatable-line"] = line if line.to_i > 0 && current_path.present?
205
- end
206
-
207
- doc.xpath("//comment()").remove
208
- rescue => e
209
- ::Rails.logger.debug("[Floatable::Rails] apply_view_annotations failed: #{e.class} - #{e.message}")
210
- end
211
-
212
- def floatable_line_from_previous_siblings(node, line_prefix, expr_line_prefix, line_comment_re, expr_comment_re)
213
- prev = node.previous_sibling
214
- while prev
215
- if prev.comment?
216
- content = prev.content.to_s.strip
217
- return content.delete_prefix(expr_line_prefix).to_i if content.match?(expr_comment_re)
218
- return content.delete_prefix(line_prefix).to_i if content.match?(line_comment_re)
219
- end
220
- prev = prev.previous_sibling
221
- end
222
- nil
223
- end
224
160
  end
225
161
  end
226
162
  end
@@ -0,0 +1,83 @@
1
+ require "pathname"
2
+
3
+ module Floatable
4
+ module Rails
5
+ module SlimViewMarkerInstrumenter
6
+ # Inserts view markers once per template render, gated by Thread.current[:floatable_enabled].
7
+ def self.instrument(source, view_path)
8
+ lines = source.to_s.split("\n", -1)
9
+ return source.to_s if lines.empty?
10
+
11
+ first_non_empty = lines.index { |l| !l.strip.empty? } || 0
12
+
13
+ # Keep optional Slim headers (doctype/html) at the very top if present.
14
+ doctype_idx = if lines[first_non_empty]&.lstrip&.match?(/\A(doctype\b|!!!\b)/)
15
+ first_non_empty
16
+ end
17
+
18
+ html_idx = nil
19
+ if doctype_idx
20
+ rel = lines[(doctype_idx + 1)..].to_a.index { |l| !l.strip.empty? && l.lstrip.match?(/\Ahtml\b/) }
21
+ html_idx = rel ? (doctype_idx + 1 + rel) : nil
22
+ end
23
+
24
+ insert_after = html_idx || doctype_idx || first_non_empty
25
+ insert_after = [ insert_after, lines.length - 1 ].min
26
+
27
+ # Choose indentation that won't break Slim parsing.
28
+ marker_indent =
29
+ begin
30
+ next_non_empty = lines[(insert_after + 1)..].to_a.find { |l| !l.strip.empty? }
31
+ if next_non_empty
32
+ next_non_empty[/\A[ \t]*/] || ""
33
+ else
34
+ base_line = lines[insert_after] || ""
35
+ (base_line[/\A[ \t]*/] || "") + " "
36
+ end
37
+ end
38
+
39
+ begin_marker = "#{marker_indent}== (Thread.current[:floatable_enabled] && defined?(@output_buffer)) ? '<!-- #{::Floatable::Rails::VIEW_COMMENT_BEGIN} #{view_path} -->' : ''"
40
+ end_marker = "#{marker_indent}== (Thread.current[:floatable_enabled] && defined?(@output_buffer)) ? '<!-- #{::Floatable::Rails::VIEW_COMMENT_END} #{view_path} -->' : ''"
41
+
42
+ out = []
43
+ lines.each_with_index do |line, idx|
44
+ out << line
45
+ out << begin_marker if idx == insert_after
46
+ end
47
+
48
+ out << end_marker
49
+ out.join("\n")
50
+ end
51
+ end
52
+
53
+ module SlimHandlerPatch
54
+ def call(template, source = nil)
55
+ return super unless template.format == :html
56
+
57
+ view_path = floatable_views_path(template.identifier)
58
+ return super if view_path.nil?
59
+
60
+ instrumented = SlimViewMarkerInstrumenter.instrument(source || template.source, view_path)
61
+ super(template, instrumented)
62
+ end
63
+
64
+ private
65
+
66
+ def floatable_views_path(filename)
67
+ return nil if filename.nil?
68
+
69
+ str = filename.to_s
70
+ return nil unless str.include?("/app/views/") || str.start_with?("app/views/")
71
+
72
+ return str if str.start_with?("app/views/")
73
+
74
+ app_root = ::Rails.root.to_s
75
+ return str unless app_root.present?
76
+
77
+ Pathname.new(str).relative_path_from(Pathname.new(app_root)).to_s
78
+ rescue
79
+ nil
80
+ end
81
+ end
82
+ end
83
+ end
@@ -1,6 +1,5 @@
1
1
  require "pathname"
2
- require "json"
3
- require "erb"
2
+ require "base64"
4
3
 
5
4
  module Floatable
6
5
  module Rails
@@ -13,17 +12,11 @@ module Floatable
13
12
  if file
14
13
  options[:"data-floatable-path"] ||= floatable_relative_path(file)
15
14
  if line
16
- options[:"data-floatable-line"] ||= line
17
- options[:"data-floatable-id"] ||= "#{options[:"data-floatable-path"]}:#{options[:"data-floatable-line"]}"
15
+ encoded = Base64.urlsafe_encode64(line.to_s, padding: false)
16
+ options[:"data-floatable-id"] ||= "#{options[:"data-floatable-path"]}:#{encoded}"
18
17
  end
19
18
  end
20
19
 
21
- options[:"data-floatable-content"] ||= floatable_content_attr(
22
- tag_name: name,
23
- options: options,
24
- content_text: block_given? ? nil : content_or_options_with_block
25
- )
26
-
27
20
  # Re-shape args for super if we mutated them
28
21
  if content_or_options_with_block.is_a?(Hash) && options
29
22
  content_or_options_with_block = options
@@ -42,17 +35,11 @@ module Floatable
42
35
  if file
43
36
  options[:"data-floatable-path"] ||= floatable_relative_path(file)
44
37
  if line
45
- options[:"data-floatable-line"] ||= line
46
- options[:"data-floatable-id"] ||= "#{options[:"data-floatable-path"]}:#{options[:"data-floatable-line"]}"
38
+ encoded = Base64.urlsafe_encode64(line.to_s, padding: false)
39
+ options[:"data-floatable-id"] ||= "#{options[:"data-floatable-path"]}:#{encoded}"
47
40
  end
48
41
  end
49
42
 
50
- options[:"data-floatable-content"] ||= floatable_content_attr(
51
- tag_name: name,
52
- options: options,
53
- content_text: nil
54
- )
55
-
56
43
  return super(name, options, open, escape)
57
44
  end
58
45
 
@@ -64,27 +51,12 @@ module Floatable
64
51
  def floatable_enabled_for_view?
65
52
  return false unless respond_to?(:request)
66
53
 
67
- ::Floatable::Rails.enabled_for?(request)
54
+ ::Floatable::Rails.instrumentation_enabled? && ::Floatable::Rails.enabled_for?(request)
68
55
  rescue => e
69
56
  ::Rails.logger.error("[Floatable::Rails::TagInstrumentation] enabled? failed: #{e.class} - #{e.message}")
70
57
  false
71
58
  end
72
59
 
73
- def floatable_content_attr(tag_name:, options:, content_text:)
74
- data = {
75
- name: tag_name.to_s,
76
- text: content_text.to_s.presence,
77
- class: options[:class].to_s.presence,
78
- id: options[:id].to_s.presence
79
- }.compact
80
-
81
- return if data.empty?
82
-
83
- ERB::Util.url_encode(JSON.generate(data))
84
- rescue
85
- nil
86
- end
87
-
88
60
  def ensure_options_hash(content_or_options_with_block, options)
89
61
  if content_or_options_with_block.is_a?(Hash) && options.nil?
90
62
  content_or_options_with_block
@@ -1,5 +1,5 @@
1
1
  module Floatable
2
2
  module Rails
3
- VERSION = "0.1.2"
3
+ VERSION = "0.1.3"
4
4
  end
5
5
  end
@@ -2,9 +2,11 @@ require "erb"
2
2
  require "floatable/rails/version"
3
3
  require "floatable/rails/engine"
4
4
 
5
- # lib/floatable/rails.rb
6
5
  module Floatable
7
6
  module Rails
7
+ VIEW_COMMENT_BEGIN = "FLOATABLE_VIEW_BEGIN".freeze
8
+ VIEW_COMMENT_END = "FLOATABLE_VIEW_END".freeze
9
+
8
10
  # Avoid namespace resolution issues for apps that define module Floatable.
9
11
  # In that case, `Rails::Application` resolves to `Floatable::Rails::Application`,
10
12
  # so provide a compatible alias.
@@ -13,6 +15,8 @@ module Floatable
13
15
  class Configuration
14
16
  # Proc: ->(request) { true/false }
15
17
  attr_accessor :enabled
18
+ # Toggle for HTML instrumentation (data attributes, view markers)
19
+ attr_accessor :instrumentation_enabled
16
20
  # URL for the Floatable toolbar script
17
21
  attr_accessor :script_src
18
22
  # Metadata
@@ -28,6 +32,8 @@ module Floatable
28
32
  ::Rails.env.development? || ENV["FLOATABLE_ENABLED"] == "1"
29
33
  end
30
34
 
35
+ @instrumentation_enabled = true
36
+
31
37
  # Default script for dev; override in host app
32
38
  @script_src = "https://assets.floatable.dev/js/toolbar.js"
33
39
 
@@ -59,6 +65,10 @@ module Floatable
59
65
  false
60
66
  end
61
67
 
68
+ def instrumentation_enabled?
69
+ config.instrumentation_enabled != false
70
+ end
71
+
62
72
  def script_src
63
73
  config.script_src
64
74
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: floatable-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tobias Almstrand
@@ -37,10 +37,10 @@ files:
37
37
  - Rakefile
38
38
  - lib/floatable/rails.rb
39
39
  - lib/floatable/rails/engine.rb
40
- - lib/floatable/rails/erb_line_instrumentation.rb
40
+ - lib/floatable/rails/erb_view_instrumentation.rb
41
41
  - lib/floatable/rails/helpers.rb
42
42
  - lib/floatable/rails/response_middleware.rb
43
- - lib/floatable/rails/slim_line_instrumentation.rb
43
+ - lib/floatable/rails/slim_view_instrumentation.rb
44
44
  - lib/floatable/rails/tag_instrumentation.rb
45
45
  - lib/floatable/rails/version.rb
46
46
  homepage: https://floatable.dev
@@ -1,166 +0,0 @@
1
- require "pathname"
2
-
3
- module Floatable
4
- module Rails
5
- module ErbLineInstrumentation
6
- LINE_MARKER_PREFIX = "FLOATABLE_LINE:".freeze
7
- EXPR_LINE_MARKER_PREFIX = "FLOATABLE_EXPR_LINE:".freeze
8
- end
9
-
10
- module ErbHandlerPatch
11
- def call(template, source)
12
- source ||= template.source
13
- template_source = source.b
14
-
15
- erb = template_source.gsub(self.class::ENCODING_TAG, "")
16
- encoding = $2
17
-
18
- erb.force_encoding valid_encoding(source.dup, encoding)
19
- erb.encode!
20
- erb.chomp! if strip_trailing_newlines
21
-
22
- options = {
23
- escape: (self.class.escape_ignore_list.include? template.type),
24
- trim: (self.class.erb_trim_mode == "-"),
25
- filename: template.identifier
26
- }
27
-
28
- if ActionView::Base.respond_to?(:annotate_rendered_view_with_filenames) &&
29
- ActionView::Base.annotate_rendered_view_with_filenames &&
30
- template.format == :html
31
- options[:preamble] = "@output_buffer.safe_append='<!-- BEGIN #{template.short_identifier}\n-->';"
32
- options[:postamble] = "@output_buffer.safe_append='<!-- END #{template.short_identifier} -->';@output_buffer"
33
- end
34
-
35
- self.class.erb_implementation.new(erb, options).src
36
- end
37
- end
38
-
39
- class ErubiWithFloatableLines < ::ActionView::Template::Handlers::ERB::Erubi
40
- LINE_MARKER_PREFIX = ErbLineInstrumentation::LINE_MARKER_PREFIX
41
- EXPR_LINE_MARKER_PREFIX = ErbLineInstrumentation::EXPR_LINE_MARKER_PREFIX
42
-
43
- def initialize(input, properties = {})
44
- @floatable_line = 1
45
- @floatable_view_path = floatable_views_path(properties[:filename])
46
- @floatable_in_views = @floatable_view_path.present?
47
- if @floatable_in_views
48
- properties = properties.dup
49
- bufvar = properties[:bufvar] || "@output_buffer"
50
- postamble = properties[:postamble] || bufvar
51
- properties[:preamble] = "#{properties[:preamble]}@output_buffer.safe_append='<!-- BEGIN #{@floatable_view_path} -->' if Thread.current[:floatable_enabled];"
52
- properties[:postamble] = "@output_buffer.safe_append='<!-- END #{@floatable_view_path} -->' if Thread.current[:floatable_enabled];#{postamble}"
53
- end
54
- super
55
- end
56
-
57
- private
58
-
59
- def add_text(text)
60
- return if text.empty?
61
-
62
- if text == "\n"
63
- @newline_pending += 1
64
- return
65
- end
66
-
67
- full_text = ("\n" * @newline_pending) + text
68
- with_buffer do
69
- src << ".safe_append="
70
- if @floatable_in_views
71
- src << "(Thread.current[:floatable_enabled] ? "
72
- src << floatable_text_literal(full_text)
73
- src << " : "
74
- src << plain_text_literal(full_text)
75
- src << ")"
76
- else
77
- src << plain_text_literal(full_text)
78
- end
79
- end
80
- @newline_pending = 0
81
- @floatable_line += full_text.count("\n")
82
- end
83
-
84
- def add_expression(indicator, code)
85
- flush_newline_if_pending(src)
86
- if @floatable_in_views
87
- with_buffer do
88
- src << ".safe_append='#{expr_line_marker(@floatable_line)}' if Thread.current[:floatable_enabled];"
89
- end
90
- end
91
- super
92
- @floatable_line += code.count("\n")
93
- end
94
-
95
- def add_code(code)
96
- flush_newline_if_pending(src)
97
- super
98
- @floatable_line += code.count("\n")
99
- end
100
-
101
- def flush_newline_if_pending(src)
102
- return if @newline_pending <= 0
103
-
104
- full_text = "\n" * @newline_pending
105
- with_buffer do
106
- src << ".safe_append="
107
- if @floatable_in_views
108
- src << "(Thread.current[:floatable_enabled] ? "
109
- src << floatable_text_literal(full_text)
110
- src << " : "
111
- src << plain_text_literal(full_text)
112
- src << ")"
113
- else
114
- src << plain_text_literal(full_text)
115
- end
116
- end
117
- @floatable_line += @newline_pending
118
- @newline_pending = 0
119
- end
120
-
121
- def line_marker(line)
122
- "<!--#{LINE_MARKER_PREFIX}#{line}-->"
123
- end
124
-
125
- def expr_line_marker(line)
126
- "<!--#{EXPR_LINE_MARKER_PREFIX}#{line}-->"
127
- end
128
-
129
- def floatable_text_literal(text)
130
- marked = add_line_markers(text, @floatable_line)
131
- plain_text_literal(marked)
132
- end
133
-
134
- def plain_text_literal(text)
135
- "'" + text.gsub(/['\\]/, '\\\\\&') + @text_end
136
- end
137
-
138
- def add_line_markers(text, start_line)
139
- lines = text.split("\n", -1)
140
- out = +""
141
- lines.each_with_index do |line_text, idx|
142
- out << line_marker(start_line + idx) << line_text
143
- out << "\n" if idx < lines.length - 1
144
- end
145
- out
146
- end
147
-
148
- def floatable_views_path(filename)
149
- return nil if filename.nil?
150
-
151
- str = filename.to_s
152
- if str.include?("/app/views/") || str.start_with?("app/views/")
153
- return str if str.start_with?("app/views/")
154
-
155
- app_root = ::Rails.root.to_s
156
- return str unless app_root.present?
157
-
158
- return Pathname.new(str).relative_path_from(Pathname.new(app_root)).to_s
159
- end
160
- nil
161
- rescue
162
- nil
163
- end
164
- end
165
- end
166
- end
@@ -1,106 +0,0 @@
1
- require "pathname"
2
-
3
- module Floatable
4
- module Rails
5
- module SlimLineInstrumenter
6
- def self.instrument(source, view_path)
7
- lines = source.to_s.split("\n", -1)
8
- first_non_empty = lines.index { |line| !line.strip.empty? }
9
- doctype_index = if first_non_empty && lines[first_non_empty].lstrip.match?(/\A(doctype\b|!!!\b)/)
10
- first_non_empty
11
- end
12
- html_index = nil
13
- if doctype_index
14
- html_index = lines[(doctype_index + 1)..].to_a.index { |line| !line.strip.empty? && line.lstrip.match?(/\Ahtml\b/) }
15
- html_index = html_index ? html_index + doctype_index + 1 : nil
16
- end
17
- header_index = html_index || doctype_index
18
- marker_indent = ""
19
- if header_index
20
- next_non_empty = lines[(header_index + 1)..].to_a.find { |line| !line.strip.empty? }
21
- marker_indent = next_non_empty ? (next_non_empty[/\A[ \t]*/] || "") : ((lines[header_index][/\A[ \t]*/] || "") + " ")
22
- end
23
- out = []
24
- begin_marker = "#{marker_indent}== (Thread.current[:floatable_enabled] && defined?(@output_buffer)) ? '<!-- BEGIN #{view_path} -->' : ''"
25
- end_marker = "#{marker_indent}== (Thread.current[:floatable_enabled] && defined?(@output_buffer)) ? '<!-- END #{view_path} -->' : ''"
26
- out << begin_marker if header_index.nil?
27
- filter_indent = nil
28
-
29
- lines.each_with_index do |line, idx|
30
- if header_index && idx < header_index
31
- out << line
32
- next
33
- end
34
-
35
- if header_index && idx == header_index
36
- out << line
37
- out << begin_marker
38
- next
39
- end
40
-
41
- indent = line[/\A[ \t]*/] || ""
42
- indent_level = indent.size
43
- stripped = line.lstrip
44
- if stripped.empty?
45
- out << line
46
- next
47
- end
48
-
49
- if stripped.match?(/\A-\s*(else|elsif|when|rescue|ensure)\b/)
50
- out << line
51
- next
52
- end
53
-
54
- if filter_indent && indent_level > filter_indent
55
- out << line
56
- next
57
- end
58
-
59
- filter_indent = nil if filter_indent && indent_level <= filter_indent
60
-
61
- if stripped.match?(/\A[a-zA-Z_][\w-]*:\s*\z/) && !stripped.start_with?("-", "=", "|")
62
- filter_indent = indent_level
63
- out << line
64
- next
65
- end
66
-
67
- out << "#{indent}== (Thread.current[:floatable_enabled] && defined?(@output_buffer)) ? '<!--#{ErbLineInstrumentation::LINE_MARKER_PREFIX}#{idx + 1}-->' : ''"
68
- out << line
69
- end
70
-
71
- out << end_marker
72
- out.join("\n")
73
- end
74
- end
75
-
76
- module SlimHandlerPatch
77
- def call(template, source = nil)
78
- return super unless template.format == :html
79
-
80
- view_path = floatable_views_path(template.identifier)
81
- return super if view_path.nil?
82
-
83
- instrumented = SlimLineInstrumenter.instrument(source || template.source, view_path)
84
- super(template, instrumented)
85
- end
86
-
87
- private
88
-
89
- def floatable_views_path(filename)
90
- return nil if filename.nil?
91
-
92
- str = filename.to_s
93
- return nil unless str.include?("/app/views/") || str.start_with?("app/views/")
94
-
95
- return str if str.start_with?("app/views/")
96
-
97
- app_root = ::Rails.root.to_s
98
- return str unless app_root.present?
99
-
100
- Pathname.new(str).relative_path_from(Pathname.new(app_root)).to_s
101
- rescue
102
- nil
103
- end
104
- end
105
- end
106
- end