floatable-rails 0.1.2 → 0.1.4
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 +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +4 -0
- data/lib/floatable/rails/engine.rb +5 -6
- data/lib/floatable/rails/erb_view_instrumentation.rb +67 -0
- data/lib/floatable/rails/response_middleware.rb +60 -124
- data/lib/floatable/rails/slim_view_instrumentation.rb +80 -0
- data/lib/floatable/rails/tag_instrumentation.rb +6 -34
- data/lib/floatable/rails/version.rb +1 -1
- data/lib/floatable/rails.rb +11 -1
- metadata +3 -3
- data/lib/floatable/rails/erb_line_instrumentation.rb +0 -166
- data/lib/floatable/rails/slim_line_instrumentation.rb +0 -106
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9288ca577a553f70abaa3928fe8dd0ac842f080a3ac4408e478fcac5c2d27c2f
|
|
4
|
+
data.tar.gz: 2947208f0c7d70d327da7ddd8d89504c92526bc142240c82ce8409af85902d63
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 158ba1e2e4b3d4a17891907c5cd5af51b428707add8b9d42950dea39d897689961b07e5d02f290bcef9ded34cf8587d99bf3f4e095367ba37151f5bf4088cfee
|
|
7
|
+
data.tar.gz: 746df7f292f50792d22366625b04d60e5a9ba5c09335a3bd0f68f6fc421d84ce81bf6bec8369c6924ca9e472e1e735de34cac12ea0f02fac12416ee5370b2a9a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.4
|
|
4
|
+
|
|
5
|
+
- Fix Slim marker insertion to avoid interfering with block rendering.
|
|
6
|
+
|
|
7
|
+
## 0.1.3
|
|
8
|
+
|
|
9
|
+
- Add `instrumentation_enabled` config to toggle HTML instrumentation separately from script injection.
|
|
10
|
+
- Switch ERB/Slim instrumentation to view markers and assign Base64-encoded ids instead of line numbers.
|
|
11
|
+
- Remove `data-floatable-content` population from tag instrumentation.
|
|
12
|
+
- Log middleware backtraces and re-raise errors in development/test.
|
|
13
|
+
|
|
3
14
|
## 0.1.2
|
|
4
15
|
|
|
5
16
|
- 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/
|
|
5
|
-
require "floatable/rails/
|
|
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.
|
|
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::
|
|
23
|
+
::ActionView::Template::Handlers::ERB.erb_implementation = ::Floatable::Rails::ErubiWithFloatableViewMarkers
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
initializer "floatable.rails.
|
|
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 "
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
105
|
+
if content.start_with?(::Floatable::Rails::VIEW_COMMENT_END)
|
|
106
|
+
view_stack.pop
|
|
107
|
+
next
|
|
108
|
+
end
|
|
123
109
|
|
|
124
|
-
|
|
110
|
+
next
|
|
111
|
+
end
|
|
125
112
|
|
|
126
|
-
|
|
127
|
-
|
|
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,80 @@
|
|
|
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) at the very top if present.
|
|
14
|
+
doctype_idx =
|
|
15
|
+
if lines[first_non_empty]&.lstrip&.match?(/\A(doctype\b|!!!\b)/)
|
|
16
|
+
first_non_empty
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Insert before the first real line to avoid nesting inside blocks.
|
|
20
|
+
insert_at = doctype_idx ? (doctype_idx + 1) : first_non_empty
|
|
21
|
+
insert_at = [ insert_at, lines.length ].min
|
|
22
|
+
|
|
23
|
+
marker_indent =
|
|
24
|
+
if insert_at < lines.length
|
|
25
|
+
lines[insert_at].to_s[/\A[ \t]*/] || ""
|
|
26
|
+
else
|
|
27
|
+
lines[first_non_empty].to_s[/\A[ \t]*/] || ""
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
begin_marker = [
|
|
31
|
+
"#{marker_indent}- (@output_buffer.safe_concat('<!-- #{::Floatable::Rails::VIEW_COMMENT_BEGIN} #{view_path} -->') if Thread.current[:floatable_enabled] && defined?(@output_buffer)); nil"
|
|
32
|
+
]
|
|
33
|
+
end_marker = [
|
|
34
|
+
"#{marker_indent}- (@output_buffer.safe_concat('<!-- #{::Floatable::Rails::VIEW_COMMENT_END} #{view_path} -->') if Thread.current[:floatable_enabled] && defined?(@output_buffer)); nil"
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
out = []
|
|
38
|
+
lines.each_with_index do |line, idx|
|
|
39
|
+
out.concat(begin_marker) if idx == insert_at
|
|
40
|
+
out << line
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
out.concat(begin_marker) if insert_at == lines.length
|
|
44
|
+
|
|
45
|
+
out.concat(end_marker)
|
|
46
|
+
out.join("\n")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
module SlimHandlerPatch
|
|
51
|
+
def call(template, source = nil)
|
|
52
|
+
return super unless template.format == :html
|
|
53
|
+
|
|
54
|
+
view_path = floatable_views_path(template.identifier)
|
|
55
|
+
return super if view_path.nil?
|
|
56
|
+
|
|
57
|
+
instrumented = SlimViewMarkerInstrumenter.instrument(source || template.source, view_path)
|
|
58
|
+
super(template, instrumented)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def floatable_views_path(filename)
|
|
64
|
+
return nil if filename.nil?
|
|
65
|
+
|
|
66
|
+
str = filename.to_s
|
|
67
|
+
return nil unless str.include?("/app/views/") || str.start_with?("app/views/")
|
|
68
|
+
|
|
69
|
+
return str if str.start_with?("app/views/")
|
|
70
|
+
|
|
71
|
+
app_root = ::Rails.root.to_s
|
|
72
|
+
return str unless app_root.present?
|
|
73
|
+
|
|
74
|
+
Pathname.new(str).relative_path_from(Pathname.new(app_root)).to_s
|
|
75
|
+
rescue
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
require "pathname"
|
|
2
|
-
require "
|
|
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
|
-
|
|
17
|
-
options[:"data-floatable-id"] ||= "#{options[:"data-floatable-path"]}:#{
|
|
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
|
-
|
|
46
|
-
options[:"data-floatable-id"] ||= "#{options[:"data-floatable-path"]}:#{
|
|
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
|
data/lib/floatable/rails.rb
CHANGED
|
@@ -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.
|
|
4
|
+
version: 0.1.4
|
|
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/
|
|
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/
|
|
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
|