floatable-rails 0.1.0
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 +7 -0
- data/CHANGELOG.md +5 -0
- data/MIT-LICENSE +20 -0
- data/README.md +71 -0
- data/Rakefile +3 -0
- data/lib/floatable/rails/engine.rb +47 -0
- data/lib/floatable/rails/erb_line_instrumentation.rb +164 -0
- data/lib/floatable/rails/helpers.rb +25 -0
- data/lib/floatable/rails/response_middleware.rb +223 -0
- data/lib/floatable/rails/slim_line_instrumentation.rb +52 -0
- data/lib/floatable/rails/tag_instrumentation.rb +209 -0
- data/lib/floatable/rails/version.rb +5 -0
- data/lib/floatable/rails.rb +90 -0
- metadata +71 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d9d58deb57c87c0e5dff62513689456adfe23791b234088e485b729963f4a504
|
|
4
|
+
data.tar.gz: 285d4e4a29c62d97cf9cfe7cbc37c8131b2d56e0954ad7230dc98ce3ba051ea3
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1198026452a8ff26ea48063bf4a7c9ba5b4886ebad8fb6dde9a5c1d5615f0a364f187e225936ba2303643d1cf08bca2fcb0632f152ff08efd288e1a329205720
|
|
7
|
+
data.tar.gz: 2acbbbb919b62d4b3134edd4147e603b5d68462ae2dd4ee400b65897538c600bd4406d7c85d0c1921e2a159b50aba204376c1f6b5b4c1a3a45fa053589a1b4f7
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright Tobias Almstrand
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Floatable::Rails
|
|
2
|
+
|
|
3
|
+
Rails integration for Floatable Toolbar. It instruments HTML output with
|
|
4
|
+
metadata and injects the Floatable script so your UI can be inspected and
|
|
5
|
+
annotated in the browser.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
Add the helper to your layout if you want explicit control:
|
|
10
|
+
|
|
11
|
+
```erb
|
|
12
|
+
<%= floatable_tag %>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The middleware also injects the script automatically for HTML responses when
|
|
16
|
+
Floatable is enabled, so the helper is optional.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
Add this line to your application's Gemfile:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
gem "floatable-rails"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
And then execute:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
$ bundle
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or install it yourself as:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
$ gem install floatable-rails
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Configuration
|
|
39
|
+
|
|
40
|
+
Create an initializer (for example, `config/initializers/floatable.rb`):
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
Floatable::Rails.configure do |config|
|
|
44
|
+
# Enable Floatable per-request.
|
|
45
|
+
# Default: enabled in development, or when FLOATABLE_ENABLED=1
|
|
46
|
+
config.enabled = lambda do |request|
|
|
47
|
+
Rails.env.development? || ENV["FLOATABLE_ENABLED"] == "1"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Metadata passed as data attributes on the script tag.
|
|
51
|
+
config.script_workspace = "your-floatable-workspace"
|
|
52
|
+
config.script_repository = "your-floatable-repo"
|
|
53
|
+
config.script_branch = "main"
|
|
54
|
+
config.script_agent_tools_mode = "remote|local"
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Release
|
|
59
|
+
|
|
60
|
+
1. Bump the version in `lib/floatable/rails/version.rb`.
|
|
61
|
+
2. Update `CHANGELOG.md`.
|
|
62
|
+
3. Build the gem: `rake build` (outputs to `pkg/`).
|
|
63
|
+
4. Push the gem: `rake release` or `gem push pkg/floatable-rails-<version>.gem`.
|
|
64
|
+
|
|
65
|
+
## Contributing
|
|
66
|
+
|
|
67
|
+
Bug reports and pull requests are welcome.
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require "rails"
|
|
2
|
+
require "floatable/rails/helpers"
|
|
3
|
+
require "floatable/rails/tag_instrumentation"
|
|
4
|
+
require "floatable/rails/erb_line_instrumentation"
|
|
5
|
+
require "floatable/rails/slim_line_instrumentation"
|
|
6
|
+
require "floatable/rails/response_middleware"
|
|
7
|
+
|
|
8
|
+
module Floatable
|
|
9
|
+
module Rails
|
|
10
|
+
class Engine < ::Rails::Engine
|
|
11
|
+
isolate_namespace Floatable::Rails
|
|
12
|
+
|
|
13
|
+
initializer "floatable.rails.view_helpers" do
|
|
14
|
+
ActiveSupport.on_load(:action_view) do
|
|
15
|
+
include ::Floatable::Rails::Helpers
|
|
16
|
+
prepend ::Floatable::Rails::TagInstrumentation
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
initializer "floatable.rails.erb_line_instrumentation" do
|
|
21
|
+
ActiveSupport.on_load(:action_view) do
|
|
22
|
+
::ActionView::Template::Handlers::ERB.prepend(::Floatable::Rails::ErbHandlerPatch)
|
|
23
|
+
::ActionView::Template::Handlers::ERB.erb_implementation = ::Floatable::Rails::ErubiWithFloatableLines
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
initializer "floatable.rails.slim_line_instrumentation" do
|
|
28
|
+
ActiveSupport.on_load(:action_view) do
|
|
29
|
+
next unless defined?(::Slim::RailsTemplate)
|
|
30
|
+
|
|
31
|
+
::Slim::RailsTemplate.prepend(::Floatable::Rails::SlimHandlerPatch)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
initializer "floatable.rails.controller_helpers" do
|
|
37
|
+
ActiveSupport.on_load(:action_controller_base) do
|
|
38
|
+
helper ::Floatable::Rails::Helpers
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
initializer "floatable.rails.middleware" do |app|
|
|
43
|
+
app.middleware.use ::Floatable::Rails::ResponseMiddleware
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
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.annotate_rendered_view_with_filenames && template.format == :html
|
|
29
|
+
options[:preamble] = "@output_buffer.safe_append='<!-- BEGIN #{template.short_identifier}\n-->';"
|
|
30
|
+
options[:postamble] = "@output_buffer.safe_append='<!-- END #{template.short_identifier} -->';@output_buffer"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
self.class.erb_implementation.new(erb, options).src
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class ErubiWithFloatableLines < ::ActionView::Template::Handlers::ERB::Erubi
|
|
38
|
+
LINE_MARKER_PREFIX = ErbLineInstrumentation::LINE_MARKER_PREFIX
|
|
39
|
+
EXPR_LINE_MARKER_PREFIX = ErbLineInstrumentation::EXPR_LINE_MARKER_PREFIX
|
|
40
|
+
|
|
41
|
+
def initialize(input, properties = {})
|
|
42
|
+
@floatable_line = 1
|
|
43
|
+
@floatable_view_path = floatable_views_path(properties[:filename])
|
|
44
|
+
@floatable_in_views = @floatable_view_path.present?
|
|
45
|
+
if @floatable_in_views
|
|
46
|
+
properties = properties.dup
|
|
47
|
+
bufvar = properties[:bufvar] || "@output_buffer"
|
|
48
|
+
postamble = properties[:postamble] || bufvar
|
|
49
|
+
properties[:preamble] = "#{properties[:preamble]}@output_buffer.safe_append='<!-- BEGIN #{@floatable_view_path} -->' if Thread.current[:floatable_enabled];"
|
|
50
|
+
properties[:postamble] = "@output_buffer.safe_append='<!-- END #{@floatable_view_path} -->' if Thread.current[:floatable_enabled];#{postamble}"
|
|
51
|
+
end
|
|
52
|
+
super
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def add_text(text)
|
|
58
|
+
return if text.empty?
|
|
59
|
+
|
|
60
|
+
if text == "\n"
|
|
61
|
+
@newline_pending += 1
|
|
62
|
+
return
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
full_text = ("\n" * @newline_pending) + text
|
|
66
|
+
with_buffer do
|
|
67
|
+
src << ".safe_append="
|
|
68
|
+
if @floatable_in_views
|
|
69
|
+
src << "(Thread.current[:floatable_enabled] ? "
|
|
70
|
+
src << floatable_text_literal(full_text)
|
|
71
|
+
src << " : "
|
|
72
|
+
src << plain_text_literal(full_text)
|
|
73
|
+
src << ")"
|
|
74
|
+
else
|
|
75
|
+
src << plain_text_literal(full_text)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
@newline_pending = 0
|
|
79
|
+
@floatable_line += full_text.count("\n")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def add_expression(indicator, code)
|
|
83
|
+
flush_newline_if_pending(src)
|
|
84
|
+
if @floatable_in_views
|
|
85
|
+
with_buffer do
|
|
86
|
+
src << ".safe_append='#{expr_line_marker(@floatable_line)}' if Thread.current[:floatable_enabled];"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
super
|
|
90
|
+
@floatable_line += code.count("\n")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def add_code(code)
|
|
94
|
+
flush_newline_if_pending(src)
|
|
95
|
+
super
|
|
96
|
+
@floatable_line += code.count("\n")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def flush_newline_if_pending(src)
|
|
100
|
+
return if @newline_pending <= 0
|
|
101
|
+
|
|
102
|
+
full_text = "\n" * @newline_pending
|
|
103
|
+
with_buffer do
|
|
104
|
+
src << ".safe_append="
|
|
105
|
+
if @floatable_in_views
|
|
106
|
+
src << "(Thread.current[:floatable_enabled] ? "
|
|
107
|
+
src << floatable_text_literal(full_text)
|
|
108
|
+
src << " : "
|
|
109
|
+
src << plain_text_literal(full_text)
|
|
110
|
+
src << ")"
|
|
111
|
+
else
|
|
112
|
+
src << plain_text_literal(full_text)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
@floatable_line += @newline_pending
|
|
116
|
+
@newline_pending = 0
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def line_marker(line)
|
|
120
|
+
"<!--#{LINE_MARKER_PREFIX}#{line}-->"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def expr_line_marker(line)
|
|
124
|
+
"<!--#{EXPR_LINE_MARKER_PREFIX}#{line}-->"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def floatable_text_literal(text)
|
|
128
|
+
marked = add_line_markers(text, @floatable_line)
|
|
129
|
+
plain_text_literal(marked)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def plain_text_literal(text)
|
|
133
|
+
"'" + text.gsub(/['\\]/, '\\\\\&') + @text_end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def add_line_markers(text, start_line)
|
|
137
|
+
lines = text.split("\n", -1)
|
|
138
|
+
out = +""
|
|
139
|
+
lines.each_with_index do |line_text, idx|
|
|
140
|
+
out << line_marker(start_line + idx) << line_text
|
|
141
|
+
out << "\n" if idx < lines.length - 1
|
|
142
|
+
end
|
|
143
|
+
out
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def floatable_views_path(filename)
|
|
147
|
+
return nil if filename.nil?
|
|
148
|
+
|
|
149
|
+
str = filename.to_s
|
|
150
|
+
if str.include?("/app/views/") || str.start_with?("app/views/")
|
|
151
|
+
return str if str.start_with?("app/views/")
|
|
152
|
+
|
|
153
|
+
app_root = ::Rails.root.to_s
|
|
154
|
+
return str unless app_root.present?
|
|
155
|
+
|
|
156
|
+
return Pathname.new(str).relative_path_from(Pathname.new(app_root)).to_s
|
|
157
|
+
end
|
|
158
|
+
nil
|
|
159
|
+
rescue
|
|
160
|
+
nil
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Floatable
|
|
2
|
+
module Rails
|
|
3
|
+
module Helpers
|
|
4
|
+
def floatable_enabled?
|
|
5
|
+
return false unless respond_to?(:request)
|
|
6
|
+
|
|
7
|
+
::Floatable::Rails.enabled_for?(request)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def floatable_tag
|
|
11
|
+
return unless floatable_enabled?
|
|
12
|
+
|
|
13
|
+
src = ::Floatable::Rails.script_src
|
|
14
|
+
return if src.blank?
|
|
15
|
+
|
|
16
|
+
javascript_include_tag(
|
|
17
|
+
src,
|
|
18
|
+
type: "module",
|
|
19
|
+
defer: false,
|
|
20
|
+
data: ::Floatable::Rails.script_data_attributes
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
require "nokogiri"
|
|
2
|
+
require "erb"
|
|
3
|
+
require "json"
|
|
4
|
+
require "floatable/rails/erb_line_instrumentation"
|
|
5
|
+
|
|
6
|
+
module Floatable
|
|
7
|
+
module Rails
|
|
8
|
+
class ResponseMiddleware
|
|
9
|
+
def initialize(app)
|
|
10
|
+
@app = app
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(env)
|
|
14
|
+
enabled = floatable_enabled?(env)
|
|
15
|
+
Thread.current[:floatable_enabled] = enabled
|
|
16
|
+
status = headers = body = nil
|
|
17
|
+
begin
|
|
18
|
+
status, headers, body = @app.call(env)
|
|
19
|
+
ensure
|
|
20
|
+
Thread.current[:floatable_enabled] = nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
return [ status, headers, body ] unless html_response?(status, headers)
|
|
24
|
+
return [ status, headers, body ] unless enabled
|
|
25
|
+
|
|
26
|
+
raw = +""
|
|
27
|
+
body.each { |chunk| raw << chunk.to_s }
|
|
28
|
+
body.close if body.respond_to?(:close)
|
|
29
|
+
|
|
30
|
+
instrumented = instrument_html(raw, env)
|
|
31
|
+
final_html = inject_script(instrumented)
|
|
32
|
+
|
|
33
|
+
headers["Content-Length"] = final_html.bytesize.to_s if headers["Content-Length"]
|
|
34
|
+
|
|
35
|
+
[ status, headers, [ final_html ] ]
|
|
36
|
+
rescue => e
|
|
37
|
+
::Rails.logger.error("[Floatable::Rails::ResponseMiddleware] failed: #{e.class} - #{e.message}")
|
|
38
|
+
[ status, headers, body ]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def html_response?(status, headers)
|
|
44
|
+
return false unless status == 200
|
|
45
|
+
ct = headers["Content-Type"] || headers["content-type"]
|
|
46
|
+
ct && ct.include?("text/html")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def floatable_enabled?(env)
|
|
50
|
+
req = ::Rack::Request.new(env)
|
|
51
|
+
::Floatable::Rails.enabled_for?(req)
|
|
52
|
+
rescue => e
|
|
53
|
+
::Rails.logger.error("[Floatable::Rails::ResponseMiddleware] enabled? failed: #{e.class} - #{e.message}")
|
|
54
|
+
false
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def instrument_html(html, env)
|
|
58
|
+
return html if html.strip.empty?
|
|
59
|
+
|
|
60
|
+
doctype = html[/\A\s*<!DOCTYPE[^>]*>/i]
|
|
61
|
+
doc = Nokogiri::HTML.parse(html)
|
|
62
|
+
doc.css("*").each do |node|
|
|
63
|
+
name = node.name.to_s.downcase
|
|
64
|
+
next if %w[script style].include?(name)
|
|
65
|
+
|
|
66
|
+
# Don't override helper-provided metadata
|
|
67
|
+
has_path = node["data-floatable-path"].present?
|
|
68
|
+
|
|
69
|
+
unless node["data-floatable-content"].present?
|
|
70
|
+
data = {
|
|
71
|
+
name: node.name,
|
|
72
|
+
text: node.text.to_s.strip.presence,
|
|
73
|
+
class: node["class"].to_s.presence,
|
|
74
|
+
id: node["id"].to_s.presence
|
|
75
|
+
}.compact
|
|
76
|
+
|
|
77
|
+
if data.any?
|
|
78
|
+
encoded = ERB::Util.url_encode(JSON.generate(data)) rescue nil
|
|
79
|
+
node["data-floatable-content"] = encoded if encoded
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
next if has_path
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
apply_view_annotations(doc)
|
|
87
|
+
# Top → down: let descendants inherit path from ancestors when safe
|
|
88
|
+
propagate_file_metadata_down(doc)
|
|
89
|
+
# Ensure ids are present and consistent once paths are known
|
|
90
|
+
assign_floatable_ids(doc)
|
|
91
|
+
|
|
92
|
+
rendered = doc.to_html
|
|
93
|
+
return rendered if doctype.nil? || rendered.lstrip.start_with?("<!DOCTYPE")
|
|
94
|
+
|
|
95
|
+
"#{doctype}\n#{rendered}"
|
|
96
|
+
rescue => e
|
|
97
|
+
::Rails.logger.debug("[Floatable::Rails] HTML instrumentation failed: #{e.class} - #{e.message}")
|
|
98
|
+
html
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Top → down: walk the tree and carry the closest ancestor's path
|
|
102
|
+
# down to all descendants that don't have their own.
|
|
103
|
+
def propagate_file_metadata_down(doc)
|
|
104
|
+
doc.children.each do |child|
|
|
105
|
+
next unless child.element?
|
|
106
|
+
propagate_file_metadata_down_from_node(child, current_path: nil)
|
|
107
|
+
end
|
|
108
|
+
rescue => e
|
|
109
|
+
::Rails.logger.debug("[Floatable::Rails] propagate_file_metadata_down failed: #{e.class} - #{e.message}")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def propagate_file_metadata_down_from_node(node, current_path:)
|
|
113
|
+
# Update context with this node's path if it has it
|
|
114
|
+
node_path = node["data-floatable-path"].presence || current_path
|
|
115
|
+
|
|
116
|
+
# If this node doesn't have path but context does, inherit it
|
|
117
|
+
if node_path && node["data-floatable-path"].blank?
|
|
118
|
+
node["data-floatable-path"] = node_path
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
next_path = layout_path?(node_path) ? nil : node_path
|
|
122
|
+
|
|
123
|
+
node.element_children.each do |child|
|
|
124
|
+
propagate_file_metadata_down_from_node(child, current_path: next_path)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def inject_script(html)
|
|
129
|
+
src = ::Floatable::Rails.script_src
|
|
130
|
+
return html if src.blank?
|
|
131
|
+
|
|
132
|
+
if html.match?(/<script\b[^>]*\bsrc=["']#{Regexp.escape(src)}["'][^>]*>/)
|
|
133
|
+
return html
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
data_attrs = ::Floatable::Rails.script_data_attributes_html
|
|
137
|
+
snippet = %(<script type="module" src="#{ERB::Util.html_escape(src)}"#{data_attrs}></script>)
|
|
138
|
+
|
|
139
|
+
if html.include?("</body>")
|
|
140
|
+
html.sub("</body>", "#{snippet}\n</body>")
|
|
141
|
+
else
|
|
142
|
+
html + snippet
|
|
143
|
+
end
|
|
144
|
+
rescue => e
|
|
145
|
+
::Rails.logger.error("[Floatable::Rails::ResponseMiddleware] inject_script failed: #{e.class} - #{e.message}")
|
|
146
|
+
html
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def assign_floatable_ids(doc)
|
|
150
|
+
doc.css("*[data-floatable-path]").each do |node|
|
|
151
|
+
line = node["data-floatable-line"].presence
|
|
152
|
+
next unless line
|
|
153
|
+
|
|
154
|
+
desired = "#{node["data-floatable-path"]}:#{line}"
|
|
155
|
+
node["data-floatable-id"] = desired if node["data-floatable-id"] != desired
|
|
156
|
+
end
|
|
157
|
+
rescue => e
|
|
158
|
+
::Rails.logger.debug("[Floatable::Rails] assign_floatable_ids failed: #{e.class} - #{e.message}")
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def layout_path?(path)
|
|
162
|
+
str = path.to_s
|
|
163
|
+
str.include?("/app/views/layouts/") || str.start_with?("app/views/layouts/")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def app_views_path?(path)
|
|
167
|
+
str = path.to_s
|
|
168
|
+
str.include?("/app/views/") || str.start_with?("app/views/")
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def apply_view_annotations(doc)
|
|
172
|
+
view_stack = []
|
|
173
|
+
line_prefix = ::Floatable::Rails::ErbLineInstrumentation::LINE_MARKER_PREFIX
|
|
174
|
+
expr_line_prefix = ::Floatable::Rails::ErbLineInstrumentation::EXPR_LINE_MARKER_PREFIX
|
|
175
|
+
line_comment_re = /\A#{Regexp.escape(line_prefix)}\d+\z/
|
|
176
|
+
expr_comment_re = /\A#{Regexp.escape(expr_line_prefix)}\d+\z/
|
|
177
|
+
doc.traverse do |node|
|
|
178
|
+
if node.comment?
|
|
179
|
+
content = node.content.to_s.strip
|
|
180
|
+
if content.start_with?("BEGIN ")
|
|
181
|
+
path = content.delete_prefix("BEGIN ").strip
|
|
182
|
+
view_stack << path if app_views_path?(path)
|
|
183
|
+
next
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
if content.start_with?("END ")
|
|
187
|
+
path = content.delete_prefix("END ").strip
|
|
188
|
+
view_stack.pop if view_stack.last == path
|
|
189
|
+
next
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
next
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
next unless node.element?
|
|
196
|
+
|
|
197
|
+
current_path = view_stack.last
|
|
198
|
+
node["data-floatable-path"] ||= current_path if current_path.present?
|
|
199
|
+
|
|
200
|
+
line = floatable_line_from_previous_siblings(node, line_prefix, expr_line_prefix, line_comment_re, expr_comment_re)
|
|
201
|
+
node["data-floatable-line"] = line if line.to_i > 0 && current_path.present?
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
doc.xpath("//comment()").remove
|
|
205
|
+
rescue => e
|
|
206
|
+
::Rails.logger.debug("[Floatable::Rails] apply_view_annotations failed: #{e.class} - #{e.message}")
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def floatable_line_from_previous_siblings(node, line_prefix, expr_line_prefix, line_comment_re, expr_comment_re)
|
|
210
|
+
prev = node.previous_sibling
|
|
211
|
+
while prev
|
|
212
|
+
if prev.comment?
|
|
213
|
+
content = prev.content.to_s.strip
|
|
214
|
+
return content.delete_prefix(expr_line_prefix).to_i if content.match?(expr_comment_re)
|
|
215
|
+
return content.delete_prefix(line_prefix).to_i if content.match?(line_comment_re)
|
|
216
|
+
end
|
|
217
|
+
prev = prev.previous_sibling
|
|
218
|
+
end
|
|
219
|
+
nil
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
out = []
|
|
9
|
+
out << "- @output_buffer.safe_concat('<!-- BEGIN #{view_path} -->') if Thread.current[:floatable_enabled] && defined?(@output_buffer)"
|
|
10
|
+
|
|
11
|
+
lines.each_with_index do |line, idx|
|
|
12
|
+
indent = line[/\A[ \t]*/] || ""
|
|
13
|
+
out << "#{indent}- @output_buffer.safe_concat('<!--#{ErbLineInstrumentation::LINE_MARKER_PREFIX}#{idx + 1}-->') if Thread.current[:floatable_enabled] && defined?(@output_buffer)"
|
|
14
|
+
out << line
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
out << "- @output_buffer.safe_concat('<!-- END #{view_path} -->') if Thread.current[:floatable_enabled] && defined?(@output_buffer)"
|
|
18
|
+
out.join("\n")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
module SlimHandlerPatch
|
|
23
|
+
def call(template, source = nil)
|
|
24
|
+
return super unless template.format == :html
|
|
25
|
+
|
|
26
|
+
view_path = floatable_views_path(template.identifier)
|
|
27
|
+
return super if view_path.nil?
|
|
28
|
+
|
|
29
|
+
instrumented = SlimLineInstrumenter.instrument(source || template.source, view_path)
|
|
30
|
+
super(template, instrumented)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def floatable_views_path(filename)
|
|
36
|
+
return nil if filename.nil?
|
|
37
|
+
|
|
38
|
+
str = filename.to_s
|
|
39
|
+
return nil unless str.include?("/app/views/") || str.start_with?("app/views/")
|
|
40
|
+
|
|
41
|
+
return str if str.start_with?("app/views/")
|
|
42
|
+
|
|
43
|
+
app_root = ::Rails.root.to_s
|
|
44
|
+
return str unless app_root.present?
|
|
45
|
+
|
|
46
|
+
Pathname.new(str).relative_path_from(Pathname.new(app_root)).to_s
|
|
47
|
+
rescue
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
require "pathname"
|
|
2
|
+
require "json"
|
|
3
|
+
require "erb"
|
|
4
|
+
|
|
5
|
+
module Floatable
|
|
6
|
+
module Rails
|
|
7
|
+
module TagInstrumentation
|
|
8
|
+
def content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block)
|
|
9
|
+
if floatable_enabled_for_view?
|
|
10
|
+
options = ensure_options_hash(content_or_options_with_block, options)
|
|
11
|
+
file, line = floatable_callsite
|
|
12
|
+
|
|
13
|
+
if file
|
|
14
|
+
options[:"data-floatable-path"] ||= floatable_relative_path(file)
|
|
15
|
+
if line
|
|
16
|
+
options[:"data-floatable-line"] ||= line
|
|
17
|
+
options[:"data-floatable-id"] ||= "#{options[:"data-floatable-path"]}:#{options[:"data-floatable-line"]}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
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
|
+
# Re-shape args for super if we mutated them
|
|
28
|
+
if content_or_options_with_block.is_a?(Hash) && options
|
|
29
|
+
content_or_options_with_block = options
|
|
30
|
+
options = nil
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
super
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def tag(name = nil, options = nil, open = false, escape = true)
|
|
38
|
+
if floatable_enabled_for_view? && name
|
|
39
|
+
options = (options || {}).dup
|
|
40
|
+
file, line = floatable_callsite
|
|
41
|
+
|
|
42
|
+
if file
|
|
43
|
+
options[:"data-floatable-path"] ||= floatable_relative_path(file)
|
|
44
|
+
if line
|
|
45
|
+
options[:"data-floatable-line"] ||= line
|
|
46
|
+
options[:"data-floatable-id"] ||= "#{options[:"data-floatable-path"]}:#{options[:"data-floatable-line"]}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
options[:"data-floatable-content"] ||= floatable_content_attr(
|
|
51
|
+
tag_name: name,
|
|
52
|
+
options: options,
|
|
53
|
+
content_text: nil
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return super(name, options, open, escape)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
super
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def floatable_enabled_for_view?
|
|
65
|
+
return false unless respond_to?(:request)
|
|
66
|
+
|
|
67
|
+
::Floatable::Rails.enabled_for?(request)
|
|
68
|
+
rescue => e
|
|
69
|
+
::Rails.logger.error("[Floatable::Rails::TagInstrumentation] enabled? failed: #{e.class} - #{e.message}")
|
|
70
|
+
false
|
|
71
|
+
end
|
|
72
|
+
|
|
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
|
+
def ensure_options_hash(content_or_options_with_block, options)
|
|
89
|
+
if content_or_options_with_block.is_a?(Hash) && options.nil?
|
|
90
|
+
content_or_options_with_block
|
|
91
|
+
else
|
|
92
|
+
options || {}
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Callsite detection
|
|
97
|
+
#
|
|
98
|
+
# We want the *template* or app file (e.g. app/views/posts/index.html.erb),
|
|
99
|
+
# not ActionView's gem helpers.
|
|
100
|
+
#
|
|
101
|
+
# Strategy:
|
|
102
|
+
# - Walk the call stack.
|
|
103
|
+
# - Pick the first frame whose path starts with Rails.root
|
|
104
|
+
# (so we ignore anything under ~/.rbenv, /gems/, etc).
|
|
105
|
+
# - Prefer files under app/views, but accept any app file as fallback.
|
|
106
|
+
#
|
|
107
|
+
def floatable_callsite
|
|
108
|
+
app_root = ::Rails.root.to_s
|
|
109
|
+
return [ nil, nil ] if app_root.blank?
|
|
110
|
+
|
|
111
|
+
locations = caller_locations(2, 200) || []
|
|
112
|
+
|
|
113
|
+
template_path = floatable_current_template_path
|
|
114
|
+
if template_path
|
|
115
|
+
loc = locations.find { |loc_item| loc_item.path.to_s == template_path }
|
|
116
|
+
return [ template_path, loc.lineno ] if loc
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# First pass: prefer app/views templates, excluding layouts when possible.
|
|
120
|
+
view_locs = locations.select do |loc|
|
|
121
|
+
path = loc.path.to_s
|
|
122
|
+
path.start_with?(app_root) && path.include?("/app/views/") && path.end_with?(".erb")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
unless view_locs.empty?
|
|
126
|
+
non_layout = view_locs.reverse.find { |loc| !loc.path.to_s.include?("/app/views/layouts/") }
|
|
127
|
+
chosen = non_layout || view_locs.last
|
|
128
|
+
return [ chosen.path, chosen.lineno ]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Second pass: any file under Rails.root (app/models, app/controllers, etc)
|
|
132
|
+
app_loc = locations.find do |loc|
|
|
133
|
+
path = loc.path.to_s
|
|
134
|
+
path.start_with?(app_root)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
if app_loc
|
|
138
|
+
return [ app_loc.path, app_loc.lineno ]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# If nothing inside the app, we give up and return nil
|
|
142
|
+
[ nil, nil ]
|
|
143
|
+
rescue => e
|
|
144
|
+
::Rails.logger.debug("[Floatable::Rails::TagInstrumentation] floatable_callsite failed: #{e.class} - #{e.message}")
|
|
145
|
+
[ nil, nil ]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def floatable_current_template_path
|
|
149
|
+
return nil unless respond_to?(:lookup_context)
|
|
150
|
+
|
|
151
|
+
current_template = floatable_current_template_identifier
|
|
152
|
+
return current_template if current_template.present?
|
|
153
|
+
|
|
154
|
+
vpath = floatable_virtual_path
|
|
155
|
+
return nil if vpath.empty?
|
|
156
|
+
|
|
157
|
+
templates = lookup_context.find_all(vpath, [], true)
|
|
158
|
+
template = templates.first || lookup_context.find_all(vpath, [], false).first
|
|
159
|
+
identifier = template&.identifier.to_s
|
|
160
|
+
identifier.presence
|
|
161
|
+
rescue
|
|
162
|
+
nil
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def floatable_current_template_identifier
|
|
166
|
+
return nil unless instance_variable_defined?(:@current_template)
|
|
167
|
+
|
|
168
|
+
template = instance_variable_get(:@current_template)
|
|
169
|
+
identifier = template&.identifier.to_s
|
|
170
|
+
identifier.presence
|
|
171
|
+
rescue
|
|
172
|
+
nil
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def floatable_virtual_path
|
|
176
|
+
vpath = nil
|
|
177
|
+
vpath = virtual_path.to_s if respond_to?(:virtual_path)
|
|
178
|
+
|
|
179
|
+
if vpath.to_s.empty?
|
|
180
|
+
vpath = instance_variable_get(:@virtual_path).to_s if instance_variable_defined?(:@virtual_path)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
if vpath.to_s.empty?
|
|
184
|
+
vpath = instance_variable_get(:@_virtual_path).to_s if instance_variable_defined?(:@_virtual_path)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
vpath.to_s
|
|
188
|
+
rescue
|
|
189
|
+
""
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def floatable_relative_path(file)
|
|
193
|
+
return nil if file.blank?
|
|
194
|
+
|
|
195
|
+
app_root = ::Rails.root.to_s
|
|
196
|
+
return file unless app_root.present?
|
|
197
|
+
|
|
198
|
+
# Only record paths inside the app root; ignore gems/rbenv/etc.
|
|
199
|
+
unless file.start_with?(app_root)
|
|
200
|
+
return nil
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
Pathname.new(file).relative_path_from(Pathname.new(app_root)).to_s
|
|
204
|
+
rescue
|
|
205
|
+
file
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
require "erb"
|
|
2
|
+
require "floatable/rails/version"
|
|
3
|
+
require "floatable/rails/engine"
|
|
4
|
+
|
|
5
|
+
# lib/floatable/rails.rb
|
|
6
|
+
module Floatable
|
|
7
|
+
module Rails
|
|
8
|
+
# Avoid namespace resolution issues for apps that define module Floatable.
|
|
9
|
+
# In that case, `Rails::Application` resolves to `Floatable::Rails::Application`,
|
|
10
|
+
# so provide a compatible alias.
|
|
11
|
+
Application = ::Rails::Application if defined?(::Rails::Application) && !const_defined?(:Application)
|
|
12
|
+
|
|
13
|
+
class Configuration
|
|
14
|
+
# Proc: ->(request) { true/false }
|
|
15
|
+
attr_accessor :enabled
|
|
16
|
+
# URL for the Floatable toolbar script
|
|
17
|
+
attr_accessor :script_src
|
|
18
|
+
# Metadata
|
|
19
|
+
attr_accessor :script_workspace
|
|
20
|
+
attr_accessor :script_repository
|
|
21
|
+
attr_accessor :script_repository_name
|
|
22
|
+
attr_accessor :script_branch
|
|
23
|
+
attr_accessor :script_agent_tools_mode
|
|
24
|
+
|
|
25
|
+
def initialize
|
|
26
|
+
# Default: enabled in development or when FLOATABLE_ENABLED=1
|
|
27
|
+
@enabled = lambda do |_request|
|
|
28
|
+
::Rails.env.development? || ENV["FLOATABLE_ENABLED"] == "1"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Default script for dev; override in host app
|
|
32
|
+
@script_src = "https://assets.floatable.dev/js/toolbar.js"
|
|
33
|
+
|
|
34
|
+
@script_workspace = nil
|
|
35
|
+
@script_repository = nil
|
|
36
|
+
@script_repository_name = nil
|
|
37
|
+
@script_branch = nil
|
|
38
|
+
@script_agent_tools_mode = nil
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class << self
|
|
43
|
+
def config
|
|
44
|
+
@config ||= Configuration.new
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def configure
|
|
48
|
+
yield(config)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Central predicate for "is Floatable active for this request?"
|
|
52
|
+
def enabled_for?(request)
|
|
53
|
+
predicate = config.enabled
|
|
54
|
+
return false unless predicate.respond_to?(:call)
|
|
55
|
+
|
|
56
|
+
!!predicate.call(request)
|
|
57
|
+
rescue => e
|
|
58
|
+
::Rails.logger.error("[Floatable::Rails] enabled_for? failed: #{e.class} - #{e.message}")
|
|
59
|
+
false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def script_src
|
|
63
|
+
config.script_src
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def script_data
|
|
67
|
+
{
|
|
68
|
+
workspace: config.script_workspace,
|
|
69
|
+
repository: config.script_repository,
|
|
70
|
+
repository_name: config.script_repository_name,
|
|
71
|
+
branch: config.script_branch,
|
|
72
|
+
agent_tools_mode: config.script_agent_tools_mode
|
|
73
|
+
}.reject { |_key, value| value.nil? || (value.respond_to?(:empty?) && value.empty?) }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def script_data_attributes
|
|
77
|
+
script_data.transform_keys { |key| :"floatable_#{key}" }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def script_data_attributes_html
|
|
81
|
+
data = script_data
|
|
82
|
+
return "" if data.empty?
|
|
83
|
+
|
|
84
|
+
data.map do |key, value|
|
|
85
|
+
%( data-floatable-#{key.to_s.tr("_", "-")}="#{ERB::Util.html_escape(value.to_s)}")
|
|
86
|
+
end.join
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: floatable-rails
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Tobias Almstrand
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 8.1.1
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 8.1.1
|
|
26
|
+
description: Instruments HTML output with metadata and injects the Floatable script
|
|
27
|
+
so your UI can be inspected and annotated in the browser.
|
|
28
|
+
email:
|
|
29
|
+
- tobias@almstrand.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- CHANGELOG.md
|
|
35
|
+
- MIT-LICENSE
|
|
36
|
+
- README.md
|
|
37
|
+
- Rakefile
|
|
38
|
+
- lib/floatable/rails.rb
|
|
39
|
+
- lib/floatable/rails/engine.rb
|
|
40
|
+
- lib/floatable/rails/erb_line_instrumentation.rb
|
|
41
|
+
- lib/floatable/rails/helpers.rb
|
|
42
|
+
- lib/floatable/rails/response_middleware.rb
|
|
43
|
+
- lib/floatable/rails/slim_line_instrumentation.rb
|
|
44
|
+
- lib/floatable/rails/tag_instrumentation.rb
|
|
45
|
+
- lib/floatable/rails/version.rb
|
|
46
|
+
homepage: https://floatable.dev
|
|
47
|
+
licenses:
|
|
48
|
+
- MIT
|
|
49
|
+
metadata:
|
|
50
|
+
homepage_uri: https://floatable.dev
|
|
51
|
+
source_code_uri: https://floatable.dev
|
|
52
|
+
changelog_uri: https://floatable.dev/blob/main/CHANGELOG.md
|
|
53
|
+
rubygems_mfa_required: 'true'
|
|
54
|
+
rdoc_options: []
|
|
55
|
+
require_paths:
|
|
56
|
+
- lib
|
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '0'
|
|
67
|
+
requirements: []
|
|
68
|
+
rubygems_version: 3.6.9
|
|
69
|
+
specification_version: 4
|
|
70
|
+
summary: Rails integration for the Floatable Toolbar.
|
|
71
|
+
test_files: []
|